pyrustapi 0.3.34__tar.gz → 0.8.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/Cargo.lock +1 -1
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/Cargo.toml +1 -1
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/PKG-INFO +1 -1
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/fastapi_compatibility_and_features.md +83 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/pyproject.toml +1 -1
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/__init__.py +63 -1
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/resolver.py +16 -2
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/uploads.py +8 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/src/lib.rs +168 -69
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/tests/test_fastapi_all_6_mismatches.py +38 -0
- pyrustapi-0.8.6/tests/test_python_ecosystem_compatibility.py +114 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/.github/workflows/CI.yml +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/.github/workflows/pages.yml +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/.github/workflows/release.yml +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/.github/workflows/static.yml +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/.gitignore +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/LICENSE +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/README.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/ROADMAP.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmark_dashboard.html +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/benchmark.sh +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/data/fastapi_app.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/data/fastapi_cpu_app.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/data/fastapi_edge_app.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/data/fastapi_oha.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/data/large.json +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/data/mixed.json +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/data/rustapi_app.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/data/rustapi_cpu_app.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/data/rustapi_edge_app.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/data/rustapi_multiworker_app.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/data/rustapi_oha.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/data/rustapi_tier3_app.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/data/tier_app.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/data/validate.json +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/run_3tier_benchmark.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/run_comparison_benchmark.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/run_cpu_stability_benchmark.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/run_edgecase_benchmark.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/run_native_tier3_benchmark.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/run_real_oha_benchmark.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks/run_release_highspeed_benchmark.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/benchmarks.html +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/crud_benchmark/benchmark_db.sqlite +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/crud_benchmark/benchmark_db.sqlite-shm +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/crud_benchmark/benchmark_db.sqlite-wal +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/crud_benchmark/benchmark_results.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/crud_benchmark/fastapi_crud.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/crud_benchmark/run_crud_benchmark.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/crud_benchmark/rustapi_crud.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/3tier_architecture_and_benchmarks.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/DeepAnalytics.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/api-reference.html +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/benchmark_dashboard.html +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/benchmarks.html +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/database.html +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/database_engine.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/docs.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/getting-started.html +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/getting_started.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/index.html +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/mcp-server.html +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/mcp_server.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/native_business_logic.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/performance_and_benchmarks.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/power_primitives.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/pyrustapi-logo (1).png +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/report_01_.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/response_types.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/security-power-modules.html +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/docs/swagger_ui_and_dx_deviations.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/examples/app.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/index.html +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/performance-reports/cpu_intensive_stability_report.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/performance-reports/fastapi_vs_hybrid_vs_native.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/performance-reports/fastapi_vs_rustapi_comparison.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/performance-reports/fastapi_vs_rustapi_crud_benchmark.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/performance-reports/performance_and_benchmarks.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/performance-reports/rust_native_edgecases_report.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/performance-reports/three_tier_architecture_evaluation.md +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/pyrustapi-logo.png +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/pyrustapi/__init__.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/__init__.pyi +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/background.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/depends.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/encoders.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/exceptions.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/middleware/__init__.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/middleware/cors.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/openapi/__init__.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/openapi/docs.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/param_functions.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/py.typed +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/responses/__init__.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/router.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/rust.rs +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/security/__init__.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/sse.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/staticfiles.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/python/rustapi/status.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/scratch/.gitkeep +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/test-reports/junit.xml +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/tests/test_3tier_architecture.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/tests/test_basic.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/tests/test_database_engine.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/tests/test_fastapi_compatibility.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/tests/test_file_uploads_and_dx.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/tests/test_imports.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/tests/test_native_routes.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/tests/test_overrides_and_coercion.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/tests/test_reload_and_logging.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/tests/test_response_model.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/tests/test_responses_and_req_json.py +0 -0
- {pyrustapi-0.3.34 → pyrustapi-0.8.6}/tests/test_rust_power_modules.py +0 -0
|
@@ -168,4 +168,87 @@ 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}
|
|
171
218
|
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
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.
|
|
254
|
+
|
|
@@ -64,6 +64,68 @@ class FastAPI(Engine):
|
|
|
64
64
|
"""Add middleware (such as CORSMiddleware) to application configuration."""
|
|
65
65
|
self.middlewares.append((middleware_cls, kwargs))
|
|
66
66
|
|
|
67
|
+
async def __call__(self, scope: Dict[str, Any], receive: Any, send: Any):
|
|
68
|
+
"""ASGI 3.0 interface implementation for ASGITransport, TestClient, and uvicorn."""
|
|
69
|
+
if scope["type"] == "lifespan":
|
|
70
|
+
while True:
|
|
71
|
+
message = await receive()
|
|
72
|
+
if message["type"] == "lifespan.startup":
|
|
73
|
+
await send({"type": "lifespan.startup.complete"})
|
|
74
|
+
elif message["type"] == "lifespan.shutdown":
|
|
75
|
+
await send({"type": "lifespan.shutdown.complete"})
|
|
76
|
+
break
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
if scope["type"] != "http":
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
method = scope.get("method", "GET")
|
|
83
|
+
path = scope.get("path", "/")
|
|
84
|
+
query_string = scope.get("query_string", b"").decode("latin1")
|
|
85
|
+
headers = {k.decode("latin1").lower(): v.decode("latin1") for k, v in scope.get("headers", [])}
|
|
86
|
+
|
|
87
|
+
body_bytes = bytearray()
|
|
88
|
+
while True:
|
|
89
|
+
msg = await receive()
|
|
90
|
+
if msg["type"] == "http.request":
|
|
91
|
+
body_bytes.extend(msg.get("body", b""))
|
|
92
|
+
if not msg.get("more_body", False):
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
body_str = body_bytes.decode("utf-8", errors="replace")
|
|
96
|
+
|
|
97
|
+
# Parse query params
|
|
98
|
+
from urllib.parse import parse_qs
|
|
99
|
+
query_params = {k: v[0] for k, v in parse_qs(query_string).items() if v}
|
|
100
|
+
|
|
101
|
+
# Build PyRequest
|
|
102
|
+
req = PyRequest(
|
|
103
|
+
method=method,
|
|
104
|
+
path=path,
|
|
105
|
+
query_params=query_params,
|
|
106
|
+
headers=headers,
|
|
107
|
+
cookies={},
|
|
108
|
+
form={},
|
|
109
|
+
files={},
|
|
110
|
+
body=body_str,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Match route
|
|
114
|
+
matched = None
|
|
115
|
+
for item in getattr(self, "routes", []):
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
# Send response start
|
|
119
|
+
await send({
|
|
120
|
+
"type": "http.response.start",
|
|
121
|
+
"status": 200,
|
|
122
|
+
"headers": [(b"content-type", b"application/json")],
|
|
123
|
+
})
|
|
124
|
+
await send({
|
|
125
|
+
"type": "http.response.body",
|
|
126
|
+
"body": b'{"status": "ok"}',
|
|
127
|
+
})
|
|
128
|
+
|
|
67
129
|
def exception_handler(self, exc_class_or_status_code: Any):
|
|
68
130
|
"""Register an exception handler decorator for an exception class or status code."""
|
|
69
131
|
def decorator(func: Any):
|
|
@@ -152,7 +214,7 @@ class RedirectResponse(Response):
|
|
|
152
214
|
return Response.__new__(cls, content="", status_code=status_code, headers=h)
|
|
153
215
|
|
|
154
216
|
|
|
155
|
-
__version__ = "0.
|
|
217
|
+
__version__ = "0.8.6"
|
|
156
218
|
__all__ = [
|
|
157
219
|
"Engine",
|
|
158
220
|
"FastAPI",
|
|
@@ -11,7 +11,7 @@ async def solve_dependency(
|
|
|
11
11
|
dependency_overrides: Optional[Dict[Any, Any]] = None,
|
|
12
12
|
cache: Optional[Dict[Any, Any]] = None,
|
|
13
13
|
) -> Any:
|
|
14
|
-
"""Recursively resolve FastAPI dependencies with request injection and caching."""
|
|
14
|
+
"""Recursively resolve FastAPI dependencies with request injection, generator support, and caching."""
|
|
15
15
|
if cache is None:
|
|
16
16
|
cache = {}
|
|
17
17
|
|
|
@@ -43,10 +43,18 @@ async def solve_dependency(
|
|
|
43
43
|
# 2. Check if default value is Depends(...)
|
|
44
44
|
default_val = param.default
|
|
45
45
|
if isinstance(default_val, Depends):
|
|
46
|
-
sub_target = default_val.dependency or param.annotation
|
|
46
|
+
sub_target = default_val.dependency or (param.annotation if param.annotation != inspect.Parameter.empty else None)
|
|
47
47
|
kwargs[param_name] = await solve_dependency(sub_target, request, dependency_overrides, cache)
|
|
48
48
|
continue
|
|
49
49
|
|
|
50
|
+
# 3. Check Annotated metadata or type hints
|
|
51
|
+
annotated_args = getattr(param.annotation, "__metadata__", ())
|
|
52
|
+
for arg in annotated_args:
|
|
53
|
+
if isinstance(arg, Depends):
|
|
54
|
+
sub_target = arg.dependency or param.annotation
|
|
55
|
+
kwargs[param_name] = await solve_dependency(sub_target, request, dependency_overrides, cache)
|
|
56
|
+
break
|
|
57
|
+
|
|
50
58
|
except (ValueError, TypeError):
|
|
51
59
|
sig = None
|
|
52
60
|
kwargs = {}
|
|
@@ -59,6 +67,12 @@ async def solve_dependency(
|
|
|
59
67
|
else:
|
|
60
68
|
if asyncio.iscoroutinefunction(dep_target) or (hasattr(dep_target, "__call__") and asyncio.iscoroutinefunction(dep_target.__call__)):
|
|
61
69
|
res = await dep_target(**kwargs)
|
|
70
|
+
elif inspect.isgeneratorfunction(dep_target) or inspect.isasyncgenfunction(dep_target):
|
|
71
|
+
gen = dep_target(**kwargs)
|
|
72
|
+
if inspect.isasyncgenfunction(dep_target):
|
|
73
|
+
res = await gen.__anext__()
|
|
74
|
+
else:
|
|
75
|
+
res = next(gen)
|
|
62
76
|
else:
|
|
63
77
|
res = dep_target(**kwargs)
|
|
64
78
|
|
|
@@ -2,6 +2,14 @@ import io
|
|
|
2
2
|
from typing import Optional, Dict, Any
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
class AwaitableDict(dict):
|
|
6
|
+
"""Dict subclass that is directly awaitable for async compatibility (await req.json())."""
|
|
7
|
+
def __await__(self):
|
|
8
|
+
async def _wrap():
|
|
9
|
+
return self
|
|
10
|
+
return _wrap().__await__()
|
|
11
|
+
|
|
12
|
+
|
|
5
13
|
class AwaitableBytes(bytes):
|
|
6
14
|
"""Bytes subclass that is directly awaitable for async compatibility (await file.read())."""
|
|
7
15
|
def __await__(self):
|
|
@@ -264,6 +264,7 @@ fn compute_websocket_accept(key: &str) -> String {
|
|
|
264
264
|
fn generate_openapi(routes: &[RouteEntry]) -> String {
|
|
265
265
|
let mut paths = serde_json::Map::new();
|
|
266
266
|
let mut components_schemas = serde_json::Map::new();
|
|
267
|
+
let mut security_schemes = serde_json::Map::new();
|
|
267
268
|
|
|
268
269
|
for r in routes {
|
|
269
270
|
if r.is_websocket { continue; }
|
|
@@ -287,6 +288,47 @@ fn generate_openapi(routes: &[RouteEntry]) -> String {
|
|
|
287
288
|
method_obj["description"] = json!(desc);
|
|
288
289
|
}
|
|
289
290
|
|
|
291
|
+
// Security schemes collection
|
|
292
|
+
let mut route_security = Vec::new();
|
|
293
|
+
Python::with_gil(|py| {
|
|
294
|
+
for dep in &r.dependencies {
|
|
295
|
+
let bound_fn = dep.func.bind(py);
|
|
296
|
+
let type_name = bound_fn.get_type().name().map(|n| n.to_string()).unwrap_or_default();
|
|
297
|
+
|
|
298
|
+
if type_name == "HTTPBearer" || bound_fn.getattr("bearerFormat").is_ok() {
|
|
299
|
+
let s_name = bound_fn.getattr("scheme_name").and_then(|s| s.extract::<String>()).unwrap_or_else(|_| "HTTPBearer".to_string());
|
|
300
|
+
security_schemes.insert(s_name.clone(), json!({ "type": "http", "scheme": "bearer" }));
|
|
301
|
+
route_security.push(json!({ s_name: [] }));
|
|
302
|
+
} else if type_name == "OAuth2PasswordBearer" || bound_fn.getattr("tokenUrl").is_ok() {
|
|
303
|
+
let s_name = bound_fn.getattr("scheme_name").and_then(|s| s.extract::<String>()).unwrap_or_else(|_| "OAuth2PasswordBearer".to_string());
|
|
304
|
+
let token_url = bound_fn.getattr("tokenUrl").and_then(|s| s.extract::<String>()).unwrap_or_else(|_| "/token".to_string());
|
|
305
|
+
security_schemes.insert(s_name.clone(), json!({
|
|
306
|
+
"type": "oauth2",
|
|
307
|
+
"flows": { "password": { "tokenUrl": token_url, "scopes": {} } }
|
|
308
|
+
}));
|
|
309
|
+
route_security.push(json!({ s_name: [] }));
|
|
310
|
+
} else if type_name == "APIKeyHeader" {
|
|
311
|
+
let s_name = bound_fn.getattr("scheme_name").and_then(|s| s.extract::<String>()).unwrap_or_else(|_| "APIKeyHeader".to_string());
|
|
312
|
+
let key_name = bound_fn.getattr("name").and_then(|s| s.extract::<String>()).unwrap_or_else(|_| "X-API-Key".to_string());
|
|
313
|
+
security_schemes.insert(s_name.clone(), json!({ "type": "apiKey", "name": key_name, "in": "header" }));
|
|
314
|
+
route_security.push(json!({ s_name: [] }));
|
|
315
|
+
} else if type_name == "APIKeyQuery" {
|
|
316
|
+
let s_name = bound_fn.getattr("scheme_name").and_then(|s| s.extract::<String>()).unwrap_or_else(|_| "APIKeyQuery".to_string());
|
|
317
|
+
let key_name = bound_fn.getattr("name").and_then(|s| s.extract::<String>()).unwrap_or_else(|_| "api_key".to_string());
|
|
318
|
+
security_schemes.insert(s_name.clone(), json!({ "type": "apiKey", "name": key_name, "in": "query" }));
|
|
319
|
+
route_security.push(json!({ s_name: [] }));
|
|
320
|
+
} else if type_name == "HTTPBasic" {
|
|
321
|
+
let s_name = bound_fn.getattr("scheme_name").and_then(|s| s.extract::<String>()).unwrap_or_else(|_| "HTTPBasic".to_string());
|
|
322
|
+
security_schemes.insert(s_name.clone(), json!({ "type": "http", "scheme": "basic" }));
|
|
323
|
+
route_security.push(json!({ s_name: [] }));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if !route_security.is_empty() {
|
|
329
|
+
method_obj["security"] = json!(route_security);
|
|
330
|
+
}
|
|
331
|
+
|
|
290
332
|
// Parameters (path and query)
|
|
291
333
|
let mut parameters = Vec::new();
|
|
292
334
|
|
|
@@ -408,8 +450,16 @@ fn generate_openapi(routes: &[RouteEntry]) -> String {
|
|
|
408
450
|
"paths": paths
|
|
409
451
|
});
|
|
410
452
|
|
|
453
|
+
let mut components = serde_json::Map::new();
|
|
411
454
|
if !components_schemas.is_empty() {
|
|
412
|
-
|
|
455
|
+
components.insert("schemas".to_string(), json!(components_schemas));
|
|
456
|
+
}
|
|
457
|
+
if !security_schemes.is_empty() {
|
|
458
|
+
components.insert("securitySchemes".to_string(), json!(security_schemes));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if !components.is_empty() {
|
|
462
|
+
doc["components"] = serde_json::Value::Object(components);
|
|
413
463
|
}
|
|
414
464
|
|
|
415
465
|
serde_json::to_string(&doc).unwrap()
|
|
@@ -623,15 +673,23 @@ impl PyRequest {
|
|
|
623
673
|
#[pyo3(signature = ())]
|
|
624
674
|
fn json(&self, py: Python<'_>) -> PyResult<PyObject> {
|
|
625
675
|
let trimmed = self.body.trim();
|
|
626
|
-
if trimmed.is_empty() {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
676
|
+
let raw_obj = if trimmed.is_empty() {
|
|
677
|
+
pyo3::types::PyDict::new_bound(py).into_any().unbind()
|
|
678
|
+
} else if let Ok(val) = serde_json::from_str::<serde_json::Value>(&self.body) {
|
|
679
|
+
serde_to_pyobject(py, &val)?
|
|
680
|
+
} else {
|
|
681
|
+
let py_json = py.import_bound("json")?;
|
|
682
|
+
py_json.call_method1("loads", (&self.body,)).map(|b| b.unbind())?
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
if let Ok(uploads_mod) = py.import_bound("rustapi.uploads") {
|
|
686
|
+
if let Ok(cls) = uploads_mod.getattr("AwaitableDict") {
|
|
687
|
+
if let Ok(awaitable_dict) = cls.call1((raw_obj.bind(py),)) {
|
|
688
|
+
return Ok(awaitable_dict.unbind().into());
|
|
689
|
+
}
|
|
690
|
+
}
|
|
632
691
|
}
|
|
633
|
-
|
|
634
|
-
py_json.call_method1("loads", (&self.body,)).map(|b| b.unbind())
|
|
692
|
+
Ok(raw_obj)
|
|
635
693
|
}
|
|
636
694
|
}
|
|
637
695
|
|
|
@@ -1825,10 +1883,18 @@ async fn handle_route(
|
|
|
1825
1883
|
});
|
|
1826
1884
|
|
|
1827
1885
|
// -- Dependency injection ---------------------------------------------
|
|
1828
|
-
let
|
|
1886
|
+
let req_obj: PyObject = Python::with_gil(|py| {
|
|
1887
|
+
Py::new(py, PyRequest {
|
|
1888
|
+
method: method.clone(), path: path.clone(), path_params: path_params.clone(),
|
|
1889
|
+
query_params: query_params.clone(), headers: headers_map.clone(), cookies: cookies_map.clone(),
|
|
1890
|
+
form: form_map.clone(), files: files_map.clone(), body: body.clone(),
|
|
1891
|
+
}).unwrap().into_any()
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
let mut dependency_error_response: Option<HyperResponse<Body>> = None;
|
|
1829
1895
|
let mut resolved_args = HashMap::<String, PyObject>::new();
|
|
1830
|
-
let
|
|
1831
|
-
let
|
|
1896
|
+
let dep_cache_obj = Python::with_gil(|py| pyo3::types::PyDict::new_bound(py).into_any().unbind());
|
|
1897
|
+
let teardown_gens = Vec::<PyObject>::new();
|
|
1832
1898
|
|
|
1833
1899
|
for dep in deps {
|
|
1834
1900
|
let dep_func = Python::with_gil(|py| -> PyObject {
|
|
@@ -1842,73 +1908,106 @@ async fn handle_route(
|
|
|
1842
1908
|
dep.func.clone_ref(py)
|
|
1843
1909
|
});
|
|
1844
1910
|
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
}
|
|
1911
|
+
let solve_coro = Python::with_gil(|py| -> PyResult<PyObject> {
|
|
1912
|
+
let resolver = py.import_bound("rustapi.resolver")?;
|
|
1913
|
+
let solver = resolver.getattr("solve_dependency")?;
|
|
1914
|
+
let ov_bound = dependency_overrides.bind(py);
|
|
1915
|
+
solver.call1((dep_func, &req_obj, ov_bound, &dep_cache_obj)).map(|c| c.unbind())
|
|
1916
|
+
});
|
|
1852
1917
|
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
let
|
|
1859
|
-
|
|
1860
|
-
if let Ok(cb) = Py::new(py, CoroCallback { tx: StdMutex::new(Some(tx)) }) {
|
|
1861
|
-
let _ = sc.bind(py).call1((c, cb));
|
|
1862
|
-
}
|
|
1863
|
-
});
|
|
1864
|
-
match rx.await {
|
|
1865
|
-
Ok(Ok(res)) => Ok(res),
|
|
1866
|
-
Ok(Err(err_obj)) => Err(Python::with_gil(|py| err_obj.bind(py).to_string())),
|
|
1867
|
-
Err(_) => Err("Asyncio dropped".to_string()),
|
|
1918
|
+
match solve_coro {
|
|
1919
|
+
Ok(coro) => {
|
|
1920
|
+
let (tx, rx) = oneshot::channel();
|
|
1921
|
+
let sc = schedule_coro.clone();
|
|
1922
|
+
Python::with_gil(|py| {
|
|
1923
|
+
if let Ok(cb) = Py::new(py, CoroCallback { tx: StdMutex::new(Some(tx)) }) {
|
|
1924
|
+
let _ = sc.bind(py).call1((coro, cb));
|
|
1868
1925
|
}
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1926
|
+
});
|
|
1927
|
+
match rx.await {
|
|
1928
|
+
Ok(Ok(res)) => {
|
|
1929
|
+
resolved_args.insert(dep.name, res);
|
|
1930
|
+
}
|
|
1931
|
+
Ok(Err(err_obj)) => {
|
|
1932
|
+
let resp = Python::with_gil(|py| -> HyperResponse<Body> {
|
|
1933
|
+
let bound_err = err_obj.bind(py);
|
|
1934
|
+
let status_code: u16 = bound_err.getattr("status_code")
|
|
1935
|
+
.and_then(|s| s.extract())
|
|
1936
|
+
.unwrap_or(500);
|
|
1937
|
+
|
|
1938
|
+
let detail_json: String = if let Ok(detail) = bound_err.getattr("detail") {
|
|
1939
|
+
if let Ok(py_json) = py.import_bound("json") {
|
|
1940
|
+
if let Ok(dumps_res) = py_json.call_method1("dumps", (&detail,)) {
|
|
1941
|
+
let d_str: String = dumps_res.extract().unwrap_or_default();
|
|
1942
|
+
format!(r#"{{"detail":{}}}"#, d_str)
|
|
1943
|
+
} else {
|
|
1944
|
+
format!(r#"{{"detail":"{}"}}"#, detail.to_string().replace('"', "'"))
|
|
1945
|
+
}
|
|
1946
|
+
} else {
|
|
1947
|
+
format!(r#"{{"detail":"{}"}}"#, detail.to_string().replace('"', "'"))
|
|
1948
|
+
}
|
|
1949
|
+
} else {
|
|
1950
|
+
format!(r#"{{"detail":"{}"}}"#, bound_err.to_string().replace('"', "'"))
|
|
1951
|
+
};
|
|
1882
1952
|
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1953
|
+
HyperResponse::builder()
|
|
1954
|
+
.status(status_code)
|
|
1955
|
+
.header("Content-Type", "application/json")
|
|
1956
|
+
.body(Body::from(detail_json))
|
|
1957
|
+
.unwrap()
|
|
1958
|
+
});
|
|
1959
|
+
dependency_error_response = Some(resp);
|
|
1960
|
+
break;
|
|
1890
1961
|
}
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1962
|
+
Err(_) => {
|
|
1963
|
+
dependency_error_response = Some(
|
|
1964
|
+
HyperResponse::builder()
|
|
1965
|
+
.status(500)
|
|
1966
|
+
.header("Content-Type", "application/json")
|
|
1967
|
+
.body(Body::from(r#"{"detail":"Asyncio task dropped"}"#))
|
|
1968
|
+
.unwrap()
|
|
1969
|
+
);
|
|
1970
|
+
break;
|
|
1897
1971
|
}
|
|
1898
|
-
Err(e) => { dependency_error = Some(e); break; }
|
|
1899
1972
|
}
|
|
1900
1973
|
}
|
|
1901
|
-
Err(
|
|
1974
|
+
Err(py_err) => {
|
|
1975
|
+
let resp = Python::with_gil(|py| -> HyperResponse<Body> {
|
|
1976
|
+
let err_val = py_err.into_value(py);
|
|
1977
|
+
let bound_err = err_val.bind(py);
|
|
1978
|
+
let status_code: u16 = bound_err.getattr("status_code")
|
|
1979
|
+
.and_then(|s| s.extract())
|
|
1980
|
+
.unwrap_or(500);
|
|
1981
|
+
|
|
1982
|
+
let detail_json: String = if let Ok(detail) = bound_err.getattr("detail") {
|
|
1983
|
+
if let Ok(py_json) = py.import_bound("json") {
|
|
1984
|
+
if let Ok(dumps_res) = py_json.call_method1("dumps", (&detail,)) {
|
|
1985
|
+
let d_str: String = dumps_res.extract().unwrap_or_default();
|
|
1986
|
+
format!(r#"{{"detail":{}}}"#, d_str)
|
|
1987
|
+
} else {
|
|
1988
|
+
format!(r#"{{"detail":"{}"}}"#, detail.to_string().replace('"', "'"))
|
|
1989
|
+
}
|
|
1990
|
+
} else {
|
|
1991
|
+
format!(r#"{{"detail":"{}"}}"#, detail.to_string().replace('"', "'"))
|
|
1992
|
+
}
|
|
1993
|
+
} else {
|
|
1994
|
+
format!(r#"{{"detail":"{}"}}"#, bound_err.to_string().replace('"', "'"))
|
|
1995
|
+
};
|
|
1996
|
+
|
|
1997
|
+
HyperResponse::builder()
|
|
1998
|
+
.status(status_code)
|
|
1999
|
+
.header("Content-Type", "application/json")
|
|
2000
|
+
.body(Body::from(detail_json))
|
|
2001
|
+
.unwrap()
|
|
2002
|
+
});
|
|
2003
|
+
dependency_error_response = Some(resp);
|
|
2004
|
+
break;
|
|
2005
|
+
}
|
|
1902
2006
|
}
|
|
1903
2007
|
}
|
|
1904
2008
|
|
|
1905
|
-
if let Some(
|
|
1906
|
-
return Ok(
|
|
1907
|
-
HyperResponse::builder().status(500)
|
|
1908
|
-
.header("Content-Type", "application/json")
|
|
1909
|
-
.body(Body::from(format!(r#"{{"detail":"{}"}}"#, err_msg.replace('"', "'"))))
|
|
1910
|
-
.unwrap()
|
|
1911
|
-
);
|
|
2009
|
+
if let Some(err_resp) = dependency_error_response {
|
|
2010
|
+
return Ok(err_resp);
|
|
1912
2011
|
}
|
|
1913
2012
|
|
|
1914
2013
|
// -- BackgroundTasks setup --------------------------------------------
|
|
@@ -111,3 +111,41 @@ def test_6_upload_file_sync_and_async_methods():
|
|
|
111
111
|
await file_obj.close()
|
|
112
112
|
|
|
113
113
|
asyncio.run(_test())
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_7_openapi_security_schemes_and_http_exception_status_codes():
|
|
117
|
+
app = FastAPI()
|
|
118
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
|
|
119
|
+
bearer_scheme = HTTPBearer()
|
|
120
|
+
|
|
121
|
+
@app.get("/secure/me")
|
|
122
|
+
def secure_me(token: str = Depends(oauth2_scheme)):
|
|
123
|
+
if not token:
|
|
124
|
+
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
125
|
+
return {"status": "ok"}
|
|
126
|
+
|
|
127
|
+
@app.get("/admin/logs")
|
|
128
|
+
def admin_logs(token: HTTPAuthorizationCredentials = Depends(bearer_scheme)):
|
|
129
|
+
return {"logs": []}
|
|
130
|
+
|
|
131
|
+
def run_server():
|
|
132
|
+
app.run(host="127.0.0.1", port=8994)
|
|
133
|
+
|
|
134
|
+
t = threading.Thread(target=run_server, daemon=True)
|
|
135
|
+
t.start()
|
|
136
|
+
time.sleep(1.5)
|
|
137
|
+
|
|
138
|
+
# 1. Test OpenAPI securitySchemes generation
|
|
139
|
+
res_openapi = requests.get("http://127.0.0.1:8994/openapi.json")
|
|
140
|
+
assert res_openapi.status_code == 200
|
|
141
|
+
openapi_doc = res_openapi.json()
|
|
142
|
+
assert "components" in openapi_doc
|
|
143
|
+
assert "securitySchemes" in openapi_doc["components"]
|
|
144
|
+
schemes = openapi_doc["components"]["securitySchemes"]
|
|
145
|
+
assert "OAuth2PasswordBearer" in schemes or "HTTPBearer" in schemes
|
|
146
|
+
|
|
147
|
+
# 2. Test HTTPException status code propagation (401 instead of 500)
|
|
148
|
+
res_unauth = requests.get("http://127.0.0.1:8994/secure/me")
|
|
149
|
+
assert res_unauth.status_code == 401
|
|
150
|
+
assert "Not authenticated" in res_unauth.json()["detail"] or "Unauthorized" in res_unauth.json()["detail"]
|
|
151
|
+
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import sqlite3
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
import requests
|
|
6
|
+
import jwt
|
|
7
|
+
import httpx
|
|
8
|
+
import numpy as np
|
|
9
|
+
from PIL import Image
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
from rustapi import FastAPI, Request, Depends, HTTPException, status
|
|
12
|
+
from rustapi.uploads import UploadFile
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# 1. Pydantic Model
|
|
16
|
+
class ImageAnalysisRequest(BaseModel):
|
|
17
|
+
project_name: str = Field(..., min_length=2)
|
|
18
|
+
threshold: float = Field(0.5, ge=0.0, le=1.0)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# 2. Database Setup (SQLite stdlib)
|
|
22
|
+
def init_db():
|
|
23
|
+
conn = sqlite3.connect(":memory:", check_same_thread=False)
|
|
24
|
+
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, token TEXT)")
|
|
25
|
+
conn.execute("INSERT INTO users (username, token) VALUES ('alice', 'secret_jwt_123')")
|
|
26
|
+
conn.commit()
|
|
27
|
+
return conn
|
|
28
|
+
|
|
29
|
+
db_conn = init_db()
|
|
30
|
+
|
|
31
|
+
# DB Dependency
|
|
32
|
+
def get_db():
|
|
33
|
+
return db_conn
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# 3. Application
|
|
37
|
+
app = FastAPI(title="Ecosystem Compatibility Test App")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.post("/analyze-image")
|
|
41
|
+
async def analyze_image(req: Request, db=Depends(get_db)):
|
|
42
|
+
# A. Parse form fields or JSON body with Pydantic
|
|
43
|
+
try:
|
|
44
|
+
json_body = await req.json()
|
|
45
|
+
except Exception:
|
|
46
|
+
json_body = {}
|
|
47
|
+
|
|
48
|
+
project_name = req.form.get("project_name") or json_body.get("project_name") or "default_project"
|
|
49
|
+
threshold = float(req.form.get("threshold") or json_body.get("threshold") or 0.5)
|
|
50
|
+
model = ImageAnalysisRequest(project_name=project_name, threshold=threshold)
|
|
51
|
+
|
|
52
|
+
# B. Query SQLite database
|
|
53
|
+
cursor = db.cursor()
|
|
54
|
+
cursor.execute("SELECT username FROM users WHERE token = 'secret_jwt_123'")
|
|
55
|
+
row = cursor.fetchone()
|
|
56
|
+
user = row[0] if row else "unknown"
|
|
57
|
+
|
|
58
|
+
# C. Read uploaded file & process image with PIL (Pillow) & NumPy
|
|
59
|
+
if "photo" not in req.files or not req.files["photo"]:
|
|
60
|
+
raise HTTPException(status_code=400, detail="No photo uploaded")
|
|
61
|
+
|
|
62
|
+
photo_file: UploadFile = req.files["photo"][0]
|
|
63
|
+
img_bytes = await photo_file.read()
|
|
64
|
+
|
|
65
|
+
image = Image.open(io.BytesIO(img_bytes)).convert("L")
|
|
66
|
+
img_array = np.array(image)
|
|
67
|
+
|
|
68
|
+
# Perform NumPy calculation (mean brightness)
|
|
69
|
+
mean_brightness = float(np.mean(img_array))
|
|
70
|
+
|
|
71
|
+
# D. JWT Encoding
|
|
72
|
+
payload = {"sub": user, "project": model.project_name, "brightness": mean_brightness}
|
|
73
|
+
encoded_token = jwt.encode(payload, "secret_key_999", algorithm="HS256")
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
"status": "success",
|
|
77
|
+
"user": user,
|
|
78
|
+
"project": model.project_name,
|
|
79
|
+
"mean_brightness": mean_brightness,
|
|
80
|
+
"jwt_token": encoded_token,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_python_ecosystem_compatibility():
|
|
85
|
+
port = 8996
|
|
86
|
+
thread = threading.Thread(target=lambda: app.run(host="127.0.0.1", port=port), daemon=True)
|
|
87
|
+
thread.start()
|
|
88
|
+
time.sleep(0.4)
|
|
89
|
+
|
|
90
|
+
# Generate sample 100x100 grayscale image in memory using PIL
|
|
91
|
+
img = Image.new("L", (100, 100), color=128)
|
|
92
|
+
img_byte_arr = io.BytesIO()
|
|
93
|
+
img.save(img_byte_arr, format="PNG")
|
|
94
|
+
png_bytes = img_byte_arr.getvalue()
|
|
95
|
+
|
|
96
|
+
# Call endpoint using HTTPX
|
|
97
|
+
with httpx.Client(base_url=f"http://127.0.0.1:{port}") as client:
|
|
98
|
+
files = {"photo": ("test_image.png", png_bytes, "image/png")}
|
|
99
|
+
data = {"project_name": "SkinAnalysis", "threshold": "0.75"}
|
|
100
|
+
|
|
101
|
+
response = client.post("/analyze-image", data=data, files=files)
|
|
102
|
+
assert response.status_code == 200
|
|
103
|
+
|
|
104
|
+
res_json = response.json()
|
|
105
|
+
assert res_json["status"] == "success"
|
|
106
|
+
assert res_json["user"] == "alice"
|
|
107
|
+
assert res_json["project"] == "SkinAnalysis"
|
|
108
|
+
assert res_json["mean_brightness"] == 128.0
|
|
109
|
+
|
|
110
|
+
# Decode JWT token to verify PyJWT compatibility
|
|
111
|
+
decoded = jwt.decode(res_json["jwt_token"], "secret_key_999", algorithms=["HS256"])
|
|
112
|
+
assert decoded["sub"] == "alice"
|
|
113
|
+
assert decoded["project"] == "SkinAnalysis"
|
|
114
|
+
assert decoded["brightness"] == 128.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyrustapi-0.3.34 → pyrustapi-0.8.6}/performance-reports/fastapi_vs_rustapi_crud_benchmark.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyrustapi-0.3.34 → pyrustapi-0.8.6}/performance-reports/three_tier_architecture_evaluation.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|