panther 5.0.0b4__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.0b4 → panther-5.0.0b5}/panther/__init__.py +1 -1
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/app.py +9 -9
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/connections.py +12 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/cursor.py +3 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/models.py +2 -2
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/queries/base_queries.py +2 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/queries/mongodb_queries.py +4 -1
- panther-5.0.0b5/panther/generics.py +165 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/response.py +49 -48
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/serializer.py +1 -23
- panther-5.0.0b5/panther.egg-info/PKG-INFO +188 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther.egg-info/SOURCES.txt +1 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther.egg-info/requires.txt +1 -1
- {panther-5.0.0b4 → panther-5.0.0b5}/setup.py +1 -1
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_database.py +2 -1
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_generics.py +71 -32
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_response.py +102 -23
- panther-5.0.0b5/tests/test_response_output_model.py +150 -0
- panther-5.0.0b4/PKG-INFO +0 -223
- panther-5.0.0b4/README.md +0 -173
- panther-5.0.0b4/panther/generics.py +0 -194
- panther-5.0.0b4/panther.egg-info/PKG-INFO +0 -223
- {panther-5.0.0b4 → panther-5.0.0b5}/LICENSE +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/_load_configs.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/_utils.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/authentications.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/background_tasks.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/base_request.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/base_websocket.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/caching.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/cli/__init__.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/cli/create_command.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/cli/main.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/cli/monitor_command.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/cli/run_command.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/cli/template.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/cli/utils.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/configs.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/__init__.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/queries/__init__.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/queries/pantherdb_queries.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/queries/queries.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/utils.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/events.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/exceptions.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/file_handler.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/logging.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/main.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/middlewares/__init__.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/middlewares/base.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/middlewares/cors.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/middlewares/monitoring.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/openapi/__init__.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/openapi/templates/openapi.html +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/openapi/urls.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/openapi/utils.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/openapi/views.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/pagination.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/__init__.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/apis.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/middlewares.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/base.html +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/create.html +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/create.js +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/detail.html +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/home.html +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/home.js +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/login.html +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/sidebar.html +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/table.html +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/table.js +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/urls.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/utils.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/views.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/permissions.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/request.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/routings.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/status.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/test.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/throttling.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/utils.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther/websocket.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther.egg-info/dependency_links.txt +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther.egg-info/entry_points.txt +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/panther.egg-info/top_level.txt +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/pyproject.toml +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/setup.cfg +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_api_kwargs.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_authentication.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_background_tasks.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_caching.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_cli.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_config.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_cors.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_database_advance.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_events.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_middlewares.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_multipart.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_openapi.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_permission.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_request.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_routing.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_run.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_serializer.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_status.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_throttling.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_utils.py +0 -0
- {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_websockets.py +0 -0
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>
|
@@ -42,6 +42,7 @@ class API:
|
|
42
42
|
methods: Specify the allowed methods.
|
43
43
|
input_model: The `request.data` will be validated with this attribute, It will raise an
|
44
44
|
`panther.exceptions.BadRequestAPIError` or put the validated data in the `request.validated_data`.
|
45
|
+
output_model: The `response.data` will be passed through this class to filter its attributes.
|
45
46
|
output_schema: This attribute only used in creation of OpenAPI scheme which is available in `panther.openapi.urls`
|
46
47
|
You may want to add its `url` to your urls.
|
47
48
|
auth: It will authenticate the user with header of its request or raise an
|
@@ -59,6 +60,7 @@ class API:
|
|
59
60
|
*,
|
60
61
|
methods: list[Literal['GET', 'POST', 'PUT', 'PATCH', 'DELETE']] | None = None,
|
61
62
|
input_model: type[ModelSerializer] | type[BaseModel] | None = None,
|
63
|
+
output_model: type[ModelSerializer] | type[BaseModel] | None = None,
|
62
64
|
output_schema: OutputSchema | None = None,
|
63
65
|
auth: bool = False,
|
64
66
|
permissions: list[type[BasePermission]] | None = None,
|
@@ -69,21 +71,14 @@ class API:
|
|
69
71
|
):
|
70
72
|
self.methods = {m.upper() for m in methods} if methods else {'GET', 'POST', 'PUT', 'PATCH', 'DELETE'}
|
71
73
|
self.input_model = input_model
|
74
|
+
self.output_model = output_model
|
72
75
|
self.output_schema = output_schema
|
73
76
|
self.auth = auth
|
74
77
|
self.permissions = permissions or []
|
75
78
|
self.throttling = throttling
|
76
79
|
self.cache = cache
|
77
|
-
self.middlewares
|
80
|
+
self.middlewares = middlewares
|
78
81
|
self.request: Request | None = None
|
79
|
-
if kwargs.pop('output_model', None):
|
80
|
-
deprecation_message = (
|
81
|
-
traceback.format_stack(limit=2)[0]
|
82
|
-
+ '\nThe `output_model` argument has been removed in Panther v5 and is no longer available.'
|
83
|
-
'\nPlease update your code to use the new approach. More info: '
|
84
|
-
'https://pantherpy.github.io/open_api/'
|
85
|
-
)
|
86
|
-
raise PantherError(deprecation_message)
|
87
82
|
if kwargs.pop('cache_exp_time', None):
|
88
83
|
deprecation_message = (
|
89
84
|
traceback.format_stack(limit=2)[0]
|
@@ -182,6 +177,8 @@ class API:
|
|
182
177
|
# 9. Clean Response
|
183
178
|
if not isinstance(response, Response):
|
184
179
|
response = Response(data=response)
|
180
|
+
if self.output_model and response.data:
|
181
|
+
response.data = await response.serialize_output(output_model=self.output_model)
|
185
182
|
if response.pagination:
|
186
183
|
response.data = await response.pagination.template(response.data)
|
187
184
|
|
@@ -228,6 +225,7 @@ class GenericAPI(metaclass=MetaGenericAPI):
|
|
228
225
|
"""
|
229
226
|
|
230
227
|
input_model: type[ModelSerializer] | type[BaseModel] | None = None
|
228
|
+
output_model: type[ModelSerializer] | type[BaseModel] | None = None
|
231
229
|
output_schema: OutputSchema | None = None
|
232
230
|
auth: bool = False
|
233
231
|
permissions: list[type[BasePermission]] | None = None
|
@@ -239,6 +237,7 @@ class GenericAPI(metaclass=MetaGenericAPI):
|
|
239
237
|
# Creating API instance to validate the attributes.
|
240
238
|
API(
|
241
239
|
input_model=cls.input_model,
|
240
|
+
output_model=cls.output_model,
|
242
241
|
output_schema=cls.output_schema,
|
243
242
|
auth=cls.auth,
|
244
243
|
permissions=cls.permissions,
|
@@ -279,6 +278,7 @@ class GenericAPI(metaclass=MetaGenericAPI):
|
|
279
278
|
|
280
279
|
return await API(
|
281
280
|
input_model=self.input_model,
|
281
|
+
output_model=self.output_model,
|
282
282
|
output_schema=self.output_schema,
|
283
283
|
auth=self.auth,
|
284
284
|
permissions=self.permissions,
|
@@ -73,6 +73,10 @@ class MongoDBConnection(BaseDatabaseConnection):
|
|
73
73
|
def session(self):
|
74
74
|
return self._database
|
75
75
|
|
76
|
+
@property
|
77
|
+
def client(self):
|
78
|
+
return self._client
|
79
|
+
|
76
80
|
|
77
81
|
class PantherDBConnection(BaseDatabaseConnection):
|
78
82
|
def init(self, path: str | None = None, encryption: bool = False):
|
@@ -90,12 +94,20 @@ class PantherDBConnection(BaseDatabaseConnection):
|
|
90
94
|
def session(self):
|
91
95
|
return self._connection
|
92
96
|
|
97
|
+
@property
|
98
|
+
def client(self):
|
99
|
+
return self._connection
|
100
|
+
|
93
101
|
|
94
102
|
class DatabaseConnection(Singleton):
|
95
103
|
@property
|
96
104
|
def session(self):
|
97
105
|
return config.DATABASE.session
|
98
106
|
|
107
|
+
@property
|
108
|
+
def client(self):
|
109
|
+
return config.DATABASE.client
|
110
|
+
|
99
111
|
|
100
112
|
class RedisConnection(Singleton, _Redis):
|
101
113
|
is_connected: bool = False
|
@@ -37,7 +37,7 @@ def validate_object_id(value, handler):
|
|
37
37
|
raise ValueError(msg) from e
|
38
38
|
|
39
39
|
|
40
|
-
ID = Annotated[str, WrapValidator(validate_object_id), PlainSerializer(lambda x: str(x), return_type=str)]
|
40
|
+
ID = Annotated[str, WrapValidator(validate_object_id), PlainSerializer(lambda x: str(x), return_type=str)] | None
|
41
41
|
|
42
42
|
|
43
43
|
class Model(PydanticBaseModel, Query):
|
@@ -46,7 +46,7 @@ class Model(PydanticBaseModel, Query):
|
|
46
46
|
return
|
47
47
|
config.MODELS.append(cls)
|
48
48
|
|
49
|
-
id: ID
|
49
|
+
id: ID = None
|
50
50
|
|
51
51
|
@property
|
52
52
|
def _id(self):
|
@@ -45,6 +45,8 @@ class BaseQuery:
|
|
45
45
|
@classmethod
|
46
46
|
async def _create_model_instance(cls, document: dict):
|
47
47
|
"""Prevent getting errors from document insertion"""
|
48
|
+
if '_id' in document:
|
49
|
+
document['id'] = document.pop('_id')
|
48
50
|
try:
|
49
51
|
return cls(**document)
|
50
52
|
except ValidationError as validation_error:
|
@@ -104,7 +104,7 @@ class BaseMongoDBQuery(BaseQuery):
|
|
104
104
|
@classmethod
|
105
105
|
async def _create_field(cls, model: type, field_name: str, value: Any) -> Any:
|
106
106
|
# Handle primary key field directly
|
107
|
-
if field_name == '
|
107
|
+
if field_name == 'id':
|
108
108
|
return value
|
109
109
|
|
110
110
|
if field_name not in model.model_fields:
|
@@ -155,6 +155,9 @@ class BaseMongoDBQuery(BaseQuery):
|
|
155
155
|
@classmethod
|
156
156
|
async def _create_model_instance(cls, document: dict) -> Self:
|
157
157
|
"""Prepares document and creates an instance of the model."""
|
158
|
+
if '_id' in document:
|
159
|
+
document['id'] = document.pop('_id')
|
160
|
+
|
158
161
|
processed_document = {
|
159
162
|
field_name: await cls._create_field(model=cls, field_name=field_name, value=field_value)
|
160
163
|
for field_name, field_value in document.items()
|
@@ -0,0 +1,165 @@
|
|
1
|
+
import contextlib
|
2
|
+
import logging
|
3
|
+
from abc import abstractmethod
|
4
|
+
|
5
|
+
from pantherdb import Cursor as PantherDBCursor
|
6
|
+
|
7
|
+
from panther import status
|
8
|
+
from panther.app import GenericAPI
|
9
|
+
from panther.configs import config
|
10
|
+
from panther.db import Model
|
11
|
+
from panther.db.connections import MongoDBConnection
|
12
|
+
from panther.db.cursor import Cursor
|
13
|
+
from panther.db.models import ID
|
14
|
+
from panther.exceptions import APIError
|
15
|
+
from panther.pagination import Pagination
|
16
|
+
from panther.request import Request
|
17
|
+
from panther.response import Response
|
18
|
+
from panther.serializer import ModelSerializer
|
19
|
+
|
20
|
+
with contextlib.suppress(ImportError):
|
21
|
+
# Only required if user wants to use mongodb
|
22
|
+
import bson
|
23
|
+
|
24
|
+
logger = logging.getLogger('panther')
|
25
|
+
|
26
|
+
|
27
|
+
class RetrieveAPI(GenericAPI):
|
28
|
+
@abstractmethod
|
29
|
+
async def get_instance(self, request: Request, **kwargs) -> Model:
|
30
|
+
"""
|
31
|
+
Should return an instance of Model, e.g. `await User.find_one()`
|
32
|
+
"""
|
33
|
+
logger.error(f'`get_instance()` method is not implemented in {self.__class__} .')
|
34
|
+
raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
35
|
+
|
36
|
+
async def get(self, request: Request, **kwargs):
|
37
|
+
instance = await self.get_instance(request=request, **kwargs)
|
38
|
+
return Response(data=instance, status_code=status.HTTP_200_OK)
|
39
|
+
|
40
|
+
|
41
|
+
class ListAPI(GenericAPI):
|
42
|
+
sort_fields: list[str] = []
|
43
|
+
search_fields: list[str] = []
|
44
|
+
filter_fields: list[str] = []
|
45
|
+
pagination: type[Pagination] | None = None
|
46
|
+
|
47
|
+
async def get_query(self, request: Request, **kwargs) -> Cursor | PantherDBCursor:
|
48
|
+
"""
|
49
|
+
Should return a Cursor, e.g. `await User.find()`
|
50
|
+
"""
|
51
|
+
logger.error(f'`get_query()` method is not implemented in {self.__class__} .')
|
52
|
+
raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
53
|
+
|
54
|
+
async def get(self, request: Request, **kwargs):
|
55
|
+
cursor, pagination = await self.prepare_cursor(request=request, **kwargs)
|
56
|
+
return Response(data=cursor, pagination=pagination, status_code=status.HTTP_200_OK)
|
57
|
+
|
58
|
+
async def prepare_cursor(self, request: Request, **kwargs) -> tuple[Cursor | PantherDBCursor, Pagination | None]:
|
59
|
+
cursor = await self.get_query(request=request, **kwargs)
|
60
|
+
if not isinstance(cursor, (Cursor, PantherDBCursor)):
|
61
|
+
logger.error(f'`{self.__class__.__name__}.get_query()` should return a Cursor, e.g. `await Model.find()`')
|
62
|
+
raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
63
|
+
|
64
|
+
query = {}
|
65
|
+
query |= self.process_filters(query_params=request.query_params, cursor=cursor)
|
66
|
+
query |= self.process_search(query_params=request.query_params)
|
67
|
+
|
68
|
+
if query:
|
69
|
+
cursor = await cursor.cls.find(cursor.filter | query)
|
70
|
+
|
71
|
+
if sort := self.process_sort(query_params=request.query_params):
|
72
|
+
cursor = cursor.sort(sort)
|
73
|
+
|
74
|
+
if pagination := self.process_pagination(query_params=request.query_params, cursor=cursor):
|
75
|
+
cursor = pagination.paginate()
|
76
|
+
|
77
|
+
return cursor, pagination
|
78
|
+
|
79
|
+
def process_filters(self, query_params: dict, cursor: Cursor | PantherDBCursor) -> dict:
|
80
|
+
_filter = {}
|
81
|
+
for field in self.filter_fields:
|
82
|
+
if field in query_params:
|
83
|
+
_filter[field] = query_params[field]
|
84
|
+
if isinstance(config.DATABASE, MongoDBConnection) and cursor.cls.model_fields[field].annotation == ID:
|
85
|
+
_filter[field] = bson.ObjectId(_filter[field])
|
86
|
+
return _filter
|
87
|
+
|
88
|
+
def process_search(self, query_params: dict) -> dict:
|
89
|
+
search_param = query_params.get('search')
|
90
|
+
if not self.search_fields or not search_param:
|
91
|
+
return {}
|
92
|
+
if isinstance(config.DATABASE, MongoDBConnection):
|
93
|
+
if search := [{field: {'$regex': search_param}} for field in self.search_fields]:
|
94
|
+
return {'$or': search}
|
95
|
+
return {field: search_param for field in self.search_fields}
|
96
|
+
|
97
|
+
def process_sort(self, query_params: dict) -> list:
|
98
|
+
sort_param = query_params.get('sort')
|
99
|
+
if not self.sort_fields or not sort_param:
|
100
|
+
return []
|
101
|
+
return [
|
102
|
+
(field, -1 if param.startswith('-') else 1)
|
103
|
+
for param in sort_param.split(',')
|
104
|
+
for field in self.sort_fields
|
105
|
+
if field == param.removeprefix('-')
|
106
|
+
]
|
107
|
+
|
108
|
+
def process_pagination(self, query_params: dict, cursor: Cursor | PantherDBCursor) -> Pagination | None:
|
109
|
+
if self.pagination:
|
110
|
+
return self.pagination(query_params=query_params, cursor=cursor)
|
111
|
+
|
112
|
+
|
113
|
+
class CreateAPI(GenericAPI):
|
114
|
+
input_model: type[ModelSerializer] | None = None
|
115
|
+
|
116
|
+
async def post(self, request: Request, **kwargs):
|
117
|
+
instance = await request.validated_data.model.insert_one(request.validated_data.model_dump())
|
118
|
+
return Response(data=instance, status_code=status.HTTP_201_CREATED)
|
119
|
+
|
120
|
+
|
121
|
+
class UpdateAPI(GenericAPI):
|
122
|
+
input_model: type[ModelSerializer] | None = None
|
123
|
+
|
124
|
+
@abstractmethod
|
125
|
+
async def get_instance(self, request: Request, **kwargs) -> Model:
|
126
|
+
"""
|
127
|
+
Should return an instance of Model, e.g. `await User.find_one()`
|
128
|
+
"""
|
129
|
+
logger.error(f'`get_instance()` method is not implemented in {self.__class__} .')
|
130
|
+
raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
131
|
+
|
132
|
+
async def put(self, request: Request, **kwargs):
|
133
|
+
instance = await self.get_instance(request=request, **kwargs)
|
134
|
+
await instance.update(request.validated_data.model_dump())
|
135
|
+
return Response(data=instance, status_code=status.HTTP_200_OK)
|
136
|
+
|
137
|
+
async def patch(self, request: Request, **kwargs):
|
138
|
+
instance = await self.get_instance(request=request, **kwargs)
|
139
|
+
await instance.update(request.validated_data.model_dump(exclude_none=True))
|
140
|
+
return Response(data=instance, status_code=status.HTTP_200_OK)
|
141
|
+
|
142
|
+
|
143
|
+
class DeleteAPI(GenericAPI):
|
144
|
+
@abstractmethod
|
145
|
+
async def get_instance(self, request: Request, **kwargs) -> Model:
|
146
|
+
"""
|
147
|
+
Should return an instance of Model, e.g. `await User.find_one()`
|
148
|
+
"""
|
149
|
+
logger.error(f'`get_instance()` method is not implemented in {self.__class__} .')
|
150
|
+
raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
151
|
+
|
152
|
+
async def pre_delete(self, instance, request: Request, **kwargs):
|
153
|
+
"""Hook for logic before deletion."""
|
154
|
+
pass
|
155
|
+
|
156
|
+
async def post_delete(self, instance, request: Request, **kwargs):
|
157
|
+
"""Hook for logic after deletion."""
|
158
|
+
pass
|
159
|
+
|
160
|
+
async def delete(self, request: Request, **kwargs):
|
161
|
+
instance = await self.get_instance(request=request, **kwargs)
|
162
|
+
await self.pre_delete(instance, request=request, **kwargs)
|
163
|
+
await instance.delete()
|
164
|
+
await self.post_delete(instance, request=request, **kwargs)
|
165
|
+
return Response(status_code=status.HTTP_204_NO_CONTENT)
|