feat: add Doris Barbell v1 scaffold
This commit is contained in:
111
home/doris-barbell/app/routes/api.py
Normal file
111
home/doris-barbell/app/routes/api.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
|
||||
from app.models import (
|
||||
ExerciseTemplateCreate,
|
||||
MeasurementEntryCreate,
|
||||
ProfileUpdate,
|
||||
RoutineCreate,
|
||||
WeightEntryCreate,
|
||||
WorkoutCreate,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix='/api')
|
||||
|
||||
|
||||
@router.get('/health')
|
||||
def health() -> dict[str, str]:
|
||||
return {'status': 'ok'}
|
||||
|
||||
|
||||
@router.get('/dashboard/summary')
|
||||
def dashboard_summary(request: Request) -> dict:
|
||||
return request.app.state.store.dashboard_summary()
|
||||
|
||||
|
||||
@router.put('/profile/{user_id}')
|
||||
def update_profile(user_id: str, payload: ProfileUpdate, request: Request) -> dict:
|
||||
try:
|
||||
return request.app.state.store.update_profile(user_id, payload.model_dump())
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=404, detail='Unknown user') from exc
|
||||
|
||||
|
||||
@router.post('/profile/{user_id}/weight', status_code=201)
|
||||
def add_weight_entry(user_id: str, payload: WeightEntryCreate, request: Request) -> dict:
|
||||
try:
|
||||
return request.app.state.store.add_weight_entry(user_id, payload.model_dump())
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=404, detail='Unknown user') from exc
|
||||
|
||||
|
||||
@router.get('/history/{user_id}/weights')
|
||||
def weight_history(user_id: str, request: Request) -> list[dict]:
|
||||
try:
|
||||
return request.app.state.store.weight_history(user_id)
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=404, detail='Unknown user') from exc
|
||||
|
||||
|
||||
@router.post('/profile/{user_id}/measurements', status_code=201)
|
||||
def add_measurement_entry(user_id: str, payload: MeasurementEntryCreate, request: Request) -> dict:
|
||||
try:
|
||||
return request.app.state.store.add_measurement_entry(user_id, payload.model_dump())
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=404, detail='Unknown user') from exc
|
||||
|
||||
|
||||
@router.get('/history/{user_id}/measurements')
|
||||
def measurement_history(user_id: str, request: Request) -> list[dict]:
|
||||
try:
|
||||
return request.app.state.store.measurement_history(user_id)
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=404, detail='Unknown user') from exc
|
||||
|
||||
|
||||
@router.post('/exercises/templates', status_code=201)
|
||||
def create_exercise_template(payload: ExerciseTemplateCreate, request: Request) -> dict:
|
||||
return request.app.state.store.add_exercise_template(payload.model_dump())
|
||||
|
||||
|
||||
@router.get('/exercises/templates')
|
||||
def list_exercise_templates(request: Request) -> list[dict]:
|
||||
return request.app.state.store.list_exercise_templates()
|
||||
|
||||
|
||||
@router.post('/routines', status_code=201)
|
||||
def create_routine(payload: RoutineCreate, request: Request) -> dict:
|
||||
try:
|
||||
return request.app.state.store.add_routine(payload.model_dump())
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=404, detail='Unknown user') from exc
|
||||
|
||||
|
||||
@router.get('/routines')
|
||||
def list_routines(request: Request) -> list[dict]:
|
||||
return request.app.state.store.list_routines()
|
||||
|
||||
|
||||
@router.post('/workouts', status_code=201)
|
||||
def add_workout(payload: WorkoutCreate, request: Request) -> dict:
|
||||
try:
|
||||
return request.app.state.store.add_workout(payload.model_dump())
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=404, detail='Unknown user') from exc
|
||||
|
||||
|
||||
@router.get('/history/{user_id}/workouts')
|
||||
def workout_history(user_id: str, request: Request) -> list[dict]:
|
||||
try:
|
||||
return request.app.state.store.workout_history(user_id)
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=404, detail='Unknown user') from exc
|
||||
|
||||
|
||||
@router.get('/progression/{user_id}/exercises')
|
||||
def exercise_progression(user_id: str, request: Request) -> list[dict]:
|
||||
try:
|
||||
return request.app.state.store.exercise_progression(user_id)
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=404, detail='Unknown user') from exc
|
||||
210
home/doris-barbell/app/routes/ui.py
Normal file
210
home/doris-barbell/app/routes/ui.py
Normal file
@@ -0,0 +1,210 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Form, HTTPException, Request, status
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory='app/templates')
|
||||
|
||||
|
||||
@router.get('/')
|
||||
def home(request: Request):
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name='dashboard.html',
|
||||
context=_dashboard_context(request),
|
||||
)
|
||||
|
||||
|
||||
@router.post('/log/weight', status_code=303)
|
||||
def log_weight(
|
||||
request: Request,
|
||||
user_id: str = Form(...),
|
||||
recorded_at: str = Form(...),
|
||||
weight_lbs: float = Form(...),
|
||||
body_fat_percent: str = Form(''),
|
||||
notes: str = Form(''),
|
||||
):
|
||||
payload = {
|
||||
'recorded_at': recorded_at,
|
||||
'weight_lbs': weight_lbs,
|
||||
'body_fat_percent': _optional_float(body_fat_percent),
|
||||
'notes': notes.strip() or None,
|
||||
}
|
||||
request.app.state.store.add_weight_entry(user_id, payload)
|
||||
return RedirectResponse(url='/', status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
@router.post('/log/measurements', status_code=303)
|
||||
def log_measurements(
|
||||
request: Request,
|
||||
user_id: str = Form(...),
|
||||
recorded_at: str = Form(...),
|
||||
waist_inches: str = Form(''),
|
||||
hip_inches: str = Form(''),
|
||||
chest_inches: str = Form(''),
|
||||
neck_inches: str = Form(''),
|
||||
notes: str = Form(''),
|
||||
):
|
||||
payload = {
|
||||
'recorded_at': recorded_at,
|
||||
'waist_inches': _optional_float(waist_inches),
|
||||
'hip_inches': _optional_float(hip_inches),
|
||||
'chest_inches': _optional_float(chest_inches),
|
||||
'neck_inches': _optional_float(neck_inches),
|
||||
'notes': notes.strip() or None,
|
||||
}
|
||||
if not any(
|
||||
payload[key] is not None
|
||||
for key in ('waist_inches', 'hip_inches', 'chest_inches', 'neck_inches')
|
||||
):
|
||||
raise HTTPException(status_code=400, detail='At least one body measurement is required.')
|
||||
request.app.state.store.add_measurement_entry(user_id, payload)
|
||||
return RedirectResponse(url='/', status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
@router.post('/templates', status_code=303)
|
||||
def create_template(
|
||||
request: Request,
|
||||
name: str = Form(...),
|
||||
primary_muscle: str = Form(''),
|
||||
equipment: str = Form(''),
|
||||
notes: str = Form(''),
|
||||
):
|
||||
request.app.state.store.add_exercise_template(
|
||||
{
|
||||
'name': name.strip(),
|
||||
'primary_muscle': primary_muscle.strip() or None,
|
||||
'equipment': equipment.strip() or None,
|
||||
'notes': notes.strip() or None,
|
||||
}
|
||||
)
|
||||
return RedirectResponse(url='/', status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
@router.post('/routines', status_code=303)
|
||||
def create_routine(
|
||||
request: Request,
|
||||
user_id: str = Form(...),
|
||||
name: str = Form(...),
|
||||
exercise_lines: str = Form(...),
|
||||
notes: str = Form(''),
|
||||
):
|
||||
payload = {
|
||||
'user_id': user_id,
|
||||
'name': name.strip(),
|
||||
'notes': notes.strip() or None,
|
||||
'exercises': _parse_routine_exercises(exercise_lines),
|
||||
}
|
||||
request.app.state.store.add_routine(payload)
|
||||
return RedirectResponse(url='/', status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
@router.post('/workouts/quick-log', status_code=303)
|
||||
def quick_log_workout(
|
||||
request: Request,
|
||||
user_id: str = Form(...),
|
||||
performed_at: str = Form(...),
|
||||
routine_name: str = Form(...),
|
||||
exercise_lines: str = Form(...),
|
||||
notes: str = Form(''),
|
||||
):
|
||||
payload = {
|
||||
'user_id': user_id,
|
||||
'performed_at': performed_at,
|
||||
'routine_name': routine_name.strip(),
|
||||
'notes': notes.strip() or None,
|
||||
'exercises': _parse_workout_exercises(exercise_lines),
|
||||
}
|
||||
request.app.state.store.add_workout(payload)
|
||||
return RedirectResponse(url='/', status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
def _dashboard_context(request: Request) -> dict:
|
||||
store = request.app.state.store
|
||||
summary = store.dashboard_summary()
|
||||
return {
|
||||
'title': 'Doris Barbell',
|
||||
'summary': summary,
|
||||
'john_weight_history': store.weight_history('john')[:5],
|
||||
'john_measurement_history': store.measurement_history('john')[:5],
|
||||
'john_workout_history': store.workout_history('john')[:5],
|
||||
'john_progression': store.exercise_progression('john')[:5],
|
||||
}
|
||||
|
||||
|
||||
def _optional_float(value: str) -> float | None:
|
||||
cleaned = value.strip()
|
||||
if not cleaned:
|
||||
return None
|
||||
try:
|
||||
return float(cleaned)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=f'Invalid numeric value: {value}') from exc
|
||||
|
||||
|
||||
def _parse_routine_exercises(raw_lines: str) -> list[dict]:
|
||||
exercises = []
|
||||
for line in raw_lines.splitlines():
|
||||
cleaned = line.strip()
|
||||
if not cleaned:
|
||||
continue
|
||||
parts = [part.strip() for part in cleaned.split('|')]
|
||||
if len(parts) < 3:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail='Routine lines must use exercise|sets|reps|optional notes format.',
|
||||
)
|
||||
exercise_name, target_sets_raw, target_reps = parts[:3]
|
||||
try:
|
||||
target_sets = int(target_sets_raw)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=f'Invalid target sets: {target_sets_raw}') from exc
|
||||
exercises.append(
|
||||
{
|
||||
'exercise_name': exercise_name,
|
||||
'target_sets': target_sets,
|
||||
'target_reps': target_reps,
|
||||
'notes': parts[3] if len(parts) > 3 and parts[3] else None,
|
||||
}
|
||||
)
|
||||
if not exercises:
|
||||
raise HTTPException(status_code=400, detail='At least one routine exercise is required.')
|
||||
return exercises
|
||||
|
||||
|
||||
def _parse_workout_exercises(raw_lines: str) -> list[dict]:
|
||||
exercises = []
|
||||
for line in raw_lines.splitlines():
|
||||
cleaned = line.strip()
|
||||
if not cleaned:
|
||||
continue
|
||||
parts = [part.strip() for part in cleaned.split('|')]
|
||||
if len(parts) < 2:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail='Workout lines must use exercise|reps@weight, reps@weight|optional notes format.',
|
||||
)
|
||||
exercise_name = parts[0]
|
||||
notes = parts[2] if len(parts) > 2 and parts[2] else None
|
||||
sets = []
|
||||
for set_chunk in parts[1].split(','):
|
||||
token = set_chunk.strip()
|
||||
if not token:
|
||||
continue
|
||||
if '@' not in token:
|
||||
raise HTTPException(status_code=400, detail=f'Invalid set token: {token}')
|
||||
reps_raw, weight_raw = [value.strip() for value in token.split('@', 1)]
|
||||
try:
|
||||
reps = int(reps_raw)
|
||||
weight_lbs = float(weight_raw)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=f'Invalid set token: {token}') from exc
|
||||
sets.append({'reps': reps, 'weight_lbs': weight_lbs})
|
||||
if not sets:
|
||||
raise HTTPException(status_code=400, detail='Each workout exercise needs at least one set.')
|
||||
exercises.append({'exercise_name': exercise_name, 'notes': notes, 'sets': sets})
|
||||
if not exercises:
|
||||
raise HTTPException(status_code=400, detail='At least one workout exercise is required.')
|
||||
return exercises
|
||||
Reference in New Issue
Block a user