backend on fastapi

This commit is contained in:
2026-09-30 00:21:17 +03:00
parent fe9221a4dd
commit 06715f952b
14 changed files with 1113 additions and 1 deletions
+65
View File
@@ -0,0 +1,65 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List
import database
import models
import schemas
from datetime import datetime
router = APIRouter(prefix="/clients", tags=["clients"])
def get_current_timestamp():
return datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%fZ')
@router.post("/", response_model=schemas.Client)
def create_client(client: schemas.ClientCreate, db: Session = Depends(database.get_db)):
# Проверяем уникальность sync_id
db_client = db.query(models.Client).filter(models.Client.sync_id == client.sync_id).first()
if db_client:
raise HTTPException(status_code=400, detail="Client with this sync_id already exists")
timestamp = get_current_timestamp()
db_client = models.Client(
**client.dict(),
created_at=timestamp,
updated_at=timestamp
)
db.add(db_client)
db.commit()
db.refresh(db_client)
return db_client
@router.get("/", response_model=List[schemas.Client])
def list_clients(skip: int = 0, limit: int = 100, db: Session = Depends(database.get_db)):
return db.query(models.Client).offset(skip).limit(limit).all()
@router.get("/{client_id}", response_model=schemas.Client)
def get_client(client_id: int, db: Session = Depends(database.get_db)):
db_client = db.query(models.Client).filter(models.Client.id == client_id).first()
if not db_client:
raise HTTPException(status_code=404, detail="Client not found")
return db_client
@router.put("/{client_id}", response_model=schemas.Client)
def update_client(client_id: int, client: schemas.ClientUpdate, db: Session = Depends(database.get_db)):
db_client = db.query(models.Client).filter(models.Client.id == client_id).first()
if not db_client:
raise HTTPException(status_code=404, detail="Client not found")
update_data = client.dict(exclude_unset=True)
for key, value in update_data.items():
setattr(db_client, key, value)
db_client.updated_at = get_current_timestamp()
db.commit()
db.refresh(db_client)
return db_client
@router.delete("/{client_id}")
def delete_client(client_id: int, db: Session = Depends(database.get_db)):
db_client = db.query(models.Client).filter(models.Client.id == client_id).first()
if not db_client:
raise HTTPException(status_code=404, detail="Client not found")
db.delete(db_client)
db.commit()
return {"status": "deleted"}
+99
View File
@@ -0,0 +1,99 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List
import database
import models
import schemas
from datetime import datetime
router = APIRouter(prefix="/exercises", tags=["exercises"])
def get_current_timestamp():
return datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%fZ')
@router.post("/", response_model=schemas.Exercise)
def create_exercise(exercise: schemas.ExerciseCreate, db: Session = Depends(database.get_db)):
db_exercise = db.query(models.Exercise).filter(models.Exercise.sync_id == exercise.sync_id).first()
if db_exercise:
raise HTTPException(status_code=400, detail="Exercise with this sync_id already exists")
timestamp = get_current_timestamp()
db_exercise = models.Exercise(
**exercise.dict(),
created_at=timestamp,
updated_at=timestamp
)
db.add(db_exercise)
db.commit()
db.refresh(db_exercise)
return db_exercise
@router.get("/", response_model=List[schemas.Exercise])
def list_exercises(workout_type_id: int = None, db: Session = Depends(database.get_db)):
query = db.query(models.Exercise)
if workout_type_id:
query = query.filter(models.Exercise.workout_type_id == workout_type_id)
return query.order_by(models.Exercise.name).all()
@router.get("/{exercise_id}", response_model=schemas.Exercise)
def get_exercise(exercise_id: int, db: Session = Depends(database.get_db)):
db_exercise = db.query(models.Exercise).filter(models.Exercise.id == exercise_id).first()
if not db_exercise:
raise HTTPException(status_code=404, detail="Exercise not found")
return db_exercise
@router.put("/{exercise_id}", response_model=schemas.Exercise)
def update_exercise(exercise_id: int, exercise: schemas.ExerciseUpdate, db: Session = Depends(database.get_db)):
db_exercise = db.query(models.Exercise).filter(models.Exercise.id == exercise_id).first()
if not db_exercise:
raise HTTPException(status_code=404, detail="Exercise not found")
update_data = exercise.dict(exclude_unset=True)
for key, value in update_data.items():
setattr(db_exercise, key, value)
db_exercise.updated_at = get_current_timestamp()
db.commit()
db.refresh(db_exercise)
return db_exercise
@router.delete("/{exercise_id}")
def delete_exercise(exercise_id: int, db: Session = Depends(database.get_db)):
db_exercise = db.query(models.Exercise).filter(models.Exercise.id == exercise_id).first()
if not db_exercise:
raise HTTPException(status_code=404, detail="Exercise not found")
db.delete(db_exercise)
db.commit()
return {"status": "deleted"}
@router.get("/history/{exercise_name}")
def get_exercise_history(exercise_name: str, client_id: int = None, limit: int = 10, db: Session = Depends(database.get_db)):
"""Получить историю упражнения для подсказок на фронте"""
query = db.query(
models.Workout.start_time,
models.Set.weight,
models.Set.reps,
models.Set.is_dropset
).join(
models.WorkoutExercise, models.WorkoutExercise.workout_id == models.Workout.id
).join(
models.Set, models.Set.workout_exercise_id == models.WorkoutExercise.id
).filter(
models.WorkoutExercise.exercise_name == exercise_name,
models.Workout.end_time.isnot(None)
)
if client_id:
query = query.filter(models.Workout.client_id == client_id)
results = query.order_by(models.Workout.start_time.desc()).limit(limit).all()
return [
{
"date": r.start_time,
"weight": r.weight,
"reps": r.reps,
"is_dropset": bool(r.is_dropset)
}
for r in results
]
+99
View File
@@ -0,0 +1,99 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List
import database
import models
import schemas
from datetime import datetime
router = APIRouter(tags=["metrics"])
def get_current_timestamp():
return datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%fZ')
# ============================================================
# METRIC TYPES
# ============================================================
@router.post("/metric-types/", response_model=schemas.MetricType)
def create_metric_type(metric_type: schemas.MetricTypeCreate, db: Session = Depends(database.get_db)):
db_mt = db.query(models.MetricType).filter(models.MetricType.sync_id == metric_type.sync_id).first()
if db_mt:
raise HTTPException(status_code=400, detail="MetricType with this sync_id already exists")
db_mt = models.MetricType(
**metric_type.dict(),
created_at=get_current_timestamp()
)
db.add(db_mt)
db.commit()
db.refresh(db_mt)
return db_mt
@router.get("/metric-types/", response_model=List[schemas.MetricType])
def list_metric_types(db: Session = Depends(database.get_db)):
return db.query(models.MetricType).order_by(models.MetricType.sort_order).all()
# ============================================================
# BODY METRICS
# ============================================================
@router.post("/body-metrics/", response_model=schemas.BodyMetric)
def create_body_metric(metric: schemas.BodyMetricCreate, db: Session = Depends(database.get_db)):
db_metric = db.query(models.BodyMetric).filter(models.BodyMetric.sync_id == metric.sync_id).first()
if db_metric:
raise HTTPException(status_code=400, detail="BodyMetric with this sync_id already exists")
db_metric = models.BodyMetric(
sync_id=metric.sync_id,
client_id=metric.client_id,
metric_date=metric.metric_date,
notes=metric.notes,
created_at=get_current_timestamp()
)
db.add(db_metric)
db.flush()
# Добавляем значения
for value_data in metric.values or []:
db_value = models.BodyMetricValue(
body_metric_id=db_metric.id,
metric_type_id=value_data.metric_type_id,
value=value_data.value
)
db.add(db_value)
db.commit()
db.refresh(db_metric)
return db_metric
@router.get("/body-metrics/", response_model=List[schemas.BodyMetric])
def list_body_metrics(client_id: int = None, db: Session = Depends(database.get_db)):
query = db.query(models.BodyMetric)
if client_id:
query = query.filter(models.BodyMetric.client_id == client_id)
return query.order_by(models.BodyMetric.metric_date.desc()).all()
@router.get("/body-metrics/history")
def get_metric_history(
client_id: int,
metric_type_id: int,
limit: int = 50,
db: Session = Depends(database.get_db)
):
"""Получить историю конкретного замера для графиков"""
results = db.query(
models.BodyMetric.metric_date,
models.BodyMetricValue.value
).join(
models.BodyMetricValue, models.BodyMetricValue.body_metric_id == models.BodyMetric.id
).filter(
models.BodyMetric.client_id == client_id,
models.BodyMetricValue.metric_type_id == metric_type_id
).order_by(models.BodyMetric.metric_date.desc()).limit(limit).all()
return [
{
"date": r.metric_date,
"value": r.value
}
for r in results
]
+186
View File
@@ -0,0 +1,186 @@
#!/usr/bin/env python3
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
import database
import models
import schemas
from datetime import datetime
router = APIRouter(prefix="/sync", tags=["sync"])
def get_current_timestamp():
return datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%fZ')
@router.post("/", response_model=schemas.SyncResponse)
def sync_data(payload: schemas.SyncPayload, db: Session = Depends(database.get_db)):
"""
Endpoint для синхронизации офлайн-очереди с клиента.
Использует sync_id для идемпотентности — если запись с таким sync_id уже есть, пропускаем.
"""
timestamp = get_current_timestamp()
created = {
"clients": 0,
"workout_types": 0,
"exercises": 0,
"workouts": 0,
"metric_types": 0,
"body_metrics": 0
}
updated = {k: 0 for k in created}
# Синхронизация клиентов
for client_data in payload.clients or []:
existing = db.query(models.Client).filter(models.Client.sync_id == client_data.sync_id).first()
if not existing:
db_client = models.Client(
**client_data.dict(),
created_at=timestamp,
updated_at=timestamp
)
db.add(db_client)
created["clients"] += 1
# Синхронизация типов тренировок
for wt_data in payload.workout_types or []:
existing = db.query(models.WorkoutType).filter(models.WorkoutType.sync_id == wt_data.sync_id).first()
if not existing:
db_wt = models.WorkoutType(
**wt_data.dict(),
created_at=timestamp
)
db.add(db_wt)
created["workout_types"] += 1
# Синхронизация упражнений
for ex_data in payload.exercises or []:
existing = db.query(models.Exercise).filter(models.Exercise.sync_id == ex_data.sync_id).first()
if not existing:
db_ex = models.Exercise(
**ex_data.dict(),
created_at=timestamp,
updated_at=timestamp
)
db.add(db_ex)
created["exercises"] += 1
# Синхронизация типов замеров
for mt_data in payload.metric_types or []:
existing = db.query(models.MetricType).filter(models.MetricType.sync_id == mt_data.sync_id).first()
if not existing:
db_mt = models.MetricType(
**mt_data.dict(),
created_at=timestamp
)
db.add(db_mt)
created["metric_types"] += 1
# Синхронизация тренировок (с упражнениями и подходами)
for workout_data in payload.workouts or []:
existing = db.query(models.Workout).filter(models.Workout.sync_id == workout_data.sync_id).first()
if not existing:
db_workout = models.Workout(
sync_id=workout_data.sync_id,
client_id=workout_data.client_id,
workout_type_id=workout_data.workout_type_id,
start_time=workout_data.start_time,
end_time=workout_data.end_time,
notes=workout_data.notes,
created_at=timestamp,
updated_at=timestamp
)
db.add(db_workout)
db.flush()
# Упражнения
for we_data in workout_data.workout_exercises or []:
db_we = models.WorkoutExercise(
sync_id=we_data.sync_id,
workout_id=db_workout.id,
exercise_id=we_data.exercise_id,
exercise_name=we_data.exercise_name,
sort_order=we_data.sort_order,
created_at=timestamp
)
db.add(db_we)
db.flush()
# Подходы
for set_data in we_data.sets or []:
db_set = models.Set(
sync_id=set_data.sync_id,
workout_exercise_id=db_we.id,
is_dropset=set_data.is_dropset,
weight=set_data.weight,
reps=set_data.reps,
rating=set_data.rating,
notes=set_data.notes,
sort_order=set_data.sort_order,
timestamp=set_data.timestamp,
created_at=timestamp
)
db.add(db_set)
db.flush()
# Дроп-сет элементы
if set_data.is_dropset:
for item_data in set_data.dropset_items or []:
db_item = models.DropsetItem(
set_id=db_set.id,
weight=item_data.weight,
reps=item_data.reps,
sort_order=item_data.sort_order
)
db.add(db_item)
# Кардио
for cardio_data in workout_data.cardio_sessions or []:
db_cardio = models.Cardio(
sync_id=cardio_data.sync_id,
workout_id=db_workout.id,
cardio_type=cardio_data.cardio_type,
purpose=cardio_data.purpose,
duration=cardio_data.duration,
distance=cardio_data.distance,
speed=cardio_data.speed,
incline=cardio_data.incline,
resistance=cardio_data.resistance,
notes=cardio_data.notes,
sort_order=cardio_data.sort_order,
created_at=timestamp
)
db.add(db_cardio)
created["workouts"] += 1
# Синхронизация замеров тела
for metric_data in payload.body_metrics or []:
existing = db.query(models.BodyMetric).filter(models.BodyMetric.sync_id == metric_data.sync_id).first()
if not existing:
db_metric = models.BodyMetric(
sync_id=metric_data.sync_id,
client_id=metric_data.client_id,
metric_date=metric_data.metric_date,
notes=metric_data.notes,
created_at=timestamp
)
db.add(db_metric)
db.flush()
for value_data in metric_data.values or []:
db_value = models.BodyMetricValue(
body_metric_id=db_metric.id,
metric_type_id=value_data.metric_type_id,
value=value_data.value
)
db.add(db_value)
created["body_metrics"] += 1
db.commit()
return schemas.SyncResponse(
status="success",
created=created,
updated=updated
)
+152
View File
@@ -0,0 +1,152 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session, joinedload
from typing import List
import database
import models
import schemas
from datetime import datetime
router = APIRouter(prefix="/workouts", tags=["workouts"])
def get_current_timestamp():
return datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%fZ')
@router.post("/", response_model=schemas.Workout)
def create_workout(workout: schemas.WorkoutCreate, db: Session = Depends(database.get_db)):
# Проверяем уникальность sync_id
db_workout = db.query(models.Workout).filter(models.Workout.sync_id == workout.sync_id).first()
if db_workout:
raise HTTPException(status_code=400, detail="Workout with this sync_id already exists")
timestamp = get_current_timestamp()
db_workout = models.Workout(
sync_id=workout.sync_id,
client_id=workout.client_id,
workout_type_id=workout.workout_type_id,
start_time=workout.start_time,
end_time=workout.end_time,
notes=workout.notes,
created_at=timestamp,
updated_at=timestamp
)
db.add(db_workout)
db.flush() # Получаем ID
# Добавляем упражнения
for we_data in workout.workout_exercises or []:
db_we = models.WorkoutExercise(
sync_id=we_data.sync_id,
workout_id=db_workout.id,
exercise_id=we_data.exercise_id,
exercise_name=we_data.exercise_name,
sort_order=we_data.sort_order,
created_at=timestamp
)
db.add(db_we)
db.flush()
# Добавляем подходы
for set_data in we_data.sets or []:
db_set = models.Set(
sync_id=set_data.sync_id,
workout_exercise_id=db_we.id,
is_dropset=set_data.is_dropset,
weight=set_data.weight,
reps=set_data.reps,
rating=set_data.rating,
notes=set_data.notes,
sort_order=set_data.sort_order,
timestamp=set_data.timestamp,
created_at=timestamp
)
db.add(db_set)
db.flush()
# Добавляем элементы дроп-сета
if set_data.is_dropset:
for item_data in set_data.dropset_items or []:
db_item = models.DropsetItem(
set_id=db_set.id,
weight=item_data.weight,
reps=item_data.reps,
sort_order=item_data.sort_order
)
db.add(db_item)
# Добавляем кардио
for cardio_data in workout.cardio_sessions or []:
db_cardio = models.Cardio(
sync_id=cardio_data.sync_id,
workout_id=db_workout.id,
cardio_type=cardio_data.cardio_type,
purpose=cardio_data.purpose,
duration=cardio_data.duration,
distance=cardio_data.distance,
speed=cardio_data.speed,
incline=cardio_data.incline,
resistance=cardio_data.resistance,
notes=cardio_data.notes,
sort_order=cardio_data.sort_order,
created_at=timestamp
)
db.add(db_cardio)
db.commit()
db.refresh(db_workout)
return db_workout
@router.get("/", response_model=List[schemas.Workout])
def list_workouts(
client_id: int = None,
skip: int = 0,
limit: int = 100,
db: Session = Depends(database.get_db)
):
query = db.query(models.Workout).options(
joinedload(models.Workout.workout_exercises)
.joinedload(models.WorkoutExercise.sets)
.joinedload(models.Set.dropset_items),
joinedload(models.Workout.cardio_sessions)
)
if client_id:
query = query.filter(models.Workout.client_id == client_id)
return query.order_by(models.Workout.start_time.desc()).offset(skip).limit(limit).all()
@router.get("/{workout_id}", response_model=schemas.Workout)
def get_workout(workout_id: int, db: Session = Depends(database.get_db)):
db_workout = db.query(models.Workout).options(
joinedload(models.Workout.workout_exercises)
.joinedload(models.WorkoutExercise.sets)
.joinedload(models.Set.dropset_items),
joinedload(models.Workout.cardio_sessions)
).filter(models.Workout.id == workout_id).first()
if not db_workout:
raise HTTPException(status_code=404, detail="Workout not found")
return db_workout
@router.put("/{workout_id}", response_model=schemas.Workout)
def update_workout(workout_id: int, workout: schemas.WorkoutUpdate, db: Session = Depends(database.get_db)):
db_workout = db.query(models.Workout).filter(models.Workout.id == workout_id).first()
if not db_workout:
raise HTTPException(status_code=404, detail="Workout not found")
update_data = workout.dict(exclude_unset=True)
for key, value in update_data.items():
setattr(db_workout, key, value)
db_workout.updated_at = get_current_timestamp()
db.commit()
db.refresh(db_workout)
return db_workout
@router.delete("/{workout_id}")
def delete_workout(workout_id: int, db: Session = Depends(database.get_db)):
db_workout = db.query(models.Workout).filter(models.Workout.id == workout_id).first()
if not db_workout:
raise HTTPException(status_code=404, detail="Workout not found")
db.delete(db_workout)
db.commit()
return {"status": "deleted"}