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/test.py CHANGED
@@ -4,7 +4,7 @@ from typing import Literal
4
4
 
5
5
  import orjson as json
6
6
 
7
- from panther.response import Response, HTMLResponse, PlainTextResponse, StreamingResponse
7
+ from panther.response import HTMLResponse, PlainTextResponse, Response
8
8
 
9
9
  __all__ = ('APIClient', 'WebsocketClient')
10
10
 
@@ -28,12 +28,12 @@ class RequestClient:
28
28
  }
29
29
 
30
30
  async def request(
31
- self,
32
- path: str,
33
- method: Literal['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
34
- payload: bytes | dict | None,
35
- headers: dict,
36
- query_params: dict,
31
+ self,
32
+ path: str,
33
+ method: Literal['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
34
+ payload: bytes | dict | None,
35
+ headers: dict,
36
+ query_params: dict,
37
37
  ) -> Response:
38
38
  headers = [(k.encode(), str(v).encode()) for k, v in headers.items()]
39
39
  if not path.startswith('/'):
@@ -56,21 +56,24 @@ class RequestClient:
56
56
  send=self.send,
57
57
  )
58
58
  response_headers = {key.decode(): value.decode() for key, value in self.header['headers']}
59
+ cookies = [(key, value) for key, value in self.header['headers'] if key.decode() == 'Set-Cookie']
59
60
  if response_headers['Content-Type'] == 'text/html; charset=utf-8':
60
61
  data = self.response.decode()
61
- return HTMLResponse(data=data, status_code=self.header['status'], headers=response_headers)
62
+ response = HTMLResponse(data=data, status_code=self.header['status'], headers=response_headers)
62
63
 
63
64
  elif response_headers['Content-Type'] == 'text/plain; charset=utf-8':
64
65
  data = self.response.decode()
65
- return PlainTextResponse(data=data, status_code=self.header['status'], headers=response_headers)
66
+ response = PlainTextResponse(data=data, status_code=self.header['status'], headers=response_headers)
66
67
 
67
68
  elif response_headers['Content-Type'] == 'application/octet-stream':
68
69
  data = self.response.decode()
69
- return PlainTextResponse(data=data, status_code=self.header['status'], headers=response_headers)
70
+ response = PlainTextResponse(data=data, status_code=self.header['status'], headers=response_headers)
70
71
 
71
72
  else:
72
73
  data = json.loads(self.response or b'null')
73
- return Response(data=data, status_code=self.header['status'], headers=response_headers)
74
+ response = Response(data=data, status_code=self.header['status'], headers=response_headers)
75
+ response.cookies = cookies
76
+ return response
74
77
 
75
78
 
76
79
  class APIClient:
@@ -78,27 +81,41 @@ class APIClient:
78
81
  self._app = app
79
82
 
80
83
  async def _send_request(
81
- self,
82
- path: str,
83
- method: Literal['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
84
- payload: dict | None,
85
- headers: dict,
86
- query_params: dict,
84
+ self,
85
+ path: str,
86
+ method: Literal['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
87
+ payload: dict | None,
88
+ headers: dict,
89
+ query_params: dict,
87
90
  ) -> Response:
88
91
  request_client = RequestClient(app=self._app)
89
92
  return await request_client.request(
90
- path=path,
91
- method=method,
92
- payload=payload,
93
- headers=headers,
94
- query_params=query_params or {},
95
- )
93
+ path=path,
94
+ method=method,
95
+ payload=payload,
96
+ headers=headers,
97
+ query_params=query_params or {},
98
+ )
99
+
100
+ async def options(
101
+ self,
102
+ path: str,
103
+ headers: dict | None = None,
104
+ query_params: dict | None = None,
105
+ ) -> Response:
106
+ return await self._send_request(
107
+ path=path,
108
+ method='OPTIONS',
109
+ payload=None,
110
+ headers=headers or {},
111
+ query_params=query_params or {},
112
+ )
96
113
 
97
114
  async def get(
98
- self,
99
- path: str,
100
- headers: dict | None = None,
101
- query_params: dict | None = None,
115
+ self,
116
+ path: str,
117
+ headers: dict | None = None,
118
+ query_params: dict | None = None,
102
119
  ) -> Response:
103
120
  return await self._send_request(
104
121
  path=path,
@@ -109,12 +126,12 @@ class APIClient:
109
126
  )
110
127
 
111
128
  async def post(
112
- self,
113
- path: str,
114
- payload: bytes | dict | None = None,
115
- headers: dict | None = None,
116
- query_params: dict | None = None,
117
- content_type: str = 'application/json',
129
+ self,
130
+ path: str,
131
+ payload: bytes | dict | None = None,
132
+ headers: dict | None = None,
133
+ query_params: dict | None = None,
134
+ content_type: str = 'application/json',
118
135
  ) -> Response:
119
136
  headers = {'content-type': content_type} | (headers or {})
120
137
  return await self._send_request(
@@ -126,12 +143,12 @@ class APIClient:
126
143
  )
127
144
 
128
145
  async def put(
129
- self,
130
- path: str,
131
- payload: bytes | dict | None = None,
132
- headers: dict | None = None,
133
- query_params: dict | None = None,
134
- content_type: Literal['application/json', 'multipart/form-data'] = 'application/json',
146
+ self,
147
+ path: str,
148
+ payload: bytes | dict | None = None,
149
+ headers: dict | None = None,
150
+ query_params: dict | None = None,
151
+ content_type: Literal['application/json', 'multipart/form-data'] = 'application/json',
135
152
  ) -> Response:
136
153
  headers = {'content-type': content_type} | (headers or {})
137
154
  return await self._send_request(
@@ -143,12 +160,12 @@ class APIClient:
143
160
  )
144
161
 
145
162
  async def patch(
146
- self,
147
- path: str,
148
- payload: bytes | dict | None = None,
149
- headers: dict | None = None,
150
- query_params: dict | None = None,
151
- content_type: Literal['application/json', 'multipart/form-data'] = 'application/json',
163
+ self,
164
+ path: str,
165
+ payload: bytes | dict | None = None,
166
+ headers: dict | None = None,
167
+ query_params: dict | None = None,
168
+ content_type: Literal['application/json', 'multipart/form-data'] = 'application/json',
152
169
  ) -> Response:
153
170
  headers = {'content-type': content_type} | (headers or {})
154
171
  return await self._send_request(
@@ -160,10 +177,10 @@ class APIClient:
160
177
  )
161
178
 
162
179
  async def delete(
163
- self,
164
- path: str,
165
- headers: dict | None = None,
166
- query_params: dict | None = None,
180
+ self,
181
+ path: str,
182
+ headers: dict | None = None,
183
+ query_params: dict | None = None,
167
184
  ) -> Response:
168
185
  return await self._send_request(
169
186
  path=path,
@@ -183,15 +200,13 @@ class WebsocketClient:
183
200
  self.responses.append(data)
184
201
 
185
202
  async def receive(self):
186
- return {
187
- 'type': 'websocket.connect'
188
- }
203
+ return {'type': 'websocket.connect'}
189
204
 
190
205
  def connect(
191
- self,
192
- path: str,
193
- headers: dict | None = None,
194
- query_params: dict | None = None,
206
+ self,
207
+ path: str,
208
+ headers: dict | None = None,
209
+ query_params: dict | None = None,
195
210
  ):
196
211
  headers = [(k.encode(), str(v).encode()) for k, v in (headers or {}).items()]
197
212
  if not path.startswith('/'):
@@ -210,13 +225,13 @@ class WebsocketClient:
210
225
  'query_string': query_params.encode(),
211
226
  'headers': headers,
212
227
  'subprotocols': [],
213
- 'state': {}
228
+ 'state': {},
214
229
  }
215
230
  asyncio.run(
216
231
  self.app(
217
232
  scope=scope,
218
233
  receive=self.receive,
219
234
  send=self.send,
220
- )
235
+ ),
221
236
  )
222
237
  return self.responses
panther/throttling.py CHANGED
@@ -1,11 +1,76 @@
1
1
  from collections import defaultdict
2
2
  from dataclasses import dataclass
3
- from datetime import timedelta
3
+ from datetime import datetime, timedelta
4
4
 
5
- throttling_storage = defaultdict(int)
5
+ from panther.db.connections import redis
6
+ from panther.exceptions import ThrottlingAPIError
7
+ from panther.request import Request
8
+ from panther.utils import round_datetime
9
+
10
+ # In-memory fallback storage for when Redis is unavailable
11
+ _fallback_throttle_storage = defaultdict(int)
6
12
 
7
13
 
8
14
  @dataclass(repr=False, eq=False)
9
- class Throttling:
15
+ class Throttle:
10
16
  rate: int
11
17
  duration: timedelta
18
+
19
+ @property
20
+ def time_window(self) -> datetime:
21
+ return round_datetime(datetime.now(), self.duration)
22
+
23
+ def build_cache_key(self, request: Request) -> str:
24
+ """
25
+ Generate a unique cache key based on time window, user or IP, and path.
26
+ This method is intended to be overridden by subclasses to customize throttling logic.
27
+ """
28
+ identifier = request.user.id if request.user else request.client.ip
29
+ return f'{self.time_window}-{identifier}-{request.path}'
30
+
31
+ async def get_request_count(self, request: Request) -> int:
32
+ """
33
+ Get the current request count for this key from Redis or fallback memory.
34
+ """
35
+ key = self.build_cache_key(request)
36
+
37
+ if redis.is_connected:
38
+ value = await redis.get(key)
39
+ return int(value) if value else 0
40
+
41
+ return _fallback_throttle_storage.get(key, 0)
42
+
43
+ async def increment_request_count(self, request: Request) -> None:
44
+ """
45
+ Increment the request count for this key and ensure TTL is set in Redis.
46
+ """
47
+ key = self.build_cache_key(request)
48
+
49
+ if redis.is_connected:
50
+ count = await redis.incrby(key, amount=1)
51
+ if count == 1:
52
+ ttl = int(self.duration.total_seconds())
53
+ await redis.expire(key, ttl)
54
+ else:
55
+ _fallback_throttle_storage[key] += 1
56
+
57
+ async def check_and_increment(self, request: Request) -> None:
58
+ """
59
+ Main throttling logic:
60
+ - Raises ThrottlingAPIError if limit exceeded.
61
+ - Otherwise increments the request count.
62
+ """
63
+ count = await self.get_request_count(request)
64
+ remaining = self.rate - count - 1
65
+ reset_time = self.time_window + self.duration
66
+ retry_after = int((reset_time - datetime.now()).total_seconds())
67
+
68
+ if remaining < 0:
69
+ raise ThrottlingAPIError(
70
+ headers={
71
+ 'Retry-After': str(retry_after),
72
+ 'X-RateLimit-Reset': str(int(reset_time.timestamp())),
73
+ },
74
+ )
75
+
76
+ await self.increment_request_count(request)
panther/utils.py CHANGED
@@ -33,7 +33,7 @@ def load_env(env_file: str | Path, /) -> dict[str, str]:
33
33
  raise ValueError(f'"{env_file}" is not a file.') from None
34
34
 
35
35
  with open(env_file) as file:
36
- for line in file.readlines():
36
+ for line in file:
37
37
  striped_line = line.strip()
38
38
  if not striped_line.startswith('#') and '=' in striped_line:
39
39
  key, value = striped_line.split('=', 1)
@@ -53,7 +53,7 @@ def generate_secret_key() -> str:
53
53
  return base64.urlsafe_b64encode(os.urandom(32)).decode()
54
54
 
55
55
 
56
- def round_datetime(dt: datetime, delta: timedelta):
56
+ def round_datetime(dt: datetime, delta: timedelta) -> datetime:
57
57
  """
58
58
  Example:
59
59
  >>> round_datetime(datetime(2024, 7, 15, 13, 22, 11, 562159), timedelta(days=2))
@@ -67,6 +67,7 @@ def round_datetime(dt: datetime, delta: timedelta):
67
67
 
68
68
  >>> round_datetime(datetime(2024, 7, 18, 13, 22, 11, 562159), timedelta(days=2))
69
69
  datetime.datetime(2024, 7, 18, 0, 0)
70
+
70
71
  """
71
72
  return datetime.min + round((dt - datetime.min) / delta) * delta
72
73
 
@@ -87,19 +88,12 @@ def scrypt(password: str, salt: bytes, digest: bool = False) -> str | bytes:
87
88
  h_len: The length in octets of the hash function (32 for SHA256).
88
89
  mf_len: The length in octets of the output of the mixing function (SMix below). Defined as r * 128 in RFC7914.
89
90
  """
90
- n = 2 ** 14 # 16384
91
+ n = 2**14 # 16384
91
92
  r = 8
92
93
  p = 10
93
94
  dk_len = 64
94
95
 
95
- derived_key = hashlib.scrypt(
96
- password=password.encode(),
97
- salt=salt,
98
- n=n,
99
- r=r,
100
- p=p,
101
- dklen=dk_len
102
- )
96
+ derived_key = hashlib.scrypt(password=password.encode(), salt=salt, n=n, r=r, p=p, dklen=dk_len)
103
97
  if digest:
104
98
  return hashlib.md5(derived_key).hexdigest()
105
99
  return derived_key
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.4
2
+ Name: panther
3
+ Version: 5.0.0b5
4
+ Summary: Fast & Friendly, Web Framework For Building Async APIs
5
+ Home-page: https://github.com/alirn76/panther
6
+ Author: Ali RajabNezhad
7
+ Author-email: alirn76@yahoo.com
8
+ License: BSD-3-Clause license
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: pantherdb~=2.3.0
18
+ Requires-Dist: orjson~=3.9.15
19
+ Requires-Dist: pydantic~=2.10.6
20
+ Requires-Dist: rich~=13.9.4
21
+ Requires-Dist: uvicorn~=0.34.0
22
+ Requires-Dist: pytz~=2025.2
23
+ Requires-Dist: Jinja2~=3.1
24
+ Requires-Dist: simple-ulid~=1.0.0
25
+ Requires-Dist: httptools~=0.6.4
26
+ Provides-Extra: full
27
+ Requires-Dist: redis==5.2.1; extra == "full"
28
+ Requires-Dist: motor~=3.7.0; extra == "full"
29
+ Requires-Dist: ipython~=9.0.2; extra == "full"
30
+ Requires-Dist: python-jose~=3.4.0; extra == "full"
31
+ Requires-Dist: ruff~=0.11.2; extra == "full"
32
+ Requires-Dist: websockets~=15.0.1; extra == "full"
33
+ Requires-Dist: cryptography~=44.0.2; extra == "full"
34
+ Requires-Dist: watchfiles~=1.0.4; extra == "full"
35
+ Provides-Extra: dev
36
+ Requires-Dist: ruff~=0.11.2; extra == "dev"
37
+ Requires-Dist: pytest~=8.3.5; extra == "dev"
38
+ Dynamic: author
39
+ Dynamic: author-email
40
+ Dynamic: classifier
41
+ Dynamic: description
42
+ Dynamic: description-content-type
43
+ Dynamic: home-page
44
+ Dynamic: license
45
+ Dynamic: license-file
46
+ Dynamic: provides-extra
47
+ Dynamic: requires-dist
48
+ Dynamic: requires-python
49
+ Dynamic: summary
50
+
51
+ [![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)
52
+
53
+ <div align="center">
54
+ <img src="https://github.com/AliRn76/panther/raw/master/docs/docs/images/logo-vertical.png" alt="Panther Logo" width="450">
55
+
56
+ # Panther
57
+
58
+ **A Fast & Friendly Web Framework for Building Async APIs with Python 3.10+**
59
+
60
+ [📚 Documentation](https://pantherpy.github.io)
61
+ </div>
62
+
63
+ ---
64
+
65
+ ## 🐾 Why Choose Panther?
66
+
67
+ Panther is designed to be **fast**, **simple**, and **powerful**. Here's what makes it special:
68
+
69
+ - **One of the fastest Python frameworks** available ([Benchmark](https://www.techempower.com/benchmarks/#section=data-r23&l=zijzen-pa7&c=4))
70
+ - **File-based database** ([PantherDB](https://pypi.org/project/pantherdb/)) - No external database setup required
71
+ - **Document-oriented ODM** - Supports MongoDB & PantherDB with familiar syntax
72
+ - **API caching system** - In-memory and Redis support
73
+ - **OpenAPI/Swagger** - Auto-generated API documentation
74
+ - **WebSocket support** - Real-time communication out of the box
75
+ - **Authentication & Permissions** - Built-in security features
76
+ - **Background tasks** - Handle long-running operations
77
+ - **Middleware & Throttling** - Extensible and configurable
78
+
79
+ ---
80
+
81
+ ## Quick Start
82
+
83
+ ### Installation
84
+
85
+ ```bash
86
+ pip install panther
87
+ ```
88
+
89
+ - Create a `main.py` file with one of the examples below.
90
+
91
+ ### Your First API
92
+
93
+ Here's a simple REST API endpoint that returns a "Hello World" message:
94
+
95
+ ```python
96
+ from datetime import datetime, timedelta
97
+ from panther import status, Panther
98
+ from panther.app import GenericAPI
99
+ from panther.openapi.urls import url_routing as openapi_url_routing
100
+ from panther.response import Response
101
+
102
+ class HelloAPI(GenericAPI):
103
+ # Cache responses for 10 seconds
104
+ cache = timedelta(seconds=10)
105
+
106
+ def get(self):
107
+ current_time = datetime.now().isoformat()
108
+ return Response(
109
+ data={'message': f'Hello from Panther! 🐾 | {current_time}'},
110
+ status_code=status.HTTP_200_OK
111
+ )
112
+
113
+ # URL routing configuration
114
+ url_routing = {
115
+ '/': HelloAPI,
116
+ 'swagger/': openapi_url_routing, # Auto-generated API docs
117
+ }
118
+
119
+ # Create your Panther app
120
+ app = Panther(__name__, configs=__name__, urls=url_routing)
121
+ ```
122
+
123
+ ### WebSocket Echo Server
124
+
125
+ Here's a simple WebSocket echo server that sends back any message it receives:
126
+
127
+ ```python
128
+ from panther import Panther
129
+ from panther.app import GenericAPI
130
+ from panther.response import HTMLResponse
131
+ from panther.websocket import GenericWebsocket
132
+
133
+ class EchoWebsocket(GenericWebsocket):
134
+ async def connect(self, **kwargs):
135
+ await self.accept()
136
+ await self.send("Connected to Panther WebSocket!")
137
+
138
+ async def receive(self, data: str | bytes):
139
+ # Echo back the received message
140
+ await self.send(f"Echo: {data}")
141
+
142
+ class WebSocketPage(GenericAPI):
143
+ def get(self):
144
+ template = """
145
+ <h2>🐾 Panther WebSocket Echo Server</h2>
146
+ <input id="msg"><button onclick="s.send(msg.value)">Send</button>
147
+ <ul id="log"></ul>
148
+ <script>
149
+ const s = new WebSocket('ws://127.0.0.1:8000/ws');
150
+ s.onmessage = e => log.innerHTML += `<li><- ${msg.value}</li><li>-> ${e.data}</li>`;
151
+ </script>
152
+ """
153
+ return HTMLResponse(template)
154
+
155
+ url_routing = {
156
+ '': WebSocketPage,
157
+ 'ws': EchoWebsocket,
158
+ }
159
+ app = Panther(__name__, configs=__name__, urls=url_routing)
160
+ ```
161
+
162
+ ### Run Your Application
163
+
164
+ 1. **Start the development server**
165
+ ```shell
166
+ $ panther run main:app
167
+ ```
168
+
169
+ 2. **Test your application**
170
+ - For the _API_ example: Visit [http://127.0.0.1:8000/](http://127.0.0.1:8000/) to see the "Hello World" response
171
+ - For the _WebSocket_ example: Visit [http://127.0.0.1:8000/](http://127.0.0.1:8000/) and send a message.
172
+
173
+ ---
174
+
175
+ ## 🙏 Acknowledgments
176
+
177
+ <div align="center">
178
+ <p>Supported by</p>
179
+ <a href="https://drive.google.com/file/d/17xe1hicIiRF7SQ-clg9SETdc19SktCbV/view?usp=sharing">
180
+ <img alt="JetBrains" src="https://github.com/AliRn76/panther/raw/master/docs/docs/images/jb_beam_50x50.png">
181
+ </a>
182
+ </div>
183
+
184
+ ---
185
+
186
+ <div align="center">
187
+ <p>⭐️ If you find Panther useful, please give it a star!</p>
188
+ </div>
@@ -0,0 +1,75 @@
1
+ panther/__init__.py,sha256=ytLbZ9XWNWjCIG-7DU0bpP1TRmk2oBs5u8RKm2A8uTA,115
2
+ panther/_load_configs.py,sha256=9SMiJm4N5wOZYYpM5BfchvHuTg7PZOmvfIkiloUQLDk,11283
3
+ panther/_utils.py,sha256=5UN0DBNTEqHejK6EOnG9IYyH1gK9OvGXYlNp5G0iFuU,4720
4
+ panther/app.py,sha256=H_tMo64KIFi79WIacEI_d8IcWxtvv9fUxNjQzTMwdqg,11449
5
+ panther/authentications.py,sha256=JdCeXKvo6iHmxeXsZEmFvXQsLkI149g1dIR_md6blV8,7844
6
+ panther/background_tasks.py,sha256=A__lY4IijGbRD9GKtbUK_c8cChtFW0jPaxoQHJ25bsk,7539
7
+ panther/base_request.py,sha256=MkzTv_Si4scJFZgHRaZurwjN6KthrKf1aqIN8u811z0,4950
8
+ panther/base_websocket.py,sha256=tCazsv5ILt-8wqYfsRB0NwXUIb-_wdeSTEBt3xjbneg,11000
9
+ panther/caching.py,sha256=nNyY6rZ9fnvsH44pk4eNaLuBYBDKw5CV6HC-7FAn6zM,2288
10
+ panther/configs.py,sha256=Hg-4B9mD4QL5aALEd7NJ8bTMikJWS1dhVtKe0n42Buc,3834
11
+ panther/events.py,sha256=-AFsJwZe9RpQ9xQQArUfqCPjv4ZRaFZ0shzTuO5WmWc,1576
12
+ panther/exceptions.py,sha256=QubEyGPnKlo4e7dR_SU2JbRB20vZ42LcUH3JvmOK5Xg,2231
13
+ panther/file_handler.py,sha256=6zXe36eaCyqtZFX2bMT9xl8tjimoHMcD7csLoPx_8EA,1323
14
+ panther/generics.py,sha256=O0XHXNKwRy3KbbE4UNJ5m-Tzn2qtNQZuu0OSf1ES03A,6806
15
+ panther/logging.py,sha256=g-RUuyCveqdMrEQXWIjIPZi2jYCJmOmZV8TvD_uMrEU,2075
16
+ panther/main.py,sha256=0i5HoJ4IGY2bF25lK1V6x7_f-boxceVz6zLj6Q6vTi8,7557
17
+ panther/pagination.py,sha256=bQEpf-FMil6zOwGuGD6VEowht2_13sT5jl-Cflwo_-E,1644
18
+ panther/permissions.py,sha256=UdPHVZYLWIYaf94OauE1QdVlj66_iE8B3rb336MBBcU,400
19
+ panther/request.py,sha256=IVuDdLdceCzo2vmICnWwoD2ag1eNc09C5XHZnULQxUw,1888
20
+ panther/response.py,sha256=WSWKlwb8l804W5SzmtKAQIavhmrdi3LHsG3xjBaMaos,10854
21
+ panther/routings.py,sha256=QwE7EyQD1wdgXS8JK80tV36tIrrBR7fRZ1OkhpA8m7s,6482
22
+ panther/serializer.py,sha256=e6iM09Uh6y6JrVGEzDlOfbB8vMTtSECW0Dy9_D6pn0A,8338
23
+ panther/status.py,sha256=Gc_PnYrHfInTsZpGbqiCfDB-py1C7Rh8KMdb6Lq9Exs,3346
24
+ panther/test.py,sha256=EReFLKhDtOoGQVTPSdtI31xi-u4SfwirA179G9_rIAE,7374
25
+ panther/throttling.py,sha256=EnU9PtulAwNTxsheun-s-kjJ1YL3jgj0bpxe8jGowlQ,2630
26
+ panther/utils.py,sha256=H2DrUz62ULv9BA6XdSJbaArRZG1ZQoJZmaFMXBvq_4c,4252
27
+ panther/websocket.py,sha256=er44pGU3Zm-glj4imS5l1Qdk7WNc_3Jpq7SPkeApPlM,1532
28
+ panther/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
+ panther/cli/create_command.py,sha256=iIAzS8C8rFKQ1idVSR5SMYRHWcwK09EYEqIqcBiiVXQ,10336
30
+ panther/cli/main.py,sha256=xWFxu_9cbrzsxgrkj0SrT2QUny3GMgJJuuAzK2OnR_M,1518
31
+ panther/cli/monitor_command.py,sha256=1hhPZNXGzvl-XXM0wwJol4dUwVgX3d00r3oV1mvNcS0,4462
32
+ panther/cli/run_command.py,sha256=ZInQQGV-QaLS7XUEUPqP_3iR2Nrto9unaOvYAs3mF9M,356
33
+ panther/cli/template.py,sha256=C3jb6m_NQRzur-_DNtEKiptMYtxTvd5MNM1qIgpFMNA,5331
34
+ panther/cli/utils.py,sha256=SjqggWpgGVH_JiMNQFnXPWzoMYxIHI2p9WO3-c59wU4,5542
35
+ panther/db/__init__.py,sha256=w9lEL0vRqb18Qx_iUJipUR_fi5GQ5uVX0DWycx14x08,50
36
+ panther/db/connections.py,sha256=RMcnArf1eurxjySpSg5afNmyUCxo_ifxhG1I8mr9L7M,4191
37
+ panther/db/cursor.py,sha256=TnbMUvEDpXGUuL42gDWT9QKFu5ymo0kLLo-Socgw7rM,1836
38
+ panther/db/models.py,sha256=E9y0ibCp1nPAKejMBtAQrkngmp3fXdFkgHfsXtfCBYM,3206
39
+ panther/db/utils.py,sha256=GiRQ4t9csEFKmGViej7dyfZaaiWMdTAQeWzdoCWTJac,1574
40
+ panther/db/queries/__init__.py,sha256=uF4gvBjLBJ-Yl3WLqoZEVNtHCVhFRKW3_Vi44pJxDNI,45
41
+ panther/db/queries/base_queries.py,sha256=0c1IxRl79C93JyEn5uno8WDBvyKTql_kyNND2ep5zqI,3817
42
+ panther/db/queries/mongodb_queries.py,sha256=rN0vKUQHtimQ0ogNacwuz5c2irkPHkn8ydjF9dU7aJQ,13468
43
+ panther/db/queries/pantherdb_queries.py,sha256=GlRRFvbaeVR3x2dYqlQIvsWxAWUcPflZ2u6kuJYvSIM,4620
44
+ panther/db/queries/queries.py,sha256=nhjrFk02O-rLUZ5slS3jHZ9wnxPrFLmiAZLaeVePKiA,12408
45
+ panther/middlewares/__init__.py,sha256=8VXd-K3L0a5ZkGb-NUipn3K8wxWAVIiOM7fQrcm_dTM,87
46
+ panther/middlewares/base.py,sha256=V5PuuemMCrQslIBK-sER4YZGdSDMzRFhZHjRUiIkhbY,721
47
+ panther/middlewares/cors.py,sha256=g4ougecREr88wnBSHziCeIVyIUnP0rYEs4-Izbf8tBI,3032
48
+ panther/middlewares/monitoring.py,sha256=ebkk84be6cGnUxi0IETnzKoYsGC2O_4kAarZ1cRynnA,1487
49
+ panther/openapi/__init__.py,sha256=UAkcGDxOltOkZwx3vZjigwfHUdb4xnidC6e_Viezfv4,47
50
+ panther/openapi/urls.py,sha256=HrYkS2sPgiTB6JpRCZaAwOSrw9lWa8EsfGJXZsp0B_0,78
51
+ panther/openapi/utils.py,sha256=_wAhTbcXvtXTMGjOUGcKm9sjDfoNhUFFsxHdrlEmguI,6492
52
+ panther/openapi/views.py,sha256=sUhjY9odz2kT_Xzl7MAyC4ARDsvPLHrH8lzNTo_Zf5U,3405
53
+ panther/openapi/templates/openapi.html,sha256=VAaJytOBFuR1rvGXOxbXOoJlurbeAl-VuTZu7Hk6LLs,889
54
+ panther/panel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
+ panther/panel/apis.py,sha256=0OWwiySdSinjYFVs6r97nVaSDO51DZ9f2VlEWAM87-E,2364
56
+ panther/panel/middlewares.py,sha256=xOYpIsy-fqc8OuieePLviUxmR6Tsu-kg3UtgerUKyHo,379
57
+ panther/panel/urls.py,sha256=10BEdB81sNSOxWKFdcAwA2AGV1ddgen4ntXcSk2nx6U,253
58
+ panther/panel/utils.py,sha256=hwHtRYiea8FNjLGhEAAyX7-1LlCSUV-4pHslG79PLCY,4258
59
+ panther/panel/views.py,sha256=go7qQwRliKOiYcQ2uJUluTRNqUh9q7KmuXc4y1eH8Q8,5040
60
+ panther/panel/templates/base.html,sha256=kHDzfmlIf14eLrZHymIHdywr36W7cJXKtqFpVhw-x34,327
61
+ panther/panel/templates/create.html,sha256=2cKjWpNElv390PPYzoI7MGqVjgy9692x3vpxwAJE7GE,581
62
+ panther/panel/templates/create.js,sha256=zO_GfaHnjVI25zx4wGhUPA7aEkCukKMpabJfuiOib7c,40180
63
+ panther/panel/templates/detail.html,sha256=wFuePktVNchECgPhMxlXjX_KH3tqQvVsTTUmtOWsqjA,1490
64
+ panther/panel/templates/home.html,sha256=vSVHoCWeqY4AhQiC-UVAvu10m2bINneO6_PLyOS9R4Q,238
65
+ panther/panel/templates/home.js,sha256=bC8io0iKdAftSvrapkwx7ZPAbVq3UzapV9sv5uWa8FY,849
66
+ panther/panel/templates/login.html,sha256=W6V1rgHAno7yTbP6Il38ZvJp4LdlJ8BjM4UuyPkjaTA,1625
67
+ panther/panel/templates/sidebar.html,sha256=XikovZsJrth0nvKogvZoh3Eb2Bq7xdeGTlsdlyud450,618
68
+ panther/panel/templates/table.html,sha256=fWdaIHEHAuwuPaAfOtXkD-3yvSocyDmtys00_D2yRh8,2176
69
+ panther/panel/templates/table.js,sha256=MTdf77571Gtmg4l8HkY-5fM-utIL3lc0O8hv6vLBCYk,10414
70
+ panther-5.0.0b5.dist-info/licenses/LICENSE,sha256=2aF1hL2aC0zRPjzUkSxJUzZbn2_uLoOkn7DHjzZni-I,1524
71
+ panther-5.0.0b5.dist-info/METADATA,sha256=pBsRQN6g2DoB1-aXY0DAAZt0A_HBwnpgdXuisbqbxoU,6269
72
+ panther-5.0.0b5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
73
+ panther-5.0.0b5.dist-info/entry_points.txt,sha256=6GPxYFGuzVfNB4YpHFJvYex6iWah5_tLnirAHwj2Qsg,51
74
+ panther-5.0.0b5.dist-info/top_level.txt,sha256=VbBs02JGXTIoHMzsX-eLOk2MCbBZzQbLhWiYpI7xI2g,8
75
+ panther-5.0.0b5.dist-info/RECORD,,
panther/monitoring.py DELETED
@@ -1,34 +0,0 @@
1
- import logging
2
- from time import perf_counter
3
- from typing import Literal
4
-
5
- from panther.base_request import BaseRequest
6
- from panther.configs import config
7
-
8
- logger = logging.getLogger('monitoring')
9
-
10
-
11
- class Monitoring:
12
- """
13
- Create Log Message Like Below:
14
- date_time | method | path | ip:port | response_time(seconds) | status
15
- """
16
- def __init__(self, is_ws: bool = False):
17
- self.is_ws = is_ws
18
-
19
- async def before(self, request: BaseRequest):
20
- if config.MONITORING:
21
- ip, port = request.client
22
-
23
- if self.is_ws:
24
- method = 'WS'
25
- else:
26
- method = request.scope['method']
27
-
28
- self.log = f'{method} | {request.path} | {ip}:{port}'
29
- self.start_time = perf_counter()
30
-
31
- async def after(self, status: int | Literal['Accepted', 'Rejected', 'Closed'], /):
32
- if config.MONITORING:
33
- response_time = perf_counter() - self.start_time # Seconds
34
- logger.info(f'{self.log} | {response_time} | {status}')