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.
Files changed (56) hide show
  1. panther/__init__.py +1 -1
  2. panther/_load_configs.py +46 -37
  3. panther/_utils.py +49 -34
  4. panther/app.py +96 -97
  5. panther/authentications.py +97 -50
  6. panther/background_tasks.py +98 -124
  7. panther/base_request.py +16 -10
  8. panther/base_websocket.py +8 -8
  9. panther/caching.py +16 -80
  10. panther/cli/create_command.py +17 -16
  11. panther/cli/main.py +1 -1
  12. panther/cli/monitor_command.py +11 -6
  13. panther/cli/run_command.py +5 -71
  14. panther/cli/template.py +7 -7
  15. panther/cli/utils.py +58 -69
  16. panther/configs.py +70 -72
  17. panther/db/connections.py +18 -24
  18. panther/db/cursor.py +0 -1
  19. panther/db/models.py +24 -8
  20. panther/db/queries/base_queries.py +2 -5
  21. panther/db/queries/mongodb_queries.py +17 -20
  22. panther/db/queries/pantherdb_queries.py +1 -1
  23. panther/db/queries/queries.py +26 -8
  24. panther/db/utils.py +1 -1
  25. panther/events.py +25 -14
  26. panther/exceptions.py +2 -7
  27. panther/file_handler.py +1 -1
  28. panther/generics.py +11 -8
  29. panther/logging.py +2 -1
  30. panther/main.py +12 -13
  31. panther/middlewares/cors.py +67 -0
  32. panther/middlewares/monitoring.py +5 -3
  33. panther/openapi/urls.py +2 -2
  34. panther/openapi/utils.py +3 -3
  35. panther/openapi/views.py +20 -37
  36. panther/pagination.py +4 -2
  37. panther/panel/apis.py +2 -7
  38. panther/panel/urls.py +2 -6
  39. panther/panel/utils.py +9 -5
  40. panther/panel/views.py +13 -22
  41. panther/permissions.py +2 -1
  42. panther/request.py +2 -1
  43. panther/response.py +53 -47
  44. panther/routings.py +12 -12
  45. panther/serializer.py +19 -20
  46. panther/test.py +73 -58
  47. panther/throttling.py +68 -3
  48. panther/utils.py +5 -11
  49. {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/METADATA +1 -1
  50. panther-5.0.0b4.dist-info/RECORD +75 -0
  51. panther/monitoring.py +0 -34
  52. panther-5.0.0b3.dist-info/RECORD +0 -75
  53. {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/WHEEL +0 -0
  54. {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/entry_points.txt +0 -0
  55. {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/licenses/LICENSE +0 -0
  56. {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 dataclasses import dataclass
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
- from panther.throttling import Throttling
9
+
11
10
 
12
11
  class JWTConfig:
13
12
  def __init__(
14
- self,
15
- key: str,
16
- algorithm: str = 'HS256',
17
- life_time: timedelta | int = timedelta(days=1),
18
- refresh_life_time: timedelta | int | None = None,
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
- DEFAULT_CACHE_EXP: timedelta | None
52
- THROTTLING: Throttling | None
53
- SECRET_KEY: bytes | None
54
- HTTP_MIDDLEWARES: list
55
- WS_MIDDLEWARES: list
56
- USER_MODEL: type[PydanticBaseModel] | None # type: type[panther.db.Model]
57
- AUTHENTICATION: type[PydanticBaseModel] | None
58
- WS_AUTHENTICATION: type[PydanticBaseModel] | None
59
- JWT_CONFIG: JWTConfig | None
60
- MODELS: list[type[PydanticBaseModel]] # type: type[panther.db.Model]
61
- FLAT_URLS: dict
62
- URLS: dict
63
- WEBSOCKET_CONNECTIONS: typing.Callable | None
64
- BACKGROUND_TASKS: bool
65
- HAS_WS: bool
66
- STARTUPS: list[Callable]
67
- SHUTDOWNS: list[Callable]
68
- TIMEZONE: str
69
- TEMPLATES_DIR: str | list[str]
70
- JINJA_ENVIRONMENT: jinja2.Environment | None
71
- AUTO_REFORMAT: bool
72
- QUERY_ENGINE: typing.Callable | None
73
- DATABASE: typing.Callable | None
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
- def refresh(self):
87
- # In some tests we need to `refresh` the `config` values
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
- 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,
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
- self,
104
- init: bool = False,
105
- host: str = 'localhost',
106
- port: int = 6379,
107
- db: int = 0,
108
- websocket_db: int = 0,
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
@@ -51,7 +51,6 @@ class Cursor(_Cursor):
51
51
  except StopIteration:
52
52
  raise
53
53
 
54
-
55
54
  def __getitem__(self, index: int | slice) -> Cursor[Self] | Self:
56
55
  document = super().__getitem__(index)
57
56
  if isinstance(document, dict):
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 Field, WrapValidator, PlainSerializer, BaseModel as PydanticBaseModel
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 scrypt, URANDOM_SIZE, timezone_now
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 = Field(default_factory=timezone_now)
67
+ date_created: datetime | None = None
59
68
 
60
69
  USERNAME_FIELD: ClassVar = 'username'
61
70
 
62
- async def update_last_login(self) -> None:
63
- await self.update(last_login=timezone_now())
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 token"""
67
- return config.AUTHENTICATION.login(str(self.id))
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._auth_token)
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 get_args, get_origin, Iterable, Sequence, Any, Union
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 InsertOneResult, InsertManyResult
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"Cannot determine element type for generic list item: {field_type}")
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"Expected a list for nested generic type {field_type}, got {type(value)}")
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"Expected a dictionary for BaseModel {field_type.__name__}, got {type(value)}")
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"with annotation {field_annotation} in model {model.__name__}."
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"got {type(value)} in model {model.__name__}"
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 if nested_field_name in value
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)
@@ -1,5 +1,5 @@
1
+ from collections.abc import Iterable
1
2
  from sys import version_info
2
- from typing import Iterable
3
3
 
4
4
  from pantherdb import Cursor
5
5
 
@@ -1,5 +1,5 @@
1
1
  import sys
2
- from typing import Sequence, Iterable
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 log_query, check_connection
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
- if await cls.count(_filter, **kwargs) > 0:
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.keys() if field != 'request'
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'\033[1mQuery -->\033[0m {class_name}.{func.__name__}() --> {(end - start) * 1_000:.2} ms')
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