From 2bc277b6073a95f8ea2b15f7638f9eb46838cc1d Mon Sep 17 00:00:00 2001 From: Jessica Smith <12jessicasmith34@gmail.com> Date: Sun, 19 Jan 2025 12:25:14 -0600 Subject: [PATCH] remove some endpoints, update some model names, remove some models, combine some model files --- examples/challenge_tracker_examples.py | 4 +- src/otf_api/api.py | 65 +++-------- src/otf_api/models/__init__.py | 17 ++- src/otf_api/models/body_composition_list.py | 1 + src/otf_api/models/bookings.py | 2 - .../models/challenge_tracker_content.py | 47 ++++---- .../models/challenge_tracker_detail.py | 105 ++++++++++-------- src/otf_api/models/classes.py | 16 +-- src/otf_api/models/enums.py | 24 +++- src/otf_api/models/latest_agreement.py | 21 ---- src/otf_api/models/lifetime_stats.py | 32 +++--- src/otf_api/models/member_detail.py | 44 ++++---- src/otf_api/models/member_membership.py | 35 +++--- src/otf_api/models/member_purchases.py | 12 +- .../models/out_of_studio_workout_history.py | 51 ++++----- .../models/performance_summary_detail.py | 35 +++--- .../models/performance_summary_list.py | 2 +- src/otf_api/models/studio_services.py | 19 ++-- src/otf_api/models/telemetry.py | 19 +++- src/otf_api/models/telemetry_hr_history.py | 34 ------ src/otf_api/models/telemetry_max_hr.py | 13 --- 21 files changed, 267 insertions(+), 331 deletions(-) delete mode 100644 src/otf_api/models/latest_agreement.py delete mode 100644 src/otf_api/models/telemetry_hr_history.py delete mode 100644 src/otf_api/models/telemetry_max_hr.py diff --git a/examples/challenge_tracker_examples.py b/examples/challenge_tracker_examples.py index 08b24a1..94cddf7 100644 --- a/examples/challenge_tracker_examples.py +++ b/examples/challenge_tracker_examples.py @@ -7,7 +7,7 @@ def main(): # challenge tracker content is an overview of the challenges OTF runs # and your participation in them - challenge_tracker_content = otf.get_challenge_tracker_content() + challenge_tracker_content = otf.get_challenge_tracker() print(challenge_tracker_content.benchmarks[0].model_dump_json(indent=4)) """ @@ -46,7 +46,7 @@ def main(): # challenge tracker details are detailed information about specific challenges # this endpoint takes an equipment type and a challenge type as arguments - tread_challenge_details = otf.get_challenge_tracker_detail(EquipmentType.Treadmill, ChallengeType.Other) + tread_challenge_details = otf.get_benchmarks(EquipmentType.Treadmill, ChallengeType.Other) print(tread_challenge_details[0].model_dump_json(indent=4)) """ diff --git a/src/otf_api/api.py b/src/otf_api/api.py index 44ba97a..116df4d 100644 --- a/src/otf_api/api.py +++ b/src/otf_api/api.py @@ -629,20 +629,6 @@ def get_member_lifetime_stats( stats = models.StatsResponse(**data["data"]) return stats - def get_latest_agreement(self) -> models.LatestAgreement: - """Get the latest agreement for the member. - - Returns: - LatestAgreement: The agreement. - - Notes: - --- - In this context, "latest" means the most recent agreement with a specific ID, not the most recent agreement - in general. The agreement ID is hardcoded in the endpoint, so it will always return the same agreement. - """ - data = self._default_request("GET", "/member/agreements/9d98fb27-0f00-4598-ad08-5b1655a59af6") - return models.LatestAgreement(**data["data"]) - def get_out_of_studio_workout_history(self) -> list[models.OutOfStudioWorkoutHistory]: """Get the member's out of studio workout history. @@ -827,21 +813,21 @@ def get_body_composition_list(self) -> list[models.BodyCompositionData]: data = self._default_request("GET", f"/member/members/{self.user.cognito_id}/body-composition") return [models.BodyCompositionData(**item) for item in data["data"]] - def get_challenge_tracker_content(self) -> models.ChallengeTrackerContent: + def get_challenge_tracker(self) -> models.ChallengeTracker: """Get the member's challenge tracker content. Returns: - ChallengeTrackerContent: The member's challenge tracker content. + ChallengeTracker: The member's challenge tracker content. """ data = self._default_request("GET", f"/challenges/v3.1/member/{self.member_uuid}") - return models.ChallengeTrackerContent(**data["Dto"]) + return models.ChallengeTracker(**data["Dto"]) - def get_challenge_tracker_detail( + def get_benchmarks( self, equipment_id: models.EquipmentType, challenge_type_id: models.ChallengeType, challenge_sub_type_id: int = 0, - ) -> list[models.ChallengeTrackerDetail]: + ) -> list[models.FitnessBenchmark]: """Get the member's challenge tracker details. Args: @@ -850,7 +836,7 @@ def get_challenge_tracker_detail( challenge_sub_type_id (int): The challenge sub type ID. Default is 0. Returns: - list[ChallengeTrackerDetail]: The member's challenge tracker details. + list[FitnessBenchmark]: The member's challenge tracker details. Notes: --- @@ -864,22 +850,16 @@ def get_challenge_tracker_detail( } data = self._default_request("GET", f"/challenges/v3/member/{self.member_uuid}/benchmarks", params=params) - return [models.ChallengeTrackerDetail(**item) for item in data["Dto"]] + return [models.FitnessBenchmark(**item) for item in data["Dto"]] - def get_challenge_tracker_participation(self, challenge_type_id: models.ChallengeType) -> Any: + def get_challenge_tracker_detail(self, challenge_type_id: models.ChallengeType) -> models.FitnessBenchmark: """Get the member's participation in a challenge. Args: challenge_type_id (ChallengeType): The challenge type ID. Returns: - Any: The member's participation in the challenge. - - Notes: - --- - I've never gotten this to return anything other than invalid response. I'm not sure if it's a bug - in my code or the API. - + FitnessBenchmark: The member's participation in the challenge. """ data = self._default_request( @@ -887,7 +867,11 @@ def get_challenge_tracker_participation(self, challenge_type_id: models.Challeng f"/challenges/v1/member/{self.member_uuid}/participation", params={"challengeTypeId": challenge_type_id.value}, ) - return data["Dto"] + + if len(data["Dto"]) > 1: + LOGGER.warning("Multiple challenge participations found, returning the first one.") + + return models.FitnessBenchmark(**data["Dto"][0]) def get_performance_summaries(self, limit: int = 5) -> list[models.PerformanceSummaryEntry]: """Get a list of performance summaries for the authenticated user. @@ -927,7 +911,7 @@ def get_performance_summary(self, performance_summary_id: str) -> models.Perform return models.PerformanceSummaryDetail(**res) - def get_hr_history(self) -> list[models.HistoryItem]: + def get_hr_history(self) -> list[models.TelemetryHistoryItem]: """Get the heartrate history for the user. Returns a list of history items that contain the max heartrate, start/end bpm for each zone, @@ -940,23 +924,8 @@ def get_hr_history(self) -> list[models.HistoryItem]: path = "/v1/physVars/maxHr/history" params = {"memberUuid": self.member_uuid} - res = self._telemetry_request("GET", path, params=params) - return [models.HistoryItem(**item) for item in res["items"]] - - def get_max_hr(self) -> models.TelemetryMaxHr: - """Get the max heartrate for the user. - - Returns a simple object that has the member_uuid and the max_hr. - - Returns: - TelemetryMaxHr: The max heartrate for the user. - """ - path = "/v1/physVars/maxHr" - - params = {"memberUuid": self.member_uuid} - - res = self._telemetry_request("GET", path, params=params) - return models.TelemetryMaxHr(**res) + resp = self._telemetry_request("GET", path, params=params) + return [models.TelemetryHistoryItem(**item) for item in resp["history"]] def get_telemetry(self, performance_summary_id: str, max_data_points: int = 120) -> models.Telemetry: """Get the telemetry for a performance summary. diff --git a/src/otf_api/models/__init__.py b/src/otf_api/models/__init__.py index 4717b55..45721c9 100644 --- a/src/otf_api/models/__init__.py +++ b/src/otf_api/models/__init__.py @@ -1,10 +1,9 @@ from .body_composition_list import BodyCompositionData from .bookings import Booking -from .challenge_tracker_content import ChallengeTrackerContent -from .challenge_tracker_detail import ChallengeTrackerDetail +from .challenge_tracker_content import ChallengeTracker +from .challenge_tracker_detail import FitnessBenchmark from .classes import OtfClass from .enums import BookingStatus, ChallengeType, ClassType, DoW, EquipmentType, StatsTime, StudioStatus -from .latest_agreement import LatestAgreement from .lifetime_stats import StatsResponse from .member_detail import MemberDetail from .member_membership import MemberMembership @@ -14,21 +13,19 @@ from .performance_summary_list import PerformanceSummaryEntry from .studio_detail import StudioDetail from .studio_services import StudioService -from .telemetry import Telemetry -from .telemetry_hr_history import HistoryItem -from .telemetry_max_hr import TelemetryMaxHr +from .telemetry import Telemetry, TelemetryHistoryItem __all__ = [ "BodyCompositionData", "Booking", "BookingStatus", - "ChallengeTrackerContent", - "ChallengeTrackerDetail", + "ChallengeParticipation", + "ChallengeTracker", "ChallengeType", "ClassType", "DoW", "EquipmentType", - "HistoryItem", + "FitnessBenchmark", "LatestAgreement", "MemberDetail", "MemberMembership", @@ -43,5 +40,5 @@ "StudioService", "StudioStatus", "Telemetry", - "TelemetryMaxHr", + "TelemetryHistoryItem", ] diff --git a/src/otf_api/models/body_composition_list.py b/src/otf_api/models/body_composition_list.py index 8e11bb2..29b0e75 100644 --- a/src/otf_api/models/body_composition_list.py +++ b/src/otf_api/models/body_composition_list.py @@ -9,6 +9,7 @@ ureg = pint.UnitRegistry() + DEFAULT_WEIGHT_DIVIDERS = [55.0, 70.0, 85.0, 100.0, 115.0, 130.0, 145.0, 160.0, 175.0, 190.0, 205.0] DEFAULT_SKELETAL_MUSCLE_MASS_DIVIDERS = [70.0, 80.0, 90.0, 100.0, 110.0, 120.0, 130.0, 140.0, 150.0, 160.0, 170.0] DEFAULT_BODY_FAT_MASS_DIVIDERS = [40.0, 60.0, 80.0, 100.0, 160.0, 220.0, 280.0, 340.0, 400.0, 460.0, 520.0] diff --git a/src/otf_api/models/bookings.py b/src/otf_api/models/bookings.py index d73fc0e..46e1431 100644 --- a/src/otf_api/models/bookings.py +++ b/src/otf_api/models/bookings.py @@ -13,9 +13,7 @@ class Coach(OtfItemBase): last_name: str | None = Field(None, alias="lastName") # unused fields - image_url: str | None = Field(None, alias="imageUrl", exclude=True, repr=False) name: str = Field(exclude=True, repr=False) - profile_picture_url: str | None = Field(None, alias="profilePictureUrl", exclude=True, repr=False) @property def full_name(self) -> str: diff --git a/src/otf_api/models/challenge_tracker_content.py b/src/otf_api/models/challenge_tracker_content.py index cb51957..b7b1570 100644 --- a/src/otf_api/models/challenge_tracker_content.py +++ b/src/otf_api/models/challenge_tracker_content.py @@ -1,38 +1,43 @@ from pydantic import Field from otf_api.models.base import OtfItemBase +from otf_api.models.enums import EquipmentType class Year(OtfItemBase): - year: str = Field(..., alias="Year") - is_participated: bool = Field(..., alias="IsParticipated") - in_progress: bool = Field(..., alias="InProgress") + year: int | None = Field(None, alias="Year") + is_participated: bool | None = Field(None, alias="IsParticipated") + in_progress: bool | None = Field(None, alias="InProgress") class Program(OtfItemBase): - challenge_category_id: int = Field(..., alias="ChallengeCategoryId") - challenge_sub_category_id: int = Field(..., alias="ChallengeSubCategoryId") - challenge_name: str = Field(..., alias="ChallengeName") - years: list[Year] = Field(..., alias="Years") - logo_url: str = Field(..., alias="LogoUrl") + # NOTE: These ones do seem to match the ProgramType enums in the OTF app. + # Leaving them as int for now though in case older data or other user's + # data doesn't match up. + challenge_category_id: int | None = Field(None, alias="ChallengeCategoryId") + challenge_sub_category_id: int | None = Field(None, alias="ChallengeSubCategoryId") + challenge_name: str | None = Field(None, alias="ChallengeName") + years: list[Year] = Field(default_factory=list, alias="Years") class Challenge(OtfItemBase): - challenge_category_id: int = Field(..., alias="ChallengeCategoryId") - challenge_sub_category_id: int = Field(..., alias="ChallengeSubCategoryId") - challenge_name: str = Field(..., alias="ChallengeName") - years: list[Year] = Field(..., alias="Years") - logo_url: str = Field(..., alias="LogoUrl") + # NOTE: The challenge category/subcategory ids here do not seem to be at + # all related to the ChallengeType enums or the few SubCategory enums I've + # been able to puzzle out. I haven't been able to link them to any code + # in the OTF app. Due to that, they are being excluded from the model for now. + challenge_category_id: int | None = Field(None, alias="ChallengeCategoryId", exclude=True, repr=False) + challenge_sub_category_id: int | None = Field(None, alias="ChallengeSubCategoryId", exclude=True, repr=False) + challenge_name: str | None = Field(None, alias="ChallengeName") + years: list[Year] = Field(default_factory=list, alias="Years") class Benchmark(OtfItemBase): - equipment_id: int = Field(..., alias="EquipmentId") - equipment_name: str = Field(..., alias="EquipmentName") - years: list[Year] = Field(..., alias="Years") - logo_url: str = Field(..., alias="LogoUrl") + equipment_id: EquipmentType | None = Field(None, alias="EquipmentId") + equipment_name: str | None = Field(None, alias="EquipmentName") + years: list[Year] = Field(default_factory=list, alias="Years") -class ChallengeTrackerContent(OtfItemBase): - programs: list[Program] = Field(..., alias="Programs") - challenges: list[Challenge] = Field(..., alias="Challenges") - benchmarks: list[Benchmark] = Field(..., alias="Benchmarks") +class ChallengeTracker(OtfItemBase): + programs: list[Program] = Field(default_factory=list, alias="Programs") + challenges: list[Challenge] = Field(default_factory=list, alias="Challenges") + benchmarks: list[Benchmark] = Field(default_factory=list, alias="Benchmarks") diff --git a/src/otf_api/models/challenge_tracker_detail.py b/src/otf_api/models/challenge_tracker_detail.py index 5c3aa1a..ce0dc96 100644 --- a/src/otf_api/models/challenge_tracker_detail.py +++ b/src/otf_api/models/challenge_tracker_detail.py @@ -4,70 +4,87 @@ from pydantic import Field from otf_api.models.base import OtfItemBase +from otf_api.models.enums import EquipmentType class MetricEntry(OtfItemBase): - title: str = Field(..., alias="Title") - equipment_id: int = Field(..., alias="EquipmentId") - entry_type: str = Field(..., alias="EntryType") - metric_key: str = Field(..., alias="MetricKey") - min_value: str = Field(..., alias="MinValue") - max_value: str = Field(..., alias="MaxValue") + title: str | None = Field(None, alias="Title") + equipment_id: EquipmentType | None = Field(None, alias="EquipmentId") + entry_type: str | None = Field(None, alias="EntryType") + metric_key: str | None = Field(None, alias="MetricKey") + min_value: str | None = Field(None, alias="MinValue") + max_value: str | None = Field(None, alias="MaxValue") class BenchmarkHistory(OtfItemBase): - studio_name: str = Field(..., alias="StudioName") - equipment_id: int = Field(..., alias="EquipmentId") - result: float | str = Field(..., alias="Result") - date_created: datetime = Field(..., alias="DateCreated") - date_updated: datetime = Field(..., alias="DateUpdated") - class_time: datetime = Field(..., alias="ClassTime") + studio_name: str | None = Field(None, alias="StudioName") + equipment_id: EquipmentType | None = Field(None, alias="EquipmentId") + class_time: datetime | None = Field(None, alias="ClassTime") challenge_sub_category_id: int | None = Field(None, alias="ChallengeSubCategoryId") - class_id: int = Field(..., alias="ClassId") - substitute_id: int | None = Field(None, alias="SubstituteId") - weight_lbs: int = Field(..., alias="WeightLBS") - class_name: str = Field(..., alias="ClassName") - coach_name: str = Field(..., alias="CoachName") - coach_image_url: str | None = Field(None, alias="CoachImageUrl", exclude=True, repr=False) + weight_lbs: int | None = Field(None, alias="WeightLBS") + class_name: str | None = Field(None, alias="ClassName") + coach_name: str | None = Field(None, alias="CoachName") + result: float | str | None = Field(None, alias="Result") workout_type_id: int | None = Field(None, alias="WorkoutTypeId") workout_id: int | None = Field(None, alias="WorkoutId") linked_challenges: list[Any] | None = Field( None, alias="LinkedChallenges", exclude=True ) # not sure what this will be, never seen it before + date_created: datetime | None = Field( + None, + alias="DateCreated", + exclude=True, + repr=False, + description="When the entry was created in database, not useful to users", + ) + date_updated: datetime | None = Field( + None, + alias="DateUpdated", + exclude=True, + repr=False, + description="When the entry was updated in database, not useful to users", + ) + class_id: int | None = Field(None, alias="ClassId", exclude=True, repr=False, description="Not used by API") + substitute_id: int | None = Field( + None, alias="SubstituteId", exclude=True, repr=False, description="Not used by API, also always seems to be 0" + ) + class ChallengeHistory(OtfItemBase): - challenge_objective: str = Field(..., alias="ChallengeObjective") - challenge_id: int = Field(..., alias="ChallengeId") - studio_id: int = Field(..., alias="StudioId") - studio_name: str = Field(..., alias="StudioName") - start_date: datetime = Field(..., alias="StartDate") - end_date: datetime = Field(..., alias="EndDate") - total_result: float | str = Field(..., alias="TotalResult") - is_finished: bool = Field(..., alias="IsFinished") - benchmark_histories: list[BenchmarkHistory] = Field(..., alias="BenchmarkHistories") + studio_name: str | None = Field(None, alias="StudioName") + start_date: datetime | None = Field(None, alias="StartDate") + end_date: datetime | None = Field(None, alias="EndDate") + total_result: float | str | None = Field(None, alias="TotalResult") + is_finished: bool | None = Field(None, alias="IsFinished") + benchmark_histories: list[BenchmarkHistory] = Field(default_factory=list, alias="BenchmarkHistories") + + challenge_id: int | None = Field(None, alias="ChallengeId", exclude=True, repr=False, description="Not used by API") + studio_id: int | None = Field(None, alias="StudioId", exclude=True, repr=False, description="Not used by API") + challenge_objective: str | None = Field( + None, alias="ChallengeObjective", exclude=True, repr=False, description="Always the string 'None'" + ) class Goal(OtfItemBase): - goal: int | Any = Field(None, alias="Goal") - goal_period: str | Any = Field(None, alias="GoalPeriod") - overall_goal: int | Any = Field(None, alias="OverallGoal") - overall_goal_period: str | Any = Field(None, alias="OverallGoalPeriod") - min_overall: int | Any = Field(None, alias="MinOverall") - min_overall_period: str | Any = Field(None, alias="MinOverallPeriod") + goal: int | None = Field(None, alias="Goal") + goal_period: str | None = Field(None, alias="GoalPeriod") + overall_goal: int | None = Field(None, alias="OverallGoal") + overall_goal_period: str | None = Field(None, alias="OverallGoalPeriod") + min_overall: int | None = Field(None, alias="MinOverall") + min_overall_period: str | None = Field(None, alias="MinOverallPeriod") -class ChallengeTrackerDetail(OtfItemBase): - challenge_category_id: int = Field(..., alias="ChallengeCategoryId") +class FitnessBenchmark(OtfItemBase): + challenge_category_id: int | None = Field(None, alias="ChallengeCategoryId") challenge_sub_category_id: int | None = Field(None, alias="ChallengeSubCategoryId") - equipment_id: int = Field(..., alias="EquipmentId") - equipment_name: str = Field(..., alias="EquipmentName") - metric_entry: MetricEntry = Field(..., alias="MetricEntry") - challenge_name: str = Field(..., alias="ChallengeName") - logo_url: str = Field(..., alias="LogoUrl") - best_record: float | str = Field(..., alias="BestRecord") - last_record: float | str = Field(..., alias="LastRecord") - previous_record: float | str = Field(..., alias="PreviousRecord") + equipment_id: EquipmentType = Field(None, alias="EquipmentId") + equipment_name: str | None = Field(None, alias="EquipmentName") + metric_entry: MetricEntry | None = Field(None, alias="MetricEntry") + challenge_name: str | None = Field(None, alias="ChallengeName") + best_record: float | str | None = Field(None, alias="BestRecord") + last_record: float | str | None = Field(None, alias="LastRecord") + previous_record: float | str | None = Field(None, alias="PreviousRecord") unit: str | None = Field(None, alias="Unit") goals: Goal | None = Field(None, alias="Goals") - challenge_histories: list[ChallengeHistory] = Field(..., alias="ChallengeHistories") + challenge_histories: list[ChallengeHistory] = Field(default_factory=list, alias="ChallengeHistories") diff --git a/src/otf_api/models/classes.py b/src/otf_api/models/classes.py index 2adea0f..8d8a2c1 100644 --- a/src/otf_api/models/classes.py +++ b/src/otf_api/models/classes.py @@ -9,28 +9,28 @@ class OtfClass(OtfItemBase): class_uuid: str = Field(alias="ot_base_class_uuid", description="The OTF class UUID") + name: str | None = Field(None, description="The name of the class") + class_type: ClassType = Field(alias="type") coach: str | None = Field(None, alias=AliasPath("coach", "first_name")) ends_at: datetime = Field( alias="ends_at_local", description="The end time of the class. Reflects local time, but the object does not have a timezone.", ) - name: str | None = Field(None, description="The name of the class") starts_at: datetime = Field( alias="starts_at_local", description="The start time of the class. Reflects local time, but the object does not have a timezone.", ) studio: StudioDetail - class_type: ClassType = Field(alias="type") # capacity/status fields - booking_capacity: int - full: bool + booking_capacity: int | None = None + full: bool | None = None + max_capacity: int | None = None + waitlist_available: bool | None = None + waitlist_size: int | None = None is_booked: bool | None = Field(None, description="Custom helper field to determine if class is already booked") - is_cancelled: bool = Field(alias="canceled") + is_cancelled: bool | None = Field(None, alias="canceled") is_home_studio: bool | None = Field(None, description="Custom helper field to determine if at home studio") - max_capacity: int - waitlist_available: bool - waitlist_size: int # unused fields class_id: str | None = Field(None, alias="id", exclude=True, repr=False, description="Not used by API") diff --git a/src/otf_api/models/enums.py b/src/otf_api/models/enums.py index ee66c00..0179270 100644 --- a/src/otf_api/models/enums.py +++ b/src/otf_api/models/enums.py @@ -125,8 +125,30 @@ class ChallengeType(IntEnum): MarathonMonth = 5 HellWeek = 52 Mayhem = 58 + _BackAtIt = 60 # nothing in the app reflects this, but the API returns it TwelveDaysOfFitness = 63 - Transformation = 64 + TransformationChallenge = 64 RemixInSix = 65 Push = 66 BackAtIt = 84 + + +class DriTriChallengeSubCategory(IntEnum): + FullRun = 1 + SprintRun = 3 + Relay = 4 + StrengthRun = 1500 + + +class MarathonMonthChallengeSubCategory(IntEnum): + Original = 1 + Full = 14 + Half = 15 + Ultra = 16 + + +# only Other, DriTri, and MarathonMonth have subcategories + +# BackAtIt and Transformation are multi-week challenges + +# RemixInSix, Mayhem, HellWeek, Push, and TwelveDaysOfFitness are multi-day challenges diff --git a/src/otf_api/models/latest_agreement.py b/src/otf_api/models/latest_agreement.py deleted file mode 100644 index 7cc45f3..0000000 --- a/src/otf_api/models/latest_agreement.py +++ /dev/null @@ -1,21 +0,0 @@ -from datetime import datetime - -from pydantic import Field - -from otf_api.models.base import OtfItemBase - - -class LatestAgreement(OtfItemBase): - file_url: str = Field(..., alias="fileUrl") - agreement_id: int = Field(..., alias="agreementId") - agreement_uuid: str = Field(..., alias="agreementUUId") - agreement_datetime: datetime = Field(..., alias="agreementDatetime") - agreement_type_id: int = Field(..., alias="agreementTypeId") - platform: None - locale: str - version: str - created_by: str = Field(..., alias="createdBy") - created_date: datetime = Field(..., alias="createdDate") - updated_by: str = Field(..., alias="updatedBy") - updated_date: datetime = Field(..., alias="updatedDate") - is_deleted: bool = Field(..., alias="isDeleted") diff --git a/src/otf_api/models/lifetime_stats.py b/src/otf_api/models/lifetime_stats.py index 6c960fa..0939a63 100644 --- a/src/otf_api/models/lifetime_stats.py +++ b/src/otf_api/models/lifetime_stats.py @@ -8,28 +8,28 @@ class OutStudioMixin(OtfItemBase): - walking_distance: float = Field(..., alias="walkingDistance") - running_distance: float = Field(..., alias="runningDistance") - cycling_distance: float = Field(..., alias="cyclingDistance") + walking_distance: float | None = Field(None, alias="walkingDistance") + running_distance: float | None = Field(None, alias="runningDistance") + cycling_distance: float | None = Field(None, alias="cyclingDistance") class InStudioMixin(OtfItemBase): - treadmill_distance: float = Field(..., alias="treadmillDistance") - treadmill_elevation_gained: float = Field(..., alias="treadmillElevationGained") - rower_distance: float = Field(..., alias="rowerDistance") - rower_watt: float = Field(..., alias="rowerWatt") + treadmill_distance: float | None = Field(None, alias="treadmillDistance") + treadmill_elevation_gained: float | None = Field(None, alias="treadmillElevationGained") + rower_distance: float | None = Field(None, alias="rowerDistance") + rower_watt: float | None = Field(None, alias="rowerWatt") class BaseStatsData(OtfItemBase): - calories: float - splat_point: float = Field(..., alias="splatPoint") - total_black_zone: float = Field(..., alias="totalBlackZone") - total_blue_zone: float = Field(..., alias="totalBlueZone") - total_green_zone: float = Field(..., alias="totalGreenZone") - total_orange_zone: float = Field(..., alias="totalOrangeZone") - total_red_zone: float = Field(..., alias="totalRedZone") - workout_duration: float = Field(..., alias="workoutDuration") - step_count: float = Field(..., alias="stepCount") + calories: float | None = None + splat_point: float | None = Field(None, alias="splatPoint") + total_black_zone: float | None = Field(None, alias="totalBlackZone") + total_blue_zone: float | None = Field(None, alias="totalBlueZone") + total_green_zone: float | None = Field(None, alias="totalGreenZone") + total_orange_zone: float | None = Field(None, alias="totalOrangeZone") + total_red_zone: float | None = Field(None, alias="totalRedZone") + workout_duration: float | None = Field(None, alias="workoutDuration") + step_count: float | None = Field(None, alias="stepCount") class InStudioStatsData(InStudioMixin, BaseStatsData): diff --git a/src/otf_api/models/member_detail.py b/src/otf_api/models/member_detail.py index 0f4e00d..527fc19 100644 --- a/src/otf_api/models/member_detail.py +++ b/src/otf_api/models/member_detail.py @@ -19,8 +19,8 @@ class MemberProfile(OtfItemBase): formula_max_hr: int | None = Field(None, alias="formulaMaxHr") automated_hr: int | None = Field(None, alias="automatedHr") - member_profile_uuid: str = Field(..., alias="memberProfileUUId", exclude=True, repr=False) - member_optin_flow_type_id: int = Field(..., alias="memberOptinFlowTypeId", exclude=True, repr=False) + member_profile_uuid: str | None = Field(None, alias="memberProfileUUId", exclude=True, repr=False) + member_optin_flow_type_id: int | None = Field(None, alias="memberOptinFlowTypeId", exclude=True, repr=False) class MemberClassSummary(OtfItemBase): @@ -51,27 +51,25 @@ class MemberDetail(OtfItemBase): home_studio: StudioDetail profile: MemberProfile = Field(..., alias="memberProfile") class_summary: MemberClassSummary | None = Field(None, alias="memberClassSummary") - addresses: list[Address] | None = None + addresses: list[Address] | None = Field(default_factory=list) - studio_display_name: str = Field( - ..., alias="userName", description="The value that is displayed on tread/rower tablets and OTBeat screens" + studio_display_name: str | None = Field( + None, alias="userName", description="The value that is displayed on tread/rower tablets and OTBeat screens" ) - first_name: str = Field(..., alias="firstName") - last_name: str = Field(..., alias="lastName") - email: str = Field(..., alias="email") - phone_number: str = Field(..., alias="phoneNumber") - birth_day: date = Field(..., alias="birthDay") - gender: str = Field(..., alias="gender") - locale: str = Field(..., alias="locale") - weight: int = Field(..., alias="weight") - weight_units: str = Field(..., alias="weightMeasure") - height: int = Field(..., alias="height") - height_units: str = Field(..., alias="heightMeasure") - - created_date: datetime = Field(..., alias="createdDate") - updated_date: datetime = Field(..., alias="updatedDate") - - # unused fields + first_name: str | None = Field(None, alias="firstName") + last_name: str | None = Field(None, alias="lastName") + email: str | None = Field(None, alias="email") + phone_number: str | None = Field(None, alias="phoneNumber") + birth_day: date | None = Field(None, alias="birthDay") + gender: str | None = Field(None, alias="gender") + locale: str | None = Field(None, alias="locale") + weight: int | None = Field(None, alias="weight") + weight_units: str | None = Field(None, alias="weightMeasure") + height: int | None = Field(None, alias="height") + height_units: str | None = Field(None, alias="heightMeasure") + + # unused fields - leaving these in for now in case someone finds a purpose for them + # but they will potentially (likely?) be removed in the future # mbo fields mbo_id: str | None = Field(None, alias="mboId", exclude=True, repr=False, description="MindBody attr") @@ -89,6 +87,9 @@ class MemberDetail(OtfItemBase): updated_by: str | None = Field(None, alias="updatedBy", exclude=True, repr=False) # unused address/member detail fields + created_date: datetime | None = Field(None, alias="createdDate", exclude=True, repr=False) + updated_date: datetime | None = Field(None, alias="updatedDate", exclude=True, repr=False) + address_line1: str | None = Field(None, alias="addressLine1", exclude=True, repr=False) address_line2: str | None = Field(None, alias="addressLine2", exclude=True, repr=False) alternate_emails: None = Field(None, alias="alternateEmails", exclude=True, repr=False) @@ -106,7 +107,6 @@ class MemberDetail(OtfItemBase): online_signup: None = Field(None, alias="onlineSignup", exclude=True, repr=False) phone_type: None = Field(None, alias="phoneType", exclude=True, repr=False) postal_code: str | None = Field(None, alias="postalCode", exclude=True, repr=False) - profile_picture_url: str | None = Field(None, alias="profilePictureUrl", exclude=True, repr=False) state: str | None = Field(None, exclude=True, repr=False) work_phone: str | None = Field(None, alias="workPhone", exclude=True, repr=False) year_imported: int | None = Field(None, alias="yearImported", exclude=True, repr=False) diff --git a/src/otf_api/models/member_membership.py b/src/otf_api/models/member_membership.py index 744c2ae..fc4b974 100644 --- a/src/otf_api/models/member_membership.py +++ b/src/otf_api/models/member_membership.py @@ -6,20 +6,21 @@ class MemberMembership(OtfItemBase): - member_membership_id: int = Field(..., alias="memberMembershipId") - member_membership_uuid: str = Field(..., alias="memberMembershipUUId") - membership_id: int = Field(..., alias="membershipId") - member_id: int = Field(..., alias="memberId") - payment_date: datetime = Field(..., alias="paymentDate") - active_date: datetime = Field(..., alias="activeDate") - expiration_date: datetime = Field(..., alias="expirationDate") - mbo_description_id: str = Field(..., alias="mboDescriptionId") - current: bool - count: int - remaining: int - name: str - created_by: str = Field(..., alias="createdBy") - created_date: datetime = Field(..., alias="createdDate") - updated_by: str = Field(..., alias="updatedBy") - updated_date: datetime = Field(..., alias="updatedDate") - is_deleted: bool = Field(..., alias="isDeleted") + payment_date: datetime | None = Field(None, alias="paymentDate") + active_date: datetime | None = Field(None, alias="activeDate") + expiration_date: datetime | None = Field(None, alias="expirationDate") + current: bool | None = None + count: int | None = None + remaining: int | None = None + name: str | None = None + updated_date: datetime | None = Field(None, alias="updatedDate") + created_date: datetime | None = Field(None, alias="createdDate") + is_deleted: bool | None = Field(None, alias="isDeleted") + + member_membership_id: int | None = Field(None, alias="memberMembershipId", exclude=True, repr=False) + member_membership_uuid: str | None = Field(None, alias="memberMembershipUUId", exclude=True, repr=False) + membership_id: int | None = Field(None, alias="membershipId", exclude=True, repr=False) + member_id: int | None = Field(None, alias="memberId", exclude=True, repr=False) + mbo_description_id: str | None = Field(None, alias="mboDescriptionId", exclude=True, repr=False) + created_by: str | None = Field(None, alias="createdBy", exclude=True, repr=False) + updated_by: str | None = Field(None, alias="updatedBy", exclude=True, repr=False) diff --git a/src/otf_api/models/member_purchases.py b/src/otf_api/models/member_purchases.py index 9a56dd8..1ee6d5e 100644 --- a/src/otf_api/models/member_purchases.py +++ b/src/otf_api/models/member_purchases.py @@ -8,12 +8,12 @@ class MemberPurchase(OtfItemBase): purchase_uuid: str = Field(..., alias="memberPurchaseUUId") - name: str - price: str - purchase_date_time: datetime = Field(..., alias="memberPurchaseDateTime") - purchase_type: str = Field(..., alias="memberPurchaseType") - status: str - quantity: int + name: str | None = None + price: str | None = None + purchase_date_time: datetime | None = Field(None, alias="memberPurchaseDateTime") + purchase_type: str | None = Field(None, alias="memberPurchaseType") + status: str | None = None + quantity: int | None = None studio: StudioDetail = Field(..., exclude=True, repr=False) member_fee_id: int | None = Field(None, alias="memberFeeId", exclude=True, repr=False) diff --git a/src/otf_api/models/out_of_studio_workout_history.py b/src/otf_api/models/out_of_studio_workout_history.py index 8ebc583..fdb3431 100644 --- a/src/otf_api/models/out_of_studio_workout_history.py +++ b/src/otf_api/models/out_of_studio_workout_history.py @@ -1,37 +1,32 @@ from datetime import datetime -from pydantic import Field +from pydantic import AliasPath, Field from otf_api.models.base import OtfItemBase -class WorkoutType(OtfItemBase): - id: int - display_name: str = Field(..., alias="displayName") - icon: str - - class OutOfStudioWorkoutHistory(OtfItemBase): - workout_date: datetime = Field(..., alias="workoutDate") - start_time: datetime = Field(..., alias="startTime") - end_time: datetime = Field(..., alias="endTime") - duration_unit: str = Field(..., alias="durationUnit") - duration: float - total_calories: int = Field(..., alias="totalCalories") - hr_percent_max: int = Field(..., alias="hrPercentMax") - distance_unit: str = Field(..., alias="distanceUnit") - total_distance: float = Field(..., alias="totalDistance") - splat_points: int = Field(..., alias="splatPoints") - target_heart_rate: int = Field(..., alias="targetHeartRate") - red_zone_seconds: int = Field(..., alias="redZoneSeconds") - orange_zone_seconds: int = Field(..., alias="orangeZoneSeconds") - green_zone_seconds: int = Field(..., alias="greenZoneSeconds") - blue_zone_seconds: int = Field(..., alias="blueZoneSeconds") - grey_zone_seconds: int = Field(..., alias="greyZoneSeconds") - total_steps: int = Field(..., alias="totalSteps") - has_detailed_data: bool = Field(..., alias="hasDetailedData") - workout_type: WorkoutType = Field(..., alias="workoutType") member_uuid: str = Field(..., alias="memberUUId") workout_uuid: str = Field(..., alias="workoutUUId") - avg_heartrate: int = Field(..., alias="avgHeartrate") - max_heartrate: int = Field(..., alias="maxHeartrate") + + workout_date: datetime | None = Field(None, alias="workoutDate") + start_time: datetime | None = Field(None, alias="startTime") + end_time: datetime | None = Field(None, alias="endTime") + duration: float | None = None + duration_unit: str | None = Field(None, alias="durationUnit") + total_calories: int | None = Field(None, alias="totalCalories") + hr_percent_max: int | None = Field(None, alias="hrPercentMax") + distance_unit: str | None = Field(None, alias="distanceUnit") + total_distance: float | None = Field(None, alias="totalDistance") + splat_points: int | None = Field(None, alias="splatPoints") + target_heart_rate: int | None = Field(None, alias="targetHeartRate") + total_steps: int | None = Field(None, alias="totalSteps") + has_detailed_data: bool | None = Field(None, alias="hasDetailedData") + avg_heartrate: int | None = Field(None, alias="avgHeartrate") + max_heartrate: int | None = Field(None, alias="maxHeartrate") + workout_type: str | None = Field(None, alias=AliasPath("workoutType", "displayName")) + red_zone_seconds: int | None = Field(None, alias="redZoneSeconds") + orange_zone_seconds: int | None = Field(None, alias="orangeZoneSeconds") + green_zone_seconds: int | None = Field(None, alias="greenZoneSeconds") + blue_zone_seconds: int | None = Field(None, alias="blueZoneSeconds") + grey_zone_seconds: int | None = Field(None, alias="greyZoneSeconds") diff --git a/src/otf_api/models/performance_summary_detail.py b/src/otf_api/models/performance_summary_detail.py index a7ccf98..f9c76ea 100644 --- a/src/otf_api/models/performance_summary_detail.py +++ b/src/otf_api/models/performance_summary_detail.py @@ -67,27 +67,18 @@ class Rower(BaseEquipment): max_cadence: PerformanceMetric -class EquipmentData(OtfItemBase): - treadmill: Treadmill - rower: Rower - - -class Class(OtfItemBase): - starts_at: datetime = Field(..., alias="starts_at_local") - name: str - - class PerformanceSummaryDetail(OtfItemBase): id: str - otf_class: Class = Field(..., alias="class") - - ratable: bool - calories_burned: int = Field(..., alias=AliasPath("details", "calories_burned")) - splat_points: int = Field(..., alias=AliasPath("details", "splat_points")) - step_count: int = Field(..., alias=AliasPath("details", "step_count")) - active_time_seconds: int = Field(..., alias=AliasPath("details", "active_time_seconds")) - zone_time_minutes: ZoneTimeMinutes = Field(..., alias=AliasPath("details", "zone_time_minutes")) - heart_rate: HeartRate = Field(..., alias=AliasPath("details", "heart_rate")) - - rower_data: Rower = Field(..., alias=AliasPath("details", "equipment_data", "rower")) - treadmill_data: Treadmill = Field(..., alias=AliasPath("details", "equipment_data", "treadmill")) + class_name: str | None = Field(None, alias=AliasPath("class", "name")) + class_starts_at: datetime | None = Field(None, alias=AliasPath("class", "starts_at_local")) + + ratable: bool | None = None + calories_burned: int | None = Field(None, alias=AliasPath("details", "calories_burned")) + splat_points: int | None = Field(None, alias=AliasPath("details", "splat_points")) + step_count: int | None = Field(None, alias=AliasPath("details", "step_count")) + active_time_seconds: int | None = Field(None, alias=AliasPath("details", "active_time_seconds")) + zone_time_minutes: ZoneTimeMinutes | None = Field(None, alias=AliasPath("details", "zone_time_minutes")) + heart_rate: HeartRate | None = Field(None, alias=AliasPath("details", "heart_rate")) + + rower_data: Rower | None = Field(None, alias=AliasPath("details", "equipment_data", "rower")) + treadmill_data: Treadmill | None = Field(None, alias=AliasPath("details", "equipment_data", "treadmill")) diff --git a/src/otf_api/models/performance_summary_list.py b/src/otf_api/models/performance_summary_list.py index d0949de..34dfd04 100644 --- a/src/otf_api/models/performance_summary_list.py +++ b/src/otf_api/models/performance_summary_list.py @@ -39,7 +39,7 @@ class PerformanceSummaryEntry(OtfItemBase): step_count: int | None = Field(None, alias=AliasPath("details", "step_count")) active_time_seconds: int | None = Field(None, alias=AliasPath("details", "active_time_seconds")) zone_time_minutes: ZoneTimeMinutes | None = Field(None, alias=AliasPath("details", "zone_time_minutes")) - ratable: bool + ratable: bool | None = None otf_class: Class | None = Field(None, alias="class") coach: str | None = Field(None, alias=AliasPath("class", "coach", "first_name")) coach_rating: CoachRating | None = Field(None, alias=AliasPath("ratings", "coach")) diff --git a/src/otf_api/models/studio_services.py b/src/otf_api/models/studio_services.py index eddec72..8549758 100644 --- a/src/otf_api/models/studio_services.py +++ b/src/otf_api/models/studio_services.py @@ -8,17 +8,16 @@ class StudioService(OtfItemBase): studio: StudioDetail = Field(..., exclude=True, repr=False) - service_uuid: str = Field(..., alias="serviceUUId") - name: str - price: str - qty: int - online_price: str = Field(..., alias="onlinePrice") - tax_rate: str = Field(..., alias="taxRate") - current: bool - is_deleted: bool = Field(..., alias="isDeleted") - created_date: datetime = Field(..., alias="createdDate") - updated_date: datetime = Field(..., alias="updatedDate") + name: str | None = None + price: str | None = None + qty: int | None = None + online_price: str | None = Field(None, alias="onlinePrice") + tax_rate: str | None = Field(None, alias="taxRate") + current: bool | None = None + is_deleted: bool | None = Field(None, alias="isDeleted") + created_date: datetime | None = Field(None, alias="createdDate") + updated_date: datetime | None = Field(None, alias="updatedDate") # unused fields diff --git a/src/otf_api/models/telemetry.py b/src/otf_api/models/telemetry.py index 351598e..be4d133 100644 --- a/src/otf_api/models/telemetry.py +++ b/src/otf_api/models/telemetry.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta from typing import Any -from pydantic import Field +from pydantic import AliasPath, Field from otf_api.models.base import OtfItemBase @@ -50,13 +50,22 @@ class TelemetryItem(OtfItemBase): class Telemetry(OtfItemBase): member_uuid: str = Field(..., alias="memberUuid") class_history_uuid: str = Field(..., alias="classHistoryUuid") - class_start_time: datetime = Field(..., alias="classStartTime") - max_hr: int = Field(..., alias="maxHr") + class_start_time: datetime | None = Field(None, alias="classStartTime") + max_hr: int | None = Field(None, alias="maxHr") zones: Zones - window_size: int = Field(..., alias="windowSize") - telemetry: list[TelemetryItem] + window_size: int | None = Field(None, alias="windowSize") + telemetry: list[TelemetryItem] = Field(default_factory=list) def __init__(self, **data: dict[str, Any]): super().__init__(**data) for telem in self.telemetry: telem.timestamp = self.class_start_time + timedelta(seconds=telem.relative_timestamp) + + +class TelemetryHistoryItem(OtfItemBase): + max_hr_type: str | None = Field(None, alias=AliasPath("maxHr", "type")) + max_hr_value: int | None = Field(None, alias=AliasPath("maxHr", "value")) + zones: Zones | None = None + change_from_previous: int | None = Field(None, alias="changeFromPrevious") + change_bucket: str | None = Field(None, alias="changeBucket") + assigned_at: datetime | None = Field(None, alias="assignedAt") diff --git a/src/otf_api/models/telemetry_hr_history.py b/src/otf_api/models/telemetry_hr_history.py deleted file mode 100644 index d163cee..0000000 --- a/src/otf_api/models/telemetry_hr_history.py +++ /dev/null @@ -1,34 +0,0 @@ -from pydantic import Field - -from otf_api.models.base import OtfItemBase - - -class MaxHr(OtfItemBase): - type: str - value: int - - -class Zone(OtfItemBase): - start_bpm: int = Field(..., alias="startBpm") - end_bpm: int = Field(..., alias="endBpm") - - -class Zones(OtfItemBase): - gray: Zone - blue: Zone - green: Zone - orange: Zone - red: Zone - - -class HistoryItem(OtfItemBase): - max_hr: MaxHr = Field(..., alias="maxHr") - zones: Zones - change_from_previous: int = Field(..., alias="changeFromPrevious") - change_bucket: str = Field(..., alias="changeBucket") - assigned_at: str = Field(..., alias="assignedAt") - - -# class TelemetryHrHistory(OtfItemBase): -# member_uuid: str = Field(..., alias="memberUuid") -# history: list[HistoryItem] diff --git a/src/otf_api/models/telemetry_max_hr.py b/src/otf_api/models/telemetry_max_hr.py deleted file mode 100644 index 32daa43..0000000 --- a/src/otf_api/models/telemetry_max_hr.py +++ /dev/null @@ -1,13 +0,0 @@ -from pydantic import Field - -from otf_api.models.base import OtfItemBase - - -class MaxHr(OtfItemBase): - type: str - value: int - - -class TelemetryMaxHr(OtfItemBase): - member_uuid: str = Field(..., alias="memberUuid") - max_hr: MaxHr = Field(..., alias="maxHr")