feat(01-01): Backend FastAPI skeleton + Docker build config
- FastAPI app with SPAStaticFiles catch-all (root_path NOT in constructor)
- config.py with DATA_PATH, PROMPTS_PATH, OUTPUTS_PATH, CAMPAIGNS_PATH, CONFIG_PATH
- Startup lifespan creates data directories automatically
- Health endpoint: GET /api/health -> {"status": "ok"}
- Dockerfile multi-stage: node:22-slim builds React, python:3.12-slim serves API+SPA
- --root-path /postgenerator set in Uvicorn CMD only (avoids Pitfall #4)
- docker-compose.yml: lab-postgenerator-app, proxy_net, named volume for data persistence
- requirements.txt with pinned versions: fastapi[standard]==0.135.1, anthropic==0.84.0
This commit is contained in:
0
backend/__init__.py
Normal file
0
backend/__init__.py
Normal file
53
backend/config.py
Normal file
53
backend/config.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Centralized configuration and path constants for PostGenerator."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Path constants (read from env, defaulting to ./data for local dev)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DATA_PATH = Path(os.getenv("DATA_PATH", "./data"))
|
||||
PROMPTS_PATH = DATA_PATH / "prompts"
|
||||
OUTPUTS_PATH = DATA_PATH / "outputs"
|
||||
CAMPAIGNS_PATH = DATA_PATH / "campaigns"
|
||||
CONFIG_PATH = DATA_PATH / "config"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# App settings
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class Settings:
|
||||
anthropic_api_key: str
|
||||
llm_model: str
|
||||
data_path: Path
|
||||
prompts_path: Path
|
||||
outputs_path: Path
|
||||
campaigns_path: Path
|
||||
config_path: Path
|
||||
|
||||
|
||||
def get_settings() -> Settings:
|
||||
"""Load settings from environment variables."""
|
||||
api_key = os.getenv("ANTHROPIC_API_KEY", "")
|
||||
if not api_key:
|
||||
import warnings
|
||||
warnings.warn(
|
||||
"ANTHROPIC_API_KEY not set — generation endpoints will fail",
|
||||
RuntimeWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
return Settings(
|
||||
anthropic_api_key=api_key,
|
||||
llm_model=os.getenv("LLM_MODEL", "claude-sonnet-4-5"),
|
||||
data_path=DATA_PATH,
|
||||
prompts_path=PROMPTS_PATH,
|
||||
outputs_path=OUTPUTS_PATH,
|
||||
campaigns_path=CAMPAIGNS_PATH,
|
||||
config_path=CONFIG_PATH,
|
||||
)
|
||||
78
backend/main.py
Normal file
78
backend/main.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""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).
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Create required data directories on startup if they do not exist."""
|
||||
for directory in (PROMPTS_PATH, OUTPUTS_PATH, CAMPAIGNS_PATH, CONFIG_PATH):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
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"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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")
|
||||
0
backend/routers/__init__.py
Normal file
0
backend/routers/__init__.py
Normal file
Reference in New Issue
Block a user