Skip to content

Commit aab9c4b

Browse files
authored
Merge pull request #103 from NodeJSmith/fix/return_workouts_missing_perf_summaries
update logic to allow returning workouts even if HR monitor data missing
2 parents fa9def4 + 42ec6ba commit aab9c4b

File tree

7 files changed

+23
-60
lines changed

7 files changed

+23
-60
lines changed

.bumpversion.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tool.bumpversion]
2-
current_version = "0.15.2"
2+
current_version = "0.15.3"
33

44
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(?:-(?P<rc_l>rc)(?P<rc>0|[1-9]\\d*))?"
55

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "otf-api"
3-
version = "0.15.2"
3+
version = "0.15.3"
44
description = "Python OrangeTheory Fitness API Client"
55
authors = [{ name = "Jessica Smith", email = "[email protected]" }]
66
requires-python = ">=3.11"

source/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
project = "OrangeTheory API"
1515
copyright = "2025, Jessica Smith"
1616
author = "Jessica Smith"
17-
release = "0.15.2"
17+
release = "0.15.3"
1818

1919
# -- General configuration ---------------------------------------------------
2020
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

src/otf_api/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def _setup_logging() -> None:
4747

4848
_setup_logging()
4949

50-
__version__ = "0.15.2"
50+
__version__ = "0.15.3"
5151

5252

5353
__all__ = ["Otf", "OtfUser", "models"]

src/otf_api/api/workouts/workout_api.py

Lines changed: 10 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -262,71 +262,31 @@ def get_workouts(
262262
bookings = self.otf.bookings.get_bookings_new(
263263
start_dtme, end_dtme, exclude_cancelled=True, remove_duplicates=True
264264
)
265-
bookings_dict = self._filter_bookings_for_workouts(bookings)
265+
filtered_bookings = [b for b in bookings if not (b.starts_at and b.starts_at > pendulum.now().naive())]
266+
bookings_list = [(b, b.workout.id if b.workout else None) for b in filtered_bookings]
266267

267-
perf_summaries_dict = self.client.get_perf_summaries_threaded(list(bookings_dict.keys()))
268+
workout_ids = [b.workout.id for b in filtered_bookings if b.workout]
269+
perf_summaries_dict = self.client.get_perf_summaries_threaded(workout_ids)
268270
telemetry_dict = self.client.get_telemetry_threaded(list(perf_summaries_dict.keys()), max_data_points)
269271
perf_summary_to_class_uuid_map = self.client.get_perf_summary_to_class_uuid_mapping()
270272

271273
workouts: list[models.Workout] = []
272-
for perf_id, perf_summary in perf_summaries_dict.items():
274+
for booking, perf_summary_id in bookings_list:
273275
try:
276+
perf_summary = perf_summaries_dict.get(perf_summary_id, {}) if perf_summary_id else {}
277+
telemetry = telemetry_dict.get(perf_summary_id, None) if perf_summary_id else None
278+
class_uuid = perf_summary_to_class_uuid_map.get(perf_summary_id, None) if perf_summary_id else None
274279
workout = models.Workout.create(
275-
**perf_summary,
276-
v2_booking=bookings_dict[perf_id],
277-
telemetry=telemetry_dict.get(perf_id),
278-
class_uuid=perf_summary_to_class_uuid_map.get(perf_id),
279-
api=self.otf,
280+
**perf_summary, v2_booking=booking, telemetry=telemetry, class_uuid=class_uuid, api=self.otf
280281
)
281282
workouts.append(workout)
282283
except ValueError:
283-
LOGGER.exception("Failed to create Workout for performance summary %s", perf_id)
284+
LOGGER.exception("Failed to create Workout for performance summary %s", perf_summary_id)
284285

285286
LOGGER.debug("Returning %d workouts", len(workouts))
286287

287288
return workouts
288289

289-
def _filter_bookings_for_workouts(self, bookings: list[models.BookingV2]) -> dict[str, models.BookingV2]:
290-
"""Filter bookings to only those that have a workout and are not in the future.
291-
292-
This is being pulled out of `get_workouts` to add more robust logging and error handling.
293-
294-
Args:
295-
bookings (list[BookingV2]): The list of bookings to filter.
296-
297-
Returns:
298-
dict[str, BookingV2]: A dictionary mapping workout IDs to bookings that have workouts.
299-
"""
300-
future_bookings = [b for b in bookings if b.starts_at and b.starts_at > pendulum.now().naive()]
301-
missing_workouts = [b for b in bookings if not b.workout and b not in future_bookings]
302-
LOGGER.debug("Found %d future bookings and %d missing workouts", len(future_bookings), len(missing_workouts))
303-
304-
if future_bookings:
305-
for booking in future_bookings:
306-
LOGGER.warning(
307-
"Booking %s for class '%s' (class_uuid=%s) is in the future, filtering out.",
308-
booking.booking_id,
309-
booking.otf_class,
310-
booking.class_uuid or "Unknown",
311-
)
312-
313-
if missing_workouts:
314-
for booking in missing_workouts:
315-
LOGGER.warning(
316-
"Booking %s for class '%s' (class_uuid=%s) is missing a workout, filtering out.",
317-
booking.booking_id,
318-
booking.otf_class,
319-
booking.class_uuid or "Unknown",
320-
)
321-
322-
bookings_dict = {
323-
b.workout.id: b for b in bookings if b.workout and b not in future_bookings and b not in missing_workouts
324-
}
325-
326-
LOGGER.debug("Filtered bookings to %d valid bookings for workouts mapping", len(bookings_dict))
327-
328-
return bookings_dict
329-
330290
def get_lifetime_workouts(self) -> list[models.Workout]:
331291
"""Get the member's lifetime workouts.
332292

src/otf_api/models/workouts/workout.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from pydantic import AliasPath, Field
44

55
from otf_api.models.base import OtfItemBase
6-
from otf_api.models.bookings import BookingV2, BookingV2Class, BookingV2Studio, BookingV2Workout, Rating
6+
from otf_api.models.bookings import BookingV2, BookingV2Class, BookingV2Studio, Rating
77
from otf_api.models.mixins import ApiMixin
88
from otf_api.models.workouts import HeartRate, Rower, Telemetry, Treadmill, ZoneTimeMinutes
99

@@ -18,9 +18,11 @@ class Workout(ApiMixin, OtfItemBase):
1818
"""
1919

2020
performance_summary_id: str = Field(
21-
..., validation_alias="id", description="Unique identifier for this performance summary"
21+
default="unknown", validation_alias="id", description="Unique identifier for this performance summary"
22+
)
23+
class_history_uuid: str = Field(
24+
default="unknown", validation_alias="id", description="Same as performance_summary_id"
2225
)
23-
class_history_uuid: str = Field(..., validation_alias="id", description="Same as performance_summary_id")
2426
booking_id: str = Field(..., description="The booking id for the new bookings endpoint.")
2527
class_uuid: str | None = Field(
2628
None, description="Used by the ratings endpoint - seems to fall off after a few months"
@@ -56,18 +58,19 @@ def __init__(self, **data):
5658
otf_class = v2_booking.otf_class
5759
v2_workout = v2_booking.workout
5860
assert isinstance(otf_class, BookingV2Class), "otf_class must be an instance of BookingV2Class"
59-
assert isinstance(v2_workout, BookingV2Workout), "v2_workout must be an instance of BookingV2Workout"
6061

6162
data["otf_class"] = otf_class
6263
data["studio"] = otf_class.studio
6364
data["coach"] = otf_class.coach
6465
data["ratable"] = v2_booking.ratable # this seems to be more accurate
6566

6667
data["booking_id"] = v2_booking.booking_id
67-
data["active_time_seconds"] = v2_workout.active_time_seconds
6868
data["class_rating"] = v2_booking.class_rating
6969
data["coach_rating"] = v2_booking.coach_rating
7070

71+
if v2_workout:
72+
data["active_time_seconds"] = v2_workout.active_time_seconds
73+
7174
telemetry: dict[str, Any] | None = data.get("telemetry")
7275
if telemetry and "maxHr" in telemetry:
7376
# max_hr seems to be left out of the heart rate data - it has peak_hr but they do not match

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)