panther 5.0.0b3__py3-none-any.whl → 5.0.0b4__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 +46 -37
- panther/_utils.py +49 -34
- panther/app.py +96 -97
- panther/authentications.py +97 -50
- panther/background_tasks.py +98 -124
- panther/base_request.py +16 -10
- panther/base_websocket.py +8 -8
- panther/caching.py +16 -80
- panther/cli/create_command.py +17 -16
- panther/cli/main.py +1 -1
- panther/cli/monitor_command.py +11 -6
- panther/cli/run_command.py +5 -71
- panther/cli/template.py +7 -7
- panther/cli/utils.py +58 -69
- panther/configs.py +70 -72
- panther/db/connections.py +18 -24
- panther/db/cursor.py +0 -1
- panther/db/models.py +24 -8
- panther/db/queries/base_queries.py +2 -5
- panther/db/queries/mongodb_queries.py +17 -20
- panther/db/queries/pantherdb_queries.py +1 -1
- panther/db/queries/queries.py +26 -8
- panther/db/utils.py +1 -1
- panther/events.py +25 -14
- panther/exceptions.py +2 -7
- panther/file_handler.py +1 -1
- panther/generics.py +11 -8
- panther/logging.py +2 -1
- panther/main.py +12 -13
- panther/middlewares/cors.py +67 -0
- panther/middlewares/monitoring.py +5 -3
- panther/openapi/urls.py +2 -2
- panther/openapi/utils.py +3 -3
- panther/openapi/views.py +20 -37
- panther/pagination.py +4 -2
- panther/panel/apis.py +2 -7
- panther/panel/urls.py +2 -6
- panther/panel/utils.py +9 -5
- panther/panel/views.py +13 -22
- panther/permissions.py +2 -1
- panther/request.py +2 -1
- panther/response.py +53 -47
- panther/routings.py +12 -12
- panther/serializer.py +19 -20
- panther/test.py +73 -58
- panther/throttling.py +68 -3
- panther/utils.py +5 -11
- {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/METADATA +1 -1
- panther-5.0.0b4.dist-info/RECORD +75 -0
- panther/monitoring.py +0 -34
- panther-5.0.0b3.dist-info/RECORD +0 -75
- {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/WHEEL +0 -0
- {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/entry_points.txt +0 -0
- {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/licenses/LICENSE +0 -0
- {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/top_level.txt +0 -0
panther/configs.py
CHANGED
@@ -1,25 +1,24 @@
|
|
1
|
-
import copy
|
2
1
|
import typing
|
3
|
-
from
|
2
|
+
from collections.abc import Callable
|
3
|
+
from dataclasses import dataclass, field
|
4
4
|
from datetime import timedelta
|
5
5
|
from pathlib import Path
|
6
|
-
from typing import Callable
|
7
6
|
|
8
7
|
import jinja2
|
9
8
|
from pydantic import BaseModel as PydanticBaseModel
|
10
|
-
|
9
|
+
|
11
10
|
|
12
11
|
class JWTConfig:
|
13
12
|
def __init__(
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
13
|
+
self,
|
14
|
+
key: str,
|
15
|
+
algorithm: str = 'HS256',
|
16
|
+
life_time: timedelta | int = timedelta(days=1),
|
17
|
+
refresh_life_time: timedelta | int | None = None,
|
19
18
|
):
|
20
19
|
self.key = key
|
21
20
|
self.algorithm = algorithm
|
22
|
-
self.life_time = life_time.total_seconds() if isinstance(life_time, timedelta) else life_time
|
21
|
+
self.life_time = int(life_time.total_seconds()) if isinstance(life_time, timedelta) else life_time
|
23
22
|
|
24
23
|
if refresh_life_time:
|
25
24
|
if isinstance(refresh_life_time, timedelta):
|
@@ -29,6 +28,14 @@ class JWTConfig:
|
|
29
28
|
else:
|
30
29
|
self.refresh_life_time = self.life_time * 2
|
31
30
|
|
31
|
+
def __eq__(self, other):
|
32
|
+
return bool(
|
33
|
+
self.key == other.key
|
34
|
+
and self.algorithm == other.algorithm
|
35
|
+
and self.life_time == other.life_time
|
36
|
+
and self.refresh_life_time == other.refresh_life_time,
|
37
|
+
)
|
38
|
+
|
32
39
|
|
33
40
|
class QueryObservable:
|
34
41
|
observers = []
|
@@ -45,77 +52,68 @@ class QueryObservable:
|
|
45
52
|
|
46
53
|
@dataclass
|
47
54
|
class Config:
|
48
|
-
BASE_DIR: Path
|
49
|
-
MONITORING: bool
|
50
|
-
LOG_QUERIES: bool
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
55
|
+
BASE_DIR: Path = Path()
|
56
|
+
MONITORING: bool = False
|
57
|
+
LOG_QUERIES: bool = False
|
58
|
+
THROTTLING = None # type: panther.throttling.Throttle
|
59
|
+
SECRET_KEY: str | None = None
|
60
|
+
HTTP_MIDDLEWARES: list = field(default_factory=list)
|
61
|
+
WS_MIDDLEWARES: list = field(default_factory=list)
|
62
|
+
USER_MODEL: type[PydanticBaseModel] | None = None
|
63
|
+
AUTHENTICATION: type[PydanticBaseModel] | None = None
|
64
|
+
WS_AUTHENTICATION: type[PydanticBaseModel] | None = None
|
65
|
+
JWT_CONFIG: JWTConfig | None = None
|
66
|
+
MODELS: list = field(default_factory=list)
|
67
|
+
FLAT_URLS: dict = field(default_factory=dict)
|
68
|
+
URLS: dict = field(default_factory=dict)
|
69
|
+
WEBSOCKET_CONNECTIONS: Callable | None = None
|
70
|
+
BACKGROUND_TASKS: bool = False
|
71
|
+
HAS_WS: bool = False
|
72
|
+
TIMEZONE: str = 'UTC'
|
73
|
+
TEMPLATES_DIR: str | list[str] = '.'
|
74
|
+
JINJA_ENVIRONMENT: jinja2.Environment | None = None
|
75
|
+
AUTO_REFORMAT: bool = False
|
76
|
+
QUERY_ENGINE: Callable | None = None
|
77
|
+
DATABASE: Callable | None = None
|
78
|
+
|
79
|
+
def refresh(self):
|
80
|
+
"""
|
81
|
+
Reset built-in fields and remove any custom (non-built-in) attributes.
|
82
|
+
* In some tests we need to `refresh` the `config` values
|
83
|
+
"""
|
84
|
+
builtin_fields = set(self.__dataclass_fields__)
|
85
|
+
current_fields = set(self.__dict__)
|
86
|
+
|
87
|
+
# Reset built-in fields
|
88
|
+
for field_name in builtin_fields:
|
89
|
+
field_def = self.__dataclass_fields__[field_name]
|
90
|
+
default = field_def.default_factory() if callable(field_def.default_factory) else field_def.default
|
91
|
+
setattr(self, field_name, default)
|
92
|
+
|
93
|
+
# Delete custom attributes
|
94
|
+
for field_name in current_fields - builtin_fields:
|
95
|
+
delattr(self, field_name)
|
96
|
+
|
97
|
+
def vars(self) -> dict[str, typing.Any]:
|
98
|
+
"""Return all config variables (built-in + custom)."""
|
99
|
+
return dict(self.__dict__)
|
74
100
|
|
75
101
|
def __setattr__(self, key, value):
|
76
102
|
super().__setattr__(key, value)
|
77
103
|
if key == 'QUERY_ENGINE' and value:
|
78
104
|
QueryObservable.update()
|
79
105
|
|
106
|
+
def __getattr__(self, item: str):
|
107
|
+
try:
|
108
|
+
return object.__getattribute__(self, item)
|
109
|
+
except AttributeError:
|
110
|
+
return None
|
111
|
+
|
80
112
|
def __setitem__(self, key, value):
|
81
113
|
setattr(self, key.upper(), value)
|
82
114
|
|
83
115
|
def __getitem__(self, item):
|
84
116
|
return getattr(self, item.upper())
|
85
117
|
|
86
|
-
|
87
|
-
|
88
|
-
for key, value in copy.deepcopy(default_configs).items():
|
89
|
-
setattr(self, key, value)
|
90
|
-
|
91
|
-
|
92
|
-
default_configs = {
|
93
|
-
'BASE_DIR': Path(),
|
94
|
-
'MONITORING': False,
|
95
|
-
'LOG_QUERIES': False,
|
96
|
-
'DEFAULT_CACHE_EXP': None,
|
97
|
-
'THROTTLING': None,
|
98
|
-
'SECRET_KEY': None,
|
99
|
-
'HTTP_MIDDLEWARES': [],
|
100
|
-
'WS_MIDDLEWARES': [],
|
101
|
-
'USER_MODEL': None,
|
102
|
-
'AUTHENTICATION': None,
|
103
|
-
'WS_AUTHENTICATION': None,
|
104
|
-
'JWT_CONFIG': None,
|
105
|
-
'MODELS': [],
|
106
|
-
'FLAT_URLS': {},
|
107
|
-
'URLS': {},
|
108
|
-
'WEBSOCKET_CONNECTIONS': None,
|
109
|
-
'BACKGROUND_TASKS': False,
|
110
|
-
'HAS_WS': False,
|
111
|
-
'STARTUPS': [],
|
112
|
-
'SHUTDOWNS': [],
|
113
|
-
'TIMEZONE': 'UTC',
|
114
|
-
'TEMPLATES_DIR': '.',
|
115
|
-
'JINJA_ENVIRONMENT': None,
|
116
|
-
'AUTO_REFORMAT': False,
|
117
|
-
'QUERY_ENGINE': None,
|
118
|
-
'DATABASE': None,
|
119
|
-
}
|
120
|
-
|
121
|
-
config = Config(**copy.deepcopy(default_configs))
|
118
|
+
|
119
|
+
config = Config()
|
panther/db/connections.py
CHANGED
@@ -38,15 +38,15 @@ class BaseDatabaseConnection:
|
|
38
38
|
|
39
39
|
class MongoDBConnection(BaseDatabaseConnection):
|
40
40
|
def init(
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
50
|
) -> None:
|
51
51
|
try:
|
52
52
|
from motor.motor_asyncio import AsyncIOMotorClient
|
@@ -55,6 +55,7 @@ class MongoDBConnection(BaseDatabaseConnection):
|
|
55
55
|
|
56
56
|
with contextlib.suppress(ImportError):
|
57
57
|
import uvloop
|
58
|
+
|
58
59
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
59
60
|
|
60
61
|
self._client: AsyncIOMotorClient = AsyncIOMotorClient(
|
@@ -81,7 +82,7 @@ class PantherDBConnection(BaseDatabaseConnection):
|
|
81
82
|
import cryptography
|
82
83
|
except ImportError as e:
|
83
84
|
raise import_error(e, package='cryptography')
|
84
|
-
params['secret_key'] = config.SECRET_KEY
|
85
|
+
params['secret_key'] = config.SECRET_KEY.encode()
|
85
86
|
|
86
87
|
self._connection: PantherDB = PantherDB(**params)
|
87
88
|
|
@@ -100,19 +101,17 @@ class RedisConnection(Singleton, _Redis):
|
|
100
101
|
is_connected: bool = False
|
101
102
|
|
102
103
|
def __init__(
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
**kwargs
|
104
|
+
self,
|
105
|
+
init: bool = False,
|
106
|
+
host: str = 'localhost',
|
107
|
+
port: int = 6379,
|
108
|
+
db: int = 0,
|
109
|
+
**kwargs,
|
110
110
|
):
|
111
111
|
if init:
|
112
112
|
self.host = host
|
113
113
|
self.port = port
|
114
114
|
self.db = db
|
115
|
-
self.websocket_db = websocket_db
|
116
115
|
self.kwargs = kwargs
|
117
116
|
|
118
117
|
super().__init__(host=host, port=port, db=db, **kwargs)
|
@@ -132,12 +131,7 @@ class RedisConnection(Singleton, _Redis):
|
|
132
131
|
|
133
132
|
def create_connection_for_websocket(self) -> _Redis:
|
134
133
|
if not hasattr(self, 'websocket_connection'):
|
135
|
-
self.websocket_connection = _Redis(
|
136
|
-
host=self.host,
|
137
|
-
port=self.port,
|
138
|
-
db=self.websocket_db,
|
139
|
-
**self.kwargs
|
140
|
-
)
|
134
|
+
self.websocket_connection = _Redis(host=self.host, port=self.port, db=0, **self.kwargs)
|
141
135
|
return self.websocket_connection
|
142
136
|
|
143
137
|
|
panther/db/cursor.py
CHANGED
panther/db/models.py
CHANGED
@@ -1,18 +1,27 @@
|
|
1
1
|
import contextlib
|
2
2
|
import os
|
3
|
+
import sys
|
3
4
|
from datetime import datetime
|
4
5
|
from typing import Annotated, ClassVar
|
5
6
|
|
6
|
-
from pydantic import
|
7
|
+
from pydantic import BaseModel as PydanticBaseModel
|
8
|
+
from pydantic import Field, PlainSerializer, WrapValidator
|
7
9
|
|
8
10
|
from panther.configs import config
|
9
11
|
from panther.db.queries import Query
|
10
|
-
from panther.utils import
|
12
|
+
from panther.utils import URANDOM_SIZE, scrypt, timezone_now
|
11
13
|
|
12
14
|
with contextlib.suppress(ImportError):
|
13
15
|
# Only required if user wants to use mongodb
|
14
16
|
import bson
|
15
17
|
|
18
|
+
if sys.version_info >= (3, 11):
|
19
|
+
from typing import Self
|
20
|
+
else:
|
21
|
+
from typing import TypeVar
|
22
|
+
|
23
|
+
Self = TypeVar('Self', bound='BaseUser')
|
24
|
+
|
16
25
|
|
17
26
|
def validate_object_id(value, handler):
|
18
27
|
if config.DATABASE.__class__.__name__ != 'MongoDBConnection':
|
@@ -55,19 +64,26 @@ class BaseUser(Model):
|
|
55
64
|
username: str
|
56
65
|
password: str = Field('', max_length=64)
|
57
66
|
last_login: datetime | None = None
|
58
|
-
date_created: datetime | None =
|
67
|
+
date_created: datetime | None = None
|
59
68
|
|
60
69
|
USERNAME_FIELD: ClassVar = 'username'
|
61
70
|
|
62
|
-
|
63
|
-
|
71
|
+
@classmethod
|
72
|
+
def insert_one(cls, _document: dict | None = None, /, **kwargs) -> Self:
|
73
|
+
kwargs['date_created'] = timezone_now()
|
74
|
+
return super().insert_one(_document, **kwargs)
|
64
75
|
|
65
76
|
async def login(self) -> dict:
|
66
|
-
"""Return dict of access and refresh
|
67
|
-
|
77
|
+
"""Return dict of access and refresh tokens"""
|
78
|
+
await self.update(last_login=timezone_now())
|
79
|
+
return await config.AUTHENTICATION.login(user=self)
|
80
|
+
|
81
|
+
async def refresh_tokens(self) -> dict:
|
82
|
+
"""Return dict of new access and refresh tokens"""
|
83
|
+
return await config.AUTHENTICATION.refresh(user=self)
|
68
84
|
|
69
85
|
async def logout(self) -> dict:
|
70
|
-
return await config.AUTHENTICATION.logout(self
|
86
|
+
return await config.AUTHENTICATION.logout(user=self)
|
71
87
|
|
72
88
|
async def set_password(self, password: str):
|
73
89
|
"""
|
@@ -1,8 +1,8 @@
|
|
1
1
|
import operator
|
2
2
|
from abc import abstractmethod
|
3
|
+
from collections.abc import Iterator
|
3
4
|
from functools import reduce
|
4
5
|
from sys import version_info
|
5
|
-
from typing import Iterator
|
6
6
|
|
7
7
|
from pydantic_core._pydantic_core import ValidationError
|
8
8
|
|
@@ -27,10 +27,7 @@ class BaseQuery:
|
|
27
27
|
@classmethod
|
28
28
|
def _clean_error_message(cls, validation_error: ValidationError, is_updating: bool = False) -> str:
|
29
29
|
error = ', '.join(
|
30
|
-
'{field}="{error}"'.format(
|
31
|
-
field='.'.join(str(loc) for loc in e['loc']),
|
32
|
-
error=e['msg']
|
33
|
-
)
|
30
|
+
'{field}="{error}"'.format(field='.'.join(str(loc) for loc in e['loc']), error=e['msg'])
|
34
31
|
for e in validation_error.errors()
|
35
32
|
if not is_updating or e['type'] != 'missing'
|
36
33
|
)
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
3
3
|
import types
|
4
4
|
import typing
|
5
5
|
from sys import version_info
|
6
|
-
from typing import
|
6
|
+
from typing import Any, Union, get_args, get_origin
|
7
7
|
|
8
8
|
from pydantic import BaseModel, ValidationError
|
9
9
|
|
@@ -14,9 +14,12 @@ from panther.db.queries.base_queries import BaseQuery
|
|
14
14
|
from panther.db.utils import prepare_id_for_query
|
15
15
|
from panther.exceptions import DatabaseError
|
16
16
|
|
17
|
+
if typing.TYPE_CHECKING:
|
18
|
+
from collections.abc import Iterable, Sequence
|
19
|
+
|
17
20
|
try:
|
18
21
|
from bson.codec_options import CodecOptions
|
19
|
-
from pymongo.results import
|
22
|
+
from pymongo.results import InsertManyResult, InsertOneResult
|
20
23
|
except ImportError:
|
21
24
|
# MongoDB-related libraries are not required by default.
|
22
25
|
# If the user intends to use MongoDB, they must install the required dependencies explicitly.
|
@@ -51,9 +54,7 @@ def get_annotation_type(annotation: Any) -> type | None:
|
|
51
54
|
return None
|
52
55
|
|
53
56
|
# Handle basic types (str, int, bool, dict) and Pydantic BaseModel subclasses
|
54
|
-
if isinstance(annotation, type) and (
|
55
|
-
annotation in (str, int, bool, dict) or issubclass(annotation, BaseModel)
|
56
|
-
):
|
57
|
+
if isinstance(annotation, type) and (annotation in (str, int, bool, dict) or issubclass(annotation, BaseModel)):
|
57
58
|
return annotation
|
58
59
|
|
59
60
|
raise DatabaseError(f'Panther does not support {annotation} as a field type for unwrapping.')
|
@@ -78,9 +79,9 @@ class BaseMongoDBQuery(BaseQuery):
|
|
78
79
|
if isinstance(field_type, (types.GenericAlias, typing._GenericAlias)):
|
79
80
|
element_type = get_annotation_type(field_type) # Unwrap further (e.g. list[str] -> str)
|
80
81
|
if element_type is None:
|
81
|
-
raise DatabaseError(f
|
82
|
+
raise DatabaseError(f'Cannot determine element type for generic list item: {field_type}')
|
82
83
|
if not isinstance(value, list): # Or check if iterable, matching the structure
|
83
|
-
raise DatabaseError(f
|
84
|
+
raise DatabaseError(f'Expected a list for nested generic type {field_type}, got {type(value)}')
|
84
85
|
return [await cls._create_list(field_type=element_type, value=item) for item in value]
|
85
86
|
|
86
87
|
# Make sure Model condition is before BaseModel.
|
@@ -90,7 +91,7 @@ class BaseMongoDBQuery(BaseQuery):
|
|
90
91
|
|
91
92
|
if isinstance(field_type, type) and issubclass(field_type, BaseModel):
|
92
93
|
if not isinstance(value, dict):
|
93
|
-
raise DatabaseError(f
|
94
|
+
raise DatabaseError(f'Expected a dictionary for BaseModel {field_type.__name__}, got {type(value)}')
|
94
95
|
|
95
96
|
return {
|
96
97
|
field_name: await cls._create_field(model=field_type, field_name=field_name, value=value[field_name])
|
@@ -117,14 +118,15 @@ class BaseMongoDBQuery(BaseQuery):
|
|
117
118
|
if unwrapped_type is None:
|
118
119
|
raise DatabaseError(
|
119
120
|
f"Could not determine a valid underlying type for field '{field_name}' "
|
120
|
-
f
|
121
|
+
f'with annotation {field_annotation} in model {model.__name__}.',
|
121
122
|
)
|
122
123
|
|
123
124
|
if get_origin(field_annotation) is list:
|
124
125
|
# Or check for general iterables if applicable
|
125
126
|
if not isinstance(value, list):
|
126
127
|
raise DatabaseError(
|
127
|
-
f"Field '{field_name}' expects a list, got {type(value)} for model {model.__name__}"
|
128
|
+
f"Field '{field_name}' expects a list, got {type(value)} for model {model.__name__}",
|
129
|
+
)
|
128
130
|
return [await cls._create_list(field_type=unwrapped_type, value=item) for item in value]
|
129
131
|
|
130
132
|
if isinstance(unwrapped_type, type) and issubclass(unwrapped_type, Model):
|
@@ -136,7 +138,7 @@ class BaseMongoDBQuery(BaseQuery):
|
|
136
138
|
if not isinstance(value, dict):
|
137
139
|
raise DatabaseError(
|
138
140
|
f"Field '{field_name}' expects a dictionary for BaseModel {unwrapped_type.__name__}, "
|
139
|
-
f
|
141
|
+
f'got {type(value)} in model {model.__name__}',
|
140
142
|
)
|
141
143
|
return {
|
142
144
|
nested_field_name: await cls._create_field(
|
@@ -144,7 +146,8 @@ class BaseMongoDBQuery(BaseQuery):
|
|
144
146
|
field_name=nested_field_name,
|
145
147
|
value=value[nested_field_name],
|
146
148
|
)
|
147
|
-
for nested_field_name in unwrapped_type.model_fields
|
149
|
+
for nested_field_name in unwrapped_type.model_fields
|
150
|
+
if nested_field_name in value
|
148
151
|
}
|
149
152
|
|
150
153
|
return value
|
@@ -223,10 +226,7 @@ class BaseMongoDBQuery(BaseQuery):
|
|
223
226
|
async def insert_one(cls, _document: dict | None = None, /, **kwargs) -> Self:
|
224
227
|
document = cls._merge(_document, kwargs)
|
225
228
|
cls._validate_data(data=document)
|
226
|
-
final_document = {
|
227
|
-
field: cls.clean_value(field=field, value=value)
|
228
|
-
for field, value in document.items()
|
229
|
-
}
|
229
|
+
final_document = {field: cls.clean_value(field=field, value=value) for field, value in document.items()}
|
230
230
|
result = await cls._create_model_instance(document=final_document)
|
231
231
|
insert_one_result: InsertOneResult = await db.session[cls.__name__].insert_one(final_document)
|
232
232
|
result.id = insert_one_result.inserted_id
|
@@ -239,10 +239,7 @@ class BaseMongoDBQuery(BaseQuery):
|
|
239
239
|
for document in documents:
|
240
240
|
prepare_id_for_query(document, is_mongo=True)
|
241
241
|
cls._validate_data(data=document)
|
242
|
-
cleaned_document = {
|
243
|
-
field: cls.clean_value(field=field, value=value)
|
244
|
-
for field, value in document.items()
|
245
|
-
}
|
242
|
+
cleaned_document = {field: cls.clean_value(field=field, value=value) for field, value in document.items()}
|
246
243
|
final_documents.append(cleaned_document)
|
247
244
|
results.append(await cls._create_model_instance(document=cleaned_document))
|
248
245
|
insert_many_result: InsertManyResult = await db.session[cls.__name__].insert_many(final_documents)
|
panther/db/queries/queries.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import sys
|
2
|
-
from
|
2
|
+
from collections.abc import Iterable, Sequence
|
3
3
|
|
4
4
|
from pantherdb import Cursor as PantherDBCursor
|
5
5
|
from pydantic import BaseModel
|
@@ -7,7 +7,7 @@ from pydantic import BaseModel
|
|
7
7
|
from panther.configs import QueryObservable
|
8
8
|
from panther.db.cursor import Cursor
|
9
9
|
from panther.db.queries.base_queries import BaseQuery
|
10
|
-
from panther.db.utils import
|
10
|
+
from panther.db.utils import check_connection, log_query
|
11
11
|
from panther.exceptions import NotFoundAPIError
|
12
12
|
|
13
13
|
__all__ = ('Query',)
|
@@ -35,7 +35,7 @@ class Query(BaseQuery):
|
|
35
35
|
else:
|
36
36
|
for kls in cls.__bases__:
|
37
37
|
if kls.__bases__.count(Query):
|
38
|
-
kls.__bases__ = (*kls.__bases__[:kls.__bases__.index(Query) + 1], parent)
|
38
|
+
kls.__bases__ = (*kls.__bases__[: kls.__bases__.index(Query) + 1], parent)
|
39
39
|
|
40
40
|
# # # # # Find # # # # #
|
41
41
|
@classmethod
|
@@ -54,6 +54,7 @@ class Query(BaseQuery):
|
|
54
54
|
>>> await User.find_one({'id': 1, 'name': 'Ali'})
|
55
55
|
or
|
56
56
|
>>> await User.find_one({'id': 1}, name='Ali')
|
57
|
+
|
57
58
|
"""
|
58
59
|
return await super().find_one(_filter, **kwargs)
|
59
60
|
|
@@ -73,6 +74,7 @@ class Query(BaseQuery):
|
|
73
74
|
>>> await User.find({'age': 18, 'name': 'Ali'})
|
74
75
|
or
|
75
76
|
>>> await User.find({'age': 18}, name='Ali')
|
77
|
+
|
76
78
|
"""
|
77
79
|
return await super().find(_filter, **kwargs)
|
78
80
|
|
@@ -92,6 +94,7 @@ class Query(BaseQuery):
|
|
92
94
|
>>> await User.first({'age': 18, 'name': 'Ali'})
|
93
95
|
or
|
94
96
|
>>> await User.first({'age': 18}, name='Ali')
|
97
|
+
|
95
98
|
"""
|
96
99
|
return await super().first(_filter, **kwargs)
|
97
100
|
|
@@ -111,6 +114,7 @@ class Query(BaseQuery):
|
|
111
114
|
>>> await User.last({'age': 18, 'name': 'Ali'})
|
112
115
|
or
|
113
116
|
>>> await User.last({'age': 18}, name='Ali')
|
117
|
+
|
114
118
|
"""
|
115
119
|
return await super().last(_filter, **kwargs)
|
116
120
|
|
@@ -135,6 +139,7 @@ class Query(BaseQuery):
|
|
135
139
|
>>> ]
|
136
140
|
|
137
141
|
>>> await User.aggregate(pipeline)
|
142
|
+
|
138
143
|
"""
|
139
144
|
return await super().aggregate(pipeline)
|
140
145
|
|
@@ -155,6 +160,7 @@ class Query(BaseQuery):
|
|
155
160
|
>>> await User.count({'age': 18, 'name': 'Ali'})
|
156
161
|
or
|
157
162
|
>>> await User.count({'age': 18}, name='Ali')
|
163
|
+
|
158
164
|
"""
|
159
165
|
return await super().count(_filter, **kwargs)
|
160
166
|
|
@@ -175,6 +181,7 @@ class Query(BaseQuery):
|
|
175
181
|
>>> await User.insert_one({'age': 18, 'name': 'Ali'})
|
176
182
|
or
|
177
183
|
>>> await User.insert_one({'age': 18}, name='Ali')
|
184
|
+
|
178
185
|
"""
|
179
186
|
return await super().insert_one(_document, **kwargs)
|
180
187
|
|
@@ -195,6 +202,7 @@ class Query(BaseQuery):
|
|
195
202
|
>>> {'age': 16, 'name': 'Amin'}
|
196
203
|
>>> ]
|
197
204
|
>>> await User.insert_many(users)
|
205
|
+
|
198
206
|
"""
|
199
207
|
return await super().insert_many(documents)
|
200
208
|
|
@@ -212,6 +220,7 @@ class Query(BaseQuery):
|
|
212
220
|
>>> user = await User.find_one(name='Ali')
|
213
221
|
|
214
222
|
>>> await user.delete()
|
223
|
+
|
215
224
|
"""
|
216
225
|
await super().delete()
|
217
226
|
|
@@ -231,6 +240,7 @@ class Query(BaseQuery):
|
|
231
240
|
>>> await User.delete_one({'age': 18, 'name': 'Ali'})
|
232
241
|
or
|
233
242
|
>>> await User.delete_one({'age': 18}, name='Ali')
|
243
|
+
|
234
244
|
"""
|
235
245
|
return await super().delete_one(_filter, **kwargs)
|
236
246
|
|
@@ -250,6 +260,7 @@ class Query(BaseQuery):
|
|
250
260
|
>>> await User.delete_many({'age': 18, 'name': 'Ali'})
|
251
261
|
or
|
252
262
|
>>> await User.delete_many({'age': 18}, name='Ali')
|
263
|
+
|
253
264
|
"""
|
254
265
|
return await super().delete_many(_filter, **kwargs)
|
255
266
|
|
@@ -271,6 +282,7 @@ class Query(BaseQuery):
|
|
271
282
|
>>> await user.update({'name': 'Saba'}, age=19)
|
272
283
|
or
|
273
284
|
>>> await user.update({'name': 'Saba', 'age': 19})
|
285
|
+
|
274
286
|
"""
|
275
287
|
await super().update(_update, **kwargs)
|
276
288
|
|
@@ -290,6 +302,7 @@ class Query(BaseQuery):
|
|
290
302
|
>>> await User.update_one({'id': 1}, {'age': 18, 'name': 'Ali'})
|
291
303
|
or
|
292
304
|
>>> await User.update_one({'id': 1}, {'age': 18}, name='Ali')
|
305
|
+
|
293
306
|
"""
|
294
307
|
return await super().update_one(_filter, _update, **kwargs)
|
295
308
|
|
@@ -309,6 +322,7 @@ class Query(BaseQuery):
|
|
309
322
|
>>> await User.update_many({'name': 'Saba'}, {'age': 18, 'name': 'Ali'})
|
310
323
|
or
|
311
324
|
>>> await User.update_many({'name': 'Saba'}, {'age': 18}, name='Ali')
|
325
|
+
|
312
326
|
"""
|
313
327
|
return await super().update_many(_filter, _update, **kwargs)
|
314
328
|
|
@@ -323,6 +337,7 @@ class Query(BaseQuery):
|
|
323
337
|
>>> from app.models import User
|
324
338
|
|
325
339
|
>>> await User.all()
|
340
|
+
|
326
341
|
"""
|
327
342
|
return await cls.find()
|
328
343
|
|
@@ -342,6 +357,7 @@ class Query(BaseQuery):
|
|
342
357
|
>>> await User.find_one_or_insert({'age': 18, 'name': 'Ali'})
|
343
358
|
or
|
344
359
|
>>> await User.find_one_or_insert({'age': 18}, name='Ali')
|
360
|
+
|
345
361
|
"""
|
346
362
|
if obj := await cls.find_one(_filter, **kwargs):
|
347
363
|
return obj, False
|
@@ -359,6 +375,7 @@ class Query(BaseQuery):
|
|
359
375
|
>>> await User.find_one_or_raise({'age': 18, 'name': 'Ali'})
|
360
376
|
or
|
361
377
|
>>> await User.find_one_or_raise({'age': 18}, name='Ali')
|
378
|
+
|
362
379
|
"""
|
363
380
|
if obj := await cls.find_one(_filter, **kwargs):
|
364
381
|
return obj
|
@@ -379,17 +396,16 @@ class Query(BaseQuery):
|
|
379
396
|
>>> await User.exists({'age': 18, 'name': 'Ali'})
|
380
397
|
or
|
381
398
|
>>> await User.exists({'age': 18}, name='Ali')
|
399
|
+
|
382
400
|
"""
|
383
|
-
|
384
|
-
return True
|
385
|
-
else:
|
386
|
-
return False
|
401
|
+
return await cls.count(_filter, **kwargs) > 0
|
387
402
|
|
388
403
|
async def save(self) -> None:
|
389
404
|
"""
|
390
405
|
Save the document
|
391
406
|
If it has `id` --> Update It
|
392
407
|
else --> Insert It
|
408
|
+
|
393
409
|
Example:
|
394
410
|
-------
|
395
411
|
>>> from app.models import User
|
@@ -402,12 +418,14 @@ class Query(BaseQuery):
|
|
402
418
|
# Insert
|
403
419
|
>>> user = User(name='Ali')
|
404
420
|
>>> await user.save()
|
421
|
+
|
405
422
|
"""
|
406
423
|
document = {
|
407
424
|
field: getattr(self, field).model_dump(by_alias=True)
|
408
425
|
if issubclass(type(getattr(self, field)), BaseModel)
|
409
426
|
else getattr(self, field)
|
410
|
-
for field in self.model_fields
|
427
|
+
for field in self.model_fields
|
428
|
+
if field != 'request'
|
411
429
|
}
|
412
430
|
|
413
431
|
if self.id:
|
panther/db/utils.py
CHANGED
@@ -20,7 +20,7 @@ def log_query(func):
|
|
20
20
|
response = await func(*args, **kwargs)
|
21
21
|
end = perf_counter()
|
22
22
|
class_name = getattr(args[0], '__name__', args[0].__class__.__name__)
|
23
|
-
logger.info(f'
|
23
|
+
logger.info(f'[Query] {class_name}.{func.__name__}() takes {(end - start) * 1_000:.3} ms')
|
24
24
|
return response
|
25
25
|
|
26
26
|
return log
|