"""
==============================
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 ActivityPhotoPrimary(Primary):
"""Represents the primary photo for an activity.
Notes
-----
Attributes for activity photos are undocumented
"""
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()