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:
commit
8f7d893a63
32 changed files with 6282 additions and 0 deletions
133
backend/innercontext/api/skincare.py
Normal file
133
backend/innercontext/api/skincare.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue