fusion-framework 1.2.0__tar.gz → 1.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.
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/Cargo.lock +4 -4
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/Cargo.toml +1 -1
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/PKG-INFO +1 -1
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/pyproject.toml +1 -1
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/python/fusion_framework/middleware.py +38 -13
- fusion_framework-1.2.2/python/fusion_framework/test_middleware.py +87 -0
- fusion_framework-1.2.0/python/fusion_framework/test_middleware.py +0 -30
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/README.md +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-core/Cargo.toml +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-core/src/api_context.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-core/src/coerce.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-core/src/dispatch.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-core/src/error.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-core/src/handler.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-core/src/headers.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-core/src/http_error.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-core/src/lib.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-core/src/main.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-core/src/naming.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-core/src/request.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-core/src/response.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-core/src/router.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-core/src/serialize.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-core/src/server.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-core/src/settings.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-core/src/status.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-py/Cargo.toml +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-py/README.md +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-py/src/api_types.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-py/src/json.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/crates/fusion-py/src/lib.rs +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/python/fusion_framework/__init__.py +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/python/fusion_framework/api.py +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/python/fusion_framework/app.py +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/python/fusion_framework/async_runtime.py +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/python/fusion_framework/config.py +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/python/fusion_framework/header.py +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/python/fusion_framework/http.py +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/python/fusion_framework/route.py +0 -0
- {fusion_framework-1.2.0 → fusion_framework-1.2.2}/python/fusion_framework/status.py +0 -0
|
@@ -124,7 +124,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
|
|
124
124
|
|
|
125
125
|
[[package]]
|
|
126
126
|
name = "fusion-core"
|
|
127
|
-
version = "1.2.
|
|
127
|
+
version = "1.2.2"
|
|
128
128
|
dependencies = [
|
|
129
129
|
"bytes",
|
|
130
130
|
"console",
|
|
@@ -138,7 +138,7 @@ dependencies = [
|
|
|
138
138
|
|
|
139
139
|
[[package]]
|
|
140
140
|
name = "fusion-ffi"
|
|
141
|
-
version = "1.2.
|
|
141
|
+
version = "1.2.2"
|
|
142
142
|
dependencies = [
|
|
143
143
|
"bytes",
|
|
144
144
|
"fusion-core",
|
|
@@ -148,7 +148,7 @@ dependencies = [
|
|
|
148
148
|
|
|
149
149
|
[[package]]
|
|
150
150
|
name = "fusion-node"
|
|
151
|
-
version = "1.2.
|
|
151
|
+
version = "1.2.2"
|
|
152
152
|
dependencies = [
|
|
153
153
|
"fusion-core",
|
|
154
154
|
"napi",
|
|
@@ -161,7 +161,7 @@ dependencies = [
|
|
|
161
161
|
|
|
162
162
|
[[package]]
|
|
163
163
|
name = "fusion-py"
|
|
164
|
-
version = "1.2.
|
|
164
|
+
version = "1.2.2"
|
|
165
165
|
dependencies = [
|
|
166
166
|
"bytes",
|
|
167
167
|
"fusion-core",
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import asyncio
|
|
6
5
|
import base64
|
|
7
6
|
import inspect
|
|
8
7
|
import json
|
|
@@ -35,7 +34,7 @@ def clear_active_global() -> None:
|
|
|
35
34
|
|
|
36
35
|
|
|
37
36
|
async def _maybe_await(value: MaybeAwaitable) -> Any:
|
|
38
|
-
if
|
|
37
|
+
if inspect.isawaitable(value):
|
|
39
38
|
return await value
|
|
40
39
|
return value
|
|
41
40
|
|
|
@@ -78,8 +77,11 @@ def _run_chain_sync(
|
|
|
78
77
|
return dispatch(index + 1, next_req)
|
|
79
78
|
|
|
80
79
|
result = middleware(req, call_next)
|
|
80
|
+
# Sync middleware may return a coroutine when the route handler is
|
|
81
|
+
# ``async def`` (HandlerInvoker is sync but yields an awaitable).
|
|
82
|
+
# Propagate to Rust so ``async_runtime.submit`` can await it.
|
|
81
83
|
if inspect.isawaitable(result):
|
|
82
|
-
|
|
84
|
+
return result
|
|
83
85
|
if _is_response(result):
|
|
84
86
|
return result
|
|
85
87
|
return result
|
|
@@ -203,6 +205,22 @@ def _merge_response_headers(result: Any, extra: dict[str, str]) -> Any:
|
|
|
203
205
|
return out
|
|
204
206
|
|
|
205
207
|
|
|
208
|
+
def _call_next_merge_headers(
|
|
209
|
+
call_next: Callable[[RequestDict], Any],
|
|
210
|
+
request: RequestDict,
|
|
211
|
+
extra: dict[str, str],
|
|
212
|
+
) -> Any:
|
|
213
|
+
"""Merge headers after ``call_next``, awaiting async handler results."""
|
|
214
|
+
result = call_next(request)
|
|
215
|
+
if inspect.isawaitable(result):
|
|
216
|
+
|
|
217
|
+
async def _await_merge() -> Any:
|
|
218
|
+
return _merge_response_headers(await result, extra)
|
|
219
|
+
|
|
220
|
+
return _await_merge()
|
|
221
|
+
return _merge_response_headers(result, extra)
|
|
222
|
+
|
|
223
|
+
|
|
206
224
|
def framework_headers() -> Middleware:
|
|
207
225
|
"""Default identity middleware: advertise Fusion on every response.
|
|
208
226
|
|
|
@@ -215,18 +233,25 @@ def framework_headers() -> Middleware:
|
|
|
215
233
|
|
|
216
234
|
extra = dict(_fp())
|
|
217
235
|
except Exception:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
hdr, "
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
236
|
+
try:
|
|
237
|
+
from fusion_framework import header as hdr
|
|
238
|
+
|
|
239
|
+
extra = {
|
|
240
|
+
getattr(hdr, "X_POWERED_BY", "X-Powered-By"): getattr(
|
|
241
|
+
hdr, "FRAMEWORK_POWERED_BY", "Fusion Framework"
|
|
242
|
+
),
|
|
243
|
+
getattr(hdr, "X_FRAMEWORK", "X-Framework"): getattr(hdr, "FRAMEWORK_ID", "Fusion"),
|
|
244
|
+
getattr(hdr, "X_FUSION_VERSION", "X-Fusion-Version"): "1.2.2",
|
|
245
|
+
}
|
|
246
|
+
except Exception:
|
|
247
|
+
extra = {
|
|
248
|
+
"X-Powered-By": "Fusion Framework",
|
|
249
|
+
"X-Framework": "Fusion",
|
|
250
|
+
"X-Fusion-Version": "1.2.2",
|
|
251
|
+
}
|
|
227
252
|
|
|
228
253
|
def middleware(request: RequestDict, call_next: Callable[[RequestDict], Any]) -> Any:
|
|
229
|
-
return
|
|
254
|
+
return _call_next_merge_headers(call_next, request, extra)
|
|
230
255
|
|
|
231
256
|
return middleware
|
|
232
257
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Unit tests for middleware chain (no server required)."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
|
|
6
|
+
from fusion_framework.middleware import (
|
|
7
|
+
bearer_jwt,
|
|
8
|
+
clear_active_global,
|
|
9
|
+
dispatch_route,
|
|
10
|
+
framework_headers,
|
|
11
|
+
require_roles,
|
|
12
|
+
set_active_global,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _handler(request):
|
|
17
|
+
return {"status": 200, "body": {"state": request.get("state", {})}}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def _async_handler(request):
|
|
21
|
+
return {"status": 200, "body": {"ok": True, "path": request.get("path")}}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _sync_invoker_like(request):
|
|
25
|
+
"""Mirrors PyO3 HandlerInvoker: sync ``__call__`` that may return a coroutine."""
|
|
26
|
+
return _async_handler(request)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_require_roles_allows_matching_role():
|
|
30
|
+
request = {"headers": {}, "state": {"jwt": {"roles": ["admin"]}}}
|
|
31
|
+
chain = [require_roles("admin", "super_admin")]
|
|
32
|
+
result = dispatch_route(request, _handler, chain)
|
|
33
|
+
assert result["status"] == 200
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_require_roles_blocks_missing_role():
|
|
37
|
+
request = {"headers": {}, "state": {"jwt": {"roles": ["user"]}}}
|
|
38
|
+
chain = [require_roles("admin")]
|
|
39
|
+
result = dispatch_route(request, _handler, chain)
|
|
40
|
+
assert result["status"] == 403
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_bearer_jwt_populates_state():
|
|
44
|
+
token = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxIiwicm9sZXMiOlsiYWRtaW4iXX0."
|
|
45
|
+
request = {"headers": {"Authorization": f"Bearer {token}"}}
|
|
46
|
+
set_active_global([bearer_jwt()])
|
|
47
|
+
try:
|
|
48
|
+
result = dispatch_route(request, _handler, [])
|
|
49
|
+
assert result["status"] == 200
|
|
50
|
+
assert result["body"]["state"]["jwt"]["sub"] == "1"
|
|
51
|
+
finally:
|
|
52
|
+
clear_active_global()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_framework_headers_awaits_async_handler():
|
|
56
|
+
"""Regression: sync framework_headers must not stringify coroutine bodies.
|
|
57
|
+
|
|
58
|
+
HandlerInvoker is sync, so the chain stays sync; call_next returns a raw
|
|
59
|
+
coroutine that must be awaited before headers are merged.
|
|
60
|
+
"""
|
|
61
|
+
clear_active_global()
|
|
62
|
+
set_active_global([framework_headers()])
|
|
63
|
+
try:
|
|
64
|
+
request = {"path": "/membership", "headers": {}, "method": "GET"}
|
|
65
|
+
result = dispatch_route(request, _sync_invoker_like, [])
|
|
66
|
+
assert inspect.isawaitable(result), "async handler result must stay awaitable for Rust"
|
|
67
|
+
resolved = asyncio.run(result)
|
|
68
|
+
assert resolved["status"] == 200
|
|
69
|
+
assert resolved["body"] == {"ok": True, "path": "/membership"}
|
|
70
|
+
headers = {str(k).lower(): v for k, v in (resolved.get("headers") or {}).items()}
|
|
71
|
+
assert "x-powered-by" in headers
|
|
72
|
+
body = resolved.get("body")
|
|
73
|
+
assert not (isinstance(body, str) and body.startswith("<coroutine"))
|
|
74
|
+
finally:
|
|
75
|
+
clear_active_global()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_sync_middleware_propagates_async_handler_coroutine():
|
|
79
|
+
clear_active_global()
|
|
80
|
+
try:
|
|
81
|
+
request = {"path": "/x", "headers": {}, "state": {"jwt": {"roles": ["admin"]}}}
|
|
82
|
+
result = dispatch_route(request, _sync_invoker_like, [require_roles("admin")])
|
|
83
|
+
assert inspect.isawaitable(result)
|
|
84
|
+
resolved = asyncio.run(result)
|
|
85
|
+
assert resolved["body"]["ok"] is True
|
|
86
|
+
finally:
|
|
87
|
+
clear_active_global()
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
"""Unit tests for middleware chain (no server required)."""
|
|
2
|
-
|
|
3
|
-
from fusion_framework.middleware import bearer_jwt, dispatch_route, require_roles, set_active_global
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def _handler(request):
|
|
7
|
-
return {"status": 200, "body": {"state": request.get("state", {})}}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def test_require_roles_allows_matching_role():
|
|
11
|
-
request = {"headers": {}, "state": {"jwt": {"roles": ["admin"]}}}
|
|
12
|
-
chain = [require_roles("admin", "super_admin")]
|
|
13
|
-
result = dispatch_route(request, _handler, chain)
|
|
14
|
-
assert result["status"] == 200
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def test_require_roles_blocks_missing_role():
|
|
18
|
-
request = {"headers": {}, "state": {"jwt": {"roles": ["user"]}}}
|
|
19
|
-
chain = [require_roles("admin")]
|
|
20
|
-
result = dispatch_route(request, _handler, chain)
|
|
21
|
-
assert result["status"] == 403
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def test_bearer_jwt_populates_state():
|
|
25
|
-
token = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxIiwicm9sZXMiOlsiYWRtaW4iXX0."
|
|
26
|
-
request = {"headers": {"Authorization": f"Bearer {token}"}}
|
|
27
|
-
set_active_global([bearer_jwt()])
|
|
28
|
-
result = dispatch_route(request, _handler, [])
|
|
29
|
-
assert result["status"] == 200
|
|
30
|
-
assert result["body"]["state"]["jwt"]["sub"] == "1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|