Bunnet - Synchronous Python ODM for MongoDB
Bunnet is a synchronous Python Object-Document Mapper (ODM) for MongoDB, built upon Pydantic for data modeling. It simplifies interaction with MongoDB collections by mapping Python classes to documents, reducing boilerplate code for common database operations like adding, updating, and deleting. Bunnet is a synchronous fork of the popular Beanie ODM, and its current version is 1.3.0 with active development and maintenance.
Common errors
-
AttributeError: 'Product' object has no attribute 'find_one_async'
cause Attempting to use an asynchronous method from BeanieODM (the async counterpart) on a Bunnet Document model.fixBunnet is synchronous. Use `find_one().run()` instead of `find_one_async()`. All operations are synchronous and do not require `await`. -
pymongo.errors.CollectionInvalid: collection name cannot be empty
cause The `init_bunnet` function was called without a valid `database` object or `document_models` list, preventing proper collection initialization.fixEnsure `init_bunnet(database=client.get_database('your_db_name'), document_models=[YourDocumentClass])` is called with both a valid `pymongo.database.Database` instance and a list of your Document classes. -
TypeError: Document.save() missing 1 required positional argument: 'self'
cause Trying to call an instance method (`.save()`, `.insert()`, `.update()`) on the `Document` class itself, rather than on an instantiated document object.fixFirst create an instance of your document (e.g., `my_product = Product(...)`), then call the method on the instance (`my_product.save()`). -
TypeError: 'Collection' object is not callable
cause Attempting to access a database collection property (e.g., `Product.Collection`) which was a pattern in older Beanie versions but is now deprecated or unsupported in Bunnet, replaced by `Settings`.fixAccess collection-level settings via `Product.Settings` or define `class Settings:` within your `Document` class to configure collection options like `name`. Do not use `Document.Collection` directly.
Warnings
- gotcha Bunnet is the synchronous version of Beanie ODM. Methods in Bunnet do not use `await` and attempts to call `async` methods (like `find_one_async`) will result in `AttributeError` or `TypeError`.
- breaking Pydantic V2 introduced significant internal changes. While Bunnet aims for compatibility, developers migrating from Pydantic V1-based applications might encounter validation or serialization issues, especially with custom types or complex data structures.
- gotcha The `replace_one` method in Bunnet (and Beanie) does not natively support `upsert=True` as a direct parameter in the same way `update_one` does in PyMongo. Attempting to use it for an upsert might not behave as expected.
- gotcha Initialization with `init_bunnet` requires a PyMongo `database` object, not the client. Passing the client directly will raise an error or lead to incorrect behavior.
Install
-
pip install bunnet -
poetry add bunnet
Imports
- Document
from bunnet import Document
- init_bunnet
from bunnet import init_bunnet
- Indexed
from bunnet import Indexed
- MongoClient
from pymongo import MongoClient
- BaseModel
from bunnet import BaseModel
from pydantic import BaseModel
Quickstart
from typing import Optional
from pymongo import MongoClient
from pydantic import BaseModel
from bunnet import Document, Indexed, init_bunnet
import os
# Define a Pydantic model for a nested object
class Category(BaseModel):
name: str
description: str
# Define a Bunnet Document model
class Product(Document):
name: str
description: Optional[str] = None
price: Indexed(float)
category: Category
# Optional: Configure collection name and other settings
class Settings:
name = "products_collection"
async def main():
# Connect to MongoDB (replace with your connection string)
# Use os.environ.get for secure credentials in production
MONGO_DETAILS = os.environ.get('MONGO_URI', 'mongodb://localhost:27017')
client = MongoClient(MONGO_DETAILS)
# Initialize Bunnet with the database and document models
# Ensure 'your_db_name' exists or will be created upon first write
init_bunnet(database=client.get_database('your_db_name'), document_models=[Product])
# Create a category instance
chocolate_category = Category(name="Chocolate", description="A preparation of roasted and ground cacao seeds.")
# Create a product instance
tonybar = Product(name="Tony's Chocolonely", price=5.95, category=chocolate_category)
# Insert the document into the database
inserted_product = tonybar.insert()
print(f"Inserted product: {inserted_product.name}")
# Find a document using Pythonic syntax
found_product = Product.find_one(Product.price < 10).run()
if found_product:
print(f"Found product: {found_product.name}")
# Update a document
updated_product = found_product.set({Product.name: "Gold Bar"})
print(f"Updated product to: {updated_product.name}")
if __name__ == "__main__":
# Bunnet is synchronous, so no asyncio.run() needed directly for document operations.
# The main function is defined as async only for consistency if a project mixed sync/async components
# For this quickstart, you would typically run the synchronous operations directly.
# However, to simulate an async context often seen in examples for its counterpart (Beanie),
# we wrap it here. For a purely synchronous application, you'd call main() directly and remove 'async/await'.
main()