otf-api 0.8.2__py3-none-any.whl → 0.9.1__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 (43) hide show
  1. otf_api/__init__.py +7 -4
  2. otf_api/api.py +699 -480
  3. otf_api/auth/__init__.py +4 -0
  4. otf_api/auth/auth.py +234 -0
  5. otf_api/auth/user.py +66 -0
  6. otf_api/auth/utils.py +129 -0
  7. otf_api/exceptions.py +38 -5
  8. otf_api/filters.py +97 -0
  9. otf_api/logging.py +19 -0
  10. otf_api/models/__init__.py +27 -38
  11. otf_api/models/body_composition_list.py +47 -50
  12. otf_api/models/bookings.py +63 -87
  13. otf_api/models/challenge_tracker_content.py +42 -21
  14. otf_api/models/challenge_tracker_detail.py +68 -48
  15. otf_api/models/classes.py +53 -62
  16. otf_api/models/enums.py +108 -30
  17. otf_api/models/lifetime_stats.py +59 -45
  18. otf_api/models/member_detail.py +95 -115
  19. otf_api/models/member_membership.py +18 -17
  20. otf_api/models/member_purchases.py +21 -127
  21. otf_api/models/mixins.py +37 -33
  22. otf_api/models/notifications.py +17 -0
  23. otf_api/models/out_of_studio_workout_history.py +22 -31
  24. otf_api/models/performance_summary_detail.py +47 -42
  25. otf_api/models/performance_summary_list.py +19 -37
  26. otf_api/models/studio_detail.py +51 -98
  27. otf_api/models/studio_services.py +27 -48
  28. otf_api/models/telemetry.py +14 -5
  29. otf_api/utils.py +134 -0
  30. {otf_api-0.8.2.dist-info → otf_api-0.9.1.dist-info}/METADATA +21 -10
  31. otf_api-0.9.1.dist-info/RECORD +35 -0
  32. {otf_api-0.8.2.dist-info → otf_api-0.9.1.dist-info}/WHEEL +1 -1
  33. otf_api/auth.py +0 -316
  34. otf_api/models/book_class.py +0 -89
  35. otf_api/models/cancel_booking.py +0 -49
  36. otf_api/models/favorite_studios.py +0 -106
  37. otf_api/models/latest_agreement.py +0 -21
  38. otf_api/models/telemetry_hr_history.py +0 -34
  39. otf_api/models/telemetry_max_hr.py +0 -13
  40. otf_api/models/total_classes.py +0 -8
  41. otf_api-0.8.2.dist-info/AUTHORS.md +0 -9
  42. otf_api-0.8.2.dist-info/RECORD +0 -36
  43. {otf_api-0.8.2.dist-info → otf_api-0.9.1.dist-info}/LICENSE +0 -0
@@ -1,4 +1,6 @@
1
- from pydantic import Field
1
+ from datetime import datetime, time
2
+
3
+ from pydantic import AliasPath, Field, field_validator
2
4
 
3
5
  from otf_api.models.base import OtfItemBase
4
6
 
@@ -19,61 +21,64 @@ class HeartRate(OtfItemBase):
19
21
  avg_hr_percent: int
20
22
 
21
23
 
22
- class PerformanceMetricFloat(OtfItemBase):
23
- display_value: float
24
+ class PerformanceMetric(OtfItemBase):
25
+ display_value: time | float
24
26
  display_unit: str
25
27
  metric_value: float
26
28
 
29
+ @field_validator("display_value", mode="before")
30
+ @classmethod
31
+ def convert_to_time_format(cls, value) -> time | float:
32
+ if not value:
33
+ return value
27
34
 
28
- class PerformanceMetricString(OtfItemBase):
29
- display_value: str
30
- display_unit: str
31
- metric_value: str
35
+ if isinstance(value, float | int):
36
+ return value
37
+
38
+ if isinstance(value, str) and ":" in value:
39
+ if value.count(":") == 1:
40
+ minutes, seconds = value.split(":")
41
+ return time(minute=int(minutes), second=int(seconds))
42
+ if value.count(":") == 2:
43
+ hours, minutes, seconds = value.split(":")
44
+ return time(hour=int(hours), minute=int(minutes), second=int(seconds))
45
+
46
+ return value
32
47
 
33
48
 
34
49
  class BaseEquipment(OtfItemBase):
35
- avg_pace: PerformanceMetricString
36
- avg_speed: PerformanceMetricFloat
37
- max_pace: PerformanceMetricString
38
- max_speed: PerformanceMetricFloat
39
- moving_time: PerformanceMetricString
40
- total_distance: PerformanceMetricFloat
50
+ avg_pace: PerformanceMetric
51
+ avg_speed: PerformanceMetric
52
+ max_pace: PerformanceMetric
53
+ max_speed: PerformanceMetric
54
+ moving_time: PerformanceMetric
55
+ total_distance: PerformanceMetric
41
56
 
42
57
 
43
58
  class Treadmill(BaseEquipment):
44
- avg_incline: PerformanceMetricFloat
45
- elevation_gained: PerformanceMetricFloat
46
- max_incline: PerformanceMetricFloat
59
+ avg_incline: PerformanceMetric
60
+ elevation_gained: PerformanceMetric
61
+ max_incline: PerformanceMetric
47
62
 
48
63
 
49
64
  class Rower(BaseEquipment):
50
- avg_cadence: PerformanceMetricFloat
51
- avg_power: PerformanceMetricFloat
52
- max_cadence: PerformanceMetricFloat
53
-
54
-
55
- class EquipmentData(OtfItemBase):
56
- treadmill: Treadmill
57
- rower: Rower
58
-
59
-
60
- class Details(OtfItemBase):
61
- calories_burned: int
62
- splat_points: int
63
- step_count: int
64
- active_time_seconds: int
65
- zone_time_minutes: ZoneTimeMinutes
66
- heart_rate: HeartRate
67
- equipment_data: EquipmentData
68
-
69
-
70
- class Class(OtfItemBase):
71
- starts_at_local: str
72
- name: str
65
+ avg_cadence: PerformanceMetric
66
+ avg_power: PerformanceMetric
67
+ max_cadence: PerformanceMetric
73
68
 
74
69
 
75
70
  class PerformanceSummaryDetail(OtfItemBase):
76
71
  id: str
77
- details: Details
78
- ratable: bool
79
- otf_class: Class = Field(..., alias="class")
72
+ class_name: str | None = Field(None, alias=AliasPath("class", "name"))
73
+ class_starts_at: datetime | None = Field(None, alias=AliasPath("class", "starts_at_local"))
74
+
75
+ ratable: bool | None = None
76
+ calories_burned: int | None = Field(None, alias=AliasPath("details", "calories_burned"))
77
+ splat_points: int | None = Field(None, alias=AliasPath("details", "splat_points"))
78
+ step_count: int | None = Field(None, alias=AliasPath("details", "step_count"))
79
+ active_time_seconds: int | None = Field(None, alias=AliasPath("details", "active_time_seconds"))
80
+ zone_time_minutes: ZoneTimeMinutes | None = Field(None, alias=AliasPath("details", "zone_time_minutes"))
81
+ heart_rate: HeartRate | None = Field(None, alias=AliasPath("details", "heart_rate"))
82
+
83
+ rower_data: Rower | None = Field(None, alias=AliasPath("details", "equipment_data", "rower"))
84
+ treadmill_data: Treadmill | None = Field(None, alias=AliasPath("details", "equipment_data", "treadmill"))
@@ -1,4 +1,6 @@
1
- from pydantic import Field
1
+ from datetime import datetime
2
+
3
+ from pydantic import AliasPath, Field
2
4
 
3
5
  from otf_api.models.base import OtfItemBase
4
6
 
@@ -11,32 +13,11 @@ class ZoneTimeMinutes(OtfItemBase):
11
13
  red: int
12
14
 
13
15
 
14
- class Details(OtfItemBase):
15
- calories_burned: int
16
- splat_points: int
17
- step_count: int
18
- active_time_seconds: int
19
- zone_time_minutes: ZoneTimeMinutes
20
-
21
-
22
- class Coach(OtfItemBase):
23
- image_url: str | None = None
24
- first_name: str
25
-
26
-
27
- class Studio(OtfItemBase):
28
- id: str
29
- license_number: str
30
- name: str
31
-
32
-
33
16
  class Class(OtfItemBase):
34
- ot_base_class_uuid: str | None = None
35
- starts_at_local: str
17
+ class_uuid: str | None = Field(None, description="Only populated if class is ratable", alias="ot_base_class_uuid")
18
+ starts_at: datetime | None = Field(None, alias="starts_at_local")
36
19
  name: str | None = None
37
20
  type: str | None = None
38
- coach: Coach
39
- studio: Studio
40
21
 
41
22
 
42
23
  class CoachRating(OtfItemBase):
@@ -51,18 +32,19 @@ class ClassRating(OtfItemBase):
51
32
  value: int
52
33
 
53
34
 
54
- class Ratings(OtfItemBase):
55
- coach: CoachRating
56
- otf_class: ClassRating = Field(..., alias="class")
57
-
58
-
59
35
  class PerformanceSummaryEntry(OtfItemBase):
60
36
  id: str = Field(..., alias="id")
61
- details: Details
62
- ratable: bool
63
- otf_class: Class = Field(..., alias="class")
64
- ratings: Ratings | None = None
65
-
66
-
67
- class PerformanceSummaryList(OtfItemBase):
68
- summaries: list[PerformanceSummaryEntry]
37
+ calories_burned: int | None = Field(None, alias=AliasPath("details", "calories_burned"))
38
+ splat_points: int | None = Field(None, alias=AliasPath("details", "splat_points"))
39
+ step_count: int | None = Field(None, alias=AliasPath("details", "step_count"))
40
+ active_time_seconds: int | None = Field(None, alias=AliasPath("details", "active_time_seconds"))
41
+ zone_time_minutes: ZoneTimeMinutes | None = Field(None, alias=AliasPath("details", "zone_time_minutes"))
42
+ ratable: bool | None = None
43
+ otf_class: Class | None = Field(None, alias="class")
44
+ coach: str | None = Field(None, alias=AliasPath("class", "coach", "first_name"))
45
+ coach_rating: CoachRating | None = Field(None, alias=AliasPath("ratings", "coach"))
46
+ class_rating: ClassRating | None = Field(None, alias=AliasPath("ratings", "class"))
47
+
48
+ @property
49
+ def is_rated(self) -> bool:
50
+ return self.coach_rating is not None or self.class_rating is not None
@@ -1,109 +1,62 @@
1
1
  from datetime import datetime
2
2
 
3
- from pydantic import Field
3
+ from pydantic import AliasChoices, AliasPath, Field
4
4
 
5
5
  from otf_api.models.base import OtfItemBase
6
+ from otf_api.models.enums import StudioStatus
7
+ from otf_api.models.mixins import AddressMixin
6
8
 
7
9
 
8
- class Country(OtfItemBase):
9
- country_id: int = Field(..., alias="countryId")
10
- country_currency_code: str | None = Field(None, alias="countryCurrencyCode")
11
- country_currency_name: str | None = Field(None, alias="countryCurrencyName")
12
- currency_alphabetic_code: str | None = Field(None, alias="currencyAlphabeticCode")
10
+ class StudioLocation(AddressMixin):
11
+ phone_number: str | None = Field(None, alias=AliasChoices("phone", "phoneNumber"))
12
+ latitude: float = Field(..., alias=AliasChoices("latitude"))
13
+ longitude: float = Field(..., alias=AliasChoices("longitude"))
13
14
 
14
-
15
- class StudioLocation(OtfItemBase):
16
- physical_address: str | None = Field(None, alias="physicalAddress")
17
- physical_address2: str | None = Field(None, alias="physicalAddress2")
18
- physical_city: str | None = Field(None, alias="physicalCity")
19
- physical_state: str | None = Field(None, alias="physicalState")
20
- physical_postal_code: str | None = Field(None, alias="physicalPostalCode")
21
- physical_region: str | None = Field(None, alias="physicalRegion", exclude=True)
22
- physical_country: str | None = Field(None, alias="physicalCountry", exclude=True)
23
- country: Country | None = Field(None, exclude=True)
24
- phone_number: str | None = Field(None, alias="phoneNumber")
25
- latitude: float | None = Field(None, exclude=True)
26
- longitude: float | None = Field(None, exclude=True)
27
-
28
-
29
- class Language(OtfItemBase):
30
- language_id: None = Field(None, alias="languageId")
31
- language_code: None = Field(None, alias="languageCode")
32
- language_name: None = Field(None, alias="languageName")
33
-
34
-
35
- class StudioLocationLocalized(OtfItemBase):
36
- language: Language | None = Field(None, exclude=True)
37
- studio_name: str | None = Field(None, alias="studioName")
38
- studio_address: str | None = Field(None, alias="studioAddress")
39
-
40
-
41
- class StudioProfiles(OtfItemBase):
42
- is_web: bool | None = Field(None, alias="isWeb")
43
- intro_capacity: int | None = Field(None, alias="introCapacity")
44
- is_crm: bool | None = Field(None, alias="isCrm")
45
-
46
-
47
- class SocialMediaLink(OtfItemBase):
48
- id: str
49
- language_id: str | None = Field(None, alias="languageId")
50
- name: str
51
- value: str
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)
52
17
 
53
18
 
54
19
  class StudioDetail(OtfItemBase):
55
- studio_id: int = Field(..., alias="studioId", exclude=True)
56
- studio_uuid: str = Field(..., alias="studioUUId")
57
- studio_location_localized: StudioLocationLocalized | None = Field(
58
- None, alias="studioLocationLocalized", exclude=True
59
- )
60
- studio_location: StudioLocation | None = Field(None, alias="studioLocation")
61
- studio_name: str | None = Field(None, alias="studioName")
62
- studio_number: str | None = Field(None, alias="studioNumber", exclude=True)
63
- studio_physical_location_id: int | None = Field(None, alias="studioPhysicalLocationId", exclude=True)
64
- studio_profiles: StudioProfiles | None = Field(None, alias="studioProfiles", exclude=True)
65
- studio_status: str | None = Field(None, alias="studioStatus", exclude=True)
66
- studio_token: str | None = Field(None, alias="studioToken", exclude=True)
67
- studio_type_id: int | None = Field(None, alias="studioTypeId", exclude=True)
68
- studio_version: str | None = Field(None, alias="studioVersion", exclude=True)
69
- mbo_studio_id: int | None = Field(None, alias="mboStudioId", exclude=True)
70
- accepts_ach: bool | None = Field(None, alias="acceptsAch", exclude=True)
71
- accepts_american_express: bool | None = Field(None, alias="acceptsAmericanExpress", exclude=True)
72
- accepts_discover: bool | None = Field(None, alias="acceptsDiscover", exclude=True)
73
- accepts_visa_master_card: bool | None = Field(None, alias="acceptsVisaMasterCard", exclude=True)
74
- allows_cr_waitlist: bool | None = Field(None, alias="allowsCrWaitlist", exclude=True)
75
- allows_dashboard_access: bool | None = Field(None, alias="allowsDashboardAccess", exclude=True)
76
- area_id: int | None = Field(None, alias="areaId", exclude=True)
77
- commission_percent: int | None = Field(None, alias="commissionPercent", exclude=True)
78
- contact_email: str | None = Field(None, alias="contactEmail", exclude=True)
79
- cr_waitlist_flag_last_updated: datetime | None = Field(None, alias="crWaitlistFlagLastUpdated", exclude=True)
80
- description: str | None = Field(None, exclude=True)
81
- distance: float | None = Field(None, exclude=True)
82
- environment: str | None = Field(None, exclude=True)
83
- is_integrated: bool | None = Field(None, alias="isIntegrated", exclude=True)
84
- is_mobile: bool | None = Field(None, alias="isMobile", exclude=True)
85
- is_otbeat: bool | None = Field(None, alias="isOtbeat", exclude=True)
86
- logo_url: str | None = Field(None, alias="logoUrl", exclude=True)
87
- market_id: int | None = Field(None, alias="marketId", exclude=True)
88
- marketing_fund_rate: int | None = Field(None, alias="marketingFundRate", exclude=True)
89
- open_date: datetime | None = Field(None, alias="openDate", exclude=True)
90
- pos_type_id: int | None = Field(None, alias="posTypeId", exclude=True)
91
- pricing_level: str | None = Field(None, alias="pricingLevel", exclude=True)
92
- re_open_date: datetime | None = Field(None, alias="reOpenDate", exclude=True)
93
- royalty_rate: int | None = Field(None, alias="royaltyRate", exclude=True)
94
- sms_package_enabled: bool | None = Field(None, alias="smsPackageEnabled", exclude=True)
95
- social_media_links: list[SocialMediaLink] | None = Field(None, alias="socialMediaLinks", exclude=True)
96
- state_id: int | None = Field(None, alias="stateId", exclude=True)
97
- tax_rate: str | None = Field(None, alias="taxRate", exclude=True)
98
- time_zone: str | None = Field(None, alias="timeZone", exclude=True)
99
-
100
-
101
- class Pagination(OtfItemBase):
102
- page_index: int | None = Field(None, alias="pageIndex")
103
- page_size: int | None = Field(None, alias="pageSize")
104
- total_count: int | None = Field(None, alias="totalCount")
105
- total_pages: int | None = Field(None, alias="totalPages")
106
-
20
+ studio_uuid: str = Field(..., alias="studioUUId", description="The OTF studio UUID")
107
21
 
108
- class StudioDetailList(OtfItemBase):
109
- studios: list[StudioDetail]
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
+ )
28
+ location: StudioLocation = Field(..., alias="studioLocation")
29
+ name: str | None = Field(None, alias="studioName")
30
+ status: StudioStatus | None = Field(
31
+ None, alias="studioStatus", description="Active, Temporarily Closed, Coming Soon"
32
+ )
33
+ time_zone: str | None = Field(None, alias="timeZone")
34
+
35
+ # flags
36
+ accepts_ach: bool | None = Field(None, alias="acceptsAch", exclude=True, repr=False)
37
+ accepts_american_express: bool | None = Field(None, alias="acceptsAmericanExpress", exclude=True, repr=False)
38
+ accepts_discover: bool | None = Field(None, alias="acceptsDiscover", exclude=True, repr=False)
39
+ accepts_visa_master_card: bool | None = Field(None, alias="acceptsVisaMasterCard", exclude=True, repr=False)
40
+ allows_cr_waitlist: bool | None = Field(None, alias="allowsCrWaitlist", exclude=True, repr=False)
41
+ allows_dashboard_access: bool | None = Field(None, alias="allowsDashboardAccess", exclude=True, repr=False)
42
+ is_crm: bool | None = Field(None, alias=AliasPath("studioProfiles", "isCrm"), exclude=True, repr=False)
43
+ is_integrated: bool | None = Field(
44
+ None, alias="isIntegrated", exclude=True, repr=False, description="Always 'True'"
45
+ )
46
+ is_mobile: bool | None = Field(None, alias="isMobile", exclude=True, repr=False)
47
+ is_otbeat: bool | None = Field(None, alias="isOtbeat", exclude=True, repr=False)
48
+ is_web: bool | None = Field(None, alias=AliasPath("studioProfiles", "isWeb"), exclude=True, repr=False)
49
+ sms_package_enabled: bool | None = Field(None, alias="smsPackageEnabled", exclude=True, repr=False)
50
+
51
+ # misc
52
+ studio_id: int | None = Field(None, alias="studioId", description="Not used by API", exclude=True, repr=False)
53
+ mbo_studio_id: int | None = Field(None, alias="mboStudioId", exclude=True, repr=False, description="MindBody attr")
54
+ open_date: datetime | None = Field(None, alias="openDate", exclude=True, repr=False)
55
+ pricing_level: str | None = Field(
56
+ None, alias="pricingLevel", exclude=True, repr=False, description="Pro, Legacy, Accelerate, or empty"
57
+ )
58
+ re_open_date: datetime | None = Field(None, alias="reOpenDate", exclude=True, repr=False)
59
+ studio_number: str | None = Field(None, alias="studioNumber", exclude=True, repr=False)
60
+ studio_physical_location_id: int | None = Field(None, alias="studioPhysicalLocationId", exclude=True, repr=False)
61
+ studio_token: str | None = Field(None, alias="studioToken", exclude=True, repr=False)
62
+ studio_type_id: int | None = Field(None, alias="studioTypeId", exclude=True, repr=False)
@@ -3,55 +3,34 @@ from datetime import datetime
3
3
  from pydantic import Field
4
4
 
5
5
  from otf_api.models.base import OtfItemBase
6
-
7
-
8
- class Currency(OtfItemBase):
9
- currency_alphabetic_code: str = Field(..., alias="currencyAlphabeticCode")
10
-
11
-
12
- class DefaultCurrency(OtfItemBase):
13
- currency_id: int = Field(..., alias="currencyId")
14
- currency: Currency
15
-
16
-
17
- class Country(OtfItemBase):
18
- country_currency_code: str = Field(..., alias="countryCurrencyCode")
19
- default_currency: DefaultCurrency = Field(..., alias="defaultCurrency")
20
-
21
-
22
- class StudioLocation(OtfItemBase):
23
- studio_location_id: int = Field(..., alias="studioLocationId")
24
- country: Country
25
-
26
-
27
- class Studio(OtfItemBase):
28
- studio_id: int = Field(..., alias="studioId")
29
- studio_location: StudioLocation = Field(..., alias="studioLocation")
6
+ from otf_api.models.studio_detail import StudioDetail
30
7
 
31
8
 
32
9
  class StudioService(OtfItemBase):
33
- service_id: int = Field(..., alias="serviceId")
10
+ studio: StudioDetail = Field(..., exclude=True, repr=False)
34
11
  service_uuid: str = Field(..., alias="serviceUUId")
35
- studio_id: int = Field(..., alias="studioId")
36
- name: str
37
- price: str
38
- qty: int
39
- mbo_program_id: int = Field(..., alias="mboProgramId")
40
- mbo_description_id: str = Field(..., alias="mboDescriptionId")
41
- mbo_product_id: int = Field(..., alias="mboProductId")
42
- online_price: str = Field(..., alias="onlinePrice")
43
- tax_rate: str = Field(..., alias="taxRate")
44
- current: bool
45
- is_web: bool = Field(..., alias="isWeb")
46
- is_crm: bool = Field(..., alias="isCrm")
47
- is_mobile: bool = Field(..., alias="isMobile")
48
- created_by: str = Field(..., alias="createdBy")
49
- created_date: datetime = Field(..., alias="createdDate")
50
- updated_by: str = Field(..., alias="updatedBy")
51
- updated_date: datetime = Field(..., alias="updatedDate")
52
- is_deleted: bool = Field(..., alias="isDeleted")
53
- studio: Studio
54
-
55
-
56
- class StudioServiceList(OtfItemBase):
57
- data: list[StudioService]
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,7 +1,7 @@
1
1
  from datetime import datetime, timedelta
2
2
  from typing import Any
3
3
 
4
- from pydantic import Field
4
+ from pydantic import AliasPath, Field
5
5
 
6
6
  from otf_api.models.base import OtfItemBase
7
7
 
@@ -50,13 +50,22 @@ class TelemetryItem(OtfItemBase):
50
50
  class Telemetry(OtfItemBase):
51
51
  member_uuid: str = Field(..., alias="memberUuid")
52
52
  class_history_uuid: str = Field(..., alias="classHistoryUuid")
53
- class_start_time: datetime = Field(..., alias="classStartTime")
54
- max_hr: int = Field(..., alias="maxHr")
53
+ class_start_time: datetime | None = Field(None, alias="classStartTime")
54
+ max_hr: int | None = Field(None, alias="maxHr")
55
55
  zones: Zones
56
- window_size: int = Field(..., alias="windowSize")
57
- telemetry: list[TelemetryItem]
56
+ window_size: int | None = Field(None, alias="windowSize")
57
+ telemetry: list[TelemetryItem] = Field(default_factory=list)
58
58
 
59
59
  def __init__(self, **data: dict[str, Any]):
60
60
  super().__init__(**data)
61
61
  for telem in self.telemetry:
62
62
  telem.timestamp = self.class_start_time + timedelta(seconds=telem.relative_timestamp)
63
+
64
+
65
+ class TelemetryHistoryItem(OtfItemBase):
66
+ max_hr_type: str | None = Field(None, alias=AliasPath("maxHr", "type"))
67
+ max_hr_value: int | None = Field(None, alias=AliasPath("maxHr", "value"))
68
+ zones: Zones | None = None
69
+ change_from_previous: int | None = Field(None, alias="changeFromPrevious")
70
+ change_bucket: str | None = Field(None, alias="changeBucket")
71
+ assigned_at: datetime | None = Field(None, alias="assignedAt")
otf_api/utils.py ADDED
@@ -0,0 +1,134 @@
1
+ import json
2
+ import typing
3
+ from datetime import date, datetime
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.models.bookings import Booking
12
+ from otf_api.models.classes import OtfClass
13
+
14
+ LOGGER = getLogger(__name__)
15
+
16
+
17
+ def get_booking_uuid(booking_or_uuid: "str | Booking") -> str:
18
+ from otf_api.models.bookings import Booking
19
+
20
+ if isinstance(booking_or_uuid, str):
21
+ return booking_or_uuid
22
+
23
+ if isinstance(booking_or_uuid, Booking):
24
+ return booking_or_uuid.booking_uuid
25
+
26
+ raise ValueError(f"Expected Booking or str, got {type(booking_or_uuid)}")
27
+
28
+
29
+ def get_class_uuid(class_or_uuid: "str | OtfClass") -> str:
30
+ from otf_api.models.classes import OtfClass
31
+
32
+ if isinstance(class_or_uuid, str):
33
+ return class_or_uuid
34
+
35
+ if isinstance(class_or_uuid, OtfClass):
36
+ return class_or_uuid.class_uuid
37
+
38
+ raise ValueError(f"Expected OtfClass or str, got {type(class_or_uuid)}")
39
+
40
+
41
+ def ensure_list(obj: list | Any | None) -> list:
42
+ if obj is None:
43
+ return []
44
+ if not isinstance(obj, list):
45
+ return [obj]
46
+ return obj
47
+
48
+
49
+ def ensure_date(date_str: str | date | None) -> date | None:
50
+ if not date_str:
51
+ return None
52
+
53
+ if isinstance(date_str, str):
54
+ return datetime.fromisoformat(date_str).date()
55
+
56
+ if isinstance(date_str, datetime):
57
+ return date_str.date()
58
+
59
+ return date_str
60
+
61
+
62
+ @attrs.define
63
+ class CacheableData:
64
+ """Represents a cacheable data object, with methods to read and write to cache."""
65
+
66
+ name: str
67
+ cache_dir: Path
68
+
69
+ def __attrs_post_init__(self):
70
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
71
+
72
+ @property
73
+ def cache_path(self) -> Path:
74
+ """The path to the cache file."""
75
+ return self.cache_dir.expanduser().joinpath(f"{self.name}_cache.json")
76
+
77
+ def get_cached_data(self, keys: list[str] | None = None) -> dict[str, str]:
78
+ """Reads the cache file and returns the data if it exists and is valid.
79
+
80
+ Returns:
81
+ dict[str, str]: The cached data, or an empty dictionary if the cache is invalid or missing.
82
+ """
83
+ LOGGER.debug(f"Loading {self.name} from cache ({self.cache_path})")
84
+ try:
85
+ if not self.cache_path.exists():
86
+ return {}
87
+
88
+ if self.cache_path.stat().st_size == 0:
89
+ return {}
90
+
91
+ data: dict[str, str] = json.loads(self.cache_path.read_text())
92
+ if not keys:
93
+ return data
94
+
95
+ if set(data.keys()).issuperset(set(keys)):
96
+ return {k: v for k, v in data.items() if k in keys}
97
+ raise ValueError(f"Data must contain all keys: {keys}")
98
+ except Exception:
99
+ LOGGER.exception(f"Failed to read {self.cache_path.name}")
100
+ return {}
101
+
102
+ def write_to_cache(self, data: dict[str, str]) -> None:
103
+ """Writes the data to the cache file."""
104
+ LOGGER.debug(f"Writing {self.name} to cache ({self.cache_path})")
105
+
106
+ # double check everything exists
107
+ if not self.cache_path.parent.exists():
108
+ self.cache_path.parent.mkdir(parents=True, exist_ok=True)
109
+
110
+ if not self.cache_path.exists():
111
+ self.cache_path.touch()
112
+
113
+ existing_data = self.get_cached_data()
114
+ data = {**existing_data, **data}
115
+
116
+ self.cache_path.write_text(json.dumps(data, indent=4, default=str))
117
+
118
+ def clear_cache(self, keys: list[str] | None = None) -> None:
119
+ """Deletes the cache file if it exists."""
120
+ if not self.cache_path.exists():
121
+ return
122
+
123
+ if not keys:
124
+ self.cache_path.unlink()
125
+ LOGGER.debug(f"{self.name} cache deleted")
126
+ return
127
+
128
+ assert isinstance(keys, list), "Keys must be a list"
129
+
130
+ data = self.get_cached_data()
131
+ for key in keys:
132
+ data.pop(key, None)
133
+
134
+ self.write_to_cache(data)
@@ -1,17 +1,16 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: otf-api
3
- Version: 0.8.2
3
+ Version: 0.9.1
4
4
  Summary: Python OrangeTheory Fitness API Client
5
5
  License: MIT
6
6
  Author: Jessica Smith
7
7
  Author-email: j.smith.git1@gmail.com
8
- Requires-Python: >=3.10,<4.0
8
+ Requires-Python: >=3.11,<4.0
9
9
  Classifier: Development Status :: 4 - Beta
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.10
15
14
  Classifier: Programming Language :: Python :: 3.11
16
15
  Classifier: Programming Language :: Python :: 3.12
17
16
  Classifier: Programming Language :: Python :: 3.13
@@ -19,12 +18,15 @@ Classifier: Topic :: Internet :: WWW/HTTP
19
18
  Classifier: Topic :: Software Development :: Libraries
20
19
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
21
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
- Requires-Dist: aiohttp (==3.10.*)
21
+ Requires-Dist: attrs (>=24.3.0,<25.0.0)
22
+ Requires-Dist: httpx (>=0.27.2)
23
23
  Requires-Dist: humanize (>=4.9.0,<5.0.0)
24
24
  Requires-Dist: inflection (==0.5.*)
25
+ Requires-Dist: mypy-boto3-cognito-idp (>=1.35.93,<2.0.0)
25
26
  Requires-Dist: pint (==0.24.*)
26
27
  Requires-Dist: pycognito (==2024.5.1)
27
- Requires-Dist: pydantic (==2.7.3)
28
+ Requires-Dist: pydantic (>=2.7.3)
29
+ Requires-Dist: yarl (>=1.18.3,<2.0.0)
28
30
  Project-URL: Documentation, https://otf-api.readthedocs.io/en/stable/
29
31
  Description-Content-Type: text/markdown
30
32
 
@@ -42,11 +44,20 @@ pip install otf-api
42
44
 
43
45
  ## Overview
44
46
 
45
- To use the API, you need to create an instance of the `Otf` class, providing your email address and password. This will authenticate you with the API and allow you to make requests. When the `Otf` object is created it automatically grabs your member details and home studio, to simplify the process of making requests.
47
+ To use the API, you need to create an instance of the `Otf` class. This will authenticate you with the API and allow you to make requests. When the `Otf` object is created it automatically grabs your member details and home studio, to simplify the process of making requests.
46
48
 
49
+ You can either pass an `OtfUser` object to the `OtfClass` or you can pass nothing and allow it to prompt you for your username and password.
47
50
 
48
- See the [examples](./examples) for more information on how to use the API.
51
+ You can also export environment variables `OTF_EMAIL` and `OTF_PASSWORD` to get these from the environment.
49
52
 
50
- Disclaimer:
51
- This project is in no way affiliated with OrangeTheory Fitness.
53
+ ```python
54
+ from otf_api import Otf, OtfUser
55
+
56
+ otf = Otf()
57
+
58
+ # OR
59
+
60
+ otf = Otf(user=OtfUser(<email_address>,<password>))
61
+
62
+ ```
52
63