From 03fa64068343759806619da7c14213ca7165ed12 Mon Sep 17 00:00:00 2001 From: Jessica Smith <12jessicasmith34@gmail.com> Date: Tue, 5 Aug 2025 18:42:17 -0500 Subject: [PATCH 1/6] bump some pre-commit versions --- .pre-commit-config.yaml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9563862b..0138b7a3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,19 +10,13 @@ repos: - id: check-yaml args: [--unsafe] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.2 + rev: v0.12.7 hooks: - - id: ruff - language_version: python3 + - id: ruff-check args: [--fix, --exit-non-zero-on-fix, --show-fixes] - id: ruff-format - repo: https://github.com/codespell-project/codespell - rev: v2.3.0 + rev: v2.4.1 hooks: - id: codespell args: [--config, .codespellrc] - # - repo: https://github.com/pre-commit/mirrors-mypy - # rev: v1.10.0 - # hooks: - # - id: mypy - # exclude: ^(tests|examples)/ From c4b9c0fd03a6fe30cde695f4fdbe5af729cbd04e Mon Sep 17 00:00:00 2001 From: Jessica Smith <12jessicasmith34@gmail.com> Date: Tue, 5 Aug 2025 18:43:03 -0500 Subject: [PATCH 2/6] add get_sort_key method that returns a tuple for sorting, to replace using plain updated_at --- src/otf_api/models/bookings/bookings_v2.py | 25 ++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/otf_api/models/bookings/bookings_v2.py b/src/otf_api/models/bookings/bookings_v2.py index faf6908e..1dd4dd8d 100644 --- a/src/otf_api/models/bookings/bookings_v2.py +++ b/src/otf_api/models/bookings/bookings_v2.py @@ -155,13 +155,16 @@ class BookingV2(ApiMixin, OtfItemBase): person_id: str created_at: datetime | None = Field( - None, - description="Date the booking was created in the system, not when the booking was made", + default=None, + description="Date the booking was created in the system", exclude=True, repr=False, ) - updated_at: datetime = Field( - description="Date the booking was updated, not when the booking was made", exclude=True, repr=False + updated_at: datetime | None = Field( + default=None, + description="Date the booking was updated in the system", + exclude=True, + repr=False, ) @property @@ -230,3 +233,17 @@ def cancel(self) -> None: self.raise_if_api_not_set() self._api.bookings.cancel_booking_new(self) + + def get_sort_key(self) -> tuple: + """Returns a tuple for sorting bookings, used when attempting to remove duplicates.""" + # Use negative timestamps to favor later ones (a more recent updated_at is better) + updated_at = self.updated_at or datetime.min # noqa DTZ901 + created_at = self.created_at or datetime.min # noqa DTZ901 + + status_priority = self.status.priority() + + return (self.starts_at, -_safe_ts(updated_at), -_safe_ts(created_at), status_priority) + + +def _safe_ts(dt: datetime | None) -> float: + return dt.timestamp() if (isinstance(dt, datetime) and dt != datetime.min) else float("inf") # noqa DTZ901 From 2fdbd976053a7d9a0042a3b41ae09c53f21e9e79 Mon Sep 17 00:00:00 2001 From: Jessica Smith <12jessicasmith34@gmail.com> Date: Tue, 5 Aug 2025 18:43:08 -0500 Subject: [PATCH 3/6] allow no-booking-id, improve how we handle sorting --- src/otf_api/api/bookings/booking_api.py | 50 +++++++++---------------- 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/src/otf_api/api/bookings/booking_api.py b/src/otf_api/api/bookings/booking_api.py index 39836516..83c63244 100644 --- a/src/otf_api/api/bookings/booking_api.py +++ b/src/otf_api/api/bookings/booking_api.py @@ -1,4 +1,5 @@ import typing +from collections import defaultdict from datetime import date, datetime, time, timedelta from logging import getLogger from typing import Literal @@ -114,62 +115,45 @@ def get_bookings_new( ) LOGGER.debug("Found %d bookings between %s and %s", len(bookings_resp), start_date, end_date) - # filter out bookings with ids that start with "no-booking-id" - # no idea what these are, but I am praying for the poor sap stuck with maintaining OTF's data model results: list[models.BookingV2] = [] - for b in bookings_resp: - if not b.get("id", "").startswith("no-booking-id"): - try: - results.append(models.BookingV2.create(**b, api=self.otf)) - except ValueError as e: - LOGGER.error("Failed to create BookingV2 from response: %s. Booking data:\n%s", e, b) - continue + try: + results.append(models.BookingV2.create(**b, api=self.otf)) + except Exception as e: + LOGGER.error( + "Failed to create BookingV2 from response: %s - %s. Booking data:\n%s", type(e).__name__, e, b + ) + continue if not remove_duplicates: return results - results = self._deduplicate_bookings(results, exclude_cancelled=exclude_cancelled) + results = self._deduplicate_bookings(results) return results - def _deduplicate_bookings( - self, results: list[models.BookingV2], exclude_cancelled: bool = True - ) -> list[models.BookingV2]: + def _deduplicate_bookings(self, results: list[models.BookingV2]) -> list[models.BookingV2]: """Deduplicate bookings by class_id, keeping the most recent booking. Args: results (list[BookingV2]): The list of bookings to deduplicate. - exclude_cancelled (bool): If True, will not include cancelled bookings in the results. Returns: list[BookingV2]: The deduplicated list of bookings. """ - # remove duplicates by class_id, keeping the one with the most recent updated_at timestamp - orig_count = len(results) - seen_classes: dict[str, models.BookingV2] = {} + classes_by_id: defaultdict[str, list[models.BookingV2]] = defaultdict(list) + keep_classes: list[models.BookingV2] = [] for booking in results: - class_id = booking.otf_class.class_id - if class_id not in seen_classes: - seen_classes[class_id] = booking - continue + classes_by_id[booking.otf_class.class_id].append(booking) - existing_booking = seen_classes[class_id] - if exclude_cancelled: - LOGGER.warning( - f"Duplicate class_id {class_id} found when `exclude_cancelled` is True, " - "this is unexpected behavior." - ) - if booking.updated_at > existing_booking.updated_at: - LOGGER.debug( - "Replacing existing booking for class_id %s with more recent booking %s", class_id, booking - ) - seen_classes[class_id] = booking + for bookings in classes_by_id.values(): + top_booking = min(bookings, key=lambda b: b.get_sort_key()) + keep_classes.append(top_booking) - results = list(seen_classes.values()) + results = list(keep_classes) results = sorted(results, key=lambda x: x.starts_at) new_count = len(results) From 42c05a3d28de9c672baac8d4109199e27705241f Mon Sep 17 00:00:00 2001 From: Jessica Smith <12jessicasmith34@gmail.com> Date: Tue, 5 Aug 2025 18:43:18 -0500 Subject: [PATCH 4/6] add priority for booking status --- src/otf_api/models/bookings/enums.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/otf_api/models/bookings/enums.py b/src/otf_api/models/bookings/enums.py index de862747..5bc99247 100644 --- a/src/otf_api/models/bookings/enums.py +++ b/src/otf_api/models/bookings/enums.py @@ -17,6 +17,25 @@ class BookingStatus(StrEnum): CancelCheckinPending = "Cancel Checkin Pending" CancelCheckinRequested = "Cancel Checkin Requested" + def priority(self) -> int: + """Returns the priority of the booking status for sorting purposes.""" + priorities = { + BookingStatus.Booked: 0, + BookingStatus.Confirmed: 1, + BookingStatus.Waitlisted: 2, + BookingStatus.Pending: 3, + BookingStatus.Requested: 4, + BookingStatus.CheckedIn: 5, + BookingStatus.CheckinPending: 6, + BookingStatus.CheckinRequested: 7, + BookingStatus.CheckinCancelled: 8, + BookingStatus.Cancelled: 9, + BookingStatus.LateCancelled: 10, + BookingStatus.CancelCheckinPending: 11, + BookingStatus.CancelCheckinRequested: 12, + } + return priorities.get(self, 999) + HISTORICAL_BOOKING_STATUSES = [ BookingStatus.CheckedIn, From d0f3711e1a4b90457423b75e11afdcc8fbaf78d5 Mon Sep 17 00:00:00 2001 From: Jessica Smith <12jessicasmith34@gmail.com> Date: Tue, 5 Aug 2025 18:43:35 -0500 Subject: [PATCH 5/6] bump patch version --- .bumpversion.toml | 2 +- pyproject.toml | 2 +- source/conf.py | 2 +- src/otf_api/__init__.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index 82691b49..ded0f196 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,5 +1,5 @@ [tool.bumpversion] -current_version = "0.15.3" +current_version = "0.15.4" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(?:-(?Prc)(?P0|[1-9]\\d*))?" diff --git a/pyproject.toml b/pyproject.toml index 7ac37b3e..2be5bbc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "otf-api" -version = "0.15.3" +version = "0.15.4" 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 49aff390..b1086aef 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.3" +release = "0.15.4" # -- 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 cfa784fe..a497bc11 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.3" +__version__ = "0.15.4" __all__ = ["Otf", "OtfUser", "models"] From eacb7417db35bcdadaeb1ccd2bb2f8cdf9acda21 Mon Sep 17 00:00:00 2001 From: Jessica Smith <12jessicasmith34@gmail.com> Date: Tue, 5 Aug 2025 18:53:30 -0500 Subject: [PATCH 6/6] bump uv.lock --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index c2f69b6c..4aa0f211 100644 --- a/uv.lock +++ b/uv.lock @@ -893,7 +893,7 @@ wheels = [ [[package]] name = "otf-api" -version = "0.15.3" +version = "0.15.4" source = { editable = "." } dependencies = [ { name = "attrs" },