From daada81183db32225d80bddcf8db42cd2ff39485 Mon Sep 17 00:00:00 2001 From: Geoff Crompton Date: Mon, 29 Apr 2013 13:34:47 +1000 Subject: [PATCH 1/6] Be specific about which API using in each call. This will allow mixing in v2 api calls in later commits. --- strava/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/strava/__init__.py b/strava/__init__.py index 0ad7dc8..656bb81 100644 --- a/strava/__init__.py +++ b/strava/__init__.py @@ -11,7 +11,7 @@ __version__ = "1.0" -BASE_API = "http://www.strava.com/api/v1" +BASE_API = "http://www.strava.com/api" from collections import defaultdict from datetime import date, timedelta @@ -68,12 +68,12 @@ class Athlete(StravaObject): """ def __init__(self, oid): super(Athlete, self).__init__(oid) - self._url = "/rides?athleteId=%s" % self.id + self._url = "/v1/rides?athleteId=%s" % self.id def rides(self, start_date=None): out = [] - url = self._url + url = '%s/v1' % self._url if start_date: url += "&startDate=%s" % start_date.isoformat() @@ -121,7 +121,7 @@ def detail(self): @property def segments(self): if not self._segments: - for effort in self.load("/rides/%s/efforts" % self.id, "efforts"): + for effort in self.load("/v1/rides/%s/efforts" % self.id, "efforts"): self._segments.append(Segment(effort)) return self._segments @@ -130,7 +130,7 @@ class RideDetail(StravaObject): def __init__(self, oid): super(RideDetail, self).__init__(oid) - self._attr = self.load("/rides/%s" % oid, 'ride') + self._attr = self.load("/v1/rides/%s" % oid, 'ride') @property def athlete(self): @@ -228,8 +228,8 @@ def detail(self): class SegmentDetail(StravaObject): def __init__(self, segment_id, effort_id): super(SegmentDetail, self).__init__(segment_id) - self._effort_attr = self.load("/efforts/%s" % effort_id, "effort") - self._segment_attr = self.load("/segments/%s" % segment_id, "segment") + self._effort_attr = self.load("/v1/efforts/%s" % effort_id, "effort") + self._segment_attr = self.load("/v1/segments/%s" % segment_id, "segment") @property def distance(self): From 7b4bdaec9ffe8eef5e1b2dee3cfe145c75e3361e Mon Sep 17 00:00:00 2001 From: Geoff Crompton Date: Wed, 1 May 2013 08:42:59 +1000 Subject: [PATCH 2/6] remove urllib usage When running pychecker on the module it complains that it's not found. Even if I modified the module to always import urllib, pychecker still complained that urllib.request and urllib.error were not module attributes. I suspect the code paths that use urllib for requests do not work, hence I've removed them with this commit. --- strava/__init__.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/strava/__init__.py b/strava/__init__.py index 656bb81..801865d 100644 --- a/strava/__init__.py +++ b/strava/__init__.py @@ -17,12 +17,7 @@ from datetime import date, timedelta import json -try: - import urllib2 -except ImportError: - import urllib - urllib2 = False - +import urllib2 class APIError(Exception): pass @@ -36,17 +31,11 @@ def __init__(self, oid): #noinspection PyUnresolvedReferences def load(self, url, key): - if urllib2: - try: - req = urllib2.Request(BASE_API + url) - rsp = urllib2.urlopen(req) - except urllib2.HTTPError as e: - raise APIError("%s: request failed: %s" % (url, e)) - else: - try: - rsp = urllib.request.urlopen(BASE_API + url) - except urllib.error.HTTPError as e: - raise APIError("%s: request failed: %s" % (url, e)) + try: + req = urllib2.Request(BASE_API + url) + rsp = urllib2.urlopen(req) + except urllib2.HTTPError as e: + raise APIError("%s: request failed: %s" % (url, e)) txt = rsp.read().decode('utf-8') try: From abe8889045ce19758c4012e83ec413726b5cf023 Mon Sep 17 00:00:00 2001 From: Geoff Crompton Date: Wed, 1 May 2013 17:46:44 +1000 Subject: [PATCH 3/6] add post() method This will (in a later commit) be used to implement logins. It's a little ironic that with my last commit having removed urllib usage, I'm now re-importing it to get the urlencode() function. --- strava/__init__.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/strava/__init__.py b/strava/__init__.py index 801865d..765c90a 100644 --- a/strava/__init__.py +++ b/strava/__init__.py @@ -18,6 +18,7 @@ import json import urllib2 +import urllib class APIError(Exception): pass @@ -43,6 +44,21 @@ def load(self, url, key): except (ValueError, KeyError) as e: raise APIError("%s: parsing response failed: %s" % (url, e)) + def post(self, url, data): + params = urllib.urlencode(data) + try: + req = urllib2.Request(BASE_API + url) + req.add_data(params) + rsp = urllib2.urlopen(req) + except urllib2.HTTPError as e: + raise APIError("%s: request failed: %s" % (url, e)) + txt = rsp.read().decode('utf-8') + + try: + return json.loads(txt) + except (ValueError) as e: + raise APIError("%s: parsing response failed: %s" % (url, e)) + @property def id(self): return self._id From 9416fe5984f291144290968bda80ac776302cf9d Mon Sep 17 00:00:00 2001 From: Geoff Crompton Date: Sat, 4 May 2013 09:49:56 +1000 Subject: [PATCH 4/6] Make key parameter of load() optional. Some of the strava API calls return multiple pieces of useful data, such as the /v2/authentication/login call. Rather than discard that data having the key optional means you can have the load() method return it all to your method. --- strava/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/strava/__init__.py b/strava/__init__.py index 765c90a..be3f65a 100644 --- a/strava/__init__.py +++ b/strava/__init__.py @@ -31,7 +31,7 @@ def __init__(self, oid): self._id = oid #noinspection PyUnresolvedReferences - def load(self, url, key): + def load(self, url, key=None): try: req = urllib2.Request(BASE_API + url) rsp = urllib2.urlopen(req) @@ -40,7 +40,10 @@ def load(self, url, key): txt = rsp.read().decode('utf-8') try: - return json.loads(txt)[key] + if key: + return json.loads(txt)[key] + else: + return json.loads(txt) except (ValueError, KeyError) as e: raise APIError("%s: parsing response failed: %s" % (url, e)) From a271a37a644ffc064ffd3b08c6d9beb3fb93707c Mon Sep 17 00:00:00 2001 From: Geoff Crompton Date: Sun, 5 May 2013 10:18:39 +1000 Subject: [PATCH 5/6] Add params argument to load() Occasionally the API requires us to pass parameters in as query arguments. So far that's been done by creating the 'url' outside the load() method. But it's quite easy to do within the load() method. This also fixes a minor bug in the url forming of Athlete.rides(). --- strava/__init__.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/strava/__init__.py b/strava/__init__.py index be3f65a..bee7bf8 100644 --- a/strava/__init__.py +++ b/strava/__init__.py @@ -31,9 +31,13 @@ def __init__(self, oid): self._id = oid #noinspection PyUnresolvedReferences - def load(self, url, key=None): + def load(self, url, key=None, params=None): try: - req = urllib2.Request(BASE_API + url) + if params: + urllib.urlencode(params) + req = urllib2.Request("%s%s?%s" % (BASE_API, url, urllib.urlencode(params))) + else: + req = urllib2.Request(BASE_API + url) rsp = urllib2.urlopen(req) except urllib2.HTTPError as e: raise APIError("%s: request failed: %s" % (url, e)) @@ -76,16 +80,15 @@ class Athlete(StravaObject): """ def __init__(self, oid): super(Athlete, self).__init__(oid) - self._url = "/v1/rides?athleteId=%s" % self.id def rides(self, start_date=None): out = [] + params = {'athleteId': self.id} - url = '%s/v1' % self._url if start_date: - url += "&startDate=%s" % start_date.isoformat() + params['startDate'] = start_date.isoformat() - for ride in self.load(url, "rides"): + for ride in self.load('/v1/rides', "rides", params): out.append(Ride(ride["id"], ride["name"])) return out From 9eb6a8e233d0f6317d11cfdad82beaa8bc928c29 Mon Sep 17 00:00:00 2001 From: Geoff Crompton Date: Sun, 5 May 2013 11:42:20 +1000 Subject: [PATCH 6/6] Add AuthenticatedAthlete, AuthenticatedRide objects and login() function. This lets us get an authentication token that we can use to access the streams part of the API, and get some interesting and useful ride data. --- strava/__init__.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/strava/__init__.py b/strava/__init__.py index bee7bf8..6a799f0 100644 --- a/strava/__init__.py +++ b/strava/__init__.py @@ -106,6 +106,29 @@ def ride_stats(self, days=7): return stats +class AuthenticatedAthlete(Athlete): + def __init__(self, oid, token, attr): + super(AuthenticatedAthlete, self).__init__(oid) + self.token = token + self._attr = attr + + def rides(self, start_date=None): + rides = super(AuthenticatedAthlete, self).rides(start_date) + out = [] + for ride in rides: + out.append(AuthenticatedRide(ride.id, ride.name, self.token)) + return out + + +def login(email, password): + """Logs in to the strava API, and returns an AuthenticatedAthlete""" + response = StravaObject(None).post("/v2/authentication/login", + {'email':email, 'password':password}) + token = response['token'] + details = response['athlete'] + _id = response['athlete']['id'] + return AuthenticatedAthlete(_id, token, details) + class Ride(StravaObject): """Information about a single ride. @@ -136,6 +159,43 @@ def segments(self): self._segments.append(Segment(effort)) return self._segments +class AuthenticatedRide(Ride): + def __init__(self, oid, name, token): + super(AuthenticatedRide, self).__init__(oid, name) + self.token = token + self._streams = None + + @property + def streams(self): + if not self._streams: + self._streams = RideStreams(self.id, self.token) + return self._streams + +class RideStreams(StravaObject): + def __init__(self, oid, token): + super(RideStreams, self).__init__(oid) + + # In other places we lazy load details to save a round trip. But in + # this case the only reason you want the stream is to get all the + # detail. So fetch it now. + self._attr = self.load("/v1/streams/%i" % self.id, params={'token':token}) + + # TODO: This allows you to access all the data from the stream, as long + # as you happen to know what that is. But perhaps we should write out + # @property methods for each of them. Perhaps explicit is better than + # implicit, but it does seem like a lot of boilerplate code. + def __getattr__(self, name): + return self._attr[name] + + @property + def speed(self): + out = [0.0] + + # Skip the first tuple, as time is always 0, and the division always + # leads to a ZeroDivisionError. + for i in zip(self.distance, self.time)[1:]: + out.append(i[0]/i[1]) + return out class RideDetail(StravaObject):