Download backend/src/cadmorph/api/app.py from bolleman/CADMorph: direct link, hf CLI and curl.
- Browser
- Download file 3.59 kB
-
https://huggingface.co/spaces/bolleman/CADMorph/resolve/main/backend/src/cadmorph/api/app.py
- Command line
-
hf download hf://spaces/bolleman/CADMorph/backend/src/cadmorph/api/app.py
-
curl -L -o app.py https://huggingface.co/spaces/bolleman/CADMorph/resolve/main/backend/src/cadmorph/api/app.py
3.59 kB
| """FastAPI application factory. | |
| No CORS middleware is registered: same-origin only by default (FR-016). | |
| All error responses use the contract's error envelope (contracts/api.md). | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import os | |
| from contextlib import asynccontextmanager, suppress | |
| from pathlib import Path | |
| from fastapi import FastAPI, Request | |
| from fastapi.exceptions import RequestValidationError | |
| from fastapi.responses import JSONResponse | |
| from starlette.exceptions import HTTPException as StarletteHTTPException | |
| from cadmorph.api.routes import max_upload_bytes, router | |
| from cadmorph.observability import configure_logging | |
| from cadmorph.pipeline import JobStore, cleanup_loop | |
| def error_envelope(code: str, message: str, comparison_id: str | None = None) -> dict: | |
| error: dict = {"code": code, "message": message} | |
| if comparison_id is not None: | |
| error["comparison_id"] = comparison_id | |
| return {"error": error} | |
| def create_app(data_dir: str | Path | None = None) -> FastAPI: | |
| configure_logging() | |
| async def lifespan(app: FastAPI): | |
| # TTL cleanup job (T045): sweep on startup, then hourly by default | |
| interval = float(os.environ.get("CADMORPH_CLEANUP_INTERVAL_S", "3600")) | |
| sweeper = asyncio.create_task(cleanup_loop(app.state.store, interval)) | |
| try: | |
| yield | |
| finally: | |
| sweeper.cancel() | |
| with suppress(asyncio.CancelledError): | |
| await sweeper | |
| app = FastAPI(title="CADMorph", version="0.1.0", lifespan=lifespan) | |
| app.state.store = JobStore(data_dir or os.environ.get("CADMORPH_DATA_DIR", "data")) | |
| app.include_router(router, prefix="/api/v1") | |
| async def upload_size_guard(request: Request, call_next): | |
| """Fast-fail oversized uploads on declared Content-Length before the | |
| body is buffered (T046). The per-file check in routes still guards | |
| undeclared-length bodies.""" | |
| declared = request.headers.get("content-length", "") | |
| cap = 2 * max_upload_bytes() + 1_048_576 # two files + form overhead | |
| if request.method == "POST" and declared.isdigit() and int(declared) > cap: | |
| return JSONResponse( | |
| status_code=413, | |
| content=error_envelope( | |
| "file_too_large", | |
| f"request body exceeds {cap // 1_048_576} MiB " | |
| f"(per-file limit {max_upload_bytes() // 1_048_576} MiB)", | |
| ), | |
| ) | |
| return await call_next(request) | |
| async def validation_handler(_: Request, exc: RequestValidationError) -> JSONResponse: | |
| detail = str(exc.errors()[:1]) | |
| return JSONResponse(status_code=400, content=error_envelope("missing_file", detail)) | |
| async def http_exception_handler(_: Request, exc: StarletteHTTPException) -> JSONResponse: | |
| """Route/method misses etc. must use the contract envelope too | |
| (contracts/api.md: error envelope on ALL non-2xx).""" | |
| code = {404: "not_found", 405: "method_not_allowed"}.get(exc.status_code, "error") | |
| return JSONResponse( | |
| status_code=exc.status_code, content=error_envelope(code, str(exc.detail)) | |
| ) | |
| async def internal_handler(_: Request, exc: Exception) -> JSONResponse: | |
| return JSONResponse(status_code=500, content=error_envelope("internal", "internal error")) | |
| return app | |
| app = create_app() | |