Files
timing/backend/api.py
T
steinhelge 330ba7a93d Initial commit: MVP tidtakingssystem
- Backend: FastAPI, EXIF-parser, EasyOCR, SQLite
- Frontend: React admin (startliste, passeringer, gjennomgang, resultater)
- Docker: docker-compose med depot/processed/data-volumer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 15:01:33 +01:00

178 lines
4.5 KiB
Python

"""
FastAPI REST API for timing-systemet.
"""
import asyncio
import logging
from contextlib import asynccontextmanager
from pathlib import Path
import aiosqlite
from fastapi import Depends, FastAPI, File, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from typing import Optional
from ingest import watch_depot, process_existing
from passage_log import delete_passage, get_passages, log_passage, resolve_passage
from profile_db import (
clear_startlist,
delete_athlete,
get_db,
import_startlist_csv,
list_athletes,
upsert_athlete,
)
from results import get_results
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
# Behandle bilder som lå i depot fra før oppstart
await process_existing()
# Start fil-overvåkning i bakgrunnen
asyncio.create_task(watch_depot())
yield
app = FastAPI(title="Timing API", version="0.1.0", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# --- DB-avhengighet ---
async def get_connection():
db = await get_db()
try:
yield db
finally:
await db.close()
# =====================
# Startliste-endepunkter
# =====================
@app.get("/api/athletes")
async def list_athletes_endpoint(db=Depends(get_connection)):
return await list_athletes(db)
@app.post("/api/athletes/import", summary="Last opp startliste som CSV")
async def import_csv(
file: UploadFile = File(...),
db=Depends(get_connection),
):
if not file.filename.endswith(".csv"):
raise HTTPException(400, "Kun CSV-filer støttes")
content = (await file.read()).decode("utf-8-sig") # utf-8-sig håndterer BOM
result = await import_startlist_csv(db, content)
return result
@app.delete("/api/athletes/all")
async def clear_all_athletes(db=Depends(get_connection)):
await clear_startlist(db)
return {"ok": True}
@app.delete("/api/athletes/{profile_id}")
async def remove_athlete(profile_id: str, db=Depends(get_connection)):
ok = await delete_athlete(db, profile_id)
if not ok:
raise HTTPException(404, "Utøver ikke funnet")
return {"ok": True}
# =====================
# Passeringer
# =====================
@app.get("/api/passages")
async def list_passages(
station: Optional[str] = None,
needs_review: Optional[bool] = None,
profile_id: Optional[str] = None,
db=Depends(get_connection),
):
return await get_passages(db, profile_id=profile_id, station=station, needs_review=needs_review)
@app.get("/api/passages/review", summary="Hent passeringer som trenger manuell gjennomgang")
async def review_queue(db=Depends(get_connection)):
return await get_passages(db, needs_review=True)
class ResolveRequest(BaseModel):
profile_id: Optional[str] = None
bib_number: Optional[str] = None
review_note: Optional[str] = None
@app.post("/api/passages/{passage_id}/resolve")
async def resolve(passage_id: str, body: ResolveRequest, db=Depends(get_connection)):
ok = await resolve_passage(
db,
passage_id,
profile_id=body.profile_id,
bib_number=body.bib_number,
review_note=body.review_note,
)
if not ok:
raise HTTPException(404, "Passering ikke funnet")
return {"ok": True}
@app.delete("/api/passages/{passage_id}")
async def remove_passage(passage_id: str, db=Depends(get_connection)):
ok = await delete_passage(db, passage_id)
if not ok:
raise HTTPException(404, "Passering ikke funnet")
return {"ok": True}
# =====================
# Resultater
# =====================
@app.get("/api/results")
async def get_results_endpoint(db=Depends(get_connection)):
return await get_results(db)
# =====================
# Bilder
# =====================
@app.get("/api/images/{path:path}")
async def serve_image(path: str):
"""Lever behandlede bilder til frontend."""
full_path = Path("/processed") / path
if not full_path.exists() or not full_path.is_file():
raise HTTPException(404, "Bilde ikke funnet")
# Sjekk at stien er innenfor /processed (path traversal)
try:
full_path.resolve().relative_to(Path("/processed").resolve())
except ValueError:
raise HTTPException(403, "Forbudt")
return FileResponse(full_path)
# =====================
# Helse
# =====================
@app.get("/api/health")
async def health():
return {"status": "ok"}