Add initial project
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
"""SQLAlchemy models module."""
|
||||
from backend.models.user import User
|
||||
from backend.models.challenge import Challenge
|
||||
from backend.models.tracking import Tracking
|
||||
from backend.models.daily_health_metrics import DailyHealthMetrics
|
||||
from backend.models.workout_session import WorkoutSession
|
||||
from backend.models.daily_context import DailyContext
|
||||
from backend.models.health_connect import (
|
||||
HealthConnectSyncLog,
|
||||
HealthConnectDailyMetrics,
|
||||
HealthConnectExerciseSession,
|
||||
)
|
||||
from backend.models.health_connect_raw import (
|
||||
HCRawSteps,
|
||||
HCRawHeartRate,
|
||||
HCRawHeartRateSample,
|
||||
HCRawRestingHeartRate,
|
||||
HCRawSleepSession,
|
||||
HCRawSleepStage,
|
||||
HCRawDistance,
|
||||
HCRawCalories,
|
||||
HCRawOxygenSaturation,
|
||||
HCRawWeight,
|
||||
HCRawHeight,
|
||||
HCRawBodyFat,
|
||||
HCRawExerciseSession,
|
||||
HCRawNutrition,
|
||||
HCRawHydration,
|
||||
HCRawHeartRateVariabilityRmssd,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"User",
|
||||
"Challenge",
|
||||
"Tracking",
|
||||
"DailyHealthMetrics",
|
||||
"WorkoutSession",
|
||||
"DailyContext",
|
||||
"HealthConnectSyncLog",
|
||||
"HealthConnectDailyMetrics",
|
||||
"HealthConnectExerciseSession",
|
||||
# Raw data models
|
||||
"HCRawSteps",
|
||||
"HCRawHeartRate",
|
||||
"HCRawHeartRateSample",
|
||||
"HCRawRestingHeartRate",
|
||||
"HCRawSleepSession",
|
||||
"HCRawSleepStage",
|
||||
"HCRawDistance",
|
||||
"HCRawCalories",
|
||||
"HCRawOxygenSaturation",
|
||||
"HCRawWeight",
|
||||
"HCRawHeight",
|
||||
"HCRawBodyFat",
|
||||
"HCRawExerciseSession",
|
||||
"HCRawNutrition",
|
||||
"HCRawHydration",
|
||||
"HCRawHeartRateVariabilityRmssd",
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Challenge SQLAlchemy model."""
|
||||
from sqlalchemy import Column, Integer, String, Date, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import date
|
||||
from backend.database import Base
|
||||
|
||||
|
||||
class Challenge(Base):
|
||||
"""Challenge model for tracking habits."""
|
||||
__tablename__ = "challenges"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, index=True)
|
||||
period_type = Column(String) # 'week', 'month', 'year'
|
||||
daily_target = Column(Integer)
|
||||
start_date = Column(Date)
|
||||
end_date = Column(Date)
|
||||
created_at = Column(Date, default=date.today)
|
||||
display_order = Column(Integer, default=0)
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
unit_type = Column(String, default="reps") # 'reps', 'minutes', 'pages', 'km'
|
||||
rest_days = Column(String, nullable=True) # comma-separated list of days
|
||||
icon = Column(String, default="fa-dumbbell")
|
||||
image_url = Column(String, nullable=True)
|
||||
description = Column(String, nullable=True)
|
||||
frequency_count = Column(Integer, nullable=True)
|
||||
selected_days = Column(String, nullable=True)
|
||||
|
||||
owner = relationship("User", back_populates="challenges")
|
||||
trackings = relationship("Tracking", back_populates="challenge", cascade="all, delete-orphan")
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Daily Context Model - Stores subjective user inputs like mood, tags, etc."""
|
||||
from sqlalchemy import Column, Integer, String, Date, ForeignKey, UniqueConstraint
|
||||
from backend.database import Base
|
||||
|
||||
class DailyContext(Base):
|
||||
__tablename__ = "daily_context"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
date = Column(Date, index=True)
|
||||
|
||||
mood_score = Column(Integer) # 1-100 slider
|
||||
tags = Column(String, nullable=True) # Comma separated tags e.g. "Alcohol,Stress"
|
||||
notes = Column(String, nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'date', name='uq_daily_context'),
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
"""DailyHealthMetrics SQLAlchemy model."""
|
||||
from sqlalchemy import Column, Integer, Date, Float, ForeignKey, UniqueConstraint, String
|
||||
from sqlalchemy.orm import relationship
|
||||
from backend.database import Base
|
||||
|
||||
|
||||
class DailyHealthMetrics(Base):
|
||||
"""Daily health metrics model for tracking health data."""
|
||||
__tablename__ = "daily_health_metrics"
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'date', name='uq_user_date'),
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
date = Column(Date, index=True)
|
||||
step_count = Column(Integer, default=0)
|
||||
distance_meters = Column(Float, default=0.0)
|
||||
calories_burned = Column(Float, default=0.0)
|
||||
sleep_duration_minutes = Column(Integer, default=0)
|
||||
deep_sleep_minutes = Column(Integer, default=0)
|
||||
light_sleep_minutes = Column(Integer, default=0)
|
||||
rem_sleep_minutes = Column(Integer, default=0)
|
||||
awake_duration_minutes = Column(Integer, default=0)
|
||||
avg_heart_rate = Column(Integer, nullable=True)
|
||||
min_heart_rate = Column(Integer, nullable=True)
|
||||
max_heart_rate = Column(Integer, nullable=True)
|
||||
avg_spo2 = Column(Float, nullable=True)
|
||||
pai_score = Column(Float, default=0.0)
|
||||
weight = Column(Float, nullable=True)
|
||||
resting_heart_rate = Column(Integer, nullable=True)
|
||||
hrv = Column(Float, nullable=True)
|
||||
|
||||
# Data provenance tracking
|
||||
data_source = Column(String(50), default="health_connect") # "health_connect", "manual"
|
||||
|
||||
user = relationship("User", back_populates="health_metrics")
|
||||
@@ -0,0 +1,181 @@
|
||||
"""
|
||||
Health Connect Data Models - Storage for data pushed from Android companion app.
|
||||
|
||||
These models store health data received from the Health Connect Android app,
|
||||
enabling the HealthConnectProvider to serve data to the API layer.
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Float, Date, DateTime, ForeignKey, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from backend.database import Base
|
||||
import datetime
|
||||
|
||||
|
||||
class HealthConnectSyncLog(Base):
|
||||
"""
|
||||
Tracks sync operations from the Android companion app.
|
||||
Used for debugging and monitoring data flow.
|
||||
"""
|
||||
__tablename__ = "health_connect_sync_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
device_id = Column(String(100)) # Android device identifier
|
||||
sync_timestamp = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
records_pushed = Column(Integer, default=0)
|
||||
status = Column(String(50)) # "success", "partial", "error"
|
||||
error_message = Column(Text, nullable=True)
|
||||
|
||||
user = relationship("User")
|
||||
|
||||
|
||||
class HealthConnectDailyMetrics(Base):
|
||||
"""
|
||||
Daily health metrics received from Health Connect.
|
||||
Mirrors DailyHealthMetrics but with Health Connect-specific metadata.
|
||||
"""
|
||||
__tablename__ = "health_connect_daily_metrics"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
date = Column(Date, index=True)
|
||||
|
||||
# Data source identification
|
||||
data_source = Column(String(200), nullable=True) # Package name of originating app (e.g., com.huami.watch.hmwatchmanager)
|
||||
data_source_name = Column(String(100), nullable=True) # Friendly name (e.g., "Zepp")
|
||||
|
||||
# Activity metrics
|
||||
step_count = Column(Integer, default=0)
|
||||
distance_meters = Column(Float, default=0.0)
|
||||
calories_burned = Column(Float, default=0.0) # ActiveCaloriesBurnedRecord
|
||||
total_calories = Column(Float, default=0.0) # TotalCaloriesBurnedRecord
|
||||
|
||||
# Sleep metrics (from SleepSessionRecord)
|
||||
sleep_duration_minutes = Column(Integer, default=0)
|
||||
deep_sleep_minutes = Column(Integer, default=0)
|
||||
light_sleep_minutes = Column(Integer, default=0)
|
||||
rem_sleep_minutes = Column(Integer, default=0)
|
||||
awake_duration_minutes = Column(Integer, default=0)
|
||||
|
||||
# Heart rate metrics (from HeartRateRecord)
|
||||
avg_heart_rate = Column(Integer, nullable=True)
|
||||
min_heart_rate = Column(Integer, nullable=True)
|
||||
max_heart_rate = Column(Integer, nullable=True)
|
||||
resting_heart_rate = Column(Integer, nullable=True)
|
||||
|
||||
# SpO2 (from OxygenSaturationRecord)
|
||||
avg_spo2 = Column(Float, nullable=True)
|
||||
min_spo2 = Column(Float, nullable=True)
|
||||
max_spo2 = Column(Float, nullable=True)
|
||||
|
||||
# Body metrics
|
||||
weight = Column(Float, nullable=True) # WeightRecord
|
||||
|
||||
# Metadata
|
||||
last_synced = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
sync_id = Column(Integer, ForeignKey("health_connect_sync_logs.id"), nullable=True)
|
||||
|
||||
user = relationship("User")
|
||||
|
||||
|
||||
class HealthConnectExerciseSession(Base):
|
||||
"""
|
||||
Exercise sessions from Health Connect ExerciseSessionRecord.
|
||||
"""
|
||||
__tablename__ = "health_connect_exercise_sessions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
|
||||
# Session timing
|
||||
start_time = Column(DateTime, index=True)
|
||||
end_time = Column(DateTime)
|
||||
|
||||
# Exercise type (Health Connect ExerciseType enum)
|
||||
exercise_type = Column(Integer) # Health Connect type ID
|
||||
exercise_type_name = Column(String(100)) # Human-readable name
|
||||
|
||||
# Metrics
|
||||
duration_seconds = Column(Integer, default=0)
|
||||
distance_meters = Column(Float, nullable=True)
|
||||
calories_burned = Column(Float, nullable=True)
|
||||
|
||||
# Heart rate during exercise
|
||||
avg_heart_rate = Column(Integer, nullable=True)
|
||||
max_heart_rate = Column(Integer, nullable=True)
|
||||
min_heart_rate = Column(Integer, nullable=True)
|
||||
|
||||
# Additional metrics
|
||||
steps = Column(Integer, nullable=True)
|
||||
elevation_gained = Column(Float, nullable=True)
|
||||
|
||||
# Metadata
|
||||
notes = Column(Text, nullable=True)
|
||||
last_synced = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
sync_id = Column(Integer, ForeignKey("health_connect_sync_logs.id"), nullable=True)
|
||||
|
||||
user = relationship("User")
|
||||
|
||||
|
||||
# Health Connect Exercise Type mapping (subset)
|
||||
HEALTH_CONNECT_EXERCISE_TYPES = {
|
||||
0: "Unknown",
|
||||
2: "Badminton",
|
||||
4: "Baseball",
|
||||
5: "Basketball",
|
||||
8: "Biking",
|
||||
9: "Biking Stationary",
|
||||
10: "Boot Camp",
|
||||
11: "Boxing",
|
||||
13: "Calisthenics",
|
||||
14: "Cricket",
|
||||
16: "Dancing",
|
||||
25: "Elliptical",
|
||||
26: "Exercise Class",
|
||||
27: "Fencing",
|
||||
28: "Football American",
|
||||
29: "Football Australian",
|
||||
31: "Frisbee Disc",
|
||||
32: "Golf",
|
||||
33: "Guided Breathing",
|
||||
34: "Gymnastics",
|
||||
35: "Handball",
|
||||
36: "High Intensity Interval Training",
|
||||
37: "Hiking",
|
||||
38: "Ice Hockey",
|
||||
39: "Ice Skating",
|
||||
44: "Martial Arts",
|
||||
46: "Paddling",
|
||||
48: "Pilates",
|
||||
50: "Racquetball",
|
||||
51: "Rock Climbing",
|
||||
52: "Roller Hockey",
|
||||
53: "Rowing",
|
||||
54: "Rowing Machine",
|
||||
55: "Rugby",
|
||||
56: "Running",
|
||||
57: "Running Treadmill",
|
||||
58: "Sailing",
|
||||
59: "Scuba Diving",
|
||||
60: "Skating",
|
||||
61: "Skiing",
|
||||
62: "Snowboarding",
|
||||
63: "Snowshoeing",
|
||||
64: "Soccer",
|
||||
65: "Softball",
|
||||
67: "Squash",
|
||||
68: "Stair Climbing",
|
||||
69: "Stair Climbing Machine",
|
||||
70: "Strength Training",
|
||||
71: "Stretching",
|
||||
72: "Surfing",
|
||||
73: "Swimming Open Water",
|
||||
74: "Swimming Pool",
|
||||
75: "Table Tennis",
|
||||
76: "Tennis",
|
||||
78: "Volleyball",
|
||||
79: "Walking",
|
||||
80: "Water Polo",
|
||||
81: "Weightlifting",
|
||||
82: "Wheelchair",
|
||||
84: "Yoga",
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
"""
|
||||
Health Connect Raw Data Models - Storage for raw health records from Health Connect.
|
||||
|
||||
These models store individual health records (not aggregated) received from
|
||||
the Android companion app, enabling accurate aggregation on-demand.
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, Text, UniqueConstraint, Index
|
||||
from sqlalchemy.orm import relationship
|
||||
from backend.database import Base
|
||||
import datetime
|
||||
|
||||
|
||||
# ==================== STEPS ====================
|
||||
|
||||
class HCRawSteps(Base):
|
||||
"""Individual step records from Health Connect StepsRecord"""
|
||||
__tablename__ = "hc_raw_steps"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
|
||||
# Record identification (from Health Connect metadata)
|
||||
record_id = Column(String(100), index=True) # metadata.id - unique per source
|
||||
data_source = Column(String(200)) # Package name (e.g., com.huami.watch.hmwatchmanager)
|
||||
data_source_name = Column(String(100)) # Friendly name (e.g., "Zepp")
|
||||
|
||||
# Timestamps
|
||||
start_time = Column(DateTime, index=True)
|
||||
end_time = Column(DateTime)
|
||||
synced_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
# Data
|
||||
count = Column(Integer)
|
||||
|
||||
# Sync tracking
|
||||
sync_id = Column(Integer, ForeignKey("health_connect_sync_logs.id"), nullable=True)
|
||||
|
||||
# Unique constraint: same user + record_id + source = unique record
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'record_id', 'data_source', name='uq_steps_record'),
|
||||
Index('ix_steps_user_date', 'user_id', 'start_time'),
|
||||
)
|
||||
|
||||
|
||||
# ==================== HEART RATE ====================
|
||||
|
||||
class HCRawHeartRate(Base):
|
||||
"""Heart rate record containers from Health Connect HeartRateRecord"""
|
||||
__tablename__ = "hc_raw_heart_rate"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
|
||||
record_id = Column(String(100), index=True)
|
||||
data_source = Column(String(200))
|
||||
data_source_name = Column(String(100))
|
||||
|
||||
start_time = Column(DateTime, index=True)
|
||||
end_time = Column(DateTime)
|
||||
synced_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
sync_id = Column(Integer, ForeignKey("health_connect_sync_logs.id"), nullable=True)
|
||||
|
||||
# Relationship to samples
|
||||
samples = relationship("HCRawHeartRateSample", back_populates="record", cascade="all, delete-orphan")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'record_id', 'data_source', name='uq_hr_record'),
|
||||
Index('ix_hr_user_date', 'user_id', 'start_time'),
|
||||
)
|
||||
|
||||
|
||||
class HCRawHeartRateSample(Base):
|
||||
"""Individual heart rate samples within a HeartRateRecord"""
|
||||
__tablename__ = "hc_raw_heart_rate_samples"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
record_id = Column(Integer, ForeignKey("hc_raw_heart_rate.id", ondelete="CASCADE"), index=True)
|
||||
|
||||
time = Column(DateTime, index=True)
|
||||
bpm = Column(Integer)
|
||||
|
||||
record = relationship("HCRawHeartRate", back_populates="samples")
|
||||
|
||||
|
||||
# ==================== RESTING HEART RATE ====================
|
||||
|
||||
class HCRawRestingHeartRate(Base):
|
||||
"""Resting heart rate records from Health Connect RestingHeartRateRecord"""
|
||||
__tablename__ = "hc_raw_resting_hr"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
|
||||
record_id = Column(String(100), index=True)
|
||||
data_source = Column(String(200))
|
||||
data_source_name = Column(String(100))
|
||||
|
||||
time = Column(DateTime, index=True)
|
||||
synced_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
bpm = Column(Integer)
|
||||
|
||||
sync_id = Column(Integer, ForeignKey("health_connect_sync_logs.id"), nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'record_id', 'data_source', name='uq_resting_hr_record'),
|
||||
Index('ix_resting_hr_user_date', 'user_id', 'time'),
|
||||
)
|
||||
|
||||
|
||||
# ==================== SLEEP ====================
|
||||
|
||||
class HCRawSleepSession(Base):
|
||||
"""Sleep session records from Health Connect SleepSessionRecord"""
|
||||
__tablename__ = "hc_raw_sleep_sessions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
|
||||
record_id = Column(String(100), index=True)
|
||||
data_source = Column(String(200))
|
||||
data_source_name = Column(String(100))
|
||||
|
||||
start_time = Column(DateTime, index=True)
|
||||
end_time = Column(DateTime)
|
||||
synced_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
title = Column(String(200), nullable=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
|
||||
sync_id = Column(Integer, ForeignKey("health_connect_sync_logs.id"), nullable=True)
|
||||
|
||||
# Relationship to stages
|
||||
stages = relationship("HCRawSleepStage", back_populates="session", cascade="all, delete-orphan")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'record_id', 'data_source', name='uq_sleep_record'),
|
||||
Index('ix_sleep_user_date', 'user_id', 'start_time'),
|
||||
)
|
||||
|
||||
|
||||
class HCRawSleepStage(Base):
|
||||
"""Sleep stages within a sleep session"""
|
||||
__tablename__ = "hc_raw_sleep_stages"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
session_id = Column(Integer, ForeignKey("hc_raw_sleep_sessions.id", ondelete="CASCADE"), index=True)
|
||||
|
||||
start_time = Column(DateTime)
|
||||
end_time = Column(DateTime)
|
||||
stage_type = Column(Integer) # Health Connect stage type enum
|
||||
stage_name = Column(String(50)) # Human readable: "awake", "light", "deep", "rem", etc.
|
||||
|
||||
session = relationship("HCRawSleepSession", back_populates="stages")
|
||||
|
||||
|
||||
# Sleep stage type constants (from Health Connect)
|
||||
SLEEP_STAGE_TYPES = {
|
||||
0: "unknown",
|
||||
1: "awake",
|
||||
2: "sleeping",
|
||||
3: "out_of_bed",
|
||||
4: "light",
|
||||
5: "deep",
|
||||
6: "rem",
|
||||
7: "awake_in_bed",
|
||||
}
|
||||
|
||||
|
||||
# ==================== DISTANCE ====================
|
||||
|
||||
class HCRawDistance(Base):
|
||||
"""Distance records from Health Connect DistanceRecord"""
|
||||
__tablename__ = "hc_raw_distance"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
|
||||
record_id = Column(String(100), index=True)
|
||||
data_source = Column(String(200))
|
||||
data_source_name = Column(String(100))
|
||||
|
||||
start_time = Column(DateTime, index=True)
|
||||
end_time = Column(DateTime)
|
||||
synced_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
distance_meters = Column(Float)
|
||||
|
||||
sync_id = Column(Integer, ForeignKey("health_connect_sync_logs.id"), nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'record_id', 'data_source', name='uq_distance_record'),
|
||||
Index('ix_distance_user_date', 'user_id', 'start_time'),
|
||||
)
|
||||
|
||||
|
||||
# ==================== CALORIES ====================
|
||||
|
||||
class HCRawCalories(Base):
|
||||
"""Calories records from Health Connect (ActiveCaloriesBurned or TotalCaloriesBurned)"""
|
||||
__tablename__ = "hc_raw_calories"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
|
||||
record_id = Column(String(100), index=True)
|
||||
data_source = Column(String(200))
|
||||
data_source_name = Column(String(100))
|
||||
|
||||
start_time = Column(DateTime, index=True)
|
||||
end_time = Column(DateTime)
|
||||
synced_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
calories = Column(Float)
|
||||
record_type = Column(String(20)) # "active" or "total"
|
||||
|
||||
sync_id = Column(Integer, ForeignKey("health_connect_sync_logs.id"), nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'record_id', 'data_source', name='uq_calories_record'),
|
||||
Index('ix_calories_user_date', 'user_id', 'start_time'),
|
||||
)
|
||||
|
||||
|
||||
# ==================== OXYGEN SATURATION (SpO2) ====================
|
||||
|
||||
class HCRawOxygenSaturation(Base):
|
||||
"""Oxygen saturation records from Health Connect OxygenSaturationRecord"""
|
||||
__tablename__ = "hc_raw_spo2"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
|
||||
record_id = Column(String(100), index=True)
|
||||
data_source = Column(String(200))
|
||||
data_source_name = Column(String(100))
|
||||
|
||||
time = Column(DateTime, index=True)
|
||||
synced_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
percentage = Column(Float)
|
||||
|
||||
sync_id = Column(Integer, ForeignKey("health_connect_sync_logs.id"), nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'record_id', 'data_source', name='uq_spo2_record'),
|
||||
Index('ix_spo2_user_date', 'user_id', 'time'),
|
||||
)
|
||||
|
||||
|
||||
# ==================== WEIGHT ====================
|
||||
|
||||
class HCRawWeight(Base):
|
||||
"""Weight records from Health Connect WeightRecord"""
|
||||
__tablename__ = "hc_raw_weight"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
|
||||
record_id = Column(String(100), index=True)
|
||||
data_source = Column(String(200))
|
||||
data_source_name = Column(String(100))
|
||||
|
||||
time = Column(DateTime, index=True)
|
||||
synced_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
weight_kg = Column(Float)
|
||||
|
||||
sync_id = Column(Integer, ForeignKey("health_connect_sync_logs.id"), nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'record_id', 'data_source', name='uq_weight_record'),
|
||||
Index('ix_weight_user_date', 'user_id', 'time'),
|
||||
)
|
||||
|
||||
|
||||
# ==================== HEIGHT ====================
|
||||
|
||||
class HCRawHeight(Base):
|
||||
"""Height records from Health Connect HeightRecord"""
|
||||
__tablename__ = "hc_raw_height"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
|
||||
record_id = Column(String(100), index=True)
|
||||
data_source = Column(String(200))
|
||||
data_source_name = Column(String(100))
|
||||
|
||||
time = Column(DateTime, index=True)
|
||||
synced_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
height_meters = Column(Float)
|
||||
|
||||
sync_id = Column(Integer, ForeignKey("health_connect_sync_logs.id"), nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'record_id', 'data_source', name='uq_height_record'),
|
||||
Index('ix_height_user_date', 'user_id', 'time'),
|
||||
)
|
||||
|
||||
|
||||
# ==================== BODY FAT ====================
|
||||
|
||||
class HCRawBodyFat(Base):
|
||||
"""Body fat records from Health Connect BodyFatRecord"""
|
||||
__tablename__ = "hc_raw_body_fat"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
|
||||
record_id = Column(String(100), index=True)
|
||||
data_source = Column(String(200))
|
||||
data_source_name = Column(String(100))
|
||||
|
||||
time = Column(DateTime, index=True)
|
||||
synced_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
percentage = Column(Float)
|
||||
|
||||
sync_id = Column(Integer, ForeignKey("health_connect_sync_logs.id"), nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'record_id', 'data_source', name='uq_body_fat_record'),
|
||||
Index('ix_body_fat_user_date', 'user_id', 'time'),
|
||||
)
|
||||
|
||||
|
||||
# ==================== EXERCISE SESSIONS ====================
|
||||
|
||||
class HCRawExerciseSession(Base):
|
||||
"""Exercise session records from Health Connect ExerciseSessionRecord"""
|
||||
__tablename__ = "hc_raw_exercise_sessions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
|
||||
record_id = Column(String(100), index=True)
|
||||
data_source = Column(String(200))
|
||||
data_source_name = Column(String(100))
|
||||
|
||||
start_time = Column(DateTime, index=True)
|
||||
end_time = Column(DateTime)
|
||||
synced_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
# Exercise details
|
||||
exercise_type = Column(Integer) # Health Connect ExerciseType enum
|
||||
exercise_type_name = Column(String(100))
|
||||
title = Column(String(200), nullable=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
|
||||
# Associated metrics (if available in the session)
|
||||
distance_meters = Column(Float, nullable=True)
|
||||
calories = Column(Float, nullable=True)
|
||||
|
||||
# Heart rate during exercise
|
||||
avg_heart_rate = Column(Integer, nullable=True)
|
||||
max_heart_rate = Column(Integer, nullable=True)
|
||||
min_heart_rate = Column(Integer, nullable=True)
|
||||
|
||||
# Additional
|
||||
steps = Column(Integer, nullable=True)
|
||||
elevation_gained_meters = Column(Float, nullable=True)
|
||||
|
||||
sync_id = Column(Integer, ForeignKey("health_connect_sync_logs.id"), nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'record_id', 'data_source', name='uq_exercise_record'),
|
||||
Index('ix_exercise_user_date', 'user_id', 'start_time'),
|
||||
)
|
||||
|
||||
|
||||
# ==================== NUTRITION ====================
|
||||
|
||||
class HCRawNutrition(Base):
|
||||
"""Nutrition records from Health Connect NutritionRecord"""
|
||||
__tablename__ = "hc_raw_nutrition"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
|
||||
record_id = Column(String(100), index=True)
|
||||
data_source = Column(String(200))
|
||||
data_source_name = Column(String(100))
|
||||
|
||||
start_time = Column(DateTime, index=True)
|
||||
end_time = Column(DateTime)
|
||||
synced_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
# Basic nutrition info
|
||||
name = Column(String(200), nullable=True)
|
||||
meal_type = Column(Integer, nullable=True) # 0=unknown, 1=breakfast, 2=lunch, 3=dinner, 4=snack
|
||||
|
||||
# Macros
|
||||
calories = Column(Float, nullable=True)
|
||||
protein_grams = Column(Float, nullable=True)
|
||||
carbohydrates_grams = Column(Float, nullable=True)
|
||||
fat_grams = Column(Float, nullable=True)
|
||||
fiber_grams = Column(Float, nullable=True)
|
||||
sugar_grams = Column(Float, nullable=True)
|
||||
|
||||
sync_id = Column(Integer, ForeignKey("health_connect_sync_logs.id"), nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'record_id', 'data_source', name='uq_nutrition_record'),
|
||||
Index('ix_nutrition_user_date', 'user_id', 'start_time'),
|
||||
)
|
||||
|
||||
|
||||
# ==================== HYDRATION ====================
|
||||
|
||||
class HCRawHydration(Base):
|
||||
"""Hydration records from Health Connect HydrationRecord"""
|
||||
__tablename__ = "hc_raw_hydration"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
|
||||
record_id = Column(String(100), index=True)
|
||||
data_source = Column(String(200))
|
||||
data_source_name = Column(String(100))
|
||||
|
||||
start_time = Column(DateTime, index=True)
|
||||
end_time = Column(DateTime)
|
||||
synced_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
volume_liters = Column(Float)
|
||||
|
||||
sync_id = Column(Integer, ForeignKey("health_connect_sync_logs.id"), nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'record_id', 'data_source', name='uq_hydration_record'),
|
||||
Index('ix_hydration_user_date', 'user_id', 'start_time'),
|
||||
)
|
||||
|
||||
|
||||
# ==================== HEART RATE VARIABILITY ====================
|
||||
|
||||
class HCRawHeartRateVariabilityRmssd(Base):
|
||||
"""HRV records from Health Connect HeartRateVariabilityRmssdRecord"""
|
||||
__tablename__ = "hc_raw_hrv_rmssd"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
|
||||
record_id = Column(String(100), index=True)
|
||||
data_source = Column(String(200))
|
||||
data_source_name = Column(String(100))
|
||||
|
||||
time = Column(DateTime, index=True)
|
||||
synced_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
rmssd = Column(Float)
|
||||
|
||||
sync_id = Column(Integer, ForeignKey("health_connect_sync_logs.id"), nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'record_id', 'data_source', name='uq_hrv_rmssd_record'),
|
||||
Index('ix_hrv_rmssd_user_date', 'user_id', 'time'),
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Tracking SQLAlchemy model."""
|
||||
from sqlalchemy import Column, Integer, Date, Boolean, Text, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from backend.database import Base
|
||||
|
||||
|
||||
class Tracking(Base):
|
||||
"""Tracking model for recording challenge progress."""
|
||||
__tablename__ = "tracking"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
challenge_id = Column(Integer, ForeignKey("challenges.id"))
|
||||
date = Column(Date)
|
||||
reps = Column(Integer, default=0)
|
||||
completed = Column(Boolean, default=False)
|
||||
notes = Column(Text, nullable=True)
|
||||
|
||||
challenge = relationship("Challenge", back_populates="trackings")
|
||||
@@ -0,0 +1,17 @@
|
||||
"""User SQLAlchemy model."""
|
||||
from sqlalchemy import Column, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
from backend.database import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""User model for authentication and ownership."""
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String, unique=True, index=True)
|
||||
hashed_password = Column(String)
|
||||
|
||||
challenges = relationship("Challenge", back_populates="owner")
|
||||
health_metrics = relationship("DailyHealthMetrics", back_populates="user")
|
||||
workouts = relationship("WorkoutSession", back_populates="user")
|
||||
@@ -0,0 +1,48 @@
|
||||
"""WorkoutSession SQLAlchemy model."""
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from backend.database import Base
|
||||
|
||||
|
||||
# Zepp activity type mapping
|
||||
ZEPP_ACTIVITY_TYPES = {
|
||||
1: "Running",
|
||||
6: "Walking",
|
||||
8: "Cycling",
|
||||
10: "Elliptical",
|
||||
16: "Free Training",
|
||||
23: "HIIT",
|
||||
52: "Stretching",
|
||||
60: "Yoga",
|
||||
}
|
||||
|
||||
|
||||
class WorkoutSession(Base):
|
||||
"""Workout data model."""
|
||||
__tablename__ = "workout_sessions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
start_time = Column(DateTime, index=True)
|
||||
end_time = Column(DateTime)
|
||||
activity_type = Column(String) # Human readable name
|
||||
activity_type_id = Column(Integer, nullable=True) # Original Zepp type ID
|
||||
duration_seconds = Column(Integer, default=0)
|
||||
distance_meters = Column(Float, default=0.0)
|
||||
calories = Column(Float, default=0.0)
|
||||
avg_pace = Column(Float, nullable=True) # seconds per meter
|
||||
max_pace = Column(Float, nullable=True)
|
||||
min_pace = Column(Float, nullable=True)
|
||||
avg_hr = Column(Integer, nullable=True)
|
||||
max_hr = Column(Integer, nullable=True)
|
||||
min_hr = Column(Integer, nullable=True)
|
||||
notes = Column(String, nullable=True)
|
||||
|
||||
# Data provenance tracking
|
||||
data_source = Column(String(50), default="health_connect") # "health_connect", "manual"
|
||||
|
||||
user = relationship("User", back_populates="workouts")
|
||||
|
||||
@staticmethod
|
||||
def get_activity_name(type_id: int) -> str:
|
||||
return ZEPP_ACTIVITY_TYPES.get(type_id, f"Activity {type_id}")
|
||||
Reference in New Issue
Block a user