mindtrace-database 0.10.0__tar.gz → 0.11.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/PKG-INFO +3 -3
- {mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace/database/backends/mongo_odm.py +293 -7
- {mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace/database/backends/unified_odm.py +22 -0
- {mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace_database.egg-info/PKG-INFO +3 -3
- {mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace_database.egg-info/requires.txt +2 -2
- {mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/pyproject.toml +3 -3
- {mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/LICENSE +0 -0
- {mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/README.md +0 -0
- {mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace/database/__init__.py +0 -0
- {mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace/database/backends/mindtrace_odm.py +0 -0
- {mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace/database/backends/redis_odm.py +0 -0
- {mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace/database/backends/registry_odm.py +0 -0
- {mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace/database/core/exceptions.py +0 -0
- {mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace/database/sample/user.py +0 -0
- {mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace_database.egg-info/SOURCES.txt +0 -0
- {mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace_database.egg-info/dependency_links.txt +0 -0
- {mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace_database.egg-info/top_level.txt +0 -0
- {mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mindtrace-database
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Database functionality for Mindtrace
|
|
5
5
|
Author: Mindtrace Team
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -11,13 +11,13 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
13
|
Requires-Dist: beanie<2,>=1.29.0
|
|
14
|
-
Requires-Dist: mindtrace-core>=0.
|
|
14
|
+
Requires-Dist: mindtrace-core>=0.11.0
|
|
15
15
|
Requires-Dist: pydantic>=2.11.1
|
|
16
16
|
Requires-Dist: redis>=4.0.0
|
|
17
17
|
Requires-Dist: redis-om>=0.3.5
|
|
18
18
|
Requires-Dist: motor>=3.3.0
|
|
19
19
|
Requires-Dist: pymongo>=4.14.0
|
|
20
|
-
Requires-Dist: mindtrace-registry>=0.
|
|
20
|
+
Requires-Dist: mindtrace-registry>=0.11.0
|
|
21
21
|
Dynamic: license-file
|
|
22
22
|
|
|
23
23
|
[](https://pypi.org/project/mindtrace-database/)
|
{mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace/database/backends/mongo_odm.py
RENAMED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from
|
|
2
|
+
from collections.abc import AsyncIterator, Iterator
|
|
3
|
+
from typing import Any, Dict, List, Optional, Type, TypeVar
|
|
3
4
|
|
|
4
5
|
from beanie import Document, PydanticObjectId, init_beanie
|
|
6
|
+
from beanie.odm.utils.dump import get_dict
|
|
7
|
+
from bson import ObjectId
|
|
8
|
+
from bson.errors import InvalidId
|
|
5
9
|
from motor.motor_asyncio import AsyncIOMotorClient
|
|
6
10
|
from pydantic import BaseModel
|
|
11
|
+
from pymongo import MongoClient
|
|
7
12
|
from pymongo.errors import DuplicateKeyError
|
|
8
13
|
|
|
9
14
|
from mindtrace.database.backends.mindtrace_odm import InitMode, MindtraceODM
|
|
@@ -44,6 +49,12 @@ class MindtraceDocument(Document):
|
|
|
44
49
|
|
|
45
50
|
ModelType = TypeVar("ModelType", bound=MindtraceDocument)
|
|
46
51
|
|
|
52
|
+
# Beanie binds each Document class to one database *process-wide*. A second ``init_beanie`` for the same
|
|
53
|
+
# ``model_cls`` (whether another ``db_name``, another URI, or another ``MongoMindtraceODM`` instance)
|
|
54
|
+
# would re-bind that class and break earlier lakes. Only the first ODM per ``model_cls`` runs
|
|
55
|
+
# ``init_beanie``; subsequent backends set ``_motor_routing`` and use Motor on ``self.client[self.db_name]``.
|
|
56
|
+
_BEANIE_INITIALIZED_MODEL_CLASSES: set[Type[Any]] = set()
|
|
57
|
+
|
|
47
58
|
|
|
48
59
|
class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
|
|
49
60
|
"""
|
|
@@ -87,6 +98,8 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
|
|
|
87
98
|
allow_index_dropping: bool = False,
|
|
88
99
|
auto_init: bool = False,
|
|
89
100
|
init_mode: InitMode | None = None,
|
|
101
|
+
*,
|
|
102
|
+
client: AsyncIOMotorClient | None = None,
|
|
90
103
|
):
|
|
91
104
|
"""
|
|
92
105
|
Initialize the MongoDB ODM backend.
|
|
@@ -127,8 +140,17 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
|
|
|
127
140
|
if not db_uri or not db_name:
|
|
128
141
|
raise ValueError("db_uri and db_name are required")
|
|
129
142
|
|
|
130
|
-
self.
|
|
143
|
+
self.db_uri = db_uri
|
|
144
|
+
if client is not None:
|
|
145
|
+
self.client = client
|
|
146
|
+
self._owns_client = False
|
|
147
|
+
else:
|
|
148
|
+
self.client = AsyncIOMotorClient(db_uri)
|
|
149
|
+
self._owns_client = True
|
|
150
|
+
self._sync_client: MongoClient | None = None
|
|
151
|
+
self._closed = False
|
|
131
152
|
self.db_name = db_name
|
|
153
|
+
self._motor_routing = False
|
|
132
154
|
self._allow_index_dropping = allow_index_dropping
|
|
133
155
|
self._is_initialized = False
|
|
134
156
|
self._model_odms: Dict[str, "MongoMindtraceODM"] = {}
|
|
@@ -152,9 +174,8 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
|
|
|
152
174
|
allow_index_dropping=allow_index_dropping,
|
|
153
175
|
auto_init=False, # We'll initialize all together
|
|
154
176
|
init_mode=init_mode,
|
|
177
|
+
client=self.client,
|
|
155
178
|
)
|
|
156
|
-
# Share the same client instance
|
|
157
|
-
odm.client = self.client
|
|
158
179
|
# Store parent reference for initialization delegation
|
|
159
180
|
odm._parent_odm = self
|
|
160
181
|
self._model_odms[name] = odm
|
|
@@ -192,6 +213,56 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
|
|
|
192
213
|
# Defer initialization - operations will auto-init on first use
|
|
193
214
|
self._needs_init = True
|
|
194
215
|
|
|
216
|
+
def _collection_name(self) -> str:
|
|
217
|
+
if self.model_cls is None:
|
|
218
|
+
raise ValueError("model_cls is required for collection name resolution")
|
|
219
|
+
return getattr(self.model_cls.Settings, "name")
|
|
220
|
+
|
|
221
|
+
def _motor_collection(self):
|
|
222
|
+
return self.client[self.db_name][self._collection_name()]
|
|
223
|
+
|
|
224
|
+
def _sync_collection(self):
|
|
225
|
+
if self._sync_client is None:
|
|
226
|
+
self._sync_client = MongoClient(self.db_uri)
|
|
227
|
+
return self._sync_client[self.db_name][self._collection_name()]
|
|
228
|
+
|
|
229
|
+
def _mongo_doc_to_model(self, doc: dict | None) -> T | None:
|
|
230
|
+
if doc is None:
|
|
231
|
+
return None
|
|
232
|
+
payload = dict(doc)
|
|
233
|
+
if "_id" in payload and "id" not in payload:
|
|
234
|
+
payload["id"] = payload["_id"]
|
|
235
|
+
return self.model_cls.model_validate(payload)
|
|
236
|
+
|
|
237
|
+
def _motor_model_dump(self, doc: BaseModel) -> dict[str, Any]:
|
|
238
|
+
"""Serialize a model for raw Motor insert/replace.
|
|
239
|
+
|
|
240
|
+
Pydantic v2 ``model_dump`` on Beanie ``Document`` can fail (class-level
|
|
241
|
+
descriptors such as ``ExpressionField``). Beanie's BSON ``get_dict`` matches
|
|
242
|
+
``Document.insert`` and avoids that serializer path.
|
|
243
|
+
"""
|
|
244
|
+
if isinstance(doc, Document):
|
|
245
|
+
return get_dict(doc, to_db=True, keep_nulls=doc.get_settings().keep_nulls)
|
|
246
|
+
return doc.model_dump(mode="python", by_alias=True, serialize_as_any=True)
|
|
247
|
+
|
|
248
|
+
def _motor_patch_fields(self, obj: BaseModel) -> dict[str, Any]:
|
|
249
|
+
"""Shallow field map from ``obj`` for merge-into-document updates (Motor mode)."""
|
|
250
|
+
if isinstance(obj, Document):
|
|
251
|
+
return {name: getattr(obj, name) for name in obj.model_fields if name != "id"}
|
|
252
|
+
return obj.model_dump(exclude={"id"}, serialize_as_any=True)
|
|
253
|
+
|
|
254
|
+
@staticmethod
|
|
255
|
+
def _to_object_id(id: str | PydanticObjectId | ObjectId) -> ObjectId:
|
|
256
|
+
# ``PydanticObjectId`` subclasses ``ObjectId``; check it first so we normalize via ``str``.
|
|
257
|
+
if isinstance(id, PydanticObjectId):
|
|
258
|
+
return ObjectId(str(id))
|
|
259
|
+
if isinstance(id, ObjectId):
|
|
260
|
+
return id
|
|
261
|
+
try:
|
|
262
|
+
return ObjectId(str(id))
|
|
263
|
+
except InvalidId as exc:
|
|
264
|
+
raise DocumentNotFoundError(f"Invalid document id {id!r}") from exc
|
|
265
|
+
|
|
195
266
|
async def _do_initialize(self):
|
|
196
267
|
"""Internal method to perform the actual initialization."""
|
|
197
268
|
if not self._is_initialized:
|
|
@@ -199,7 +270,12 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
|
|
|
199
270
|
# Multi-model mode: initialize all models together
|
|
200
271
|
document_models = list(self._models.values())
|
|
201
272
|
else:
|
|
202
|
-
# Single model mode
|
|
273
|
+
# Single model mode: at most one init_beanie per Document subclass in this process.
|
|
274
|
+
if self.model_cls in _BEANIE_INITIALIZED_MODEL_CLASSES:
|
|
275
|
+
self._motor_routing = True
|
|
276
|
+
self._is_initialized = True
|
|
277
|
+
return
|
|
278
|
+
_BEANIE_INITIALIZED_MODEL_CLASSES.add(self.model_cls)
|
|
203
279
|
document_models = [self.model_cls]
|
|
204
280
|
|
|
205
281
|
await init_beanie(
|
|
@@ -253,6 +329,31 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
|
|
|
253
329
|
self._allow_index_dropping = allow_index_dropping
|
|
254
330
|
await self._do_initialize()
|
|
255
331
|
|
|
332
|
+
def close(self) -> None:
|
|
333
|
+
"""Release resources held by this ODM.
|
|
334
|
+
|
|
335
|
+
Closes the lazily-created synchronous client if present, and closes
|
|
336
|
+
the owning motor client if this ODM created it. ODMs constructed with
|
|
337
|
+
an injected ``client=`` hold a non-owning reference and do not close
|
|
338
|
+
it on shutdown — responsibility stays with whoever created the client.
|
|
339
|
+
|
|
340
|
+
Idempotent: safe to call multiple times.
|
|
341
|
+
"""
|
|
342
|
+
if self._closed:
|
|
343
|
+
return
|
|
344
|
+
if self._sync_client is not None:
|
|
345
|
+
try:
|
|
346
|
+
self._sync_client.close()
|
|
347
|
+
except Exception:
|
|
348
|
+
pass
|
|
349
|
+
self._sync_client = None
|
|
350
|
+
if self._owns_client and self.client is not None:
|
|
351
|
+
try:
|
|
352
|
+
self.client.close()
|
|
353
|
+
except Exception:
|
|
354
|
+
pass
|
|
355
|
+
self._closed = True
|
|
356
|
+
|
|
256
357
|
def __getattr__(self, name: str):
|
|
257
358
|
"""Support attribute-based access to model-specific ODMs in multi-model mode.
|
|
258
359
|
|
|
@@ -340,11 +441,22 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
|
|
|
340
441
|
doc.id = None
|
|
341
442
|
|
|
342
443
|
try:
|
|
444
|
+
if self._motor_routing:
|
|
445
|
+
raw = self._motor_model_dump(doc)
|
|
446
|
+
raw.pop("_id", None)
|
|
447
|
+
if raw.get("id") is None:
|
|
448
|
+
raw.pop("id", None)
|
|
449
|
+
result = await self._motor_collection().insert_one(raw)
|
|
450
|
+
inserted = await self._motor_collection().find_one({"_id": result.inserted_id})
|
|
451
|
+
if inserted is None:
|
|
452
|
+
raise DuplicateInsertError("insert_one did not persist a readable document")
|
|
453
|
+
out = self._mongo_doc_to_model(inserted)
|
|
454
|
+
if out is None:
|
|
455
|
+
raise DuplicateInsertError("Could not deserialize inserted document")
|
|
456
|
+
return out
|
|
343
457
|
return await doc.insert()
|
|
344
458
|
except DuplicateKeyError as e:
|
|
345
459
|
raise DuplicateInsertError(f"Duplicate key error: {str(e)}")
|
|
346
|
-
except Exception as e:
|
|
347
|
-
raise DuplicateInsertError(str(e))
|
|
348
460
|
|
|
349
461
|
async def get(self, id: str | PydanticObjectId, fetch_links: bool = False) -> T:
|
|
350
462
|
"""
|
|
@@ -381,6 +493,18 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
|
|
|
381
493
|
if not self._is_initialized:
|
|
382
494
|
await self.initialize()
|
|
383
495
|
|
|
496
|
+
if self._motor_routing:
|
|
497
|
+
if fetch_links:
|
|
498
|
+
raise NotImplementedError("fetch_links is not supported for this database routing mode")
|
|
499
|
+
oid = self._to_object_id(id)
|
|
500
|
+
doc = await self._motor_collection().find_one({"_id": oid})
|
|
501
|
+
if not doc:
|
|
502
|
+
raise DocumentNotFoundError(f"Object with id {id} not found")
|
|
503
|
+
out = self._mongo_doc_to_model(doc)
|
|
504
|
+
if out is None:
|
|
505
|
+
raise DocumentNotFoundError(f"Object with id {id} not found")
|
|
506
|
+
return out
|
|
507
|
+
|
|
384
508
|
doc = await self.model_cls.get(id, fetch_links=fetch_links)
|
|
385
509
|
if not doc:
|
|
386
510
|
raise DocumentNotFoundError(f"Object with id {id} not found")
|
|
@@ -421,6 +545,29 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
|
|
|
421
545
|
if not self._is_initialized:
|
|
422
546
|
await self.initialize()
|
|
423
547
|
|
|
548
|
+
if self._motor_routing:
|
|
549
|
+
if isinstance(obj, self.model_cls):
|
|
550
|
+
if not obj.id:
|
|
551
|
+
raise DocumentNotFoundError("Document must have an id to be updated")
|
|
552
|
+
oid = self._to_object_id(obj.id)
|
|
553
|
+
raw = self._motor_model_dump(obj)
|
|
554
|
+
await self._motor_collection().replace_one({"_id": oid}, raw)
|
|
555
|
+
return obj
|
|
556
|
+
if not hasattr(obj, "id") or not obj.id:
|
|
557
|
+
raise DocumentNotFoundError("Document must have an id to be updated")
|
|
558
|
+
oid = self._to_object_id(obj.id)
|
|
559
|
+
existing = await self._motor_collection().find_one({"_id": oid})
|
|
560
|
+
if not existing:
|
|
561
|
+
raise DocumentNotFoundError(f"Object with id {obj.id} not found")
|
|
562
|
+
doc = self._mongo_doc_to_model(existing)
|
|
563
|
+
if doc is None:
|
|
564
|
+
raise DocumentNotFoundError(f"Object with id {obj.id} not found")
|
|
565
|
+
for key, value in self._motor_patch_fields(obj).items():
|
|
566
|
+
setattr(doc, key, value)
|
|
567
|
+
raw = self._motor_model_dump(doc)
|
|
568
|
+
await self._motor_collection().replace_one({"_id": oid}, raw)
|
|
569
|
+
return doc
|
|
570
|
+
|
|
424
571
|
# Check if obj is already a document instance
|
|
425
572
|
if isinstance(obj, self.model_cls):
|
|
426
573
|
# If it's already a document instance, just save it
|
|
@@ -471,6 +618,13 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
|
|
|
471
618
|
if not self._is_initialized:
|
|
472
619
|
await self.initialize()
|
|
473
620
|
|
|
621
|
+
if self._motor_routing:
|
|
622
|
+
oid = self._to_object_id(id)
|
|
623
|
+
result = await self._motor_collection().delete_one({"_id": oid})
|
|
624
|
+
if result.deleted_count == 0:
|
|
625
|
+
raise DocumentNotFoundError(f"Object with id {id} not found")
|
|
626
|
+
return
|
|
627
|
+
|
|
474
628
|
doc = await self.model_cls.get(id)
|
|
475
629
|
if doc:
|
|
476
630
|
await doc.delete()
|
|
@@ -502,6 +656,10 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
|
|
|
502
656
|
if not self._is_initialized:
|
|
503
657
|
await self.initialize()
|
|
504
658
|
|
|
659
|
+
if self._motor_routing:
|
|
660
|
+
raw = await self._motor_collection().find({}).to_list(length=None)
|
|
661
|
+
return [m for m in (self._mongo_doc_to_model(d) for d in raw) if m is not None]
|
|
662
|
+
|
|
505
663
|
return await self.model_cls.find_all().to_list()
|
|
506
664
|
|
|
507
665
|
async def find(self, *args, fetch_links: bool = False, **kwargs) -> List[T]:
|
|
@@ -541,10 +699,136 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
|
|
|
541
699
|
# Remove fetch_links from kwargs if present (it's a parameter, not a query field)
|
|
542
700
|
kwargs_without_fetch_links = {k: v for k, v in kwargs.items() if k != "fetch_links"}
|
|
543
701
|
|
|
702
|
+
if self._motor_routing:
|
|
703
|
+
if fetch_links:
|
|
704
|
+
raise NotImplementedError("fetch_links is not supported for this database routing mode")
|
|
705
|
+
if len(args) != 1 or not isinstance(args[0], dict) or kwargs_without_fetch_links:
|
|
706
|
+
raise NotImplementedError(
|
|
707
|
+
"Only a single dict filter is supported for find() in this database routing mode"
|
|
708
|
+
)
|
|
709
|
+
raw = await self._motor_collection().find(args[0]).to_list(length=None)
|
|
710
|
+
return [m for m in (self._mongo_doc_to_model(d) for d in raw) if m is not None]
|
|
711
|
+
|
|
544
712
|
# In Beanie, fetch_links is passed as a parameter to find(), not called as a method
|
|
545
713
|
query = self.model_cls.find(*args, fetch_links=fetch_links, **kwargs_without_fetch_links)
|
|
546
714
|
return await query.to_list()
|
|
547
715
|
|
|
716
|
+
async def find_iter(
|
|
717
|
+
self,
|
|
718
|
+
query: dict[str, Any] | None = None,
|
|
719
|
+
*,
|
|
720
|
+
sort: list[tuple[str, int]] | None = None,
|
|
721
|
+
batch_size: int | None = None,
|
|
722
|
+
limit: int | None = None,
|
|
723
|
+
) -> AsyncIterator[T]:
|
|
724
|
+
"""
|
|
725
|
+
Lazily iterate over matching documents using a Mongo cursor.
|
|
726
|
+
|
|
727
|
+
This is intended for large scans where materializing the full result set
|
|
728
|
+
would be wasteful.
|
|
729
|
+
"""
|
|
730
|
+
if self._models is not None:
|
|
731
|
+
raise ValueError("Cannot use find_iter() in multi-model mode. Use db.model_name.find_iter() instead.")
|
|
732
|
+
|
|
733
|
+
if not self._is_initialized:
|
|
734
|
+
await self.initialize()
|
|
735
|
+
|
|
736
|
+
cursor = self._motor_collection().find(query or {})
|
|
737
|
+
if sort:
|
|
738
|
+
cursor = cursor.sort(sort)
|
|
739
|
+
if batch_size is not None:
|
|
740
|
+
cursor = cursor.batch_size(batch_size)
|
|
741
|
+
if limit is not None:
|
|
742
|
+
cursor = cursor.limit(limit)
|
|
743
|
+
|
|
744
|
+
try:
|
|
745
|
+
async for raw in cursor:
|
|
746
|
+
model = self._mongo_doc_to_model(raw)
|
|
747
|
+
if model is not None:
|
|
748
|
+
yield model
|
|
749
|
+
finally:
|
|
750
|
+
await cursor.close()
|
|
751
|
+
|
|
752
|
+
async def find_window(
|
|
753
|
+
self,
|
|
754
|
+
query: dict[str, Any] | None = None,
|
|
755
|
+
*,
|
|
756
|
+
sort: list[tuple[str, int]] | None = None,
|
|
757
|
+
limit: int | None = None,
|
|
758
|
+
) -> List[T]:
|
|
759
|
+
"""
|
|
760
|
+
Return a bounded, sorted window of matching documents.
|
|
761
|
+
|
|
762
|
+
This is useful for page-oriented callers that need ``limit + 1`` rows to
|
|
763
|
+
determine whether another page exists without loading the full collection.
|
|
764
|
+
"""
|
|
765
|
+
if self._models is not None:
|
|
766
|
+
raise ValueError("Cannot use find_window() in multi-model mode. Use db.model_name.find_window() instead.")
|
|
767
|
+
|
|
768
|
+
if not self._is_initialized:
|
|
769
|
+
await self.initialize()
|
|
770
|
+
|
|
771
|
+
cursor = self._motor_collection().find(query or {})
|
|
772
|
+
if sort:
|
|
773
|
+
cursor = cursor.sort(sort)
|
|
774
|
+
raw = await cursor.to_list(length=limit)
|
|
775
|
+
return [m for m in (self._mongo_doc_to_model(d) for d in raw) if m is not None]
|
|
776
|
+
|
|
777
|
+
def find_iter_sync(
|
|
778
|
+
self,
|
|
779
|
+
query: dict[str, Any] | None = None,
|
|
780
|
+
*,
|
|
781
|
+
sort: list[tuple[str, int]] | None = None,
|
|
782
|
+
batch_size: int | None = None,
|
|
783
|
+
limit: int | None = None,
|
|
784
|
+
) -> Iterator[T]:
|
|
785
|
+
"""Lazily iterate over matching documents using a synchronous Mongo cursor."""
|
|
786
|
+
if self._models is not None:
|
|
787
|
+
raise ValueError(
|
|
788
|
+
"Cannot use find_iter_sync() in multi-model mode. Use db.model_name.find_iter_sync() instead."
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
try:
|
|
792
|
+
asyncio.get_running_loop()
|
|
793
|
+
raise RuntimeError("find_iter_sync() called from async context. Use async for item in find_iter() instead.")
|
|
794
|
+
except RuntimeError as exc:
|
|
795
|
+
if "no running event loop" not in str(exc).lower():
|
|
796
|
+
raise
|
|
797
|
+
|
|
798
|
+
if not self._is_initialized:
|
|
799
|
+
self.initialize_sync()
|
|
800
|
+
|
|
801
|
+
cursor = self._sync_collection().find(query or {})
|
|
802
|
+
if sort:
|
|
803
|
+
cursor = cursor.sort(sort)
|
|
804
|
+
if batch_size is not None:
|
|
805
|
+
cursor = cursor.batch_size(batch_size)
|
|
806
|
+
if limit is not None:
|
|
807
|
+
cursor = cursor.limit(limit)
|
|
808
|
+
|
|
809
|
+
def _generator() -> Iterator[T]:
|
|
810
|
+
try:
|
|
811
|
+
for raw in cursor:
|
|
812
|
+
model = self._mongo_doc_to_model(raw)
|
|
813
|
+
if model is not None:
|
|
814
|
+
yield model
|
|
815
|
+
finally:
|
|
816
|
+
cursor.close()
|
|
817
|
+
|
|
818
|
+
return _generator()
|
|
819
|
+
|
|
820
|
+
async def count_documents(self, query: dict[str, Any] | None = None) -> int:
|
|
821
|
+
"""Return the number of documents matching ``query`` without loading them."""
|
|
822
|
+
if self._models is not None:
|
|
823
|
+
raise ValueError(
|
|
824
|
+
"Cannot use count_documents() in multi-model mode. Use db.model_name.count_documents() instead."
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
if not self._is_initialized:
|
|
828
|
+
await self.initialize()
|
|
829
|
+
|
|
830
|
+
return await self._motor_collection().count_documents(query or {})
|
|
831
|
+
|
|
548
832
|
async def aggregate(self, pipeline: list) -> List[T]:
|
|
549
833
|
"""
|
|
550
834
|
Execute a MongoDB aggregation pipeline.
|
|
@@ -568,6 +852,8 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
|
|
|
568
852
|
# Auto-initialize if needed (backward compatible)
|
|
569
853
|
if not self._is_initialized:
|
|
570
854
|
await self.initialize()
|
|
855
|
+
if self._motor_routing:
|
|
856
|
+
return await self._motor_collection().aggregate(pipeline).to_list(None)
|
|
571
857
|
return await self.model_cls.get_motor_collection().aggregate(pipeline).to_list(None)
|
|
572
858
|
|
|
573
859
|
def get_raw_model(self) -> Type[T]:
|
{mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace/database/backends/unified_odm.py
RENAMED
|
@@ -532,6 +532,7 @@ class UnifiedMindtraceODM(MindtraceODM):
|
|
|
532
532
|
self.unified_model_cls = unified_model_cls
|
|
533
533
|
self._unified_models = unified_models
|
|
534
534
|
self._model_odms: Dict[str, "UnifiedMindtraceODM"] = {}
|
|
535
|
+
self._closed = False
|
|
535
536
|
|
|
536
537
|
# Support multi-model mode with unified models
|
|
537
538
|
if unified_models is not None:
|
|
@@ -634,6 +635,27 @@ class UnifiedMindtraceODM(MindtraceODM):
|
|
|
634
635
|
return self._model_odms[name]
|
|
635
636
|
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
|
|
636
637
|
|
|
638
|
+
def close(self) -> None:
|
|
639
|
+
"""Release resources held by the underlying backends.
|
|
640
|
+
|
|
641
|
+
Closes any child UnifiedMindtraceODM instances (multi-model mode)
|
|
642
|
+
and each backend that exposes a ``close`` method. Idempotent.
|
|
643
|
+
"""
|
|
644
|
+
if self._closed:
|
|
645
|
+
return
|
|
646
|
+
for odm in self._model_odms.values():
|
|
647
|
+
try:
|
|
648
|
+
odm.close()
|
|
649
|
+
except Exception:
|
|
650
|
+
pass
|
|
651
|
+
for backend in (self.mongo_backend, self.redis_backend):
|
|
652
|
+
if backend is not None and hasattr(backend, "close"):
|
|
653
|
+
try:
|
|
654
|
+
backend.close()
|
|
655
|
+
except Exception:
|
|
656
|
+
pass
|
|
657
|
+
self._closed = True
|
|
658
|
+
|
|
637
659
|
def _get_active_backend(self):
|
|
638
660
|
"""
|
|
639
661
|
Get the currently active backend based on preference and availability.
|
{mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace_database.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mindtrace-database
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Database functionality for Mindtrace
|
|
5
5
|
Author: Mindtrace Team
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -11,13 +11,13 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
13
|
Requires-Dist: beanie<2,>=1.29.0
|
|
14
|
-
Requires-Dist: mindtrace-core>=0.
|
|
14
|
+
Requires-Dist: mindtrace-core>=0.11.0
|
|
15
15
|
Requires-Dist: pydantic>=2.11.1
|
|
16
16
|
Requires-Dist: redis>=4.0.0
|
|
17
17
|
Requires-Dist: redis-om>=0.3.5
|
|
18
18
|
Requires-Dist: motor>=3.3.0
|
|
19
19
|
Requires-Dist: pymongo>=4.14.0
|
|
20
|
-
Requires-Dist: mindtrace-registry>=0.
|
|
20
|
+
Requires-Dist: mindtrace-registry>=0.11.0
|
|
21
21
|
Dynamic: license-file
|
|
22
22
|
|
|
23
23
|
[](https://pypi.org/project/mindtrace-database/)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mindtrace-database"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.11.0"
|
|
4
4
|
description = "Database functionality for Mindtrace"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -13,13 +13,13 @@ classifiers = [
|
|
|
13
13
|
]
|
|
14
14
|
dependencies = [
|
|
15
15
|
"beanie>=1.29.0, <2",
|
|
16
|
-
"mindtrace-core>=0.
|
|
16
|
+
"mindtrace-core>=0.11.0",
|
|
17
17
|
"pydantic>=2.11.1",
|
|
18
18
|
"redis>=4.0.0",
|
|
19
19
|
"redis-om>=0.3.5",
|
|
20
20
|
"motor>=3.3.0",
|
|
21
21
|
"pymongo>=4.14.0",
|
|
22
|
-
"mindtrace-registry>=0.
|
|
22
|
+
"mindtrace-registry>=0.11.0",
|
|
23
23
|
]
|
|
24
24
|
|
|
25
25
|
[project.urls]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace/database/backends/mindtrace_odm.py
RENAMED
|
File without changes
|
{mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace/database/backends/redis_odm.py
RENAMED
|
File without changes
|
{mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace/database/backends/registry_odm.py
RENAMED
|
File without changes
|
{mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace/database/core/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
{mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace_database.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{mindtrace_database-0.10.0 → mindtrace_database-0.11.0}/mindtrace_database.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|