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.
- panther/__init__.py +1 -1
- panther/_load_configs.py +46 -37
- panther/_utils.py +49 -34
- panther/app.py +96 -97
- panther/authentications.py +97 -50
- panther/background_tasks.py +98 -124
- panther/base_request.py +16 -10
- panther/base_websocket.py +8 -8
- panther/caching.py +16 -80
- panther/cli/create_command.py +17 -16
- panther/cli/main.py +1 -1
- panther/cli/monitor_command.py +11 -6
- panther/cli/run_command.py +5 -71
- panther/cli/template.py +7 -7
- panther/cli/utils.py +58 -69
- panther/configs.py +70 -72
- panther/db/connections.py +30 -24
- panther/db/cursor.py +3 -1
- panther/db/models.py +26 -10
- panther/db/queries/base_queries.py +4 -5
- panther/db/queries/mongodb_queries.py +21 -21
- panther/db/queries/pantherdb_queries.py +1 -1
- panther/db/queries/queries.py +26 -8
- panther/db/utils.py +1 -1
- panther/events.py +25 -14
- panther/exceptions.py +2 -7
- panther/file_handler.py +1 -1
- panther/generics.py +74 -100
- panther/logging.py +2 -1
- panther/main.py +12 -13
- panther/middlewares/cors.py +67 -0
- panther/middlewares/monitoring.py +5 -3
- panther/openapi/urls.py +2 -2
- panther/openapi/utils.py +3 -3
- panther/openapi/views.py +20 -37
- panther/pagination.py +4 -2
- panther/panel/apis.py +2 -7
- panther/panel/urls.py +2 -6
- panther/panel/utils.py +9 -5
- panther/panel/views.py +13 -22
- panther/permissions.py +2 -1
- panther/request.py +2 -1
- panther/response.py +101 -94
- panther/routings.py +12 -12
- panther/serializer.py +20 -43
- panther/test.py +73 -58
- panther/throttling.py +68 -3
- panther/utils.py +5 -11
- panther-5.0.0b5.dist-info/METADATA +188 -0
- panther-5.0.0b5.dist-info/RECORD +75 -0
- panther/monitoring.py +0 -34
- panther-5.0.0b3.dist-info/METADATA +0 -223
- panther-5.0.0b3.dist-info/RECORD +0 -75
- {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/WHEEL +0 -0
- {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/entry_points.txt +0 -0
- {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/licenses/LICENSE +0 -0
- {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
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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
|
-
|
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
|
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
|
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
|
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
|
+
[](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)
|
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}')
|