panther 3.9.0__py3-none-any.whl → 4.0.1__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 +168 -171
- panther/_utils.py +26 -49
- panther/app.py +85 -105
- panther/authentications.py +86 -55
- panther/background_tasks.py +25 -14
- panther/base_request.py +38 -14
- panther/base_websocket.py +171 -94
- panther/caching.py +60 -25
- panther/cli/create_command.py +20 -10
- panther/cli/monitor_command.py +63 -37
- panther/cli/template.py +38 -20
- panther/cli/utils.py +32 -18
- panther/configs.py +65 -58
- panther/db/connections.py +139 -0
- panther/db/cursor.py +43 -0
- panther/db/models.py +64 -29
- panther/db/queries/__init__.py +1 -1
- panther/db/queries/base_queries.py +127 -0
- panther/db/queries/mongodb_queries.py +77 -38
- panther/db/queries/pantherdb_queries.py +59 -30
- panther/db/queries/queries.py +232 -117
- panther/db/utils.py +17 -18
- panther/events.py +44 -0
- panther/exceptions.py +26 -12
- panther/file_handler.py +2 -2
- panther/generics.py +163 -0
- panther/logging.py +7 -2
- panther/main.py +112 -188
- panther/middlewares/base.py +3 -0
- panther/monitoring.py +8 -5
- panther/pagination.py +48 -0
- panther/panel/apis.py +32 -5
- panther/panel/urls.py +2 -1
- panther/permissions.py +3 -3
- panther/request.py +6 -13
- panther/response.py +114 -34
- panther/routings.py +83 -66
- panther/serializer.py +131 -25
- panther/test.py +31 -21
- panther/utils.py +28 -16
- panther/websocket.py +7 -4
- {panther-3.9.0.dist-info → panther-4.0.1.dist-info}/METADATA +93 -71
- panther-4.0.1.dist-info/RECORD +57 -0
- {panther-3.9.0.dist-info → panther-4.0.1.dist-info}/WHEEL +1 -1
- panther/db/connection.py +0 -92
- panther/middlewares/db.py +0 -18
- panther/middlewares/redis.py +0 -47
- panther-3.9.0.dist-info/RECORD +0 -54
- {panther-3.9.0.dist-info → panther-4.0.1.dist-info}/LICENSE +0 -0
- {panther-3.9.0.dist-info → panther-4.0.1.dist-info}/entry_points.txt +0 -0
- {panther-3.9.0.dist-info → panther-4.0.1.dist-info}/top_level.txt +0 -0
panther/__init__.py
CHANGED
panther/_load_configs.py
CHANGED
@@ -1,48 +1,49 @@
|
|
1
|
-
import
|
2
|
-
import platform
|
1
|
+
import logging
|
3
2
|
import sys
|
4
|
-
from datetime import timedelta
|
5
3
|
from importlib import import_module
|
6
|
-
from
|
7
|
-
from typing import Callable
|
8
|
-
|
9
|
-
from pydantic._internal._model_construction import ModelMetaclass
|
4
|
+
from multiprocessing import Manager
|
10
5
|
|
11
6
|
from panther._utils import import_class
|
7
|
+
from panther.background_tasks import background_tasks
|
8
|
+
from panther.base_websocket import WebsocketConnections
|
9
|
+
from panther.cli.utils import import_error
|
12
10
|
from panther.configs import JWTConfig, config
|
11
|
+
from panther.db.connections import redis
|
13
12
|
from panther.db.queries.mongodb_queries import BaseMongoDBQuery
|
14
13
|
from panther.db.queries.pantherdb_queries import BasePantherDBQuery
|
15
|
-
from panther.exceptions import
|
14
|
+
from panther.exceptions import PantherError
|
16
15
|
from panther.middlewares.base import WebsocketMiddleware, HTTPMiddleware
|
16
|
+
from panther.panel.urls import urls as panel_urls
|
17
17
|
from panther.routings import finalize_urls, flatten_urls
|
18
|
-
from panther.throttling import Throttling
|
19
18
|
|
20
19
|
__all__ = (
|
21
20
|
'load_configs_module',
|
21
|
+
'load_redis',
|
22
|
+
'load_startup',
|
23
|
+
'load_shutdown',
|
24
|
+
'load_timezone',
|
25
|
+
'load_database',
|
22
26
|
'load_secret_key',
|
23
27
|
'load_monitoring',
|
28
|
+
'load_throttling',
|
29
|
+
'load_user_model',
|
24
30
|
'load_log_queries',
|
31
|
+
'load_middlewares',
|
32
|
+
'load_auto_reformat',
|
25
33
|
'load_background_tasks',
|
26
|
-
'load_throttling',
|
27
34
|
'load_default_cache_exp',
|
28
|
-
'load_pantherdb_encryption',
|
29
|
-
'load_middlewares',
|
30
|
-
'load_user_model',
|
31
35
|
'load_authentication_class',
|
32
|
-
'load_jwt_config',
|
33
|
-
'load_startup',
|
34
|
-
'load_shutdown',
|
35
|
-
'load_auto_reformat',
|
36
|
-
'collect_all_models',
|
37
36
|
'load_urls',
|
38
|
-
'
|
37
|
+
'load_websocket_connections',
|
39
38
|
)
|
40
39
|
|
40
|
+
logger = logging.getLogger('panther')
|
41
|
+
|
41
42
|
|
42
|
-
def load_configs_module(
|
43
|
-
"""Read the config file
|
44
|
-
if
|
45
|
-
_module = sys.modules[
|
43
|
+
def load_configs_module(module_name: str, /) -> dict:
|
44
|
+
"""Read the config file as dict"""
|
45
|
+
if module_name:
|
46
|
+
_module = sys.modules[module_name]
|
46
47
|
else:
|
47
48
|
try:
|
48
49
|
_module = import_module('core.configs')
|
@@ -51,45 +52,92 @@ def load_configs_module(_configs, /) -> dict:
|
|
51
52
|
return _module.__dict__
|
52
53
|
|
53
54
|
|
54
|
-
def
|
55
|
-
if
|
56
|
-
|
57
|
-
|
55
|
+
def load_redis(_configs: dict, /) -> None:
|
56
|
+
if redis_config := _configs.get('REDIS'):
|
57
|
+
# Check redis module installation
|
58
|
+
try:
|
59
|
+
from redis.asyncio import Redis
|
60
|
+
except ImportError as e:
|
61
|
+
raise import_error(e, package='redis')
|
62
|
+
redis_class_path = redis_config.get('class', 'panther.db.connections.RedisConnection')
|
63
|
+
redis_class = import_class(redis_class_path)
|
64
|
+
# We have to create another dict then pop the 'class' else we can't pass the tests
|
65
|
+
args = redis_config.copy()
|
66
|
+
args.pop('class', None)
|
67
|
+
redis_class(**args, init=True)
|
58
68
|
|
59
69
|
|
60
|
-
def
|
61
|
-
|
70
|
+
def load_startup(_configs: dict, /) -> None:
|
71
|
+
if startup := _configs.get('STARTUP'):
|
72
|
+
config.STARTUP = import_class(startup)
|
62
73
|
|
63
74
|
|
64
|
-
def
|
65
|
-
|
75
|
+
def load_shutdown(_configs: dict, /) -> None:
|
76
|
+
if shutdown := _configs.get('SHUTDOWN'):
|
77
|
+
config.SHUTDOWN = import_class(shutdown)
|
66
78
|
|
67
79
|
|
68
|
-
def
|
69
|
-
|
80
|
+
def load_timezone(_configs: dict, /) -> None:
|
81
|
+
if timezone := _configs.get('TIMEZONE'):
|
82
|
+
config.TIMEZONE = timezone
|
70
83
|
|
71
84
|
|
72
|
-
def
|
73
|
-
|
85
|
+
def load_database(_configs: dict, /) -> None:
|
86
|
+
database_config = _configs.get('DATABASE', {})
|
87
|
+
if 'engine' in database_config:
|
88
|
+
if 'class' not in database_config['engine']:
|
89
|
+
raise _exception_handler(field='DATABASE', error=f'`engine["class"]` not found.')
|
74
90
|
|
91
|
+
engine_class_path = database_config['engine']['class']
|
92
|
+
engine_class = import_class(engine_class_path)
|
93
|
+
# We have to create another dict then pop the 'class' else we can't pass the tests
|
94
|
+
args = database_config['engine'].copy()
|
95
|
+
args.pop('class')
|
96
|
+
config.DATABASE = engine_class(**args)
|
75
97
|
|
76
|
-
|
77
|
-
|
98
|
+
if engine_class_path == 'panther.db.connections.PantherDBConnection':
|
99
|
+
config.QUERY_ENGINE = BasePantherDBQuery
|
100
|
+
elif engine_class_path == 'panther.db.connections.MongoDBConnection':
|
101
|
+
config.QUERY_ENGINE = BaseMongoDBQuery
|
78
102
|
|
103
|
+
if 'query' in database_config:
|
104
|
+
if config.QUERY_ENGINE:
|
105
|
+
logger.warning('`DATABASE.query` has already been filled.')
|
106
|
+
config.QUERY_ENGINE = import_class(database_config['query'])
|
79
107
|
|
80
|
-
def load_pantherdb_encryption(configs: dict, /) -> bool:
|
81
|
-
return configs.get('PANTHERDB_ENCRYPTION', config['pantherdb_encryption'])
|
82
108
|
|
109
|
+
def load_secret_key(_configs: dict, /) -> None:
|
110
|
+
if secret_key := _configs.get('SECRET_KEY'):
|
111
|
+
config.SECRET_KEY = secret_key.encode()
|
83
112
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
113
|
+
|
114
|
+
def load_monitoring(_configs: dict, /) -> None:
|
115
|
+
if _configs.get('MONITORING'):
|
116
|
+
config.MONITORING = True
|
117
|
+
|
118
|
+
|
119
|
+
def load_throttling(_configs: dict, /) -> None:
|
120
|
+
if throttling := _configs.get('THROTTLING'):
|
121
|
+
config.THROTTLING = throttling
|
122
|
+
|
123
|
+
|
124
|
+
def load_user_model(_configs: dict, /) -> None:
|
125
|
+
config.USER_MODEL = import_class(_configs.get('USER_MODEL', 'panther.db.models.BaseUser'))
|
126
|
+
config.MODELS.append(config.USER_MODEL)
|
127
|
+
|
128
|
+
|
129
|
+
def load_log_queries(_configs: dict, /) -> None:
|
130
|
+
if _configs.get('LOG_QUERIES'):
|
131
|
+
config.LOG_QUERIES = True
|
132
|
+
|
133
|
+
|
134
|
+
def load_middlewares(_configs: dict, /) -> None:
|
88
135
|
from panther.middlewares import BaseMiddleware
|
89
136
|
|
90
137
|
middlewares = {'http': [], 'ws': []}
|
91
138
|
|
92
|
-
|
139
|
+
# Collect Middlewares
|
140
|
+
for middleware in _configs.get('MIDDLEWARES') or []:
|
93
141
|
if not isinstance(middleware, list | tuple):
|
94
142
|
raise _exception_handler(field='MIDDLEWARES', error=f'{middleware} should have 2 part: (path, kwargs)')
|
95
143
|
|
@@ -103,164 +151,113 @@ def load_middlewares(configs: dict, /) -> dict:
|
|
103
151
|
else:
|
104
152
|
path, data = middleware
|
105
153
|
|
106
|
-
if path.find('panther.middlewares.db.DatabaseMiddleware') != -1:
|
107
|
-
# Keep it simple for now, we are going to make it dynamic in the next patch
|
108
|
-
if data['url'].split(':')[0] == 'pantherdb':
|
109
|
-
config['query_engine'] = BasePantherDBQuery
|
110
|
-
else:
|
111
|
-
config['query_engine'] = BaseMongoDBQuery
|
112
154
|
try:
|
113
|
-
|
155
|
+
middleware_class = import_class(path)
|
114
156
|
except (AttributeError, ModuleNotFoundError):
|
115
157
|
raise _exception_handler(field='MIDDLEWARES', error=f'{path} is not a valid middleware path')
|
116
158
|
|
117
|
-
if issubclass(
|
159
|
+
if issubclass(middleware_class, BaseMiddleware) is False:
|
118
160
|
raise _exception_handler(field='MIDDLEWARES', error='is not a sub class of BaseMiddleware')
|
119
161
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
if
|
124
|
-
middlewares['ws'].append(
|
125
|
-
|
162
|
+
if middleware_class.__bases__[0] in (BaseMiddleware, HTTPMiddleware):
|
163
|
+
middlewares['http'].append((middleware_class, data))
|
164
|
+
|
165
|
+
if middleware_class.__bases__[0] in (BaseMiddleware, WebsocketMiddleware):
|
166
|
+
middlewares['ws'].append((middleware_class, data))
|
167
|
+
|
168
|
+
config.HTTP_MIDDLEWARES = middlewares['http']
|
169
|
+
config.WS_MIDDLEWARES = middlewares['ws']
|
126
170
|
|
127
171
|
|
128
|
-
def
|
129
|
-
|
172
|
+
def load_auto_reformat(_configs: dict, /) -> None:
|
173
|
+
if _configs.get('AUTO_REFORMAT'):
|
174
|
+
config.AUTO_REFORMAT = True
|
130
175
|
|
131
176
|
|
132
|
-
def
|
133
|
-
|
177
|
+
def load_background_tasks(_configs: dict, /) -> None:
|
178
|
+
if _configs.get('BACKGROUND_TASKS'):
|
179
|
+
config.BACKGROUND_TASKS = True
|
180
|
+
background_tasks.initialize()
|
134
181
|
|
135
182
|
|
136
|
-
def
|
183
|
+
def load_default_cache_exp(_configs: dict, /) -> None:
|
184
|
+
if default_cache_exp := _configs.get('DEFAULT_CACHE_EXP'):
|
185
|
+
config.DEFAULT_CACHE_EXP = default_cache_exp
|
186
|
+
|
187
|
+
|
188
|
+
def load_authentication_class(_configs: dict, /) -> None:
|
189
|
+
"""Should be after `load_secret_key()`"""
|
190
|
+
if authentication := _configs.get('AUTHENTICATION'):
|
191
|
+
config.AUTHENTICATION = import_class(authentication)
|
192
|
+
|
193
|
+
if ws_authentication := _configs.get('WS_AUTHENTICATION'):
|
194
|
+
config.WS_AUTHENTICATION = import_class(ws_authentication)
|
195
|
+
|
196
|
+
load_jwt_config(_configs)
|
197
|
+
|
198
|
+
|
199
|
+
def load_jwt_config(_configs: dict, /) -> None:
|
137
200
|
"""Only Collect JWT Config If Authentication Is JWTAuthentication"""
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
def load_shutdown(configs: dict, /) -> Callable:
|
154
|
-
return configs.get('SHUTDOWN') and import_class(configs['SHUTDOWN'])
|
155
|
-
|
156
|
-
|
157
|
-
def load_auto_reformat(configs: dict, /) -> bool:
|
158
|
-
return configs.get('AUTO_REFORMAT', config['auto_reformat'])
|
159
|
-
|
160
|
-
|
161
|
-
def collect_all_models() -> list[dict]:
|
162
|
-
"""Collecting all models for panel APIs"""
|
163
|
-
from panther.db.models import Model
|
164
|
-
|
165
|
-
# Just load all the python files from 'base_dir',
|
166
|
-
# so Model.__subclasses__ can find all the subclasses
|
167
|
-
slash = '\\' if platform.system() == 'Windows' else '/'
|
168
|
-
_parts = '_tail' if sys.version_info >= (3, 12) else '_parts'
|
169
|
-
python_files = [
|
170
|
-
f for f in config['base_dir'].rglob('*.py')
|
171
|
-
if not f.name.startswith('_') and 'site-packages' not in getattr(f.parents, _parts)
|
172
|
-
]
|
173
|
-
for file in python_files:
|
174
|
-
# Analyse the file
|
175
|
-
with Path(file).open() as f:
|
176
|
-
node = ast.parse(f.read())
|
177
|
-
|
178
|
-
model_imported = False
|
179
|
-
panther_imported = False
|
180
|
-
panther_called = False
|
181
|
-
for n in node.body:
|
182
|
-
match n:
|
183
|
-
|
184
|
-
# from panther.db import Model
|
185
|
-
case ast.ImportFrom(module='panther.db', names=[ast.alias(name='Model')]):
|
186
|
-
model_imported = True
|
187
|
-
|
188
|
-
# from panther.db.models import ..., Model, ...
|
189
|
-
case ast.ImportFrom(module='panther.db.models', names=[*names]):
|
190
|
-
try:
|
191
|
-
next(v for v in names if v.name == 'Model')
|
192
|
-
model_imported = True
|
193
|
-
except StopIteration:
|
194
|
-
pass
|
195
|
-
|
196
|
-
# from panther import Panther, ...
|
197
|
-
case ast.ImportFrom(module='panther', names=[ast.alias(name='Panther'), *_]):
|
198
|
-
panther_imported = True
|
199
|
-
|
200
|
-
# from panther import ..., Panther
|
201
|
-
case ast.ImportFrom(module='panther', names=[*_, ast.alias(name='Panther')]):
|
202
|
-
panther_imported = True
|
203
|
-
|
204
|
-
# ... = Panther(...)
|
205
|
-
case ast.Assign(value=ast.Call(func=ast.Name(id='Panther'))):
|
206
|
-
panther_called = True
|
207
|
-
|
208
|
-
# Panther() should not be called in the file and Model() should be imported,
|
209
|
-
# We check the import of the Panther to make sure he is calling the panther.Panther and not any Panther
|
210
|
-
if panther_imported and panther_called or not model_imported:
|
211
|
-
continue
|
212
|
-
|
213
|
-
# Load the module
|
214
|
-
dotted_f = str(file).removeprefix(f'{config["base_dir"]}{slash}').removesuffix('.py').replace(slash, '.')
|
215
|
-
import_module(dotted_f)
|
216
|
-
|
217
|
-
return [
|
218
|
-
{
|
219
|
-
'name': m.__name__,
|
220
|
-
'module': m.__module__,
|
221
|
-
'class': m
|
222
|
-
} for m in Model.__subclasses__() if m.__module__ != 'panther.db.models'
|
223
|
-
]
|
224
|
-
|
225
|
-
|
226
|
-
def load_urls(configs: dict, /, urls: dict | None) -> tuple[dict, dict]:
|
201
|
+
auth_is_jwt = (
|
202
|
+
getattr(config.AUTHENTICATION, '__name__', None) == 'JWTAuthentication' or
|
203
|
+
getattr(config.WS_AUTHENTICATION, '__name__', None) == 'QueryParamJWTAuthentication'
|
204
|
+
)
|
205
|
+
jwt = _configs.get('JWTConfig', {})
|
206
|
+
if auth_is_jwt or jwt:
|
207
|
+
if 'key' not in jwt:
|
208
|
+
if config.SECRET_KEY is None:
|
209
|
+
raise _exception_handler(field='JWTConfig', error='`JWTConfig.key` or `SECRET_KEY` is required.')
|
210
|
+
jwt['key'] = config.SECRET_KEY.decode()
|
211
|
+
config.JWT_CONFIG = JWTConfig(**jwt)
|
212
|
+
|
213
|
+
|
214
|
+
def load_urls(_configs: dict, /, urls: dict | None) -> None:
|
227
215
|
"""
|
228
216
|
Return tuple of all urls (as a flat dict) and (as a nested dict)
|
229
217
|
"""
|
230
218
|
if isinstance(urls, dict):
|
231
|
-
|
232
|
-
return collected_urls, finalize_urls(collected_urls)
|
219
|
+
pass
|
233
220
|
|
234
|
-
|
235
|
-
raise _exception_handler(field='URLs', error='
|
221
|
+
elif (url_routing := _configs.get('URLs')) is None:
|
222
|
+
raise _exception_handler(field='URLs', error='required.')
|
236
223
|
|
237
|
-
|
224
|
+
elif isinstance(url_routing, dict):
|
238
225
|
error = (
|
239
226
|
"can't be 'dict', you may want to pass it's value directly to Panther(). " 'Example: Panther(..., urls=...)'
|
240
227
|
)
|
241
228
|
raise _exception_handler(field='URLs', error=error)
|
242
229
|
|
243
|
-
|
230
|
+
elif not isinstance(url_routing, str):
|
244
231
|
error = 'should be dotted string.'
|
245
232
|
raise _exception_handler(field='URLs', error=error)
|
246
233
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
234
|
+
else:
|
235
|
+
try:
|
236
|
+
urls = import_class(url_routing)
|
237
|
+
except ModuleNotFoundError as e:
|
238
|
+
raise _exception_handler(field='URLs', error=e)
|
251
239
|
|
252
|
-
|
253
|
-
|
240
|
+
if not isinstance(urls, dict):
|
241
|
+
raise _exception_handler(field='URLs', error='should point to a dict.')
|
254
242
|
|
255
|
-
|
256
|
-
|
243
|
+
config.FLAT_URLS = flatten_urls(urls)
|
244
|
+
config.URLS = finalize_urls(config.FLAT_URLS)
|
245
|
+
config.URLS['_panel'] = finalize_urls(flatten_urls(panel_urls))
|
257
246
|
|
258
247
|
|
259
|
-
def
|
260
|
-
|
248
|
+
def load_websocket_connections():
|
249
|
+
"""Should be after `load_redis()`"""
|
250
|
+
if config.HAS_WS:
|
251
|
+
# Check `websockets`
|
252
|
+
try:
|
253
|
+
import websockets
|
254
|
+
except ImportError as e:
|
255
|
+
raise import_error(e, package='websockets')
|
261
256
|
|
262
|
-
|
257
|
+
# Use the redis pubsub if `redis.is_connected`, else use the `multiprocessing.Manager`
|
258
|
+
pubsub_connection = redis.create_connection_for_websocket() if redis.is_connected else Manager()
|
259
|
+
config.WEBSOCKET_CONNECTIONS = WebsocketConnections(pubsub_connection=pubsub_connection)
|
263
260
|
|
264
261
|
|
265
|
-
def _exception_handler(field: str, error: str | Exception) ->
|
266
|
-
return
|
262
|
+
def _exception_handler(field: str, error: str | Exception) -> PantherError:
|
263
|
+
return PantherError(f"Invalid '{field}': {error}")
|
panther/_utils.py
CHANGED
@@ -4,57 +4,17 @@ import logging
|
|
4
4
|
import re
|
5
5
|
import subprocess
|
6
6
|
import types
|
7
|
+
from typing import Any, Generator, Iterator, AsyncGenerator
|
7
8
|
from collections.abc import Callable
|
8
9
|
from traceback import TracebackException
|
9
|
-
from uuid import uuid4
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
from panther import status
|
14
|
-
from panther.exceptions import PantherException
|
11
|
+
from panther.exceptions import PantherError
|
15
12
|
from panther.file_handler import File
|
16
13
|
|
17
14
|
logger = logging.getLogger('panther')
|
18
15
|
|
19
16
|
|
20
|
-
|
21
|
-
bytes_headers = [[k.encode(), str(v).encode()] for k, v in (headers or {}).items()]
|
22
|
-
await send({
|
23
|
-
'type': 'http.response.start',
|
24
|
-
'status': status_code,
|
25
|
-
'headers': bytes_headers,
|
26
|
-
})
|
27
|
-
|
28
|
-
|
29
|
-
async def _http_response_body(send: Callable, /, body: bytes | None = None) -> None:
|
30
|
-
if body is None:
|
31
|
-
await send({'type': 'http.response.body'})
|
32
|
-
else:
|
33
|
-
await send({'type': 'http.response.body', 'body': body})
|
34
|
-
|
35
|
-
|
36
|
-
async def http_response(
|
37
|
-
send: Callable,
|
38
|
-
/,
|
39
|
-
*,
|
40
|
-
status_code: int,
|
41
|
-
monitoring=None, # type: MonitoringMiddleware | None
|
42
|
-
headers: dict | None = None,
|
43
|
-
body: bytes | None = None,
|
44
|
-
exception: bool = False,
|
45
|
-
) -> None:
|
46
|
-
if exception:
|
47
|
-
body = json.dumps({'detail': status.status_text[status_code]})
|
48
|
-
elif status_code == status.HTTP_204_NO_CONTENT or body == b'null':
|
49
|
-
body = None
|
50
|
-
|
51
|
-
await monitoring.after(status_code)
|
52
|
-
|
53
|
-
await _http_response_start(send, headers=headers, status_code=status_code)
|
54
|
-
await _http_response_body(send, body=body)
|
55
|
-
|
56
|
-
|
57
|
-
def import_class(dotted_path: str, /) -> type:
|
17
|
+
def import_class(dotted_path: str, /) -> type[Any]:
|
58
18
|
"""
|
59
19
|
Example:
|
60
20
|
-------
|
@@ -109,10 +69,6 @@ def read_multipart_form_data(boundary: str, body: bytes) -> dict:
|
|
109
69
|
return data
|
110
70
|
|
111
71
|
|
112
|
-
def generate_ws_connection_id() -> str:
|
113
|
-
return uuid4().hex
|
114
|
-
|
115
|
-
|
116
72
|
def is_function_async(func: Callable) -> bool:
|
117
73
|
"""
|
118
74
|
Sync result is 0 --> False
|
@@ -137,7 +93,7 @@ def reformat_code(base_dir):
|
|
137
93
|
subprocess.run(['ruff', 'format', base_dir])
|
138
94
|
subprocess.run(['ruff', 'check', '--select', 'I', '--fix', base_dir])
|
139
95
|
except FileNotFoundError:
|
140
|
-
raise
|
96
|
+
raise PantherError("No module named 'ruff', Hint: `pip install ruff`")
|
141
97
|
|
142
98
|
|
143
99
|
def check_function_type_endpoint(endpoint: types.FunctionType) -> Callable:
|
@@ -155,4 +111,25 @@ def check_class_type_endpoint(endpoint: Callable) -> Callable:
|
|
155
111
|
logger.critical(f'You may have forgotten to inherit from GenericAPI on the {endpoint.__name__}()')
|
156
112
|
raise TypeError
|
157
113
|
|
158
|
-
return endpoint.call_method
|
114
|
+
return endpoint().call_method
|
115
|
+
|
116
|
+
|
117
|
+
def async_next(iterator: Iterator):
|
118
|
+
"""
|
119
|
+
The StopIteration exception is a special case in Python,
|
120
|
+
particularly when it comes to asynchronous programming and the use of asyncio.
|
121
|
+
This is because StopIteration is not meant to be caught in the traditional sense;
|
122
|
+
it's used internally by Python to signal the end of an iteration.
|
123
|
+
"""
|
124
|
+
try:
|
125
|
+
return next(iterator)
|
126
|
+
except StopIteration:
|
127
|
+
raise StopAsyncIteration
|
128
|
+
|
129
|
+
|
130
|
+
async def to_async_generator(generator: Generator) -> AsyncGenerator:
|
131
|
+
while True:
|
132
|
+
try:
|
133
|
+
yield await asyncio.to_thread(async_next, iter(generator))
|
134
|
+
except StopAsyncIteration:
|
135
|
+
break
|