panther 5.0.0b3__tar.gz → 5.0.0b5__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-5.0.0b5/PKG-INFO +188 -0
- panther-5.0.0b5/README.md +138 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/__init__.py +1 -1
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/_load_configs.py +46 -37
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/_utils.py +49 -34
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/app.py +96 -97
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/authentications.py +97 -50
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/background_tasks.py +98 -124
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/base_request.py +16 -10
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/base_websocket.py +8 -8
- panther-5.0.0b5/panther/caching.py +64 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/cli/create_command.py +17 -16
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/cli/main.py +1 -1
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/cli/monitor_command.py +11 -6
- panther-5.0.0b5/panther/cli/run_command.py +16 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/cli/template.py +7 -7
- panther-5.0.0b5/panther/cli/utils.py +156 -0
- panther-5.0.0b5/panther/configs.py +119 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/db/connections.py +30 -24
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/db/cursor.py +3 -1
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/db/models.py +26 -10
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/db/queries/base_queries.py +4 -5
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/db/queries/mongodb_queries.py +21 -21
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/db/queries/pantherdb_queries.py +1 -1
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/db/queries/queries.py +26 -8
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/db/utils.py +1 -1
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/events.py +25 -14
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/exceptions.py +2 -7
- panther-5.0.0b5/panther/generics.py +165 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/logging.py +2 -1
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/main.py +12 -13
- panther-5.0.0b5/panther/middlewares/cors.py +67 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/middlewares/monitoring.py +5 -3
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/openapi/urls.py +2 -2
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/openapi/utils.py +3 -3
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/openapi/views.py +20 -37
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/pagination.py +4 -2
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/panel/apis.py +2 -7
- panther-5.0.0b5/panther/panel/urls.py +9 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/panel/utils.py +9 -5
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/panel/views.py +13 -22
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/permissions.py +2 -1
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/request.py +2 -1
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/response.py +101 -94
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/routings.py +12 -12
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/serializer.py +20 -43
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/test.py +73 -58
- panther-5.0.0b5/panther/throttling.py +76 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/utils.py +5 -11
- panther-5.0.0b5/panther.egg-info/PKG-INFO +188 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther.egg-info/SOURCES.txt +6 -2
- {panther-5.0.0b3 → panther-5.0.0b5}/panther.egg-info/requires.txt +1 -1
- {panther-5.0.0b3 → panther-5.0.0b5}/pyproject.toml +2 -1
- {panther-5.0.0b3 → panther-5.0.0b5}/setup.py +3 -6
- panther-5.0.0b5/tests/test_api_kwargs.py +176 -0
- panther-5.0.0b5/tests/test_authentication.py +395 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/tests/test_background_tasks.py +86 -7
- {panther-5.0.0b3 → panther-5.0.0b5}/tests/test_caching.py +17 -26
- {panther-5.0.0b3 → panther-5.0.0b5}/tests/test_cli.py +25 -26
- panther-5.0.0b5/tests/test_config.py +320 -0
- panther-5.0.0b5/tests/test_cors.py +84 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/tests/test_database.py +19 -17
- {panther-5.0.0b3 → panther-5.0.0b5}/tests/test_database_advance.py +26 -13
- {panther-5.0.0b3 → panther-5.0.0b5}/tests/test_events.py +39 -21
- {panther-5.0.0b3 → panther-5.0.0b5}/tests/test_generics.py +80 -37
- {panther-5.0.0b3 → panther-5.0.0b5}/tests/test_middlewares.py +17 -16
- {panther-5.0.0b3 → panther-5.0.0b5}/tests/test_multipart.py +61 -55
- {panther-5.0.0b3 → panther-5.0.0b5}/tests/test_openapi.py +7 -2
- panther-5.0.0b5/tests/test_permission.py +123 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/tests/test_request.py +5 -2
- {panther-5.0.0b3 → panther-5.0.0b5}/tests/test_response.py +231 -74
- panther-5.0.0b5/tests/test_response_output_model.py +150 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/tests/test_routing.py +169 -105
- {panther-5.0.0b3 → panther-5.0.0b5}/tests/test_run.py +7 -8
- {panther-5.0.0b3 → panther-5.0.0b5}/tests/test_serializer.py +24 -36
- {panther-5.0.0b3 → panther-5.0.0b5}/tests/test_throttling.py +42 -4
- {panther-5.0.0b3 → panther-5.0.0b5}/tests/test_utils.py +39 -26
- {panther-5.0.0b3 → panther-5.0.0b5}/tests/test_websockets.py +7 -10
- panther-5.0.0b3/PKG-INFO +0 -223
- panther-5.0.0b3/README.md +0 -173
- panther-5.0.0b3/panther/caching.py +0 -128
- panther-5.0.0b3/panther/cli/run_command.py +0 -82
- panther-5.0.0b3/panther/cli/utils.py +0 -167
- panther-5.0.0b3/panther/configs.py +0 -121
- panther-5.0.0b3/panther/generics.py +0 -191
- panther-5.0.0b3/panther/monitoring.py +0 -34
- panther-5.0.0b3/panther/panel/urls.py +0 -13
- panther-5.0.0b3/panther/throttling.py +0 -11
- panther-5.0.0b3/panther.egg-info/PKG-INFO +0 -223
- panther-5.0.0b3/tests/test_authentication.py +0 -172
- panther-5.0.0b3/tests/test_panel_apis.py +0 -30
- {panther-5.0.0b3 → panther-5.0.0b5}/LICENSE +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/cli/__init__.py +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/db/__init__.py +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/db/queries/__init__.py +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/file_handler.py +1 -1
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/middlewares/__init__.py +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/middlewares/base.py +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/openapi/__init__.py +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/openapi/templates/openapi.html +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/panel/__init__.py +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/panel/middlewares.py +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/panel/templates/base.html +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/panel/templates/create.html +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/panel/templates/create.js +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/panel/templates/detail.html +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/panel/templates/home.html +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/panel/templates/home.js +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/panel/templates/login.html +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/panel/templates/sidebar.html +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/panel/templates/table.html +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/panel/templates/table.js +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/status.py +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther/websocket.py +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther.egg-info/dependency_links.txt +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther.egg-info/entry_points.txt +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/panther.egg-info/top_level.txt +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/setup.cfg +0 -0
- {panther-5.0.0b3 → panther-5.0.0b5}/tests/test_status.py +1 -1
panther-5.0.0b5/PKG-INFO
ADDED
@@ -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,138 @@
|
|
1
|
+
[](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)
|
2
|
+
|
3
|
+
<div align="center">
|
4
|
+
<img src="https://github.com/AliRn76/panther/raw/master/docs/docs/images/logo-vertical.png" alt="Panther Logo" width="450">
|
5
|
+
|
6
|
+
# Panther
|
7
|
+
|
8
|
+
**A Fast & Friendly Web Framework for Building Async APIs with Python 3.10+**
|
9
|
+
|
10
|
+
[📚 Documentation](https://pantherpy.github.io)
|
11
|
+
</div>
|
12
|
+
|
13
|
+
---
|
14
|
+
|
15
|
+
## 🐾 Why Choose Panther?
|
16
|
+
|
17
|
+
Panther is designed to be **fast**, **simple**, and **powerful**. Here's what makes it special:
|
18
|
+
|
19
|
+
- **One of the fastest Python frameworks** available ([Benchmark](https://www.techempower.com/benchmarks/#section=data-r23&l=zijzen-pa7&c=4))
|
20
|
+
- **File-based database** ([PantherDB](https://pypi.org/project/pantherdb/)) - No external database setup required
|
21
|
+
- **Document-oriented ODM** - Supports MongoDB & PantherDB with familiar syntax
|
22
|
+
- **API caching system** - In-memory and Redis support
|
23
|
+
- **OpenAPI/Swagger** - Auto-generated API documentation
|
24
|
+
- **WebSocket support** - Real-time communication out of the box
|
25
|
+
- **Authentication & Permissions** - Built-in security features
|
26
|
+
- **Background tasks** - Handle long-running operations
|
27
|
+
- **Middleware & Throttling** - Extensible and configurable
|
28
|
+
|
29
|
+
---
|
30
|
+
|
31
|
+
## Quick Start
|
32
|
+
|
33
|
+
### Installation
|
34
|
+
|
35
|
+
```bash
|
36
|
+
pip install panther
|
37
|
+
```
|
38
|
+
|
39
|
+
- Create a `main.py` file with one of the examples below.
|
40
|
+
|
41
|
+
### Your First API
|
42
|
+
|
43
|
+
Here's a simple REST API endpoint that returns a "Hello World" message:
|
44
|
+
|
45
|
+
```python
|
46
|
+
from datetime import datetime, timedelta
|
47
|
+
from panther import status, Panther
|
48
|
+
from panther.app import GenericAPI
|
49
|
+
from panther.openapi.urls import url_routing as openapi_url_routing
|
50
|
+
from panther.response import Response
|
51
|
+
|
52
|
+
class HelloAPI(GenericAPI):
|
53
|
+
# Cache responses for 10 seconds
|
54
|
+
cache = timedelta(seconds=10)
|
55
|
+
|
56
|
+
def get(self):
|
57
|
+
current_time = datetime.now().isoformat()
|
58
|
+
return Response(
|
59
|
+
data={'message': f'Hello from Panther! 🐾 | {current_time}'},
|
60
|
+
status_code=status.HTTP_200_OK
|
61
|
+
)
|
62
|
+
|
63
|
+
# URL routing configuration
|
64
|
+
url_routing = {
|
65
|
+
'/': HelloAPI,
|
66
|
+
'swagger/': openapi_url_routing, # Auto-generated API docs
|
67
|
+
}
|
68
|
+
|
69
|
+
# Create your Panther app
|
70
|
+
app = Panther(__name__, configs=__name__, urls=url_routing)
|
71
|
+
```
|
72
|
+
|
73
|
+
### WebSocket Echo Server
|
74
|
+
|
75
|
+
Here's a simple WebSocket echo server that sends back any message it receives:
|
76
|
+
|
77
|
+
```python
|
78
|
+
from panther import Panther
|
79
|
+
from panther.app import GenericAPI
|
80
|
+
from panther.response import HTMLResponse
|
81
|
+
from panther.websocket import GenericWebsocket
|
82
|
+
|
83
|
+
class EchoWebsocket(GenericWebsocket):
|
84
|
+
async def connect(self, **kwargs):
|
85
|
+
await self.accept()
|
86
|
+
await self.send("Connected to Panther WebSocket!")
|
87
|
+
|
88
|
+
async def receive(self, data: str | bytes):
|
89
|
+
# Echo back the received message
|
90
|
+
await self.send(f"Echo: {data}")
|
91
|
+
|
92
|
+
class WebSocketPage(GenericAPI):
|
93
|
+
def get(self):
|
94
|
+
template = """
|
95
|
+
<h2>🐾 Panther WebSocket Echo Server</h2>
|
96
|
+
<input id="msg"><button onclick="s.send(msg.value)">Send</button>
|
97
|
+
<ul id="log"></ul>
|
98
|
+
<script>
|
99
|
+
const s = new WebSocket('ws://127.0.0.1:8000/ws');
|
100
|
+
s.onmessage = e => log.innerHTML += `<li><- ${msg.value}</li><li>-> ${e.data}</li>`;
|
101
|
+
</script>
|
102
|
+
"""
|
103
|
+
return HTMLResponse(template)
|
104
|
+
|
105
|
+
url_routing = {
|
106
|
+
'': WebSocketPage,
|
107
|
+
'ws': EchoWebsocket,
|
108
|
+
}
|
109
|
+
app = Panther(__name__, configs=__name__, urls=url_routing)
|
110
|
+
```
|
111
|
+
|
112
|
+
### Run Your Application
|
113
|
+
|
114
|
+
1. **Start the development server**
|
115
|
+
```shell
|
116
|
+
$ panther run main:app
|
117
|
+
```
|
118
|
+
|
119
|
+
2. **Test your application**
|
120
|
+
- For the _API_ example: Visit [http://127.0.0.1:8000/](http://127.0.0.1:8000/) to see the "Hello World" response
|
121
|
+
- For the _WebSocket_ example: Visit [http://127.0.0.1:8000/](http://127.0.0.1:8000/) and send a message.
|
122
|
+
|
123
|
+
---
|
124
|
+
|
125
|
+
## 🙏 Acknowledgments
|
126
|
+
|
127
|
+
<div align="center">
|
128
|
+
<p>Supported by</p>
|
129
|
+
<a href="https://drive.google.com/file/d/17xe1hicIiRF7SQ-clg9SETdc19SktCbV/view?usp=sharing">
|
130
|
+
<img alt="JetBrains" src="https://github.com/AliRn76/panther/raw/master/docs/docs/images/jb_beam_50x50.png">
|
131
|
+
</a>
|
132
|
+
</div>
|
133
|
+
|
134
|
+
---
|
135
|
+
|
136
|
+
<div align="center">
|
137
|
+
<p>⭐️ If you find Panther useful, please give it a star!</p>
|
138
|
+
</div>
|
@@ -6,8 +6,8 @@ from multiprocessing import Manager
|
|
6
6
|
|
7
7
|
import jinja2
|
8
8
|
|
9
|
-
from panther._utils import
|
10
|
-
from panther.background_tasks import
|
9
|
+
from panther._utils import check_class_type_endpoint, check_function_type_endpoint, import_class
|
10
|
+
from panther.background_tasks import _background_tasks
|
11
11
|
from panther.base_websocket import WebsocketConnections
|
12
12
|
from panther.cli.utils import import_error
|
13
13
|
from panther.configs import JWTConfig, config
|
@@ -15,34 +15,35 @@ from panther.db.connections import redis
|
|
15
15
|
from panther.db.queries.mongodb_queries import BaseMongoDBQuery
|
16
16
|
from panther.db.queries.pantherdb_queries import BasePantherDBQuery
|
17
17
|
from panther.exceptions import PantherError
|
18
|
-
from panther.middlewares.base import
|
18
|
+
from panther.middlewares.base import HTTPMiddleware, WebsocketMiddleware
|
19
19
|
from panther.middlewares.monitoring import MonitoringMiddleware, WebsocketMonitoringMiddleware
|
20
20
|
from panther.panel.views import HomeView
|
21
21
|
from panther.routings import finalize_urls, flatten_urls
|
22
22
|
|
23
23
|
__all__ = (
|
24
|
+
'check_endpoints_inheritance',
|
25
|
+
'load_authentication_class',
|
26
|
+
'load_auto_reformat',
|
27
|
+
'load_background_tasks',
|
24
28
|
'load_configs_module',
|
25
|
-
'load_redis',
|
26
|
-
'load_startup',
|
27
|
-
'load_shutdown',
|
28
|
-
'load_timezone',
|
29
29
|
'load_database',
|
30
|
-
'load_secret_key',
|
31
|
-
'load_throttling',
|
32
|
-
'load_user_model',
|
33
30
|
'load_log_queries',
|
34
31
|
'load_middlewares',
|
32
|
+
'load_other_configs',
|
33
|
+
'load_redis',
|
34
|
+
'load_secret_key',
|
35
|
+
'load_shutdown',
|
36
|
+
'load_startup',
|
35
37
|
'load_templates_dir',
|
36
|
-
'
|
37
|
-
'
|
38
|
-
'load_default_cache_exp',
|
38
|
+
'load_throttling',
|
39
|
+
'load_timezone',
|
39
40
|
'load_urls',
|
40
|
-
'
|
41
|
+
'load_user_model',
|
41
42
|
'load_websocket_connections',
|
42
|
-
'check_endpoints_inheritance',
|
43
43
|
)
|
44
44
|
|
45
45
|
logger = logging.getLogger('panther')
|
46
|
+
monitoring_logger = logging.getLogger('monitoring')
|
46
47
|
|
47
48
|
|
48
49
|
def load_configs_module(module_name: str, /) -> dict:
|
@@ -88,7 +89,7 @@ def load_timezone(_configs: dict, /) -> None:
|
|
88
89
|
|
89
90
|
|
90
91
|
def load_templates_dir(_configs: dict, /) -> None:
|
91
|
-
if templates_dir := _configs.get('TEMPLATES_DIR'):
|
92
|
+
if templates_dir := _configs.get('TEMPLATES_DIR', '.'):
|
92
93
|
config.TEMPLATES_DIR = templates_dir
|
93
94
|
|
94
95
|
if config.TEMPLATES_DIR == '.':
|
@@ -100,8 +101,8 @@ def load_templates_dir(_configs: dict, /) -> None:
|
|
100
101
|
jinja2.FileSystemLoader(searchpath=config.TEMPLATES_DIR),
|
101
102
|
jinja2.PackageLoader(package_name='panther', package_path='panel/templates/'),
|
102
103
|
jinja2.PackageLoader(package_name='panther', package_path='openapi/templates/'),
|
103
|
-
)
|
104
|
-
)
|
104
|
+
),
|
105
|
+
),
|
105
106
|
)
|
106
107
|
|
107
108
|
|
@@ -109,7 +110,7 @@ def load_database(_configs: dict, /) -> None:
|
|
109
110
|
database_config = _configs.get('DATABASE', {})
|
110
111
|
if 'engine' in database_config:
|
111
112
|
if 'class' not in database_config['engine']:
|
112
|
-
raise _exception_handler(field='DATABASE', error=
|
113
|
+
raise _exception_handler(field='DATABASE', error='`engine["class"]` not found.')
|
113
114
|
|
114
115
|
engine_class_path = database_config['engine']['class']
|
115
116
|
engine_class = import_class(engine_class_path)
|
@@ -131,7 +132,7 @@ def load_database(_configs: dict, /) -> None:
|
|
131
132
|
|
132
133
|
def load_secret_key(_configs: dict, /) -> None:
|
133
134
|
if secret_key := _configs.get('SECRET_KEY'):
|
134
|
-
config.SECRET_KEY = secret_key
|
135
|
+
config.SECRET_KEY = secret_key
|
135
136
|
|
136
137
|
|
137
138
|
def load_throttling(_configs: dict, /) -> None:
|
@@ -141,7 +142,8 @@ def load_throttling(_configs: dict, /) -> None:
|
|
141
142
|
|
142
143
|
def load_user_model(_configs: dict, /) -> None:
|
143
144
|
config.USER_MODEL = import_class(_configs.get('USER_MODEL', 'panther.db.models.BaseUser'))
|
144
|
-
config.
|
145
|
+
if config.USER_MODEL not in config.MODELS:
|
146
|
+
config.MODELS.append(config.USER_MODEL)
|
145
147
|
|
146
148
|
|
147
149
|
def load_log_queries(_configs: dict, /) -> None:
|
@@ -162,12 +164,14 @@ def load_middlewares(_configs: dict, /) -> None:
|
|
162
164
|
_deprecated_warning(
|
163
165
|
field='MIDDLEWARES',
|
164
166
|
message='`data` does not supported in middlewares anymore, as your data is static you may want '
|
165
|
-
|
167
|
+
'to pass them to your middleware with config variables',
|
166
168
|
)
|
167
169
|
middleware = middleware[0]
|
168
170
|
else:
|
169
171
|
raise _exception_handler(
|
170
|
-
field='MIDDLEWARES',
|
172
|
+
field='MIDDLEWARES',
|
173
|
+
error=f'{middleware} should be dotted path or type of a middleware class',
|
174
|
+
)
|
171
175
|
|
172
176
|
# `middleware` can be type or path of a class
|
173
177
|
if not callable(middleware):
|
@@ -175,9 +179,12 @@ def load_middlewares(_configs: dict, /) -> None:
|
|
175
179
|
middleware = import_class(middleware)
|
176
180
|
except (AttributeError, ModuleNotFoundError):
|
177
181
|
raise _exception_handler(
|
178
|
-
field='MIDDLEWARES',
|
182
|
+
field='MIDDLEWARES',
|
183
|
+
error=f'{middleware} is not a valid middleware path or type',
|
184
|
+
)
|
179
185
|
|
180
186
|
if issubclass(middleware, (MonitoringMiddleware, WebsocketMonitoringMiddleware)):
|
187
|
+
monitoring_logger.debug('') # Initiated
|
181
188
|
config.MONITORING = True
|
182
189
|
|
183
190
|
if issubclass(middleware, HTTPMiddleware):
|
@@ -187,7 +194,7 @@ def load_middlewares(_configs: dict, /) -> None:
|
|
187
194
|
else:
|
188
195
|
raise _exception_handler(
|
189
196
|
field='MIDDLEWARES',
|
190
|
-
error='is not a sub class of `HTTPMiddleware` or `WebsocketMiddleware`'
|
197
|
+
error='is not a sub class of `HTTPMiddleware` or `WebsocketMiddleware`',
|
191
198
|
)
|
192
199
|
|
193
200
|
config.HTTP_MIDDLEWARES = middlewares['http']
|
@@ -202,12 +209,14 @@ def load_auto_reformat(_configs: dict, /) -> None:
|
|
202
209
|
def load_background_tasks(_configs: dict, /) -> None:
|
203
210
|
if _configs.get('BACKGROUND_TASKS'):
|
204
211
|
config.BACKGROUND_TASKS = True
|
205
|
-
|
212
|
+
_background_tasks.initialize()
|
206
213
|
|
207
214
|
|
208
|
-
def
|
209
|
-
|
210
|
-
|
215
|
+
def load_other_configs(_configs: dict, /) -> None:
|
216
|
+
known_configs = set(config.__dataclass_fields__)
|
217
|
+
for key, value in _configs.items():
|
218
|
+
if key.isupper() and key not in known_configs:
|
219
|
+
config[key] = value
|
211
220
|
|
212
221
|
|
213
222
|
def load_urls(_configs: dict, /, urls: dict | None) -> None:
|
@@ -222,7 +231,7 @@ def load_urls(_configs: dict, /, urls: dict | None) -> None:
|
|
222
231
|
|
223
232
|
elif isinstance(url_routing, dict):
|
224
233
|
error = (
|
225
|
-
"can't be 'dict', you may want to pass it's value directly to Panther().
|
234
|
+
"can't be 'dict', you may want to pass it's value directly to Panther(). Example: Panther(..., urls=...)"
|
226
235
|
)
|
227
236
|
raise _exception_handler(field='URLs', error=error)
|
228
237
|
|
@@ -256,18 +265,18 @@ def load_authentication_class(_configs: dict, /) -> None:
|
|
256
265
|
|
257
266
|
def load_jwt_config(_configs: dict, /) -> None:
|
258
267
|
"""Only Collect JWT Config If Authentication Is JWTAuthentication"""
|
259
|
-
|
260
|
-
getattr(config.AUTHENTICATION, '__name__', None) == 'JWTAuthentication' or
|
261
|
-
getattr(config.WS_AUTHENTICATION, '__name__', None) == 'QueryParamJWTAuthentication'
|
262
|
-
)
|
263
|
-
jwt = _configs.get('JWTConfig', {})
|
268
|
+
from panther.authentications import JWTAuthentication
|
264
269
|
|
270
|
+
auth_is_jwt = (config.AUTHENTICATION and issubclass(config.AUTHENTICATION, JWTAuthentication)) or (
|
271
|
+
config.WS_AUTHENTICATION and issubclass(config.WS_AUTHENTICATION, JWTAuthentication)
|
272
|
+
)
|
273
|
+
jwt = _configs.get('JWT_CONFIG', {})
|
265
274
|
using_panel_views = HomeView in config.FLAT_URLS.values()
|
266
275
|
if auth_is_jwt or using_panel_views:
|
267
276
|
if 'key' not in jwt:
|
268
277
|
if config.SECRET_KEY is None:
|
269
278
|
raise _exception_handler(field='JWTConfig', error='`JWTConfig.key` or `SECRET_KEY` is required.')
|
270
|
-
jwt['key'] = config.SECRET_KEY
|
279
|
+
jwt['key'] = config.SECRET_KEY
|
271
280
|
config.JWT_CONFIG = JWTConfig(**jwt)
|
272
281
|
|
273
282
|
|
@@ -287,7 +296,7 @@ def load_websocket_connections():
|
|
287
296
|
|
288
297
|
def check_endpoints_inheritance():
|
289
298
|
"""Should be after `load_urls()`"""
|
290
|
-
for
|
299
|
+
for endpoint in config.FLAT_URLS.values():
|
291
300
|
if endpoint == {}:
|
292
301
|
continue
|
293
302
|
|
@@ -4,9 +4,9 @@ import logging
|
|
4
4
|
import re
|
5
5
|
import subprocess
|
6
6
|
import types
|
7
|
-
from collections.abc import Callable
|
7
|
+
from collections.abc import AsyncGenerator, Callable, Generator, Iterator
|
8
8
|
from traceback import TracebackException
|
9
|
-
from typing import Any
|
9
|
+
from typing import Any
|
10
10
|
|
11
11
|
from panther.exceptions import PantherError
|
12
12
|
from panther.file_handler import File
|
@@ -20,51 +20,65 @@ def import_class(dotted_path: str, /) -> type[Any]:
|
|
20
20
|
-------
|
21
21
|
Input: panther.db.models.User
|
22
22
|
Output: User (The Class)
|
23
|
+
|
23
24
|
"""
|
24
25
|
path, name = dotted_path.rsplit('.', 1)
|
25
26
|
module = importlib.import_module(path)
|
26
27
|
return getattr(module, name)
|
27
28
|
|
28
29
|
|
30
|
+
NEWLINE_CRLF = b'\r\n' # Windows-style
|
31
|
+
NEWLINE_LF = b'\n' # Unix/Linux-style
|
32
|
+
|
33
|
+
# Regex patterns for CRLF (Windows)
|
34
|
+
FIELD_PATTERN_CRLF = re.compile(rb'Content-Disposition: form-data; name="(.*)"\r\n\r\n(.*)', flags=re.DOTALL)
|
35
|
+
FILE_PATTERN_CRLF = re.compile(rb'Content-Disposition: form-data; name="(.*)"; filename="(.*)"\r\nContent-Type: (.*)')
|
36
|
+
|
37
|
+
# Regex patterns for LF (Linux)
|
38
|
+
FIELD_PATTERN_LF = re.compile(rb'Content-Disposition: form-data; name="(.*)"\n\n(.*)', flags=re.DOTALL)
|
39
|
+
FILE_PATTERN_LF = re.compile(rb'Content-Disposition: form-data; name="(.*)"; filename="(.*)"\nContent-Type: (.*)')
|
40
|
+
|
41
|
+
|
29
42
|
def read_multipart_form_data(boundary: str, body: bytes) -> dict:
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
+ b'Content-Type: )(.*)'
|
42
|
-
)
|
43
|
+
boundary_bytes = b'--' + boundary.encode()
|
44
|
+
|
45
|
+
# Choose newline type and corresponding patterns
|
46
|
+
if body.endswith(NEWLINE_CRLF):
|
47
|
+
newline = NEWLINE_CRLF
|
48
|
+
field_pattern = FIELD_PATTERN_CRLF
|
49
|
+
file_pattern = FILE_PATTERN_CRLF
|
50
|
+
else:
|
51
|
+
newline = NEWLINE_LF
|
52
|
+
field_pattern = FIELD_PATTERN_LF
|
53
|
+
file_pattern = FILE_PATTERN_LF
|
43
54
|
|
44
55
|
data = {}
|
45
|
-
for
|
46
|
-
|
56
|
+
for part in body.split(boundary_bytes):
|
57
|
+
part = part.removeprefix(newline).removesuffix(newline)
|
47
58
|
|
48
|
-
if
|
59
|
+
if part in (b'', b'--'):
|
49
60
|
continue
|
50
61
|
|
51
|
-
if match :=
|
52
|
-
|
62
|
+
if match := field_pattern.match(string=part):
|
63
|
+
field_name, value = match.groups()
|
53
64
|
data[field_name.decode('utf-8')] = value.decode('utf-8')
|
65
|
+
continue
|
54
66
|
|
67
|
+
try:
|
68
|
+
headers, file_content = part.split(2 * newline, 1)
|
69
|
+
except ValueError:
|
70
|
+
logger.error('Malformed part, skipping.')
|
71
|
+
continue
|
72
|
+
|
73
|
+
if match := file_pattern.match(string=headers):
|
74
|
+
field_name, file_name, content_type = match.groups()
|
75
|
+
data[field_name.decode('utf-8')] = File(
|
76
|
+
file_name=file_name.decode('utf-8'),
|
77
|
+
content_type=content_type.decode('utf-8'),
|
78
|
+
file=file_content,
|
79
|
+
)
|
55
80
|
else:
|
56
|
-
|
57
|
-
|
58
|
-
if match := re.match(pattern=file_pattern, string=file_meta_data):
|
59
|
-
_, field_name, _, file_name, _, content_type = match.groups()
|
60
|
-
file = File(
|
61
|
-
file_name=file_name.decode('utf-8'),
|
62
|
-
content_type=content_type.decode('utf-8'),
|
63
|
-
file=value,
|
64
|
-
)
|
65
|
-
data[field_name.decode('utf-8')] = file
|
66
|
-
else:
|
67
|
-
logger.error('Unrecognized Pattern')
|
81
|
+
logger.error('Unrecognized multipart format')
|
68
82
|
|
69
83
|
return data
|
70
84
|
|
@@ -94,7 +108,8 @@ def check_function_type_endpoint(endpoint: types.FunctionType) -> Callable:
|
|
94
108
|
# Function Doesn't Have @API Decorator
|
95
109
|
if not hasattr(endpoint, '__wrapped__'):
|
96
110
|
raise PantherError(
|
97
|
-
f'You may have forgotten to use `@API()` on the `{endpoint.__module__}.{endpoint.__name__}()`'
|
111
|
+
f'You may have forgotten to use `@API()` on the `{endpoint.__module__}.{endpoint.__name__}()`',
|
112
|
+
)
|
98
113
|
|
99
114
|
|
100
115
|
def check_class_type_endpoint(endpoint: Callable) -> Callable:
|
@@ -104,7 +119,7 @@ def check_class_type_endpoint(endpoint: Callable) -> Callable:
|
|
104
119
|
if not issubclass(endpoint, (GenericAPI, GenericWebsocket)):
|
105
120
|
raise PantherError(
|
106
121
|
f'You may have forgotten to inherit from `panther.app.GenericAPI` or `panther.app.GenericWebsocket` '
|
107
|
-
f'on the `{endpoint.__module__}.{endpoint.__name__}()`'
|
122
|
+
f'on the `{endpoint.__module__}.{endpoint.__name__}()`',
|
108
123
|
)
|
109
124
|
|
110
125
|
|