| """AmanPay FastAPI application entry point. |
| |
| Run with: ``uvicorn api.main:app --host 0.0.0.0 --port 8000`` |
| """ |
|
|
| from __future__ import annotations |
|
|
| import logging |
| import os |
| from contextlib import asynccontextmanager |
|
|
| |
| try: |
| from dotenv import load_dotenv |
| load_dotenv() |
| except Exception: |
| pass |
|
|
| |
| |
| |
| |
| |
| |
| try: |
| import huggingface_hub |
| from huggingface_hub import (batch_bucket_files, bucket_info, |
| download_bucket_files, list_bucket_tree) |
| except Exception: |
| pass |
|
|
| |
| try: |
| import torch |
| torch.set_grad_enabled(False) |
| torch.set_num_threads(max(1, int(os.getenv("AMANPAY_TORCH_THREADS", os.cpu_count() or 2)))) |
| except Exception: |
| pass |
|
|
| from fastapi import FastAPI |
| from fastapi.middleware.cors import CORSMiddleware |
|
|
| from api.dependencies import state |
| from api.observability import (init_sentry, metrics_middleware, metrics_response, |
| record_readiness, setup_logging) |
| from api.routers.notifications import router as notifications_router |
| from api.routes import router |
|
|
| logging.basicConfig(level=logging.INFO, |
| format="%(asctime)s %(levelname)s %(name)s: %(message)s") |
| setup_logging() |
| init_sentry() |
| logger = logging.getLogger("amanpay.api") |
|
|
|
|
| @asynccontextmanager |
| async def lifespan(app: FastAPI): |
| """Startup must NOT block on model loading. |
| |
| The authenticator weights are fetched from the HF Hub and loaded lazily in a background |
| daemon thread, so `application startup` completes immediately and the container becomes |
| healthy right away. This prevents HF Spaces' launch health-check from timing out (and the |
| container being killed) when a weight download is slow or hangs. Model-dependent endpoints |
| return a clean 503 ("Model not loaded") until `state.loaded` flips true; `/healthz` and the |
| static UI serve without the model. |
| """ |
| import threading |
|
|
| config_path = os.getenv("AMANPAY_CONFIG") |
| checkpoint = os.getenv("AMANPAY_CHECKPOINT") |
|
|
| def _load_models() -> None: |
| try: |
| logger.info("Loading model in background (config=%s, checkpoint=%s)", |
| config_path, checkpoint) |
| state.load(config_path=config_path, checkpoint=checkpoint) |
| logger.info("Model loaded (background); state.loaded=%s", state.loaded) |
| except Exception as exc: |
| logger.warning("Background model load failed (endpoints stay 503 until retried): %s", |
| exc.__class__.__name__) |
|
|
| |
| if os.getenv("AMANPAY_BLOCKING_MODEL_LOAD", "0").strip().lower() in ("1", "true", "yes"): |
| _load_models() |
| else: |
| threading.Thread(target=_load_models, name="amanpay-model-load", daemon=True).start() |
|
|
| from amanpay.version import read_build_info |
| commit = (read_build_info(_ROOT) or {}).get("commit", "") |
|
|
| |
| |
| _d2_runtime = None |
| from amanpay.identity.config import is_d2_enabled |
| if is_d2_enabled(): |
| try: |
| from amanpay.identity.runtime import build_runtime |
| from api.identity_routes import set_runtime |
| _d2_runtime = build_runtime(source_commit=commit) |
| set_runtime(_d2_runtime) |
| logger.info("D2 identity runtime ready") |
| except Exception as exc: |
| logger.warning("D2 identity runtime unavailable (endpoints 503): %s", |
| exc.__class__.__name__) |
|
|
| |
| |
| _d3_runtime = None |
| from amanpay.finance.config import is_d3_enabled |
| if is_d3_enabled(): |
| try: |
| from amanpay.finance.runtime import build_runtime as build_d3_runtime |
| from api.finance_routes import set_runtime as set_d3_runtime |
| _d3_runtime = build_d3_runtime( |
| storage=(_d2_runtime.storage if _d2_runtime is not None else None), |
| identity=(_d2_runtime.identity if _d2_runtime is not None else None), |
| source_commit=commit) |
| set_d3_runtime(_d3_runtime) |
| logger.info("D3 finance runtime ready") |
| except Exception as exc: |
| logger.warning("D3 finance runtime unavailable (endpoints 503): %s", |
| exc.__class__.__name__) |
|
|
| |
| |
| from amanpay.identity.config import is_demo_bootstrap |
| if is_demo_bootstrap() and _d2_runtime is not None: |
| try: |
| from amanpay.demo import ensure_demo |
| ensure_demo(_d2_runtime, _d3_runtime) |
| logger.info("demo bootstrap complete") |
| except Exception as exc: |
| logger.warning("demo bootstrap skipped: %s", exc.__class__.__name__) |
|
|
| logger.info("Application startup complete (model loading off the critical path)") |
| yield |
| if _d2_runtime is not None: |
| try: |
| _d2_runtime.storage.coordinator.snapshot_on_shutdown() |
| _d2_runtime.close() |
| except Exception: |
| pass |
| if _d3_runtime is not None: |
| try: |
| _d3_runtime.close() |
| except Exception: |
| pass |
| logger.info("Shutting down") |
|
|
|
|
| app = FastAPI( |
| title="AmanPay Biometric API", |
| description="Multi-modal (face + fingerprint) biometric authentication.", |
| version="0.1.0", |
| lifespan=lifespan, |
| ) |
|
|
| |
| |
| |
| _origins = [o.strip() for o in os.getenv("AMANPAY_ALLOWED_ORIGINS", "").split(",") if o.strip()] |
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=_origins or ["*"], |
| allow_credentials=bool(_origins), |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| app.middleware("http")(metrics_middleware) |
|
|
|
|
| @app.get("/metrics", include_in_schema=False) |
| def metrics(): |
| """Prometheus metrics (request rate/latency + payment/auth domain signals).""" |
| return metrics_response() |
|
|
|
|
| @app.get("/healthz", include_in_schema=False) |
| def healthz(): |
| """Liveness: lightweight, no dependency checks.""" |
| return {"status": "ok"} |
|
|
|
|
| @app.get("/readyz", include_in_schema=False) |
| def readyz(): |
| """Readiness: verify the datastore (and Redis when REDIS_URL is set) are reachable. |
| Returns 503 if a required dependency is down. Never exposes connection details.""" |
| from fastapi.responses import JSONResponse |
| checks: dict = {} |
| try: |
| ping = getattr(state.store, "ping", None) |
| checks["datastore"] = bool(ping()) if ping else True |
| except Exception: |
| checks["datastore"] = False |
| if os.getenv("REDIS_URL"): |
| try: |
| from amanpay.storage.kv import get_kv |
| checks["redis"] = bool(get_kv().ping()) |
| except Exception: |
| checks["redis"] = False |
| checks["model"] = bool(state.loaded) |
| for dep, ok in checks.items(): |
| record_readiness(dep, ok) |
| ready = all(checks.values()) |
| return JSONResponse({"ready": ready, "checks": checks}, |
| status_code=200 if ready else 503) |
|
|
|
|
| app.include_router(router) |
| app.include_router(notifications_router) |
|
|
| |
| from api.agent_security_routes import router as agent_security_router |
| app.include_router(agent_security_router) |
|
|
| |
| from amanpay.agentic_orchestration.api import router as ai_router |
| app.include_router(ai_router) |
|
|
| |
| from amanpay.agentic_orchestration.consent_api import router as consent_router |
| app.include_router(consent_router) |
|
|
| |
| |
| |
| from amanpay.identity.config import is_d2_enabled as _d2_enabled |
| if _d2_enabled(): |
| from api.identity_routes import router as identity_router, install_error_handler |
| app.include_router(identity_router) |
| install_error_handler(app) |
|
|
| |
| |
| from amanpay.finance.config import is_d3_enabled as _d3_enabled |
| if _d3_enabled(): |
| from api.finance_routes import (router as finance_router, |
| install_error_handler as install_finance_errors) |
| app.include_router(finance_router) |
| install_finance_errors(app) |
|
|
|
|
| _ROOT = os.path.dirname(os.path.dirname(__file__)) |
| _FRONTEND = os.path.join(_ROOT, "frontend", "index.html") |
| _WEB_DIST = os.path.join(_ROOT, "web", "dist") |
|
|
|
|
| def _use_react() -> bool: |
| """React UI unless AMANPAY_UI=legacy or the build is absent (rollback-safe).""" |
| return (os.getenv("AMANPAY_UI", "react").lower() != "legacy" |
| and os.path.exists(os.path.join(_WEB_DIST, "index.html"))) |
|
|
|
|
| |
| if os.path.isdir(os.path.join(_WEB_DIST, "assets")): |
| from fastapi.staticfiles import StaticFiles |
| app.mount("/assets", StaticFiles(directory=os.path.join(_WEB_DIST, "assets")), name="assets") |
|
|
|
|
| @app.get("/", include_in_schema=False) |
| def root(): |
| """Serve the web UI — React build by default, legacy single-file as rollback.""" |
| from fastapi.responses import FileResponse, JSONResponse |
| if _use_react(): |
| return FileResponse(os.path.join(_WEB_DIST, "index.html")) |
| if os.path.exists(_FRONTEND): |
| return FileResponse(_FRONTEND) |
| return JSONResponse({"name": "AmanPay Biometric API", "docs": "/docs"}) |
|
|
|
|
| _FRONTEND_DIR = os.path.dirname(_FRONTEND) |
|
|
|
|
| @app.get("/manifest.json", include_in_schema=False) |
| def manifest(): |
| """PWA manifest — makes AmanPay installable on Android/iOS home screens.""" |
| from fastapi.responses import FileResponse, JSONResponse |
| p = os.path.join(_FRONTEND_DIR, "manifest.json") |
| return FileResponse(p, media_type="application/manifest+json") if os.path.exists(p) \ |
| else JSONResponse({}, status_code=404) |
|
|
|
|
| @app.get("/sw.js", include_in_schema=False) |
| def service_worker(): |
| """Service worker (must be served from scope root to control the app).""" |
| from fastapi.responses import FileResponse, JSONResponse |
| p = os.path.join(_FRONTEND_DIR, "sw.js") |
| return FileResponse(p, media_type="application/javascript") if os.path.exists(p) \ |
| else JSONResponse({}, status_code=404) |
|
|
|
|
| @app.get("/icon-{size}.png", include_in_schema=False) |
| def app_icon(size: str): |
| """PWA / apple-touch icons.""" |
| from fastapi.responses import FileResponse, JSONResponse |
| p = os.path.join(_FRONTEND_DIR, f"icon-{size}.png") |
| return FileResponse(p, media_type="image/png") if os.path.exists(p) \ |
| else JSONResponse({}, status_code=404) |
|
|
|
|
| @app.get("/info", include_in_schema=False) |
| def info() -> dict: |
| return {"name": "AmanPay Biometric API", "docs": "/docs", "health": "/health"} |
|
|
|
|
| @app.get("/version") |
| def version() -> dict: |
| """Non-sensitive build/version info for deploy verification and the UI footer. |
| Contains NO tokens, secrets, DB/Redis URLs, or env values.""" |
| from amanpay.version import build_version_info |
| try: |
| from amanpay.payments.registry import provider_name_for |
| provider_mode = provider_name_for("SA") |
| except Exception: |
| provider_mode = "mock" |
| return build_version_info(_ROOT, |
| ui_mode="react" if _use_react() else "legacy", |
| provider_mode=provider_mode) |
|
|