from __future__ import annotations from datetime import datetime import sqlmodel from pydantic import BaseModel, Field from sqlalchemy import Column from sqlalchemy.types import JSON from sqlmodel import SQLModel from tracker.constants import MealLogSource, MealPattern, MealSlot class TrackerUser(SQLModel, table=True): """Dashboard user owning tracker rows.""" __tablename__ = "tracker_users" user_id: int | None = sqlmodel.Field(default=None, primary_key=True) email: str = sqlmodel.Field(unique=True, index=True) label: str = sqlmodel.Field(default="") role: str | None = sqlmodel.Field(default=None) created_at: datetime = sqlmodel.Field(default_factory=datetime.now) class Exercise(SQLModel, table=True): """Exercise catalog entry, seeded from wger/exercemus data.""" __tablename__ = "exercises" exercise_id: int | None = sqlmodel.Field(primary_key=True) name: str = sqlmodel.Field(unique=True, index=True) muscle_group: str = sqlmodel.Field() equipment: str = sqlmodel.Field() wger_id: int | None = sqlmodel.Field(index=True) starting_bodyweight_fraction: float | None = sqlmodel.Field(default=None) starting_load_kg: float | None = sqlmodel.Field(default=None) starting_load_reference: str = sqlmodel.Field(default="") class Program(SQLModel, table=True): """Named workout program template.""" __tablename__ = "programs" program_id: int | None = sqlmodel.Field(primary_key=True) user_id: int | None = sqlmodel.Field(default=None, index=True) name: str = sqlmodel.Field(unique=True) description: str = sqlmodel.Field() created_at: datetime = sqlmodel.Field(default_factory=datetime.now) class ProgramExercise(SQLModel, table=True): """An exercise within a program day, with rep/set targets.""" __tablename__ = "program_exercises" program_id: int = sqlmodel.Field(foreign_key="programs.program_id", primary_key=True) day_label: str = sqlmodel.Field(primary_key=True) exercise_id: int = sqlmodel.Field(foreign_key="exercises.exercise_id", primary_key=True) order_index: int = sqlmodel.Field() target_sets: int = sqlmodel.Field() target_rep_min: int = sqlmodel.Field() target_rep_max: int = sqlmodel.Field() weight_increment_kg: float = sqlmodel.Field() rest_seconds: int = sqlmodel.Field() superset_group: int | None = sqlmodel.Field(index=True) starting_weight_kg: float | None = sqlmodel.Field(default=None) class WorkoutLog(SQLModel, table=True): """Per-set workout log entry.""" __tablename__ = "workout_logs" log_id: int | None = sqlmodel.Field(primary_key=True) user_id: int | None = sqlmodel.Field(default=None, index=True) exercise_id: int = sqlmodel.Field(foreign_key="exercises.exercise_id", index=True) program_id: int = sqlmodel.Field(foreign_key="programs.program_id") day_label: str = sqlmodel.Field() logged_at: datetime = sqlmodel.Field(default_factory=datetime.now, index=True) set_number: int = sqlmodel.Field() reps_target: int = sqlmodel.Field() reps_done: int = sqlmodel.Field() weight_kg: float = sqlmodel.Field() completed: bool = sqlmodel.Field() class BodyWeightLog(SQLModel, table=True): """Daily body weight measurement.""" __tablename__ = "body_weight_logs" log_id: int | None = sqlmodel.Field(primary_key=True) user_id: int | None = sqlmodel.Field(default=None, index=True) logged_at: datetime = sqlmodel.Field(default_factory=datetime.now, index=True) weight_kg: float = sqlmodel.Field() class RunLog(SQLModel, table=True): """Running session for a specific program day (same scope as strength WorkoutLog batches).""" __tablename__ = "run_logs" run_log_id: int | None = sqlmodel.Field(default=None, primary_key=True) user_id: int | None = sqlmodel.Field(default=None, index=True) program_id: int | None = sqlmodel.Field(default=None, foreign_key="programs.program_id", index=True) day_label: str | None = sqlmodel.Field(default=None, index=True) logged_at: datetime = sqlmodel.Field(default_factory=datetime.now, index=True) distance_km: float = sqlmodel.Field() duration_minutes: float = sqlmodel.Field() notes: str = sqlmodel.Field(default="") class PlannedMeal(SQLModel, table=True): """A planned meal row in tracker.db (recipe id references parser corpus).""" __tablename__ = "planned_meals" planned_meal_id: int | None = sqlmodel.Field(default=None, primary_key=True) user_id: int | None = sqlmodel.Field(default=None, index=True) plan_date: str = sqlmodel.Field(index=True) meal_slot: str = sqlmodel.Field(index=True) recipe_id: int | None = sqlmodel.Field(default=None, index=True) notes: str = sqlmodel.Field(default="") planned_add_ins: list[str] = sqlmodel.Field( default_factory=list, sa_column=Column(JSON), ) class MealLogEntry(SQLModel, table=True): """Logged meal adherence or manual food entry.""" __tablename__ = "meal_log_entries" meal_log_id: int | None = sqlmodel.Field(default=None, primary_key=True) user_id: int | None = sqlmodel.Field(default=None, index=True) logged_at: datetime = sqlmodel.Field(default_factory=datetime.now, index=True) log_date: str = sqlmodel.Field(index=True) meal_slot: str = sqlmodel.Field(index=True) description: str = sqlmodel.Field() kcal: int | None = None protein_g: float | None = None recipe_id: int | None = sqlmodel.Field(default=None, index=True) source: str = sqlmodel.Field() class RecipeSearchDefaults(BaseModel): """Default filters for recipe search (agent profile).""" max_active_minutes: int | None = None max_total_minutes: int | None = None min_calories: int | None = None max_calories: int | None = None min_protein: float | None = None search_limit: int | None = None class AgentProfileYaml(BaseModel): """Optional fields loaded from agent_profile.yaml (partial).""" pantry_user_id: int | None = None meal_pattern: MealPattern | None = None default_timezone: str | None = None costco_event_label: str | None = None costco_duration_minutes: int | None = None weekly_climbing_sessions_target: int | None = None calorie_policy: str | None = None recipe_search_defaults: RecipeSearchDefaults | None = None raw_addins_defaults: list[str] = Field(default_factory=list) class ResolvedAgentProfile(BaseModel): """Merged agent profile for API and MCP (config + YAML).""" pantry_user_id: int active_program_id: int | None meal_pattern: MealPattern default_timezone: str costco_event_label: str costco_duration_minutes: int weekly_climbing_sessions_target: int | None calorie_policy: str recipe_search_defaults: RecipeSearchDefaults raw_addins_defaults: list[str] parser_data_dir: str default_calendar_id: str chores_calendar_id: str | None workout_calendar_id: str | None tracker_app_url: str class MealPlanUpsertInput(BaseModel): plan_date: str meal_slot: MealSlot recipe_id: int | None = None notes: str = "" planned_add_ins: list[str] = Field(default_factory=list) class MealPlanRow(BaseModel): planned_meal_id: int plan_date: str meal_slot: MealSlot recipe_id: int | None notes: str planned_add_ins: list[str] class MealLogCreateInput(BaseModel): log_date: str meal_slot: MealSlot description: str kcal: int | None = None protein_g: float | None = None recipe_id: int | None = None source: MealLogSource class MealLogRow(BaseModel): meal_log_id: int logged_at: datetime log_date: str meal_slot: MealSlot description: str kcal: int | None protein_g: float | None recipe_id: int | None source: MealLogSource