From 970a519e300f6904d9ee86bb69f1c97c81778bd0 Mon Sep 17 00:00:00 2001 From: Jessica Smith <12jessicasmith34@gmail.com> Date: Tue, 13 Aug 2024 17:54:45 -0500 Subject: [PATCH] halfway --- examples/challenge_tracker_examples.py | 14 +- src/otf_api/api.py | 16 +- src/otf_api/auth.py | 3 +- src/otf_api/models/base.py | 44 +++ .../models/responses/body_composition_list.py | 296 +++++++++--------- .../responses/challenge_tracker_detail.py | 109 +++---- src/otf_api/models/responses/enums.py | 2 +- 7 files changed, 284 insertions(+), 200 deletions(-) diff --git a/examples/challenge_tracker_examples.py b/examples/challenge_tracker_examples.py index bb40a4f..b3880a7 100644 --- a/examples/challenge_tracker_examples.py +++ b/examples/challenge_tracker_examples.py @@ -1,15 +1,25 @@ import asyncio +import json import os +import stackprinter +from attr import asdict from otf_api import Otf +from otf_api.api import c from otf_api.models.responses import ChallengeType, EquipmentType +stackprinter.set_excepthook(style="darkbg2") # for jupyter notebooks try style='lightbg' + USERNAME = os.getenv("OTF_EMAIL") PASSWORD = os.getenv("OTF_PASSWORD") +DEVICE_KEY = os.getenv("OTF_DEVICE_KEY") async def main(): - otf = Otf(USERNAME, PASSWORD) + otf = Otf(USERNAME, PASSWORD, device_key=DEVICE_KEY) + + body_comp = await otf.get_body_composition_list() + print(json.dumps(c.unstructure(body_comp.data[0]), indent=4)) # challenge tracker content is an overview of the challenges OTF runs # and your participation in them @@ -53,7 +63,7 @@ async 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 = await otf.get_challenge_tracker_detail(EquipmentType.Treadmill, ChallengeType.Other) - print(tread_challenge_details.details[0].model_dump_json(indent=4)) + print(json.dumps(asdict(tread_challenge_details.details[0]), indent=4, default=str)) """ { diff --git a/src/otf_api/api.py b/src/otf_api/api.py index 6fef960..4af9ef1 100644 --- a/src/otf_api/api.py +++ b/src/otf_api/api.py @@ -7,7 +7,11 @@ from typing import Any import aiohttp +import cattrs import requests +from attrs import has +from cattrs.gen import make_dict_structure_fn +from cattrs.strategies import use_class_methods from loguru import logger from yarl import URL @@ -46,6 +50,12 @@ WorkoutList, ) +c = cattrs.Converter() +c.register_structure_hook(str | float, lambda v, _: v) +c.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v)) +c.register_structure_hook_factory(has, lambda cl: make_dict_structure_fn(cl, c, _cattrs_use_alias=True)) +use_class_methods(c, "_structure", "_unstructure") + class AlreadyBookedError(Exception): pass @@ -558,7 +568,9 @@ async def get_challenge_tracker_detail( data = await self._default_request("GET", f"/challenges/v3/member/{self._member_id}/benchmarks", params=params) - return ChallengeTrackerDetailList(details=data["Dto"]) + retval = c.structure({"details": data["Dto"]}, ChallengeTrackerDetailList) + return retval + # return ChallengeTrackerDetailList(details=data["Dto"]) async def get_challenge_tracker_participation(self, challenge_type_id: ChallengeType) -> typing.Any: """Get the member's participation in a challenge. @@ -943,7 +955,7 @@ async def get_body_composition_list(self) -> BodyCompositionList: """ data = await self._default_request("GET", f"/member/members/{self._member_uuid}/body-composition") - return BodyCompositionList(data=data["data"]) + return c.structure(data, BodyCompositionList) def active_time_to_data_points(active_time: int) -> float: diff --git a/src/otf_api/auth.py b/src/otf_api/auth.py index ce11d9d..b5185af 100644 --- a/src/otf_api/auth.py +++ b/src/otf_api/auth.py @@ -63,7 +63,8 @@ def device_key(self) -> str | None: @device_key.setter def device_key(self, value: str | None): if not value: - logger.info("Clearing device key") + if self._device_key: + logger.info("Clearing device key") self._device_key = value return diff --git a/src/otf_api/models/base.py b/src/otf_api/models/base.py index 2e6aa2c..44034b6 100644 --- a/src/otf_api/models/base.py +++ b/src/otf_api/models/base.py @@ -1,8 +1,13 @@ import inspect import typing +from datetime import datetime from enum import Enum +from logging import getLogger from typing import Any, ClassVar, TypeVar +import attrs +import cattrs +import cattrs.strategies from box import Box from inflection import humanize from pydantic import BaseModel, ConfigDict @@ -14,6 +19,45 @@ from pydantic.main import IncEx T = TypeVar("T", bound="OtfItemBase") +converter = cattrs.Converter() +converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v)) +converter.register_structure_hook_factory( + attrs.has, lambda cl: cattrs.gen.make_dict_structure_fn(cl, converter, _cattrs_use_alias=True) +) +cattrs.strategies.use_class_methods(converter, "_structure", "_unstructure") + +logger = getLogger(__name__) + + +class AttrsMixin: + _attrs_converter: ClassVar[cattrs.Converter] = converter + + def _unstructure(self): + data = attrs.asdict(self) + + for f in attrs.fields(type(self)): + if f.metadata.get("exclude"): + data.pop(f.name, None) + + return data + + @classmethod + def _structure(cls, data: dict): + for f in attrs.fields(cls): + if attrs.has(f.type): + sub_data = cls._attrs_converter.structure(data, f.type) + data[f.alias] = sub_data + for k in attrs.fields(f.type): + data.pop(k.alias, None) + elif f.alias == "member_id": + data[f.alias] = str(data.pop(f.alias)) + + known_data = {k: v for k, v in data.items() if k in [f.alias for f in attrs.fields(cls)]} + unknown_data = {k: v for k, v in data.items() if k not in known_data} + + logger.debug(f"Unknown data for {cls.__name__}: {unknown_data}") + + return cls(**known_data) class BetterDumperMixin: diff --git a/src/otf_api/models/responses/body_composition_list.py b/src/otf_api/models/responses/body_composition_list.py index bf4e55b..149b037 100644 --- a/src/otf_api/models/responses/body_composition_list.py +++ b/src/otf_api/models/responses/body_composition_list.py @@ -1,9 +1,12 @@ -import inspect from datetime import datetime from enum import Enum +import cattrs import pint -from pydantic import BaseModel, ConfigDict, Field, field_validator +from attr import asdict +from attrs import define, field, fields, has +from cattrs.gen import make_dict_structure_fn +from cattrs.strategies._class_methods import use_class_methods ureg = pint.UnitRegistry() @@ -98,158 +101,170 @@ def get_body_fat_percent_dividers_female(age: int) -> list[float]: return [0.0, 0.0, 0.0, 0.0] -class LeanBodyMass(BaseModel): - model_config: ConfigDict = ConfigDict(extra="ignore") - left_arm: float = Field(..., alias="lbmOfLeftArm") - left_leg: float = Field(..., alias="lbmOfLeftLeg") - right_arm: float = Field(..., alias="lbmOfRightArm") - right_leg: float = Field(..., alias="lbmOfRightLeg") - trunk: float = Field(..., alias="lbmOfTrunk") - - -class LeanBodyMassPercent(BaseModel): - model_config: ConfigDict = ConfigDict(extra="ignore") - left_arm: float = Field(..., alias="lbmPercentOfLeftArm") - left_leg: float = Field(..., alias="lbmPercentOfLeftLeg") - right_arm: float = Field(..., alias="lbmPercentOfRightArm") - right_leg: float = Field(..., alias="lbmPercentOfRightLeg") - trunk: float = Field(..., alias="lbmPercentOfTrunk") - - -class BodyFatMass(BaseModel): - model_config: ConfigDict = ConfigDict(extra="ignore") - control: float = Field(..., alias="bfmControl") - left_arm: float = Field(..., alias="bfmOfLeftArm") - left_leg: float = Field(..., alias="bfmOfLeftLeg") - right_arm: float = Field(..., alias="bfmOfRightArm") - right_leg: float = Field(..., alias="bfmOfRightLeg") - trunk: float = Field(..., alias="bfmOfTrunk") - - -class BodyFatMassPercent(BaseModel): - model_config: ConfigDict = ConfigDict(extra="ignore") - left_arm: float = Field(..., alias="bfmPercentOfLeftArm") - left_leg: float = Field(..., alias="bfmPercentOfLeftLeg") - right_arm: float = Field(..., alias="bfmPercentOfRightArm") - right_leg: float = Field(..., alias="bfmPercentOfRightLeg") - trunk: float = Field(..., alias="bfmPercentOfTrunk") - - -class TotalBodyWeight(BaseModel): - model_config: ConfigDict = ConfigDict(extra="ignore") - right_arm: float = Field(..., alias="tbwOfRightArm") - left_arm: float = Field(..., alias="tbwOfLeftArm") - trunk: float = Field(..., alias="tbwOfTrunk") - right_leg: float = Field(..., alias="tbwOfRightLeg") - left_leg: float = Field(..., alias="tbwOfLeftLeg") - - -class IntraCellularWater(BaseModel): - model_config: ConfigDict = ConfigDict(extra="ignore") - right_arm: float = Field(..., alias="icwOfRightArm") - left_arm: float = Field(..., alias="icwOfLeftArm") - trunk: float = Field(..., alias="icwOfTrunk") - right_leg: float = Field(..., alias="icwOfRightLeg") - left_leg: float = Field(..., alias="icwOfLeftLeg") - - -class ExtraCellularWater(BaseModel): - model_config: ConfigDict = ConfigDict(extra="ignore") - right_arm: float = Field(..., alias="ecwOfRightArm") - left_arm: float = Field(..., alias="ecwOfLeftArm") - trunk: float = Field(..., alias="ecwOfTrunk") - right_leg: float = Field(..., alias="ecwOfRightLeg") - left_leg: float = Field(..., alias="ecwOfLeftLeg") - - -class ExtraCellularWaterOverTotalBodyWater(BaseModel): - model_config: ConfigDict = ConfigDict(extra="ignore") - right_arm: float = Field(..., alias="ecwOverTBWOfRightArm") - left_arm: float = Field(..., alias="ecwOverTBWOfLeftArm") - trunk: float = Field(..., alias="ecwOverTBWOfTrunk") - right_leg: float = Field(..., alias="ecwOverTBWOfRightLeg") - left_leg: float = Field(..., alias="ecwOverTBWOfLeftLeg") - - -class BodyCompositionData(BaseModel): - member_uuid: str = Field(..., alias="memberUUId") - member_id: str = Field(..., alias="memberId") - scan_result_uuid: str = Field(..., alias="scanResultUUId") - inbody_id: str = Field(..., alias="id", exclude=True, description="InBody ID, same as email address") +@define +class LeanBodyMass: + left_arm: float = field(alias="lbmOfLeftArm") + left_leg: float = field(alias="lbmOfLeftLeg") + right_arm: float = field(alias="lbmOfRightArm") + right_leg: float = field(alias="lbmOfRightLeg") + trunk: float = field(alias="lbmOfTrunk") + + +@define +class LeanBodyMassPercent: + left_arm: float = field(alias="lbmPercentOfLeftArm") + left_leg: float = field(alias="lbmPercentOfLeftLeg") + right_arm: float = field(alias="lbmPercentOfRightArm") + right_leg: float = field(alias="lbmPercentOfRightLeg") + trunk: float = field(alias="lbmPercentOfTrunk") + + +@define +class BodyFatMass: + control: float = field(alias="bfmControl") + left_arm: float = field(alias="bfmOfLeftArm") + left_leg: float = field(alias="bfmOfLeftLeg") + right_arm: float = field(alias="bfmOfRightArm") + right_leg: float = field(alias="bfmOfRightLeg") + trunk: float = field(alias="bfmOfTrunk") + + +@define +class BodyFatMassPercent: + left_arm: float = field(alias="bfmPercentOfLeftArm") + left_leg: float = field(alias="bfmPercentOfLeftLeg") + right_arm: float = field(alias="bfmPercentOfRightArm") + right_leg: float = field(alias="bfmPercentOfRightLeg") + trunk: float = field(alias="bfmPercentOfTrunk") + + +@define +class TotalBodyWeight: + right_arm: float = field(alias="tbwOfRightArm") + left_arm: float = field(alias="tbwOfLeftArm") + trunk: float = field(alias="tbwOfTrunk") + right_leg: float = field(alias="tbwOfRightLeg") + left_leg: float = field(alias="tbwOfLeftLeg") + + +@define +class IntraCellularWater: + right_arm: float = field(alias="icwOfRightArm") + left_arm: float = field(alias="icwOfLeftArm") + trunk: float = field(alias="icwOfTrunk") + right_leg: float = field(alias="icwOfRightLeg") + left_leg: float = field(alias="icwOfLeftLeg") + + +@define +class ExtraCellularWater: + right_arm: float = field(alias="ecwOfRightArm") + left_arm: float = field(alias="ecwOfLeftArm") + trunk: float = field(alias="ecwOfTrunk") + right_leg: float = field(alias="ecwOfRightLeg") + left_leg: float = field(alias="ecwOfLeftLeg") + + +@define() +class ExtraCellularWaterOverTotalBodyWater: + right_arm: float = field(alias="ecwOverTBWOfRightArm") + left_arm: float = field(alias="ecwOverTBWOfLeftArm") + trunk: float = field(alias="ecwOverTBWOfTrunk") + right_leg: float = field(alias="ecwOverTBWOfRightLeg") + left_leg: float = field(alias="ecwOverTBWOfLeftLeg") + + +@define +class BodyCompositionData: + member_uuid: str = field(alias="memberUUId") + member_id: str = field(alias="memberId") + scan_result_uuid: str = field(alias="scanResultUUId") + inbody_id: str = field(alias="id", metadata={"description": "InBody ID, same as email address"}) email: str - height: str = Field(..., description="Height in cm") + height: str = field(metadata={"description": "Height in cm"}) gender: Gender age: int - scan_datetime: datetime = Field(..., alias="testDatetime") - provided_weight: float = Field( - ..., alias="weight", description="Weight in pounds, provided by member at time of scan" + scan_datetime: datetime = field(alias="testDatetime") + provided_weight: float = field( + alias="weight", metadata={"description": "Weight in pounds, provided by member at time of scan"} ) lean_body_mass_details: LeanBodyMass lean_body_mass_percent_details: LeanBodyMassPercent - total_body_weight: float = Field(..., alias="tbw", description="Total body weight in pounds, based on scan results") - dry_lean_mass: float = Field(..., alias="dlm") - body_fat_mass: float = Field(..., alias="bfm") - lean_body_mass: float = Field(..., alias="lbm") - skeletal_muscle_mass: float = Field(..., alias="smm") - body_mass_index: float = Field(..., alias="bmi") - percent_body_fat: float = Field(..., alias="pbf") - basal_metabolic_rate: float = Field(..., alias="bmr") - in_body_type: str = Field(..., alias="inBodyType") - - body_fat_mass: float = Field(..., alias="bfm") - skeletal_muscle_mass: float = Field(..., alias="smm") + total_body_weight: float = field( + alias="tbw", metadata={"description": "Total body weight in pounds, based on scan results"} + ) + dry_lean_mass: float = field(alias="dlm") + body_fat_mass: float = field(alias="bfm") + lean_body_mass: float = field(alias="lbm") + skeletal_muscle_mass: float = field(alias="smm") + body_mass_index: float = field(alias="bmi") + percent_body_fat: float = field(alias="pbf") + basal_metabolic_rate: float = field(alias="bmr") + in_body_type: str = field(alias="inBodyType") + + body_fat_mass: float = field(alias="bfm") + skeletal_muscle_mass: float = field(alias="smm") # excluded because they are only useful for end result of calculations - body_fat_mass_dividers: list[float] = Field(..., alias="bfmGraphScale", exclude=True) - body_fat_mass_plot_point: float = Field(..., alias="pfatnew", exclude=True) - skeletal_muscle_mass_dividers: list[float] = Field(..., alias="smmGraphScale", exclude=True) - skeletal_muscle_mass_plot_point: float = Field(..., alias="psmm", exclude=True) - weight_dividers: list[float] = Field(..., alias="wtGraphScale", exclude=True) - weight_plot_point: float = Field(..., alias="pwt", exclude=True) + body_fat_mass_dividers: list[float] = field(alias="bfmGraphScale", metadata={"exclude": True}) + body_fat_mass_plot_point: float = field(alias="pfatnew", metadata={"exclude": True}) + skeletal_muscle_mass_dividers: list[float] = field(alias="smmGraphScale", metadata={"exclude": True}) + skeletal_muscle_mass_plot_point: float = field(alias="psmm", metadata={"exclude": True}) + weight_dividers: list[float] = field(alias="wtGraphScale", metadata={"exclude": True}) + weight_plot_point: float = field(alias="pwt", metadata={"exclude": True}) # excluded due to 0 values - body_fat_mass_details: BodyFatMass = Field(..., exclude=True) - body_fat_mass_percent_details: BodyFatMassPercent = Field(..., exclude=True) - total_body_weight_details: TotalBodyWeight = Field(..., exclude=True) - intra_cellular_water_details: IntraCellularWater = Field(..., exclude=True) - extra_cellular_water_details: ExtraCellularWater = Field(..., exclude=True) - extra_cellular_water_over_total_body_water_details: ExtraCellularWaterOverTotalBodyWater = Field(..., exclude=True) - visceral_fat_level: float = Field(..., alias="vfl", exclude=True) - visceral_fat_area: float = Field(..., alias="vfa", exclude=True) - body_comp_measurement: float = Field(..., alias="bcm", exclude=True) - total_body_weight_over_lean_body_mass: float = Field(..., alias="tbwOverLBM", exclude=True) - intracellular_water: float = Field(..., alias="icw", exclude=True) - extracellular_water: float = Field(..., alias="ecw", exclude=True) - lean_body_mass_control: float = Field(..., alias="lbmControl", exclude=True) - - def __init__(self, **data): - # populate child models - child_model_dict = { - k: v.annotation - for k, v in self.model_fields.items() - if inspect.isclass(v.annotation) and issubclass(v.annotation, BaseModel) - } - for k, v in child_model_dict.items(): - data[k] = v(**data) - - super().__init__(**data) - - @field_validator("member_id", mode="before") - @classmethod - def int_to_str(cls, v: int): - return str(v) + body_fat_mass_details: BodyFatMass = field(metadata={"exclude": True}) + body_fat_mass_percent_details: BodyFatMassPercent = field(metadata={"exclude": True}) + total_body_weight_details: TotalBodyWeight = field(metadata={"exclude": True}) + intra_cellular_water_details: IntraCellularWater = field(metadata={"exclude": True}) + extra_cellular_water_details: ExtraCellularWater = field(metadata={"exclude": True}) + extra_cellular_water_over_total_body_water_details: ExtraCellularWaterOverTotalBodyWater = field( + alias="ecwOverTBW", metadata={"exclude": True} + ) + visceral_fat_level: float = field(alias="vfl", metadata={"exclude": True}) + visceral_fat_area: float = field(alias="vfa", metadata={"exclude": True}) + body_comp_measurement: float = field(alias="bcm", metadata={"exclude": True}) + total_body_weight_over_lean_body_mass: float = field(alias="tbwOverLBM") + intracellular_water: float = field(alias="icw", metadata={"exclude": True}) + extracellular_water: float = field(alias="ecw", metadata={"exclude": True}) + lean_body_mass_control: float = field(alias="lbmControl", metadata={"exclude": True}) - @field_validator("skeletal_muscle_mass_dividers", "weight_dividers", "body_fat_mass_dividers", mode="before") - @classmethod - def convert_dividers_to_float_list(cls, v: str): - return [float(i) for i in v.split(";")] + def _unstructure(self): + data = asdict(self) + + for f in fields(type(self)): + if f.metadata.get("exclude"): + data.pop(f.name, None) + + return data - @field_validator("total_body_weight", mode="before") @classmethod - def convert_body_weight_from_kg_to_pounds(cls, v: float): - return ureg.Quantity(v, ureg.kilogram).to(ureg.pound).magnitude + def _structure(cls, data: dict): + c = cattrs.Converter() + c.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v)) + c.register_structure_hook_factory(has, lambda cl: make_dict_structure_fn(cl, c, _cattrs_use_alias=True)) + use_class_methods(c, "_structure", "_unstructure") + + for f in fields(cls): + if has(f.type): + sub_data = c.structure(data, f.type) + data[f.alias] = sub_data + for k in fields(f.type): + data.pop(k.alias, None) + elif f.alias in ["wtGraphScale", "smmGraphScale", "bfmGraphScale"]: + data[f.alias] = [float(i) for i in data[f.alias].split(";")] + elif f.alias == "tbw": + val = data.pop(f.alias, None) + data[f.alias] = ureg.Quantity(val, ureg.kilogram).to(ureg.pound).magnitude + elif f.alias == "member_id": + data[f.alias] = str(data.pop(f.alias)) + + known_data = {k: v for k, v in data.items() if k in [f.alias for f in fields(cls)]} + + return cls(**known_data) @property def body_fat_mass_relative_descriptor(self) -> AverageType: @@ -300,5 +315,6 @@ def body_fat_percent_relative_descriptor(self) -> BodyFatPercentIndicator: ) -class BodyCompositionList(BaseModel): +@define +class BodyCompositionList: data: list[BodyCompositionData] diff --git a/src/otf_api/models/responses/challenge_tracker_detail.py b/src/otf_api/models/responses/challenge_tracker_detail.py index 38b62d5..1d0f61b 100644 --- a/src/otf_api/models/responses/challenge_tracker_detail.py +++ b/src/otf_api/models/responses/challenge_tracker_detail.py @@ -1,68 +1,69 @@ from datetime import datetime from typing import Any -from pydantic import Field +from attrs import define, field -from otf_api.models.base import OtfItemBase +@define +class MetricEntry: + 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") -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") +@define +class BenchmarkHistory: + 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") + challenge_sub_category_id: None = field(alias="ChallengeSubCategoryId") + class_id: int = field(alias="ClassId") + substitute_id: int | None = field(alias="SubstituteId") + weight_lbs: int = field(alias="WeightLBS") + class_name: str = field(alias="ClassName") + coach_name: str = field(alias="CoachName") + coach_image_url: str = field(alias="CoachImageUrl") + workout_type_id: None = field(alias="WorkoutTypeId") + workout_id: None = field(alias="WorkoutId") + linked_challenges: list[Any] = field(alias="LinkedChallenges") # not sure what this will be, never seen it before -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") - challenge_sub_category_id: None = Field(..., alias="ChallengeSubCategoryId") - class_id: int = Field(..., alias="ClassId") - substitute_id: int | None = Field(..., alias="SubstituteId") - weight_lbs: int = Field(..., alias="WeightLBS") - class_name: str = Field(..., alias="ClassName") - coach_name: str = Field(..., alias="CoachName") - coach_image_url: str = Field(..., alias="CoachImageUrl") - workout_type_id: None = Field(..., alias="WorkoutTypeId") - workout_id: None = Field(..., alias="WorkoutId") - linked_challenges: list[Any] = Field( - ..., alias="LinkedChallenges" - ) # not sure what this will be, never seen it before +@define +class ChallengeHistory: + 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") -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") +@define +class ChallengeTrackerDetail: + challenge_category_id: int = field(alias="ChallengeCategoryId") + challenge_sub_category_id: None = field(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") + unit: str | None = field(alias="Unit") + goals: None = field(alias="Goals") + challenge_histories: list[ChallengeHistory] = field(alias="ChallengeHistories") -class ChallengeTrackerDetail(OtfItemBase): - challenge_category_id: int = Field(..., alias="ChallengeCategoryId") - challenge_sub_category_id: None = Field(..., 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") - unit: str | None = Field(..., alias="Unit") - goals: None = Field(..., alias="Goals") - challenge_histories: list[ChallengeHistory] = Field(..., alias="ChallengeHistories") - -class ChallengeTrackerDetailList(OtfItemBase): +@define +class ChallengeTrackerDetailList: details: list[ChallengeTrackerDetail] diff --git a/src/otf_api/models/responses/enums.py b/src/otf_api/models/responses/enums.py index 271cd07..50d4b5e 100644 --- a/src/otf_api/models/responses/enums.py +++ b/src/otf_api/models/responses/enums.py @@ -19,7 +19,7 @@ class ChallengeType(int, Enum): TwelveDaysOfFitness = 63 Transformation = 64 RemixInSix = 65 - Push = 66 + Push30 = 66 BackAtIt = 84