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

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) 

9 

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

34 

35INHERITANCE_FIELD_NAME = "_cls" 1fabcde

36 

37RELATION_TYPE = dict[str, RelationalFieldInfo] 1fabcde

38 

39_cashed_collection: dict[Any, CollectionConfig] = {} 1fabcde

40_cashed_field_info: dict[str, RELATION_TYPE] = {} 1fabcde

41 

42 

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

47 

48 for key in list(_cashed_field_info.keys()): 1fabcde

49 del _cashed_field_info[key] 1fabcde

50 

51 

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

57 

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

62 

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 

69 

70 raise AttributeError(f"type object '{cls.__name__}' has no attribute '{name}'") 1fabcde

71 

72 

73class _BaseDocument(BaseModel, metaclass=ODMMeta): 1fabcde

74 """ 

75 This class will handle all database and model-related configuration. 

76 """ 

77 

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

85 

86 """ 1fabcde

87 Definition of ODMConfig fields: 

88 

89 collection_name: This field will overwrite collection name. 

90 

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'. 

95 

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. 

98 

99 indexes: Define list of IndexModel to manage indexes for a model 

100 

101 database: Handle multiple database configurations using this field. 

102 The default database will be None. 

103 """ 

104 

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

114 

115 def dict(self, **kwargs: Any) -> DICT_TYPE: 1fabcde

116 return self.model_dump(**kwargs) 1fabcde

117 

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

147 

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

157 

158 model, child_model = cls.__get_collection_class() 1fabcde

159 

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

167 

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

178 

179 @classmethod 1fabcde

180 def _database_name(cls) -> Optional[str]: 1fabcde

181 config = cls.__get_collection_config() 1fabcde

182 return config.database_name 1fabcde

183 

184 @classmethod 1fabcde

185 def _get_collection_name(cls) -> str: 1fabcde

186 return cls.__get_collection_config().collection_name 1fabcde

187 

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

194 

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

201 

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

206 

207 return cast(Collection[Any], collection) 1fabcde

208 

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

213 

214 return cast(AsyncCollection[Any], collection) 1fabcde

215 

216 @classmethod 1fabcde

217 def _db(cls) -> str: 1fabcde

218 return cls._get_collection_name() 1fabcde

219 

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

226 

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

235 

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

244 

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

250 

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

258 

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. 

263 

264 id: For '_id' and 'id' fields will be excluded on the object creation. 

265 So that 'id' creation happens on the database only. 

266 

267 None: Exclude null fields to improve database storage efficiency. 

268 """ 

269 return self.dict(exclude=self.get_exclude_fields()) 1fabcde

270 

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 ) 

278 

279 return client.start_session(**kwargs) 1fabcde

280 

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 ) 

288 

289 return client.start_session(**kwargs) 1fabcde

290 

291 def __str__(self) -> str: 1fabcde

292 return super().__repr__() 1fabcde

293 

294 

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 """ 

300 

301 _id: ODMObjectId = PrivateAttr(default_factory=ODMObjectId) 1fabcde

302 id: ODMObjectId = Field(default_factory=ODMObjectId) 1fabcde

303 

304 def __init__(self, *args: list[Any], **kwargs: Any) -> None: 1fabcde

305 super().__init__(*args, **kwargs) 1fabcde

306 

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

313 

314 object.__setattr__(self, "id", id) 1fabcde

315 object.__setattr__(self, "_id", id) 1fabcde

316 

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

322 

323 return data 1fabcde

324 

325 def _update_new_id(self, new_id: ODMObjectId) -> None: 1fabcde

326 self.__dict__.update({"_id": new_id, "id": new_id}) 1fabcde

327 

328 def create(self, **kwargs: Any) -> Self: 1fabcde

329 data = self._prepare_crate_data(**kwargs) 1fabcde

330 

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

335 

336 return self 1fabcde

337 

338 async def acreate(self, **kwargs: Any) -> Self: 1fabcde

339 data = self._prepare_crate_data(**kwargs) 1fabcde

340 

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

344 

345 return self 1fabcde

346 

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

351 

352 validate_filter_dict(cls, filter) 1fabcde

353 

354 if cls._get_child() is not None: 1fabcde

355 filter = {**cls.get_inheritance_key(), **filter} 1fabcde

356 

357 return filter 1fabcde

358 

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

368 

369 filter = cls._validate_and_prepare_filter(filter) 1fabcde

370 

371 _collection = cls._get_collection() 1fabcde

372 

373 if projection: 1fabcde

374 return _collection.find(filter, projection, **kwargs) 1fabcde

375 

376 return _collection.find(filter, **kwargs) 1fabcde

377 

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

387 

388 filter = cls._validate_and_prepare_filter(filter) 1fabcde

389 _collection = cls._async_get_collection() 1fabcde

390 

391 if projection: 1fabcde

392 return _collection.find(filter, projection, **kwargs) 1fabcde

393 

394 return _collection.find(filter, **kwargs) 1fabcde

395 

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

411 

412 return query_set 1fabcde

413 

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

418 

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 

423 

424 model_children[child_model_name] = model # type: ignore 1fabcde

425 

426 return model_children 1fabcde

427 

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

438 

439 return cls(**data) 1fabcde

440 

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

453 

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

461 

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

474 

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

482 

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

494 

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

499 

500 if not obj: 1fabcde

501 return None 1fabcde

502 

503 if cls._has_children(): 1fabcde

504 model_children = cls._get_child_models() 1fabcde

505 return cls._prepare_class_instance(model_children, data) 1fabcde

506 

507 return cls(**data) 1fabcde

508 

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

520 

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

525 

526 if not obj: 1fabcde

527 return None 1fabcde

528 

529 if cls._has_children(): 1fabcde

530 model_children = cls._get_child_models() 1fabcde

531 return cls._prepare_class_instance(model_children, data) 1fabcde

532 

533 return cls(**data) 1fabcde

534 

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

545 

546 raise ObjectDoesNotExist("Object not found.") 1fabcde

547 

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

558 

559 raise ObjectDoesNotExist("Object not found.") 1fabcde

560 

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

571 

572 return cls(**filter).create(), True 1fabcde

573 

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

584 

585 new_instance = await cls(**filter).acreate() 1fabcde

586 

587 return new_instance, True 1fabcde

588 

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

592 

593 _collection = cls._get_collection() 1fabcde

594 

595 return _collection.count_documents(filter, **kwargs) 1fabcde

596 

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

602 

603 _collection = cls._async_get_collection() 1fabcde

604 

605 return await _collection.count_documents(filter, **kwargs) 1fabcde

606 

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

614 

615 return False 1fabcde

616 

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

624 

625 return False 1fabcde

626 

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 

651 

652 return pipeline 1fabcde

653 

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

658 

659 return dict2obj(obj) 1fabcde

660 

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

673 

674 query = _collection.aggregate(pipeline, **kwargs) 1fabcde

675 

676 for obj in query: 1fabcde

677 yield cls._get_aggregation_obj(obj, get_raw=get_raw) 1fabcde

678 

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

695 

696 async for obj in query: 1fabcde

697 yield cls._get_aggregation_obj(obj, get_raw=get_raw) 1fabcde

698 

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

702 

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

707 

708 for data in cls.aggregate(pipeline, get_raw=True, **kwargs): 1fabcde

709 return cls(**data) 1fabcde

710 

711 raise ObjectDoesNotExist("Object not found.") 1fabcde

712 

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

719 

720 async for data in cls.aaggregate(pipeline, get_raw=True, **kwargs): 1fabcde

721 return cls(**data) 1fabcde

722 

723 raise ObjectDoesNotExist("Object not found.") 1fabcde

724 

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

730 

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

736 

737 updated_data["$set"]["updated_at"] = datetime_now 1fabcde

738 

739 self.__dict__.update({"updated_at": datetime_now}) 1fabcde

740 

741 return updated_data 1fabcde

742 

743 def update(self, raw: Optional[DICT_TYPE] = None, **kwargs: Any) -> UpdateResult: 1fabcde

744 filter = {"_id": self.id} 1fabcde

745 

746 updated_data = self._get_update_dict(raw) 1fabcde

747 

748 return self.update_one(filter, updated_data, **kwargs) 1fabcde

749 

750 async def aupdate( 1fabcde

751 self, raw: Optional[DICT_TYPE] = None, **kwargs: Any 

752 ) -> UpdateResult: 

753 filter = {"_id": self.id} 1fabcde

754 

755 updated_data = self._get_update_dict(raw) 1fabcde

756 

757 return await self.aupdate_one(filter, updated_data, **kwargs) 1fabcde

758 

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

764 

765 _collection = cls._get_collection() 1fabcde

766 

767 return _collection.update_one(filter, data, **kwargs) 1fabcde

768 

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

775 

776 return await _collection.update_one(filter, data, **kwargs) 1fabcde

777 

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

783 

784 _collection = cls._get_collection() 1fabcde

785 

786 return _collection.update_many(filter, data, **kwargs) 1fabcde

787 

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

794 

795 return await _collection.update_many(filter, data, **kwargs) 1fabcde

796 

797 def delete(self, **kwargs: Any) -> DeleteResult: 1fabcde

798 return self.delete_one({"_id": self.id}, **kwargs) 1fabcde

799 

800 async def adelete(self, **kwargs: Any) -> DeleteResult: 1fabcde

801 return await self.adelete_one({"_id": self.id}, **kwargs) 1fabcde

802 

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

807 

808 print(f"delete_many filter: {filter}, kwargs: {kwargs}") 1fabcde

809 

810 _collection = cls._get_collection() 1fabcde

811 return _collection.delete_one(filter, **kwargs) 1fabcde

812 

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

818 

819 return await _collection.delete_one(filter, **kwargs) 1fabcde

820 

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

825 

826 _collection = cls._get_collection() 1fabcde

827 return _collection.delete_many(filter, **kwargs) 1fabcde

828 

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

834 

835 return await _collection.delete_many(filter, **kwargs) 1fabcde

836 

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

843 

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

849 

850 return await _collection.bulk_write(requests, **kwargs) 1fabcde

851 

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

859 

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

870 

871 return loadable_fields_info 1fabcde

872 

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

878 

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

883 

884 return fields_id_dict, field_data_data 1fabcde

885 

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

897 

898 results.append(obj) 1fabcde

899 

900 return results 1fabcde

901 

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

915 

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 ) 

923 

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

927 

928 field_data_data[field] = {obj.id: obj for obj in query} 1fabcde

929 

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

937 

938 return results 1fabcde

939 

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

951 

952 results.append(obj) 1fabcde

953 

954 return results 1fabcde

955 

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

969 

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 ) 

977 

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

981 

982 field_data_data[field] = {obj.id: obj async for obj in query} 1fabcde

983 

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

991 

992 return results 1fabcde