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/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
 
panther/generics.py CHANGED
@@ -116,7 +116,8 @@ class ListAPI(GenericAPI, CursorRequired):
116
116
  if hasattr(self, 'sort_fields') and 'sort' in query_params:
117
117
  return [
118
118
  (field, -1 if param[0] == '-' else 1)
119
- for field in self.sort_fields for param in query_params['sort'].split(',')
119
+ for field in self.sort_fields
120
+ for param in query_params['sort'].split(',')
120
121
  if field == param.removeprefix('-')
121
122
  ]
122
123
 
@@ -129,11 +130,13 @@ class CreateAPI(GenericAPI):
129
130
  input_model: type[ModelSerializer]
130
131
 
131
132
  async def post(self, request: Request, **kwargs):
132
- instance = await request.validated_data.create(validated_data={
133
- field: getattr(request.validated_data, field)
134
- for field in request.validated_data.model_fields_set
135
- if field != 'request'
136
- })
133
+ instance = await request.validated_data.create(
134
+ validated_data={
135
+ field: getattr(request.validated_data, field)
136
+ for field in request.validated_data.model_fields_set
137
+ if field != 'request'
138
+ },
139
+ )
137
140
  return Response(data=instance, status_code=status.HTTP_201_CREATED)
138
141
 
139
142
 
@@ -146,7 +149,7 @@ class UpdateAPI(GenericAPI, ObjectRequired):
146
149
 
147
150
  await request.validated_data.update(
148
151
  instance=instance,
149
- validated_data=request.validated_data.model_dump(by_alias=True)
152
+ validated_data=request.validated_data.model_dump(by_alias=True),
150
153
  )
151
154
  return Response(data=instance, status_code=status.HTTP_200_OK)
152
155
 
@@ -156,7 +159,7 @@ class UpdateAPI(GenericAPI, ObjectRequired):
156
159
 
157
160
  await request.validated_data.partial_update(
158
161
  instance=instance,
159
- validated_data=request.validated_data.model_dump(exclude_none=True, by_alias=True)
162
+ validated_data=request.validated_data.model_dump(exclude_none=True, by_alias=True),
160
163
  )
161
164
  return Response(data=instance, status_code=status.HTTP_200_OK)
162
165
 
panther/logging.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  from pathlib import Path
3
+
3
4
  from panther.configs import config
4
5
 
5
6
  LOGS_DIR = config.BASE_DIR / 'logs'
@@ -64,5 +65,5 @@ LOGGING = {
64
65
  'handlers': ['default', 'query_file'],
65
66
  'level': 'DEBUG',
66
67
  },
67
- }
68
+ },
68
69
  }
panther/main.py CHANGED
@@ -8,13 +8,13 @@ from pathlib import Path
8
8
  import panther.logging
9
9
  from panther import status
10
10
  from panther._load_configs import *
11
- from panther._utils import traceback_message, reformat_code
11
+ from panther._utils import reformat_code, traceback_message
12
12
  from panther.app import GenericAPI
13
13
  from panther.base_websocket import Websocket
14
14
  from panther.cli.utils import print_info
15
15
  from panther.configs import config
16
16
  from panther.events import Event
17
- from panther.exceptions import APIError, PantherError, NotFoundAPIError, BaseError, UpgradeRequiredError
17
+ from panther.exceptions import APIError, BaseError, NotFoundAPIError, PantherError, UpgradeRequiredError
18
18
  from panther.request import Request
19
19
  from panther.response import Response
20
20
  from panther.routings import find_endpoint
@@ -35,6 +35,7 @@ class Panther:
35
35
  If the configuration is defined in the current file, you can also set this to `__name__`.
36
36
  urls: A dictionary containing your URL routing.
37
37
  If not provided, Panther will attempt to load `URLs` from the configs module.
38
+
38
39
  """
39
40
  self._configs_module_name = configs
40
41
  self._urls = urls
@@ -45,7 +46,7 @@ class Panther:
45
46
  self.load_configs()
46
47
  if config.AUTO_REFORMAT:
47
48
  reformat_code(base_dir=config.BASE_DIR)
48
- except Exception as e: # noqa: BLE001
49
+ except Exception as e:
49
50
  logger.error(e.args[0] if isinstance(e, PantherError) else traceback_message(exception=e))
50
51
  sys.exit()
51
52
 
@@ -69,7 +70,7 @@ class Panther:
69
70
  load_middlewares(self._configs_module)
70
71
  load_auto_reformat(self._configs_module)
71
72
  load_background_tasks(self._configs_module)
72
- load_default_cache_exp(self._configs_module)
73
+ load_other_configs(self._configs_module)
73
74
  load_urls(self._configs_module, urls=self._urls)
74
75
  load_authentication_class(self._configs_module)
75
76
  load_websocket_connections()
@@ -79,15 +80,15 @@ class Panther:
79
80
  async def __call__(self, scope: dict, receive: Callable, send: Callable) -> None:
80
81
  if scope['type'] == 'lifespan':
81
82
  message = await receive()
82
- if message["type"] == 'lifespan.startup':
83
+ if message['type'] == 'lifespan.startup':
83
84
  if config.HAS_WS:
84
85
  await config.WEBSOCKET_CONNECTIONS.start()
85
86
  try:
86
87
  await Event.run_startups()
87
88
  except Exception as e:
88
89
  logger.error(e)
89
- raise e
90
- elif message["type"] == 'lifespan.shutdown':
90
+ raise
91
+ elif message['type'] == 'lifespan.shutdown':
91
92
  # It's not happening :\, so handle the shutdowns in __del__ ...
92
93
  pass
93
94
  return
@@ -119,7 +120,6 @@ class Panther:
119
120
 
120
121
  return await config.WEBSOCKET_CONNECTIONS.listen(connection=final_connection)
121
122
 
122
-
123
123
  async def handle_ws(self, scope: dict, receive: Callable, send: Callable) -> None:
124
124
  # Create Temp Connection
125
125
  connection = Websocket(scope=scope, receive=receive, send=send)
@@ -139,7 +139,6 @@ class Panther:
139
139
  logger.error(traceback_message(exception=e))
140
140
  await connection.close()
141
141
 
142
-
143
142
  @classmethod
144
143
  async def handle_http_endpoint(cls, request: Request) -> Response:
145
144
  # Find Endpoint
@@ -176,7 +175,7 @@ class Panther:
176
175
  logger.error('You forgot to return `response` on the `Middlewares.__call__()`')
177
176
  response = Response(
178
177
  data={'detail': 'Internal Server Error'},
179
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
178
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
180
179
  )
181
180
  # Handle `APIError` Exceptions
182
181
  except APIError as e:
@@ -186,15 +185,15 @@ class Panther:
186
185
  status_code=e.status_code,
187
186
  )
188
187
  # Handle Unknown Exceptions
189
- except Exception as e: # noqa: BLE001 - Blind Exception
188
+ except Exception as e:
190
189
  logger.error(traceback_message(exception=e))
191
190
  response = Response(
192
191
  data={'detail': 'Internal Server Error'},
193
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
192
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
194
193
  )
195
194
 
196
195
  # Return Response
197
- await response.send(send, receive)
196
+ await response.send(send=send, receive=receive)
198
197
 
199
198
  def __del__(self):
200
199
  Event.run_shutdowns()
@@ -0,0 +1,67 @@
1
+ from panther.configs import config
2
+ from panther.middlewares import HTTPMiddleware
3
+ from panther.request import Request
4
+ from panther.response import Response
5
+
6
+
7
+ class CORSMiddleware(HTTPMiddleware):
8
+ """
9
+ Middleware to handle Cross-Origin Resource Sharing (CORS) for Panther applications.
10
+
11
+ This middleware automatically adds the appropriate CORS headers to all HTTP responses
12
+ based on configuration variables defined in your Panther config file (e.g., core/configs.py).
13
+ It also handles preflight (OPTIONS) requests.
14
+
15
+ Configuration attributes (set these in your config):
16
+ ---------------------------------------------------
17
+ ALLOW_ORIGINS: list[str]
18
+ List of allowed origins. Use ["*"] to allow all origins. Default: ["*"]
19
+ ALLOW_METHODS: list[str]
20
+ List of allowed HTTP methods. Default: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
21
+ ALLOW_HEADERS: list[str]
22
+ List of allowed request headers. Use ["*"] to allow all headers. Default: ["*"]
23
+ ALLOW_CREDENTIALS: bool
24
+ Whether to allow credentials (cookies, authorization headers, etc.). Default: False
25
+ EXPOSE_HEADERS: list[str]
26
+ List of headers that can be exposed to the browser. Default: []
27
+ CORS_MAX_AGE: int
28
+ Number of seconds browsers are allowed to cache preflight responses. Default: 600
29
+
30
+ Usage:
31
+ ------
32
+ 1. Set the above config variables in your config file as needed.
33
+ 2. Add 'panther.middlewares.cors.CORSMiddleware' to your MIDDLEWARES list.
34
+ """
35
+
36
+ async def __call__(self, request: Request) -> Response:
37
+ # Fetch CORS settings from config, with defaults
38
+ allow_origins = config.ALLOW_ORIGINS or ['*']
39
+ allow_methods = config.ALLOW_METHODS or ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']
40
+ allow_headers = config.ALLOW_HEADERS or ['*']
41
+ allow_credentials = config.ALLOW_CREDENTIALS or False
42
+ expose_headers = config.EXPOSE_HEADERS or []
43
+ max_age = config.CORS_MAX_AGE or 600
44
+
45
+ # Handle preflight (OPTIONS) requests
46
+ if request.method == 'OPTIONS':
47
+ response = Response(status_code=204)
48
+ else:
49
+ response = await self.dispatch(request=request)
50
+
51
+ origin = request.headers['origin'] or '*'
52
+ if '*' in allow_origins:
53
+ allow_origin = '*'
54
+ elif origin in allow_origins:
55
+ allow_origin = origin
56
+ else:
57
+ allow_origin = allow_origins[0] if allow_origins else '*'
58
+
59
+ response.headers['Access-Control-Allow-Origin'] = allow_origin
60
+ response.headers['Access-Control-Allow-Methods'] = ', '.join(allow_methods)
61
+ response.headers['Access-Control-Allow-Headers'] = ', '.join(allow_headers)
62
+ response.headers['Access-Control-Max-Age'] = str(max_age)
63
+ if allow_credentials:
64
+ response.headers['Access-Control-Allow-Credentials'] = 'true'
65
+ if expose_headers:
66
+ response.headers['Access-Control-Expose-Headers'] = ', '.join(expose_headers)
67
+ return response
@@ -11,7 +11,7 @@ logger = logging.getLogger('monitoring')
11
11
  class MonitoringMiddleware(HTTPMiddleware):
12
12
  """
13
13
  Create Log Message Like Below:
14
- date_time | method | path | ip:port | response_time(seconds) | status
14
+ datetime | method | path | ip:port | response_time(seconds) | status
15
15
  """
16
16
 
17
17
  async def __call__(self, request: Request):
@@ -24,11 +24,13 @@ class MonitoringMiddleware(HTTPMiddleware):
24
24
  logger.info(f'{method} | {request.path} | {request.client} | {response_time} | {response.status_code}')
25
25
  return response
26
26
 
27
+
27
28
  class WebsocketMonitoringMiddleware(WebsocketMiddleware):
28
29
  """
29
30
  Create Log Message Like Below:
30
- date_time | WS | path | ip:port | connection_time(seconds) | status
31
+ datetime | WS | path | ip:port | connection_time(seconds) | status
31
32
  """
33
+
32
34
  ConnectedConnectionTime = ' - '
33
35
 
34
36
  async def __call__(self, connection: Websocket):
@@ -39,4 +41,4 @@ class WebsocketMonitoringMiddleware(WebsocketMiddleware):
39
41
 
40
42
  connection_time = perf_counter() - start_time # Seconds
41
43
  logger.info(f'WS | {connection.path} | {connection.client} | {connection_time} | {connection.state}')
42
- return connection
44
+ return connection
panther/openapi/urls.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from panther.openapi.views import OpenAPI
2
2
 
3
- urls = {
3
+ url_routing = {
4
4
  '': OpenAPI,
5
- }
5
+ }
panther/openapi/utils.py CHANGED
@@ -17,9 +17,9 @@ class OutputSchema:
17
17
  """
18
18
 
19
19
  def __init__(
20
- self,
21
- model: type[ModelSerializer] | type[pydantic.BaseModel] = EmptyResponseModel,
22
- status_code: int = status.HTTP_200_OK,
20
+ self,
21
+ model: type[ModelSerializer] | type[pydantic.BaseModel] = EmptyResponseModel,
22
+ status_code: int = status.HTTP_200_OK,
23
23
  ):
24
24
  self.model = model
25
25
  self.status_code = status_code
panther/openapi/views.py CHANGED
@@ -16,43 +16,31 @@ class OpenAPI(GenericAPI):
16
16
  schema = endpoint.output_schema.model.schema()
17
17
  else:
18
18
  status_code = parsed.status_code
19
- schema = {
20
- 'properties': {
21
- k: {'default': v} for k, v in parsed.data.items()
22
- }
23
- }
19
+ schema = {'properties': {k: {'default': v} for k, v in parsed.data.items()}}
24
20
 
25
21
  responses = {}
26
22
  if schema:
27
- responses = {
28
- 'responses': {
29
- status_code: {
30
- 'content': {
31
- 'application/json': {
32
- 'schema': schema
33
- }
34
- }
35
- }
36
- }
37
- }
23
+ responses = {'responses': {status_code: {'content': {'application/json': {'schema': schema}}}}}
38
24
  request_body = {}
39
25
  if endpoint.input_model and method in ['post', 'put', 'patch']:
40
26
  request_body = {
41
27
  'requestBody': {
42
28
  'required': True,
43
29
  'content': {
44
- 'application/json': {
45
- 'schema': endpoint.input_model.schema() if endpoint.input_model else {}
46
- }
47
- }
48
- }
30
+ 'application/json': {'schema': endpoint.input_model.schema() if endpoint.input_model else {}},
31
+ },
32
+ },
49
33
  }
50
34
 
51
- content = {
52
- 'title': parsed.title,
53
- 'summary': endpoint.__doc__,
54
- 'tags': ['.'.join(endpoint.__module__.rsplit('.')[:-1]) or endpoint.__module__],
55
- } | responses | request_body
35
+ content = (
36
+ {
37
+ 'title': parsed.title,
38
+ 'summary': endpoint.__doc__,
39
+ 'tags': ['.'.join(endpoint.__module__.rsplit('.')[:-1]) or endpoint.__module__],
40
+ }
41
+ | responses
42
+ | request_body
43
+ )
56
44
  return {method: content}
57
45
 
58
46
  def get(self):
@@ -70,15 +58,15 @@ class OpenAPI(GenericAPI):
70
58
 
71
59
  if isinstance(endpoint, types.FunctionType):
72
60
  methods = endpoint.methods
73
- if methods is None or 'POST' in methods:
61
+ if 'POST' in methods:
74
62
  paths[url] |= self.get_content(endpoint, 'post')
75
- if methods is None or 'GET' in methods:
63
+ if 'GET' in methods:
76
64
  paths[url] |= self.get_content(endpoint, 'get')
77
- if methods is None or 'PUT' in methods:
65
+ if 'PUT' in methods:
78
66
  paths[url] |= self.get_content(endpoint, 'put')
79
- if methods is None or 'PATCH' in methods:
67
+ if 'PATCH' in methods:
80
68
  paths[url] |= self.get_content(endpoint, 'patch')
81
- if methods is None or 'DELETE' in methods:
69
+ if 'DELETE' in methods:
82
70
  paths[url] |= self.get_content(endpoint, 'delete')
83
71
  else:
84
72
  if endpoint.post is not GenericAPI.post:
@@ -92,10 +80,5 @@ class OpenAPI(GenericAPI):
92
80
  if endpoint.delete is not GenericAPI.delete:
93
81
  paths[url] |= self.get_content(endpoint, 'delete')
94
82
 
95
- openapi_content = {
96
- 'openapi': '3.0.0',
97
- 'paths': paths,
98
- 'components': {}
99
- }
100
- print(f'{openapi_content=}')
83
+ openapi_content = {'openapi': '3.0.0', 'paths': paths, 'components': {}}
101
84
  return TemplateResponse(name='openapi.html', context={'openapi_content': openapi_content})
panther/pagination.py CHANGED
@@ -1,6 +1,7 @@
1
- from panther.db.cursor import Cursor
2
1
  from pantherdb import Cursor as PantherDBCursor
3
2
 
3
+ from panther.db.cursor import Cursor
4
+
4
5
 
5
6
  class Pagination:
6
7
  """
@@ -14,6 +15,7 @@ class Pagination:
14
15
  'results': [...]
15
16
  }
16
17
  """
18
+
17
19
  DEFAULT_LIMIT = 20
18
20
  DEFAULT_SKIP = 0
19
21
 
@@ -47,5 +49,5 @@ class Pagination:
47
49
  'count': count,
48
50
  'next': self.build_next_params() if has_next else None,
49
51
  'previous': self.build_previous_params() if self.skip else None,
50
- 'results': response
52
+ 'results': response,
51
53
  }
panther/panel/apis.py CHANGED
@@ -3,8 +3,7 @@ import contextlib
3
3
  from panther import status
4
4
  from panther.app import API
5
5
  from panther.configs import config
6
- from panther.db.connections import db
7
- from panther.db.connections import redis
6
+ from panther.db.connections import db, redis
8
7
  from panther.panel.utils import get_model_fields
9
8
  from panther.request import Request
10
9
  from panther.response import Response
@@ -16,11 +15,7 @@ with contextlib.suppress(ImportError):
16
15
 
17
16
  @API(methods=['GET'])
18
17
  async def models_api():
19
- return [{
20
- 'name': model.__name__,
21
- 'module': model.__module__,
22
- 'index': i
23
- } for i, model in enumerate(config.MODELS)]
18
+ return [{'name': model.__name__, 'module': model.__module__, 'index': i} for i, model in enumerate(config.MODELS)]
24
19
 
25
20
 
26
21
  @API(methods=['GET', 'POST'])
panther/panel/urls.py CHANGED
@@ -1,10 +1,6 @@
1
- from panther.panel.views import TableView, CreateView, LoginView, DetailView, HomeView
1
+ from panther.panel.views import CreateView, DetailView, HomeView, LoginView, TableView
2
2
 
3
- urls = {
4
- # '': models_api,
5
- # '<index>/': documents_api,
6
- # '<index>/<document_id>/': single_document_api,
7
- # 'health': healthcheck_api,
3
+ url_routing = {
8
4
  '': HomeView,
9
5
  '<index>/': TableView,
10
6
  '<index>/create/': CreateView,
panther/panel/utils.py CHANGED
@@ -51,6 +51,7 @@ def clean_model_schema(schema: dict) -> dict:
51
51
  'is_male': {'title': 'Is Male', 'type': ['boolean', 'null'], 'required': True}
52
52
  }
53
53
  }
54
+
54
55
  """
55
56
 
56
57
  result = defaultdict(dict)
@@ -108,8 +109,11 @@ def get_model_fields(model):
108
109
 
109
110
 
110
111
  def get_models():
111
- return [{
112
- 'index': i,
113
- 'name': model.__name__,
114
- 'module': model.__module__,
115
- } for i, model in enumerate(config.MODELS)]
112
+ return [
113
+ {
114
+ 'index': i,
115
+ 'name': model.__name__,
116
+ 'module': model.__module__,
117
+ }
118
+ for i, model in enumerate(config.MODELS)
119
+ ]
panther/panel/views.py CHANGED
@@ -4,22 +4,24 @@ from panther import status
4
4
  from panther.app import API, GenericAPI
5
5
  from panther.configs import config
6
6
  from panther.db.models import BaseUser
7
- from panther.exceptions import RedirectAPIError, AuthenticationAPIError
7
+ from panther.exceptions import AuthenticationAPIError, RedirectAPIError
8
8
  from panther.panel.middlewares import RedirectToSlashMiddleware
9
- from panther.panel.utils import get_models, clean_model_schema
9
+ from panther.panel.utils import clean_model_schema, get_models
10
10
  from panther.permissions import BasePermission
11
11
  from panther.request import Request
12
- from panther.response import TemplateResponse, Response, Cookie, RedirectResponse
12
+ from panther.response import Cookie, RedirectResponse, Response, TemplateResponse
13
13
 
14
14
  logger = logging.getLogger('panther')
15
15
 
16
16
 
17
17
  class AdminPanelPermission(BasePermission):
18
+ """We didn't want to change AUTHENTICATION class of user, so we use permission class for this purpose."""
19
+
18
20
  @classmethod
19
21
  async def authorization(cls, request: Request) -> bool:
20
22
  from panther.authentications import CookieJWTAuthentication
21
23
 
22
- try: # We don't want to set AUTHENTICATION class, so we have to use permission classes
24
+ try:
23
25
  await CookieJWTAuthentication.authentication(request=request)
24
26
  return True
25
27
  except AuthenticationAPIError:
@@ -50,22 +52,14 @@ class LoginView(GenericAPI):
50
52
  status_code=status.HTTP_400_BAD_REQUEST,
51
53
  context={'error': 'Authentication Error'},
52
54
  )
53
- tokens = JWTAuthentication.login(user.id)
55
+ tokens = await JWTAuthentication.login(user=user)
54
56
  return RedirectResponse(
55
57
  url=request.query_params.get('redirect_to', '..'),
56
58
  status_code=status.HTTP_302_FOUND,
57
59
  set_cookies=[
58
- Cookie(
59
- key='access_token',
60
- value=tokens['access_token'],
61
- max_age=config.JWT_CONFIG.life_time
62
- ),
63
- Cookie(
64
- key='refresh_token',
65
- value=tokens['refresh_token'],
66
- max_age=config.JWT_CONFIG.refresh_life_time
67
- )
68
- ]
60
+ Cookie(key='access_token', value=tokens['access_token'], max_age=config.JWT_CONFIG.life_time),
61
+ Cookie(key='refresh_token', value=tokens['refresh_token'], max_age=config.JWT_CONFIG.refresh_life_time),
62
+ ],
69
63
  )
70
64
 
71
65
 
@@ -93,7 +87,7 @@ class TableView(GenericAPI):
93
87
  'fields': clean_model_schema(model.schema()),
94
88
  'tables': get_models(),
95
89
  'records': Response.prepare_data(data),
96
- }
90
+ },
97
91
  )
98
92
 
99
93
 
@@ -108,7 +102,7 @@ class CreateView(GenericAPI):
108
102
  context={
109
103
  'fields': clean_model_schema(model.schema()),
110
104
  'tables': get_models(),
111
- }
105
+ },
112
106
  )
113
107
 
114
108
  async def post(self, request: Request, index: int):
@@ -129,10 +123,7 @@ class DetailView(GenericAPI):
129
123
  obj = await model.find_one_or_raise(id=document_id)
130
124
  return TemplateResponse(
131
125
  name='detail.html',
132
- context={
133
- 'fields': clean_model_schema(model.schema()),
134
- 'data': obj.model_dump()
135
- }
126
+ context={'fields': clean_model_schema(model.schema()), 'data': obj.model_dump()},
136
127
  )
137
128
 
138
129
  async def put(self, request: Request, index: int, document_id: str):
panther/permissions.py CHANGED
@@ -1,9 +1,10 @@
1
1
  from panther.request import Request
2
+ from panther.websocket import Websocket
2
3
 
3
4
 
4
5
  class BasePermission:
5
6
  @classmethod
6
- async def authorization(cls, request: Request) -> bool:
7
+ async def authorization(cls, request: Request | Websocket) -> bool:
7
8
  return True
8
9
 
9
10