panther 4.3.5__tar.gz → 4.3.7__tar.gz
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-4.3.5 → panther-4.3.7}/PKG-INFO +15 -3
- {panther-4.3.5 → panther-4.3.7}/panther/__init__.py +1 -1
- {panther-4.3.5 → panther-4.3.7}/panther/_load_configs.py +12 -8
- {panther-4.3.5 → panther-4.3.7}/panther/_utils.py +1 -1
- {panther-4.3.5 → panther-4.3.7}/panther/cli/monitor_command.py +20 -3
- {panther-4.3.5 → panther-4.3.7}/panther/monitoring.py +3 -13
- {panther-4.3.5 → panther-4.3.7}/panther/response.py +0 -1
- {panther-4.3.5 → panther-4.3.7}/panther.egg-info/PKG-INFO +15 -3
- {panther-4.3.5 → panther-4.3.7}/panther.egg-info/requires.txt +1 -1
- {panther-4.3.5 → panther-4.3.7}/setup.py +1 -1
- {panther-4.3.5 → panther-4.3.7}/tests/test_multipart.py +31 -5
- {panther-4.3.5 → panther-4.3.7}/tests/test_utils.py +1 -1
- {panther-4.3.5 → panther-4.3.7}/LICENSE +0 -0
- {panther-4.3.5 → panther-4.3.7}/README.md +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/app.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/authentications.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/background_tasks.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/base_request.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/base_websocket.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/caching.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/cli/__init__.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/cli/create_command.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/cli/main.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/cli/run_command.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/cli/template.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/cli/utils.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/configs.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/db/__init__.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/db/connections.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/db/cursor.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/db/models.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/db/queries/__init__.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/db/queries/base_queries.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/db/queries/mongodb_queries.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/db/queries/pantherdb_queries.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/db/queries/queries.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/db/utils.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/events.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/exceptions.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/file_handler.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/generics.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/logging.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/main.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/middlewares/__init__.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/middlewares/base.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/pagination.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/panel/__init__.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/panel/apis.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/panel/urls.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/panel/utils.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/permissions.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/request.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/routings.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/serializer.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/status.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/test.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/throttling.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/utils.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther/websocket.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther.egg-info/SOURCES.txt +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther.egg-info/dependency_links.txt +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther.egg-info/entry_points.txt +0 -0
- {panther-4.3.5 → panther-4.3.7}/panther.egg-info/top_level.txt +0 -0
- {panther-4.3.5 → panther-4.3.7}/pyproject.toml +0 -0
- {panther-4.3.5 → panther-4.3.7}/setup.cfg +0 -0
- {panther-4.3.5 → panther-4.3.7}/tests/test_authentication.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/tests/test_background_tasks.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/tests/test_caching.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/tests/test_cli.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/tests/test_database.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/tests/test_events.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/tests/test_generics.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/tests/test_panel_apis.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/tests/test_request.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/tests/test_response.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/tests/test_routing.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/tests/test_run.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/tests/test_serializer.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/tests/test_status.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/tests/test_throttling.py +0 -0
- {panther-4.3.5 → panther-4.3.7}/tests/test_websockets.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: panther
|
3
|
-
Version: 4.3.
|
3
|
+
Version: 4.3.7
|
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
|
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
14
14
|
Requires-Python: >=3.10
|
15
15
|
Description-Content-Type: text/markdown
|
16
16
|
License-File: LICENSE
|
17
|
-
Requires-Dist: pantherdb~=2.1.
|
17
|
+
Requires-Dist: pantherdb~=2.1.1
|
18
18
|
Requires-Dist: pydantic~=2.8.2
|
19
19
|
Requires-Dist: rich~=13.7.1
|
20
20
|
Requires-Dist: uvicorn~=0.27.1
|
@@ -33,6 +33,18 @@ Requires-Dist: watchfiles~=0.21.0; extra == "full"
|
|
33
33
|
Provides-Extra: dev
|
34
34
|
Requires-Dist: ruff~=0.1.9; extra == "dev"
|
35
35
|
Requires-Dist: pytest~=8.3.3; extra == "dev"
|
36
|
+
Dynamic: author
|
37
|
+
Dynamic: author-email
|
38
|
+
Dynamic: classifier
|
39
|
+
Dynamic: description
|
40
|
+
Dynamic: description-content-type
|
41
|
+
Dynamic: home-page
|
42
|
+
Dynamic: license
|
43
|
+
Dynamic: license-file
|
44
|
+
Dynamic: provides-extra
|
45
|
+
Dynamic: requires-dist
|
46
|
+
Dynamic: requires-python
|
47
|
+
Dynamic: summary
|
36
48
|
|
37
49
|
|
38
50
|
[](https://pypi.org/project/panther/) [](https://pypi.org/project/panther/) [](https://codecov.io/github/AliRn76/panther) [](https://pepy.tech/project/panther) [](https://github.com/alirn76/panther/blob/main/LICENSE)
|
@@ -154,22 +154,26 @@ def load_middlewares(_configs: dict, /) -> None:
|
|
154
154
|
# Collect Middlewares
|
155
155
|
for middleware in _configs.get('MIDDLEWARES') or []:
|
156
156
|
if not isinstance(middleware, list | tuple):
|
157
|
-
|
157
|
+
path_or_type = middleware
|
158
|
+
data = {}
|
158
159
|
|
159
|
-
|
160
|
-
|
160
|
+
elif len(middleware) == 1:
|
161
|
+
path_or_type = middleware[0]
|
161
162
|
data = {}
|
162
163
|
|
163
164
|
elif len(middleware) > 2:
|
164
165
|
raise _exception_handler(field='MIDDLEWARES', error=f'{middleware} too many arguments')
|
165
166
|
|
166
167
|
else:
|
167
|
-
|
168
|
+
path_or_type, data = middleware
|
168
169
|
|
169
|
-
|
170
|
-
middleware_class =
|
171
|
-
|
172
|
-
|
170
|
+
if callable(path_or_type):
|
171
|
+
middleware_class = path_or_type
|
172
|
+
else:
|
173
|
+
try:
|
174
|
+
middleware_class = import_class(path_or_type)
|
175
|
+
except (AttributeError, ModuleNotFoundError):
|
176
|
+
raise _exception_handler(field='MIDDLEWARES', error=f'{path_or_type} is not a valid middleware path')
|
173
177
|
|
174
178
|
if issubclass(middleware_class, BaseMiddleware) is False:
|
175
179
|
raise _exception_handler(field='MIDDLEWARES', error='is not a sub class of BaseMiddleware')
|
@@ -49,7 +49,7 @@ def read_multipart_form_data(boundary: str, body: bytes) -> dict:
|
|
49
49
|
if row in (b'', b'--'):
|
50
50
|
continue
|
51
51
|
|
52
|
-
if match := re.match(pattern=field_pattern, string=row):
|
52
|
+
if match := re.match(pattern=field_pattern, string=row, flags=re.DOTALL):
|
53
53
|
_, field_name, _, value = match.groups()
|
54
54
|
data[field_name.decode('utf-8')] = value.decode('utf-8')
|
55
55
|
|
@@ -19,7 +19,7 @@ from panther.configs import config
|
|
19
19
|
with contextlib.suppress(ImportError):
|
20
20
|
from watchfiles import watch
|
21
21
|
|
22
|
-
|
22
|
+
logger = logging.getLogger('panther')
|
23
23
|
|
24
24
|
|
25
25
|
class Monitoring:
|
@@ -30,7 +30,7 @@ class Monitoring:
|
|
30
30
|
def monitor(self) -> None:
|
31
31
|
if error := self.initialize():
|
32
32
|
# Don't continue if initialize() has error
|
33
|
-
|
33
|
+
logger.error(error)
|
34
34
|
return
|
35
35
|
|
36
36
|
with (
|
@@ -51,7 +51,10 @@ class Monitoring:
|
|
51
51
|
|
52
52
|
for _ in watching:
|
53
53
|
for line in f.readlines():
|
54
|
-
|
54
|
+
# line = date_time | method | path | ip:port | response_time(seconds) | status
|
55
|
+
columns = line.split('|')
|
56
|
+
columns[4] = self._clean_response_time(float(columns[4]))
|
57
|
+
self.rows.append(columns)
|
55
58
|
live.update(self.generate_table())
|
56
59
|
|
57
60
|
def initialize(self) -> str:
|
@@ -100,5 +103,19 @@ class Monitoring:
|
|
100
103
|
lines = (os.get_terminal_size()[1] - 6) // 2
|
101
104
|
self.rows = deque(self.rows, maxlen=lines)
|
102
105
|
|
106
|
+
@classmethod
|
107
|
+
def _clean_response_time(cls, response_time: int) -> str:
|
108
|
+
time_unit = ' s'
|
109
|
+
|
110
|
+
if response_time < 0.01:
|
111
|
+
response_time = response_time * 1_000
|
112
|
+
time_unit = 'ms'
|
113
|
+
|
114
|
+
elif response_time >= 10:
|
115
|
+
response_time = response_time / 60
|
116
|
+
time_unit = ' m'
|
117
|
+
|
118
|
+
return f'{round(response_time, 4)} {time_unit}'
|
119
|
+
|
103
120
|
|
104
121
|
monitor = Monitoring().monitor
|
@@ -11,7 +11,7 @@ logger = logging.getLogger('monitoring')
|
|
11
11
|
class Monitoring:
|
12
12
|
"""
|
13
13
|
Create Log Message Like Below:
|
14
|
-
|
14
|
+
date_time | method | path | ip:port | response_time(seconds) | status
|
15
15
|
"""
|
16
16
|
def __init__(self, is_ws: bool = False):
|
17
17
|
self.is_ws = is_ws
|
@@ -30,15 +30,5 @@ class Monitoring:
|
|
30
30
|
|
31
31
|
async def after(self, status: int | Literal['Accepted', 'Rejected', 'Closed'], /):
|
32
32
|
if config.MONITORING:
|
33
|
-
response_time = perf_counter() - self.start_time
|
34
|
-
|
35
|
-
|
36
|
-
if response_time < 0.01:
|
37
|
-
response_time = response_time * 1_000
|
38
|
-
time_unit = 'ms'
|
39
|
-
|
40
|
-
elif response_time >= 10:
|
41
|
-
response_time = response_time / 60
|
42
|
-
time_unit = ' m'
|
43
|
-
|
44
|
-
logger.info(f'{self.log} | {round(response_time, 4)} {time_unit} | {status}')
|
33
|
+
response_time = perf_counter() - self.start_time # Seconds
|
34
|
+
logger.info(f'{self.log} | {response_time} | {status}')
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: panther
|
3
|
-
Version: 4.3.
|
3
|
+
Version: 4.3.7
|
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
|
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
14
14
|
Requires-Python: >=3.10
|
15
15
|
Description-Content-Type: text/markdown
|
16
16
|
License-File: LICENSE
|
17
|
-
Requires-Dist: pantherdb~=2.1.
|
17
|
+
Requires-Dist: pantherdb~=2.1.1
|
18
18
|
Requires-Dist: pydantic~=2.8.2
|
19
19
|
Requires-Dist: rich~=13.7.1
|
20
20
|
Requires-Dist: uvicorn~=0.27.1
|
@@ -33,6 +33,18 @@ Requires-Dist: watchfiles~=0.21.0; extra == "full"
|
|
33
33
|
Provides-Extra: dev
|
34
34
|
Requires-Dist: ruff~=0.1.9; extra == "dev"
|
35
35
|
Requires-Dist: pytest~=8.3.3; extra == "dev"
|
36
|
+
Dynamic: author
|
37
|
+
Dynamic: author-email
|
38
|
+
Dynamic: classifier
|
39
|
+
Dynamic: description
|
40
|
+
Dynamic: description-content-type
|
41
|
+
Dynamic: home-page
|
42
|
+
Dynamic: license
|
43
|
+
Dynamic: license-file
|
44
|
+
Dynamic: provides-extra
|
45
|
+
Dynamic: requires-dist
|
46
|
+
Dynamic: requires-python
|
47
|
+
Dynamic: summary
|
36
48
|
|
37
49
|
|
38
50
|
[](https://pypi.org/project/panther/) [](https://pypi.org/project/panther/) [](https://codecov.io/github/AliRn76/panther) [](https://pepy.tech/project/panther) [](https://github.com/alirn76/panther/blob/main/LICENSE)
|
@@ -53,17 +53,23 @@ async def complex_multipart_api(request: Request):
|
|
53
53
|
}
|
54
54
|
}
|
55
55
|
|
56
|
+
@API()
|
57
|
+
async def multipart_api(request: Request):
|
58
|
+
return request.data
|
59
|
+
|
56
60
|
|
57
61
|
urls = {
|
58
62
|
'flat_multipart': flat_multipart_api,
|
59
63
|
'single_file_multipart': single_file_multipart_api,
|
60
64
|
'several_file_multipart': several_file_multipart_api,
|
61
65
|
'complex_multipart': complex_multipart_api,
|
66
|
+
'multiline_multipart': multipart_api,
|
62
67
|
}
|
63
68
|
|
64
69
|
|
65
70
|
class TestMultipart(IsolatedAsyncioTestCase):
|
66
|
-
|
71
|
+
CONTENT_TYPE_1 = 'multipart/form-data; boundary=--------------------------201301649688174364392792'
|
72
|
+
CONTENT_TYPE_2 = 'multipart/form-data; boundary=----geckoformboundaryc30219e1237602175b34337f41ace019'
|
67
73
|
FLAT_PAYLOAD = (
|
68
74
|
b'----------------------------201301649688174364392792\r\n'
|
69
75
|
b'Content-Disposition: form-data; name="name"\r\n\r\n'
|
@@ -108,6 +114,18 @@ class TestMultipart(IsolatedAsyncioTestCase):
|
|
108
114
|
b'25\r\n'
|
109
115
|
b'----------------------------201301649688174364392792--\r\n'
|
110
116
|
)
|
117
|
+
MULTI_LINE_PAYLOAD = (
|
118
|
+
b'------geckoformboundaryc30219e1237602175b34337f41ace019\r\n'
|
119
|
+
b'Content-Disposition: form-data; name="team"\r\n\r\n'
|
120
|
+
b'SRE\r\n'
|
121
|
+
b'------geckoformboundaryc30219e1237602175b34337f41ace019\r\n'
|
122
|
+
b'Content-Disposition: form-data; name="phone"\r\n\r\n'
|
123
|
+
b'09033333333\r\n'
|
124
|
+
b'------geckoformboundaryc30219e1237602175b34337f41ace019\r\n'
|
125
|
+
b'Content-Disposition: form-data; name="message"\r\n\r\n'
|
126
|
+
b'My\r\nName\r\nIs\r\nAli\r\n\r\n'
|
127
|
+
b'------geckoformboundaryc30219e1237602175b34337f41ace019--\r\n'
|
128
|
+
)
|
111
129
|
|
112
130
|
@classmethod
|
113
131
|
def setUpClass(cls) -> None:
|
@@ -117,7 +135,7 @@ class TestMultipart(IsolatedAsyncioTestCase):
|
|
117
135
|
async def test_flat_multipart(self):
|
118
136
|
res = await self.client.post(
|
119
137
|
'flat_multipart',
|
120
|
-
content_type=self.
|
138
|
+
content_type=self.CONTENT_TYPE_1,
|
121
139
|
payload=self.FLAT_PAYLOAD,
|
122
140
|
)
|
123
141
|
assert res.status_code == 200
|
@@ -126,7 +144,7 @@ class TestMultipart(IsolatedAsyncioTestCase):
|
|
126
144
|
async def test_single_file_multipart(self):
|
127
145
|
res = await self.client.post(
|
128
146
|
'single_file_multipart',
|
129
|
-
content_type=self.
|
147
|
+
content_type=self.CONTENT_TYPE_1,
|
130
148
|
payload=self.SINGLE_FILE_PAYLOAD,
|
131
149
|
)
|
132
150
|
|
@@ -140,7 +158,7 @@ class TestMultipart(IsolatedAsyncioTestCase):
|
|
140
158
|
async def test_several_file_multipart(self):
|
141
159
|
res = await self.client.post(
|
142
160
|
'several_file_multipart',
|
143
|
-
content_type=self.
|
161
|
+
content_type=self.CONTENT_TYPE_1,
|
144
162
|
payload=self.SEVERAL_FILE_PAYLOAD,
|
145
163
|
)
|
146
164
|
|
@@ -161,7 +179,7 @@ class TestMultipart(IsolatedAsyncioTestCase):
|
|
161
179
|
async def test_complex_multipart(self):
|
162
180
|
res = await self.client.post(
|
163
181
|
'complex_multipart',
|
164
|
-
content_type=self.
|
182
|
+
content_type=self.CONTENT_TYPE_1,
|
165
183
|
payload=self.COMPLEX_PAYLOAD,
|
166
184
|
)
|
167
185
|
|
@@ -180,3 +198,11 @@ class TestMultipart(IsolatedAsyncioTestCase):
|
|
180
198
|
'file': 'Hello World2\n',
|
181
199
|
}
|
182
200
|
}
|
201
|
+
|
202
|
+
async def test_multiline_multipart(self):
|
203
|
+
res = await self.client.post(
|
204
|
+
'multiline_multipart',
|
205
|
+
content_type=self.CONTENT_TYPE_2,
|
206
|
+
payload=self.MULTI_LINE_PAYLOAD,
|
207
|
+
)
|
208
|
+
assert res.data == {'team': 'SRE', 'phone': '09033333333', 'message': 'My\r\nName\r\nIs\r\nAli\r\n'}
|
@@ -295,7 +295,7 @@ class TestLoadConfigs(TestCase):
|
|
295
295
|
MIDDLEWARES = []
|
296
296
|
|
297
297
|
assert len(captured.records) == 1
|
298
|
-
assert captured.records[0].getMessage() == "Invalid 'MIDDLEWARES': fake.module
|
298
|
+
assert captured.records[0].getMessage() == "Invalid 'MIDDLEWARES': fake.module is not a valid middleware path"
|
299
299
|
|
300
300
|
def test_middlewares_too_many_args(self):
|
301
301
|
global MIDDLEWARES
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|