Skip to content

Commit

Permalink
Merge pull request stravalib#18 from ghisprince/master
Browse files Browse the repository at this point in the history
add support for friends, followers, kudos and photos
  • Loading branch information
hozn committed Jul 7, 2014
2 parents 7157b9f + 28e01ed commit 50457fe
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 28 deletions.
42 changes: 40 additions & 2 deletions stravalib/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,12 @@ def get_activities(self, before=None, after=None, limit=None):
:param limit: How many maximum activities to return.
:type limit: int
"""
#if before and after:
# raise ValueError("Cannot specify both 'before' and 'after' params.")

if before:
if isinstance(before, str):
before = dateparser.parse(before, ignoretz=True)
before = time.mktime(before.timetuple())

if after:
if isinstance(after, str):
after = dateparser.parse(after, ignoretz=True)
Expand Down Expand Up @@ -533,6 +532,45 @@ 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.
http://strava.github.io/api/v3/kudos/#list
:param activity_id: The activity for which to fetch kudos.
:param limit: Max rows to return (default unlimited).
:type limit: int
:return: An iterator of :class:`stravalib.model.Athlete` objects.
:rtype: :class:`BatchedResultsIterator`
"""
result_fetcher = functools.partial(self.protocol.get,
'/activities/{id}/kudos',
id=activity_id)

return BatchedResultsIterator(entity=model.ActivityKudos,
bind_client=self,
result_fetcher=result_fetcher,
limit=limit)

def get_activity_photos(self, activity_id):
"""
Gets the photos from an activity.
http://strava.github.io/api/v3/photos/
:param activity_id: The activity for which to fetch kudos.
:return: An iterator of :class:`stravalib.model.ActivityPhoto` objects.
:rtype: :class:`BatchedResultsIterator`
"""
result_fetcher = functools.partial(self.protocol.get,
'/activities/{id}/photos',
id=activity_id)

return BatchedResultsIterator(entity=model.ActivityPhoto,
bind_client=self,
result_fetcher=result_fetcher)

def get_gear(self, gear_id):
"""
Get details for an item of gear.
Expand Down
87 changes: 87 additions & 0 deletions stravalib/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,19 +275,81 @@ class Athlete(LoadableEntity):
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 '<Athlete id={id} firstname={fname} lastname={lname}>'.format(id=self.id,
fname=fname,
lname=lname)
@property
def friends(self):
"""
Iterator of :class:`stravalib.model.Athlete` objects for this activity.
"""
if self._friends is None:
self.assert_bind_client()
if self.friend_count > 0:
self._friends = self.bind_client.get_athlete_friends(self.id)
else:
# Shortcut if we know there aren't any
self._friends = []
return self._friends

@property
def followers(self):
"""
Iterator of :class:`stravalib.model.Athlete` objects for this activity.
"""
if self._followers is None:
self.assert_bind_client()
if self.follower_count > 0:
self._followers = self.bind_client.get_athlete_followers(self.id)
else:
# Shortcut if we know there aren't any
self._followers = []
return self._followers

class ActivityComment(LoadableEntity):
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):
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)

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

class Map(IdentifiableEntity):
id = Attribute(unicode, (SUMMARY,DETAILED)) #: Alpha-numeric identifier
polyline = Attribute(str, (SUMMARY,DETAILED)) #: Google polyline encoding
Expand Down Expand Up @@ -431,6 +493,8 @@ class Activity(LoadableEntity):

_comments = None
_zones = None
_kudos = None
_photos = None
#_gear = None

TYPES = (RIDE, RUN, SWIM, HIKE, WALK, NORDICSKI, ALPINESKI, BACKCOUNTRYSKI,
Expand Down Expand Up @@ -523,6 +587,29 @@ def zones(self):
self._zones = self.bind_client.get_activity_zones(self.id)
return self._zones

@property
def kudos(self):
"""
:class:`list` of :class:`stravalib.model.ActivityKudos` objects for this activity.
"""
if self._kudos is None:
self.assert_bind_client()
self._kudos = self.bind_client.get_activity_kudos(self.id)
return self._kudos

@property
def photos(self):
"""
:class:`list` of :class:`stravalib.model.ActivityPhoto` objects for this activity.
"""
if self._photos is None:
if self.photo_count > 0:
self.assert_bind_client()
self._photos = self.bind_client.get_activity_photos(self.id)
else:
self._photos = []
return self._photos

class SegmentLeaderboardEntry(BoundEntity):
"""
Represents a single entry on a segment leaderboard.
Expand Down
82 changes: 56 additions & 26 deletions stravalib/tests/functional/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@
import datetime

class ClientTest(FunctionalTestBase):

def test_get_activity(self):
""" Test basic activity fetching. """
activity = self.client.get_activity(96089609)
self.assertEquals('El Dorado County, CA, USA', activity.location_city)

self.assertIsInstance(activity.start_latlng, attributes.LatLon)
self.assertAlmostEquals(-120.4357631, activity.start_latlng.lon, places=2)
self.assertAlmostEquals(38.74263759999999, activity.start_latlng.lat, places=2)

self.assertIsInstance(activity.map, model.Map)

self.assertIsInstance(activity.athlete, model.Athlete)
self.assertEquals(1513, activity.athlete.id)

#self.assertAlmostEqual(first, second, places, msg, delta)
# Ensure that iw as read in with correct units
self.assertEquals(22.5308, float(uh.kilometers(activity.distance)))
Expand All @@ -33,12 +33,12 @@ def test_get_activity_zones(self):
print zones
self.assertEquals(1, len(zones))
self.assertIsInstance(zones[0], model.PaceActivityZone)

# Indirectly
activity = self.client.get_activity(99895560)
self.assertEquals(len(zones), len(activity.zones))
self.assertEquals(zones[0].score, activity.zones[0].score)

def test_activity_comments(self):
"""
Test loading comments for already-loaded activity.
Expand All @@ -48,37 +48,66 @@ def test_activity_comments(self):
comments= list(activity.comments)
self.assertEquals(3, len(comments))
self.assertEquals("I love Gordo's. I've been eating there for 20 years!", comments[0].text)


def test_activity_photos(self):
"""
Test photos on activity
"""
activity = self.client.get_activity(152668627)
self.assertTrue(activity.photo_count > 0)
photos = list(activity.photos)
self.assertEqual(len(photos), 1)
self.assertEqual(len(photos), activity.photo_count)
self.assertIsInstance(photos[0], model.ActivityPhoto)

def test_activity_kudos(self):
"""
Test kudos on activity
"""
activity = self.client.get_activity(152668627)
self.assertTrue(activity.kudos_count > 0)
kudos = list(activity.kudos)
self.assertGreater(len(kudos), 6)
self.assertEqual(len(kudos), activity.kudos_count)
self.assertIsInstance(kudos[0], model.ActivityKudos )

def test_get_curr_athlete(self):
athlete = self.client.get_athlete()

# Just some basic sanity checks here
self.assertEquals('Jeff', athlete.firstname)
self.assertEquals('Remer', athlete.lastname)
self.assertEquals(3, len(athlete.clubs))
self.assertEquals('Team Roaring Mouse', athlete.clubs[0].name)

self.assertEquals(1, len(athlete.shoes))
print athlete.shoes

self.assertIsInstance(athlete.shoes[0], model.Shoe)
self.assertIsInstance(athlete.clubs[0], model.Club)

self.assertIsInstance(athlete.bikes[0], model.Bike)


friends = list(athlete.friends)
self.assertGreater(len(friends), 1)
self.assertIsInstance(friends[0], model.Athlete)

followers = list(athlete.followers)
self.assertGreater(len(followers), 1)
self.assertIsInstance(followers[0], model.Athlete)

def test_get_athlete_clubs(self):
clubs = self.client.get_athlete_clubs()
self.assertEquals(3, len(clubs))
self.assertEquals('Team Roaring Mouse', clubs[0].name)
self.assertEquals('Team Strava Cycling', clubs[1].name)
self.assertEquals('Team Strava Cyclocross', clubs[2].name)

clubs_indirect = self.client.get_athlete().clubs
self.assertEquals(3, len(clubs_indirect))
self.assertEquals(clubs[0].name, clubs_indirect[0].name)
self.assertEquals(clubs[1].name, clubs_indirect[1].name)
self.assertEquals(clubs[2].name, clubs_indirect[2].name)



def test_get_gear(self):
g = self.client.get_gear("g69911")
self.assertTrue(float(g.distance) >= 3264.67)
Expand All @@ -96,38 +125,39 @@ def test_get_segment_leaderboard(self):
print(lb.entry_count)
for i,e in enumerate(lb):
print '{0}: {1}'.format(i, e)

self.assertEquals(15, len(lb.entries)) # 10 top results, 5 bottom results
self.assertIsInstance(lb.entries[0], model.SegmentLeaderboardEntry)
self.assertEquals(1, lb.entries[0].rank)
self.assertTrue(lb.effort_count > 8000) # At time of writing 8206

# Check the relationships
athlete = lb[0].athlete
print(athlete)
self.assertEquals(lb[0].athlete_name, "{0} {1}".format(athlete.firstname, athlete.lastname))

effort = lb[0].effort
print effort
self.assertIsInstance(effort, model.SegmentEffort)
self.assertEquals('Hawk Hill', effort.name)

activity = lb[0].activity
self.assertIsInstance(activity, model.Activity)
# Can't assert much since #1 ranked activity will likely change in the future.

def test_get_segment(self):
segment = self.client.get_segment(229781)
self.assertIsInstance(segment, model.Segment)
print segment
self.assertEquals('Hawk Hill', segment.name)
self.assertAlmostEqual(2.68, float(uh.kilometers(segment.distance)), places=2)

# Fetch leaderboard
lb = segment.leaderboard
self.assertEquals(15, len(lb)) # 10 top results, 5 bottom results

def test_get_segment_efforts(self):
# test with string
efforts = self.client.get_segment_efforts(4357415,
start_date_local = "2012-12-23T00:00:00Z",
end_date_local = "2012-12-23T11:00:00Z",)
Expand All @@ -145,6 +175,7 @@ def test_get_segment_efforts(self):

self.assertGreater(i, 2)

# also test with datetime object
start_date = datetime.datetime(2012, 12, 31, 6, 0)
end_date = start_date + datetime.timedelta(hours=12)
efforts = self.client.get_segment_efforts(4357415,
Expand All @@ -164,18 +195,17 @@ def test_get_segment_efforts(self):

self.assertGreater(i, 2)


def test_segment_explorer(self):
bounds = (37.821362,-122.505373,37.842038,-122.465977)
results = self.client.explore_segments(bounds)

# This might be brittle
self.assertEquals('Hawk Hill', results[0].name)

# Fetch full segment
segment = results[0].segment
self.assertEquals(results[0].name, segment.name)

# For some reason these don't follow the simple math rules one might expect (so we round to int)
self.assertAlmostEqual(results[0].elev_difference, segment.elevation_high - segment.elevation_low, places=0)

0 comments on commit 50457fe

Please sign in to comment.