Files
MileVault/backend/app/models/user.py
T
owain 264c27469b
Build and push images / build-backend (push) Successful in 6s
Build and push images / build-worker (push) Successful in 6s
Build and push images / build-frontend (push) Successful in 7s
Fix DB init - composite PK for hypertable + separate transactions
2026-06-06 15:01:39 +01:00

201 lines
8.4 KiB
Python

from sqlalchemy import (
Column, Integer, String, Float, DateTime, Boolean,
ForeignKey, Text, JSON, Index, UniqueConstraint
)
from sqlalchemy.orm import relationship
from datetime import datetime, timezone
from app.core.database import Base
def now_utc():
return datetime.now(timezone.utc)
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
username = Column(String(64), unique=True, nullable=False, index=True)
email = Column(String(256), unique=True, nullable=True)
hashed_password = Column(String(256), nullable=True)
is_admin = Column(Boolean, default=False)
pocketid_sub = Column(String(256), unique=True, nullable=True)
created_at = Column(DateTime(timezone=True), default=now_utc)
activities = relationship("Activity", back_populates="user", cascade="all, delete-orphan")
health_metrics = relationship("HealthMetric", back_populates="user", cascade="all, delete-orphan")
named_routes = relationship("NamedRoute", back_populates="user", cascade="all, delete-orphan")
class Activity(Base):
__tablename__ = "activities"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
name = Column(String(256), nullable=False)
sport_type = Column(String(64), nullable=False)
start_time = Column(DateTime(timezone=True), nullable=False, index=True)
end_time = Column(DateTime(timezone=True), nullable=True)
distance_m = Column(Float, nullable=True)
duration_s = Column(Float, nullable=True)
elevation_gain_m = Column(Float, nullable=True)
elevation_loss_m = Column(Float, nullable=True)
avg_heart_rate = Column(Float, nullable=True)
max_heart_rate = Column(Float, nullable=True)
avg_cadence = Column(Float, nullable=True)
avg_power = Column(Float, nullable=True)
normalized_power = Column(Float, nullable=True)
avg_speed_ms = Column(Float, nullable=True)
max_speed_ms = Column(Float, nullable=True)
avg_temperature_c = Column(Float, nullable=True)
calories = Column(Float, nullable=True)
training_stress_score = Column(Float, nullable=True)
vo2max_estimate = Column(Float, nullable=True)
named_route_id = Column(Integer, ForeignKey("named_routes.id"), nullable=True)
polyline = Column(Text, nullable=True)
bounding_box = Column(JSON, nullable=True)
source_file = Column(String(512), nullable=True)
source_type = Column(String(32), nullable=True)
garmin_activity_id = Column(String(64), nullable=True, unique=True)
strava_activity_id = Column(String(64), nullable=True, unique=True)
hr_zones = Column(JSON, nullable=True)
created_at = Column(DateTime(timezone=True), default=now_utc)
user = relationship("User", back_populates="activities")
data_points = relationship("ActivityDataPoint", back_populates="activity", cascade="all, delete-orphan")
named_route = relationship("NamedRoute", back_populates="activities")
laps = relationship("ActivityLap", back_populates="activity", cascade="all, delete-orphan")
class ActivityDataPoint(Base):
"""
TimescaleDB hypertable - one row per second of activity data.
Composite primary key (activity_id, timestamp) satisfies TimescaleDB's
requirement that the partition column be part of the primary key.
"""
__tablename__ = "activity_data_points"
activity_id = Column(Integer, ForeignKey("activities.id"), nullable=False, primary_key=True)
timestamp = Column(DateTime(timezone=True), nullable=False, primary_key=True)
latitude = Column(Float, nullable=True)
longitude = Column(Float, nullable=True)
altitude_m = Column(Float, nullable=True)
heart_rate = Column(Float, nullable=True)
cadence = Column(Float, nullable=True)
speed_ms = Column(Float, nullable=True)
power = Column(Float, nullable=True)
temperature_c = Column(Float, nullable=True)
distance_m = Column(Float, nullable=True)
activity = relationship("Activity", back_populates="data_points")
class ActivityLap(Base):
__tablename__ = "activity_laps"
id = Column(Integer, primary_key=True)
activity_id = Column(Integer, ForeignKey("activities.id"), nullable=False, index=True)
lap_number = Column(Integer, nullable=False)
start_time = Column(DateTime(timezone=True), nullable=True)
duration_s = Column(Float, nullable=True)
distance_m = Column(Float, nullable=True)
avg_heart_rate = Column(Float, nullable=True)
avg_cadence = Column(Float, nullable=True)
avg_speed_ms = Column(Float, nullable=True)
avg_power = Column(Float, nullable=True)
activity = relationship("Activity", back_populates="laps")
class NamedRoute(Base):
__tablename__ = "named_routes"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
name = Column(String(256), nullable=False)
description = Column(Text, nullable=True)
sport_type = Column(String(64), nullable=True)
reference_polyline = Column(Text, nullable=True)
bounding_box = Column(JSON, nullable=True)
distance_m = Column(Float, nullable=True)
created_at = Column(DateTime(timezone=True), default=now_utc)
user = relationship("User", back_populates="named_routes")
activities = relationship("Activity", back_populates="named_route")
segments = relationship("RouteSegment", back_populates="route", cascade="all, delete-orphan")
class RouteSegment(Base):
__tablename__ = "route_segments"
id = Column(Integer, primary_key=True)
route_id = Column(Integer, ForeignKey("named_routes.id"), nullable=False, index=True)
name = Column(String(256), nullable=False)
start_distance_m = Column(Float, nullable=False)
end_distance_m = Column(Float, nullable=False)
description = Column(Text, nullable=True)
route = relationship("NamedRoute", back_populates="segments")
class PersonalRecord(Base):
__tablename__ = "personal_records"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
activity_id = Column(Integer, ForeignKey("activities.id"), nullable=False)
sport_type = Column(String(64), nullable=False)
distance_m = Column(Float, nullable=False)
distance_label = Column(String(32), nullable=False)
duration_s = Column(Float, nullable=False)
achieved_at = Column(DateTime(timezone=True), nullable=False)
is_current_record = Column(Boolean, default=True)
__table_args__ = (
UniqueConstraint("user_id", "sport_type", "distance_m", "is_current_record",
name="uq_pr_current"),
)
class HealthMetric(Base):
__tablename__ = "health_metrics"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
date = Column(DateTime(timezone=True), nullable=False)
resting_hr = Column(Float, nullable=True)
max_hr_day = Column(Float, nullable=True)
avg_hr_day = Column(Float, nullable=True)
hrv_status = Column(String(32), nullable=True)
hrv_nightly_avg = Column(Float, nullable=True)
hrv_5min_high = Column(Float, nullable=True)
hrv_5min_low = Column(Float, nullable=True)
sleep_duration_s = Column(Float, nullable=True)
sleep_deep_s = Column(Float, nullable=True)
sleep_light_s = Column(Float, nullable=True)
sleep_rem_s = Column(Float, nullable=True)
sleep_awake_s = Column(Float, nullable=True)
sleep_score = Column(Float, nullable=True)
sleep_start = Column(DateTime(timezone=True), nullable=True)
sleep_end = Column(DateTime(timezone=True), nullable=True)
weight_kg = Column(Float, nullable=True)
bmi = Column(Float, nullable=True)
body_fat_pct = Column(Float, nullable=True)
muscle_mass_kg = Column(Float, nullable=True)
vo2max = Column(Float, nullable=True)
fitness_age = Column(Integer, nullable=True)
training_load = Column(Float, nullable=True)
recovery_time_h = Column(Float, nullable=True)
avg_stress = Column(Float, nullable=True)
steps = Column(Integer, nullable=True)
floors_climbed = Column(Integer, nullable=True)
active_calories = Column(Float, nullable=True)
total_calories = Column(Float, nullable=True)
spo2_avg = Column(Float, nullable=True)
__table_args__ = (
UniqueConstraint("user_id", "date", name="uq_health_user_date"),
Index("ix_health_user_date", "user_id", "date"),
)
user = relationship("User", back_populates="health_metrics")