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.
- mdb_engine/README.md +144 -0
- mdb_engine/__init__.py +37 -0
- mdb_engine/auth/README.md +631 -0
- mdb_engine/auth/__init__.py +128 -0
- mdb_engine/auth/casbin_factory.py +199 -0
- mdb_engine/auth/casbin_models.py +46 -0
- mdb_engine/auth/config_defaults.py +71 -0
- mdb_engine/auth/config_helpers.py +213 -0
- mdb_engine/auth/cookie_utils.py +158 -0
- mdb_engine/auth/decorators.py +350 -0
- mdb_engine/auth/dependencies.py +747 -0
- mdb_engine/auth/helpers.py +64 -0
- mdb_engine/auth/integration.py +578 -0
- mdb_engine/auth/jwt.py +225 -0
- mdb_engine/auth/middleware.py +241 -0
- mdb_engine/auth/oso_factory.py +323 -0
- mdb_engine/auth/provider.py +570 -0
- mdb_engine/auth/restrictions.py +271 -0
- mdb_engine/auth/session_manager.py +477 -0
- mdb_engine/auth/token_lifecycle.py +213 -0
- mdb_engine/auth/token_store.py +289 -0
- mdb_engine/auth/users.py +1516 -0
- mdb_engine/auth/utils.py +614 -0
- mdb_engine/cli/__init__.py +13 -0
- mdb_engine/cli/commands/__init__.py +7 -0
- mdb_engine/cli/commands/generate.py +105 -0
- mdb_engine/cli/commands/migrate.py +83 -0
- mdb_engine/cli/commands/show.py +70 -0
- mdb_engine/cli/commands/validate.py +63 -0
- mdb_engine/cli/main.py +41 -0
- mdb_engine/cli/utils.py +92 -0
- mdb_engine/config.py +217 -0
- mdb_engine/constants.py +160 -0
- mdb_engine/core/README.md +542 -0
- mdb_engine/core/__init__.py +42 -0
- mdb_engine/core/app_registration.py +392 -0
- mdb_engine/core/connection.py +243 -0
- mdb_engine/core/engine.py +749 -0
- mdb_engine/core/index_management.py +162 -0
- mdb_engine/core/manifest.py +2793 -0
- mdb_engine/core/seeding.py +179 -0
- mdb_engine/core/service_initialization.py +355 -0
- mdb_engine/core/types.py +413 -0
- mdb_engine/database/README.md +522 -0
- mdb_engine/database/__init__.py +31 -0
- mdb_engine/database/abstraction.py +635 -0
- mdb_engine/database/connection.py +387 -0
- mdb_engine/database/scoped_wrapper.py +1721 -0
- mdb_engine/embeddings/README.md +184 -0
- mdb_engine/embeddings/__init__.py +62 -0
- mdb_engine/embeddings/dependencies.py +193 -0
- mdb_engine/embeddings/service.py +759 -0
- mdb_engine/exceptions.py +167 -0
- mdb_engine/indexes/README.md +651 -0
- mdb_engine/indexes/__init__.py +21 -0
- mdb_engine/indexes/helpers.py +145 -0
- mdb_engine/indexes/manager.py +895 -0
- mdb_engine/memory/README.md +451 -0
- mdb_engine/memory/__init__.py +30 -0
- mdb_engine/memory/service.py +1285 -0
- mdb_engine/observability/README.md +515 -0
- mdb_engine/observability/__init__.py +42 -0
- mdb_engine/observability/health.py +296 -0
- mdb_engine/observability/logging.py +161 -0
- mdb_engine/observability/metrics.py +297 -0
- mdb_engine/routing/README.md +462 -0
- mdb_engine/routing/__init__.py +73 -0
- mdb_engine/routing/websockets.py +813 -0
- mdb_engine/utils/__init__.py +7 -0
- mdb_engine-0.1.6.dist-info/METADATA +213 -0
- mdb_engine-0.1.6.dist-info/RECORD +75 -0
- mdb_engine-0.1.6.dist-info/WHEEL +5 -0
- mdb_engine-0.1.6.dist-info/entry_points.txt +2 -0
- mdb_engine-0.1.6.dist-info/licenses/LICENSE +661 -0
- 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
|
+
# ============================================================================
|