backend on fastapi
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
ivan-igorevich@MacBook-Pro-16.local.32588
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||||
|
|
||||||
|
SQLALCHEMY_DATABASE_URL = "sqlite:///./gym.db"
|
||||||
|
|
||||||
|
engine = create_engine(
|
||||||
|
SQLALCHEMY_DATABASE_URL,
|
||||||
|
connect_args={"check_same_thread": False} # только для SQLite
|
||||||
|
)
|
||||||
|
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
# Dependency
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
Binary file not shown.
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from database import engine, Base
|
||||||
|
import routers.clients
|
||||||
|
import routers.workouts
|
||||||
|
import routers.exercises
|
||||||
|
import routers.metrics
|
||||||
|
import routers.sync
|
||||||
|
|
||||||
|
# Создаём таблицы
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="Gym Tracker API",
|
||||||
|
description="API для трекера тренировок с офлайн-синхронизацией",
|
||||||
|
version="1.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
# CORS для локальной разработки
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"], # В продакшене заменить на конкретные домены
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Подключаем роутеры
|
||||||
|
app.include_router(routers.clients.router)
|
||||||
|
app.include_router(routers.workouts.router)
|
||||||
|
app.include_router(routers.exercises.router)
|
||||||
|
app.include_router(routers.metrics.router)
|
||||||
|
app.include_router(routers.sync.router)
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def root():
|
||||||
|
return {
|
||||||
|
"message": "Gym Tracker API",
|
||||||
|
"docs": "/docs",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health_check():
|
||||||
|
return {"status": "ok"}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, Float, ForeignKey, CheckConstraint, Index
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from database import Base
|
||||||
|
|
||||||
|
class Client(Base):
|
||||||
|
__tablename__ = "clients"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
sync_id = Column(String, unique=True, nullable=False, index=True)
|
||||||
|
full_name = Column(String, nullable=False)
|
||||||
|
birth_date = Column(String)
|
||||||
|
goal = Column(String, default="")
|
||||||
|
notes = Column(String, default="")
|
||||||
|
is_active = Column(Integer, default=1)
|
||||||
|
created_at = Column(String)
|
||||||
|
updated_at = Column(String)
|
||||||
|
|
||||||
|
workouts = relationship("Workout", back_populates="client")
|
||||||
|
body_metrics = relationship("BodyMetric", back_populates="client")
|
||||||
|
|
||||||
|
class WorkoutType(Base):
|
||||||
|
__tablename__ = "workout_types"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
sync_id = Column(String, unique=True, nullable=False, index=True)
|
||||||
|
name = Column(String, unique=True, nullable=False)
|
||||||
|
sort_order = Column(Integer, default=0)
|
||||||
|
created_at = Column(String)
|
||||||
|
|
||||||
|
exercises = relationship("Exercise", back_populates="workout_type")
|
||||||
|
workouts = relationship("Workout", back_populates="workout_type")
|
||||||
|
|
||||||
|
class Exercise(Base):
|
||||||
|
__tablename__ = "exercises"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
sync_id = Column(String, unique=True, nullable=False, index=True)
|
||||||
|
workout_type_id = Column(Integer, ForeignKey("workout_types.id", ondelete="SET NULL"))
|
||||||
|
name = Column(String, nullable=False, index=True)
|
||||||
|
created_at = Column(String)
|
||||||
|
updated_at = Column(String)
|
||||||
|
|
||||||
|
workout_type = relationship("WorkoutType", back_populates="exercises")
|
||||||
|
workout_exercises = relationship("WorkoutExercise", back_populates="exercise")
|
||||||
|
|
||||||
|
class Workout(Base):
|
||||||
|
__tablename__ = "workouts"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
sync_id = Column(String, unique=True, nullable=False, index=True)
|
||||||
|
client_id = Column(Integer, ForeignKey("clients.id", ondelete="SET NULL"))
|
||||||
|
workout_type_id = Column(Integer, ForeignKey("workout_types.id", ondelete="SET NULL"))
|
||||||
|
start_time = Column(String, nullable=False)
|
||||||
|
end_time = Column(String)
|
||||||
|
notes = Column(String, default="")
|
||||||
|
synced = Column(Integer, default=0)
|
||||||
|
created_at = Column(String)
|
||||||
|
updated_at = Column(String)
|
||||||
|
|
||||||
|
client = relationship("Client", back_populates="workouts")
|
||||||
|
workout_type = relationship("WorkoutType", back_populates="workouts")
|
||||||
|
workout_exercises = relationship("WorkoutExercise", back_populates="workout", cascade="all, delete-orphan")
|
||||||
|
cardio_sessions = relationship("Cardio", back_populates="workout", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
class WorkoutExercise(Base):
|
||||||
|
__tablename__ = "workout_exercises"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
sync_id = Column(String, unique=True, nullable=False, index=True)
|
||||||
|
workout_id = Column(Integer, ForeignKey("workouts.id", ondelete="CASCADE"))
|
||||||
|
exercise_id = Column(Integer, ForeignKey("exercises.id", ondelete="SET NULL"))
|
||||||
|
exercise_name = Column(String, nullable=False)
|
||||||
|
sort_order = Column(Integer, default=0)
|
||||||
|
created_at = Column(String)
|
||||||
|
|
||||||
|
workout = relationship("Workout", back_populates="workout_exercises")
|
||||||
|
exercise = relationship("Exercise", back_populates="workout_exercises")
|
||||||
|
sets = relationship("Set", back_populates="workout_exercise", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
class Set(Base):
|
||||||
|
__tablename__ = "sets"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
sync_id = Column(String, unique=True, nullable=False, index=True)
|
||||||
|
workout_exercise_id = Column(Integer, ForeignKey("workout_exercises.id", ondelete="CASCADE"))
|
||||||
|
is_dropset = Column(Integer, default=0)
|
||||||
|
weight = Column(Float)
|
||||||
|
reps = Column(Integer)
|
||||||
|
rating = Column(Integer)
|
||||||
|
notes = Column(String, default="")
|
||||||
|
sort_order = Column(Integer, default=0)
|
||||||
|
timestamp = Column(String, nullable=False)
|
||||||
|
created_at = Column(String)
|
||||||
|
|
||||||
|
workout_exercise = relationship("WorkoutExercise", back_populates="sets")
|
||||||
|
dropset_items = relationship("DropsetItem", back_populates="set", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint(
|
||||||
|
"(is_dropset = 0 AND weight IS NOT NULL AND reps IS NOT NULL) OR "
|
||||||
|
"(is_dropset = 1 AND weight IS NULL AND reps IS NULL)",
|
||||||
|
name="check_set_data"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class DropsetItem(Base):
|
||||||
|
__tablename__ = "dropset_items"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
set_id = Column(Integer, ForeignKey("sets.id", ondelete="CASCADE"))
|
||||||
|
weight = Column(Float, nullable=False)
|
||||||
|
reps = Column(Integer, nullable=False)
|
||||||
|
sort_order = Column(Integer, default=0)
|
||||||
|
|
||||||
|
set = relationship("Set", back_populates="dropset_items")
|
||||||
|
|
||||||
|
class Cardio(Base):
|
||||||
|
__tablename__ = "cardio"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
sync_id = Column(String, unique=True, nullable=False, index=True)
|
||||||
|
workout_id = Column(Integer, ForeignKey("workouts.id", ondelete="CASCADE"))
|
||||||
|
cardio_type = Column(String, nullable=False)
|
||||||
|
purpose = Column(String, nullable=False)
|
||||||
|
duration = Column(Integer)
|
||||||
|
distance = Column(Float)
|
||||||
|
speed = Column(Float)
|
||||||
|
incline = Column(Float)
|
||||||
|
resistance = Column(Integer)
|
||||||
|
notes = Column(String, default="")
|
||||||
|
sort_order = Column(Integer, default=0)
|
||||||
|
created_at = Column(String)
|
||||||
|
|
||||||
|
workout = relationship("Workout", back_populates="cardio_sessions")
|
||||||
|
|
||||||
|
class MetricType(Base):
|
||||||
|
__tablename__ = "metric_types"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
sync_id = Column(String, unique=True, nullable=False, index=True)
|
||||||
|
name = Column(String, unique=True, nullable=False)
|
||||||
|
unit = Column(String, default="см")
|
||||||
|
category = Column(String, default="body")
|
||||||
|
sort_order = Column(Integer, default=0)
|
||||||
|
created_at = Column(String)
|
||||||
|
|
||||||
|
metric_values = relationship("BodyMetricValue", back_populates="metric_type")
|
||||||
|
|
||||||
|
class BodyMetric(Base):
|
||||||
|
__tablename__ = "body_metrics"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
sync_id = Column(String, unique=True, nullable=False, index=True)
|
||||||
|
client_id = Column(Integer, ForeignKey("clients.id", ondelete="SET NULL"))
|
||||||
|
metric_date = Column(String, nullable=False)
|
||||||
|
notes = Column(String, default="")
|
||||||
|
created_at = Column(String)
|
||||||
|
|
||||||
|
client = relationship("Client", back_populates="body_metrics")
|
||||||
|
values = relationship("BodyMetricValue", back_populates="body_metric", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
class BodyMetricValue(Base):
|
||||||
|
__tablename__ = "body_metric_values"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
body_metric_id = Column(Integer, ForeignKey("body_metrics.id", ondelete="CASCADE"))
|
||||||
|
metric_type_id = Column(Integer, ForeignKey("metric_types.id", ondelete="CASCADE"))
|
||||||
|
value = Column(Float, nullable=False)
|
||||||
|
|
||||||
|
body_metric = relationship("BodyMetric", back_populates="values")
|
||||||
|
metric_type = relationship("MetricType", back_populates="metric_values")
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fastapi==0.104.1
|
||||||
|
uvicorn[standard]==0.24.0
|
||||||
|
sqlalchemy==2.0.23
|
||||||
|
pydantic==2.5.0
|
||||||
|
python-multipart==0.0.6
|
||||||
@@ -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"}
|
||||||
@@ -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
|
||||||
|
]
|
||||||
@@ -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
|
||||||
|
]
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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"}
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# CLIENTS
|
||||||
|
# ============================================================
|
||||||
|
class ClientBase(BaseModel):
|
||||||
|
full_name: str
|
||||||
|
birth_date: Optional[str] = None
|
||||||
|
goal: Optional[str] = ""
|
||||||
|
notes: Optional[str] = ""
|
||||||
|
|
||||||
|
class ClientCreate(ClientBase):
|
||||||
|
sync_id: str
|
||||||
|
|
||||||
|
class ClientUpdate(BaseModel):
|
||||||
|
full_name: Optional[str] = None
|
||||||
|
birth_date: Optional[str] = None
|
||||||
|
goal: Optional[str] = None
|
||||||
|
notes: Optional[str] = None
|
||||||
|
is_active: Optional[int] = None
|
||||||
|
|
||||||
|
class Client(ClientBase):
|
||||||
|
id: int
|
||||||
|
sync_id: str
|
||||||
|
is_active: int
|
||||||
|
created_at: str
|
||||||
|
updated_at: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# WORKOUT TYPES
|
||||||
|
# ============================================================
|
||||||
|
class WorkoutTypeBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
sort_order: Optional[int] = 0
|
||||||
|
|
||||||
|
class WorkoutTypeCreate(WorkoutTypeBase):
|
||||||
|
sync_id: str
|
||||||
|
|
||||||
|
class WorkoutType(WorkoutTypeBase):
|
||||||
|
id: int
|
||||||
|
sync_id: str
|
||||||
|
created_at: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# EXERCISES
|
||||||
|
# ============================================================
|
||||||
|
class ExerciseBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
workout_type_id: Optional[int] = None
|
||||||
|
|
||||||
|
class ExerciseCreate(ExerciseBase):
|
||||||
|
sync_id: str
|
||||||
|
|
||||||
|
class ExerciseUpdate(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
workout_type_id: Optional[int] = None
|
||||||
|
|
||||||
|
class Exercise(ExerciseBase):
|
||||||
|
id: int
|
||||||
|
sync_id: str
|
||||||
|
created_at: str
|
||||||
|
updated_at: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# DROPSET ITEMS
|
||||||
|
# ============================================================
|
||||||
|
class DropsetItemBase(BaseModel):
|
||||||
|
weight: float = Field(gt=0)
|
||||||
|
reps: int = Field(gt=0)
|
||||||
|
sort_order: int = 0
|
||||||
|
|
||||||
|
class DropsetItemCreate(DropsetItemBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class DropsetItem(DropsetItemBase):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# SETS
|
||||||
|
# ============================================================
|
||||||
|
class SetBase(BaseModel):
|
||||||
|
is_dropset: int = 0
|
||||||
|
weight: Optional[float] = None
|
||||||
|
reps: Optional[int] = None
|
||||||
|
rating: Optional[int] = Field(None, ge=1, le=5)
|
||||||
|
notes: Optional[str] = ""
|
||||||
|
sort_order: int = 0
|
||||||
|
timestamp: str
|
||||||
|
|
||||||
|
class SetCreate(SetBase):
|
||||||
|
sync_id: str
|
||||||
|
dropset_items: Optional[List[DropsetItemCreate]] = []
|
||||||
|
|
||||||
|
class Set(SetBase):
|
||||||
|
id: int
|
||||||
|
sync_id: str
|
||||||
|
created_at: str
|
||||||
|
dropset_items: List[DropsetItem] = []
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# WORKOUT EXERCISES
|
||||||
|
# ============================================================
|
||||||
|
class WorkoutExerciseBase(BaseModel):
|
||||||
|
exercise_id: Optional[int] = None
|
||||||
|
exercise_name: str
|
||||||
|
sort_order: int = 0
|
||||||
|
|
||||||
|
class WorkoutExerciseCreate(WorkoutExerciseBase):
|
||||||
|
sync_id: str
|
||||||
|
sets: Optional[List[SetCreate]] = []
|
||||||
|
|
||||||
|
class WorkoutExercise(WorkoutExerciseBase):
|
||||||
|
id: int
|
||||||
|
sync_id: str
|
||||||
|
created_at: str
|
||||||
|
sets: List[Set] = []
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# CARDIO
|
||||||
|
# ============================================================
|
||||||
|
class CardioBase(BaseModel):
|
||||||
|
cardio_type: str
|
||||||
|
purpose: str
|
||||||
|
duration: Optional[int] = None
|
||||||
|
distance: Optional[float] = None
|
||||||
|
speed: Optional[float] = None
|
||||||
|
incline: Optional[float] = None
|
||||||
|
resistance: Optional[int] = None
|
||||||
|
notes: Optional[str] = ""
|
||||||
|
sort_order: int = 0
|
||||||
|
|
||||||
|
class CardioCreate(CardioBase):
|
||||||
|
sync_id: str
|
||||||
|
|
||||||
|
class Cardio(CardioBase):
|
||||||
|
id: int
|
||||||
|
sync_id: str
|
||||||
|
created_at: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# WORKOUTS
|
||||||
|
# ============================================================
|
||||||
|
class WorkoutBase(BaseModel):
|
||||||
|
client_id: Optional[int] = None
|
||||||
|
workout_type_id: Optional[int] = None
|
||||||
|
start_time: str
|
||||||
|
end_time: Optional[str] = None
|
||||||
|
notes: Optional[str] = ""
|
||||||
|
|
||||||
|
class WorkoutCreate(WorkoutBase):
|
||||||
|
sync_id: str
|
||||||
|
workout_exercises: Optional[List[WorkoutExerciseCreate]] = []
|
||||||
|
cardio_sessions: Optional[List[CardioCreate]] = []
|
||||||
|
|
||||||
|
class WorkoutUpdate(BaseModel):
|
||||||
|
workout_type_id: Optional[int] = None
|
||||||
|
end_time: Optional[str] = None
|
||||||
|
notes: Optional[str] = None
|
||||||
|
synced: Optional[int] = None
|
||||||
|
|
||||||
|
class Workout(WorkoutBase):
|
||||||
|
id: int
|
||||||
|
sync_id: str
|
||||||
|
synced: int
|
||||||
|
created_at: str
|
||||||
|
updated_at: str
|
||||||
|
workout_exercises: List[WorkoutExercise] = []
|
||||||
|
cardio_sessions: List[Cardio] = []
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# METRIC TYPES
|
||||||
|
# ============================================================
|
||||||
|
class MetricTypeBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
unit: Optional[str] = "см"
|
||||||
|
category: Optional[str] = "body"
|
||||||
|
sort_order: Optional[int] = 0
|
||||||
|
|
||||||
|
class MetricTypeCreate(MetricTypeBase):
|
||||||
|
sync_id: str
|
||||||
|
|
||||||
|
class MetricType(MetricTypeBase):
|
||||||
|
id: int
|
||||||
|
sync_id: str
|
||||||
|
created_at: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# BODY METRIC VALUES
|
||||||
|
# ============================================================
|
||||||
|
class BodyMetricValueBase(BaseModel):
|
||||||
|
metric_type_id: int
|
||||||
|
value: float
|
||||||
|
|
||||||
|
class BodyMetricValueCreate(BodyMetricValueBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class BodyMetricValue(BodyMetricValueBase):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# BODY METRICS
|
||||||
|
# ============================================================
|
||||||
|
class BodyMetricBase(BaseModel):
|
||||||
|
client_id: Optional[int] = None
|
||||||
|
metric_date: str
|
||||||
|
notes: Optional[str] = ""
|
||||||
|
|
||||||
|
class BodyMetricCreate(BodyMetricBase):
|
||||||
|
sync_id: str
|
||||||
|
values: Optional[List[BodyMetricValueCreate]] = []
|
||||||
|
|
||||||
|
class BodyMetric(BodyMetricBase):
|
||||||
|
id: int
|
||||||
|
sync_id: str
|
||||||
|
created_at: str
|
||||||
|
values: List[BodyMetricValue] = []
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# SYNC
|
||||||
|
# ============================================================
|
||||||
|
class SyncPayload(BaseModel):
|
||||||
|
clients: Optional[List[ClientCreate]] = []
|
||||||
|
workout_types: Optional[List[WorkoutTypeCreate]] = []
|
||||||
|
exercises: Optional[List[ExerciseCreate]] = []
|
||||||
|
workouts: Optional[List[WorkoutCreate]] = []
|
||||||
|
metric_types: Optional[List[MetricTypeCreate]] = []
|
||||||
|
body_metrics: Optional[List[BodyMetricCreate]] = []
|
||||||
|
|
||||||
|
class SyncResponse(BaseModel):
|
||||||
|
status: str
|
||||||
|
created: dict
|
||||||
|
updated: dict
|
||||||
@@ -19,3 +19,4 @@ clients (ФИО, ДР, цель)
|
|||||||
│ 1:N
|
│ 1:N
|
||||||
└──► body_metric_values ──► metric_types (справочник)
|
└──► body_metric_values ──► metric_types (справочник)
|
||||||
```
|
```
|
||||||
|
|
||||||
Reference in New Issue
Block a user