lightodm 0.1.0__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.
- lightodm/__init__.py +53 -0
- lightodm/connection.py +338 -0
- lightodm/model.py +613 -0
- lightodm/py.typed +0 -0
- lightodm-0.1.0.dist-info/METADATA +346 -0
- lightodm-0.1.0.dist-info/RECORD +8 -0
- lightodm-0.1.0.dist-info/WHEEL +4 -0
- lightodm-0.1.0.dist-info/licenses/LICENSE +201 -0
lightodm/model.py
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MongoDB Base Model for Pydantic
|
|
3
|
+
|
|
4
|
+
Provides ODM functionality for MongoDB with both sync and async support.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import AsyncIterator, Iterator, List, Optional, Type, TypeVar
|
|
8
|
+
|
|
9
|
+
from bson import ObjectId
|
|
10
|
+
from motor.motor_asyncio import AsyncIOMotorCollection
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
12
|
+
from pymongo.collection import Collection as PyMongoCollection
|
|
13
|
+
|
|
14
|
+
from lightodm.connection import get_async_database, get_collection
|
|
15
|
+
|
|
16
|
+
# TypeVar for generic class methods
|
|
17
|
+
T = TypeVar("T", bound="MongoBaseModel")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def generate_id() -> str:
|
|
21
|
+
"""
|
|
22
|
+
Generate a new MongoDB ObjectId as a string.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
String representation of a new ObjectId
|
|
26
|
+
"""
|
|
27
|
+
return str(ObjectId())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MongoBaseModel(BaseModel):
|
|
31
|
+
"""
|
|
32
|
+
Base class for MongoDB document models with ODM functionality.
|
|
33
|
+
|
|
34
|
+
Provides both synchronous and asynchronous methods for CRUD operations.
|
|
35
|
+
Maps Pydantic 'id' field to MongoDB '_id' field.
|
|
36
|
+
|
|
37
|
+
Subclasses must define an inner Settings class with 'name' attribute:
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
class User(MongoBaseModel):
|
|
41
|
+
class Settings:
|
|
42
|
+
name = "users"
|
|
43
|
+
|
|
44
|
+
name: str
|
|
45
|
+
email: str
|
|
46
|
+
age: Optional[int] = None
|
|
47
|
+
|
|
48
|
+
# Sync usage
|
|
49
|
+
user = User(name="John", email="john@example.com")
|
|
50
|
+
user.save()
|
|
51
|
+
|
|
52
|
+
found_user = User.get("some_id")
|
|
53
|
+
users = User.find({"age": {"$gt": 18}})
|
|
54
|
+
|
|
55
|
+
# Async usage
|
|
56
|
+
await user.asave()
|
|
57
|
+
found_user = await User.aget("some_id")
|
|
58
|
+
users = await User.afind({"age": {"$gt": 18}})
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
62
|
+
|
|
63
|
+
# ID field that maps to MongoDB _id
|
|
64
|
+
id: Optional[str] = Field(default_factory=generate_id, alias="_id")
|
|
65
|
+
|
|
66
|
+
# Settings inner class - must be overridden in subclasses
|
|
67
|
+
class Settings:
|
|
68
|
+
name: Optional[str] = None # MongoDB collection name
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def _uses_mongo_id_alias(cls) -> bool:
|
|
72
|
+
field = cls.model_fields.get("id")
|
|
73
|
+
if field is None:
|
|
74
|
+
return False
|
|
75
|
+
alias = getattr(field, "serialization_alias", None) or getattr(field, "alias", None)
|
|
76
|
+
if alias is None:
|
|
77
|
+
alias = getattr(field, "validation_alias", None)
|
|
78
|
+
return alias == "_id"
|
|
79
|
+
|
|
80
|
+
def __init_subclass__(cls, **kwargs):
|
|
81
|
+
"""
|
|
82
|
+
Validate Settings class is properly defined in subclass.
|
|
83
|
+
"""
|
|
84
|
+
super().__init_subclass__(**kwargs)
|
|
85
|
+
# Skip validation for the base class itself
|
|
86
|
+
if cls.__name__ == "MongoBaseModel":
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
# Check if Settings class exists and has name attribute
|
|
90
|
+
if not hasattr(cls, "Settings"):
|
|
91
|
+
# Allow intermediate base classes without Settings
|
|
92
|
+
pass
|
|
93
|
+
elif hasattr(cls.Settings, "name") and cls.Settings.name is None:
|
|
94
|
+
# Settings exists but name is None - could be intermediate class
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def _validate_collection_name(cls):
|
|
99
|
+
"""Ensure Settings.name is defined in subclass"""
|
|
100
|
+
if not hasattr(cls, "Settings"):
|
|
101
|
+
raise NotImplementedError(f"{cls.__name__} must define an inner 'Settings' class")
|
|
102
|
+
if not hasattr(cls.Settings, "name") or cls.Settings.name is None:
|
|
103
|
+
raise NotImplementedError(
|
|
104
|
+
f"{cls.__name__}.Settings must define 'name' attribute with the collection name"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def _get_collection_name(cls) -> str:
|
|
109
|
+
"""Get the collection name from Settings.name"""
|
|
110
|
+
cls._validate_collection_name()
|
|
111
|
+
return cls.Settings.name
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def get_collection(cls) -> PyMongoCollection:
|
|
115
|
+
"""
|
|
116
|
+
Get synchronous MongoDB collection.
|
|
117
|
+
|
|
118
|
+
Override this method to provide custom connection logic.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
PyMongo Collection instance
|
|
122
|
+
"""
|
|
123
|
+
collection_name = cls._get_collection_name()
|
|
124
|
+
return get_collection(collection_name)
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
async def get_async_collection(cls) -> AsyncIOMotorCollection:
|
|
128
|
+
"""
|
|
129
|
+
Get asynchronous MongoDB collection.
|
|
130
|
+
|
|
131
|
+
Override this method to provide custom connection logic.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Motor AsyncIOMotorCollection instance
|
|
135
|
+
"""
|
|
136
|
+
collection_name = cls._get_collection_name()
|
|
137
|
+
db = await get_async_database()
|
|
138
|
+
return db[collection_name]
|
|
139
|
+
|
|
140
|
+
def _to_mongo_dict(self, exclude_none: bool = False) -> dict:
|
|
141
|
+
"""
|
|
142
|
+
Convert model to dictionary for MongoDB, handling id -> _id mapping.
|
|
143
|
+
|
|
144
|
+
Only serializes Pydantic fields - class attributes like collection_name
|
|
145
|
+
are automatically excluded.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
exclude_none: If True, exclude fields with None values
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Dictionary suitable for MongoDB insertion/update
|
|
152
|
+
"""
|
|
153
|
+
data = self.model_dump(by_alias=True, exclude_none=exclude_none)
|
|
154
|
+
if not self._uses_mongo_id_alias():
|
|
155
|
+
if "id" in data and "_id" not in data:
|
|
156
|
+
data["_id"] = data.pop("id")
|
|
157
|
+
else:
|
|
158
|
+
data.pop("id", None)
|
|
159
|
+
# Manually add extra fields that were captured
|
|
160
|
+
extra_fields = self.__pydantic_extra__
|
|
161
|
+
if extra_fields:
|
|
162
|
+
for key, value in extra_fields.items():
|
|
163
|
+
if not exclude_none or value is not None:
|
|
164
|
+
data[key] = value
|
|
165
|
+
return data
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def _from_mongo_dict(cls: Type[T], data: dict) -> Optional[T]:
|
|
169
|
+
"""
|
|
170
|
+
Create model instance from MongoDB document.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
data: MongoDB document dictionary
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Model instance or None if data is None
|
|
177
|
+
"""
|
|
178
|
+
if data is None:
|
|
179
|
+
return None
|
|
180
|
+
if not cls._uses_mongo_id_alias() and "_id" in data and "id" not in data:
|
|
181
|
+
data = dict(data)
|
|
182
|
+
data["id"] = data["_id"]
|
|
183
|
+
|
|
184
|
+
return cls.model_validate(data)
|
|
185
|
+
|
|
186
|
+
# ==================== CRUD Operations (Sync) ====================
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def get(cls: Type[T], id: str) -> Optional[T]:
|
|
190
|
+
"""
|
|
191
|
+
Retrieve a document by ID (synchronous).
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
id: Document ID
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Model instance or None if not found
|
|
198
|
+
"""
|
|
199
|
+
collection = cls.get_collection()
|
|
200
|
+
doc = collection.find_one({"_id": id})
|
|
201
|
+
return cls._from_mongo_dict(doc)
|
|
202
|
+
|
|
203
|
+
def save(self, exclude_none: bool = False) -> str:
|
|
204
|
+
"""
|
|
205
|
+
Save/upsert the document (synchronous).
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
exclude_none: If True, exclude fields with None values from update
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Document ID
|
|
212
|
+
"""
|
|
213
|
+
collection = self.get_collection()
|
|
214
|
+
data = self._to_mongo_dict(exclude_none=exclude_none)
|
|
215
|
+
doc_id = data.get("_id")
|
|
216
|
+
if doc_id is None:
|
|
217
|
+
raise ValueError("Document ID is required")
|
|
218
|
+
|
|
219
|
+
collection.replace_one({"_id": doc_id}, data, upsert=True)
|
|
220
|
+
return doc_id
|
|
221
|
+
|
|
222
|
+
def delete(self) -> bool:
|
|
223
|
+
"""
|
|
224
|
+
Delete the document (synchronous).
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
True if document was deleted, False otherwise
|
|
228
|
+
"""
|
|
229
|
+
if not self.id:
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
collection = self.get_collection()
|
|
233
|
+
result = collection.delete_one({"_id": self.id})
|
|
234
|
+
return result.deleted_count > 0
|
|
235
|
+
|
|
236
|
+
@classmethod
|
|
237
|
+
def find_one(cls: Type[T], filter: dict, **kwargs) -> Optional[T]:
|
|
238
|
+
"""
|
|
239
|
+
Find a single document (synchronous).
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
filter: MongoDB filter dictionary
|
|
243
|
+
**kwargs: Additional arguments passed to find_one (e.g., sort, projection)
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Model instance or None if not found
|
|
247
|
+
"""
|
|
248
|
+
collection = cls.get_collection()
|
|
249
|
+
doc = collection.find_one(filter, **kwargs)
|
|
250
|
+
return cls._from_mongo_dict(doc)
|
|
251
|
+
|
|
252
|
+
@classmethod
|
|
253
|
+
def find(cls: Type[T], filter: dict, **kwargs) -> List[T]:
|
|
254
|
+
"""
|
|
255
|
+
Find multiple documents (synchronous).
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
filter: MongoDB filter dictionary
|
|
259
|
+
**kwargs: Additional arguments passed to find (e.g., sort, limit, skip, projection)
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
List of model instances
|
|
263
|
+
"""
|
|
264
|
+
collection = cls.get_collection()
|
|
265
|
+
cursor = collection.find(filter, **kwargs)
|
|
266
|
+
return [cls._from_mongo_dict(doc) for doc in cursor]
|
|
267
|
+
|
|
268
|
+
@classmethod
|
|
269
|
+
def find_iter(cls: Type[T], filter: dict, **kwargs) -> Iterator[T]:
|
|
270
|
+
"""
|
|
271
|
+
Find multiple documents with iterator (synchronous).
|
|
272
|
+
Useful for large result sets to avoid loading all into memory.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
filter: MongoDB filter dictionary
|
|
276
|
+
**kwargs: Additional arguments passed to find
|
|
277
|
+
|
|
278
|
+
Yields:
|
|
279
|
+
Model instances one at a time
|
|
280
|
+
"""
|
|
281
|
+
collection = cls.get_collection()
|
|
282
|
+
cursor = collection.find(filter, **kwargs)
|
|
283
|
+
for doc in cursor:
|
|
284
|
+
yield cls._from_mongo_dict(doc)
|
|
285
|
+
|
|
286
|
+
@classmethod
|
|
287
|
+
def count(cls, filter: dict = None) -> int:
|
|
288
|
+
"""
|
|
289
|
+
Count documents matching filter (synchronous).
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
filter: MongoDB filter dictionary (default: {} for all documents)
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Number of matching documents
|
|
296
|
+
"""
|
|
297
|
+
collection = cls.get_collection()
|
|
298
|
+
return collection.count_documents(filter or {})
|
|
299
|
+
|
|
300
|
+
@classmethod
|
|
301
|
+
def update_one(cls, filter: dict, update: dict, upsert: bool = False) -> bool:
|
|
302
|
+
"""
|
|
303
|
+
Update a single document (synchronous).
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
filter: MongoDB filter dictionary
|
|
307
|
+
update: MongoDB update dictionary (should include operators like $set)
|
|
308
|
+
upsert: If True, insert document if not found
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
True if document was modified, False otherwise
|
|
312
|
+
"""
|
|
313
|
+
collection = cls.get_collection()
|
|
314
|
+
result = collection.update_one(filter, update, upsert=upsert)
|
|
315
|
+
return result.modified_count > 0 or (upsert and result.upserted_id is not None)
|
|
316
|
+
|
|
317
|
+
@classmethod
|
|
318
|
+
def update_many(cls, filter: dict, update: dict) -> int:
|
|
319
|
+
"""
|
|
320
|
+
Update multiple documents (synchronous).
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
filter: MongoDB filter dictionary
|
|
324
|
+
update: MongoDB update dictionary (should include operators like $set)
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Number of documents modified
|
|
328
|
+
"""
|
|
329
|
+
collection = cls.get_collection()
|
|
330
|
+
result = collection.update_many(filter, update)
|
|
331
|
+
return result.modified_count
|
|
332
|
+
|
|
333
|
+
@classmethod
|
|
334
|
+
def delete_one(cls, filter: dict) -> bool:
|
|
335
|
+
"""
|
|
336
|
+
Delete a single document (synchronous).
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
filter: MongoDB filter dictionary
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
True if document was deleted, False otherwise
|
|
343
|
+
"""
|
|
344
|
+
collection = cls.get_collection()
|
|
345
|
+
result = collection.delete_one(filter)
|
|
346
|
+
return result.deleted_count > 0
|
|
347
|
+
|
|
348
|
+
@classmethod
|
|
349
|
+
def delete_many(cls, filter: dict) -> int:
|
|
350
|
+
"""
|
|
351
|
+
Delete multiple documents (synchronous).
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
filter: MongoDB filter dictionary
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
Number of documents deleted
|
|
358
|
+
"""
|
|
359
|
+
collection = cls.get_collection()
|
|
360
|
+
result = collection.delete_many(filter)
|
|
361
|
+
return result.deleted_count
|
|
362
|
+
|
|
363
|
+
# ==================== CRUD Operations (Async) ====================
|
|
364
|
+
|
|
365
|
+
@classmethod
|
|
366
|
+
async def aget(cls: Type[T], id: str) -> Optional[T]:
|
|
367
|
+
"""
|
|
368
|
+
Retrieve a document by ID (asynchronous).
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
id: Document ID
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Model instance or None if not found
|
|
375
|
+
"""
|
|
376
|
+
collection = await cls.get_async_collection()
|
|
377
|
+
doc = await collection.find_one({"_id": id})
|
|
378
|
+
return cls._from_mongo_dict(doc)
|
|
379
|
+
|
|
380
|
+
async def asave(self, exclude_none: bool = False) -> str:
|
|
381
|
+
"""
|
|
382
|
+
Save/upsert the document (asynchronous).
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
exclude_none: If True, exclude fields with None values from update
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Document ID
|
|
389
|
+
"""
|
|
390
|
+
collection = await self.get_async_collection()
|
|
391
|
+
data = self._to_mongo_dict(exclude_none=exclude_none)
|
|
392
|
+
doc_id = data.get("_id")
|
|
393
|
+
if doc_id is None:
|
|
394
|
+
raise ValueError("Document ID is required")
|
|
395
|
+
|
|
396
|
+
await collection.replace_one({"_id": doc_id}, data, upsert=True)
|
|
397
|
+
return doc_id
|
|
398
|
+
|
|
399
|
+
async def adelete(self) -> bool:
|
|
400
|
+
"""
|
|
401
|
+
Delete the document (asynchronous).
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
True if document was deleted, False otherwise
|
|
405
|
+
"""
|
|
406
|
+
if not self.id:
|
|
407
|
+
return False
|
|
408
|
+
|
|
409
|
+
collection = await self.get_async_collection()
|
|
410
|
+
result = await collection.delete_one({"_id": self.id})
|
|
411
|
+
return result.deleted_count > 0
|
|
412
|
+
|
|
413
|
+
@classmethod
|
|
414
|
+
async def afind_one(cls: Type[T], filter: dict, **kwargs) -> Optional[T]:
|
|
415
|
+
"""
|
|
416
|
+
Find a single document (asynchronous).
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
filter: MongoDB filter dictionary
|
|
420
|
+
**kwargs: Additional arguments passed to find_one (e.g., sort, projection)
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
Model instance or None if not found
|
|
424
|
+
"""
|
|
425
|
+
collection = await cls.get_async_collection()
|
|
426
|
+
doc = await collection.find_one(filter, **kwargs)
|
|
427
|
+
return cls._from_mongo_dict(doc)
|
|
428
|
+
|
|
429
|
+
@classmethod
|
|
430
|
+
async def afind(cls: Type[T], filter: dict, **kwargs) -> List[T]:
|
|
431
|
+
"""
|
|
432
|
+
Find multiple documents (asynchronous).
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
filter: MongoDB filter dictionary
|
|
436
|
+
**kwargs: Additional arguments passed to find (e.g., sort, limit, skip, projection)
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
List of model instances
|
|
440
|
+
"""
|
|
441
|
+
collection = await cls.get_async_collection()
|
|
442
|
+
cursor = collection.find(filter, **kwargs)
|
|
443
|
+
docs = await cursor.to_list(length=None)
|
|
444
|
+
return [cls._from_mongo_dict(doc) for doc in docs]
|
|
445
|
+
|
|
446
|
+
@classmethod
|
|
447
|
+
async def afind_iter(cls: Type[T], filter: dict, **kwargs) -> AsyncIterator[T]:
|
|
448
|
+
"""
|
|
449
|
+
Find multiple documents with async iterator.
|
|
450
|
+
Useful for large result sets to avoid loading all into memory.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
filter: MongoDB filter dictionary
|
|
454
|
+
**kwargs: Additional arguments passed to find
|
|
455
|
+
|
|
456
|
+
Yields:
|
|
457
|
+
Model instances one at a time
|
|
458
|
+
"""
|
|
459
|
+
collection = await cls.get_async_collection()
|
|
460
|
+
cursor = collection.find(filter, **kwargs)
|
|
461
|
+
async for doc in cursor:
|
|
462
|
+
yield cls._from_mongo_dict(doc)
|
|
463
|
+
|
|
464
|
+
@classmethod
|
|
465
|
+
async def acount(cls, filter: dict = None) -> int:
|
|
466
|
+
"""
|
|
467
|
+
Count documents matching filter (asynchronous).
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
filter: MongoDB filter dictionary (default: {} for all documents)
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
Number of matching documents
|
|
474
|
+
"""
|
|
475
|
+
collection = await cls.get_async_collection()
|
|
476
|
+
return await collection.count_documents(filter or {})
|
|
477
|
+
|
|
478
|
+
@classmethod
|
|
479
|
+
async def aupdate_one(cls, filter: dict, update: dict, upsert: bool = False) -> bool:
|
|
480
|
+
"""
|
|
481
|
+
Update a single document (asynchronous).
|
|
482
|
+
|
|
483
|
+
Args:
|
|
484
|
+
filter: MongoDB filter dictionary
|
|
485
|
+
update: MongoDB update dictionary (should include operators like $set)
|
|
486
|
+
upsert: If True, insert document if not found
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
True if document was modified, False otherwise
|
|
490
|
+
"""
|
|
491
|
+
collection = await cls.get_async_collection()
|
|
492
|
+
result = await collection.update_one(filter, update, upsert=upsert)
|
|
493
|
+
return result.modified_count > 0 or (upsert and result.upserted_id is not None)
|
|
494
|
+
|
|
495
|
+
@classmethod
|
|
496
|
+
async def aupdate_many(cls, filter: dict, update: dict) -> int:
|
|
497
|
+
"""
|
|
498
|
+
Update multiple documents (asynchronous).
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
filter: MongoDB filter dictionary
|
|
502
|
+
update: MongoDB update dictionary (should include operators like $set)
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
Number of documents modified
|
|
506
|
+
"""
|
|
507
|
+
collection = await cls.get_async_collection()
|
|
508
|
+
result = await collection.update_many(filter, update)
|
|
509
|
+
return result.modified_count
|
|
510
|
+
|
|
511
|
+
@classmethod
|
|
512
|
+
async def adelete_one(cls, filter: dict) -> bool:
|
|
513
|
+
"""
|
|
514
|
+
Delete a single document (asynchronous).
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
filter: MongoDB filter dictionary
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
True if document was deleted, False otherwise
|
|
521
|
+
"""
|
|
522
|
+
collection = await cls.get_async_collection()
|
|
523
|
+
result = await collection.delete_one(filter)
|
|
524
|
+
return result.deleted_count > 0
|
|
525
|
+
|
|
526
|
+
@classmethod
|
|
527
|
+
async def adelete_many(cls, filter: dict) -> int:
|
|
528
|
+
"""
|
|
529
|
+
Delete multiple documents (asynchronous).
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
filter: MongoDB filter dictionary
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
Number of documents deleted
|
|
536
|
+
"""
|
|
537
|
+
collection = await cls.get_async_collection()
|
|
538
|
+
result = await collection.delete_many(filter)
|
|
539
|
+
return result.deleted_count
|
|
540
|
+
|
|
541
|
+
# ==================== Aggregation Operations ====================
|
|
542
|
+
|
|
543
|
+
@classmethod
|
|
544
|
+
def aggregate(cls: Type[T], pipeline: List[dict], **kwargs) -> List[dict]:
|
|
545
|
+
"""
|
|
546
|
+
Run aggregation pipeline (synchronous).
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
pipeline: MongoDB aggregation pipeline
|
|
550
|
+
**kwargs: Additional arguments passed to aggregate
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
List of result documents
|
|
554
|
+
"""
|
|
555
|
+
collection = cls.get_collection()
|
|
556
|
+
cursor = collection.aggregate(pipeline, **kwargs)
|
|
557
|
+
return list(cursor)
|
|
558
|
+
|
|
559
|
+
@classmethod
|
|
560
|
+
async def aaggregate(cls: Type[T], pipeline: List[dict], **kwargs) -> List[dict]:
|
|
561
|
+
"""
|
|
562
|
+
Run aggregation pipeline (asynchronous).
|
|
563
|
+
|
|
564
|
+
Args:
|
|
565
|
+
pipeline: MongoDB aggregation pipeline
|
|
566
|
+
**kwargs: Additional arguments passed to aggregate
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
List of result documents
|
|
570
|
+
"""
|
|
571
|
+
collection = await cls.get_async_collection()
|
|
572
|
+
cursor = collection.aggregate(pipeline, **kwargs)
|
|
573
|
+
return await cursor.to_list(length=None)
|
|
574
|
+
|
|
575
|
+
# ==================== Bulk Operations ====================
|
|
576
|
+
|
|
577
|
+
@classmethod
|
|
578
|
+
def insert_many(cls: Type[T], documents: List[T]) -> List[str]:
|
|
579
|
+
"""
|
|
580
|
+
Insert multiple documents (synchronous).
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
documents: List of model instances
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
List of inserted document IDs
|
|
587
|
+
"""
|
|
588
|
+
if not documents:
|
|
589
|
+
return []
|
|
590
|
+
|
|
591
|
+
collection = cls.get_collection()
|
|
592
|
+
docs = [doc._to_mongo_dict() for doc in documents]
|
|
593
|
+
result = collection.insert_many(docs)
|
|
594
|
+
return [str(id) for id in result.inserted_ids]
|
|
595
|
+
|
|
596
|
+
@classmethod
|
|
597
|
+
async def ainsert_many(cls: Type[T], documents: List[T]) -> List[str]:
|
|
598
|
+
"""
|
|
599
|
+
Insert multiple documents (asynchronous).
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
documents: List of model instances
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
List of inserted document IDs
|
|
606
|
+
"""
|
|
607
|
+
if not documents:
|
|
608
|
+
return []
|
|
609
|
+
|
|
610
|
+
collection = await cls.get_async_collection()
|
|
611
|
+
docs = [doc._to_mongo_dict() for doc in documents]
|
|
612
|
+
result = await collection.insert_many(docs)
|
|
613
|
+
return [str(id) for id in result.inserted_ids]
|
lightodm/py.typed
ADDED
|
File without changes
|