refactor: extract Pydantic models to models.py

Phase 1.3 - Data Models isolieren

NEUE DATEI:
- backend/models.py: Alle Pydantic Models (122 Zeilen)
  * ProfileCreate, ProfileUpdate
  * WeightEntry, CircumferenceEntry, CaliperEntry
  * ActivityEntry, NutritionDay
  * LoginRequest, PasswordResetRequest, PasswordResetConfirm
  * AdminProfileUpdate

ÄNDERUNGEN:
- backend/main.py:
  * Import models from models.py
  * Entfernt: ~60 Zeilen Model-Definitionen
  * Von 2025 → 1878 Zeilen (-147 Zeilen / -7%)

PROGRESS:
 db.py: Database + init_db
 auth.py: Auth functions + dependencies
 models.py: Pydantic schemas

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-03-19 09:53:51 +01:00
parent d826524789
commit d2791fe0bf
2 changed files with 125 additions and 68 deletions

View File

@ -17,6 +17,11 @@ from starlette.requests import Request
from db import get_db, get_cursor, r2d, init_db
from auth import hash_pin, verify_pin, make_token, get_session, require_auth, require_auth_flexible, require_admin
from models import (
ProfileCreate, ProfileUpdate, WeightEntry, CircumferenceEntry,
CaliperEntry, ActivityEntry, NutritionDay, LoginRequest,
PasswordResetRequest, PasswordResetConfirm, AdminProfileUpdate
)
DATA_DIR = Path(os.getenv("DATA_DIR", "./data"))
PHOTOS_DIR = Path(os.getenv("PHOTOS_DIR", "./photos"))
@ -63,60 +68,10 @@ def get_pid(x_profile_id: Optional[str] = Header(default=None)) -> str:
raise HTTPException(400, "Kein Profil gefunden")
# ── Models ────────────────────────────────────────────────────────────────────
class ProfileCreate(BaseModel):
name: str
avatar_color: Optional[str] = '#1D9E75'
sex: Optional[str] = 'm'
dob: Optional[str] = None
height: Optional[float] = 178
goal_weight: Optional[float] = None
goal_bf_pct: Optional[float] = None
class ProfileUpdate(BaseModel):
name: Optional[str] = None
avatar_color: Optional[str] = None
sex: Optional[str] = None
dob: Optional[str] = None
height: Optional[float] = None
goal_weight: Optional[float] = None
goal_bf_pct: Optional[float] = None
class WeightEntry(BaseModel):
date: str; weight: float; note: Optional[str]=None
class CircumferenceEntry(BaseModel):
date: str
c_neck: Optional[float]=None; c_chest: Optional[float]=None
c_waist: Optional[float]=None; c_belly: Optional[float]=None
c_hip: Optional[float]=None; c_thigh: Optional[float]=None
c_calf: Optional[float]=None; c_arm: Optional[float]=None
notes: Optional[str]=None; photo_id: Optional[str]=None
class CaliperEntry(BaseModel):
date: str; sf_method: Optional[str]='jackson3'
sf_chest: Optional[float]=None; sf_axilla: Optional[float]=None
sf_triceps: Optional[float]=None; sf_subscap: Optional[float]=None
sf_suprailiac: Optional[float]=None; sf_abdomen: Optional[float]=None
sf_thigh: Optional[float]=None; sf_calf_med: Optional[float]=None
sf_lowerback: Optional[float]=None; sf_biceps: Optional[float]=None
body_fat_pct: Optional[float]=None; lean_mass: Optional[float]=None
fat_mass: Optional[float]=None; notes: Optional[str]=None
class ActivityEntry(BaseModel):
date: str; start_time: Optional[str]=None; end_time: Optional[str]=None
activity_type: str; duration_min: Optional[float]=None
kcal_active: Optional[float]=None; kcal_resting: Optional[float]=None
hr_avg: Optional[float]=None; hr_max: Optional[float]=None
distance_km: Optional[float]=None; rpe: Optional[int]=None
source: Optional[str]='manual'; notes: Optional[str]=None
class NutritionDay(BaseModel):
date: str; kcal: Optional[float]=None; protein_g: Optional[float]=None
fat_g: Optional[float]=None; carbs_g: Optional[float]=None
# ── Profiles ──────────────────────────────────────────────────────────────────
from datetime import timedelta
# Models moved to models.py
# Auth functions moved to auth.py
@app.get("/api/profiles")
@ -1092,17 +1047,6 @@ def get_ai_usage(x_profile_id: Optional[str]=Header(default=None), session: dict
}
# ── Auth ──────────────────────────────────────────────────────────────────────
class LoginRequest(BaseModel):
email: str
password: str
class PasswordResetRequest(BaseModel):
email: str
class PasswordResetConfirm(BaseModel):
token: str
new_password: str
@app.post("/api/auth/login")
@limiter.limit("5/minute")
async def login(req: LoginRequest, request: Request):
@ -1250,12 +1194,6 @@ def password_reset_confirm(req: PasswordResetConfirm):
return {"ok": True, "message": "Passwort erfolgreich zurückgesetzt"}
# ── Admin ─────────────────────────────────────────────────────────────────────
class AdminProfileUpdate(BaseModel):
role: Optional[str] = None
ai_enabled: Optional[int] = None
ai_limit_day: Optional[int] = None
export_enabled: Optional[int] = None
@app.get("/api/admin/profiles")
def admin_list_profiles(session: dict=Depends(require_admin)):
"""Admin: List all profiles with stats."""

119
backend/models.py Normal file
View File

@ -0,0 +1,119 @@
"""
Pydantic Models for Mitai Jinkendo API
Data validation schemas for request/response bodies.
"""
from typing import Optional
from pydantic import BaseModel
# ── Profile Models ────────────────────────────────────────────────────────────
class ProfileCreate(BaseModel):
name: str
avatar_color: Optional[str] = '#1D9E75'
sex: Optional[str] = 'm'
dob: Optional[str] = None
height: Optional[float] = 178
goal_weight: Optional[float] = None
goal_bf_pct: Optional[float] = None
class ProfileUpdate(BaseModel):
name: Optional[str] = None
avatar_color: Optional[str] = None
sex: Optional[str] = None
dob: Optional[str] = None
height: Optional[float] = None
goal_weight: Optional[float] = None
goal_bf_pct: Optional[float] = None
# ── Tracking Models ───────────────────────────────────────────────────────────
class WeightEntry(BaseModel):
date: str
weight: float
note: Optional[str] = None
class CircumferenceEntry(BaseModel):
date: str
c_neck: Optional[float] = None
c_chest: Optional[float] = None
c_waist: Optional[float] = None
c_belly: Optional[float] = None
c_hip: Optional[float] = None
c_thigh: Optional[float] = None
c_calf: Optional[float] = None
c_arm: Optional[float] = None
notes: Optional[str] = None
photo_id: Optional[str] = None
class CaliperEntry(BaseModel):
date: str
sf_method: Optional[str] = 'jackson3'
sf_chest: Optional[float] = None
sf_axilla: Optional[float] = None
sf_triceps: Optional[float] = None
sf_subscap: Optional[float] = None
sf_suprailiac: Optional[float] = None
sf_abdomen: Optional[float] = None
sf_thigh: Optional[float] = None
sf_calf_med: Optional[float] = None
sf_lowerback: Optional[float] = None
sf_biceps: Optional[float] = None
body_fat_pct: Optional[float] = None
lean_mass: Optional[float] = None
fat_mass: Optional[float] = None
notes: Optional[str] = None
class ActivityEntry(BaseModel):
date: str
start_time: Optional[str] = None
end_time: Optional[str] = None
activity_type: str
duration_min: Optional[float] = None
kcal_active: Optional[float] = None
kcal_resting: Optional[float] = None
hr_avg: Optional[float] = None
hr_max: Optional[float] = None
distance_km: Optional[float] = None
rpe: Optional[int] = None
source: Optional[str] = 'manual'
notes: Optional[str] = None
class NutritionDay(BaseModel):
date: str
kcal: Optional[float] = None
protein_g: Optional[float] = None
fat_g: Optional[float] = None
carbs_g: Optional[float] = None
# ── Auth Models ───────────────────────────────────────────────────────────────
class LoginRequest(BaseModel):
email: str
password: str
class PasswordResetRequest(BaseModel):
email: str
class PasswordResetConfirm(BaseModel):
token: str
new_password: str
# ── Admin Models ──────────────────────────────────────────────────────────────
class AdminProfileUpdate(BaseModel):
role: Optional[str] = None
ai_enabled: Optional[int] = None
ai_limit_day: Optional[int] = None
export_enabled: Optional[int] = None