Spaces:
Running
Running
File size: 6,817 Bytes
79b0bef 4dc0836 79b0bef | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | """
src/api/main.py
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
FastAPI application β entry point for the Clinical NLP API.
Start the server
ββββββββββββββββ
Development (auto-reload on code changes):
uvicorn src.api.main:app --reload --port 8000
Production (multiple workers):
uvicorn src.api.main:app --workers 4 --port 8000
API documentation
βββββββββββββββββ
Swagger UI : http://localhost:8000/docs
ReDoc : http://localhost:8000/redoc
OpenAPI : http://localhost:8000/openapi.json
Environment variables
βββββββββββββββββββββ
DATABASE_URL β SQLAlchemy DB URL (default: SQLite)
API_DEBUG β enable /docs and /redoc (default: true)
API_PORT β server port (used by the run helper)
LOG_LEVEL β DEBUG / INFO / WARNING (default: INFO)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
"""
from __future__ import annotations
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from src.api.routes import entities, icd, model, notes
from src.api.schemas import HealthResponse
from src.db.connection import check_connection, create_all_tables
from src.utils.config import APIConfig
from src.utils.logger import get_logger
logger = get_logger(__name__)
# ββ Lifespan ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# FastAPI's lifespan replaces the older @app.on_event pattern.
# Code before `yield` runs at startup; code after runs at shutdown.
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Run startup and shutdown tasks for the application."""
# ββ Startup βββββββββββββββββββββββββββββββββββββββββββββββββββ
logger.info("=" * 50)
logger.info(" Clinical NLP API v%s", APIConfig.version)
logger.info("=" * 50)
# Ensure all database tables exist before the first request
try:
create_all_tables()
db_status = "connected" if check_connection() else "unreachable"
except Exception as exc:
logger.error("Database setup failed: %s", exc)
db_status = "unreachable"
# Attach db_status to app.state so the health endpoint can read it
app.state.db_status = db_status
logger.info("Database: %s", db_status)
# Eagerly load the NER pipeline, ICD-10 mapper, and classifier now,
# rather than lazily on whichever request happens to be first.
# Most deployment platforms hold off routing traffic until the
# health check passes, so this cost is absorbed into startup time
# rather than a real user's request.
#
# Skippable via WARM_UP_MODELS=false -- on a memory-constrained host
# (e.g. Render's free 512Mi tier) the hybrid NER pipeline + classifier
# can get OOM-killed by the OS at boot, which no try/except below can
# catch or recover from since it isn't a Python exception. Skipping
# warm-up at least lets the API start and serve DB-backed endpoints;
# NLP endpoints will still need enough memory when actually called.
if APIConfig.warm_up_models:
try:
notes.warm_up()
except Exception:
# Non-fatal: whatever didn't warm up here will load lazily on
# whichever request needs it. exc_info=True so a resource-
# contention failure (e.g. an import failing under low memory)
# is actually diagnosable instead of logging an empty message.
logger.exception("Model warm-up failed β falling back to lazy loading")
else:
logger.info("WARM_UP_MODELS=false β skipping eager model load, will load lazily per-request")
logger.info("API ready at http://%s:%s", APIConfig.host, APIConfig.port)
yield # application is running
# ββ Shutdown ββββββββββββββββββββββββββββββββββββββββββββββββββ
logger.info("Shutting down Clinical NLP API...")
# ββ Application βββββββββββββββββββββββββββββββββββββββββββββββββββ
app = FastAPI(
title = APIConfig.title,
version = APIConfig.version,
description = APIConfig.description,
docs_url = "/docs" if APIConfig.debug else None,
redoc_url = "/redoc" if APIConfig.debug else None,
lifespan = lifespan,
)
# CORS β allow the Streamlit dashboard (and any other origin in dev)
# to call the API from the browser.
# In production, restrict origins to your actual dashboard domain.
app.add_middleware(
CORSMiddleware,
allow_origins = ["*"], # tighten in production
allow_credentials = True,
allow_methods = ["*"],
allow_headers = ["*"],
)
# ββ Routers βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
app.include_router(notes.router)
app.include_router(entities.router)
app.include_router(icd.router)
app.include_router(model.router)
# ββ Health check ββββββββββββββββββββββββββββββββββββββββββββββββββ
@app.get(
"/health",
response_model = HealthResponse,
tags = ["Health"],
summary = "API health check",
)
def health() -> HealthResponse:
"""Return the current health status of the API.
Used by load balancers, monitoring tools, and the Streamlit
dashboard to confirm the API is running before making requests.
Returns:
:class:`HealthResponse` with status, version, DB state, and
whether semantic ICD-10 matching is available or degraded.
"""
db_status = getattr(app.state, "db_status", "unknown")
return HealthResponse(
status = "ok",
version = APIConfig.version,
database = db_status,
icd10_embedding_available = notes.icd10_embedding_available(),
)
@app.get("/", include_in_schema=False)
def root():
"""Redirect root to the API docs."""
from fastapi.responses import RedirectResponse
return RedirectResponse(url="/docs")
|