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.
Files changed (81) hide show
  1. {panther-4.3.5 → panther-4.3.7}/PKG-INFO +15 -3
  2. {panther-4.3.5 → panther-4.3.7}/panther/__init__.py +1 -1
  3. {panther-4.3.5 → panther-4.3.7}/panther/_load_configs.py +12 -8
  4. {panther-4.3.5 → panther-4.3.7}/panther/_utils.py +1 -1
  5. {panther-4.3.5 → panther-4.3.7}/panther/cli/monitor_command.py +20 -3
  6. {panther-4.3.5 → panther-4.3.7}/panther/monitoring.py +3 -13
  7. {panther-4.3.5 → panther-4.3.7}/panther/response.py +0 -1
  8. {panther-4.3.5 → panther-4.3.7}/panther.egg-info/PKG-INFO +15 -3
  9. {panther-4.3.5 → panther-4.3.7}/panther.egg-info/requires.txt +1 -1
  10. {panther-4.3.5 → panther-4.3.7}/setup.py +1 -1
  11. {panther-4.3.5 → panther-4.3.7}/tests/test_multipart.py +31 -5
  12. {panther-4.3.5 → panther-4.3.7}/tests/test_utils.py +1 -1
  13. {panther-4.3.5 → panther-4.3.7}/LICENSE +0 -0
  14. {panther-4.3.5 → panther-4.3.7}/README.md +0 -0
  15. {panther-4.3.5 → panther-4.3.7}/panther/app.py +0 -0
  16. {panther-4.3.5 → panther-4.3.7}/panther/authentications.py +0 -0
  17. {panther-4.3.5 → panther-4.3.7}/panther/background_tasks.py +0 -0
  18. {panther-4.3.5 → panther-4.3.7}/panther/base_request.py +0 -0
  19. {panther-4.3.5 → panther-4.3.7}/panther/base_websocket.py +0 -0
  20. {panther-4.3.5 → panther-4.3.7}/panther/caching.py +0 -0
  21. {panther-4.3.5 → panther-4.3.7}/panther/cli/__init__.py +0 -0
  22. {panther-4.3.5 → panther-4.3.7}/panther/cli/create_command.py +0 -0
  23. {panther-4.3.5 → panther-4.3.7}/panther/cli/main.py +0 -0
  24. {panther-4.3.5 → panther-4.3.7}/panther/cli/run_command.py +0 -0
  25. {panther-4.3.5 → panther-4.3.7}/panther/cli/template.py +0 -0
  26. {panther-4.3.5 → panther-4.3.7}/panther/cli/utils.py +0 -0
  27. {panther-4.3.5 → panther-4.3.7}/panther/configs.py +0 -0
  28. {panther-4.3.5 → panther-4.3.7}/panther/db/__init__.py +0 -0
  29. {panther-4.3.5 → panther-4.3.7}/panther/db/connections.py +0 -0
  30. {panther-4.3.5 → panther-4.3.7}/panther/db/cursor.py +0 -0
  31. {panther-4.3.5 → panther-4.3.7}/panther/db/models.py +0 -0
  32. {panther-4.3.5 → panther-4.3.7}/panther/db/queries/__init__.py +0 -0
  33. {panther-4.3.5 → panther-4.3.7}/panther/db/queries/base_queries.py +0 -0
  34. {panther-4.3.5 → panther-4.3.7}/panther/db/queries/mongodb_queries.py +0 -0
  35. {panther-4.3.5 → panther-4.3.7}/panther/db/queries/pantherdb_queries.py +0 -0
  36. {panther-4.3.5 → panther-4.3.7}/panther/db/queries/queries.py +0 -0
  37. {panther-4.3.5 → panther-4.3.7}/panther/db/utils.py +0 -0
  38. {panther-4.3.5 → panther-4.3.7}/panther/events.py +0 -0
  39. {panther-4.3.5 → panther-4.3.7}/panther/exceptions.py +0 -0
  40. {panther-4.3.5 → panther-4.3.7}/panther/file_handler.py +0 -0
  41. {panther-4.3.5 → panther-4.3.7}/panther/generics.py +0 -0
  42. {panther-4.3.5 → panther-4.3.7}/panther/logging.py +0 -0
  43. {panther-4.3.5 → panther-4.3.7}/panther/main.py +0 -0
  44. {panther-4.3.5 → panther-4.3.7}/panther/middlewares/__init__.py +0 -0
  45. {panther-4.3.5 → panther-4.3.7}/panther/middlewares/base.py +0 -0
  46. {panther-4.3.5 → panther-4.3.7}/panther/pagination.py +0 -0
  47. {panther-4.3.5 → panther-4.3.7}/panther/panel/__init__.py +0 -0
  48. {panther-4.3.5 → panther-4.3.7}/panther/panel/apis.py +0 -0
  49. {panther-4.3.5 → panther-4.3.7}/panther/panel/urls.py +0 -0
  50. {panther-4.3.5 → panther-4.3.7}/panther/panel/utils.py +0 -0
  51. {panther-4.3.5 → panther-4.3.7}/panther/permissions.py +0 -0
  52. {panther-4.3.5 → panther-4.3.7}/panther/request.py +0 -0
  53. {panther-4.3.5 → panther-4.3.7}/panther/routings.py +0 -0
  54. {panther-4.3.5 → panther-4.3.7}/panther/serializer.py +0 -0
  55. {panther-4.3.5 → panther-4.3.7}/panther/status.py +0 -0
  56. {panther-4.3.5 → panther-4.3.7}/panther/test.py +0 -0
  57. {panther-4.3.5 → panther-4.3.7}/panther/throttling.py +0 -0
  58. {panther-4.3.5 → panther-4.3.7}/panther/utils.py +0 -0
  59. {panther-4.3.5 → panther-4.3.7}/panther/websocket.py +0 -0
  60. {panther-4.3.5 → panther-4.3.7}/panther.egg-info/SOURCES.txt +0 -0
  61. {panther-4.3.5 → panther-4.3.7}/panther.egg-info/dependency_links.txt +0 -0
  62. {panther-4.3.5 → panther-4.3.7}/panther.egg-info/entry_points.txt +0 -0
  63. {panther-4.3.5 → panther-4.3.7}/panther.egg-info/top_level.txt +0 -0
  64. {panther-4.3.5 → panther-4.3.7}/pyproject.toml +0 -0
  65. {panther-4.3.5 → panther-4.3.7}/setup.cfg +0 -0
  66. {panther-4.3.5 → panther-4.3.7}/tests/test_authentication.py +0 -0
  67. {panther-4.3.5 → panther-4.3.7}/tests/test_background_tasks.py +0 -0
  68. {panther-4.3.5 → panther-4.3.7}/tests/test_caching.py +0 -0
  69. {panther-4.3.5 → panther-4.3.7}/tests/test_cli.py +0 -0
  70. {panther-4.3.5 → panther-4.3.7}/tests/test_database.py +0 -0
  71. {panther-4.3.5 → panther-4.3.7}/tests/test_events.py +0 -0
  72. {panther-4.3.5 → panther-4.3.7}/tests/test_generics.py +0 -0
  73. {panther-4.3.5 → panther-4.3.7}/tests/test_panel_apis.py +0 -0
  74. {panther-4.3.5 → panther-4.3.7}/tests/test_request.py +0 -0
  75. {panther-4.3.5 → panther-4.3.7}/tests/test_response.py +0 -0
  76. {panther-4.3.5 → panther-4.3.7}/tests/test_routing.py +0 -0
  77. {panther-4.3.5 → panther-4.3.7}/tests/test_run.py +0 -0
  78. {panther-4.3.5 → panther-4.3.7}/tests/test_serializer.py +0 -0
  79. {panther-4.3.5 → panther-4.3.7}/tests/test_status.py +0 -0
  80. {panther-4.3.5 → panther-4.3.7}/tests/test_throttling.py +0 -0
  81. {panther-4.3.5 → panther-4.3.7}/tests/test_websockets.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: panther
3
- Version: 4.3.5
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.0
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
  [![PyPI](https://img.shields.io/pypi/v/panther?label=PyPI)](https://pypi.org/project/panther/) [![PyVersion](https://img.shields.io/pypi/pyversions/panther.svg)](https://pypi.org/project/panther/) [![codecov](https://codecov.io/github/AliRn76/panther/graph/badge.svg?token=YWFQA43GSP)](https://codecov.io/github/AliRn76/panther) [![Downloads](https://static.pepy.tech/badge/panther/month)](https://pepy.tech/project/panther) [![license](https://img.shields.io/github/license/alirn76/panther.svg)](https://github.com/alirn76/panther/blob/main/LICENSE)
@@ -1,6 +1,6 @@
1
1
  from panther.main import Panther # noqa: F401
2
2
 
3
- __version__ = '4.3.5'
3
+ __version__ = '4.3.7'
4
4
 
5
5
 
6
6
  def version():
@@ -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
- raise _exception_handler(field='MIDDLEWARES', error=f'{middleware} should have 2 part: (path, kwargs)')
157
+ path_or_type = middleware
158
+ data = {}
158
159
 
159
- if len(middleware) == 1:
160
- path = middleware[0]
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
- path, data = middleware
168
+ path_or_type, data = middleware
168
169
 
169
- try:
170
- middleware_class = import_class(path)
171
- except (AttributeError, ModuleNotFoundError):
172
- raise _exception_handler(field='MIDDLEWARES', error=f'{path} is not a valid middleware path')
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
- loggerr = logging.getLogger('panther')
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
- loggerr.error(error)
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
- self.rows.append(line.split('|'))
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
- date time | method | path | ip:port | response_time [ms, s] | status
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
- time_unit = ' s'
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}')
@@ -13,7 +13,6 @@ else:
13
13
 
14
14
  import orjson as json
15
15
  from pydantic import BaseModel
16
- from jinja2 import Environment, FileSystemLoader
17
16
 
18
17
  from panther import status
19
18
  from panther.configs import config
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: panther
3
- Version: 4.3.5
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.0
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
  [![PyPI](https://img.shields.io/pypi/v/panther?label=PyPI)](https://pypi.org/project/panther/) [![PyVersion](https://img.shields.io/pypi/pyversions/panther.svg)](https://pypi.org/project/panther/) [![codecov](https://codecov.io/github/AliRn76/panther/graph/badge.svg?token=YWFQA43GSP)](https://codecov.io/github/AliRn76/panther) [![Downloads](https://static.pepy.tech/badge/panther/month)](https://pepy.tech/project/panther) [![license](https://img.shields.io/github/license/alirn76/panther.svg)](https://github.com/alirn76/panther/blob/main/LICENSE)
@@ -1,4 +1,4 @@
1
- pantherdb~=2.1.0
1
+ pantherdb~=2.1.1
2
2
  pydantic~=2.8.2
3
3
  rich~=13.7.1
4
4
  uvicorn~=0.27.1
@@ -14,7 +14,7 @@ with open('README.md', encoding='utf-8') as file:
14
14
  DESCRIPTION = file.read()
15
15
 
16
16
  INSTALL_REQUIRES = [
17
- 'pantherdb~=2.1.0',
17
+ 'pantherdb~=2.1.1',
18
18
  'pydantic~=2.8.2',
19
19
  'rich~=13.7.1',
20
20
  'uvicorn~=0.27.1',
@@ -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
- CONTENT_TYPE = 'multipart/form-data; boundary=--------------------------201301649688174364392792'
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.CONTENT_TYPE,
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.CONTENT_TYPE,
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.CONTENT_TYPE,
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.CONTENT_TYPE,
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 should have 2 part: (path, kwargs)"
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