strava-api 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Steven Chanin
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,221 @@
1
+ = strava-api
2
+
3
+ Strava (http://www.strava.com/) allows access to it's data via a JSON api. This gem wraps that API an allows you to interact with Ruby classes instead.
4
+
5
+ = Installation
6
+
7
+ gem install strava-api
8
+
9
+ depending on your installation, you may need to use sudo
10
+
11
+ = Requirements
12
+
13
+ httparty [http://github.com/jnunemaker/httparty]
14
+
15
+ mocha (only if you want to run the tests) [http://github.com/floehopper/mocha]
16
+
17
+ = Usage
18
+ StravaApi is a Ruby wrapper for working with Strava API.
19
+
20
+ == Loading the StravaApi gem classes
21
+ Before you can use the gem's classes and methods, you have to load them
22
+
23
+ in a Rails app
24
+ require 'strava-api'
25
+
26
+ in a plain irb session
27
+ require 'rubygems'
28
+ require 'strava-api'
29
+
30
+ == Creating a StravaApi::Base object
31
+ In order to interact with the Strava API, you need a StravaApi::Base object
32
+
33
+ s = StravaApi::Base.new
34
+
35
+ == Clubs
36
+ The Strava API allows you to search for clubs by name,
37
+
38
+ #returns an array of StravaApi::Club objects (each object only has minimal attributes)
39
+ s.clubs("part of club name")
40
+
41
+ get full details on a club,
42
+
43
+ #returns a StravaApi::Club with full attributes
44
+ s.club_show(club_id)
45
+
46
+ and see the members of a club
47
+
48
+ #returns an array of StravaApi::Member objects
49
+ s.club_members(club_id)
50
+
51
+ Alternatively, you can work in a more object oriented way. The .clubs("part of club name") method returns an array of StravaApi::Club objects. The StravaApi::Club object makes club_show and club_members available as instance methods
52
+
53
+ my_club = s.clubs("Fred's Riders").first
54
+ my_club.show
55
+ my_club.members
56
+
57
+ == Rides
58
+ Using the API, you can search for rides using any combination of criteria
59
+
60
+ * club_id: Optional. Id of the Club for which to search for member's Rides.
61
+ * athlete_id: Optional. Id of the Athlete for which to search for Rides.
62
+ * athlete_name: Optional. Username of the Athlete for which to search for Rides.
63
+ * start_date: Optional. Day on which to start search for Rides. The date should be formatted YYYY-MM-DD. The date is the local time of when the ride started.
64
+ * end_date: Optional. Day on which to end search for Rides. The date should be formatted YYYY-MM-DD. The date is the local time of when the ride started.
65
+ * start_id: Optional. Only return Rides with an Id greater than or equal to the startId.
66
+ * offset: Optional. Rather than returning the first 50 matching rides, return the rides <offset> from the top of the results.
67
+
68
+ To get set of rides (by club),
69
+
70
+ #returns an array of up to 50 StravaApi::Ride objects (each object has minimal attributes)
71
+ s.rides(:club_id => club_id)
72
+
73
+ by athlete and start date
74
+
75
+ s.rides(:athlete_id => athlete_id, :start_date => Date.civil(2010,9,21))
76
+
77
+ etc
78
+
79
+ To see details on a particular ride
80
+
81
+ #returns a StravaApi::Ride with full attributes
82
+ s.ride_show(ride_id)
83
+
84
+ A ride is composed of a list of efforts (time, etc spent on each segment which makes up part of the ride). To see the efforts for a particular ride,
85
+
86
+ #returns an array of StravaApi::Effort objects (each object has minimal attributes)
87
+ s.ride_efforts(ride_id)
88
+
89
+ Alternatively, you can work in a more object oriented way. The .rides(:filter => value) method returns an array of StravaApi::Ride objects. The StravaApi::Ride object makes ride_show and ride_efforts available as instance methods
90
+
91
+ my_ride = s.rides(:club_id => my_club.id).first
92
+ my_ride.show
93
+ my_ride.efforts
94
+
95
+ === When more than 50 rides match your criteria
96
+ In response to a .rides call, Strava will return the first 50 rides that match whatever criteria are supplied. To see all matching rides, you will need to call .rides and also include an :offset parameter
97
+
98
+ s.rides(:club_id => club_id, :offset => 50)
99
+
100
+ In order to see all the rides, you will have to nest your .rides call inside of some sort of loop that updates offset after processing each block of rides
101
+
102
+ offset = 0
103
+ while (rides = s.rides(:club_id => club_id, :offset => offset, :start_date => Date.civil(2010,9,15))) && !rides.empty? do
104
+ #work on that set of rides
105
+ offset += rides.size
106
+ end
107
+ puts "received a total of #{offset} rides"
108
+
109
+ == Segments
110
+ Rides can be thought of as a sequence of segments (e.g. my "trip to the kitchen" is "up the stairs" + "down the hall" + "across the living room"). When rides are uploaded to Strava, it breaks them down into the segments they contain based on the route taken. Segments have names, measurements, and keep statistics about who rode them on which date, how long it took, etc.
111
+
112
+ To find a segment by name,
113
+
114
+ #returns an array of StravaApi::Segment objects (each object has minimal attributes)
115
+ s.segments("part of segment name")
116
+
117
+ to see measurements / details on a segment,
118
+
119
+ #returns a StravaApi::Segment with full attributes
120
+ s.segment_show(segment_id)
121
+
122
+ to see who rode a segment and how they did,
123
+
124
+ #returns an array of up to 50 StravaApi::Effort objects (each object has minimal attributes)
125
+ s.segment_efforts(segment_id, {:param => value})
126
+
127
+ To refine the data returned by segment_efforts, you can pass any combination of optional parameters:
128
+
129
+ * club_id: Optional. Id of the Club for which to search for member's Efforts.
130
+ * athlete_id: Optional. Id of the Athlete for which to search for Efforts.
131
+ * athlete_name: Optional. Username of the Athlete for which to search for Rides.
132
+ * start_date: Optional. Day on which to start search for Efforts. The date should be formatted YYYY-MM-DD. The date is the local time of when the effort started.
133
+ * end_date: Optional. Day on which to end search for Efforts. The date should be formatted YYYY-MM-DD. The date is the local time of when the effort started.
134
+ * start_id: Optional. Only return Effforts with an Id greater than or equal to the startId.
135
+ * best: Optional. Default is true. Whether to only show an athlete's best effort (shortest elapsed time) vs. all their efforts if there are multiple matching effort for a single athlete.
136
+ * offset: Optional. Rather than returning the first 50 matching rides, return the rides <offset> from the top of the results.
137
+
138
+ Example
139
+
140
+ #returns the rides on that segment by members of a specific club on or after 7/1/10.
141
+ #only shows each riders best time
142
+ s.segment_efforts(segment_id, :club_id => club_id, :start_date => Date.civil(2010,7,1), :best => true)
143
+
144
+ Alternatively, you can work in a more object oriented way. Given a StravaApi::Segment object returned by another method call, you can get it's full details and the efforts made on that segment using the .show and .efforts instance methods
145
+
146
+ #my_segment is a StravaApi::Segment
147
+
148
+ my_segment.show
149
+
150
+ my_segment.efforts(:athlete_id => 123)
151
+
152
+ === When more than 50 efforts match your criteria
153
+ In response to a .segment_efforts call, Strava will return the first 50 efforts that match whatever criteria are supplied. To see all matching efforts, you will need to call .segment_efforts and also include an :offset parameter.
154
+
155
+ See the instructions above for an example of how this works with .rides.
156
+
157
+ == Efforts
158
+ To get full details on an effort (i.e. specifics on how a rider did on a specific stretch of a ride), you
159
+
160
+ ##returns a StravaApi::Effort with full attributes
161
+ s.effort_show(effort_id)
162
+
163
+ If you have an efforts object (StravaApi::Effort) that has been returned by another call (for example my_ride.efforts), you can use the .show method to fill out the missing details on that effort
164
+
165
+ my_ride.efforts.first.show
166
+
167
+ == Error Handling
168
+ The StravaApi gem raises errors under various conditions.
169
+
170
+ If an error is raised, check .errors on your StravaApi::Base object to see more details
171
+
172
+ s.errors
173
+
174
+ === StravaApi::NetworkError
175
+ This is raised when the attempt to connect to Strava via httparty raises any of the following:
176
+ * HTTParty::UnsupportedFormat
177
+ * HTTParty::UnsupportedURIScheme
178
+ * HTTParty::ResponseError
179
+ * HTTParty::RedirectionTooDeep
180
+
181
+ === StravaApi::InvalidResponseError
182
+ This is raised is the call to Strava returns a result describing an error rather than the data you requested.
183
+
184
+ === StravaApi::CommandError
185
+ This is raised if your request causes Strava to return a 500 Error.
186
+ This is also raised if you attempt to run a command that lacks required parameters. For example, trying to call
187
+ .clubs("")
188
+ with an empty string or
189
+ .rides()
190
+ with no options will raise this error.
191
+
192
+ === StravaApi::InternalErrorInternalError
193
+ This is raised if you attempt to access an invalid property of a result. For example,
194
+
195
+ my_seg = s.segment_show(segment_id)
196
+ # => returns a <StravaApi::Segment>
197
+
198
+ my_seg.bogus_property
199
+
200
+ will raise a StravaApi::InternalErrorInternalError
201
+
202
+
203
+ this returns distance, maximum speed, average speed, etc, etc for that part of the ride.
204
+
205
+ = Contributors
206
+
207
+ StravaApi is maintained by {Steve Chanin}[http://devleverage.com].
208
+
209
+ == Note on Patches/Pull Requests
210
+
211
+ * Fork the project.
212
+ * Make your feature addition or bug fix.
213
+ * Add tests for it. This is important so I don't break it in a
214
+ future version unintentionally.
215
+ * Commit, do not mess with rakefile, version, or history.
216
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
217
+ * Send me a pull request. Bonus points for topic branches.
218
+
219
+ == Copyright
220
+
221
+ Copyright (c) 2010 Steven Chanin. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "strava-api"
8
+ gem.summary = %Q{Provides a Ruby interface to the Strava api}
9
+ gem.description = %Q{Strava (http://www.strava.com/) allows access to it's data via a JSON api. This gem wraps that API an allows you to interact with Ruby classes instead.}
10
+ gem.email = "schanin@devleverage.com"
11
+ gem.homepage = "http://github.com/stevenchanin/strava-api"
12
+ gem.authors = ["Steven Chanin"]
13
+ #not sure why files wasn't picking up subdirectors of lib when it seems to do so for hominid...
14
+ gem.files = FileList['{lib,test}/**/*'] + %w(CHANGELOG.rdoc init.rb LICENSE Rakefile README.rdoc) - FileList['test/*.log']
15
+ gem.add_dependency "httparty", " ~> 0.6.1"
16
+ gem.add_dependency "mocha", " ~> 0.9.8"
17
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
18
+ end
19
+ Jeweler::GemcutterTasks.new
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
22
+ end
23
+
24
+ require 'rake/testtask'
25
+ Rake::TestTask.new(:test) do |test|
26
+ test.libs << 'lib' << 'test'
27
+ test.pattern = 'test/**/test_*.rb'
28
+ test.verbose = true
29
+ end
30
+
31
+ begin
32
+ require 'rcov/rcovtask'
33
+ Rcov::RcovTask.new do |test|
34
+ test.libs << 'test'
35
+ test.pattern = 'test/**/test_*.rb'
36
+ test.verbose = true
37
+ end
38
+ rescue LoadError
39
+ task :rcov do
40
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
41
+ end
42
+ end
43
+
44
+ task :test => :check_dependencies
45
+
46
+ task :default => :test
47
+
48
+ require 'rake/rdoctask'
49
+ Rake::RDocTask.new do |rdoc|
50
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
51
+
52
+ rdoc.rdoc_dir = 'rdoc'
53
+ rdoc.title = "strava-api #{version}"
54
+ rdoc.rdoc_files.include('README*')
55
+ rdoc.rdoc_files.include('lib/**/*.rb')
56
+ end
data/lib/strava-api.rb ADDED
@@ -0,0 +1,22 @@
1
+ require 'httparty'
2
+ require 'strava-api/exceptions'
3
+
4
+ #classes used to hold results from Strava
5
+ require 'strava-api/hash_based_store'
6
+ require 'strava-api/club'
7
+ require 'strava-api/member'
8
+ require 'strava-api/bike'
9
+ require 'strava-api/ride'
10
+ require 'strava-api/segment'
11
+ require 'strava-api/effort'
12
+
13
+ module StravaApi
14
+ #everything now in independent class files
15
+ end
16
+
17
+ #classes to perform network access to Strava
18
+ require 'strava-api/clubs'
19
+ require 'strava-api/rides'
20
+ require 'strava-api/segments'
21
+ require 'strava-api/efforts'
22
+ require 'strava-api/base'
@@ -0,0 +1,37 @@
1
+ module StravaApi
2
+ class Base
3
+ include HTTParty
4
+
5
+ include StravaApi::Clubs
6
+ include StravaApi::Rides
7
+ include StravaApi::Segments
8
+ include StravaApi::Efforts
9
+
10
+ format :json
11
+ base_uri 'www.strava.com/api/v1'
12
+
13
+ attr_reader :errors
14
+
15
+ def initialize
16
+ @errors = []
17
+ end
18
+
19
+ def call(command, key, options)
20
+ begin
21
+ result = self.class.get("/#{command}", :query => options)
22
+ rescue HTTParty::UnsupportedFormat, HTTParty::UnsupportedURIScheme, HTTParty::ResponseError, HTTParty::RedirectionTooDeep
23
+ raise NetworkError.new
24
+ end
25
+
26
+ if result && result.parsed_response == "<html><body><h1>500 Internal Server Error</h1></body></html>"
27
+ @errors << "Strava returned a 500 error"
28
+ raise CommandError.new
29
+ end
30
+
31
+ @errors << result["error"] if result && result["error"]
32
+ raise InvalidResponseError.new if result.nil? || !result["error"].blank? || result[key].nil?
33
+
34
+ result
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,8 @@
1
+ module StravaApi
2
+ class Bike < HashBasedStore
3
+ ATTRIBUTE_MAP = {'name' => :name, 'id' => :id }
4
+ def initialize(connection, options = {})
5
+ super(connection, ATTRIBUTE_MAP, {}, options)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,17 @@
1
+ module StravaApi
2
+ class Club < HashBasedStore
3
+ ATTRIBUTE_MAP = {'name' => :name, 'id' => :id, 'description' => :description, 'location' => :location }
4
+ def initialize(connection, options = {})
5
+ super(connection, ATTRIBUTE_MAP, {}, options)
6
+ end
7
+
8
+ def show
9
+ self.merge(@connection.club_show(self.id))
10
+ self
11
+ end
12
+
13
+ def members
14
+ @connection.club_members(self.id)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,28 @@
1
+ module StravaApi
2
+ module Clubs
3
+ #returns all clubs, don't need an offset
4
+ def clubs(name)
5
+ raise StravaApi::CommandError if name.blank?
6
+
7
+ name = name.strip
8
+ raise StravaApi::CommandError if name.empty?
9
+
10
+ result = call("clubs", "clubs", {:name => name})
11
+
12
+ result["clubs"].collect {|item| Club.new(self, item)}
13
+ end
14
+
15
+ def club_show(id)
16
+ result = call("clubs/#{id}", "club", {})
17
+
18
+ Club.new(self, result["club"])
19
+ end
20
+
21
+ #returns all members, don't need an offset
22
+ def club_members(id)
23
+ result = call("clubs/#{id}/members", "members", {})
24
+
25
+ result["members"].collect {|item| Member.new(self, item)}
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,50 @@
1
+ module StravaApi
2
+ # {
3
+ # "id"=>688432,
4
+ # "elapsedTime"=>598,
5
+ # "segment"=>{"name"=>"Panoramic to Pan Toll","id"=>156},
6
+ #
7
+ # "athlete"=>{"name"=>"Julian Bill", "username"=>"jbill", "id"=>1139},
8
+ # "averageSpeed"=>14317.7658862876,
9
+ # "startDate"=>"2010-02-28T18:10:07Z",
10
+ # "timeZoneOffset"=>-8.0,
11
+ # "maximumSpeed"=>18894.384,
12
+ # "averageWatts"=>287.765,
13
+ # "elevationGain"=>151.408,
14
+ # "ride"=>{"name"=>"02/28/10 San Francisco, CA", "id"=>77563},
15
+ # "movingTime"=>598,
16
+ # "distance"=>2344.82,
17
+ #
18
+ # "rank" => 1
19
+ # }
20
+ class Effort < HashBasedStore
21
+ ATTRIBUTE_MAP = {
22
+ "id" => :id,
23
+ "elapsedTime" => :elapsed_time,
24
+ "segment" => :segment,
25
+
26
+ "athlete" => :athlete,
27
+ "averageSpeed" => :average_speed,
28
+ "startDate" => :start_date,
29
+ "timeZoneOffset" => :time_zone_offset,
30
+ "maximumSpeed" => :maximum_speed,
31
+ "averageWatts"=> :average_watts,
32
+ "elevationGain"=> :elevation_gain,
33
+ "ride" => :ride,
34
+ "movingTime" => :moving_time,
35
+ "distance"=> :distance,
36
+ "rank" => :rank
37
+ }
38
+
39
+ NESTED_CLASS_MAP = { :segment => Segment, :athlete => Member, :ride => Ride }
40
+
41
+ def initialize(connection, options = {})
42
+ super(connection, ATTRIBUTE_MAP, NESTED_CLASS_MAP, options)
43
+ end
44
+
45
+ def show
46
+ self.merge(@connection.effort_show(self.id))
47
+ self
48
+ end
49
+ end #class Effort
50
+ end