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.
- panther/__init__.py +1 -1
- panther/_load_configs.py +168 -171
- panther/_utils.py +26 -49
- panther/app.py +85 -105
- panther/authentications.py +86 -55
- panther/background_tasks.py +25 -14
- panther/base_request.py +38 -14
- panther/base_websocket.py +172 -94
- panther/caching.py +60 -25
- panther/cli/create_command.py +20 -10
- panther/cli/monitor_command.py +63 -37
- panther/cli/template.py +40 -20
- panther/cli/utils.py +32 -18
- panther/configs.py +65 -58
- panther/db/connections.py +139 -0
- panther/db/cursor.py +43 -0
- panther/db/models.py +64 -29
- panther/db/queries/__init__.py +1 -1
- panther/db/queries/base_queries.py +127 -0
- panther/db/queries/mongodb_queries.py +77 -38
- panther/db/queries/pantherdb_queries.py +59 -30
- panther/db/queries/queries.py +232 -117
- panther/db/utils.py +17 -18
- panther/events.py +44 -0
- panther/exceptions.py +26 -12
- panther/file_handler.py +2 -2
- panther/generics.py +163 -0
- panther/logging.py +7 -2
- panther/main.py +111 -188
- panther/middlewares/base.py +3 -0
- panther/monitoring.py +8 -5
- panther/pagination.py +48 -0
- panther/panel/apis.py +32 -5
- panther/panel/urls.py +2 -1
- panther/permissions.py +3 -3
- panther/request.py +6 -13
- panther/response.py +114 -34
- panther/routings.py +83 -66
- panther/serializer.py +214 -33
- panther/test.py +31 -21
- panther/utils.py +28 -16
- panther/websocket.py +7 -4
- {panther-3.8.2.dist-info → panther-4.0.0.dist-info}/METADATA +93 -71
- panther-4.0.0.dist-info/RECORD +57 -0
- {panther-3.8.2.dist-info → panther-4.0.0.dist-info}/WHEEL +1 -1
- panther/db/connection.py +0 -92
- panther/middlewares/db.py +0 -18
- panther/middlewares/redis.py +0 -47
- panther-3.8.2.dist-info/RECORD +0 -54
- {panther-3.8.2.dist-info → panther-4.0.0.dist-info}/LICENSE +0 -0
- {panther-3.8.2.dist-info → panther-4.0.0.dist-info}/entry_points.txt +0 -0
- {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
|
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
|
-
|
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
|
-
|
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
|
-
|
40
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
49
|
-
self.update(last_login=datetime.now())
|
84
|
+
return derived_key == stored_hash
|
panther/db/queries/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
from panther.db.queries.queries import
|
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
|
-
|
4
|
-
from
|
5
|
-
|
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
|
-
|
19
|
-
|
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,
|
24
|
-
if document := db.session[cls.__name__].find_one(cls._merge(
|
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,
|
30
|
-
|
31
|
-
|
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
|
35
|
-
|
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
|
39
|
-
|
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,
|
45
|
-
return db.session[cls.__name__].count_documents(cls._merge(
|
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,
|
50
|
-
document = cls._merge(
|
51
|
-
|
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,
|
60
|
-
result = db.session[cls.__name__].delete_one(cls._merge(
|
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,
|
65
|
-
result = db.session[cls.__name__].delete_many(cls._merge(
|
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
|
-
|
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':
|
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,
|
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(
|
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,
|
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(
|
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
|