mongo-store
A lightweight MongoDB data layer for Python asyncio apps — define your models as pure JSON schemas, query with GQL tree syntax that compiles to a single $lookup aggregation, and get role-based access control out of the box.
Features
- Pure JSON schemas, zero code — a model is just a dict: fields, relations, computes, indexes.
- Read-time defaults & computed columns — writes store only user data; reads fill defaults and run
fn/asyncFncomputes. - GQL tree queries → one
$lookup— nested relations resolve via a single aggregation pipeline; never hand-write$lookupagain. - Smart mutation —
mutation()auto-detects upsert by_id+ unique index and recursively fills relation children. - Soft-delete built in — every schema auto-registers a
<Model>Deletedarchive collection;remove()archives before deleting. - Permission context — ContextVar-based roles (
super_admin/admin/guest/creator...), schema/field-level read/write whitelists, automatic owner-condition injection. - Async-first — built on PyMongo's
AsyncMongoClient(pymongo >= 4.9).
Installation
pip install mongo-store-py
The distribution name is
mongo-store-py; the import package ismongo_store:from mongo_store import init, store.
Requires Python 3.10+ and MongoDB.
Renamed package. This project was previously published as
mongo-store. Please usemongo-store-pygoing forward — the API and themongo_storeimport name are unchanged.
Quick start
from pymongo import AsyncMongoClient
from mongo_store import init, store
client = AsyncMongoClient("mongodb://localhost:27017")
await init(client["mydb"]) # idempotently creates indexes for registered schemas
# Register a schema (pure JSON)
store.register({
"name": "Post", # model name used in GQL
"collection": "posts", # optional, defaults to name
"idPrefix": "PT", # string _id: prefix + base36 timestamp + random
"fields": {
"title": {"type": "string", "default": ""},
"status": {"type": "string", "default": "draft"},
"tags": {"type": "array", "default": []},
},
"computes": {
"statusLabel": {"type": "string", "depends": ["status"],
"fn": lambda doc: doc["status"].upper()},
},
"indexes": [{"keys": {"status": 1, "createdAt": -1}}],
})
# Write — only user data; defaults are filled on read
doc = await store.insert("Post", {"title": "Hello"})
# Query — GQL tree syntax, values referenced from params via @key
items = await store.query(
"Post($condition:@c0,$sort:@s1,$limit:@l) { title, status, statusLabel }",
{"c0": {"status": "draft"}, "s1": {"createdAt": -1}, "l": 20},
)
GQL syntax
Model($condition:@c0,$sort:@s1,$skip:@sk,$limit:@l1) {
field1, field2, obj.subField,
Relation($condition:@c2,$sort:@s3,$limit:@l2) { f3, Nested { f4 } }
}
- Values come from the params dict:
{"c0": {...}, "s1": {...}}. - Object sub-fields use dot notation; relations are declared in the schema (
type: "many" | "one") and resolved automatically — do not hand-write$lookup. $pipelinepasses a raw aggregation through as-is (no compute/defaults/permission trimming) — use with care; preferstore.aggregate(model, pipeline)for group/sum needs.
Query & write API
items = await store.query(gql, params) # list[dict]
one = await store.query_one(gql, params) # dict | None
page = await store.query_with_count(gql, params) # {'items','total','hasMore','page','pageSize'} (pageSize capped at 5000)
exists = await store.exists("Post", {"_id": pid})
n = await store.count("Post", {"status": "active"})
doc = await store.insert("Post", {...}) # auto _id / createdAt / updatedAt
docs = await store.insert_many("Post", [{...}, ...])
await store.update("Post", {"_id": pid}, {"status": "live"}) # plain fields → $set
await store.update("Post", {"_id": pid}, {"$inc": {"views": 1}}) # '$'-prefixed keys pass through as operators
await store.update_many("Post", {"type": t}, {"status": "live"})
r = await store.remove("Post", {"_id": pid}) # archives to <collection>_deleted first
await store.mutation("Post", {...}) # smart upsert + recursive relation children
await store.upsert("Post", {"code": "A1"}, {...}) # explicit-condition upsert (no relation handling)
rows = await store.aggregate("Post", pipeline) # native aggregation
Notes:
Nonevalues are stripped before persisting;_idcannot be changed viaupdate.createdAt/updatedAt(ms) are framework-maintained — do not set them manually.- Snake-case aliases available:
query_one,insert_many,update_many,parse_gql,build_pipeline, ...
Permission context
# Set once per request (in router/dependency layer)
store.set_context({"userId": uid, "roles": ["editor"]})
# Internal/cron jobs — bypass permission checks
await store.run_as_internal(lambda: store.remove("Post", {"_id": pid}))
super_admin/admin/internalroles pass everything; other roles are checked against schema-level and field-levelread/writewhitelists;guestcan never write.creatoris a pseudo-role resolved bydoc.createdBy == ctx.userId; schemas granting it automatically get owner conditions injected on queries and ownership checks on update/remove.- No context set → permission checks disabled (backward compatible).
- Denied access raises
store.PermissionError(withstatus = 403).
Schema reference
{
"name": "Order",
"collection": "orders",
"idPrefix": "OD",
"timestamps": True, # default: auto-maintain createdAt/updatedAt (ms)
"fields": {
"_id": "string", # shorthand
"title": {"type": "string", "default": ""},
"meta": {"type": "object", "default": {}, "fields": {...}}, # nested object fields
},
"relations": {
"items": {"model": "OrderItem", "type": "many",
"localField": "_id", "foreignField": "orderId"},
},
"computes": {
"total": {"type": "float", "depends": ["amount"], "fn": lambda d: d["amount"] * 1.1},
"itemCount": {"type": "int", "lookup": {"$size": {"$ifNull": ["$items", []]}}},
},
"indexes": [
{"keys": {"status": 1}},
{"keys": {"code": 1}, "options": {"unique": True}},
],
"read": ["editor", "viewer"], # optional schema-level role whitelists
"write": ["editor"],
}
Types: string | int | long | float | double | boolean | array | object | date | any.
License
Metadata
Release files for mongo-store-py 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| mongo_store_py-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for mongo_store_py-0.1.0-py3-none-any.whl |
Release files / mongo_store_py-0.1.0-py3-none-any.whl
| Download URL | mongo_store_py-0.1.0-py3-none-any.whl |
|---|---|
| Size | 30.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4ebe5854cf3b0d5ce771fb4382efa4d016ee9af5f070854dee814c1c3831f558
|
|
BLAKE2b-256 checksum How to use checksums |
e1b23e39534f940024e7d096cbf9357f251a75bff3e66b8ff688313fa0ef22b8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.4
|
Security for mongo-store-py 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.