Coverage for mongodb_odm / models.py: 99%
511 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-07 08:37 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-07 08:37 +0000
1from collections.abc import AsyncIterator, Iterator, Sequence 1fabcde
2from datetime import datetime 1fabcde
3from typing import ( 1fabcde
4 Any,
5 Optional,
6 Union,
7 cast,
8)
10from mongodb_odm.connection import db, get_client 1fabcde
11from mongodb_odm.data_conversion import dict2obj 1fabcde
12from mongodb_odm.exceptions import InvalidConfiguration, ObjectDoesNotExist 1fabcde
13from mongodb_odm.fields import Field 1fabcde
14from mongodb_odm.types import DICT_TYPE, SORT_TYPE, ODMObjectId, WriteOp 1fabcde
15from mongodb_odm.utils._internal_models import CollectionConfig, RelationalFieldInfo 1fabcde
16from mongodb_odm.utils.utils import ( 1fabcde
17 convert_model_to_collection,
18 get_database_name,
19 get_model_fields,
20 get_relationship_fields_info,
21)
22from mongodb_odm.utils.validation import validate_filter_dict 1fabcde
23from pydantic import BaseModel, PrivateAttr 1fabcde
24from pydantic._internal._model_construction import ModelMetaclass 1fabcde
25from pymongo import AsyncMongoClient, IndexModel, MongoClient 1fabcde
26from pymongo.asynchronous.client_session import AsyncClientSession 1fabcde
27from pymongo.asynchronous.collection import AsyncCollection 1fabcde
28from pymongo.asynchronous.cursor import AsyncCursor 1fabcde
29from pymongo.client_session import ClientSession 1fabcde
30from pymongo.collection import Collection 1fabcde
31from pymongo.cursor import Cursor 1fabcde
32from pymongo.results import BulkWriteResult, DeleteResult, UpdateResult 1fabcde
33from typing_extensions import Self 1fabcde
35INHERITANCE_FIELD_NAME = "_cls" 1fabcde
37RELATION_TYPE = dict[str, RelationalFieldInfo] 1fabcde
39_cashed_collection: dict[Any, CollectionConfig] = {} 1fabcde
40_cashed_field_info: dict[str, RELATION_TYPE] = {} 1fabcde
43def _clear_cache() -> None: 1fabcde
44 global _cashed_collection, _cashed_field_info
45 for key in list(_cashed_collection.keys()): 1fabcde
46 del _cashed_collection[key] 1fabcde
48 for key in list(_cashed_field_info.keys()): 1fabcde
49 del _cashed_field_info[key] 1fabcde
52class ODMMeta(ModelMetaclass): 1fabcde
53 def __getattr__(cls, name: str) -> str: 1fabcde
54 # Avoid recursion for internal pydantic attributes
55 if name.startswith("__pydantic"): 1fabcde
56 raise AttributeError(name) 1fabcde
58 try: 1fabcde
59 # Try to access model_fields without triggering __getattr__ for it
60 # In Pydantic v2, fields are stored in __pydantic_fields__
61 fields = cls.__dict__.get("__pydantic_fields__") 1fabcde
63 if fields and name in fields: 1fabcde
64 if name == "id": 1fabcde
65 return "_id" 1fabcde
66 return name 1fabcde
67 except Exception:
68 pass
70 raise AttributeError(f"type object '{cls.__name__}' has no attribute '{name}'") 1fabcde
73class _BaseDocument(BaseModel, metaclass=ODMMeta): 1fabcde
74 """
75 This class will handle all database and model-related configuration.
76 """
78 class ODMConfig: 1fabcde
79 # Those fields will work as the default value of any child class.
80 collection_name: Optional[str] = None 1fabcde
81 allow_inheritance: bool = False 1fabcde
82 index_inheritance_field: bool = True 1fabcde
83 indexes: list[IndexModel] = [] 1fabcde
84 database: Optional[str] = None 1fabcde
86 """ 1fabcde
87 Definition of ODMConfig fields:
89 collection_name: This field will overwrite collection name.
91 allow_inheritance: It enables a model to have a child.
92 We will manage a single field for parent and child collection.
93 The collection name will be according to the parent model configuration.
94 Child collection data will be separated by field name '_cls'.
96 index_inheritance_field: By default, we create an index for '_cls'
97 if the model has a child. Users may control that by using this field.
99 indexes: Define list of IndexModel to manage indexes for a model
101 database: Handle multiple database configurations using this field.
102 The default database will be None.
103 """
105 def __setattr__(self, key: str, value: Any) -> None: 1fabcde
106 """
107 Add '# type: ignore' as a comment if get type error while getting this value
108 """
109 if key == "_id" or key == "id": 1fabcde
110 self.__dict__["id"] = value 1fabcde
111 self.__dict__["_id"] = value 1fabcde
112 else:
113 self.__dict__[key] = value 1fabcde
115 def dict(self, **kwargs: Any) -> DICT_TYPE: 1fabcde
116 return self.model_dump(**kwargs) 1fabcde
118 @classmethod 1fabcde
119 def __get_collection_class(cls) -> tuple[str, Optional[str]]: 1fabcde
120 """
121 Get model class.
122 if called from a child class:
123 return (parent-class, child-class)
124 else:
125 return (class, None)
126 """
127 model: Any = cls 1fabcde
128 if model.__base__ != Document: 1fabcde
129 base_model = model.__base__ 1fabcde
130 if ( 1fab
131 not hasattr(base_model.ODMConfig, "allow_inheritance")
132 or base_model.ODMConfig.allow_inheritance is not True
133 ):
134 raise InvalidConfiguration( 1fabcde
135 f"Invalid model inheritance. {base_model} does not allow model inheritance."
136 )
137 if ( 1f
138 base_model.ODMConfig.allow_inheritance is True
139 and model.ODMConfig.allow_inheritance is True
140 ):
141 raise InvalidConfiguration( 1fabcde
142 f"Child Model{model.__name__} should declare a separate ODMConfig class."
143 )
144 return base_model, model 1fabcde
145 else:
146 return model, None 1fabcde
148 @classmethod 1fabcde
149 def __get_collection_config(cls) -> CollectionConfig: 1fabcde
150 """
151 Get collection configuration for a model.
152 Get data from the cache if it is already calculated.
153 """
154 global _cashed_collection
155 if cls in _cashed_collection: 1fabcde
156 return _cashed_collection[cls] 1fabcde
158 model, child_model = cls.__get_collection_class() 1fabcde
160 has_children = False 1fabcde
161 if ( 1fab
162 hasattr(cls.ODMConfig, "allow_inheritance")
163 and cls.ODMConfig.allow_inheritance is True
164 ):
165 """Check if this is a model that allows inheritance and has a child model.""" 1abcde
166 has_children = len(cls.__subclasses__()) > 0 1fabcde
168 child_collection_name = ( 1fabcde
169 convert_model_to_collection(child_model) if child_model else None
170 )
171 _cashed_collection[cls] = CollectionConfig( 1fabcde
172 collection_name=convert_model_to_collection(model),
173 child_collection_name=child_collection_name,
174 database_name=get_database_name(model),
175 has_children=has_children,
176 )
177 return _cashed_collection[cls] 1fabcde
179 @classmethod 1fabcde
180 def _database_name(cls) -> Optional[str]: 1fabcde
181 config = cls.__get_collection_config() 1fabcde
182 return config.database_name 1fabcde
184 @classmethod 1fabcde
185 def _get_collection_name(cls) -> str: 1fabcde
186 return cls.__get_collection_config().collection_name 1fabcde
188 @classmethod 1fabcde
189 def _get_child(cls) -> Optional[str]: 1fabcde
190 """
191 Get the child collection name if it has a parent class.
192 """
193 return cls.__get_collection_config().child_collection_name 1fabcde
195 @classmethod 1fabcde
196 def _has_children(cls) -> bool: 1fabcde
197 """
198 Check if a model has child class
199 """
200 return cls.__get_collection_config().has_children 1fabcde
202 @classmethod 1fabcde
203 def _get_collection(cls) -> Collection[Any]: 1fabcde
204 db_connection = db(cls._database_name()) 1fabcde
205 collection = db_connection[cls._get_collection_name()] 1fabcde
207 return cast(Collection[Any], collection) 1fabcde
209 @classmethod 1fabcde
210 def _async_get_collection(cls) -> AsyncCollection[Any]: 1fabcde
211 db_connection = db(cls._database_name(), is_async_action=True) 1fabcde
212 collection = db_connection[cls._get_collection_name()] 1fabcde
214 return cast(AsyncCollection[Any], collection) 1fabcde
216 @classmethod 1fabcde
217 def _db(cls) -> str: 1fabcde
218 return cls._get_collection_name() 1fabcde
220 @classmethod 1fabcde
221 def get_inheritance_key(cls) -> DICT_TYPE: 1fabcde
222 """
223 Get child filter keys
224 """
225 return {INHERITANCE_FIELD_NAME: cls._get_child()} 1fabcde
227 @classmethod 1fabcde
228 def get_parent_child_fields(cls) -> DICT_TYPE: 1fabcde
229 fields = get_model_fields(cls) 1fabcde
230 if cls._has_children(): 1fabcde
231 for model in cls.__subclasses__(): 1fabcde
232 child_fields = get_model_fields(model) 1fabcde
233 fields.update(child_fields) 1fabcde
234 return fields 1fabcde
236 @classmethod 1fabcde
237 def get_relational_field_info(cls) -> RELATION_TYPE: 1fabcde
238 """
239 Get all relational field information.
240 Get data from from cache if it's already calculated.
241 """
242 global _cashed_field_info
243 field_info_key = f"{hash(cls)}-field_info" # unique key for each model 1fabcde
245 cached_field_info = _cashed_field_info.get(field_info_key) 1fabcde
246 if not cached_field_info: 1fabcde
247 cached_field_info = get_relationship_fields_info(cls) 1fabcde
248 _cashed_field_info[field_info_key] = cached_field_info 1fabcde
249 return cached_field_info 1fabcde
251 @classmethod 1fabcde
252 def get_exclude_fields(cls) -> set[str]: 1fabcde
253 """
254 Get all fields that should not pass while creating or updating an object.
255 """
256 relational_fields = cls.get_relational_field_info().keys() 1fabcde
257 return {"_id", "id", *relational_fields} 1fabcde
259 def to_mongo(self) -> DICT_TYPE: 1fabcde
260 """
261 relational-field: The relational field is only for representational use only.
262 Do not store those fields in the database.
264 id: For '_id' and 'id' fields will be excluded on the object creation.
265 So that 'id' creation happens on the database only.
267 None: Exclude null fields to improve database storage efficiency.
268 """
269 return self.dict(exclude=self.get_exclude_fields()) 1fabcde
271 @classmethod 1fabcde
272 def start_session(cls, **kwargs: Any) -> ClientSession: 1fabcde
273 client = get_client() 1fabcde
274 if not isinstance(client, MongoClient): 1fabcde
275 raise InvalidConfiguration( 1fabcde
276 "Client is not configured for sync operations use 'astart_session' instead."
277 )
279 return client.start_session(**kwargs) 1fabcde
281 @classmethod 1fabcde
282 def astart_session(cls, **kwargs: Any) -> AsyncClientSession: 1fabcde
283 client = get_client() 1fabcde
284 if not isinstance(client, AsyncMongoClient): 1fabcde
285 raise InvalidConfiguration( 1fabcde
286 "Client is not configured for async operations use 'start_session' instead."
287 )
289 return client.start_session(**kwargs) 1fabcde
291 def __str__(self) -> str: 1fabcde
292 return super().__repr__() 1fabcde
295class Document(_BaseDocument): 1fabcde
296 """
297 id: For '_id' and 'id' fields will be excluded on the object creation.
298 So that 'id' creation happens on the database only.
299 """
301 _id: ODMObjectId = PrivateAttr(default_factory=ODMObjectId) 1fabcde
302 id: ODMObjectId = Field(default_factory=ODMObjectId) 1fabcde
304 def __init__(self, *args: list[Any], **kwargs: Any) -> None: 1fabcde
305 super().__init__(*args, **kwargs) 1fabcde
307 if "_id" in kwargs: 1fabcde
308 id = ODMObjectId(kwargs["_id"]) 1fabcde
309 elif "id" in kwargs: 1fabcde
310 id = ODMObjectId(kwargs["id"]) 1fabcde
311 else:
312 id = ODMObjectId() 1fabcde
314 object.__setattr__(self, "id", id) 1fabcde
315 object.__setattr__(self, "_id", id) 1fabcde
317 def _prepare_crate_data(self, **kwargs: Any) -> DICT_TYPE: 1fabcde
318 data = self.to_mongo() 1fabcde
319 if self._get_child() is not None: 1fabcde
320 # Assign the '_cls' field if the model is a child.
321 data = {**self.get_inheritance_key(), **data} 1fabcde
323 return data 1fabcde
325 def _update_new_id(self, new_id: ODMObjectId) -> None: 1fabcde
326 self.__dict__.update({"_id": new_id, "id": new_id}) 1fabcde
328 def create(self, **kwargs: Any) -> Self: 1fabcde
329 data = self._prepare_crate_data(**kwargs) 1fabcde
331 _collection = self._get_collection() 1fabcde
332 result = _collection.insert_one(data, **kwargs) 1fabcde
333 inserted_id = result.inserted_id 1fabcde
334 self._update_new_id(inserted_id) 1fabcde
336 return self 1fabcde
338 async def acreate(self, **kwargs: Any) -> Self: 1fabcde
339 data = self._prepare_crate_data(**kwargs) 1fabcde
341 _collection = self._async_get_collection() 1fabcde
342 inserted_id = (await _collection.insert_one(data, **kwargs)).inserted_id 1fabcde
343 self._update_new_id(inserted_id) 1fabcde
345 return self 1fabcde
347 @classmethod 1fabcde
348 def _validate_and_prepare_filter(cls, filter: Optional[DICT_TYPE]) -> DICT_TYPE: 1fabcde
349 if filter is None: 1fabcde
350 filter = {} 1fabcde
352 validate_filter_dict(cls, filter) 1fabcde
354 if cls._get_child() is not None: 1fabcde
355 filter = {**cls.get_inheritance_key(), **filter} 1fabcde
357 return filter 1fabcde
359 @classmethod 1fabcde
360 def find_raw( 1fabcde
361 cls,
362 filter: Optional[DICT_TYPE] = None,
363 projection: Optional[DICT_TYPE] = None,
364 **kwargs: Any,
365 ) -> Cursor[Any]:
366 if projection is None: 1fabcde
367 projection = {} 1fabcde
369 filter = cls._validate_and_prepare_filter(filter) 1fabcde
371 _collection = cls._get_collection() 1fabcde
373 if projection: 1fabcde
374 return _collection.find(filter, projection, **kwargs) 1fabcde
376 return _collection.find(filter, **kwargs) 1fabcde
378 @classmethod 1fabcde
379 def afind_raw( 1fabcde
380 cls,
381 filter: Optional[DICT_TYPE] = None,
382 projection: Optional[DICT_TYPE] = None,
383 **kwargs: Any,
384 ) -> AsyncCursor[Any]:
385 if projection is None: 1fabcde
386 projection = {} 1fabcde
388 filter = cls._validate_and_prepare_filter(filter) 1fabcde
389 _collection = cls._async_get_collection() 1fabcde
391 if projection: 1fabcde
392 return _collection.find(filter, projection, **kwargs) 1fabcde
394 return _collection.find(filter, **kwargs) 1fabcde
396 @classmethod 1fabcde
397 def _prepare_query( 1fabcde
398 cls,
399 query_set: Any,
400 sort: Optional[SORT_TYPE] = None,
401 skip: Optional[int] = None,
402 limit: Optional[int] = None,
403 ) -> Any:
404 """Helper method to prepare query with sort, skip and limit parameters."""
405 if sort: 1fabcde
406 query_set = query_set.sort(sort) 1fabcde
407 if skip: 1fabcde
408 query_set = query_set.skip(skip) 1fabcde
409 if limit: 1fabcde
410 query_set = query_set.limit(limit) 1fabcde
412 return query_set 1fabcde
414 @classmethod 1fabcde
415 def _get_child_models(cls) -> dict[str, Self]: 1fabcde
416 """Helper method to get child models mapping."""
417 model_children: dict[str, Self] = {} 1fabcde
419 for model in cls.__subclasses__(): 1fabcde
420 child_model_name = model._get_child() 1fabcde
421 if child_model_name is None: 1fabcde
422 continue
424 model_children[child_model_name] = model # type: ignore 1fabcde
426 return model_children 1fabcde
428 @classmethod 1fabcde
429 def _prepare_class_instance( 1fabcde
430 cls,
431 model_children: dict[str, Self],
432 data: DICT_TYPE,
433 ) -> Self:
434 if data.get(INHERITANCE_FIELD_NAME) in model_children: 1fabcde
435 """If this is a child model then convert it to that child model.""" 1abcde
436 kls = model_children[data[INHERITANCE_FIELD_NAME]] 1fabcde
437 return kls(**data) # type: ignore 1fabcde
439 return cls(**data) 1fabcde
441 @classmethod 1fabcde
442 def find( 1fabcde
443 cls,
444 filter: Optional[DICT_TYPE] = None,
445 projection: Optional[DICT_TYPE] = None,
446 sort: Optional[SORT_TYPE] = None,
447 skip: Optional[int] = None,
448 limit: Optional[int] = None,
449 **kwargs: Any,
450 ) -> Iterator[Self]:
451 qs = cls.find_raw(filter, projection, **kwargs) 1fabcde
452 qs = cls._prepare_query(qs, sort, skip, limit) 1fabcde
454 if cls._has_children(): 1fabcde
455 model_children = cls._get_child_models() 1fabcde
456 for data in qs: 1fabcde
457 yield cls._prepare_class_instance(model_children, data) 1fabcde
458 else:
459 for data in qs: 1fabcde
460 yield cls(**data) 1fabcde
462 @classmethod 1fabcde
463 async def afind( 1fabcde
464 cls,
465 filter: Optional[DICT_TYPE] = None,
466 projection: Optional[DICT_TYPE] = None,
467 sort: Optional[SORT_TYPE] = None,
468 skip: Optional[int] = None,
469 limit: Optional[int] = None,
470 **kwargs: Any,
471 ) -> AsyncIterator[Self]:
472 qs = cls.afind_raw(filter, projection, **kwargs) 1fabcde
473 qs = cls._prepare_query(qs, sort, skip, limit) 1fabcde
475 if cls._has_children(): 1fabcde
476 model_children = cls._get_child_models() 1fabcde
477 async for data in qs: 1fabcde
478 yield cls._prepare_class_instance(model_children, data) 1fabcde
479 else:
480 async for data in qs: 1fabcde
481 yield cls(**data) 1fabcde
483 @classmethod 1fabcde
484 def find_one( 1fabcde
485 cls,
486 filter: Optional[DICT_TYPE] = None,
487 projection: Optional[DICT_TYPE] = None,
488 sort: Optional[SORT_TYPE] = None,
489 **kwargs: Any,
490 ) -> Optional[Self]:
491 qs = cls.find_raw(filter, projection=projection, **kwargs) 1fabcde
492 if sort: 1fabcde
493 qs = qs.sort(sort) 1fabcde
495 obj = None 1fabcde
496 for data in qs.limit(1): 1fabcde
497 """limit 1 is equivalent to find_one and that is implemented in pymongo find_one""" 1abcde
498 obj = data 1fabcde
500 if not obj: 1fabcde
501 return None 1fabcde
503 if cls._has_children(): 1fabcde
504 model_children = cls._get_child_models() 1fabcde
505 return cls._prepare_class_instance(model_children, data) 1fabcde
507 return cls(**data) 1fabcde
509 @classmethod 1fabcde
510 async def afind_one( 1fabcde
511 cls,
512 filter: Optional[DICT_TYPE] = None,
513 projection: Optional[DICT_TYPE] = None,
514 sort: Optional[SORT_TYPE] = None,
515 **kwargs: Any,
516 ) -> Optional[Self]:
517 qs = cls.afind_raw(filter, projection=projection, **kwargs) 1fabcde
518 if sort: 1fabcde
519 qs = qs.sort(sort) 1fabcde
521 obj = None 1fabcde
522 async for data in qs.limit(1): 1fabcde
523 """limit 1 is equivalent to find_one and that is implemented in pymongo find_one""" 1abcde
524 obj = data 1fabcde
526 if not obj: 1fabcde
527 return None 1fabcde
529 if cls._has_children(): 1fabcde
530 model_children = cls._get_child_models() 1fabcde
531 return cls._prepare_class_instance(model_children, data) 1fabcde
533 return cls(**data) 1fabcde
535 @classmethod 1fabcde
536 def get( 1fabcde
537 cls,
538 filter: DICT_TYPE,
539 sort: Optional[SORT_TYPE] = None,
540 **kwargs: Any,
541 ) -> Self:
542 obj = cls.find_one(filter, sort=sort, **kwargs) 1fabcde
543 if obj: 1fabcde
544 return obj 1fabcde
546 raise ObjectDoesNotExist("Object not found.") 1fabcde
548 @classmethod 1fabcde
549 async def aget( 1fabcde
550 cls,
551 filter: DICT_TYPE,
552 sort: Optional[SORT_TYPE] = None,
553 **kwargs: Any,
554 ) -> Self:
555 obj = await cls.afind_one(filter, sort=sort, **kwargs) 1fabcde
556 if obj: 1fabcde
557 return obj 1fabcde
559 raise ObjectDoesNotExist("Object not found.") 1fabcde
561 @classmethod 1fabcde
562 def get_or_create( 1fabcde
563 cls,
564 filter: DICT_TYPE,
565 sort: Optional[SORT_TYPE] = None,
566 **kwargs: Any,
567 ) -> tuple[Self, bool]:
568 obj = cls.find_one(filter, sort=sort, **kwargs) 1fabcde
569 if obj: 1fabcde
570 return obj, False 1fabcde
572 return cls(**filter).create(), True 1fabcde
574 @classmethod 1fabcde
575 async def aget_or_create( 1fabcde
576 cls,
577 filter: DICT_TYPE,
578 sort: Optional[SORT_TYPE] = None,
579 **kwargs: Any,
580 ) -> tuple[Self, bool]:
581 obj = await cls.afind_one(filter, sort=sort, **kwargs) 1fabcde
582 if obj: 1fabcde
583 return obj, False 1fabcde
585 new_instance = await cls(**filter).acreate() 1fabcde
587 return new_instance, True 1fabcde
589 @classmethod 1fabcde
590 def count_documents(cls, filter: Optional[DICT_TYPE] = None, **kwargs: Any) -> int: 1fabcde
591 filter = cls._validate_and_prepare_filter(filter) 1fabcde
593 _collection = cls._get_collection() 1fabcde
595 return _collection.count_documents(filter, **kwargs) 1fabcde
597 @classmethod 1fabcde
598 async def acount_documents( 1fabcde
599 cls, filter: Optional[DICT_TYPE] = None, **kwargs: Any
600 ) -> int:
601 filter = cls._validate_and_prepare_filter(filter) 1fabcde
603 _collection = cls._async_get_collection() 1fabcde
605 return await _collection.count_documents(filter, **kwargs) 1fabcde
607 @classmethod 1fabcde
608 def exists(cls, filter: Optional[DICT_TYPE] = None, **kwargs: Any) -> bool: 1fabcde
609 """
610 It does not need to count all documents and avoid unnecessary db overhead.
611 """
612 for _ in cls.find_raw(filter, projection={"_id": 1}, **kwargs).limit(1): 1fabcde
613 return True 1fabcde
615 return False 1fabcde
617 @classmethod 1fabcde
618 async def aexists(cls, filter: Optional[DICT_TYPE] = None, **kwargs: Any) -> bool: 1fabcde
619 """
620 It does not need to count all documents and avoid unnecessary db overhead.
621 """
622 async for _ in cls.afind_raw(filter, projection={"_id": 1}, **kwargs).limit(1): 1fabcde
623 return True 1fabcde
625 return False 1fabcde
627 @classmethod 1fabcde
628 def _prepare_aggregation_pipeline( 1fabcde
629 cls,
630 pipeline: list[Any],
631 inheritance_filter: bool = True,
632 ) -> list[Any]:
633 if inheritance_filter and cls._get_child() is not None: 1fabcde
634 """ 1abcde
635 If aggregate was called from the child model then add the "$match" stage
636 to separate the document from other child and parent.
637 """
638 if len(pipeline) > 0 and "$match" in pipeline[0]: 1fabcde
639 """ 1abcde
640 If the first stage of the pipeline is "$match"
641 update it with the '_cls' field.
642 """
643 pipeline[0]["$match"] = { 1fabcde
644 f"{INHERITANCE_FIELD_NAME}": cls._get_child(),
645 **pipeline[0]["$match"],
646 }
647 else:
648 pipeline = [ 1fabcde
649 {"$match": {f"{INHERITANCE_FIELD_NAME}": cls._get_child()}}
650 ] + pipeline
652 return pipeline 1fabcde
654 @classmethod 1fabcde
655 def _get_aggregation_obj(cls, obj: DICT_TYPE, get_raw: bool = False) -> Any: 1fabcde
656 if get_raw is True: 1fabcde
657 return obj 1fabcde
659 return dict2obj(obj) 1fabcde
661 @classmethod 1fabcde
662 def aggregate( 1fabcde
663 cls,
664 pipeline: list[Any],
665 get_raw: bool = False,
666 inheritance_filter: bool = True,
667 **kwargs: Any,
668 ) -> Iterator[Any]:
669 pipeline = cls._prepare_aggregation_pipeline( 1fabcde
670 pipeline, inheritance_filter=inheritance_filter
671 )
672 _collection = cls._get_collection() 1fabcde
674 query = _collection.aggregate(pipeline, **kwargs) 1fabcde
676 for obj in query: 1fabcde
677 yield cls._get_aggregation_obj(obj, get_raw=get_raw) 1fabcde
679 @classmethod 1fabcde
680 async def aaggregate( 1fabcde
681 cls,
682 pipeline: list[Any],
683 get_raw: bool = False,
684 inheritance_filter: bool = True,
685 **kwargs: Any,
686 ) -> AsyncIterator[Any]:
687 """
688 Return an async iterator for MongoDB aggregation results.
689 """
690 pipeline = cls._prepare_aggregation_pipeline( 1fabcde
691 pipeline, inheritance_filter=inheritance_filter
692 )
693 _collection = cls._async_get_collection() 1fabcde
694 query = await _collection.aggregate(pipeline, **kwargs) 1fabcde
696 async for obj in query: 1fabcde
697 yield cls._get_aggregation_obj(obj, get_raw=get_raw) 1fabcde
699 @classmethod 1fabcde
700 def _get_pipeline_for_random_one(cls, filter: DICT_TYPE) -> list[DICT_TYPE]: 1fabcde
701 return [{"$match": filter}, {"$sample": {"size": 1}}] 1fabcde
703 @classmethod 1fabcde
704 def get_random_one(cls, filter: Optional[DICT_TYPE] = None, **kwargs: Any) -> Self: 1fabcde
705 filter = cls._validate_and_prepare_filter(filter) 1fabcde
706 pipeline = cls._get_pipeline_for_random_one(filter) 1fabcde
708 for data in cls.aggregate(pipeline, get_raw=True, **kwargs): 1fabcde
709 return cls(**data) 1fabcde
711 raise ObjectDoesNotExist("Object not found.") 1fabcde
713 @classmethod 1fabcde
714 async def aget_random_one( 1fabcde
715 cls, filter: Optional[DICT_TYPE] = None, **kwargs: Any
716 ) -> Self:
717 filter = cls._validate_and_prepare_filter(filter) 1fabcde
718 pipeline = cls._get_pipeline_for_random_one(filter) 1fabcde
720 async for data in cls.aaggregate(pipeline, get_raw=True, **kwargs): 1fabcde
721 return cls(**data) 1fabcde
723 raise ObjectDoesNotExist("Object not found.") 1fabcde
725 def _get_update_dict(self, raw: Optional[DICT_TYPE] = None) -> DICT_TYPE: 1fabcde
726 if raw: 1fabcde
727 updated_data = raw 1fabcde
728 else:
729 updated_data = {"$set": self.to_mongo()} 1fabcde
731 if hasattr(self, "updated_at"): 1fabcde
732 # Programmatically assign updated_at at the time of updating document.
733 datetime_now = datetime.now() 1fabcde
734 if "$set" not in updated_data: 1fabcde
735 updated_data["$set"] = {} 1fabcde
737 updated_data["$set"]["updated_at"] = datetime_now 1fabcde
739 self.__dict__.update({"updated_at": datetime_now}) 1fabcde
741 return updated_data 1fabcde
743 def update(self, raw: Optional[DICT_TYPE] = None, **kwargs: Any) -> UpdateResult: 1fabcde
744 filter = {"_id": self.id} 1fabcde
746 updated_data = self._get_update_dict(raw) 1fabcde
748 return self.update_one(filter, updated_data, **kwargs) 1fabcde
750 async def aupdate( 1fabcde
751 self, raw: Optional[DICT_TYPE] = None, **kwargs: Any
752 ) -> UpdateResult:
753 filter = {"_id": self.id} 1fabcde
755 updated_data = self._get_update_dict(raw) 1fabcde
757 return await self.aupdate_one(filter, updated_data, **kwargs) 1fabcde
759 @classmethod 1fabcde
760 def update_one( 1fabcde
761 cls, filter: DICT_TYPE, data: DICT_TYPE, **kwargs: Any
762 ) -> UpdateResult:
763 filter = cls._validate_and_prepare_filter(filter) 1fabcde
765 _collection = cls._get_collection() 1fabcde
767 return _collection.update_one(filter, data, **kwargs) 1fabcde
769 @classmethod 1fabcde
770 async def aupdate_one( 1fabcde
771 cls, filter: DICT_TYPE, data: DICT_TYPE, **kwargs: Any
772 ) -> UpdateResult:
773 filter = cls._validate_and_prepare_filter(filter) 1fabcde
774 _collection = cls._async_get_collection() 1fabcde
776 return await _collection.update_one(filter, data, **kwargs) 1fabcde
778 @classmethod 1fabcde
779 def update_many( 1fabcde
780 cls, filter: DICT_TYPE, data: DICT_TYPE, **kwargs: Any
781 ) -> UpdateResult:
782 filter = cls._validate_and_prepare_filter(filter) 1fabcde
784 _collection = cls._get_collection() 1fabcde
786 return _collection.update_many(filter, data, **kwargs) 1fabcde
788 @classmethod 1fabcde
789 async def aupdate_many( 1fabcde
790 cls, filter: DICT_TYPE, data: DICT_TYPE, **kwargs: Any
791 ) -> UpdateResult:
792 filter = cls._validate_and_prepare_filter(filter) 1fabcde
793 _collection = cls._async_get_collection() 1fabcde
795 return await _collection.update_many(filter, data, **kwargs) 1fabcde
797 def delete(self, **kwargs: Any) -> DeleteResult: 1fabcde
798 return self.delete_one({"_id": self.id}, **kwargs) 1fabcde
800 async def adelete(self, **kwargs: Any) -> DeleteResult: 1fabcde
801 return await self.adelete_one({"_id": self.id}, **kwargs) 1fabcde
803 @classmethod 1fabcde
804 def delete_one(cls, filter: DICT_TYPE, **kwargs: Any) -> DeleteResult: 1fabcde
805 """Will perform as Pymongo delete_one function."""
806 filter = cls._validate_and_prepare_filter(filter) 1fabcde
808 print(f"delete_many filter: {filter}, kwargs: {kwargs}") 1fabcde
810 _collection = cls._get_collection() 1fabcde
811 return _collection.delete_one(filter, **kwargs) 1fabcde
813 @classmethod 1fabcde
814 async def adelete_one(cls, filter: DICT_TYPE, **kwargs: Any) -> DeleteResult: 1fabcde
815 """Will perform as Pymongo delete_one function."""
816 filter = cls._validate_and_prepare_filter(filter) 1fabcde
817 _collection = cls._async_get_collection() 1fabcde
819 return await _collection.delete_one(filter, **kwargs) 1fabcde
821 @classmethod 1fabcde
822 def delete_many(cls, filter: DICT_TYPE, **kwargs: Any) -> DeleteResult: 1fabcde
823 """Will perform as Pymongo delete_many function."""
824 filter = cls._validate_and_prepare_filter(filter) 1fabcde
826 _collection = cls._get_collection() 1fabcde
827 return _collection.delete_many(filter, **kwargs) 1fabcde
829 @classmethod 1fabcde
830 async def adelete_many(cls, filter: DICT_TYPE, **kwargs: Any) -> DeleteResult: 1fabcde
831 """Will perform as Pymongo delete_many function."""
832 filter = cls._validate_and_prepare_filter(filter) 1fabcde
833 _collection = cls._async_get_collection() 1fabcde
835 return await _collection.delete_many(filter, **kwargs) 1fabcde
837 @classmethod 1fabcde
838 def bulk_write( 1fabcde
839 cls, requests: Sequence[WriteOp[Any]], **kwargs: Any
840 ) -> BulkWriteResult:
841 _collection = cls._get_collection() 1fabcde
842 return _collection.bulk_write(requests, **kwargs) 1fabcde
844 @classmethod 1fabcde
845 async def abulk_write( 1fabcde
846 cls, requests: Sequence[WriteOp[Any]], **kwargs: Any
847 ) -> BulkWriteResult:
848 _collection = cls._async_get_collection() 1fabcde
850 return await _collection.bulk_write(requests, **kwargs) 1fabcde
852 @classmethod 1fabcde
853 def _get_loadable_fields_info( 1fabcde
854 cls,
855 fields: Optional[list[str]] = None,
856 ) -> RELATION_TYPE:
857 """Get model relational field from cache"""
858 cached_field_info = cls.get_relational_field_info() 1fabcde
860 """Match with user given fields""" 1abcde
861 loadable_fields_info: Optional[RELATION_TYPE] = None 1fabcde
862 if fields: 1fabcde
863 loadable_fields_info = {} 1fabcde
864 for field in fields: 1fabcde
865 if field not in cached_field_info: 1fabcde
866 raise Exception(f'Invalid field "{field}"')
867 loadable_fields_info[field] = cached_field_info[field] 1fabcde
868 else:
869 loadable_fields_info = {**cached_field_info} 1fabcde
871 return loadable_fields_info 1fabcde
873 @classmethod 1fabcde
874 def _get_instance_related_info( 1fabcde
875 cls, loadable_fields_info: RELATION_TYPE
876 ) -> tuple[dict[str, list[Any]], DICT_TYPE]:
877 field_keys = loadable_fields_info.keys() 1fabcde
879 fields_id_dict: dict[str, list[RELATION_TYPE]] = { 1fabcde
880 field: [] for field in field_keys
881 }
882 field_data_data: DICT_TYPE = {field: {} for field in field_keys} 1fabcde
884 return fields_id_dict, field_data_data 1fabcde
886 @classmethod 1fabcde
887 def _get_objects_and_update_fields_id( 1fabcde
888 cls,
889 object_list: Union[Iterator[Self], Sequence[Self]],
890 loadable_fields_info: RELATION_TYPE,
891 fields_id_dict: dict[str, list[Any]],
892 ) -> list[Self]:
893 results: list[Self] = [] 1fabcde
894 for obj in object_list: 1fabcde
895 for field, field_info in loadable_fields_info.items(): 1fabcde
896 fields_id_dict[field].append(obj.__dict__[field_info.local_field]) 1fabcde
898 results.append(obj) 1fabcde
900 return results 1fabcde
902 @classmethod 1fabcde
903 def load_related( 1fabcde
904 cls,
905 object_list: Union[Iterator[Self], Sequence[Self]],
906 fields: Optional[list[str]] = None,
907 **kwargs: Any,
908 ) -> Sequence[Self]:
909 """
910 This method will load related documents from the database
911 according to the specified fields.
912 """
913 if fields is None: 1fabcde
914 fields = [] 1fabcde
916 loadable_fields_info = cls._get_loadable_fields_info(fields) 1fabcde
917 fields_id_dict, field_data_data = cls._get_instance_related_info( 1fabcde
918 loadable_fields_info
919 )
920 results = cls._get_objects_and_update_fields_id( 1fabcde
921 object_list, loadable_fields_info, fields_id_dict
922 )
924 """Load all document for all relational model""" 1abcde
925 for field, ids in fields_id_dict.items(): 1fabcde
926 query = loadable_fields_info[field].model.find({"_id": {"$in": ids}}) 1fabcde
928 field_data_data[field] = {obj.id: obj for obj in query} 1fabcde
930 """Assign loaded document with results""" 1abcde
931 for obj in results: 1fabcde
932 for field, field_info in loadable_fields_info.items(): 1fabcde
933 field_obj = field_data_data[field].get( 1fabcde
934 obj.__dict__[field_info.local_field]
935 )
936 obj.__dict__[field] = field_obj 1fabcde
938 return results 1fabcde
940 @classmethod 1fabcde
941 async def _async_get_objects_and_update_fields_id( 1fabcde
942 cls,
943 object_list: AsyncIterator[Self],
944 loadable_fields_info: RELATION_TYPE,
945 fields_id_dict: dict[str, list[Any]],
946 ) -> list[Self]:
947 results: list[Self] = [] 1fabcde
948 async for obj in object_list: 1fabcde
949 for field, field_info in loadable_fields_info.items(): 1fabcde
950 fields_id_dict[field].append(obj.__dict__[field_info.local_field]) 1fabcde
952 results.append(obj) 1fabcde
954 return results 1fabcde
956 @classmethod 1fabcde
957 async def aload_related( 1fabcde
958 cls,
959 object_list: AsyncIterator[Self],
960 fields: Optional[list[str]] = None,
961 **kwargs: Any,
962 ) -> Sequence[Self]:
963 """
964 This method will load related documents from the database
965 according to the specified fields.
966 """
967 if fields is None: 1fabcde
968 fields = [] 1fabcde
970 loadable_fields_info = cls._get_loadable_fields_info(fields) 1fabcde
971 fields_id_dict, field_data_data = cls._get_instance_related_info( 1fabcde
972 loadable_fields_info
973 )
974 results = await cls._async_get_objects_and_update_fields_id( 1fabcde
975 object_list, loadable_fields_info, fields_id_dict
976 )
978 """Load all document for all relational model""" 1abcde
979 for field, ids in fields_id_dict.items(): 1fabcde
980 query = loadable_fields_info[field].model.afind({"_id": {"$in": ids}}) 1fabcde
982 field_data_data[field] = {obj.id: obj async for obj in query} 1fabcde
984 """Assign loaded document with results""" 1abcde
985 for obj in results: 1fabcde
986 for field, field_info in loadable_fields_info.items(): 1fabcde
987 field_obj = field_data_data[field].get( 1fabcde
988 obj.__dict__[field_info.local_field]
989 )
990 obj.__dict__[field] = field_obj 1fabcde
992 return results 1fabcde