{"id":34107723,"url":"https://github.com/diprog/mongospec","last_synced_at":"2026-04-09T00:31:28.006Z","repository":{"id":294014180,"uuid":"985746142","full_name":"diprog/mongospec","owner":"diprog","description":"⚡ Blazing-fast async MongoDB ODM with msgspec serialization and automatic collection binding","archived":false,"fork":false,"pushed_at":"2026-03-31T15:23:08.000Z","size":1674,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-03-31T17:37:07.185Z","etag":null,"topics":["async","database","high-performance","mongodb","mongojet","motor","msgspec","odm","pymongo","python","python313"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/diprog.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-05-18T12:49:34.000Z","updated_at":"2026-03-31T15:23:12.000Z","dependencies_parsed_at":null,"dependency_job_id":"a0bfb234-cab4-42d8-aae8-149f3c15308f","html_url":"https://github.com/diprog/mongospec","commit_stats":null,"previous_names":["diprog/mongospec"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/diprog/mongospec","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diprog%2Fmongospec","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diprog%2Fmongospec/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diprog%2Fmongospec/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diprog%2Fmongospec/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/diprog","download_url":"https://codeload.github.com/diprog/mongospec/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diprog%2Fmongospec/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31579844,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-08T14:31:17.711Z","status":"ssl_error","status_checked_at":"2026-04-08T14:31:17.202Z","response_time":54,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","database","high-performance","mongodb","mongojet","motor","msgspec","odm","pymongo","python","python313"],"created_at":"2025-12-14T18:08:43.218Z","updated_at":"2026-04-09T00:31:27.997Z","avatar_url":"https://github.com/diprog.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"assets/logo.svg\" width=\"35%\" alt=\"mongospec\"/\u003e\n\u003c/p\u003e\n\n[![PyPI](https://img.shields.io/pypi/v/mongospec?color=blue\u0026label=PyPI%20package)](https://pypi.org/project/mongospec/)\n[![Python](https://img.shields.io/badge/python-3.13%2B-blue)](https://www.python.org/downloads/)\n[![License](https://img.shields.io/badge/license-MIT-green)](https://opensource.org/licenses/MIT)\n\nMinimal **async** MongoDB ODM built for *speed* and *simplicity*, featuring automatic collection binding,\n[msgspec](https://github.com/jcrist/msgspec) integration, and first-class asyncio support.\n\n---\n\n## Table of Contents\n\n1. [Installation](#installation)  \n2. [Quick Start](#quick-start)  \n3. [Examples](#examples)  \n4. [Key Features](#key-features)  \n5. [Core Concepts](#core-concepts)  \n   - [Document Models](#document-models)  \n   - [Connection Management](#connection-management)  \n   - [Collection Binding](#collection-binding)  \n   - [CRUD Operations](#crud-operations)\n   - [Document References](#document-references)\n   - [Indexes](#indexes)\n   - [Lifecycle Hooks](#lifecycle-hooks)\n   - [Contrib: KV Store](#contrib-kv-store)\n6. [Contributing](#contributing)\n7. [License](#license)\n\n---\n\n## Installation\n\n```bash\npip install mongospec\n```\n\nRequires **Python 3.13+** and a running MongoDB 6.0+ server.\n\n---\n\n## Quick Start\n\n```python\nimport asyncio\nfrom datetime import datetime\nfrom typing import ClassVar, Sequence\n\nimport mongojet\nimport msgspec\n\nimport mongospec\nfrom mongospec import MongoDocument\nfrom mongojet import IndexModel\n\n\nclass User(MongoDocument):\n    __collection_name__ = \"users\"\n    __indexes__: ClassVar[Sequence[IndexModel]] = [\n        IndexModel(keys=[(\"email\", 1)], options={\"unique\": True})\n    ]\n\n    name: str\n    email: str\n    created_at: datetime = msgspec.field(default_factory=datetime.now)\n\n\nasync def main() -\u003e None:\n    client = await mongojet.create_client(\"mongodb://localhost:27017\")\n    await mongospec.init(client.get_database(\"example_db\"), document_types=[User])\n\n    user = User(name=\"Alice\", email=\"alice@example.com\")\n    await user.insert()\n    print(\"Inserted:\", user)\n\n    fetched = await User.find_one({\"email\": \"alice@example.com\"})\n    print(\"Fetched:\", fetched)\n\n    await fetched.delete()\n    await mongospec.close()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n---\n\n## Examples\n\nAll other usage examples have been moved to standalone scripts in the\n[`examples/`](./examples) directory.\nEach file is self-contained and can be executed directly:\n\n| Script                     | What it covers                               |\n|----------------------------|----------------------------------------------|\n| `quick_start.py`           | End-to-end “hello world”                     |\n| `document_models.py`       | Defining typed models \u0026 indexes              |\n| `connection_management.py` | Initialising the ODM and binding collections |\n| `collection_binding.py`    | Using models immediately after init          |\n| `index_creation.py`        | Unique, compound \u0026 text indexes              |\n| `create_documents.py`      | Single \u0026 bulk inserts, conditional insert    |\n| `read_documents.py`        | Queries, cursors, projections                |\n| `update_documents.py`      | Field updates, atomic \u0026 versioned updates    |\n| `delete_documents.py`      | Single \u0026 batch deletes                       |\n| `count_documents.py`       | Fast counts \u0026 estimated counts               |\n| `working_with_cursors.py`  | Batch processing large result sets           |\n| `batch_operations.py`      | Bulk insert / update / delete                |\n| `atomic_updates.py`        | Optimistic-locking with version field        |\n| `upsert_operations.py`     | Upsert via `save` and `update_one`           |\n| `document_references.py`   | Transparent typed document references         |\n| `projection_example.py`    | Field selection for performance              |\n\n---\n\n## Key Features\n\n* **Zero-boilerplate models** – automatic collection resolution \u0026 binding.\n* **Async first** – built on `mongojet`, fully `await`-able API.\n* **Typed \u0026 fast** – data classes powered by `msgspec` for\n  ultra-fast (de)serialization.\n* **Document references** – transparent typed references with\n  automatic resolution, batch fetching, and cascading save.\n* **Declarative indexes** – define indexes right on the model with\n  familiar `pymongo`/`mongojet` `IndexModel`s.\n* **Batteries included** – helpers for common CRUD patterns, bulk and\n  atomic operations, cursors, projections, upserts and more.\n\n---\n\n## Core Concepts\n\n### Document Models\n\nDefine your schema by subclassing **`MongoDocument`**\nand adding typed attributes.\nSee **[`examples/document_models.py`](./examples/document_models.py)**.\n\n### Connection Management\n\nInitialise once with `mongospec.init(...)`, passing a\n`mongojet.Database` and the list of models to bind.\nSee **[`examples/connection_management.py`](./examples/connection_management.py)**.\n\n### Collection Binding\n\nAfter initialisation every model knows its collection and can be used\nimmediately – no manual wiring required.\nSee **[`examples/collection_binding.py`](./examples/collection_binding.py)**.\n\n### CRUD Operations\n\nThe `MongoDocument` class (and its mixins) exposes a rich async CRUD API:\n`insert`, `find`, `update`, `delete`, `count`, cursors, bulk helpers,\natomic `find_one_and_update`, upserts, etc.\nSee scripts in `examples/` grouped by operation type.\n\n### Document References\n\nAny field typed as a `MongoDocument` subclass is automatically a reference —\nstored as plain `ObjectId` in MongoDB, resolved transparently on read.\n\n```python\nfrom mongospec import MongoDocument\n\nclass User(MongoDocument):\n    name: str\n\nclass Tag(MongoDocument):\n    label: str\n\nclass Post(MongoDocument):\n    title: str\n    author: User                          # required reference\n    reviewer: User | None = None          # optional\n    tags: list[Tag] = []                  # list of references\n\n# Create — pass real document objects\npost = Post(title=\"Hello\", author=user, tags=[tag])\nawait post.insert()\n\n# Read — references auto-resolved (full type support for linters)\npost = await Post.find_one({\"title\": \"Hello\"})\nprint(post.author.name)                   # \"Alice\" — full autocomplete\n\n# Batch resolve (minimal queries: 1 per referenced class)\nposts = await Post.find_all({})           # auto-resolved\n\n# Skip resolution for performance\npost = await Post.find_one({...}, resolve_refs=False)\n\n# Cascading save (enabled by default)\npost.author.name = \"Updated\"\nawait post.save()                          # saves post AND author\n```\n\n### Recursive Insert\n\nIf a document graph contains unsaved referenced documents, use\n`insert_recursive()` on the root document. The ODM inserts unsaved child\nreferences first, then the parent, and returns a `RecursiveInsertResult`\ncontaining all created documents in insertion order.\n\n```python\nresult = await Post(\n    title=\"Hello\",\n    author=existing_user,\n    tags=[Tag(label=\"python\"), Tag(label=\"async\")],\n).insert_recursive()\n\nprint([type(doc).__name__ for doc in result.created_documents])\n# ['Tag', 'Tag', 'Post']\n\nawait result.rollback()  # deletes created docs in reverse order\n```\n\nIf recursive insertion fails after partial success, `mongospec` performs a\nbest-effort rollback and raises `RecursiveInsertError` with the partial\n`result`.\n\nSee **[`examples/document_references.py`](./examples/document_references.py)** for\na complete walkthrough.\n\n---\n\n### Indexes\n\nDeclare indexes in `__indexes__` as a `Sequence[IndexModel]`\n(unique, compound, text, …).\nIndexes are created automatically at init time.\nSee **[`examples/index_creation.py`](./examples/index_creation.py)**.\n\n### Automatic Discovery of Document Models\n\nIn 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:\n\n```python\nfrom mongospec.utils import collect_document_types\n\ndocument_types = collect_document_types(\"myapp.db.models\")\nawait mongospec.init(db, document_types=document_types)\n\n```\n\nThis function supports:\n\n* Recursive import of all submodules in the target package\n* Filtering by base class (default: `MongoDocument`)\n* Optional exclusion of abstract or re-exported classes\n* Regex or callable-based module filtering\n* Graceful handling of import errors\n\n**Usage Example:**\n\n```python\nfrom mongospec.utils import collect_document_types\n\n# Collect all document models in `myapp.db.models` and its submodules\nmodels = collect_document_types(\n    \"myapp.db.models\",\n    ignore_abstract=True,\n    local_only=True,\n    on_error=\"warn\",\n)\n\nawait mongospec.init(db, document_types=models)\n```\n\n**Advanced options include:**\n\n* `predicate=...` to filter only specific model types\n* `return_map=True` to get a `{qualified_name: class}` dict\n* `module_filter=\".*models.*\"` to restrict traversal\n\nSee the full function signature in [`mongospec/utils.py`](./mongospec/utils.py).\n\n---\n\n### Lifecycle Hooks\n\n`MongoDocument` provides two hooks that subclasses can override to inject\ncustom logic before write operations:\n\n| Hook | Called by | Purpose |\n|------|-----------|---------|\n| `__pre_save__(self) -\u003e None` | `insert()`, `insert_one()`, `insert_many()`, `save()` | Mutate instance fields before serialization |\n| `__pre_update__(cls, update) -\u003e dict` | `update_one()`, `update_many()`, `update_by_id()`, `find_one_and_update()` | Modify the update document before execution |\n\n**Example — automatic `updated_at`:**\n\n```python\nfrom datetime import datetime, UTC\nfrom typing import Any\n\nimport msgspec\n\nfrom mongospec import MongoDocument\n\n\nclass Document(MongoDocument):\n    created_at: datetime = msgspec.field(default_factory=lambda: datetime.now(UTC))\n    updated_at: datetime = msgspec.field(default_factory=lambda: datetime.now(UTC))\n\n    def __pre_save__(self) -\u003e None:\n        self.updated_at = datetime.now(UTC)\n\n    @classmethod\n    def __pre_update__(cls, update: dict[str, Any]) -\u003e dict[str, Any]:\n        update.setdefault(\"$set\", {}).setdefault(\"updated_at\", datetime.now(UTC))\n        return update\n```\n\nNow every insert, save, or update operation automatically keeps `updated_at`\nin sync — no caller-side boilerplate needed.\n\n---\n\n### Contrib: KV Store\n\n`mongospec.contrib.kv_store` provides a ready-made async key-value store\nbacked by a MongoDB collection. Designed for multiple inheritance with\nproject-specific base documents.\n\n```python\nfrom mongospec.contrib.kv_store import KVStore, KVStoreItem\nfrom myapp.db import Document  # your base with timestamps, hooks, etc.\n\n\nclass AppStorage(KVStore, Document):\n    __collection_name__ = \"app_storage\"\n```\n\nA unique index on `key` is created automatically at init time.\n\n**Direct usage:**\n\n```python\nawait AppStorage.set(\"theme\", \"dark\")\ntheme = await AppStorage.get(\"theme\")            # \"dark\"\ntheme = await AppStorage.get_or_default(\"x\", 0)  # 0 (no KeyError)\nawait AppStorage.set_default(\"theme\", \"light\")   # \"dark\" (atomic, no overwrite)\n\nawait AppStorage.set_many({\"a\": 1, \"b\": 2})\nall_pairs = await AppStorage.get_all()            # {\"theme\": \"dark\", \"a\": 1, \"b\": 2}\nall_keys  = await AppStorage.keys()               # [\"theme\", \"a\", \"b\"]\n\nawait AppStorage.has(\"theme\")                     # True\nawait AppStorage.delete_key(\"theme\")              # True\n```\n\n**Typed accessor (`KVStoreItem`):**\n\n```python\nAppStorageItem = KVStoreItem.of(AppStorage)\n\nmax_retries = AppStorageItem[int](\"max_retries\", default=3)\n\nvalue = await max_retries.get()          # 3 (default, not persisted)\nawait max_retries.set_default()          # atomically persist default if missing\nawait max_retries.set(10)\nawait max_retries.has()                  # True\nawait max_retries.delete()               # True\n```\n\n| `KVStore` method | Description |\n|------------------|-------------|\n| `set(key, value)` | Upsert a value by key |\n| `get(key)` | Get value or raise `KeyError` |\n| `get_or_default(key, default)` | Get value or return default |\n| `set_default(key, value)` | Atomic insert-if-absent (`$setOnInsert`) |\n| `delete_key(key)` | Delete a key, return `True` if existed |\n| `has(key)` | Check key existence |\n| `get_all()` | Return all pairs as `dict` |\n| `keys()` | Return all key names |\n| `set_many(items)` | Upsert multiple pairs |\n\n---\n\n## Contributing\n\nContributions, issues and feature requests are welcome.\n\n---\n\n## License\n\n[MIT](https://opensource.org/licenses/MIT)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdiprog%2Fmongospec","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdiprog%2Fmongospec","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdiprog%2Fmongospec/lists"}