nike_v2_neura 0.3.7
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.
- data/.gitignore +19 -0
- data/Gemfile +2 -0
- data/README.md +100 -0
- data/Rakefile +13 -0
- data/lib/ext/core_ext.rb +18 -0
- data/lib/nike_v2.rb +33 -0
- data/lib/nike_v2/activities.rb +102 -0
- data/lib/nike_v2/activity.rb +73 -0
- data/lib/nike_v2/base.rb +17 -0
- data/lib/nike_v2/experience_type.rb +27 -0
- data/lib/nike_v2/gps_data.rb +17 -0
- data/lib/nike_v2/metric.rb +48 -0
- data/lib/nike_v2/metrics.rb +45 -0
- data/lib/nike_v2/person.rb +18 -0
- data/lib/nike_v2/resource.rb +69 -0
- data/lib/nike_v2/summary.rb +40 -0
- data/lib/nike_v2/version.rb +3 -0
- data/nike_v2.gemspec +28 -0
- data/spec/factories/activity_factory.rb +7 -0
- data/spec/factories/person_factory.rb +6 -0
- data/spec/fixtures/nike_api_cassettes/activities.yml +370 -0
- data/spec/fixtures/nike_api_cassettes/activity.yml +153 -0
- data/spec/fixtures/nike_api_cassettes/gps_data.yml +36 -0
- data/spec/fixtures/nike_api_cassettes/summary.yml +36 -0
- data/spec/nike_v2/activites_spec.rb +38 -0
- data/spec/nike_v2/activity_spec.rb +24 -0
- data/spec/nike_v2/base_spec.rb +11 -0
- data/spec/nike_v2/gps_data_spec.rb +15 -0
- data/spec/nike_v2/metric_spec.rb +29 -0
- data/spec/nike_v2/metrics_spec.rb +22 -0
- data/spec/nike_v2/person_spec.rb +22 -0
- data/spec/nike_v2/resource_spec.rb +13 -0
- data/spec/nike_v2/summary_spec.rb +16 -0
- data/spec/spec_helper.rb +16 -0
- metadata +240 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
NikeV2
|
2
|
+
====================
|
3
|
+
|
4
|
+

|
5
|
+
|
6
|
+
Learn more about the Nike+ API at [https://developer.nike.com/](https://developer.nike.com/).
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
``` bash
|
10
|
+
gem install nike_v2
|
11
|
+
```
|
12
|
+
|
13
|
+
### Comming Soon
|
14
|
+
Once Nike releases the OAuth api this gem will include the ability to fetch the access token too
|
15
|
+
|
16
|
+
## Examples
|
17
|
+
|
18
|
+
In order to utilize the Nike+ API in its current state, you'll need to do the following:
|
19
|
+
* Sign up for a Nike+ account at [http://nikeplus.nike.com/plus/](http://nikeplus.nike.com/plus/)
|
20
|
+
* Log into the developer portal and generate an access token at [https://developer.nike.com/](https://developer.nike.com/)
|
21
|
+
|
22
|
+
``` ruby
|
23
|
+
require 'nike_v2'
|
24
|
+
|
25
|
+
# Initialize a person
|
26
|
+
person = NikeV2::Person.new(access_token: 'a1b2c3d4')
|
27
|
+
# Fetch persons summary
|
28
|
+
person.summary
|
29
|
+
# Fetch a persons activities
|
30
|
+
person.activities
|
31
|
+
# Load more data for an activity
|
32
|
+
activity = person.activities.first
|
33
|
+
activity.fetch_data
|
34
|
+
# Fetch GPS Data for an activity
|
35
|
+
activity.gps_data
|
36
|
+
```
|
37
|
+
|
38
|
+
The activities api allows you to directly pass arguements to the Nike+ api. It also allows you to prefetch the metrics for each activity returned
|
39
|
+
``` ruby
|
40
|
+
#fetch 99 activities
|
41
|
+
person.activities(:count => 99)
|
42
|
+
#prefetch the metrics for activities
|
43
|
+
person.activities(:build_metrics => true)
|
44
|
+
```
|
45
|
+
|
46
|
+
We also smart load the metrics for activities now so you don't have to explicity load them
|
47
|
+
``` ruby
|
48
|
+
person.activities.total_fuel #fetches the metrics if they aren't already loaded
|
49
|
+
=> 394
|
50
|
+
```
|
51
|
+
|
52
|
+
As of version 0.3.0 you can cache calls to the Nike+ V2 api. We use the ApiCache (https://github.com/mloughran/api_cache) gem for this and all options in cache directive are passed to the api_cache config. Setting config.cache false disables the cache
|
53
|
+
``` ruby
|
54
|
+
# config/initializers/nike_v2.rb
|
55
|
+
NikeV2.configure do |config|
|
56
|
+
config.cache = {
|
57
|
+
:cache => 3600
|
58
|
+
}
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
Possible options:
|
63
|
+
```
|
64
|
+
{
|
65
|
+
:cache => 600, # 10 minutes After this time fetch new data
|
66
|
+
:valid => 86400, # 1 day Maximum time to use old data
|
67
|
+
# :forever is a valid option
|
68
|
+
:period => 60, # 1 minute Maximum frequency to call API
|
69
|
+
:timeout => 5 # 5 seconds API response timeout
|
70
|
+
:fail => # Value returned instead of exception on failure
|
71
|
+
}
|
72
|
+
```
|
73
|
+
## Making it Better
|
74
|
+
|
75
|
+
* Fork the project.
|
76
|
+
* Make your feature addition or bug fix in a new topic branch within your repo.
|
77
|
+
* Add specs for any new or modified functionality.
|
78
|
+
* Commit and push your changes to Github
|
79
|
+
* Send a pull request
|
80
|
+
|
81
|
+
## Inspiration
|
82
|
+
I used Kevin Thompson's NikeApi gem as inspiration. Find it here: https://github.com/kevinthompson/nike_api
|
83
|
+
|
84
|
+
|
85
|
+
## Copyright
|
86
|
+
|
87
|
+
Copyright (c) 2013 SmashTank Apps, LLC.
|
88
|
+
|
89
|
+
This program is free software: you can redistribute it and/or modify
|
90
|
+
it under the terms of the GNU General Public License as published by
|
91
|
+
the Free Software Foundation, either version 3 of the License, or
|
92
|
+
(at your option) any later version.
|
93
|
+
|
94
|
+
This program is distributed in the hope that it will be useful,
|
95
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
96
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
97
|
+
GNU General Public License for more details.
|
98
|
+
|
99
|
+
You should have received a copy of the GNU General Public License
|
100
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
RSpec::Core::RakeTask.new(:spec)
|
3
|
+
task default: :spec
|
4
|
+
|
5
|
+
require 'rdoc/task'
|
6
|
+
Rake::RDocTask.new do |rdoc|
|
7
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
8
|
+
|
9
|
+
rdoc.rdoc_dir = 'rdoc'
|
10
|
+
rdoc.title = "nike_v2 #{version}"
|
11
|
+
rdoc.rdoc_files.include('README*')
|
12
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
13
|
+
end
|
data/lib/ext/core_ext.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
class String
|
2
|
+
def underscore
|
3
|
+
self.gsub(/::/, '/').
|
4
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
5
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
6
|
+
tr("-", "_").
|
7
|
+
downcase
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Range
|
12
|
+
def intersection(other)
|
13
|
+
raise ArgumentError, 'value must be a Range' unless other.kind_of?(Range)
|
14
|
+
return nil if (self.max < other.begin or other.max < self.begin)
|
15
|
+
[self.begin, other.begin].max..[self.max, other.max].min
|
16
|
+
end
|
17
|
+
alias_method :&, :intersection
|
18
|
+
end
|
data/lib/nike_v2.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'alchemist'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'ext/core_ext'
|
4
|
+
require 'tzinfo'
|
5
|
+
|
6
|
+
module NikeV2
|
7
|
+
def self.configuration
|
8
|
+
@configuration ||= Configuration.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.configure
|
12
|
+
yield(configuration) if block_given?
|
13
|
+
end
|
14
|
+
|
15
|
+
class Configuration
|
16
|
+
attr_accessor :cache
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@cache = false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'nike_v2/base'
|
25
|
+
require 'nike_v2/metric'
|
26
|
+
require 'nike_v2/metrics'
|
27
|
+
require 'nike_v2/experience_type'
|
28
|
+
require 'nike_v2/resource'
|
29
|
+
require 'nike_v2/person'
|
30
|
+
require 'nike_v2/activity'
|
31
|
+
require 'nike_v2/activities'
|
32
|
+
require 'nike_v2/summary'
|
33
|
+
require 'nike_v2/gps_data'
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'enumerator'
|
2
|
+
module NikeV2
|
3
|
+
class Activities < Resource
|
4
|
+
include Enumerable
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
API_ARGS = [:offset, :count, :start_date, :end_date]
|
8
|
+
|
9
|
+
def_delegators :@activities_array, :[]=, :<<, :[], :count, :length, :each, :first, :last, :collect
|
10
|
+
def_delegator :@person, :access_token
|
11
|
+
|
12
|
+
API_URL = '/me/sport/activities'
|
13
|
+
|
14
|
+
Metrics::METRIC_TYPES.each do |type|
|
15
|
+
self.class_eval do
|
16
|
+
|
17
|
+
method_var_name = 'total_' + type.downcase
|
18
|
+
instance_variable_set('@' + method_var_name, 0.00)
|
19
|
+
define_method(method_var_name){ ivar = instance_variable_get('@' + method_var_name); ivar ||= sum_of(method_var_name)}
|
20
|
+
|
21
|
+
|
22
|
+
define_method("total_#{type.downcase}_during") do |start_date, end_date, convert = false|
|
23
|
+
@activities_array.reject{|a| ((a.started_at..a.ended_at) & (start_date..end_date)).nil?}.collect{|a| a.send("total_#{type.downcase}_during", start_date, end_date, convert)}.inject(:+)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(attributes = {})
|
29
|
+
raise "#{self.class} requires a person." unless attributes.keys.include?(:person)
|
30
|
+
@build_metrics = attributes.delete(:build_metrics) || false
|
31
|
+
api_args = extract_api_args(attributes)
|
32
|
+
set_attributes(attributes)
|
33
|
+
@activities_array = []
|
34
|
+
|
35
|
+
|
36
|
+
#TODO: make it pass blocks
|
37
|
+
activities = fetch_data(api_args)
|
38
|
+
if !activities.nil? && !activities['data'].nil?
|
39
|
+
build_activities(activities.delete('data'))
|
40
|
+
super(activities)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def fetch_more
|
45
|
+
unless self.paging['next'].nil? || self.paging['next'] == ''
|
46
|
+
fetch_and_build_activities
|
47
|
+
end
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def fetch_all
|
52
|
+
until (self.paging['next'].nil? || self.paging['next'] == '') do
|
53
|
+
fetch_and_build_activities
|
54
|
+
end
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def paging
|
59
|
+
@paging ||= ''
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
|
65
|
+
def sum_of(method_var_name)
|
66
|
+
self.collect(&:"#{method_var_name}").inject(:+) || 0.00
|
67
|
+
end
|
68
|
+
|
69
|
+
def build_activities(data)
|
70
|
+
if data
|
71
|
+
data.each do |activity|
|
72
|
+
self << NikeV2::Activity.new({:person => self}.merge(activity))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
if @build_metrics
|
76
|
+
self.collect(&:load_data)
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
def extract_api_args(args)
|
82
|
+
args.inject({}){|h,a| h[camelize_word(a.first)] = a.last if API_ARGS.include?(a.first); h}
|
83
|
+
end
|
84
|
+
|
85
|
+
def camelize_word(word, first_letter_in_uppercase = false)
|
86
|
+
if !!first_letter_in_uppercase
|
87
|
+
word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
88
|
+
else
|
89
|
+
(word[0].to_s.downcase + camelize_word(word, true)[1..-1]).to_sym
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def fetch_and_build_activities
|
94
|
+
url, query = self.paging['next'].match(/^(.*?)\?(.*)$/)[1,2]
|
95
|
+
query = query.split(/&/).inject({}){|h,item| k, v = item.split(/\=/); h[k] = v;h}
|
96
|
+
activities = fetch_data(query)
|
97
|
+
build_activities(activities.delete('data'))
|
98
|
+
|
99
|
+
@paging = activities.delete('paging')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'nike_v2/metrics'
|
2
|
+
module NikeV2
|
3
|
+
class Activity < Resource
|
4
|
+
extend Forwardable
|
5
|
+
def_delegator :@person, :access_token
|
6
|
+
|
7
|
+
Metrics::METRIC_TYPES.each do |type|
|
8
|
+
self.class_eval do
|
9
|
+
def_delegator :metrics, "total_#{type.downcase}"
|
10
|
+
def_delegator :metrics, type.downcase
|
11
|
+
def_delegator :metrics, "total_#{type.downcase}_during"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
API_URL = '/me/sport/activities'
|
16
|
+
|
17
|
+
def initialize(attributes = {})
|
18
|
+
raise "#{self.class} requires s person." unless attributes.keys.include?(:person)
|
19
|
+
raise "#{self.class} requires an activityId." unless attributes.keys.include?('activityId')
|
20
|
+
|
21
|
+
build_metrics(attributes)
|
22
|
+
super(attributes)
|
23
|
+
end
|
24
|
+
|
25
|
+
def tz
|
26
|
+
TZInfo::Timezone.get(self.activity_time_zone)
|
27
|
+
end
|
28
|
+
|
29
|
+
def gps_data
|
30
|
+
@gps_data ||= NikeV2::GpsData.new(:activity => self)
|
31
|
+
end
|
32
|
+
|
33
|
+
def metrics
|
34
|
+
load_data unless @metrics.is_a?(NikeV2::Metrics)
|
35
|
+
@metrics
|
36
|
+
end
|
37
|
+
|
38
|
+
def load_data
|
39
|
+
data = fetch_data
|
40
|
+
build_metrics(data)
|
41
|
+
set_attributes(data)
|
42
|
+
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def started_at
|
47
|
+
@started_at ||= Time.parse(self.start_time.gsub('Z', '-') + '00:00')
|
48
|
+
end
|
49
|
+
|
50
|
+
# some reason activities aren't always complete so we need metrics to figure out how long they are
|
51
|
+
def ended_at
|
52
|
+
@ended_at ||= self.respond_to?(:end_time) ? Time.parse(self.end_time.gsub('Z', '-') + '00:00') : started_at + (metrics.durations.to.seconds).to_f
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_tz(time)
|
56
|
+
if time.respond_to?(:strftime)
|
57
|
+
return Time.parse(time.strftime('%Y-%m-%d %H:%M:%S ' + self.tz.to_s))
|
58
|
+
else
|
59
|
+
return Time.parse(time + ' ' + self.tz.to_s)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
def api_url
|
65
|
+
API_URL + "/#{self.activity_id}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def build_metrics(data)
|
69
|
+
metrics = data.delete('metrics') || []
|
70
|
+
@metrics = metrics.empty? ? nil : NikeV2::Metrics.new(self, metrics)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/nike_v2/base.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module NikeV2
|
2
|
+
class Base
|
3
|
+
def initialize(attributes={})
|
4
|
+
@created_at = attributes.delete('created_at')
|
5
|
+
set_attributes(attributes)
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
def set_attributes(attributes)
|
10
|
+
attributes.each do |(attr, val)|
|
11
|
+
attr = attr.to_s.underscore
|
12
|
+
instance_variable_set("@#{attr}", val)
|
13
|
+
instance_eval "def #{attr}() @#{attr} end"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module NikeV2
|
2
|
+
class ExperienceType
|
3
|
+
TO_S_MAP = {
|
4
|
+
'fuelband' => 'Fuel Band',
|
5
|
+
'npluskinecttrg' => 'Nike+ Kinect',
|
6
|
+
'running' => 'Running'
|
7
|
+
}
|
8
|
+
|
9
|
+
TO_KEY_MAP = TO_S_MAP.invert
|
10
|
+
|
11
|
+
def initialize(code)
|
12
|
+
@code = code
|
13
|
+
end
|
14
|
+
|
15
|
+
def code
|
16
|
+
@code
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
TO_S_MAP[code.downcase]
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.code_from_string(string)
|
24
|
+
TO_KEY_MAP[string]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module NikeV2
|
2
|
+
class GpsData < Resource
|
3
|
+
extend Forwardable
|
4
|
+
def_delegator :@activity, :access_token
|
5
|
+
|
6
|
+
def initialize(attributes = {})
|
7
|
+
raise "#{self.class} requires an activity." unless attributes.keys.include?(:activity)
|
8
|
+
set_attributes(attributes)
|
9
|
+
super(fetch_data)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def api_url
|
14
|
+
self.activity.send(:api_url) + '/gps'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module NikeV2
|
2
|
+
class Metric
|
3
|
+
|
4
|
+
def initialize(activity, data)
|
5
|
+
@activity = activity
|
6
|
+
@unit = data['intervalUnit']
|
7
|
+
@type = data ['metricType']
|
8
|
+
@values = data['values'].collect(&:to_f)
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
def type
|
13
|
+
@type
|
14
|
+
end
|
15
|
+
|
16
|
+
def values
|
17
|
+
@values
|
18
|
+
end
|
19
|
+
|
20
|
+
def total
|
21
|
+
@total ||= values.inject(:+)
|
22
|
+
end
|
23
|
+
|
24
|
+
def total_during(start, stop, convert_to_local = false)
|
25
|
+
if convert_to_local
|
26
|
+
start = @activity.to_tz(start)
|
27
|
+
stop = @activity.to_tz(stop)
|
28
|
+
end
|
29
|
+
during(start, stop).collect(&:to_f).inject(:+) rescue 0
|
30
|
+
end
|
31
|
+
|
32
|
+
def during(start, stop)
|
33
|
+
start_point = time_to_index(start)
|
34
|
+
duration = time_to_index(stop) - start_point
|
35
|
+
@values[start_point, duration]
|
36
|
+
end
|
37
|
+
|
38
|
+
def duration
|
39
|
+
@values.length.send(@unit.downcase)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def time_to_index(time)
|
44
|
+
difference = time.to_i - @activity.started_at.to_i
|
45
|
+
difference.s.to.send(@unit.downcase).to_s.to_i
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|