fix: matched_count for idempotent updates; document drop-on-missing-key (v0.1.2)

- update_document_field/update_document_operator/document_pop_array return
  matched_count > 0, so an idempotent write that matched a doc but changed nothing
  ( to the same value,  of an absent value) reports success instead of False
  (L19)
- document the bare-proxy escape hatch is mongo.collection(name) not mongo[name], and
  that get_document_hashmap/get_document_fields skip docs missing the key (nits).

Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
disqualifier 2026-06-29 17:58:26 -04:00
parent 3ceef9c4a8
commit 51592567e1
4 changed files with 39 additions and 12 deletions

View File

@ -8,13 +8,13 @@ helpers for the common paths, with a raw escape hatch for everything else.
`requirements.txt`: `requirements.txt`:
``` ```
mongo @ git+ssh://git@git.rethinkstudios.io/rethink-public/mongo.git@v0.1.1 mongo @ git+ssh://git@git.rethinkstudios.io/rethink-public/mongo.git@v0.1.2
``` ```
Direct: Direct:
```bash ```bash
pip install "mongo @ git+ssh://git@git.rethinkstudios.io/rethink-public/mongo.git@v0.1.1" pip install "mongo @ git+ssh://git@git.rethinkstudios.io/rethink-public/mongo.git@v0.1.2"
``` ```
Requires `motor` and `pymongo` (pulled transitively). Requires `motor` and `pymongo` (pulled transitively).

View File

@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "mongo" name = "mongo"
version = "0.1.1" version = "0.1.2"
description = "async mongodb wrapper over motor with a raw escape hatch" description = "async mongodb wrapper over motor with a raw escape hatch"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [

View File

@ -7,7 +7,11 @@ def __getattr__(name: str):
"""proxy bare package attribute access to the default instance (PEP 562) """proxy bare package attribute access to the default instance (PEP 562)
lets `import mongo; await mongo.get_documents(...)` work after init(). lets `import mongo; await mongo.get_documents(...)` work after init().
`from mongo import func` still won't see this (resolved before 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.
""" """
if not name.startswith("_") and hasattr(Mongo, name): if not name.startswith("_") and hasattr(Mongo, name):
return getattr(instance(), name) return getattr(instance(), name)

View File

@ -181,7 +181,11 @@ class Mongo:
return [] return []
async def get_document_hashmap(self, collection: str, target: dict, key: str) -> dict: async def get_document_hashmap(self, collection: str, target: dict, key: str) -> dict:
"""return matching documents keyed into a dict by the given field""" """return matching documents keyed into a dict by the given field
documents missing `key` are skipped (not in the result); a later document
with a duplicate key value overwrites an earlier one.
"""
try: try:
cursor = self._db[collection].find(target) cursor = self._db[collection].find(target)
return {doc[key]: doc async for doc in cursor if key in doc} return {doc[key]: doc async for doc in cursor if key in doc}
@ -192,7 +196,11 @@ class Mongo:
async def get_document_fields( async def get_document_fields(
self, collection: str, target: dict, key: str, fields: Optional[dict] = None self, collection: str, target: dict, key: str, fields: Optional[dict] = None
) -> List: ) -> List:
"""return a flat list of one field's value across matching documents""" """return a flat list of one field's value across matching documents
documents missing `key` are skipped, so the list length may be smaller than
the match count.
"""
try: try:
cursor = self._db[collection].find(target, fields) cursor = self._db[collection].find(target, fields)
return [doc[key] async for doc in cursor if key in doc] return [doc[key] async for doc in cursor if key in doc]
@ -299,10 +307,15 @@ class Mongo:
return 0 return 0
async def update_document_field(self, collection: str, target: dict, updates: dict) -> bool: async def update_document_field(self, collection: str, target: dict, updates: dict) -> bool:
"""$set one or more fields on a single document""" """$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.
"""
try: try:
response = await self._db[collection].update_one(target, {"$set": updates}) response = await self._db[collection].update_one(target, {"$set": updates})
return response.modified_count > 0 return response.matched_count > 0
except Exception: except Exception:
log.exception(f"db.update_document_field() on {collection}") log.exception(f"db.update_document_field() on {collection}")
return False return False
@ -310,10 +323,15 @@ class Mongo:
async def update_document_operator( async def update_document_operator(
self, collection: str, target: dict, update_query: dict self, collection: str, target: dict, update_query: dict
) -> bool: ) -> bool:
"""apply raw update operators ($set/$inc/$unset/...) to a single document""" """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.
"""
try: try:
response = await self._db[collection].update_one(target, update_query) response = await self._db[collection].update_one(target, update_query)
return response.modified_count > 0 return response.matched_count > 0
except Exception: except Exception:
log.exception(f"db.update_document_operator() on {collection}") log.exception(f"db.update_document_operator() on {collection}")
return False return False
@ -369,10 +387,15 @@ class Mongo:
async def document_pop_array( async def document_pop_array(
self, collection: str, target: dict, array: str, value: Any self, collection: str, target: dict, array: str, value: Any
) -> bool: ) -> bool:
"""$pull a value from an array field""" """$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.
"""
try: try:
response = await self._db[collection].update_one(target, {"$pull": {array: value}}) response = await self._db[collection].update_one(target, {"$pull": {array: value}})
return response.modified_count > 0 return response.matched_count > 0
except Exception: except Exception:
log.exception(f"db.document_pop_array() on {collection}") log.exception(f"db.document_pop_array() on {collection}")
return False return False