mdb-engine 0.1.6__py3-none-any.whl

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 (75) hide show
  1. mdb_engine/README.md +144 -0
  2. mdb_engine/__init__.py +37 -0
  3. mdb_engine/auth/README.md +631 -0
  4. mdb_engine/auth/__init__.py +128 -0
  5. mdb_engine/auth/casbin_factory.py +199 -0
  6. mdb_engine/auth/casbin_models.py +46 -0
  7. mdb_engine/auth/config_defaults.py +71 -0
  8. mdb_engine/auth/config_helpers.py +213 -0
  9. mdb_engine/auth/cookie_utils.py +158 -0
  10. mdb_engine/auth/decorators.py +350 -0
  11. mdb_engine/auth/dependencies.py +747 -0
  12. mdb_engine/auth/helpers.py +64 -0
  13. mdb_engine/auth/integration.py +578 -0
  14. mdb_engine/auth/jwt.py +225 -0
  15. mdb_engine/auth/middleware.py +241 -0
  16. mdb_engine/auth/oso_factory.py +323 -0
  17. mdb_engine/auth/provider.py +570 -0
  18. mdb_engine/auth/restrictions.py +271 -0
  19. mdb_engine/auth/session_manager.py +477 -0
  20. mdb_engine/auth/token_lifecycle.py +213 -0
  21. mdb_engine/auth/token_store.py +289 -0
  22. mdb_engine/auth/users.py +1516 -0
  23. mdb_engine/auth/utils.py +614 -0
  24. mdb_engine/cli/__init__.py +13 -0
  25. mdb_engine/cli/commands/__init__.py +7 -0
  26. mdb_engine/cli/commands/generate.py +105 -0
  27. mdb_engine/cli/commands/migrate.py +83 -0
  28. mdb_engine/cli/commands/show.py +70 -0
  29. mdb_engine/cli/commands/validate.py +63 -0
  30. mdb_engine/cli/main.py +41 -0
  31. mdb_engine/cli/utils.py +92 -0
  32. mdb_engine/config.py +217 -0
  33. mdb_engine/constants.py +160 -0
  34. mdb_engine/core/README.md +542 -0
  35. mdb_engine/core/__init__.py +42 -0
  36. mdb_engine/core/app_registration.py +392 -0
  37. mdb_engine/core/connection.py +243 -0
  38. mdb_engine/core/engine.py +749 -0
  39. mdb_engine/core/index_management.py +162 -0
  40. mdb_engine/core/manifest.py +2793 -0
  41. mdb_engine/core/seeding.py +179 -0
  42. mdb_engine/core/service_initialization.py +355 -0
  43. mdb_engine/core/types.py +413 -0
  44. mdb_engine/database/README.md +522 -0
  45. mdb_engine/database/__init__.py +31 -0
  46. mdb_engine/database/abstraction.py +635 -0
  47. mdb_engine/database/connection.py +387 -0
  48. mdb_engine/database/scoped_wrapper.py +1721 -0
  49. mdb_engine/embeddings/README.md +184 -0
  50. mdb_engine/embeddings/__init__.py +62 -0
  51. mdb_engine/embeddings/dependencies.py +193 -0
  52. mdb_engine/embeddings/service.py +759 -0
  53. mdb_engine/exceptions.py +167 -0
  54. mdb_engine/indexes/README.md +651 -0
  55. mdb_engine/indexes/__init__.py +21 -0
  56. mdb_engine/indexes/helpers.py +145 -0
  57. mdb_engine/indexes/manager.py +895 -0
  58. mdb_engine/memory/README.md +451 -0
  59. mdb_engine/memory/__init__.py +30 -0
  60. mdb_engine/memory/service.py +1285 -0
  61. mdb_engine/observability/README.md +515 -0
  62. mdb_engine/observability/__init__.py +42 -0
  63. mdb_engine/observability/health.py +296 -0
  64. mdb_engine/observability/logging.py +161 -0
  65. mdb_engine/observability/metrics.py +297 -0
  66. mdb_engine/routing/README.md +462 -0
  67. mdb_engine/routing/__init__.py +73 -0
  68. mdb_engine/routing/websockets.py +813 -0
  69. mdb_engine/utils/__init__.py +7 -0
  70. mdb_engine-0.1.6.dist-info/METADATA +213 -0
  71. mdb_engine-0.1.6.dist-info/RECORD +75 -0
  72. mdb_engine-0.1.6.dist-info/WHEEL +5 -0
  73. mdb_engine-0.1.6.dist-info/entry_points.txt +2 -0
  74. mdb_engine-0.1.6.dist-info/licenses/LICENSE +661 -0
  75. mdb_engine-0.1.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,635 @@
1
+ """
2
+ App Database Wrapper
3
+
4
+ A MongoDB-style database abstraction layer for apps.
5
+ Follows MongoDB API conventions for familiarity and ease of use.
6
+
7
+ This module provides an easy-to-use API that matches MongoDB's API closely,
8
+ so apps can use familiar MongoDB methods. All operations automatically
9
+ handle app scoping and indexing behind the scenes.
10
+
11
+ This module is part of MDB_ENGINE - MongoDB Engine.
12
+
13
+ Usage:
14
+ from mdb_engine.database import AppDB, Collection
15
+
16
+ # In FastAPI route
17
+ @bp.get("/")
18
+ async def my_route(db: AppDB = Depends(get_app_db)):
19
+ # MongoDB-style operations - familiar API!
20
+ doc = await db.my_collection.find_one({"_id": "doc_123"})
21
+ docs = await db.my_collection.find({"status": "active"}).to_list(length=10)
22
+ await db.my_collection.insert_one({"name": "Test"})
23
+ count = await db.my_collection.count_documents({})
24
+ """
25
+
26
+ import logging
27
+ from typing import Any, Callable, Dict, List, Optional
28
+
29
+ from ..exceptions import MongoDBEngineError
30
+ from .scoped_wrapper import ScopedMongoWrapper
31
+
32
+ try:
33
+ from pymongo.errors import (AutoReconnect, ConnectionFailure,
34
+ InvalidOperation, OperationFailure,
35
+ ServerSelectionTimeoutError)
36
+ except ImportError:
37
+ OperationFailure = Exception
38
+ AutoReconnect = Exception
39
+ ConnectionFailure = Exception
40
+ ServerSelectionTimeoutError = Exception
41
+
42
+ try:
43
+ from motor.motor_asyncio import AsyncIOMotorCursor
44
+ from pymongo.results import (DeleteResult, InsertManyResult,
45
+ InsertOneResult, UpdateResult)
46
+ except ImportError:
47
+ AsyncIOMotorCursor = None
48
+ InsertOneResult = None
49
+ InsertManyResult = None
50
+ UpdateResult = None
51
+ DeleteResult = None
52
+ logging.warning("Failed to import Motor types. Type hints may not work correctly.")
53
+
54
+ logger = logging.getLogger(__name__)
55
+
56
+
57
+ class Collection:
58
+ """
59
+ A MongoDB collection wrapper that follows MongoDB API conventions.
60
+
61
+ This class wraps a ScopedCollectionWrapper and provides MongoDB-style methods
62
+ that match the familiar Motor/pymongo API. All operations automatically handle
63
+ app scoping and indexing.
64
+
65
+ Example:
66
+ collection = Collection(scoped_wrapper.my_collection)
67
+ doc = await collection.find_one({"_id": "doc_123"})
68
+ docs = await collection.find({"status": "active"}).to_list(length=10)
69
+ await collection.insert_one({"name": "Test"})
70
+ count = await collection.count_documents({})
71
+ """
72
+
73
+ def __init__(self, scoped_collection):
74
+ """
75
+ Initialize a Collection wrapper around a ScopedCollectionWrapper.
76
+
77
+ Args:
78
+ scoped_collection: A ScopedCollectionWrapper instance
79
+ """
80
+ self._collection = scoped_collection
81
+
82
+ async def find_one(
83
+ self, filter: Optional[Dict[str, Any]] = None, *args, **kwargs
84
+ ) -> Optional[Dict[str, Any]]:
85
+ """
86
+ Find a single document matching the filter.
87
+
88
+ This matches MongoDB's find_one() API exactly.
89
+
90
+ Args:
91
+ filter: Optional dict of field/value pairs to filter by
92
+ Example: {"_id": "doc_123"}, {"status": "active"}
93
+ *args, **kwargs: Additional arguments passed to find_one()
94
+ (e.g., projection, sort, etc.)
95
+
96
+ Returns:
97
+ The document as a dict, or None if not found
98
+
99
+ Example:
100
+ doc = await collection.find_one({"_id": "doc_123"})
101
+ doc = await collection.find_one({"status": "active"})
102
+ """
103
+ try:
104
+ return await self._collection.find_one(filter or {}, *args, **kwargs)
105
+ except (OperationFailure, ConnectionFailure, ServerSelectionTimeoutError):
106
+ logger.exception("Database operation failed in find_one")
107
+ return None
108
+ except (InvalidOperation, TypeError, ValueError, AttributeError) as e:
109
+ logger.exception("Error in find_one")
110
+ raise MongoDBEngineError(
111
+ "Error retrieving document",
112
+ context={"operation": "find_one"},
113
+ ) from e
114
+
115
+ def find(
116
+ self, filter: Optional[Dict[str, Any]] = None, *args, **kwargs
117
+ ) -> AsyncIOMotorCursor:
118
+ """
119
+ Find documents matching the filter.
120
+
121
+ This matches MongoDB's find() API exactly. Returns a cursor
122
+ that you can iterate or call .to_list() on.
123
+
124
+ Args:
125
+ filter: Optional dict of field/value pairs to filter by
126
+ Example: {"status": "active"}, {"age": {"$gte": 18}}
127
+ *args, **kwargs: Additional arguments passed to find()
128
+ (e.g., projection, sort, limit, skip, etc.)
129
+
130
+ Returns:
131
+ AsyncIOMotorCursor that can be iterated or converted to list
132
+
133
+ Example:
134
+ cursor = collection.find({"status": "active"})
135
+ docs = await cursor.to_list(length=10)
136
+
137
+ # Or with sort, limit
138
+ cursor = collection.find({"status": "active"}).sort("created_at", -1).limit(10)
139
+ docs = await cursor.to_list(length=None)
140
+ """
141
+ return self._collection.find(filter or {}, *args, **kwargs)
142
+
143
+ async def insert_one(
144
+ self, document: Dict[str, Any], *args, **kwargs
145
+ ) -> InsertOneResult:
146
+ """
147
+ Insert a single document.
148
+
149
+ This matches MongoDB's insert_one() API exactly.
150
+
151
+ Args:
152
+ document: The document to insert
153
+ *args, **kwargs: Additional arguments passed to insert_one()
154
+
155
+ Returns:
156
+ InsertOneResult with inserted_id
157
+
158
+ Example:
159
+ result = await collection.insert_one({"name": "Test"})
160
+ print(result.inserted_id)
161
+ """
162
+ try:
163
+ return await self._collection.insert_one(document, *args, **kwargs)
164
+ except (OperationFailure, AutoReconnect) as e:
165
+ logger.exception("Database operation failed in insert_one")
166
+ raise MongoDBEngineError(
167
+ "Failed to insert document", context={"operation": "insert_one"}
168
+ ) from e
169
+ except (InvalidOperation, TypeError, ValueError, AttributeError) as e:
170
+ logger.exception("Error in insert_one")
171
+ raise MongoDBEngineError(
172
+ "Error inserting document",
173
+ context={"operation": "insert_one"},
174
+ ) from e
175
+
176
+ async def insert_many(
177
+ self, documents: List[Dict[str, Any]], *args, **kwargs
178
+ ) -> InsertManyResult:
179
+ """
180
+ Insert multiple documents at once.
181
+
182
+ This matches MongoDB's insert_many() API exactly.
183
+
184
+ Args:
185
+ documents: List of documents to insert
186
+ *args, **kwargs: Additional arguments passed to insert_many()
187
+ (e.g., ordered=True/False)
188
+
189
+ Returns:
190
+ InsertManyResult with inserted_ids
191
+
192
+ Example:
193
+ result = await collection.insert_many([{"name": "A"}, {"name": "B"}])
194
+ print(result.inserted_ids)
195
+ """
196
+ try:
197
+ return await self._collection.insert_many(documents, *args, **kwargs)
198
+ except (OperationFailure, AutoReconnect) as e:
199
+ logger.exception("Database operation failed in insert_many")
200
+ raise MongoDBEngineError(
201
+ "Failed to insert documents",
202
+ context={"operation": "insert_many", "count": len(documents)},
203
+ ) from e
204
+ except (InvalidOperation, TypeError, ValueError, AttributeError) as e:
205
+ logger.exception("Error in insert_many")
206
+ raise MongoDBEngineError(
207
+ "Error inserting documents",
208
+ context={"operation": "insert_many", "count": len(documents)},
209
+ ) from e
210
+
211
+ async def update_one(
212
+ self, filter: Dict[str, Any], update: Dict[str, Any], *args, **kwargs
213
+ ) -> UpdateResult:
214
+ """
215
+ Update a single document matching the filter.
216
+
217
+ This matches MongoDB's update_one() API exactly.
218
+
219
+ Args:
220
+ filter: Dict of field/value pairs to match documents
221
+ Example: {"_id": "doc_123"}
222
+ update: Update operations (e.g., {"$set": {...}}, {"$inc": {...}})
223
+ *args, **kwargs: Additional arguments passed to update_one()
224
+ (e.g., upsert=True/False)
225
+
226
+ Returns:
227
+ UpdateResult with modified_count and upserted_id
228
+
229
+ Example:
230
+ result = await collection.update_one(
231
+ {"_id": "doc_123"},
232
+ {"$set": {"status": "active"}}
233
+ )
234
+ """
235
+ try:
236
+ return await self._collection.update_one(filter, update, *args, **kwargs)
237
+ except (OperationFailure, AutoReconnect) as e:
238
+ logger.exception("Database operation failed in update_one")
239
+ raise MongoDBEngineError(
240
+ "Failed to update document", context={"operation": "update_one"}
241
+ ) from e
242
+ except (InvalidOperation, TypeError, ValueError, AttributeError) as e:
243
+ logger.exception("Error in update_one")
244
+ raise MongoDBEngineError(
245
+ "Error updating document",
246
+ context={"operation": "update_one"},
247
+ ) from e
248
+
249
+ async def update_many(
250
+ self, filter: Dict[str, Any], update: Dict[str, Any], *args, **kwargs
251
+ ) -> UpdateResult:
252
+ """
253
+ Update multiple documents matching the filter.
254
+
255
+ This matches MongoDB's update_many() API exactly.
256
+
257
+ Args:
258
+ filter: Dict of field/value pairs to match documents
259
+ update: Update operations (e.g., {"$set": {...}}, {"$inc": {...}})
260
+ *args, **kwargs: Additional arguments passed to update_many()
261
+ (e.g., upsert=True/False)
262
+
263
+ Returns:
264
+ UpdateResult with modified_count
265
+
266
+ Example:
267
+ result = await collection.update_many(
268
+ {"status": "pending"},
269
+ {"$set": {"status": "active"}}
270
+ )
271
+ """
272
+ try:
273
+ return await self._collection.update_many(filter, update, *args, **kwargs)
274
+ except (OperationFailure, AutoReconnect) as e:
275
+ logger.exception("Database operation failed in update_many")
276
+ raise MongoDBEngineError(
277
+ "Failed to update documents", context={"operation": "update_many"}
278
+ ) from e
279
+ except (InvalidOperation, TypeError, ValueError, AttributeError) as e:
280
+ logger.exception("Error in update_many")
281
+ raise MongoDBEngineError(
282
+ "Error updating documents",
283
+ context={"operation": "update_many"},
284
+ ) from e
285
+
286
+ async def replace_one(
287
+ self, filter: Dict[str, Any], replacement: Dict[str, Any], *args, **kwargs
288
+ ) -> UpdateResult:
289
+ """
290
+ Replace a single document matching the filter.
291
+
292
+ This matches MongoDB's replace_one() API exactly.
293
+ Replaces the entire document with the replacement document.
294
+
295
+ Args:
296
+ filter: Dict of field/value pairs to match documents
297
+ Example: {"_id": "doc_123"}
298
+ replacement: The replacement document (entire document, not update operators)
299
+ *args, **kwargs: Additional arguments passed to replace_one()
300
+ (e.g., upsert=True/False)
301
+
302
+ Returns:
303
+ UpdateResult with modified_count and upserted_id
304
+
305
+ Example:
306
+ result = await collection.replace_one(
307
+ {"_id": "doc_123"},
308
+ {"_id": "doc_123", "name": "New Name", "status": "active"}
309
+ )
310
+ """
311
+ try:
312
+ # The underlying _collection is a ScopedCollectionWrapper
313
+ # It doesn't have replace_one, so we use delete + insert for true replacement
314
+ # This ensures proper app_id scoping
315
+ upsert = kwargs.get("upsert", False)
316
+
317
+ # Try to delete first (if document exists)
318
+ delete_result = await self._collection.delete_one(filter)
319
+
320
+ # If document was deleted or upsert is True, insert the replacement
321
+ if delete_result.deleted_count > 0 or upsert:
322
+ # Insert the replacement document (app_id will be auto-injected)
323
+ insert_result = await self._collection.insert_one(replacement)
324
+ # Return an UpdateResult-like object
325
+ from pymongo.results import UpdateResult
326
+
327
+ # Create UpdateResult with proper structure
328
+ # modified_count = 1 if we deleted and inserted, 0 if we only inserted (upsert)
329
+ modified_count = 1 if delete_result.deleted_count > 0 else 0
330
+ upserted_id = (
331
+ insert_result.inserted_id
332
+ if upsert and delete_result.deleted_count == 0
333
+ else None
334
+ )
335
+
336
+ # Create a proper UpdateResult
337
+ # UpdateResult expects raw_result dict with specific keys
338
+ raw_result = {
339
+ "ok": 1.0,
340
+ "n": modified_count,
341
+ "nModified": modified_count,
342
+ }
343
+ if upserted_id:
344
+ raw_result["upserted"] = upserted_id
345
+
346
+ return UpdateResult(raw_result, acknowledged=True)
347
+ else:
348
+ # Document not found and upsert=False
349
+ from pymongo.results import UpdateResult
350
+
351
+ return UpdateResult(
352
+ raw_result={"ok": 1.0, "n": 0, "nModified": 0}, acknowledged=True
353
+ )
354
+ except (OperationFailure, AutoReconnect) as e:
355
+ logger.exception("Database operation failed in replace_one")
356
+ raise MongoDBEngineError(
357
+ "Failed to replace document", context={"operation": "replace_one"}
358
+ ) from e
359
+ except (InvalidOperation, TypeError, ValueError, AttributeError) as e:
360
+ logger.exception("Error in replace_one")
361
+ raise MongoDBEngineError(
362
+ "Error replacing document",
363
+ context={"operation": "replace_one"},
364
+ ) from e
365
+
366
+ async def delete_one(self, filter: Dict[str, Any], *args, **kwargs) -> DeleteResult:
367
+ """
368
+ Delete a single document matching the filter.
369
+
370
+ This matches MongoDB's delete_one() API exactly.
371
+
372
+ Args:
373
+ filter: Dict of field/value pairs to match documents
374
+ Example: {"_id": "doc_123"}
375
+ *args, **kwargs: Additional arguments passed to delete_one()
376
+
377
+ Returns:
378
+ DeleteResult with deleted_count
379
+
380
+ Example:
381
+ result = await collection.delete_one({"_id": "doc_123"})
382
+ print(result.deleted_count)
383
+ """
384
+ try:
385
+ return await self._collection.delete_one(filter, *args, **kwargs)
386
+ except (OperationFailure, AutoReconnect) as e:
387
+ logger.exception("Database operation failed in delete_one")
388
+ raise MongoDBEngineError(
389
+ "Failed to delete document", context={"operation": "delete_one"}
390
+ ) from e
391
+ except (InvalidOperation, TypeError, ValueError, AttributeError) as e:
392
+ logger.exception("Error in delete_one")
393
+ raise MongoDBEngineError(
394
+ "Error deleting document",
395
+ context={"operation": "delete_one"},
396
+ ) from e
397
+
398
+ async def delete_many(
399
+ self, filter: Optional[Dict[str, Any]] = None, *args, **kwargs
400
+ ) -> DeleteResult:
401
+ """
402
+ Delete multiple documents matching the filter.
403
+
404
+ This matches MongoDB's delete_many() API exactly.
405
+
406
+ Args:
407
+ filter: Optional dict of field/value pairs to match documents
408
+ *args, **kwargs: Additional arguments passed to delete_many()
409
+
410
+ Returns:
411
+ DeleteResult with deleted_count
412
+
413
+ Example:
414
+ result = await collection.delete_many({"status": "deleted"})
415
+ print(result.deleted_count)
416
+ """
417
+ try:
418
+ return await self._collection.delete_many(filter or {}, *args, **kwargs)
419
+ except (OperationFailure, AutoReconnect) as e:
420
+ logger.exception("Database operation failed in delete_many")
421
+ raise MongoDBEngineError(
422
+ "Failed to delete documents", context={"operation": "delete_many"}
423
+ ) from e
424
+ except (InvalidOperation, TypeError, ValueError, AttributeError) as e:
425
+ logger.exception("Error in delete_many")
426
+ raise MongoDBEngineError(
427
+ "Error deleting documents",
428
+ context={"operation": "delete_many"},
429
+ ) from e
430
+
431
+ async def count_documents(
432
+ self, filter: Optional[Dict[str, Any]] = None, *args, **kwargs
433
+ ) -> int:
434
+ """
435
+ Count documents matching the filter.
436
+
437
+ This matches MongoDB's count_documents() API exactly.
438
+
439
+ Args:
440
+ filter: Optional dict of field/value pairs to filter by
441
+ *args, **kwargs: Additional arguments passed to count_documents()
442
+ (e.g., limit, skip)
443
+
444
+ Returns:
445
+ Number of matching documents
446
+
447
+ Example:
448
+ count = await collection.count_documents({"status": "active"})
449
+ count = await collection.count_documents({}) # Count all
450
+ """
451
+ try:
452
+ return await self._collection.count_documents(filter or {}, *args, **kwargs)
453
+ except (OperationFailure, ConnectionFailure, ServerSelectionTimeoutError):
454
+ logger.exception("Database operation failed in count_documents")
455
+ return 0
456
+ except (InvalidOperation, TypeError, ValueError, AttributeError) as e:
457
+ logger.exception("Error in count_documents")
458
+ raise MongoDBEngineError(
459
+ "Error counting documents",
460
+ context={"operation": "count_documents"},
461
+ ) from e
462
+
463
+ def aggregate(
464
+ self, pipeline: List[Dict[str, Any]], *args, **kwargs
465
+ ) -> AsyncIOMotorCursor:
466
+ """
467
+ Perform aggregation pipeline.
468
+
469
+ This matches MongoDB's aggregate() API exactly.
470
+
471
+ Args:
472
+ pipeline: List of aggregation stages
473
+ *args, **kwargs: Additional arguments passed to aggregate()
474
+
475
+ Returns:
476
+ AsyncIOMotorCursor for iterating results
477
+
478
+ Example:
479
+ pipeline = [
480
+ {"$match": {"status": "active"}},
481
+ {"$group": {"_id": "$category", "count": {"$sum": 1}}}
482
+ ]
483
+ cursor = collection.aggregate(pipeline)
484
+ results = await cursor.to_list(length=None)
485
+ """
486
+ return self._collection.aggregate(pipeline, *args, **kwargs)
487
+
488
+
489
+ class AppDB:
490
+ """
491
+ A MongoDB-style database interface for apps.
492
+
493
+ This class wraps ScopedMongoWrapper and provides MongoDB-style methods
494
+ that match the familiar Motor/pymongo API. All operations automatically
495
+ handle app scoping and indexing.
496
+
497
+ Example:
498
+ from mdb_engine.database import AppDB
499
+ from mdb_engine.database import get_app_db
500
+
501
+ @bp.get("/")
502
+ async def my_route(db: AppDB = Depends(get_app_db)):
503
+ # MongoDB-style operations - familiar API!
504
+ doc = await db.users.find_one({"_id": "user_123"})
505
+ docs = await db.users.find({"status": "active"}).to_list(length=10)
506
+ await db.users.insert_one({"name": "John", "email": "john@example.com"})
507
+ count = await db.users.count_documents({})
508
+ """
509
+
510
+ def __init__(self, scoped_wrapper: ScopedMongoWrapper):
511
+ """
512
+ Initialize AppDB with a ScopedMongoWrapper.
513
+
514
+ Args:
515
+ scoped_wrapper: A ScopedMongoWrapper instance (typically from application layer)
516
+ """
517
+ if not ScopedMongoWrapper:
518
+ raise RuntimeError("ScopedMongoWrapper is not available. Check imports.")
519
+
520
+ self._wrapper = scoped_wrapper
521
+ self._collection_cache: Dict[str, Collection] = {}
522
+
523
+ def collection(self, name: str) -> Collection:
524
+ """
525
+ Get a Collection wrapper for a collection by name.
526
+
527
+ Args:
528
+ name: The collection name (base name, without app prefix)
529
+ Example: "users", "products", "orders"
530
+
531
+ Returns:
532
+ A Collection instance for easy database operations
533
+
534
+ Example:
535
+ users = db.collection("users")
536
+ doc = await users.get("user_123")
537
+ """
538
+ if name in self._collection_cache:
539
+ return self._collection_cache[name]
540
+
541
+ # Get the scoped collection from wrapper
542
+ scoped_collection = getattr(self._wrapper, name)
543
+
544
+ # Create and cache Collection wrapper
545
+ collection = Collection(scoped_collection)
546
+ self._collection_cache[name] = collection
547
+
548
+ return collection
549
+
550
+ def __getattr__(self, name: str) -> Collection:
551
+ """
552
+ Allow direct access to collections as attributes.
553
+
554
+ Example:
555
+ db.users.get("user_123") # Instead of db.collection("users").get("user_123")
556
+ """
557
+ # Only proxy collection names, not internal attributes
558
+ if name.startswith("_"):
559
+ raise AttributeError(
560
+ f"'{type(self).__name__}' object has no attribute '{name}'"
561
+ )
562
+ return self.collection(name)
563
+
564
+ @property
565
+ def raw(self) -> ScopedMongoWrapper:
566
+ """
567
+ Access the underlying ScopedMongoWrapper for advanced operations.
568
+
569
+ Use this if you need to access MongoDB-specific features that aren't
570
+ covered by the simple API. For most cases, you won't need this.
571
+
572
+ Example:
573
+ # Advanced aggregation
574
+ pipeline = [{"$match": {...}}, {"$group": {...}}]
575
+ results = await db.raw.my_collection.aggregate(pipeline).to_list(None)
576
+ """
577
+ return self._wrapper
578
+
579
+ @property
580
+ def database(self):
581
+ """
582
+ Access the underlying AsyncIOMotorDatabase (unscoped).
583
+
584
+ This is useful for advanced operations that need direct access to the
585
+ real database without scoping, such as index management or administrative
586
+ operations.
587
+
588
+ Returns:
589
+ The underlying AsyncIOMotorDatabase instance
590
+
591
+ Example:
592
+ # Access underlying database for index management
593
+ real_db = db.database
594
+ collection = real_db["my_collection"]
595
+ index_manager = AsyncAtlasIndexManager(collection)
596
+ """
597
+ return self._wrapper.database
598
+
599
+
600
+ # FastAPI dependency helper
601
+ async def get_app_db(request, get_scoped_db_func: Callable) -> AppDB:
602
+ """
603
+ FastAPI Dependency: Provides an AppDB instance.
604
+
605
+ This is a convenience wrapper around get_scoped_db that returns
606
+ an AppDB instance instead of ScopedMongoWrapper.
607
+
608
+ Args:
609
+ request: FastAPI Request object
610
+ get_scoped_db_func: Required callable that takes a request and returns ScopedMongoWrapper.
611
+
612
+ Usage:
613
+ from mdb_engine.database import get_app_db
614
+ from my_app import get_scoped_db # Your application layer
615
+
616
+ @bp.get("/")
617
+ async def my_route(
618
+ request: Request,
619
+ db: AppDB = Depends(lambda r: get_app_db(r, get_scoped_db_func=get_scoped_db))
620
+ ):
621
+ doc = await db.users.get("user_123")
622
+ """
623
+ if not get_scoped_db_func:
624
+ raise ValueError(
625
+ "get_app_db requires get_scoped_db_func parameter. "
626
+ "Provide a callable that takes a Request and returns ScopedMongoWrapper."
627
+ )
628
+
629
+ scoped_db = await get_scoped_db_func(request)
630
+ return AppDB(scoped_db)
631
+
632
+
633
+ # ============================================================================
634
+ # Database Factory - Convenient factory for creating scoped database interfaces
635
+ # ============================================================================