- Updated migration logic in main.py to allow skipping migrations during local development with SKIP_DB_MIGRATE environment variable. - Improved error handling for migration failures, ensuring the application does not start if migrations are incomplete. - Added a new health check endpoint (/api/health/ready) to verify database connection and essential tables, aiding in production debugging. - Enhanced run_migrations.py to support ordered execution of migration files and improved transaction handling. - Updated requirements.txt to include sqlparse for SQL statement parsing when psql is unavailable.
182 lines
5.6 KiB
Python
182 lines
5.6 KiB
Python
"""
|
|
Shinkan Jinkendo - Main Application Entry Point
|
|
|
|
Trainer- und Vereinsplattform für Kampfsport-Trainingsplanung
|
|
"""
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import JSONResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
import os
|
|
import sys
|
|
|
|
from slowapi import _rate_limit_exceeded_handler
|
|
from slowapi.errors import RateLimitExceeded
|
|
|
|
from version import APP_VERSION, BUILD_DATE, DB_SCHEMA_VERSION, MODULE_VERSIONS
|
|
|
|
# Run database migrations before API start — halbes Schema ist schlimmer als kein Start
|
|
# Lokal ohne DB / nur Tests: SKIP_DB_MIGRATE=1
|
|
if os.getenv("SKIP_DB_MIGRATE", "").strip().lower() in ("1", "true", "yes"):
|
|
print("⚠ SKIP_DB_MIGRATE=1 — Migrationen wurden übersprungen (nur für Entwicklung ohne DB)")
|
|
else:
|
|
try:
|
|
import run_migrations
|
|
|
|
rc = run_migrations.main()
|
|
if rc != 0:
|
|
print(f"✗ Datenbank-Migration fehlgeschlagen (Exit-Code {rc}). Start abgebrochen.")
|
|
sys.exit(1)
|
|
print("✓ Database migrations completed")
|
|
except SystemExit:
|
|
raise
|
|
except Exception as e:
|
|
print(f"✗ Migration-Laufzeitfehler: {e}")
|
|
sys.exit(1)
|
|
|
|
from routers.auth import limiter as auth_rate_limiter
|
|
|
|
# Initialize FastAPI app
|
|
app = FastAPI(
|
|
title="Shinkan Jinkendo API",
|
|
description="Trainer- und Vereinsplattform für Kampfsport-Trainingsplanung",
|
|
version=APP_VERSION
|
|
)
|
|
|
|
# SlowAPI: Rate Limits auf /api/auth/* (Decorator in routers/auth.py)
|
|
app.state.limiter = auth_rate_limiter
|
|
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
|
|
# CORS — kommaseparierte Liste (z. B. https://dev.shinkan… und http://192.168.x.x:3098)
|
|
_cors_raw = os.getenv("ALLOWED_ORIGINS", "http://localhost:3098")
|
|
ALLOWED_ORIGINS = [o.strip() for o in _cors_raw.split(",") if o.strip()]
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=ALLOWED_ORIGINS,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# TODO: Initialize Database with migrations
|
|
|
|
# Version Endpoint (public, no auth)
|
|
@app.get("/api/version")
|
|
def get_version():
|
|
"""Get application version and build info"""
|
|
return {
|
|
"app_version": APP_VERSION,
|
|
"build_date": BUILD_DATE,
|
|
"backend_version": APP_VERSION,
|
|
"modules": MODULE_VERSIONS,
|
|
"db_schema_version": DB_SCHEMA_VERSION,
|
|
"environment": os.getenv("ENVIRONMENT", "development")
|
|
}
|
|
|
|
# Health Check
|
|
@app.get("/health")
|
|
def health_check():
|
|
"""Health check endpoint"""
|
|
return {"status": "healthy", "version": APP_VERSION}
|
|
|
|
|
|
@app.get("/api/health/ready")
|
|
def health_ready():
|
|
"""
|
|
Verbindung + Kern-Tabellen prüfen (ohne Login).
|
|
Nutzen bei Prod-Debugging: wenn schema_complete=false, Migrationen nicht vollständig.
|
|
"""
|
|
from db import get_db, get_cursor
|
|
|
|
REQUIRED = (
|
|
"schema_migrations",
|
|
"skills",
|
|
"skill_main_categories",
|
|
"skill_categories",
|
|
"maturity_models",
|
|
"sessions",
|
|
)
|
|
tables: dict = {}
|
|
err: Optional[str] = None
|
|
try:
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
cur.execute("SELECT 1")
|
|
for tbl in REQUIRED:
|
|
cur.execute(
|
|
"""
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM information_schema.tables
|
|
WHERE table_schema = 'public' AND table_name = %s
|
|
)
|
|
""",
|
|
(tbl,),
|
|
)
|
|
row = cur.fetchone()
|
|
tables[tbl] = bool(next(iter(row.values()))) if row else False
|
|
migration_count = 0
|
|
if tables.get("schema_migrations"):
|
|
cur.execute("SELECT COUNT(*)::int FROM schema_migrations")
|
|
mig_row = cur.fetchone()
|
|
migration_count = (
|
|
int(list(mig_row.values())[0]) if mig_row is not None else 0
|
|
)
|
|
except Exception as e:
|
|
err = str(e)
|
|
migration_count = 0
|
|
|
|
complete = bool(err is None and all(tables.get(t) for t in REQUIRED))
|
|
return {
|
|
"status": "ready" if complete else "degraded",
|
|
"database": err is None,
|
|
"detail": err,
|
|
"schema_complete": complete,
|
|
"tables": tables,
|
|
"schema_migrations_count": migration_count,
|
|
}
|
|
|
|
|
|
# Root Endpoint
|
|
@app.get("/")
|
|
def read_root():
|
|
"""Root endpoint - API info"""
|
|
return {
|
|
"app": "Shinkan Jinkendo API",
|
|
"version": APP_VERSION,
|
|
"docs": "/docs",
|
|
"health": "/health"
|
|
}
|
|
|
|
# Register routers
|
|
from routers import auth, profiles, exercises, clubs, skills, training_planning, catalogs, maturity_models, matrix_stack_bundle, import_wiki, import_wiki_admin
|
|
|
|
app.include_router(auth.router)
|
|
app.include_router(profiles.router)
|
|
app.include_router(exercises.router)
|
|
app.include_router(clubs.router)
|
|
app.include_router(skills.router)
|
|
app.include_router(training_planning.router)
|
|
app.include_router(catalogs.router)
|
|
app.include_router(maturity_models.router)
|
|
app.include_router(matrix_stack_bundle.router)
|
|
app.include_router(import_wiki.router)
|
|
app.include_router(import_wiki_admin.router)
|
|
|
|
# Lokale Medien (Übungen-Uploads) unter MEDIA_ROOT, ausliefern unter /media/...
|
|
_media_dir = os.getenv("MEDIA_ROOT", str(Path(__file__).resolve().parent / "media"))
|
|
Path(_media_dir).mkdir(parents=True, exist_ok=True)
|
|
app.mount("/media", StaticFiles(directory=_media_dir), name="media")
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(
|
|
"main:app",
|
|
host="0.0.0.0",
|
|
port=8000,
|
|
reload=True
|
|
)
|