panther 5.0.0b2__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.
- panther/__init__.py +1 -1
- panther/_load_configs.py +46 -37
- panther/_utils.py +49 -34
- panther/app.py +96 -97
- panther/authentications.py +97 -50
- panther/background_tasks.py +98 -124
- panther/base_request.py +16 -10
- panther/base_websocket.py +8 -8
- panther/caching.py +16 -80
- panther/cli/create_command.py +17 -16
- panther/cli/main.py +1 -1
- panther/cli/monitor_command.py +11 -6
- panther/cli/run_command.py +5 -71
- panther/cli/template.py +7 -7
- panther/cli/utils.py +58 -69
- panther/configs.py +70 -72
- panther/db/connections.py +18 -24
- panther/db/cursor.py +0 -1
- panther/db/models.py +24 -8
- panther/db/queries/base_queries.py +2 -5
- panther/db/queries/mongodb_queries.py +17 -20
- panther/db/queries/pantherdb_queries.py +1 -1
- panther/db/queries/queries.py +26 -8
- panther/db/utils.py +1 -1
- panther/events.py +25 -14
- panther/exceptions.py +2 -7
- panther/file_handler.py +1 -1
- panther/generics.py +11 -8
- panther/logging.py +2 -1
- panther/main.py +12 -13
- panther/middlewares/cors.py +67 -0
- panther/middlewares/monitoring.py +5 -3
- panther/openapi/urls.py +2 -2
- panther/openapi/utils.py +3 -3
- panther/openapi/views.py +20 -37
- panther/pagination.py +4 -2
- panther/panel/apis.py +2 -7
- panther/panel/urls.py +2 -6
- panther/panel/utils.py +9 -5
- panther/panel/views.py +17 -23
- panther/permissions.py +2 -1
- panther/request.py +2 -1
- panther/response.py +53 -47
- panther/routings.py +12 -12
- panther/serializer.py +19 -20
- panther/test.py +73 -58
- panther/throttling.py +68 -3
- panther/utils.py +5 -11
- {panther-5.0.0b2.dist-info → panther-5.0.0b4.dist-info}/METADATA +1 -1
- panther-5.0.0b4.dist-info/RECORD +75 -0
- panther/monitoring.py +0 -34
- panther-5.0.0b2.dist-info/RECORD +0 -75
- {panther-5.0.0b2.dist-info → panther-5.0.0b4.dist-info}/WHEEL +0 -0
- {panther-5.0.0b2.dist-info → panther-5.0.0b4.dist-info}/entry_points.txt +0 -0
- {panther-5.0.0b2.dist-info → panther-5.0.0b4.dist-info}/licenses/LICENSE +0 -0
- {panther-5.0.0b2.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.
|
5
|
+
from panther.utils import Singleton
|
6
6
|
|
7
7
|
logger = logging.getLogger('panther')
|
8
8
|
|
9
9
|
|
10
|
-
class Event:
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
@
|
20
|
-
def shutdown(func):
|
21
|
-
|
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
|
-
@
|
28
|
-
async def run_startups():
|
29
|
-
for func in
|
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
|
-
@
|
39
|
-
def run_shutdowns():
|
40
|
-
for func in
|
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
|
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
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
|
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(
|
133
|
-
|
134
|
-
|
135
|
-
|
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
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
|
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,
|
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:
|
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
|
-
|
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[
|
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
|
90
|
-
elif message[
|
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:
|
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
|
-
|
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
|
-
|
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
panther/openapi/utils.py
CHANGED
@@ -17,9 +17,9 @@ class OutputSchema:
|
|
17
17
|
"""
|
18
18
|
|
19
19
|
def __init__(
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
61
|
+
if 'POST' in methods:
|
74
62
|
paths[url] |= self.get_content(endpoint, 'post')
|
75
|
-
if
|
63
|
+
if 'GET' in methods:
|
76
64
|
paths[url] |= self.get_content(endpoint, 'get')
|
77
|
-
if
|
65
|
+
if 'PUT' in methods:
|
78
66
|
paths[url] |= self.get_content(endpoint, 'put')
|
79
|
-
if
|
67
|
+
if 'PATCH' in methods:
|
80
68
|
paths[url] |= self.get_content(endpoint, 'patch')
|
81
|
-
if
|
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
|
1
|
+
from panther.panel.views import CreateView, DetailView, HomeView, LoginView, TableView
|
2
2
|
|
3
|
-
|
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
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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
@@ -2,23 +2,26 @@ import logging
|
|
2
2
|
|
3
3
|
from panther import status
|
4
4
|
from panther.app import API, GenericAPI
|
5
|
-
from panther.authentications import JWTAuthentication, CookieJWTAuthentication
|
6
5
|
from panther.configs import config
|
7
6
|
from panther.db.models import BaseUser
|
8
|
-
from panther.exceptions import
|
7
|
+
from panther.exceptions import AuthenticationAPIError, RedirectAPIError
|
9
8
|
from panther.panel.middlewares import RedirectToSlashMiddleware
|
10
|
-
from panther.panel.utils import
|
9
|
+
from panther.panel.utils import clean_model_schema, get_models
|
11
10
|
from panther.permissions import BasePermission
|
12
11
|
from panther.request import Request
|
13
|
-
from panther.response import
|
12
|
+
from panther.response import Cookie, RedirectResponse, Response, TemplateResponse
|
14
13
|
|
15
14
|
logger = logging.getLogger('panther')
|
16
15
|
|
17
16
|
|
18
17
|
class AdminPanelPermission(BasePermission):
|
18
|
+
"""We didn't want to change AUTHENTICATION class of user, so we use permission class for this purpose."""
|
19
|
+
|
19
20
|
@classmethod
|
20
21
|
async def authorization(cls, request: Request) -> bool:
|
21
|
-
|
22
|
+
from panther.authentications import CookieJWTAuthentication
|
23
|
+
|
24
|
+
try:
|
22
25
|
await CookieJWTAuthentication.authentication(request=request)
|
23
26
|
return True
|
24
27
|
except AuthenticationAPIError:
|
@@ -32,6 +35,8 @@ class LoginView(GenericAPI):
|
|
32
35
|
return TemplateResponse(name='login.html')
|
33
36
|
|
34
37
|
async def post(self, request: Request):
|
38
|
+
from panther.authentications import JWTAuthentication
|
39
|
+
|
35
40
|
user: BaseUser = await config.USER_MODEL.find_one({config.USER_MODEL.USERNAME_FIELD: request.data['username']})
|
36
41
|
if user is None:
|
37
42
|
logger.debug('User not found.')
|
@@ -47,22 +52,14 @@ class LoginView(GenericAPI):
|
|
47
52
|
status_code=status.HTTP_400_BAD_REQUEST,
|
48
53
|
context={'error': 'Authentication Error'},
|
49
54
|
)
|
50
|
-
tokens = JWTAuthentication.login(user
|
55
|
+
tokens = await JWTAuthentication.login(user=user)
|
51
56
|
return RedirectResponse(
|
52
57
|
url=request.query_params.get('redirect_to', '..'),
|
53
58
|
status_code=status.HTTP_302_FOUND,
|
54
59
|
set_cookies=[
|
55
|
-
Cookie(
|
56
|
-
|
57
|
-
|
58
|
-
max_age=config.JWT_CONFIG.life_time
|
59
|
-
),
|
60
|
-
Cookie(
|
61
|
-
key='refresh_token',
|
62
|
-
value=tokens['refresh_token'],
|
63
|
-
max_age=config.JWT_CONFIG.refresh_life_time
|
64
|
-
)
|
65
|
-
]
|
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
|
+
],
|
66
63
|
)
|
67
64
|
|
68
65
|
|
@@ -90,7 +87,7 @@ class TableView(GenericAPI):
|
|
90
87
|
'fields': clean_model_schema(model.schema()),
|
91
88
|
'tables': get_models(),
|
92
89
|
'records': Response.prepare_data(data),
|
93
|
-
}
|
90
|
+
},
|
94
91
|
)
|
95
92
|
|
96
93
|
|
@@ -105,7 +102,7 @@ class CreateView(GenericAPI):
|
|
105
102
|
context={
|
106
103
|
'fields': clean_model_schema(model.schema()),
|
107
104
|
'tables': get_models(),
|
108
|
-
}
|
105
|
+
},
|
109
106
|
)
|
110
107
|
|
111
108
|
async def post(self, request: Request, index: int):
|
@@ -126,10 +123,7 @@ class DetailView(GenericAPI):
|
|
126
123
|
obj = await model.find_one_or_raise(id=document_id)
|
127
124
|
return TemplateResponse(
|
128
125
|
name='detail.html',
|
129
|
-
context={
|
130
|
-
'fields': clean_model_schema(model.schema()),
|
131
|
-
'data': obj.model_dump()
|
132
|
-
}
|
126
|
+
context={'fields': clean_model_schema(model.schema()), 'data': obj.model_dump()},
|
133
127
|
)
|
134
128
|
|
135
129
|
async def put(self, request: Request, index: int, document_id: str):
|