Minimal async MongoDB ODM built for speed and simplicity, featuring automatic collection binding, msgspec integration, and first-class asyncio support.
pip install mongospecRequires Python 3.13+ and a running MongoDB 6.0+ server.
import asyncio
from datetime import datetime
from typing import ClassVar, Sequence
import mongojet
import msgspec
import mongospec
from mongospec import MongoDocument
from mongojet import IndexModel
class User(MongoDocument):
__collection_name__ = "users"
__indexes__: ClassVar[Sequence[IndexModel]] = [
IndexModel(keys=[("email", 1)], options={"unique": True})
]
name: str
email: str
created_at: datetime = msgspec.field(default_factory=datetime.now)
async def main() -> None:
client = await mongojet.create_client("mongodb://localhost:27017")
await mongospec.init(client.get_database("example_db"), document_types=[User])
user = User(name="Alice", email="alice@example.com")
await user.insert()
print("Inserted:", user)
fetched = await User.find_one({"email": "alice@example.com"})
print("Fetched:", fetched)
await fetched.delete()
await mongospec.close()
if __name__ == "__main__":
asyncio.run(main())All other usage examples have been moved to standalone scripts in the
examples/ directory.
Each file is self-contained and can be executed directly:
| Script | What it covers |
|---|---|
quick_start.py |
End-to-end “hello world” |
document_models.py |
Defining typed models & indexes |
connection_management.py |
Initialising the ODM and binding collections |
collection_binding.py |
Using models immediately after init |
index_creation.py |
Unique, compound & text indexes |
create_documents.py |
Single & bulk inserts, conditional insert |
read_documents.py |
Queries, cursors, projections |
update_documents.py |
Field updates, atomic & versioned updates |
delete_documents.py |
Single & batch deletes |
count_documents.py |
Fast counts & estimated counts |
working_with_cursors.py |
Batch processing large result sets |
batch_operations.py |
Bulk insert / update / delete |
atomic_updates.py |
Optimistic-locking with version field |
upsert_operations.py |
Upsert via save and update_one |
projection_example.py |
Field selection for performance |
- Zero-boilerplate models – automatic collection resolution & binding.
- Async first – built on
mongojet, fullyawait-able API. - Typed & fast – data classes powered by
msgspecfor ultra-fast (de)serialization. - Declarative indexes – define indexes right on the model with
familiar
pymongo/mongojetIndexModels. - Batteries included – helpers for common CRUD patterns, bulk and atomic operations, cursors, projections, upserts and more.
Define your schema by subclassing MongoDocument
and adding typed attributes.
See examples/document_models.py.
Initialise once with mongospec.init(...), passing a
mongojet.Database and the list of models to bind.
See examples/connection_management.py.
After initialisation every model knows its collection and can be used
immediately – no manual wiring required.
See examples/collection_binding.py.
The MongoDocument class (and its mixins) exposes a rich async CRUD API:
insert, find, update, delete, count, cursors, bulk helpers,
atomic find_one_and_update, upserts, etc.
See scripts in examples/ grouped by operation type.
Declare indexes in __indexes__ as a Sequence[IndexModel]
(unique, compound, text, …).
Indexes are created automatically at init time.
See examples/index_creation.py.
In addition to manually listing document classes when calling mongospec.init(...), you can use the utility function collect_document_types(...) to automatically discover all models in a package:
from mongospec.utils import collect_document_types
document_types = collect_document_types("myapp.db.models")
await mongospec.init(db, document_types=document_types)This function supports:
- Recursive import of all submodules in the target package
- Filtering by base class (default:
MongoDocument) - Optional exclusion of abstract or re-exported classes
- Regex or callable-based module filtering
- Graceful handling of import errors
Usage Example:
from mongospec.utils import collect_document_types
# Collect all document models in `myapp.db.models` and its submodules
models = collect_document_types(
"myapp.db.models",
ignore_abstract=True,
local_only=True,
on_error="warn",
)
await mongospec.init(db, document_types=models)Advanced options include:
predicate=...to filter only specific model typesreturn_map=Trueto get a{qualified_name: class}dictmodule_filter=".*models.*"to restrict traversal
See the full function signature in mongospec/utils.py.
MongoDocument provides two hooks that subclasses can override to inject
custom logic before write operations:
| Hook | Called by | Purpose |
|---|---|---|
__pre_save__(self) -> None |
insert(), insert_one(), insert_many(), save() |
Mutate instance fields before serialization |
__pre_update__(cls, update) -> dict |
update_one(), update_many(), update_by_id(), find_one_and_update() |
Modify the update document before execution |
Example — automatic updated_at:
from datetime import datetime, UTC
from typing import Any
import msgspec
from mongospec import MongoDocument
class Document(MongoDocument):
created_at: datetime = msgspec.field(default_factory=lambda: datetime.now(UTC))
updated_at: datetime = msgspec.field(default_factory=lambda: datetime.now(UTC))
def __pre_save__(self) -> None:
self.updated_at = datetime.now(UTC)
@classmethod
def __pre_update__(cls, update: dict[str, Any]) -> dict[str, Any]:
update.setdefault("$set", {}).setdefault("updated_at", datetime.now(UTC))
return updateNow every insert, save, or update operation automatically keeps updated_at
in sync — no caller-side boilerplate needed.
mongospec.contrib.kv_store provides a ready-made async key-value store
backed by a MongoDB collection. Designed for multiple inheritance with
project-specific base documents.
from mongospec.contrib.kv_store import KVStore, KVStoreItem
from myapp.db import Document # your base with timestamps, hooks, etc.
class AppStorage(KVStore, Document):
__collection_name__ = "app_storage"A unique index on key is created automatically at init time.
Direct usage:
await AppStorage.set("theme", "dark")
theme = await AppStorage.get("theme") # "dark"
theme = await AppStorage.get_or_default("x", 0) # 0 (no KeyError)
await AppStorage.set_default("theme", "light") # "dark" (atomic, no overwrite)
await AppStorage.set_many({"a": 1, "b": 2})
all_pairs = await AppStorage.get_all() # {"theme": "dark", "a": 1, "b": 2}
all_keys = await AppStorage.keys() # ["theme", "a", "b"]
await AppStorage.has("theme") # True
await AppStorage.delete_key("theme") # TrueTyped accessor (KVStoreItem):
AppStorageItem = KVStoreItem.of(AppStorage)
max_retries = AppStorageItem[int]("max_retries", default=3)
value = await max_retries.get() # 3 (default, not persisted)
await max_retries.set_default() # atomically persist default if missing
await max_retries.set(10)
await max_retries.has() # True
await max_retries.delete() # TrueKVStore method |
Description |
|---|---|
set(key, value) |
Upsert a value by key |
get(key) |
Get value or raise KeyError |
get_or_default(key, default) |
Get value or return default |
set_default(key, value) |
Atomic insert-if-absent ($setOnInsert) |
delete_key(key) |
Delete a key, return True if existed |
has(key) |
Check key existence |
get_all() |
Return all pairs as dict |
keys() |
Return all key names |
set_many(items) |
Upsert multiple pairs |
Contributions, issues and feature requests are welcome.