Files
postgenerator/backend/main.py
Michele d64c7f4524 feat(03-01): SwipeService CRUD + Pydantic schemas + FastAPI router
- backend/schemas/swipe.py: SwipeItem, SwipeItemCreate, SwipeItemUpdate, SwipeListResponse
- backend/services/swipe_service.py: SwipeService con load/save/add/update/delete/mark_used su swipe_file.json
- backend/routers/swipe.py: 5 endpoint REST (GET/POST/PUT/DELETE/mark-used), lazy init pattern
- backend/main.py: registra swipe.router prima del SPA catch-all mount
2026-03-09 00:22:12 +01:00

106 lines
3.8 KiB
Python

"""PostGenerator FastAPI application.
IMPORTANT: root_path is intentionally NOT set in the FastAPI() constructor.
It is passed only via Uvicorn's --root-path flag at runtime to avoid the
double-path bug (Pitfall #4).
"""
import shutil
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from backend.config import CAMPAIGNS_PATH, CONFIG_PATH, OUTPUTS_PATH, PROMPTS_PATH
from backend.routers import calendar, export, generate, prompts, settings, swipe
# ---------------------------------------------------------------------------
# SPA static files with catch-all fallback
# ---------------------------------------------------------------------------
class SPAStaticFiles(StaticFiles):
"""Serve a React SPA: return index.html for any 404 so the client-side
router handles unknown paths instead of returning a 404 from the server."""
async def get_response(self, path: str, scope):
try:
return await super().get_response(path, scope)
except Exception:
# Fall back to index.html for client-side routing
return await super().get_response("index.html", scope)
# ---------------------------------------------------------------------------
# Startup / shutdown lifecycle
# ---------------------------------------------------------------------------
# Directory dei prompt di default (inclusa nel source)
_DEFAULT_PROMPTS_DIR = Path(__file__).parent / "data" / "prompts"
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Create required data directories on startup if they do not exist.
Also copies default prompts to PROMPTS_PATH on first run (when empty).
"""
# Crea directory dati
for directory in (PROMPTS_PATH, OUTPUTS_PATH, CAMPAIGNS_PATH, CONFIG_PATH):
directory.mkdir(parents=True, exist_ok=True)
# Copia prompt default al primo avvio (se PROMPTS_PATH è vuota)
if _DEFAULT_PROMPTS_DIR.exists() and not any(PROMPTS_PATH.glob("*.txt")):
for prompt_file in _DEFAULT_PROMPTS_DIR.glob("*.txt"):
dest = PROMPTS_PATH / prompt_file.name
if not dest.exists():
shutil.copy2(prompt_file, dest)
yield
# Nothing to clean up on shutdown
# ---------------------------------------------------------------------------
# Application
# ---------------------------------------------------------------------------
# CRITICAL: Do NOT pass root_path here — use Uvicorn --root-path instead.
app = FastAPI(
title="PostGenerator",
description="Instagram carousel post generator powered by Claude",
version="0.1.0",
lifespan=lifespan,
# root_path is intentionally absent — set via Uvicorn --root-path at runtime
)
# ---------------------------------------------------------------------------
# API routes (must be registered BEFORE the SPA catch-all mount)
# ---------------------------------------------------------------------------
@app.get("/api/health")
async def health() -> dict:
"""Health check endpoint."""
return {"status": "ok"}
# Include all API routers — ORDER MATTERS (registered before SPA catch-all)
app.include_router(calendar.router)
app.include_router(generate.router)
app.include_router(export.router)
app.include_router(settings.router)
app.include_router(prompts.router)
app.include_router(swipe.router)
# ---------------------------------------------------------------------------
# SPA catch-all mount (MUST be last — catches everything not matched above)
# ---------------------------------------------------------------------------
_STATIC_DIR = Path(__file__).parent.parent / "static"
if _STATIC_DIR.exists():
app.mount("/", SPAStaticFiles(directory=str(_STATIC_DIR), html=True), name="spa")