Source code for stravalib.model

"""
==============================
Model Functions and Classes
==============================
This module contains entity classes for representing 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 datetime import date, datetime
from functools import wraps
from typing import (
    TYPE_CHECKING,
    Any,
    Callable,
    ClassVar,
    Literal,
    Optional,
    TypeVar,
    Union,
    get_args,
)

from pydantic import BaseModel, Field, root_validator, validator
from pydantic.datetime_parse import parse_datetime
from typing_extensions import Self

from stravalib import exc, strava_model
from stravalib import unithelper as uh
from stravalib.field_conversions import (
    enum_value,
    enum_values,
    time_interval,
    timezone,
)
from stravalib.strava_model import (
    ActivityStats,
    ActivityTotal,
    ActivityType,
    ActivityZone,
    BaseStream,
    Comment,
    DetailedActivity,
    DetailedAthlete,
    DetailedClub,
    DetailedGear,
    DetailedSegment,
    DetailedSegmentEffort,
    ExplorerSegment,
    Lap,
    LatLng,
    PhotosSummary,
    PolylineMap,
    Primary,
    SportType,
    SummaryClub,
    SummaryGear,
    SummaryPRSegmentEffort,
    SummarySegmentEffort,
    TimedZoneRange,
)
from stravalib.strava_model import Route as RouteStrava

if TYPE_CHECKING:
    from stravalib.client import BatchedResultsIterator

LOGGER = logging.getLogger(__name__)

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


[docs]def lazy_property(fn: Callable[[U], T]) -> Optional[T]: """ 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) -> Optional[T]: 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:
[docs]def check_valid_location( location: Optional[Union[list[float], str]] ) -> Optional[list[float]]: """ 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 by # Strava return None else: return location if location else None
# Create alias for this type so docs are more readable AllDateTypes = Union[datetime, str, bytes, int, float]
[docs]def naive_datetime(value: Optional[AllDateTypes]) -> Optional[datetime]: """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. """ if value: dt = parse_datetime(value) return dt.replace(tzinfo=None) else: return None
[docs]class DeprecatedSerializableMixin(BaseModel): """ Provides backward compatibility with legacy BaseEntity Inherits from the `pydantic.BaseModel` class. """
[docs] @classmethod def deserialize(cls, attribute_value_mapping: dict[str, Any]) -> Self: """ Creates and returns a new object based on serialized (dict) struct. Parameters ---------- attribute_value_mapping : dict A dictionary representing the serialized data. Returns ------- DeprecatedSerializableMixin A new instance of the class created from the serialized data. Deprecated ---------- 1.0.0 The `deserialize()` method is deprecated in favor of `parse_obj()` method. For more details, refer to the Pydantic documentation: https://docs.pydantic.dev/usage/models/#helper-functions """ exc.warn_method_deprecation( cls, "deserialize()", "parse_obj()", "https://docs.pydantic.dev/usage/models/#helper-functions", ) return cls.parse_obj(attribute_value_mapping)
[docs] def from_dict(self, attribute_value_mapping: dict[str, Any]) -> None: """ Deserializes v into self, resetting and/or overwriting existing fields. Parameters ---------- attribute_value_mapping : dict A dictionary that will be deserialized into the parent object. Deprecated ---------- 1.0.0 The `from_dict()` method is deprecated in favor of `parse_obj()` method. For more details, refer to the Pydantic documentation: https://docs.pydantic.dev/usage/models/#helper-functions """ exc.warn_method_deprecation( self.__class__, "from_dict()", "parse_obj()", "https://docs.pydantic.dev/usage/models/#helper-functions", ) # Ugly hack is necessary because parse_obj does not behave in-place but # returns a new object self.__init__(**self.parse_obj(attribute_value_mapping).dict()) # type: ignore[misc]
[docs] def to_dict(self) -> dict[str, Any]: """ Returns a dict representation of self Returns ------- dict A dictionary containing the data from the instance. Deprecated ---------- The `to_dict()` method is deprecated in favor of `dict()` method. For more details, refer to the Pydantic documentation: https://docs.pydantic.dev/1.10/usage/exporting_models/ """ exc.warn_method_deprecation( self.__class__, "to_dict()", "dict()", "https://docs.pydantic.dev/1.10/usage/exporting_models/", ) return self.dict()
[docs]class BackwardCompatibilityMixin: """ Mixin that intercepts attribute lookup and raises warnings or modifies return values based on what is defined in the following class attributes: * _field_conversions * _deprecated_fields (TODO) * _unsupported_fields (TODO) """ pass def __getattribute__(self, attr: str) -> Any: """A method to retrieve attributes from the object. Parameters ---------- attr : str The name of the attribute to retrieve. Returns ------- Any The value of the requested attribute. Notes ----- This method is called whenever an attribute is accessed on the object. It is used to control attribute retrieval and perform additional actions. """ value = object.__getattribute__(self, attr) if attr in ["_field_conversions", "bound_client"] or attr.startswith( "_" ): return value try: if attr in self._field_conversions: return self._field_conversions[attr](value) except AttributeError: # Current model class has no field conversions defined pass try: value.bound_client = self.bound_client return value except (AttributeError, ValueError): pass try: for v in value: v.bound_client = self.bound_client return value except (AttributeError, ValueError, TypeError): # TypeError if v is not iterable pass return value
[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: Optional[Any] = Field(None, exclude=True)
[docs]class LatLon(LatLng, BackwardCompatibilityMixin, DeprecatedSerializableMixin): """ Enables backward compatibility for legacy namedtuple """ # TODO: stravalib/model.py:377: error: Incompatible return value type (got "Optional[List[Optional[float]]]", expected "Optional[List[float]]") [return-value] # this error doesn't make sense as the types are correct based on what the # error says it wants to see. so why is it throwing the error?
[docs] @root_validator def check_valid_latlng(cls, values: list[float]) -> Optional[list[float]]: """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 return values if values else 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 Club( DetailedClub, DeprecatedSerializableMixin, BackwardCompatibilityMixin, BoundClientEntity, ): """ Represents a single club with detailed information about the club including club name, id, location, activity types, etc. See Also -------- DetailedClub : A class representing a club's detailed information. DeprecatedSerializableMixin : A mixin to provide backward compatibility with legacy BaseEntity. BackwardCompatibilityMixin : A mixin to provide backward compatibility with legacy BaseDetailedEntity. BoundClientEntity : A mixin to bind the club with a Strava API client. """ # Undocumented attributes: profile: Optional[str] = None description: Optional[str] = None club_type: Optional[str] = None _field_conversions = {"activity_types": enum_values} @lazy_property def members(self) -> BatchedResultsIterator[Athlete]: """ 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[Activity]: """ 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 Gear( DetailedGear, DeprecatedSerializableMixin, BackwardCompatibilityMixin ): """ Represents a piece of gear (equipment) used in physical activities. """ _field_conversions = {"distance": uh.meters}
[docs]class Bike(Gear): """ Represents a bike as a "type" / using the structure of `stravalib.model.Gear`. """ pass
[docs]class Shoe(Gear): """ Represents a Shoes as a "type" / using the structure of `stravalib.model.Gear`. """ pass
[docs]class ActivityTotals( ActivityTotal, DeprecatedSerializableMixin, BackwardCompatibilityMixin ): """An objecting containing a set of total values for an activity including elapsed time, moving time, distance and elevation gain.""" _field_conversions = { "elapsed_time": time_interval, "moving_time": time_interval, "distance": uh.meters, "elevation_gain": uh.meters, }
[docs]class AthleteStats( ActivityStats, DeprecatedSerializableMixin, BackwardCompatibilityMixin ): """ 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: Optional[ActivityTotals] = None recent_run_totals: Optional[ActivityTotals] = None recent_swim_totals: Optional[ActivityTotals] = None ytd_ride_totals: Optional[ActivityTotals] = None ytd_run_totals: Optional[ActivityTotals] = None ytd_swim_totals: Optional[ActivityTotals] = None all_ride_totals: Optional[ActivityTotals] = None all_run_totals: Optional[ActivityTotals] = None all_swim_totals: Optional[ActivityTotals] = None _field_conversions = { "biggest_ride_distance": uh.meters, "biggest_climb_elevation_gain": uh.meters, }
[docs]class Athlete( DetailedAthlete, DeprecatedSerializableMixin, BackwardCompatibilityMixin, BoundClientEntity, ): """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. """ # Field overrides from superclass for type extensions: clubs: Optional[list[SummaryClub]] = None bikes: Optional[list[SummaryGear]] = None shoes: Optional[list[SummaryGear]] = None # Undocumented attributes: is_authenticated: Optional[bool] = None athlete_type: Optional[Literal["cyclist", "runner"]] = None friend: Optional[str] = None follower: Optional[str] = None approve_followers: Optional[bool] = None badge_type_id: Optional[int] = None mutual_friend_count: Optional[int] = None date_preference: Optional[str] = None email: Optional[str] = None super_user: Optional[bool] = None email_language: Optional[str] = None max_heartrate: Optional[float] = None username: Optional[str] = None description: Optional[str] = None instagram_username: Optional[str] = None offer_in_app_payment: Optional[bool] = None global_privacy: Optional[bool] = None receive_newsletter: Optional[bool] = None email_kom_lost: Optional[bool] = None dateofbirth: Optional[date] = None facebook_sharing_enabled: Optional[bool] = None profile_original: Optional[str] = None premium_expiration_date: Optional[int] = None email_send_follower_notices: Optional[bool] = None plan: Optional[str] = None agreed_to_terms: Optional[str] = None follower_request_count: Optional[int] = None email_facebook_twitter_friend_joins: Optional[bool] = None receive_kudos_emails: Optional[bool] = None receive_follower_feed_emails: Optional[bool] = None receive_comment_emails: Optional[bool] = None sample_race_distance: Optional[int] = None sample_race_time: Optional[int] = None membership: Optional[str] = None admin: Optional[bool] = None owner: Optional[bool] = None subscription_permissions: Optional[list[bool]] = None
[docs] @validator("athlete_type", pre=True) def to_str_representation( cls, raw_type: int ) -> Optional[Literal["cyclist", "runner"]]: """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
@lazy_property def authenticated_athlete(self) -> Athlete: """ Returns an `Athlete` object containing Strava account data for the (authenticated) athlete. Returns ------- Athlete The detailed information of the authenticated athlete. """ assert self.bound_client is not None, "Bound client is not set." return self.bound_client.get_athlete() @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. """ if not self.is_authenticated_athlete(): raise exc.NotAuthenticatedAthlete( "Statistics are only available for the authenticated athlete" ) assert self.bound_client is not None, "Bound client is not set." return self.bound_client.get_athlete_stats(self.id)
[docs] def is_authenticated_athlete(self) -> bool: """Check if the athlete is authenticated Returns ------- bool Whether the athlete is the authenticated athlete (or not). """ if self.is_authenticated is None: if self.resource_state == 3: # If the athlete is in detailed state it must be the authenticated athlete self.is_authenticated = True else: # We need to check this athlete's id matches the authenticated athlete's id authenticated_athlete = self.authenticated_athlete if authenticated_athlete is None: return False self.is_authenticated = authenticated_athlete.id == self.id return self.is_authenticated
# TODO: better description
[docs]class ActivityComment(Comment): """Field overrides the athlete attribute to be of type :class:`Athlete` rather than :class:`SummaryAthlete` """ athlete: Optional[Athlete] = None
[docs]class ActivityPhotoPrimary(Primary): """Represents the primary photo for an activity. Notes ----- Attributes for activity photos are undocumented """ use_primary_photo: Optional[bool] = None
[docs]class ActivityPhotoMeta(PhotosSummary): """The photos structure returned with the activity. Not to be confused with the full loaded photos for an activity. """ # Field overrides from superclass for type extensions: primary: Optional[ActivityPhotoPrimary] = None # Undocumented attributes: use_primary_photo: Optional[bool] = None
[docs]class ActivityPhoto(BackwardCompatibilityMixin, DeprecatedSerializableMixin): """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 """ athlete_id: Optional[int] = None activity_id: Optional[int] = None activity_name: Optional[str] = None ref: Optional[str] = None uid: Optional[str] = None unique_id: Optional[str] = None caption: Optional[str] = None type: Optional[str] = None uploaded_at: Optional[datetime] = None created_at: Optional[datetime] = None created_at_local: Optional[datetime] = None location: Optional[LatLon] = None urls: Optional[dict[str, str]] = None # TODO - is this a float return? sizes: Optional[dict[str, float]] = None post_id: Optional[int] = None default_photo: Optional[bool] = None source: Optional[int] = None _naive_local = validator("created_at_local", allow_reuse=True)( naive_datetime ) _check_latlng = validator("location", allow_reuse=True, pre=True)( check_valid_location ) def __repr__(self) -> str: 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.id return "<{clz} {type} {idfield}={id}>".format( clz=self.__class__.__name__, type=photo_type, idfield=idfield, id=idval, )
[docs]class ActivityKudos(Athlete): """ Represents kudos an athlete received on an activity. Notes ----- Activity kudos are a subset of athlete properties. """ pass
[docs]class ActivityLap( Lap, BackwardCompatibilityMixin, DeprecatedSerializableMixin, BoundClientEntity, ): # Field overrides from superclass for type extensions: activity: Optional[Activity] = None athlete: Optional[Athlete] = None # Undocumented attributes: average_watts: Optional[float] = None average_heartrate: Optional[float] = None max_heartrate: Optional[float] = None device_watts: Optional[bool] = None _field_conversions = { "elapsed_time": time_interval, "moving_time": time_interval, "distance": uh.meters, "total_elevation_gain": uh.meters, "average_speed": uh.meters_per_second, "max_speed": uh.meters_per_second, } _naive_local = validator("start_date_local", allow_reuse=True)( naive_datetime )
[docs]class Map(PolylineMap): """Pass through object. Inherits from PolyLineMap""" pass
[docs]class Split( strava_model.Split, BackwardCompatibilityMixin, DeprecatedSerializableMixin, ): """ A split -- may be metric or standard units (which has no bearing on the units used in this object, just the binning of values). """ # Undocumented attributes: average_heartrate: Optional[float] = None average_grade_adjusted_speed: Optional[float] = None _field_conversions = { "elapsed_time": time_interval, "moving_time": time_interval, "distance": uh.meters, "elevation_difference": uh.meters, "average_speed": uh.meters_per_second, "average_grade_adjusted_speed": uh.meters_per_second, }
[docs]class SegmentExplorerResult( ExplorerSegment, BackwardCompatibilityMixin, DeprecatedSerializableMixin, BoundClientEntity, ): """ Represents a segment result from the segment explorer feature. (These are not full segment objects, but the segment object can be fetched via the 'segment' property of this object.) """ # Field overrides from superclass for type extensions: start_latlng: Optional[LatLon] = None end_latlng: Optional[LatLon] = None # Undocumented attributes: starred: Optional[bool] = None _field_conversions = {"elev_difference", uh.meters, "distance", uh.meters} _check_latlng = validator( "start_latlng", "end_latlng", allow_reuse=True, pre=True )(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 AthleteSegmentStats( SummarySegmentEffort, BackwardCompatibilityMixin, DeprecatedSerializableMixin, ): """ A structure being returned for segment stats for current athlete. """ # Undocumented attributes: effort_count: Optional[int] = None pr_elapsed_time: Optional[int] = None pr_date: Optional[date] = None _field_conversions = { "elapsed_time": time_interval, "pr_elapsed_time": time_interval, "distance": uh.meters, } _naive_local = validator("start_date_local", allow_reuse=True)( naive_datetime )
[docs]class AthletePrEffort( SummaryPRSegmentEffort, BackwardCompatibilityMixin, DeprecatedSerializableMixin, ): # Undocumented attributes: distance: Optional[float] = None start_date: Optional[datetime] = None start_date_local: Optional[datetime] = None is_kom: Optional[bool] = None _field_conversions = { "pr_elapsed_time": time_interval, "distance": uh.meters, } _naive_local = validator("start_date_local", allow_reuse=True)( naive_datetime ) @property def elapsed_time(self) -> Optional[int]: # For backward compatibility return self.pr_elapsed_time
[docs]class Segment( DetailedSegment, BackwardCompatibilityMixin, DeprecatedSerializableMixin, BoundClientEntity, ): """ Represents a single Strava segment. """ # Field overrides from superclass for type extensions: start_latlng: Optional[LatLon] = None end_latlng: Optional[LatLon] = None map: Optional[Map] = None athlete_segment_stats: Optional[AthleteSegmentStats] = None athlete_pr_effort: Optional[AthletePrEffort] = None activity_type: Optional[ActivityType] = None # type: ignore[assignment] # Undocumented attributes: start_latitude: Optional[float] = None end_latitude: Optional[float] = None start_longitude: Optional[float] = None end_longitude: Optional[float] = None starred: Optional[bool] = None pr_time: Optional[int] = None starred_date: Optional[datetime] = None elevation_profile: Optional[str] = None _field_conversions = { "distance": uh.meters, "elevation_high": uh.meters, "elevation_low": uh.meters, "total_elevation_gain": uh.meters, "pr_time": time_interval, } _latlng_check = validator( "start_latlng", "end_latlng", allow_reuse=True, pre=True )(check_valid_location)
[docs]class SegmentEffortAchievement(BaseModel): """ An undocumented structure being returned for segment efforts. """ rank: Optional[int] = None """ Rank in segment (either overall leader board, or pr rank) """ type: Optional[str] = None """ The type of achievement -- e.g. 'year_pr' or 'overall' """ type_id: Optional[int] = None """ Numeric ID for type of achievement? (6 = year_pr, 2 = overall ??? other?) """ effort_count: Optional[int] = None
[docs]class BaseEffort( DetailedSegmentEffort, BackwardCompatibilityMixin, DeprecatedSerializableMixin, BoundClientEntity, ): """ Base class for a best effort or segment effort. """ # Field overrides from superclass for type extensions: segment: Optional[Segment] = None activity: Optional[Activity] = None athlete: Optional[Athlete] = None _field_conversions = { "moving_time": time_interval, "elapsed_time": time_interval, "distance": uh.meters, } _naive_local = validator("start_date_local", allow_reuse=True)( naive_datetime )
[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: Optional[list[SegmentEffortAchievement]] = None
[docs]class Activity( DetailedActivity, BackwardCompatibilityMixin, DeprecatedSerializableMixin, BoundClientEntity, ): """ Represents an activity (ride, run, etc.). """ # field overrides from superclass for type extensions: athlete: Optional[Athlete] = None start_latlng: Optional[LatLon] = None end_latlng: Optional[LatLon] = None map: Optional[Map] = None gear: Optional[Gear] = None # Ignoring types here given there are overrides best_efforts: Optional[list[BestEffort]] = None # type: ignore[assignment] segment_efforts: Optional[list[SegmentEffort]] = None # type: ignore[assignment] splits_metric: Optional[list[Split]] = None # type: ignore[assignment] splits_standard: Optional[list[Split]] = None # type: ignore[assignment] photos: Optional[ActivityPhotoMeta] = None laps: Optional[list[Lap]] = None # Added for backward compatibility # TODO maybe deprecate? TYPES: ClassVar[tuple[Any, ...]] = get_args( ActivityType.__fields__["__root__"].type_ ) # TODO this and line 1055 above changed from str to Any SPORT_TYPES: ClassVar[tuple[Any, ...]] = get_args( SportType.__fields__["__root__"].type_ ) # Undocumented attributes: guid: Optional[str] = None utc_offset: Optional[float] = None location_city: Optional[str] = None location_state: Optional[str] = None location_country: Optional[str] = None start_latitude: Optional[float] = None start_longitude: Optional[float] = None pr_count: Optional[int] = None suffer_score: Optional[int] = None has_heartrate: Optional[bool] = None average_heartrate: Optional[float] = None max_heartrate: Optional[int] = None average_cadence: Optional[float] = None average_temp: Optional[int] = None instagram_primary_photo: Optional[str] = None partner_logo_url: Optional[str] = None partner_brand_tag: Optional[str] = None from_accepted_tag: Optional[bool] = None segment_leaderboard_opt_out: Optional[bool] = None perceived_exertion: Optional[int] = None _field_conversions = { "moving_time": time_interval, "elapsed_time": time_interval, "timezone": timezone, "distance": uh.meters, "total_elevation_gain": uh.meters, "average_speed": uh.meters_per_second, "max_speed": uh.meters_per_second, "type": enum_value, "sport_type": enum_value, } _latlng_check = validator( "start_latlng", "end_latlng", allow_reuse=True, pre=True )(check_valid_location) _naive_local = validator("start_date_local", allow_reuse=True)( naive_datetime ) @lazy_property def comments(self) -> BatchedResultsIterator[ActivityComment]: assert self.bound_client is not None return self.bound_client.get_activity_comments(self.id) @lazy_property def zones(self) -> list[BaseActivityZone]: """Retrieve a list of zones for an activity. Returns ------- py:class:`list` A list of :class:`stravalib.model.BaseActivityZone` objects. """ assert self.bound_client is not None return self.bound_client.get_activity_zones(self.id) @lazy_property def kudos(self) -> BatchedResultsIterator[ActivityKudos]: assert self.bound_client is not None return self.bound_client.get_activity_kudos(self.id) @lazy_property def full_photos(self) -> BatchedResultsIterator[ActivityPhoto]: assert self.bound_client is not None return self.bound_client.get_activity_photos( self.id, only_instagram=False )
[docs]class DistributionBucket(TimedZoneRange): """ A single distribution bucket object, used for activity zones. """ _field_conversions = {"time": uh.seconds}
[docs]class BaseActivityZone( ActivityZone, BackwardCompatibilityMixin, DeprecatedSerializableMixin, BoundClientEntity, ): """ Base class for activity zones. A collection of :class:`stravalib.model.DistributionBucket` objects. """ # Field overrides from superclass for type extensions: # Using type that is currently mimicking legacy behavior... distribution_buckets: Optional[list[DistributionBucket]] = None # type: ignore[assignment] # TODO: ignoring type given legacy support will be deprecated type: Optional[Literal["heartrate", "power", "pace"]] = None # type: ignore[assignment]
[docs]class Stream( BaseStream, BackwardCompatibilityMixin, DeprecatedSerializableMixin ): """ Stream of readings from the activity, effort or segment. """ type: Optional[str] = None # Not using the typed subclasses from the generated model # for backward compatibility: data: Optional[list[Any]] = None
[docs]class Route( RouteStrava, BackwardCompatibilityMixin, DeprecatedSerializableMixin, BoundClientEntity, ): """ Represents a Route. """ # Superclass field overrides for using extended types athlete: Optional[Athlete] = None map: Optional[Map] = None segments: Optional[list[Segment]] # type: ignore[assignment] _field_conversions = {"distance": uh.meters, "elevation_gain": uh.meters}
[docs]class Subscription( BackwardCompatibilityMixin, DeprecatedSerializableMixin, BoundClientEntity ): """ Represents a Webhook Event Subscription. """ OBJECT_TYPE_ACTIVITY: ClassVar[str] = "activity" ASPECT_TYPE_CREATE: ClassVar[str] = "create" VERIFY_TOKEN_DEFAULT: ClassVar[str] = "STRAVA" id: Optional[int] = None application_id: Optional[int] = None object_type: Optional[str] = None aspect_type: Optional[str] = None callback_url: Optional[str] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None
[docs]class SubscriptionCallback( BackwardCompatibilityMixin, DeprecatedSerializableMixin ): """ Represents a Webhook Event Subscription Callback. """ hub_mode: Optional[str] = Field(None, alias="hub.mode") hub_verify_token: Optional[str] = Field(None, alias="hub.verify_token") hub_challenge: Optional[str] = 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( BackwardCompatibilityMixin, DeprecatedSerializableMixin, BoundClientEntity ): """ Represents a Webhook Event Subscription Update. """ subscription_id: Optional[int] = None owner_id: Optional[int] = None object_id: Optional[int] = None object_type: Optional[str] = None aspect_type: Optional[str] = None event_time: Optional[datetime] = None updates: Optional[dict[str, Any]] = None
SegmentEffort.update_forward_refs() ActivityLap.update_forward_refs() BestEffort.update_forward_refs()