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/__init__.py
CHANGED
panther/_load_configs.py
CHANGED
@@ -6,8 +6,8 @@ from multiprocessing import Manager
|
|
6
6
|
|
7
7
|
import jinja2
|
8
8
|
|
9
|
-
from panther._utils import
|
10
|
-
from panther.background_tasks import
|
9
|
+
from panther._utils import check_class_type_endpoint, check_function_type_endpoint, import_class
|
10
|
+
from panther.background_tasks import _background_tasks
|
11
11
|
from panther.base_websocket import WebsocketConnections
|
12
12
|
from panther.cli.utils import import_error
|
13
13
|
from panther.configs import JWTConfig, config
|
@@ -15,34 +15,35 @@ from panther.db.connections import redis
|
|
15
15
|
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
|
-
from panther.middlewares.base import
|
18
|
+
from panther.middlewares.base import HTTPMiddleware, WebsocketMiddleware
|
19
19
|
from panther.middlewares.monitoring import MonitoringMiddleware, WebsocketMonitoringMiddleware
|
20
20
|
from panther.panel.views import HomeView
|
21
21
|
from panther.routings import finalize_urls, flatten_urls
|
22
22
|
|
23
23
|
__all__ = (
|
24
|
+
'check_endpoints_inheritance',
|
25
|
+
'load_authentication_class',
|
26
|
+
'load_auto_reformat',
|
27
|
+
'load_background_tasks',
|
24
28
|
'load_configs_module',
|
25
|
-
'load_redis',
|
26
|
-
'load_startup',
|
27
|
-
'load_shutdown',
|
28
|
-
'load_timezone',
|
29
29
|
'load_database',
|
30
|
-
'load_secret_key',
|
31
|
-
'load_throttling',
|
32
|
-
'load_user_model',
|
33
30
|
'load_log_queries',
|
34
31
|
'load_middlewares',
|
32
|
+
'load_other_configs',
|
33
|
+
'load_redis',
|
34
|
+
'load_secret_key',
|
35
|
+
'load_shutdown',
|
36
|
+
'load_startup',
|
35
37
|
'load_templates_dir',
|
36
|
-
'
|
37
|
-
'
|
38
|
-
'load_default_cache_exp',
|
38
|
+
'load_throttling',
|
39
|
+
'load_timezone',
|
39
40
|
'load_urls',
|
40
|
-
'
|
41
|
+
'load_user_model',
|
41
42
|
'load_websocket_connections',
|
42
|
-
'check_endpoints_inheritance',
|
43
43
|
)
|
44
44
|
|
45
45
|
logger = logging.getLogger('panther')
|
46
|
+
monitoring_logger = logging.getLogger('monitoring')
|
46
47
|
|
47
48
|
|
48
49
|
def load_configs_module(module_name: str, /) -> dict:
|
@@ -88,7 +89,7 @@ def load_timezone(_configs: dict, /) -> None:
|
|
88
89
|
|
89
90
|
|
90
91
|
def load_templates_dir(_configs: dict, /) -> None:
|
91
|
-
if templates_dir := _configs.get('TEMPLATES_DIR'):
|
92
|
+
if templates_dir := _configs.get('TEMPLATES_DIR', '.'):
|
92
93
|
config.TEMPLATES_DIR = templates_dir
|
93
94
|
|
94
95
|
if config.TEMPLATES_DIR == '.':
|
@@ -100,8 +101,8 @@ def load_templates_dir(_configs: dict, /) -> None:
|
|
100
101
|
jinja2.FileSystemLoader(searchpath=config.TEMPLATES_DIR),
|
101
102
|
jinja2.PackageLoader(package_name='panther', package_path='panel/templates/'),
|
102
103
|
jinja2.PackageLoader(package_name='panther', package_path='openapi/templates/'),
|
103
|
-
)
|
104
|
-
)
|
104
|
+
),
|
105
|
+
),
|
105
106
|
)
|
106
107
|
|
107
108
|
|
@@ -109,7 +110,7 @@ def load_database(_configs: dict, /) -> None:
|
|
109
110
|
database_config = _configs.get('DATABASE', {})
|
110
111
|
if 'engine' in database_config:
|
111
112
|
if 'class' not in database_config['engine']:
|
112
|
-
raise _exception_handler(field='DATABASE', error=
|
113
|
+
raise _exception_handler(field='DATABASE', error='`engine["class"]` not found.')
|
113
114
|
|
114
115
|
engine_class_path = database_config['engine']['class']
|
115
116
|
engine_class = import_class(engine_class_path)
|
@@ -131,7 +132,7 @@ def load_database(_configs: dict, /) -> None:
|
|
131
132
|
|
132
133
|
def load_secret_key(_configs: dict, /) -> None:
|
133
134
|
if secret_key := _configs.get('SECRET_KEY'):
|
134
|
-
config.SECRET_KEY = secret_key
|
135
|
+
config.SECRET_KEY = secret_key
|
135
136
|
|
136
137
|
|
137
138
|
def load_throttling(_configs: dict, /) -> None:
|
@@ -141,7 +142,8 @@ def load_throttling(_configs: dict, /) -> None:
|
|
141
142
|
|
142
143
|
def load_user_model(_configs: dict, /) -> None:
|
143
144
|
config.USER_MODEL = import_class(_configs.get('USER_MODEL', 'panther.db.models.BaseUser'))
|
144
|
-
config.
|
145
|
+
if config.USER_MODEL not in config.MODELS:
|
146
|
+
config.MODELS.append(config.USER_MODEL)
|
145
147
|
|
146
148
|
|
147
149
|
def load_log_queries(_configs: dict, /) -> None:
|
@@ -162,12 +164,14 @@ def load_middlewares(_configs: dict, /) -> None:
|
|
162
164
|
_deprecated_warning(
|
163
165
|
field='MIDDLEWARES',
|
164
166
|
message='`data` does not supported in middlewares anymore, as your data is static you may want '
|
165
|
-
|
167
|
+
'to pass them to your middleware with config variables',
|
166
168
|
)
|
167
169
|
middleware = middleware[0]
|
168
170
|
else:
|
169
171
|
raise _exception_handler(
|
170
|
-
field='MIDDLEWARES',
|
172
|
+
field='MIDDLEWARES',
|
173
|
+
error=f'{middleware} should be dotted path or type of a middleware class',
|
174
|
+
)
|
171
175
|
|
172
176
|
# `middleware` can be type or path of a class
|
173
177
|
if not callable(middleware):
|
@@ -175,9 +179,12 @@ def load_middlewares(_configs: dict, /) -> None:
|
|
175
179
|
middleware = import_class(middleware)
|
176
180
|
except (AttributeError, ModuleNotFoundError):
|
177
181
|
raise _exception_handler(
|
178
|
-
field='MIDDLEWARES',
|
182
|
+
field='MIDDLEWARES',
|
183
|
+
error=f'{middleware} is not a valid middleware path or type',
|
184
|
+
)
|
179
185
|
|
180
186
|
if issubclass(middleware, (MonitoringMiddleware, WebsocketMonitoringMiddleware)):
|
187
|
+
monitoring_logger.debug('') # Initiated
|
181
188
|
config.MONITORING = True
|
182
189
|
|
183
190
|
if issubclass(middleware, HTTPMiddleware):
|
@@ -187,7 +194,7 @@ def load_middlewares(_configs: dict, /) -> None:
|
|
187
194
|
else:
|
188
195
|
raise _exception_handler(
|
189
196
|
field='MIDDLEWARES',
|
190
|
-
error='is not a sub class of `HTTPMiddleware` or `WebsocketMiddleware`'
|
197
|
+
error='is not a sub class of `HTTPMiddleware` or `WebsocketMiddleware`',
|
191
198
|
)
|
192
199
|
|
193
200
|
config.HTTP_MIDDLEWARES = middlewares['http']
|
@@ -202,12 +209,14 @@ def load_auto_reformat(_configs: dict, /) -> None:
|
|
202
209
|
def load_background_tasks(_configs: dict, /) -> None:
|
203
210
|
if _configs.get('BACKGROUND_TASKS'):
|
204
211
|
config.BACKGROUND_TASKS = True
|
205
|
-
|
212
|
+
_background_tasks.initialize()
|
206
213
|
|
207
214
|
|
208
|
-
def
|
209
|
-
|
210
|
-
|
215
|
+
def load_other_configs(_configs: dict, /) -> None:
|
216
|
+
known_configs = set(config.__dataclass_fields__)
|
217
|
+
for key, value in _configs.items():
|
218
|
+
if key.isupper() and key not in known_configs:
|
219
|
+
config[key] = value
|
211
220
|
|
212
221
|
|
213
222
|
def load_urls(_configs: dict, /, urls: dict | None) -> None:
|
@@ -222,7 +231,7 @@ def load_urls(_configs: dict, /, urls: dict | None) -> None:
|
|
222
231
|
|
223
232
|
elif isinstance(url_routing, dict):
|
224
233
|
error = (
|
225
|
-
"can't be 'dict', you may want to pass it's value directly to Panther().
|
234
|
+
"can't be 'dict', you may want to pass it's value directly to Panther(). Example: Panther(..., urls=...)"
|
226
235
|
)
|
227
236
|
raise _exception_handler(field='URLs', error=error)
|
228
237
|
|
@@ -256,18 +265,18 @@ def load_authentication_class(_configs: dict, /) -> None:
|
|
256
265
|
|
257
266
|
def load_jwt_config(_configs: dict, /) -> None:
|
258
267
|
"""Only Collect JWT Config If Authentication Is JWTAuthentication"""
|
259
|
-
|
260
|
-
getattr(config.AUTHENTICATION, '__name__', None) == 'JWTAuthentication' or
|
261
|
-
getattr(config.WS_AUTHENTICATION, '__name__', None) == 'QueryParamJWTAuthentication'
|
262
|
-
)
|
263
|
-
jwt = _configs.get('JWTConfig', {})
|
268
|
+
from panther.authentications import JWTAuthentication
|
264
269
|
|
270
|
+
auth_is_jwt = (config.AUTHENTICATION and issubclass(config.AUTHENTICATION, JWTAuthentication)) or (
|
271
|
+
config.WS_AUTHENTICATION and issubclass(config.WS_AUTHENTICATION, JWTAuthentication)
|
272
|
+
)
|
273
|
+
jwt = _configs.get('JWT_CONFIG', {})
|
265
274
|
using_panel_views = HomeView in config.FLAT_URLS.values()
|
266
275
|
if auth_is_jwt or using_panel_views:
|
267
276
|
if 'key' not in jwt:
|
268
277
|
if config.SECRET_KEY is None:
|
269
278
|
raise _exception_handler(field='JWTConfig', error='`JWTConfig.key` or `SECRET_KEY` is required.')
|
270
|
-
jwt['key'] = config.SECRET_KEY
|
279
|
+
jwt['key'] = config.SECRET_KEY
|
271
280
|
config.JWT_CONFIG = JWTConfig(**jwt)
|
272
281
|
|
273
282
|
|
@@ -287,7 +296,7 @@ def load_websocket_connections():
|
|
287
296
|
|
288
297
|
def check_endpoints_inheritance():
|
289
298
|
"""Should be after `load_urls()`"""
|
290
|
-
for
|
299
|
+
for endpoint in config.FLAT_URLS.values():
|
291
300
|
if endpoint == {}:
|
292
301
|
continue
|
293
302
|
|
panther/_utils.py
CHANGED
@@ -4,9 +4,9 @@ import logging
|
|
4
4
|
import re
|
5
5
|
import subprocess
|
6
6
|
import types
|
7
|
-
from collections.abc import Callable
|
7
|
+
from collections.abc import AsyncGenerator, Callable, Generator, Iterator
|
8
8
|
from traceback import TracebackException
|
9
|
-
from typing import Any
|
9
|
+
from typing import Any
|
10
10
|
|
11
11
|
from panther.exceptions import PantherError
|
12
12
|
from panther.file_handler import File
|
@@ -20,51 +20,65 @@ def import_class(dotted_path: str, /) -> type[Any]:
|
|
20
20
|
-------
|
21
21
|
Input: panther.db.models.User
|
22
22
|
Output: User (The Class)
|
23
|
+
|
23
24
|
"""
|
24
25
|
path, name = dotted_path.rsplit('.', 1)
|
25
26
|
module = importlib.import_module(path)
|
26
27
|
return getattr(module, name)
|
27
28
|
|
28
29
|
|
30
|
+
NEWLINE_CRLF = b'\r\n' # Windows-style
|
31
|
+
NEWLINE_LF = b'\n' # Unix/Linux-style
|
32
|
+
|
33
|
+
# Regex patterns for CRLF (Windows)
|
34
|
+
FIELD_PATTERN_CRLF = re.compile(rb'Content-Disposition: form-data; name="(.*)"\r\n\r\n(.*)', flags=re.DOTALL)
|
35
|
+
FILE_PATTERN_CRLF = re.compile(rb'Content-Disposition: form-data; name="(.*)"; filename="(.*)"\r\nContent-Type: (.*)')
|
36
|
+
|
37
|
+
# Regex patterns for LF (Linux)
|
38
|
+
FIELD_PATTERN_LF = re.compile(rb'Content-Disposition: form-data; name="(.*)"\n\n(.*)', flags=re.DOTALL)
|
39
|
+
FILE_PATTERN_LF = re.compile(rb'Content-Disposition: form-data; name="(.*)"; filename="(.*)"\nContent-Type: (.*)')
|
40
|
+
|
41
|
+
|
29
42
|
def read_multipart_form_data(boundary: str, body: bytes) -> dict:
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
+ b'Content-Type: )(.*)'
|
42
|
-
)
|
43
|
+
boundary_bytes = b'--' + boundary.encode()
|
44
|
+
|
45
|
+
# Choose newline type and corresponding patterns
|
46
|
+
if body.endswith(NEWLINE_CRLF):
|
47
|
+
newline = NEWLINE_CRLF
|
48
|
+
field_pattern = FIELD_PATTERN_CRLF
|
49
|
+
file_pattern = FILE_PATTERN_CRLF
|
50
|
+
else:
|
51
|
+
newline = NEWLINE_LF
|
52
|
+
field_pattern = FIELD_PATTERN_LF
|
53
|
+
file_pattern = FILE_PATTERN_LF
|
43
54
|
|
44
55
|
data = {}
|
45
|
-
for
|
46
|
-
|
56
|
+
for part in body.split(boundary_bytes):
|
57
|
+
part = part.removeprefix(newline).removesuffix(newline)
|
47
58
|
|
48
|
-
if
|
59
|
+
if part in (b'', b'--'):
|
49
60
|
continue
|
50
61
|
|
51
|
-
if match :=
|
52
|
-
|
62
|
+
if match := field_pattern.match(string=part):
|
63
|
+
field_name, value = match.groups()
|
53
64
|
data[field_name.decode('utf-8')] = value.decode('utf-8')
|
65
|
+
continue
|
54
66
|
|
67
|
+
try:
|
68
|
+
headers, file_content = part.split(2 * newline, 1)
|
69
|
+
except ValueError:
|
70
|
+
logger.error('Malformed part, skipping.')
|
71
|
+
continue
|
72
|
+
|
73
|
+
if match := file_pattern.match(string=headers):
|
74
|
+
field_name, file_name, content_type = match.groups()
|
75
|
+
data[field_name.decode('utf-8')] = File(
|
76
|
+
file_name=file_name.decode('utf-8'),
|
77
|
+
content_type=content_type.decode('utf-8'),
|
78
|
+
file=file_content,
|
79
|
+
)
|
55
80
|
else:
|
56
|
-
|
57
|
-
|
58
|
-
if match := re.match(pattern=file_pattern, string=file_meta_data):
|
59
|
-
_, field_name, _, file_name, _, content_type = match.groups()
|
60
|
-
file = File(
|
61
|
-
file_name=file_name.decode('utf-8'),
|
62
|
-
content_type=content_type.decode('utf-8'),
|
63
|
-
file=value,
|
64
|
-
)
|
65
|
-
data[field_name.decode('utf-8')] = file
|
66
|
-
else:
|
67
|
-
logger.error('Unrecognized Pattern')
|
81
|
+
logger.error('Unrecognized multipart format')
|
68
82
|
|
69
83
|
return data
|
70
84
|
|
@@ -94,7 +108,8 @@ def check_function_type_endpoint(endpoint: types.FunctionType) -> Callable:
|
|
94
108
|
# Function Doesn't Have @API Decorator
|
95
109
|
if not hasattr(endpoint, '__wrapped__'):
|
96
110
|
raise PantherError(
|
97
|
-
f'You may have forgotten to use `@API()` on the `{endpoint.__module__}.{endpoint.__name__}()`'
|
111
|
+
f'You may have forgotten to use `@API()` on the `{endpoint.__module__}.{endpoint.__name__}()`',
|
112
|
+
)
|
98
113
|
|
99
114
|
|
100
115
|
def check_class_type_endpoint(endpoint: Callable) -> Callable:
|
@@ -104,7 +119,7 @@ def check_class_type_endpoint(endpoint: Callable) -> Callable:
|
|
104
119
|
if not issubclass(endpoint, (GenericAPI, GenericWebsocket)):
|
105
120
|
raise PantherError(
|
106
121
|
f'You may have forgotten to inherit from `panther.app.GenericAPI` or `panther.app.GenericWebsocket` '
|
107
|
-
f'on the `{endpoint.__module__}.{endpoint.__name__}()`'
|
122
|
+
f'on the `{endpoint.__module__}.{endpoint.__name__}()`',
|
108
123
|
)
|
109
124
|
|
110
125
|
|