panther 5.0.0b3__py3-none-any.whl → 5.0.0b5__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 +30 -24
- panther/db/cursor.py +3 -1
- panther/db/models.py +26 -10
- panther/db/queries/base_queries.py +4 -5
- panther/db/queries/mongodb_queries.py +21 -21
- 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 +74 -100
- 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 +13 -22
- panther/permissions.py +2 -1
- panther/request.py +2 -1
- panther/response.py +101 -94
- panther/routings.py +12 -12
- panther/serializer.py +20 -43
- panther/test.py +73 -58
- panther/throttling.py +68 -3
- panther/utils.py +5 -11
- panther-5.0.0b5.dist-info/METADATA +188 -0
- panther-5.0.0b5.dist-info/RECORD +75 -0
- panther/monitoring.py +0 -34
- panther-5.0.0b3.dist-info/METADATA +0 -223
- panther-5.0.0b3.dist-info/RECORD +0 -75
- {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/WHEEL +0 -0
- {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/entry_points.txt +0 -0
- {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/licenses/LICENSE +0 -0
- {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/top_level.txt +0 -0
panther/generics.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import contextlib
|
2
2
|
import logging
|
3
|
+
from abc import abstractmethod
|
3
4
|
|
4
5
|
from pantherdb import Cursor as PantherDBCursor
|
5
6
|
|
@@ -7,7 +8,9 @@ from panther import status
|
|
7
8
|
from panther.app import GenericAPI
|
8
9
|
from panther.configs import config
|
9
10
|
from panther.db import Model
|
11
|
+
from panther.db.connections import MongoDBConnection
|
10
12
|
from panther.db.cursor import Cursor
|
13
|
+
from panther.db.models import ID
|
11
14
|
from panther.exceptions import APIError
|
12
15
|
from panther.pagination import Pagination
|
13
16
|
from panther.request import Request
|
@@ -21,56 +24,42 @@ with contextlib.suppress(ImportError):
|
|
21
24
|
logger = logging.getLogger('panther')
|
22
25
|
|
23
26
|
|
24
|
-
class
|
25
|
-
|
26
|
-
|
27
|
-
logger.critical(f'`{self.__class__.__name__}.object()` should return instance of a Model --> `find_one()`')
|
28
|
-
raise APIError
|
29
|
-
|
30
|
-
async def object(self, request: Request, **kwargs):
|
27
|
+
class RetrieveAPI(GenericAPI):
|
28
|
+
@abstractmethod
|
29
|
+
async def get_instance(self, request: Request, **kwargs) -> Model:
|
31
30
|
"""
|
32
|
-
|
31
|
+
Should return an instance of Model, e.g. `await User.find_one()`
|
33
32
|
"""
|
34
|
-
logger.error(f'`
|
33
|
+
logger.error(f'`get_instance()` method is not implemented in {self.__class__} .')
|
35
34
|
raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
36
35
|
|
36
|
+
async def get(self, request: Request, **kwargs):
|
37
|
+
instance = await self.get_instance(request=request, **kwargs)
|
38
|
+
return Response(data=instance, status_code=status.HTTP_200_OK)
|
39
|
+
|
37
40
|
|
38
|
-
class
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
41
|
+
class ListAPI(GenericAPI):
|
42
|
+
sort_fields: list[str] = []
|
43
|
+
search_fields: list[str] = []
|
44
|
+
filter_fields: list[str] = []
|
45
|
+
pagination: type[Pagination] | None = None
|
43
46
|
|
44
|
-
async def
|
47
|
+
async def get_query(self, request: Request, **kwargs) -> Cursor | PantherDBCursor:
|
45
48
|
"""
|
46
|
-
|
47
|
-
Should return `.find()`
|
49
|
+
Should return a Cursor, e.g. `await User.find()`
|
48
50
|
"""
|
49
|
-
logger.error(f'`
|
51
|
+
logger.error(f'`get_query()` method is not implemented in {self.__class__} .')
|
50
52
|
raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
51
53
|
|
52
|
-
|
53
|
-
class RetrieveAPI(GenericAPI, ObjectRequired):
|
54
|
-
async def get(self, request: Request, **kwargs):
|
55
|
-
instance = await self.object(request=request, **kwargs)
|
56
|
-
self._check_object(instance)
|
57
|
-
|
58
|
-
return Response(data=instance, status_code=status.HTTP_200_OK)
|
59
|
-
|
60
|
-
|
61
|
-
class ListAPI(GenericAPI, CursorRequired):
|
62
|
-
sort_fields: list[str]
|
63
|
-
search_fields: list[str]
|
64
|
-
filter_fields: list[str]
|
65
|
-
pagination: type[Pagination]
|
66
|
-
|
67
54
|
async def get(self, request: Request, **kwargs):
|
68
55
|
cursor, pagination = await self.prepare_cursor(request=request, **kwargs)
|
69
56
|
return Response(data=cursor, pagination=pagination, status_code=status.HTTP_200_OK)
|
70
57
|
|
71
58
|
async def prepare_cursor(self, request: Request, **kwargs) -> tuple[Cursor | PantherDBCursor, Pagination | None]:
|
72
|
-
cursor = await self.
|
73
|
-
|
59
|
+
cursor = await self.get_query(request=request, **kwargs)
|
60
|
+
if not isinstance(cursor, (Cursor, PantherDBCursor)):
|
61
|
+
logger.error(f'`{self.__class__.__name__}.get_query()` should return a Cursor, e.g. `await Model.find()`')
|
62
|
+
raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
74
63
|
|
75
64
|
query = {}
|
76
65
|
query |= self.process_filters(query_params=request.query_params, cursor=cursor)
|
@@ -89,103 +78,88 @@ class ListAPI(GenericAPI, CursorRequired):
|
|
89
78
|
|
90
79
|
def process_filters(self, query_params: dict, cursor: Cursor | PantherDBCursor) -> dict:
|
91
80
|
_filter = {}
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
# Change type of the value if it is ObjectId
|
98
|
-
if cursor.cls.model_fields[field].metadata[0].func.__name__ == 'validate_object_id':
|
99
|
-
_filter[field] = bson.ObjectId(query_params[field])
|
100
|
-
continue
|
101
|
-
_filter[field] = query_params[field]
|
81
|
+
for field in self.filter_fields:
|
82
|
+
if field in query_params:
|
83
|
+
_filter[field] = query_params[field]
|
84
|
+
if isinstance(config.DATABASE, MongoDBConnection) and cursor.cls.model_fields[field].annotation == ID:
|
85
|
+
_filter[field] = bson.ObjectId(_filter[field])
|
102
86
|
return _filter
|
103
87
|
|
104
88
|
def process_search(self, query_params: dict) -> dict:
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
return {field: value for field in self.search_fields}
|
113
|
-
return {}
|
89
|
+
search_param = query_params.get('search')
|
90
|
+
if not self.search_fields or not search_param:
|
91
|
+
return {}
|
92
|
+
if isinstance(config.DATABASE, MongoDBConnection):
|
93
|
+
if search := [{field: {'$regex': search_param}} for field in self.search_fields]:
|
94
|
+
return {'$or': search}
|
95
|
+
return {field: search_param for field in self.search_fields}
|
114
96
|
|
115
97
|
def process_sort(self, query_params: dict) -> list:
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
98
|
+
sort_param = query_params.get('sort')
|
99
|
+
if not self.sort_fields or not sort_param:
|
100
|
+
return []
|
101
|
+
return [
|
102
|
+
(field, -1 if param.startswith('-') else 1)
|
103
|
+
for param in sort_param.split(',')
|
104
|
+
for field in self.sort_fields
|
105
|
+
if field == param.removeprefix('-')
|
106
|
+
]
|
122
107
|
|
123
108
|
def process_pagination(self, query_params: dict, cursor: Cursor | PantherDBCursor) -> Pagination | None:
|
124
|
-
if
|
109
|
+
if self.pagination:
|
125
110
|
return self.pagination(query_params=query_params, cursor=cursor)
|
126
111
|
|
127
112
|
|
128
113
|
class CreateAPI(GenericAPI):
|
129
|
-
input_model: type[ModelSerializer]
|
114
|
+
input_model: type[ModelSerializer] | None = None
|
130
115
|
|
131
116
|
async def post(self, request: Request, **kwargs):
|
132
|
-
instance = await request.validated_data.
|
133
|
-
field: getattr(request.validated_data, field)
|
134
|
-
for field in request.validated_data.model_fields_set
|
135
|
-
if field != 'request'
|
136
|
-
})
|
117
|
+
instance = await request.validated_data.model.insert_one(request.validated_data.model_dump())
|
137
118
|
return Response(data=instance, status_code=status.HTTP_201_CREATED)
|
138
119
|
|
139
120
|
|
140
|
-
class UpdateAPI(GenericAPI
|
141
|
-
input_model: type[ModelSerializer]
|
121
|
+
class UpdateAPI(GenericAPI):
|
122
|
+
input_model: type[ModelSerializer] | None = None
|
142
123
|
|
143
|
-
|
144
|
-
|
145
|
-
|
124
|
+
@abstractmethod
|
125
|
+
async def get_instance(self, request: Request, **kwargs) -> Model:
|
126
|
+
"""
|
127
|
+
Should return an instance of Model, e.g. `await User.find_one()`
|
128
|
+
"""
|
129
|
+
logger.error(f'`get_instance()` method is not implemented in {self.__class__} .')
|
130
|
+
raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
146
131
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
)
|
132
|
+
async def put(self, request: Request, **kwargs):
|
133
|
+
instance = await self.get_instance(request=request, **kwargs)
|
134
|
+
await instance.update(request.validated_data.model_dump())
|
151
135
|
return Response(data=instance, status_code=status.HTTP_200_OK)
|
152
136
|
|
153
137
|
async def patch(self, request: Request, **kwargs):
|
154
|
-
instance = await self.
|
155
|
-
|
156
|
-
|
157
|
-
await request.validated_data.partial_update(
|
158
|
-
instance=instance,
|
159
|
-
validated_data=request.validated_data.model_dump(exclude_none=True, by_alias=True)
|
160
|
-
)
|
138
|
+
instance = await self.get_instance(request=request, **kwargs)
|
139
|
+
await instance.update(request.validated_data.model_dump(exclude_none=True))
|
161
140
|
return Response(data=instance, status_code=status.HTTP_200_OK)
|
162
141
|
|
163
142
|
|
164
|
-
class DeleteAPI(GenericAPI
|
143
|
+
class DeleteAPI(GenericAPI):
|
144
|
+
@abstractmethod
|
145
|
+
async def get_instance(self, request: Request, **kwargs) -> Model:
|
146
|
+
"""
|
147
|
+
Should return an instance of Model, e.g. `await User.find_one()`
|
148
|
+
"""
|
149
|
+
logger.error(f'`get_instance()` method is not implemented in {self.__class__} .')
|
150
|
+
raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
151
|
+
|
165
152
|
async def pre_delete(self, instance, request: Request, **kwargs):
|
153
|
+
"""Hook for logic before deletion."""
|
166
154
|
pass
|
167
155
|
|
168
156
|
async def post_delete(self, instance, request: Request, **kwargs):
|
157
|
+
"""Hook for logic after deletion."""
|
169
158
|
pass
|
170
159
|
|
171
160
|
async def delete(self, request: Request, **kwargs):
|
172
|
-
instance = await self.
|
173
|
-
self._check_object(instance)
|
174
|
-
|
161
|
+
instance = await self.get_instance(request=request, **kwargs)
|
175
162
|
await self.pre_delete(instance, request=request, **kwargs)
|
176
163
|
await instance.delete()
|
177
164
|
await self.post_delete(instance, request=request, **kwargs)
|
178
|
-
|
179
165
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
180
|
-
|
181
|
-
|
182
|
-
class ListCreateAPI(CreateAPI, ListAPI):
|
183
|
-
pass
|
184
|
-
|
185
|
-
|
186
|
-
class UpdateDeleteAPI(UpdateAPI, DeleteAPI):
|
187
|
-
pass
|
188
|
-
|
189
|
-
|
190
|
-
class RetrieveUpdateDeleteAPI(RetrieveAPI, UpdateAPI, DeleteAPI):
|
191
|
-
pass
|
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
|
+
]
|