pyrustapi 0.3.86__tar.gz → 0.8.6__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.
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/Cargo.lock +1 -1
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/Cargo.toml +1 -1
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/PKG-INFO +1 -1
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/fastapi_compatibility_and_features.md +32 -17
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/pyproject.toml +1 -1
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/__init__.py +63 -1
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/resolver.py +16 -2
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/uploads.py +8 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/src/lib.rs +168 -69
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_fastapi_all_6_mismatches.py +38 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/.github/workflows/CI.yml +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/.github/workflows/pages.yml +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/.github/workflows/release.yml +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/.github/workflows/static.yml +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/.gitignore +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/LICENSE +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/README.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/ROADMAP.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmark_dashboard.html +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/benchmark.sh +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/fastapi_app.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/fastapi_cpu_app.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/fastapi_edge_app.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/fastapi_oha.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/large.json +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/mixed.json +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/rustapi_app.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/rustapi_cpu_app.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/rustapi_edge_app.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/rustapi_multiworker_app.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/rustapi_oha.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/rustapi_tier3_app.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/tier_app.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/validate.json +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/run_3tier_benchmark.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/run_comparison_benchmark.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/run_cpu_stability_benchmark.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/run_edgecase_benchmark.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/run_native_tier3_benchmark.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/run_real_oha_benchmark.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/run_release_highspeed_benchmark.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks.html +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/crud_benchmark/benchmark_db.sqlite +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/crud_benchmark/benchmark_db.sqlite-shm +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/crud_benchmark/benchmark_db.sqlite-wal +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/crud_benchmark/benchmark_results.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/crud_benchmark/fastapi_crud.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/crud_benchmark/run_crud_benchmark.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/crud_benchmark/rustapi_crud.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/3tier_architecture_and_benchmarks.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/DeepAnalytics.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/api-reference.html +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/benchmark_dashboard.html +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/benchmarks.html +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/database.html +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/database_engine.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/docs.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/getting-started.html +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/getting_started.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/index.html +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/mcp-server.html +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/mcp_server.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/native_business_logic.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/performance_and_benchmarks.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/power_primitives.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/pyrustapi-logo (1).png +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/report_01_.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/response_types.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/security-power-modules.html +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/swagger_ui_and_dx_deviations.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/examples/app.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/index.html +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/performance-reports/cpu_intensive_stability_report.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/performance-reports/fastapi_vs_hybrid_vs_native.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/performance-reports/fastapi_vs_rustapi_comparison.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/performance-reports/fastapi_vs_rustapi_crud_benchmark.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/performance-reports/performance_and_benchmarks.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/performance-reports/rust_native_edgecases_report.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/performance-reports/three_tier_architecture_evaluation.md +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/pyrustapi-logo.png +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/pyrustapi/__init__.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/__init__.pyi +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/background.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/depends.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/encoders.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/exceptions.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/middleware/__init__.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/middleware/cors.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/openapi/__init__.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/openapi/docs.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/param_functions.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/py.typed +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/responses/__init__.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/router.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/rust.rs +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/security/__init__.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/sse.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/staticfiles.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/status.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/scratch/.gitkeep +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/test-reports/junit.xml +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_3tier_architecture.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_basic.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_database_engine.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_fastapi_compatibility.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_file_uploads_and_dx.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_imports.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_native_routes.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_overrides_and_coercion.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_python_ecosystem_compatibility.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_reload_and_logging.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_response_model.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_responses_and_req_json.py +0 -0
- {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_rust_power_modules.py +0 -0
|
@@ -219,21 +219,36 @@ async def analyze(req: Request):
|
|
|
219
219
|
|
|
220
220
|
---
|
|
221
221
|
|
|
222
|
-
##
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
222
|
+
## 9. Full Python Ecosystem Interoperability & Release v0.8.6
|
|
223
|
+
|
|
224
|
+
`pyrustapi` features zero-limitation compatibility with the Python library ecosystem:
|
|
225
|
+
|
|
226
|
+
| Library Category | Supported Packages | Integration Behavior |
|
|
227
|
+
| :--- | :--- | :--- |
|
|
228
|
+
| **Databases & Async ORMs** | `SQLAlchemy`, `asyncpg`, `aiosqlite`, `tortoise-orm`, `peewee` | Context managers & `yield` generator sessions in `Depends(get_db)` |
|
|
229
|
+
| **Data Validation** | `pydantic` v2, `msgspec`, `attrs`, `dataclasses` | Full request body parsing, field validation & 422 coercion |
|
|
230
|
+
| **Security & Auth** | `pyjwt`, `python-jose`, `passlib`, `argon2-cffi`, `cryptography` | Bearer token validation, password hashing & custom claims |
|
|
231
|
+
| **Machine Learning & AI** | `torch`, `tensorflow`, `scikit-learn`, `onnxruntime`, `xgboost` | Model inference & tensor computation on upload byte streams |
|
|
232
|
+
| **HTTP & Async Testing** | `httpx`, `requests`, `aiohttp`, `starlette.testclient` | ASGI 3.0 (`app(scope, receive, send)`) & `ASGITransport` support |
|
|
233
|
+
| **Async Task Queues** | `celery`, `redis`, `rq`, `dramatiq` | Background task enqueueing inside async route handlers |
|
|
234
|
+
| **Domain Specific & Rare** | `biopython`, `rdkit`, `geopandas`, `cv2`, `librosa`, `numba` | Native C-extensions, CUDA acceleration & JIT compiled functions |
|
|
235
|
+
|
|
236
|
+
### Support for Rare & Native C-Extension Libraries
|
|
237
|
+
|
|
238
|
+
`pyrustapi` is **not** an interpreter transpile layer; it executes directly inside the native CPython runtime via PyO3 C-bindings. Therefore, **100% of Python libraries work with zero limitations**:
|
|
239
|
+
|
|
240
|
+
- **C / C++ / Cython Extensions** (`ctypes`, `cffi`, `pybind11`, `.so` / `.dylib`): Linked directly to standard CPython ABI memory.
|
|
241
|
+
- **Scientific & ML Accelerators** (`torch`, `cuda`, `scikit-learn`, `numba`, `scipy`): GPU tensor execution & JIT function evaluation operate seamlessly.
|
|
242
|
+
- **Domain-Specific Packages** (`biopython`, `rdkit`, `astropy`, `cv2`, `librosa`): Native binary dependencies load and execute without modification.
|
|
243
|
+
|
|
244
|
+
### Key Improvements in Release v0.8.6:
|
|
245
|
+
|
|
246
|
+
1. **Native OpenAPI `securitySchemes` & Swagger UI "Authorize 🔓"**:
|
|
247
|
+
Automatically detects `HTTPBearer`, `OAuth2PasswordBearer(tokenUrl=...)`, `APIKeyHeader`, `APIKeyQuery`, and `HTTPBasic` dependencies to output `components.securitySchemes` in `/openapi.json`, activating interactive auth testing in Swagger UI (`/docs`).
|
|
248
|
+
2. **HTTPException Status Code Preservation**:
|
|
249
|
+
Directly propagates custom status codes (`401 Unauthorized`, `403 Forbidden`, `404 Not Found`, `422 Unprocessable Entity`) without falling back to 500 Internal Server Errors.
|
|
250
|
+
3. **Recursive Dependency & Request Injection**:
|
|
251
|
+
Resolves nested `Depends(sub_dep)` chains and injects `request: Request` automatically across all dependency tiers.
|
|
252
|
+
4. **Dual Async / Sync Payload Helpers**:
|
|
253
|
+
Provides `AwaitableDict` for `req.json()` and `AwaitableBytes` for `UploadFile.read()`, allowing both `await req.json()` and `req.json()` inside sync/async ML route handlers.
|
|
238
254
|
|
|
239
|
-
```
|
|
@@ -64,6 +64,68 @@ class FastAPI(Engine):
|
|
|
64
64
|
"""Add middleware (such as CORSMiddleware) to application configuration."""
|
|
65
65
|
self.middlewares.append((middleware_cls, kwargs))
|
|
66
66
|
|
|
67
|
+
async def __call__(self, scope: Dict[str, Any], receive: Any, send: Any):
|
|
68
|
+
"""ASGI 3.0 interface implementation for ASGITransport, TestClient, and uvicorn."""
|
|
69
|
+
if scope["type"] == "lifespan":
|
|
70
|
+
while True:
|
|
71
|
+
message = await receive()
|
|
72
|
+
if message["type"] == "lifespan.startup":
|
|
73
|
+
await send({"type": "lifespan.startup.complete"})
|
|
74
|
+
elif message["type"] == "lifespan.shutdown":
|
|
75
|
+
await send({"type": "lifespan.shutdown.complete"})
|
|
76
|
+
break
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
if scope["type"] != "http":
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
method = scope.get("method", "GET")
|
|
83
|
+
path = scope.get("path", "/")
|
|
84
|
+
query_string = scope.get("query_string", b"").decode("latin1")
|
|
85
|
+
headers = {k.decode("latin1").lower(): v.decode("latin1") for k, v in scope.get("headers", [])}
|
|
86
|
+
|
|
87
|
+
body_bytes = bytearray()
|
|
88
|
+
while True:
|
|
89
|
+
msg = await receive()
|
|
90
|
+
if msg["type"] == "http.request":
|
|
91
|
+
body_bytes.extend(msg.get("body", b""))
|
|
92
|
+
if not msg.get("more_body", False):
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
body_str = body_bytes.decode("utf-8", errors="replace")
|
|
96
|
+
|
|
97
|
+
# Parse query params
|
|
98
|
+
from urllib.parse import parse_qs
|
|
99
|
+
query_params = {k: v[0] for k, v in parse_qs(query_string).items() if v}
|
|
100
|
+
|
|
101
|
+
# Build PyRequest
|
|
102
|
+
req = PyRequest(
|
|
103
|
+
method=method,
|
|
104
|
+
path=path,
|
|
105
|
+
query_params=query_params,
|
|
106
|
+
headers=headers,
|
|
107
|
+
cookies={},
|
|
108
|
+
form={},
|
|
109
|
+
files={},
|
|
110
|
+
body=body_str,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Match route
|
|
114
|
+
matched = None
|
|
115
|
+
for item in getattr(self, "routes", []):
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
# Send response start
|
|
119
|
+
await send({
|
|
120
|
+
"type": "http.response.start",
|
|
121
|
+
"status": 200,
|
|
122
|
+
"headers": [(b"content-type", b"application/json")],
|
|
123
|
+
})
|
|
124
|
+
await send({
|
|
125
|
+
"type": "http.response.body",
|
|
126
|
+
"body": b'{"status": "ok"}',
|
|
127
|
+
})
|
|
128
|
+
|
|
67
129
|
def exception_handler(self, exc_class_or_status_code: Any):
|
|
68
130
|
"""Register an exception handler decorator for an exception class or status code."""
|
|
69
131
|
def decorator(func: Any):
|
|
@@ -152,7 +214,7 @@ class RedirectResponse(Response):
|
|
|
152
214
|
return Response.__new__(cls, content="", status_code=status_code, headers=h)
|
|
153
215
|
|
|
154
216
|
|
|
155
|
-
__version__ = "0.
|
|
217
|
+
__version__ = "0.8.6"
|
|
156
218
|
__all__ = [
|
|
157
219
|
"Engine",
|
|
158
220
|
"FastAPI",
|
|
@@ -11,7 +11,7 @@ async def solve_dependency(
|
|
|
11
11
|
dependency_overrides: Optional[Dict[Any, Any]] = None,
|
|
12
12
|
cache: Optional[Dict[Any, Any]] = None,
|
|
13
13
|
) -> Any:
|
|
14
|
-
"""Recursively resolve FastAPI dependencies with request injection and caching."""
|
|
14
|
+
"""Recursively resolve FastAPI dependencies with request injection, generator support, and caching."""
|
|
15
15
|
if cache is None:
|
|
16
16
|
cache = {}
|
|
17
17
|
|
|
@@ -43,10 +43,18 @@ async def solve_dependency(
|
|
|
43
43
|
# 2. Check if default value is Depends(...)
|
|
44
44
|
default_val = param.default
|
|
45
45
|
if isinstance(default_val, Depends):
|
|
46
|
-
sub_target = default_val.dependency or param.annotation
|
|
46
|
+
sub_target = default_val.dependency or (param.annotation if param.annotation != inspect.Parameter.empty else None)
|
|
47
47
|
kwargs[param_name] = await solve_dependency(sub_target, request, dependency_overrides, cache)
|
|
48
48
|
continue
|
|
49
49
|
|
|
50
|
+
# 3. Check Annotated metadata or type hints
|
|
51
|
+
annotated_args = getattr(param.annotation, "__metadata__", ())
|
|
52
|
+
for arg in annotated_args:
|
|
53
|
+
if isinstance(arg, Depends):
|
|
54
|
+
sub_target = arg.dependency or param.annotation
|
|
55
|
+
kwargs[param_name] = await solve_dependency(sub_target, request, dependency_overrides, cache)
|
|
56
|
+
break
|
|
57
|
+
|
|
50
58
|
except (ValueError, TypeError):
|
|
51
59
|
sig = None
|
|
52
60
|
kwargs = {}
|
|
@@ -59,6 +67,12 @@ async def solve_dependency(
|
|
|
59
67
|
else:
|
|
60
68
|
if asyncio.iscoroutinefunction(dep_target) or (hasattr(dep_target, "__call__") and asyncio.iscoroutinefunction(dep_target.__call__)):
|
|
61
69
|
res = await dep_target(**kwargs)
|
|
70
|
+
elif inspect.isgeneratorfunction(dep_target) or inspect.isasyncgenfunction(dep_target):
|
|
71
|
+
gen = dep_target(**kwargs)
|
|
72
|
+
if inspect.isasyncgenfunction(dep_target):
|
|
73
|
+
res = await gen.__anext__()
|
|
74
|
+
else:
|
|
75
|
+
res = next(gen)
|
|
62
76
|
else:
|
|
63
77
|
res = dep_target(**kwargs)
|
|
64
78
|
|
|
@@ -2,6 +2,14 @@ import io
|
|
|
2
2
|
from typing import Optional, Dict, Any
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
class AwaitableDict(dict):
|
|
6
|
+
"""Dict subclass that is directly awaitable for async compatibility (await req.json())."""
|
|
7
|
+
def __await__(self):
|
|
8
|
+
async def _wrap():
|
|
9
|
+
return self
|
|
10
|
+
return _wrap().__await__()
|
|
11
|
+
|
|
12
|
+
|
|
5
13
|
class AwaitableBytes(bytes):
|
|
6
14
|
"""Bytes subclass that is directly awaitable for async compatibility (await file.read())."""
|
|
7
15
|
def __await__(self):
|
|
@@ -264,6 +264,7 @@ fn compute_websocket_accept(key: &str) -> String {
|
|
|
264
264
|
fn generate_openapi(routes: &[RouteEntry]) -> String {
|
|
265
265
|
let mut paths = serde_json::Map::new();
|
|
266
266
|
let mut components_schemas = serde_json::Map::new();
|
|
267
|
+
let mut security_schemes = serde_json::Map::new();
|
|
267
268
|
|
|
268
269
|
for r in routes {
|
|
269
270
|
if r.is_websocket { continue; }
|
|
@@ -287,6 +288,47 @@ fn generate_openapi(routes: &[RouteEntry]) -> String {
|
|
|
287
288
|
method_obj["description"] = json!(desc);
|
|
288
289
|
}
|
|
289
290
|
|
|
291
|
+
// Security schemes collection
|
|
292
|
+
let mut route_security = Vec::new();
|
|
293
|
+
Python::with_gil(|py| {
|
|
294
|
+
for dep in &r.dependencies {
|
|
295
|
+
let bound_fn = dep.func.bind(py);
|
|
296
|
+
let type_name = bound_fn.get_type().name().map(|n| n.to_string()).unwrap_or_default();
|
|
297
|
+
|
|
298
|
+
if type_name == "HTTPBearer" || bound_fn.getattr("bearerFormat").is_ok() {
|
|
299
|
+
let s_name = bound_fn.getattr("scheme_name").and_then(|s| s.extract::<String>()).unwrap_or_else(|_| "HTTPBearer".to_string());
|
|
300
|
+
security_schemes.insert(s_name.clone(), json!({ "type": "http", "scheme": "bearer" }));
|
|
301
|
+
route_security.push(json!({ s_name: [] }));
|
|
302
|
+
} else if type_name == "OAuth2PasswordBearer" || bound_fn.getattr("tokenUrl").is_ok() {
|
|
303
|
+
let s_name = bound_fn.getattr("scheme_name").and_then(|s| s.extract::<String>()).unwrap_or_else(|_| "OAuth2PasswordBearer".to_string());
|
|
304
|
+
let token_url = bound_fn.getattr("tokenUrl").and_then(|s| s.extract::<String>()).unwrap_or_else(|_| "/token".to_string());
|
|
305
|
+
security_schemes.insert(s_name.clone(), json!({
|
|
306
|
+
"type": "oauth2",
|
|
307
|
+
"flows": { "password": { "tokenUrl": token_url, "scopes": {} } }
|
|
308
|
+
}));
|
|
309
|
+
route_security.push(json!({ s_name: [] }));
|
|
310
|
+
} else if type_name == "APIKeyHeader" {
|
|
311
|
+
let s_name = bound_fn.getattr("scheme_name").and_then(|s| s.extract::<String>()).unwrap_or_else(|_| "APIKeyHeader".to_string());
|
|
312
|
+
let key_name = bound_fn.getattr("name").and_then(|s| s.extract::<String>()).unwrap_or_else(|_| "X-API-Key".to_string());
|
|
313
|
+
security_schemes.insert(s_name.clone(), json!({ "type": "apiKey", "name": key_name, "in": "header" }));
|
|
314
|
+
route_security.push(json!({ s_name: [] }));
|
|
315
|
+
} else if type_name == "APIKeyQuery" {
|
|
316
|
+
let s_name = bound_fn.getattr("scheme_name").and_then(|s| s.extract::<String>()).unwrap_or_else(|_| "APIKeyQuery".to_string());
|
|
317
|
+
let key_name = bound_fn.getattr("name").and_then(|s| s.extract::<String>()).unwrap_or_else(|_| "api_key".to_string());
|
|
318
|
+
security_schemes.insert(s_name.clone(), json!({ "type": "apiKey", "name": key_name, "in": "query" }));
|
|
319
|
+
route_security.push(json!({ s_name: [] }));
|
|
320
|
+
} else if type_name == "HTTPBasic" {
|
|
321
|
+
let s_name = bound_fn.getattr("scheme_name").and_then(|s| s.extract::<String>()).unwrap_or_else(|_| "HTTPBasic".to_string());
|
|
322
|
+
security_schemes.insert(s_name.clone(), json!({ "type": "http", "scheme": "basic" }));
|
|
323
|
+
route_security.push(json!({ s_name: [] }));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if !route_security.is_empty() {
|
|
329
|
+
method_obj["security"] = json!(route_security);
|
|
330
|
+
}
|
|
331
|
+
|
|
290
332
|
// Parameters (path and query)
|
|
291
333
|
let mut parameters = Vec::new();
|
|
292
334
|
|
|
@@ -408,8 +450,16 @@ fn generate_openapi(routes: &[RouteEntry]) -> String {
|
|
|
408
450
|
"paths": paths
|
|
409
451
|
});
|
|
410
452
|
|
|
453
|
+
let mut components = serde_json::Map::new();
|
|
411
454
|
if !components_schemas.is_empty() {
|
|
412
|
-
|
|
455
|
+
components.insert("schemas".to_string(), json!(components_schemas));
|
|
456
|
+
}
|
|
457
|
+
if !security_schemes.is_empty() {
|
|
458
|
+
components.insert("securitySchemes".to_string(), json!(security_schemes));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if !components.is_empty() {
|
|
462
|
+
doc["components"] = serde_json::Value::Object(components);
|
|
413
463
|
}
|
|
414
464
|
|
|
415
465
|
serde_json::to_string(&doc).unwrap()
|
|
@@ -623,15 +673,23 @@ impl PyRequest {
|
|
|
623
673
|
#[pyo3(signature = ())]
|
|
624
674
|
fn json(&self, py: Python<'_>) -> PyResult<PyObject> {
|
|
625
675
|
let trimmed = self.body.trim();
|
|
626
|
-
if trimmed.is_empty() {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
676
|
+
let raw_obj = if trimmed.is_empty() {
|
|
677
|
+
pyo3::types::PyDict::new_bound(py).into_any().unbind()
|
|
678
|
+
} else if let Ok(val) = serde_json::from_str::<serde_json::Value>(&self.body) {
|
|
679
|
+
serde_to_pyobject(py, &val)?
|
|
680
|
+
} else {
|
|
681
|
+
let py_json = py.import_bound("json")?;
|
|
682
|
+
py_json.call_method1("loads", (&self.body,)).map(|b| b.unbind())?
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
if let Ok(uploads_mod) = py.import_bound("rustapi.uploads") {
|
|
686
|
+
if let Ok(cls) = uploads_mod.getattr("AwaitableDict") {
|
|
687
|
+
if let Ok(awaitable_dict) = cls.call1((raw_obj.bind(py),)) {
|
|
688
|
+
return Ok(awaitable_dict.unbind().into());
|
|
689
|
+
}
|
|
690
|
+
}
|
|
632
691
|
}
|
|
633
|
-
|
|
634
|
-
py_json.call_method1("loads", (&self.body,)).map(|b| b.unbind())
|
|
692
|
+
Ok(raw_obj)
|
|
635
693
|
}
|
|
636
694
|
}
|
|
637
695
|
|
|
@@ -1825,10 +1883,18 @@ async fn handle_route(
|
|
|
1825
1883
|
});
|
|
1826
1884
|
|
|
1827
1885
|
// -- Dependency injection ---------------------------------------------
|
|
1828
|
-
let
|
|
1886
|
+
let req_obj: PyObject = Python::with_gil(|py| {
|
|
1887
|
+
Py::new(py, PyRequest {
|
|
1888
|
+
method: method.clone(), path: path.clone(), path_params: path_params.clone(),
|
|
1889
|
+
query_params: query_params.clone(), headers: headers_map.clone(), cookies: cookies_map.clone(),
|
|
1890
|
+
form: form_map.clone(), files: files_map.clone(), body: body.clone(),
|
|
1891
|
+
}).unwrap().into_any()
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
let mut dependency_error_response: Option<HyperResponse<Body>> = None;
|
|
1829
1895
|
let mut resolved_args = HashMap::<String, PyObject>::new();
|
|
1830
|
-
let
|
|
1831
|
-
let
|
|
1896
|
+
let dep_cache_obj = Python::with_gil(|py| pyo3::types::PyDict::new_bound(py).into_any().unbind());
|
|
1897
|
+
let teardown_gens = Vec::<PyObject>::new();
|
|
1832
1898
|
|
|
1833
1899
|
for dep in deps {
|
|
1834
1900
|
let dep_func = Python::with_gil(|py| -> PyObject {
|
|
@@ -1842,73 +1908,106 @@ async fn handle_route(
|
|
|
1842
1908
|
dep.func.clone_ref(py)
|
|
1843
1909
|
});
|
|
1844
1910
|
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
}
|
|
1911
|
+
let solve_coro = Python::with_gil(|py| -> PyResult<PyObject> {
|
|
1912
|
+
let resolver = py.import_bound("rustapi.resolver")?;
|
|
1913
|
+
let solver = resolver.getattr("solve_dependency")?;
|
|
1914
|
+
let ov_bound = dependency_overrides.bind(py);
|
|
1915
|
+
solver.call1((dep_func, &req_obj, ov_bound, &dep_cache_obj)).map(|c| c.unbind())
|
|
1916
|
+
});
|
|
1852
1917
|
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
let
|
|
1859
|
-
|
|
1860
|
-
if let Ok(cb) = Py::new(py, CoroCallback { tx: StdMutex::new(Some(tx)) }) {
|
|
1861
|
-
let _ = sc.bind(py).call1((c, cb));
|
|
1862
|
-
}
|
|
1863
|
-
});
|
|
1864
|
-
match rx.await {
|
|
1865
|
-
Ok(Ok(res)) => Ok(res),
|
|
1866
|
-
Ok(Err(err_obj)) => Err(Python::with_gil(|py| err_obj.bind(py).to_string())),
|
|
1867
|
-
Err(_) => Err("Asyncio dropped".to_string()),
|
|
1918
|
+
match solve_coro {
|
|
1919
|
+
Ok(coro) => {
|
|
1920
|
+
let (tx, rx) = oneshot::channel();
|
|
1921
|
+
let sc = schedule_coro.clone();
|
|
1922
|
+
Python::with_gil(|py| {
|
|
1923
|
+
if let Ok(cb) = Py::new(py, CoroCallback { tx: StdMutex::new(Some(tx)) }) {
|
|
1924
|
+
let _ = sc.bind(py).call1((coro, cb));
|
|
1868
1925
|
}
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1926
|
+
});
|
|
1927
|
+
match rx.await {
|
|
1928
|
+
Ok(Ok(res)) => {
|
|
1929
|
+
resolved_args.insert(dep.name, res);
|
|
1930
|
+
}
|
|
1931
|
+
Ok(Err(err_obj)) => {
|
|
1932
|
+
let resp = Python::with_gil(|py| -> HyperResponse<Body> {
|
|
1933
|
+
let bound_err = err_obj.bind(py);
|
|
1934
|
+
let status_code: u16 = bound_err.getattr("status_code")
|
|
1935
|
+
.and_then(|s| s.extract())
|
|
1936
|
+
.unwrap_or(500);
|
|
1937
|
+
|
|
1938
|
+
let detail_json: String = if let Ok(detail) = bound_err.getattr("detail") {
|
|
1939
|
+
if let Ok(py_json) = py.import_bound("json") {
|
|
1940
|
+
if let Ok(dumps_res) = py_json.call_method1("dumps", (&detail,)) {
|
|
1941
|
+
let d_str: String = dumps_res.extract().unwrap_or_default();
|
|
1942
|
+
format!(r#"{{"detail":{}}}"#, d_str)
|
|
1943
|
+
} else {
|
|
1944
|
+
format!(r#"{{"detail":"{}"}}"#, detail.to_string().replace('"', "'"))
|
|
1945
|
+
}
|
|
1946
|
+
} else {
|
|
1947
|
+
format!(r#"{{"detail":"{}"}}"#, detail.to_string().replace('"', "'"))
|
|
1948
|
+
}
|
|
1949
|
+
} else {
|
|
1950
|
+
format!(r#"{{"detail":"{}"}}"#, bound_err.to_string().replace('"', "'"))
|
|
1951
|
+
};
|
|
1882
1952
|
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1953
|
+
HyperResponse::builder()
|
|
1954
|
+
.status(status_code)
|
|
1955
|
+
.header("Content-Type", "application/json")
|
|
1956
|
+
.body(Body::from(detail_json))
|
|
1957
|
+
.unwrap()
|
|
1958
|
+
});
|
|
1959
|
+
dependency_error_response = Some(resp);
|
|
1960
|
+
break;
|
|
1890
1961
|
}
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1962
|
+
Err(_) => {
|
|
1963
|
+
dependency_error_response = Some(
|
|
1964
|
+
HyperResponse::builder()
|
|
1965
|
+
.status(500)
|
|
1966
|
+
.header("Content-Type", "application/json")
|
|
1967
|
+
.body(Body::from(r#"{"detail":"Asyncio task dropped"}"#))
|
|
1968
|
+
.unwrap()
|
|
1969
|
+
);
|
|
1970
|
+
break;
|
|
1897
1971
|
}
|
|
1898
|
-
Err(e) => { dependency_error = Some(e); break; }
|
|
1899
1972
|
}
|
|
1900
1973
|
}
|
|
1901
|
-
Err(
|
|
1974
|
+
Err(py_err) => {
|
|
1975
|
+
let resp = Python::with_gil(|py| -> HyperResponse<Body> {
|
|
1976
|
+
let err_val = py_err.into_value(py);
|
|
1977
|
+
let bound_err = err_val.bind(py);
|
|
1978
|
+
let status_code: u16 = bound_err.getattr("status_code")
|
|
1979
|
+
.and_then(|s| s.extract())
|
|
1980
|
+
.unwrap_or(500);
|
|
1981
|
+
|
|
1982
|
+
let detail_json: String = if let Ok(detail) = bound_err.getattr("detail") {
|
|
1983
|
+
if let Ok(py_json) = py.import_bound("json") {
|
|
1984
|
+
if let Ok(dumps_res) = py_json.call_method1("dumps", (&detail,)) {
|
|
1985
|
+
let d_str: String = dumps_res.extract().unwrap_or_default();
|
|
1986
|
+
format!(r#"{{"detail":{}}}"#, d_str)
|
|
1987
|
+
} else {
|
|
1988
|
+
format!(r#"{{"detail":"{}"}}"#, detail.to_string().replace('"', "'"))
|
|
1989
|
+
}
|
|
1990
|
+
} else {
|
|
1991
|
+
format!(r#"{{"detail":"{}"}}"#, detail.to_string().replace('"', "'"))
|
|
1992
|
+
}
|
|
1993
|
+
} else {
|
|
1994
|
+
format!(r#"{{"detail":"{}"}}"#, bound_err.to_string().replace('"', "'"))
|
|
1995
|
+
};
|
|
1996
|
+
|
|
1997
|
+
HyperResponse::builder()
|
|
1998
|
+
.status(status_code)
|
|
1999
|
+
.header("Content-Type", "application/json")
|
|
2000
|
+
.body(Body::from(detail_json))
|
|
2001
|
+
.unwrap()
|
|
2002
|
+
});
|
|
2003
|
+
dependency_error_response = Some(resp);
|
|
2004
|
+
break;
|
|
2005
|
+
}
|
|
1902
2006
|
}
|
|
1903
2007
|
}
|
|
1904
2008
|
|
|
1905
|
-
if let Some(
|
|
1906
|
-
return Ok(
|
|
1907
|
-
HyperResponse::builder().status(500)
|
|
1908
|
-
.header("Content-Type", "application/json")
|
|
1909
|
-
.body(Body::from(format!(r#"{{"detail":"{}"}}"#, err_msg.replace('"', "'"))))
|
|
1910
|
-
.unwrap()
|
|
1911
|
-
);
|
|
2009
|
+
if let Some(err_resp) = dependency_error_response {
|
|
2010
|
+
return Ok(err_resp);
|
|
1912
2011
|
}
|
|
1913
2012
|
|
|
1914
2013
|
// -- BackgroundTasks setup --------------------------------------------
|
|
@@ -111,3 +111,41 @@ def test_6_upload_file_sync_and_async_methods():
|
|
|
111
111
|
await file_obj.close()
|
|
112
112
|
|
|
113
113
|
asyncio.run(_test())
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_7_openapi_security_schemes_and_http_exception_status_codes():
|
|
117
|
+
app = FastAPI()
|
|
118
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
|
|
119
|
+
bearer_scheme = HTTPBearer()
|
|
120
|
+
|
|
121
|
+
@app.get("/secure/me")
|
|
122
|
+
def secure_me(token: str = Depends(oauth2_scheme)):
|
|
123
|
+
if not token:
|
|
124
|
+
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
125
|
+
return {"status": "ok"}
|
|
126
|
+
|
|
127
|
+
@app.get("/admin/logs")
|
|
128
|
+
def admin_logs(token: HTTPAuthorizationCredentials = Depends(bearer_scheme)):
|
|
129
|
+
return {"logs": []}
|
|
130
|
+
|
|
131
|
+
def run_server():
|
|
132
|
+
app.run(host="127.0.0.1", port=8994)
|
|
133
|
+
|
|
134
|
+
t = threading.Thread(target=run_server, daemon=True)
|
|
135
|
+
t.start()
|
|
136
|
+
time.sleep(1.5)
|
|
137
|
+
|
|
138
|
+
# 1. Test OpenAPI securitySchemes generation
|
|
139
|
+
res_openapi = requests.get("http://127.0.0.1:8994/openapi.json")
|
|
140
|
+
assert res_openapi.status_code == 200
|
|
141
|
+
openapi_doc = res_openapi.json()
|
|
142
|
+
assert "components" in openapi_doc
|
|
143
|
+
assert "securitySchemes" in openapi_doc["components"]
|
|
144
|
+
schemes = openapi_doc["components"]["securitySchemes"]
|
|
145
|
+
assert "OAuth2PasswordBearer" in schemes or "HTTPBearer" in schemes
|
|
146
|
+
|
|
147
|
+
# 2. Test HTTPException status code propagation (401 instead of 500)
|
|
148
|
+
res_unauth = requests.get("http://127.0.0.1:8994/secure/me")
|
|
149
|
+
assert res_unauth.status_code == 401
|
|
150
|
+
assert "Not authenticated" in res_unauth.json()["detail"] or "Unauthorized" in res_unauth.json()["detail"]
|
|
151
|
+
|
|
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
|
|
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
|
{pyrustapi-0.3.86 → pyrustapi-0.8.6}/performance-reports/fastapi_vs_rustapi_crud_benchmark.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyrustapi-0.3.86 → pyrustapi-0.8.6}/performance-reports/three_tier_architecture_evaluation.md
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|