panther 4.0.0__py3-none-any.whl → 4.1.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  from panther.main import Panther # noqa: F401
2
2
 
3
- __version__ = '4.0.0'
3
+ __version__ = '4.1.0'
4
4
 
5
5
 
6
6
  def version():
panther/app.py CHANGED
@@ -95,7 +95,7 @@ class API:
95
95
  if not isinstance(response, Response):
96
96
  response = Response(data=response)
97
97
  if self.output_model and response.data:
98
- response.data = response.apply_output_model(response.data, output_model=self.output_model)
98
+ response.data = await response.apply_output_model(output_model=self.output_model)
99
99
 
100
100
  # 10. Set New Response To Cache
101
101
  if self.cache and self.request.method == 'GET':
panther/base_request.py CHANGED
@@ -27,9 +27,9 @@ class Headers:
27
27
  sec_websocket_version: str
28
28
  sec_websocket_key: str
29
29
 
30
- def __init__(self, headers):
31
- self.__headers = headers
32
- self.__pythonic_headers = {k.lower().replace('-', '_'): v for k, v in headers.items()}
30
+ def __init__(self, headers: list):
31
+ self.__headers = {header[0].decode('utf-8'): header[1].decode('utf-8') for header in headers}
32
+ self.__pythonic_headers = {k.lower().replace('-', '_'): v for k, v in self.__headers.items()}
33
33
 
34
34
  def __getattr__(self, item: str):
35
35
  if result := self.__pythonic_headers.get(item):
@@ -68,8 +68,7 @@ class BaseRequest:
68
68
  @property
69
69
  def headers(self) -> Headers:
70
70
  if self._headers is None:
71
- _headers = {header[0].decode('utf-8'): header[1].decode('utf-8') for header in self.scope['headers']}
72
- self._headers = Headers(_headers)
71
+ self._headers = Headers(self.scope['headers'])
73
72
  return self._headers
74
73
 
75
74
  @property
@@ -77,8 +76,7 @@ class BaseRequest:
77
76
  if self._params is None:
78
77
  self._params = {}
79
78
  if (query_string := self.scope['query_string']) != b'':
80
- query_string = query_string.decode('utf-8').split('&')
81
- for param in query_string:
79
+ for param in query_string.decode('utf-8').split('&'):
82
80
  k, *_, v = param.split('=')
83
81
  self._params[k] = v
84
82
  return self._params
panther/base_websocket.py CHANGED
@@ -182,10 +182,9 @@ class WebsocketConnections(Singleton):
182
182
  but they have same Manager()
183
183
  """
184
184
 
185
- if config.HAS_WS:
186
- # Schedule the async function to run in the background,
187
- # We don't need to await for this task
188
- asyncio.create_task(self())
185
+ # Schedule the async function to run in the background,
186
+ # We don't need to await for this task
187
+ asyncio.create_task(self())
189
188
 
190
189
  @classmethod
191
190
  async def handle_authentication(cls, connection: Websocket):
panther/generics.py CHANGED
@@ -27,7 +27,7 @@ class ObjectRequired:
27
27
  logger.critical(f'`{self.__class__.__name__}.object()` should return instance of a Model --> `find_one()`')
28
28
  raise APIError
29
29
 
30
- async def object(self, request: Request, **kwargs) -> Model:
30
+ async def object(self, request: Request, **kwargs):
31
31
  """
32
32
  Used in `RetrieveAPI`, `UpdateAPI`, `DeleteAPI`
33
33
  """
@@ -35,18 +35,18 @@ class ObjectRequired:
35
35
  raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)
36
36
 
37
37
 
38
- class ObjectsRequired:
39
- def _check_objects(self, cursor):
38
+ class CursorRequired:
39
+ def _check_cursor(self, cursor):
40
40
  if isinstance(cursor, (Cursor, PantherDBCursor)) is False:
41
- logger.critical(f'`{self.__class__.__name__}.objects()` should return a Cursor --> `find()`')
41
+ logger.critical(f'`{self.__class__.__name__}.cursor()` should return a Cursor --> `find()`')
42
42
  raise APIError
43
43
 
44
- async def objects(self, request: Request, **kwargs) -> Cursor | PantherDBCursor:
44
+ async def cursor(self, request: Request, **kwargs) -> Cursor | PantherDBCursor:
45
45
  """
46
46
  Used in `ListAPI`
47
47
  Should return `.find()`
48
48
  """
49
- logger.error(f'`objects()` method is not implemented in {self.__class__} .')
49
+ logger.error(f'`cursor()` method is not implemented in {self.__class__} .')
50
50
  raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)
51
51
 
52
52
 
@@ -58,15 +58,19 @@ class RetrieveAPI(GenericAPI, ObjectRequired):
58
58
  return Response(data=instance, status_code=status.HTTP_200_OK)
59
59
 
60
60
 
61
- class ListAPI(GenericAPI, ObjectsRequired):
61
+ class ListAPI(GenericAPI, CursorRequired):
62
62
  sort_fields: list[str]
63
63
  search_fields: list[str]
64
64
  filter_fields: list[str]
65
65
  pagination: type[Pagination]
66
66
 
67
67
  async def get(self, request: Request, **kwargs):
68
- cursor = await self.objects(request=request, **kwargs)
69
- self._check_objects(cursor)
68
+ cursor = await self.prepare_cursor(request=request, **kwargs)
69
+ return Response(data=cursor, status_code=status.HTTP_200_OK)
70
+
71
+ async def prepare_cursor(self, request: Request, **kwargs):
72
+ cursor = await self.cursor(request=request, **kwargs)
73
+ self._check_cursor(cursor)
70
74
 
71
75
  query = {}
72
76
  query |= self.process_filters(query_params=request.query_params, cursor=cursor)
@@ -81,7 +85,7 @@ class ListAPI(GenericAPI, ObjectsRequired):
81
85
  if pagination := self.process_pagination(query_params=request.query_params, cursor=cursor):
82
86
  cursor = await pagination.paginate()
83
87
 
84
- return Response(data=cursor, status_code=status.HTTP_200_OK)
88
+ return cursor
85
89
 
86
90
  def process_filters(self, query_params: dict, cursor: Cursor | PantherDBCursor) -> dict:
87
91
  _filter = {}
@@ -90,6 +94,7 @@ class ListAPI(GenericAPI, ObjectsRequired):
90
94
  if field in query_params:
91
95
  if config.DATABASE.__class__.__name__ == 'MongoDBConnection':
92
96
  with contextlib.suppress(Exception):
97
+ # Change type of the value if it is ObjectId
93
98
  if cursor.cls.model_fields[field].metadata[0].func.__name__ == 'validate_object_id':
94
99
  _filter[field] = bson.ObjectId(query_params[field])
95
100
  continue
@@ -161,3 +166,7 @@ class DeleteAPI(GenericAPI, ObjectRequired):
161
166
 
162
167
  await instance.delete()
163
168
  return Response(status_code=status.HTTP_204_NO_CONTENT)
169
+
170
+
171
+ class ListCreateAPI(CreateAPI, ListAPI):
172
+ pass
panther/main.py CHANGED
@@ -70,7 +70,8 @@ class Panther:
70
70
  if scope['type'] == 'lifespan':
71
71
  message = await receive()
72
72
  if message["type"] == 'lifespan.startup':
73
- await config.WEBSOCKET_CONNECTIONS.start()
73
+ if config.HAS_WS:
74
+ await config.WEBSOCKET_CONNECTIONS.start()
74
75
  await Event.run_startups()
75
76
  elif message["type"] == 'lifespan.shutdown':
76
77
  # It's not happening :\, so handle the shutdowns in __del__ ...
panther/response.py CHANGED
@@ -1,10 +1,9 @@
1
1
  import asyncio
2
2
  from types import NoneType
3
- from typing import Generator, AsyncGenerator
3
+ from typing import Generator, AsyncGenerator, Any, Type
4
4
 
5
5
  import orjson as json
6
- from pydantic import BaseModel as PydanticBaseModel
7
- from pydantic._internal._model_construction import ModelMetaclass
6
+ from pydantic import BaseModel
8
7
 
9
8
  from panther import status
10
9
  from panther._utils import to_async_generator
@@ -12,7 +11,7 @@ from panther.db.cursor import Cursor
12
11
  from pantherdb import Cursor as PantherDBCursor
13
12
  from panther.monitoring import Monitoring
14
13
 
15
- ResponseDataTypes = list | tuple | set | Cursor | PantherDBCursor | dict | int | float | str | bool | bytes | NoneType | ModelMetaclass
14
+ ResponseDataTypes = list | tuple | set | Cursor | PantherDBCursor | dict | int | float | str | bool | bytes | NoneType | Type[BaseModel]
16
15
  IterableDataTypes = list | tuple | set | Cursor | PantherDBCursor
17
16
  StreamingDataTypes = Generator | AsyncGenerator
18
17
 
@@ -32,6 +31,7 @@ class Response:
32
31
  :param status_code: should be int
33
32
  """
34
33
  self.headers = headers or {}
34
+ self.initial_data = data
35
35
  self.data = self.prepare_data(data=data)
36
36
  self.status_code = self.check_status_code(status_code=status_code)
37
37
 
@@ -68,7 +68,7 @@ class Response:
68
68
  elif isinstance(data, dict):
69
69
  return {key: self.prepare_data(value) for key, value in data.items()}
70
70
 
71
- elif issubclass(type(data), PydanticBaseModel):
71
+ elif issubclass(type(data), BaseModel):
72
72
  return data.model_dump()
73
73
 
74
74
  elif isinstance(data, IterableDataTypes):
@@ -79,25 +79,42 @@ class Response:
79
79
  raise TypeError(msg)
80
80
 
81
81
  @classmethod
82
- def check_status_code(cls, status_code: any):
82
+ def check_status_code(cls, status_code: Any):
83
83
  if not isinstance(status_code, int):
84
84
  error = f'Response `status_code` Should Be `int`. (`{status_code}` is {type(status_code)})'
85
85
  raise TypeError(error)
86
86
  return status_code
87
87
 
88
- @classmethod
89
- def apply_output_model(cls, data: any, /, output_model: ModelMetaclass):
88
+ async def apply_output_model(self, output_model: Type[BaseModel]):
90
89
  """This method is called in API.__call__"""
90
+
91
91
  # Dict
92
- if isinstance(data, dict):
92
+ if isinstance(self.data, dict):
93
+ # Apply `validation_alias` (id -> _id)
93
94
  for field_name, field in output_model.model_fields.items():
94
- if field.validation_alias and field_name in data:
95
- data[field.validation_alias] = data.pop(field_name)
96
- return output_model(**data).model_dump()
95
+ if field.validation_alias and field_name in self.data:
96
+ self.data[field.validation_alias] = self.data.pop(field_name)
97
+ output = output_model(**self.data)
98
+ if hasattr(output_model, 'prepare_response'):
99
+ return await output.prepare_response(instance=self.initial_data, data=output.model_dump())
100
+ return output.model_dump()
97
101
 
98
102
  # Iterable
99
- if isinstance(data, IterableDataTypes):
100
- return [cls.apply_output_model(d, output_model=output_model) for d in data]
103
+ results = []
104
+ if isinstance(self.data, IterableDataTypes):
105
+ for i, d in enumerate(self.data):
106
+ # Apply `validation_alias` (id -> _id)
107
+ for field_name, field in output_model.model_fields.items():
108
+ if field.validation_alias and field_name in d:
109
+ d[field.validation_alias] = d.pop(field_name)
110
+
111
+ output = output_model(**d)
112
+ if hasattr(output_model, 'prepare_response'):
113
+ result = await output.prepare_response(instance=self.initial_data[i], data=output.model_dump())
114
+ else:
115
+ result = output.model_dump()
116
+ results.append(result)
117
+ return results
101
118
 
102
119
  # Str | Bool | Bytes
103
120
  msg = 'Type of Response data is not match with `output_model`.\n*hint: You may want to remove `output_model`'
panther/serializer.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import typing
2
- from typing import TypeVar, Type
2
+ from typing import Any
3
3
 
4
4
  from pydantic import create_model, BaseModel, ConfigDict
5
5
  from pydantic.fields import FieldInfo, Field
@@ -205,13 +205,13 @@ class ModelSerializer(metaclass=MetaModelSerializer):
205
205
  model: type[BaseModel]
206
206
  request: Request
207
207
 
208
- async def create(self, validated_data: dict) -> Model:
208
+ async def create(self, validated_data: dict):
209
209
  """
210
210
  validated_data = ModelSerializer.model_dump()
211
211
  """
212
212
  return await self.model.insert_one(validated_data)
213
213
 
214
- async def update(self, instance: Model, validated_data: dict) -> Model:
214
+ async def update(self, instance: Model, validated_data: dict):
215
215
  """
216
216
  instance = UpdateAPI.object()
217
217
  validated_data = ModelSerializer.model_dump()
@@ -219,10 +219,13 @@ class ModelSerializer(metaclass=MetaModelSerializer):
219
219
  await instance.update(validated_data)
220
220
  return instance
221
221
 
222
- async def partial_update(self, instance: Model, validated_data: dict) -> Model:
222
+ async def partial_update(self, instance: Model, validated_data: dict):
223
223
  """
224
224
  instance = UpdateAPI.object()
225
225
  validated_data = ModelSerializer.model_dump(exclude_none=True)
226
226
  """
227
227
  await instance.update(validated_data)
228
228
  return instance
229
+
230
+ async def prepare_response(self, instance: Any, data: dict) -> dict:
231
+ return data
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: panther
3
- Version: 4.0.0
3
+ Version: 4.1.0
4
4
  Summary: Fast & Friendly, Web Framework For Building Async APIs
5
5
  Home-page: https://github.com/alirn76/panther
6
6
  Author: Ali RajabNezhad
@@ -56,7 +56,7 @@ Requires-Dist: watchfiles ~=0.21.0 ; extra == 'full'
56
56
  - Support Custom **Middlewares**
57
57
  - Support Custom **Throttling**
58
58
  - Support **Function-Base** and **Class-Base** APIs
59
- - It's One Of The **Fastest Python Frameworks**
59
+ - It's One Of The **Fastest Python Framework**
60
60
  ---
61
61
 
62
62
  ### Supported by
@@ -68,25 +68,6 @@ Requires-Dist: watchfiles ~=0.21.0 ; extra == 'full'
68
68
 
69
69
  ---
70
70
 
71
- ### Benchmark
72
-
73
- | Framework | Throughput (Request/Second) |
74
- |------------|-----------------------------|
75
- | Blacksheep | 5,339 |
76
- | Muffin | 5,320 |
77
- | Panther | 5,112 |
78
- | Sanic | 3,660 |
79
- | FastAPI | 3,260 |
80
- | Tornado | 2,081 |
81
- | Bottle | 2,045 |
82
- | Django | 821 |
83
- | Flask | 749 |
84
-
85
-
86
- > **More Detail:** https://GitHub.com/PantherPy/frameworks-benchmark
87
-
88
- ---
89
-
90
71
  ### Installation
91
72
  ```shell
92
73
  $ pip install panther
@@ -105,7 +86,7 @@ $ pip install panther
105
86
  ```shell
106
87
  $ panther run --reload
107
88
  ```
108
- _* Panther uses [Uvicorn](https://github.com/encode/uvicorn) as ASGI (Asynchronous Server Gateway Interface) but you can run the project with [Granian](https://pypi.org/project/granian/), [daphne](https://pypi.org/project/daphne/) or any ASGI server too_
89
+ _* Panther uses [Uvicorn](https://github.com/encode/uvicorn) as ASGI (Asynchronous Server Gateway Interface) but you can run the project with [Granian](https://pypi.org/project/granian/), [daphne](https://pypi.org/project/daphne/) or any ASGI server_
109
90
 
110
91
  - #### Monitoring Requests
111
92
 
@@ -1,26 +1,26 @@
1
- panther/__init__.py,sha256=0iWjkkDL_Hzzizq4nbcZTNPUIHXJSl9kdGprGpK2cNs,110
1
+ panther/__init__.py,sha256=y4niTNbN1Jwrm8eXNSrzLlVOGoajNgm3E6I1fs2i7QA,110
2
2
  panther/_load_configs.py,sha256=AVkoixkUFkBQiTmrLrwCmg0eiPW2U_Uw2EGNEGQRfnI,9281
3
3
  panther/_utils.py,sha256=xeVR0yHvczhXv2XXrpoa6SHpGTDTFxNxiemXTdbsqjM,4279
4
- panther/app.py,sha256=X4vmpj5QuEt10iSVso89hGfyw75vemWtHPtdrXX4nD4,7276
4
+ panther/app.py,sha256=a0QLpxM5fwLyWDHZjVaYrLw0_Rfx3p2NnZWcfyCzwf4,7267
5
5
  panther/authentications.py,sha256=gf7BVyQ8vXKhiumJAtD0aAK7uIHWx_snbOKYAKrYuVw,5677
6
6
  panther/background_tasks.py,sha256=HBYubDIiO_673cl_5fqCUP9zzimzRgRkDSkag9Msnbs,7656
7
- panther/base_request.py,sha256=w48-1gQzJi5m9AUZzspdugffUS73lZ8Lw0N9AND_XDM,4064
8
- panther/base_websocket.py,sha256=svVAT06YZXkiSV15lO509l_S3o4Pm--sxtbnXCSGszo,10585
7
+ panther/base_request.py,sha256=Fwwpm-9bjAZdpzSdakmSas5BD3gh1nrc6iGcBxwa_94,4001
8
+ panther/base_websocket.py,sha256=hJN_ItUGLpk0QMWrExlDHQahiu7hYgc_jVvHWxqqpq4,10547
9
9
  panther/caching.py,sha256=ltuJYdjNiAaKIs3jpO5EBpL8Y6CF1vAIQqh8J_Np10g,4098
10
10
  panther/configs.py,sha256=EaLApT6nYcguBoNXBG_8n6DU6HTNxsulI2943j8UAkE,3174
11
11
  panther/events.py,sha256=bxDqrfiNNBlvD03vEk2LDK4xbMzTMFVcgAjx2ein7mI,1158
12
12
  panther/exceptions.py,sha256=7rHdJIES2__kqOStIqbHl3Uxask2lzKgLQlkZvvDwFA,1591
13
13
  panther/file_handler.py,sha256=XnomEigCUYOaXjkH4kD1kzpUbL2i9lLnR5kerruF6BA,846
14
- panther/generics.py,sha256=eR97UgsH2WdqWTR5aw94niIJwIAlhgAz2wNVz_N3vIk,6311
14
+ panther/generics.py,sha256=HGbCGvHEpbWaY4MrtI_Wphkm9YEGR8PV_X9uERSRGVI,6576
15
15
  panther/logging.py,sha256=t0nQXsSIwIxShqFnjRGp6lhO4Ybf1SnwJraDSTqMHFM,2211
16
- panther/main.py,sha256=9LScJXNsj_LEeX1OrsHKWqKDFNA8UKn-Fr9rAXZN-mQ,9296
16
+ panther/main.py,sha256=UbIxwaojvY_vH9nYfBpkulRBqVEj4Lbl81Er4XW_KCY,9334
17
17
  panther/monitoring.py,sha256=y1F3c8FJlnmooM-m1nSyOTa9eWq0v1nHnmw9zz-4Kls,1314
18
18
  panther/pagination.py,sha256=efpsWMgLBaTWXhnhMAf6fyIrGTmVOFbmHpX03GgEJh0,1574
19
19
  panther/permissions.py,sha256=9-J5vzvEKa_PITwEVQbZZv8PG2FOu05YBlD5yMrKcfc,348
20
20
  panther/request.py,sha256=F9ZiAWSse7_6moAzqdoFInUN4zTKlzijh9AdU9w3Jfw,1673
21
- panther/response.py,sha256=OvuxKueM5FJ7gg9icsR5NjVxkU9_4dsgShcOJFMcdaY,6458
21
+ panther/response.py,sha256=G0Ychc3Tp5UANtmst7o4npOrmdca6kKC_HYYoeYWLUQ,7266
22
22
  panther/routings.py,sha256=1eqbjubLnUUEQRlz8mIF464ImvCMjyasiekHBtxEQoQ,6218
23
- panther/serializer.py,sha256=dvPbDf6qICMuhTtX1ZXW0G5_BCHcLwughGOb1w3umXo,9002
23
+ panther/serializer.py,sha256=MBT43UG8YBjp-UGaqe5-SPqQHIcDEjLAdBjHAVKyMJo,9059
24
24
  panther/status.py,sha256=Gc_PnYrHfInTsZpGbqiCfDB-py1C7Rh8KMdb6Lq9Exs,3346
25
25
  panther/test.py,sha256=RsQtP5IURLWR__BihOjruWoX3NscmGDqDqj1CfAb3bI,7037
26
26
  panther/throttling.py,sha256=mVa_mGv6w_Ad7LLtV4eG5QpDwwNsk4QjFFi0mIHQBnE,231
@@ -49,9 +49,9 @@ panther/panel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
49
  panther/panel/apis.py,sha256=COsbwKZyTgyHvHYbpDfusifAH9ojMS3z1KhZCt9M-Ms,2428
50
50
  panther/panel/urls.py,sha256=JiV-H4dWE-m_bfaTTVxzOxTvJmOWhyLOvcbM7xU3Bn4,240
51
51
  panther/panel/utils.py,sha256=0Rv79oR5IEqalqwpRKQHMn1p5duVY5mxMqDKiA5mWx4,437
52
- panther-4.0.0.dist-info/LICENSE,sha256=2aF1hL2aC0zRPjzUkSxJUzZbn2_uLoOkn7DHjzZni-I,1524
53
- panther-4.0.0.dist-info/METADATA,sha256=F2RJJ4ZFjxJF_CZJm2Z9b03wW-wlZLZtC0xXHcOK17g,6968
54
- panther-4.0.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
55
- panther-4.0.0.dist-info/entry_points.txt,sha256=6GPxYFGuzVfNB4YpHFJvYex6iWah5_tLnirAHwj2Qsg,51
56
- panther-4.0.0.dist-info/top_level.txt,sha256=VbBs02JGXTIoHMzsX-eLOk2MCbBZzQbLhWiYpI7xI2g,8
57
- panther-4.0.0.dist-info/RECORD,,
52
+ panther-4.1.0.dist-info/LICENSE,sha256=2aF1hL2aC0zRPjzUkSxJUzZbn2_uLoOkn7DHjzZni-I,1524
53
+ panther-4.1.0.dist-info/METADATA,sha256=oxZtMCsV7y5Hdhejw9V3NTrXaOof5nz0jLlvHDeE2pA,6376
54
+ panther-4.1.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
55
+ panther-4.1.0.dist-info/entry_points.txt,sha256=6GPxYFGuzVfNB4YpHFJvYex6iWah5_tLnirAHwj2Qsg,51
56
+ panther-4.1.0.dist-info/top_level.txt,sha256=VbBs02JGXTIoHMzsX-eLOk2MCbBZzQbLhWiYpI7xI2g,8
57
+ panther-4.1.0.dist-info/RECORD,,