Files
dotfiles/python/api/main.py
2026-01-22 21:26:38 -05:00

114 lines
3.3 KiB
Python

"""FastAPI interface for Contact database."""
import shutil
import subprocess
import tempfile
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from os import environ
from pathlib import Path
from typing import Annotated
import typer
import uvicorn
from fastapi import FastAPI
from python.api.routers import contact_router, create_frontend_router
from python.orm.base import get_postgres_engine
def create_app(frontend_dir: Path | None = None) -> FastAPI:
"""Create and configure the FastAPI application."""
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Manage application lifespan."""
app.state.engine = get_postgres_engine()
yield
app.state.engine.dispose()
app = FastAPI(title="Contact Database API", lifespan=lifespan)
app.include_router(contact_router)
if frontend_dir:
print(f"Serving frontend from {frontend_dir}")
frontend_router = create_frontend_router(frontend_dir)
app.include_router(frontend_router)
return app
cli = typer.Typer()
def build_frontend(source_dir: Path | None, cache_dir: Path | None = None) -> Path | None:
"""Run npm build and copy output to a temp directory.
Works even if source_dir is read-only by copying to a temp directory first.
Args:
source_dir: Frontend source directory.
cache_dir: Optional npm cache directory for faster repeated builds.
Returns:
Path to frontend build directory, or None if no source_dir provided.
"""
if not source_dir:
return None
if not source_dir.exists():
error = f"Error: Frontend directory {source_dir} does not exist"
raise FileExistsError(error)
print(f"Building frontend from {source_dir}...")
# Copy source to a writable temp directory
build_dir = Path(tempfile.mkdtemp(prefix="contact_frontend_build_"))
shutil.copytree(source_dir, build_dir, dirs_exist_ok=True)
env = dict(environ)
if cache_dir:
cache_dir.mkdir(parents=True, exist_ok=True)
env["npm_config_cache"] = str(cache_dir)
subprocess.run(["npm", "install"], cwd=build_dir, env=env, check=True)
subprocess.run(["npm", "run", "build"], cwd=build_dir, env=env, check=True)
dist_dir = build_dir / "dist"
if not dist_dir.exists():
error = f"Build output not found at {dist_dir}"
raise FileNotFoundError(error)
output_dir = Path(tempfile.mkdtemp(prefix="contact_frontend_"))
shutil.copytree(dist_dir, output_dir, dirs_exist_ok=True)
print(f"Frontend built and copied to {output_dir}")
shutil.rmtree(build_dir)
return output_dir
@cli.command()
def serve(
frontend_dir: Annotated[
Path | None,
typer.Option(
"--frontend-dir",
"-f",
help="Frontend source directory. If provided, runs npm build and serves from temp dir.",
),
] = None,
host: Annotated[str, typer.Option("--host", "-h", help="Host to bind to")] = "0.0.0.0",
port: Annotated[int, typer.Option("--port", "-p", help="Port to bind to")] = 8000,
) -> None:
"""Start the Contact API server."""
serve_dir = build_frontend(frontend_dir)
app = create_app(frontend_dir=serve_dir)
uvicorn.run(app, host=host, port=port)
if __name__ == "__main__":
cli()