panther 3.1.4__py3-none-any.whl → 3.1.5__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 CHANGED
@@ -1,6 +1,6 @@
1
1
  from panther.main import Panther # noqa: F401
2
2
 
3
- __version__ = '3.1.4'
3
+ __version__ = '3.1.5'
4
4
 
5
5
 
6
6
  def version():
panther/_load_configs.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import ast
2
- import os
3
2
  import platform
4
3
  import sys
5
4
  from datetime import timedelta
@@ -11,7 +10,6 @@ from pydantic._internal._model_construction import ModelMetaclass
11
10
  from panther._utils import import_class
12
11
  from panther.configs import JWTConfig, config
13
12
  from panther.exceptions import PantherException
14
- from panther.middlewares import BaseMiddleware
15
13
  from panther.routings import finalize_urls, flatten_urls
16
14
  from panther.throttling import Throttling
17
15
 
@@ -73,6 +71,8 @@ def load_default_cache_exp(configs: dict, /) -> timedelta | None:
73
71
 
74
72
  def load_middlewares(configs: dict, /) -> list:
75
73
  """Collect The Middlewares & Set db_engine If One Of Middlewares Was For DB"""
74
+ from panther.middlewares import BaseMiddleware
75
+
76
76
  middlewares = []
77
77
 
78
78
  for path, data in configs.get('MIDDLEWARES', []):
@@ -113,46 +113,64 @@ def collect_all_models() -> list[dict]:
113
113
  """Collecting all models for panel APIs"""
114
114
  from panther.db.models import Model
115
115
 
116
- collected_models = []
117
-
118
- for root, _, files in os.walk(config['base_dir']):
119
- # Traverse through each directory
120
- for f in files:
121
- # Traverse through each file of directory
122
- if f == 'models.py':
123
- slash = '\\' if platform.system() == 'Windows' else '/'
124
-
125
- # If the file was "models.py" read it
126
- file_path = f'{root}{slash}models.py'
127
- with Path(file_path).open() as file:
128
- # Parse the file with ast
129
- node = ast.parse(file.read())
130
- for n in node.body:
131
- # Find classes in each element of files' body
132
- if type(n) is ast.ClassDef and n.bases:
133
- class_path = (
134
- file_path.removesuffix(f'{slash}models.py')
135
- .removeprefix(f'{config["base_dir"]}{slash}')
136
- .replace(slash, '.')
137
- )
138
- # We don't need to import the package classes
139
- if class_path.find('site-packages') == -1:
140
- # Import the class to check his parents and siblings
141
- klass = import_class(f'{class_path}.models.{n.name}')
142
-
143
- collected_models.extend(
144
- [
145
- {
146
- 'name': n.name,
147
- 'path': file_path,
148
- 'class': klass,
149
- 'app': class_path.split('.'),
150
- }
151
- for parent in klass.__mro__
152
- if parent is Model
153
- ]
154
- )
155
- return collected_models
116
+ # Just load all the python files from 'base_dir',
117
+ # so Model.__subclasses__ can find all the subclasses
118
+ slash = '\\' if platform.system() == 'Windows' else '/'
119
+ python_files = [
120
+ f for f in config['base_dir'].rglob('*.py')
121
+ if not f.name.startswith('_') and 'site-packages' not in f.parents._parts
122
+ ]
123
+ for file in python_files:
124
+ # Analyse the file
125
+ with Path(file).open() as f:
126
+ node = ast.parse(f.read())
127
+
128
+ model_imported = False
129
+ panther_imported = False
130
+ panther_called = False
131
+ for n in node.body:
132
+ match n:
133
+
134
+ # from panther.db import Model
135
+ case ast.ImportFrom(module='panther.db', names=[ast.alias(name='Model')]):
136
+ model_imported = True
137
+
138
+ # from panther.db.models import ..., Model, ...
139
+ case ast.ImportFrom(module='panther.db.models', names=[*names]):
140
+ try:
141
+ next(v for v in names if v.name == 'Model')
142
+ model_imported = True
143
+ except StopIteration:
144
+ pass
145
+
146
+ # from panther import Panther, ...
147
+ case ast.ImportFrom(module='panther', names=[ast.alias(name='Panther'), *_]):
148
+ panther_imported = True
149
+
150
+ # from panther import ..., Panther
151
+ case ast.ImportFrom(module='panther', names=[*_, ast.alias(name='Panther')]):
152
+ panther_imported = True
153
+
154
+ # ... = Panther(...)
155
+ case ast.Assign(value=ast.Call(func=ast.Name(id='Panther'))):
156
+ panther_called = True
157
+
158
+ # Panther() should not be called in the file and Model() should be imported,
159
+ # We check the import of the Panther to make sure he is calling the panther.Panther and not any Panther
160
+ if panther_imported and panther_called or not model_imported:
161
+ continue
162
+
163
+ # Load the module
164
+ dotted_f = str(file).removeprefix(f'{config["base_dir"]}{slash}').removesuffix('.py').replace(slash, '.')
165
+ import_module(dotted_f)
166
+
167
+ return [
168
+ {
169
+ 'name': m.__name__,
170
+ 'module': m.__module__,
171
+ 'class': m
172
+ } for m in Model.__subclasses__() if m.__module__ != 'panther.db.models'
173
+ ]
156
174
 
157
175
 
158
176
  def load_urls(configs: dict, /, urls: dict | None) -> tuple[dict, dict]:
panther/_utils.py CHANGED
@@ -45,7 +45,7 @@ async def http_response(
45
45
  elif status_code == status.HTTP_204_NO_CONTENT or body == b'null':
46
46
  body = None
47
47
 
48
- await monitoring.after(status_code=status_code)
48
+ await monitoring.after(status_code)
49
49
 
50
50
  await _http_response_start(send, headers=headers, status_code=status_code)
51
51
  await _http_response_body(send, body=body)
panther/main.py CHANGED
@@ -127,16 +127,20 @@ class Panther:
127
127
 
128
128
  async def handle_ws(self, scope: dict, receive: Callable, send: Callable) -> None:
129
129
  from panther.websocket import GenericWebsocket, Websocket
130
+ monitoring = Monitoring(is_active=config['monitoring'], is_ws=True)
130
131
 
131
132
  temp_connection = Websocket(scope=scope, receive=receive, send=send)
133
+ await monitoring.before(request=temp_connection)
132
134
 
133
135
  endpoint, found_path = find_endpoint(path=temp_connection.path)
134
136
  if endpoint is None:
137
+ await monitoring.after('Rejected')
135
138
  return await temp_connection.close(status.WS_1000_NORMAL_CLOSURE)
136
139
  path_variables: dict = collect_path_variables(request_path=temp_connection.path, found_path=found_path)
137
140
 
138
141
  if not issubclass(endpoint, GenericWebsocket):
139
142
  logger.critical(f'You may have forgotten to inherit from GenericWebsocket on the {endpoint.__name__}()')
143
+ await monitoring.after('Rejected')
140
144
  return await temp_connection.close(status.WS_1014_BAD_GATEWAY)
141
145
 
142
146
  del temp_connection
@@ -152,12 +156,15 @@ class Panther:
152
156
  break
153
157
  else:
154
158
  await self.websocket_connections.new_connection(connection=connection)
159
+ await monitoring.after('Accepted')
155
160
  await connection.listen()
156
161
 
157
162
  # Call 'After' Middleware
158
163
  for middleware in config['reversed_middlewares']:
159
164
  with contextlib.suppress(APIException):
160
165
  await middleware.after(response=connection)
166
+
167
+ await monitoring.after('Closed')
161
168
  return None
162
169
 
163
170
  async def handle_http(self, scope: dict, receive: Callable, send: Callable) -> None:
panther/monitoring.py CHANGED
@@ -1,7 +1,8 @@
1
1
  import logging
2
2
  from time import perf_counter
3
+ from typing import Literal
3
4
 
4
- from panther.request import Request
5
+ from panther.base_request import BaseRequest
5
6
 
6
7
 
7
8
  logger = logging.getLogger('monitoring')
@@ -10,18 +11,31 @@ logger = logging.getLogger('monitoring')
10
11
  class Monitoring:
11
12
  """
12
13
  Create Log Message Like Below:
13
- [method] path | ip:port | response_time ms | status_code
14
+ date time | method | path | ip:port | response_time [ms, s] | status
14
15
  """
15
- def __init__(self, is_active: bool):
16
+ def __init__(self, is_active: bool, is_ws: bool = False):
16
17
  self.is_active = is_active
18
+ self.is_ws = is_ws
17
19
 
18
- async def before(self, request: Request):
20
+ async def before(self, request: BaseRequest):
19
21
  if self.is_active:
20
22
  ip, port = request.client
21
- self.log = f'{request.method} | {request.path} | {ip}:{port}'
23
+
24
+ if self.is_ws:
25
+ method = 'WS'
26
+ else:
27
+ method = request.scope['method']
28
+
29
+ self.log = f'{method} | {request.path} | {ip}:{port}'
22
30
  self.start_time = perf_counter()
23
31
 
24
- async def after(self, status_code: int):
32
+ async def after(self, status: int | Literal['Accepted', 'Rejected', 'Closed'], /):
25
33
  if self.is_active:
26
- response_time = (perf_counter() - self.start_time) * 1_000
27
- logger.info(f'{self.log} | {response_time: .3} ms | {status_code}')
34
+ response_time = perf_counter() - self.start_time
35
+ time_unit = ' s'
36
+
37
+ if response_time < 0.01:
38
+ response_time = response_time * 1_000
39
+ time_unit = 'ms'
40
+
41
+ logger.info(f'{self.log} | {round(response_time, 4)} {time_unit} | {status}')
panther/panel/apis.py CHANGED
@@ -8,16 +8,11 @@ from panther.response import Response
8
8
 
9
9
  @API()
10
10
  async def models_api():
11
- result = []
12
- for i, m in enumerate(config['models']):
13
- data = {
14
- 'name': m['name'],
15
- 'app': '.'.join(a for a in m['app']),
16
- 'path': m['path'],
17
- 'index': i
18
- }
19
- result.append(data)
20
- return result
11
+ return [{
12
+ 'name': m['name'],
13
+ 'module': m['module'],
14
+ 'index': i
15
+ } for i, m in enumerate(config['models'])]
21
16
 
22
17
 
23
18
  @API()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: panther
3
- Version: 3.1.4
3
+ Version: 3.1.5
4
4
  Summary: Fast & Friendly, Web Framework For Building Async APIs
5
5
  Home-page: https://github.com/alirn76/panther
6
6
  Author: Ali RajabNezhad
@@ -1,6 +1,6 @@
1
- panther/__init__.py,sha256=LFvWeZTInMxk2FOKc0DiIGiqvu2sPsp7wQRb00FAvho,110
2
- panther/_load_configs.py,sha256=ZkeKEIDzdGN7zDK4mklpkC1uK9w_fyVVmscvKILEc9E,7126
3
- panther/_utils.py,sha256=72VdkiAjFJt0zz-c7eb8cWvhCoz3Hswov4nuTrLgi80,4358
1
+ panther/__init__.py,sha256=6LwXZVmGyh5YmGDfYREWyRqoMsv9NiCxNibakKFUzJo,110
2
+ panther/_load_configs.py,sha256=S5zN5ErktjXCIi7cgg8HZwRCez51qiYxa1Ppu2No9Xw,7476
3
+ panther/_utils.py,sha256=H0VIv9VPt7mIDZo-N7x5ZaMMes1jo60wp1TtxkNvFnQ,4346
4
4
  panther/app.py,sha256=8-W1XsOfVjNcP1RxV8X9TgQzshP9Qk9E0LBcXBf25aM,7961
5
5
  panther/authentications.py,sha256=Fh7Z7rpysSJXN9xEKJqH94oYf2L3jJQxr6fPhRz3UFg,4149
6
6
  panther/background_tasks.py,sha256=J6JrjiEzyGohYsaFZHnZtXeP52piP84RlGFLsKOFa-Y,7225
@@ -11,8 +11,8 @@ panther/configs.py,sha256=lvmPDT_W8gnXNv214zLQedYFZhM78E-3XGa7KyWQbo4,1920
11
11
  panther/exceptions.py,sha256=DB6nGfU5LxrglEN_I-HInMqdIA3ZmN8rRv0ynEuQyGA,1332
12
12
  panther/file_handler.py,sha256=b-paQykGfjlvslfEkM23V50v8bosjRLq1S1HmsmlXEc,855
13
13
  panther/logging.py,sha256=DZVf3nxzLodT-hD4820J1jEAffU8zIxXRPKs2lbP8ho,2074
14
- panther/main.py,sha256=VoNVXEO1usr08EfZ6fdISS-DL8j5J1IF8j9DKt5hS1c,9985
15
- panther/monitoring.py,sha256=FlrgkapOs5_iDMPcM32L_SH7U68WOOnyYeeLa1oi6Zg,788
14
+ panther/main.py,sha256=F8yGaVN97JMD0FKWvTPuGZ43g1BGdEEtDzjWSelG4mg,10301
15
+ panther/monitoring.py,sha256=krmcoTUcV12pHwCFVAHywgsp1-US9cyiMlgsrMJdUQ0,1203
16
16
  panther/permissions.py,sha256=Q-l25369yQaP-tY11tFQm-v9PPh8iVImbfUpY3pnQUk,355
17
17
  panther/request.py,sha256=W3L_udvIxgjqVxxc8aaKDIQBVjdc7TTrFyoOJQ8M8FA,1773
18
18
  panther/response.py,sha256=HcL8nuXpZX_vhT9QUw0dnbtP75T_LbvKAWGT48_LPuU,3382
@@ -42,12 +42,12 @@ panther/middlewares/base.py,sha256=kBJp0U6982pfwDRSzDfMRfdUra9oVoL9Ms52EavW0r4,2
42
42
  panther/middlewares/db.py,sha256=Dotc7LPKEZ26w3yK0SY_PhO47JEKJtYpVZPO9SLA2Lg,638
43
43
  panther/middlewares/redis.py,sha256=Wa_uvTe2yGw9LYO1CehrUYYXc6hdyNMOJc7X5lDJ5LU,1374
44
44
  panther/panel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
- panther/panel/apis.py,sha256=9sKeAbVeRJV-FSTgz88waeLcTFWzcP4iYm5tmBgPrJk,1834
45
+ panther/panel/apis.py,sha256=f4eWTi212ZnnPYcxuU4SdJ-7yYQxZFImaXgTTnXw_fc,1703
46
46
  panther/panel/urls.py,sha256=BQkWqSJBPP3VEQYeorKSHIRx-PUl21Y7Z6NFylmhs1I,192
47
47
  panther/panel/utils.py,sha256=0Rv79oR5IEqalqwpRKQHMn1p5duVY5mxMqDKiA5mWx4,437
48
- panther-3.1.4.dist-info/LICENSE,sha256=2aF1hL2aC0zRPjzUkSxJUzZbn2_uLoOkn7DHjzZni-I,1524
49
- panther-3.1.4.dist-info/METADATA,sha256=-ZvktMPOa3he8rECzC_N5-kTkjjkOZGqAnoIq27iyfc,6076
50
- panther-3.1.4.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
51
- panther-3.1.4.dist-info/entry_points.txt,sha256=6GPxYFGuzVfNB4YpHFJvYex6iWah5_tLnirAHwj2Qsg,51
52
- panther-3.1.4.dist-info/top_level.txt,sha256=VbBs02JGXTIoHMzsX-eLOk2MCbBZzQbLhWiYpI7xI2g,8
53
- panther-3.1.4.dist-info/RECORD,,
48
+ panther-3.1.5.dist-info/LICENSE,sha256=2aF1hL2aC0zRPjzUkSxJUzZbn2_uLoOkn7DHjzZni-I,1524
49
+ panther-3.1.5.dist-info/METADATA,sha256=YmW-daTXDu085RPgfX1YbQYKkq6SewmDkw4QR50E-eY,6076
50
+ panther-3.1.5.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
51
+ panther-3.1.5.dist-info/entry_points.txt,sha256=6GPxYFGuzVfNB4YpHFJvYex6iWah5_tLnirAHwj2Qsg,51
52
+ panther-3.1.5.dist-info/top_level.txt,sha256=VbBs02JGXTIoHMzsX-eLOk2MCbBZzQbLhWiYpI7xI2g,8
53
+ panther-3.1.5.dist-info/RECORD,,