Skip to content

Commit 4bd7010

Browse files
authored
Merge pull request #105 from NodeJSmith/fix/improve_sorting
Fix/improve sorting
2 parents aab9c4b + eacb741 commit 4bd7010

File tree

9 files changed

+65
-51
lines changed

9 files changed

+65
-51
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.3"
2+
current_version = "0.15.4"
33

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

.pre-commit-config.yaml

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,13 @@ repos:
1010
- id: check-yaml
1111
args: [--unsafe]
1212
- repo: https://github.com/astral-sh/ruff-pre-commit
13-
rev: v0.9.2
13+
rev: v0.12.7
1414
hooks:
15-
- id: ruff
16-
language_version: python3
15+
- id: ruff-check
1716
args: [--fix, --exit-non-zero-on-fix, --show-fixes]
1817
- id: ruff-format
1918
- repo: https://github.com/codespell-project/codespell
20-
rev: v2.3.0
19+
rev: v2.4.1
2120
hooks:
2221
- id: codespell
2322
args: [--config, .codespellrc]
24-
# - repo: https://github.com/pre-commit/mirrors-mypy
25-
# rev: v1.10.0
26-
# hooks:
27-
# - id: mypy
28-
# exclude: ^(tests|examples)/

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.3"
3+
version = "0.15.4"
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.3"
17+
release = "0.15.4"
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.3"
50+
__version__ = "0.15.4"
5151

5252

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

src/otf_api/api/bookings/booking_api.py

Lines changed: 17 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import typing
2+
from collections import defaultdict
23
from datetime import date, datetime, time, timedelta
34
from logging import getLogger
45
from typing import Literal
@@ -114,62 +115,45 @@ def get_bookings_new(
114115
)
115116
LOGGER.debug("Found %d bookings between %s and %s", len(bookings_resp), start_date, end_date)
116117

117-
# filter out bookings with ids that start with "no-booking-id"
118-
# no idea what these are, but I am praying for the poor sap stuck with maintaining OTF's data model
119118
results: list[models.BookingV2] = []
120-
121119
for b in bookings_resp:
122-
if not b.get("id", "").startswith("no-booking-id"):
123-
try:
124-
results.append(models.BookingV2.create(**b, api=self.otf))
125-
except ValueError as e:
126-
LOGGER.error("Failed to create BookingV2 from response: %s. Booking data:\n%s", e, b)
127-
continue
120+
try:
121+
results.append(models.BookingV2.create(**b, api=self.otf))
122+
except Exception as e:
123+
LOGGER.error(
124+
"Failed to create BookingV2 from response: %s - %s. Booking data:\n%s", type(e).__name__, e, b
125+
)
126+
continue
128127

129128
if not remove_duplicates:
130129
return results
131130

132-
results = self._deduplicate_bookings(results, exclude_cancelled=exclude_cancelled)
131+
results = self._deduplicate_bookings(results)
133132

134133
return results
135134

136-
def _deduplicate_bookings(
137-
self, results: list[models.BookingV2], exclude_cancelled: bool = True
138-
) -> list[models.BookingV2]:
135+
def _deduplicate_bookings(self, results: list[models.BookingV2]) -> list[models.BookingV2]:
139136
"""Deduplicate bookings by class_id, keeping the most recent booking.
140137
141138
Args:
142139
results (list[BookingV2]): The list of bookings to deduplicate.
143-
exclude_cancelled (bool): If True, will not include cancelled bookings in the results.
144140
145141
Returns:
146142
list[BookingV2]: The deduplicated list of bookings.
147143
"""
148-
# remove duplicates by class_id, keeping the one with the most recent updated_at timestamp
149-
150144
orig_count = len(results)
151145

152-
seen_classes: dict[str, models.BookingV2] = {}
146+
classes_by_id: defaultdict[str, list[models.BookingV2]] = defaultdict(list)
147+
keep_classes: list[models.BookingV2] = []
153148

154149
for booking in results:
155-
class_id = booking.otf_class.class_id
156-
if class_id not in seen_classes:
157-
seen_classes[class_id] = booking
158-
continue
150+
classes_by_id[booking.otf_class.class_id].append(booking)
159151

160-
existing_booking = seen_classes[class_id]
161-
if exclude_cancelled:
162-
LOGGER.warning(
163-
f"Duplicate class_id {class_id} found when `exclude_cancelled` is True, "
164-
"this is unexpected behavior."
165-
)
166-
if booking.updated_at > existing_booking.updated_at:
167-
LOGGER.debug(
168-
"Replacing existing booking for class_id %s with more recent booking %s", class_id, booking
169-
)
170-
seen_classes[class_id] = booking
152+
for bookings in classes_by_id.values():
153+
top_booking = min(bookings, key=lambda b: b.get_sort_key())
154+
keep_classes.append(top_booking)
171155

172-
results = list(seen_classes.values())
156+
results = list(keep_classes)
173157
results = sorted(results, key=lambda x: x.starts_at)
174158

175159
new_count = len(results)

src/otf_api/models/bookings/bookings_v2.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,13 +155,16 @@ class BookingV2(ApiMixin, OtfItemBase):
155155
person_id: str
156156

157157
created_at: datetime | None = Field(
158-
None,
159-
description="Date the booking was created in the system, not when the booking was made",
158+
default=None,
159+
description="Date the booking was created in the system",
160160
exclude=True,
161161
repr=False,
162162
)
163-
updated_at: datetime = Field(
164-
description="Date the booking was updated, not when the booking was made", exclude=True, repr=False
163+
updated_at: datetime | None = Field(
164+
default=None,
165+
description="Date the booking was updated in the system",
166+
exclude=True,
167+
repr=False,
165168
)
166169

167170
@property
@@ -230,3 +233,17 @@ def cancel(self) -> None:
230233
self.raise_if_api_not_set()
231234

232235
self._api.bookings.cancel_booking_new(self)
236+
237+
def get_sort_key(self) -> tuple:
238+
"""Returns a tuple for sorting bookings, used when attempting to remove duplicates."""
239+
# Use negative timestamps to favor later ones (a more recent updated_at is better)
240+
updated_at = self.updated_at or datetime.min # noqa DTZ901
241+
created_at = self.created_at or datetime.min # noqa DTZ901
242+
243+
status_priority = self.status.priority()
244+
245+
return (self.starts_at, -_safe_ts(updated_at), -_safe_ts(created_at), status_priority)
246+
247+
248+
def _safe_ts(dt: datetime | None) -> float:
249+
return dt.timestamp() if (isinstance(dt, datetime) and dt != datetime.min) else float("inf") # noqa DTZ901

src/otf_api/models/bookings/enums.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,25 @@ class BookingStatus(StrEnum):
1717
CancelCheckinPending = "Cancel Checkin Pending"
1818
CancelCheckinRequested = "Cancel Checkin Requested"
1919

20+
def priority(self) -> int:
21+
"""Returns the priority of the booking status for sorting purposes."""
22+
priorities = {
23+
BookingStatus.Booked: 0,
24+
BookingStatus.Confirmed: 1,
25+
BookingStatus.Waitlisted: 2,
26+
BookingStatus.Pending: 3,
27+
BookingStatus.Requested: 4,
28+
BookingStatus.CheckedIn: 5,
29+
BookingStatus.CheckinPending: 6,
30+
BookingStatus.CheckinRequested: 7,
31+
BookingStatus.CheckinCancelled: 8,
32+
BookingStatus.Cancelled: 9,
33+
BookingStatus.LateCancelled: 10,
34+
BookingStatus.CancelCheckinPending: 11,
35+
BookingStatus.CancelCheckinRequested: 12,
36+
}
37+
return priorities.get(self, 999)
38+
2039

2140
HISTORICAL_BOOKING_STATUSES = [
2241
BookingStatus.CheckedIn,

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)