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.
Files changed (110) hide show
  1. panther-5.0.0b5/PKG-INFO +188 -0
  2. panther-5.0.0b5/README.md +138 -0
  3. {panther-5.0.0b4 → panther-5.0.0b5}/panther/__init__.py +1 -1
  4. {panther-5.0.0b4 → panther-5.0.0b5}/panther/app.py +9 -9
  5. {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/connections.py +12 -0
  6. {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/cursor.py +3 -0
  7. {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/models.py +2 -2
  8. {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/queries/base_queries.py +2 -0
  9. {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/queries/mongodb_queries.py +4 -1
  10. panther-5.0.0b5/panther/generics.py +165 -0
  11. {panther-5.0.0b4 → panther-5.0.0b5}/panther/response.py +49 -48
  12. {panther-5.0.0b4 → panther-5.0.0b5}/panther/serializer.py +1 -23
  13. panther-5.0.0b5/panther.egg-info/PKG-INFO +188 -0
  14. {panther-5.0.0b4 → panther-5.0.0b5}/panther.egg-info/SOURCES.txt +1 -0
  15. {panther-5.0.0b4 → panther-5.0.0b5}/panther.egg-info/requires.txt +1 -1
  16. {panther-5.0.0b4 → panther-5.0.0b5}/setup.py +1 -1
  17. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_database.py +2 -1
  18. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_generics.py +71 -32
  19. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_response.py +102 -23
  20. panther-5.0.0b5/tests/test_response_output_model.py +150 -0
  21. panther-5.0.0b4/PKG-INFO +0 -223
  22. panther-5.0.0b4/README.md +0 -173
  23. panther-5.0.0b4/panther/generics.py +0 -194
  24. panther-5.0.0b4/panther.egg-info/PKG-INFO +0 -223
  25. {panther-5.0.0b4 → panther-5.0.0b5}/LICENSE +0 -0
  26. {panther-5.0.0b4 → panther-5.0.0b5}/panther/_load_configs.py +0 -0
  27. {panther-5.0.0b4 → panther-5.0.0b5}/panther/_utils.py +0 -0
  28. {panther-5.0.0b4 → panther-5.0.0b5}/panther/authentications.py +0 -0
  29. {panther-5.0.0b4 → panther-5.0.0b5}/panther/background_tasks.py +0 -0
  30. {panther-5.0.0b4 → panther-5.0.0b5}/panther/base_request.py +0 -0
  31. {panther-5.0.0b4 → panther-5.0.0b5}/panther/base_websocket.py +0 -0
  32. {panther-5.0.0b4 → panther-5.0.0b5}/panther/caching.py +0 -0
  33. {panther-5.0.0b4 → panther-5.0.0b5}/panther/cli/__init__.py +0 -0
  34. {panther-5.0.0b4 → panther-5.0.0b5}/panther/cli/create_command.py +0 -0
  35. {panther-5.0.0b4 → panther-5.0.0b5}/panther/cli/main.py +0 -0
  36. {panther-5.0.0b4 → panther-5.0.0b5}/panther/cli/monitor_command.py +0 -0
  37. {panther-5.0.0b4 → panther-5.0.0b5}/panther/cli/run_command.py +0 -0
  38. {panther-5.0.0b4 → panther-5.0.0b5}/panther/cli/template.py +0 -0
  39. {panther-5.0.0b4 → panther-5.0.0b5}/panther/cli/utils.py +0 -0
  40. {panther-5.0.0b4 → panther-5.0.0b5}/panther/configs.py +0 -0
  41. {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/__init__.py +0 -0
  42. {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/queries/__init__.py +0 -0
  43. {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/queries/pantherdb_queries.py +0 -0
  44. {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/queries/queries.py +0 -0
  45. {panther-5.0.0b4 → panther-5.0.0b5}/panther/db/utils.py +0 -0
  46. {panther-5.0.0b4 → panther-5.0.0b5}/panther/events.py +0 -0
  47. {panther-5.0.0b4 → panther-5.0.0b5}/panther/exceptions.py +0 -0
  48. {panther-5.0.0b4 → panther-5.0.0b5}/panther/file_handler.py +0 -0
  49. {panther-5.0.0b4 → panther-5.0.0b5}/panther/logging.py +0 -0
  50. {panther-5.0.0b4 → panther-5.0.0b5}/panther/main.py +0 -0
  51. {panther-5.0.0b4 → panther-5.0.0b5}/panther/middlewares/__init__.py +0 -0
  52. {panther-5.0.0b4 → panther-5.0.0b5}/panther/middlewares/base.py +0 -0
  53. {panther-5.0.0b4 → panther-5.0.0b5}/panther/middlewares/cors.py +0 -0
  54. {panther-5.0.0b4 → panther-5.0.0b5}/panther/middlewares/monitoring.py +0 -0
  55. {panther-5.0.0b4 → panther-5.0.0b5}/panther/openapi/__init__.py +0 -0
  56. {panther-5.0.0b4 → panther-5.0.0b5}/panther/openapi/templates/openapi.html +0 -0
  57. {panther-5.0.0b4 → panther-5.0.0b5}/panther/openapi/urls.py +0 -0
  58. {panther-5.0.0b4 → panther-5.0.0b5}/panther/openapi/utils.py +0 -0
  59. {panther-5.0.0b4 → panther-5.0.0b5}/panther/openapi/views.py +0 -0
  60. {panther-5.0.0b4 → panther-5.0.0b5}/panther/pagination.py +0 -0
  61. {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/__init__.py +0 -0
  62. {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/apis.py +0 -0
  63. {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/middlewares.py +0 -0
  64. {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/base.html +0 -0
  65. {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/create.html +0 -0
  66. {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/create.js +0 -0
  67. {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/detail.html +0 -0
  68. {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/home.html +0 -0
  69. {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/home.js +0 -0
  70. {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/login.html +0 -0
  71. {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/sidebar.html +0 -0
  72. {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/table.html +0 -0
  73. {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/templates/table.js +0 -0
  74. {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/urls.py +0 -0
  75. {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/utils.py +0 -0
  76. {panther-5.0.0b4 → panther-5.0.0b5}/panther/panel/views.py +0 -0
  77. {panther-5.0.0b4 → panther-5.0.0b5}/panther/permissions.py +0 -0
  78. {panther-5.0.0b4 → panther-5.0.0b5}/panther/request.py +0 -0
  79. {panther-5.0.0b4 → panther-5.0.0b5}/panther/routings.py +0 -0
  80. {panther-5.0.0b4 → panther-5.0.0b5}/panther/status.py +0 -0
  81. {panther-5.0.0b4 → panther-5.0.0b5}/panther/test.py +0 -0
  82. {panther-5.0.0b4 → panther-5.0.0b5}/panther/throttling.py +0 -0
  83. {panther-5.0.0b4 → panther-5.0.0b5}/panther/utils.py +0 -0
  84. {panther-5.0.0b4 → panther-5.0.0b5}/panther/websocket.py +0 -0
  85. {panther-5.0.0b4 → panther-5.0.0b5}/panther.egg-info/dependency_links.txt +0 -0
  86. {panther-5.0.0b4 → panther-5.0.0b5}/panther.egg-info/entry_points.txt +0 -0
  87. {panther-5.0.0b4 → panther-5.0.0b5}/panther.egg-info/top_level.txt +0 -0
  88. {panther-5.0.0b4 → panther-5.0.0b5}/pyproject.toml +0 -0
  89. {panther-5.0.0b4 → panther-5.0.0b5}/setup.cfg +0 -0
  90. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_api_kwargs.py +0 -0
  91. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_authentication.py +0 -0
  92. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_background_tasks.py +0 -0
  93. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_caching.py +0 -0
  94. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_cli.py +0 -0
  95. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_config.py +0 -0
  96. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_cors.py +0 -0
  97. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_database_advance.py +0 -0
  98. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_events.py +0 -0
  99. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_middlewares.py +0 -0
  100. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_multipart.py +0 -0
  101. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_openapi.py +0 -0
  102. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_permission.py +0 -0
  103. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_request.py +0 -0
  104. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_routing.py +0 -0
  105. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_run.py +0 -0
  106. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_serializer.py +0 -0
  107. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_status.py +0 -0
  108. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_throttling.py +0 -0
  109. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_utils.py +0 -0
  110. {panther-5.0.0b4 → panther-5.0.0b5}/tests/test_websockets.py +0 -0
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.4
2
+ Name: panther
3
+ Version: 5.0.0b5
4
+ Summary: Fast & Friendly, Web Framework For Building Async APIs
5
+ Home-page: https://github.com/alirn76/panther
6
+ Author: Ali RajabNezhad
7
+ Author-email: alirn76@yahoo.com
8
+ License: BSD-3-Clause license
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: pantherdb~=2.3.0
18
+ Requires-Dist: orjson~=3.9.15
19
+ Requires-Dist: pydantic~=2.10.6
20
+ Requires-Dist: rich~=13.9.4
21
+ Requires-Dist: uvicorn~=0.34.0
22
+ Requires-Dist: pytz~=2025.2
23
+ Requires-Dist: Jinja2~=3.1
24
+ Requires-Dist: simple-ulid~=1.0.0
25
+ Requires-Dist: httptools~=0.6.4
26
+ Provides-Extra: full
27
+ Requires-Dist: redis==5.2.1; extra == "full"
28
+ Requires-Dist: motor~=3.7.0; extra == "full"
29
+ Requires-Dist: ipython~=9.0.2; extra == "full"
30
+ Requires-Dist: python-jose~=3.4.0; extra == "full"
31
+ Requires-Dist: ruff~=0.11.2; extra == "full"
32
+ Requires-Dist: websockets~=15.0.1; extra == "full"
33
+ Requires-Dist: cryptography~=44.0.2; extra == "full"
34
+ Requires-Dist: watchfiles~=1.0.4; extra == "full"
35
+ Provides-Extra: dev
36
+ Requires-Dist: ruff~=0.11.2; extra == "dev"
37
+ Requires-Dist: pytest~=8.3.5; extra == "dev"
38
+ Dynamic: author
39
+ Dynamic: author-email
40
+ Dynamic: classifier
41
+ Dynamic: description
42
+ Dynamic: description-content-type
43
+ Dynamic: home-page
44
+ Dynamic: license
45
+ Dynamic: license-file
46
+ Dynamic: provides-extra
47
+ Dynamic: requires-dist
48
+ Dynamic: requires-python
49
+ Dynamic: summary
50
+
51
+ [![PyPI](https://img.shields.io/pypi/v/panther?label=PyPI)](https://pypi.org/project/panther/) [![PyVersion](https://img.shields.io/pypi/pyversions/panther.svg)](https://pypi.org/project/panther/) [![codecov](https://codecov.io/github/AliRn76/panther/graph/badge.svg?token=YWFQA43GSP)](https://codecov.io/github/AliRn76/panther) [![Downloads](https://static.pepy.tech/badge/panther/month)](https://pepy.tech/project/panther) [![license](https://img.shields.io/github/license/alirn76/panther.svg)](https://github.com/alirn76/panther/blob/main/LICENSE)
52
+
53
+ <div align="center">
54
+ <img src="https://github.com/AliRn76/panther/raw/master/docs/docs/images/logo-vertical.png" alt="Panther Logo" width="450">
55
+
56
+ # Panther
57
+
58
+ **A Fast & Friendly Web Framework for Building Async APIs with Python 3.10+**
59
+
60
+ [📚 Documentation](https://pantherpy.github.io)
61
+ </div>
62
+
63
+ ---
64
+
65
+ ## 🐾 Why Choose Panther?
66
+
67
+ Panther is designed to be **fast**, **simple**, and **powerful**. Here's what makes it special:
68
+
69
+ - **One of the fastest Python frameworks** available ([Benchmark](https://www.techempower.com/benchmarks/#section=data-r23&l=zijzen-pa7&c=4))
70
+ - **File-based database** ([PantherDB](https://pypi.org/project/pantherdb/)) - No external database setup required
71
+ - **Document-oriented ODM** - Supports MongoDB & PantherDB with familiar syntax
72
+ - **API caching system** - In-memory and Redis support
73
+ - **OpenAPI/Swagger** - Auto-generated API documentation
74
+ - **WebSocket support** - Real-time communication out of the box
75
+ - **Authentication & Permissions** - Built-in security features
76
+ - **Background tasks** - Handle long-running operations
77
+ - **Middleware & Throttling** - Extensible and configurable
78
+
79
+ ---
80
+
81
+ ## Quick Start
82
+
83
+ ### Installation
84
+
85
+ ```bash
86
+ pip install panther
87
+ ```
88
+
89
+ - Create a `main.py` file with one of the examples below.
90
+
91
+ ### Your First API
92
+
93
+ Here's a simple REST API endpoint that returns a "Hello World" message:
94
+
95
+ ```python
96
+ from datetime import datetime, timedelta
97
+ from panther import status, Panther
98
+ from panther.app import GenericAPI
99
+ from panther.openapi.urls import url_routing as openapi_url_routing
100
+ from panther.response import Response
101
+
102
+ class HelloAPI(GenericAPI):
103
+ # Cache responses for 10 seconds
104
+ cache = timedelta(seconds=10)
105
+
106
+ def get(self):
107
+ current_time = datetime.now().isoformat()
108
+ return Response(
109
+ data={'message': f'Hello from Panther! 🐾 | {current_time}'},
110
+ status_code=status.HTTP_200_OK
111
+ )
112
+
113
+ # URL routing configuration
114
+ url_routing = {
115
+ '/': HelloAPI,
116
+ 'swagger/': openapi_url_routing, # Auto-generated API docs
117
+ }
118
+
119
+ # Create your Panther app
120
+ app = Panther(__name__, configs=__name__, urls=url_routing)
121
+ ```
122
+
123
+ ### WebSocket Echo Server
124
+
125
+ Here's a simple WebSocket echo server that sends back any message it receives:
126
+
127
+ ```python
128
+ from panther import Panther
129
+ from panther.app import GenericAPI
130
+ from panther.response import HTMLResponse
131
+ from panther.websocket import GenericWebsocket
132
+
133
+ class EchoWebsocket(GenericWebsocket):
134
+ async def connect(self, **kwargs):
135
+ await self.accept()
136
+ await self.send("Connected to Panther WebSocket!")
137
+
138
+ async def receive(self, data: str | bytes):
139
+ # Echo back the received message
140
+ await self.send(f"Echo: {data}")
141
+
142
+ class WebSocketPage(GenericAPI):
143
+ def get(self):
144
+ template = """
145
+ <h2>🐾 Panther WebSocket Echo Server</h2>
146
+ <input id="msg"><button onclick="s.send(msg.value)">Send</button>
147
+ <ul id="log"></ul>
148
+ <script>
149
+ const s = new WebSocket('ws://127.0.0.1:8000/ws');
150
+ s.onmessage = e => log.innerHTML += `<li><- ${msg.value}</li><li>-> ${e.data}</li>`;
151
+ </script>
152
+ """
153
+ return HTMLResponse(template)
154
+
155
+ url_routing = {
156
+ '': WebSocketPage,
157
+ 'ws': EchoWebsocket,
158
+ }
159
+ app = Panther(__name__, configs=__name__, urls=url_routing)
160
+ ```
161
+
162
+ ### Run Your Application
163
+
164
+ 1. **Start the development server**
165
+ ```shell
166
+ $ panther run main:app
167
+ ```
168
+
169
+ 2. **Test your application**
170
+ - For the _API_ example: Visit [http://127.0.0.1:8000/](http://127.0.0.1:8000/) to see the "Hello World" response
171
+ - For the _WebSocket_ example: Visit [http://127.0.0.1:8000/](http://127.0.0.1:8000/) and send a message.
172
+
173
+ ---
174
+
175
+ ## 🙏 Acknowledgments
176
+
177
+ <div align="center">
178
+ <p>Supported by</p>
179
+ <a href="https://drive.google.com/file/d/17xe1hicIiRF7SQ-clg9SETdc19SktCbV/view?usp=sharing">
180
+ <img alt="JetBrains" src="https://github.com/AliRn76/panther/raw/master/docs/docs/images/jb_beam_50x50.png">
181
+ </a>
182
+ </div>
183
+
184
+ ---
185
+
186
+ <div align="center">
187
+ <p>⭐️ If you find Panther useful, please give it a star!</p>
188
+ </div>
@@ -0,0 +1,138 @@
1
+ [![PyPI](https://img.shields.io/pypi/v/panther?label=PyPI)](https://pypi.org/project/panther/) [![PyVersion](https://img.shields.io/pypi/pyversions/panther.svg)](https://pypi.org/project/panther/) [![codecov](https://codecov.io/github/AliRn76/panther/graph/badge.svg?token=YWFQA43GSP)](https://codecov.io/github/AliRn76/panther) [![Downloads](https://static.pepy.tech/badge/panther/month)](https://pepy.tech/project/panther) [![license](https://img.shields.io/github/license/alirn76/panther.svg)](https://github.com/alirn76/panther/blob/main/LICENSE)
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>
@@ -1,6 +1,6 @@
1
1
  from panther.main import Panther # noqa: F401
2
2
 
3
- __version__ = '5.0.0beta4'
3
+ __version__ = '5.0.0beta5'
4
4
 
5
5
 
6
6
  def version():
@@ -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: list[[HTTPMiddleware]] | None = 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
@@ -36,6 +36,9 @@ class Cursor(_Cursor):
36
36
  def __aiter__(self) -> Self:
37
37
  return self
38
38
 
39
+ def __iter__(self) -> Self:
40
+ return self
41
+
39
42
  async def next(self) -> Self:
40
43
  return await self.cls._create_model_instance(document=super().next())
41
44
 
@@ -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 | None = Field(None, validation_alias='_id', alias='_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 == '_id':
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)