otf-api 0.12.0__py3-none-any.whl → 0.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. otf_api/__init__.py +35 -3
  2. otf_api/api/__init__.py +3 -0
  3. otf_api/api/_compat.py +77 -0
  4. otf_api/api/api.py +80 -0
  5. otf_api/api/bookings/__init__.py +3 -0
  6. otf_api/api/bookings/booking_api.py +541 -0
  7. otf_api/api/bookings/booking_client.py +112 -0
  8. otf_api/api/client.py +203 -0
  9. otf_api/api/members/__init__.py +3 -0
  10. otf_api/api/members/member_api.py +187 -0
  11. otf_api/api/members/member_client.py +112 -0
  12. otf_api/api/studios/__init__.py +3 -0
  13. otf_api/api/studios/studio_api.py +173 -0
  14. otf_api/api/studios/studio_client.py +120 -0
  15. otf_api/api/utils.py +307 -0
  16. otf_api/api/workouts/__init__.py +3 -0
  17. otf_api/api/workouts/workout_api.py +333 -0
  18. otf_api/api/workouts/workout_client.py +140 -0
  19. otf_api/auth/__init__.py +1 -1
  20. otf_api/auth/auth.py +155 -89
  21. otf_api/auth/user.py +5 -17
  22. otf_api/auth/utils.py +27 -2
  23. otf_api/cache.py +132 -0
  24. otf_api/exceptions.py +18 -6
  25. otf_api/models/__init__.py +25 -21
  26. otf_api/models/bookings/__init__.py +23 -0
  27. otf_api/models/bookings/bookings.py +134 -0
  28. otf_api/models/{bookings_v2.py → bookings/bookings_v2.py} +72 -31
  29. otf_api/models/bookings/classes.py +124 -0
  30. otf_api/models/{enums.py → bookings/enums.py} +7 -81
  31. otf_api/{filters.py → models/bookings/filters.py} +39 -11
  32. otf_api/models/{ratings.py → bookings/ratings.py} +2 -6
  33. otf_api/models/members/__init__.py +5 -0
  34. otf_api/models/members/member_detail.py +149 -0
  35. otf_api/models/members/member_membership.py +26 -0
  36. otf_api/models/members/member_purchases.py +29 -0
  37. otf_api/models/members/notifications.py +17 -0
  38. otf_api/models/mixins.py +48 -1
  39. otf_api/models/studios/__init__.py +5 -0
  40. otf_api/models/studios/enums.py +11 -0
  41. otf_api/models/studios/studio_detail.py +93 -0
  42. otf_api/models/studios/studio_services.py +36 -0
  43. otf_api/models/workouts/__init__.py +31 -0
  44. otf_api/models/{body_composition_list.py → workouts/body_composition_list.py} +140 -71
  45. otf_api/models/workouts/challenge_tracker_content.py +50 -0
  46. otf_api/models/workouts/challenge_tracker_detail.py +99 -0
  47. otf_api/models/workouts/enums.py +70 -0
  48. otf_api/models/workouts/lifetime_stats.py +96 -0
  49. otf_api/models/workouts/out_of_studio_workout_history.py +32 -0
  50. otf_api/models/{performance_summary.py → workouts/performance_summary.py} +19 -5
  51. otf_api/models/workouts/telemetry.py +88 -0
  52. otf_api/models/{workout.py → workouts/workout.py} +34 -20
  53. {otf_api-0.12.0.dist-info → otf_api-0.13.0.dist-info}/METADATA +4 -2
  54. otf_api-0.13.0.dist-info/RECORD +59 -0
  55. {otf_api-0.12.0.dist-info → otf_api-0.13.0.dist-info}/WHEEL +1 -1
  56. otf_api/api.py +0 -1682
  57. otf_api/logging.py +0 -19
  58. otf_api/models/bookings.py +0 -109
  59. otf_api/models/challenge_tracker_content.py +0 -59
  60. otf_api/models/challenge_tracker_detail.py +0 -88
  61. otf_api/models/classes.py +0 -70
  62. otf_api/models/lifetime_stats.py +0 -78
  63. otf_api/models/member_detail.py +0 -121
  64. otf_api/models/member_membership.py +0 -26
  65. otf_api/models/member_purchases.py +0 -29
  66. otf_api/models/notifications.py +0 -17
  67. otf_api/models/out_of_studio_workout_history.py +0 -32
  68. otf_api/models/studio_detail.py +0 -71
  69. otf_api/models/studio_services.py +0 -36
  70. otf_api/models/telemetry.py +0 -84
  71. otf_api/utils.py +0 -164
  72. otf_api-0.12.0.dist-info/RECORD +0 -38
  73. {otf_api-0.12.0.dist-info → otf_api-0.13.0.dist-info}/licenses/LICENSE +0 -0
  74. {otf_api-0.12.0.dist-info → otf_api-0.13.0.dist-info}/top_level.txt +0 -0
@@ -1,71 +0,0 @@
1
- from datetime import datetime
2
-
3
- from pydantic import AliasChoices, AliasPath, Field
4
-
5
- from otf_api.models.base import OtfItemBase
6
- from otf_api.models.enums import StudioStatus
7
- from otf_api.models.mixins import AddressMixin
8
-
9
-
10
- class StudioLocation(AddressMixin, OtfItemBase):
11
- phone_number: str | None = Field(None, validation_alias=AliasChoices("phone", "phoneNumber"))
12
- latitude: float | None = Field(None, validation_alias=AliasChoices("latitude"))
13
- longitude: float | None = Field(None, validation_alias=AliasChoices("longitude"))
14
-
15
- physical_region: str | None = Field(None, alias="physicalRegion", exclude=True, repr=False)
16
- physical_country_id: int | None = Field(None, alias="physicalCountryId", exclude=True, repr=False)
17
-
18
-
19
- class StudioDetail(OtfItemBase):
20
- studio_uuid: str = Field(..., alias="studioUUId", description="The OTF studio UUID")
21
-
22
- contact_email: str | None = Field(None, alias="contactEmail")
23
- distance: float | None = Field(
24
- None,
25
- description="Distance from latitude and longitude provided to `search_studios_by_geo` method,\
26
- NULL if that method was not used",
27
- exclude=True,
28
- repr=False,
29
- )
30
- location: StudioLocation = Field(..., alias="studioLocation", default_factory=StudioLocation) # type: ignore
31
- name: str | None = Field(None, alias="studioName")
32
- status: StudioStatus | None = Field(
33
- None, alias="studioStatus", description="Active, Temporarily Closed, Coming Soon"
34
- )
35
- time_zone: str | None = Field(None, alias="timeZone")
36
-
37
- # flags
38
- accepts_ach: bool | None = Field(None, alias="acceptsAch", exclude=True, repr=False)
39
- accepts_american_express: bool | None = Field(None, alias="acceptsAmericanExpress", exclude=True, repr=False)
40
- accepts_discover: bool | None = Field(None, alias="acceptsDiscover", exclude=True, repr=False)
41
- accepts_visa_master_card: bool | None = Field(None, alias="acceptsVisaMasterCard", exclude=True, repr=False)
42
- allows_cr_waitlist: bool | None = Field(None, alias="allowsCrWaitlist", exclude=True, repr=False)
43
- allows_dashboard_access: bool | None = Field(None, alias="allowsDashboardAccess", exclude=True, repr=False)
44
- is_crm: bool | None = Field(None, validation_alias=AliasPath("studioProfiles", "isCrm"), exclude=True, repr=False)
45
- is_integrated: bool | None = Field(
46
- None, alias="isIntegrated", exclude=True, repr=False, description="Always 'True'"
47
- )
48
- is_mobile: bool | None = Field(None, alias="isMobile", exclude=True, repr=False)
49
- is_otbeat: bool | None = Field(None, alias="isOtbeat", exclude=True, repr=False)
50
- is_web: bool | None = Field(None, validation_alias=AliasPath("studioProfiles", "isWeb"), exclude=True, repr=False)
51
- sms_package_enabled: bool | None = Field(None, alias="smsPackageEnabled", exclude=True, repr=False)
52
-
53
- # misc
54
- studio_id: int | None = Field(None, alias="studioId", description="Not used by API", exclude=True, repr=False)
55
- mbo_studio_id: int | None = Field(None, alias="mboStudioId", exclude=True, repr=False, description="MindBody attr")
56
- open_date: datetime | None = Field(None, alias="openDate", exclude=True, repr=False)
57
- pricing_level: str | None = Field(
58
- None, alias="pricingLevel", exclude=True, repr=False, description="Pro, Legacy, Accelerate, or empty"
59
- )
60
- re_open_date: datetime | None = Field(None, alias="reOpenDate", exclude=True, repr=False)
61
- studio_number: str | None = Field(None, alias="studioNumber", exclude=True, repr=False)
62
- studio_physical_location_id: int | None = Field(None, alias="studioPhysicalLocationId", exclude=True, repr=False)
63
- studio_token: str | None = Field(None, alias="studioToken", exclude=True, repr=False)
64
- studio_type_id: int | None = Field(None, alias="studioTypeId", exclude=True, repr=False)
65
-
66
- @classmethod
67
- def create_empty_model(cls, studio_uuid: str) -> "StudioDetail":
68
- """Create an empty model with the given studio_uuid."""
69
-
70
- # pylance doesn't know that the rest of the fields default to None, so we use type: ignore
71
- return StudioDetail(studioUUId=studio_uuid, studioName="Studio Not Found", studioStatus="Unknown") # type: ignore
@@ -1,36 +0,0 @@
1
- from datetime import datetime
2
-
3
- from pydantic import Field
4
-
5
- from otf_api.models.base import OtfItemBase
6
- from otf_api.models.studio_detail import StudioDetail
7
-
8
-
9
- class StudioService(OtfItemBase):
10
- studio: StudioDetail = Field(..., exclude=True, repr=False)
11
- service_uuid: str = Field(..., alias="serviceUUId")
12
- name: str | None = None
13
- price: str | None = None
14
- qty: int | None = None
15
- online_price: str | None = Field(None, alias="onlinePrice")
16
- tax_rate: str | None = Field(None, alias="taxRate")
17
- current: bool | None = None
18
- is_deleted: bool | None = Field(None, alias="isDeleted")
19
- created_date: datetime | None = Field(None, alias="createdDate")
20
- updated_date: datetime | None = Field(None, alias="updatedDate")
21
-
22
- # unused fields
23
-
24
- # ids
25
- mbo_program_id: int | None = Field(None, alias="mboProgramId", exclude=True, repr=False)
26
- mbo_description_id: str | None = Field(None, alias="mboDescriptionId", exclude=True, repr=False)
27
- mbo_product_id: int | None = Field(None, alias="mboProductId", exclude=True, repr=False)
28
- service_id: int | None = Field(None, alias="serviceId", exclude=True, repr=False)
29
- studio_id: int | None = Field(None, alias="studioId", exclude=True, repr=False)
30
- created_by: str | None = Field(None, alias="createdBy", exclude=True, repr=False)
31
- updated_by: str | None = Field(None, alias="updatedBy", exclude=True, repr=False)
32
-
33
- # flags
34
- is_web: bool | None = Field(None, alias="isWeb", exclude=True, repr=False)
35
- is_crm: bool | None = Field(None, alias="isCrm", exclude=True, repr=False)
36
- is_mobile: bool | None = Field(None, alias="isMobile", exclude=True, repr=False)
@@ -1,84 +0,0 @@
1
- from datetime import datetime, timedelta
2
- from typing import Any
3
-
4
- from pydantic import AliasPath, Field, field_serializer
5
-
6
- from otf_api.models.base import OtfItemBase
7
-
8
-
9
- class Zone(OtfItemBase):
10
- start_bpm: int = Field(..., alias="startBpm")
11
- end_bpm: int = Field(..., alias="endBpm")
12
-
13
-
14
- class Zones(OtfItemBase):
15
- gray: Zone
16
- blue: Zone
17
- green: Zone
18
- orange: Zone
19
- red: Zone
20
-
21
-
22
- class TreadData(OtfItemBase):
23
- tread_speed: float = Field(..., alias="treadSpeed")
24
- tread_incline: float = Field(..., alias="treadIncline")
25
- agg_tread_distance: int = Field(..., alias="aggTreadDistance")
26
-
27
-
28
- class RowData(OtfItemBase):
29
- row_speed: float = Field(..., alias="rowSpeed")
30
- row_pps: float = Field(..., alias="rowPps")
31
- row_spm: float = Field(..., alias="rowSpm")
32
- agg_row_distance: int = Field(..., alias="aggRowDistance")
33
- row_pace: int = Field(..., alias="rowPace")
34
-
35
-
36
- class TelemetryItem(OtfItemBase):
37
- relative_timestamp: int = Field(..., alias="relativeTimestamp")
38
- hr: int | None = None
39
- agg_splats: int = Field(..., alias="aggSplats")
40
- agg_calories: int = Field(..., alias="aggCalories")
41
- timestamp: datetime | None = Field(
42
- None,
43
- init_var=False,
44
- description="The timestamp of the telemetry item, calculated from the class start time and relative timestamp.",
45
- )
46
- tread_data: TreadData | None = Field(None, alias="treadData")
47
- row_data: RowData | None = Field(None, alias="rowData")
48
-
49
-
50
- class Telemetry(OtfItemBase):
51
- member_uuid: str = Field(..., alias="memberUuid")
52
- performance_summary_id: str = Field(
53
- ..., alias="classHistoryUuid", description="The ID of the performance summary this telemetry item belongs to."
54
- )
55
- class_history_uuid: str = Field(..., alias="classHistoryUuid", description="The same as performance_summary_id.")
56
- class_start_time: datetime | None = Field(None, alias="classStartTime")
57
- max_hr: int | None = Field(None, alias="maxHr")
58
- zones: Zones
59
- window_size: int | None = Field(None, alias="windowSize")
60
- telemetry: list[TelemetryItem] = Field(default_factory=list)
61
-
62
- def __init__(self, **data: dict[str, Any]):
63
- super().__init__(**data)
64
- for telem in self.telemetry:
65
- if self.class_start_time is None:
66
- continue
67
-
68
- telem.timestamp = self.class_start_time + timedelta(seconds=telem.relative_timestamp)
69
-
70
- @field_serializer("telemetry", when_used="json")
71
- def reduce_telemetry_list(self, value: list[TelemetryItem]) -> list[TelemetryItem]:
72
- """Reduces the telemetry list to only include the first 10 items."""
73
- if len(value) > 10:
74
- return value[:5] + value[-5:]
75
- return value
76
-
77
-
78
- class TelemetryHistoryItem(OtfItemBase):
79
- max_hr_type: str | None = Field(None, validation_alias=AliasPath("maxHr", "type"))
80
- max_hr_value: int | None = Field(None, validation_alias=AliasPath("maxHr", "value"))
81
- zones: Zones | None = None
82
- change_from_previous: int | None = Field(None, alias="changeFromPrevious")
83
- change_bucket: str | None = Field(None, alias="changeBucket")
84
- assigned_at: datetime | None = Field(None, alias="assignedAt")
otf_api/utils.py DELETED
@@ -1,164 +0,0 @@
1
- import json
2
- import typing
3
- from datetime import date, datetime, time
4
- from logging import getLogger
5
- from pathlib import Path
6
- from typing import Any
7
-
8
- import attrs
9
-
10
- if typing.TYPE_CHECKING:
11
- from otf_api import models
12
-
13
- LOGGER = getLogger(__name__)
14
-
15
- MIN_TIME = datetime.min.time()
16
-
17
-
18
- def get_booking_uuid(booking_or_uuid: "str | models.Booking") -> str:
19
- from otf_api.models.bookings import Booking
20
-
21
- if isinstance(booking_or_uuid, str):
22
- return booking_or_uuid
23
-
24
- if isinstance(booking_or_uuid, Booking):
25
- return booking_or_uuid.booking_uuid
26
-
27
- raise ValueError(f"Expected Booking or str, got {type(booking_or_uuid)}")
28
-
29
-
30
- def get_booking_id(booking_or_id: "str | models.BookingV2") -> str:
31
- from otf_api.models.bookings_v2 import BookingV2
32
-
33
- if isinstance(booking_or_id, str):
34
- return booking_or_id
35
-
36
- if isinstance(booking_or_id, BookingV2):
37
- return booking_or_id.booking_id
38
-
39
- raise ValueError(f"Expected BookingV2 or str, got {type(booking_or_id)}")
40
-
41
-
42
- def get_class_uuid(class_or_uuid: "str | models.OtfClass | models.BookingV2Class") -> str:
43
- if isinstance(class_or_uuid, str):
44
- return class_or_uuid
45
-
46
- if hasattr(class_or_uuid, "class_uuid"):
47
- class_uuid = getattr(class_or_uuid, "class_uuid", None)
48
- if class_uuid:
49
- return class_uuid
50
- raise ValueError("Class does not have a class_uuid")
51
-
52
- raise ValueError(f"Expected OtfClass, BookingV2Class, or str, got {type(class_or_uuid)}")
53
-
54
-
55
- def ensure_list(obj: list | Any | None) -> list:
56
- if obj is None:
57
- return []
58
- if not isinstance(obj, list):
59
- return [obj]
60
- return obj
61
-
62
-
63
- def ensure_datetime(date_str: str | datetime | None, combine_with: time = MIN_TIME) -> datetime | None:
64
- if not date_str:
65
- return None
66
-
67
- if isinstance(date_str, str):
68
- return datetime.fromisoformat(date_str)
69
-
70
- if isinstance(date_str, datetime):
71
- return date_str
72
-
73
- if isinstance(date_str, date):
74
- return datetime.combine(date_str, combine_with)
75
-
76
- raise ValueError(f"Expected str or datetime, got {type(date_str)}")
77
-
78
-
79
- def ensure_date(date_str: str | date | None) -> date | None:
80
- if not date_str:
81
- return None
82
-
83
- if isinstance(date_str, str):
84
- return datetime.fromisoformat(date_str).date()
85
-
86
- if isinstance(date_str, datetime):
87
- return date_str.date()
88
-
89
- return date_str
90
-
91
-
92
- @attrs.define
93
- class CacheableData:
94
- """Represents a cacheable data object, with methods to read and write to cache."""
95
-
96
- name: str
97
- cache_dir: Path
98
-
99
- def __attrs_post_init__(self):
100
- self.cache_path.parent.mkdir(parents=True, exist_ok=True)
101
-
102
- @property
103
- def cache_path(self) -> Path:
104
- """The path to the cache file."""
105
- return self.cache_dir.expanduser().joinpath(f"{self.name}_cache.json")
106
-
107
- def get_cached_data(self, keys: list[str] | None = None) -> dict[str, str]:
108
- """Reads the cache file and returns the data if it exists and is valid.
109
-
110
- Returns:
111
- dict[str, str]: The cached data, or an empty dictionary if the cache is invalid or missing.
112
- """
113
- LOGGER.debug(f"Loading {self.name} from cache ({self.cache_path})")
114
- try:
115
- if not self.cache_path.exists():
116
- return {}
117
-
118
- if self.cache_path.stat().st_size == 0:
119
- return {}
120
-
121
- data: dict[str, str] = json.loads(self.cache_path.read_text())
122
- if not keys:
123
- return data
124
-
125
- if set(data.keys()).issuperset(set(keys)):
126
- return {k: v for k, v in data.items() if k in keys}
127
- raise ValueError(f"Data must contain all keys: {keys}")
128
- except Exception:
129
- LOGGER.exception(f"Failed to read {self.cache_path.name}")
130
- return {}
131
-
132
- def write_to_cache(self, data: dict[str, str]) -> None:
133
- """Writes the data to the cache file."""
134
- LOGGER.debug(f"Writing {self.name} to cache ({self.cache_path})")
135
-
136
- # double check everything exists
137
- if not self.cache_path.parent.exists():
138
- self.cache_path.parent.mkdir(parents=True, exist_ok=True)
139
-
140
- if not self.cache_path.exists():
141
- self.cache_path.touch()
142
-
143
- existing_data = self.get_cached_data()
144
- data = {**existing_data, **data}
145
-
146
- self.cache_path.write_text(json.dumps(data, indent=4, default=str))
147
-
148
- def clear_cache(self, keys: list[str] | None = None) -> None:
149
- """Deletes the cache file if it exists."""
150
- if not self.cache_path.exists():
151
- return
152
-
153
- if not keys:
154
- self.cache_path.unlink()
155
- LOGGER.debug(f"{self.name} cache deleted")
156
- return
157
-
158
- assert isinstance(keys, list), "Keys must be a list"
159
-
160
- data = self.get_cached_data()
161
- for key in keys:
162
- data.pop(key, None)
163
-
164
- self.write_to_cache(data)
@@ -1,38 +0,0 @@
1
- otf_api/__init__.py,sha256=QbShJl50bmebZLa_dNvs9PAoexI6pHGY1d_3endsC7M,205
2
- otf_api/api.py,sha256=EtdFB-ZYQAP3b5Ghz-uNQIm_p6wr7Yf8fyJyL6iEBc0,67554
3
- otf_api/exceptions.py,sha256=GISekwF5dPt0Ol0WCU55kE5ODc5VxicNEEhmlguuE0U,1815
4
- otf_api/filters.py,sha256=fk2bFGi3srjS96qZlaDx-ARZRaj93NUTUdMJ01TX420,3702
5
- otf_api/logging.py,sha256=PRZpCaJ1F1Xya3L9Efkt3mKS5_QNr3sXjEUERSxYjvE,563
6
- otf_api/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- otf_api/utils.py,sha256=Yr_BKzQ28ARzHN5FvMkVIgZ1_3icZKaqFzs4SDv6ugw,4879
8
- otf_api/auth/__init__.py,sha256=PuhtiZ02GVP8zVSn1O-fhaYm7JM_tq5yUFATk_-8upk,132
9
- otf_api/auth/auth.py,sha256=5cPELC9Soif5knDDHm55ii1OMEPkJlGUphAdbOEmaRo,13278
10
- otf_api/auth/user.py,sha256=XlK3nbqJA4fF5UFmw2tt0eAle4QOQd6trnW72QrBsx4,2681
11
- otf_api/auth/utils.py,sha256=vc2pEyU-3yKdv0mR6r_zSHZyMw92j5UeIN_kzcb6TeE,2909
12
- otf_api/models/__init__.py,sha256=MSinaMQaBTGscL-YRKJ3axFiItQ1HoH62wC2xdaBMgk,1876
13
- otf_api/models/base.py,sha256=KJlIxl_sRj6f-g5vKYPw4yV6fGDk-fwZ93EO0JGPYMw,202
14
- otf_api/models/body_composition_list.py,sha256=jGdR-9ScvIOtULJNB99aYh2INk2ihoHAnTWtbQCIea4,12202
15
- otf_api/models/bookings.py,sha256=fnHZmN2F8fVSWS_tco3IwjO9SJbbxzuIs2WJcCC0gNs,4781
16
- otf_api/models/bookings_v2.py,sha256=PFSlORNZF45TRvgLwewbVGoiOd3oDG1YGvvFMtXtbpo,6533
17
- otf_api/models/challenge_tracker_content.py,sha256=5Ucu1n4W15v1rzhoXNvAD9tCSg3JTUiR92HHiDAxRec,2597
18
- otf_api/models/challenge_tracker_detail.py,sha256=c2Ds7Kv2-VaPtxoXSUTI5zrmU1A1dcSaM1UIolwSVxU,4323
19
- otf_api/models/classes.py,sha256=aKV6LGEh0YiPxyOaoMD0gaQOSHqs69cYHhP9H_2p_jY,3051
20
- otf_api/models/enums.py,sha256=6P7wOYwvZkBLfNckKaNWv6Po94yg6lq0qyNNNOSqEFg,4633
21
- otf_api/models/lifetime_stats.py,sha256=qpPCJuL68KhEbsIZ6cTx1dhh7RzDH5xWEAnLIsq8mZE,2853
22
- otf_api/models/member_detail.py,sha256=UzUttKuF_P3bvzEy_ZXBwDVwj52JtpDhGuPuAtQqT0I,6611
23
- otf_api/models/member_membership.py,sha256=jZwHzwtVyMUr8dWGlFbMYj9qveCbiOblWW5szXDUFFo,1338
24
- otf_api/models/member_purchases.py,sha256=Ne7ByEbGTqTJhuEyCgywWe8I3nc-D46qw09up7ys38s,1627
25
- otf_api/models/mixins.py,sha256=RBuAvAN1lYQpas0brACBof-6R4EqwGJL8HWHt41eQNg,2368
26
- otf_api/models/notifications.py,sha256=AkmIfiIiU6wox_7puyenbhCX10SFvBD5eBAovcurRgY,833
27
- otf_api/models/out_of_studio_workout_history.py,sha256=Kjkb8HW7k0qGMW3rAKXxZQju4MYglmmSRUdRx6FW_MQ,1714
28
- otf_api/models/performance_summary.py,sha256=vUQJum2lW6zHnYMOvdWwClWwrIwef6WojfKqyXUxTno,2914
29
- otf_api/models/ratings.py,sha256=RVsOGqx_eaB5i60pMRNR4xqYkQZhwRrLeSvmcFEDTgw,934
30
- otf_api/models/studio_detail.py,sha256=2gq0A27NOZGz_PTBvsB-dkzm01nYc9FHmx1NON6xp6U,4187
31
- otf_api/models/studio_services.py,sha256=aGLQMQmjGVpI6YxzAl-mcp3Y9cHPXuH9dIqrl6E-78E,1665
32
- otf_api/models/telemetry.py,sha256=PQ_CbADW5-t-U2iEQJGugNy-c4rD0q76TfyIqeFnTho,3170
33
- otf_api/models/workout.py,sha256=P3xVTvcYrm_RdU6qi3Xm2BXTxxvhvF0dgoEcODY41AA,3678
34
- otf_api-0.12.0.dist-info/licenses/LICENSE,sha256=UaPT9ynYigC3nX8n22_rC37n-qmTRKLFaHrtUwF9ktE,1071
35
- otf_api-0.12.0.dist-info/METADATA,sha256=jmck88-0JUoy5Y8oezk2KnBJhJ63jxgwZX2xZu7OoKA,2145
36
- otf_api-0.12.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
37
- otf_api-0.12.0.dist-info/top_level.txt,sha256=KAhYg1X2YG0LkTuVRhUV1I_AReNZUVNdEan7cp0pEE4,8
38
- otf_api-0.12.0.dist-info/RECORD,,