mirror of
https://github.com/sstent/FitTrack_ReportGenerator.git
synced 2026-01-27 09:32:12 +00:00
This commit introduces the initial version of the FitTrack Report Generator, a FastAPI application for analyzing workout files. Key features include: - Parsing of FIT, TCX, and GPX workout files. - Analysis of power, heart rate, speed, and elevation data. - Generation of summary reports and charts. - REST API for single and batch workout analysis. The project structure has been set up with a `src` directory for core logic, an `api` directory for the FastAPI application, and a `tests` directory for unit, integration, and contract tests. The development workflow is configured to use Docker and modern Python tooling.
81 lines
2.5 KiB
Python
81 lines
2.5 KiB
Python
from datetime import date, datetime, timedelta
|
|
from itertools import chain
|
|
|
|
from pydantic import Field, ValidationInfo, field_validator
|
|
from pydantic.dataclasses import dataclass
|
|
from typing_extensions import Self
|
|
|
|
from .. import http
|
|
from ..utils import (
|
|
camel_to_snake_dict,
|
|
format_end_date,
|
|
get_localized_datetime,
|
|
)
|
|
from ._base import MAX_WORKERS, Data
|
|
|
|
|
|
@dataclass
|
|
class WeightData(Data):
|
|
sample_pk: int
|
|
calendar_date: date
|
|
weight: int
|
|
source_type: str
|
|
weight_delta: float
|
|
datetime_utc: datetime = Field(..., alias="timestamp_gmt")
|
|
datetime_local: datetime = Field(..., alias="date")
|
|
bmi: float | None = None
|
|
body_fat: float | None = None
|
|
body_water: float | None = None
|
|
bone_mass: int | None = None
|
|
muscle_mass: int | None = None
|
|
physique_rating: float | None = None
|
|
visceral_fat: float | None = None
|
|
metabolic_age: int | None = None
|
|
|
|
@field_validator("datetime_local", mode="before")
|
|
@classmethod
|
|
def to_localized_datetime(cls, v: int, info: ValidationInfo) -> datetime:
|
|
return get_localized_datetime(info.data["datetime_utc"].timestamp() * 1000, v)
|
|
|
|
@classmethod
|
|
def get(
|
|
cls, day: date | str, *, client: http.Client | None = None
|
|
) -> Self | None:
|
|
client = client or http.client
|
|
path = f"/weight-service/weight/dayview/{day}"
|
|
data = client.connectapi(path)
|
|
day_weight_list = data["dateWeightList"] if data else []
|
|
|
|
if not day_weight_list:
|
|
return None
|
|
|
|
# Get first (most recent) weight entry for the day
|
|
weight_data = camel_to_snake_dict(day_weight_list[0])
|
|
return cls(**weight_data)
|
|
|
|
@classmethod
|
|
def list(
|
|
cls,
|
|
end: date | str | None = None,
|
|
days: int = 1,
|
|
*,
|
|
client: http.Client | None = None,
|
|
max_workers: int = MAX_WORKERS,
|
|
) -> list[Self]:
|
|
client = client or http.client
|
|
end = format_end_date(end)
|
|
start = end - timedelta(days=days - 1)
|
|
|
|
data = client.connectapi(
|
|
f"/weight-service/weight/range/{start}/{end}?includeAll=true"
|
|
)
|
|
weight_summaries = data["dailyWeightSummaries"] if data else []
|
|
weight_metrics = chain.from_iterable(
|
|
summary["allWeightMetrics"] for summary in weight_summaries
|
|
)
|
|
weight_data_list = (
|
|
cls(**camel_to_snake_dict(weight_data))
|
|
for weight_data in weight_metrics
|
|
)
|
|
return sorted(weight_data_list, key=lambda d: d.datetime_utc)
|