diff --git a/distribute_setup.py b/distribute_setup.py index bce23a64..4bd0e23e 100644 --- a/distribute_setup.py +++ b/distribute_setup.py @@ -140,17 +140,17 @@ def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, except ImportError: return _do_download(version, download_base, to_dir, download_delay) try: - pkg_resources.require("distribute>="+version) + pkg_resources.require("distribute>=" + version) return except pkg_resources.VersionConflict: e = sys.exc_info()[1] if was_imported: sys.stderr.write( - "The required version of distribute (>=%s) is not available,\n" - "and can't be installed while this script is running. Please\n" - "install a more recent version first, using\n" - "'easy_install -U distribute'." - "\n\n(Currently using %r)\n" % (version, e.args[0])) + "The required version of distribute (>=%s) is not available,\n" + "and can't be installed while this script is running. Please\n" + "install a more recent version first, using\n" + "'easy_install -U distribute'." + "\n\n(Currently using %r)\n" % (version, e.args[0])) sys.exit(2) else: del pkg_resources, sys.modules['pkg_resources'] # reload ok @@ -163,6 +163,7 @@ def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, if not no_fake: _create_fake_setuptools_pkg_info(to_dir) + def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay=15): """Download distribute from a specified location and return its filename @@ -275,6 +276,7 @@ def _after_install(dist): placeholder = dist.get_command_obj('install').install_purelib _create_fake_setuptools_pkg_info(placeholder) + def _create_fake_setuptools_pkg_info(placeholder): if not placeholder or not os.path.exists(placeholder): log.warn('Could not find the install location') @@ -327,7 +329,7 @@ def _before_install(): def _under_prefix(location): if 'install' not in sys.argv: return True - args = sys.argv[sys.argv.index('install')+1:] + args = sys.argv[sys.argv.index('install') + 1:] for index, arg in enumerate(args): for option in ('--root', '--prefix'): if arg.startswith('%s=' % option): @@ -335,7 +337,7 @@ def _under_prefix(location): return location.startswith(top_dir) elif arg == option: if len(args) > index: - top_dir = args[index+1] + top_dir = args[index + 1] return location.startswith(top_dir) elif option == '--user' and USER_SITE is not None: return location.startswith(USER_SITE) @@ -382,7 +384,7 @@ def _fake_setuptools(): log.warn('Egg installation') pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') if (os.path.exists(pkg_info) and - _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): + _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): log.warn('Already patched.') return log.warn('Patching...') @@ -421,7 +423,7 @@ def _extractall(self, path=".", members=None): # Extract directories with a safe mode. directories.append(tarinfo) tarinfo = copy.copy(tarinfo) - tarinfo.mode = 448 # decimal for oct 0700 + tarinfo.mode = 448 # decimal for oct 0700 self.extract(tarinfo, path) # Reverse sort directories. diff --git a/setup.py b/setup.py index 9ebc8c41..889e2fa0 100644 --- a/setup.py +++ b/setup.py @@ -17,9 +17,9 @@ news = open(news).read() parts = re.split(r'([0-9\.]+)\s*\n\r?-+\n\r?', news) found_news = '' -for i in range(len(parts)-1): +for i in range(len(parts) - 1): if parts[i] == version: - found_news = parts[i+i] + found_news = parts[i + i] break if not found_news: warnings.warn('No news for this version found.') @@ -31,38 +31,38 @@ if found_news: title = 'Changes in %s' % version - long_description += "\n%s\n%s\n" % (title, '-'*len(title)) + long_description += '\n%s\n%s\n' % (title, '-' * len(title)) long_description += found_news setup( - name = "stravalib", - version = version, - author = "Hans Lellelid", - author_email = "hans@xmpl.org", - url = "http://github.com/hozn/stravalib", - license = "Apache", - description = "Python library for interacting with Strava v3 REST API", - long_description = long_description, - packages = find_packages(), + name='stravalib', + version=version, + author='Hans Lellelid', + author_email='hans@xmpl.org', + url='http://github.com/hozn/stravalib', + license='Apache', + description='Python library for interacting with Strava v3 REST API', + long_description=long_description, + packages=find_packages(), include_package_data=True, package_data={'stravalib': ['tests/resources/*']}, - install_requires=['python-dateutil{0}'.format('>=2.0,<3.0dev' if sys.version_info[0] == 3 else '>=1.5,<2.0dev'), # version 1.x is for python 2 and version 2.x is for python 3. + install_requires=['python-dateutil{0}'.format('>=2.0,<3.0dev' if sys.version_info[0] == 3 else '>=1.5,<2.0dev'), # version 1.x is for python 2 and version 2.x is for python 3. 'pytz', 'requests>=2.0,<3.0dev', 'beautifulsoup4>=4.0,<5.0dev', 'units'], - tests_require = ['nose>=1.0.3'], - test_suite = 'stravalib.tests', + tests_require=['nose>=1.0.3'], + test_suite='stravalib.tests', classifiers=[ - 'Development Status :: 3 - Alpha', - 'License :: OSI Approved :: Apache Software License', - 'Intended Audience :: Developers', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - "Topic :: Software Development :: Libraries :: Python Modules", + 'Development Status :: 3 - Alpha', + 'License :: OSI Approved :: Apache Software License', + 'Intended Audience :: Developers', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Topic :: Software Development :: Libraries :: Python Modules', ], use_2to3=True, - zip_safe=False # Technically it should be fine, but there are issues w/ 2to3 + zip_safe=False # Technically it should be fine, but there are issues w/ 2to3 ) diff --git a/stravalib/__init__.py b/stravalib/__init__.py index 8c4d9143..f57cfffb 100644 --- a/stravalib/__init__.py +++ b/stravalib/__init__.py @@ -1 +1 @@ -from stravalib.client import Client \ No newline at end of file +from stravalib.client import Client diff --git a/stravalib/attributes.py b/stravalib/attributes.py index 2f9f0941..9fbde507 100644 --- a/stravalib/attributes.py +++ b/stravalib/attributes.py @@ -1,8 +1,8 @@ """ Attribute types used for the model. -The types system provides a mechanism for serializing/un the data to/from JSON -structures and for capturing additional information about the model attributes. +The types system provides a mechanism for serializing/un the data to/from JSON +structures and for capturing additional information about the model attributes. """ import logging from datetime import datetime, timedelta, tzinfo, date @@ -12,25 +12,26 @@ import pytz from units.quantity import Quantity -import stravalib.model +import stravalib.model META = 1 SUMMARY = 2 DETAILED = 3 + class Attribute(object): """ Base descriptor class for a Strava model attribute. """ _type = None - + def __init__(self, type_, resource_states=None, units=None): self.log = logging.getLogger('{0.__module__}.{0.__name__}'.format(self.__class__)) self.type = type_ self.resource_states = resource_states self.data = WeakKeyDictionary() self.units = units - + def __get__(self, obj, clazz): if obj is not None: # It is being called on an object (not class) @@ -43,13 +44,13 @@ def __get__(self, obj, clazz): else: # Rather than return the wrapped value, return the actual descriptor object return self - + def __set__(self, obj, val): if val is not None: self.data[obj] = self.unmarshal(val) else: self.data[obj] = None - + @property def type(self): return self._type @@ -57,20 +58,20 @@ def type(self): @type.setter def type(self, v): self._type = v - + def marshal(self, v): """ Turn this value into format for wire (JSON). - + (By default this will just return the underlying object; subclasses can override for specific behaviors -- e.g. date formatting.) """ return v - + def unmarshal(self, v): """ Convert the value from parsed JSON structure to native python representation. - + By default this will leave the value as-is since the JSON parsing routines typically convert to native types. The exception may be date strings or other more complex types, where subclasses will override this behavior. @@ -83,12 +84,13 @@ def unmarshal(self, v): v = self.type(v) return v + class DateAttribute(Attribute): """ """ def __init__(self, resource_states=None): super(DateAttribute, self).__init__(date, resource_states=resource_states) - + def unmarshal(self, v): """ Convert a date in "2012-12-13" format to a :class:`datetime.date` object. @@ -97,14 +99,15 @@ def unmarshal(self, v): # 2012-12-13 v = datetime.strptime(v, "%Y-%m-%d").date() return v - + + class TimestampAttribute(Attribute): """ """ def __init__(self, resource_states=None, tzinfo=pytz.utc): super(TimestampAttribute, self).__init__(datetime, resource_states=resource_states) self.tzinfo = tzinfo - + def unmarshal(self, v): """ Convert a timestamp in "2012-12-13T03:43:19Z" format to a `datetime.datetime` object. @@ -114,8 +117,10 @@ def unmarshal(self, v): v = datetime.strptime(v, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=self.tzinfo) return v + LatLon = namedtuple('LatLon', ['lat', 'lon']) + class LocationAttribute(Attribute): """ """ @@ -128,7 +133,8 @@ def unmarshal(self, v): if not isinstance(v, LatLon): v = LatLon(lat=v[0], lon=v[1]) return v - + + class TimezoneAttribute(Attribute): """ """ @@ -137,7 +143,7 @@ def __init__(self, resource_states=None): def unmarshal(self, v): """ - Convert a timestamp in format "(GMT-08:00) America/Los_Angeles" to + Convert a timestamp in format "(GMT-08:00) America/Los_Angeles" to a `pytz.timestamp` object. """ if not isinstance(v, tzinfo): @@ -145,18 +151,19 @@ def unmarshal(self, v): tzname = v.split(' ')[-1] v = pytz.timezone(tzname) return v - + + class TimeIntervalAttribute(Attribute): """ Handles time durations, assumes upstream int value in seconds. """ def __init__(self, resource_states=None): super(TimeIntervalAttribute, self).__init__(int, resource_states=resource_states) - + def unmarshal(self, v): """ Convert the value from parsed JSON structure to native python representation. - + By default this will leave the value as-is since the JSON parsing routines typically convert to native types. The exception may be date strings or other more complex types, where subclasses will override this behavior. @@ -165,16 +172,17 @@ def unmarshal(self, v): v = timedelta(seconds=v) return v + class EntityAttribute(Attribute): """ Attribute for another entity. """ _lazytype = None - + def __init__(self, *args, **kwargs): super(EntityAttribute, self).__init__(*args, **kwargs) self.bind_clients = WeakKeyDictionary() - + @property def type(self): if self._lazytype: @@ -182,7 +190,7 @@ def type(self): else: clazz = self._type return clazz - + @type.setter def type(self, v): if isinstance(v, (str, bytes)): @@ -190,7 +198,7 @@ def type(self, v): self._lazytype = v else: self._type = v - + def __set__(self, obj, val): if val is not None: # If the "owning" object has a bind_client set, we want to pass that @@ -198,7 +206,7 @@ def __set__(self, obj, val): self.data[obj] = self.unmarshal(val, bind_client=getattr(obj, 'bind_client')) else: self.data[obj] = None - + def unmarshal(self, value, bind_client=None): """ Cast the specified value to the entity type. @@ -208,19 +216,20 @@ def unmarshal(self, value, bind_client=None): o = self.type() if bind_client is not None and hasattr(o.__class__, 'bind_client'): o.bind_client = bind_client - + if isinstance(value, dict): - for (k,v) in value.items(): + for (k, v) in value.items(): if not hasattr(o.__class__, k): self.log.warning("Unable to set attribute {0} on entity {1!r}".format(k, o)) else: #self.log.debug("Setting attribute {0} on entity {1!r}".format(k, o)) - setattr(o, k, v) + setattr(o, k, v) value = o else: raise Exception("Unable to unmarshall object {0!r}".format(value)) return value - + + class EntityCollection(EntityAttribute): def unmarshal(self, values, bind_client=None): @@ -233,4 +242,4 @@ def unmarshal(self, values, bind_client=None): entity = super(EntityCollection, self).unmarshal(v, bind_client=bind_client) #print "-------- Got entity: %r" % (entity,) results.append(entity) - return results \ No newline at end of file + return results diff --git a/stravalib/client.py b/stravalib/client.py index bf974ff8..343fb267 100644 --- a/stravalib/client.py +++ b/stravalib/client.py @@ -24,6 +24,7 @@ except: unicode = str + class Client(object): """ Main client class for interacting with the exposed Strava v3 API methods. @@ -80,7 +81,6 @@ def access_token(self, v): """ self.protocol.access_token = v - def authorization_url(self, client_id, redirect_uri, approval_prompt='auto', scope=None, state=None): """ @@ -131,14 +131,12 @@ def exchange_code_for_token(self, client_id, client_secret, code): client_secret=client_secret, code=code) - def _utc_datetime_to_epoch(self, activity_datetime): if isinstance(activity_datetime, str): activity_datetime = dateparser.parse(activity_datetime, ignoretz=True) return calendar.timegm(activity_datetime.timetuple()) - def get_activities(self, before=None, after=None, limit=None): """ Get activities for authenticated user sorted by newest first. @@ -200,7 +198,6 @@ def get_athlete(self, athlete_id=None): return model.Athlete.deserialize(raw, bind_client=self) - def get_athlete_friends(self, athlete_id=None, limit=None): """ Gets friends for current (or specified) athlete. @@ -228,7 +225,6 @@ def get_athlete_friends(self, athlete_id=None, limit=None): result_fetcher=result_fetcher, limit=limit) - def get_athlete_followers(self, athlete_id=None, limit=None): """ Gets followers for current (or specified) athlete. @@ -256,7 +252,6 @@ def get_athlete_followers(self, athlete_id=None, limit=None): result_fetcher=result_fetcher, limit=limit) - def get_both_following(self, athlete_id, limit=None): """ Retrieve the athletes who both the authenticated user and the indicated @@ -282,7 +277,6 @@ def get_both_following(self, athlete_id, limit=None): result_fetcher=result_fetcher, limit=limit) - def get_athlete_koms(self, athlete_id, limit=None): """ Gets Q/KOMs/CRs for specified athlete. @@ -309,7 +303,6 @@ def get_athlete_koms(self, athlete_id, limit=None): result_fetcher=result_fetcher, limit=limit) - def get_athlete_clubs(self): """ List the clubs for the currently authenticated athlete. @@ -322,7 +315,6 @@ def get_athlete_clubs(self): club_structs = self.protocol.get('/athlete/clubs') return [model.Club.deserialize(raw, bind_client=self) for raw in club_structs] - def get_club(self, club_id): """ Return a specific club object. @@ -337,7 +329,6 @@ def get_club(self, club_id): raw = self.protocol.get("/clubs/{id}", id=club_id) return model.Club.deserialize(raw, bind_client=self) - def get_club_members(self, club_id, limit=None): """ Gets the member objects for specified club ID. @@ -360,7 +351,6 @@ def get_club_members(self, club_id, limit=None): return BatchedResultsIterator(entity=model.Athlete, bind_client=self, result_fetcher=result_fetcher, limit=limit) - def get_club_activities(self, club_id, limit=None): """ Gets the activities associated with specified club. @@ -383,7 +373,6 @@ def get_club_activities(self, club_id, limit=None): return BatchedResultsIterator(entity=model.Activity, bind_client=self, result_fetcher=result_fetcher, limit=limit) - def get_activity(self, activity_id): """ Gets specified activity. @@ -400,7 +389,6 @@ def get_activity(self, activity_id): raw = self.protocol.get('/activities/{id}', id=activity_id) return model.Activity.deserialize(raw, bind_client=self) - def get_friend_activities(self, limit=None): """ Gets activities for friends (of currently authenticated athlete). @@ -418,7 +406,6 @@ def get_friend_activities(self, limit=None): return BatchedResultsIterator(entity=model.Activity, bind_client=self, result_fetcher=result_fetcher, limit=limit) - def create_activity(self, name, activity_type, start_date_local, elapsed_time, description=None, distance=None): """ @@ -473,7 +460,6 @@ def create_activity(self, name, activity_type, start_date_local, elapsed_time, return model.Activity.deserialize(raw_activity, bind_client=self) - def update_activity(self, activity_id, name=None, activity_type=None, private=None, commute=None, trainer=None, gear_id=None, description=None): @@ -521,7 +507,6 @@ def update_activity(self, activity_id, name=None, activity_type=None, raw_activity = self.protocol.put('/activities/{activity_id}', **params) return model.Activity.deserialize(raw_activity, bind_client=self) - def upload_activity(self, activity_file, data_type, name=None, activity_type=None, private=None, external_id=None): """ @@ -582,7 +567,6 @@ def upload_activity(self, activity_file, data_type, name=None, return ActivityUploader(self, response=initial_response) - def get_activity_zones(self, activity_id): """ Gets zones for activity. @@ -601,7 +585,6 @@ def get_activity_zones(self, activity_id): # We use a factory to give us the correct zone based on type. return [model.BaseActivityZone.deserialize(z, bind_client=self) for z in zones] - def get_activity_comments(self, activity_id, markdown=False, limit=None): """ Gets the comments for an activity. @@ -628,7 +611,6 @@ def get_activity_comments(self, activity_id, markdown=False, limit=None): result_fetcher=result_fetcher, limit=limit) - def get_activity_kudos(self, activity_id, limit=None): """ Gets the kudos for an activity. @@ -653,7 +635,6 @@ def get_activity_kudos(self, activity_id, limit=None): result_fetcher=result_fetcher, limit=limit) - def get_activity_photos(self, activity_id): """ Gets the photos from an activity. @@ -674,7 +655,6 @@ def get_activity_photos(self, activity_id): bind_client=self, result_fetcher=result_fetcher) - def get_activity_laps(self, activity_id): """ Gets the laps from an activity. @@ -695,7 +675,6 @@ def get_activity_laps(self, activity_id): bind_client=self, result_fetcher=result_fetcher) - def get_gear(self, gear_id): """ Get details for an item of gear. @@ -710,7 +689,6 @@ def get_gear(self, gear_id): """ return model.Gear.deserialize(self.protocol.get('/gear/{id}', id=gear_id)) - def get_segment_effort(self, effort_id): """ Return a specific segment effort by ID. @@ -741,7 +719,6 @@ def get_segment(self, segment_id): return model.Segment.deserialize(self.protocol.get('/segments/{id}', id=segment_id), bind_client=self) - def get_starred_segment(self, limit=None): """ Returns a summary representation of the segments starred by the @@ -768,7 +745,6 @@ def get_starred_segment(self, limit=None): result_fetcher=result_fetcher, limit=limit) - def get_segment_leaderboard(self, segment_id, gender=None, age_group=None, weight_class=None, following=None, club_id=None, timeframe=None, top_results_limit=None, page=None): @@ -857,10 +833,9 @@ def get_segment_leaderboard(self, segment_id, gender=None, age_group=None, weigh **params), bind_client=self) - def get_segment_efforts(self, segment_id, athlete_id=None, start_date_local=None, end_date_local=None, - limit=None ): + limit=None): """ Gets all efforts on a particular segment sorted by start_date_local @@ -882,9 +857,9 @@ def get_segment_efforts(self, segment_id, athlete_id=None, http://strava.github.io/api/v3/segments/#all_efforts :param segment_id: ID of the segment. - :type segment_id: int + :type segment_id: param - :param athlete_id: (optional) ID of athlete. + :int athlete_id: (optional) ID of athlete. :type athlete_id: int :param start_date_local: (optional) efforts before this date will be excluded. @@ -927,7 +902,6 @@ def get_segment_efforts(self, segment_id, athlete_id=None, return BatchedResultsIterator(entity=model.BaseEffort, bind_client=self, result_fetcher=result_fetcher, limit=limit) - def explore_segments(self, bounds, activity_type=None, min_cat=None, max_cat=None): """ Returns an array of up to 10 segments. @@ -970,8 +944,7 @@ def explore_segments(self, bounds, activity_type=None, min_cat=None, max_cat=Non raw = self.protocol.get('/segments/explore', **params) return [model.SegmentExplorerResult.deserialize(v, bind_client=self) - for v in raw['segments']] - + for v in raw['segments']] def get_activity_streams(self, activity_id, types=None, resolution=None, series_type=None): @@ -1017,7 +990,7 @@ def get_activity_streams(self, activity_id, types=None, # stream are comma seperated list if types is not None: - types= ",".join(types) + types = ",".join(types) params = {} if resolution is not None: @@ -1026,20 +999,16 @@ def get_activity_streams(self, activity_id, types=None, if series_type is not None: params["series_type"] = series_type - result_fetcher = functools.partial( - self.protocol.get, - '/activities/{id}/streams/{types}'.format( - id=activity_id, - types=types), - **params) + result_fetcher = functools.partial(self.protocol.get, + '/activities/{id}/streams/{types}'.format(id=activity_id, types=types), + **params) streams = BatchedResultsIterator(entity=model.Stream, bind_client=self, result_fetcher=result_fetcher) # Pack streams into dictionary - return {i.type : i for i in streams} - + return {i.type: i for i in streams} def get_effort_streams(self, effort_id, types=None, resolution=None, series_type=None): @@ -1085,7 +1054,7 @@ def get_effort_streams(self, effort_id, types=None, resolution=None, # stream are comma seperated list if types is not None: - types= ",".join(types) + types = ",".join(types) params = {} if resolution is not None: @@ -1094,19 +1063,16 @@ def get_effort_streams(self, effort_id, types=None, resolution=None, if series_type is not None: params["series_type"] = series_type - result_fetcher = functools.partial( - self.protocol.get, - '/segment_efforts/{id}/streams/{types}'.format(id=effort_id, - types=types), - **params) + result_fetcher = functools.partial(self.protocol.get, + '/segment_efforts/{id}/streams/{types}'.format(id=effort_id, types=types), + **params) streams = BatchedResultsIterator(entity=model.Stream, bind_client=self, result_fetcher=result_fetcher) # Pack streams into dictionary - return {i.type : i for i in streams} - + return {i.type: i for i in streams} def get_segment_streams(self, segment_id, types=None, resolution=None, series_type=None): @@ -1151,7 +1117,7 @@ def get_segment_streams(self, segment_id, types=None, resolution=None, # stream are comma seperated list if types is not None: - types= ",".join(types) + types = ",".join(types) params = {} if resolution is not None: @@ -1160,18 +1126,16 @@ def get_segment_streams(self, segment_id, types=None, resolution=None, if series_type is not None: params["series_type"] = series_type - result_fetcher = functools.partial( - self.protocol.get, - '/segments/{id}/streams/{types}'.format(id=segment_id, - types=types), - **params) + result_fetcher = functools.partial(self.protocol.get, + '/segments/{id}/streams/{types}'.format(id=segment_id, types=types), + **params) streams = BatchedResultsIterator(entity=model.Stream, bind_client=self, result_fetcher=result_fetcher) # Pack streams into dictionary - return {i.type : i for i in streams} + return {i.type: i for i in streams} class BatchedResultsIterator(object): diff --git a/stravalib/exc.py b/stravalib/exc.py index 42839344..e532800d 100644 --- a/stravalib/exc.py +++ b/stravalib/exc.py @@ -3,41 +3,48 @@ class AuthError(RuntimeError): pass + class LoginFailed(AuthError): pass + class LoginRequired(AuthError): """ Login is required to perform specified action. """ - + + class UnboundEntity(RuntimeError): """ Exception used to indicate that a model Entity is not bound to client instances. """ - + class Fault(RuntimeError): """ Container for exceptions raised by the remote server. """ - + class RateLimitExceeded(RuntimeError): """ Exception raised when the client rate limit has been exceeded. - + http://strava.github.io/api/#access """ - + + class ActivityUploadFailed(RuntimeError): pass + class ErrorProcessingActivity(ActivityUploadFailed): pass + class CreatedActivityDeleted(ActivityUploadFailed): pass + class TimeoutExceeded(RuntimeError): - pass \ No newline at end of file + pass diff --git a/stravalib/model.py b/stravalib/model.py index d27a534d..bfcc35f4 100644 --- a/stravalib/model.py +++ b/stravalib/model.py @@ -14,6 +14,7 @@ TimeIntervalAttribute, TimezoneAttribute, DateAttribute) + class BaseEntity(object): """ A base class for all entities in the system, including objects that may not @@ -31,7 +32,7 @@ def from_dict(self, d): Only defined attributes will be set; warnings will be logged for invalid attributes. """ - for (k,v) in d.items(): + for (k, v) in d.items(): # Only set defined attributes. if hasattr(self.__class__, k): self.log.debug("Setting attribute `{0}` [{1}] on entity {2} with value {3!r}".format(k, getattr(self.__class__, k).__class__.__name__, self, v)) @@ -62,24 +63,27 @@ def __repr__(self): return '<{0} {1}>'.format(self.__class__.__name__, ' '.join(attrs)) + class ResourceStateEntity(BaseEntity): """ Mixin for entities that include the resource_state attribute. """ - resource_state = Attribute(int, (META,SUMMARY,DETAILED)) #: The detail-level for this entity. + resource_state = Attribute(int, (META, SUMMARY, DETAILED)) #: The detail-level for this entity. + class IdentifiableEntity(ResourceStateEntity): """ Mixin for entities that include an ID attribute. """ - id = Attribute(int, (META,SUMMARY,DETAILED)) #: The numeric ID for this entity. + id = Attribute(int, (META, SUMMARY, DETAILED)) #: The numeric ID for this entity. + class BoundEntity(BaseEntity): """ Base class for entities that support lazy loading additional data using a bound client. """ - bind_client = None #: The :class:`stravalib.client.Client` that can be used to load related resources. + bind_client = None #: The :class:`stravalib.client.Client` that can be used to load related resources. def __init__(self, bind_client=None, **kwargs): """ @@ -103,11 +107,11 @@ def deserialize(cls, v, bind_client=None): o.from_dict(v) return o - def assert_bind_client(self): if self.bind_client is None: raise exc.UnboundEntity("Unable to fetch objects for unbound {0} entity.".format(self.__class__)) + class LoadableEntity(BoundEntity, IdentifiableEntity): """ Base class for entities that are bound and have an ID associated with them. @@ -121,7 +125,8 @@ def expand(self): (THIS IS NOT IMPLEMENTED CURRENTLY.) """ - raise NotImplementedError() # This is a little harder now due to resource states, etc. + raise NotImplementedError() # This is a little harder now due to resource states, etc. + class Club(LoadableEntity): """ @@ -129,9 +134,9 @@ class Club(LoadableEntity): Currently summary and detail resource states have the same attributes. """ - name = Attribute(unicode, (SUMMARY,DETAILED)) #: Name of the club. - profile_medium = Attribute(unicode, (SUMMARY,DETAILED)) #: URL to a 62x62 pixel club picture - profile = Attribute(unicode, (SUMMARY,DETAILED)) #: URL to a 124x124 pixel club picture + name = Attribute(unicode, (SUMMARY, DETAILED)) #: Name of the club. + profile_medium = Attribute(unicode, (SUMMARY, DETAILED)) #: URL to a 62x62 pixel club picture + profile = Attribute(unicode, (SUMMARY, DETAILED)) #: URL to a 124x124 pixel club picture @property def members(self): @@ -149,17 +154,18 @@ def activities(self): self._activities = self.bind_client.get_club_activities(self.id) return self._activities + class Gear(IdentifiableEntity): """ Information about Gear (bike or shoes) used during activity. """ - id = Attribute(unicode, (META,SUMMARY,DETAILED)) #: Alpha-numeric gear ID. - name = Attribute(unicode, (SUMMARY,DETAILED)) #: Name athlete entered for bike (does not apply to shoes) - distance = Attribute(float, (SUMMARY,DETAILED), units=uh.meters) #: Distance for this bike/shoes. - primary = Attribute(bool, (SUMMARY,DETAILED)) #: athlete's default bike/shoes - brand_name = Attribute(unicode, (DETAILED,)) #: Brand name of bike/shoes. - model_name = Attribute(unicode, (DETAILED,)) #: Modelname of bike/shoes. - description = Attribute(unicode, (DETAILED,)) #: Description of bike/shoe item. + id = Attribute(unicode, (META, SUMMARY, DETAILED)) #: Alpha-numeric gear ID. + name = Attribute(unicode, (SUMMARY, DETAILED)) #: Name athlete entered for bike (does not apply to shoes) + distance = Attribute(float, (SUMMARY, DETAILED), units=uh.meters) #: Distance for this bike/shoes. + primary = Attribute(bool, (SUMMARY, DETAILED)) #: athlete's default bike/shoes + brand_name = Attribute(unicode, (DETAILED,)) #: Brand name of bike/shoes. + model_name = Attribute(unicode, (DETAILED,)) #: Modelname of bike/shoes. + description = Attribute(unicode, (DETAILED,)) #: Description of bike/shoe item. @classmethod def deserialize(cls, v): @@ -178,116 +184,120 @@ def deserialize(cls, v): o.from_dict(v) return o + class Bike(Gear): """ Represents an athlete's bike. """ - frame_type = Attribute(int, (DETAILED,)) #: (detailed-only) Type of bike frame. + frame_type = Attribute(int, (DETAILED,)) #: (detailed-only) Type of bike frame. + class Shoe(Gear): """ Represent's an athlete's pair of shoes. """ + class ActivityTotals(BaseEntity): """ Represent ytd/recent/all run/ride totals. """ - achievement_count = Attribute(int) #: How many achievements - count = Attribute(int) #: How many activities - distance = Attribute(float, units=uh.meters) #: Total distance travelled - elapsed_time = TimeIntervalAttribute() #: :class:`datetime.timedelta` of total elapsed time - elevation_gain = Attribute(float, units=uh.meters) #: Total elevation gain - moving_time = TimeIntervalAttribute() #: :class:`datetime.timedelta` of total moving time + achievement_count = Attribute(int) #: How many achievements + count = Attribute(int) #: How many activities + distance = Attribute(float, units=uh.meters) #: Total distance travelled + elapsed_time = TimeIntervalAttribute() #: :class:`datetime.timedelta` of total elapsed time + elevation_gain = Attribute(float, units=uh.meters) #: Total elevation gain + moving_time = TimeIntervalAttribute() #: :class:`datetime.timedelta` of total moving time + class Athlete(LoadableEntity): """ Represents a Strava athlete. """ - firstname = Attribute(unicode, (SUMMARY,DETAILED)) #: Athlete's first name. - lastname = Attribute(unicode, (SUMMARY,DETAILED)) #: Athlete's last name. - profile_medium = Attribute(unicode, (SUMMARY,DETAILED)) #: URL to a 62x62 pixel profile picture - profile = Attribute(unicode, (SUMMARY,DETAILED)) #: URL to a 124x124 pixel profile picture - city = Attribute(unicode, (SUMMARY,DETAILED)) #: Athlete's home city - state = Attribute(unicode, (SUMMARY,DETAILED)) #: Athlete's home state - country = Attribute(unicode, (SUMMARY,DETAILED)) #: Athlete's home country - sex = Attribute(unicode, (SUMMARY,DETAILED)) #: Athlete's sex ('M', 'F' or null) - friend = Attribute(unicode, (SUMMARY,DETAILED)) #: 'pending', 'accepted', 'blocked' or 'null' the authenticated athlete's following status of this athlete - follower = Attribute(unicode, (SUMMARY,DETAILED)) #: 'pending', 'accepted', 'blocked' or 'null' this athlete's following status of the authenticated athlete - premium = Attribute(bool, (SUMMARY,DETAILED)) #: Whether athlete is a premium member (true/false) + firstname = Attribute(unicode, (SUMMARY, DETAILED)) #: Athlete's first name. + lastname = Attribute(unicode, (SUMMARY, DETAILED)) #: Athlete's last name. + profile_medium = Attribute(unicode, (SUMMARY, DETAILED)) #: URL to a 62x62 pixel profile picture + profile = Attribute(unicode, (SUMMARY, DETAILED)) #: URL to a 124x124 pixel profile picture + city = Attribute(unicode, (SUMMARY, DETAILED)) #: Athlete's home city + state = Attribute(unicode, (SUMMARY, DETAILED)) #: Athlete's home state + country = Attribute(unicode, (SUMMARY, DETAILED)) #: Athlete's home country + sex = Attribute(unicode, (SUMMARY, DETAILED)) #: Athlete's sex ('M', 'F' or null) + friend = Attribute(unicode, (SUMMARY, DETAILED)) #: 'pending', 'accepted', 'blocked' or 'null' the authenticated athlete's following status of this athlete + follower = Attribute(unicode, (SUMMARY, DETAILED)) #: 'pending', 'accepted', 'blocked' or 'null' this athlete's following status of the authenticated athlete + premium = Attribute(bool, (SUMMARY, DETAILED)) #: Whether athlete is a premium member (true/false) - created_at = TimestampAttribute((SUMMARY,DETAILED)) #: :class:`datetime.datetime` when athlete record was created. - updated_at = TimestampAttribute((SUMMARY,DETAILED)) #: :class:`datetime.datetime` when athlete record was last updated. + created_at = TimestampAttribute((SUMMARY, DETAILED)) #: :class:`datetime.datetime` when athlete record was created. + updated_at = TimestampAttribute((SUMMARY, DETAILED)) #: :class:`datetime.datetime` when athlete record was last updated. - approve_followers = Attribute(bool, (SUMMARY,DETAILED)) #: Whether athlete has elected to approve followers + approve_followers = Attribute(bool, (SUMMARY, DETAILED)) #: Whether athlete has elected to approve followers - badge_type_id = Attribute(int, (SUMMARY,DETAILED)) #: (undocumented) + badge_type_id = Attribute(int, (SUMMARY, DETAILED)) #: (undocumented) - follower_count = Attribute(int, (DETAILED,)) #: (detailed-only) How many people are following this athlete - friend_count = Attribute(int, (DETAILED,)) #: (detailed-only) How many people is this athlete following - mutual_friend_count = Attribute(int, (DETAILED,)) #: (detailed-only) How many people are both following and being followed by this athlete - date_preference = Attribute(unicode, (DETAILED,)) #: (detailed-only) Athlete's preferred date representation (e.g. "%m/%d/%Y") - measurement_preference = Attribute(unicode, (DETAILED,)) #: (detailed-only) How athlete prefers to see measurements (i.e. "feet" (or what "meters"?)) - email = Attribute(unicode, (DETAILED,)) #: (detailed-only) Athlete's email address + follower_count = Attribute(int, (DETAILED,)) #: (detailed-only) How many people are following this athlete + friend_count = Attribute(int, (DETAILED,)) #: (detailed-only) How many people is this athlete following + mutual_friend_count = Attribute(int, (DETAILED,)) #: (detailed-only) How many people are both following and being followed by this athlete + date_preference = Attribute(unicode, (DETAILED,)) #: (detailed-only) Athlete's preferred date representation (e.g. "%m/%d/%Y") + measurement_preference = Attribute(unicode, (DETAILED,)) #: (detailed-only) How athlete prefers to see measurements (i.e. "feet" (or what "meters"?)) + email = Attribute(unicode, (DETAILED,)) #: (detailed-only) Athlete's email address - clubs = EntityCollection(Club, (DETAILED,)) #: (detailed-only) Which clubs athlete belongs to. (:class:`list` of :class:`stravalib.model.Club`) - bikes = EntityCollection(Bike, (DETAILED,)) #: (detailed-only) Which bikes this athlete owns. (:class:`list` of :class:`stravalib.model.Bike`) - shoes = EntityCollection(Shoe, (DETAILED,)) #: (detailed-only) Which shoes this athlete owns. (:class:`list` of :class:`stravalib.model.Shoe`) + clubs = EntityCollection(Club, (DETAILED,)) #: (detailed-only) Which clubs athlete belongs to. (:class:`list` of :class:`stravalib.model.Club`) + bikes = EntityCollection(Bike, (DETAILED,)) #: (detailed-only) Which bikes this athlete owns. (:class:`list` of :class:`stravalib.model.Bike`) + shoes = EntityCollection(Shoe, (DETAILED,)) #: (detailed-only) Which shoes this athlete owns. (:class:`list` of :class:`stravalib.model.Shoe`) # Some undocumented summary & detailed attributes - ytd_run_totals = EntityAttribute(ActivityTotals, (SUMMARY, DETAILED)) #: (undocumented) Year-to-date totals for runs. (:class:`stravalib.model.ActivityTotals`) - recent_run_totals = EntityAttribute(ActivityTotals, (SUMMARY, DETAILED)) #: (undocumented) Recent totals for runs. (:class:`stravalib.model.ActivityTotals`) - all_run_totals = EntityAttribute(ActivityTotals, (SUMMARY, DETAILED)) #: (undocumented) All-time totals for runs. (:class:`stravalib.model.ActivityTotals`) + ytd_run_totals = EntityAttribute(ActivityTotals, (SUMMARY, DETAILED)) #: (undocumented) Year-to-date totals for runs. (:class:`stravalib.model.ActivityTotals`) + recent_run_totals = EntityAttribute(ActivityTotals, (SUMMARY, DETAILED)) #: (undocumented) Recent totals for runs. (:class:`stravalib.model.ActivityTotals`) + all_run_totals = EntityAttribute(ActivityTotals, (SUMMARY, DETAILED)) #: (undocumented) All-time totals for runs. (:class:`stravalib.model.ActivityTotals`) - ytd_ride_totals = EntityAttribute(ActivityTotals, (SUMMARY, DETAILED)) #: (undocumented) Year-to-date totals for rides. (:class:`stravalib.model.ActivityTotals`) - recent_ride_totals = EntityAttribute(ActivityTotals, (SUMMARY, DETAILED)) #: (undocumented) Recent totals for rides. (:class:`stravalib.model.ActivityTotals`) - all_ride_totals = EntityAttribute(ActivityTotals, (SUMMARY, DETAILED)) #: (undocumented) All-time totals for rides. (:class:`stravalib.model.ActivityTotals`) + ytd_ride_totals = EntityAttribute(ActivityTotals, (SUMMARY, DETAILED)) #: (undocumented) Year-to-date totals for rides. (:class:`stravalib.model.ActivityTotals`) + recent_ride_totals = EntityAttribute(ActivityTotals, (SUMMARY, DETAILED)) #: (undocumented) Recent totals for rides. (:class:`stravalib.model.ActivityTotals`) + all_ride_totals = EntityAttribute(ActivityTotals, (SUMMARY, DETAILED)) #: (undocumented) All-time totals for rides. (:class:`stravalib.model.ActivityTotals`) - super_user = Attribute(bool, (SUMMARY,DETAILED)) #: (undocumented) Whether athlete is a super user (not - biggest_ride_distance = Attribute(float, (SUMMARY,DETAILED), units=uh.meters) #: (undocumented) Longest ride for athlete. - biggest_climb_elevation_gain = Attribute(float, (SUMMARY,DETAILED), units=uh.meters) #: (undocumented) Greatest single elevation gain for athlete. + super_user = Attribute(bool, (SUMMARY, DETAILED)) #: (undocumented) Whether athlete is a super user (not + biggest_ride_distance = Attribute(float, (SUMMARY, DETAILED), units=uh.meters) #: (undocumented) Longest ride for athlete. + biggest_climb_elevation_gain = Attribute(float, (SUMMARY, DETAILED), units=uh.meters) #: (undocumented) Greatest single elevation gain for athlete. - email_language = Attribute(unicode, (SUMMARY,DETAILED)) #: The user's preferred lang/locale (e.g. en-US) + email_language = Attribute(unicode, (SUMMARY, DETAILED)) #: The user's preferred lang/locale (e.g. en-US) # A bunch more undocumented detailed-resolution attribs - weight = Attribute(float, (DETAILED,), units=uh.kg) #: (undocumented, detailed-only) Athlete's configured weight. - max_heartrate = Attribute(float, (DETAILED,)) #: (undocumented, detailed-only) Athlete's configured max HR - - username = Attribute(unicode, (DETAILED,)) #: (undocumented, detailed-only) Athlete's username. - description = Attribute(unicode, (DETAILED,)) #: (undocumented, detailed-only) Athlete's personal description - instagram_username = Attribute(unicode, (DETAILED,)) #: (undocumented, detailed-only) Associated instagram username - - offer_in_app_payment = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) - global_privacy = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) Whether athlete has global privacy enabled. - receive_newsletter = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) Whether athlete has elected to receive newsletter - email_kom_lost = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) Whether athlete has elected to receive emails when KOMs are lost. - dateofbirth = DateAttribute((DETAILED,)) #: (undocumented, detailed-only) Athlete's date of birth - facebook_sharing_enabled = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) Whether Athlete has enabled sharing on Facebook - ftp = Attribute(unicode, (DETAILED,)) # (undocumented, detailed-only) - profile_original = Attribute(unicode, (DETAILED,)) #: (undocumented, detailed-only) - premium_expiration_date = Attribute(int, (DETAILED,)) #: (undocumented, detailed-only) When does premium membership expire (:class:`int` unix epoch) - email_send_follower_notices = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) - plan = Attribute(unicode, (DETAILED,)) #: (undocumented, detailed-only) - agreed_to_terms = Attribute(unicode, (DETAILED,)) #: (undocumented, detailed-only) Whether athlete has agreed to terms - follower_request_count = Attribute(int, (DETAILED,)) #: (undocumented, detailed-only) How many people have requested to follow this athlete - email_facebook_twitter_friend_joins = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) Whether athlete has elected to receve emails when a twitter or facebook friend joins Strava - receive_kudos_emails = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) Whether athlete has elected to receive emails on kudos - receive_follower_feed_emails = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) Whether athlete has elected to receive emails on new followers - receive_comment_emails = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) Whether athlete has elected to receive emails on activity comments - - sample_race_distance = Attribute(int, (DETAILED,)) # (undocumented, detailed-only) - sample_race_time = Attribute(int, (DETAILED,)) # (undocumented, detailed-only) + weight = Attribute(float, (DETAILED,), units=uh.kg) #: (undocumented, detailed-only) Athlete's configured weight. + max_heartrate = Attribute(float, (DETAILED,)) #: (undocumented, detailed-only) Athlete's configured max HR + + username = Attribute(unicode, (DETAILED)) #: (undocumented, detailed-only) Athlete's username. + description = Attribute(unicode, (DETAILED,)) #: (undocumented, detailed-only) Athlete's personal description + instagram_username = Attribute(unicode, (DETAILED,)) #: (undocumented, detailed-only) Associated instagram username + + offer_in_app_payment = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) + global_privacy = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) Whether athlete has global privacy enabled. + receive_newsletter = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) Whether athlete has elected to receive newsletter + email_kom_lost = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) Whether athlete has elected to receive emails when KOMs are lost. + dateofbirth = DateAttribute((DETAILED,)) #: (undocumented, detailed-only) Athlete's date of birth + facebook_sharing_enabled = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) Whether Athlete has enabled sharing on Facebook + ftp = Attribute(unicode, (DETAILED,)) #: (undocumented, detailed-only) + profile_original = Attribute(unicode, (DETAILED,)) #: (undocumented, detailed-only) + premium_expiration_date = Attribute(int, (DETAILED,)) #: (undocumented, detailed-only) When does premium membership expire (:class:`int` unix epoch) + email_send_follower_notices = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) + plan = Attribute(unicode, (DETAILED,)) #: (undocumented, detailed-only) + agreed_to_terms = Attribute(unicode, (DETAILED,)) #: (undocumented, detailed-only) Whether athlete has agreed to terms + follower_request_count = Attribute(int, (DETAILED,)) #: (undocumented, detailed-only) How many people have requested to follow this athlete + email_facebook_twitter_friend_joins = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) Whether athlete has elected to receve emails when a twitter or facebook friend joins Strava + receive_kudos_emails = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) Whether athlete has elected to receive emails on kudos + receive_follower_feed_emails = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) Whether athlete has elected to receive emails on new followers + receive_comment_emails = Attribute(bool, (DETAILED,)) #: (undocumented, detailed-only) Whether athlete has elected to receive emails on activity comments + + sample_race_distance = Attribute(int, (DETAILED,)) # (undocumented, detailed-only) + sample_race_time = Attribute(int, (DETAILED,)) # (undocumented, detailed-only) _friends = None _followers = None - def __repr__(self): fname = self.firstname and self.firstname.encode('utf-8') lname = self.lastname and self.lastname.encode('utf-8') return ''.format(id=self.id, fname=fname, lname=lname) + @property def friends(self): """ @@ -316,88 +326,94 @@ def followers(self): self._followers = [] return self._followers + class ActivityComment(LoadableEntity): """ Comments attached to an activity. """ - activity_id = Attribute(int, (META,SUMMARY,DETAILED)) #: ID of activity - text = Attribute(unicode, (META,SUMMARY,DETAILED)) #: Text of comment - created_at = TimestampAttribute((SUMMARY,DETAILED)) #: :class:`datetime.datetime` when was coment created - athlete = EntityAttribute(Athlete, (SUMMARY,DETAILED)) #: Associated :class:`stravalib.model.Athlete` (summary-level representation) + activity_id = Attribute(int, (META, SUMMARY, DETAILED)) #: ID of activity + text = Attribute(unicode, (META, SUMMARY, DETAILED)) #: Text of comment + created_at = TimestampAttribute((SUMMARY, DETAILED)) #: :class:`datetime.datetime` when was coment created + athlete = EntityAttribute(Athlete, (SUMMARY, DETAILED)) #: Associated :class:`stravalib.model.Athlete` (summary-level representation) + class ActivityPhoto(LoadableEntity): """ Information about photos attached to an activity. """ - activity_id = Attribute(int, (META,SUMMARY,DETAILED)) #: ID of activity - ref = Attribute(unicode, (META,SUMMARY,DETAILED)) #: ref eg. "http://instagram.com/p/eAvA-tir85/" - uid = Attribute(unicode, (META,SUMMARY,DETAILED)) #: unique id - caption = Attribute(unicode, (META,SUMMARY,DETAILED)) #: caption on photo - type = Attribute(unicode, (META,SUMMARY,DETAILED)) #: type of photo #left this off to prevent name clash - uploaded_at = TimestampAttribute((SUMMARY,DETAILED)) #: :class:`datetime.datetime` when was phto uploaded - created_at = TimestampAttribute((SUMMARY,DETAILED)) #: :class:`datetime.datetime` when was phto created - location = LocationAttribute() #: Start lat/lon of photo + activity_id = Attribute(int, (META, SUMMARY, DETAILED)) #: ID of activity + ref = Attribute(unicode, (META, SUMMARY, DETAILED)) #: ref eg. "http://instagram.com/p/eAvA-tir85/" + uid = Attribute(unicode, (META, SUMMARY, DETAILED)) #: unique id + caption = Attribute(unicode, (META, SUMMARY, DETAILED)) #: caption on photo + type = Attribute(unicode, (META, SUMMARY, DETAILED)) #: type of photo #left this off to prevent name clash + uploaded_at = TimestampAttribute((SUMMARY, DETAILED)) #: :class:`datetime.datetime` when was phto uploaded + created_at = TimestampAttribute((SUMMARY, DETAILED)) #: :class:`datetime.datetime` when was phto created + location = LocationAttribute() #: Start lat/lon of photo + class ActivityKudos(LoadableEntity): """ Activity kudos are a subset of athlete properties. """ - firstname = Attribute(unicode, (SUMMARY,DETAILED)) #: Athlete's first name. - lastname = Attribute(unicode, (SUMMARY,DETAILED)) #: Athlete's last name. - profile_medium = Attribute(unicode, (SUMMARY,DETAILED)) #: URL to a 62x62 pixel profile picture - profile = Attribute(unicode, (SUMMARY,DETAILED)) #: URL to a 124x124 pixel profile picture - city = Attribute(unicode, (SUMMARY,DETAILED)) #: Athlete's home city - state = Attribute(unicode, (SUMMARY,DETAILED)) #: Athlete's home state - country = Attribute(unicode, (SUMMARY,DETAILED)) #: Athlete's home country - sex = Attribute(unicode, (SUMMARY,DETAILED)) #: Athlete's sex ('M', 'F' or null) - friend = Attribute(unicode, (SUMMARY,DETAILED)) #: 'pending', 'accepted', 'blocked' or 'null' the authenticated athlete's following status of this athlete - follower = Attribute(unicode, (SUMMARY,DETAILED)) #: 'pending', 'accepted', 'blocked' or 'null' this athlete's following status of the authenticated athlete - premium = Attribute(bool, (SUMMARY,DETAILED)) #: Whether athlete is a premium member (true/false) + firstname = Attribute(unicode, (SUMMARY, DETAILED)) #: Athlete's first name. + lastname = Attribute(unicode, (SUMMARY, DETAILED)) #: Athlete's last name. + profile_medium = Attribute(unicode, (SUMMARY, DETAILED)) #: URL to a 62x62 pixel profile picture + profile = Attribute(unicode, (SUMMARY, DETAILED)) #: URL to a 124x124 pixel profile picture + city = Attribute(unicode, (SUMMARY, DETAILED)) #: Athlete's home city + state = Attribute(unicode, (SUMMARY, DETAILED)) #: Athlete's home state + country = Attribute(unicode, (SUMMARY, DETAILED)) #: Athlete's home country + sex = Attribute(unicode, (SUMMARY, DETAILED)) #: Athlete's sex ('M', 'F' or null) + friend = Attribute(unicode, (SUMMARY, DETAILED)) #: 'pending', 'accepted', 'blocked' or 'null' the authenticated athlete's following status of this athlete + follower = Attribute(unicode, (SUMMARY, DETAILED)) #: 'pending', 'accepted', 'blocked' or 'null' this athlete's following status of the authenticated athlete + premium = Attribute(bool, (SUMMARY, DETAILED)) #: Whether athlete is a premium member (true/false) - created_at = TimestampAttribute((SUMMARY,DETAILED)) #: :class:`datetime.datetime` when athlete record was created. - updated_at = TimestampAttribute((SUMMARY,DETAILED)) #: :class:`datetime.datetime` when athlete record was last updated. + created_at = TimestampAttribute((SUMMARY, DETAILED)) #: :class:`datetime.datetime` when athlete record was created. + updated_at = TimestampAttribute((SUMMARY, DETAILED)) #: :class:`datetime.datetime` when athlete record was last updated. + + approve_followers = Attribute(bool, (SUMMARY, DETAILED)) #: Whether athlete has elected to approve followers - approve_followers = Attribute(bool, (SUMMARY,DETAILED)) #: Whether athlete has elected to approve followers class ActivityLap(LoadableEntity): - name = Attribute(unicode, (SUMMARY,DETAILED)) #: Name of lap - activity = EntityAttribute("Activity", (SUMMARY,DETAILED)) #: The associated :class:`stravalib.model.Activity` - athlete = EntityAttribute(Athlete, (SUMMARY,DETAILED)) #: The associated :class:`stravalib.model.Athlete` - - - elapsed_time = TimeIntervalAttribute((SUMMARY, DETAILED)) #: :class:`datetime.timedelta` of elapsed time for lap - moving_time = TimeIntervalAttribute((SUMMARY, DETAILED)) #: :class:`datetime.timedelta` of moving time for lap - start_date = TimestampAttribute((SUMMARY,DETAILED)) #: :class:`datetime.datetime` when lap was started in GMT - start_date_local = TimestampAttribute((SUMMARY,DETAILED), tzinfo=None) #: :class:`datetime.datetime` when lap was started local - distance = Attribute(float, (SUMMARY,DETAILED), units=uh.meters) #: The distance for this lap. - start_index= Attribute(int, (SUMMARY,DETAILED)) #: - end_index= Attribute(int, (SUMMARY,DETAILED)) #: - total_elevation_gain = Attribute(float, (SUMMARY,DETAILED,), units=uh.meters) #: What is total elevation gain for lap - average_speed = Attribute(float, (SUMMARY,DETAILED,), units=uh.meters_per_second) #: Average speed for lap - max_speed = Attribute(float, (SUMMARY,DETAILED,), units=uh.meters_per_second) #: Max speed for lap - average_cadence = Attribute(float, (SUMMARY,DETAILED,)) #: Average cadence for lap - average_watts = Attribute(float, (SUMMARY,DETAILED,)) #: Average watts for lap - average_heartrate = Attribute(float, (SUMMARY,DETAILED,)) #: Average heartrate for lap - max_heartrate = Attribute(float, (SUMMARY,DETAILED,)) #: Max heartrate for lap - lap_index = Attribute(int, (SUMMARY,DETAILED)) #: Index of lap + name = Attribute(unicode, (SUMMARY, DETAILED)) #: Name of lap + activity = EntityAttribute("Activity", (SUMMARY, DETAILED)) #: The associated :class:`stravalib.model.Activity` + athlete = EntityAttribute(Athlete, (SUMMARY, DETAILED)) #: The associated :class:`stravalib.model.Athlete` + + elapsed_time = TimeIntervalAttribute((SUMMARY, DETAILED)) #: :class:`datetime.timedelta` of elapsed time for lap + moving_time = TimeIntervalAttribute((SUMMARY, DETAILED)) #: :class:`datetime.timedelta` of moving time for lap + start_date = TimestampAttribute((SUMMARY, DETAILED)) #: :class:`datetime.datetime` when lap was started in GMT + start_date_local = TimestampAttribute((SUMMARY, DETAILED), tzinfo=None) #: :class:`datetime.datetime` when lap was started local + distance = Attribute(float, (SUMMARY, DETAILED), units=uh.meters) #: The distance for this lap. + start_index = Attribute(int, (SUMMARY, DETAILED)) #: + end_index = Attribute(int, (SUMMARY, DETAILED)) #: + total_elevation_gain = Attribute(float, (SUMMARY, DETAILED,), units=uh.meters) #: What is total elevation gain for lap + average_speed = Attribute(float, (SUMMARY, DETAILED,), units=uh.meters_per_second) #: Average speed for lap + max_speed = Attribute(float, (SUMMARY, DETAILED,), units=uh.meters_per_second) #: Max speed for lap + average_cadence = Attribute(float, (SUMMARY, DETAILED,)) #: Average cadence for lap + average_watts = Attribute(float, (SUMMARY, DETAILED,)) #: Average watts for lap + average_heartrate = Attribute(float, (SUMMARY, DETAILED,)) #: Average heartrate for lap + max_heartrate = Attribute(float, (SUMMARY, DETAILED,)) #: Max heartrate for lap + lap_index = Attribute(int, (SUMMARY, DETAILED)) #: Index of lap + class Map(IdentifiableEntity): - id = Attribute(unicode, (SUMMARY,DETAILED)) #: Alpha-numeric identifier - polyline = Attribute(str, (SUMMARY,DETAILED)) #: Google polyline encoding - summary_polyline = Attribute(str, (SUMMARY,DETAILED)) #: Google polyline encoding for summary shape + id = Attribute(unicode, (SUMMARY, DETAILED)) #: Alpha-numeric identifier + polyline = Attribute(str, (SUMMARY, DETAILED)) #: Google polyline encoding + summary_polyline = Attribute(str, (SUMMARY, DETAILED)) #: Google polyline encoding for summary shape + class Split(BaseEntity): """ A split -- may be metric or standard units (which has no bearing on the units used in this object, just the binning of values). """ - distance = Attribute(float, units=uh.meters) #: Distance for this split - elapsed_time = TimeIntervalAttribute() #: :class:`datetime.timedelta` of elapsed time for split - elevation_difference = Attribute(float, units=uh.meters) #: Elevation difference for split - moving_time = TimeIntervalAttribute() #: :class:`datetime.timedelta` of moving time for split - average_heartrate = Attribute(float) #: Average HR for split - split = Attribute(int) #: Which split number + distance = Attribute(float, units=uh.meters) #: Distance for this split + elapsed_time = TimeIntervalAttribute() #: :class:`datetime.timedelta` of elapsed time for split + elevation_difference = Attribute(float, units=uh.meters) #: Elevation difference for split + moving_time = TimeIntervalAttribute() #: :class:`datetime.timedelta` of moving time for split + average_heartrate = Attribute(float) #: Average HR for split + split = Attribute(int) #: Which split number + class SegmentExplorerResult(LoadableEntity): """ @@ -407,16 +423,16 @@ class SegmentExplorerResult(LoadableEntity): via the 'segment' property of this object.) """ _segment = None - id = Attribute(int) #: ID of the segment. - name = Attribute(unicode) #: Name of the segment - climb_category = Attribute(int) #: Climb category for the segment (0 is higher) - climb_category_desc = Attribute(unicode) #: Climb category text - avg_grade = Attribute(float) #: Average grade for segment. - start_latlng = LocationAttribute() #: Start lat/lon for segment - end_latlng = LocationAttribute() #: End lat/lon for segment - elev_difference = Attribute(float, units=uh.meters) #: Total elevation difference over segment. - distance = Attribute(float, units=uh.meters) #: Distance of segment. - points = Attribute(str) #: Encoded Google polyline of points in segment + id = Attribute(int) #: ID of the segment. + name = Attribute(unicode) #: Name of the segment + climb_category = Attribute(int) #: Climb category for the segment (0 is higher) + climb_category_desc = Attribute(unicode) #: Climb category text + avg_grade = Attribute(float) #: Average grade for segment. + start_latlng = LocationAttribute() #: Start lat/lon for segment + end_latlng = LocationAttribute() #: End lat/lon for segment + elev_difference = Attribute(float, units=uh.meters) #: Total elevation difference over segment. + distance = Attribute(float, units=uh.meters) #: Distance of segment. + points = Attribute(str) #: Encoded Google polyline of points in segment @property def segment(self): @@ -427,41 +443,42 @@ def segment(self): self._segment = self.bind_client.get_segment(self.id) return self._segment + class Segment(LoadableEntity): """ Represents a single Strava segment. """ _leaderboard = None - name = Attribute(unicode, (SUMMARY,DETAILED)) #: Name of the segment. - activity_type = Attribute(unicode, (SUMMARY,DETAILED)) #: Activity type of segment ('Ride' or 'Run') - distance = Attribute(float, (SUMMARY,DETAILED), units=uh.meters) #: Distance of segment - average_grade = Attribute(float, (SUMMARY,DETAILED)) #: Average grade (%) for segment - maximum_grade = Attribute(float, (SUMMARY,DETAILED)) #: Maximum grade (%) for segment - elevation_high = Attribute(float, (SUMMARY,DETAILED), units=uh.meters) #: The highest point of the segment. - elevation_low = Attribute(float, (SUMMARY,DETAILED), units=uh.meters) #: The lowest point of the segment. - start_latlng = LocationAttribute((SUMMARY,DETAILED)) #: The start lat/lon (:class:`tuple`) - end_latlng = LocationAttribute((SUMMARY,DETAILED)) #: The end lat/lon (:class:`tuple`) - start_latitude = Attribute(float, (SUMMARY,DETAILED)) #: The start latitude (:class:`float`) - end_latitude = Attribute(float, (SUMMARY,DETAILED)) #: The end latitude (:class:`float`) - start_longitude = Attribute(float, (SUMMARY,DETAILED)) #: The start longitude (:class:`float`) - end_longitude = Attribute(float, (SUMMARY,DETAILED)) #: The end longitude (:class:`float`) - climb_category = Attribute(int, (SUMMARY,DETAILED)) # 0-5, lower is harder - city = Attribute(unicode, (SUMMARY,DETAILED)) #: The city this segment is in. - state = Attribute(unicode, (SUMMARY,DETAILED)) #: The state this segment is in. - country = Attribute(unicode, (SUMMARY,DETAILED)) #: The country this segment is in. - private = Attribute(bool, (SUMMARY,DETAILED)) #: Whether this is a private segment. - starred = Attribute(bool, (SUMMARY,DETAILED)) #: Whether this segment is starred by authenticated athlete + name = Attribute(unicode, (SUMMARY, DETAILED)) #: Name of the segment. + activity_type = Attribute(unicode, (SUMMARY, DETAILED)) #: Activity type of segment ('Ride' or 'Run') + distance = Attribute(float, (SUMMARY, DETAILED), units=uh.meters) #: Distance of segment + average_grade = Attribute(float, (SUMMARY, DETAILED)) #: Average grade (%) for segment + maximum_grade = Attribute(float, (SUMMARY, DETAILED)) #: Maximum grade (%) for segment + elevation_high = Attribute(float, (SUMMARY, DETAILED), units=uh.meters) #: The highest point of the segment. + elevation_low = Attribute(float, (SUMMARY, DETAILED), units=uh.meters) #: The lowest point of the segment. + start_latlng = LocationAttribute((SUMMARY, DETAILED)) #: The start lat/lon (:class:`tuple`) + end_latlng = LocationAttribute((SUMMARY, DETAILED)) #: The end lat/lon (:class:`tuple`) + start_latitude = Attribute(float, (SUMMARY, DETAILED)) #: The start latitude (:class:`float`) + end_latitude = Attribute(float, (SUMMARY, DETAILED)) #: The end latitude (:class:`float`) + start_longitude = Attribute(float, (SUMMARY, DETAILED)) #: The start longitude (:class:`float`) + end_longitude = Attribute(float, (SUMMARY, DETAILED)) #: The end longitude (:class:`float`) + climb_category = Attribute(int, (SUMMARY, DETAILED)) # 0-5, lower is harder + city = Attribute(unicode, (SUMMARY, DETAILED)) #: The city this segment is in. + state = Attribute(unicode, (SUMMARY, DETAILED)) #: The state this segment is in. + country = Attribute(unicode, (SUMMARY, DETAILED)) #: The country this segment is in. + private = Attribute(bool, (SUMMARY, DETAILED)) #: Whether this is a private segment. + starred = Attribute(bool, (SUMMARY, DETAILED)) #: Whether this segment is starred by authenticated athlete # detailed attribs - created_at = TimestampAttribute((DETAILED,)) #: :class:`datetime.datetime` when was segment created. - updated_at = TimestampAttribute((DETAILED,)) #: :class:`datetime.datetime` when was segment last updated. - total_elevation_gain = Attribute(float, (DETAILED,), units=uh.meters) #: What is total elevation gain for segment. - map = EntityAttribute(Map, (DETAILED,)) #: :class:`stravalib.model.Map` object for segment. - effort_count = Attribute(int, (DETAILED,)) #: How many times has this segment been ridden. - athlete_count = Attribute(int, (DETAILED,)) #: How many athletes have ridden this segment - hazardous = Attribute(bool, (DETAILED,)) #: Whether this segment has been flagged as hazardous - star_count = Attribute(int, (DETAILED,)) #: number of stars on this segment. + created_at = TimestampAttribute((DETAILED,)) #: :class:`datetime.datetime` when was segment created. + updated_at = TimestampAttribute((DETAILED,)) #: :class:`datetime.datetime` when was segment last updated. + total_elevation_gain = Attribute(float, (DETAILED,), units=uh.meters) #: What is total elevation gain for segment. + map = EntityAttribute(Map, (DETAILED,)) #: :class:`stravalib.model.Map` object for segment. + effort_count = Attribute(int, (DETAILED,)) #: How many times has this segment been ridden. + athlete_count = Attribute(int, (DETAILED,)) #: How many athletes have ridden this segment + hazardous = Attribute(bool, (DETAILED,)) #: Whether this segment has been flagged as hazardous + star_count = Attribute(int, (DETAILED,)) #: number of stars on this segment. @property def leaderboard(self): @@ -474,38 +491,41 @@ def leaderboard(self): self._leaderboard = self.bind_client.get_segment_leaderboard(self.id) return self._leaderboard + class BaseEffort(LoadableEntity): """ Base class for a best effort or segment effort. """ - name = Attribute(unicode, (SUMMARY,DETAILED)) #: The name of the segment - segment = EntityAttribute(Segment, (SUMMARY,DETAILED)) #: The associated :class:`stravalib.model.Segment` for this effort - activity = EntityAttribute("Activity", (SUMMARY,DETAILED)) #: The associated :class:`stravalib.model.Activity` - athlete = EntityAttribute(Athlete, (SUMMARY,DETAILED)) #: The associated :class:`stravalib.model.Athlete` - kom_rank = Attribute(int, (SUMMARY,DETAILED)) #: 1-10 segment KOM ranking for athlete at time of upload - pr_rank = Attribute(int, (SUMMARY,DETAILED)) #: 1-3 personal record ranking for athlete at time of upload - moving_time = TimeIntervalAttribute((SUMMARY,DETAILED)) #: :class:`datetime.timedelta` - elapsed_time = TimeIntervalAttribute((SUMMARY,DETAILED))#: :class:`datetime.timedelta` - start_date = TimestampAttribute((SUMMARY,DETAILED)) #: :class:`datetime.datetime` when effort was started in GMT - start_date_local = TimestampAttribute((SUMMARY,DETAILED), tzinfo=None) #: :class:`datetime.datetime` when effort was started in activity timezone for this effort - distance = Attribute(int, (SUMMARY,DETAILED), units=uh.meters) #: The distance for this effort. - average_watts = Attribute(float, (SUMMARY,DETAILED)) #: Average power during effort - average_heartrate = Attribute(float, (SUMMARY,DETAILED)) #: Average HR during effort - max_heartrate = Attribute(float, (SUMMARY,DETAILED)) #: Max HR during effort - average_cadence = Attribute(float, (SUMMARY,DETAILED)) #: Average cadence during effort - start_index = Attribute(int, (SUMMARY,DETAILED)) # the activity stream index of the start of this effort - end_index = Attribute(int, (SUMMARY,DETAILED)) # the activity stream index of the end of this effort + name = Attribute(unicode, (SUMMARY, DETAILED)) #: The name of the segment + segment = EntityAttribute(Segment, (SUMMARY, DETAILED)) #: The associated :class:`stravalib.model.Segment` for this effort + activity = EntityAttribute("Activity", (SUMMARY, DETAILED)) #: The associated :class:`stravalib.model.Activity` + athlete = EntityAttribute(Athlete, (SUMMARY, DETAILED)) #: The associated :class:`stravalib.model.Athlete` + kom_rank = Attribute(int, (SUMMARY, DETAILED)) #: 1-10 segment KOM ranking for athlete at time of upload + pr_rank = Attribute(int, (SUMMARY, DETAILED)) #: 1-3 personal record ranking for athlete at time of upload + moving_time = TimeIntervalAttribute((SUMMARY, DETAILED)) #: :class:`datetime.timedelta` + elapsed_time = TimeIntervalAttribute((SUMMARY, DETAILED)) #: :class:`datetime.timedelta` + start_date = TimestampAttribute((SUMMARY, DETAILED)) #: :class:`datetime.datetime` when effort was started in GMT + start_date_local = TimestampAttribute((SUMMARY, DETAILED), tzinfo=None) #: :class:`datetime.datetime` when effort was started in activity timezone for this effort + distance = Attribute(int, (SUMMARY, DETAILED), units=uh.meters) #: The distance for this effort. + average_watts = Attribute(float, (SUMMARY, DETAILED)) #: Average power during effort + average_heartrate = Attribute(float, (SUMMARY, DETAILED)) #: Average HR during effort + max_heartrate = Attribute(float, (SUMMARY, DETAILED)) #: Max HR during effort + average_cadence = Attribute(float, (SUMMARY, DETAILED)) #: Average cadence during effort + start_index = Attribute(int, (SUMMARY, DETAILED)) # the activity stream index of the start of this effort + end_index = Attribute(int, (SUMMARY, DETAILED)) # the activity stream index of the end of this effort + class BestEffort(BaseEffort): """ Class representing a best effort (e.g. best time for 5k) """ + class SegmentEffort(BaseEffort): """ Class representing a best effort on a particular segment. """ - hidden = Attribute(bool, (SUMMARY,DETAILED,)) # indicates a hidden/non-important effort when returned as part of an activity, value may change over time. + hidden = Attribute(bool, (SUMMARY, DETAILED,)) # indicates a hidden/non-important effort when returned as part of an activity, value may change over time. class Activity(LoadableEntity): @@ -513,35 +533,35 @@ class Activity(LoadableEntity): Represents an activity (ride, run, etc.). """ # "Constants" for types of activities - RIDE = "Ride" - RUN = "Run" - SWIM = "Swim" - WALK = "Walk" - - ALPINESKI = "AlpineSki" - BACKCOUNTRYSKI = "BackcountrySki" - CANOEING = "Canoeing" - CROSSCOUNTRYSKIING = "CrossCountrySkiing" - CROSSFIT = "Crossfit" - ELLIPTICAL = "Elliptical" - HIKE = "Hike" - ICESKATE = "IceSkate" - INLINESKATE = "InlineSkate" - KAYAKING = "Kayaking" - KITESURF = "Kitesurf" - NORDICSKI = "NordicSki" - ROCKCLIMBING = "RockClimbing" - ROLLERSKI = "RollerSki" - ROWING = "Rowing" - SNOWBOARD = "Snowboard" - SNOWSHOE = "Snowshoe" - STAIRSTEPPER = "StairStepper" - STANDUPPADDLING = "StandUpPaddling" - SURFING = "Surfing" - WEIGHTTRAINING = "WeightTraining" - WINDSURF = "Windsurf" - WORKOUT = "Workout" - YOGA = "Yoga" + RIDE = "Ride" + RUN = "Run" + SWIM = "Swim" + WALK = "Walk" + + ALPINESKI = "AlpineSki" + BACKCOUNTRYSKI = "BackcountrySki" + CANOEING = "Canoeing" + CROSSCOUNTRYSKIING = "CrossCountrySkiing" + CROSSFIT = "Crossfit" + ELLIPTICAL = "Elliptical" + HIKE = "Hike" + ICESKATE = "IceSkate" + INLINESKATE = "InlineSkate" + KAYAKING = "Kayaking" + KITESURF = "Kitesurf" + NORDICSKI = "NordicSki" + ROCKCLIMBING = "RockClimbing" + ROLLERSKI = "RollerSki" + ROWING = "Rowing" + SNOWBOARD = "Snowboard" + SNOWSHOE = "Snowshoe" + STAIRSTEPPER = "StairStepper" + STANDUPPADDLING = "StandUpPaddling" + SURFING = "Surfing" + WEIGHTTRAINING = "WeightTraining" + WINDSURF = "Windsurf" + WORKOUT = "Workout" + YOGA = "Yoga" _comments = None _zones = None @@ -550,73 +570,73 @@ class Activity(LoadableEntity): #_gear = None _laps = None - TYPES = ( RIDE, RUN, SWIM, WALK, ALPINESKI, BACKCOUNTRYSKI, CANOEING, - CROSSCOUNTRYSKIING, CROSSFIT, ELLIPTICAL, HIKE, ICESKATE, - INLINESKATE, KAYAKING, KITESURF, NORDICSKI, ROCKCLIMBING, - ROLLERSKI, ROWING, SNOWBOARD, SNOWSHOE, STAIRSTEPPER, - STANDUPPADDLING, SURFING, WEIGHTTRAINING, WINDSURF, WORKOUT, YOGA) - - guid = Attribute(unicode, (SUMMARY,DETAILED)) #: (undocumented) - - external_id = Attribute(unicode, (SUMMARY,DETAILED)) #: An external ID for the activity (relevant when specified during upload). - upload_id = Attribute(unicode, (SUMMARY,DETAILED)) #: The upload ID for an activit. - athlete = EntityAttribute(Athlete, (SUMMARY,DETAILED)) #: The associated :class:`stravalib.model.Athlete` that performed this activity. - name = Attribute(unicode, (SUMMARY,DETAILED)) #: The name of the activity. - distance = Attribute(float, (SUMMARY,DETAILED), units=uh.meters) #: The distance for the activity. - moving_time = TimeIntervalAttribute((SUMMARY,DETAILED)) #: The moving time duration for this activity. - elapsed_time = TimeIntervalAttribute((SUMMARY,DETAILED)) #: The total elapsed time (including stopped time) for this activity. - total_elevation_gain = Attribute(float, (SUMMARY,DETAILED), units=uh.meters) #: Total elevation gain for activity. - type = Attribute(unicode, (SUMMARY,DETAILED)) #: The activity type. - start_date = TimestampAttribute((SUMMARY,DETAILED)) #: :class:`datetime.datetime` when activity was started in GMT - start_date_local = TimestampAttribute((SUMMARY,DETAILED), tzinfo=None) #: :class:`datetime.datetime` when activity was started in activity timezone - timezone = TimezoneAttribute((SUMMARY,DETAILED)) #: The timezone for activity. - start_latlng = LocationAttribute((SUMMARY,DETAILED))#: The start location (lat/lon :class:`tuple`) - end_latlng = LocationAttribute((SUMMARY,DETAILED)) #: The end location (lat/lon :class:`tuple`) - - location_city = Attribute(unicode, (SUMMARY,DETAILED)) #: The activity location city - location_state = Attribute(unicode, (SUMMARY,DETAILED)) #: The activity location state - location_country = Attribute(unicode, (SUMMARY,DETAILED)) #: The activity location state - start_latitude = Attribute(float, (SUMMARY,DETAILED)) #: The start latitude - start_longitude = Attribute(float, (SUMMARY,DETAILED)) #: The start longitude - - achievement_count = Attribute(int, (SUMMARY,DETAILED)) #: How many achievements earned for the activity - kudos_count = Attribute(int, (SUMMARY,DETAILED)) #: How many kudos received for activity - comment_count = Attribute(int, (SUMMARY,DETAILED)) #: How many comments for activity. - athlete_count = Attribute(int, (SUMMARY,DETAILED)) #: How many other athlete's participated in activity - photo_count = Attribute(int, (SUMMARY,DETAILED)) #: How many photos linked to activity - map = EntityAttribute(Map, (SUMMARY,DETAILED)) #: :class:`stravavlib.model.Map` of activity. - - trainer = Attribute(bool, (SUMMARY,DETAILED)) #: Whether activity was performed on a stationary trainer. - commute = Attribute(bool, (SUMMARY,DETAILED)) #: Whether activity is a commute. - manual = Attribute(bool, (SUMMARY,DETAILED)) #: Whether activity was manually entered. - private = Attribute(bool, (SUMMARY,DETAILED)) #: Whether activity is private - flagged = Attribute(bool, (SUMMARY,DETAILED)) #: Whether activity was flagged. - - gear_id = Attribute(unicode, (SUMMARY,DETAILED)) #: Which bike/shoes were used on activity. + TYPES = (RIDE, RUN, SWIM, WALK, ALPINESKI, BACKCOUNTRYSKI, CANOEING, + CROSSCOUNTRYSKIING, CROSSFIT, ELLIPTICAL, HIKE, ICESKATE, + INLINESKATE, KAYAKING, KITESURF, NORDICSKI, ROCKCLIMBING, + ROLLERSKI, ROWING, SNOWBOARD, SNOWSHOE, STAIRSTEPPER, + STANDUPPADDLING, SURFING, WEIGHTTRAINING, WINDSURF, WORKOUT, YOGA) + + guid = Attribute(unicode, (SUMMARY, DETAILED)) #: (undocumented) + + external_id = Attribute(unicode, (SUMMARY, DETAILED)) #: An external ID for the activity (relevant when specified during upload). + upload_id = Attribute(unicode, (SUMMARY, DETAILED)) #: The upload ID for an activit. + athlete = EntityAttribute(Athlete, (SUMMARY, DETAILED)) #: The associated :class:`stravalib.model.Athlete` that performed this activity. + name = Attribute(unicode, (SUMMARY, DETAILED)) #: The name of the activity. + distance = Attribute(float, (SUMMARY, DETAILED), units=uh.meters) #: The distance for the activity. + moving_time = TimeIntervalAttribute((SUMMARY, DETAILED)) #: The moving time duration for this activity. + elapsed_time = TimeIntervalAttribute((SUMMARY, DETAILED)) #: The total elapsed time (including stopped time) for this activity. + total_elevation_gain = Attribute(float, (SUMMARY, DETAILED), units=uh.meters) #: Total elevation gain for activity. + type = Attribute(unicode, (SUMMARY, DETAILED)) #: The activity type. + start_date = TimestampAttribute((SUMMARY, DETAILED)) #: :class:`datetime.datetime` when activity was started in GMT + start_date_local = TimestampAttribute((SUMMARY, DETAILED), tzinfo=None) #: :class:`datetime.datetime` when activity was started in activity timezone + timezone = TimezoneAttribute((SUMMARY, DETAILED)) #: The timezone for activity. + start_latlng = LocationAttribute((SUMMARY, DETAILED)) #: The start location (lat/lon :class:`tuple`) + end_latlng = LocationAttribute((SUMMARY, DETAILED)) #: The end location (lat/lon :class:`tuple`) + + location_city = Attribute(unicode, (SUMMARY, DETAILED)) #: The activity location city + location_state = Attribute(unicode, (SUMMARY, DETAILED)) #: The activity location state + location_country = Attribute(unicode, (SUMMARY, DETAILED)) #: The activity location state + start_latitude = Attribute(float, (SUMMARY, DETAILED)) #: The start latitude + start_longitude = Attribute(float, (SUMMARY, DETAILED)) #: The start longitude + + achievement_count = Attribute(int, (SUMMARY, DETAILED)) #: How many achievements earned for the activity + kudos_count = Attribute(int, (SUMMARY, DETAILED)) #: How many kudos received for activity + comment_count = Attribute(int, (SUMMARY, DETAILED)) #: How many comments for activity. + athlete_count = Attribute(int, (SUMMARY, DETAILED)) #: How many other athlete's participated in activity + photo_count = Attribute(int, (SUMMARY, DETAILED)) #: How many photos linked to activity + map = EntityAttribute(Map, (SUMMARY, DETAILED)) #: :class:`stravavlib.model.Map` of activity. + + trainer = Attribute(bool, (SUMMARY, DETAILED)) #: Whether activity was performed on a stationary trainer. + commute = Attribute(bool, (SUMMARY, DETAILED)) #: Whether activity is a commute. + manual = Attribute(bool, (SUMMARY, DETAILED)) #: Whether activity was manually entered. + private = Attribute(bool, (SUMMARY, DETAILED)) #: Whether activity is private + flagged = Attribute(bool, (SUMMARY, DETAILED)) #: Whether activity was flagged. + + gear_id = Attribute(unicode, (SUMMARY, DETAILED)) #: Which bike/shoes were used on activity. gear = EntityAttribute(Gear, (DETAILED,)) - average_speed = Attribute(float, (SUMMARY,DETAILED), units=uh.meters_per_second) #: Average speed for activity. - max_speed = Attribute(float, (SUMMARY,DETAILED), units=uh.meters_per_second) #: Max speed for activity + average_speed = Attribute(float, (SUMMARY, DETAILED), units=uh.meters_per_second) #: Average speed for activity. + max_speed = Attribute(float, (SUMMARY, DETAILED), units=uh.meters_per_second) #: Max speed for activity - device_watts = Attribute(bool, (SUMMARY,DETAILED)) #: True if the watts are from a power meter, false if estimated + device_watts = Attribute(bool, (SUMMARY, DETAILED)) #: True if the watts are from a power meter, false if estimated - truncated = Attribute(int, (SUMMARY,DETAILED)) #: Only present if activity is owned by authenticated athlete, set to 0 if not truncated by privacy zones - has_kudoed = Attribute(bool, (SUMMARY,DETAILED)) #: If authenticated user has kudoed this activity + truncated = Attribute(int, (SUMMARY, DETAILED)) #: Only present if activity is owned by authenticated athlete, set to 0 if not truncated by privacy zones + has_kudoed = Attribute(bool, (SUMMARY, DETAILED)) #: If authenticated user has kudoed this activity - best_efforts = EntityCollection(BestEffort, (DETAILED,)) #: :class:`list` of metric :class:`stravalib.model.BestEffort` summaries - segment_efforts = EntityCollection(SegmentEffort, (DETAILED,)) #: :class:`list` of :class:`stravalib.model.SegmentEffort` efforts for activity. - splits_metric = EntityCollection(Split, (DETAILED,)) #: :class:`list` of metric :class:`stravalib.model.Split` summaries (running activities only) - splits_standard = EntityCollection(Split, (DETAILED,)) #: :class:`list` of standard/imperial :class:`stravalib.model.Split` summaries (running activities only) + best_efforts = EntityCollection(BestEffort, (DETAILED,)) #: :class:`list` of metric :class:`stravalib.model.BestEffort` summaries + segment_efforts = EntityCollection(SegmentEffort, (DETAILED,)) #: :class:`list` of :class:`stravalib.model.SegmentEffort` efforts for activity. + splits_metric = EntityCollection(Split, (DETAILED,)) #: :class:`list` of metric :class:`stravalib.model.Split` summaries (running activities only) + splits_standard = EntityCollection(Split, (DETAILED,)) #: :class:`list` of standard/imperial :class:`stravalib.model.Split` summaries (running activities only) # Undocumented attributes - average_watts = Attribute(float, (SUMMARY,DETAILED)) #: (undocumented) Average power during activity - weighted_average_watts = Attribute(int, (SUMMARY,DETAILED)) # rides with power meter data only similar to xPower or Normalized Power - average_heartrate = Attribute(float, (SUMMARY,DETAILED)) #: (undocumented) Average HR during activity - max_heartrate = Attribute(int, (SUMMARY,DETAILED)) #: (undocumented) Max HR during activity - average_cadence = Attribute(float, (SUMMARY,DETAILED)) #: (undocumented) Average cadence during activity - kilojoules = Attribute(float, (SUMMARY,DETAILED)) #: (undocumented) Kilojoules of energy used during activity - device_watts = Attribute(bool, (SUMMARY,DETAILED)) # true if the watts are from a power meter, false if estimated - average_temp = Attribute(int, (SUMMARY,DETAILED)) #: (undocumented) Average temperature (when available from device) during activity. + average_watts = Attribute(float, (SUMMARY, DETAILED)) #: (undocumented) Average power during activity + weighted_average_watts = Attribute(int, (SUMMARY, DETAILED)) # rides with power meter data only similar to xPower or Normalized Power + average_heartrate = Attribute(float, (SUMMARY, DETAILED)) #: (undocumented) Average HR during activity + max_heartrate = Attribute(int, (SUMMARY, DETAILED)) #: (undocumented) Max HR during activity + average_cadence = Attribute(float, (SUMMARY, DETAILED)) #: (undocumented) Average cadence during activity + kilojoules = Attribute(float, (SUMMARY, DETAILED)) #: (undocumented) Kilojoules of energy used during activity + device_watts = Attribute(bool, (SUMMARY, DETAILED)) # true if the watts are from a power meter, false if estimated + average_temp = Attribute(int, (SUMMARY, DETAILED)) #: (undocumented) Average temperature (when available from device) during activity. calories = Attribute(float, (DETAILED,)) #: Calculation of how many calories burned on activity description = Attribute(unicode, (DETAILED,)) #: (undocumented) Description of activity. @@ -679,6 +699,7 @@ def photos(self): self._photos = [] return self._photos + class SegmentLeaderboardEntry(BoundEntity): """ Represents a single entry on a segment leaderboard. @@ -690,20 +711,20 @@ class SegmentLeaderboardEntry(BoundEntity): _activity = None _effort = None - effort_id = Attribute(int) #: The numeric ID for the segment effort. - athlete_id = Attribute(int) #: The numeric ID for the athlete. - athlete_name = Attribute(unicode) #: The athlete's name. - athlete_gender = Attribute(unicode) #: The athlete's sex (M/F) - athlete_profile = Attribute(unicode) #: Link to athlete profile photo - average_hr = Attribute(float) #: The athlete's average HR for this effort - average_watts = Attribute(float) #: The athlete's average power for this effort - distance = Attribute(float, units=uh.meters) #: The distance for this effort. - elapsed_time = TimeIntervalAttribute() #: The elapsed time for this effort - moving_time = TimeIntervalAttribute() #: The moving time for this effort - start_date = TimestampAttribute((SUMMARY,DETAILED)) #: :class:`datetime.datetime` when this effot was started in GMT - start_date_local = TimestampAttribute((SUMMARY,DETAILED), tzinfo=None) #: :class:`datetime.datetime` when this effort was started in activity timezone - activity_id = Attribute(int) #: The numeric ID of the associated activity for this effort. - rank = Attribute(int) #: The rank on the leaderboard. + effort_id = Attribute(int) #: The numeric ID for the segment effort. + athlete_id = Attribute(int) #: The numeric ID for the athlete. + athlete_name = Attribute(unicode) #: The athlete's name. + athlete_gender = Attribute(unicode) #: The athlete's sex (M/F) + athlete_profile = Attribute(unicode) #: Link to athlete profile photo + average_hr = Attribute(float) #: The athlete's average HR for this effort + average_watts = Attribute(float) #: The athlete's average power for this effort + distance = Attribute(float, units=uh.meters) #: The distance for this effort. + elapsed_time = TimeIntervalAttribute() #: The elapsed time for this effort + moving_time = TimeIntervalAttribute() #: The moving time for this effort + start_date = TimestampAttribute((SUMMARY, DETAILED)) #: :class:`datetime.datetime` when this effot was started in GMT + start_date_local = TimestampAttribute((SUMMARY, DETAILED), tzinfo=None) #: :class:`datetime.datetime` when this effort was started in activity timezone + activity_id = Attribute(int) #: The numeric ID of the associated activity for this effort. + rank = Attribute(int) #: The rank on the leaderboard. def __repr__(self): return ''.format(self.rank, self.athlete_name) @@ -735,6 +756,7 @@ def effort(self): self._effort = self.bind_client.get_segment_effort(self.effort_id) return self._effort + class SegmentLeaderboard(Sequence, BoundEntity): """ The ranked leaderboard for a segment. @@ -757,13 +779,15 @@ def __contains__(self, k): def __getitem__(self, k): return self.entries[k] + class DistributionBucket(BaseEntity): """ A single distribution bucket object, used for activity zones. """ - max = Attribute(int) #: Max datatpoint - min = Attribute(int) #: Min datapoint - time = Attribute(int, units=uh.seconds) #: Time in seconds (*not* a :class:`datetime.timedelta`) + max = Attribute(int) #: Max datatpoint + min = Attribute(int) #: Min datapoint + time = Attribute(int, units=uh.seconds) #: Time in seconds (*not* a :class:`datetime.timedelta`) + class BaseActivityZone(LoadableEntity): """ @@ -771,9 +795,9 @@ class BaseActivityZone(LoadableEntity): A collection of :class:`stravalib.model.DistributionBucket` objects. """ - distribution_buckets = EntityCollection(DistributionBucket, (SUMMARY, DETAILED)) #: The collection of :class:`stravalib.model.DistributionBucket` objects - type = Attribute(unicode, (SUMMARY, DETAILED)) #: Type of activity zone (heartrate, power, pace). - sensor_based = Attribute(bool, (SUMMARY, DETAILED)) #: Whether zone data is sensor-based (as opposed to calculated) + distribution_buckets = EntityCollection(DistributionBucket, (SUMMARY, DETAILED)) #: The collection of :class:`stravalib.model.DistributionBucket` objects + type = Attribute(unicode, (SUMMARY, DETAILED)) #: Type of activity zone (heartrate, power, pace). + sensor_based = Attribute(bool, (SUMMARY, DETAILED)) #: Whether zone data is sensor-based (as opposed to calculated) @classmethod def deserialize(cls, v, bind_client=None): @@ -799,18 +823,20 @@ class HeartrateActivityZone(BaseActivityZone): """ Activity zone for heart rate. """ - score = Attribute(int, (SUMMARY, DETAILED)) #: The score (suffer score) for this HR zone. - points = Attribute(int, (SUMMARY, DETAILED)) #: The points for this HR zone. - custom_zones = Attribute(bool, (SUMMARY, DETAILED)) #: Whether athlete has setup custom zones. - max = Attribute(int, (SUMMARY, DETAILED)) #: The max heartrate + score = Attribute(int, (SUMMARY, DETAILED)) #: The score (suffer score) for this HR zone. + points = Attribute(int, (SUMMARY, DETAILED)) #: The points for this HR zone. + custom_zones = Attribute(bool, (SUMMARY, DETAILED)) #: Whether athlete has setup custom zones. + max = Attribute(int, (SUMMARY, DETAILED)) #: The max heartrate + class PaceActivityZone(BaseActivityZone): """ Activity zone for pace. """ - score = Attribute(int, (SUMMARY, DETAILED)) #: The score for this zone. - sample_race_distance = Attribute(int, (SUMMARY, DETAILED), units=uh.meters) #: (Not sure?) - sample_race_time = TimeIntervalAttribute((SUMMARY, DETAILED)) #: (Not sure?) + score = Attribute(int, (SUMMARY, DETAILED)) #: The score for this zone. + sample_race_distance = Attribute(int, (SUMMARY, DETAILED), units=uh.meters) #: (Not sure?) + sample_race_time = TimeIntervalAttribute((SUMMARY, DETAILED)) #: (Not sure?) + class PowerActivityZone(BaseActivityZone): """ @@ -818,18 +844,20 @@ class PowerActivityZone(BaseActivityZone): """ # these 2 below were removed according to June 3, 2014 update @ # http://strava.github.io/api/v3/changelog/ - bike_weight = Attribute(float, (SUMMARY, DETAILED), units=uh.kgs) #: Weight of bike being used (factored into power calculations) - athlete_weight = Attribute(float, (SUMMARY, DETAILED), units=uh.kgs) #: Weight of athlete (factored into power calculations) + bike_weight = Attribute(float, (SUMMARY, DETAILED), units=uh.kgs) #: Weight of bike being used (factored into power calculations) + athlete_weight = Attribute(float, (SUMMARY, DETAILED), units=uh.kgs) #: Weight of athlete (factored into power calculations) + class Stream(LoadableEntity): """ Stream of readings from the activity, effort or segment. """ type = Attribute(unicode) - data = Attribute(list,) #: array of values - series_type = Attribute(unicode, ) #: type of stream: time, latlng, distance, altitude, velocity_smooth, heartrate, cadence, watts, temp, moving, grade_smooth - original_size = Attribute(int, ) #: the size of the complete stream (when not reduced with resolution) + data = Attribute(list,) #: array of values + series_type = Attribute(unicode, ) #: type of stream: time, latlng, distance, altitude, velocity_smooth, heartrate, cadence, watts, temp, moving, grade_smooth + original_size = Attribute(int, ) #: the size of the complete stream (when not reduced with resolution) resolution = Attribute(unicode, ) #: (optional, default is 'all') the desired number of data points. 'low' (100), 'medium' (1000), 'high' (10000) or 'all' + def __repr__(self): return ''.format(self.type, self.resolution, diff --git a/stravalib/protocol.py b/stravalib/protocol.py index 838f59cf..faf6e8b9 100644 --- a/stravalib/protocol.py +++ b/stravalib/protocol.py @@ -13,6 +13,7 @@ from stravalib import exc + class ApiV3(object): """ This class is responsible for performing the HTTP requests, rate limiting, and error handling. @@ -101,8 +102,8 @@ def exchange_code_for_token(self, client_id, client_secret, code): :rtype: str """ response = self._request('https://{0}/oauth/token'.format(self.server), - params={'client_id': client_id, 'client_secret': client_secret, 'code': code}, - method='POST') + params={'client_id': client_id, 'client_secret': client_secret, 'code': code}, + method='POST') token = response['access_token'] self.access_token = token return token @@ -136,7 +137,7 @@ def _request(self, url, params=None, files=None, method='GET', check_for_errors= self._handle_protocol_error(raw) # 204 = No content - if raw.status_code in [204,]: + if raw.status_code in [204]: resp = [] else: resp = raw.json() @@ -208,7 +209,7 @@ def get(self, url, check_for_errors=True, **kwargs): """ referenced = self._extract_referenced_vars(url) url = url.format(**kwargs) - params = dict([(k,v) for k,v in kwargs.items() if not k in referenced]) + params = dict([(k, v) for k, v in kwargs.items() if not k in referenced]) return self._request(url, params=params, check_for_errors=check_for_errors) def post(self, url, files=None, check_for_errors=True, **kwargs): @@ -217,7 +218,7 @@ def post(self, url, files=None, check_for_errors=True, **kwargs): """ referenced = self._extract_referenced_vars(url) url = url.format(**kwargs) - params = dict([(k,v) for k,v in kwargs.items() if not k in referenced]) + params = dict([(k, v) for k, v in kwargs.items() if not k in referenced]) return self._request(url, params=params, files=files, method='POST', check_for_errors=check_for_errors) def put(self, url, check_for_errors=True, **kwargs): @@ -226,5 +227,5 @@ def put(self, url, check_for_errors=True, **kwargs): """ referenced = self._extract_referenced_vars(url) url = url.format(**kwargs) - params = dict([(k,v) for k,v in kwargs.items() if not k in referenced]) - return self._request(url, params=params, method='PUT', check_for_errors=check_for_errors) \ No newline at end of file + params = dict([(k, v) for k, v in kwargs.items() if not k in referenced]) + return self._request(url, params=params, method='PUT', check_for_errors=check_for_errors) diff --git a/stravalib/tests/__init__.py b/stravalib/tests/__init__.py index 894cf1ff..3f615a08 100644 --- a/stravalib/tests/__init__.py +++ b/stravalib/tests/__init__.py @@ -15,9 +15,9 @@ class TestBase(TestCase): - + def setUp(self): super(TestBase, self).setUp() - + def tearDown(self): - super(TestBase, self).tearDown() \ No newline at end of file + super(TestBase, self).tearDown() diff --git a/stravalib/tests/functional/__init__.py b/stravalib/tests/functional/__init__.py index 3951599e..13c3d783 100644 --- a/stravalib/tests/functional/__init__.py +++ b/stravalib/tests/functional/__init__.py @@ -15,7 +15,7 @@ class FunctionalTestBase(TestBase): def setUp(self): super(FunctionalTestBase, self).setUp() if not os.path.exists(TEST_CFG): - raise Exception("Unable to run the write tests without a tests.ini that defines an access_token with write privs.") + raise Exception("Unable to run the write tests without a test.ini in that defines an access_token with write privs.") cfg = ConfigParser.SafeConfigParser() with open(TEST_CFG) as fp: @@ -23,4 +23,4 @@ def setUp(self): access_token = cfg.get('write_tests', 'access_token') self.client = Client(access_token=access_token) - \ No newline at end of file + diff --git a/stravalib/tests/test_attributes.py b/stravalib/tests/test_attributes.py index fb7f29a6..a098bcda 100644 --- a/stravalib/tests/test_attributes.py +++ b/stravalib/tests/test_attributes.py @@ -28,5 +28,5 @@ def test_unmarshal_non_ascii_chars(self): u'id': 874283, u'friend': None } - athlete = EntityAttribute(Athlete, (SUMMARY,DETAILED)) + athlete = EntityAttribute(Athlete, (SUMMARY, DETAILED)) athlete.unmarshal(NON_ASCII_DATA) diff --git a/stravalib/tests/test_model.py b/stravalib/tests/test_model.py index b4102620..76e636c4 100644 --- a/stravalib/tests/test_model.py +++ b/stravalib/tests/test_model.py @@ -5,63 +5,62 @@ from stravalib.tests import TestBase from units.quantity import Quantity + class ModelTest(TestBase): - + def setUp(self): super(ModelTest, self).setUp() - + def test_entity_collections(self): - + a = model.Athlete() d = {'clubs': [{'resource_state': 2, 'id': 7, 'name': 'Team Roaring Mouse'}, - {'resource_state': 2, 'id': 1, 'name': 'Team Strava Cycling'}, - {'resource_state': 2, 'id': 34444, 'name': 'Team Strava Cyclocross'}] - } + {'resource_state': 2, 'id': 1, 'name': 'Team Strava Cycling'}, + {'resource_state': 2, 'id': 34444, 'name': 'Team Strava Cyclocross'}] + } a.from_dict(d) - + self.assertEquals(3, len(a.clubs)) self.assertEquals('Team Roaring Mouse', a.clubs[0].name) - + def test_speed_units(self): a = model.Activity() - - a.max_speed = 1000 # m/s - a.average_speed = 1000 # m/s + + a.max_speed = 1000 # m/s + a.average_speed = 1000 # m/s self.assertEquals(3600.0, float(uh.kph(a.max_speed))) self.assertEquals(3600.0, float(uh.kph(a.average_speed))) - + a.max_speed = uh.mph(1.0) #print repr(a.max_speed) - + self.assertAlmostEqual(1.61, float(uh.kph(a.max_speed)), places=2) - + def test_time_intervals(self): segment = model.Segment() # s.pr_time = XXXX - + split = model.Split() split.moving_time = 3.1 split.elapsed_time = 5.73 - - - + def test_distance_units(self): - + # Gear g = model.Gear() g.distance = 1000 self.assertEquals(1.0, float(uh.kilometers(g.distance))) - + # Metric Split split = model.Split() - split.distance = 1000 # meters - split.elevation_difference = 1000 # meters + split.distance = 1000 # meters + split.elevation_difference = 1000 # meters self.assertIsInstance(split.distance, Quantity) self.assertIsInstance(split.elevation_difference, Quantity) self.assertEquals(1.0, float(uh.kilometers(split.distance))) self.assertEquals(1.0, float(uh.kilometers(split.elevation_difference))) split = None - + # Segment s = model.Segment() s.distance = 1000 @@ -75,17 +74,17 @@ def test_distance_units(self): self.assertEquals(2.0, float(uh.kilometers(s.elevation_high))) self.assertEquals(1.0, float(uh.kilometers(s.elevation_low))) self.assertEquals(1.0, float(uh.kilometers(s.pr_distance))) - + # Activity a = model.Activity() - a.distance = 1000 # m - a.total_elevation_gain = 1000 # m + a.distance = 1000 # m + a.total_elevation_gain = 1000 # m self.assertIsInstance(a.distance, Quantity) self.assertIsInstance(a.total_elevation_gain, Quantity) self.assertEquals(1.0, float(uh.kilometers(a.distance))) self.assertEquals(1.0, float(uh.kilometers(a.total_elevation_gain))) - + def test_weight_units(self): """ """ - # PowerActivityZone \ No newline at end of file + # PowerActivityZone diff --git a/stravalib/unithelper.py b/stravalib/unithelper.py index a1054e8b..80cbdf8e 100644 --- a/stravalib/unithelper.py +++ b/stravalib/unithelper.py @@ -1,7 +1,7 @@ """ Helpers for converting Strava's units to something more practical. -These are really just thin wrappers to the brilliant 'units' python library. +These are really just thin wrappers to the brilliant 'units' python library. """ from units import unit import units.predefined @@ -23,16 +23,18 @@ kilogram = kilograms = kg = kgs = unit('kg') pound = pounds = lb = lbs = unit('lb') + def c2f(celsius): """ Convert Celcius to Farenheit """ - return 9.0/5.0 * celsius + 32 + return (9.0 / 5.0) * celsius + 32 + def timedelta_to_seconds(td): """ Converts a timedelta to total seconds, including support for microseconds. - + Return value is (potentially truncated) integer. - + (This is built-in in Python >= 2.7, but we are still supporting Python 2.6 here.) :param td: The timedelta object :type td: :class:`datetime.timedelta` @@ -41,4 +43,4 @@ def timedelta_to_seconds(td): """ if not td: return None - return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6 \ No newline at end of file + return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6 diff --git a/stravalib/util/limiter.py b/stravalib/util/limiter.py index cf703f69..28876524 100644 --- a/stravalib/util/limiter.py +++ b/stravalib/util/limiter.py @@ -1,19 +1,19 @@ """ Rate limiter classes. -These are basically callables that when called register that a request was +These are basically callables that when called register that a request was issued. Depending on how they are configured that may cause a pause or exception if a rate limit has been exceeded. Obviously it is up to the calling code to ensure that these callables are invoked with every (successful?) call to the backend -API. (There is probably a better way to hook these into the requests library -directly ... TBD.) +API. (There is probably a better way to hook these into the requests library +directly ... TBD.) From the Strava docs: - Strava API usage is limited on a per-application basis using a short term, - 15 minute, limit and a long term, daily, limit. The default rate limit allows + Strava API usage is limited on a per-application basis using a short term, + 15 minute, limit and a long term, daily, limit. The default rate limit allows 600 requests every 15 minutes, with up to 30,000 requests per day. - - This limit allows applications to make 40 requests per minute for about + + This limit allows applications to make 40 requests per minute for about half the day. """ from __future__ import division @@ -34,20 +34,21 @@ def total_seconds(td): class RateLimiter(object): - + def __init__(self): self.log = logging.getLogger('{0.__module__}.{0.__name__}'.format(self.__class__)) self.rules = [] - + def __call__(self): """ Register another request is being issued. """ for r in self.rules: r() - + + class RateLimitRule(object): - + def __init__(self, requests, seconds, raise_exc=False): """ :param requests: Number of requests for limit. @@ -59,43 +60,44 @@ def __init__(self, requests, seconds, raise_exc=False): self.requests = requests self.tab = collections.deque(maxlen=self.requests) self.raise_exc = raise_exc - + def __call__(self): """ Register another request is being issued. - - Depending on configuration of the rule will pause if rate limit has + + Depending on configuration of the rule will pause if rate limit has been reached, or raise exception, etc. - """ + """ # First check if the deque is full; that indicates that we'd better check whether # we need to pause. if len(self.tab) == self.requests: # Grab the oldest (leftmost) timestamp and check to see if it is greater than 1 second delta = datetime.now() - self.tab[0] - if delta < self.timeframe: # Has it been less than configured timeframe since oldest request? + if delta < self.timeframe: # Has it been less than configured timeframe since oldest request? if self.raise_exc: raise exc.RateLimitExceeded("Rate limit exceeded (can try again in {0})".format(self.timeframe - delta)) else: - # Wait the difference between timeframe and the oldest request. + # Wait the difference between timeframe and the oldest request. td = self.timeframe - delta sleeptime = hasattr(td, 'total_seconds') and td.total_seconds() or total_seconds(td) self.log.debug("Rate limit triggered; sleeping for {0}".format(sleeptime)) time.sleep(sleeptime) - self.tab.append(datetime.now()) - + self.tab.append(datetime.now()) + + class DefaultRateLimiter(RateLimiter): """ Implements something similar to the default rate limit for Strava apps. - + To do this correctly we would actually need to change our logic to reset the limit at midnight, etc. Will make this more complex in the future. - - Strava API usage is limited on a per-application basis using a short term, - 15 minute, limit and a long term, daily, limit. The default rate limit allows + + Strava API usage is limited on a per-application basis using a short term, + 15 minute, limit and a long term, daily, limit. The default rate limit allows 600 requests every 15 minutes, with up to 30,000 requests per day. """ - + def __init__(self): super(DefaultRateLimiter, self).__init__() self.rules.append(RateLimitRule(requests=40, seconds=60, raise_exc=False)) - self.rules.append(RateLimitRule(requests=30000, seconds=3600*24, raise_exc=True)) \ No newline at end of file + self.rules.append(RateLimitRule(requests=30000, seconds=(3600 * 24), raise_exc=True))