STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
main.py
Go to the documentation of this file.
1"""FastAPI application for Pintle Engine Design.
2
3Run with:
4 cd /home/adnan/EngineDesign
5 uvicorn backend.main:app --reload --port 8000
6"""
7
8import os
9import sys
10from pathlib import Path
11from contextlib import asynccontextmanager
12
13from fastapi import FastAPI, Request
14from fastapi.exceptions import RequestValidationError
15from fastapi.middleware.cors import CORSMiddleware
16from fastapi.responses import JSONResponse
17
18# Ensure project root is in path for imports
19project_root = Path(__file__).resolve().parents[1]
20if str(project_root) not in sys.path:
21 sys.path.insert(0, str(project_root))
22
23# Warm the physics accelerator (engine/accel) for backend-launched work, including
24# the Layer-1 optimizer. This MUST run before the engine/optimizer modules import
25# and before the Layer-1 ProcessPool spawns. Opt out with ED_ACCEL=off.
26#
27# Correction to what this comment used to claim: there is NO runtime self-check
28# against Python (see engine/accel/__init__.py).
29# Equivalence is enforced ahead of time by tests/test_numba_ab_parity.py, which
30# diffs the accelerated and Python paths live on the same inputs.
31try:
32 from engine import accel as _accel
33 if _accel.enabled():
34 _accel.warmup()
35 print("[accel] numba kernels warmed (ED_ACCEL=numba)")
36 else:
37 print("[accel] disabled; using the Python path")
38except Exception as _ed_err: # pragma: no cover - accelerator is best-effort
39 print(f"[accel] unavailable, using Python path: {_ed_err}")
40
41# Import control router first (required for controller)
42from backend.routers import control
43
44# Import other routers optionally (may fail if dependencies missing)
45_optional_routers = {}
46for router_name in ['config', 'configs_store', 'documents', 'users', 'evaluate', 'timeseries', 'flight', 'geometry', 'optimizer', 'layerx']:
47 try:
48 router_module = __import__(f'backend.routers.{router_name}', fromlist=[router_name])
49 _optional_routers[router_name] = router_module
50 except (ImportError, TypeError) as e:
51 print(f"Warning: Router '{router_name}' unavailable (non-critical): {e}")
52
53@asynccontextmanager
54async def lifespan(app: FastAPI):
55 """Application lifespan.
56
57 Live state is per-user now (backend/session.py), so there is no global config
58 to load at boot: each user's session loads configs/default.yaml on creation,
59 the first time that user hits the API. Nothing to do here.
60 """
61 yield # App runs here
62
63
64app = FastAPI(
65 title="Hotbench Engine API",
66 description="Liquid engine design and analysis: Forward Mode, Time-Series, the optimizer layers and Layer X",
67 version="1.0.0",
68 lifespan=lifespan,
69)
70
71
72def _sentence(text: str) -> str:
73 t = " ".join(str(text).split())
74 if not t:
75 return "Something went wrong."
76 t = t[0].upper() + t[1:]
77 return t if t[-1] in ".!?" else t + "."
78
79
80def _one_problem(err: dict) -> str:
81 loc = [str(p) for p in err.get("loc", ()) if p not in ("body", "query", "path")]
82 field = " ".join(loc).replace("_", " ")
83 msg = str(err.get("msg") or "is not valid").removeprefix("Value error, ")
84 return f"{field}: {msg}" if field else msg
85
86
87def validation_sentence(errors: list) -> str:
88 """FastAPI's request-validation list ([{loc, msg, type}, ...]) as the one plain sentence every
89 error response carries (``{"detail": "<sentence>"}``). Every problem, each named by its field,
90 joined with '; ' -- as feed-twin's backend/errors.validation_sentence does. A sentence that
91 names only the first leaves the person fixing one field per round trip."""
92 said = "; ".join(p for p in (_one_problem(e) for e in errors) if p)
93 if not said:
94 return "Some of the values were not accepted."
95 return _sentence(said)
96
97
98@app.exception_handler(RequestValidationError)
99async def _validation_as_sentence(request: Request, exc: RequestValidationError) -> JSONResponse:
100 errors = list(exc.errors())
101 # ``errors`` keeps each problem's field path next to the sentence, so a page that has its own
102 # names for its fields (Layer X's rail: "Dome (psia)") can still say them in its own words.
103 return JSONResponse(
104 status_code=422,
105 content={
106 "detail": validation_sentence(errors),
107 "errors": [{"loc": [str(p) for p in e.get("loc", ())], "msg": str(e.get("msg", ""))} for e in errors],
108 },
109 )
110
111
112@app.exception_handler(Exception)
113async def _unhandled_as_sentence(request: Request, exc: Exception) -> JSONResponse:
114 # A JSON sentence instead of Starlette's bare "Internal Server Error"; the traceback still goes
115 # to the server log.
116 first = (str(exc).strip().splitlines() or [""])[0][:200]
117 what = f"{type(exc).__name__}: {first}" if first else type(exc).__name__
118 return JSONResponse(status_code=500, content={"detail": f"The engine design server hit an error ({what})."})
119
120if 'layerx' in _optional_routers:
121 # Refuses an oversized drawing upload before Starlette reads (and spools) its body.
122 app.add_middleware(_optional_routers['layerx'].UploadSizeGuard)
123
124# CORS middleware for React frontend
125app.add_middleware(
126 CORSMiddleware,
127 allow_origins=[
128 "http://localhost:5173", # Vite dev server
129 "http://localhost:3000", # Alternative React port
130 "http://127.0.0.1:5173",
131 "http://127.0.0.1:3000",
132 "https://engine-design.starberkeley.org",
133 ],
134 allow_credentials=True,
135 allow_methods=["*"],
136 allow_headers=["*"],
137)
138
139# Auth is Caddy's job: the reverse proxy verifies the session before any request
140# reaches this app (deploy/Caddyfile), and this container never publishes a host
141# port, so it is only reachable through Caddy. The app therefore does no auth --
142# it only reads X-Auth-Email for per-user data (backend/userdata.py), which falls
143# back to a local user in dev where there is no Caddy.
144
145# Include routers (control is required, others optional)
146app.include_router(control.router)
147
148# Include optional routers if they loaded successfully
149for router_name, router_module in _optional_routers.items():
150 try:
151 app.include_router(router_module.router)
152 print(f"✅ Loaded router: {router_name}")
153 except Exception as e:
154 print(f"Warning: Failed to include router '{router_name}': {e}")
155
156
157@app.get("/")
158async def root():
159 """Root endpoint - API info."""
160 return {
161 "name": "Pintle Engine Design API",
162 "version": "1.0.0",
163 "docs": "/docs",
164 }
165
166
167@app.get("/api/health")
168async def health():
169 """Health check endpoint.
170
171 A liveness probe with no user context, so it no longer reports config_loaded
172 (that is a per-user fact now -- see GET /api/config).
173 """
174 return {"status": "healthy"}
175
static std::vector< string_t > split(const string_t &reference_string)
split the string input to reference tokens
Definition json.hpp:14471
str _sentence(str text)
Definition main.py:72
JSONResponse _unhandled_as_sentence(Request request, Exception exc)
Definition main.py:113
str _one_problem(dict err)
Definition main.py:80
str validation_sentence(list errors)
Definition main.py:87
lifespan(FastAPI app)
Definition main.py:54
JSONResponse _validation_as_sentence(Request request, RequestValidationError exc)
Definition main.py:99