-
Notifications
You must be signed in to change notification settings - Fork 4
feat: add workout trends API (gateway endpoints) #136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from .trend_api import TrendApi | ||
|
|
||
| __all__ = ["TrendApi"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import typing | ||
| from datetime import date | ||
|
|
||
| import pendulum | ||
|
|
||
| from otf_api.api import utils | ||
| from otf_api.models.trends import ( | ||
| TrendType, | ||
| WorkoutStatsPreviewResponse, | ||
| WorkoutStatsResponse, | ||
| ) | ||
|
|
||
| from .trend_client import TrendClient | ||
|
|
||
| if typing.TYPE_CHECKING: | ||
| from otf_api import Otf | ||
| from otf_api.api.client import OtfClient | ||
|
|
||
| DEFAULT_PREVIEW_WORKOUT_COUNT = 10 | ||
|
|
||
|
|
||
| class TrendApi: | ||
| """API for retrieving workout trend data from OrangeTheory. | ||
|
|
||
| Provides methods to get per-metric trend data (splat points, heart rate, | ||
| treadmill speed, rower stats) over time, as well as a preview across all metrics. | ||
| """ | ||
|
|
||
| def __init__(self, otf: "Otf", otf_client: "OtfClient"): | ||
| self.otf = otf | ||
| self.client = TrendClient(otf_client) | ||
|
|
||
| def get_workout_stats( | ||
| self, | ||
| trend_type: TrendType | str, | ||
| start_date: date | str | None = None, | ||
| end_date: date | str | None = None, | ||
| ) -> WorkoutStatsResponse: | ||
| """Get detailed workout stats for a specific metric over a date range. | ||
|
|
||
| Args: | ||
| trend_type: The trend metric to retrieve (e.g. TrendType.SplatPoints). | ||
| start_date: Start of the date range. Defaults to 90 days ago. | ||
| end_date: End of the date range. Defaults to today. | ||
|
|
||
| Returns: | ||
| WorkoutStatsResponse: The stat data with individual data points per workout. | ||
| """ | ||
| start = utils.ensure_date(start_date) or pendulum.today().subtract(days=90).date() | ||
| end = utils.ensure_date(end_date) or pendulum.today().date() | ||
|
|
||
| start_str = pendulum.instance(pendulum.datetime(start.year, start.month, start.day)).to_iso8601_string() | ||
| end_str = pendulum.instance(pendulum.datetime(end.year, end.month, end.day, 23, 59, 59)).to_iso8601_string() | ||
|
|
||
| stats_key = str(trend_type) | ||
| data = self.client.get_workout_stats(stats_key, start_str, end_str) | ||
| return WorkoutStatsResponse(**data) | ||
|
|
||
| def get_workout_stats_preview( | ||
| self, workout_count: int = DEFAULT_PREVIEW_WORKOUT_COUNT | ||
| ) -> WorkoutStatsPreviewResponse: | ||
| """Get a preview of workout stats across all trend metrics. | ||
|
|
||
| Args: | ||
| workout_count: Number of recent workouts to include. Default is 10. | ||
|
|
||
| Returns: | ||
| WorkoutStatsPreviewResponse: Preview data with stats across all metrics. | ||
| """ | ||
| data = self.client.get_workout_stats_preview(workout_count) | ||
| return WorkoutStatsPreviewResponse(**data) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| from typing import Any | ||
|
|
||
| from otf_api.api.client import API_GATEWAY_BASE_URL, OtfClient | ||
|
|
||
|
|
||
| class TrendClient: | ||
| """Client for retrieving workout trends/stats data from the OTF Gateway API.""" | ||
|
|
||
| def __init__(self, client: OtfClient): | ||
| self.client = client | ||
|
|
||
| def gateway_request( | ||
| self, method: str, path: str, params: dict[str, Any] | None = None, headers: dict[str, Any] | None = None | ||
| ) -> Any: # noqa: ANN401 | ||
| """Perform an API request to the Gateway API.""" | ||
| return self.client.do(method, API_GATEWAY_BASE_URL, path, params, headers=headers) | ||
|
|
||
| def get_workout_stats(self, stats_key: str, start_date: str, end_date: str) -> dict: | ||
| """Retrieve workout stats for a specific metric over a date range. | ||
|
|
||
| Args: | ||
| stats_key: The stat key (e.g. 'splat_points', 'average_hr'). | ||
| start_date: ISO-format start date. | ||
| end_date: ISO-format end date. | ||
| """ | ||
| return self.gateway_request( | ||
| "GET", | ||
| f"/consumer-mobile/v1/users/me/workout-stats/{stats_key}", | ||
| params={"start": start_date, "end": end_date}, | ||
| ) | ||
|
|
||
| def get_workout_stats_preview(self, workout_count: int = 10) -> dict: | ||
| """Retrieve a preview of workout stats across all metrics. | ||
|
|
||
| Args: | ||
| workout_count: Number of recent workouts to include. Default is 10. | ||
| """ | ||
| return self.gateway_request( | ||
| "GET", | ||
| "/consumer-mobile/v1/users/me/workout-stats/preview", | ||
| params={"workoutCount": workout_count}, | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| from .enums import TrendCategory, TrendType | ||
| from .trends import ( | ||
| PreviewStat, | ||
| StatPoint, | ||
| WorkoutStatsPreviewResponse, | ||
| WorkoutStatsResponse, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "PreviewStat", | ||
| "StatPoint", | ||
| "TrendCategory", | ||
| "TrendType", | ||
| "WorkoutStatsPreviewResponse", | ||
| "WorkoutStatsResponse", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| from enum import StrEnum | ||
|
|
||
|
|
||
| class TrendCategory(StrEnum): | ||
| """Categories that group related workout trend metrics.""" | ||
|
|
||
| Effort = "effort" | ||
| Treadmill = "treadmill" | ||
| Rower = "rower" | ||
|
|
||
|
|
||
| class TrendType(StrEnum): | ||
| """Available workout stat keys for the trends API.""" | ||
|
|
||
| SplatPoints = "splat_points" | ||
| AverageHeartRate = "average_hr" | ||
| PeakHeartRate = "peak_hr" | ||
| TreadmillTopSpeed = "tread_top_speed" | ||
| RowerSplitTime = "rower_500m_split_time" | ||
| RowerTopPower = "rower_top_power" | ||
|
|
||
| @property | ||
| def category(self) -> TrendCategory: | ||
| """Get the category this trend type belongs to.""" | ||
| return _TREND_CATEGORY_MAP[self] | ||
|
|
||
|
|
||
| _TREND_CATEGORY_MAP: dict[TrendType, TrendCategory] = { | ||
| TrendType.SplatPoints: TrendCategory.Effort, | ||
| TrendType.AverageHeartRate: TrendCategory.Effort, | ||
| TrendType.PeakHeartRate: TrendCategory.Effort, | ||
| TrendType.TreadmillTopSpeed: TrendCategory.Treadmill, | ||
| TrendType.RowerSplitTime: TrendCategory.Rower, | ||
| TrendType.RowerTopPower: TrendCategory.Rower, | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| from datetime import datetime | ||
|
|
||
| from pydantic import Field | ||
|
|
||
| from otf_api.models.base import OtfItemBase | ||
|
|
||
|
|
||
| class StatPoint(OtfItemBase): | ||
| """A single data point in a workout stats time series.""" | ||
|
|
||
| date: datetime | None = None | ||
| value: float | None = None | ||
| workout_id: str | None = None | ||
| class_type: str | None = None | ||
| class_name: str | None = None | ||
|
|
||
|
|
||
| class WorkoutStatsResponse(OtfItemBase): | ||
| """Response from the workout stats endpoint for a single metric over a date range.""" | ||
|
|
||
| start_date_time: datetime | None = Field(None, validation_alias="start") | ||
| end_date_time: datetime | None = Field(None, validation_alias="end") | ||
| stat_key: str | None = None | ||
| unit: str | None = None | ||
| value_type: str | None = None | ||
| points: list[StatPoint] = Field(default_factory=list) | ||
|
|
||
|
|
||
| class PreviewStat(OtfItemBase): | ||
| """A single metric's data within a workout stats preview response.""" | ||
|
|
||
| points: list[StatPoint] = Field(default_factory=list) | ||
| stat_key: str | None = None | ||
| unit: str | None = None | ||
| value_type: str | None = None | ||
|
|
||
|
|
||
| class WorkoutStatsPreviewResponse(OtfItemBase): | ||
| """Response from the workout stats preview endpoint, containing all metrics.""" | ||
|
|
||
| start_date_time: datetime | None = Field(None, validation_alias="start") | ||
| end_date_time: datetime | None = Field(None, validation_alias="end") | ||
| stats: list[PreviewStat] = Field(default_factory=list) | ||
| requested_workout_count: int | None = None |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.