fenrir-framework 1.2.2__tar.gz → 2.2.2__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.
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/PKG-INFO +36 -7
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/README.md +33 -6
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/__init__.py +30 -4
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/app.py +45 -14
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/cli.py +2 -2
- fenrir_framework-2.2.2/fenrir/middleware.py +399 -0
- fenrir_framework-2.2.2/fenrir/pagination.py +132 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/routing.py +16 -5
- fenrir_framework-2.2.2/fenrir/sessions.py +304 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/websocket.py +29 -3
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir_framework.egg-info/PKG-INFO +36 -7
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir_framework.egg-info/SOURCES.txt +3 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir_framework.egg-info/requires.txt +3 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/pyproject.toml +4 -1
- fenrir_framework-2.2.2/tests/test_new_middleware_features.py +978 -0
- fenrir_framework-1.2.2/fenrir/sessions.py +0 -93
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/LICENSE +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/background.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/bottle.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/compat.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/config.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/context.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/dependencies.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/exceptions.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/falcon.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/helpers.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/json.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/logo.jpg +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/logo.png +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/openapi.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/request.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/response.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/sanic.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/security.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/signals.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/sse.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/templating.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/testing.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/upload.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir/views.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir_framework.egg-info/dependency_links.txt +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir_framework.egg-info/entry_points.txt +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/fenrir_framework.egg-info/top_level.txt +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/setup.cfg +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_appctx.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_async.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_basic.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_blueprints.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_circular_deps.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_cli.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_config.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_context.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_converters.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_custom_template.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_dep_overrides.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_dependencies.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_falcon_compat.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_form_file.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_helpers.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_instance_config.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_json.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_json_tag.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_logging.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_logo_route.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_middleware.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_new_features.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_regression.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_reqctx.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_request.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_resources.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_router_circular.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_routing.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_sanic_compat.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_security.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_session_interface.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_signals.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_sse.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_strict_content_type.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_subclassing.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_templating.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_testing.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_user_error_handler.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_validation.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_views.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_websocket.py +0 -0
- {fenrir_framework-1.2.2 → fenrir_framework-2.2.2}/tests/test_yield_deps.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fenrir-framework
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: A hybrid Python web framework combining Flask, FastAPI, Bottle, Falcon, and Sanic
|
|
5
5
|
Requires-Python: >=3.8
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -11,6 +11,8 @@ Requires-Dist: asteri>=2.2.2
|
|
|
11
11
|
Requires-Dist: itsdangerous>=2.0.0
|
|
12
12
|
Requires-Dist: python-multipart>=0.0.18
|
|
13
13
|
Requires-Dist: typing_extensions>=4.0.0
|
|
14
|
+
Provides-Extra: redis
|
|
15
|
+
Requires-Dist: redis>=4.0.0; extra == "redis"
|
|
14
16
|
Dynamic: license-file
|
|
15
17
|
|
|
16
18
|
<p align="center">
|
|
@@ -22,7 +24,7 @@ Dynamic: license-file
|
|
|
22
24
|
[](https://pypi.org/project/fenrir-framework/)
|
|
23
25
|
[](https://opensource.org/licenses/MIT)
|
|
24
26
|
[](https://www.python.org/)
|
|
25
|
-
[](https://github.com/IshikawaUta/fenrir/actions)
|
|
26
28
|
[](https://github.com/IshikawaUta/fenrir/actions/workflows/test.yml)
|
|
27
29
|
[]()
|
|
28
30
|
|
|
@@ -83,7 +85,7 @@ logging.basicConfig(level=logging.INFO)
|
|
|
83
85
|
logger = logging.getLogger("demo")
|
|
84
86
|
|
|
85
87
|
# Initialize the Hybrid App
|
|
86
|
-
app = Fenrir(title="Fenrir Hybrid Demo", version="
|
|
88
|
+
app = Fenrir(title="Fenrir Hybrid Demo", version="2.2.2")
|
|
87
89
|
|
|
88
90
|
# --- 1. FastAPI-Style Validation & DI ---
|
|
89
91
|
class UserRegister(BaseModel):
|
|
@@ -178,7 +180,7 @@ fenrir info demo_app:app
|
|
|
178
180
|
|
|
179
181
|
## 🧪 Comprehensive Test Suite
|
|
180
182
|
|
|
181
|
-
Fenrir is thoroughly covered by an automated test suite comprising **
|
|
183
|
+
Fenrir is thoroughly covered by an automated test suite comprising **528 tests** validating every single component, compat namespace, file upload, routing detail, and CLI functionality. The suite runs automatically via **GitHub Actions** on every push across Python **3.8 – 3.13**.
|
|
182
184
|
|
|
183
185
|
Run the test suite locally:
|
|
184
186
|
|
|
@@ -188,13 +190,40 @@ PYTHONPATH=. pytest -v
|
|
|
188
190
|
|
|
189
191
|
### Output:
|
|
190
192
|
```text
|
|
191
|
-
|
|
193
|
+
======================= 528 passed, 1 skipped in 3.4s ========================
|
|
192
194
|
```
|
|
193
195
|
|
|
194
196
|
---
|
|
195
197
|
|
|
196
198
|
## 🔄 Changelog
|
|
197
199
|
|
|
200
|
+
### v2.2.2 — Major Feature Update
|
|
201
|
+
|
|
202
|
+
New middleware, session backends, pagination, and more:
|
|
203
|
+
|
|
204
|
+
**New Middleware (`fenrir.middleware`)**
|
|
205
|
+
- **CORSMiddleware**: Full CORS support for HTTP and WebSocket with configurable origins, methods, headers, credentials, and max-age.
|
|
206
|
+
- **GZipMiddleware**: Automatic gzip compression for responses above a configurable size threshold.
|
|
207
|
+
- **RequestIDMiddleware**: Auto-generates unique request IDs or forwards client-provided IDs via configurable header.
|
|
208
|
+
- **RateLimitMiddleware**: Sliding-window rate limiter per client IP with configurable limits and block status code.
|
|
209
|
+
|
|
210
|
+
**New Session Backends (`fenrir.sessions`)**
|
|
211
|
+
- **InMemorySessionInterface**: In-memory session storage with TTL expiration, suitable for single-process apps and testing.
|
|
212
|
+
- **RedisSessionInterface**: Redis-backed session storage with support for both sync (`fakeredis`) and async (`redis.asyncio`) clients. Install with `pip install fenrir-framework[redis]`.
|
|
213
|
+
|
|
214
|
+
**New Pagination Utilities (`fenrir.pagination`)**
|
|
215
|
+
- **PaginationParams**: Pydantic model for query parameters (`page`, `page_size`, `sort_by`, `sort_order`).
|
|
216
|
+
- **paginate()**: Utility to paginate SQLAlchemy-style query results with metadata.
|
|
217
|
+
- **paginate_dict()**: Utility to paginate lists of dictionaries.
|
|
218
|
+
|
|
219
|
+
**New Features**
|
|
220
|
+
- **WebSocket per-route timeout**: `@app.websocket("/ws", timeout=5.0)` raises `WebSocketTimeout` if no message received within the timeout.
|
|
221
|
+
- **Multiple response models per status**: `response_models={200: SuccessModel, 404: ErrorModel}` applies different models based on the actual response status code.
|
|
222
|
+
|
|
223
|
+
**Improvements**
|
|
224
|
+
- ASGI middleware stack is now built once and cached, with automatic invalidation when new middleware is added.
|
|
225
|
+
- Zero deprecation warnings across the entire test suite (528 tests).
|
|
226
|
+
|
|
198
227
|
### v1.2.2 — Logo & Favicon Patch
|
|
199
228
|
|
|
200
229
|
High-quality logo assets and resolved CLI template favicon issues:
|
|
@@ -213,7 +242,7 @@ Logo and favicon assets are now properly included in the package distribution:
|
|
|
213
242
|
- Moved `logo.png` and `logo.jpg` from repository root to `fenrir/` package directory.
|
|
214
243
|
- Added `[tool.setuptools.package-data]` configuration in `pyproject.toml` to include image files: `fenrir = ["logo.png", "logo.jpg"]`.
|
|
215
244
|
- Updated `fenrir/cli.py` `cmd_new()` function to look for logos in the fenrir package directory first, with fallbacks for development mode.
|
|
216
|
-
- **Result**: All tests pass (
|
|
245
|
+
- **Result**: All tests pass (528 unit tests). `fenrir new` now works correctly in all environments.
|
|
217
246
|
|
|
218
247
|
### v1.1.1 — Python 3.8–3.10 Full Compatibility Patch
|
|
219
248
|
|
|
@@ -245,7 +274,7 @@ Five test failures on Python 3.8 CI were identified and patched:
|
|
|
245
274
|
|
|
246
275
|
### v0.1.0 — Initial Release
|
|
247
276
|
- Core ASGI framework with Flask, FastAPI, Sanic, Falcon, and Bottle hybridization.
|
|
248
|
-
-
|
|
277
|
+
- 528 automated unit tests.
|
|
249
278
|
- Premium CLI tooling (`run`, `routes`, `shell`, `bench`, `new`, `info`).
|
|
250
279
|
- Auto-generated OpenAPI/Swagger documentation.
|
|
251
280
|
- WebSocket and Server-Sent Events support.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://pypi.org/project/fenrir-framework/)
|
|
8
8
|
[](https://opensource.org/licenses/MIT)
|
|
9
9
|
[](https://www.python.org/)
|
|
10
|
-
[](https://github.com/IshikawaUta/fenrir/actions)
|
|
11
11
|
[](https://github.com/IshikawaUta/fenrir/actions/workflows/test.yml)
|
|
12
12
|
[]()
|
|
13
13
|
|
|
@@ -68,7 +68,7 @@ logging.basicConfig(level=logging.INFO)
|
|
|
68
68
|
logger = logging.getLogger("demo")
|
|
69
69
|
|
|
70
70
|
# Initialize the Hybrid App
|
|
71
|
-
app = Fenrir(title="Fenrir Hybrid Demo", version="
|
|
71
|
+
app = Fenrir(title="Fenrir Hybrid Demo", version="2.2.2")
|
|
72
72
|
|
|
73
73
|
# --- 1. FastAPI-Style Validation & DI ---
|
|
74
74
|
class UserRegister(BaseModel):
|
|
@@ -163,7 +163,7 @@ fenrir info demo_app:app
|
|
|
163
163
|
|
|
164
164
|
## 🧪 Comprehensive Test Suite
|
|
165
165
|
|
|
166
|
-
Fenrir is thoroughly covered by an automated test suite comprising **
|
|
166
|
+
Fenrir is thoroughly covered by an automated test suite comprising **528 tests** validating every single component, compat namespace, file upload, routing detail, and CLI functionality. The suite runs automatically via **GitHub Actions** on every push across Python **3.8 – 3.13**.
|
|
167
167
|
|
|
168
168
|
Run the test suite locally:
|
|
169
169
|
|
|
@@ -173,13 +173,40 @@ PYTHONPATH=. pytest -v
|
|
|
173
173
|
|
|
174
174
|
### Output:
|
|
175
175
|
```text
|
|
176
|
-
|
|
176
|
+
======================= 528 passed, 1 skipped in 3.4s ========================
|
|
177
177
|
```
|
|
178
178
|
|
|
179
179
|
---
|
|
180
180
|
|
|
181
181
|
## 🔄 Changelog
|
|
182
182
|
|
|
183
|
+
### v2.2.2 — Major Feature Update
|
|
184
|
+
|
|
185
|
+
New middleware, session backends, pagination, and more:
|
|
186
|
+
|
|
187
|
+
**New Middleware (`fenrir.middleware`)**
|
|
188
|
+
- **CORSMiddleware**: Full CORS support for HTTP and WebSocket with configurable origins, methods, headers, credentials, and max-age.
|
|
189
|
+
- **GZipMiddleware**: Automatic gzip compression for responses above a configurable size threshold.
|
|
190
|
+
- **RequestIDMiddleware**: Auto-generates unique request IDs or forwards client-provided IDs via configurable header.
|
|
191
|
+
- **RateLimitMiddleware**: Sliding-window rate limiter per client IP with configurable limits and block status code.
|
|
192
|
+
|
|
193
|
+
**New Session Backends (`fenrir.sessions`)**
|
|
194
|
+
- **InMemorySessionInterface**: In-memory session storage with TTL expiration, suitable for single-process apps and testing.
|
|
195
|
+
- **RedisSessionInterface**: Redis-backed session storage with support for both sync (`fakeredis`) and async (`redis.asyncio`) clients. Install with `pip install fenrir-framework[redis]`.
|
|
196
|
+
|
|
197
|
+
**New Pagination Utilities (`fenrir.pagination`)**
|
|
198
|
+
- **PaginationParams**: Pydantic model for query parameters (`page`, `page_size`, `sort_by`, `sort_order`).
|
|
199
|
+
- **paginate()**: Utility to paginate SQLAlchemy-style query results with metadata.
|
|
200
|
+
- **paginate_dict()**: Utility to paginate lists of dictionaries.
|
|
201
|
+
|
|
202
|
+
**New Features**
|
|
203
|
+
- **WebSocket per-route timeout**: `@app.websocket("/ws", timeout=5.0)` raises `WebSocketTimeout` if no message received within the timeout.
|
|
204
|
+
- **Multiple response models per status**: `response_models={200: SuccessModel, 404: ErrorModel}` applies different models based on the actual response status code.
|
|
205
|
+
|
|
206
|
+
**Improvements**
|
|
207
|
+
- ASGI middleware stack is now built once and cached, with automatic invalidation when new middleware is added.
|
|
208
|
+
- Zero deprecation warnings across the entire test suite (528 tests).
|
|
209
|
+
|
|
183
210
|
### v1.2.2 — Logo & Favicon Patch
|
|
184
211
|
|
|
185
212
|
High-quality logo assets and resolved CLI template favicon issues:
|
|
@@ -198,7 +225,7 @@ Logo and favicon assets are now properly included in the package distribution:
|
|
|
198
225
|
- Moved `logo.png` and `logo.jpg` from repository root to `fenrir/` package directory.
|
|
199
226
|
- Added `[tool.setuptools.package-data]` configuration in `pyproject.toml` to include image files: `fenrir = ["logo.png", "logo.jpg"]`.
|
|
200
227
|
- Updated `fenrir/cli.py` `cmd_new()` function to look for logos in the fenrir package directory first, with fallbacks for development mode.
|
|
201
|
-
- **Result**: All tests pass (
|
|
228
|
+
- **Result**: All tests pass (528 unit tests). `fenrir new` now works correctly in all environments.
|
|
202
229
|
|
|
203
230
|
### v1.1.1 — Python 3.8–3.10 Full Compatibility Patch
|
|
204
231
|
|
|
@@ -230,7 +257,7 @@ Five test failures on Python 3.8 CI were identified and patched:
|
|
|
230
257
|
|
|
231
258
|
### v0.1.0 — Initial Release
|
|
232
259
|
- Core ASGI framework with Flask, FastAPI, Sanic, Falcon, and Bottle hybridization.
|
|
233
|
-
-
|
|
260
|
+
- 528 automated unit tests.
|
|
234
261
|
- Premium CLI tooling (`run`, `routes`, `shell`, `bench`, `new`, `info`).
|
|
235
262
|
- Auto-generated OpenAPI/Swagger documentation.
|
|
236
263
|
- WebSocket and Server-Sent Events support.
|
|
@@ -2,7 +2,7 @@ from fenrir.app import Fenrir, Blueprint
|
|
|
2
2
|
from fenrir.context import request, g, current_app, session
|
|
3
3
|
from fenrir.dependencies import Depends, Query, Header, Cookie, Body, Path, Form, File
|
|
4
4
|
from fenrir.upload import UploadFile
|
|
5
|
-
from fenrir.websocket import WebSocket, WebSocketDisconnect
|
|
5
|
+
from fenrir.websocket import WebSocket, WebSocketDisconnect, WebSocketTimeout
|
|
6
6
|
from fenrir.exceptions import (
|
|
7
7
|
HTTPException,
|
|
8
8
|
HTTPBadRequest,
|
|
@@ -47,14 +47,25 @@ import fenrir.bottle as bottle
|
|
|
47
47
|
import fenrir.falcon as falcon
|
|
48
48
|
import fenrir.sanic as sanic
|
|
49
49
|
from fenrir.testing import TestClient, FenrirTestClient
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
from fenrir.middleware import (
|
|
51
|
+
CORSMiddleware,
|
|
52
|
+
GZipMiddleware,
|
|
53
|
+
RequestIDMiddleware,
|
|
54
|
+
RateLimitMiddleware,
|
|
55
|
+
)
|
|
56
|
+
from fenrir.pagination import PaginationParams, paginate, paginate_dict
|
|
57
|
+
from fenrir.sessions import (
|
|
58
|
+
RedisSessionInterface,
|
|
59
|
+
InMemorySessionInterface,
|
|
60
|
+
InMemorySessionBackend,
|
|
61
|
+
ServerSideSession,
|
|
62
|
+
)
|
|
52
63
|
|
|
53
64
|
|
|
54
65
|
# Re-export Annotated for convenient use with param markers
|
|
55
66
|
from fenrir.compat import Annotated
|
|
56
67
|
|
|
57
|
-
__version__ = "
|
|
68
|
+
__version__ = "2.2.2"
|
|
58
69
|
__all__ = [
|
|
59
70
|
# Core app
|
|
60
71
|
"Fenrir",
|
|
@@ -79,6 +90,7 @@ __all__ = [
|
|
|
79
90
|
# WebSocket
|
|
80
91
|
"WebSocket",
|
|
81
92
|
"WebSocketDisconnect",
|
|
93
|
+
"WebSocketTimeout",
|
|
82
94
|
# Exceptions
|
|
83
95
|
"HTTPException",
|
|
84
96
|
"HTTPBadRequest",
|
|
@@ -144,5 +156,19 @@ __all__ = [
|
|
|
144
156
|
# Testing
|
|
145
157
|
"TestClient",
|
|
146
158
|
"FenrirTestClient",
|
|
159
|
+
# Middleware
|
|
160
|
+
"CORSMiddleware",
|
|
161
|
+
"GZipMiddleware",
|
|
162
|
+
"RequestIDMiddleware",
|
|
163
|
+
"RateLimitMiddleware",
|
|
164
|
+
# Pagination
|
|
165
|
+
"PaginationParams",
|
|
166
|
+
"paginate",
|
|
167
|
+
"paginate_dict",
|
|
168
|
+
# Server-side sessions
|
|
169
|
+
"RedisSessionInterface",
|
|
170
|
+
"InMemorySessionInterface",
|
|
171
|
+
"InMemorySessionBackend",
|
|
172
|
+
"ServerSideSession",
|
|
147
173
|
]
|
|
148
174
|
|
|
@@ -92,7 +92,7 @@ class Fenrir:
|
|
|
92
92
|
self,
|
|
93
93
|
import_name: str = None,
|
|
94
94
|
title: str = "Fenrir API",
|
|
95
|
-
version: str = "
|
|
95
|
+
version: str = "2.2.2",
|
|
96
96
|
template_folder: str = "templates",
|
|
97
97
|
renderer: Any = None,
|
|
98
98
|
docs_url: str = "/docs",
|
|
@@ -168,6 +168,7 @@ class Fenrir:
|
|
|
168
168
|
self.router = Router(route_class=route_class)
|
|
169
169
|
self.middlewares: Dict[str, List[Callable]] = {"request": [], "response": []}
|
|
170
170
|
self._asgi_middlewares: List = [] # ASGI-style middleware stack
|
|
171
|
+
self._asgi_app: Any = None # Built middleware app (cached)
|
|
171
172
|
self._wsgi_mounts: List = [] # (prefix, WsgiToAsgi) pairs
|
|
172
173
|
self.listeners: Dict[str, List[Callable]] = {
|
|
173
174
|
"before_server_start": [],
|
|
@@ -204,9 +205,9 @@ class Fenrir:
|
|
|
204
205
|
return handler
|
|
205
206
|
return decorator
|
|
206
207
|
|
|
207
|
-
def websocket(self, path: str):
|
|
208
|
+
def websocket(self, path: str, timeout: float = None):
|
|
208
209
|
def decorator(handler):
|
|
209
|
-
self.add_websocket_route(path, handler)
|
|
210
|
+
self.add_websocket_route(path, handler, ws_timeout=timeout)
|
|
210
211
|
return handler
|
|
211
212
|
return decorator
|
|
212
213
|
|
|
@@ -228,8 +229,8 @@ class Fenrir:
|
|
|
228
229
|
def add_route(self, path: str, handler: Any, methods: List[str] = None, **route_kwargs):
|
|
229
230
|
self.router.add_route(path, handler, methods, **route_kwargs)
|
|
230
231
|
|
|
231
|
-
def add_websocket_route(self, path: str, handler: Any):
|
|
232
|
-
self.router.add_websocket_route(path, handler)
|
|
232
|
+
def add_websocket_route(self, path: str, handler: Any, ws_timeout: float = None):
|
|
233
|
+
self.router.add_websocket_route(path, handler, ws_timeout=ws_timeout)
|
|
233
234
|
|
|
234
235
|
def include_router(self, router: Router, prefix: str = ""):
|
|
235
236
|
self.router.include_router(router, prefix=prefix)
|
|
@@ -241,6 +242,7 @@ class Fenrir:
|
|
|
241
242
|
as ``await mw(scope, receive, send)``.
|
|
242
243
|
"""
|
|
243
244
|
self._asgi_middlewares.append((middleware_class, options))
|
|
245
|
+
self._asgi_app = None # invalidate cached middleware stack
|
|
244
246
|
|
|
245
247
|
def mount_wsgi(self, path: str, wsgi_app: Any) -> None:
|
|
246
248
|
"""Mount a WSGI application (e.g. a Bottle app) under *path*.
|
|
@@ -398,12 +400,15 @@ class Fenrir:
|
|
|
398
400
|
global _active_app
|
|
399
401
|
_active_app = self
|
|
400
402
|
|
|
401
|
-
#
|
|
402
|
-
if self._asgi_middlewares:
|
|
403
|
+
# Build ASGI middleware stack once and cache it
|
|
404
|
+
if self._asgi_middlewares and not self._asgi_app:
|
|
403
405
|
app = self._dispatch
|
|
404
406
|
for mw_class, mw_options in reversed(self._asgi_middlewares):
|
|
405
407
|
app = mw_class(app, **mw_options)
|
|
406
|
-
|
|
408
|
+
self._asgi_app = app
|
|
409
|
+
|
|
410
|
+
if self._asgi_app:
|
|
411
|
+
await self._asgi_app(scope, receive, send)
|
|
407
412
|
return
|
|
408
413
|
|
|
409
414
|
await self._dispatch(scope, receive, send)
|
|
@@ -450,7 +455,8 @@ class Fenrir:
|
|
|
450
455
|
await send({"type": "websocket.close", "code": 1008})
|
|
451
456
|
return
|
|
452
457
|
|
|
453
|
-
|
|
458
|
+
ws_timeout = getattr(route, "ws_timeout", None)
|
|
459
|
+
ws = WebSocket(scope, receive, send, timeout=ws_timeout)
|
|
454
460
|
resp = Response(status=200)
|
|
455
461
|
token_req = _request_ctx_var.set(req)
|
|
456
462
|
token_g = _g_ctx_var.set(G())
|
|
@@ -569,6 +575,9 @@ class Fenrir:
|
|
|
569
575
|
elif hasattr(route, "response_model") and route.response_model is not None:
|
|
570
576
|
coerced = self._apply_response_model(route, response_obj)
|
|
571
577
|
response_obj = coerced
|
|
578
|
+
elif hasattr(route, "response_models") and route.response_models:
|
|
579
|
+
coerced = self._apply_response_model(route, response_obj)
|
|
580
|
+
response_obj = coerced
|
|
572
581
|
|
|
573
582
|
# Convert handler result to Response object (only if not already a Response)
|
|
574
583
|
if not isinstance(response_obj, Response):
|
|
@@ -678,11 +687,37 @@ class Fenrir:
|
|
|
678
687
|
return await to_thread(func, *args, **kwargs)
|
|
679
688
|
|
|
680
689
|
def _apply_response_model(self, route: Route, content: Any) -> Response:
|
|
681
|
-
"""Serialise *content* through the route's response_model if defined.
|
|
690
|
+
"""Serialise *content* through the route's response_model if defined.
|
|
691
|
+
|
|
692
|
+
Also supports ``response_models`` dict: ``{status_code: model}`` for
|
|
693
|
+
multiple response models per status code (runtime validation).
|
|
694
|
+
"""
|
|
695
|
+
# Determine actual status code from content tuple
|
|
696
|
+
actual_status = getattr(route, "status_code", 200)
|
|
697
|
+
actual_content = content
|
|
698
|
+
if isinstance(content, tuple):
|
|
699
|
+
if len(content) == 2:
|
|
700
|
+
actual_content, actual_status = content
|
|
701
|
+
elif len(content) == 3:
|
|
702
|
+
actual_content, actual_status, _ = content
|
|
703
|
+
|
|
704
|
+
# Check response_models dict first (multiple models per status)
|
|
705
|
+
response_models = getattr(route, "response_models", {})
|
|
706
|
+
if response_models and actual_status in response_models:
|
|
707
|
+
rm = response_models[actual_status]
|
|
708
|
+
return self._serialize_with_model(rm, actual_content, actual_status, route)
|
|
709
|
+
|
|
710
|
+
# Fall back to the single response_model
|
|
682
711
|
rm = getattr(route, "response_model", None)
|
|
683
712
|
if rm is None:
|
|
713
|
+
if isinstance(content, tuple):
|
|
714
|
+
return self._coerce_response(content)
|
|
684
715
|
return self._coerce_response(content)
|
|
685
716
|
|
|
717
|
+
return self._serialize_with_model(rm, actual_content, actual_status, route)
|
|
718
|
+
|
|
719
|
+
def _serialize_with_model(self, rm: Any, content: Any, status: int, route: Route) -> Response:
|
|
720
|
+
"""Serialize *content* through a Pydantic model."""
|
|
686
721
|
try:
|
|
687
722
|
from pydantic import BaseModel, TypeAdapter
|
|
688
723
|
|
|
@@ -690,13 +725,9 @@ class Fenrir:
|
|
|
690
725
|
exclude_defaults = getattr(route, "response_model_exclude_defaults", False)
|
|
691
726
|
include = getattr(route, "response_model_include", None)
|
|
692
727
|
exclude = getattr(route, "response_model_exclude", None)
|
|
693
|
-
status = getattr(route, "status_code", 200)
|
|
694
728
|
|
|
695
729
|
if isinstance(rm, type) and issubclass(rm, BaseModel):
|
|
696
|
-
# Always go through response_model.model_validate so extra fields
|
|
697
|
-
# from a different Pydantic model are properly stripped.
|
|
698
730
|
if isinstance(content, BaseModel):
|
|
699
|
-
# Convert via dict to let response_model pick only its fields
|
|
700
731
|
raw_dict = content.model_dump()
|
|
701
732
|
validated = rm.model_validate(raw_dict)
|
|
702
733
|
elif isinstance(content, dict):
|
|
@@ -375,7 +375,7 @@ def cmd_new(args):
|
|
|
375
375
|
import os
|
|
376
376
|
import sys
|
|
377
377
|
|
|
378
|
-
app = Fenrir(title="My Fenrir Application", version="
|
|
378
|
+
app = Fenrir(title="My Fenrir Application", version="2.2.2")
|
|
379
379
|
|
|
380
380
|
@app.get("/")
|
|
381
381
|
async def home():
|
|
@@ -682,7 +682,7 @@ if __name__ == "__main__":
|
|
|
682
682
|
</div>
|
|
683
683
|
<div class="info-item">
|
|
684
684
|
<span class="info-label">Framework Engine</span>
|
|
685
|
-
<span class="info-value">Fenrir
|
|
685
|
+
<span class="info-value">Fenrir v2.2.2</span>
|
|
686
686
|
</div>
|
|
687
687
|
</div>
|
|
688
688
|
|