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.
Files changed (114) hide show
  1. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/Cargo.lock +1 -1
  2. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/Cargo.toml +1 -1
  3. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/PKG-INFO +1 -1
  4. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/fastapi_compatibility_and_features.md +32 -17
  5. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/pyproject.toml +1 -1
  6. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/__init__.py +63 -1
  7. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/resolver.py +16 -2
  8. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/uploads.py +8 -0
  9. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/src/lib.rs +168 -69
  10. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_fastapi_all_6_mismatches.py +38 -0
  11. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/.github/workflows/CI.yml +0 -0
  12. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/.github/workflows/pages.yml +0 -0
  13. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/.github/workflows/release.yml +0 -0
  14. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/.github/workflows/static.yml +0 -0
  15. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/.gitignore +0 -0
  16. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/LICENSE +0 -0
  17. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/README.md +0 -0
  18. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/ROADMAP.md +0 -0
  19. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmark_dashboard.html +0 -0
  20. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/benchmark.sh +0 -0
  21. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/fastapi_app.py +0 -0
  22. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/fastapi_cpu_app.py +0 -0
  23. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/fastapi_edge_app.py +0 -0
  24. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/fastapi_oha.py +0 -0
  25. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/large.json +0 -0
  26. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/mixed.json +0 -0
  27. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/rustapi_app.py +0 -0
  28. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/rustapi_cpu_app.py +0 -0
  29. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/rustapi_edge_app.py +0 -0
  30. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/rustapi_multiworker_app.py +0 -0
  31. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/rustapi_oha.py +0 -0
  32. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/rustapi_tier3_app.py +0 -0
  33. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/tier_app.py +0 -0
  34. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/data/validate.json +0 -0
  35. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/run_3tier_benchmark.py +0 -0
  36. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/run_comparison_benchmark.py +0 -0
  37. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/run_cpu_stability_benchmark.py +0 -0
  38. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/run_edgecase_benchmark.py +0 -0
  39. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/run_native_tier3_benchmark.py +0 -0
  40. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/run_real_oha_benchmark.py +0 -0
  41. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks/run_release_highspeed_benchmark.py +0 -0
  42. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/benchmarks.html +0 -0
  43. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/crud_benchmark/benchmark_db.sqlite +0 -0
  44. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/crud_benchmark/benchmark_db.sqlite-shm +0 -0
  45. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/crud_benchmark/benchmark_db.sqlite-wal +0 -0
  46. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/crud_benchmark/benchmark_results.md +0 -0
  47. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/crud_benchmark/fastapi_crud.py +0 -0
  48. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/crud_benchmark/run_crud_benchmark.py +0 -0
  49. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/crud_benchmark/rustapi_crud.py +0 -0
  50. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/3tier_architecture_and_benchmarks.md +0 -0
  51. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/DeepAnalytics.md +0 -0
  52. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/api-reference.html +0 -0
  53. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/benchmark_dashboard.html +0 -0
  54. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/benchmarks.html +0 -0
  55. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/database.html +0 -0
  56. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/database_engine.md +0 -0
  57. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/docs.md +0 -0
  58. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/getting-started.html +0 -0
  59. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/getting_started.md +0 -0
  60. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/index.html +0 -0
  61. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/mcp-server.html +0 -0
  62. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/mcp_server.md +0 -0
  63. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/native_business_logic.md +0 -0
  64. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/performance_and_benchmarks.md +0 -0
  65. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/power_primitives.md +0 -0
  66. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/pyrustapi-logo (1).png +0 -0
  67. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/report_01_.md +0 -0
  68. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/response_types.md +0 -0
  69. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/security-power-modules.html +0 -0
  70. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/docs/swagger_ui_and_dx_deviations.md +0 -0
  71. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/examples/app.py +0 -0
  72. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/index.html +0 -0
  73. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/performance-reports/cpu_intensive_stability_report.md +0 -0
  74. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/performance-reports/fastapi_vs_hybrid_vs_native.md +0 -0
  75. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/performance-reports/fastapi_vs_rustapi_comparison.md +0 -0
  76. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/performance-reports/fastapi_vs_rustapi_crud_benchmark.md +0 -0
  77. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/performance-reports/performance_and_benchmarks.md +0 -0
  78. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/performance-reports/rust_native_edgecases_report.md +0 -0
  79. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/performance-reports/three_tier_architecture_evaluation.md +0 -0
  80. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/pyrustapi-logo.png +0 -0
  81. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/pyrustapi/__init__.py +0 -0
  82. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/__init__.pyi +0 -0
  83. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/background.py +0 -0
  84. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/depends.py +0 -0
  85. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/encoders.py +0 -0
  86. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/exceptions.py +0 -0
  87. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/middleware/__init__.py +0 -0
  88. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/middleware/cors.py +0 -0
  89. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/openapi/__init__.py +0 -0
  90. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/openapi/docs.py +0 -0
  91. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/param_functions.py +0 -0
  92. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/py.typed +0 -0
  93. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/responses/__init__.py +0 -0
  94. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/router.py +0 -0
  95. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/rust.rs +0 -0
  96. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/security/__init__.py +0 -0
  97. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/sse.py +0 -0
  98. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/staticfiles.py +0 -0
  99. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/python/rustapi/status.py +0 -0
  100. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/scratch/.gitkeep +0 -0
  101. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/test-reports/junit.xml +0 -0
  102. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_3tier_architecture.py +0 -0
  103. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_basic.py +0 -0
  104. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_database_engine.py +0 -0
  105. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_fastapi_compatibility.py +0 -0
  106. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_file_uploads_and_dx.py +0 -0
  107. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_imports.py +0 -0
  108. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_native_routes.py +0 -0
  109. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_overrides_and_coercion.py +0 -0
  110. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_python_ecosystem_compatibility.py +0 -0
  111. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_reload_and_logging.py +0 -0
  112. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_response_model.py +0 -0
  113. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_responses_and_req_json.py +0 -0
  114. {pyrustapi-0.3.86 → pyrustapi-0.8.6}/tests/test_rust_power_modules.py +0 -0
@@ -1427,7 +1427,7 @@ dependencies = [
1427
1427
 
1428
1428
  [[package]]
1429
1429
  name = "rustapi"
1430
- version = "0.3.86"
1430
+ version = "0.8.6"
1431
1431
  dependencies = [
1432
1432
  "argon2",
1433
1433
  "base64 0.23.0",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rustapi"
3
- version = "0.3.86"
3
+ version = "0.8.6"
4
4
  edition = "2021"
5
5
  description = "A FastAPI-style Python web framework with a Rust tokio/hyper core, plus a built-in MCP server (JSON-RPC over Streamable HTTP)."
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyrustapi
3
- Version: 0.3.86
3
+ Version: 0.8.6
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: Programming Language :: Python :: 3
@@ -219,21 +219,36 @@ async def analyze(req: Request):
219
219
 
220
220
  ---
221
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`.
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
- ```
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "pyrustapi"
7
- version = "0.3.86"
7
+ version = "0.8.6"
8
8
  description = "A FastAPI-style Python web framework with a Rust tokio/hyper core, and a built-in MCP server."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -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.3.86"
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
- doc["components"] = json!({ "schemas": components_schemas });
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
- let dict = pyo3::types::PyDict::new_bound(py);
628
- return Ok(dict.unbind().into());
629
- }
630
- if let Ok(val) = serde_json::from_str::<serde_json::Value>(&self.body) {
631
- return serde_to_pyobject(py, &val);
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
- let py_json = py.import_bound("json")?;
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 mut dependency_error: Option<String> = None;
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 mut dep_cache = HashMap::<isize, PyObject>::new();
1831
- let mut teardown_gens = Vec::<PyObject>::new();
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
- if dep.use_cache {
1846
- if let Some(cached) = dep_cache.get(&dep.id) {
1847
- let v = Python::with_gil(|py| cached.clone_ref(py));
1848
- resolved_args.insert(dep.name.clone(), v);
1849
- continue;
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
- let dep_res: Result<PyObject, String> = if dep._is_async {
1854
- let coro = Python::with_gil(|py| dep_func.call0(py).map_err(|e| e.to_string()));
1855
- match coro {
1856
- Ok(c) => {
1857
- let (tx, rx) = oneshot::channel();
1858
- let sc = schedule_coro.clone();
1859
- Python::with_gil(|py| {
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
- Err(e) => Err(e),
1871
- }
1872
- } else {
1873
- let sem = gil_sem.clone();
1874
- let dep_func_c = Python::with_gil(|py| dep_func.clone_ref(py));
1875
- tokio::task::spawn_blocking(move || {
1876
- let _permit = sem.try_acquire().ok();
1877
- Python::with_gil(|py| dep_func_c.call0(py).map_err(|e| e.to_string()))
1878
- })
1879
- .await
1880
- .unwrap_or_else(|_| Err("Panic".to_string()))
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
- match dep_res {
1884
- Ok(obj) => {
1885
- let val = Python::with_gil(|py| -> Result<PyObject, String> {
1886
- if dep.is_generator {
1887
- Ok(py.import_bound("builtins").unwrap().call_method1("next", (&obj,)).unwrap().into())
1888
- } else {
1889
- Ok(obj.clone_ref(py))
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
- match val {
1893
- Ok(v) => {
1894
- if dep.is_generator { teardown_gens.push(obj); }
1895
- if dep.use_cache { dep_cache.insert(dep.id, Python::with_gil(|py| v.clone_ref(py))); }
1896
- resolved_args.insert(dep.name, v);
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(e) => { dependency_error = Some(e); break; }
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(err_msg) = dependency_error {
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