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.
Files changed (116) hide show
  1. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/Cargo.lock +1 -1
  2. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/Cargo.toml +1 -1
  3. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/PKG-INFO +1 -1
  4. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/fastapi_compatibility_and_features.md +68 -0
  5. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/pyproject.toml +1 -1
  6. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/__init__.py +30 -2
  7. pyrustapi-0.3.86/python/rustapi/resolver.py +66 -0
  8. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/router.py +29 -0
  9. pyrustapi-0.3.86/python/rustapi/security/__init__.py +178 -0
  10. pyrustapi-0.3.86/python/rustapi/uploads.py +58 -0
  11. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/src/lib.rs +47 -2
  12. pyrustapi-0.3.86/test-reports/junit.xml +1 -0
  13. pyrustapi-0.3.86/tests/test_fastapi_all_6_mismatches.py +113 -0
  14. pyrustapi-0.3.86/tests/test_python_ecosystem_compatibility.py +114 -0
  15. pyrustapi-0.3.33/python/rustapi/security/__init__.py +0 -80
  16. pyrustapi-0.3.33/test-reports/junit.xml +0 -1
  17. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/.github/workflows/CI.yml +0 -0
  18. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/.github/workflows/pages.yml +0 -0
  19. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/.github/workflows/release.yml +0 -0
  20. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/.github/workflows/static.yml +0 -0
  21. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/.gitignore +0 -0
  22. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/LICENSE +0 -0
  23. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/README.md +0 -0
  24. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/ROADMAP.md +0 -0
  25. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmark_dashboard.html +0 -0
  26. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/benchmark.sh +0 -0
  27. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/fastapi_app.py +0 -0
  28. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/fastapi_cpu_app.py +0 -0
  29. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/fastapi_edge_app.py +0 -0
  30. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/fastapi_oha.py +0 -0
  31. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/large.json +0 -0
  32. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/mixed.json +0 -0
  33. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/rustapi_app.py +0 -0
  34. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/rustapi_cpu_app.py +0 -0
  35. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/rustapi_edge_app.py +0 -0
  36. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/rustapi_multiworker_app.py +0 -0
  37. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/rustapi_oha.py +0 -0
  38. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/rustapi_tier3_app.py +0 -0
  39. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/tier_app.py +0 -0
  40. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/data/validate.json +0 -0
  41. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/run_3tier_benchmark.py +0 -0
  42. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/run_comparison_benchmark.py +0 -0
  43. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/run_cpu_stability_benchmark.py +0 -0
  44. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/run_edgecase_benchmark.py +0 -0
  45. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/run_native_tier3_benchmark.py +0 -0
  46. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/run_real_oha_benchmark.py +0 -0
  47. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks/run_release_highspeed_benchmark.py +0 -0
  48. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/benchmarks.html +0 -0
  49. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/crud_benchmark/benchmark_db.sqlite +0 -0
  50. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/crud_benchmark/benchmark_db.sqlite-shm +0 -0
  51. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/crud_benchmark/benchmark_db.sqlite-wal +0 -0
  52. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/crud_benchmark/benchmark_results.md +0 -0
  53. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/crud_benchmark/fastapi_crud.py +0 -0
  54. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/crud_benchmark/run_crud_benchmark.py +0 -0
  55. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/crud_benchmark/rustapi_crud.py +0 -0
  56. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/3tier_architecture_and_benchmarks.md +0 -0
  57. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/DeepAnalytics.md +0 -0
  58. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/api-reference.html +0 -0
  59. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/benchmark_dashboard.html +0 -0
  60. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/benchmarks.html +0 -0
  61. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/database.html +0 -0
  62. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/database_engine.md +0 -0
  63. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/docs.md +0 -0
  64. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/getting-started.html +0 -0
  65. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/getting_started.md +0 -0
  66. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/index.html +0 -0
  67. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/mcp-server.html +0 -0
  68. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/mcp_server.md +0 -0
  69. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/native_business_logic.md +0 -0
  70. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/performance_and_benchmarks.md +0 -0
  71. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/power_primitives.md +0 -0
  72. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/pyrustapi-logo (1).png +0 -0
  73. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/report_01_.md +0 -0
  74. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/response_types.md +0 -0
  75. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/security-power-modules.html +0 -0
  76. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/docs/swagger_ui_and_dx_deviations.md +0 -0
  77. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/examples/app.py +0 -0
  78. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/index.html +0 -0
  79. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/performance-reports/cpu_intensive_stability_report.md +0 -0
  80. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/performance-reports/fastapi_vs_hybrid_vs_native.md +0 -0
  81. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/performance-reports/fastapi_vs_rustapi_comparison.md +0 -0
  82. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/performance-reports/fastapi_vs_rustapi_crud_benchmark.md +0 -0
  83. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/performance-reports/performance_and_benchmarks.md +0 -0
  84. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/performance-reports/rust_native_edgecases_report.md +0 -0
  85. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/performance-reports/three_tier_architecture_evaluation.md +0 -0
  86. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/pyrustapi-logo.png +0 -0
  87. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/pyrustapi/__init__.py +0 -0
  88. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/__init__.pyi +0 -0
  89. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/background.py +0 -0
  90. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/depends.py +0 -0
  91. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/encoders.py +0 -0
  92. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/exceptions.py +0 -0
  93. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/middleware/__init__.py +0 -0
  94. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/middleware/cors.py +0 -0
  95. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/openapi/__init__.py +0 -0
  96. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/openapi/docs.py +0 -0
  97. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/param_functions.py +0 -0
  98. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/py.typed +0 -0
  99. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/responses/__init__.py +0 -0
  100. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/rust.rs +0 -0
  101. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/sse.py +0 -0
  102. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/staticfiles.py +0 -0
  103. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/python/rustapi/status.py +0 -0
  104. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/scratch/.gitkeep +0 -0
  105. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_3tier_architecture.py +0 -0
  106. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_basic.py +0 -0
  107. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_database_engine.py +0 -0
  108. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_fastapi_compatibility.py +0 -0
  109. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_file_uploads_and_dx.py +0 -0
  110. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_imports.py +0 -0
  111. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_native_routes.py +0 -0
  112. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_overrides_and_coercion.py +0 -0
  113. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_reload_and_logging.py +0 -0
  114. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_response_model.py +0 -0
  115. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/tests/test_responses_and_req_json.py +0 -0
  116. {pyrustapi-0.3.33 → pyrustapi-0.3.86}/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.33"
1430
+ version = "0.3.86"
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.33"
3
+ version = "0.3.86"
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.33
3
+ Version: 0.3.86
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: Programming Language :: Python :: 3
@@ -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
  ```
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "pyrustapi"
7
- version = "0.3.33"
7
+ version = "0.3.86"
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"
@@ -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 __init__(self, *args, **kwargs):
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.33"
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
- fn read(&self, py: Python<'_>) -> PyObject {
510
- PyBytes::new_bound(py, &self.file_data).into()
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>