pyrustapi 0.3.33__tar.gz → 0.3.86__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.33 → pyrustapi-0.3.86}/Cargo.lock +1 -1
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/Cargo.toml +1 -1
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/PKG-INFO +1 -1
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/fastapi_compatibility_and_features.md +68 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/pyproject.toml +1 -1
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/__init__.py +30 -2
- pyrustapi-0.3.86/python/rustapi/resolver.py +66 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/router.py +29 -0
- pyrustapi-0.3.86/python/rustapi/security/__init__.py +178 -0
- pyrustapi-0.3.86/python/rustapi/uploads.py +58 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/src/lib.rs +47 -2
- pyrustapi-0.3.86/test-reports/junit.xml +1 -0
- pyrustapi-0.3.86/tests/test_fastapi_all_6_mismatches.py +113 -0
- pyrustapi-0.3.86/tests/test_python_ecosystem_compatibility.py +114 -0
- pyrustapi-0.3.33/python/rustapi/security/__init__.py +0 -80
- pyrustapi-0.3.33/test-reports/junit.xml +0 -1
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/.github/workflows/CI.yml +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/.github/workflows/pages.yml +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/.github/workflows/release.yml +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/.github/workflows/static.yml +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/.gitignore +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/LICENSE +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/README.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/ROADMAP.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmark_dashboard.html +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/benchmark.sh +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/fastapi_app.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/fastapi_cpu_app.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/fastapi_edge_app.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/fastapi_oha.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/large.json +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/mixed.json +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/rustapi_app.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/rustapi_cpu_app.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/rustapi_edge_app.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/rustapi_multiworker_app.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/rustapi_oha.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/rustapi_tier3_app.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/tier_app.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/validate.json +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/run_3tier_benchmark.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/run_comparison_benchmark.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/run_cpu_stability_benchmark.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/run_edgecase_benchmark.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/run_native_tier3_benchmark.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/run_real_oha_benchmark.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/run_release_highspeed_benchmark.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks.html +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/crud_benchmark/benchmark_db.sqlite +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/crud_benchmark/benchmark_db.sqlite-shm +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/crud_benchmark/benchmark_db.sqlite-wal +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/crud_benchmark/benchmark_results.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/crud_benchmark/fastapi_crud.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/crud_benchmark/run_crud_benchmark.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/crud_benchmark/rustapi_crud.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/3tier_architecture_and_benchmarks.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/DeepAnalytics.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/api-reference.html +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/benchmark_dashboard.html +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/benchmarks.html +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/database.html +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/database_engine.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/docs.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/getting-started.html +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/getting_started.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/index.html +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/mcp-server.html +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/mcp_server.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/native_business_logic.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/performance_and_benchmarks.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/power_primitives.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/pyrustapi-logo (1).png +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/report_01_.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/response_types.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/security-power-modules.html +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/swagger_ui_and_dx_deviations.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/examples/app.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/index.html +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/performance-reports/cpu_intensive_stability_report.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/performance-reports/fastapi_vs_hybrid_vs_native.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/performance-reports/fastapi_vs_rustapi_comparison.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/performance-reports/fastapi_vs_rustapi_crud_benchmark.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/performance-reports/performance_and_benchmarks.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/performance-reports/rust_native_edgecases_report.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/performance-reports/three_tier_architecture_evaluation.md +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/pyrustapi-logo.png +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/pyrustapi/__init__.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/__init__.pyi +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/background.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/depends.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/encoders.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/exceptions.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/middleware/__init__.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/middleware/cors.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/openapi/__init__.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/openapi/docs.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/param_functions.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/py.typed +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/responses/__init__.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/rust.rs +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/sse.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/staticfiles.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/status.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/scratch/.gitkeep +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_3tier_architecture.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_basic.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_database_engine.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_fastapi_compatibility.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_file_uploads_and_dx.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_imports.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_native_routes.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_overrides_and_coercion.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_reload_and_logging.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_response_model.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_responses_and_req_json.py +0 -0
- {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_rust_power_modules.py +0 -0
|
@@ -168,4 +168,72 @@ def get_item(
|
|
|
168
168
|
# Convert dataclass/Pydantic model to JSON dict
|
|
169
169
|
user_dict = jsonable_encoder(user)
|
|
170
170
|
return {"item_id": item_id, "query": q, "user": user_dict}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## 7. Python Ecosystem Compatibility Matrix
|
|
176
|
+
|
|
177
|
+
`rustapi` is built to seamlessly integrate with standard Python database, machine learning, security, and utility libraries:
|
|
178
|
+
|
|
179
|
+
| Library Domain | Package | Verified Feature & Usage | Status |
|
|
180
|
+
| :--- | :--- | :--- | :--- |
|
|
181
|
+
| **Data Validation** | `pydantic` v2 | `BaseModel`, `Field`, `validator`, `EmailStr` in route bodies | ✅ Verified |
|
|
182
|
+
| **HTTP Clients** | `httpx`, `requests` | `httpx.AsyncClient` & `requests` testing, multi-part uploads & streams | ✅ Verified |
|
|
183
|
+
| **Databases & ORMs** | `sqlite3`, `sqlalchemy` | Session context managers & query execution inside `Depends(get_db)` | ✅ Verified |
|
|
184
|
+
| **Image Processing** | `PIL` / `Pillow` | Image decoding & matrix transformations on `UploadFile` streams | ✅ Verified |
|
|
185
|
+
| **JWT & Security** | `pyjwt`, `passlib`, `argon2` | `jwt.encode`, `jwt.decode`, password hashing inside auth endpoints | ✅ Verified |
|
|
186
|
+
| **Numeric & ML** | `numpy` | `np.array` operations on request payloads & image byte arrays | ✅ Verified |
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
import io
|
|
190
|
+
import jwt
|
|
191
|
+
import numpy as np
|
|
192
|
+
from PIL import Image
|
|
193
|
+
from pydantic import BaseModel, Field
|
|
194
|
+
from rustapi import FastAPI, Request, Depends, HTTPException
|
|
195
|
+
from rustapi.uploads import UploadFile
|
|
196
|
+
|
|
197
|
+
app = FastAPI()
|
|
198
|
+
|
|
199
|
+
class AnalysisModel(BaseModel):
|
|
200
|
+
project_name: str = Field(..., min_length=2)
|
|
201
|
+
|
|
202
|
+
@app.post("/analyze")
|
|
203
|
+
async def analyze(req: Request):
|
|
204
|
+
if "photo" not in req.files:
|
|
205
|
+
raise HTTPException(status_code=400, detail="No photo uploaded")
|
|
206
|
+
|
|
207
|
+
file_obj: UploadFile = req.files["photo"][0]
|
|
208
|
+
img_bytes = await file_obj.read() # Both await file.read() and file.read() work
|
|
209
|
+
|
|
210
|
+
# Process image with Pillow & NumPy
|
|
211
|
+
image = Image.open(io.BytesIO(img_bytes)).convert("L")
|
|
212
|
+
array = np.array(image)
|
|
213
|
+
brightness = float(np.mean(array))
|
|
214
|
+
|
|
215
|
+
# Encode JWT response
|
|
216
|
+
token = jwt.encode({"brightness": brightness}, "secret", algorithm="HS256")
|
|
217
|
+
return {"status": "success", "token": token}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 8. FastAPI Migration Mismatch Resolution (v0.3.34)
|
|
223
|
+
|
|
224
|
+
The following 6 FastAPI compatibility enhancements have been resolved natively in `pyrustapi` v0.3.34:
|
|
225
|
+
|
|
226
|
+
1. **Sub-Router Nesting (`APIRouter.include_router`)**:
|
|
227
|
+
`router.include_router(sub_router, prefix="/v1", tags=["sub"])` allows sub-routers to mount additional sub-routers with inherited path prefixes and tags.
|
|
228
|
+
2. **`FastAPI` Constructor Kwargs**:
|
|
229
|
+
`FastAPI(title="My API", description="...", version="1.0.0", openapi_url="/openapi.json", docs_url="/docs", redoc_url="/redoc")` accepts all standard metadata keyword arguments without constructor errors.
|
|
230
|
+
3. **Custom Exception Handlers (`@app.exception_handler`)**:
|
|
231
|
+
Register custom status code or exception class handlers via `@app.exception_handler(CustomException)`.
|
|
232
|
+
4. **`HTTPBearer()` Credentials Container**:
|
|
233
|
+
`security = HTTPBearer()` returns `HTTPAuthorizationCredentials(scheme="Bearer", credentials="...")` when passed to `Depends(security)`.
|
|
234
|
+
5. **Recursive Dependency Injection (`solve_dependency`)**:
|
|
235
|
+
Injects `request: Request` / `req` parameters automatically and recursively resolves nested `Depends(sub_dep)` calls.
|
|
236
|
+
6. **Dual Async / Sync `UploadFile` Methods**:
|
|
237
|
+
`UploadFile` methods (`file.read()`, `file.seek()`, `file.close()`) support both asynchronous (`await file.read()`) and synchronous (`file.read()`) execution, plus `.file` returns `io.BytesIO`.
|
|
238
|
+
|
|
171
239
|
```
|
|
@@ -35,14 +35,42 @@ from . import responses, middleware
|
|
|
35
35
|
class FastAPI(Engine):
|
|
36
36
|
"""FastAPI-compatible application class wrapping the Rust Tokio core engine."""
|
|
37
37
|
|
|
38
|
-
def
|
|
38
|
+
def __new__(cls, *args, **kwargs):
|
|
39
|
+
return super().__new__(cls)
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
title: str = "RustAPI",
|
|
44
|
+
description: str = "",
|
|
45
|
+
version: str = "0.1.0",
|
|
46
|
+
openapi_url: Optional[str] = "/openapi.json",
|
|
47
|
+
docs_url: Optional[str] = "/docs",
|
|
48
|
+
redoc_url: Optional[str] = "/redoc",
|
|
49
|
+
swagger_ui_oauth2_redirect_url: Optional[str] = "/docs/oauth2-redirect",
|
|
50
|
+
**kwargs: Any,
|
|
51
|
+
):
|
|
39
52
|
super().__init__()
|
|
53
|
+
self.title = title
|
|
54
|
+
self.description = description
|
|
55
|
+
self.version = version
|
|
56
|
+
self.openapi_url = openapi_url
|
|
57
|
+
self.docs_url = docs_url
|
|
58
|
+
self.redoc_url = redoc_url
|
|
59
|
+
self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url
|
|
60
|
+
self.exception_handlers: Dict[Any, Any] = {}
|
|
40
61
|
self.middlewares: list = []
|
|
41
62
|
|
|
42
63
|
def add_middleware(self, middleware_cls: type, **kwargs: Any):
|
|
43
64
|
"""Add middleware (such as CORSMiddleware) to application configuration."""
|
|
44
65
|
self.middlewares.append((middleware_cls, kwargs))
|
|
45
66
|
|
|
67
|
+
def exception_handler(self, exc_class_or_status_code: Any):
|
|
68
|
+
"""Register an exception handler decorator for an exception class or status code."""
|
|
69
|
+
def decorator(func: Any):
|
|
70
|
+
self.exception_handlers[exc_class_or_status_code] = func
|
|
71
|
+
return func
|
|
72
|
+
return decorator
|
|
73
|
+
|
|
46
74
|
def frontend(self, path: str = "/", directory: str = "dist"):
|
|
47
75
|
"""Serve a built static frontend app (e.g. Vite, React, Vue, Svelte output directory)."""
|
|
48
76
|
from .staticfiles import StaticFiles
|
|
@@ -124,7 +152,7 @@ class RedirectResponse(Response):
|
|
|
124
152
|
return Response.__new__(cls, content="", status_code=status_code, headers=h)
|
|
125
153
|
|
|
126
154
|
|
|
127
|
-
__version__ = "0.3.
|
|
155
|
+
__version__ = "0.3.86"
|
|
128
156
|
__all__ = [
|
|
129
157
|
"Engine",
|
|
130
158
|
"FastAPI",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import asyncio
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
from .depends import Depends
|
|
5
|
+
from .exceptions import HTTPException
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def solve_dependency(
|
|
9
|
+
dep_target: Any,
|
|
10
|
+
request: Any,
|
|
11
|
+
dependency_overrides: Optional[Dict[Any, Any]] = None,
|
|
12
|
+
cache: Optional[Dict[Any, Any]] = None,
|
|
13
|
+
) -> Any:
|
|
14
|
+
"""Recursively resolve FastAPI dependencies with request injection and caching."""
|
|
15
|
+
if cache is None:
|
|
16
|
+
cache = {}
|
|
17
|
+
|
|
18
|
+
if dependency_overrides and dep_target in dependency_overrides:
|
|
19
|
+
dep_target = dependency_overrides[dep_target]
|
|
20
|
+
|
|
21
|
+
if isinstance(dep_target, Depends):
|
|
22
|
+
dep_target = dep_target.dependency
|
|
23
|
+
|
|
24
|
+
if not callable(dep_target):
|
|
25
|
+
return dep_target
|
|
26
|
+
|
|
27
|
+
target_id = id(dep_target)
|
|
28
|
+
if target_id in cache:
|
|
29
|
+
return cache[target_id]
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
sig = inspect.signature(dep_target)
|
|
33
|
+
kwargs = {}
|
|
34
|
+
for param_name, param in sig.parameters.items():
|
|
35
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
# 1. Inject request if parameter expects Request
|
|
39
|
+
if param_name in ("request", "req") or (param.annotation and getattr(param.annotation, "__name__", "") in ("PyRequest", "Request")):
|
|
40
|
+
kwargs[param_name] = request
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
# 2. Check if default value is Depends(...)
|
|
44
|
+
default_val = param.default
|
|
45
|
+
if isinstance(default_val, Depends):
|
|
46
|
+
sub_target = default_val.dependency or param.annotation
|
|
47
|
+
kwargs[param_name] = await solve_dependency(sub_target, request, dependency_overrides, cache)
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
except (ValueError, TypeError):
|
|
51
|
+
sig = None
|
|
52
|
+
kwargs = {}
|
|
53
|
+
|
|
54
|
+
if not kwargs and not sig:
|
|
55
|
+
try:
|
|
56
|
+
res = dep_target(request) if callable(dep_target) else dep_target
|
|
57
|
+
except TypeError:
|
|
58
|
+
res = dep_target()
|
|
59
|
+
else:
|
|
60
|
+
if asyncio.iscoroutinefunction(dep_target) or (hasattr(dep_target, "__call__") and asyncio.iscoroutinefunction(dep_target.__call__)):
|
|
61
|
+
res = await dep_target(**kwargs)
|
|
62
|
+
else:
|
|
63
|
+
res = dep_target(**kwargs)
|
|
64
|
+
|
|
65
|
+
cache[target_id] = res
|
|
66
|
+
return res
|
|
@@ -45,6 +45,35 @@ class APIRouter:
|
|
|
45
45
|
return func
|
|
46
46
|
return decorator
|
|
47
47
|
|
|
48
|
+
def include_router(
|
|
49
|
+
self,
|
|
50
|
+
router: APIRouter,
|
|
51
|
+
prefix: str = "",
|
|
52
|
+
tags: Optional[List[str]] = None,
|
|
53
|
+
dependencies: Optional[List[Any]] = None,
|
|
54
|
+
**kwargs: Any,
|
|
55
|
+
):
|
|
56
|
+
"""Include a sub-router into this APIRouter instance."""
|
|
57
|
+
base_prefix = f"{prefix.rstrip('/')}{router.prefix}"
|
|
58
|
+
merged_tags = list(dict.fromkeys((tags or []) + (router.tags or [])))
|
|
59
|
+
merged_deps = (dependencies or []) + (router.dependencies or [])
|
|
60
|
+
|
|
61
|
+
for item in router.routes:
|
|
62
|
+
method, sub_path, func, response_model, route_kwargs = item
|
|
63
|
+
full_path = f"{base_prefix}{sub_path}".replace("//", "/")
|
|
64
|
+
if not full_path.startswith("/"):
|
|
65
|
+
full_path = f"/{full_path}"
|
|
66
|
+
|
|
67
|
+
r_kw = route_kwargs.copy() if route_kwargs else {}
|
|
68
|
+
r_tags = list(dict.fromkeys(merged_tags + r_kw.get("tags", [])))
|
|
69
|
+
r_deps = merged_deps + r_kw.get("dependencies", [])
|
|
70
|
+
if r_tags:
|
|
71
|
+
r_kw["tags"] = r_tags
|
|
72
|
+
if r_deps:
|
|
73
|
+
r_kw["dependencies"] = r_deps
|
|
74
|
+
|
|
75
|
+
self.routes.append((method, full_path, func, response_model, r_kw))
|
|
76
|
+
|
|
48
77
|
def get(self, path: str, response_model: Optional[Type[Any]] = None, **kwargs: Any):
|
|
49
78
|
return self._add("GET", path, response_model=response_model, **kwargs)
|
|
50
79
|
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any
|
|
2
|
+
from ..exceptions import HTTPException
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class HTTPAuthorizationCredentials:
|
|
6
|
+
"""FastAPI-compatible HTTP Authorization credentials container."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, scheme: str, credentials: str):
|
|
9
|
+
self.scheme = scheme
|
|
10
|
+
self.credentials = credentials
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HTTPBasic:
|
|
14
|
+
"""HTTP Basic authentication dependency helper."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, *, realm: Optional[str] = None, auto_error: bool = True):
|
|
17
|
+
self.realm = realm
|
|
18
|
+
self.auto_error = auto_error
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HTTPBearer:
|
|
22
|
+
"""HTTP Bearer authentication dependency helper."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, *, bearerFormat: Optional[str] = None, auto_error: bool = True):
|
|
25
|
+
self.bearerFormat = bearerFormat
|
|
26
|
+
self.auto_error = auto_error
|
|
27
|
+
|
|
28
|
+
def __call__(self, request: Any = None, req: Any = None) -> Optional[HTTPAuthorizationCredentials]:
|
|
29
|
+
r = request or req
|
|
30
|
+
if not r or not hasattr(r, "headers"):
|
|
31
|
+
if self.auto_error:
|
|
32
|
+
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
auth_header = r.headers.get("authorization") or r.headers.get("Authorization")
|
|
36
|
+
if not auth_header:
|
|
37
|
+
if self.auto_error:
|
|
38
|
+
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
scheme, _, credentials = auth_header.partition(" ")
|
|
42
|
+
if scheme.lower() != "bearer" or not credentials:
|
|
43
|
+
if self.auto_error:
|
|
44
|
+
raise HTTPException(status_code=401, detail="Invalid authentication scheme")
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
return HTTPAuthorizationCredentials(scheme="Bearer", credentials=credentials)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class OAuth2PasswordBearer:
|
|
51
|
+
"""OAuth2 password flow with bearer token dependency helper."""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
tokenUrl: str,
|
|
56
|
+
scheme_name: Optional[str] = None,
|
|
57
|
+
scopes: Optional[Dict[str, str]] = None,
|
|
58
|
+
description: Optional[str] = None,
|
|
59
|
+
auto_error: bool = True,
|
|
60
|
+
):
|
|
61
|
+
self.tokenUrl = tokenUrl
|
|
62
|
+
self.scheme_name = scheme_name
|
|
63
|
+
self.scopes = scopes or {}
|
|
64
|
+
self.description = description
|
|
65
|
+
self.auto_error = auto_error
|
|
66
|
+
|
|
67
|
+
def __call__(self, request: Any = None, req: Any = None) -> Optional[str]:
|
|
68
|
+
r = request or req
|
|
69
|
+
if not r or not hasattr(r, "headers"):
|
|
70
|
+
if self.auto_error:
|
|
71
|
+
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
auth_header = r.headers.get("authorization") or r.headers.get("Authorization")
|
|
75
|
+
if not auth_header:
|
|
76
|
+
if self.auto_error:
|
|
77
|
+
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
scheme, _, token = auth_header.partition(" ")
|
|
81
|
+
if scheme.lower() != "bearer" or not token:
|
|
82
|
+
if self.auto_error:
|
|
83
|
+
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
return token
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class OAuth2PasswordRequestForm:
|
|
90
|
+
"""OAuth2 password request form for token extraction."""
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
grant_type: str = "password",
|
|
95
|
+
username: str = "",
|
|
96
|
+
password: str = "",
|
|
97
|
+
scope: str = "",
|
|
98
|
+
client_id: Optional[str] = None,
|
|
99
|
+
client_secret: Optional[str] = None,
|
|
100
|
+
):
|
|
101
|
+
self.grant_type = grant_type
|
|
102
|
+
self.username = username
|
|
103
|
+
self.password = password
|
|
104
|
+
self.scopes = scope.split()
|
|
105
|
+
self.client_id = client_id
|
|
106
|
+
self.client_secret = client_secret
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class APIKeyHeader:
|
|
110
|
+
"""API Key authentication header dependency helper."""
|
|
111
|
+
|
|
112
|
+
def __init__(self, *, name: str, auto_error: bool = True):
|
|
113
|
+
self.name = name
|
|
114
|
+
self.auto_error = auto_error
|
|
115
|
+
|
|
116
|
+
def __call__(self, request: Any = None, req: Any = None) -> Optional[str]:
|
|
117
|
+
r = request or req
|
|
118
|
+
if not r or not hasattr(r, "headers"):
|
|
119
|
+
if self.auto_error:
|
|
120
|
+
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
key = r.headers.get(self.name) or r.headers.get(self.name.lower())
|
|
124
|
+
if not key and self.auto_error:
|
|
125
|
+
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
126
|
+
return key
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class APIKeyQuery:
|
|
130
|
+
"""API Key authentication query parameter dependency helper."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, *, name: str, auto_error: bool = True):
|
|
133
|
+
self.name = name
|
|
134
|
+
self.auto_error = auto_error
|
|
135
|
+
|
|
136
|
+
def __call__(self, request: Any = None, req: Any = None) -> Optional[str]:
|
|
137
|
+
r = request or req
|
|
138
|
+
if not r or not hasattr(r, "query_params"):
|
|
139
|
+
if self.auto_error:
|
|
140
|
+
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
key = r.query_params.get(self.name)
|
|
144
|
+
if not key and self.auto_error:
|
|
145
|
+
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
146
|
+
return key
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class APIKeyCookie:
|
|
150
|
+
"""API Key authentication cookie dependency helper."""
|
|
151
|
+
|
|
152
|
+
def __init__(self, *, name: str, auto_error: bool = True):
|
|
153
|
+
self.name = name
|
|
154
|
+
self.auto_error = auto_error
|
|
155
|
+
|
|
156
|
+
def __call__(self, request: Any = None, req: Any = None) -> Optional[str]:
|
|
157
|
+
r = request or req
|
|
158
|
+
if not r or not hasattr(r, "cookies"):
|
|
159
|
+
if self.auto_error:
|
|
160
|
+
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
key = r.cookies.get(self.name)
|
|
164
|
+
if not key and self.auto_error:
|
|
165
|
+
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
166
|
+
return key
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
__all__ = [
|
|
170
|
+
"HTTPAuthorizationCredentials",
|
|
171
|
+
"HTTPBasic",
|
|
172
|
+
"HTTPBearer",
|
|
173
|
+
"OAuth2PasswordBearer",
|
|
174
|
+
"OAuth2PasswordRequestForm",
|
|
175
|
+
"APIKeyHeader",
|
|
176
|
+
"APIKeyQuery",
|
|
177
|
+
"APIKeyCookie",
|
|
178
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from typing import Optional, Dict, Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AwaitableBytes(bytes):
|
|
6
|
+
"""Bytes subclass that is directly awaitable for async compatibility (await file.read())."""
|
|
7
|
+
def __await__(self):
|
|
8
|
+
async def _wrap():
|
|
9
|
+
return self
|
|
10
|
+
return _wrap().__await__()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AwaitableInt(int):
|
|
14
|
+
"""Integer subclass that is directly awaitable for async compatibility (await file.seek())."""
|
|
15
|
+
def __await__(self):
|
|
16
|
+
async def _wrap():
|
|
17
|
+
return self
|
|
18
|
+
return _wrap().__await__()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AwaitableNone:
|
|
22
|
+
"""None wrapper that is directly awaitable for async compatibility (await file.close())."""
|
|
23
|
+
def __await__(self):
|
|
24
|
+
async def _wrap():
|
|
25
|
+
return None
|
|
26
|
+
return _wrap().__await__()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class UploadFile:
|
|
30
|
+
"""FastAPI-compatible UploadFile supporting both async (await file.read()) and sync access."""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
file: Optional[io.BytesIO] = None,
|
|
35
|
+
filename: Optional[str] = "",
|
|
36
|
+
content_type: str = "",
|
|
37
|
+
headers: Optional[Dict[str, str]] = None,
|
|
38
|
+
):
|
|
39
|
+
self.file = file or io.BytesIO()
|
|
40
|
+
self.filename = filename or ""
|
|
41
|
+
self.content_type = content_type or ""
|
|
42
|
+
self.headers = headers or {}
|
|
43
|
+
|
|
44
|
+
def read(self, size: int = -1) -> AwaitableBytes:
|
|
45
|
+
content = self.file.read(size)
|
|
46
|
+
return AwaitableBytes(content)
|
|
47
|
+
|
|
48
|
+
def seek(self, offset: int = 0) -> AwaitableInt:
|
|
49
|
+
res = self.file.seek(offset)
|
|
50
|
+
return AwaitableInt(res)
|
|
51
|
+
|
|
52
|
+
def write(self, data: bytes) -> AwaitableInt:
|
|
53
|
+
res = self.file.write(data)
|
|
54
|
+
return AwaitableInt(res)
|
|
55
|
+
|
|
56
|
+
def close(self) -> AwaitableNone:
|
|
57
|
+
self.file.close()
|
|
58
|
+
return AwaitableNone()
|
|
@@ -506,8 +506,53 @@ struct PyUploadFile {
|
|
|
506
506
|
|
|
507
507
|
#[pymethods]
|
|
508
508
|
impl PyUploadFile {
|
|
509
|
-
|
|
510
|
-
|
|
509
|
+
#[new]
|
|
510
|
+
#[pyo3(signature = (filename="".to_string(), content_type="".to_string(), file_data=Vec::new()))]
|
|
511
|
+
fn new(filename: String, content_type: String, file_data: Vec<u8>) -> Self {
|
|
512
|
+
PyUploadFile { filename, content_type, file_data }
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
#[pyo3(signature = (_size=-1))]
|
|
516
|
+
fn read(&self, py: Python<'_>, _size: i64) -> PyResult<PyObject> {
|
|
517
|
+
let bytes_obj = PyBytes::new_bound(py, &self.file_data);
|
|
518
|
+
if let Ok(py_module) = py.import_bound("rustapi.uploads") {
|
|
519
|
+
if let Ok(awaitable_cls) = py_module.getattr("AwaitableBytes") {
|
|
520
|
+
if let Ok(res) = awaitable_cls.call1((&bytes_obj,)) {
|
|
521
|
+
return Ok(res.unbind());
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
Ok(bytes_obj.into())
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
#[pyo3(signature = (_offset=0))]
|
|
529
|
+
fn seek(&self, py: Python<'_>, _offset: i64) -> PyResult<PyObject> {
|
|
530
|
+
if let Ok(py_module) = py.import_bound("rustapi.uploads") {
|
|
531
|
+
if let Ok(awaitable_cls) = py_module.getattr("AwaitableInt") {
|
|
532
|
+
if let Ok(res) = awaitable_cls.call1((0,)) {
|
|
533
|
+
return Ok(res.unbind());
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
Ok(0.to_object(py))
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
fn close(&self, py: Python<'_>) -> PyResult<PyObject> {
|
|
541
|
+
if let Ok(py_module) = py.import_bound("rustapi.uploads") {
|
|
542
|
+
if let Ok(awaitable_cls) = py_module.getattr("AwaitableNone") {
|
|
543
|
+
if let Ok(res) = awaitable_cls.call0() {
|
|
544
|
+
return Ok(res.unbind());
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
Ok(py.None())
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
#[getter]
|
|
552
|
+
fn file(&self, py: Python<'_>) -> PyResult<PyObject> {
|
|
553
|
+
let io = py.import_bound("io")?;
|
|
554
|
+
let bytes_obj = PyBytes::new_bound(py, &self.file_data);
|
|
555
|
+
io.call_method1("BytesIO", (bytes_obj,)).map(|b| b.unbind())
|
|
511
556
|
}
|
|
512
557
|
}
|
|
513
558
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?><testsuites name="pytest tests"><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="68" time="2.909" timestamp="2026-08-05T23:40:38.618470+05:30" hostname="boopathirajsMBP.lan"><testcase classname="tests.test_3tier_architecture" name="test_tier1_pure_python" time="0.015" /><testcase classname="tests.test_3tier_architecture" name="test_tier2_hybrid_rust_db" time="0.004" /><testcase classname="tests.test_3tier_architecture" name="test_tier2_hybrid_rust_template" time="0.005" /><testcase classname="tests.test_3tier_architecture" name="test_tier2_hybrid_rust_jwt" time="0.005" /><testcase classname="tests.test_3tier_architecture" name="test_tier3_native_rust_routes" time="0.009" /><testcase classname="tests.test_basic" name="test_root_endpoint" time="0.008" /><testcase classname="tests.test_basic" name="test_sync_endpoint" time="0.016" /><testcase classname="tests.test_basic" name="test_async_endpoint" time="0.106" /><testcase classname="tests.test_basic" name="test_path_and_query_params" time="0.005" /><testcase classname="tests.test_basic" name="test_pydantic_validation" time="0.004" /><testcase classname="tests.test_basic" name="test_openapi_generation" time="0.015" /><testcase classname="tests.test_basic" name="test_docs_endpoint_serves_swagger_ui" time="0.003" /><testcase classname="tests.test_basic" name="test_not_found_error" time="0.003" /><testcase classname="tests.test_basic" name="test_mcp_initialize" time="0.003" /><testcase classname="tests.test_basic" name="test_mcp_tools_list" time="0.003" /><testcase classname="tests.test_basic" name="test_mcp_tools_call" time="0.004" /><testcase classname="tests.test_basic" name="test_mcp_tools_call_string_result" time="0.003" /><testcase classname="tests.test_basic" name="test_mcp_tools_call_unknown" time="0.004" /><testcase classname="tests.test_basic" name="test_mcp_resources_list_and_read" time="0.021" /><testcase classname="tests.test_basic" name="test_mcp_resources_read_unknown" time="0.003" /><testcase classname="tests.test_basic" name="test_mcp_prompts_list_and_get" time="0.006" /><testcase classname="tests.test_basic" name="test_mcp_ping" time="0.004" /><testcase classname="tests.test_basic" name="test_mcp_unknown_method" time="0.004" /><testcase classname="tests.test_basic" name="test_mcp_notification_no_response_body" time="0.003" /><testcase classname="tests.test_database_engine" name="test_native_database_execute_and_query" time="0.007" /><testcase classname="tests.test_database_engine" name="test_direct_db_methods" time="0.002" /><testcase classname="tests.test_fastapi_all_6_mismatches" name="test_1_apirouter_include_router" time="0.002" /><testcase classname="tests.test_fastapi_all_6_mismatches" name="test_2_fastapi_constructor_kwargs" time="0.002" /><testcase classname="tests.test_fastapi_all_6_mismatches" name="test_3_exception_handlers" time="0.004" /><testcase classname="tests.test_fastapi_all_6_mismatches" name="test_4_http_bearer_and_credentials" time="0.001" /><testcase classname="tests.test_fastapi_all_6_mismatches" name="test_5_dependency_resolver_with_request_and_nested_depends" time="0.008" /><testcase classname="tests.test_fastapi_all_6_mismatches" name="test_6_upload_file_sync_and_async_methods" time="0.001" /><testcase classname="tests.test_fastapi_compatibility" name="test_fastapi_and_request_aliases" time="0.000" /><testcase classname="tests.test_fastapi_compatibility" name="test_status_codes" time="0.001" /><testcase classname="tests.test_fastapi_compatibility" name="test_openapi_docs_html_generation" time="0.001" /><testcase classname="tests.test_fastapi_compatibility" name="test_jsonable_encoder" time="0.002" /><testcase classname="tests.test_fastapi_compatibility" name="test_sse_helpers" time="0.001" /><testcase classname="tests.test_fastapi_compatibility" name="test_param_functions" time="0.001" /><testcase classname="tests.test_fastapi_compatibility" name="test_security_helpers" time="0.001" /><testcase classname="tests.test_fastapi_compatibility" name="test_websocket_exceptions" time="0.052" /><testcase classname="tests.test_fastapi_compatibility" name="test_frontend_app_serving" time="0.008" /><testcase classname="tests.test_fastapi_compatibility" name="test_app_and_router_advanced_fastapi_features" time="0.005" /><testcase classname="tests.test_fastapi_compatibility" name="test_user_router_tags_in_openapi_spec" time="0.311" /><testcase classname="tests.test_file_uploads_and_dx" name="test_single_file_upload" time="0.007" /><testcase classname="tests.test_file_uploads_and_dx" name="test_binary_image_upload" time="0.023" /><testcase classname="tests.test_file_uploads_and_dx" name="test_multiple_file_upload" time="0.004" /><testcase classname="tests.test_file_uploads_and_dx" name="test_custom_openapi_route" time="0.003" /><testcase classname="tests.test_imports" name="test_public_package_exports_work" time="0.001" /><testcase classname="tests.test_imports" name="test_pyrustapi_alias_import_works" time="0.001" /><testcase classname="tests.test_imports" name="test_repo_root_import_prefers_workspace_package" time="0.261" /><testcase classname="tests.test_native_routes" name="test_native_route_registration_and_execution" time="0.003" /><testcase classname="tests.test_overrides_and_coercion" name="test_dependency_overrides" time="0.009" /><testcase classname="tests.test_overrides_and_coercion" name="test_strict_type_coercion_valid_int" time="0.005" /><testcase classname="tests.test_overrides_and_coercion" name="test_strict_type_coercion_invalid_int_returns_422" time="0.006" /><testcase classname="tests.test_overrides_and_coercion" name="test_missing_required_query_param_returns_422" time="0.007" /><testcase classname="tests.test_reload_and_logging" name="test_reload_mode_spawns_server_and_outputs_logs" time="0.811" /><testcase classname="tests.test_response_model" name="test_single_response_model_filters_private_fields" time="0.022" /><testcase classname="tests.test_response_model" name="test_list_response_model_filters_items" time="0.004" /><testcase classname="tests.test_responses_and_req_json" name="test_html_response" time="0.009" /><testcase classname="tests.test_responses_and_req_json" name="test_plain_text_response" time="0.004" /><testcase classname="tests.test_responses_and_req_json" name="test_json_response" time="0.005" /><testcase classname="tests.test_responses_and_req_json" name="test_redirect_response" time="0.008" /><testcase classname="tests.test_responses_and_req_json" name="test_req_json_parsing" time="0.087" /><testcase classname="tests.test_rust_power_modules" name="test_jwt_encode_and_decode" time="0.007" /><testcase classname="tests.test_rust_power_modules" name="test_jwt_decode_invalid_secret_raises_value_error" time="0.001" /><testcase classname="tests.test_rust_power_modules" name="test_argon2_password_hashing_and_verification" time="0.136" /><testcase classname="tests.test_rust_power_modules" name="test_minijinja_template_rendering" time="0.001" /><testcase classname="tests.test_rust_power_modules" name="test_integrated_http_auth_route" time="0.075" /></testsuite></testsuites>
|