mindtrace-database 0.9.4__tar.gz → 0.10.1__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.
Files changed (18) hide show
  1. {mindtrace_database-0.9.4 → mindtrace_database-0.10.1}/PKG-INFO +3 -3
  2. {mindtrace_database-0.9.4 → mindtrace_database-0.10.1}/mindtrace/database/__init__.py +9 -1
  3. {mindtrace_database-0.9.4 → mindtrace_database-0.10.1}/mindtrace/database/backends/mongo_odm.py +133 -2
  4. {mindtrace_database-0.9.4 → mindtrace_database-0.10.1}/mindtrace/database/backends/redis_odm.py +5 -17
  5. {mindtrace_database-0.9.4 → mindtrace_database-0.10.1}/mindtrace_database.egg-info/PKG-INFO +3 -3
  6. {mindtrace_database-0.9.4 → mindtrace_database-0.10.1}/mindtrace_database.egg-info/requires.txt +2 -2
  7. {mindtrace_database-0.9.4 → mindtrace_database-0.10.1}/pyproject.toml +3 -3
  8. {mindtrace_database-0.9.4 → mindtrace_database-0.10.1}/LICENSE +0 -0
  9. {mindtrace_database-0.9.4 → mindtrace_database-0.10.1}/README.md +0 -0
  10. {mindtrace_database-0.9.4 → mindtrace_database-0.10.1}/mindtrace/database/backends/mindtrace_odm.py +0 -0
  11. {mindtrace_database-0.9.4 → mindtrace_database-0.10.1}/mindtrace/database/backends/registry_odm.py +0 -0
  12. {mindtrace_database-0.9.4 → mindtrace_database-0.10.1}/mindtrace/database/backends/unified_odm.py +0 -0
  13. {mindtrace_database-0.9.4 → mindtrace_database-0.10.1}/mindtrace/database/core/exceptions.py +0 -0
  14. {mindtrace_database-0.9.4 → mindtrace_database-0.10.1}/mindtrace/database/sample/user.py +0 -0
  15. {mindtrace_database-0.9.4 → mindtrace_database-0.10.1}/mindtrace_database.egg-info/SOURCES.txt +0 -0
  16. {mindtrace_database-0.9.4 → mindtrace_database-0.10.1}/mindtrace_database.egg-info/dependency_links.txt +0 -0
  17. {mindtrace_database-0.9.4 → mindtrace_database-0.10.1}/mindtrace_database.egg-info/top_level.txt +0 -0
  18. {mindtrace_database-0.9.4 → mindtrace_database-0.10.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mindtrace-database
3
- Version: 0.9.4
3
+ Version: 0.10.1
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.9.4
14
+ Requires-Dist: mindtrace-core>=0.10.1
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.9.4
20
+ Requires-Dist: mindtrace-registry>=0.10.1
21
21
  Dynamic: license-file
22
22
 
23
23
  [![PyPI version](https://img.shields.io/pypi/v/mindtrace-database)](https://pypi.org/project/mindtrace-database/)
@@ -4,7 +4,6 @@ from beanie import Link
4
4
  from mindtrace.database.backends.mindtrace_odm import InitMode, MindtraceODM
5
5
  from mindtrace.database.backends.mongo_odm import MindtraceDocument, MongoMindtraceODM
6
6
  from mindtrace.database.backends.redis_odm import MindtraceRedisDocument, RedisMindtraceODM
7
- from mindtrace.database.backends.registry_odm import RegistryMindtraceODM
8
7
  from mindtrace.database.backends.unified_odm import (
9
8
  BackendType,
10
9
  UnifiedMindtraceDocument,
@@ -27,3 +26,12 @@ __all__ = [
27
26
  "UnifiedMindtraceDocument",
28
27
  "UnifiedMindtraceODM",
29
28
  ]
29
+
30
+
31
+ def __getattr__(name):
32
+ if name == "RegistryMindtraceODM":
33
+ from mindtrace.database.backends.registry_odm import RegistryMindtraceODM
34
+
35
+ globals()["RegistryMindtraceODM"] = RegistryMindtraceODM
36
+ return RegistryMindtraceODM
37
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -1,7 +1,10 @@
1
1
  import asyncio
2
- from typing import Dict, List, Optional, Type, TypeVar
2
+ from typing import Any, Dict, List, Optional, Type, TypeVar
3
3
 
4
4
  from beanie import Document, PydanticObjectId, init_beanie
5
+ from beanie.odm.utils.dump import get_dict
6
+ from bson import ObjectId
7
+ from bson.errors import InvalidId
5
8
  from motor.motor_asyncio import AsyncIOMotorClient
6
9
  from pydantic import BaseModel
7
10
  from pymongo.errors import DuplicateKeyError
@@ -44,6 +47,12 @@ class MindtraceDocument(Document):
44
47
 
45
48
  ModelType = TypeVar("ModelType", bound=MindtraceDocument)
46
49
 
50
+ # Beanie binds each Document class to one database *process-wide*. A second ``init_beanie`` for the same
51
+ # ``model_cls`` (whether another ``db_name``, another URI, or another ``MongoMindtraceODM`` instance)
52
+ # would re-bind that class and break earlier lakes. Only the first ODM per ``model_cls`` runs
53
+ # ``init_beanie``; subsequent backends set ``_motor_routing`` and use Motor on ``self.client[self.db_name]``.
54
+ _BEANIE_INITIALIZED_MODEL_CLASSES: set[Type[Any]] = set()
55
+
47
56
 
48
57
  class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
49
58
  """
@@ -129,6 +138,7 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
129
138
 
130
139
  self.client = AsyncIOMotorClient(db_uri)
131
140
  self.db_name = db_name
141
+ self._motor_routing = False
132
142
  self._allow_index_dropping = allow_index_dropping
133
143
  self._is_initialized = False
134
144
  self._model_odms: Dict[str, "MongoMindtraceODM"] = {}
@@ -192,6 +202,51 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
192
202
  # Defer initialization - operations will auto-init on first use
193
203
  self._needs_init = True
194
204
 
205
+ def _collection_name(self) -> str:
206
+ if self.model_cls is None:
207
+ raise ValueError("model_cls is required for collection name resolution")
208
+ return getattr(self.model_cls.Settings, "name")
209
+
210
+ def _motor_collection(self):
211
+ return self.client[self.db_name][self._collection_name()]
212
+
213
+ def _mongo_doc_to_model(self, doc: dict | None) -> T | None:
214
+ if doc is None:
215
+ return None
216
+ payload = dict(doc)
217
+ if "_id" in payload and "id" not in payload:
218
+ payload["id"] = payload["_id"]
219
+ return self.model_cls.model_validate(payload)
220
+
221
+ def _motor_model_dump(self, doc: BaseModel) -> dict[str, Any]:
222
+ """Serialize a model for raw Motor insert/replace.
223
+
224
+ Pydantic v2 ``model_dump`` on Beanie ``Document`` can fail (class-level
225
+ descriptors such as ``ExpressionField``). Beanie's BSON ``get_dict`` matches
226
+ ``Document.insert`` and avoids that serializer path.
227
+ """
228
+ if isinstance(doc, Document):
229
+ return get_dict(doc, to_db=True, keep_nulls=doc.get_settings().keep_nulls)
230
+ return doc.model_dump(mode="python", by_alias=True, serialize_as_any=True)
231
+
232
+ def _motor_patch_fields(self, obj: BaseModel) -> dict[str, Any]:
233
+ """Shallow field map from ``obj`` for merge-into-document updates (Motor mode)."""
234
+ if isinstance(obj, Document):
235
+ return {name: getattr(obj, name) for name in obj.model_fields if name != "id"}
236
+ return obj.model_dump(exclude={"id"}, serialize_as_any=True)
237
+
238
+ @staticmethod
239
+ def _to_object_id(id: str | PydanticObjectId | ObjectId) -> ObjectId:
240
+ # ``PydanticObjectId`` subclasses ``ObjectId``; check it first so we normalize via ``str``.
241
+ if isinstance(id, PydanticObjectId):
242
+ return ObjectId(str(id))
243
+ if isinstance(id, ObjectId):
244
+ return id
245
+ try:
246
+ return ObjectId(str(id))
247
+ except InvalidId as exc:
248
+ raise DocumentNotFoundError(f"Invalid document id {id!r}") from exc
249
+
195
250
  async def _do_initialize(self):
196
251
  """Internal method to perform the actual initialization."""
197
252
  if not self._is_initialized:
@@ -199,7 +254,12 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
199
254
  # Multi-model mode: initialize all models together
200
255
  document_models = list(self._models.values())
201
256
  else:
202
- # Single model mode
257
+ # Single model mode: at most one init_beanie per Document subclass in this process.
258
+ if self.model_cls in _BEANIE_INITIALIZED_MODEL_CLASSES:
259
+ self._motor_routing = True
260
+ self._is_initialized = True
261
+ return
262
+ _BEANIE_INITIALIZED_MODEL_CLASSES.add(self.model_cls)
203
263
  document_models = [self.model_cls]
204
264
 
205
265
  await init_beanie(
@@ -340,6 +400,19 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
340
400
  doc.id = None
341
401
 
342
402
  try:
403
+ if self._motor_routing:
404
+ raw = self._motor_model_dump(doc)
405
+ raw.pop("_id", None)
406
+ if raw.get("id") is None:
407
+ raw.pop("id", None)
408
+ result = await self._motor_collection().insert_one(raw)
409
+ inserted = await self._motor_collection().find_one({"_id": result.inserted_id})
410
+ if inserted is None:
411
+ raise DuplicateInsertError("insert_one did not persist a readable document")
412
+ out = self._mongo_doc_to_model(inserted)
413
+ if out is None:
414
+ raise DuplicateInsertError("Could not deserialize inserted document")
415
+ return out
343
416
  return await doc.insert()
344
417
  except DuplicateKeyError as e:
345
418
  raise DuplicateInsertError(f"Duplicate key error: {str(e)}")
@@ -381,6 +454,18 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
381
454
  if not self._is_initialized:
382
455
  await self.initialize()
383
456
 
457
+ if self._motor_routing:
458
+ if fetch_links:
459
+ raise NotImplementedError("fetch_links is not supported for this database routing mode")
460
+ oid = self._to_object_id(id)
461
+ doc = await self._motor_collection().find_one({"_id": oid})
462
+ if not doc:
463
+ raise DocumentNotFoundError(f"Object with id {id} not found")
464
+ out = self._mongo_doc_to_model(doc)
465
+ if out is None:
466
+ raise DocumentNotFoundError(f"Object with id {id} not found")
467
+ return out
468
+
384
469
  doc = await self.model_cls.get(id, fetch_links=fetch_links)
385
470
  if not doc:
386
471
  raise DocumentNotFoundError(f"Object with id {id} not found")
@@ -421,6 +506,29 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
421
506
  if not self._is_initialized:
422
507
  await self.initialize()
423
508
 
509
+ if self._motor_routing:
510
+ if isinstance(obj, self.model_cls):
511
+ if not obj.id:
512
+ raise DocumentNotFoundError("Document must have an id to be updated")
513
+ oid = self._to_object_id(obj.id)
514
+ raw = self._motor_model_dump(obj)
515
+ await self._motor_collection().replace_one({"_id": oid}, raw)
516
+ return obj
517
+ if not hasattr(obj, "id") or not obj.id:
518
+ raise DocumentNotFoundError("Document must have an id to be updated")
519
+ oid = self._to_object_id(obj.id)
520
+ existing = await self._motor_collection().find_one({"_id": oid})
521
+ if not existing:
522
+ raise DocumentNotFoundError(f"Object with id {obj.id} not found")
523
+ doc = self._mongo_doc_to_model(existing)
524
+ if doc is None:
525
+ raise DocumentNotFoundError(f"Object with id {obj.id} not found")
526
+ for key, value in self._motor_patch_fields(obj).items():
527
+ setattr(doc, key, value)
528
+ raw = self._motor_model_dump(doc)
529
+ await self._motor_collection().replace_one({"_id": oid}, raw)
530
+ return doc
531
+
424
532
  # Check if obj is already a document instance
425
533
  if isinstance(obj, self.model_cls):
426
534
  # If it's already a document instance, just save it
@@ -471,6 +579,13 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
471
579
  if not self._is_initialized:
472
580
  await self.initialize()
473
581
 
582
+ if self._motor_routing:
583
+ oid = self._to_object_id(id)
584
+ result = await self._motor_collection().delete_one({"_id": oid})
585
+ if result.deleted_count == 0:
586
+ raise DocumentNotFoundError(f"Object with id {id} not found")
587
+ return
588
+
474
589
  doc = await self.model_cls.get(id)
475
590
  if doc:
476
591
  await doc.delete()
@@ -502,6 +617,10 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
502
617
  if not self._is_initialized:
503
618
  await self.initialize()
504
619
 
620
+ if self._motor_routing:
621
+ raw = await self._motor_collection().find({}).to_list(length=None)
622
+ return [m for m in (self._mongo_doc_to_model(d) for d in raw) if m is not None]
623
+
505
624
  return await self.model_cls.find_all().to_list()
506
625
 
507
626
  async def find(self, *args, fetch_links: bool = False, **kwargs) -> List[T]:
@@ -541,6 +660,16 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
541
660
  # Remove fetch_links from kwargs if present (it's a parameter, not a query field)
542
661
  kwargs_without_fetch_links = {k: v for k, v in kwargs.items() if k != "fetch_links"}
543
662
 
663
+ if self._motor_routing:
664
+ if fetch_links:
665
+ raise NotImplementedError("fetch_links is not supported for this database routing mode")
666
+ if len(args) != 1 or not isinstance(args[0], dict) or kwargs_without_fetch_links:
667
+ raise NotImplementedError(
668
+ "Only a single dict filter is supported for find() in this database routing mode"
669
+ )
670
+ raw = await self._motor_collection().find(args[0]).to_list(length=None)
671
+ return [m for m in (self._mongo_doc_to_model(d) for d in raw) if m is not None]
672
+
544
673
  # In Beanie, fetch_links is passed as a parameter to find(), not called as a method
545
674
  query = self.model_cls.find(*args, fetch_links=fetch_links, **kwargs_without_fetch_links)
546
675
  return await query.to_list()
@@ -568,6 +697,8 @@ class MongoMindtraceODM[T: MindtraceDocument](MindtraceODM):
568
697
  # Auto-initialize if needed (backward compatible)
569
698
  if not self._is_initialized:
570
699
  await self.initialize()
700
+ if self._motor_routing:
701
+ return await self._motor_collection().aggregate(pipeline).to_list(None)
571
702
  return await self.model_cls.get_motor_collection().aggregate(pipeline).to_list(None)
572
703
 
573
704
  def get_raw_model(self) -> Type[T]:
@@ -777,25 +777,13 @@ class RedisMindtraceODM(MindtraceODM):
777
777
 
778
778
  # After saving, ensure the index is working - if it has 0 docs, recreate it
779
779
  # This handles the case where index was created before documents were inserted
780
+ # NOTE: Do NOT run Migrator().run() here. It can detect a schema hash mismatch
781
+ # (e.g. if test cleanup deleted the hash key) and DROP+CREATE the index after
782
+ # the doc was saved, causing a race where find()/all() returns empty before
783
+ # re-indexing completes. _do_initialize() (via self.initialize()) and
784
+ # _ensure_index_has_documents() are sufficient.
780
785
  try:
781
786
  self._ensure_index_has_documents(self.model_cls)
782
- # After ensuring index, try running Migrator to make redis-om aware of it
783
- # This ensures redis-om's find() method can use the index
784
- try:
785
- import os
786
-
787
- original_redis_url = os.environ.get("REDIS_OM_URL", None)
788
- if self.redis_url:
789
- os.environ["REDIS_OM_URL"] = self.redis_url
790
- try:
791
- Migrator().run()
792
- finally:
793
- if original_redis_url:
794
- os.environ["REDIS_OM_URL"] = original_redis_url
795
- elif "REDIS_OM_URL" in os.environ:
796
- del os.environ["REDIS_OM_URL"]
797
- except Exception:
798
- pass # Don't fail if Migrator fails
799
787
  except Exception:
800
788
  pass # Don't fail insert if index check fails
801
789
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mindtrace-database
3
- Version: 0.9.4
3
+ Version: 0.10.1
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.9.4
14
+ Requires-Dist: mindtrace-core>=0.10.1
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.9.4
20
+ Requires-Dist: mindtrace-registry>=0.10.1
21
21
  Dynamic: license-file
22
22
 
23
23
  [![PyPI version](https://img.shields.io/pypi/v/mindtrace-database)](https://pypi.org/project/mindtrace-database/)
@@ -1,8 +1,8 @@
1
1
  beanie<2,>=1.29.0
2
- mindtrace-core>=0.9.4
2
+ mindtrace-core>=0.10.1
3
3
  pydantic>=2.11.1
4
4
  redis>=4.0.0
5
5
  redis-om>=0.3.5
6
6
  motor>=3.3.0
7
7
  pymongo>=4.14.0
8
- mindtrace-registry>=0.9.4
8
+ mindtrace-registry>=0.10.1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mindtrace-database"
3
- version = "0.9.4"
3
+ version = "0.10.1"
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.9.4",
16
+ "mindtrace-core>=0.10.1",
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.9.4",
22
+ "mindtrace-registry>=0.10.1",
23
23
  ]
24
24
 
25
25
  [project.urls]