From 73135b362459abe1b2edaa7de582d279333a6cee Mon Sep 17 00:00:00 2001 From: Jessica Smith <12jessicasmith34@gmail.com> Date: Mon, 14 Oct 2024 09:06:51 -0500 Subject: [PATCH 1/6] refactor(models): pull models out of responses dir --- src/otf_api/models/__init__.py | 55 ++++++++---------- .../{responses => }/body_composition_list.py | 0 .../models/{responses => }/book_class.py | 0 .../models/{responses => }/bookings.py | 0 .../models/{responses => }/cancel_booking.py | 0 .../challenge_tracker_content.py | 0 .../challenge_tracker_detail.py | 0 src/otf_api/models/{responses => }/classes.py | 0 src/otf_api/models/{responses => }/enums.py | 0 .../{responses => }/favorite_studios.py | 0 .../{responses => }/latest_agreement.py | 0 .../models/{responses => }/lifetime_stats.py | 0 .../models/{responses => }/member_detail.py | 0 .../{responses => }/member_membership.py | 0 .../{responses => }/member_purchases.py | 0 .../out_of_studio_workout_history.py | 0 .../performance_summary_detail.py | 0 .../performance_summary_list.py | 0 src/otf_api/models/responses/__init__.py | 57 ------------------- .../models/{responses => }/studio_detail.py | 0 .../models/{responses => }/studio_services.py | 0 .../models/{responses => }/telemetry.py | 0 .../{responses => }/telemetry_hr_history.py | 0 .../{responses => }/telemetry_max_hr.py | 0 .../models/{responses => }/total_classes.py | 0 25 files changed, 23 insertions(+), 89 deletions(-) rename src/otf_api/models/{responses => }/body_composition_list.py (100%) rename src/otf_api/models/{responses => }/book_class.py (100%) rename src/otf_api/models/{responses => }/bookings.py (100%) rename src/otf_api/models/{responses => }/cancel_booking.py (100%) rename src/otf_api/models/{responses => }/challenge_tracker_content.py (100%) rename src/otf_api/models/{responses => }/challenge_tracker_detail.py (100%) rename src/otf_api/models/{responses => }/classes.py (100%) rename src/otf_api/models/{responses => }/enums.py (100%) rename src/otf_api/models/{responses => }/favorite_studios.py (100%) rename src/otf_api/models/{responses => }/latest_agreement.py (100%) rename src/otf_api/models/{responses => }/lifetime_stats.py (100%) rename src/otf_api/models/{responses => }/member_detail.py (100%) rename src/otf_api/models/{responses => }/member_membership.py (100%) rename src/otf_api/models/{responses => }/member_purchases.py (100%) rename src/otf_api/models/{responses => }/out_of_studio_workout_history.py (100%) rename src/otf_api/models/{responses => }/performance_summary_detail.py (100%) rename src/otf_api/models/{responses => }/performance_summary_list.py (100%) delete mode 100644 src/otf_api/models/responses/__init__.py rename src/otf_api/models/{responses => }/studio_detail.py (100%) rename src/otf_api/models/{responses => }/studio_services.py (100%) rename src/otf_api/models/{responses => }/telemetry.py (100%) rename src/otf_api/models/{responses => }/telemetry_hr_history.py (100%) rename src/otf_api/models/{responses => }/telemetry_max_hr.py (100%) rename src/otf_api/models/{responses => }/total_classes.py (100%) diff --git a/src/otf_api/models/__init__.py b/src/otf_api/models/__init__.py index c32726b..0965205 100644 --- a/src/otf_api/models/__init__.py +++ b/src/otf_api/models/__init__.py @@ -1,35 +1,26 @@ -from .responses import ( - BodyCompositionList, - BookClass, - BookingList, - BookingStatus, - CancelBooking, - ChallengeTrackerContent, - ChallengeTrackerDetailList, - ChallengeType, - ClassType, - DoW, - EquipmentType, - FavoriteStudioList, - LatestAgreement, - MemberDetail, - MemberMembership, - MemberPurchaseList, - OtfClassList, - OutOfStudioWorkoutHistoryList, - Pagination, - PerformanceSummaryDetail, - PerformanceSummaryList, - StatsResponse, - StatsTime, - StudioDetail, - StudioDetailList, - StudioServiceList, - Telemetry, - TelemetryHrHistory, - TelemetryMaxHr, - TotalClasses, -) +from .body_composition_list import BodyCompositionList +from .book_class import BookClass +from .bookings import BookingList, BookingStatus +from .cancel_booking import CancelBooking +from .challenge_tracker_content import ChallengeTrackerContent +from .challenge_tracker_detail import ChallengeTrackerDetailList +from .classes import ClassType, DoW, OtfClassList +from .enums import ChallengeType, EquipmentType +from .favorite_studios import FavoriteStudioList +from .latest_agreement import LatestAgreement +from .lifetime_stats import StatsResponse, StatsTime +from .member_detail import MemberDetail +from .member_membership import MemberMembership +from .member_purchases import MemberPurchaseList +from .out_of_studio_workout_history import OutOfStudioWorkoutHistoryList +from .performance_summary_detail import PerformanceSummaryDetail +from .performance_summary_list import PerformanceSummaryList +from .studio_detail import Pagination, StudioDetail, StudioDetailList +from .studio_services import StudioServiceList +from .telemetry import Telemetry +from .telemetry_hr_history import TelemetryHrHistory +from .telemetry_max_hr import TelemetryMaxHr +from .total_classes import TotalClasses __all__ = [ "BodyCompositionList", diff --git a/src/otf_api/models/responses/body_composition_list.py b/src/otf_api/models/body_composition_list.py similarity index 100% rename from src/otf_api/models/responses/body_composition_list.py rename to src/otf_api/models/body_composition_list.py diff --git a/src/otf_api/models/responses/book_class.py b/src/otf_api/models/book_class.py similarity index 100% rename from src/otf_api/models/responses/book_class.py rename to src/otf_api/models/book_class.py diff --git a/src/otf_api/models/responses/bookings.py b/src/otf_api/models/bookings.py similarity index 100% rename from src/otf_api/models/responses/bookings.py rename to src/otf_api/models/bookings.py diff --git a/src/otf_api/models/responses/cancel_booking.py b/src/otf_api/models/cancel_booking.py similarity index 100% rename from src/otf_api/models/responses/cancel_booking.py rename to src/otf_api/models/cancel_booking.py diff --git a/src/otf_api/models/responses/challenge_tracker_content.py b/src/otf_api/models/challenge_tracker_content.py similarity index 100% rename from src/otf_api/models/responses/challenge_tracker_content.py rename to src/otf_api/models/challenge_tracker_content.py diff --git a/src/otf_api/models/responses/challenge_tracker_detail.py b/src/otf_api/models/challenge_tracker_detail.py similarity index 100% rename from src/otf_api/models/responses/challenge_tracker_detail.py rename to src/otf_api/models/challenge_tracker_detail.py diff --git a/src/otf_api/models/responses/classes.py b/src/otf_api/models/classes.py similarity index 100% rename from src/otf_api/models/responses/classes.py rename to src/otf_api/models/classes.py diff --git a/src/otf_api/models/responses/enums.py b/src/otf_api/models/enums.py similarity index 100% rename from src/otf_api/models/responses/enums.py rename to src/otf_api/models/enums.py diff --git a/src/otf_api/models/responses/favorite_studios.py b/src/otf_api/models/favorite_studios.py similarity index 100% rename from src/otf_api/models/responses/favorite_studios.py rename to src/otf_api/models/favorite_studios.py diff --git a/src/otf_api/models/responses/latest_agreement.py b/src/otf_api/models/latest_agreement.py similarity index 100% rename from src/otf_api/models/responses/latest_agreement.py rename to src/otf_api/models/latest_agreement.py diff --git a/src/otf_api/models/responses/lifetime_stats.py b/src/otf_api/models/lifetime_stats.py similarity index 100% rename from src/otf_api/models/responses/lifetime_stats.py rename to src/otf_api/models/lifetime_stats.py diff --git a/src/otf_api/models/responses/member_detail.py b/src/otf_api/models/member_detail.py similarity index 100% rename from src/otf_api/models/responses/member_detail.py rename to src/otf_api/models/member_detail.py diff --git a/src/otf_api/models/responses/member_membership.py b/src/otf_api/models/member_membership.py similarity index 100% rename from src/otf_api/models/responses/member_membership.py rename to src/otf_api/models/member_membership.py diff --git a/src/otf_api/models/responses/member_purchases.py b/src/otf_api/models/member_purchases.py similarity index 100% rename from src/otf_api/models/responses/member_purchases.py rename to src/otf_api/models/member_purchases.py diff --git a/src/otf_api/models/responses/out_of_studio_workout_history.py b/src/otf_api/models/out_of_studio_workout_history.py similarity index 100% rename from src/otf_api/models/responses/out_of_studio_workout_history.py rename to src/otf_api/models/out_of_studio_workout_history.py diff --git a/src/otf_api/models/responses/performance_summary_detail.py b/src/otf_api/models/performance_summary_detail.py similarity index 100% rename from src/otf_api/models/responses/performance_summary_detail.py rename to src/otf_api/models/performance_summary_detail.py diff --git a/src/otf_api/models/responses/performance_summary_list.py b/src/otf_api/models/performance_summary_list.py similarity index 100% rename from src/otf_api/models/responses/performance_summary_list.py rename to src/otf_api/models/performance_summary_list.py diff --git a/src/otf_api/models/responses/__init__.py b/src/otf_api/models/responses/__init__.py deleted file mode 100644 index 0965205..0000000 --- a/src/otf_api/models/responses/__init__.py +++ /dev/null @@ -1,57 +0,0 @@ -from .body_composition_list import BodyCompositionList -from .book_class import BookClass -from .bookings import BookingList, BookingStatus -from .cancel_booking import CancelBooking -from .challenge_tracker_content import ChallengeTrackerContent -from .challenge_tracker_detail import ChallengeTrackerDetailList -from .classes import ClassType, DoW, OtfClassList -from .enums import ChallengeType, EquipmentType -from .favorite_studios import FavoriteStudioList -from .latest_agreement import LatestAgreement -from .lifetime_stats import StatsResponse, StatsTime -from .member_detail import MemberDetail -from .member_membership import MemberMembership -from .member_purchases import MemberPurchaseList -from .out_of_studio_workout_history import OutOfStudioWorkoutHistoryList -from .performance_summary_detail import PerformanceSummaryDetail -from .performance_summary_list import PerformanceSummaryList -from .studio_detail import Pagination, StudioDetail, StudioDetailList -from .studio_services import StudioServiceList -from .telemetry import Telemetry -from .telemetry_hr_history import TelemetryHrHistory -from .telemetry_max_hr import TelemetryMaxHr -from .total_classes import TotalClasses - -__all__ = [ - "BodyCompositionList", - "BookClass", - "BookingList", - "BookingStatus", - "CancelBooking", - "ChallengeTrackerContent", - "ChallengeTrackerDetailList", - "ChallengeType", - "ClassType", - "DoW", - "EquipmentType", - "FavoriteStudioList", - "LatestAgreement", - "MemberDetail", - "MemberMembership", - "MemberPurchaseList", - "OtfClassList", - "OutOfStudioWorkoutHistoryList", - "Pagination", - "PerformanceSummaryDetail", - "PerformanceSummaryList", - "StatsResponse", - "StatsTime", - "StudioDetail", - "StudioDetailList", - "StudioServiceList", - "StudioStatus", - "Telemetry", - "TelemetryHrHistory", - "TelemetryMaxHr", - "TotalClasses", -] diff --git a/src/otf_api/models/responses/studio_detail.py b/src/otf_api/models/studio_detail.py similarity index 100% rename from src/otf_api/models/responses/studio_detail.py rename to src/otf_api/models/studio_detail.py diff --git a/src/otf_api/models/responses/studio_services.py b/src/otf_api/models/studio_services.py similarity index 100% rename from src/otf_api/models/responses/studio_services.py rename to src/otf_api/models/studio_services.py diff --git a/src/otf_api/models/responses/telemetry.py b/src/otf_api/models/telemetry.py similarity index 100% rename from src/otf_api/models/responses/telemetry.py rename to src/otf_api/models/telemetry.py diff --git a/src/otf_api/models/responses/telemetry_hr_history.py b/src/otf_api/models/telemetry_hr_history.py similarity index 100% rename from src/otf_api/models/responses/telemetry_hr_history.py rename to src/otf_api/models/telemetry_hr_history.py diff --git a/src/otf_api/models/responses/telemetry_max_hr.py b/src/otf_api/models/telemetry_max_hr.py similarity index 100% rename from src/otf_api/models/responses/telemetry_max_hr.py rename to src/otf_api/models/telemetry_max_hr.py diff --git a/src/otf_api/models/responses/total_classes.py b/src/otf_api/models/total_classes.py similarity index 100% rename from src/otf_api/models/responses/total_classes.py rename to src/otf_api/models/total_classes.py From d671bc2c39c32058e26421b9208ccd31e3bfe5ec Mon Sep 17 00:00:00 2001 From: Jessica Smith <12jessicasmith34@gmail.com> Date: Mon, 14 Oct 2024 09:18:59 -0500 Subject: [PATCH 2/6] feat(models): start updating models to handle missing data, exclude data, remove unnecessary sub models --- examples/challenge_tracker_examples.py | 2 +- examples/class_bookings_examples.py | 15 +- ruff.toml | 1 + src/otf_api/models/__init__.py | 10 +- src/otf_api/models/base.py | 17 +- src/otf_api/models/body_composition_list.py | 17 +- src/otf_api/models/book_class.py | 442 +++--------------- src/otf_api/models/bookings.py | 141 ++---- src/otf_api/models/cancel_booking.py | 114 ++--- .../models/challenge_tracker_detail.py | 25 +- src/otf_api/models/classes.py | 86 +--- src/otf_api/models/enums.py | 64 +++ src/otf_api/models/favorite_studios.py | 36 +- src/otf_api/models/lifetime_stats.py | 18 - src/otf_api/models/member_detail.py | 35 +- src/otf_api/models/member_purchases.py | 20 +- src/otf_api/models/mixins.py | 44 ++ .../models/out_of_studio_workout_history.py | 6 +- .../models/performance_summary_list.py | 7 +- src/otf_api/models/studio_detail.py | 150 +++--- src/otf_api/models/studio_services.py | 4 +- 21 files changed, 416 insertions(+), 838 deletions(-) create mode 100644 src/otf_api/models/mixins.py diff --git a/examples/challenge_tracker_examples.py b/examples/challenge_tracker_examples.py index bb40a4f..6746c5c 100644 --- a/examples/challenge_tracker_examples.py +++ b/examples/challenge_tracker_examples.py @@ -2,7 +2,7 @@ import os from otf_api import Otf -from otf_api.models.responses import ChallengeType, EquipmentType +from otf_api.models import ChallengeType, EquipmentType USERNAME = os.getenv("OTF_EMAIL") PASSWORD = os.getenv("OTF_PASSWORD") diff --git a/examples/class_bookings_examples.py b/examples/class_bookings_examples.py index 57921a5..bb9a220 100644 --- a/examples/class_bookings_examples.py +++ b/examples/class_bookings_examples.py @@ -1,9 +1,9 @@ import asyncio import os +from datetime import datetime from otf_api import Otf -from otf_api.models.responses.bookings import BookingStatus -from otf_api.models.responses.classes import DoW +from otf_api.models.classes import DoW USERNAME = os.getenv("OTF_EMAIL") PASSWORD = os.getenv("OTF_PASSWORD") @@ -12,10 +12,7 @@ async def main(): otf = otf = Otf(USERNAME, PASSWORD) - resp = await otf.get_member_purchases() - print(resp.model_dump_json(indent=4)) - - resp = await otf.get_bookings(start_date="2024-06-19", status=BookingStatus.LateCancelled) + resp = await otf.get_bookings(start_date=datetime.today().date()) print(resp.model_dump_json(indent=4)) studios = await otf.search_studios_by_geo(40.7831, 73.9712, distance=100) @@ -26,7 +23,7 @@ async def main(): # You can pass a list of studio_uuids or, if you want to get classes from your home studio, leave it empty # this also takes a start date, end date, and limit - these are not sent to the API, they are used in the # client to filter the results - classes = await otf.get_classes(studio_uuids, day_of_week=[DoW.tuesday, DoW.thursday, DoW.saturday]) + classes = await otf.get_classes(studio_uuids, day_of_week=[DoW.TUESDAY, DoW.THURSDAY, DoW.SATURDAY]) print(classes.classes[0].model_dump_json(indent=4)) @@ -61,8 +58,8 @@ async def main(): bookings = await otf.get_bookings() - print("Next Upcoming Class:") - print(bookings.bookings[0].model_dump_json(indent=4)) + print("Latest Upcoming Class:") + print(bookings.bookings[-1].model_dump_json(indent=4)) """ { diff --git a/ruff.toml b/ruff.toml index 331782f..2cab2db 100644 --- a/ruff.toml +++ b/ruff.toml @@ -59,6 +59,7 @@ ignore = [ "PD901", # pandas-vet - pandas-df-variable-name "RUF015", # ruff - unnecessary-iterable-allocation-for-first-element "PTH118", # flake8-use-pathlib - os-path-join + "DTZ002", ] # Allow fix for all enabled rules (when `--fix`) is provided. diff --git a/src/otf_api/models/__init__.py b/src/otf_api/models/__init__.py index 0965205..de04968 100644 --- a/src/otf_api/models/__init__.py +++ b/src/otf_api/models/__init__.py @@ -1,14 +1,14 @@ from .body_composition_list import BodyCompositionList from .book_class import BookClass -from .bookings import BookingList, BookingStatus +from .bookings import Booking, BookingList from .cancel_booking import CancelBooking from .challenge_tracker_content import ChallengeTrackerContent from .challenge_tracker_detail import ChallengeTrackerDetailList -from .classes import ClassType, DoW, OtfClassList -from .enums import ChallengeType, EquipmentType +from .classes import OtfClass, OtfClassList +from .enums import BookingStatus, ChallengeType, ClassType, DoW, EquipmentType, StatsTime, StudioStatus from .favorite_studios import FavoriteStudioList from .latest_agreement import LatestAgreement -from .lifetime_stats import StatsResponse, StatsTime +from .lifetime_stats import StatsResponse from .member_detail import MemberDetail from .member_membership import MemberMembership from .member_purchases import MemberPurchaseList @@ -25,6 +25,7 @@ __all__ = [ "BodyCompositionList", "BookClass", + "Booking", "BookingList", "BookingStatus", "CancelBooking", @@ -40,6 +41,7 @@ "MemberMembership", "MemberPurchaseList", "OtfClassList", + "OtfClass", "OutOfStudioWorkoutHistoryList", "Pagination", "PerformanceSummaryDetail", diff --git a/src/otf_api/models/base.py b/src/otf_api/models/base.py index 72dfa8d..293b920 100644 --- a/src/otf_api/models/base.py +++ b/src/otf_api/models/base.py @@ -4,19 +4,4 @@ class OtfItemBase(BaseModel): - model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True, extra="allow") - - -class OtfListBase(BaseModel): - model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True, extra="allow") - collection_field: ClassVar[str] = "data" - - @property - def collection(self) -> list[OtfItemBase]: - return getattr(self, self.collection_field) - - def to_json(self, **kwargs) -> str: - kwargs.setdefault("indent", 4) - kwargs.setdefault("exclude_none", True) - - return self.model_dump_json(**kwargs) + model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True, extra="ignore") diff --git a/src/otf_api/models/body_composition_list.py b/src/otf_api/models/body_composition_list.py index 8366d74..5283704 100644 --- a/src/otf_api/models/body_composition_list.py +++ b/src/otf_api/models/body_composition_list.py @@ -3,9 +3,9 @@ from enum import Enum import pint -from pydantic import BaseModel, ConfigDict, Field, field_validator +from pydantic import BaseModel, Field, field_validator -from otf_api.models.base import OtfItemBase, OtfListBase +from otf_api.models.base import OtfItemBase ureg = pint.UnitRegistry() @@ -101,7 +101,6 @@ def get_body_fat_percent_dividers_female(age: int) -> list[float]: class LeanBodyMass(OtfItemBase): - model_config: ConfigDict = ConfigDict(extra="ignore") left_arm: float = Field(..., alias="lbmOfLeftArm") left_leg: float = Field(..., alias="lbmOfLeftLeg") right_arm: float = Field(..., alias="lbmOfRightArm") @@ -110,7 +109,6 @@ class LeanBodyMass(OtfItemBase): class LeanBodyMassPercent(OtfItemBase): - model_config: ConfigDict = ConfigDict(extra="ignore") left_arm: float = Field(..., alias="lbmPercentOfLeftArm") left_leg: float = Field(..., alias="lbmPercentOfLeftLeg") right_arm: float = Field(..., alias="lbmPercentOfRightArm") @@ -119,7 +117,6 @@ class LeanBodyMassPercent(OtfItemBase): class BodyFatMass(OtfItemBase): - model_config: ConfigDict = ConfigDict(extra="ignore") control: float = Field(..., alias="bfmControl") left_arm: float = Field(..., alias="bfmOfLeftArm") left_leg: float = Field(..., alias="bfmOfLeftLeg") @@ -129,7 +126,6 @@ class BodyFatMass(OtfItemBase): class BodyFatMassPercent(OtfItemBase): - model_config: ConfigDict = ConfigDict(extra="ignore") left_arm: float = Field(..., alias="bfmPercentOfLeftArm") left_leg: float = Field(..., alias="bfmPercentOfLeftLeg") right_arm: float = Field(..., alias="bfmPercentOfRightArm") @@ -138,7 +134,6 @@ class BodyFatMassPercent(OtfItemBase): class TotalBodyWeight(OtfItemBase): - model_config: ConfigDict = ConfigDict(extra="ignore") right_arm: float = Field(..., alias="tbwOfRightArm") left_arm: float = Field(..., alias="tbwOfLeftArm") trunk: float = Field(..., alias="tbwOfTrunk") @@ -147,7 +142,6 @@ class TotalBodyWeight(OtfItemBase): class IntraCellularWater(OtfItemBase): - model_config: ConfigDict = ConfigDict(extra="ignore") right_arm: float = Field(..., alias="icwOfRightArm") left_arm: float = Field(..., alias="icwOfLeftArm") trunk: float = Field(..., alias="icwOfTrunk") @@ -156,7 +150,6 @@ class IntraCellularWater(OtfItemBase): class ExtraCellularWater(OtfItemBase): - model_config: ConfigDict = ConfigDict(extra="ignore") right_arm: float = Field(..., alias="ecwOfRightArm") left_arm: float = Field(..., alias="ecwOfLeftArm") trunk: float = Field(..., alias="ecwOfTrunk") @@ -165,7 +158,6 @@ class ExtraCellularWater(OtfItemBase): class ExtraCellularWaterOverTotalBodyWater(OtfItemBase): - model_config: ConfigDict = ConfigDict(extra="ignore") right_arm: float = Field(..., alias="ecwOverTBWOfRightArm") left_arm: float = Field(..., alias="ecwOverTBWOfLeftArm") trunk: float = Field(..., alias="ecwOverTBWOfTrunk") @@ -200,9 +192,6 @@ class BodyCompositionData(OtfItemBase): 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) @@ -302,5 +291,5 @@ def body_fat_percent_relative_descriptor(self) -> BodyFatPercentIndicator: ) -class BodyCompositionList(OtfListBase): +class BodyCompositionList(OtfItemBase): data: list[BodyCompositionData] diff --git a/src/otf_api/models/book_class.py b/src/otf_api/models/book_class.py index e9119c2..9f90223 100644 --- a/src/otf_api/models/book_class.py +++ b/src/otf_api/models/book_class.py @@ -1,3 +1,4 @@ +from collections.abc import Hashable from datetime import datetime from typing import Any @@ -6,402 +7,83 @@ from otf_api.models.base import OtfItemBase -class MemberProfile(OtfItemBase): - is_latest_agreement_signed: bool = Field(..., alias="isLatestAgreementSigned") - - -class StudioProfiles(OtfItemBase): - studio_id: int = Field(..., alias="studioId") - is_franchise_agreement_enabled: int = Field(..., alias="isFranchiseAgreementEnabled") - - -class HomeStudio(OtfItemBase): - studio_uuid: str = Field(..., alias="studioUUId") - studio_name: str = Field(..., alias="studioName") - description: str - contact_email: str = Field(..., alias="contactEmail") - status: str - logo_url: str = Field(..., alias="logoUrl") - time_zone: str = Field(..., alias="timeZone") - mbo_studio_id: int = Field(..., alias="mboStudioId") - studio_id: int = Field(..., alias="studioId") - allows_cr_waitlist: bool = Field(..., alias="allowsCRWaitlist") - cr_waitlist_flag_last_updated: datetime = Field(..., alias="crWaitlistFlagLastUpdated") - studio_profiles: StudioProfiles = Field(..., alias="studioProfiles") - - -class MemberService(OtfItemBase): - member_service_id: int = Field(..., alias="memberServiceId") - member_service_uuid: str = Field(..., alias="memberServiceUUId") - service_name: str = Field(..., alias="serviceName") - studio_id: int = Field(..., alias="studioId") - mbo_client_service_id: int = Field(..., alias="mboClientServiceId") - current: bool - member_id: int = Field(..., alias="memberId") - service_id: int = Field(..., alias="serviceId") - remaining: int - count: int - payment_date: datetime = Field(..., alias="paymentDate") - expiration_date: datetime = Field(..., alias="expirationDate") - active_date: datetime = Field(..., alias="activeDate") - created_by: str | None = Field(None, alias="createdBy") - created_date: datetime | None = Field(None, alias="createdDate") - updated_by: str | None = Field(None, alias="updatedBy") - updated_date: datetime | None = Field(None, alias="updatedDate") - is_deleted: bool = Field(..., alias="isDeleted") - - -class ServiceItem(OtfItemBase): - service_id: int = Field(..., alias="serviceId") - name: str - member_service: MemberService = Field(..., alias="MemberService") - - -class Member(OtfItemBase): - member_id: int = Field(..., alias="memberId") - member_uuid: str = Field(..., alias="memberUUId") - cognito_id: str = Field(..., alias="cognitoId") - home_studio_id: int = Field(..., alias="homeStudioId") - mbo_studio_id: int = Field(..., alias="mboStudioId") - mbo_id: str = Field(..., alias="mboId") - mbo_unique_id: int = Field(..., alias="mboUniqueId") - mbo_status: str = Field(..., alias="mboStatus") - user_name: str = Field(..., alias="userName") - first_name: str = Field(..., alias="firstName") - last_name: str = Field(..., alias="lastName") - email: str - profile_picture_url: str | None = Field(..., alias="profilePictureUrl") - alternate_emails: None = Field(..., alias="alternateEmails") - address_line1: str | None = Field(..., alias="addressLine1") - address_line2: str | None = Field(..., alias="addressLine2") - city: str | None - state: str | None - postal_code: str | None = Field(..., alias="postalCode") - phone_number: str = Field(..., alias="phoneNumber") - home_phone: str = Field(..., alias="homePhone") - work_phone: str | None = Field(..., alias="workPhone") - phone_type: None = Field(..., alias="phoneType") - birth_day: str = Field(..., alias="birthDay") - cc_last4: str = Field(..., alias="ccLast4") - cc_type: str = Field(..., alias="ccType") - gender: str - liability: None - locale: str - weight: int - weight_measure: str = Field(..., alias="weightMeasure") - height: int - height_measure: str = Field(..., alias="heightMeasure") - max_hr: int = Field(..., alias="maxHr") - intro_neccessary: bool = Field(..., alias="introNeccessary") - online_signup: None = Field(..., alias="onlineSignup") - year_imported: int = Field(..., alias="yearImported") - is_member_verified: bool = Field(..., alias="isMemberVerified") - lead_prospect: bool = Field(..., alias="leadProspect") - created_by: str | None = Field(None, alias="createdBy") - created_date: datetime | None = Field(None, alias="createdDate") - updated_by: str | None = Field(None, alias="updatedBy") - updated_date: datetime | None = Field(None, alias="updatedDate") - is_deleted: bool = Field(..., alias="isDeleted") - member_profile: MemberProfile = Field(..., alias="memberProfile") - home_studio: HomeStudio = Field(..., alias="homeStudio") - membership: None - service: list[ServiceItem] - notes: str - - -class Studio(OtfItemBase): - studio_uuid: str = Field(..., alias="studioUUId") - studio_name: str = Field(..., alias="studioName") - description: str - contact_email: str = Field(..., alias="contactEmail") - status: str - logo_url: str = Field(..., alias="logoUrl") - time_zone: str = Field(..., alias="timeZone") - mbo_studio_id: int = Field(..., alias="mboStudioId") - studio_id: int = Field(..., alias="studioId") - allows_cr_waitlist: bool = Field(..., alias="allowsCRWaitlist") - cr_waitlist_flag_last_updated: datetime = Field(..., alias="crWaitlistFlagLastUpdated") - - -class Location(OtfItemBase): - location_id: int = Field(..., alias="locationId") - location_uuid: str = Field(..., alias="locationUUId") - - -class Coach(OtfItemBase): - coach_uuid: str = Field(..., alias="coachUUId") - first_name: str = Field(..., alias="firstName") - last_name: str = Field(..., alias="lastName") - mbo_coach_id: int = Field(..., alias="mboCoachId") - name: str - - class Class(OtfItemBase): - class_id: int = Field(..., alias="classId") - class_uuid: str = Field(..., alias="classUUId") - mbo_studio_id: int = Field(..., alias="mboStudioId") - mbo_class_id: int = Field(..., alias="mboClassId") - mbo_class_schedule_id: int = Field(..., alias="mboClassScheduleId") - mbo_program_id: int = Field(..., alias="mboProgramId") - studio_id: int = Field(..., alias="studioId") - coach_id: int = Field(..., alias="coachId") - location_id: int = Field(..., alias="locationId") - name: str - description: str - program_name: str = Field(..., alias="programName") - program_schedule_type: str = Field(..., alias="programScheduleType") - program_cancel_offset: int = Field(..., alias="programCancelOffset") - max_capacity: int = Field(..., alias="maxCapacity") - total_booked: int = Field(..., alias="totalBooked") - web_capacity: int = Field(..., alias="webCapacity") - web_booked: int = Field(..., alias="webBooked") - total_booked_waitlist: int = Field(..., alias="totalBookedWaitlist") - start_date_time: datetime = Field(..., alias="startDateTime") - end_date_time: datetime = Field(..., alias="endDateTime") - is_cancelled: bool = Field(..., alias="isCancelled") - substitute: bool - is_active: bool = Field(..., alias="isActive") - is_waitlist_available: bool = Field(..., alias="isWaitlistAvailable") - is_enrolled: bool = Field(..., alias="isEnrolled") - is_hide_cancel: bool = Field(..., alias="isHideCancel") - is_available: bool = Field(..., alias="isAvailable") - room_number: int = Field(..., alias="roomNumber") + class_id: int = Field(None, alias="classId") + class_uuid: str = Field(None, alias="classUUId") + mbo_studio_id: int | None = Field(None, alias="mboStudioId") + mbo_class_id: int | None = Field(None, alias="mboClassId") + mbo_class_schedule_id: int | None = Field(None, alias="mboClassScheduleId") + mbo_program_id: int | None = Field(None, alias="mboProgramId") + studio_id: int | None = Field(None, alias="studioId") + coach_id: int | None = Field(None, alias="coachId") + location_id: int | None = Field(None, alias="locationId") + name: str | None = None + description: str | None = None + program_name: str | None = Field(None, alias="programName") + program_schedule_type: str | None = Field(None, alias="programScheduleType") + program_cancel_offset: int | None = Field(None, alias="programCancelOffset") + max_capacity: int | None = Field(None, alias="maxCapacity") + total_booked: int | None = Field(None, alias="totalBooked") + web_capacity: int | None = Field(None, alias="webCapacity") + web_booked: int | None = Field(None, alias="webBooked") + total_booked_waitlist: int | None = Field(None, alias="totalBookedWaitlist") + start_date_time: datetime | None = Field(None, alias="startDateTime") + end_date_time: datetime | None = Field(None, alias="endDateTime") + is_cancelled: bool | None = Field(None, alias="isCancelled") + substitute: bool | None = None + is_active: bool | None = Field(None, alias="isActive") + is_waitlist_available: bool | None = Field(None, alias="isWaitlistAvailable") + is_enrolled: bool | None = Field(None, alias="isEnrolled") + is_hide_cancel: bool | None = Field(None, alias="isHideCancel") + is_available: bool | None = Field(None, alias="isAvailable") + room_number: int | None = Field(None, alias="roomNumber") created_by: str | None = Field(None, alias="createdBy") created_date: datetime | None = Field(None, alias="createdDate") updated_by: str | None = Field(None, alias="updatedBy") updated_date: datetime | None = Field(None, alias="updatedDate") - is_deleted: bool = Field(..., alias="isDeleted") - studio: Studio - location: Location - coach: Coach - attributes: dict[str, Any] - - -class Class1(OtfItemBase): - mbo_studio_id: int = Field(..., alias="mboStudioId") - studio_uuid: str = Field(..., alias="studioUUId") - - -class CustomData(OtfItemBase): - otf_class: Class1 = Field(..., alias="class") + is_deleted: bool | None = Field(None, alias="isDeleted") + studio: dict[Hashable, Any] | None = Field(None, exclude=True) + location: dict[Hashable, Any] | None = Field(None, exclude=True) + coach: dict[Hashable, Any] | None = Field(None, exclude=True) + attributes: dict[str, Any] | None = Field(None, exclude=True) class SavedBooking(OtfItemBase): class_booking_id: int = Field(..., alias="classBookingId") class_booking_uuid: str = Field(..., alias="classBookingUUId") - studio_id: int = Field(..., alias="studioId") - class_id: int = Field(..., alias="classId") - is_intro: bool = Field(..., alias="isIntro") - member_id: int = Field(..., alias="memberId") - mbo_member_id: str = Field(..., alias="mboMemberId") - mbo_class_id: int = Field(..., alias="mboClassId") - mbo_visit_id: int | None = Field(..., alias="mboVisitId") - mbo_waitlist_entry_id: int | None = Field(..., alias="mboWaitlistEntryId") - mbo_sync_message: str | None = Field(..., alias="mboSyncMessage") - status: str - booked_date: datetime = Field(..., alias="bookedDate") - checked_in_date: datetime | None = Field(..., alias="checkedInDate") - cancelled_date: datetime | None = Field(..., alias="cancelledDate") + studio_id: int | None = Field(None, alias="studioId") + class_id: int | None = Field(None, alias="classId") + is_intro: bool | None = Field(None, alias="isIntro") + member_id: int | None = Field(None, alias="memberId") + mbo_member_id: str | None = Field(None, alias="mboMemberId") + mbo_class_id: int | None = Field(None, alias="mboClassId") + mbo_visit_id: int | None = Field(None, alias="mboVisitId") + mbo_waitlist_entry_id: int | None = Field(None, alias="mboWaitlistEntryId") + mbo_sync_message: str | None = Field(None, alias="mboSyncMessage") + status: str | None = None + booked_date: datetime | None = Field(None, alias="bookedDate") + checked_in_date: datetime | None = Field(None, alias="checkedInDate") + cancelled_date: datetime | None = Field(None, alias="cancelledDate") created_by: str | None = Field(None, alias="createdBy") created_date: datetime | None = Field(None, alias="createdDate") updated_by: str | None = Field(None, alias="updatedBy") updated_date: datetime | None = Field(None, alias="updatedDate") - is_deleted: bool = Field(..., alias="isDeleted") - member: Member + is_deleted: bool | None = Field(None, alias="isDeleted") + member: dict[Hashable, Any] | None = Field(None, exclude=True) otf_class: Class = Field(..., alias="class") - custom_data: CustomData = Field(..., alias="customData") - attributes: dict[str, Any] - - -class FieldModel(OtfItemBase): - xsi_nil: str = Field(..., alias="xsiNil") - - -class FacilitySquareFeet(OtfItemBase): - field_: FieldModel - - -class TreatmentRooms(OtfItemBase): - field_: FieldModel + custom_data: Any | None = Field(None, alias="customData", exclude=True) + attributes: dict[str, Any] | None = Field(None, exclude=True) -class Location1(OtfItemBase): - site_id: str | Any = Field(..., alias="siteId") - business_description: str | Any = Field(..., alias="businessDescription") - additional_image_ur_ls: str | Any = Field(..., alias="additionalImageUrLs") - facility_square_feet: FacilitySquareFeet | Any = Field(..., alias="facilitySquareFeet") - treatment_rooms: TreatmentRooms | Any = Field(..., alias="treatmentRooms") - has_classes: str = Field(..., alias="hasClasses") - id: str - name: str - address: str - address2: str - tax1: str - tax2: str - tax3: str - tax4: str - tax5: str - phone: str - city: str - state_prov_code: str = Field(..., alias="stateProvCode") - postal_code: str = Field(..., alias="postalCode") - latitude: str - longitude: str - - -class MaxCapacity(OtfItemBase): - field_: FieldModel | Any - - -class WebCapacity(OtfItemBase): - field_: FieldModel | Any - - -class TotalBookedWaitlist(OtfItemBase): - field_: FieldModel | Any - - -class WebBooked(OtfItemBase): - field_: FieldModel | Any - - -class SemesterId(OtfItemBase): - field_: FieldModel | Any - - -class Program(OtfItemBase): - id: str - name: str - schedule_type: str = Field(..., alias="scheduleType") - cancel_offset: str = Field(..., alias="cancelOffset") - - -class DefaultTimeLength(OtfItemBase): - field_: FieldModel - - -class SessionType(OtfItemBase): - default_time_length: DefaultTimeLength = Field(..., alias="defaultTimeLength") - program_id: str = Field(..., alias="programId") - num_deducted: str = Field(..., alias="numDeducted") - id: str - name: str - site_id: str = Field(..., alias="siteId") - cross_regional_booking_performed: str = Field(..., alias="crossRegionalBookingPerformed") - available_for_add_on: str = Field(..., alias="availableForAddOn") - - -class ClassDescription(OtfItemBase): - id: str - name: str - description: str - prereq: str - notes: str - last_updated: datetime = Field(..., alias="lastUpdated") - program: Program - session_type: SessionType = Field(..., alias="sessionType") - - -class Staff(OtfItemBase): - email: str - mobile_phone: str = Field(..., alias="mobilePhone") - state: str - country: str - sort_order: str = Field(..., alias="sortOrder") - appointment_trn: str = Field(..., alias="appointmentTrn") - reservation_trn: str = Field(..., alias="reservationTrn") - independent_contractor: str = Field(..., alias="independentContractor") - always_allow_double_booking: str = Field(..., alias="alwaysAllowDoubleBooking") - id: str - name: str - first_name: str = Field(..., alias="firstName") - last_name: str = Field(..., alias="lastName") - bio: str - is_male: str = Field(..., alias="isMale") - - -class AgreementDate(OtfItemBase): - field_: FieldModel - - -class ReleasedBy(OtfItemBase): - field_: FieldModel - - -class Liability(OtfItemBase): - is_released: str = Field(..., alias="isReleased") - agreement_date: AgreementDate = Field(..., alias="agreementDate") - released_by: ReleasedBy = Field(..., alias="releasedBy") - - -class FirstAppointmentDate(OtfItemBase): - field_: FieldModel - - -class Client(OtfItemBase): - notes: str - mobile_provider: str = Field(..., alias="mobileProvider") - appointment_gender_preference: str = Field(..., alias="appointmentGenderPreference") - is_company: str = Field(..., alias="isCompany") - liability_release: str = Field(..., alias="liabilityRelease") - promotional_email_opt_in: str = Field(..., alias="promotionalEmailOptIn") - creation_date: datetime = Field(..., alias="creationDate") - liability: Liability - unique_id: str = Field(..., alias="uniqueId") - action: str - id: str - first_name: str = Field(..., alias="firstName") - last_name: str = Field(..., alias="lastName") - email: str - email_opt_in: str = Field(..., alias="emailOptIn") - address_line1: str = Field(..., alias="addressLine1") - address_line2: str = Field(..., alias="addressLine2") - city: str - state: str - postal_code: str = Field(..., alias="postalCode") - country: str - mobile_phone: str = Field(..., alias="mobilePhone") - home_phone: str = Field(..., alias="homePhone") - birth_date: datetime = Field(..., alias="birthDate") - first_appointment_date: FirstAppointmentDate = Field(..., alias="firstAppointmentDate") - referred_by: str = Field(..., alias="referredBy") - red_alert: str = Field(..., alias="redAlert") - is_prospect: str = Field(..., alias="isProspect") - contact_method: str = Field(..., alias="contactMethod") - member_uuid: str = Field(..., alias="memberUUId") - - -class MboClass(OtfItemBase): - class_schedule_id: str = Field(..., alias="classScheduleId") - location: Location1 - max_capacity: MaxCapacity = Field(..., alias="maxCapacity") - web_capacity: WebCapacity = Field(..., alias="webCapacity") - total_booked: int | None = Field(..., alias="totalBooked") - total_booked_waitlist: TotalBookedWaitlist = Field(..., alias="totalBookedWaitlist") - web_booked: WebBooked = Field(..., alias="webBooked") - semester_id: SemesterId = Field(..., alias="semesterId") - is_canceled: str = Field(..., alias="isCanceled") - substitute: str - active: str - is_waitlist_available: str = Field(..., alias="isWaitlistAvailable") - is_enrolled: str = Field(..., alias="isEnrolled") - hide_cancel: str = Field(..., alias="hideCancel") - id: str - is_available: str = Field(..., alias="isAvailable") - start_date_time: datetime = Field(..., alias="startDateTime") - end_date_time: datetime = Field(..., alias="endDateTime") - class_description: ClassDescription = Field(..., alias="classDescription") - staff: Staff - class_uuid: str = Field(..., alias="classUUId") - client: Client - - -class MboResponseItem(OtfItemBase): - class_booking_uuid: str | Any = Field(..., alias="classBookingUUId") - action: str | Any - otf_class: MboClass | Any = Field(..., alias="class") +class BookClass(OtfItemBase): + saved_bookings: list[SavedBooking] = Field(None, alias="savedBookings") + mbo_response: list[dict[Hashable, Any]] | Any | None = Field(None, alias="mboResponse", exclude=True) + @property + def booking(self) -> SavedBooking: + return self.saved_bookings[0] -class BookClass(OtfItemBase): - saved_bookings: list[SavedBooking] = Field(..., alias="savedBookings") - mbo_response: list[MboResponseItem] = Field(..., alias="mboResponse") + @property + def booking_uuid(self) -> str: + """Returns the booking UUID for the class. This can be used to cancel the class.""" + return self.booking.class_booking_uuid diff --git a/src/otf_api/models/bookings.py b/src/otf_api/models/bookings.py index 7410b48..04d78db 100644 --- a/src/otf_api/models/bookings.py +++ b/src/otf_api/models/bookings.py @@ -1,50 +1,24 @@ +from collections.abc import Hashable from datetime import datetime -from enum import Enum -from typing import ClassVar +from typing import Any from pydantic import Field -from otf_api.models.base import OtfItemBase, OtfListBase -from otf_api.models.responses.classes import OtfClassTimeMixin - - -class StudioStatus(str, Enum): - OTHER = "OTHER" - ACTIVE = "Active" - INACTIVE = "Inactive" - COMING_SOON = "Coming Soon" - TEMP_CLOSED = "Temporarily Closed" - PERM_CLOSED = "Permanently Closed" - - -class BookingStatus(str, Enum): - CheckedIn = "Checked In" - CancelCheckinPending = "Cancel Checkin Pending" - CancelCheckinRequested = "Cancel Checkin Requested" - Cancelled = "Cancelled" - LateCancelled = "Late Cancelled" - Booked = "Booked" - Waitlisted = "Waitlisted" - CheckinPending = "Checkin Pending" - CheckinRequested = "Checkin Requested" - CheckinCancelled = "Checkin Cancelled" - - @classmethod - def get_case_insensitive(cls, value: str) -> str: - lcase_to_actual = {item.value.lower(): item.value for item in cls} - return lcase_to_actual[value.lower()] +from otf_api.models.base import OtfItemBase +from otf_api.models.enums import BookingStatus, StudioStatus +from otf_api.models.mixins import OtfClassTimeMixin class Location(OtfItemBase): - address_one: str = Field(alias="address1") + address_one: str | None = Field(None, alias="address1") address_two: str | None = Field(alias="address2") - city: str + city: str | None = None country: str | None = None distance: float | None = None - latitude: float = Field(alias="latitude") location_name: str | None = Field(None, alias="locationName") - longitude: float = Field(alias="longitude") - phone_number: str = Field(alias="phone") + latitude: float | None = Field(None, alias="latitude") + longitude: float | None = Field(None, alias="longitude") + phone_number: str | None = Field(None, alias="phone") postal_code: str | None = Field(None, alias="postalCode") state: str | None = None @@ -52,54 +26,40 @@ class Location(OtfItemBase): class Coach(OtfItemBase): coach_uuid: str = Field(alias="coachUUId") name: str - first_name: str = Field(alias="firstName") - last_name: str = Field(alias="lastName") - image_url: str = Field(alias="imageUrl", exclude=True) + first_name: str | None = Field(None, alias="firstName") + last_name: str | None = Field(None, alias="lastName") + image_url: str | None = Field(None, alias="imageUrl", exclude=True) profile_picture_url: str | None = Field(None, alias="profilePictureUrl", exclude=True) -class Currency(OtfItemBase): - currency_alphabetic_code: str = Field(alias="currencyAlphabeticCode") - - -class DefaultCurrency(OtfItemBase): - currency_id: int = Field(alias="currencyId") - currency: Currency - - -class StudioLocationCountry(OtfItemBase): - country_currency_code: str = Field(alias="countryCurrencyCode") - default_currency: DefaultCurrency = Field(alias="defaultCurrency") - - class StudioLocation(OtfItemBase): - latitude: float = Field(alias="latitude") - longitude: float = Field(alias="longitude") - phone_number: str = Field(alias="phoneNumber") - physical_city: str = Field(alias="physicalCity") - physical_address: str = Field(alias="physicalAddress") - physical_address2: str | None = Field(alias="physicalAddress2") - physical_state: str = Field(alias="physicalState") - physical_postal_code: str = Field(alias="physicalPostalCode") - physical_region: str = Field(alias="physicalRegion", exclude=True) - physical_country_id: int = Field(alias="physicalCountryId", exclude=True) - physical_country: str = Field(alias="physicalCountry") - country: StudioLocationCountry = Field(alias="country", exclude=True) + latitude: float | None = Field(None, alias="latitude") + longitude: float | None = Field(None, alias="longitude") + phone_number: str | None = Field(None, alias="phoneNumber") + physical_city: str | None = Field(None, alias="physicalCity") + physical_address: str | None = Field(None, alias="physicalAddress") + physical_address2: str | None = Field(None, alias="physicalAddress2") + physical_state: str | None = Field(None, alias="physicalState") + physical_postal_code: str | None = Field(None, alias="physicalPostalCode") + physical_region: str | None = Field(None, alias="physicalRegion", exclude=True) + physical_country_id: int | None = Field(None, alias="physicalCountryId", exclude=True) + physical_country: str | None = Field(None, alias="physicalCountry") + country: dict[Hashable, Any] | None = Field(None, alias="country", exclude=True) class Studio(OtfItemBase): studio_uuid: str = Field(alias="studioUUId") studio_name: str = Field(alias="studioName") + studio_id: int = Field(alias="studioId") description: str | None = None - contact_email: str = Field(alias="contactEmail", exclude=True) - status: StudioStatus - logo_url: str | None = Field(alias="logoUrl", exclude=True) + contact_email: str | None = Field(None, alias="contactEmail", exclude=True) + status: StudioStatus | None = None + logo_url: str | None = Field(None, alias="logoUrl", exclude=True) time_zone: str = Field(alias="timeZone") - mbo_studio_id: int = Field(alias="mboStudioId", exclude=True) - studio_id: int = Field(alias="studioId") + mbo_studio_id: int | None = Field(None, alias="mboStudioId", exclude=True) allows_cr_waitlist: bool | None = Field(None, alias="allowsCRWaitlist") - cr_waitlist_flag_last_updated: datetime = Field(alias="crWaitlistFlagLastUpdated", exclude=True) - studio_location: StudioLocation = Field(alias="studioLocation", exclude=True) + cr_waitlist_flag_last_updated: datetime | None = Field(None, alias="crWaitlistFlagLastUpdated", exclude=True) + studio_location: StudioLocation | None = Field(None, alias="studioLocation", exclude=True) class OtfClass(OtfItemBase, OtfClassTimeMixin): @@ -110,11 +70,11 @@ class OtfClass(OtfItemBase, OtfClassTimeMixin): ends_at_local: datetime = Field(alias="endDateTime") is_available: bool = Field(alias="isAvailable") is_cancelled: bool = Field(alias="isCancelled") - program_name: str = Field(alias="programName") - coach_id: int = Field(alias="coachId") - studio: Studio - coach: Coach - location: Location + program_name: str | None = Field(None, alias="programName") + coach_id: int | None = Field(None, alias="coachId") + studio: Studio | None = None + coach: Coach | None = None + location: Location | None = None virtual_class: bool | None = Field(None, alias="virtualClass") @@ -122,39 +82,38 @@ class Member(OtfItemBase): member_uuid: str = Field(alias="memberUUId") first_name: str = Field(alias="firstName") last_name: str = Field(alias="lastName") - email: str - phone_number: str = Field(alias="phoneNumber") - gender: str - cc_last_4: str = Field(alias="ccLast4", exclude=True) + email: str | None = None + phone_number: str | None = Field(None, alias="phoneNumber") + gender: str | None = None + cc_last_4: str | None = Field(None, alias="ccLast4", exclude=True) class Booking(OtfItemBase): class_booking_id: int = Field(alias="classBookingId") - class_booking_uuid: str = Field(alias="classBookingUUId") + class_booking_uuid: str = Field(alias="classBookingUUId", description="ID used to cancel the booking") studio_id: int = Field(alias="studioId") class_id: int = Field(alias="classId") is_intro: bool = Field(alias="isIntro") member_id: int = Field(alias="memberId") - mbo_member_id: str = Field(alias="mboMemberId", exclude=True) - mbo_class_id: int = Field(alias="mboClassId", exclude=True) + mbo_member_id: str | None = Field(None, alias="mboMemberId", exclude=True) + mbo_class_id: int | None = Field(None, alias="mboClassId", exclude=True) mbo_visit_id: int | None = Field(None, alias="mboVisitId", exclude=True) - mbo_waitlist_entry_id: int | None = Field(alias="mboWaitlistEntryId", exclude=True) - mbo_sync_message: str | None = Field(alias="mboSyncMessage", exclude=True) + mbo_waitlist_entry_id: int | None = Field(None, alias="mboWaitlistEntryId", exclude=True) + mbo_sync_message: str | None = Field(None, alias="mboSyncMessage", exclude=True) status: BookingStatus booked_date: datetime | None = Field(None, alias="bookedDate") - checked_in_date: datetime | None = Field(alias="checkedInDate") - cancelled_date: datetime | None = Field(alias="cancelledDate") + checked_in_date: datetime | None = Field(None, alias="checkedInDate") + cancelled_date: datetime | None = Field(None, alias="cancelledDate") created_by: str = Field(alias="createdBy", exclude=True) created_date: datetime = Field(alias="createdDate") updated_by: str = Field(alias="updatedBy", exclude=True) updated_date: datetime = Field(alias="updatedDate") is_deleted: bool = Field(alias="isDeleted") - member: Member = Field(exclude=True) + member: Member | None = Field(None, exclude=True) waitlist_position: int | None = Field(None, alias="waitlistPosition") otf_class: OtfClass = Field(alias="class") is_home_studio: bool | None = Field(None, description="Custom helper field to determine if at home studio") -class BookingList(OtfListBase): - collection_field: ClassVar[str] = "bookings" +class BookingList(OtfItemBase): bookings: list[Booking] diff --git a/src/otf_api/models/cancel_booking.py b/src/otf_api/models/cancel_booking.py index 6c13340..0b43aa8 100644 --- a/src/otf_api/models/cancel_booking.py +++ b/src/otf_api/models/cancel_booking.py @@ -1,95 +1,49 @@ +from collections.abc import Hashable from datetime import datetime +from typing import Any from pydantic import Field from otf_api.models.base import OtfItemBase -class Studio(OtfItemBase): - studio_uuid: str = Field(..., alias="studioUUId") - studio_name: str = Field(..., alias="studioName") - description: str - contact_email: str = Field(..., alias="contactEmail") - status: str - logo_url: str = Field(..., alias="logoUrl") - time_zone: str = Field(..., alias="timeZone") - mbo_studio_id: int = Field(..., alias="mboStudioId") - studio_id: int = Field(..., alias="studioId") - allows_cr_waitlist: bool = Field(..., alias="allowsCRWaitlist") - cr_waitlist_flag_last_updated: datetime = Field(..., alias="crWaitlistFlagLastUpdated") - - -class Coach(OtfItemBase): - coach_uuid: str = Field(..., alias="coachUUId") - name: str - first_name: str = Field(..., alias="firstName") - last_name: str = Field(..., alias="lastName") - mbo_coach_id: int = Field(..., alias="mboCoachId") - - class Class(OtfItemBase): class_uuid: str = Field(..., alias="classUUId") - name: str - description: str - start_date_time: datetime = Field(..., alias="startDateTime") - end_date_time: datetime = Field(..., alias="endDateTime") - is_available: bool = Field(..., alias="isAvailable") - is_cancelled: bool = Field(..., alias="isCancelled") - total_booked: int = Field(..., alias="totalBooked") - mbo_class_id: int = Field(..., alias="mboClassId") - mbo_studio_id: int = Field(..., alias="mboStudioId") - studio: Studio - coach: Coach - - -class HomeStudio(OtfItemBase): - studio_uuid: str = Field(..., alias="studioUUId") - studio_name: str = Field(..., alias="studioName") - description: str - contact_email: str = Field(..., alias="contactEmail") - status: str - logo_url: str = Field(..., alias="logoUrl") - time_zone: str = Field(..., alias="timeZone") - mbo_studio_id: int = Field(..., alias="mboStudioId") - studio_id: int = Field(..., alias="studioId") - allows_cr_waitlist: bool = Field(..., alias="allowsCRWaitlist") - cr_waitlist_flag_last_updated: datetime = Field(..., alias="crWaitlistFlagLastUpdated") - - -class Member(OtfItemBase): - member_id: int = Field(..., alias="memberId") - member_uuid: str = Field(..., alias="memberUUId") - email: str - phone_number: str = Field(..., alias="phoneNumber") - first_name: str = Field(..., alias="firstName") - last_name: str = Field(..., alias="lastName") - mbo_id: str = Field(..., alias="mboId") - cc_last4: str = Field(..., alias="ccLast4") - mbo_studio_id: int = Field(..., alias="mboStudioId") - home_studio: HomeStudio = Field(..., alias="homeStudio") + name: str | None = None + description: str | None = None + start_date_time: datetime | None = Field(None, alias="startDateTime") + end_date_time: datetime | None = Field(None, alias="endDateTime") + is_available: bool | None = Field(None, alias="isAvailable") + is_cancelled: bool | None = Field(None, alias="isCancelled") + total_booked: int | None = Field(None, alias="totalBooked") + mbo_class_id: int | None = Field(None, alias="mboClassId") + mbo_studio_id: int | None = Field(None, alias="mboStudioId") + studio: dict[Hashable, Any] | None = None + coach: dict[Hashable, Any] | None = None class CancelBooking(OtfItemBase): class_booking_id: int = Field(..., alias="classBookingId") class_booking_uuid: str = Field(..., alias="classBookingUUId") - studio_id: int = Field(..., alias="studioId") - class_id: int = Field(..., alias="classId") - is_intro: bool = Field(..., alias="isIntro") - member_id: int = Field(..., alias="memberId") - mbo_member_id: str = Field(..., alias="mboMemberId") - mbo_class_id: int = Field(..., alias="mboClassId") - mbo_visit_id: int = Field(..., alias="mboVisitId") - mbo_waitlist_entry_id: int | None = Field(..., alias="mboWaitlistEntryId") - mbo_sync_message: str = Field(..., alias="mboSyncMessage") - status: str - booked_date: datetime = Field(..., alias="bookedDate") - checked_in_date: datetime | None = Field(..., alias="checkedInDate") - cancelled_date: datetime = Field(..., alias="cancelledDate") - 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") otf_class: Class = Field(..., alias="class") - member: Member - continue_retry: bool = Field(..., alias="continueRetry") + + studio_id: int | None = Field(None, alias="studioId") + class_id: int | None = Field(None, alias="classId") + is_intro: bool | None = Field(None, alias="isIntro") + member_id: int | None = Field(None, alias="memberId") + mbo_member_id: str | None = Field(None, alias="mboMemberId") + mbo_class_id: int | None = Field(None, alias="mboClassId") + mbo_visit_id: int | None = Field(None, alias="mboVisitId") + mbo_waitlist_entry_id: int | None = Field(None, alias="mboWaitlistEntryId") + mbo_sync_message: str | None = Field(None, alias="mboSyncMessage") + status: str | None = None + booked_date: datetime | None = Field(None, alias="bookedDate") + checked_in_date: datetime | None = Field(None, alias="checkedInDate") + cancelled_date: datetime | None = Field(None, alias="cancelledDate") + created_by: str | None = Field(None, alias="createdBy") + created_date: datetime | None = Field(None, alias="createdDate") + updated_by: str | None = Field(None, alias="updatedBy") + updated_date: datetime | None = Field(None, alias="updatedDate") + is_deleted: bool | None = Field(None, alias="isDeleted") + member: dict[Hashable, Any] | None = None + continue_retry: bool | None = Field(None, alias="continueRetry") diff --git a/src/otf_api/models/challenge_tracker_detail.py b/src/otf_api/models/challenge_tracker_detail.py index c28d49e..f332224 100644 --- a/src/otf_api/models/challenge_tracker_detail.py +++ b/src/otf_api/models/challenge_tracker_detail.py @@ -1,9 +1,9 @@ from datetime import datetime -from typing import Any, ClassVar +from typing import Any from pydantic import Field -from otf_api.models.base import OtfItemBase, OtfListBase +from otf_api.models.base import OtfItemBase class MetricEntry(OtfItemBase): @@ -22,17 +22,17 @@ class BenchmarkHistory(OtfItemBase): date_created: datetime = Field(..., alias="DateCreated") date_updated: datetime = Field(..., alias="DateUpdated") class_time: datetime = Field(..., alias="ClassTime") - challenge_sub_category_id: int | None = Field(..., alias="ChallengeSubCategoryId") + challenge_sub_category_id: int | None = Field(None, alias="ChallengeSubCategoryId") class_id: int = Field(..., alias="ClassId") - substitute_id: int | None = Field(..., alias="SubstituteId") + 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 = Field(..., alias="CoachImageUrl") - workout_type_id: int | None = Field(..., alias="WorkoutTypeId") - workout_id: int | None = Field(..., alias="WorkoutId") - linked_challenges: list[Any] = Field( - ..., alias="LinkedChallenges" + coach_image_url: str | None = Field(None, alias="CoachImageUrl", exclude=True) + 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 @@ -50,7 +50,7 @@ class ChallengeHistory(OtfItemBase): class ChallengeTrackerDetail(OtfItemBase): challenge_category_id: int = Field(..., alias="ChallengeCategoryId") - challenge_sub_category_id: int | None = Field(..., alias="ChallengeSubCategoryId") + 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") @@ -59,11 +59,10 @@ class ChallengeTrackerDetail(OtfItemBase): 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") + unit: str | None = Field(None, alias="Unit") goals: None = Field(..., alias="Goals") challenge_histories: list[ChallengeHistory] = Field(..., alias="ChallengeHistories") -class ChallengeTrackerDetailList(OtfListBase): - collection_field: ClassVar[str] = "details" +class ChallengeTrackerDetailList(OtfItemBase): details: list[ChallengeTrackerDetail] diff --git a/src/otf_api/models/classes.py b/src/otf_api/models/classes.py index 58629aa..4f34350 100644 --- a/src/otf_api/models/classes.py +++ b/src/otf_api/models/classes.py @@ -1,43 +1,10 @@ from datetime import datetime -from enum import Enum -from typing import ClassVar -from humanize import precisedelta from pydantic import Field -from otf_api.models.base import OtfItemBase, OtfListBase - - -class DoW(str, Enum): - monday = "monday" - tuesday = "tuesday" - wednesday = "wednesday" - thursday = "thursday" - friday = "friday" - saturday = "saturday" - sunday = "sunday" - - @classmethod - def get_case_insensitive(cls, value: str) -> "DoW": - lcase_to_actual = {item.value.lower(): item for item in cls} - return lcase_to_actual[value.lower()] - - -class ClassType(str, Enum): - ORANGE_60_MIN_2G = "Orange 60 Min 2G" - TREAD_50 = "Tread 50" - STRENGTH_50 = "Strength 50" - ORANGE_3G = "Orange 3G" - ORANGE_60_TORNADO = "Orange 60 - Tornado" - ORANGE_TORNADO = "Orange Tornado" - ORANGE_90_MIN_3G = "Orange 90 Min 3G" - VIP_CLASS = "VIP Class" - OTHER = "Other" - - @classmethod - def get_case_insensitive(cls, value: str) -> str: - lcase_to_actual = {item.value.lower(): item.value for item in cls} - return lcase_to_actual[value.lower()] +from otf_api.models.base import OtfItemBase +from otf_api.models.enums import DoW +from otf_api.models.mixins import OtfClassTimeMixin class Address(OtfItemBase): @@ -66,45 +33,6 @@ class Coach(OtfItemBase): image_url: str | None = None -class OtfClassTimeMixin: - starts_at_local: datetime - ends_at_local: datetime - name: str - - @property - def day_of_week(self) -> str: - return self.starts_at_local.strftime("%A") - - @property - def date(self) -> str: - return self.starts_at_local.strftime("%Y-%m-%d") - - @property - def time(self) -> str: - """Returns time in 12 hour clock format, with no leading 0""" - val = self.starts_at_local.strftime("%I:%M %p") - if val[0] == "0": - val = " " + val[1:] - - return val - - @property - def duration(self) -> str: - duration = self.ends_at_local - self.starts_at_local - human_val: str = precisedelta(duration, minimum_unit="minutes") - if human_val == "1 hour and 30 minutes": - return "90 minutes" - return human_val - - @property - def class_type(self) -> ClassType: - for class_type in ClassType: - if class_type.value in self.name: - return class_type - - return ClassType.OTHER - - class OtfClass(OtfItemBase, OtfClassTimeMixin): id: str ot_class_uuid: str = Field( @@ -142,7 +70,11 @@ def day_of_week_enum(self) -> DoW: dow = self.starts_at_local.strftime("%A") return DoW.get_case_insensitive(dow) + @property + def actual_class_uuid(self) -> str: + """The UUID used to book the class""" + return self.ot_class_uuid + -class OtfClassList(OtfListBase): - collection_field: ClassVar[str] = "classes" +class OtfClassList(OtfItemBase): classes: list[OtfClass] diff --git a/src/otf_api/models/enums.py b/src/otf_api/models/enums.py index 617267d..c43f8a0 100644 --- a/src/otf_api/models/enums.py +++ b/src/otf_api/models/enums.py @@ -1,6 +1,70 @@ from enum import Enum +class StudioStatus(str, Enum): + OTHER = "OTHER" + ACTIVE = "Active" + INACTIVE = "Inactive" + COMING_SOON = "Coming Soon" + TEMP_CLOSED = "Temporarily Closed" + PERM_CLOSED = "Permanently Closed" + + +class BookingStatus(str, Enum): + CheckedIn = "Checked In" + CancelCheckinPending = "Cancel Checkin Pending" + CancelCheckinRequested = "Cancel Checkin Requested" + Cancelled = "Cancelled" + LateCancelled = "Late Cancelled" + Booked = "Booked" + Waitlisted = "Waitlisted" + CheckinPending = "Checkin Pending" + CheckinRequested = "Checkin Requested" + CheckinCancelled = "Checkin Cancelled" + + +class DoW(str, Enum): + MONDAY = "monday" + TUESDAY = "tuesday" + WEDNESDAY = "wednesday" + THURSDAY = "thursday" + FRIDAY = "friday" + SATURDAY = "saturday" + SUNDAY = "sunday" + + @classmethod + def get_case_insensitive(cls, value: str) -> "DoW": + lcase_to_actual = {item.value.lower(): item for item in cls} + return lcase_to_actual[value.lower()] + + +class ClassType(str, Enum): + ORANGE_60_MIN_2G = "Orange 60 Min 2G" + TREAD_50 = "Tread 50" + STRENGTH_50 = "Strength 50" + ORANGE_3G = "Orange 3G" + ORANGE_60_TORNADO = "Orange 60 - Tornado" + ORANGE_TORNADO = "Orange Tornado" + ORANGE_90_MIN_3G = "Orange 90 Min 3G" + VIP_CLASS = "VIP Class" + OTHER = "Other" + + @classmethod + def get_case_insensitive(cls, value: str) -> str: + lcase_to_actual = {item.value.lower(): item.value for item in cls} + return lcase_to_actual[value.lower()] + + +class StatsTime(str, Enum): + LastYear = "lastYear" + ThisYear = "thisYear" + LastMonth = "lastMonth" + ThisMonth = "thisMonth" + LastWeek = "lastWeek" + ThisWeek = "thisWeek" + AllTime = "allTime" + + class EquipmentType(int, Enum): Treadmill = 2 Strider = 3 diff --git a/src/otf_api/models/favorite_studios.py b/src/otf_api/models/favorite_studios.py index b2ea96c..e646b40 100644 --- a/src/otf_api/models/favorite_studios.py +++ b/src/otf_api/models/favorite_studios.py @@ -1,9 +1,8 @@ from datetime import datetime -from typing import ClassVar from pydantic import Field -from otf_api.models.base import OtfItemBase, OtfListBase +from otf_api.models.base import OtfItemBase class Location(OtfItemBase): @@ -32,7 +31,7 @@ class StudioLocation(OtfItemBase): bill_to_country_id: int = Field(..., alias="billToCountryId") bill_to_country: str = Field(..., alias="billToCountry") ship_to_address: str = Field(..., alias="shipToAddress") - ship_to_address2: str | None = Field(..., alias="shipToAddress2") + ship_to_address2: str | None = Field(None, alias="shipToAddress2") ship_to_city: str = Field(..., alias="shipToCity") ship_to_state: str = Field(..., alias="shipToState") ship_to_postal_code: str = Field(..., alias="shipToPostalCode") @@ -40,7 +39,7 @@ class StudioLocation(OtfItemBase): ship_to_country_id: int = Field(..., alias="shipToCountryId") ship_to_country: str = Field(..., alias="shipToCountry") physical_address: str = Field(..., alias="physicalAddress") - physical_address2: str | None = Field(..., alias="physicalAddress2") + physical_address2: str | None = Field(None, alias="physicalAddress2") physical_city: str = Field(..., alias="physicalCity") physical_state: str = Field(..., alias="physicalState") physical_postal_code: str = Field(..., alias="physicalPostalCode") @@ -57,30 +56,30 @@ class FavoriteStudio(OtfItemBase): studio_uuid: str = Field(..., alias="studioUUId") mbo_studio_id: int = Field(..., alias="mboStudioId") studio_name: str = Field(..., alias="studioName") - area_id: int | None = Field(..., alias="areaId") - market_id: int | None = Field(..., alias="marketId") - state_id: int | None = Field(..., alias="stateId") + area_id: int | None = Field(None, alias="areaId") + market_id: int | None = Field(None, alias="marketId") + state_id: int | None = Field(None, alias="stateId") studio_physical_location_id: int = Field(..., alias="studioPhysicalLocationId") studio_number: str = Field(..., alias="studioNumber") description: str | None = None - studio_version: str | None = Field(..., alias="studioVersion") + studio_version: str | None = Field(None, alias="studioVersion") studio_token: str = Field(..., alias="studioToken") studio_status: str = Field(..., alias="studioStatus") open_date: datetime = Field(..., alias="openDate") studio_type_id: int = Field(..., alias="studioTypeId") - pos_type_id: int | None = Field(..., alias="posTypeId") - logo_url: str | None = Field(..., alias="logoUrl") - page_color1: str | None = Field(..., alias="pageColor1") - page_color2: str | None = Field(..., alias="pageColor2") - page_color3: str | None = Field(..., alias="pageColor3") - page_color4: str | None = Field(..., alias="pageColor4") + pos_type_id: int | None = Field(None, alias="posTypeId") + logo_url: str | None = Field(None, alias="logoUrl") + page_color1: str | None = Field(None, alias="pageColor1") + page_color2: str | None = Field(None, alias="pageColor2") + page_color3: str | None = Field(None, alias="pageColor3") + page_color4: str | None = Field(None, alias="pageColor4") accepts_visa_master_card: bool = Field(..., alias="acceptsVisaMasterCard") accepts_american_express: bool = Field(..., alias="acceptsAmericanExpress") accepts_discover: bool = Field(..., alias="acceptsDiscover") accepts_ach: bool = Field(..., alias="acceptsACH") - sms_package_enabled: bool | None = Field(..., alias="smsPackageEnabled") - allows_dashboard_access: bool | None = Field(..., alias="allowsDashboardAccess") - pricing_level: str | None = Field(..., alias="pricingLevel") + sms_package_enabled: bool | None = Field(None, alias="smsPackageEnabled") + allows_dashboard_access: bool | None = Field(None, alias="allowsDashboardAccess") + pricing_level: str | None = Field(None, alias="pricingLevel") contact_email: str = Field(..., alias="contactEmail") time_zone: str = Field(..., alias="timeZone") environment: str @@ -95,8 +94,7 @@ class FavoriteStudio(OtfItemBase): studio_location: StudioLocation = Field(..., alias="studioLocation") -class FavoriteStudioList(OtfListBase): - collection_field: ClassVar[str] = "studios" +class FavoriteStudioList(OtfItemBase): studios: list[FavoriteStudio] @property diff --git a/src/otf_api/models/lifetime_stats.py b/src/otf_api/models/lifetime_stats.py index a48b958..f4d19a8 100644 --- a/src/otf_api/models/lifetime_stats.py +++ b/src/otf_api/models/lifetime_stats.py @@ -1,26 +1,8 @@ -from enum import Enum - from pydantic import Field from otf_api.models.base import OtfItemBase -class StatsTime(str, Enum): - LastYear = "lastYear" - ThisYear = "thisYear" - LastMonth = "lastMonth" - ThisMonth = "thisMonth" - LastWeek = "lastWeek" - ThisWeek = "thisWeek" - AllTime = "allTime" - - -class StatsType(str, Enum): - Home = "outStudio" - Studio = "inStudio" - All = "allStats" - - class OutStudioMixin(OtfItemBase): walking_distance: float = Field(..., alias="walkingDistance") running_distance: float = Field(..., alias="runningDistance") diff --git a/src/otf_api/models/member_detail.py b/src/otf_api/models/member_detail.py index 8b5daf7..0f0a9a7 100644 --- a/src/otf_api/models/member_detail.py +++ b/src/otf_api/models/member_detail.py @@ -1,7 +1,6 @@ from datetime import date, datetime -from typing import Any -from pydantic import Field +from pydantic import Field, field_validator from otf_api.models.base import OtfItemBase @@ -16,12 +15,6 @@ class Address(OtfItemBase): postal_code: str = Field(..., alias="postalCode") country: str - def __init__(self, **data): - if "memberaddressUUId" in data: - data["memberAddressUUId"] = data.pop("memberaddressUUId") - - super().__init__(**data) - class MemberCreditCard(OtfItemBase): name_on_card: str = Field(..., alias="nameOnCard") @@ -93,18 +86,18 @@ class MemberDetail(OtfItemBase): first_name: str = Field(..., alias="firstName") last_name: str = Field(..., alias="lastName") email: str - profile_picture_url: str | None = Field(..., alias="profilePictureUrl") + profile_picture_url: str | None = Field(None, alias="profilePictureUrl") alternate_emails: None = Field(..., alias="alternateEmails") - address_line1: str | None = Field(..., alias="addressLine1") - address_line2: str | None = Field(..., alias="addressLine2") + address_line1: str | None = Field(None, alias="addressLine1") + address_line2: str | None = Field(None, alias="addressLine2") city: str | None state: str | None - postal_code: str | None = Field(..., alias="postalCode") + postal_code: str | None = Field(None, alias="postalCode") phone_number: str = Field(..., alias="phoneNumber") - home_phone: str | None = Field(..., alias="homePhone") - work_phone: str | None = Field(..., alias="workPhone") + home_phone: str | None = Field(None, alias="homePhone") + work_phone: str | None = Field(None, alias="workPhone") phone_type: None = Field(..., alias="phoneType") - birth_day: date | str = Field(..., alias="birthDay") + birth_day: date = Field(..., alias="birthDay") cc_last4: str = Field(..., alias="ccLast4") cc_type: str = Field(..., alias="ccType") gender: str @@ -133,7 +126,11 @@ class MemberDetail(OtfItemBase): otf_acs_id: str = Field(..., alias="otfAcsId") member_class_summary: MemberClassSummary | None = Field(None, alias="memberClassSummary") - def __init__(self, **data: Any): - super().__init__(**data) - if self.birth_day and isinstance(self.birth_day, str): - self.birth_day = datetime.strptime(self.birth_day, "%Y-%m-%d").date() # noqa + @field_validator("birth_day") + @classmethod + def validate_birth_day(cls, value: date | str | None, **_kwargs) -> date | None: + if value is None: + return value + if not isinstance(value, date): + return datetime.strptime(value, "%Y-%m-%d").date() # noqa + return value diff --git a/src/otf_api/models/member_purchases.py b/src/otf_api/models/member_purchases.py index cb55e5b..7890910 100644 --- a/src/otf_api/models/member_purchases.py +++ b/src/otf_api/models/member_purchases.py @@ -2,7 +2,7 @@ from pydantic import Field -from otf_api.models.base import OtfItemBase, OtfListBase +from otf_api.models.base import OtfItemBase class Location(OtfItemBase): @@ -44,7 +44,7 @@ class StudioLocation(OtfItemBase): bill_to_country_id: int = Field(..., alias="billToCountryId") bill_to_country: str = Field(..., alias="billToCountry") ship_to_address: str = Field(..., alias="shipToAddress") - ship_to_address2: str | None = Field(..., alias="shipToAddress2") + ship_to_address2: str | None = Field(None, alias="shipToAddress2") ship_to_city: str = Field(..., alias="shipToCity") ship_to_state: str = Field(..., alias="shipToState") ship_to_postal_code: str = Field(..., alias="shipToPostalCode") @@ -52,7 +52,7 @@ class StudioLocation(OtfItemBase): ship_to_country_id: int = Field(..., alias="shipToCountryId") ship_to_country: str = Field(..., alias="shipToCountry") physical_address: str = Field(..., alias="physicalAddress") - physical_address2: str | None = Field(..., alias="physicalAddress2") + physical_address2: str | None = Field(None, alias="physicalAddress2") physical_city: str = Field(..., alias="physicalCity") physical_state: str = Field(..., alias="physicalState") physical_postal_code: str = Field(..., alias="physicalPostalCode") @@ -118,18 +118,18 @@ class MemberPurchase(OtfItemBase): member_purchase_date_time: datetime = Field(..., alias="memberPurchaseDateTime") member_purchase_type: str = Field(..., alias="memberPurchaseType") status: str - member_service_id: int | None = Field(..., alias="memberServiceId") - member_membership_id: int | None = Field(..., alias="memberMembershipId") - member_fee_id: int | None = Field(..., alias="memberFeeId") - pos_contract_id: int | None = Field(..., alias="posContractId") + member_service_id: int | None = Field(None, alias="memberServiceId") + member_membership_id: int | None = Field(None, alias="memberMembershipId") + member_fee_id: int | None = Field(None, alias="memberFeeId") + pos_contract_id: int | None = Field(None, alias="posContractId") pos_product_id: int = Field(..., alias="posProductId") - pos_description_id: int | None = Field(..., alias="posDescriptionId") - pos_pmt_ref_no: int | None = Field(..., alias="posPmtRefNo") + pos_description_id: int | None = Field(None, alias="posDescriptionId") + pos_pmt_ref_no: int | None = Field(None, alias="posPmtRefNo") pos_sale_id: int = Field(..., alias="posSaleId") quantity: int member_id: int = Field(..., alias="memberId") studio: Studio -class MemberPurchaseList(OtfListBase): +class MemberPurchaseList(OtfItemBase): data: list[MemberPurchase] diff --git a/src/otf_api/models/mixins.py b/src/otf_api/models/mixins.py new file mode 100644 index 0000000..ccc544e --- /dev/null +++ b/src/otf_api/models/mixins.py @@ -0,0 +1,44 @@ +from datetime import datetime + +from humanize import precisedelta + +from otf_api.models.enums import ClassType + + +class OtfClassTimeMixin: + starts_at_local: datetime + ends_at_local: datetime + name: str + + @property + def day_of_week(self) -> str: + return self.starts_at_local.strftime("%A") + + @property + def date(self) -> str: + return self.starts_at_local.strftime("%Y-%m-%d") + + @property + def time(self) -> str: + """Returns time in 12 hour clock format, with no leading 0""" + val = self.starts_at_local.strftime("%I:%M %p") + if val[0] == "0": + val = " " + val[1:] + + return val + + @property + def duration(self) -> str: + duration = self.ends_at_local - self.starts_at_local + human_val: str = precisedelta(duration, minimum_unit="minutes") + if human_val == "1 hour and 30 minutes": + return "90 minutes" + return human_val + + @property + def class_type(self) -> ClassType: + for class_type in ClassType: + if class_type.value in self.name: + return class_type + + return ClassType.OTHER 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 30b0714..eec28ff 100644 --- a/src/otf_api/models/out_of_studio_workout_history.py +++ b/src/otf_api/models/out_of_studio_workout_history.py @@ -2,7 +2,7 @@ from pydantic import Field -from otf_api.models.base import OtfItemBase, OtfListBase +from otf_api.models.base import OtfItemBase class WorkoutType(OtfItemBase): @@ -37,5 +37,5 @@ class OutOfStudioWorkoutHistory(OtfItemBase): max_heartrate: int = Field(..., alias="maxHeartrate") -class OutOfStudioWorkoutHistoryList(OtfListBase): - data: list[OutOfStudioWorkoutHistory] +class OutOfStudioWorkoutHistoryList(OtfItemBase): + workouts: list[OutOfStudioWorkoutHistory] diff --git a/src/otf_api/models/performance_summary_list.py b/src/otf_api/models/performance_summary_list.py index 57f7684..955f85c 100644 --- a/src/otf_api/models/performance_summary_list.py +++ b/src/otf_api/models/performance_summary_list.py @@ -1,8 +1,6 @@ -from typing import ClassVar - from pydantic import Field -from otf_api.models.base import OtfItemBase, OtfListBase +from otf_api.models.base import OtfItemBase class ZoneTimeMinutes(OtfItemBase): @@ -66,6 +64,5 @@ class PerformanceSummaryEntry(OtfItemBase): ratings: Ratings | None = None -class PerformanceSummaryList(OtfListBase): - collection_field: ClassVar[str] = "summaries" +class PerformanceSummaryList(OtfItemBase): summaries: list[PerformanceSummaryEntry] diff --git a/src/otf_api/models/studio_detail.py b/src/otf_api/models/studio_detail.py index d75020e..dae6c72 100644 --- a/src/otf_api/models/studio_detail.py +++ b/src/otf_api/models/studio_detail.py @@ -1,113 +1,109 @@ from datetime import datetime -from typing import ClassVar from pydantic import Field -from otf_api.models.base import OtfItemBase, OtfListBase +from otf_api.models.base import OtfItemBase class Country(OtfItemBase): country_id: int = Field(..., alias="countryId") - country_currency_code: str = Field(..., alias="countryCurrencyCode") - country_currency_name: str = Field(..., alias="countryCurrencyName") - currency_alphabetic_code: str = Field(..., alias="currencyAlphabeticCode") + country_currency_code: str | None = Field(None, alias="countryCurrencyCode") + country_currency_name: str | None = Field(None, alias="countryCurrencyName") + currency_alphabetic_code: str | None = Field(None, alias="currencyAlphabeticCode") class StudioLocation(OtfItemBase): - physical_address: str = Field(..., alias="physicalAddress") - physical_address2: str | None = Field(..., alias="physicalAddress2") - physical_city: str = Field(..., alias="physicalCity") - physical_state: str = Field(..., alias="physicalState") - physical_postal_code: str = Field(..., alias="physicalPostalCode") - physical_region: str = Field(..., alias="physicalRegion") - physical_country: str = Field(..., alias="physicalCountry") - country: Country - phone_number: str = Field(..., alias="phoneNumber") - latitude: float - longitude: float + physical_address: str | None = Field(None, alias="physicalAddress") + physical_address2: str | None = Field(None, alias="physicalAddress2") + physical_city: str | None = Field(None, alias="physicalCity") + physical_state: str | None = Field(None, alias="physicalState") + physical_postal_code: str | None = Field(None, alias="physicalPostalCode") + physical_region: str | None = Field(None, alias="physicalRegion", exclude=True) + physical_country: str | None = Field(None, alias="physicalCountry", exclude=True) + country: Country | None = Field(None, exclude=True) + phone_number: str | None = Field(None, alias="phoneNumber") + latitude: float | None = Field(None, exclude=True) + longitude: float | None = Field(None, exclude=True) class Language(OtfItemBase): - language_id: None = Field(..., alias="languageId") - language_code: None = Field(..., alias="languageCode") - language_name: None = Field(..., alias="languageName") + language_id: None = Field(None, alias="languageId") + language_code: None = Field(None, alias="languageCode") + language_name: None = Field(None, alias="languageName") class StudioLocationLocalized(OtfItemBase): - language: Language - studio_name: str | None = Field(..., alias="studioName") - studio_address: str | None = Field(..., alias="studioAddress") + language: Language | None = Field(None, exclude=True) + studio_name: str | None = Field(None, alias="studioName") + studio_address: str | None = Field(None, alias="studioAddress") class StudioProfiles(OtfItemBase): - is_web: bool = Field(..., alias="isWeb") - intro_capacity: int = Field(..., alias="introCapacity") - is_crm: bool | None = Field(..., alias="isCrm") + is_web: bool | None = Field(None, alias="isWeb") + intro_capacity: int | None = Field(None, alias="introCapacity") + is_crm: bool | None = Field(None, alias="isCrm") class SocialMediaLink(OtfItemBase): id: str - language_id: str = Field(..., alias="languageId") + language_id: str | None = Field(None, alias="languageId") name: str value: str class StudioDetail(OtfItemBase): - studio_id: int = Field(..., alias="studioId") + studio_id: int = Field(..., alias="studioId", exclude=True) studio_uuid: str = Field(..., alias="studioUUId") - mbo_studio_id: int | None = Field(..., alias="mboStudioId") - studio_number: str = Field(..., alias="studioNumber") - studio_name: str = Field(..., alias="studioName") - studio_physical_location_id: int = Field(..., alias="studioPhysicalLocationId") - time_zone: str | None = Field(..., alias="timeZone") - contact_email: str | None = Field(..., alias="contactEmail") - studio_token: str = Field(..., alias="studioToken") - environment: str - pricing_level: str | None = Field(..., alias="pricingLevel") - tax_rate: str | None = Field(..., alias="taxRate") - accepts_visa_master_card: bool = Field(..., alias="acceptsVisaMasterCard") - accepts_american_express: bool = Field(..., alias="acceptsAmericanExpress") - accepts_discover: bool = Field(..., alias="acceptsDiscover") - accepts_ach: bool = Field(..., alias="acceptsAch") - is_integrated: bool = Field(..., alias="isIntegrated") - description: str | None = None - studio_version: str | None = Field(..., alias="studioVersion") - studio_status: str = Field(..., alias="studioStatus") - open_date: datetime | None = Field(..., alias="openDate") - re_open_date: datetime | None = Field(..., alias="reOpenDate") - studio_type_id: int | None = Field(..., alias="studioTypeId") - pos_type_id: int | None = Field(..., alias="posTypeId") - market_id: int | None = Field(..., alias="marketId") - area_id: int | None = Field(..., alias="areaId") - state_id: int | None = Field(..., alias="stateId") - logo_url: str | None = Field(..., alias="logoUrl") - page_color1: str | None = Field(..., alias="pageColor1") - page_color2: str | None = Field(..., alias="pageColor2") - page_color3: str | None = Field(..., alias="pageColor3") - page_color4: str | None = Field(..., alias="pageColor4") - sms_package_enabled: bool | None = Field(..., alias="smsPackageEnabled") - allows_dashboard_access: bool | None = Field(..., alias="allowsDashboardAccess") - allows_cr_waitlist: bool = Field(..., alias="allowsCrWaitlist") - cr_waitlist_flag_last_updated: datetime | None = Field(..., alias="crWaitlistFlagLastUpdated") - royalty_rate: int | None = Field(..., alias="royaltyRate") - marketing_fund_rate: int | None = Field(..., alias="marketingFundRate") - commission_percent: int | None = Field(..., alias="commissionPercent") - is_mobile: bool | None = Field(..., alias="isMobile") - is_otbeat: bool | None = Field(..., alias="isOtbeat") - distance: float | None = None - studio_location: StudioLocation = Field(..., alias="studioLocation") - studio_location_localized: StudioLocationLocalized = Field(..., alias="studioLocationLocalized") - studio_profiles: StudioProfiles = Field(..., alias="studioProfiles") - social_media_links: list[SocialMediaLink] = Field(..., alias="socialMediaLinks") + studio_location_localized: StudioLocationLocalized | None = Field( + None, alias="studioLocationLocalized", exclude=True + ) + studio_location: StudioLocation | None = Field(None, alias="studioLocation") + studio_name: str | None = Field(None, alias="studioName") + studio_number: str | None = Field(None, alias="studioNumber", exclude=True) + studio_physical_location_id: int | None = Field(None, alias="studioPhysicalLocationId", exclude=True) + studio_profiles: StudioProfiles | None = Field(None, alias="studioProfiles", exclude=True) + studio_status: str | None = Field(None, alias="studioStatus", exclude=True) + studio_token: str | None = Field(None, alias="studioToken", exclude=True) + studio_type_id: int | None = Field(None, alias="studioTypeId", exclude=True) + studio_version: str | None = Field(None, alias="studioVersion", exclude=True) + mbo_studio_id: int | None = Field(None, alias="mboStudioId", exclude=True) + accepts_ach: bool | None = Field(None, alias="acceptsAch", exclude=True) + accepts_american_express: bool | None = Field(None, alias="acceptsAmericanExpress", exclude=True) + accepts_discover: bool | None = Field(None, alias="acceptsDiscover", exclude=True) + accepts_visa_master_card: bool | None = Field(None, alias="acceptsVisaMasterCard", exclude=True) + allows_cr_waitlist: bool | None = Field(None, alias="allowsCrWaitlist", exclude=True) + allows_dashboard_access: bool | None = Field(None, alias="allowsDashboardAccess", exclude=True) + area_id: int | None = Field(None, alias="areaId", exclude=True) + commission_percent: int | None = Field(None, alias="commissionPercent", exclude=True) + contact_email: str | None = Field(None, alias="contactEmail", exclude=True) + cr_waitlist_flag_last_updated: datetime | None = Field(None, alias="crWaitlistFlagLastUpdated", exclude=True) + description: str | None = Field(None, exclude=True) + distance: float | None = Field(None, exclude=True) + environment: str | None = Field(None, exclude=True) + is_integrated: bool | None = Field(None, alias="isIntegrated", exclude=True) + is_mobile: bool | None = Field(None, alias="isMobile", exclude=True) + is_otbeat: bool | None = Field(None, alias="isOtbeat", exclude=True) + logo_url: str | None = Field(None, alias="logoUrl", exclude=True) + market_id: int | None = Field(None, alias="marketId", exclude=True) + marketing_fund_rate: int | None = Field(None, alias="marketingFundRate", exclude=True) + open_date: datetime | None = Field(None, alias="openDate", exclude=True) + pos_type_id: int | None = Field(None, alias="posTypeId", exclude=True) + pricing_level: str | None = Field(None, alias="pricingLevel", exclude=True) + re_open_date: datetime | None = Field(None, alias="reOpenDate", exclude=True) + royalty_rate: int | None = Field(None, alias="royaltyRate", exclude=True) + sms_package_enabled: bool | None = Field(None, alias="smsPackageEnabled", exclude=True) + social_media_links: list[SocialMediaLink] | None = Field(None, alias="socialMediaLinks", exclude=True) + state_id: int | None = Field(None, alias="stateId", exclude=True) + tax_rate: str | None = Field(None, alias="taxRate", exclude=True) + time_zone: str | None = Field(None, alias="timeZone", exclude=True) class Pagination(OtfItemBase): - page_index: int = Field(..., alias="pageIndex") - page_size: int = Field(..., alias="pageSize") - total_count: int = Field(..., alias="totalCount") - total_pages: int = Field(..., alias="totalPages") + page_index: int | None = Field(None, alias="pageIndex") + page_size: int | None = Field(None, alias="pageSize") + total_count: int | None = Field(None, alias="totalCount") + total_pages: int | None = Field(None, alias="totalPages") -class StudioDetailList(OtfListBase): - collection_field: ClassVar[str] = "studios" +class StudioDetailList(OtfItemBase): studios: list[StudioDetail] diff --git a/src/otf_api/models/studio_services.py b/src/otf_api/models/studio_services.py index a037f55..f25b161 100644 --- a/src/otf_api/models/studio_services.py +++ b/src/otf_api/models/studio_services.py @@ -2,7 +2,7 @@ from pydantic import Field -from otf_api.models.base import OtfItemBase, OtfListBase +from otf_api.models.base import OtfItemBase class Currency(OtfItemBase): @@ -53,5 +53,5 @@ class StudioService(OtfItemBase): studio: Studio -class StudioServiceList(OtfListBase): +class StudioServiceList(OtfItemBase): data: list[StudioService] From d1a0bf908c964e440a4d8f57836f4aa0bf1a865d Mon Sep 17 00:00:00 2001 From: Jessica Smith <12jessicasmith34@gmail.com> Date: Mon, 14 Oct 2024 09:32:49 -0500 Subject: [PATCH 3/6] import models instead of list of models, import exceptions, use stdlib logger instead of loguru --- src/otf_api/api.py | 187 ++++++++++++++++---------------------- src/otf_api/exceptions.py | 18 ++++ 2 files changed, 94 insertions(+), 111 deletions(-) create mode 100644 src/otf_api/exceptions.py diff --git a/src/otf_api/api.py b/src/otf_api/api.py index 12b4a7d..1ecdda9 100644 --- a/src/otf_api/api.py +++ b/src/otf_api/api.py @@ -10,44 +10,9 @@ from loguru import logger from yarl import URL +from otf_api import models from otf_api.auth import OtfUser -from otf_api.models import ( - BodyCompositionList, - BookClass, - BookingList, - BookingStatus, - CancelBooking, - ChallengeTrackerContent, - ChallengeTrackerDetailList, - ChallengeType, - ClassType, - DoW, - EquipmentType, - FavoriteStudioList, - LatestAgreement, - MemberDetail, - MemberMembership, - MemberPurchaseList, - OtfClassList, - OutOfStudioWorkoutHistoryList, - Pagination, - PerformanceSummaryDetail, - PerformanceSummaryList, - StatsResponse, - StatsTime, - StudioDetail, - StudioDetailList, - StudioServiceList, - Telemetry, - TelemetryHrHistory, - TelemetryMaxHr, - TotalClasses, -) - - -class AlreadyBookedError(Exception): - pass - +from otf_api.exceptions import AlreadyBookedError if typing.TYPE_CHECKING: from loguru import Logger @@ -92,7 +57,7 @@ def __init__( user (OtfUser, optional): A user object. Default is None. """ - self.member: MemberDetail + self.member: models.MemberDetail self.home_studio_uuid: str if user: @@ -119,7 +84,7 @@ def __init__( self.member = self._get_member_details_sync() self.home_studio_uuid = self.member.home_studio.studio_uuid - def _get_member_details_sync(self) -> MemberDetail: + def _get_member_details_sync(self): """Get the member details synchronously. This is used to get the member details when the API is first initialized, to let use initialize @@ -130,10 +95,10 @@ def _get_member_details_sync(self) -> MemberDetail: """ url = f"https://{API_BASE_URL}/member/members/{self._member_id}" resp = requests.get(url, headers=self.headers) - return MemberDetail(**resp.json()["data"]) + return models.MemberDetail(**resp.json()["data"]) @property - def headers(self) -> dict[str, str]: + def headers(self): """Get the headers for the API request.""" # check the token before making a request in case it has expired @@ -146,7 +111,7 @@ def headers(self) -> dict[str, str]: } @property - def session(self) -> aiohttp.ClientSession: + def session(self): """Get the aiohttp session.""" if not getattr(self, "_session", None): self._session = aiohttp.ClientSession(headers=self.headers) @@ -224,7 +189,7 @@ async def _performance_summary_request( """Perform an API request to the performance summary API.""" return await self._do(method, API_IO_BASE_URL, url, params, headers) - async def get_body_composition_list(self) -> BodyCompositionList: + async def get_body_composition_list(self): """Get the member's body composition list. Returns: @@ -232,7 +197,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 models.BodyCompositionList(data=data["data"]) async def get_classes( self, @@ -241,11 +206,11 @@ async def get_classes( start_date: str | None = None, end_date: str | None = None, limit: int | None = None, - class_type: ClassType | list[ClassType] | None = None, + class_type: models.ClassType | list[models.ClassType] | None = None, exclude_cancelled: bool = False, - day_of_week: list[DoW] | None = None, + day_of_week: list[models.DoW] | None = None, start_time: list[str] | None = None, - ) -> OtfClassList: + ): """Get the classes for the user. Returns a list of classes that are available for the user, based on the studio UUIDs provided. If no studio @@ -278,7 +243,7 @@ class types can be provided, if there are multiple there will be a call per clas params = {"studio_ids": studio_uuids} classes_resp = await self._classes_request("GET", path, params=params) - classes_list = OtfClassList(classes=classes_resp["items"]) + classes_list = models.OtfClassList(classes=classes_resp["items"]) if start_date: start_dtme = datetime.strptime(start_date, "%Y-%m-%d") # noqa @@ -319,7 +284,7 @@ class types can be provided, if there are multiple there will be a call per clas classes_list.classes = list(filter(lambda c: not c.canceled, classes_list.classes)) - booking_resp = await self.get_bookings(start_date, end_date, status=BookingStatus.Booked) + booking_resp = await self.get_bookings(start_date, end_date, status=models.BookingStatus.Booked) booked_classes = {b.otf_class.class_uuid for b in booking_resp.bookings} for otf_class in classes_list.classes: @@ -327,7 +292,7 @@ class types can be provided, if there are multiple there will be a call per clas return classes_list - async def get_total_classes(self) -> TotalClasses: + async def get_total_classes(self): """Get the member's total classes. This is a simple object reflecting the total number of classes attended, both in-studio and OT Live. @@ -335,9 +300,9 @@ async def get_total_classes(self) -> TotalClasses: TotalClasses: The member's total classes. """ data = await self._default_request("GET", "/mobile/v1/members/classes/summary") - return TotalClasses(**data["data"]) + return models.TotalClasses(**data["data"]) - async def book_class(self, class_uuid: str) -> BookClass | typing.Any: + async def book_class(self, class_uuid: str): """Book a class by class_uuid. Args: @@ -362,10 +327,10 @@ async def book_class(self, class_uuid: str) -> BookClass | typing.Any: raise AlreadyBookedError(f"Class {class_uuid} is already booked.") raise Exception(f"Error booking class {class_uuid}: {json.dumps(resp)}") - data = BookClass(**resp["data"]) + data = models.BookClass(**resp["data"]) return data - async def cancel_booking(self, booking_uuid: str) -> CancelBooking: + async def cancel_booking(self, booking_uuid: str): """Cancel a class by booking_uuid. Args: @@ -379,17 +344,17 @@ async def cancel_booking(self, booking_uuid: str) -> CancelBooking: resp = await self._default_request( "DELETE", f"/member/members/{self._member_id}/bookings/{booking_uuid}", params=params ) - return CancelBooking(**resp["data"]) + return models.CancelBooking(**resp["data"]) async def get_bookings( self, start_date: date | str | None = None, end_date: date | str | None = None, - status: BookingStatus | None = None, + status: models.BookingStatus | None = None, limit: int | None = None, exclude_cancelled: bool = True, exclude_checkedin: bool = True, - ) -> BookingList: + ): """Get the member's bookings. Args: @@ -426,7 +391,7 @@ async def get_bookings( used. I'm not sure if this is a bug or if the API is supposed to work this way. """ - if exclude_cancelled and status == BookingStatus.Cancelled: + if exclude_cancelled and status == models.BookingStatus.Cancelled: logger.warning( "Cannot exclude cancelled bookings when status is Cancelled. Setting exclude_cancelled to False." ) @@ -446,7 +411,7 @@ async def get_bookings( bookings = res["data"][:limit] if limit else res["data"] - data = BookingList(bookings=bookings) + data = models.BookingList(bookings=bookings) data.bookings = sorted(data.bookings, key=lambda x: x.otf_class.starts_at_local) for booking in data.bookings: @@ -458,14 +423,14 @@ async def get_bookings( booking.is_home_studio = False if exclude_cancelled: - data.bookings = [b for b in data.bookings if b.status != BookingStatus.Cancelled] + data.bookings = [b for b in data.bookings if b.status != models.BookingStatus.Cancelled] if exclude_checkedin: - data.bookings = [b for b in data.bookings if b.status != BookingStatus.CheckedIn] + data.bookings = [b for b in data.bookings if b.status != models.BookingStatus.CheckedIn] return data - async def _get_bookings_old(self, status: BookingStatus | None = None) -> BookingList: + async def _get_bookings_old(self, status: models.BookingStatus | None = None): """Get the member's bookings. Args: @@ -497,10 +462,10 @@ async def _get_bookings_old(self, status: BookingStatus | None = None) -> Bookin """ if status and status not in [ - BookingStatus.Cancelled, - BookingStatus.Booked, - BookingStatus.CheckedIn, - BookingStatus.Waitlisted, + models.BookingStatus.Cancelled, + models.BookingStatus.Booked, + models.BookingStatus.CheckedIn, + models.BookingStatus.Waitlisted, ]: raise ValueError( "Invalid status provided. Only Cancelled, Booked, CheckedIn, Waitlisted, and None are supported." @@ -512,20 +477,23 @@ async def _get_bookings_old(self, status: BookingStatus | None = None) -> Bookin res = await self._default_request("GET", f"/member/members/{self._member_id}/bookings", params=params) - return BookingList(bookings=res["data"]) + return models.BookingList(bookings=res["data"]) - async def get_challenge_tracker_content(self) -> ChallengeTrackerContent: + async def get_challenge_tracker_content(self): """Get the member's challenge tracker content. Returns: ChallengeTrackerContent: The member's challenge tracker content. """ data = await self._default_request("GET", f"/challenges/v3.1/member/{self._member_id}") - return ChallengeTrackerContent(**data["Dto"]) + return models.ChallengeTrackerContent(**data["Dto"]) async def get_challenge_tracker_detail( - self, equipment_id: EquipmentType, challenge_type_id: ChallengeType, challenge_sub_type_id: int = 0 - ) -> ChallengeTrackerDetailList: + self, + equipment_id: models.EquipmentType, + challenge_type_id: models.ChallengeType, + challenge_sub_type_id: int = 0, + ): """Get the member's challenge tracker details. Args: @@ -549,9 +517,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"]) + return models.ChallengeTrackerDetailList(details=data["Dto"]) - async def get_challenge_tracker_participation(self, challenge_type_id: ChallengeType) -> typing.Any: + async def get_challenge_tracker_participation(self, challenge_type_id: models.ChallengeType): """Get the member's participation in a challenge. Args: @@ -575,7 +543,7 @@ async def get_challenge_tracker_participation(self, challenge_type_id: Challenge async def get_member_detail( self, include_addresses: bool = True, include_class_summary: bool = True, include_credit_card: bool = False - ) -> MemberDetail: + ): """Get the member details. Args: @@ -611,9 +579,9 @@ async def get_member_detail( params = {"include": ",".join(include)} if include else None data = await self._default_request("GET", f"/member/members/{self._member_id}", params=params) - return MemberDetail(**data["data"]) + return models.MemberDetail(**data["data"]) - async def get_member_membership(self) -> MemberMembership: + async def get_member_membership(self): """Get the member's membership details. Returns: @@ -621,18 +589,18 @@ async def get_member_membership(self) -> MemberMembership: """ data = await self._default_request("GET", f"/member/members/{self._member_id}/memberships") - return MemberMembership(**data["data"]) + return models.MemberMembership(**data["data"]) - async def get_member_purchases(self) -> MemberPurchaseList: + async def get_member_purchases(self): """Get the member's purchases, including monthly subscriptions and class packs. Returns: MemberPurchaseList: The member's purchases. """ data = await self._default_request("GET", f"/member/members/{self._member_id}/purchases") - return MemberPurchaseList(data=data["data"]) + return models.MemberPurchaseList(data=data["data"]) - async def get_member_lifetime_stats(self, select_time: StatsTime = StatsTime.AllTime) -> StatsResponse: + async def get_member_lifetime_stats(self, select_time: models.StatsTime = models.StatsTime.AllTime): """Get the member's lifetime stats. Args: @@ -649,10 +617,9 @@ async def get_member_lifetime_stats(self, select_time: StatsTime = StatsTime.All data = await self._default_request("GET", f"/performance/v2/{self._member_id}/over-time/{select_time.value}") - stats = StatsResponse(**data["data"]) - return stats + return models.StatsResponse(**data["data"]) - async def get_out_of_studio_workout_history(self) -> OutOfStudioWorkoutHistoryList: + async def get_out_of_studio_workout_history(self): """Get the member's out of studio workout history. Returns: @@ -660,9 +627,9 @@ async def get_out_of_studio_workout_history(self) -> OutOfStudioWorkoutHistoryLi """ data = await self._default_request("GET", f"/member/members/{self._member_id}/out-of-studio-workout") - return OutOfStudioWorkoutHistoryList(data=data["data"]) + return models.OutOfStudioWorkoutHistoryList(data=data["data"]) - async def get_favorite_studios(self) -> FavoriteStudioList: + async def get_favorite_studios(self): """Get the member's favorite studios. Returns: @@ -670,9 +637,9 @@ async def get_favorite_studios(self) -> FavoriteStudioList: """ data = await self._default_request("GET", f"/member/members/{self._member_id}/favorite-studios") - return FavoriteStudioList(studios=data["data"]) + return models.FavoriteStudioList(studios=data["data"]) - async def get_latest_agreement(self) -> LatestAgreement: + async def get_latest_agreement(self): """Get the latest agreement for the member. Returns: @@ -684,9 +651,9 @@ async def get_latest_agreement(self) -> LatestAgreement: in general. The agreement ID is hardcoded in the endpoint, so it will always return the same agreement. """ data = await self._default_request("GET", "/member/agreements/9d98fb27-0f00-4598-ad08-5b1655a59af6") - return LatestAgreement(**data["data"]) + return models.LatestAgreement(**data["data"]) - async def get_studio_services(self, studio_uuid: str | None = None) -> StudioServiceList: + async def get_studio_services(self, studio_uuid: str | None = None): """Get the services available at a specific studio. If no studio UUID is provided, the member's home studio will be used. @@ -699,9 +666,9 @@ async def get_studio_services(self, studio_uuid: str | None = None) -> StudioSer """ studio_uuid = studio_uuid or self.home_studio_uuid data = await self._default_request("GET", f"/member/studios/{studio_uuid}/services") - return StudioServiceList(data=data["data"]) + return models.StudioServiceList(data=data["data"]) - async def get_performance_summaries(self, limit: int = 30) -> PerformanceSummaryList: + async def get_performance_summaries(self, limit: int = 30): """Get a list of performance summaries for the authenticated user. Args: @@ -719,10 +686,9 @@ async def get_performance_summaries(self, limit: int = 30) -> PerformanceSummary path = "/v1/performance-summaries" params = {"limit": limit} res = await self._performance_summary_request("GET", path, headers=self._perf_api_headers, params=params) - retval = PerformanceSummaryList(summaries=res["items"]) - return retval + return models.PerformanceSummaryList(summaries=res["items"]) - async def get_performance_summary(self, performance_summary_id: str) -> PerformanceSummaryDetail: + async def get_performance_summary(self, performance_summary_id: str): """Get a detailed performance summary for a given workout. Args: @@ -734,10 +700,9 @@ async def get_performance_summary(self, performance_summary_id: str) -> Performa path = f"/v1/performance-summaries/{performance_summary_id}" res = await self._performance_summary_request("GET", path, headers=self._perf_api_headers) - retval = PerformanceSummaryDetail(**res) - return retval + return models.PerformanceSummaryDetail(**res) - async def get_studio_detail(self, studio_uuid: str | None = None) -> StudioDetail: + async def get_studio_detail(self, studio_uuid: str | None = None): """Get detailed information about a specific studio. If no studio UUID is provided, it will default to the user's home studio. @@ -754,7 +719,7 @@ async def get_studio_detail(self, studio_uuid: str | None = None) -> StudioDetai params = {"include": "locations"} res = await self._default_request("GET", path, params=params) - return StudioDetail(**res["data"]) + return models.StudioDetail(**res["data"]) async def search_studios_by_geo( self, @@ -763,7 +728,7 @@ async def search_studios_by_geo( distance: float = 50, page_index: int = 1, page_size: int = 50, - ) -> StudioDetailList: + ): """Search for studios by geographic location. Args: @@ -806,21 +771,21 @@ async def search_studios_by_geo( "distance": distance, } - all_results: list[StudioDetail] = [] + all_results: list[models.StudioDetail] = [] while True: res = await self._default_request("GET", path, params=params) - pagination = Pagination(**res["data"].pop("pagination")) - all_results.extend([StudioDetail(**studio) for studio in res["data"]["studios"]]) + pagination = models.Pagination(**res["data"].pop("pagination")) + all_results.extend([models.StudioDetail(**studio) for studio in res["data"]["studios"]]) if len(all_results) == pagination.total_count: break params["pageIndex"] += 1 - return StudioDetailList(studios=all_results) + return models.StudioDetailList(studios=all_results) - async def get_hr_history(self) -> TelemetryHrHistory: + async def get_hr_history(self): """Get the heartrate history for the user. Returns a list of history items that contain the max heartrate, start/end bpm for each zone, @@ -834,9 +799,9 @@ async def get_hr_history(self) -> TelemetryHrHistory: params = {"memberUuid": self._member_id} res = await self._telemetry_request("GET", path, params=params) - return TelemetryHrHistory(**res) + return models.TelemetryHrHistory(**res) - async def get_max_hr(self) -> TelemetryMaxHr: + async def get_max_hr(self): """Get the max heartrate for the user. Returns a simple object that has the member_uuid and the max_hr. @@ -849,9 +814,9 @@ async def get_max_hr(self) -> TelemetryMaxHr: params = {"memberUuid": self._member_id} res = await self._telemetry_request("GET", path, params=params) - return TelemetryMaxHr(**res) + return models.TelemetryMaxHr(**res) - async def get_telemetry(self, performance_summary_id: str, max_data_points: int = 120) -> Telemetry: + async def get_telemetry(self, performance_summary_id: str, max_data_points: int = 120): """Get the telemetry for a performance summary. This returns an object that contains the max heartrate, start/end bpm for each zone, @@ -869,11 +834,11 @@ async def get_telemetry(self, performance_summary_id: str, max_data_points: int params = {"classHistoryUuid": performance_summary_id, "maxDataPoints": max_data_points} res = await self._telemetry_request("GET", path, params=params) - return Telemetry(**res) + return models.Telemetry(**res) # the below do not return any data for me, so I can't test them - async def _get_member_services(self, active_only: bool = True) -> typing.Any: + async def _get_member_services(self, active_only: bool = True): """Get the member's services. Args: @@ -888,7 +853,7 @@ async def _get_member_services(self, active_only: bool = True) -> typing.Any: ) return data - async def _get_aspire_data(self, datetime: str | None = None, unit: str | None = None) -> typing.Any: + async def _get_aspire_data(self, datetime: str | None = None, unit: str | None = None): """Get data from the member's aspire wearable. Note: I don't have an aspire wearable, so I can't test this. diff --git a/src/otf_api/exceptions.py b/src/otf_api/exceptions.py new file mode 100644 index 0000000..d827d9c --- /dev/null +++ b/src/otf_api/exceptions.py @@ -0,0 +1,18 @@ +class BookingError(Exception): + booking_uuid: str | None + + def __init__(self, message: str, booking_uuid: str | None = None): + super().__init__(message) + self.booking_uuid = booking_uuid + + +class AlreadyBookedError(BookingError): ... + + +class BookingAlreadyCancelledError(BookingError): ... + + +class OutsideSchedulingWindowError(Exception): ... + + +class BookingNotFoundError(Exception): ... From a7acb81e4be7a05ee66384c9abf676c3a79b0ee7 Mon Sep 17 00:00:00 2001 From: Jessica Smith <12jessicasmith34@gmail.com> Date: Mon, 14 Oct 2024 09:33:27 -0500 Subject: [PATCH 4/6] remove pendulum, use stdlib logger instead of loguru --- poetry.lock | 138 +--------------------------------------- pyproject.toml | 3 - src/otf_api/__init__.py | 8 --- src/otf_api/auth.py | 21 +++--- 4 files changed, 12 insertions(+), 158 deletions(-) diff --git a/poetry.lock b/poetry.lock index ea8077f..fa86ca6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1948,105 +1948,6 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] -[[package]] -name = "pendulum" -version = "3.0.0" -description = "Python datetimes made easy" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pendulum-3.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2cf9e53ef11668e07f73190c805dbdf07a1939c3298b78d5a9203a86775d1bfd"}, - {file = "pendulum-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb551b9b5e6059377889d2d878d940fd0bbb80ae4810543db18e6f77b02c5ef6"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c58227ac260d5b01fc1025176d7b31858c9f62595737f350d22124a9a3ad82d"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60fb6f415fea93a11c52578eaa10594568a6716602be8430b167eb0d730f3332"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b69f6b4dbcb86f2c2fe696ba991e67347bcf87fe601362a1aba6431454b46bde"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:138afa9c373ee450ede206db5a5e9004fd3011b3c6bbe1e57015395cd076a09f"}, - {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:83d9031f39c6da9677164241fd0d37fbfc9dc8ade7043b5d6d62f56e81af8ad2"}, - {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c2308af4033fa534f089595bcd40a95a39988ce4059ccd3dc6acb9ef14ca44a"}, - {file = "pendulum-3.0.0-cp310-none-win_amd64.whl", hash = "sha256:9a59637cdb8462bdf2dbcb9d389518c0263799189d773ad5c11db6b13064fa79"}, - {file = "pendulum-3.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3725245c0352c95d6ca297193192020d1b0c0f83d5ee6bb09964edc2b5a2d508"}, - {file = "pendulum-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c035f03a3e565ed132927e2c1b691de0dbf4eb53b02a5a3c5a97e1a64e17bec"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597e66e63cbd68dd6d58ac46cb7a92363d2088d37ccde2dae4332ef23e95cd00"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99a0f8172e19f3f0c0e4ace0ad1595134d5243cf75985dc2233e8f9e8de263ca"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77d8839e20f54706aed425bec82a83b4aec74db07f26acd039905d1237a5e1d4"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afde30e8146292b059020fbc8b6f8fd4a60ae7c5e6f0afef937bbb24880bdf01"}, - {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:660434a6fcf6303c4efd36713ca9212c753140107ee169a3fc6c49c4711c2a05"}, - {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dee9e5a48c6999dc1106eb7eea3e3a50e98a50651b72c08a87ee2154e544b33e"}, - {file = "pendulum-3.0.0-cp311-none-win_amd64.whl", hash = "sha256:d4cdecde90aec2d67cebe4042fd2a87a4441cc02152ed7ed8fb3ebb110b94ec4"}, - {file = "pendulum-3.0.0-cp311-none-win_arm64.whl", hash = "sha256:773c3bc4ddda2dda9f1b9d51fe06762f9200f3293d75c4660c19b2614b991d83"}, - {file = "pendulum-3.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:409e64e41418c49f973d43a28afe5df1df4f1dd87c41c7c90f1a63f61ae0f1f7"}, - {file = "pendulum-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a38ad2121c5ec7c4c190c7334e789c3b4624798859156b138fcc4d92295835dc"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde4d0b2024b9785f66b7f30ed59281bd60d63d9213cda0eb0910ead777f6d37"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2c5675769fb6d4c11238132962939b960fcb365436b6d623c5864287faa319"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af95e03e066826f0f4c65811cbee1b3123d4a45a1c3a2b4fc23c4b0dff893b5"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2165a8f33cb15e06c67070b8afc87a62b85c5a273e3aaa6bc9d15c93a4920d6f"}, - {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ad5e65b874b5e56bd942546ea7ba9dd1d6a25121db1c517700f1c9de91b28518"}, - {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17fe4b2c844bbf5f0ece69cfd959fa02957c61317b2161763950d88fed8e13b9"}, - {file = "pendulum-3.0.0-cp312-none-win_amd64.whl", hash = "sha256:78f8f4e7efe5066aca24a7a57511b9c2119f5c2b5eb81c46ff9222ce11e0a7a5"}, - {file = "pendulum-3.0.0-cp312-none-win_arm64.whl", hash = "sha256:28f49d8d1e32aae9c284a90b6bb3873eee15ec6e1d9042edd611b22a94ac462f"}, - {file = "pendulum-3.0.0-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:d4e2512f4e1a4670284a153b214db9719eb5d14ac55ada5b76cbdb8c5c00399d"}, - {file = "pendulum-3.0.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:3d897eb50883cc58d9b92f6405245f84b9286cd2de6e8694cb9ea5cb15195a32"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e169cc2ca419517f397811bbe4589cf3cd13fca6dc38bb352ba15ea90739ebb"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17c3084a4524ebefd9255513692f7e7360e23c8853dc6f10c64cc184e1217ab"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:826d6e258052715f64d05ae0fc9040c0151e6a87aae7c109ba9a0ed930ce4000"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2aae97087872ef152a0c40e06100b3665d8cb86b59bc8471ca7c26132fccd0f"}, - {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ac65eeec2250d03106b5e81284ad47f0d417ca299a45e89ccc69e36130ca8bc7"}, - {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a5346d08f3f4a6e9e672187faa179c7bf9227897081d7121866358af369f44f9"}, - {file = "pendulum-3.0.0-cp37-none-win_amd64.whl", hash = "sha256:235d64e87946d8f95c796af34818c76e0f88c94d624c268693c85b723b698aa9"}, - {file = "pendulum-3.0.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:6a881d9c2a7f85bc9adafcfe671df5207f51f5715ae61f5d838b77a1356e8b7b"}, - {file = "pendulum-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d7762d2076b9b1cb718a6631ad6c16c23fc3fac76cbb8c454e81e80be98daa34"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e8e36a8130819d97a479a0e7bf379b66b3b1b520e5dc46bd7eb14634338df8c"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7dc843253ac373358ffc0711960e2dd5b94ab67530a3e204d85c6e8cb2c5fa10"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a78ad3635d609ceb1e97d6aedef6a6a6f93433ddb2312888e668365908c7120"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a137e9e0d1f751e60e67d11fc67781a572db76b2296f7b4d44554761049d6"}, - {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c95984037987f4a457bb760455d9ca80467be792236b69d0084f228a8ada0162"}, - {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d29c6e578fe0f893766c0d286adbf0b3c726a4e2341eba0917ec79c50274ec16"}, - {file = "pendulum-3.0.0-cp38-none-win_amd64.whl", hash = "sha256:deaba8e16dbfcb3d7a6b5fabdd5a38b7c982809567479987b9c89572df62e027"}, - {file = "pendulum-3.0.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b11aceea5b20b4b5382962b321dbc354af0defe35daa84e9ff3aae3c230df694"}, - {file = "pendulum-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a90d4d504e82ad236afac9adca4d6a19e4865f717034fc69bafb112c320dcc8f"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:825799c6b66e3734227756fa746cc34b3549c48693325b8b9f823cb7d21b19ac"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad769e98dc07972e24afe0cff8d365cb6f0ebc7e65620aa1976fcfbcadc4c6f3"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6fc26907eb5fb8cc6188cc620bc2075a6c534d981a2f045daa5f79dfe50d512"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c717eab1b6d898c00a3e0fa7781d615b5c5136bbd40abe82be100bb06df7a56"}, - {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3ddd1d66d1a714ce43acfe337190be055cdc221d911fc886d5a3aae28e14b76d"}, - {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:822172853d7a9cf6da95d7b66a16c7160cb99ae6df55d44373888181d7a06edc"}, - {file = "pendulum-3.0.0-cp39-none-win_amd64.whl", hash = "sha256:840de1b49cf1ec54c225a2a6f4f0784d50bd47f68e41dc005b7f67c7d5b5f3ae"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b1f74d1e6ffe5d01d6023870e2ce5c2191486928823196f8575dcc786e107b1"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:729e9f93756a2cdfa77d0fc82068346e9731c7e884097160603872686e570f07"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e586acc0b450cd21cbf0db6bae386237011b75260a3adceddc4be15334689a9a"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e7944ffc1f0099a79ff468ee9630c73f8c7835cd76fdb57ef7320e6a409df4"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fa30af36bd8e50686846bdace37cf6707bdd044e5cb6e1109acbad3277232e04"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:440215347b11914ae707981b9a57ab9c7b6983ab0babde07063c6ee75c0dc6e7"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:314c4038dc5e6a52991570f50edb2f08c339debdf8cea68ac355b32c4174e820"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5acb1d386337415f74f4d1955c4ce8d0201978c162927d07df8eb0692b2d8533"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a789e12fbdefaffb7b8ac67f9d8f22ba17a3050ceaaa635cd1cc4645773a4b1e"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:860aa9b8a888e5913bd70d819306749e5eb488e6b99cd6c47beb701b22bdecf5"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5ebc65ea033ef0281368217fbf59f5cb05b338ac4dd23d60959c7afcd79a60a0"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d9fef18ab0386ef6a9ac7bad7e43ded42c83ff7ad412f950633854f90d59afa8"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1c134ba2f0571d0b68b83f6972e2307a55a5a849e7dac8505c715c531d2a8795"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:385680812e7e18af200bb9b4a49777418c32422d05ad5a8eb85144c4a285907b"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eec91cd87c59fb32ec49eb722f375bd58f4be790cae11c1b70fac3ee4f00da0"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4386bffeca23c4b69ad50a36211f75b35a4deb6210bdca112ac3043deb7e494a"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dfbcf1661d7146d7698da4b86e7f04814221081e9fe154183e34f4c5f5fa3bf8"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:04a1094a5aa1daa34a6b57c865b25f691848c61583fb22722a4df5699f6bf74c"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5b0ec85b9045bd49dd3a3493a5e7ddfd31c36a2a60da387c419fa04abcaecb23"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0a15b90129765b705eb2039062a6daf4d22c4e28d1a54fa260892e8c3ae6e157"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:bb8f6d7acd67a67d6fedd361ad2958ff0539445ef51cbe8cd288db4306503cd0"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd69b15374bef7e4b4440612915315cc42e8575fcda2a3d7586a0d88192d0c88"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc00f8110db6898360c53c812872662e077eaf9c75515d53ecc65d886eec209a"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:83a44e8b40655d0ba565a5c3d1365d27e3e6778ae2a05b69124db9e471255c4a"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1a3604e9fbc06b788041b2a8b78f75c243021e0f512447806a6d37ee5214905d"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:92c307ae7accebd06cbae4729f0ba9fa724df5f7d91a0964b1b972a22baa482b"}, - {file = "pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e"}, -] - -[package.dependencies] -python-dateutil = ">=2.6" -tzdata = ">=2020.1" - -[package.extras] -test = ["time-machine (>=2.6.0)"] - [[package]] name = "pint" version = "0.24.3" @@ -2820,32 +2721,6 @@ typing-extensions = "*" dev = ["mypy", "packaging", "pre-commit", "pytest", "pytest-cov", "rich-codex", "ruff", "types-setuptools"] docs = ["markdown-include", "mkdocs", "mkdocs-glightbox", "mkdocs-material-extensions", "mkdocs-material[imaging] (>=9.5.18,<9.6.0)", "mkdocs-rss-plugin", "mkdocstrings[python]", "rich-codex"] -[[package]] -name = "ruff" -version = "0.4.9" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruff-0.4.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b262ed08d036ebe162123170b35703aaf9daffecb698cd367a8d585157732991"}, - {file = "ruff-0.4.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:98ec2775fd2d856dc405635e5ee4ff177920f2141b8e2d9eb5bd6efd50e80317"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4555056049d46d8a381f746680db1c46e67ac3b00d714606304077682832998e"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e91175fbe48f8a2174c9aad70438fe9cb0a5732c4159b2a10a3565fea2d94cde"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e8e7b95673f22e0efd3571fb5b0cf71a5eaaa3cc8a776584f3b2cc878e46bff"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2d45ddc6d82e1190ea737341326ecbc9a61447ba331b0a8962869fcada758505"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78de3fdb95c4af084087628132336772b1c5044f6e710739d440fc0bccf4d321"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:06b60f91bfa5514bb689b500a25ba48e897d18fea14dce14b48a0c40d1635893"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88bffe9c6a454bf8529f9ab9091c99490578a593cc9f9822b7fc065ee0712a06"}, - {file = "ruff-0.4.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:673bddb893f21ab47a8334c8e0ea7fd6598ecc8e698da75bcd12a7b9d0a3206e"}, - {file = "ruff-0.4.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8c1aff58c31948cc66d0b22951aa19edb5af0a3af40c936340cd32a8b1ab7438"}, - {file = "ruff-0.4.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:784d3ec9bd6493c3b720a0b76f741e6c2d7d44f6b2be87f5eef1ae8cc1d54c84"}, - {file = "ruff-0.4.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:732dd550bfa5d85af8c3c6cbc47ba5b67c6aed8a89e2f011b908fc88f87649db"}, - {file = "ruff-0.4.9-py3-none-win32.whl", hash = "sha256:8064590fd1a50dcf4909c268b0e7c2498253273309ad3d97e4a752bb9df4f521"}, - {file = "ruff-0.4.9-py3-none-win_amd64.whl", hash = "sha256:e0a22c4157e53d006530c902107c7f550b9233e9706313ab57b892d7197d8e52"}, - {file = "ruff-0.4.9-py3-none-win_arm64.whl", hash = "sha256:5d5460f789ccf4efd43f265a58538a2c24dbce15dbf560676e430375f20a8198"}, - {file = "ruff-0.4.9.tar.gz", hash = "sha256:f1cb0828ac9533ba0135d148d214e284711ede33640465e706772645483427e3"}, -] - [[package]] name = "s3transfer" version = "0.10.2" @@ -2997,17 +2872,6 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] -[[package]] -name = "tzdata" -version = "2024.1" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -files = [ - {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, - {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, -] - [[package]] name = "urllib3" version = "2.2.3" @@ -3267,4 +3131,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "4a4e79967b4a36bdce74f42fcc3c8d47193190d4451a97e6c5017c6342f31f00" +content-hash = "0be2467afb5dafa076cb1815bd601f01355fa9eaee71a49b429ff8a5c3b63719" diff --git a/pyproject.toml b/pyproject.toml index 9d0852b..52fe03f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,8 +25,6 @@ python = "^3.10" aiohttp = "3.10.*" humanize = "^4.9.0" inflection = "0.5.*" -loguru = "0.7.2" -pendulum = "^3.0.0" pint = "0.24.*" pycognito = "2024.5.1" pydantic = "2.7.3" @@ -43,7 +41,6 @@ pytest = "8.2.2" pytest-asyncio = "0.23.7" pytest-cov = "5.0.0" pytest-loguru = "0.4.0" -ruff = "0.4.9" tox = "4.15.1" twine = "5.1.1" diff --git a/src/otf_api/__init__.py b/src/otf_api/__init__.py index 779f276..5932d50 100644 --- a/src/otf_api/__init__.py +++ b/src/otf_api/__init__.py @@ -1,8 +1,3 @@ -import os -import sys - -from loguru import logger - from .api import Otf from .auth import OtfUser @@ -10,6 +5,3 @@ __all__ = ["Otf", "OtfUser"] - -logger.remove() -logger.add(sink=sys.stdout, level=os.getenv("OTF_LOG_LEVEL", "INFO")) diff --git a/src/otf_api/auth.py b/src/otf_api/auth.py index b5185af..cad7107 100644 --- a/src/otf_api/auth.py +++ b/src/otf_api/auth.py @@ -1,9 +1,9 @@ import typing +from datetime import datetime, timedelta +from logging import getLogger from typing import Any import jwt -import pendulum -from loguru import logger from pycognito import AWSSRP, Cognito, MFAChallengeException from pycognito.exceptions import TokenVerificationException from pydantic import Field @@ -15,6 +15,7 @@ from boto3.session import Session from botocore.config import Config +LOGGER = getLogger(__name__) CLIENT_ID = "65knvqta6p37efc2l3eh26pl5o" # from otlive USER_POOL_ID = "us-east-1_dYDxUeyL1" @@ -64,12 +65,12 @@ def device_key(self) -> str | None: def device_key(self, value: str | None): if not value: if self._device_key: - logger.info("Clearing device key") + LOGGER.debug("Clearing device key") self._device_key = value return redacted_value = value[:4] + "*" * (len(value) - 8) + value[-4:] - logger.info(f"Setting device key: {redacted_value}") + LOGGER.debug(f"Setting device key: {redacted_value}") self._device_key = value def _set_tokens(self, tokens: dict[str, Any]): @@ -116,7 +117,7 @@ def authenticate(self, password: str, client_metadata: dict[str, Any] | None = N try: self.renew_access_token() except TokenVerificationException: - logger.error("Failed to renew access token. Confirming device.") + LOGGER.error("Failed to renew access token. Confirming device.") self.device_key = None aws.confirm_device(tokens) @@ -129,11 +130,11 @@ def check_token(self, renew: bool = True) -> bool: """ if not self.access_token: raise AttributeError("Access Token Required to Check Token") - now = pendulum.now() + now = datetime.now() # noqa dec_access_token = jwt.decode(self.access_token, options={"verify_signature": False}) - exp = pendulum.DateTime.fromtimestamp(dec_access_token["exp"]) - if now > exp.subtract(minutes=15): + exp = datetime.fromtimestamp(dec_access_token["exp"]) # noqa + if now > exp - timedelta(minutes=15): expired = True if renew: self.renew_access_token() @@ -147,7 +148,7 @@ def renew_access_token(self): self._add_secret_hash(auth_params, "SECRET_HASH") if self.device_key: - logger.info("Using device key for refresh token") + LOGGER.debug("Using device key for refresh token") auth_params["DEVICE_KEY"] = self.device_key refresh_response = self.client.initiate_auth( @@ -311,5 +312,5 @@ def get_tokens(self) -> dict[str, str]: } @property - def device_key(self) -> str: + def device_key(self): return self.cognito.device_key From bbb6267b0bf053e567296bb2e28e0f6ffefe2b16 Mon Sep 17 00:00:00 2001 From: Jessica Smith <12jessicasmith34@gmail.com> Date: Mon, 14 Oct 2024 09:34:09 -0500 Subject: [PATCH 5/6] =?UTF-8?q?Bump=20version:=200.6.4=20=E2=86=92=200.7.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.toml | 5 +++-- pyproject.toml | 2 +- src/otf_api/__init__.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index 1e74bf0..5967231 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,10 +1,11 @@ [tool.bumpversion] -current_version = "0.6.4" +current_version = "0.7.0" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(?:-(?Pdev)(?P0|[1-9]\\d*))?" serialize = ["{major}.{minor}.{patch}-{dev_l}{dev}", "{major}.{minor}.{patch}"] -commit = false +commit = true +commit_message = "Bump version: {current_version} → {new_version}" tag = true tag_name = "v{new_version}" tag_message = "Bump version: {current_version} → {new_version}" diff --git a/pyproject.toml b/pyproject.toml index 52fe03f..a18a5cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "otf-api" -version = "0.6.4" +version = "0.7.0" description = "Python OrangeTheory Fitness API Client" authors = ["Jessica Smith "] license = "MIT" diff --git a/src/otf_api/__init__.py b/src/otf_api/__init__.py index 5932d50..834bb87 100644 --- a/src/otf_api/__init__.py +++ b/src/otf_api/__init__.py @@ -1,7 +1,7 @@ from .api import Otf from .auth import OtfUser -__version__ = "0.6.4" +__version__ = "0.7.0" __all__ = ["Otf", "OtfUser"] From 185b04bc24886e7155ce17989e85424ec0d231de Mon Sep 17 00:00:00 2001 From: Jessica Smith <12jessicasmith34@gmail.com> Date: Mon, 14 Oct 2024 09:49:41 -0500 Subject: [PATCH 6/6] chore(workflows): remove unused release workflow file to clean up repository chore(workflows): remove commented-out release job from python_package.yml to simplify workflow ci(test_and_lint.yml): add ruff to dependencies since it's removed from pyproject.toml --- .github/workflows/python_package.yml | 18 --- .github/workflows/release.yml | 163 --------------------------- .github/workflows/test_and_lint.yml | 2 +- 3 files changed, 1 insertion(+), 182 deletions(-) delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/python_package.yml b/.github/workflows/python_package.yml index 6925db0..3a5146d 100644 --- a/.github/workflows/python_package.yml +++ b/.github/workflows/python_package.yml @@ -60,21 +60,3 @@ jobs: - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - - # create-release: - # name: Create a release - # environment: - # name: release - # url: https://pypi.org/p/otf-api - # needs: [publish-pypi-dists] - # runs-on: ubuntu-latest - # permissions: - # id-token: write # IMPORTANT: this permission is mandatory for trusted publishing - - # steps: - # - name: Release - # uses: softprops/action-gh-release@v2 - # with: - # generate_release_notes: true - # files: "./dist" - # make_latest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index b32507b..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,163 +0,0 @@ -# # Publish package on release branch if it's tagged with 'v*' - -# name: build & release - -# # Controls when the action will run. -# on: -# # Triggers the workflow on push or pull request events but only for the master branch -# push: -# branch: [main, master] -# tags: -# - 'v*' - -# # Allows you to run this workflow manually from the Actions tab -# workflow_dispatch: - -# # A workflow run is made up of one or more jobs that can run sequentially or in parallel -# jobs: -# release: -# runs-on: ubuntu-latest - -# strategy: -# matrix: -# python-versions: ['3.9'] - -# # map step outputs to job outputs so they can be share among jobs -# outputs: -# package_version: ${{ steps.variables_step.outputs.package_version }} -# package_name: ${{ steps.variables_step.outputs.package_name }} -# repo_name: ${{ steps.variables_step.outputs.repo_name }} -# repo_owner: ${{ steps.variables_step.outputs.repo_owner }} - -# # Steps represent a sequence of tasks that will be executed as part of the job -# steps: -# # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it -# - uses: actions/checkout@v4 - -# - name: build change log -# id: build_changelog -# uses: mikepenz/release-changelog-builder-action@v3.2.0 -# env: -# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -# - uses: actions/setup-python@v4 -# with: -# python-version: ${{ matrix.python-versions }} - -# - name: Install dependencies -# run: | -# python -m pip install --upgrade pip -# pip install tox-gh-actions poetry - -# # declare package_version, repo_owner, repo_name, package_name so you may use it in web hooks. -# - name: Declare variables for convenient use -# id: variables_step -# run: | -# echo "repo_owner=${GITHUB_REPOSITORY%/*}" >> $GITHUB_OUTPUT -# echo "repo_name=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT -# echo "package_name=`poetry version | awk '{print $1}'`" >> $GITHUB_OUTPUT -# echo "package_version=`poetry version --short`" >> $GITHUB_OUTPUT -# shell: bash - -# - name: publish documentation -# run: | -# poetry install -E doc -# mkdocs build -# git config --global user.name Docs deploy -# git config --global user.email docs@dummy.bot.com -# mike deploy -p -f --ignore `poetry version --short` latest -# mike set-default -p `poetry version --short` - -# - name: Build wheels and source tarball -# run: | -# poetry lock -# poetry build - -# - name: Create Release -# id: create_release -# uses: actions/create-release@v1 -# env: -# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -# with: -# tag_name: ${{ github.ref_name }} -# release_name: Release ${{ github.ref_name }} -# body: ${{ steps.build_changelog.outputs.changelog }} -# draft: false -# prerelease: false - -# - name: publish to PYPI -# uses: pypa/gh-action-pypi-publish@release/v1 -# with: -# user: __token__ -# password: ${{ secrets.PYPI_API_TOKEN }} -# skip_existing: true - -# notification: -# needs: release -# if: always() -# runs-on: ubuntu-latest -# steps: -# - uses: martialonline/workflow-status@v2 -# id: check - -# - name: build success notification via email -# if: ${{ steps.check.outputs.status == 'success' }} -# uses: dawidd6/action-send-mail@v3 -# with: -# server_address: ${{ secrets.BUILD_NOTIFY_MAIL_SERVER }} -# server_port: ${{ secrets.BUILD_NOTIFY_MAIL_PORT }} -# username: ${{ secrets.BUILD_NOTIFY_MAIL_FROM }} -# password: ${{ secrets.BUILD_NOTIFY_MAIL_PASSWORD }} -# from: build-bot -# to: ${{ secrets.BUILD_NOTIFY_MAIL_RCPT }} -# subject: ${{ needs.release.outputs.package_name }}.${{ needs.release.outputs.package_version}} build successfully -# convert_markdown: true -# html_body: | -# ## Build Success -# ${{ needs.release.outputs.package_name }}.${{ needs.release.outputs.package_version }} has been published to PYPI - -# ## Change Details -# ${{ github.event.head_commit.message }} - -# For more information, please check change history at https://${{ needs.release.outputs.repo_owner }}.github.io/${{ needs.release.outputs.repo_name }}/${{ needs.release.outputs.package_version }}/history - -# ## Package Download -# The package is available at: https://pypi.org/project/${{ needs.release.outputs.package_name }}/ - -# - name: build failure notification via email -# if: ${{ steps.check.outputs.status == 'failure' }} -# uses: dawidd6/action-send-mail@v3 -# with: -# server_address: ${{ secrets.BUILD_NOTIFY_MAIL_SERVER }} -# server_port: ${{ secrets.BUILD_NOTIFY_MAIL_PORT }} -# username: ${{ secrets.BUILD_NOTIFY_MAIL_FROM }} -# password: ${{ secrets.BUILD_NOTIFY_MAIL_PASSWORD }} -# from: build-bot -# to: ${{ secrets.BUILD_NOTIFY_MAIL_RCPT }} -# subject: ${{ needs.release.outputs.package_name }}.${{ needs.release.outputs.package_version}} build failure -# convert_markdown: true -# html_body: | -# ## Change Details -# ${{ github.event.head_commit.message }} - -# ## Status: ${{ steps.check.outputs.status }} - -# ## View Log -# https://github.com/${{ needs.release.outputs.repo_owner }}/${{ needs.release.outputs.repo_name }}/actions - -# # - name: Dingtalk Robot Notify -# # if: always() -# # uses: leafney/dingtalk-action@v1.0.0 -# # env: -# # DINGTALK_ACCESS_TOKEN: ${{ secrets.DINGTALK_ACCESS_TOKEN }} -# # DINGTALK_SECRET: ${{ secrets.DINGTALK_SECRET }} -# # with: -# # msgtype: markdown -# # title: CI Notification | Success -# # text: | -# # ### ${{ needs.release.outputs.package_name }} Build Success -# # ${{ needs.release.outputs.package_version }} has been published to PYPI -# # ### Change History -# # Please check change history at https://${{ needs.release.outputs.repo_owner }}.github.io/${{ needs.release.outputs.repo_name }}/latest/history -# # ### Package Download -# # Please download the package at: https://pypi.org/project/${{ needs.release.outputs.repo_name }}/ diff --git a/.github/workflows/test_and_lint.yml b/.github/workflows/test_and_lint.yml index 6b13c46..4409656 100644 --- a/.github/workflows/test_and_lint.yml +++ b/.github/workflows/test_and_lint.yml @@ -34,7 +34,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox + pip install tox ruff - name: Run tests run: tox -e ${{ matrix.python-version }}