panther 5.0.0b3__py3-none-any.whl → 5.0.0b5__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 (57) 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 +30 -24
  18. panther/db/cursor.py +3 -1
  19. panther/db/models.py +26 -10
  20. panther/db/queries/base_queries.py +4 -5
  21. panther/db/queries/mongodb_queries.py +21 -21
  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 +74 -100
  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 +101 -94
  44. panther/routings.py +12 -12
  45. panther/serializer.py +20 -43
  46. panther/test.py +73 -58
  47. panther/throttling.py +68 -3
  48. panther/utils.py +5 -11
  49. panther-5.0.0b5.dist-info/METADATA +188 -0
  50. panther-5.0.0b5.dist-info/RECORD +75 -0
  51. panther/monitoring.py +0 -34
  52. panther-5.0.0b3.dist-info/METADATA +0 -223
  53. panther-5.0.0b3.dist-info/RECORD +0 -75
  54. {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/WHEEL +0 -0
  55. {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/entry_points.txt +0 -0
  56. {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/licenses/LICENSE +0 -0
  57. {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/top_level.txt +0 -0
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':
@@ -28,7 +37,7 @@ def validate_object_id(value, handler):
28
37
  raise ValueError(msg) from e
29
38
 
30
39
 
31
- ID = Annotated[str, WrapValidator(validate_object_id), PlainSerializer(lambda x: str(x), return_type=str)]
40
+ ID = Annotated[str, WrapValidator(validate_object_id), PlainSerializer(lambda x: str(x), return_type=str)] | None
32
41
 
33
42
 
34
43
  class Model(PydanticBaseModel, Query):
@@ -37,7 +46,7 @@ class Model(PydanticBaseModel, Query):
37
46
  return
38
47
  config.MODELS.append(cls)
39
48
 
40
- id: ID | None = Field(None, validation_alias='_id', alias='_id')
49
+ id: ID = None
41
50
 
42
51
  @property
43
52
  def _id(self):
@@ -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
  )
@@ -48,6 +45,8 @@ class BaseQuery:
48
45
  @classmethod
49
46
  async def _create_model_instance(cls, document: dict):
50
47
  """Prevent getting errors from document insertion"""
48
+ if '_id' in document:
49
+ document['id'] = document.pop('_id')
51
50
  try:
52
51
  return cls(**document)
53
52
  except ValidationError as validation_error:
@@ -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])
@@ -103,7 +104,7 @@ class BaseMongoDBQuery(BaseQuery):
103
104
  @classmethod
104
105
  async def _create_field(cls, model: type, field_name: str, value: Any) -> Any:
105
106
  # Handle primary key field directly
106
- if field_name == '_id':
107
+ if field_name == 'id':
107
108
  return value
108
109
 
109
110
  if field_name not in model.model_fields:
@@ -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
@@ -152,6 +155,9 @@ class BaseMongoDBQuery(BaseQuery):
152
155
  @classmethod
153
156
  async def _create_model_instance(cls, document: dict) -> Self:
154
157
  """Prepares document and creates an instance of the model."""
158
+ if '_id' in document:
159
+ document['id'] = document.pop('_id')
160
+
155
161
  processed_document = {
156
162
  field_name: await cls._create_field(model=cls, field_name=field_name, value=field_value)
157
163
  for field_name, field_value in document.items()
@@ -223,10 +229,7 @@ class BaseMongoDBQuery(BaseQuery):
223
229
  async def insert_one(cls, _document: dict | None = None, /, **kwargs) -> Self:
224
230
  document = cls._merge(_document, kwargs)
225
231
  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
- }
232
+ final_document = {field: cls.clean_value(field=field, value=value) for field, value in document.items()}
230
233
  result = await cls._create_model_instance(document=final_document)
231
234
  insert_one_result: InsertOneResult = await db.session[cls.__name__].insert_one(final_document)
232
235
  result.id = insert_one_result.inserted_id
@@ -239,10 +242,7 @@ class BaseMongoDBQuery(BaseQuery):
239
242
  for document in documents:
240
243
  prepare_id_for_query(document, is_mongo=True)
241
244
  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
- }
245
+ cleaned_document = {field: cls.clean_value(field=field, value=value) for field, value in document.items()}
246
246
  final_documents.append(cleaned_document)
247
247
  results.append(await cls._create_model_instance(document=cleaned_document))
248
248
  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
panther/events.py CHANGED
@@ -2,31 +2,36 @@ import asyncio
2
2
  import logging
3
3
 
4
4
  from panther._utils import is_function_async
5
- from panther.configs import config
5
+ from panther.utils import Singleton
6
6
 
7
7
  logger = logging.getLogger('panther')
8
8
 
9
9
 
10
- class Event:
11
- @staticmethod
12
- def startup(func):
13
- config.STARTUPS.append(func)
10
+ class Event(Singleton):
11
+ _startups = []
12
+ _shutdowns = []
13
+
14
+ @classmethod
15
+ def startup(cls, func):
16
+ cls._startups.append(func)
14
17
 
15
18
  def wrapper():
16
19
  return func()
20
+
17
21
  return wrapper
18
22
 
19
- @staticmethod
20
- def shutdown(func):
21
- config.SHUTDOWNS.append(func)
23
+ @classmethod
24
+ def shutdown(cls, func):
25
+ cls._shutdowns.append(func)
22
26
 
23
27
  def wrapper():
24
28
  return func()
29
+
25
30
  return wrapper
26
31
 
27
- @staticmethod
28
- async def run_startups():
29
- for func in config.STARTUPS:
32
+ @classmethod
33
+ async def run_startups(cls):
34
+ for func in cls._startups:
30
35
  try:
31
36
  if is_function_async(func):
32
37
  await func()
@@ -35,9 +40,9 @@ class Event:
35
40
  except Exception as e:
36
41
  logger.error(f'{func.__name__}() startup event got error: {e}')
37
42
 
38
- @staticmethod
39
- def run_shutdowns():
40
- for func in config.SHUTDOWNS:
43
+ @classmethod
44
+ def run_shutdowns(cls):
45
+ for func in cls._shutdowns:
41
46
  if is_function_async(func):
42
47
  try:
43
48
  asyncio.run(func())
@@ -48,3 +53,9 @@ class Event:
48
53
  pass
49
54
  else:
50
55
  func()
56
+
57
+ @classmethod
58
+ def clear(cls):
59
+ """Clear all stored events (useful for testing)"""
60
+ cls._startups.clear()
61
+ cls._shutdowns.clear()
panther/exceptions.py CHANGED
@@ -13,12 +13,7 @@ class BaseError(Exception):
13
13
  detail: str | dict | list = 'Internal Server Error'
14
14
  status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR
15
15
 
16
- def __init__(
17
- self,
18
- detail: str | dict | list = None,
19
- status_code: int = None,
20
- headers: dict = None
21
- ):
16
+ def __init__(self, detail: str | dict | list = None, status_code: int = None, headers: dict = None):
22
17
  self.detail = detail or self.detail
23
18
  self.status_code = status_code or self.status_code
24
19
  self.headers = headers
@@ -81,5 +76,5 @@ class ThrottlingAPIError(APIError):
81
76
 
82
77
  class InvalidPathVariableAPIError(APIError):
83
78
  def __init__(self, value: str, variable_type: type):
84
- detail = f"Path variable '{value}' should be '{variable_type.__name__}'"
79
+ detail = f'Path variable `{value}` is not `{variable_type.__name__}`'
85
80
  super().__init__(detail=detail, status_code=status.HTTP_400_BAD_REQUEST)
panther/file_handler.py CHANGED
@@ -1,8 +1,8 @@
1
1
  from functools import cached_property
2
2
 
3
- from panther import status
4
3
  from pydantic import BaseModel, field_validator, model_serializer
5
4
 
5
+ from panther import status
6
6
  from panther.exceptions import APIError
7
7
 
8
8