Initial commit: backend API, data models, and test suite

FastAPI backend for personal health and skincare data with MCP export.
Includes SQLModel models for products, inventory, medications, lab results,
routines, and skin condition snapshots. Pytest suite with 111 tests running
on SQLite in-memory (no PostgreSQL required).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Piotr Oleszczyk 2026-02-26 15:10:24 +01:00
commit 8f7d893a63
32 changed files with 6282 additions and 0 deletions

View file

@ -0,0 +1,133 @@
from datetime import date
from typing import Optional
from uuid import UUID, uuid4
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, SQLModel, select
from db import get_session
from innercontext.models import SkinConditionSnapshot
from innercontext.models.enums import (
BarrierState,
OverallSkinState,
SkinConcern,
SkinTrend,
SkinType,
)
router = APIRouter()
# ---------------------------------------------------------------------------
# Schemas
# ---------------------------------------------------------------------------
class SnapshotCreate(SQLModel):
snapshot_date: date
overall_state: Optional[OverallSkinState] = None
trend: Optional[SkinTrend] = None
skin_type: Optional[SkinType] = None
hydration_level: Optional[int] = None
sebum_tzone: Optional[int] = None
sebum_cheeks: Optional[int] = None
sensitivity_level: Optional[int] = None
barrier_state: Optional[BarrierState] = None
active_concerns: list[SkinConcern] = []
risks: list[str] = []
priorities: list[str] = []
notes: Optional[str] = None
class SnapshotUpdate(SQLModel):
snapshot_date: Optional[date] = None
overall_state: Optional[OverallSkinState] = None
trend: Optional[SkinTrend] = None
skin_type: Optional[SkinType] = None
hydration_level: Optional[int] = None
sebum_tzone: Optional[int] = None
sebum_cheeks: Optional[int] = None
sensitivity_level: Optional[int] = None
barrier_state: Optional[BarrierState] = None
active_concerns: Optional[list[SkinConcern]] = None
risks: Optional[list[str]] = None
priorities: Optional[list[str]] = None
notes: Optional[str] = None
# ---------------------------------------------------------------------------
# Helper
# ---------------------------------------------------------------------------
def get_or_404(session: Session, model, record_id) -> object:
obj = session.get(model, record_id)
if obj is None:
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
return obj
# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------
@router.get("/", response_model=list[SkinConditionSnapshot])
def list_snapshots(
from_date: Optional[date] = None,
to_date: Optional[date] = None,
overall_state: Optional[OverallSkinState] = None,
session: Session = Depends(get_session),
):
stmt = select(SkinConditionSnapshot)
if from_date is not None:
stmt = stmt.where(SkinConditionSnapshot.snapshot_date >= from_date)
if to_date is not None:
stmt = stmt.where(SkinConditionSnapshot.snapshot_date <= to_date)
if overall_state is not None:
stmt = stmt.where(SkinConditionSnapshot.overall_state == overall_state)
return session.exec(stmt).all()
@router.post("/", response_model=SkinConditionSnapshot, status_code=201)
def create_snapshot(
data: SnapshotCreate, session: Session = Depends(get_session)
):
snapshot = SkinConditionSnapshot(id=uuid4(), **data.model_dump())
session.add(snapshot)
session.commit()
session.refresh(snapshot)
return snapshot
@router.get("/{snapshot_id}", response_model=SkinConditionSnapshot)
def get_snapshot(snapshot_id: UUID, session: Session = Depends(get_session)):
return get_or_404(session, SkinConditionSnapshot, snapshot_id)
@router.patch("/{snapshot_id}", response_model=SkinConditionSnapshot)
def update_snapshot(
snapshot_id: UUID,
data: SnapshotUpdate,
session: Session = Depends(get_session),
):
snapshot = get_or_404(session, SkinConditionSnapshot, snapshot_id)
for key, value in data.model_dump(exclude_unset=True).items():
setattr(snapshot, key, value)
session.add(snapshot)
session.commit()
session.refresh(snapshot)
return snapshot
@router.delete("/{snapshot_id}", status_code=204)
def delete_snapshot(snapshot_id: UUID, session: Session = Depends(get_session)):
snapshot = get_or_404(session, SkinConditionSnapshot, snapshot_id)
session.delete(snapshot)
session.commit()