mirror of
https://github.com/RichieCahill/dotfiles.git
synced 2026-04-17 04:58:19 -04:00
118 lines
3.6 KiB
Python
118 lines
3.6 KiB
Python
"""FastAPI interface for Contact database."""
|
|
|
|
import logging
|
|
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.common import configure_logger
|
|
from python.orm.common import get_postgres_engine
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
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:
|
|
logger.info(f"Serving frontend from {frontend_dir}")
|
|
frontend_router = create_frontend_router(frontend_dir)
|
|
app.include_router(frontend_router)
|
|
|
|
return app
|
|
|
|
|
|
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"Frontend directory {source_dir} does not exist"
|
|
raise FileExistsError(error)
|
|
|
|
logger.info("Building frontend from %s...", 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) # noqa: S607
|
|
subprocess.run(["npm", "run", "build"], cwd=build_dir, env=env, check=True) # noqa: S607
|
|
|
|
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)
|
|
logger.info(f"Frontend built and copied to {output_dir}")
|
|
|
|
shutil.rmtree(build_dir)
|
|
|
|
return output_dir
|
|
|
|
|
|
def serve(
|
|
host: Annotated[str, typer.Option("--host", "-h", help="Host to bind to")],
|
|
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,
|
|
port: Annotated[int, typer.Option("--port", "-p", help="Port to bind to")] = 8000,
|
|
log_level: Annotated[str, typer.Option("--log-level", "-l", help="Log level")] = "INFO",
|
|
) -> None:
|
|
"""Start the Contact API server."""
|
|
configure_logger(log_level)
|
|
|
|
cache_dir = Path(environ["HOME"]) / ".npm"
|
|
serve_dir = build_frontend(frontend_dir, cache_dir=cache_dir)
|
|
|
|
app = create_app(frontend_dir=serve_dir)
|
|
uvicorn.run(app, host=host, port=port)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
typer.run(serve)
|