diff --git a/.bumpversion.toml b/.bumpversion.toml index 3be582c..82691b4 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,5 +1,5 @@ [tool.bumpversion] -current_version = "0.15.2" +current_version = "0.15.3" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(?:-(?Prc)(?P0|[1-9]\\d*))?" diff --git a/pyproject.toml b/pyproject.toml index c33fe56..7ac37b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "otf-api" -version = "0.15.2" +version = "0.15.3" description = "Python OrangeTheory Fitness API Client" authors = [{ name = "Jessica Smith", email = "j.smith.git1@gmail.com" }] requires-python = ">=3.11" diff --git a/source/conf.py b/source/conf.py index c07eb49..49aff39 100644 --- a/source/conf.py +++ b/source/conf.py @@ -14,7 +14,7 @@ project = "OrangeTheory API" copyright = "2025, Jessica Smith" author = "Jessica Smith" -release = "0.15.2" +release = "0.15.3" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/src/otf_api/__init__.py b/src/otf_api/__init__.py index 8a076b3..cfa784f 100644 --- a/src/otf_api/__init__.py +++ b/src/otf_api/__init__.py @@ -47,7 +47,7 @@ def _setup_logging() -> None: _setup_logging() -__version__ = "0.15.2" +__version__ = "0.15.3" __all__ = ["Otf", "OtfUser", "models"] diff --git a/src/otf_api/api/workouts/workout_api.py b/src/otf_api/api/workouts/workout_api.py index e63159e..b3a7796 100644 --- a/src/otf_api/api/workouts/workout_api.py +++ b/src/otf_api/api/workouts/workout_api.py @@ -262,71 +262,31 @@ def get_workouts( bookings = self.otf.bookings.get_bookings_new( start_dtme, end_dtme, exclude_cancelled=True, remove_duplicates=True ) - bookings_dict = self._filter_bookings_for_workouts(bookings) + filtered_bookings = [b for b in bookings if not (b.starts_at and b.starts_at > pendulum.now().naive())] + bookings_list = [(b, b.workout.id if b.workout else None) for b in filtered_bookings] - perf_summaries_dict = self.client.get_perf_summaries_threaded(list(bookings_dict.keys())) + workout_ids = [b.workout.id for b in filtered_bookings if b.workout] + perf_summaries_dict = self.client.get_perf_summaries_threaded(workout_ids) telemetry_dict = self.client.get_telemetry_threaded(list(perf_summaries_dict.keys()), max_data_points) perf_summary_to_class_uuid_map = self.client.get_perf_summary_to_class_uuid_mapping() workouts: list[models.Workout] = [] - for perf_id, perf_summary in perf_summaries_dict.items(): + for booking, perf_summary_id in bookings_list: try: + perf_summary = perf_summaries_dict.get(perf_summary_id, {}) if perf_summary_id else {} + telemetry = telemetry_dict.get(perf_summary_id, None) if perf_summary_id else None + class_uuid = perf_summary_to_class_uuid_map.get(perf_summary_id, None) if perf_summary_id else None workout = models.Workout.create( - **perf_summary, - v2_booking=bookings_dict[perf_id], - telemetry=telemetry_dict.get(perf_id), - class_uuid=perf_summary_to_class_uuid_map.get(perf_id), - api=self.otf, + **perf_summary, v2_booking=booking, telemetry=telemetry, class_uuid=class_uuid, api=self.otf ) workouts.append(workout) except ValueError: - LOGGER.exception("Failed to create Workout for performance summary %s", perf_id) + LOGGER.exception("Failed to create Workout for performance summary %s", perf_summary_id) LOGGER.debug("Returning %d workouts", len(workouts)) return workouts - def _filter_bookings_for_workouts(self, bookings: list[models.BookingV2]) -> dict[str, models.BookingV2]: - """Filter bookings to only those that have a workout and are not in the future. - - This is being pulled out of `get_workouts` to add more robust logging and error handling. - - Args: - bookings (list[BookingV2]): The list of bookings to filter. - - Returns: - dict[str, BookingV2]: A dictionary mapping workout IDs to bookings that have workouts. - """ - future_bookings = [b for b in bookings if b.starts_at and b.starts_at > pendulum.now().naive()] - missing_workouts = [b for b in bookings if not b.workout and b not in future_bookings] - LOGGER.debug("Found %d future bookings and %d missing workouts", len(future_bookings), len(missing_workouts)) - - if future_bookings: - for booking in future_bookings: - LOGGER.warning( - "Booking %s for class '%s' (class_uuid=%s) is in the future, filtering out.", - booking.booking_id, - booking.otf_class, - booking.class_uuid or "Unknown", - ) - - if missing_workouts: - for booking in missing_workouts: - LOGGER.warning( - "Booking %s for class '%s' (class_uuid=%s) is missing a workout, filtering out.", - booking.booking_id, - booking.otf_class, - booking.class_uuid or "Unknown", - ) - - bookings_dict = { - b.workout.id: b for b in bookings if b.workout and b not in future_bookings and b not in missing_workouts - } - - LOGGER.debug("Filtered bookings to %d valid bookings for workouts mapping", len(bookings_dict)) - - return bookings_dict - def get_lifetime_workouts(self) -> list[models.Workout]: """Get the member's lifetime workouts. diff --git a/src/otf_api/models/workouts/workout.py b/src/otf_api/models/workouts/workout.py index 00a4bc4..1d593f7 100644 --- a/src/otf_api/models/workouts/workout.py +++ b/src/otf_api/models/workouts/workout.py @@ -3,7 +3,7 @@ from pydantic import AliasPath, Field from otf_api.models.base import OtfItemBase -from otf_api.models.bookings import BookingV2, BookingV2Class, BookingV2Studio, BookingV2Workout, Rating +from otf_api.models.bookings import BookingV2, BookingV2Class, BookingV2Studio, Rating from otf_api.models.mixins import ApiMixin from otf_api.models.workouts import HeartRate, Rower, Telemetry, Treadmill, ZoneTimeMinutes @@ -18,9 +18,11 @@ class Workout(ApiMixin, OtfItemBase): """ performance_summary_id: str = Field( - ..., validation_alias="id", description="Unique identifier for this performance summary" + default="unknown", validation_alias="id", description="Unique identifier for this performance summary" + ) + class_history_uuid: str = Field( + default="unknown", validation_alias="id", description="Same as performance_summary_id" ) - class_history_uuid: str = Field(..., validation_alias="id", description="Same as performance_summary_id") booking_id: str = Field(..., description="The booking id for the new bookings endpoint.") class_uuid: str | None = Field( None, description="Used by the ratings endpoint - seems to fall off after a few months" @@ -56,7 +58,6 @@ def __init__(self, **data): otf_class = v2_booking.otf_class v2_workout = v2_booking.workout assert isinstance(otf_class, BookingV2Class), "otf_class must be an instance of BookingV2Class" - assert isinstance(v2_workout, BookingV2Workout), "v2_workout must be an instance of BookingV2Workout" data["otf_class"] = otf_class data["studio"] = otf_class.studio @@ -64,10 +65,12 @@ def __init__(self, **data): data["ratable"] = v2_booking.ratable # this seems to be more accurate data["booking_id"] = v2_booking.booking_id - data["active_time_seconds"] = v2_workout.active_time_seconds data["class_rating"] = v2_booking.class_rating data["coach_rating"] = v2_booking.coach_rating + if v2_workout: + data["active_time_seconds"] = v2_workout.active_time_seconds + telemetry: dict[str, Any] | None = data.get("telemetry") if telemetry and "maxHr" in telemetry: # max_hr seems to be left out of the heart rate data - it has peak_hr but they do not match diff --git a/uv.lock b/uv.lock index 33e1e28..c2f69b6 100644 --- a/uv.lock +++ b/uv.lock @@ -893,7 +893,7 @@ wheels = [ [[package]] name = "otf-api" -version = "0.15.2" +version = "0.15.3" source = { editable = "." } dependencies = [ { name = "attrs" },