panther 4.0.1__py3-none-any.whl → 4.1.1__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.1'
3
+ __version__ = '4.1.1'
4
4
 
5
5
 
6
6
  def version():
panther/app.py CHANGED
@@ -95,7 +95,9 @@ 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
+ if response.pagination:
100
+ response.data = await response.pagination.template(response.data)
99
101
 
100
102
  # 10. Set New Response To Cache
101
103
  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/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, pagination = await self.prepare_cursor(request=request, **kwargs)
69
+ return Response(data=cursor, pagination=pagination, status_code=status.HTTP_200_OK)
70
+
71
+ async def prepare_cursor(self, request: Request, **kwargs) -> tuple[Cursor | PantherDBCursor, Pagination | None]:
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)
@@ -79,9 +83,9 @@ class ListAPI(GenericAPI, ObjectsRequired):
79
83
  cursor = cursor.sort(sort)
80
84
 
81
85
  if pagination := self.process_pagination(query_params=request.query_params, cursor=cursor):
82
- cursor = await pagination.paginate()
86
+ cursor = pagination.paginate()
83
87
 
84
- return Response(data=cursor, status_code=status.HTTP_200_OK)
88
+ return cursor, pagination
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/pagination.py CHANGED
@@ -36,7 +36,10 @@ class Pagination:
36
36
  previous_skip = max(self.skip - self.limit, 0)
37
37
  return f'?limit={self.limit}&skip={previous_skip}'
38
38
 
39
- async def paginate(self):
39
+ def paginate(self):
40
+ return self.cursor.skip(skip=self.skip).limit(limit=self.limit)
41
+
42
+ async def template(self, response: list):
40
43
  count = await self.cursor.cls.count(self.cursor.filter)
41
44
  has_next = not bool(self.limit + self.skip >= count)
42
45
 
@@ -44,5 +47,5 @@ class Pagination:
44
47
  'count': count,
45
48
  'next': self.build_next_params() if has_next else None,
46
49
  'previous': self.build_previous_params() if self.skip else None,
47
- 'results': self.cursor.skip(skip=self.skip).limit(limit=self.limit)
50
+ 'results': response
48
51
  }
panther/response.py CHANGED
@@ -1,18 +1,18 @@
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
11
10
  from panther.db.cursor import Cursor
12
11
  from pantherdb import Cursor as PantherDBCursor
13
12
  from panther.monitoring import Monitoring
13
+ from panther.pagination import Pagination
14
14
 
15
- ResponseDataTypes = list | tuple | set | Cursor | PantherDBCursor | dict | int | float | str | bool | bytes | NoneType | ModelMetaclass
15
+ ResponseDataTypes = list | tuple | set | Cursor | PantherDBCursor | dict | int | float | str | bool | bytes | NoneType | Type[BaseModel]
16
16
  IterableDataTypes = list | tuple | set | Cursor | PantherDBCursor
17
17
  StreamingDataTypes = Generator | AsyncGenerator
18
18
 
@@ -25,13 +25,20 @@ class Response:
25
25
  data: ResponseDataTypes = None,
26
26
  headers: dict | None = None,
27
27
  status_code: int = status.HTTP_200_OK,
28
+ pagination: Pagination | None = None,
28
29
  ):
29
30
  """
30
31
  :param data: should be an instance of ResponseDataTypes
31
32
  :param headers: should be dict of headers
32
33
  :param status_code: should be int
34
+ :param pagination: instance of Pagination or None
35
+ Its template() method will be used
33
36
  """
34
37
  self.headers = headers or {}
38
+ self.pagination: Pagination | None = pagination
39
+ if isinstance(data, Cursor):
40
+ data = list(data)
41
+ self.initial_data = data
35
42
  self.data = self.prepare_data(data=data)
36
43
  self.status_code = self.check_status_code(status_code=status_code)
37
44
 
@@ -60,7 +67,7 @@ class Response:
60
67
  def headers(self, headers: dict):
61
68
  self._headers = headers
62
69
 
63
- def prepare_data(self, data: any):
70
+ def prepare_data(self, data: Any):
64
71
  """Make sure the response data is only ResponseDataTypes or Iterable of ResponseDataTypes"""
65
72
  if isinstance(data, (int | float | str | bool | bytes | NoneType)):
66
73
  return data
@@ -68,7 +75,7 @@ class Response:
68
75
  elif isinstance(data, dict):
69
76
  return {key: self.prepare_data(value) for key, value in data.items()}
70
77
 
71
- elif issubclass(type(data), PydanticBaseModel):
78
+ elif issubclass(type(data), BaseModel):
72
79
  return data.model_dump()
73
80
 
74
81
  elif isinstance(data, IterableDataTypes):
@@ -79,25 +86,42 @@ class Response:
79
86
  raise TypeError(msg)
80
87
 
81
88
  @classmethod
82
- def check_status_code(cls, status_code: any):
89
+ def check_status_code(cls, status_code: Any):
83
90
  if not isinstance(status_code, int):
84
91
  error = f'Response `status_code` Should Be `int`. (`{status_code}` is {type(status_code)})'
85
92
  raise TypeError(error)
86
93
  return status_code
87
94
 
88
- @classmethod
89
- def apply_output_model(cls, data: any, /, output_model: ModelMetaclass):
95
+ async def apply_output_model(self, output_model: Type[BaseModel]):
90
96
  """This method is called in API.__call__"""
97
+
91
98
  # Dict
92
- if isinstance(data, dict):
99
+ if isinstance(self.data, dict):
100
+ # Apply `validation_alias` (id -> _id)
93
101
  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()
102
+ if field.validation_alias and field_name in self.data:
103
+ self.data[field.validation_alias] = self.data.pop(field_name)
104
+ output = output_model(**self.data)
105
+ if hasattr(output_model, 'prepare_response'):
106
+ return await output.prepare_response(instance=self.initial_data, data=output.model_dump())
107
+ return output.model_dump()
97
108
 
98
109
  # Iterable
99
- if isinstance(data, IterableDataTypes):
100
- return [cls.apply_output_model(d, output_model=output_model) for d in data]
110
+ results = []
111
+ if isinstance(self.data, IterableDataTypes):
112
+ for i, d in enumerate(self.data):
113
+ # Apply `validation_alias` (id -> _id)
114
+ for field_name, field in output_model.model_fields.items():
115
+ if field.validation_alias and field_name in d:
116
+ d[field.validation_alias] = d.pop(field_name)
117
+
118
+ output = output_model(**d)
119
+ if hasattr(output_model, 'prepare_response'):
120
+ result = await output.prepare_response(instance=self.initial_data[i], data=output.model_dump())
121
+ else:
122
+ result = output.model_dump()
123
+ results.append(result)
124
+ return results
101
125
 
102
126
  # Str | Bool | Bytes
103
127
  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.1
3
+ Version: 4.1.1
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=5Kdy8QK5KIDYGPfC-2xwNeQosFAlIMtIBeSSQbwt_u8,110
1
+ panther/__init__.py,sha256=37OBLxeY8rais9kzGaOXnI_uUmSu-VS2X5mzPiPGZa4,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=vb4j8CKidFHD5HIfK1t96fr8URMYxkroW8dQH9SVj14,7385
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
7
+ panther/base_request.py,sha256=Fwwpm-9bjAZdpzSdakmSas5BD3gh1nrc6iGcBxwa_94,4001
8
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=SIK1Wqpfb_jKKt4xJPbYIhMY0QhtbhOXS68dIW4Y0bU,6671
15
15
  panther/logging.py,sha256=t0nQXsSIwIxShqFnjRGp6lhO4Ybf1SnwJraDSTqMHFM,2211
16
16
  panther/main.py,sha256=UbIxwaojvY_vH9nYfBpkulRBqVEj4Lbl81Er4XW_KCY,9334
17
17
  panther/monitoring.py,sha256=y1F3c8FJlnmooM-m1nSyOTa9eWq0v1nHnmw9zz-4Kls,1314
18
- panther/pagination.py,sha256=efpsWMgLBaTWXhnhMAf6fyIrGTmVOFbmHpX03GgEJh0,1574
18
+ panther/pagination.py,sha256=ANJrEF0q1nVAfD33I4nZfUUxFcETzJb01gIhbZX3HEw,1639
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=Njp4zJozNic8J4ucG8Sgh-xeBZOgtoz2cfdDkJlGOWU,7582
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.1.dist-info/LICENSE,sha256=2aF1hL2aC0zRPjzUkSxJUzZbn2_uLoOkn7DHjzZni-I,1524
53
- panther-4.0.1.dist-info/METADATA,sha256=FX3DcJ0oTxuuJFxZNcl2y6XGPJe_nTSFHfn8_Dty64Q,6968
54
- panther-4.0.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
55
- panther-4.0.1.dist-info/entry_points.txt,sha256=6GPxYFGuzVfNB4YpHFJvYex6iWah5_tLnirAHwj2Qsg,51
56
- panther-4.0.1.dist-info/top_level.txt,sha256=VbBs02JGXTIoHMzsX-eLOk2MCbBZzQbLhWiYpI7xI2g,8
57
- panther-4.0.1.dist-info/RECORD,,
52
+ panther-4.1.1.dist-info/LICENSE,sha256=2aF1hL2aC0zRPjzUkSxJUzZbn2_uLoOkn7DHjzZni-I,1524
53
+ panther-4.1.1.dist-info/METADATA,sha256=pFsxiCf8SXniQAHMq7DmwsuAa8EQmBlVFbjkLhfs4wQ,6376
54
+ panther-4.1.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
55
+ panther-4.1.1.dist-info/entry_points.txt,sha256=6GPxYFGuzVfNB4YpHFJvYex6iWah5_tLnirAHwj2Qsg,51
56
+ panther-4.1.1.dist-info/top_level.txt,sha256=VbBs02JGXTIoHMzsX-eLOk2MCbBZzQbLhWiYpI7xI2g,8
57
+ panther-4.1.1.dist-info/RECORD,,