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.
Files changed (57) 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 +30 -24
  18. panther/db/cursor.py +3 -1
  19. panther/db/models.py +26 -10
  20. panther/db/queries/base_queries.py +4 -5
  21. panther/db/queries/mongodb_queries.py +21 -21
  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 +74 -100
  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 +101 -94
  44. panther/routings.py +12 -12
  45. panther/serializer.py +20 -43
  46. panther/test.py +73 -58
  47. panther/throttling.py +68 -3
  48. panther/utils.py +5 -11
  49. panther-5.0.0b5.dist-info/METADATA +188 -0
  50. panther-5.0.0b5.dist-info/RECORD +75 -0
  51. panther/monitoring.py +0 -34
  52. panther-5.0.0b3.dist-info/METADATA +0 -223
  53. panther-5.0.0b3.dist-info/RECORD +0 -75
  54. {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/WHEEL +0 -0
  55. {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/entry_points.txt +0 -0
  56. {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/licenses/LICENSE +0 -0
  57. {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/top_level.txt +0 -0
panther/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from panther.main import Panther # noqa: F401
2
2
 
3
- __version__ = '5.0.0beta3'
3
+ __version__ = '5.0.0beta5'
4
4
 
5
5
 
6
6
  def version():
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 import_class, check_function_type_endpoint, check_class_type_endpoint
10
- from panther.background_tasks import background_tasks
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 WebsocketMiddleware, HTTPMiddleware
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
- 'load_auto_reformat',
37
- 'load_background_tasks',
38
- 'load_default_cache_exp',
38
+ 'load_throttling',
39
+ 'load_timezone',
39
40
  'load_urls',
40
- 'load_authentication_class',
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=f'`engine["class"]` not found.')
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.encode()
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.MODELS.append(config.USER_MODEL)
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
- 'to pass them to your middleware with config variables'
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', error=f'{middleware} should be dotted path or type of a middleware class')
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', error=f'{middleware} is not a valid middleware path or type')
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
- background_tasks.initialize()
212
+ _background_tasks.initialize()
206
213
 
207
214
 
208
- def load_default_cache_exp(_configs: dict, /) -> None:
209
- if default_cache_exp := _configs.get('DEFAULT_CACHE_EXP'):
210
- config.DEFAULT_CACHE_EXP = default_cache_exp
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(). " 'Example: Panther(..., urls=...)'
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
- auth_is_jwt = (
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.decode()
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 _, endpoint in config.FLAT_URLS.items():
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, Generator, Iterator, AsyncGenerator
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
- boundary = b'--' + boundary.encode()
31
- new_line = b'\r\n' if body[-2:] == b'\r\n' else b'\n'
32
-
33
- field_pattern = (
34
- rb'(Content-Disposition: form-data; name=")(.*)("'
35
- + 2 * new_line
36
- + b')(.*)'
37
- )
38
- file_pattern = (
39
- rb'(Content-Disposition: form-data; name=")(.*)("; filename=")(.*)("'
40
- + new_line
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 _row in body.split(boundary):
46
- row = _row.removeprefix(new_line).removesuffix(new_line)
56
+ for part in body.split(boundary_bytes):
57
+ part = part.removeprefix(newline).removesuffix(newline)
47
58
 
48
- if row in (b'', b'--'):
59
+ if part in (b'', b'--'):
49
60
  continue
50
61
 
51
- if match := re.match(pattern=field_pattern, string=row, flags=re.DOTALL):
52
- _, field_name, _, value = match.groups()
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
- file_meta_data, value = row.split(2 * new_line, 1)
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