annator-atom / serve.py
techprotrade's picture
Login UI + Neon-ready DATABASE_URL (2026-07-30 06:41 UTC) (part 2)
66309d3 verified
Raw
History Blame Contribute Delete
8.41 kB
"""
Annator full stack for Hugging Face Docker Spaces.
- FastAPI backend (main_api_app) on one process
- AIMONEYFLOW + client HTML static frontend
- Listens on 0.0.0.0:7860
"""
from __future__ import annotations
import logging
import os
import sys
from pathlib import Path
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
log = logging.getLogger("annator-hf")
ROOT = Path(__file__).resolve().parent
BACKEND = ROOT / "backend"
FRONTEND = ROOT / "frontend"
# --- HF / Space environment defaults (before backend import) ---
os.environ.setdefault("ENVIRONMENT", "development") # keep /docs available
os.environ.setdefault("ALLOWED_HOSTS", "*")
os.environ.setdefault(
"ALLOWED_ORIGINS",
"*,https://techprotrade-annator-atom.hf.space,http://localhost:7860",
)
if not os.environ.get('DATABASE_URL') and os.environ.get('NEON_DATABASE_URL'):
os.environ['DATABASE_URL'] = os.environ['NEON_DATABASE_URL']
os.environ.setdefault("DATABASE_URL", f"sqlite:///{(ROOT / 'data' / 'atom.db').as_posix()}")
os.environ.setdefault("SKIP_USER_BOOTSTRAP", "true")
os.environ.setdefault("ATOM_MOCK_DATABASE", "false")
os.environ.setdefault("HF_SPACE", "1")
os.environ.setdefault("PORT", "7860")
(ROOT / "data").mkdir(parents=True, exist_ok=True)
if str(BACKEND) not in sys.path:
sys.path.insert(0, str(BACKEND))
os.chdir(BACKEND)
app = None
mode = "unknown"
try:
from main_api_app import app as _app # type: ignore
app = _app
mode = "full"
log.info("Loaded main_api_app (full backend)")
except Exception as exc: # noqa: BLE001
log.exception("Full backend failed to import: %s", exc)
try:
from main_api_app_safe import app as _app # type: ignore
app = _app
mode = "safe"
log.warning("Fell back to main_api_app_safe")
except Exception as exc2: # noqa: BLE001
log.exception("Safe backend also failed: %s", exc2)
from fastapi import FastAPI
app = FastAPI(title="Annator Atom (degraded)")
mode = "degraded"
@app.get("/api/health")
def _deg_health():
return {"status": "degraded", "error": str(exc), "fallback_error": str(exc2)}
from fastapi import HTTPException
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
# Drop TrustedHost so HF Space host is accepted
try:
from starlette.middleware.trustedhost import TrustedHostMiddleware
app.user_middleware = [ # type: ignore[attr-defined]
m
for m in getattr(app, "user_middleware", [])
if getattr(m, "cls", None) is not TrustedHostMiddleware
]
app.middleware_stack = None
except Exception as exc: # noqa: BLE001
log.warning("Could not adjust TrustedHostMiddleware: %s", exc)
# Final response headers for HF iframe + CSP (runs outermost when added last)
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request as StarletteRequest
from starlette.responses import Response as StarletteResponse
class HfBrowserCompatMiddleware(BaseHTTPMiddleware):
CSP = (
"default-src 'self'; "
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net blob:; "
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
"font-src 'self' https://fonts.gstatic.com data:; "
"img-src 'self' data: https: blob:; "
"connect-src 'self' https: wss: ws:; "
"worker-src 'self' blob:; "
"frame-ancestors 'self' https://huggingface.co https://*.huggingface.co https://*.hf.space; "
"base-uri 'self'; "
"object-src 'none'"
)
async def dispatch(self, request: StarletteRequest, call_next):
response: StarletteResponse = await call_next(request)
# Remove frame deny so Space can load in HF shell iframe
if "x-frame-options" in response.headers:
del response.headers["x-frame-options"]
response.headers["Content-Security-Policy"] = self.CSP
response.headers.setdefault("X-Content-Type-Options", "nosniff")
return response
app.add_middleware(HfBrowserCompatMiddleware)
# Guarantee PDF Workflow Hub is mounted even if main_api_app import order skipped it
try:
from api.pdf_workflow_routes import router as _pdf_hub # type: ignore
already = any(
getattr(r, "path", "").startswith("/api/pdf")
for r in app.router.routes
)
if not already:
app.include_router(_pdf_hub)
log.info("PDF Workflow Hub attached from serve.py")
else:
log.info("PDF Workflow Hub already present on app")
except Exception as exc: # noqa: BLE001
log.warning("Could not attach PDF Workflow Hub: %s", exc)
def _strip_root_routes() -> None:
"""main_api_app registers GET / — remove so frontend can own the landing page."""
kept = []
removed = 0
for route in list(app.router.routes):
path = getattr(route, "path", None)
methods = getattr(route, "methods", None) or set()
if path == "/" and (not methods or "GET" in methods or "HEAD" in methods):
removed += 1
continue
kept.append(route)
if removed:
app.router.routes = kept
log.info("Removed %s existing root route(s) for frontend landing", removed)
_strip_root_routes()
@app.get("/api/hf/status")
async def hf_status():
return {
"status": "ok",
"mode": mode,
"frontend": FRONTEND.exists(),
"backend_path": str(BACKEND),
"port": int(os.getenv("PORT", "7860")),
"space": "techprotrade/annator-atom",
}
@app.get("/health")
@app.get("/api/health")
async def health():
return {"status": "ok", "mode": mode, "service": "annator-full-hf"}
@app.get("/api/platform")
async def platform_info():
"""Former root JSON payload still available under /api/platform."""
return {
"name": "ATOM Platform API",
"version": "2.1.0",
"status": "running",
"mode": mode,
"docs": "/docs",
"frontend": "/",
"clients": "/clients/",
}
# Static frontend (API routes already registered — these come after)
if FRONTEND.exists():
assets = FRONTEND / "assets"
clients = FRONTEND / "clients"
if assets.exists():
app.mount("/assets", StaticFiles(directory=str(assets)), name="assets")
if clients.exists():
app.mount("/clients", StaticFiles(directory=str(clients), html=True), name="clients")
index_file = FRONTEND / "index.html"
@app.get("/")
async def spa_root():
if index_file.exists():
return FileResponse(index_file)
return JSONResponse(
{"message": "Annator Atom", "mode": mode, "hint": "/docs", "clients": "/clients/"}
)
# Explicit HTML pages from AIMONEYFLOW
for html_path in FRONTEND.glob("*.html"):
name = html_path.name
async def _serve_html(path: Path = html_path): # noqa: B023
return FileResponse(path)
# Register /dashboard.html etc. (avoid double-register index)
if name != "index.html":
app.add_api_route(f"/{name}", _serve_html, methods=["GET"], name=f"html_{name}")
# Fallback for other static files under frontend (css/js next to html)
@app.get("/{filename:path}")
async def frontend_fallback(filename: str):
# Never shadow API / docs
if (
filename.startswith("api/")
or filename.startswith("docs")
or filename.startswith("redoc")
or filename.startswith("openapi")
or filename.startswith("health")
):
raise HTTPException(status_code=404, detail="Not found")
candidate = (FRONTEND / filename).resolve()
try:
candidate.relative_to(FRONTEND.resolve())
except ValueError as exc:
raise HTTPException(status_code=404, detail="Not found") from exc
if candidate.is_file():
return FileResponse(candidate)
raise HTTPException(status_code=404, detail="Not found")
log.info("Mounted frontend from %s", FRONTEND)
else:
log.warning("Frontend directory missing: %s", FRONTEND)
if __name__ == "__main__":
import uvicorn
port = int(os.getenv("PORT", "7860"))
uvicorn.run("serve:app", host="0.0.0.0", port=port, factory=False)