Files
lifetrack/apps/api/app/modules/imports/router.py
T
MeeJayandClaude Opus 5 93f0689c1e Initial import: LifeTrack v1 (santé, vape, finances)
Tracker de vie auto-hébergé : suivi poids/calories/sport avec planning de
pesées, sevrage tabac (vape) avec modèle de coût DIY et économies, et
finances personnelles avec import de relevés bancaires.

Architecture : FastAPI + SQLAlchemy 2.0 + PostgreSQL 16, React 18 + TS +
Vite + Tailwind + ECharts, déploiement Docker Compose. Modules
auto-découverts des deux côtés (pkgutil / import.meta.glob) et framework
de connecteurs à deux voies (importeurs de fichiers + ingestion JSON)
pour brancher de nouvelles sources sans toucher au noyau.

Validé : 292 tests pytest, tsc + vite build, contrat API/web vérifié
contre le schéma OpenAPI, et déploiement Docker réel sur PostgreSQL 16
(28 tables, SPA servie par nginx, wizard de premier démarrage).

Documentation : README.md, docs/GUIDE.md, CONVENTIONS.md, et les
documents de conception et de recherche dans docs/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 10:48:57 +02:00

90 lines
3.0 KiB
Python

from typing import Annotated
from fastapi import APIRouter, Depends, File, Form, Header, Query, UploadFile
from fastapi.security import HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
from app.core.config import get_settings
from app.core.database import get_db
from app.core.dependencies import authenticate_ingest, bearer, get_current_user
from app.core.errors import PayloadTooLargeError
from app.core.pagination import Page, PageParams, paginate
from app.modules.auth.models import User
from app.modules.imports import service
from app.modules.imports.schemas import (
ImportRunRead,
IngestPayload,
IngestResponse,
SourceInfo,
)
router = APIRouter(prefix="/imports", tags=["imports"])
ingest_router = APIRouter(prefix="/ingest", tags=["ingest"])
# Mounted by the module loader in addition to `router` (architecture §5.5).
extra_routers = [ingest_router]
DbDep = Annotated[Session, Depends(get_db)]
UserDep = Annotated[User, Depends(get_current_user)]
@router.get("/sources", response_model=list[SourceInfo])
def list_sources(user: UserDep) -> list[SourceInfo]:
return service.list_sources()
@router.post("", response_model=ImportRunRead, status_code=201)
def upload_import(
db: DbDep,
user: UserDep,
file: Annotated[UploadFile, File()],
source: Annotated[str, Form()],
) -> ImportRunRead:
data = file.file.read()
if len(data) > get_settings().max_upload_bytes:
raise PayloadTooLargeError("Fichier trop volumineux (limite : 20 Mio).")
filename = file.filename or "import"
importer_id = service.resolve_importer_id(source, filename, data[:4096])
run = service.run_import(db, user, importer_id, filename, data)
return ImportRunRead.model_validate(run)
@router.get("", response_model=Page[ImportRunRead])
def list_imports(
db: DbDep,
user: UserDep,
params: Annotated[PageParams, Depends()],
domain: Annotated[str | None, Query()] = None,
sort: Annotated[str | None, Query()] = None,
) -> Page[ImportRunRead]:
stmt = service.runs_query(user.id, domain, sort)
items, total = paginate(db, stmt, params)
return Page(
items=[ImportRunRead.model_validate(run) for run in items],
total=total,
page=params.page,
page_size=params.page_size,
)
@router.get("/{run_id}", response_model=ImportRunRead)
def get_import(run_id: int, db: DbDep, user: UserDep) -> ImportRunRead:
return ImportRunRead.model_validate(service.get_run(db, user.id, run_id))
@router.delete("/{run_id}", status_code=204)
def rollback_import(run_id: int, db: DbDep, user: UserDep) -> None:
service.rollback_run(db, user.id, run_id)
@ingest_router.post("/{domain}", response_model=IngestResponse)
def ingest(
domain: str,
payload: IngestPayload,
db: DbDep,
credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(bearer)],
x_api_key: Annotated[str | None, Header(alias="X-API-Key")] = None,
) -> IngestResponse:
user = authenticate_ingest(db, credentials, x_api_key, f"ingest:{domain}")
return service.run_ingest(db, user, domain, payload)