Files
lifetrack/apps/api/app/modules/vape/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

323 lines
9.9 KiB
Python

"""Vape API (datamodel-health-vape.md §8.4) — thin routing layer.
Static sub-paths (/mixes/calculator, /coils/stats, /coils/change) are declared
before their `{id}` siblings so FastAPI never routes them to the item handler.
"""
from datetime import date
from typing import Annotated
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.dependencies import get_current_user
from app.core.pagination import Page, PageParams
from app.modules.auth.models import User
from app.modules.vape import service
from app.modules.vape.models import LiquidEntryKind, ProductKind
from app.modules.vape.schemas import (
CoilChangeCreate,
CoilChangeCreated,
CoilChangeRead,
CoilChangeUpdate,
LiquidEntryCreate,
LiquidEntryRead,
LiquidEntryUpdate,
MilestoneRead,
MixCalculatorRequest,
MixCalculatorResult,
MixCreate,
MixRead,
MixUpdate,
ProductCreate,
ProductRead,
ProductUpdate,
PurchaseCreate,
PurchaseRead,
PurchaseUpdate,
StatsResponse,
VapeDashboard,
VapeSettingsPut,
VapeSettingsRead,
)
router = APIRouter(prefix="/vape", tags=["vape"])
DbDep = Annotated[Session, Depends(get_db)]
UserDep = Annotated[User, Depends(get_current_user)]
PageDep = Annotated[PageParams, Depends()]
FromQuery = Annotated[date | None, Query(alias="from")]
ToQuery = Annotated[date | None, Query()]
TzQuery = Annotated[str | None, Query()]
SortQuery = Annotated[str | None, Query()]
# --------------------------------------------------------------------- settings
@router.get("/settings", response_model=VapeSettingsRead)
def read_settings(db: DbDep, user: UserDep) -> VapeSettingsRead:
return VapeSettingsRead.model_validate(service.get_settings(db, user.id))
@router.put("/settings", response_model=VapeSettingsRead)
def write_settings(
payload: VapeSettingsPut, db: DbDep, user: UserDep
) -> VapeSettingsRead:
return VapeSettingsRead.model_validate(service.put_settings(db, user.id, payload))
# --------------------------------------------------------------------- products
@router.get("/products", response_model=Page[ProductRead])
def list_products(
db: DbDep,
user: UserDep,
params: PageDep,
kind: Annotated[ProductKind | None, Query()] = None,
include_archived: Annotated[bool, Query()] = False,
sort: SortQuery = None,
) -> Page[ProductRead]:
return service.list_products(db, user.id, params, kind, include_archived, sort)
@router.post("/products", response_model=ProductRead, status_code=201)
def create_product(payload: ProductCreate, db: DbDep, user: UserDep) -> ProductRead:
return service.create_product(db, user.id, payload)
@router.patch("/products/{product_id}", response_model=ProductRead)
def update_product(
product_id: int, payload: ProductUpdate, db: DbDep, user: UserDep
) -> ProductRead:
return service.update_product(db, user.id, product_id, payload)
@router.delete("/products/{product_id}", status_code=204)
def delete_product(product_id: int, db: DbDep, user: UserDep) -> None:
service.delete_product(db, user.id, product_id)
# ------------------------------------------------------------------------ mixes
@router.get("/mixes", response_model=Page[MixRead])
def list_mixes(
db: DbDep,
user: UserDep,
params: PageDep,
include_archived: Annotated[bool, Query()] = False,
sort: SortQuery = None,
) -> Page[MixRead]:
return service.list_mixes(db, user.id, params, include_archived, sort)
@router.post("/mixes", response_model=MixRead, status_code=201)
def create_mix(payload: MixCreate, db: DbDep, user: UserDep) -> MixRead:
return service.create_mix(db, user.id, payload)
@router.post("/mixes/calculator", response_model=MixCalculatorResult)
def mix_calculator(
payload: MixCalculatorRequest, db: DbDep, user: UserDep
) -> MixCalculatorResult:
"""Stateless recipe assistant — persists nothing (§6.3)."""
return service.mix_calculator(db, user.id, payload)
@router.post("/mixes/{mix_id}/activate", response_model=MixRead)
def activate_mix(mix_id: int, db: DbDep, user: UserDep) -> MixRead:
return service.activate_mix(db, user.id, mix_id)
@router.patch("/mixes/{mix_id}", response_model=MixRead)
def update_mix(mix_id: int, payload: MixUpdate, db: DbDep, user: UserDep) -> MixRead:
return service.update_mix(db, user.id, mix_id, payload)
@router.delete("/mixes/{mix_id}", status_code=204)
def delete_mix(mix_id: int, db: DbDep, user: UserDep) -> None:
service.delete_mix(db, user.id, mix_id)
# ---------------------------------------------------------------------- liquids
@router.get("/liquids", response_model=Page[LiquidEntryRead])
def list_liquids(
db: DbDep,
user: UserDep,
params: PageDep,
from_: FromQuery = None,
to: ToQuery = None,
kind: Annotated[LiquidEntryKind | None, Query()] = None,
sort: SortQuery = None,
) -> Page[LiquidEntryRead]:
return service.list_liquids(db, user.id, params, from_, to, kind, sort)
@router.post("/liquids", response_model=LiquidEntryRead, status_code=201)
def create_liquid(
payload: LiquidEntryCreate, db: DbDep, user: UserDep
) -> LiquidEntryRead:
return service.create_liquid(db, user.id, payload)
@router.patch("/liquids/{entry_id}", response_model=LiquidEntryRead)
def update_liquid(
entry_id: int, payload: LiquidEntryUpdate, db: DbDep, user: UserDep
) -> LiquidEntryRead:
return service.update_liquid(db, user.id, entry_id, payload)
@router.delete("/liquids/{entry_id}", status_code=204)
def delete_liquid(entry_id: int, db: DbDep, user: UserDep) -> None:
service.delete_liquid(db, user.id, entry_id)
# ------------------------------------------------------------------------ coils
@router.get("/coils", response_model=Page[CoilChangeRead])
def list_coils(
db: DbDep,
user: UserDep,
params: PageDep,
from_: FromQuery = None,
to: ToQuery = None,
sort: SortQuery = None,
tz: TzQuery = None,
) -> Page[CoilChangeRead]:
return service.list_coils(db, user.id, params, from_, to, sort, tz)
@router.get("/coils/stats", response_model=StatsResponse)
def coil_stats(db: DbDep, user: UserDep, tz: TzQuery = None) -> StatsResponse:
return service.coil_stats(db, user.id, tz)
@router.post("/coils/change", response_model=CoilChangeCreated, status_code=201)
def quick_coil_change(
db: DbDep,
user: UserDep,
payload: CoilChangeCreate | None = None,
tz: TzQuery = None,
) -> CoilChangeCreated:
"""One-click « Résistance changée » : registers a change at `now` (§12.6)."""
return service.create_coil(db, user.id, payload or CoilChangeCreate(), tz)
@router.post("/coils", response_model=CoilChangeCreated, status_code=201)
def create_coil(
payload: CoilChangeCreate, db: DbDep, user: UserDep, tz: TzQuery = None
) -> CoilChangeCreated:
return service.create_coil(db, user.id, payload, tz)
@router.patch("/coils/{coil_id}", response_model=CoilChangeRead)
def update_coil(
coil_id: int,
payload: CoilChangeUpdate,
db: DbDep,
user: UserDep,
tz: TzQuery = None,
) -> CoilChangeRead:
return service.update_coil(db, user.id, coil_id, payload, tz)
@router.delete("/coils/{coil_id}", status_code=204)
def delete_coil(coil_id: int, db: DbDep, user: UserDep) -> None:
service.delete_coil(db, user.id, coil_id)
# -------------------------------------------------------------------- purchases
@router.get("/purchases", response_model=Page[PurchaseRead])
def list_purchases(
db: DbDep,
user: UserDep,
params: PageDep,
from_: FromQuery = None,
to: ToQuery = None,
product_id: Annotated[int | None, Query()] = None,
sort: SortQuery = None,
) -> Page[PurchaseRead]:
return service.list_purchases(db, user.id, params, from_, to, product_id, sort)
@router.post("/purchases", response_model=PurchaseRead, status_code=201)
def create_purchase(payload: PurchaseCreate, db: DbDep, user: UserDep) -> PurchaseRead:
return service.create_purchase(db, user.id, payload)
@router.patch("/purchases/{purchase_id}", response_model=PurchaseRead)
def update_purchase(
purchase_id: int, payload: PurchaseUpdate, db: DbDep, user: UserDep
) -> PurchaseRead:
return service.update_purchase(db, user.id, purchase_id, payload)
@router.delete("/purchases/{purchase_id}", status_code=204)
def delete_purchase(purchase_id: int, db: DbDep, user: UserDep) -> None:
service.delete_purchase(db, user.id, purchase_id)
# ------------------------------------------------------------- stats & overview
@router.get("/stats/consumption", response_model=StatsResponse)
def consumption_stats(
db: DbDep,
user: UserDep,
from_: FromQuery = None,
to: ToQuery = None,
tz: TzQuery = None,
) -> StatsResponse:
return service.consumption_stats(db, user.id, from_, to, tz)
@router.get("/stats/nicotine", response_model=StatsResponse)
def nicotine_stats(
db: DbDep,
user: UserDep,
from_: FromQuery = None,
to: ToQuery = None,
tz: TzQuery = None,
) -> StatsResponse:
return service.nicotine_stats(db, user.id, from_, to, tz)
@router.get("/stats/costs", response_model=StatsResponse)
def cost_stats(
db: DbDep,
user: UserDep,
from_: FromQuery = None,
to: ToQuery = None,
tz: TzQuery = None,
) -> StatsResponse:
return service.cost_stats(db, user.id, from_, to, tz)
@router.get("/stats/savings", response_model=StatsResponse)
def savings_stats(
db: DbDep,
user: UserDep,
from_: FromQuery = None,
to: ToQuery = None,
tz: TzQuery = None,
) -> StatsResponse:
return service.savings_stats(db, user.id, from_, to, tz)
@router.get("/milestones", response_model=list[MilestoneRead])
def milestones(db: DbDep, user: UserDep, tz: TzQuery = None) -> list[MilestoneRead]:
return service.milestones(db, user.id, tz)
@router.get("/dashboard", response_model=VapeDashboard)
def dashboard(db: DbDep, user: UserDep, tz: TzQuery = None) -> VapeDashboard:
return service.dashboard(db, user.id, tz)