feat: integrate configuration system into FastAPI application

- Use configurable app title, description, and API settings
- Add template context injection for all HTML routes
- Implement file upload validation with size/extension limits
- Add enhanced health check and info endpoints with feature flags
- Support conditional API documentation based on settings
This commit is contained in:
William Valentin
2025-09-14 15:57:08 -07:00
parent 4f94583b54
commit c4fd9427ed

View File

@@ -16,22 +16,23 @@ from fastapi.staticfiles import StaticFiles # type: ignore
from fastapi.templating import Jinja2Templates # type: ignore from fastapi.templating import Jinja2Templates # type: ignore
from pydantic import BaseModel from pydantic import BaseModel
from .core.config import settings
from .core.templates import UnitTemplate, template_registry from .core.templates import UnitTemplate, template_registry
from .core.unit_file import SystemdUnitFile, UnitType, ValidationError, create_unit_file from .core.unit_file import SystemdUnitFile, UnitType, ValidationError, create_unit_file
# Create FastAPI app # Create FastAPI app
app = FastAPI( app = FastAPI(
title="UnitForge", title=settings.api_title,
description="Create, validate, and manage systemd unit files", description=settings.api_description,
version="1.0.0", version=settings.api_version,
docs_url="/api/docs", docs_url=settings.api_docs_url if settings.api_docs_enabled else None,
redoc_url="/api/redoc", redoc_url=settings.api_redoc_url if settings.redoc_enabled else None,
) )
# Add CORS middleware # Add CORS middleware
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=settings.cors_origins,
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
@@ -39,8 +40,8 @@ app.add_middleware(
# Setup templates and static files # Setup templates and static files
BASE_DIR = Path(__file__).resolve().parent.parent.parent BASE_DIR = Path(__file__).resolve().parent.parent.parent
TEMPLATES_DIR = BASE_DIR / "frontend" / "templates" TEMPLATES_DIR = BASE_DIR / settings.templates_dir
STATIC_DIR = BASE_DIR / "frontend" / "static" STATIC_DIR = BASE_DIR / settings.static_dir
templates = Jinja2Templates(directory=str(TEMPLATES_DIR)) templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
@@ -134,19 +135,25 @@ def template_to_dict(template: UnitTemplate) -> Dict[str, Any]:
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
async def index(request: Request): async def index(request: Request):
"""Serve the main web interface.""" """Serve the main web interface."""
return templates.TemplateResponse("index.html", {"request": request}) context = {"request": request}
context.update(settings.get_template_context())
return templates.TemplateResponse("index.html", context)
@app.get("/editor", response_class=HTMLResponse) @app.get("/editor", response_class=HTMLResponse)
async def editor(request: Request): async def editor(request: Request):
"""Serve the unit file editor interface.""" """Serve the unit file editor interface."""
return templates.TemplateResponse("editor.html", {"request": request}) context = {"request": request}
context.update(settings.get_template_context())
return templates.TemplateResponse("editor.html", context)
@app.get("/templates", response_class=HTMLResponse) @app.get("/templates", response_class=HTMLResponse)
async def templates_page(request: Request): async def templates_page(request: Request):
"""Serve the templates browser interface.""" """Serve the templates browser interface."""
return templates.TemplateResponse("templates.html", {"request": request}) context = {"request": request}
context.update(settings.get_template_context())
return templates.TemplateResponse("templates.html", context)
# API Routes # API Routes
@@ -343,6 +350,22 @@ async def download_unit_file(unit_file: UnitFileContent):
async def upload_unit_file(file: UploadFile = File(...)): async def upload_unit_file(file: UploadFile = File(...)):
"""Upload and validate a unit file.""" """Upload and validate a unit file."""
try: try:
# Check file size
if file.size and file.size > settings.max_upload_size:
raise HTTPException(
status_code=413,
detail=f"File too large. Maximum size is {settings.max_upload_size} bytes"
)
# Check file extension
if file.filename:
file_ext = Path(file.filename).suffix.lower()
if file_ext not in settings.allowed_extensions:
raise HTTPException(
status_code=400,
detail=f"File type not allowed. Allowed types: {', '.join(settings.allowed_extensions)}"
)
content = await file.read() content = await file.read()
content_str = content.decode("utf-8") content_str = content.decode("utf-8")
@@ -386,11 +409,23 @@ async def upload_unit_file(file: UploadFile = File(...)):
async def get_info(): async def get_info():
"""Get application information.""" """Get application information."""
return { return {
"name": "UnitForge", "name": settings.app_name,
"version": "1.0.0", "version": settings.app_version,
"description": "Create, validate, and manage systemd unit files", "description": settings.app_description,
"environment": settings.environment,
"debug": settings.debug,
"supported_types": [t.value for t in UnitType], "supported_types": [t.value for t in UnitType],
"template_count": len(template_registry.list_templates()), "template_count": len(template_registry.list_templates()),
"max_upload_size": settings.max_upload_size,
"allowed_extensions": settings.allowed_extensions,
"features": {
"api_metrics": settings.enable_api_metrics,
"template_caching": settings.enable_template_caching,
"validation_caching": settings.enable_validation_caching,
"api_docs": settings.api_docs_enabled,
"swagger_ui": settings.swagger_ui_enabled,
"redoc": settings.redoc_enabled,
}
} }
@@ -398,10 +433,19 @@ async def get_info():
@app.get("/health") @app.get("/health")
async def health_check(): async def health_check():
"""Health check endpoint.""" """Health check endpoint."""
return {"status": "healthy", "service": "unitforge"} if not settings.health_check_enabled:
raise HTTPException(status_code=404, detail="Health check disabled")
return {
"status": "healthy",
"service": settings.app_name.lower(),
"version": settings.app_version,
"environment": settings.environment,
"timestamp": __import__("datetime").datetime.utcnow().isoformat()
}
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn # type: ignore import uvicorn # type: ignore
uvicorn.run(app, host="0.0.0.0", port=8000) # nosec B104 uvicorn.run(app, host=settings.host, port=settings.port) # nosec B104