Skip to content

Running in production

import logging
from typing import Annotated

from mongospecs import Action, Index, Registry, SpecConfig, logging_listener
from mongospecs.msgspec import Spec


class Account(Spec):
    __spec__ = SpecConfig(collection="accounts", indexes=[Index("email", unique=True), Index("-balance")])
    email: str = ""
    balance: Annotated[int, "cents"] = 0


events = []
registry = await Registry(
    "mongodb://localhost/app",
    models=[Account],
    max_time_ms=5000,  # default time limit for reads
    on_command=[events.append, logging_listener(level=logging.DEBUG)],
).open()

Indexes

Nothing is built implicitly. plan_indexes() compares what is declared with what exists; apply_indexes() creates what is missing and only drops or recreates with drop=True.

plan = await registry.plan_indexes()
assert {a.name: a.action for a in plan} == {"email_1": Action.CREATE, "balance_-1": Action.CREATE}
await registry.apply_indexes(plan)
assert (await registry.plan_indexes()).in_sync
Action Meaning Destructive
CREATE declared, missing no
RETAIN declared and identical no
RECREATE same name, different definition yes
CONFLICT same keys under another name yes
DROP exists, not declared yes

plan.advisories lists non-blocking findings, such as a reverse relation whose foreign key has no index.

Observability

Every driver command produces started and succeeded/failed events carrying the model and the mongospecs operation.

await Account(email="a@example.com").insert()
started = [e for e in events if e.phase == "started" and e.model]
assert (started[-1].model, started[-1].op, started[-1].command_name) == ("Account", "insert", "insert")

logging_listener(), otel_listener() (extra otel) and sentry_listener() (extra sentry) are ready-made listeners. With the Sentry SDK, database work shows up as Account.insert, Account.find and so on inside the active trace, on Sentry or any compatible server.

Errors

mongospecs raises its own errors and keeps the driver error as __cause__. See Errors for each one, what to do about it and what an API client sees.

One registry per model, or explicit managers

Model.find() and the other class methods use the registry the model is bound to, and a model class can be bound to only one open registry at a time. When you need the same model on several registries, or one registry per thread, ask the registry for a manager instead: registry.manager(Article).find(...). Documents loaded through a manager are saved through it.

More than one database

archive = Account.using(db="app_archive")
await archive.insert(Account(email="old@example.com"))
assert await archive.count() == 1 and await Account.count() == 1

await registry.client.drop_database("app_archive")
await registry.client.drop_database("app")
await registry.close()

Also: SpecConfig(shard_key=[...]) adds the shard key to document-level writes so they are targeted; Model.watch() opens a change stream; SpecConfig(json_schema_validation="strict") installs a $jsonSchema validator derived from the model when indexes are applied.