strava 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +6 -0
- data/.yardopts +1 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +302 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/strava.rb +79 -0
- data/lib/strava/activity.rb +224 -0
- data/lib/strava/adapters/httparty_adapter.rb +9 -0
- data/lib/strava/athlete.rb +378 -0
- data/lib/strava/base.rb +86 -0
- data/lib/strava/client.rb +65 -0
- data/lib/strava/club.rb +164 -0
- data/lib/strava/club_announcement.rb +28 -0
- data/lib/strava/comment.rb +36 -0
- data/lib/strava/error.rb +15 -0
- data/lib/strava/gear.rb +41 -0
- data/lib/strava/group_event.rb +62 -0
- data/lib/strava/lap.rb +39 -0
- data/lib/strava/leaderboard.rb +57 -0
- data/lib/strava/leaderboard_entry.rb +48 -0
- data/lib/strava/photo.rb +50 -0
- data/lib/strava/route.rb +51 -0
- data/lib/strava/running_race.rb +50 -0
- data/lib/strava/segment.rb +100 -0
- data/lib/strava/segment_effort.rb +45 -0
- data/lib/strava/stream.rb +33 -0
- data/lib/strava/stream_set.rb +62 -0
- data/lib/strava/usage.rb +38 -0
- data/lib/strava/version.rb +4 -0
- data/strava.gemspec +29 -0
- metadata +135 -0
data/lib/strava/base.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
module Strava
|
2
|
+
# Base class for Strava objects.
|
3
|
+
# Handles setting up the object, mainly data and a client.
|
4
|
+
#
|
5
|
+
# @abstract
|
6
|
+
class Base
|
7
|
+
attr_reader :response, :client, :id
|
8
|
+
|
9
|
+
def initialize(data, client: nil, token: nil, **opts)
|
10
|
+
raise 'missing client or access token' unless (client || token)
|
11
|
+
@client = client || Client.new(token)
|
12
|
+
if data.is_a?(Hash)
|
13
|
+
@id = data['id']
|
14
|
+
set_ivars
|
15
|
+
update(data, **opts)
|
16
|
+
else
|
17
|
+
@id = data
|
18
|
+
set_ivars
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Parse incoming data.
|
23
|
+
# Should be defined by subclasses.
|
24
|
+
#
|
25
|
+
# @abstract
|
26
|
+
def update(data, **opts)
|
27
|
+
@response = data
|
28
|
+
@resource_state = data['resource_state']
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
# Set up instance variables upon instantiation.
|
33
|
+
# Should be defined by subclasses.
|
34
|
+
# May not always be necessary.
|
35
|
+
#
|
36
|
+
# @abstract
|
37
|
+
# @return [void]
|
38
|
+
private def set_ivars
|
39
|
+
# this should be defined by subclasses
|
40
|
+
end
|
41
|
+
|
42
|
+
private def parse_data(existing, data, klass: nil, **opts)
|
43
|
+
existing ||= {}
|
44
|
+
case data
|
45
|
+
when [], {}
|
46
|
+
[]
|
47
|
+
when Array
|
48
|
+
data.map do |hash|
|
49
|
+
current = existing[hash['id']]
|
50
|
+
if current
|
51
|
+
current.send(:update, hash, **opts)
|
52
|
+
else
|
53
|
+
current = klass.new(hash, **opts)
|
54
|
+
existing[current.id] = current
|
55
|
+
end
|
56
|
+
existing[current.id]
|
57
|
+
end
|
58
|
+
when Hash
|
59
|
+
existing[data['id']] = klass.new(data, **opts)
|
60
|
+
else
|
61
|
+
# raise
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def resource_state
|
66
|
+
self.class.resource_states[@resource_state]
|
67
|
+
end
|
68
|
+
|
69
|
+
def summary?
|
70
|
+
@resource_state == 2
|
71
|
+
end
|
72
|
+
|
73
|
+
def detailed?
|
74
|
+
@resource_state == 3
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.resource_states
|
78
|
+
@resource_states ||= {
|
79
|
+
1 => 'meta',
|
80
|
+
2 => 'summary',
|
81
|
+
3 => 'detailed',
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
module Strava
|
3
|
+
class Client
|
4
|
+
attr_reader :token
|
5
|
+
# @return [Usage] Information on API quota usage
|
6
|
+
attr_reader :usage
|
7
|
+
BASE_URL = 'https://www.strava.com/api/v3/' # can be overridden for individual requests
|
8
|
+
|
9
|
+
def initialize(token)
|
10
|
+
@token = token
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(path, **params)
|
14
|
+
make_request(:get, path, **params)
|
15
|
+
end
|
16
|
+
|
17
|
+
def post(path, **params)
|
18
|
+
make_request(:post, path, **params)
|
19
|
+
end
|
20
|
+
|
21
|
+
def put(path, **params)
|
22
|
+
make_request(:put, path, **params)
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete(path, **params)
|
26
|
+
make_request(:delete, path, **params)
|
27
|
+
end
|
28
|
+
|
29
|
+
def make_request(verb, path, **params)
|
30
|
+
puts (params[:host] || BASE_URL) + path
|
31
|
+
handle_params(params)
|
32
|
+
res = HTTParty.send(verb, (params.delete(:host) || BASE_URL) + path, query: params)
|
33
|
+
check_for_error(res)
|
34
|
+
res
|
35
|
+
end
|
36
|
+
|
37
|
+
def handle_params(params)
|
38
|
+
if @token
|
39
|
+
params.merge!(access_token: @token)
|
40
|
+
else
|
41
|
+
params.merge!(client_id: Strava.client_id, client_secret: Strava.secret)
|
42
|
+
end
|
43
|
+
params.reverse_each { |k, v| params.delete(k) if v.nil? }
|
44
|
+
end
|
45
|
+
|
46
|
+
def check_for_error(response)
|
47
|
+
@usage = Usage.new(response.headers['X-Ratelimit-Limit'], response.headers['X-Ratelimit-Usage'])
|
48
|
+
case response.code
|
49
|
+
when 401, 403
|
50
|
+
raise Strava::AccessError.new(response.to_h)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
## non athlete calls
|
56
|
+
def list_races(year = Time.now.year)
|
57
|
+
RunningRace.list_races(self, year)
|
58
|
+
end
|
59
|
+
|
60
|
+
def segment_explorer(bounds = '37.821362,-122.505373,37.842038,-122.465977')
|
61
|
+
Segment.explorer(self, bounds)
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
data/lib/strava/club.rb
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
module Strava
|
2
|
+
# Clubs represent groups of athletes on Strava. They can be public or private.
|
3
|
+
# Clubs have both summary and detailed representations.
|
4
|
+
#
|
5
|
+
# @see https://strava.github.io/api/v3/clubs/ Strava Docs - Clubs
|
6
|
+
class Club < Base
|
7
|
+
|
8
|
+
# Set up instance variables upon instantiation.
|
9
|
+
#
|
10
|
+
# @abstract
|
11
|
+
# @return [void]
|
12
|
+
private def set_ivars
|
13
|
+
@activities = {}
|
14
|
+
@group_events = {}
|
15
|
+
@announcements = []
|
16
|
+
@members = {}
|
17
|
+
@admins = []
|
18
|
+
@segment_efforts = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
# Update an existing club.
|
22
|
+
# Used by other methods in the gem.
|
23
|
+
# Should not be used directly.
|
24
|
+
#
|
25
|
+
# @param data [Hash] data to update the club with
|
26
|
+
# @return [self]
|
27
|
+
def update(data, **opts)
|
28
|
+
@response = data
|
29
|
+
@id = data["id"]
|
30
|
+
@resource_state = data['resource_state']
|
31
|
+
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def activities(per_page: nil, page: nil, before: nil)
|
36
|
+
if page || per_page || before
|
37
|
+
get_activities(per_page: per_page, page: page, before: before)
|
38
|
+
else
|
39
|
+
get_activities if @activities.empty?
|
40
|
+
@activities.values
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def group_events(per_page: nil, page: nil, before: nil)
|
45
|
+
if page || per_page || before
|
46
|
+
get_group_events(per_page: per_page, page: page, before: before)
|
47
|
+
else
|
48
|
+
get_group_events if @group_events.empty?
|
49
|
+
@group_events.values
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def announcements
|
54
|
+
get_announcements if @announcements.empty?
|
55
|
+
@announcements
|
56
|
+
end
|
57
|
+
|
58
|
+
def members(per_page: nil, page: nil)
|
59
|
+
if page || per_page
|
60
|
+
get_members(per_page: per_page, page: page)
|
61
|
+
else
|
62
|
+
get_members if @members.empty? || !@members_fetched
|
63
|
+
@members_fetched = true
|
64
|
+
@members.values
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def admins(per_page: nil, page: nil)
|
69
|
+
if page || per_page
|
70
|
+
get_admins(per_page: per_page, page: page)
|
71
|
+
else
|
72
|
+
get_admins if @admins.empty?
|
73
|
+
@admins
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# {"success"=>true, "active"=>false}
|
78
|
+
def join
|
79
|
+
res = client.post(path_join).to_h
|
80
|
+
end
|
81
|
+
|
82
|
+
# {"success"=>true, "active"=>true, "membership"=>"member"}
|
83
|
+
def leave
|
84
|
+
res = client.post(path_leave).to_h
|
85
|
+
end
|
86
|
+
|
87
|
+
def get_details
|
88
|
+
return self if detailed?
|
89
|
+
res = client.get(path_base).to_h
|
90
|
+
update(res)
|
91
|
+
end
|
92
|
+
|
93
|
+
private def get_activities(per_page: nil, page: nil, before: nil)
|
94
|
+
res = client.get(path_activities, per_page: per_page, page: page, before: before).to_a
|
95
|
+
parse_data(@activities, res, klass: Activity, client: @client)
|
96
|
+
end
|
97
|
+
|
98
|
+
private def get_group_events(per_page: nil, page: nil, before: nil)
|
99
|
+
res = client.get(path_group_events, per_page: per_page, page: page, before: before).to_a
|
100
|
+
parse_data(@group_events, res, klass: Activity, client: @client)
|
101
|
+
end
|
102
|
+
|
103
|
+
private def get_announcements
|
104
|
+
res = client.get(path_announcements).to_a
|
105
|
+
@announcements = parse_data({}, res, klass: ClubAnnouncement, client: @client)
|
106
|
+
end
|
107
|
+
|
108
|
+
private def get_members
|
109
|
+
res = client.get(path_members).to_a
|
110
|
+
parse_data(@members, res, klass: Athlete, client: @client)
|
111
|
+
end
|
112
|
+
|
113
|
+
private def get_admins
|
114
|
+
res = client.get(path_admins).to_a
|
115
|
+
@admins = parse_data(@members, res, klass: Athlete, client: @client)
|
116
|
+
end
|
117
|
+
|
118
|
+
private def path_base
|
119
|
+
"clubs/#{id}"
|
120
|
+
end
|
121
|
+
|
122
|
+
private def path_activities
|
123
|
+
"#{path_base}/activities"
|
124
|
+
end
|
125
|
+
|
126
|
+
private def path_group_events
|
127
|
+
"#{path_base}/group_events"
|
128
|
+
end
|
129
|
+
|
130
|
+
private def path_announcements
|
131
|
+
"#{path_base}/announcements"
|
132
|
+
end
|
133
|
+
|
134
|
+
private def path_members
|
135
|
+
"#{path_base}/members"
|
136
|
+
end
|
137
|
+
|
138
|
+
private def path_admins
|
139
|
+
"#{path_base}/admins"
|
140
|
+
end
|
141
|
+
|
142
|
+
private def path_join
|
143
|
+
"#{path_base}/join"
|
144
|
+
end
|
145
|
+
|
146
|
+
private def path_leave
|
147
|
+
"#{path_base}/leave"
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
__END__
|
154
|
+
|
155
|
+
ca = Strava::Athlete.current_athlete;
|
156
|
+
club = ca.clubs.last
|
157
|
+
club.admins
|
158
|
+
club.members
|
159
|
+
club.get_details
|
160
|
+
club.activities
|
161
|
+
club.group_events
|
162
|
+
club.announcements
|
163
|
+
club.leave
|
164
|
+
club.join
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Strava
|
2
|
+
# Class to represent Strava Club Announcement
|
3
|
+
# https://strava.github.io/api/v3/activities/
|
4
|
+
class ClubAnnouncement < Base
|
5
|
+
|
6
|
+
def update(data, **opts)
|
7
|
+
@response = data
|
8
|
+
@id = data['id']
|
9
|
+
@resource_state = data['resource_state']
|
10
|
+
|
11
|
+
@message = data['message']
|
12
|
+
@created_at = data['created_at']
|
13
|
+
@club_id = data['club_id']
|
14
|
+
@athlete = Athlete.new(data['athlete'], client: @client)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
__END__
|
21
|
+
|
22
|
+
ca = Strava::Athlete.current_athlete;
|
23
|
+
ca.activities;
|
24
|
+
ca.activities(page: 2);
|
25
|
+
ca.activities(page: 3);
|
26
|
+
ca.activities(page: 4);
|
27
|
+
act = ca.activities.detect{|act| act.response['comment_count'] > 0 && act.response['kudos_count'] > 0 }
|
28
|
+
act.comments
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Strava
|
2
|
+
# Class to represent Strava Activity
|
3
|
+
# https://strava.github.io/api/v3/activities/
|
4
|
+
class Comment < Base
|
5
|
+
|
6
|
+
attr_reader :activity_id
|
7
|
+
|
8
|
+
def update(data, **opts)
|
9
|
+
@response = data
|
10
|
+
@id = data['id']
|
11
|
+
@resource_state = data['resource_state']
|
12
|
+
|
13
|
+
@text = data['text']
|
14
|
+
@activity_id = data['activity_id']
|
15
|
+
@athlete = Athlete.new(data['athlete'], client: @client)
|
16
|
+
end
|
17
|
+
|
18
|
+
def delete
|
19
|
+
res = client.delete(path_base).to_h
|
20
|
+
end
|
21
|
+
|
22
|
+
def path_base
|
23
|
+
"activities/#{activity_id}/comments/#{id}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
__END__
|
29
|
+
|
30
|
+
ca = Strava::Athlete.current_athlete;
|
31
|
+
ca.activities;
|
32
|
+
ca.activities(page: 2);
|
33
|
+
ca.activities(page: 3);
|
34
|
+
ca.activities(page: 4);
|
35
|
+
act = ca.activities.detect{|act| act.response['comment_count'] > 0 && act.response['kudos_count'] > 0 }
|
36
|
+
act.comments
|
data/lib/strava/error.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
{"message"=>"Authorization Error", "errors"=>[{"resource"=>"AccessToken", "field"=>"write_permission", "code"=>"missing"}]}
|
2
|
+
|
3
|
+
module Strava
|
4
|
+
class Error < StandardError
|
5
|
+
attr_accessor :response, :strava_errors
|
6
|
+
end
|
7
|
+
|
8
|
+
class AccessError < Error
|
9
|
+
def initialize(response)
|
10
|
+
message = response['message']
|
11
|
+
strava_errors = response['errors']
|
12
|
+
super(message)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/strava/gear.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
module Strava
|
2
|
+
# Gear represents both shoes and bikes.
|
3
|
+
# These are returned as part of the athlete summary.
|
4
|
+
#
|
5
|
+
# @see https://strava.github.io/api/v3/gear/ Strava Gear API Docs
|
6
|
+
class Gear < Base
|
7
|
+
|
8
|
+
# Updates gear with passed data attributes.
|
9
|
+
#
|
10
|
+
# @param data [Hash] data hash containing gear data
|
11
|
+
# @return [self]
|
12
|
+
def update(data, **opts)
|
13
|
+
@response = data
|
14
|
+
@id = data['id']
|
15
|
+
@resource_state = data['resource_state']
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
# Retrieve full details for Gear object.
|
20
|
+
# Sets all data attributes on self.
|
21
|
+
#
|
22
|
+
# @return [Hash] raw API response
|
23
|
+
def get_details
|
24
|
+
return self if detailed?
|
25
|
+
res = client.get(path_base).to_h
|
26
|
+
update(res)
|
27
|
+
res
|
28
|
+
end
|
29
|
+
|
30
|
+
# URL path for Gear object.
|
31
|
+
#
|
32
|
+
# @return [String] URL path
|
33
|
+
private def path_base
|
34
|
+
"gear/#{id}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
__END__
|
40
|
+
|
41
|
+
ca = Strava::Athlete.current_athlete;
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Strava
|
2
|
+
# Group events for Strava Clubs
|
3
|
+
#
|
4
|
+
# @see http://strava.github.io/api/v3/club_group_events/ Strava Docs - Group Events
|
5
|
+
class GroupEvent < Base
|
6
|
+
|
7
|
+
def update(data, **opts)
|
8
|
+
@response = data
|
9
|
+
@id = data["id"]
|
10
|
+
@resource_state = data['resource_state']
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_details
|
14
|
+
return self if detailed?
|
15
|
+
res = client.get(path_base).to_h
|
16
|
+
update(res)
|
17
|
+
res
|
18
|
+
end
|
19
|
+
|
20
|
+
def athletes(per_page: nil, page: nil)
|
21
|
+
if page || per_page
|
22
|
+
get_athletes(per_page: per_page, page: page)
|
23
|
+
else
|
24
|
+
get_athletes if @athletes.empty?
|
25
|
+
@athletes.values
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def delete
|
30
|
+
res = client.delete(path_base).to_h
|
31
|
+
end
|
32
|
+
|
33
|
+
# {"success"=>true, "active"=>false}
|
34
|
+
def join
|
35
|
+
res = client.post(path_rsvp).to_h
|
36
|
+
end
|
37
|
+
|
38
|
+
# {"success"=>true, "active"=>true, "membership"=>"member"}
|
39
|
+
def leave
|
40
|
+
res = client.delete(path_rsvp).to_h
|
41
|
+
end
|
42
|
+
|
43
|
+
private def path_base
|
44
|
+
"group_events/#{id}"
|
45
|
+
end
|
46
|
+
|
47
|
+
private def path_rsvp
|
48
|
+
"#{path_base}/rsvps"
|
49
|
+
end
|
50
|
+
|
51
|
+
private def path_athletes
|
52
|
+
"#{path_base}/athletes"
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
__END__
|
59
|
+
|
60
|
+
ca = Strava::Athlete.current_athlete;
|
61
|
+
miz = ca.clubs.last;
|
62
|
+
miz.group_events
|