""" FastAPI main application. This module creates and configures the FastAPI application with: - All API routers registered - Lifespan events for database initialization - Static file serving for the frontend - CORS middleware - Exception handlers """ import sys from pathlib import Path # Add backend directory to Python path for imports to work backend_dir = Path(__file__).parent.parent if str(backend_dir) not in sys.path: sys.path.insert(0, str(backend_dir)) from contextlib import asynccontextmanager from typing import Callable from fastapi import FastAPI, Request, status from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware from sqlalchemy.exc import SQLAlchemyError from app.config import get_settings from app.database import init_db, get_engine from app.models import Base # noqa: F401 - Import to ensure models are registered # Import API routers from app.api.ssh_keys import router as ssh_keys_router from app.api.servers import router as servers_router from app.api.status import router as status_router @asynccontextmanager async def lifespan(app: FastAPI): # noqa: ARG001 - Unused app parameter """ Lifespan context manager for FastAPI application. Handles startup and shutdown events: - Startup: Initialize database and create tables - Shutdown: Close database connections Yields: None """ # Startup settings = get_settings() # Initialize database init_db(settings.db_path) # Create all tables engine = get_engine() if engine is not None: Base.metadata.create_all(engine) # Ensure required directories exist settings.data_dir.mkdir(parents=True, exist_ok=True) settings.ssh_keys_dir.mkdir(parents=True, exist_ok=True) settings.repos_dir.mkdir(parents=True, exist_ok=True) yield # Shutdown # Close database connections if engine is not None: engine.dispose() def create_app(lifespan_handler: Callable = lifespan) -> FastAPI: """ Create and configure the FastAPI application. Args: lifespan_handler: Lifespan context manager (for testing) Returns: Configured FastAPI application instance """ settings = get_settings() app = FastAPI( title="Git Manager API", description="API for managing Gitea server mirrors and SSH keys", version="1.0.0", lifespan=lifespan_handler, docs_url="/api/docs", redoc_url="/api/redoc", openapi_url="/api/openapi.json" ) # Configure CORS app.add_middleware( CORSMiddleware, allow_origins=["*"], # In production, specify exact origins allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Register exception handlers register_exception_handlers(app) # Register API routers app.include_router(ssh_keys_router) app.include_router(servers_router) app.include_router(status_router) # Mount static files for frontend # Check if frontend build exists frontend_path = Path(__file__).parent.parent.parent / "frontend" / "dist" if frontend_path.exists(): app.mount("/", StaticFiles(directory=str(frontend_path), html=True), name="frontend") return app def register_exception_handlers(app: FastAPI) -> None: """ Register global exception handlers for the application. Args: app: FastAPI application instance """ @app.exception_handler(SQLAlchemyError) async def sqlalchemy_error_handler( request: Request, # noqa: ARG001 - Unused request parameter exc: SQLAlchemyError ): """Handle SQLAlchemy database errors.""" return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={ "code": 500, "message": "Database error occurred", "data": {"detail": str(exc)} } ) @app.exception_handler(ValueError) async def value_error_handler( request: Request, # noqa: ARG001 - Unused request parameter exc: ValueError ): """Handle ValueError exceptions.""" return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, content={ "code": 400, "message": str(exc), "data": None } ) @app.exception_handler(Exception) async def general_exception_handler( request: Request, # noqa: ARG001 - Unused request parameter exc: Exception ): """Handle all other exceptions.""" return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={ "code": 500, "message": "Internal server error", "data": {"detail": str(exc)} } ) # Create the application instance app = create_app() if __name__ == "__main__": import uvicorn settings = get_settings() uvicorn.run( "app.main:app", host=settings.host, port=settings.port, reload=True )