diff --git a/README.md b/README.md index 1e2add4..7aa9e94 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,18 @@ helpers for the common paths, with a raw escape hatch for everything else. `requirements.txt`: ``` -mongo @ git+ssh://git@git.rethinkstudios.io/rethink-public/mongo.git@v0.1.7 +mongo @ git+ssh://git@git.rethinkstudios.io/rethink-public/mongo.git@v0.1.8 ``` Direct: ```bash -pip install "mongo @ git+ssh://git@git.rethinkstudios.io/rethink-public/mongo.git@v0.1.7" +pip install "mongo @ git+ssh://git@git.rethinkstudios.io/rethink-public/mongo.git@v0.1.8" ``` Requires `motor` and `pymongo` (pulled transitively). -Drop the `@v0.1.7` suffix from the line above to install the latest unpinned. +Drop the `@v0.1.8` suffix from the line above to install the latest unpinned. ## Usage diff --git a/pyproject.toml b/pyproject.toml index 84bbebb..88be7d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mongo" -version = "0.1.7" +version = "0.1.8" description = "async mongodb wrapper over motor with a raw escape hatch" requires-python = ">=3.10" dependencies = [ diff --git a/src/mongo/__init__.py b/src/mongo/__init__.py index 1f9d4cd..0e32d92 100644 --- a/src/mongo/__init__.py +++ b/src/mongo/__init__.py @@ -7,11 +7,10 @@ def __getattr__(name: str): """proxy bare package attribute access to the default instance (PEP 562) lets `import mongo; await mongo.get_documents(...)` work after init(). - `from mongo import func` still won't see this (resolved before init). - note: the bare-proxy raw escape hatch is `mongo.collection(name)`, not - `mongo[name]` — module-level subscripting isn't a thing in python, so the proxy - can only forward named attributes. on a Mongo instance both `db[name]` and - `db.collection(name)` work. + `from mongo import func` still won't see this (resolved before init). the + bare-proxy raw escape hatch is `mongo.collection(name)`, not `mongo[name]` — + module-level subscripting isn't a thing in python, so the proxy only forwards + named attributes (an instance's `db[name]` still works fine). """ if not name.startswith("_") and hasattr(MongoDB, name): return getattr(instance(), name) diff --git a/src/mongo/mongo.py b/src/mongo/mongo.py index 4807fdd..c303f44 100644 --- a/src/mongo/mongo.py +++ b/src/mongo/mongo.py @@ -12,8 +12,7 @@ object (preferred), one client per process: async with MongoDB(conn_string, database) as db: await db.get_documents("users", {}) -`Mongo` remains a back-compat alias of `MongoDB` — existing `Mongo(...)` call sites keep -working unchanged. +`Mongo` is a back-compat alias of `MongoDB`; old `Mongo(...)` call sites still work. module proxy (back-compat), arm once then call bare: import mongo # not `from mongo import ...` @@ -21,12 +20,11 @@ module proxy (back-compat), arm once then call bare: await mongo.get_documents("users", {"active": True}) errors: - wrapped methods log and swallow, returning a safe default - (False / [] / {} / 0 / None). .collection(name) / db[name] return - the raw motor collection: full driver surface, raises, nothing swallowed. - NOTE: mongo SWALLOWS by design — this is the one deliberate difference from the - newer datastore trio (redis/psql/mysql), which is fail-loud. mongo's contract is - kept as-is so existing consumers' branch-on-result control flow doesn't break. + wrapped methods log and swallow, returning a safe default (False / [] / {} / 0 / + None). .collection(name) / db[name] return the raw motor collection: full driver + surface, raises, nothing swallowed. mongo SWALLOWS by design — the one deliberate + difference from the fail-loud redis/psql/mysql trio, kept so existing consumers' + branch-on-result control flow doesn't break. naming consistency with the trio (all additive — old names still work): - class is `MongoDB` (was `Mongo`, kept as alias) @@ -54,23 +52,26 @@ class MongoDB: """async mongodb wrapper; one client per process, attach to bot as bot.db""" def __init__(self, connection_string: str, database: str): - # motor builds the client eagerly here (no network I/O yet). a syntactically - # malformed URI (e.g. "mongodb://") makes the driver raise InvalidURI at - # construction, before connect() — a deploy-time value, so it surfaces at startup - # either way and there is nothing to clean up. + # motor builds the client eagerly (no I/O yet); a bad URI raises InvalidURI + # here with nothing built. an invalid db name raises below, after the client + # exists — close it before propagating so it doesn't outlive the raise. self._client = AsyncIOMotorClient(connection_string) - self._db = self._client[database] + try: + self._db = self._client[database] + except BaseException: + self._client.close() + raise async def connect(self) -> "MongoDB": """validate the connection with a ping and return self - motor connects lazily, so this is optional — call it to fail early on a bad - URI/credentials rather than on the first real op (parallel to the trio's - connect()). raises on a bad connection, unlike the swallowing wrapped methods. + optional — motor connects lazily, so this just fails early on a bad + URI/credentials (parallel to the trio's connect()). raises, unlike the + swallowing wrapped methods. - an instance is single-use: pymongo 4.x `close()` is irreversible, and connect() - only pings (it does not rebuild the client). do not reuse an instance after - close()/`async with` exit — construct a fresh MongoDB. + an instance is single-use: pymongo 4.x `close()` is irreversible and + connect() doesn't rebuild the client. construct a fresh MongoDB instead of + reusing one after close()/`async with` exit. """ await self._client.admin.command("ping") return self @@ -79,10 +80,9 @@ class MongoDB: try: return await self.connect() except BaseException: - # connect()/ping failing here would otherwise leak the motor client (built - # eagerly in __init__ with a background topology monitor + pool) — __aexit__ - # is not called when __aenter__ raises. close it before propagating so a - # retry loop against a flapping server doesn't accumulate live clients. + # __aexit__ is not called when __aenter__ raises, so a failed ping here + # would otherwise leak the motor client built in __init__. close it + # before propagating. self.close() raise @@ -122,9 +122,7 @@ class MongoDB: """create a collection, optionally seeding ONE index `index` is a single index key-spec (e.g. `[("a", 1), ("b", 1)]` builds one - compound index, NOT two). for multiple indexes call `create_index` per index. - renamed from `indexes` in v0.1.6 to reflect that it seeds a single index; pass it - positionally, or update a keyword call site from `indexes=` to `index=`. + compound index, NOT two); call `create_index` per index for multiples. """ try: await self._db.create_collection(collection) @@ -188,11 +186,10 @@ class MongoDB: ) -> int: """insert many documents, returning the inserted count - on a partial failure (e.g. ordered=False with one duplicate key among - several documents) the driver raises BulkWriteError AFTER inserting the - non-failing documents; that count is read from `e.details['nInserted']` - so the return value stays honest — a caller retrying the whole batch on a - wrongly-reported 0 would re-insert documents that already landed. + on a partial failure (e.g. ordered=False with one duplicate key) the driver + raises BulkWriteError AFTER inserting the rest; count comes from + `e.details['nInserted']` so a caller retrying on a wrongly-reported 0 doesn't + re-insert documents that already landed. """ try: result = await self._db[collection].insert_many(documents, ordered=ordered) @@ -379,9 +376,9 @@ class MongoDB: async def update_document_field(self, collection: str, target: dict, updates: dict) -> bool: """$set one or more fields on a single document - returns True when a document matched, even if the write changed nothing (a - `$set` to the identical value leaves `modified_count=0`); use `matched_count` - so an idempotent no-op on an existing doc isn't misread as a failure. + returns True when a document matched, even if `$set` to an identical value + leaves `modified_count=0` — uses `matched_count` so an idempotent no-op isn't + misread as a failure. """ try: response = await self._db[collection].update_one(target, {"$set": updates}) @@ -395,9 +392,8 @@ class MongoDB: ) -> bool: """apply raw update operators ($set/$inc/$unset/...) to a single document - returns True when a document matched, even if the operators changed nothing - (e.g. `$set` to an identical value) — use `matched_count` so an idempotent - write on an existing doc isn't misread as a failure. + returns True when a document matched, even if the operators changed nothing — + uses `matched_count` so an idempotent write isn't misread as a failure. """ try: response = await self._db[collection].update_one(target, update_query) @@ -409,10 +405,9 @@ class MongoDB: async def upsert_document_field(self, collection: str, target: dict, updates: dict) -> bool: """$set fields on a single document, creating it if absent - returns True when a document matched or was upserted, even if the write - changed nothing (a `$set` to the identical value leaves `modified_count=0`); - use `matched_count` so an idempotent no-op upsert on an existing doc isn't - misread as a failure — mirrors update_document_field/update_document_operator. + returns True when a document matched or was upserted, even if `$set` to an + identical value leaves `modified_count=0` — mirrors update_document_field's + matched_count-over-modified_count contract. """ try: response = await self._db[collection].update_one( @@ -439,9 +434,8 @@ class MongoDB: ) -> bool: """$push a value onto an array field - returns True when a document matched (consistent with the other single-doc - update helpers); $push always mutates, so matched_count and modified_count - agree here in practice. + returns True when a document matched; $push always mutates, so matched_count + and modified_count agree here in practice. """ try: response = await self._db[collection].update_one(target, {"$push": {array: value}}) @@ -456,8 +450,7 @@ class MongoDB: ) -> bool: """$push to an array and $set a field in one update - returns True when a document matched (consistent with the other single-doc - update helpers). + returns True when a document matched (matched_count, not modified_count). """ try: response = await self._db[collection].update_one( @@ -474,9 +467,8 @@ class MongoDB: ) -> bool: """$pull a value from an array field - returns True when a document matched, even if nothing was pulled (the value - was absent) — use `matched_count` so a no-op pull on an existing doc isn't - misread as a failure. + returns True when a document matched, even if the value was absent and + nothing was pulled — uses `matched_count`, not `modified_count`. """ try: response = await self._db[collection].update_one(target, {"$pull": {array: value}}) @@ -556,10 +548,9 @@ class MongoDB: # bulk / checks async def bulk_write(self, collection: str, operations: list, ordered: bool = True) -> dict: - """ - run a batch of pymongo write ops (InsertOne/UpdateOne/DeleteOne/...) + """run a batch of pymongo write ops (InsertOne/UpdateOne/DeleteOne/...) - caller builds the ops from pymongo, e.g.: + caller builds the ops, e.g.: from pymongo import UpdateOne ops = [UpdateOne({'_id': i}, {'$set': {...}}, upsert=True) for i in ids] returns a summary dict of counts, or an empty dict on failure @@ -580,8 +571,7 @@ class MongoDB: async def document_check_multi( self, collection: str, target: dict, checks: List[str], value: str ) -> bool: - """ - return whether a doc matching target has value in any of the given arrays + """return whether a doc matching target has value in any of the given arrays target is a normal filter dict (merged into the query), not a bare _id """ @@ -602,12 +592,9 @@ Mongo = MongoDB # ----------------------------------------------------------------------------- -# backwards-compat module proxy -# - lets legacy call sites keep using `await mongo.get_documents(...)` -# without an object, after a one-time `mongo.init(conn, db)` -# - works with `import mongo; mongo.func(...)` (resolved at call time) -# - does NOT work with `from mongo import func` (resolved at import, -# before init runs) — switch those sites to `import mongo` +# backwards-compat module proxy: `import mongo; mongo.init(conn, db)` then +# bare `await mongo.func(...)`. needs `import mongo` — `from mongo import func` +# resolves at import time, before init() runs. _default: Optional[MongoDB] = None