diff --git a/strava/__init__.py b/strava/__init__.py index 0ad7dc8..6a799f0 100644 --- a/strava/__init__.py +++ b/strava/__init__.py @@ -11,18 +11,14 @@ __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 import json -try: - import urllib2 -except ImportError: - import urllib - urllib2 = False - +import urllib2 +import urllib class APIError(Exception): pass @@ -35,25 +31,41 @@ def __init__(self, oid): self._id = oid #noinspection PyUnresolvedReferences - def load(self, url, key): - if urllib2: - try: + def load(self, url, key=None, params=None): + try: + 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)) - else: - try: - rsp = urllib.request.urlopen(BASE_API + url) - except urllib.error.HTTPError as e: - raise APIError("%s: request failed: %s" % (url, e)) + 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)[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)) + 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 @@ -68,16 +80,15 @@ class Athlete(StravaObject): """ def __init__(self, oid): super(Athlete, self).__init__(oid) - self._url = "/rides?athleteId=%s" % self.id def rides(self, start_date=None): out = [] + params = {'athleteId': self.id} - url = 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 @@ -95,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. @@ -121,16 +155,53 @@ 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 +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): 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 +299,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):