Source code for stravalib.model

"""
==============================
Model Functions and Classes
==============================
This module contains entity classes that represent the various Strava
datatypes, such as `Activity`, `Gear`, and more. These entities inherit
fields from superclasses in `strava_model.py`, which is generated from
the official Strava API specification. The classes in this module add
behavior such as type enrichment, unit conversion, and lazy loading
related entities from the API.
"""

from __future__ import annotations

import logging
from abc import ABC, abstractmethod
from collections.abc import Callable, Mapping, Sequence
from datetime import date, datetime, timedelta
from functools import wraps
from typing import (
    TYPE_CHECKING,
    Annotated,
    Any,
    ClassVar,
    Generic,
    Literal,
    TypeVar,
    Union,
    get_args,
)

import pytz
from dateutil import parser
from dateutil.parser import ParserError
from pydantic import (
    AliasChoices,
    BaseModel,
    Field,
    GetCoreSchemaHandler,
    GetJsonSchemaHandler,
    field_validator,
    model_validator,
)
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import core_schema
from pytz import UnknownTimeZoneError

from stravalib import exc, strava_model
from stravalib.strava_model import (
    ActivityType,
    BaseStream,
    Comment,
    ExplorerSegment,
    LatLng,
    PolylineMap,
    SportType,
    SummaryGear,
)
from stravalib.unit_helper import _Quantity

if TYPE_CHECKING:
    from stravalib.client import BatchedResultsIterator

LOGGER = logging.getLogger(__name__)

T = TypeVar("T")
U = TypeVar("U", bound="BoundClientEntity")


# Create alias for this type so docs are more readable
AllDateTypes = Union[
    datetime,
    str,
    bytes,
    int,
    float,
]


# Could naive_datetime also be run in a validator?
[docs] def naive_datetime(value: AllDateTypes | None) -> datetime | None: """Utility helper that parses a datetime value provided in JSON, string, int or other formats and returns a datetime.datetime object. Parameters ---------- value : str, int A value representing a date/time that may be presented in string, int, deserialized or other format. Returns ------- datetime.datetime A datetime object representing the datetime input value. Notes ----- Valid str, following formats work (from pydantic docs): YYYY-MM-DD[T]HH:MM[:SS[.ffffff]][Z or [±]HH[:]MM] """ if value is None: return None if isinstance(value, (int, float)): # If epoch is given, we have to assume it's UTC. When using the # regular fromtimestamp(), epoch will be interpreted as in the # _local_ timezone. dt = datetime.utcfromtimestamp(value) return dt.replace(tzinfo=None) elif isinstance(value, str): try: dt = parser.parse(value) return dt.replace(tzinfo=None) except ParserError: # Maybe timestamp was passed as string, e.g '1714305600'? try: dt = datetime.utcfromtimestamp(float(value)) return dt.replace(tzinfo=None) except ValueError: LOGGER.error(f"Invalid datetime value: {value}") raise elif isinstance(value, datetime): return value.replace(tzinfo=None) else: raise ValueError(f"Unsupported value type: {type(value)}")
[docs] def lazy_property(fn: Callable[[U], T]) -> T | None: """ Should be used to decorate the functions that return a lazily loaded entity (collection), e.g., the members of a club. Parameters ---------- fn : Callable The input function that returns the lazily loaded entity (collection). Returns ------- property A lazily loaded property. Raises ------ exc.UnboundEntity If the object is unbound (self.bound_client is None) or has a None ID (self.id is None). Notes ----- Assumes that fn (like a regular getter property) has as single argument a reference to self, and uses one of the (bound) client methods to retrieve an entity (collection) by self.id. """ @wraps(fn) def wrapper(obj: U) -> T | None: try: if obj.bound_client is None: raise exc.UnboundEntity( f"Unable to fetch objects for unbound {obj.__class__} entity." ) if obj.id is None: # type: ignore[attr-defined] LOGGER.warning( f"Cannot retrieve {obj.__class__}.{fn.__name__}, self.id is None" ) return None return fn(obj) except AttributeError as e: raise exc.UnboundEntity( f"Unable to fetch objects for unbound {obj.__class__} entity: {e}" ) return property(wrapper) # type: ignore[return-value]
# Custom validators for some edge cases: # This method checks a list of floats - ie a stream not just a single lat/lon
[docs] def check_valid_location( location: Sequence[float] | str | None, ) -> list[float] | None: """ Validate a list of location xy values. Converts a list of floating point values stored as strings to floats and returns either a list of floats or None if no location data is found. This function is used to validate LatLon object inputs Parameters ---------- location : List of floats Either a List of x,y floating point values or strings or None (The legacy serialized format is str) Returns -------- List or None Either returns a List of floating point values representing location x,y data or None if empty list is returned from the API. Raises ------ AttributeError If empty list is returned, raises AttributeError """ # Legacy serialized form is str, so in case of attempting to de-serialize # from local storage: if isinstance(location, str): try: return [float(l) for l in location.split(",")] except AttributeError: # Location for activities without GPS may be returned as empty list return None # Because this could be any Sequence type, explicitly return list elif location: return list(location) else: return None
# Custom types: BaseType = TypeVar("BaseType") CustomType = TypeVar("CustomType") class _CustomTypeAnnotation(Generic[BaseType, CustomType], ABC): _type: type[CustomType] @classmethod @abstractmethod def validate(cls, value: BaseType) -> CustomType: ... @classmethod @abstractmethod def get_core_schema(cls) -> Mapping[str, Any]: ... @classmethod def __get_pydantic_core_schema__( cls, _source_type: BaseType, _handler: GetCoreSchemaHandler, ) -> core_schema.CoreSchema: from_schema = core_schema.chain_schema( [ cls.get_core_schema(), core_schema.no_info_plain_validator_function(cls.validate), ] ) return core_schema.json_or_python_schema( json_schema=from_schema, python_schema=core_schema.union_schema( [core_schema.is_instance_schema(cls._type), from_schema] ), serialization=core_schema.plain_serializer_function_ser_schema( lambda v: v ), ) @classmethod def __get_pydantic_json_schema__( cls, _core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler, ) -> JsonSchemaValue: return handler(cls.get_core_schema()) class _CustomIntAnnotation(_CustomTypeAnnotation[int, Any], ABC): @classmethod def get_core_schema(cls) -> Mapping[str, Any]: return core_schema.int_schema() class _CustomFloatAnnotation(_CustomTypeAnnotation[float, Any], ABC): @classmethod def get_core_schema(cls) -> Mapping[str, Any]: return core_schema.float_schema() class _CustomStrAnnotation(_CustomTypeAnnotation[str, Any], ABC): @classmethod def get_core_schema(cls) -> Mapping[str, Any]: return core_schema.str_schema()
[docs] class Duration(int): """ A class that calculates the duration or time elapsed for an activity, activity segment, or activity lap. This class extends the built-in `int` class to represent durations in seconds and provides a method to convert this duration to a `datetime.timedelta` object. Methods ------- timedelta() -> timedelta Converts the duration to a `datetime.timedelta` object. Examples -------- >>> activity = client.get_activity(11416949675) >>> activity.elapsed_time 3214 >>> activity.elapsed_time.timedelta() datetime.timedelta(seconds=3214) """
[docs] def timedelta(self) -> timedelta: """ Converts the duration to a `datetime.timedelta` object. Returns ------- timedelta A `datetime.timedelta` object representing the duration in seconds. """ return timedelta(seconds=float(self))
class _DurationAnnotation(_CustomIntAnnotation): _type = Duration @classmethod def validate(cls, value: int) -> Duration: return Duration(value)
[docs] class Timezone(str): """A class to represent and manipulate time zone information. This class extends the built-in `str` class to include a method for converting the time zone string to a pytz time zone object. Methods ------- timezone() -> pytz._UTCclass | pytz.tzinfo.StaticTzInfo | pytz.tzinfo.DstTzInfo | None Converts the time zone string to a pytz time zone object. Examples -------- >>> activity = client.get_activity(11416949675) >>> activity.timezone '(GMT+01:00) Europe/Amsterdam' >>> activity.timezone.timezone() <DstTzInfo 'Europe/Amsterdam' LMT+0:18:00 STD> """
[docs] def timezone( self, ) -> ( pytz._UTCclass | pytz.tzinfo.StaticTzInfo | pytz.tzinfo.DstTzInfo | None ): """ Converts the time zone string to a pytz time zone object. This method attempts to convert the time zone string, which can either be in the format "(GMT-08:00) America/Los_Angeles" or "America/Los_Angeles", to a corresponding pytz time zone object. Returns ------- pytz._UTCclass, pytz.tzinfo.StaticTzInfo, `pytz.tzinfo.DstTzInfo`, or `None`The corresponding `pytz` time zone object if the time zone string is recognized, otherwise `None`. Raises ------ `UnknownTimeZoneError` If the time zone string is not recognized. """ if " " in self: # (GMT-08:00) America/Los_Angeles tzname = self.split(" ", 1)[1] else: # America/Los_Angeles tzname = self try: return pytz.timezone(tzname) except UnknownTimeZoneError as e: LOGGER.warning( f"Encountered unknown time zone {tzname}, returning None" ) return None
class _TimezoneAnnotation(_CustomStrAnnotation): _type = Timezone @classmethod def validate(cls, value: str) -> Timezone: return Timezone(value)
[docs] class Distance(_Quantity): """A class for representing distances as quantities (using meter as unit, which is the implicit default of Strava). These quantities can then be converted to other units such as feet or meters using the `stravalib.unit_helper` module. Examples ---------- Once you have distance in meters, you can then use the unit_helper module to convert to other units such as feet >>> from stravalib import unit_helper >>> activity = client.get_activity(11416949675) >>> activity.distance 8055.9 >>> activity.distance.quantity() <Quantity(8055.9, 'meter')> >>> unit_helper.feet(activity.distance) <Quantity(26430.1181, 'foot')> >>> unit_helper.miles(activity.distance) <Quantity(5.00570419, 'mile')> >>> unit_helper.miles(activity.distance).magnitude 5.00570419 """ unit = "meters"
class _DistanceAnnotation(_CustomFloatAnnotation): _type = Distance @classmethod def validate(cls, value: float) -> Distance: return Distance(value)
[docs] class Velocity(_Quantity): """ A class for representing velocities as quantities (using meters per second as unit, which is the implicit default of Strava). These quantities can then be converted to other units such as km/h or mph using the `stravalib.unit_helper` module. Examples -------- >>> from stravalib import unit_helper >>> activity = client.get_activity(11854593990) >>> activity.average_speed 3.53 >>> activity.average_speed.quantity() <Quantity(3.53, 'meter / second')> >>> unit_helper.miles_per_hour(activity.average_speed) <Quantity(7.89638511, 'mile / hour')> >>> unit_helper.miles_per_hour(activity.average_speed).magnitude 7.896385110952039 """ unit = "meters/second"
class _VelocityAnnotation(_CustomFloatAnnotation): _type = Velocity @classmethod def validate(cls, value: float) -> Velocity: return Velocity(value) DurationType = Annotated[Duration, _DurationAnnotation] DistanceType = Annotated[Distance, _DistanceAnnotation] VelocityType = Annotated[Velocity, _VelocityAnnotation] TimezoneType = Annotated[Timezone, _TimezoneAnnotation]
[docs] class BoundClientEntity(BaseModel): """A class that bounds the Client object to the model.""" # Using Any as type here to prevent catch-22 between circular import and # Pydantic forward-referencing issues "resolved" by PEP-8 violations. # See e.g. https://github.com/pydantic/pydantic/issues/1873 bound_client: Any | None = Field(None, exclude=True)
[docs] class RelaxedActivityType(ActivityType): """This object supports allowing an array of Literal values to be used for Activity Type. by default, the generated `strava_model` module only allows a Literal that includes types: `Ride` and `Run`. """
[docs] @model_validator(mode="before") def check_activity_type(cls, values: str) -> str: """Pydantic validator that checks whether an activity type value is valid prior to populating the model. If the available activity type is not valid, it assigns the value to be "Workout". Parameters ---------- values : str A dictionary containing an activity type key value pair. Returns ------- str A dictionary with a validated activity type value assigned. """ if values not in get_args( ActivityType.model_fields["root"].annotation ): LOGGER.warning( f'Unexpected activity type. Given={values}, replacing by "Workout"' ) values = "Workout" return values
def __eq__(self, other: Any) -> bool: if isinstance(other, RelaxedActivityType): return other.root == self.root elif isinstance(other, str): return other == self.root return False
[docs] class RelaxedSportType(SportType): """A class that extends the list of Literal values allowed for Sport Types that are defined in the generated `strava_model` module. """
[docs] @model_validator(mode="before") def check_sport_type(cls, values: str) -> str: """Pydantic validator that checks whether a sport type value is valid prior to populating the model. If the existing sport type is not valid, it assigns the value to be "Workout". Parameters ---------- values : str A dictionary containing an sport type key value pair. Returns ------- str A str containing the validated sport type. """ if values not in SportType.__annotations__["root"]: LOGGER.warning( f'Unexpected sport type. Given={values}, replacing by "Workout"' ) values = "Workout" return values
def __eq__(self, other: Any) -> bool: if isinstance(other, RelaxedSportType): return other.root == self.root elif isinstance(other, str): return other == self.root return False
[docs] class LatLon(LatLng): """Stores lat / lon values or None."""
[docs] @model_validator(mode="before") def check_valid_latlng(cls, values: Sequence[float]) -> list[float] | None: """Validate that Strava returned an actual lat/lon rather than an empty list. If list is empty, return None Parameters ---------- values: List The list of lat/lon values returned by Strava. This list will be empty if there was no GPS associated with the activity. Returns ------- List or None list of lat/lon values or None """ # Strava sometimes returns empty list in case of activities without GPS # Explicitly return a list to make mypy happy if values: return list(values) else: return None
@property def lat(self) -> float: """The latitude value of an x,y coordinate. Returns ------- float The latitude value. """ return self.root[0] @property def lon(self) -> float: """ The longitude value of an x,y coordinate. Returns ------- float The longitude value. """ return self.root[1]
[docs] class MetaClub(strava_model.MetaClub, BoundClientEntity): """ Represents an identifiable club with lazily loaded properties to obtain this club's members and activities. """ @lazy_property def members(self) -> BatchedResultsIterator[strava_model.ClubAthlete]: """ Lazy property to retrieve club members stored as Athlete objects. Returns ------- List A list of club members stored as Athlete objects. """ assert self.bound_client is not None, "Bound client is not set." return self.bound_client.get_club_members(self.id) @lazy_property def activities(self) -> BatchedResultsIterator[ClubActivity]: """ Lazy property to retrieve club activities. Returns ------- Iterator An iterator of Activity objects representing club activities. """ assert self.bound_client is not None, "Bound client is not set." return self.bound_client.get_club_activities(self.id)
[docs] class SummaryClub(MetaClub, strava_model.SummaryClub): """ Represents a single club with detailed information about the club including the club's location, activity types, etc. See Also -------- DetailedClub : A class representing a club's detailed information. Notes ----- Clubs are the only object that can have multiple valid `activity_types`. Activities only have one. Endpoint docs are found here: https://developers.strava.com/docs/reference/#api-models-SummaryClub """ # Undocumented attributes: profile: str | None = None description: str | None = None club_type: str | None = None
[docs] class DetailedClub(SummaryClub, strava_model.DetailedClub): """The detailed club object contains all of the club data available to the authenticated Athlete.""" ...
[docs] class ActivityTotals(strava_model.ActivityTotal): """Contains a set of total values for an activity including elapsed time, moving time, distance and elevation gain. """ # Attribute overrides for custom types: distance: DistanceType | None = None elevation_gain: DistanceType | None = None elapsed_time: DurationType | None = None moving_time: DurationType | None = None
[docs] class AthleteStats(strava_model.ActivityStats): """ Summary totals for rides, runs and swims, as shown in an athlete's public profile. Non-public activities are not counted for these totals. """ # Field overrides from superclass for type extensions: recent_ride_totals: ActivityTotals | None = None recent_run_totals: ActivityTotals | None = None recent_swim_totals: ActivityTotals | None = None ytd_ride_totals: ActivityTotals | None = None ytd_run_totals: ActivityTotals | None = None ytd_swim_totals: ActivityTotals | None = None all_ride_totals: ActivityTotals | None = None all_run_totals: ActivityTotals | None = None all_swim_totals: ActivityTotals | None = None biggest_ride_distance: DistanceType | None = None biggest_climb_elevation_gain: DistanceType | None = None
[docs] class MetaAthlete(strava_model.MetaAthlete, BoundClientEntity): """ Represents an identifiable athlete with lazily loaded property to obtain this athlete's summary stats. """ # Undocumented resource_state: int | None = None @lazy_property def stats(self) -> AthleteStats: """ Grabs statistics for an (authenticated) athlete. Returns ------- Associated :class:`stravalib.model.AthleteStats` Raises ------ `stravalib.exc.NotAuthenticatedAthlete` exception if authentication is missing. """ assert self.bound_client is not None, "Bound client is not set." return self.bound_client.get_athlete_stats(self.id)
[docs] class SummaryAthlete(MetaAthlete, strava_model.SummaryAthlete): """The Summary Athlete object. This is redefined here to inherit the `BoundClient` which allows API access for lazy methods.""" ...
[docs] class DetailedAthlete(SummaryAthlete, strava_model.DetailedAthlete): """Represents high level athlete information including their name, email, clubs they belong to, bikes, shoes, etc. Notes ------ Also provides access to detailed athlete stats upon request. Many attributes in this object are undocumented by Strava and could be modified at any time. """ # Field overrides from superclass for type extensions: clubs: Sequence[SummaryClub] | None = None # Undocumented attributes: athlete_type: Literal["cyclist", "runner"] | None = None friend: str | None = None follower: str | None = None approve_followers: bool | None = None badge_type_id: int | None = None mutual_friend_count: int | None = None date_preference: str | None = None email: str | None = None super_user: bool | None = None email_language: str | None = None max_heartrate: float | None = None username: str | None = None description: str | None = None instagram_username: str | None = None offer_in_app_payment: bool | None = None global_privacy: bool | None = None receive_newsletter: bool | None = None email_kom_lost: bool | None = None dateofbirth: date | None = None facebook_sharing_enabled: bool | None = None profile_original: str | None = None premium_expiration_date: int | None = None email_send_follower_notices: bool | None = None plan: str | None = None agreed_to_terms: str | None = None follower_request_count: int | None = None email_facebook_twitter_friend_joins: bool | None = None receive_kudos_emails: bool | None = None receive_follower_feed_emails: bool | None = None receive_comment_emails: bool | None = None sample_race_distance: int | None = None sample_race_time: int | None = None membership: str | None = None admin: bool | None = None """Indicates if the athlete has admin privileges.""" owner: bool | None = None subscription_permissions: Sequence[bool] | None = None
[docs] @field_validator("athlete_type", mode="before") def to_str_representation( cls, raw_type: int ) -> Literal["cyclist", "runner"] | None: """Replaces legacy 'ChoicesAttribute' class. Parameters ---------- raw_type : int The raw integer representing the athlete type. Returns ------- str The string representation of the athlete type. """ if raw_type == 0: return "cyclist" elif raw_type == 1: return "runner" else: LOGGER.warning(f"Unknown athlete type value: {raw_type}") return None
[docs] class ActivityPhotoPrimary(strava_model.Primary): """ Represents the primary photo for an activity. Attributes ---------- use_primary_photo : bool, optional Indicates whether the photo is used as the primary photo. Notes ----- https://developers.strava.com/docs/reference/#api-models-PhotosSummary_primary This object is not returned in strava_model, but it is documented in the Strava API V3 spec. The Spec documentation deviates from what is actually returned. This model represents what is actually returned. """ # Strava_model has this defined as id but unique_id is the correct attr name # it's also defined as an int but should be a string unique_id: str | None media_type: int | None = None
class PhotosSummary(strava_model.PhotosSummary): """ Represents the metadata of photos returned with the activity. Not to be confused with the fully loaded photos for an activity. Attributes ---------- primary : ActivityPhoto_primary, optional The primary photo for the activity. use_primary_photo : bool, optional Indicates whether the primary photo is used. Not currently documented by Strava. Notes ----- Undocumented attributes could be changed by Strava at any time. THe API spec specifies that this object returns count and ActivityPhoto_primary https://developers.strava.com/docs/reference/#api-models-PhotosSummary """ primary: ActivityPhotoPrimary | None = None # Undocumented by strava use_primary_photo: bool | None = None
[docs] class ActivityPhoto(BaseModel): """A full photo record attached to an activity. Notes ----- Warning: this entity is undocumented by Strava and there is no official endpoint to retrieve it """ activity_id: int | None = None activity_name: str | None = None athlete_id: int | None = None caption: str | None = None created_at: datetime | None = None created_at_local: datetime | None = None default_photo: bool | None = None location: LatLon | None = None post_id: int | None = None sizes: dict[str, Sequence[int]] | None = None source: int | None = None type: int | None = None unique_id: str | None = None uploaded_at: datetime | None = None urls: dict[str, str] | None = None # TODO: i don't see these in the actual returns but they are used below ref: str | None = None uid: str | None = None _naive_local = field_validator("created_at_local")(naive_datetime) _check_latlng = field_validator("location", mode="before")( check_valid_location ) def __repr__(self) -> str: """Return a string representation of the instance. This representation varies according to the source of the photo (native, from Instagram, or an unknown source. This representation includes the class name, photo type, and a key identifier.""" if self.source == 1: photo_type = "native" idfield = "unique_id" idval = self.unique_id elif self.source == 2: photo_type = "instagram" idfield = "uid" idval = self.uid else: photo_type = "(no type)" idfield = "id" idval = self.uid return f"<{self.__class__.__name__} {photo_type} {idfield}={idval}>"
[docs] class Lap( strava_model.Lap, BoundClientEntity, ): """An object that represents an Activity lap. This object inherits the BoundClient to support API access through lazy methods. """ # Field overrides from superclass for type extensions: activity: MetaActivity | None = None athlete: MetaAthlete | None = None distance: DistanceType | None = None total_elevation_gain: DistanceType | None = None elapsed_time: DurationType | None = None moving_time: DurationType | None = None average_speed: VelocityType | None = None max_speed: VelocityType | None = None # Undocumented attributes: average_watts: float | None = None average_heartrate: float | None = None max_heartrate: float | None = None device_watts: bool | None = None _naive_local = field_validator("start_date_local")(naive_datetime)
[docs] class Map(PolylineMap): """Pass through object. Inherits from PolyLineMap""" pass
[docs] class Split( strava_model.Split, ): """ A split -- may be metric or standard units (which has no bearing on the units used in this object, just the binning of values). """ # Attribute overrides for type extensions: distance: DistanceType | None = None elevation_difference: DistanceType | None = None elapsed_time: DurationType | None = None moving_time: DurationType | None = None average_speed: VelocityType | None = None # Undocumented attributes: average_heartrate: float | None = None average_grade_adjusted_speed: VelocityType | None = None
[docs] class SegmentExplorerResult( ExplorerSegment, BoundClientEntity, ): """Represents a segment result from the segment explorer feature. Notes ----- These do not represent full segment objects. The segment object can be fetched using the 'segment' property of this object. """ # Field overrides from superclass for type extensions: elev_difference: DistanceType | None = None distance: DistanceType | None = None start_latlng: LatLon | None = None end_latlng: LatLon | None = None # Undocumented attributes: starred: bool | None = None _check_latlng = field_validator( "start_latlng", "end_latlng", mode="before" )(check_valid_location) @lazy_property def segment(self) -> Segment: """Returns the associated (full) :class:`Segment` object. This property is lazy-loaded. Segment objects will be fetched from the bound client only when explicitly accessed for the first time. Returns ------- Segment object or None The associated Segment object, if available; otherwise, returns None. """ assert self.bound_client is not None return self.bound_client.get_segment(self.id)
[docs] class AthletePrEffort( strava_model.SummaryPRSegmentEffort, ): """An object that holds athlete PR effort attributes.""" # Override fields from superclass to match actual responses by Strava API: activity_id: int | None = Field( validation_alias=AliasChoices("pr_activity_id", "activity_id"), default=None, ) elapsed_time: int | None = Field( validation_alias=AliasChoices("pr_elapsed_time", "elapsed_time"), default=None, ) # Attribute overrides for type extensions: pr_elapsed_time: DurationType | None = None # Undocumented attributes: distance: DistanceType | None = None start_date: datetime | None = None start_date_local: datetime | None = None is_kom: bool | None = None _naive_local = field_validator("start_date_local")(naive_datetime)
[docs] class SummarySegment(strava_model.SummarySegment, BoundClientEntity): """Contains summary information for a specific segment Notes ----- Several attributes represent overrides from the superclass to support accurate typing. """ # Field overrides from superclass for type extensions: start_latlng: LatLon | None = None end_latlng: LatLon | None = None athlete_pr_effort: AthletePrEffort | None = None # Ignore because the spec is incorrectly typed - Optional[Literal["Ride", "Run"]] activity_type: RelaxedActivityType | None = None # type: ignore[assignment] athlete_segment_stats: AthleteSegmentStats | None = ( None # Actually, this is only part of a (detailed) segment response ) _latlng_check = field_validator( "start_latlng", "end_latlng", mode="before" )(check_valid_location)
[docs] class Segment(SummarySegment, strava_model.DetailedSegment): """ Represents a single Strava segment. """ # Field overrides from superclass for type extensions: map: Map | None = None distance: DistanceType | None = None elevation_high: DistanceType | None = None elevation_low: DistanceType | None = None total_elevation_gain: DistanceType | None = None # Undocumented attributes: start_latitude: float | None = None end_latitude: float | None = None start_longitude: float | None = None end_longitude: float | None = None starred: bool | None = None pr_time: timedelta | None = None starred_date: datetime | None = None elevation_profile: str | None = None
[docs] class SegmentEffortAchievement(BaseModel): """ An undocumented structure being returned for segment efforts. Notes ----- Undocumented Strava elements can change at any time without notice. """ rank: int | None = None """ Rank in segment (either overall leader board, or pr rank) """ type: str | None = None """ The type of achievement -- e.g. 'year_pr' or 'overall' """ type_id: int | None = None """ Numeric ID for type of achievement? (6 = year_pr, 2 = overall ??? other?) """ effort_count: int | None = None
[docs] class SummarySegmentEffort(strava_model.SummarySegmentEffort): """Returns summary information for a segment in an activity.""" # Override superclass fields to match actual Strava API responses activity_id: int | None = Field( validation_alias=AliasChoices("pr_activity_id", "activity_id"), default=None, ) elapsed_time: int | None = Field( validation_alias=AliasChoices("pr_elapsed_time", "elapsed_time"), default=None, ) _naive_local = field_validator("start_date_local")(naive_datetime)
[docs] class BaseEffort( SummarySegmentEffort, strava_model.DetailedSegmentEffort, ): """Base class for a best effort or segment effort.""" # Field overrides from superclass for type extensions: segment: SummarySegment | None = None activity: MetaActivity | None = None athlete: MetaAthlete | None = None elapsed_time: DurationType | None = None moving_time: DurationType | None = None distance: DistanceType | None = None
[docs] class BestEffort(BaseEffort): """Class representing a best effort (e.g. best time for 5k)""" pass
[docs] class SegmentEffort(BaseEffort): """Class representing a best effort on a particular segment.""" achievements: Sequence[SegmentEffortAchievement] | None = None
[docs] class AthleteSegmentStats( SummarySegmentEffort, ): """ A structure being returned for segment stats for current athlete. """ # Attribute overrides for type extensions: distance: DistanceType | None = None elapsed_time: DurationType | None = None # Undocumented attributes: effort_count: int | None = None pr_date: date | None = None
[docs] class MetaActivity(strava_model.MetaActivity, BoundClientEntity): """ Represents an identifiable activity with lazily loaded properties to collect this activity's comments, zones, kudos and photos. """ @lazy_property def comments(self) -> BatchedResultsIterator[Comment]: """Retrieves comments for a specific activity id.""" assert self.bound_client is not None return self.bound_client.get_activity_comments(self.id) @lazy_property def zones(self) -> list[ActivityZone]: """Retrieve a list of zones for an activity. Returns ------- list A list of :class:`stravalib.model.ActivityZone` objects. """ assert self.bound_client is not None return self.bound_client.get_activity_zones(self.id) @lazy_property def kudos(self) -> BatchedResultsIterator[SummaryAthlete]: """Retrieves the kudos provided for a specific activity.""" assert self.bound_client is not None return self.bound_client.get_activity_kudos(self.id) @lazy_property def full_photos(self) -> BatchedResultsIterator[ActivityPhoto]: """Retrieves activity photos for a specific activity by id.""" assert self.bound_client is not None return self.bound_client.get_activity_photos( self.id, only_instagram=False )
[docs] class SummaryActivity(MetaActivity, strava_model.SummaryActivity): """The Activity object that contains high level summary activity for an activity. Notes ----- In the case that the Strava spec is misaligned with the data actually returned, we override attributes as needed. """ # field overrides from superclass for type extensions: athlete: MetaAthlete | None = None average_speed: VelocityType | None = None distance: DistanceType | None = None elapsed_time: DurationType | None = None # These force validator to run on lat/lon start_latlng: LatLon | None = None end_latlng: LatLon | None = None map: Map | None = None max_speed: VelocityType | None = None moving_time: DurationType | None = None type: RelaxedActivityType | None = None sport_type: RelaxedSportType | None = None timezone: TimezoneType | None = None total_elevation_gain: DistanceType | None = None # Undocumented attributes: utc_offset: float | None = None location_city: str | None = None location_state: str | None = None location_country: str | None = None pr_count: int | None = None suffer_score: int | None = None has_heartrate: bool | None = None average_heartrate: float | None = None max_heartrate: int | None = None average_cadence: float | None = None from_accepted_tag: bool | None = None visibility: str | None = None _latlng_check = field_validator( "start_latlng", "end_latlng", mode="before" )(check_valid_location)
[docs] class DetailedActivity( SummaryActivity, strava_model.DetailedActivity, ): """ Represents an activity (ride, run, etc.). """ # Field overrides from superclass for type extensions: gear: SummaryGear | None = None best_efforts: Sequence[BestEffort] | None = None # TODO: returning empty Sequence should be DetailedSegmentEffort object # TODO: test on activity with actual segments segment_efforts: Sequence[SegmentEffort] | None = None # TODO: Returns Split object - check returns for that object splits_metric: Sequence[Split] | None = None splits_standard: Sequence[Split] | None = None photos: PhotosSummary | None = None laps: Sequence[Lap] | None = None # Added for backward compatibility # TODO maybe deprecate? TYPES: ClassVar[tuple[Any, ...]] = get_args( ActivityType.model_fields["root"].annotation ) SPORT_TYPES: ClassVar[tuple[Any, ...]] = get_args( SportType.model_fields["root"].annotation ) # Undocumented attributes: guid: str | None = None start_latitude: float | None = None start_longitude: float | None = None average_temp: int | None = None instagram_primary_photo: str | None = None partner_logo_url: str | None = None partner_brand_tag: str | None = None segment_leaderboard_opt_out: bool | None = None perceived_exertion: int | None = None prefer_perceived_exertion: bool | None = None private_note: str | None = None _naive_local = field_validator("start_date_local")(naive_datetime)
[docs] class ClubActivity(strava_model.ClubActivity): """Represents an activity returned from a club. Notes ----- The Strava API specification suggests that this should return a `MetaAthlete` Object for the athlete associated with this activity. However, the object spec is missing what is actually returned, i.e., resource_state, first name, and last initial. This object matches the actual return data, not the spec. """ # Intentional class override as spec returns metaAthlete object # (which only contains id) athlete: strava_model.ClubAthlete | None = None # type: ignore[assignment]
[docs] class TimedZoneDistribution(strava_model.TimedZoneRange): """ A single distribution bucket object, used for activity zones. Notes ----- Min/max value types are overridden. The Strava API incorrectly types zones as being `int`. However it can return `int` or `float` (floats are returned for pace, ints for heartrate and power). """ # Type overrides to support pace values returned as ints min: float | None = None # type: ignore[assignment] max: float | None = None # type: ignore[assignment]
[docs] class ActivityZone( strava_model.ActivityZone, BoundClientEntity, ): """ Base class for activity zones. A collection of :class:`stravalib.strava_model.TimedZoneDistribution` objects. """ # Field overrides from superclass for type extensions: distribution_buckets: Sequence[TimedZoneDistribution] | None = None # type: ignore[assignment] # strava_model only contains heartrate and power (ints), but also returns pace (float) type: Literal["heartrate", "power", "pace"] | None = None # type: ignore[assignment]
[docs] class Stream(BaseStream): """Stream of readings from the activity, effort or segment.""" type: str | None = None # Not using the typed subclasses from the generated model # for backward compatibility: data: Sequence[Any] | None = None
[docs] class Route( strava_model.Route, BoundClientEntity, ): """Represents a spatial route created by an athlete.""" # Superclass field overrides for using extended types distance: DistanceType | None = None elevation_gain: DistanceType | None = None athlete: SummaryAthlete | None = None map: Map | None = None segments: Sequence[SummarySegment] | None = None
[docs] class Subscription(BaseModel): """ Represents a Webhook Event Subscription. """ OBJECT_TYPE_ACTIVITY: ClassVar[str] = "activity" ASPECT_TYPE_CREATE: ClassVar[str] = "create" VERIFY_TOKEN_DEFAULT: ClassVar[str] = "STRAVA" id: int | None = None application_id: int | None = None object_type: str | None = None aspect_type: str | None = None callback_url: str | None = None created_at: datetime | None = None updated_at: datetime | None = None
[docs] class SubscriptionCallback(BaseModel): """ Represents a Webhook Event Subscription Callback. """ hub_mode: str | None = Field(None, alias="hub.mode") hub_verify_token: str | None = Field(None, alias="hub.verify_token") hub_challenge: str | None = Field(None, alias="hub.challenge")
[docs] def validate_token( self, verify_token: str = Subscription.VERIFY_TOKEN_DEFAULT ) -> None: """ Validate the subscription's hub_verify_token against the provided token. Parameters ---------- verify_token : str The token to verify subscription If not provided, it uses the default `VERIFY_TOKEN_DEFAULT`. Returns ------- None None if an assertion error is not raised Raises ------ AssertionError If the hub_verify_token does not match the provided verify_token. """ assert self.hub_verify_token == verify_token
[docs] class SubscriptionUpdate(BaseModel): """ Represents a Webhook Event Subscription Update. """ subscription_id: int | None = None owner_id: int | None = None object_id: int | None = None object_type: str | None = None aspect_type: str | None = None event_time: datetime | None = None updates: dict[str, Any] | None = None