panther 4.3.7__py3-none-any.whl → 5.0.0b2__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 (59) hide show
  1. panther/__init__.py +1 -1
  2. panther/_load_configs.py +78 -64
  3. panther/_utils.py +1 -1
  4. panther/app.py +126 -60
  5. panther/authentications.py +26 -9
  6. panther/base_request.py +27 -2
  7. panther/base_websocket.py +26 -27
  8. panther/cli/create_command.py +1 -0
  9. panther/cli/main.py +19 -27
  10. panther/cli/monitor_command.py +8 -4
  11. panther/cli/template.py +11 -6
  12. panther/cli/utils.py +3 -2
  13. panther/configs.py +7 -9
  14. panther/db/cursor.py +23 -7
  15. panther/db/models.py +26 -19
  16. panther/db/queries/base_queries.py +1 -1
  17. panther/db/queries/mongodb_queries.py +177 -13
  18. panther/db/queries/pantherdb_queries.py +5 -5
  19. panther/db/queries/queries.py +1 -1
  20. panther/events.py +10 -4
  21. panther/exceptions.py +24 -2
  22. panther/generics.py +2 -2
  23. panther/main.py +90 -117
  24. panther/middlewares/__init__.py +1 -1
  25. panther/middlewares/base.py +15 -19
  26. panther/middlewares/monitoring.py +42 -0
  27. panther/openapi/__init__.py +1 -0
  28. panther/openapi/templates/openapi.html +27 -0
  29. panther/openapi/urls.py +5 -0
  30. panther/openapi/utils.py +167 -0
  31. panther/openapi/views.py +101 -0
  32. panther/pagination.py +1 -1
  33. panther/panel/middlewares.py +10 -0
  34. panther/panel/templates/base.html +14 -0
  35. panther/panel/templates/create.html +21 -0
  36. panther/panel/templates/create.js +1270 -0
  37. panther/panel/templates/detail.html +55 -0
  38. panther/panel/templates/home.html +9 -0
  39. panther/panel/templates/home.js +30 -0
  40. panther/panel/templates/login.html +47 -0
  41. panther/panel/templates/sidebar.html +13 -0
  42. panther/panel/templates/table.html +73 -0
  43. panther/panel/templates/table.js +339 -0
  44. panther/panel/urls.py +10 -5
  45. panther/panel/utils.py +98 -0
  46. panther/panel/views.py +143 -0
  47. panther/request.py +3 -0
  48. panther/response.py +91 -53
  49. panther/routings.py +7 -2
  50. panther/serializer.py +1 -1
  51. panther/utils.py +34 -26
  52. panther/websocket.py +3 -0
  53. {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/METADATA +19 -17
  54. panther-5.0.0b2.dist-info/RECORD +75 -0
  55. {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/WHEEL +1 -1
  56. panther-4.3.7.dist-info/RECORD +0 -57
  57. {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/entry_points.txt +0 -0
  58. {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/licenses/LICENSE +0 -0
  59. {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/top_level.txt +0 -0
panther/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from panther.main import Panther # noqa: F401
2
2
 
3
- __version__ = '4.3.7'
3
+ __version__ = '5.0.0beta2'
4
4
 
5
5
 
6
6
  def version():
panther/_load_configs.py CHANGED
@@ -16,7 +16,8 @@ from panther.db.queries.mongodb_queries import BaseMongoDBQuery
16
16
  from panther.db.queries.pantherdb_queries import BasePantherDBQuery
17
17
  from panther.exceptions import PantherError
18
18
  from panther.middlewares.base import WebsocketMiddleware, HTTPMiddleware
19
- from panther.panel.urls import urls as panel_urls
19
+ from panther.middlewares.monitoring import MonitoringMiddleware, WebsocketMonitoringMiddleware
20
+ from panther.panel.views import HomeView
20
21
  from panther.routings import finalize_urls, flatten_urls
21
22
 
22
23
  __all__ = (
@@ -27,7 +28,6 @@ __all__ = (
27
28
  'load_timezone',
28
29
  'load_database',
29
30
  'load_secret_key',
30
- 'load_monitoring',
31
31
  'load_throttling',
32
32
  'load_user_model',
33
33
  'load_log_queries',
@@ -36,8 +36,8 @@ __all__ = (
36
36
  'load_auto_reformat',
37
37
  'load_background_tasks',
38
38
  'load_default_cache_exp',
39
- 'load_authentication_class',
40
39
  'load_urls',
40
+ 'load_authentication_class',
41
41
  'load_websocket_connections',
42
42
  'check_endpoints_inheritance',
43
43
  )
@@ -94,7 +94,15 @@ def load_templates_dir(_configs: dict, /) -> None:
94
94
  if config.TEMPLATES_DIR == '.':
95
95
  config.TEMPLATES_DIR = config.BASE_DIR
96
96
 
97
- config.JINJA_ENVIRONMENT = jinja2.Environment(loader=jinja2.FileSystemLoader(config.TEMPLATES_DIR))
97
+ config.JINJA_ENVIRONMENT = jinja2.Environment(
98
+ loader=jinja2.ChoiceLoader(
99
+ loaders=(
100
+ jinja2.FileSystemLoader(searchpath=config.TEMPLATES_DIR),
101
+ jinja2.PackageLoader(package_name='panther', package_path='panel/templates/'),
102
+ jinja2.PackageLoader(package_name='panther', package_path='openapi/templates/'),
103
+ )
104
+ )
105
+ )
98
106
 
99
107
 
100
108
  def load_database(_configs: dict, /) -> None:
@@ -126,11 +134,6 @@ def load_secret_key(_configs: dict, /) -> None:
126
134
  config.SECRET_KEY = secret_key.encode()
127
135
 
128
136
 
129
- def load_monitoring(_configs: dict, /) -> None:
130
- if _configs.get('MONITORING'):
131
- config.MONITORING = True
132
-
133
-
134
137
  def load_throttling(_configs: dict, /) -> None:
135
138
  if throttling := _configs.get('THROTTLING'):
136
139
  config.THROTTLING = throttling
@@ -147,42 +150,45 @@ def load_log_queries(_configs: dict, /) -> None:
147
150
 
148
151
 
149
152
  def load_middlewares(_configs: dict, /) -> None:
150
- from panther.middlewares import BaseMiddleware
151
-
152
153
  middlewares = {'http': [], 'ws': []}
153
154
 
154
155
  # Collect Middlewares
155
156
  for middleware in _configs.get('MIDDLEWARES') or []:
156
- if not isinstance(middleware, list | tuple):
157
- path_or_type = middleware
158
- data = {}
159
-
160
- elif len(middleware) == 1:
161
- path_or_type = middleware[0]
162
- data = {}
163
-
164
- elif len(middleware) > 2:
165
- raise _exception_handler(field='MIDDLEWARES', error=f'{middleware} too many arguments')
166
-
167
- else:
168
- path_or_type, data = middleware
169
-
170
- if callable(path_or_type):
171
- middleware_class = path_or_type
172
- else:
157
+ # This block is for Backward Compatibility
158
+ if isinstance(middleware, list | tuple):
159
+ if len(middleware) == 1:
160
+ middleware = middleware[0]
161
+ elif len(middleware) == 2:
162
+ _deprecated_warning(
163
+ field='MIDDLEWARES',
164
+ message='`data` does not supported in middlewares anymore, as your data is static you may want '
165
+ 'to pass them to your middleware with config variables'
166
+ )
167
+ middleware = middleware[0]
168
+ else:
169
+ raise _exception_handler(
170
+ field='MIDDLEWARES', error=f'{middleware} should be dotted path or type of a middleware class')
171
+
172
+ # `middleware` can be type or path of a class
173
+ if not callable(middleware):
173
174
  try:
174
- middleware_class = import_class(path_or_type)
175
+ middleware = import_class(middleware)
175
176
  except (AttributeError, ModuleNotFoundError):
176
- raise _exception_handler(field='MIDDLEWARES', error=f'{path_or_type} is not a valid middleware path')
177
+ raise _exception_handler(
178
+ field='MIDDLEWARES', error=f'{middleware} is not a valid middleware path or type')
177
179
 
178
- if issubclass(middleware_class, BaseMiddleware) is False:
179
- raise _exception_handler(field='MIDDLEWARES', error='is not a sub class of BaseMiddleware')
180
+ if issubclass(middleware, (MonitoringMiddleware, WebsocketMonitoringMiddleware)):
181
+ config.MONITORING = True
180
182
 
181
- if middleware_class.__bases__[0] in (BaseMiddleware, HTTPMiddleware):
182
- middlewares['http'].append((middleware_class, data))
183
-
184
- if middleware_class.__bases__[0] in (BaseMiddleware, WebsocketMiddleware):
185
- middlewares['ws'].append((middleware_class, data))
183
+ if issubclass(middleware, HTTPMiddleware):
184
+ middlewares['http'].append(middleware)
185
+ elif issubclass(middleware, WebsocketMiddleware):
186
+ middlewares['ws'].append(middleware)
187
+ else:
188
+ raise _exception_handler(
189
+ field='MIDDLEWARES',
190
+ error='is not a sub class of `HTTPMiddleware` or `WebsocketMiddleware`'
191
+ )
186
192
 
187
193
  config.HTTP_MIDDLEWARES = middlewares['http']
188
194
  config.WS_MIDDLEWARES = middlewares['ws']
@@ -204,32 +210,6 @@ def load_default_cache_exp(_configs: dict, /) -> None:
204
210
  config.DEFAULT_CACHE_EXP = default_cache_exp
205
211
 
206
212
 
207
- def load_authentication_class(_configs: dict, /) -> None:
208
- """Should be after `load_secret_key()`"""
209
- if authentication := _configs.get('AUTHENTICATION'):
210
- config.AUTHENTICATION = import_class(authentication)
211
-
212
- if ws_authentication := _configs.get('WS_AUTHENTICATION'):
213
- config.WS_AUTHENTICATION = import_class(ws_authentication)
214
-
215
- load_jwt_config(_configs)
216
-
217
-
218
- def load_jwt_config(_configs: dict, /) -> None:
219
- """Only Collect JWT Config If Authentication Is JWTAuthentication"""
220
- auth_is_jwt = (
221
- getattr(config.AUTHENTICATION, '__name__', None) == 'JWTAuthentication' or
222
- getattr(config.WS_AUTHENTICATION, '__name__', None) == 'QueryParamJWTAuthentication'
223
- )
224
- jwt = _configs.get('JWTConfig', {})
225
- if auth_is_jwt or jwt:
226
- if 'key' not in jwt:
227
- if config.SECRET_KEY is None:
228
- raise _exception_handler(field='JWTConfig', error='`JWTConfig.key` or `SECRET_KEY` is required.')
229
- jwt['key'] = config.SECRET_KEY.decode()
230
- config.JWT_CONFIG = JWTConfig(**jwt)
231
-
232
-
233
213
  def load_urls(_configs: dict, /, urls: dict | None) -> None:
234
214
  """
235
215
  Return tuple of all urls (as a flat dict) and (as a nested dict)
@@ -261,7 +241,34 @@ def load_urls(_configs: dict, /, urls: dict | None) -> None:
261
241
 
262
242
  config.FLAT_URLS = flatten_urls(urls)
263
243
  config.URLS = finalize_urls(config.FLAT_URLS)
264
- config.URLS['_panel'] = finalize_urls(flatten_urls(panel_urls))
244
+
245
+
246
+ def load_authentication_class(_configs: dict, /) -> None:
247
+ """Should be after `load_secret_key()` and `load_urls()`"""
248
+ if authentication := _configs.get('AUTHENTICATION'):
249
+ config.AUTHENTICATION = import_class(authentication)
250
+
251
+ if ws_authentication := _configs.get('WS_AUTHENTICATION'):
252
+ config.WS_AUTHENTICATION = import_class(ws_authentication)
253
+
254
+ load_jwt_config(_configs)
255
+
256
+
257
+ def load_jwt_config(_configs: dict, /) -> None:
258
+ """Only Collect JWT Config If Authentication Is JWTAuthentication"""
259
+ auth_is_jwt = (
260
+ getattr(config.AUTHENTICATION, '__name__', None) == 'JWTAuthentication' or
261
+ getattr(config.WS_AUTHENTICATION, '__name__', None) == 'QueryParamJWTAuthentication'
262
+ )
263
+ jwt = _configs.get('JWTConfig', {})
264
+
265
+ using_panel_views = HomeView in config.FLAT_URLS.values()
266
+ if auth_is_jwt or using_panel_views:
267
+ if 'key' not in jwt:
268
+ if config.SECRET_KEY is None:
269
+ raise _exception_handler(field='JWTConfig', error='`JWTConfig.key` or `SECRET_KEY` is required.')
270
+ jwt['key'] = config.SECRET_KEY.decode()
271
+ config.JWT_CONFIG = JWTConfig(**jwt)
265
272
 
266
273
 
267
274
  def load_websocket_connections():
@@ -281,6 +288,9 @@ def load_websocket_connections():
281
288
  def check_endpoints_inheritance():
282
289
  """Should be after `load_urls()`"""
283
290
  for _, endpoint in config.FLAT_URLS.items():
291
+ if endpoint == {}:
292
+ continue
293
+
284
294
  if isinstance(endpoint, types.FunctionType):
285
295
  check_function_type_endpoint(endpoint=endpoint)
286
296
  else:
@@ -289,3 +299,7 @@ def check_endpoints_inheritance():
289
299
 
290
300
  def _exception_handler(field: str, error: str | Exception) -> PantherError:
291
301
  return PantherError(f"Invalid '{field}': {error}")
302
+
303
+
304
+ def _deprecated_warning(field: str, message: str):
305
+ return logger.warning(f"DEPRECATED '{field}': {message}")
panther/_utils.py CHANGED
@@ -10,7 +10,6 @@ from typing import Any, Generator, Iterator, AsyncGenerator
10
10
 
11
11
  from panther.exceptions import PantherError
12
12
  from panther.file_handler import File
13
- from panther.websocket import GenericWebsocket
14
13
 
15
14
  logger = logging.getLogger('panther')
16
15
 
@@ -100,6 +99,7 @@ def check_function_type_endpoint(endpoint: types.FunctionType) -> Callable:
100
99
 
101
100
  def check_class_type_endpoint(endpoint: Callable) -> Callable:
102
101
  from panther.app import GenericAPI
102
+ from panther.websocket import GenericWebsocket
103
103
 
104
104
  if not issubclass(endpoint, (GenericAPI, GenericWebsocket)):
105
105
  raise PantherError(
panther/app.py CHANGED
@@ -1,7 +1,9 @@
1
1
  import functools
2
2
  import logging
3
+ import traceback
4
+ import typing
3
5
  from datetime import timedelta
4
- from typing import Literal
6
+ from typing import Literal, Callable
5
7
 
6
8
  from orjson import JSONDecodeError
7
9
  from pydantic import ValidationError, BaseModel
@@ -22,6 +24,10 @@ from panther.exceptions import (
22
24
  ThrottlingAPIError,
23
25
  BadRequestAPIError
24
26
  )
27
+ from panther.exceptions import PantherError
28
+ from panther.middlewares import HTTPMiddleware
29
+ from panther.openapi import OutputSchema
30
+ from panther.permissions import BasePermission
25
31
  from panther.request import Request
26
32
  from panther.response import Response
27
33
  from panther.serializer import ModelSerializer
@@ -33,87 +39,127 @@ logger = logging.getLogger('panther')
33
39
 
34
40
 
35
41
  class API:
42
+ """
43
+ input_model: The `request.data` will be validated with this attribute, It will raise an
44
+ `panther.exceptions.BadRequestAPIError` or put the validated data in the `request.validated_data`.
45
+ output_schema: This attribute only used in creation of OpenAPI scheme which is available in `panther.openapi.urls`
46
+ You may want to add its `url` to your urls.
47
+ auth: It will authenticate the user with header of its request or raise an
48
+ `panther.exceptions.AuthenticationAPIError`.
49
+ permissions: List of permissions that will be called sequentially after authentication to authorize the user.
50
+ throttling: It will limit the users' request on a specific (time-bucket, path)
51
+ cache: Response of the request will be cached.
52
+ cache_exp_time: Specify the expiry time of the cache. (default is `config.DEFAULT_CACHE_EXP`)
53
+ methods: Specify the allowed methods.
54
+ middlewares: These middlewares have inner priority than global middlewares.
55
+ """
56
+ func: Callable
57
+
36
58
  def __init__(
37
59
  self,
38
60
  *,
61
+ methods: list[Literal['GET', 'POST', 'PUT', 'PATCH', 'DELETE']] | None = None,
39
62
  input_model: type[ModelSerializer] | type[BaseModel] | None = None,
40
- output_model: type[ModelSerializer] | type[BaseModel] | None = None,
63
+ output_model: type[BaseModel] | None = None,
64
+ output_schema: OutputSchema | None = None,
41
65
  auth: bool = False,
42
- permissions: list | None = None,
66
+ permissions: list[BasePermission] | None = None,
43
67
  throttling: Throttling | None = None,
44
68
  cache: bool = False,
45
69
  cache_exp_time: timedelta | int | None = None,
46
- methods: list[Literal['GET', 'POST', 'PUT', 'PATCH', 'DELETE']] | None = None,
70
+ middlewares: list[HTTPMiddleware] | None = None,
47
71
  ):
72
+ self.methods = {m.upper() for m in methods} if methods else None
48
73
  self.input_model = input_model
49
- self.output_model = output_model
74
+ self.output_schema = output_schema
50
75
  self.auth = auth
51
76
  self.permissions = permissions or []
52
77
  self.throttling = throttling
53
78
  self.cache = cache
54
- self.cache_exp_time = cache_exp_time # or config.DEFAULT_CACHE_EXP
55
- self.methods = methods
79
+ self.cache_exp_time = cache_exp_time
80
+ self.middlewares: list[HTTPMiddleware] | None = middlewares
56
81
  self.request: Request | None = None
82
+ if output_model:
83
+ deprecation_message = (
84
+ traceback.format_stack(limit=2)[0] +
85
+ '\nThe `output_model` argument has been removed in Panther v5 and is no longer available.'
86
+ '\nPlease update your code to use the new approach. More info: '
87
+ 'https://pantherpy.github.io/open_api/'
88
+ )
89
+ raise PantherError(deprecation_message)
57
90
 
58
91
  def __call__(self, func):
92
+ self.func = func
93
+
59
94
  @functools.wraps(func)
60
95
  async def wrapper(request: Request) -> Response:
61
- self.request = request
96
+ chained_func = self.handle_endpoint
97
+ if self.middlewares:
98
+ for middleware in reversed(self.middlewares):
99
+ chained_func = middleware(chained_func)
100
+ return await chained_func(request=request)
101
+
102
+ # Store attributes on the function, so have the same behaviour as class-based (useful in `openapi.view.OpenAPI`)
103
+ wrapper.auth = self.auth
104
+ wrapper.methods = self.methods
105
+ wrapper.permissions = self.permissions
106
+ wrapper.input_model = self.input_model
107
+ wrapper.output_schema = self.output_schema
108
+ return wrapper
62
109
 
63
- # 0. Preflight
64
- if self.request.method == 'OPTIONS':
65
- return self.options()
110
+ async def handle_endpoint(self, request: Request) -> Response:
111
+ self.request = request
66
112
 
67
- # 1. Check Method
68
- if self.methods and self.request.method not in self.methods:
69
- raise MethodNotAllowedAPIError
113
+ # 0. Preflight
114
+ if self.request.method == 'OPTIONS':
115
+ return self.options()
70
116
 
71
- # 2. Authentication
72
- await self.handle_authentication()
117
+ # 1. Check Method
118
+ if self.methods and self.request.method not in self.methods:
119
+ raise MethodNotAllowedAPIError
73
120
 
74
- # 3. Permissions
75
- await self.handle_permission()
121
+ # 2. Authentication
122
+ await self.handle_authentication()
76
123
 
77
- # 4. Throttling
78
- await self.handle_throttling()
124
+ # 3. Permissions
125
+ await self.handle_permission()
79
126
 
80
- # 5. Validate Input
81
- if self.request.method in ['POST', 'PUT', 'PATCH']:
82
- self.handle_input_validation()
127
+ # 4. Throttling
128
+ await self.handle_throttling()
83
129
 
84
- # 6. Get Cached Response
85
- if self.cache and self.request.method == 'GET':
86
- if cached := await get_response_from_cache(request=self.request, cache_exp_time=self.cache_exp_time):
87
- return Response(data=cached.data, headers=cached.headers, status_code=cached.status_code)
130
+ # 5. Validate Input
131
+ if self.request.method in {'POST', 'PUT', 'PATCH'}:
132
+ self.handle_input_validation()
88
133
 
89
- # 7. Put PathVariables and Request(If User Wants It) In kwargs
90
- kwargs = self.request.clean_parameters(func)
134
+ # 6. Get Cached Response
135
+ if self.cache and self.request.method == 'GET':
136
+ if cached := await get_response_from_cache(request=self.request, cache_exp_time=self.cache_exp_time):
137
+ return Response(data=cached.data, headers=cached.headers, status_code=cached.status_code)
91
138
 
92
- # 8. Call Endpoint
93
- if is_function_async(func):
94
- response = await func(**kwargs)
95
- else:
96
- response = func(**kwargs)
139
+ # 7. Put PathVariables and Request(If User Wants It) In kwargs
140
+ kwargs = self.request.clean_parameters(self.func)
97
141
 
98
- # 9. Clean Response
99
- if not isinstance(response, Response):
100
- response = Response(data=response)
101
- if self.output_model and response.data:
102
- response.data = await response.apply_output_model(output_model=self.output_model)
103
- if response.pagination:
104
- response.data = await response.pagination.template(response.data)
142
+ # 8. Call Endpoint
143
+ if is_function_async(self.func):
144
+ response = await self.func(**kwargs)
145
+ else:
146
+ response = self.func(**kwargs)
105
147
 
106
- # 10. Set New Response To Cache
107
- if self.cache and self.request.method == 'GET':
108
- await set_response_in_cache(request=self.request, response=response, cache_exp_time=self.cache_exp_time)
148
+ # 9. Clean Response
149
+ if not isinstance(response, Response):
150
+ response = Response(data=response)
151
+ if response.pagination:
152
+ response.data = await response.pagination.template(response.data)
109
153
 
110
- # 11. Warning CacheExpTime
111
- if self.cache_exp_time and self.cache is False:
112
- logger.warning('"cache_exp_time" won\'t work while "cache" is False')
154
+ # 10. Set New Response To Cache
155
+ if self.cache and self.request.method == 'GET':
156
+ await set_response_in_cache(request=self.request, response=response, cache_exp_time=self.cache_exp_time)
113
157
 
114
- return response
158
+ # 11. Warning CacheExpTime
159
+ if self.cache_exp_time and self.cache is False:
160
+ logger.warning('"cache_exp_time" won\'t work while "cache" is False')
115
161
 
116
- return wrapper
162
+ return response
117
163
 
118
164
  async def handle_authentication(self) -> None:
119
165
  if self.auth:
@@ -144,7 +190,7 @@ class API:
144
190
  @classmethod
145
191
  def options(cls):
146
192
  headers = {
147
- 'Access-Control-Allow-Methods': 'DELETE, GET, PATCH, POST, PUT, OPTIONS',
193
+ 'Access-Control-Allow-Methods': 'DELETE, GET, PATCH, POST, PUT, OPTIONS, HEAD',
148
194
  'Access-Control-Allow-Headers': 'Accept, Authorization, User-Agent, Content-Type',
149
195
  }
150
196
  return Response(headers=headers)
@@ -165,14 +211,39 @@ class API:
165
211
  raise JSONDecodeAPIError
166
212
 
167
213
 
168
- class GenericAPI:
214
+ class MetaGenericAPI(type):
215
+ def __new__(
216
+ cls,
217
+ cls_name: str,
218
+ bases: tuple[type[typing.Any], ...],
219
+ namespace: dict[str, typing.Any],
220
+ **kwargs
221
+ ):
222
+ if cls_name == 'GenericAPI':
223
+ return super().__new__(cls, cls_name, bases, namespace)
224
+ if 'output_model' in namespace:
225
+ deprecation_message = (
226
+ traceback.format_stack(limit=2)[0] +
227
+ '\nThe `output_model` argument has been removed in Panther v5 and is no longer available.'
228
+ '\nPlease update your code to use the new approach. More info: '
229
+ 'https://pantherpy.github.io/open_api/'
230
+ )
231
+ raise PantherError(deprecation_message)
232
+ return super().__new__(cls, cls_name, bases, namespace)
233
+
234
+
235
+ class GenericAPI(metaclass=MetaGenericAPI):
236
+ """
237
+ Check out the documentation of `panther.app.API()`.
238
+ """
169
239
  input_model: type[ModelSerializer] | type[BaseModel] | None = None
170
- output_model: type[ModelSerializer] | type[BaseModel] | None = None
240
+ output_schema: OutputSchema | None = None
171
241
  auth: bool = False
172
242
  permissions: list | None = None
173
243
  throttling: Throttling | None = None
174
244
  cache: bool = False
175
245
  cache_exp_time: timedelta | int | None = None
246
+ middlewares: list[HTTPMiddleware] | None = None
176
247
 
177
248
  async def get(self, *args, **kwargs):
178
249
  raise MethodNotAllowedAPIError
@@ -189,12 +260,6 @@ class GenericAPI:
189
260
  async def delete(self, *args, **kwargs):
190
261
  raise MethodNotAllowedAPIError
191
262
 
192
- async def get_input_model(self, request: Request) -> type[ModelSerializer] | type[BaseModel] | None:
193
- return None
194
-
195
- async def get_output_model(self, request: Request) -> type[ModelSerializer] | type[BaseModel] | None:
196
- return None
197
-
198
263
  async def call_method(self, request: Request):
199
264
  match request.method:
200
265
  case 'GET':
@@ -213,11 +278,12 @@ class GenericAPI:
213
278
  raise MethodNotAllowedAPIError
214
279
 
215
280
  return await API(
216
- input_model=self.input_model or await self.get_input_model(request=request),
217
- output_model=self.output_model or await self.get_output_model(request=request),
281
+ input_model=self.input_model,
282
+ output_schema=self.output_schema,
218
283
  auth=self.auth,
219
284
  permissions=self.permissions,
220
285
  throttling=self.throttling,
221
286
  cache=self.cache,
222
287
  cache_exp_time=self.cache_exp_time,
288
+ middlewares=self.middlewares,
223
289
  )(func)(request=request)
@@ -29,9 +29,9 @@ class BaseAuthentication:
29
29
  msg = f'{cls.__name__}.authentication() is not implemented.'
30
30
  raise cls.exception(msg) from None
31
31
 
32
- @staticmethod
33
- def exception(message: str, /) -> type[AuthenticationAPIError]:
34
- logger.error(f'Authentication Error: "{message}"')
32
+ @classmethod
33
+ def exception(cls, message: str | Exception, /) -> type[AuthenticationAPIError]:
34
+ logger.error(f'{cls.__name__} Error: "{message}"')
35
35
  return AuthenticationAPIError
36
36
 
37
37
 
@@ -151,16 +151,33 @@ class JWTAuthentication(BaseAuthentication):
151
151
  key = generate_hash_value_from_string(token)
152
152
  return bool(await redis.exists(key))
153
153
 
154
- @staticmethod
155
- def exception(message: str | JWTError | UnicodeEncodeError, /) -> type[AuthenticationAPIError]:
156
- logger.error(f'JWT Authentication Error: "{message}"')
157
- return AuthenticationAPIError
158
-
159
154
 
160
155
  class QueryParamJWTAuthentication(JWTAuthentication):
161
156
  @classmethod
162
157
  def get_authorization_header(cls, request: Request | Websocket) -> str:
163
158
  if auth := request.query_params.get('authorization'):
164
159
  return auth
165
- msg = 'Authorization is required'
160
+ msg = '`authorization` query param not found.'
161
+ raise cls.exception(msg) from None
162
+
163
+
164
+ class CookieJWTAuthentication(JWTAuthentication):
165
+ @classmethod
166
+ def get_authorization_header(cls, request: Request | Websocket) -> str:
167
+ if token := request.headers.get_cookies().get('access_token'):
168
+ return token
169
+ msg = '`access_token` Cookie not found.'
166
170
  raise cls.exception(msg) from None
171
+
172
+ @classmethod
173
+ async def authentication(cls, request: Request | Websocket) -> Model:
174
+ token = cls.get_authorization_header(request)
175
+
176
+ if redis.is_connected and await cls._check_in_cache(token=token):
177
+ msg = 'User logged out'
178
+ raise cls.exception(msg) from None
179
+
180
+ payload = cls.decode_jwt(token)
181
+ user = await cls.get_user(payload)
182
+ user._auth_token = token
183
+ return user
panther/base_request.py CHANGED
@@ -1,4 +1,3 @@
1
- from collections import namedtuple
2
1
  from collections.abc import Callable
3
2
  from urllib.parse import parse_qsl
4
3
 
@@ -46,14 +45,40 @@ class Headers:
46
45
  items = ', '.join(f'{k}={v}' for k, v in self.__headers.items())
47
46
  return f'Headers({items})'
48
47
 
48
+ def __contains__(self, item):
49
+ return (item in self.__headers) or (item in self.__pythonic_headers)
50
+
49
51
  __repr__ = __str__
50
52
 
51
53
  @property
52
54
  def __dict__(self):
53
55
  return self.__headers
54
56
 
57
+ def get_cookies(self) -> dict:
58
+ """
59
+ request.headers.cookie:
60
+ 'csrftoken=aaa; sessionid=bbb; access_token=ccc; refresh_token=ddd'
61
+
62
+ request.headers.get_cookies():
63
+ {
64
+ 'csrftoken': 'aaa',
65
+ 'sessionid': 'bbb',
66
+ 'access_token': 'ccc',
67
+ 'refresh_token': 'ddd',
68
+ }
69
+ """
70
+ if self.cookie:
71
+ return {k.strip(): v for k, v in (c.split('=', maxsplit=1) for c in self.cookie.split(';'))}
72
+ return {}
73
+
74
+
75
+ class Address:
76
+ def __init__(self, ip, port):
77
+ self.ip = ip
78
+ self.port = port
55
79
 
56
- Address = namedtuple('Address', ['ip', 'port'])
80
+ def __str__(self):
81
+ return f'{self.ip}:{self.port}'
57
82
 
58
83
 
59
84
  class BaseRequest: