panther 3.8.2__py3-none-any.whl → 4.0.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.
Files changed (52) hide show
  1. panther/__init__.py +1 -1
  2. panther/_load_configs.py +168 -171
  3. panther/_utils.py +26 -49
  4. panther/app.py +85 -105
  5. panther/authentications.py +86 -55
  6. panther/background_tasks.py +25 -14
  7. panther/base_request.py +38 -14
  8. panther/base_websocket.py +172 -94
  9. panther/caching.py +60 -25
  10. panther/cli/create_command.py +20 -10
  11. panther/cli/monitor_command.py +63 -37
  12. panther/cli/template.py +40 -20
  13. panther/cli/utils.py +32 -18
  14. panther/configs.py +65 -58
  15. panther/db/connections.py +139 -0
  16. panther/db/cursor.py +43 -0
  17. panther/db/models.py +64 -29
  18. panther/db/queries/__init__.py +1 -1
  19. panther/db/queries/base_queries.py +127 -0
  20. panther/db/queries/mongodb_queries.py +77 -38
  21. panther/db/queries/pantherdb_queries.py +59 -30
  22. panther/db/queries/queries.py +232 -117
  23. panther/db/utils.py +17 -18
  24. panther/events.py +44 -0
  25. panther/exceptions.py +26 -12
  26. panther/file_handler.py +2 -2
  27. panther/generics.py +163 -0
  28. panther/logging.py +7 -2
  29. panther/main.py +111 -188
  30. panther/middlewares/base.py +3 -0
  31. panther/monitoring.py +8 -5
  32. panther/pagination.py +48 -0
  33. panther/panel/apis.py +32 -5
  34. panther/panel/urls.py +2 -1
  35. panther/permissions.py +3 -3
  36. panther/request.py +6 -13
  37. panther/response.py +114 -34
  38. panther/routings.py +83 -66
  39. panther/serializer.py +214 -33
  40. panther/test.py +31 -21
  41. panther/utils.py +28 -16
  42. panther/websocket.py +7 -4
  43. {panther-3.8.2.dist-info → panther-4.0.0.dist-info}/METADATA +93 -71
  44. panther-4.0.0.dist-info/RECORD +57 -0
  45. {panther-3.8.2.dist-info → panther-4.0.0.dist-info}/WHEEL +1 -1
  46. panther/db/connection.py +0 -92
  47. panther/middlewares/db.py +0 -18
  48. panther/middlewares/redis.py +0 -47
  49. panther-3.8.2.dist-info/RECORD +0 -54
  50. {panther-3.8.2.dist-info → panther-4.0.0.dist-info}/LICENSE +0 -0
  51. {panther-3.8.2.dist-info → panther-4.0.0.dist-info}/entry_points.txt +0 -0
  52. {panther-3.8.2.dist-info → panther-4.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,139 @@
1
+ import asyncio
2
+ import contextlib
3
+ from abc import abstractmethod
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from pantherdb import PantherDB
7
+
8
+ from panther.cli.utils import import_error
9
+ from panther.configs import config
10
+ from panther.utils import Singleton
11
+
12
+ try:
13
+ from redis.asyncio import Redis as _Redis
14
+ except ImportError:
15
+ # This '_Redis' is not going to be used,
16
+ # If user really wants to use redis,
17
+ # we are going to force him to install it in `panther._load_configs.load_redis`
18
+ _Redis = type('_Redis', (), {'__new__': lambda x: x})
19
+
20
+ if TYPE_CHECKING:
21
+ from pymongo.database import Database
22
+
23
+
24
+ class BaseDatabaseConnection:
25
+ def __init__(self, *args, **kwargs):
26
+ """Initialized in application startup"""
27
+ self.init(*args, **kwargs)
28
+
29
+ @abstractmethod
30
+ def init(self, *args, **kwargs):
31
+ pass
32
+
33
+ @property
34
+ @abstractmethod
35
+ def session(self):
36
+ pass
37
+
38
+
39
+ class MongoDBConnection(BaseDatabaseConnection):
40
+ def init(
41
+ self,
42
+ host: str = 'localhost',
43
+ port: int = 27017,
44
+ document_class: dict[str, Any] | None = None,
45
+ tz_aware: bool | None = None,
46
+ connect: bool | None = None,
47
+ type_registry=None, # type: bson.codec_options.TypeRegistry
48
+ database: str | None = None,
49
+ **kwargs: Any,
50
+ ) -> None:
51
+ try:
52
+ from motor.motor_asyncio import AsyncIOMotorClient
53
+ except ModuleNotFoundError as e:
54
+ raise import_error(e, package='motor')
55
+
56
+ with contextlib.suppress(ImportError):
57
+ import uvloop
58
+ asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
59
+
60
+ self._client: AsyncIOMotorClient = AsyncIOMotorClient(
61
+ host=host,
62
+ port=port,
63
+ document_class=document_class,
64
+ tz_aware=tz_aware,
65
+ connect=connect,
66
+ type_registry=type_registry,
67
+ **kwargs,
68
+ )
69
+ self._database: Database = self._client.get_database(name=database)
70
+
71
+ @property
72
+ def session(self):
73
+ return self._database
74
+
75
+
76
+ class PantherDBConnection(BaseDatabaseConnection):
77
+ def init(self, path: str | None = None, encryption: bool = False):
78
+ params = {'db_name': str(path), 'return_dict': True, 'return_cursor': True}
79
+ if encryption:
80
+ try:
81
+ import cryptography
82
+ except ImportError as e:
83
+ raise import_error(e, package='cryptography')
84
+ params['secret_key'] = config.SECRET_KEY
85
+
86
+ self._connection: PantherDB = PantherDB(**params)
87
+
88
+ @property
89
+ def session(self):
90
+ return self._connection
91
+
92
+
93
+ class DatabaseConnection(Singleton):
94
+ @property
95
+ def session(self):
96
+ return config.DATABASE.session
97
+
98
+
99
+ class RedisConnection(Singleton, _Redis):
100
+ is_connected: bool = False
101
+
102
+ def __init__(
103
+ self,
104
+ init: bool = False,
105
+ host: str = 'localhost',
106
+ port: int = 6379,
107
+ db: int = 0,
108
+ websocket_db: int = 0,
109
+ **kwargs
110
+ ):
111
+ if init:
112
+ self.host = host
113
+ self.port = port
114
+ self.db = db
115
+ self.websocket_db = websocket_db
116
+ self.kwargs = kwargs
117
+
118
+ super().__init__(host=host, port=port, db=db, **kwargs)
119
+ self.is_connected = True
120
+ self.sync_ping()
121
+
122
+ def sync_ping(self):
123
+ from redis import Redis
124
+
125
+ Redis(host=self.host, port=self.port, **self.kwargs).ping()
126
+
127
+ def create_connection_for_websocket(self) -> _Redis:
128
+ if not hasattr(self, 'websocket_connection'):
129
+ self.websocket_connection = _Redis(
130
+ host=self.host,
131
+ port=self.port,
132
+ db=self.websocket_db,
133
+ **self.kwargs
134
+ )
135
+ return self.websocket_connection
136
+
137
+
138
+ db: DatabaseConnection = DatabaseConnection()
139
+ redis: RedisConnection = RedisConnection()
panther/db/cursor.py ADDED
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from sys import version_info
4
+
5
+ try:
6
+ from pymongo.cursor import Cursor as _Cursor
7
+ except ImportError:
8
+ # This '_Cursor' is not going to be used,
9
+ # If user really wants to use it,
10
+ # we are going to force him to install it in `panther.db.connections.MongoDBConnection.init`
11
+ _Cursor = type('_Cursor', (), {})
12
+
13
+ if version_info >= (3, 11):
14
+ from typing import Self
15
+ else:
16
+ from typing import TypeVar
17
+
18
+ Self = TypeVar('Self', bound='BaseMongoDBQuery')
19
+
20
+
21
+ class Cursor(_Cursor):
22
+ models = {}
23
+
24
+ def __init__(self, collection, *args, cls=None, **kwargs):
25
+ # cls.__name__ and collection.name are equal.
26
+ if cls:
27
+ self.models[collection.name] = cls
28
+ self.cls = cls
29
+ self.filter = kwargs['filter']
30
+ else:
31
+ self.cls = self.models[collection.name]
32
+ super().__init__(collection, *args, **kwargs)
33
+
34
+ def next(self) -> Self:
35
+ return self.cls._create_model_instance(document=super().next())
36
+
37
+ __next__ = next
38
+
39
+ def __getitem__(self, index: int | slice) -> Cursor[Self] | Self:
40
+ result = super().__getitem__(index)
41
+ if isinstance(result, dict):
42
+ return self.cls._create_model_instance(document=result)
43
+ return result
panther/db/models.py CHANGED
@@ -1,49 +1,84 @@
1
+ import contextlib
2
+ import os
1
3
  from datetime import datetime
4
+ from typing import Annotated
2
5
 
3
- import bson
4
- from pydantic import BaseModel as PydanticBaseModel
5
- from pydantic import Field, field_validator
6
+ from pydantic import Field, WrapValidator, PlainSerializer, BaseModel as PydanticBaseModel
6
7
 
7
8
  from panther.configs import config
8
9
  from panther.db.queries import Query
10
+ from panther.utils import scrypt, URANDOM_SIZE, timezone_now
9
11
 
12
+ with contextlib.suppress(ImportError):
13
+ # Only required if user wants to use mongodb
14
+ import bson
10
15
 
11
- class Model(PydanticBaseModel, Query):
12
- id: str | None = Field(None, validation_alias='_id')
13
-
14
- @field_validator('id', mode='before')
15
- def validate_id(cls, value: str | int | bson.ObjectId) -> str:
16
- if isinstance(value, int):
17
- pass
18
16
 
19
- elif isinstance(value, str):
17
+ def validate_object_id(value, handler):
18
+ if config.DATABASE.__class__.__name__ == 'MongoDBConnection':
19
+ if isinstance(value, bson.ObjectId):
20
+ return value
21
+ else:
20
22
  try:
21
- bson.ObjectId(value)
23
+ return bson.ObjectId(value)
22
24
  except bson.objectid.InvalidId as e:
23
25
  msg = 'Invalid ObjectId'
24
26
  raise ValueError(msg) from e
27
+ return str(value)
25
28
 
26
- elif not isinstance(value, bson.ObjectId):
27
- msg = 'ObjectId required'
28
- raise ValueError(msg) from None
29
29
 
30
- return str(value)
30
+ ID = Annotated[str, WrapValidator(validate_object_id), PlainSerializer(lambda x: str(x), return_type=str)]
31
31
 
32
- @property
33
- def _id(self) -> int | bson.ObjectId | None:
34
- if config['query_engine'].__name__ == 'BasePantherDBQuery':
35
- return int(self.id)
36
- else:
37
- return bson.ObjectId(self.id) if self.id else None
38
32
 
39
- def dict(self, *args, **kwargs) -> dict:
40
- return self.model_dump(*args, **kwargs)
33
+ class Model(PydanticBaseModel, Query):
34
+ def __init_subclass__(cls, **kwargs):
35
+ if cls.__module__ == 'panther.db.models' and cls.__name__ == 'BaseUser':
36
+ return
37
+ config.MODELS.append(cls)
38
+
39
+ id: ID | None = Field(None, validation_alias='_id')
40
+
41
+ @property
42
+ def _id(self):
43
+ """
44
+ return
45
+ `str` for PantherDB
46
+ `ObjectId` for MongoDB
47
+ """
48
+ return self.id
41
49
 
42
50
 
43
51
  class BaseUser(Model):
44
- first_name: str = Field('', max_length=64)
45
- last_name: str = Field('', max_length=64)
46
- last_login: datetime = None
52
+ password: str = Field('', max_length=64)
53
+ last_login: datetime | None = None
54
+ date_created: datetime | None = Field(default_factory=timezone_now)
55
+
56
+ async def update_last_login(self) -> None:
57
+ await self.update(last_login=timezone_now())
58
+
59
+ async def login(self) -> dict:
60
+ """Return dict of access and refresh token"""
61
+ return config.AUTHENTICATION.login(self.id)
62
+
63
+ async def logout(self) -> dict:
64
+ return await config.AUTHENTICATION.logout(self._auth_token)
65
+
66
+ def set_password(self, password: str):
67
+ """
68
+ URANDOM_SIZE = 16 char -->
69
+ salt = 16 bytes
70
+ salt.hex() = 32 char
71
+ derived_key = 32 char
72
+ """
73
+ salt = os.urandom(URANDOM_SIZE)
74
+ derived_key = scrypt(password=password, salt=salt, digest=True)
75
+
76
+ self.password = f'{salt.hex()}{derived_key}'
77
+
78
+ def check_password(self, new_password: str) -> bool:
79
+ size = URANDOM_SIZE * 2
80
+ salt = self.password[:size]
81
+ stored_hash = self.password[size:]
82
+ derived_key = scrypt(password=new_password, salt=bytes.fromhex(salt), digest=True)
47
83
 
48
- def update_last_login(self) -> None:
49
- self.update(last_login=datetime.now())
84
+ return derived_key == stored_hash
@@ -1 +1 @@
1
- from panther.db.queries.queries import * # noqa: F403
1
+ from panther.db.queries.queries import Query
@@ -0,0 +1,127 @@
1
+ import operator
2
+ from abc import abstractmethod
3
+ from functools import reduce
4
+ from sys import version_info
5
+ from typing import Iterator
6
+
7
+ from pydantic_core._pydantic_core import ValidationError
8
+
9
+ from panther.db.cursor import Cursor
10
+ from panther.db.utils import prepare_id_for_query
11
+ from panther.exceptions import DatabaseError
12
+
13
+ if version_info >= (3, 11):
14
+ from typing import Self
15
+ else:
16
+ from typing import TypeVar
17
+
18
+ Self = TypeVar('Self', bound='BaseQuery')
19
+
20
+
21
+ class BaseQuery:
22
+ @classmethod
23
+ def _merge(cls, *args, is_mongo: bool = False) -> dict:
24
+ prepare_id_for_query(*args, is_mongo=is_mongo)
25
+ return reduce(operator.ior, filter(None, args), {})
26
+
27
+ @classmethod
28
+ def _clean_error_message(cls, validation_error: ValidationError, is_updating: bool = False) -> str:
29
+ error = ', '.join(
30
+ '{field}="{error}"'.format(
31
+ field='.'.join(loc for loc in e['loc']),
32
+ error=e['msg']
33
+ )
34
+ for e in validation_error.errors()
35
+ if not is_updating or e['type'] != 'missing'
36
+ )
37
+ return f'{cls.__name__}({error})' if error else ''
38
+
39
+ @classmethod
40
+ def _validate_data(cls, *, data: dict, is_updating: bool = False):
41
+ """Validate document before inserting to collection"""
42
+ try:
43
+ cls(**data)
44
+ except ValidationError as validation_error:
45
+ if error := cls._clean_error_message(validation_error=validation_error, is_updating=is_updating):
46
+ raise DatabaseError(error)
47
+
48
+ @classmethod
49
+ def _create_model_instance(cls, document: dict):
50
+ """Prevent getting errors from document insertion"""
51
+ try:
52
+ return cls(**document)
53
+ except ValidationError as validation_error:
54
+ if error := cls._clean_error_message(validation_error=validation_error):
55
+ raise DatabaseError(error)
56
+
57
+ @classmethod
58
+ @abstractmethod
59
+ async def find_one(cls, *args, **kwargs) -> Self | None:
60
+ raise NotImplementedError
61
+
62
+ @classmethod
63
+ @abstractmethod
64
+ async def find(cls, *args, **kwargs) -> list[Self] | Cursor:
65
+ raise NotImplementedError
66
+
67
+ @classmethod
68
+ @abstractmethod
69
+ async def first(cls, *args, **kwargs) -> Self | None:
70
+ raise NotImplementedError
71
+
72
+ @classmethod
73
+ @abstractmethod
74
+ async def last(cls, *args, **kwargs):
75
+ raise NotImplementedError
76
+
77
+ @classmethod
78
+ @abstractmethod
79
+ async def aggregate(cls, *args, **kwargs) -> Iterator[dict]:
80
+ raise NotImplementedError
81
+
82
+ # # # # # Count # # # # #
83
+ @classmethod
84
+ @abstractmethod
85
+ async def count(cls, *args, **kwargs) -> int:
86
+ raise NotImplementedError
87
+
88
+ # # # # # Insert # # # # #
89
+ @classmethod
90
+ @abstractmethod
91
+ async def insert_one(cls, *args, **kwargs) -> Self:
92
+ raise NotImplementedError
93
+
94
+ @classmethod
95
+ @abstractmethod
96
+ async def insert_many(cls, *args, **kwargs) -> list[Self]:
97
+ raise NotImplementedError
98
+
99
+ # # # # # Delete # # # # #
100
+ @abstractmethod
101
+ async def delete(self) -> None:
102
+ raise NotImplementedError
103
+
104
+ @classmethod
105
+ @abstractmethod
106
+ async def delete_one(cls, *args, **kwargs) -> bool:
107
+ raise NotImplementedError
108
+
109
+ @classmethod
110
+ @abstractmethod
111
+ async def delete_many(cls, *args, **kwargs) -> int:
112
+ raise NotImplementedError
113
+
114
+ # # # # # Update # # # # #
115
+ @abstractmethod
116
+ async def update(self, *args, **kwargs) -> None:
117
+ raise NotImplementedError
118
+
119
+ @classmethod
120
+ @abstractmethod
121
+ async def update_one(cls, *args, **kwargs) -> bool:
122
+ raise NotImplementedError
123
+
124
+ @classmethod
125
+ @abstractmethod
126
+ async def update_many(cls, *args, **kwargs) -> int:
127
+ raise NotImplementedError
@@ -1,8 +1,20 @@
1
+ from __future__ import annotations
2
+
1
3
  from sys import version_info
4
+ from typing import Iterable, Sequence
5
+
6
+ from panther.db.connections import db
7
+ from panther.db.cursor import Cursor
8
+ from panther.db.queries.base_queries import BaseQuery
9
+ from panther.db.utils import prepare_id_for_query
2
10
 
3
- from panther.db.connection import db
4
- from panther.db.utils import merge_dicts, prepare_id_for_query
5
- from panther.exceptions import DBException
11
+ try:
12
+ from bson.codec_options import CodecOptions
13
+ except ImportError:
14
+ # This 'CodecOptions' is not going to be used,
15
+ # If user really wants to use it,
16
+ # we are going to force him to install it in `panther.db.connections.MongoDBConnection.init`
17
+ CodecOptions = type('CodecOptions', (), {})
6
18
 
7
19
  if version_info >= (3, 11):
8
20
  from typing import Self
@@ -12,78 +24,105 @@ else:
12
24
  Self = TypeVar('Self', bound='BaseMongoDBQuery')
13
25
 
14
26
 
15
- class BaseMongoDBQuery:
27
+ class BaseMongoDBQuery(BaseQuery):
16
28
  @classmethod
17
- def _merge(cls, *args) -> dict:
18
- prepare_id_for_query(*args, is_mongo=True)
19
- return merge_dicts(*args)
29
+ def _merge(cls, *args, is_mongo: bool = True) -> dict:
30
+ return super()._merge(*args, is_mongo=is_mongo)
31
+
32
+ # TODO: https://jira.mongodb.org/browse/PYTHON-4192
33
+ # @classmethod
34
+ # def collection(cls):
35
+ # return db.session.get_collection(name=cls.__name__, codec_options=CodecOptions(document_class=cls))
20
36
 
21
37
  # # # # # Find # # # # #
22
38
  @classmethod
23
- def find_one(cls, _data: dict | None = None, /, **kwargs) -> Self | None:
24
- if document := db.session[cls.__name__].find_one(cls._merge(_data, kwargs)):
39
+ async def find_one(cls, _filter: dict | None = None, /, **kwargs) -> Self | None:
40
+ if document := await db.session[cls.__name__].find_one(cls._merge(_filter, kwargs)):
25
41
  return cls._create_model_instance(document=document)
26
42
  return None
27
43
 
28
44
  @classmethod
29
- def find(cls, _data: dict | None = None, /, **kwargs) -> list[Self]:
30
- documents = db.session[cls.__name__].find(cls._merge(_data, kwargs))
31
- return [cls._create_model_instance(document=document) for document in documents]
45
+ async def find(cls, _filter: dict | None = None, /, **kwargs) -> Cursor:
46
+ return Cursor(cls=cls, collection=db.session[cls.__name__].delegate, filter=cls._merge(_filter, kwargs))
47
+
48
+ @classmethod
49
+ async def first(cls, _filter: dict | None = None, /, **kwargs) -> Self | None:
50
+ cursor = await cls.find(_filter, **kwargs)
51
+ for result in cursor.sort('_id', 1).limit(-1):
52
+ return result
53
+ return None
32
54
 
33
55
  @classmethod
34
- def first(cls, _data: dict | None = None, /, **kwargs) -> Self | None:
35
- return cls.find_one(_data, **kwargs)
56
+ async def last(cls, _filter: dict | None = None, /, **kwargs) -> Self | None:
57
+ cursor = await cls.find(_filter, **kwargs)
58
+ for result in cursor.sort('_id', -1).limit(-1):
59
+ return result
60
+ return None
36
61
 
37
62
  @classmethod
38
- def last(cls, _data: dict | None = None, /, **kwargs):
39
- msg = 'last() is not supported in MongoDB yet.'
40
- raise DBException(msg)
63
+ async def aggregate(cls, pipeline: Sequence[dict]) -> Iterable[dict]:
64
+ return await db.session[cls.__name__].aggregate(pipeline)
41
65
 
42
66
  # # # # # Count # # # # #
43
67
  @classmethod
44
- def count(cls, _data: dict | None = None, /, **kwargs) -> int:
45
- return db.session[cls.__name__].count_documents(cls._merge(_data, kwargs))
68
+ async def count(cls, _filter: dict | None = None, /, **kwargs) -> int:
69
+ return await db.session[cls.__name__].count_documents(cls._merge(_filter, kwargs))
46
70
 
47
71
  # # # # # Insert # # # # #
48
72
  @classmethod
49
- def insert_one(cls, _data: dict | None = None, **kwargs) -> Self:
50
- document = cls._merge(_data, kwargs)
51
- document['id'] = db.session[cls.__name__].insert_one(document).inserted_id
73
+ async def insert_one(cls, _document: dict | None = None, /, **kwargs) -> Self:
74
+ document = cls._merge(_document, kwargs)
75
+ cls._validate_data(data=document)
76
+
77
+ await db.session[cls.__name__].insert_one(document)
52
78
  return cls._create_model_instance(document=document)
53
79
 
80
+ @classmethod
81
+ async def insert_many(cls, documents: Iterable[dict]) -> list[Self]:
82
+ for document in documents:
83
+ prepare_id_for_query(document, is_mongo=True)
84
+ cls._validate_data(data=document)
85
+
86
+ await db.session[cls.__name__].insert_many(documents)
87
+ return [cls._create_model_instance(document=document) for document in documents]
88
+
54
89
  # # # # # Delete # # # # #
55
- def delete(self) -> None:
56
- db.session[self.__class__.__name__].delete_one({'_id': self._id})
90
+ async def delete(self) -> None:
91
+ await db.session[self.__class__.__name__].delete_one({'_id': self._id})
57
92
 
58
93
  @classmethod
59
- def delete_one(cls, _data: dict | None = None, /, **kwargs) -> bool:
60
- result = db.session[cls.__name__].delete_one(cls._merge(_data, kwargs))
94
+ async def delete_one(cls, _filter: dict | None = None, /, **kwargs) -> bool:
95
+ result = await db.session[cls.__name__].delete_one(cls._merge(_filter, kwargs))
61
96
  return bool(result.deleted_count)
62
97
 
63
98
  @classmethod
64
- def delete_many(cls, _data: dict | None = None, /, **kwargs) -> int:
65
- result = db.session[cls.__name__].delete_many(cls._merge(_data, kwargs))
99
+ async def delete_many(cls, _filter: dict | None = None, /, **kwargs) -> int:
100
+ result = await db.session[cls.__name__].delete_many(cls._merge(_filter, kwargs))
66
101
  return result.deleted_count
67
102
 
68
103
  # # # # # Update # # # # #
69
- def update(self, **kwargs) -> None:
70
- for field, value in kwargs.items():
104
+ async def update(self, _update: dict | None = None, /, **kwargs) -> None:
105
+ document = self._merge(_update, kwargs)
106
+ document.pop('_id', None)
107
+ self._validate_data(data=document, is_updating=True)
108
+
109
+ for field, value in document.items():
71
110
  setattr(self, field, value)
72
- update_fields = {'$set': kwargs}
73
- db.session[self.__class__.__name__].update_one({'_id': self._id}, update_fields)
111
+ update_fields = {'$set': document}
112
+ await db.session[self.__class__.__name__].update_one({'_id': self._id}, update_fields)
74
113
 
75
114
  @classmethod
76
- def update_one(cls, _filter: dict, _data: dict | None = None, /, **kwargs) -> bool:
115
+ async def update_one(cls, _filter: dict, _update: dict | None = None, /, **kwargs) -> bool:
77
116
  prepare_id_for_query(_filter, is_mongo=True)
78
- update_fields = {'$set': cls._merge(_data, kwargs)}
117
+ update_fields = {'$set': cls._merge(_update, kwargs)}
79
118
 
80
- result = db.session[cls.__name__].update_one(_filter, update_fields)
119
+ result = await db.session[cls.__name__].update_one(_filter, update_fields)
81
120
  return bool(result.matched_count)
82
121
 
83
122
  @classmethod
84
- def update_many(cls, _filter: dict, _data: dict | None = None, /, **kwargs) -> int:
123
+ async def update_many(cls, _filter: dict, _update: dict | None = None, /, **kwargs) -> int:
85
124
  prepare_id_for_query(_filter, is_mongo=True)
86
- update_fields = {'$set': cls._merge(_data, kwargs)}
125
+ update_fields = {'$set': cls._merge(_update, kwargs)}
87
126
 
88
- result = db.session[cls.__name__].update_many(_filter, update_fields)
127
+ result = await db.session[cls.__name__].update_many(_filter, update_fields)
89
128
  return result.modified_count