bridge_api 0.1.55 → 0.1.60

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +17 -0
  4. data/lib/bridge_api.rb +5 -3
  5. data/lib/bridge_api/api_array.rb +4 -2
  6. data/lib/bridge_api/client.rb +124 -56
  7. data/lib/bridge_api/client/account.rb +2 -1
  8. data/lib/bridge_api/client/affiliation.rb +13 -11
  9. data/lib/bridge_api/client/clone_object.rb +3 -3
  10. data/lib/bridge_api/client/course_template.rb +2 -0
  11. data/lib/bridge_api/client/custom_field.rb +2 -0
  12. data/lib/bridge_api/client/data_dump.rb +2 -0
  13. data/lib/bridge_api/client/enrollment.rb +2 -0
  14. data/lib/bridge_api/client/group.rb +2 -0
  15. data/lib/bridge_api/client/learner_item.rb +2 -0
  16. data/lib/bridge_api/client/live_course.rb +2 -0
  17. data/lib/bridge_api/client/live_course_enrollment.rb +2 -0
  18. data/lib/bridge_api/client/live_course_session.rb +3 -2
  19. data/lib/bridge_api/client/manager.rb +2 -0
  20. data/lib/bridge_api/client/program.rb +3 -1
  21. data/lib/bridge_api/client/program_enrollment.rb +2 -0
  22. data/lib/bridge_api/client/role.rb +2 -0
  23. data/lib/bridge_api/client/sub_account.rb +2 -0
  24. data/lib/bridge_api/client/user.rb +6 -5
  25. data/lib/bridge_api/version.rb +3 -1
  26. data/spec/bridge_api/client/account_spec.rb +6 -6
  27. data/spec/bridge_api/client/affiliations_spec.rb +3 -2
  28. data/spec/bridge_api/client/clone_object_spec.rb +8 -9
  29. data/spec/bridge_api/client/course_template_spec.rb +2 -0
  30. data/spec/bridge_api/client/custom_field_spec.rb +2 -0
  31. data/spec/bridge_api/client/data_dump_spec.rb +2 -0
  32. data/spec/bridge_api/client/enrollment_spec.rb +2 -0
  33. data/spec/bridge_api/client/group_spec.rb +2 -0
  34. data/spec/bridge_api/client/learner_items_spec.rb +2 -0
  35. data/spec/bridge_api/client/live_course_enrollments_spec.rb +2 -0
  36. data/spec/bridge_api/client/live_course_session_spec.rb +5 -3
  37. data/spec/bridge_api/client/live_course_spec.rb +2 -0
  38. data/spec/bridge_api/client/manager_spec.rb +2 -0
  39. data/spec/bridge_api/client/program_enrollment_spec.rb +2 -0
  40. data/spec/bridge_api/client/role_spec.rb +2 -0
  41. data/spec/bridge_api/client/sub_account_spec.rb +6 -4
  42. data/spec/bridge_api/client/user_spec.rb +2 -1
  43. data/spec/bridge_api/client_spec.rb +99 -5
  44. data/spec/support/fake_bridge.rb +4 -2
  45. data/spec/test_helper.rb +3 -1
  46. metadata +16 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83609f373a095db86732ec01372a9bb683d4bb0371fabf6eb1937f620420d815
4
- data.tar.gz: e661ce7bea32043846bd1e2b0f3891eb6595a2c93409a37f1dd3ddb4341f60a7
3
+ metadata.gz: ba858770698bd9b2eae79613a356e002d75b7660786e33c53d999859cf9b809b
4
+ data.tar.gz: a9e81c5c0758deab4985332dbaec93a2dcc71f931a60033897f637bff9952efd
5
5
  SHA512:
6
- metadata.gz: 68d26347ea96b47f03f1c960245741357f439b9ad81fbee453ccecb41f4fde87256c616c5772c35e4604154c48ad731220e11877e1be49ca3560518668844d1d
7
- data.tar.gz: 42dea873909be1b5758ad7a896ed396d29ae7c93d5c08dfb234d20163100557b7ba45efc2319cf5e5e8ce06b6460fefc3265161385ce9222bf34fda6a98fe824
6
+ metadata.gz: a470154415cfe7cb6b4899d4826d63dc8e0f7ad5320229c2985d079bc2fc159ce809c16ede74cb278ffefe0c13047a697f10293afd754a2d9ef8c44a01971167
7
+ data.tar.gz: db6d7da0e8afe793a2e80ddf2c45ffead4256adef5559f2f2d5c10bec578ef8ef47de3b8f4d5113187f2bbd46cf332c76ee04e514d95daa06770eb55dbc1de11
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- bridge_api (0.1.55)
4
+ bridge_api (0.1.60)
5
5
  faraday (~> 0.9.0)
6
6
  faraday_middleware (>= 0.12.2)
7
7
  footrest (>= 0.5.1)
data/README.md CHANGED
@@ -25,3 +25,20 @@ instantiating several instances of `client`, you can also supply your client
25
25
  initializer with `master_rate_limit: true`. This will use a redis
26
26
  keystore to track your rate limits across all instances of `client` and
27
27
  properly throttle your application as needed.
28
+
29
+ ## Token Pools
30
+
31
+ This supports token pooling to get around rate limiting issues in bridge (but should never be used without Core approval)
32
+ To use with Tokens:
33
+ ```
34
+ client = BridgeAPI::Client.new(tokens: ['token1', 'token2'], prefix: "https://yourdomain.bridgeapp.com")
35
+ ```
36
+ To use with API Keys:
37
+ ```
38
+ client = BridgeAPI::Client.new(api_keys: {'key1' => 'secret1', 'key2' => 'secret2'}, prefix: "https://yourdomain.bridgeapp.com")
39
+ ```
40
+
41
+ The logic will start with the first token provided, and check against the redis cache to see what its current limit
42
+ is. If its already under the limit threshold, it will rotate to the next in the pool, and perform the same check. It
43
+ will do this till it finds one that is not under the threshold, and use it. If none are found, it will sleep for the
44
+ configured time, and then try to make a call.
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bridge_api/version'
2
4
  require 'bridge_api/client'
3
5
 
4
6
  module BridgeAPI
5
7
  class << self
6
8
  require 'logger'
7
- attr_writer :enforce_rate_limits, :rate_limit_min, :rate_limits, :max_sleep_seconds, :min_sleep_seconds,
9
+ attr_writer :enforce_rate_limits, :beginning_rate_limit, :rate_limits, :max_sleep_seconds, :min_sleep_seconds,
8
10
  :logger, :master_rate_limit, :master_mutex, :rate_limit_threshold
9
11
 
10
12
  def configure
@@ -15,8 +17,8 @@ module BridgeAPI
15
17
  @enforce_rate_limits ||= false
16
18
  end
17
19
 
18
- def rate_limit_min
19
- @rate_limit_min ||= 30
20
+ def beginning_rate_limit
21
+ @beginning_rate_limit ||= 30
20
22
  end
21
23
 
22
24
  def rate_limits
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module BridgeAPI
3
4
  class ApiArray
@@ -18,7 +19,7 @@ module BridgeAPI
18
19
  @linked = {}
19
20
  @meta = {}
20
21
  @extra_meta_fields = []
21
- pattern = /.*(\/api\/.*)/
22
+ pattern = %r{.*(/api/.*)}
22
23
  path = response.env.url
23
24
  matches = pattern.match(path.to_s)
24
25
  mapping = nil
@@ -67,7 +68,6 @@ module BridgeAPI
67
68
  !@next_page.nil?
68
69
  end
69
70
 
70
-
71
71
  def next_page
72
72
  load_page(@next_page)
73
73
  end
@@ -119,8 +119,10 @@ module BridgeAPI
119
119
 
120
120
  def get_response_content(response)
121
121
  return [] unless response.body.is_a?(Hash)
122
+
122
123
  content = response.body.reject { |k, _v| @meta_fields.include?(k) || @extra_meta_fields.include?(k) }
123
124
  return content.values[0] unless content.empty?
125
+
124
126
  []
125
127
  end
126
128
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'footrest/client'
2
4
  require 'faraday'
3
5
  require 'footrest'
@@ -12,60 +14,126 @@ module BridgeAPI
12
14
  class Client < Footrest::Client
13
15
  require 'bridge_api/api_array'
14
16
 
15
- DATA_DUMP_DOWNLOAD_PATH = '/data_dumps/download'.freeze
16
- DATA_DUMP_PATH = '/data_dumps'.freeze
17
- COURSE_TEMPLATE_PATH = '/course_templates'.freeze
18
- ENROLLMENT_PATH = '/enrollments'.freeze
19
- LTI_TOOLS_PATH = '/lti_tools'.freeze
20
- PROGRAM_PATH = '/programs'.freeze
21
- PROGRAM_ENROLLMENT_PATH = '/learners'.freeze
22
- USER_PATH = '/users'.freeze
23
- GROUPS_PATH = '/groups'.freeze
24
- MANAGER_PATH = '/managers'.freeze
25
- ADMIN_PATH = '/admin'.freeze
26
- AUTHOR_PATH = '/author'.freeze
27
- LEARNER_PATH = '/learner'.freeze
28
- LEARNERS_PATH = '/learners'.freeze
29
- LEARNER_ITEMS_PATH = '/learner_items'.freeze
30
- CUSTOM_FIELD_PATH = '/custom_fields'.freeze
31
- SUB_ACCOUNT_PATH = '/sub_accounts'.freeze
32
- SUPPORT_PATH = '/support'.freeze
33
- ACCOUNT_PATH = '/accounts'.freeze
34
- CLONE_OBJECTS_PATH = '/clone_objects'.freeze
35
- API_VERSION = 1
36
- API_PATH = '/api'.freeze
37
- ROLE_PATH = '/roles'.freeze
38
- AFFILIATED_SUBACCOUNTS = '/affiliated_sub_accounts'.freeze
39
- BATCH_PATH = '/batch'.freeze
40
- LIVE_COURSES_PATH = '/live_courses'.freeze
41
- SESSIONS_PATH = '/sessions'.freeze
42
- PUBLISH_PATH = '/publish'.freeze
43
- WEB_CONFERENCE_PATH = '/web_conference'.freeze
44
- RESTORE_PATH = '/restore'.freeze
45
- DUE_DATE_PATH = '/due_date'.freeze
46
- RESET_PATH = '/reset'.freeze
47
- RESULT_MAPPING = {}
17
+ DATA_DUMP_DOWNLOAD_PATH = '/data_dumps/download'
18
+ DATA_DUMP_PATH = '/data_dumps'
19
+ COURSE_TEMPLATE_PATH = '/course_templates'
20
+ ENROLLMENT_PATH = '/enrollments'
21
+ LTI_TOOLS_PATH = '/lti_tools'
22
+ PROGRAM_PATH = '/programs'
23
+ PROGRAM_ENROLLMENT_PATH = '/learners'
24
+ USER_PATH = '/users'
25
+ GROUPS_PATH = '/groups'
26
+ MANAGER_PATH = '/managers'
27
+ ADMIN_PATH = '/admin'
28
+ AUTHOR_PATH = '/author'
29
+ LEARNER_PATH = '/learner'
30
+ LEARNERS_PATH = '/learners'
31
+ LEARNER_ITEMS_PATH = '/learner_items'
32
+ CUSTOM_FIELD_PATH = '/custom_fields'
33
+ SUB_ACCOUNT_PATH = '/sub_accounts'
34
+ SUPPORT_PATH = '/support'
35
+ ACCOUNT_PATH = '/accounts'
36
+ CLONE_OBJECTS_PATH = '/clone_objects'
37
+ API_VERSION = 1
38
+ API_PATH = '/api'
39
+ ROLE_PATH = '/roles'
40
+ AFFILIATED_SUBACCOUNTS = '/affiliated_sub_accounts'
41
+ BATCH_PATH = '/batch'
42
+ LIVE_COURSES_PATH = '/live_courses'
43
+ SESSIONS_PATH = '/sessions'
44
+ PUBLISH_PATH = '/publish'
45
+ WEB_CONFERENCE_PATH = '/web_conference'
46
+ RESTORE_PATH = '/restore'
47
+ DUE_DATE_PATH = '/due_date'
48
+ RESET_PATH = '/reset'
49
+ RESULT_MAPPING = {}
48
50
 
49
51
  Dir[File.dirname(__FILE__) + '/client/*.rb'].each do |file|
50
52
  require file
51
53
  include const_get(File.basename(file).gsub('.rb', '').split('_').map(&:capitalize).join('').to_s)
52
54
  end
53
55
 
56
+ def initialize(options = {}, &block)
57
+ if BridgeAPI.enforce_rate_limits && has_token_pool?(options)
58
+ options = initialize_from_token_pool(options)
59
+ end
60
+ super
61
+ end
62
+
54
63
  # Override Footrest request for ApiArray support
55
64
  def request(method, &block)
56
- enforce_rate_limits
65
+ if has_token_pool?(config)
66
+ (config[:api_tokens] || config[:api_keys].keys).size.times do
67
+ break unless rate_limit_reached?
68
+
69
+ rotate_token!
70
+ end
71
+ end
72
+ enforce_rate_limits if rate_limit_reached?
57
73
  response = connection.send(method, &block)
58
74
  apply_rate_limits(response)
59
75
  ApiArray.process_response(response, self, RESULT_MAPPING)
60
76
  end
61
77
 
78
+ def has_token_pool?(config)
79
+ config[:api_keys].is_a?(Hash) && config[:api_keys].keys.count >= 1 ||
80
+ config[:api_tokens].is_a?(Array) && config[:api_tokens].count >= 1
81
+ end
82
+
83
+ # Since a pool is passed in, initialize first token with the first token passed in
84
+ def initialize_from_token_pool(config)
85
+ if config[:api_keys].is_a?(Hash)
86
+ creds = config[:api_keys].first
87
+ config[:api_key] ||= creds[0]
88
+ config[:api_secret] ||= creds[1]
89
+ elsif config[:api_tokens].is_a?(Array)
90
+ config[:token] ||= config[:api_tokens].first
91
+ end
92
+ config
93
+ end
94
+
95
+ # rotates to the next token in the pool (by order in which they were provided)
96
+ def rotate_token!
97
+ BridgeAPI.master_mutex.synchronize do
98
+ old_api_key = config[:api_key]
99
+ if config[:api_keys].is_a?(Hash)
100
+ keys = config[:api_keys].keys
101
+ return if keys.count <= 1
102
+
103
+ key = get_next_key(keys, config[:api_key])
104
+ config[:api_key] = key
105
+ config[:api_secret] = config[:api_keys][key]
106
+ elsif config[:api_tokens].is_a?(Array)
107
+ keys = config[:api_tokens]
108
+ return if keys.count <= 1
109
+
110
+ token = get_next_key(keys, config[:token])
111
+ config[:token] = token
112
+ end
113
+ BridgeAPI.logger.debug('rotating API Keys')
114
+ set_connection(config)
115
+ end
116
+ end
117
+
118
+ def get_next_key(keys, current_key)
119
+ i = keys.index(current_key) || -1
120
+ i = (i + 2) > keys.count ? 0 : (i + 1)
121
+ keys[i]
122
+ end
123
+
124
+ def rate_limit_reached?
125
+ return false unless BridgeAPI.enforce_rate_limits && limit_remaining.present?
126
+
127
+ limit_remaining < BridgeAPI.rate_limit_threshold
128
+ end
129
+
62
130
  def enforce_rate_limits
63
- return unless BridgeAPI.enforce_rate_limits && limit_remaining.present?
64
- return unless limit_remaining < BridgeAPI.rate_limit_threshold
65
- tts = ((BridgeAPI.rate_limit_min - limit_remaining) / 5).ceil
66
- tts = BridgeAPI.min_sleep_seconds if tts < BridgeAPI.min_sleep_seconds
67
- tts = BridgeAPI.max_sleep_seconds if tts > BridgeAPI.max_sleep_seconds
68
- message = "Bridge API rate limit minimum #{BridgeAPI.rate_limit_min} reached for key: '#{config[:api_key]}'. "\
131
+ return unless rate_limit_reached?
132
+
133
+ tts = ((BridgeAPI.beginning_rate_limit - limit_remaining) / 5).ceil
134
+ tts = BridgeAPI.min_sleep_seconds if tts < BridgeAPI.min_sleep_seconds
135
+ tts = BridgeAPI.max_sleep_seconds if tts > BridgeAPI.max_sleep_seconds
136
+ message = "Bridge API rate limit minimum #{BridgeAPI.rate_limit_threshold} reached for key: '#{config[:api_key]}'. "\
69
137
  "Sleeping for #{tts} second(s) to catch up ~zzZZ~. "\
70
138
  "Limit Remaining: #{limit_remaining}"
71
139
  BridgeAPI.logger.debug(message)
@@ -79,6 +147,7 @@ module BridgeAPI
79
147
  def apply_rate_limits(response)
80
148
  limit = response.headers['x-rate-limit-remaining']
81
149
  return if limit.nil?
150
+
82
151
  BridgeAPI.logger.debug("BRIDGE RATE LIMIT REMAINING: #{limit}")
83
152
  self.limit_remaining = limit.to_i
84
153
  end
@@ -88,8 +157,8 @@ module BridgeAPI
88
157
  BridgeAPI.master_mutex.synchronize do
89
158
  limit = PaulWalker::RateLimit.get(config[:api_key], config[:api_key])
90
159
  if limit.nil?
91
- PaulWalker::RateLimit.add(config[:api_key], config[:api_key], 0, BridgeAPI::rate_limit_min)
92
- limit = {current: 0}.with_indifferent_access
160
+ PaulWalker::RateLimit.add(config[:api_key], config[:api_key], 0, BridgeAPI.beginning_rate_limit)
161
+ limit = { current: 0 }.with_indifferent_access
93
162
  end
94
163
  limit['current']
95
164
  end
@@ -101,7 +170,7 @@ module BridgeAPI
101
170
  def limit_remaining=(value)
102
171
  if using_master_rate_limit?
103
172
  BridgeAPI.master_mutex.synchronize do
104
- PaulWalker::RateLimit.add(config[:api_key], config[:api_key], value, BridgeAPI::rate_limit_min)
173
+ PaulWalker::RateLimit.add(config[:api_key], config[:api_key], value, BridgeAPI.beginning_rate_limit)
105
174
  end
106
175
  else
107
176
  BridgeAPI.rate_limits[config[:api_key]] = value
@@ -110,26 +179,25 @@ module BridgeAPI
110
179
 
111
180
  def set_connection(config)
112
181
  config[:logger] = config[:logging] if config[:logging]
113
- @connection = Faraday.new(url: config[:prefix]) do |faraday|
114
- faraday.request :multipart
115
- faraday.request :url_encoded
182
+ @connection = Faraday.new(url: config[:prefix]) do |faraday|
183
+ faraday.request :multipart
184
+ faraday.request :url_encoded
116
185
  if config[:logger] == true
117
186
  faraday.response :logger
118
187
  elsif config[:logger]
119
188
  faraday.use Faraday::Response::Logger, config[:logger]
120
189
  end
121
- faraday.use Footrest::FollowRedirects, limit: 5 unless config[:follow_redirects] == false
122
- faraday.adapter Faraday.default_adapter
123
- faraday.use Footrest::ParseJson, content_type: /\bjson$/
124
- faraday.use Footrest::RaiseFootrestErrors
125
- faraday.use Footrest::Pagination
126
- faraday.headers[:accept] = 'application/json'
127
- faraday.headers[:authorization] = "Bearer #{config[:token]}" if config[:token]
128
- faraday.headers[:user_agent] = 'Footrest'
190
+ faraday.use Footrest::FollowRedirects, limit: 5 unless config[:follow_redirects] == false
191
+ faraday.adapter Faraday.default_adapter
192
+ faraday.use Footrest::ParseJson, content_type: /\bjson$/
193
+ faraday.use Footrest::RaiseFootrestErrors
194
+ faraday.use Footrest::Pagination
195
+ faraday.headers[:accept] = 'application/json'
196
+ faraday.headers[:user_agent] = 'Footrest'
129
197
  if config[:api_key] && config[:api_secret]
130
- faraday.headers[:authorization] = 'Basic ' + Base64.strict_encode64("#{config[:api_key]}:#{config[:api_secret]}")
198
+ faraday.headers[:authorization] = 'Basic ' + Base64.strict_encode64("#{config[:api_key]}:#{config[:api_secret]}")
131
199
  elsif config[:token]
132
- faraday.headers[:authorization] = "Bearer #{config[:token]}"
200
+ faraday.headers[:authorization] = "Bearer #{config[:token]}"
133
201
  else
134
202
  raise 'No api authorization provided'
135
203
  end
@@ -1,7 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BridgeAPI
2
4
  class Client
3
5
  module Account
4
-
5
6
  def create_account(params = {})
6
7
  post("#{API_PATH}#{ADMIN_PATH}/sub_accounts", params)
7
8
  end
@@ -1,17 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BridgeAPI
2
- class Client
3
- module Affiliation
4
- def list_affiliations(params = {})
5
- get("#{API_PATH}#{AUTHOR_PATH}#{AFFILIATED_SUBACCOUNTS}", params)
6
- end
4
+ class Client
5
+ module Affiliation
6
+ def list_affiliations(params = {})
7
+ get("#{API_PATH}#{AUTHOR_PATH}#{AFFILIATED_SUBACCOUNTS}", params)
8
+ end
7
9
 
8
- def revoke_affiliations_batch(params = {})
9
- put("#{API_PATH}#{AUTHOR_PATH}#{AFFILIATED_SUBACCOUNTS}/revoke_batch", params)
10
- end
10
+ def revoke_affiliations_batch(params = {})
11
+ put("#{API_PATH}#{AUTHOR_PATH}#{AFFILIATED_SUBACCOUNTS}/revoke_batch", params)
12
+ end
11
13
 
12
- def share_affiliations_batch(params = {})
13
- put("#{API_PATH}#{AUTHOR_PATH}#{AFFILIATED_SUBACCOUNTS}/share_batch", params)
14
- end
14
+ def share_affiliations_batch(params = {})
15
+ put("#{API_PATH}#{AUTHOR_PATH}#{AFFILIATED_SUBACCOUNTS}/share_batch", params)
15
16
  end
16
17
  end
17
18
  end
19
+ end
@@ -1,15 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BridgeAPI
2
4
  class Client
3
5
  module CloneObject
4
-
5
6
  def clone_objects(params = {})
6
7
  post("#{API_PATH}#{ADMIN_PATH}#{CLONE_OBJECTS_PATH}", params)
7
8
  end
8
9
 
9
- def clone_object_status(id, params={})
10
+ def clone_object_status(id, params = {})
10
11
  get("#{API_PATH}#{ADMIN_PATH}#{CLONE_OBJECTS_PATH}/#{id}", params)
11
12
  end
12
-
13
13
  end
14
14
  end
15
15
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BridgeAPI
2
4
  class Client
3
5
  module CourseTemplate
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BridgeAPI
2
4
  class Client
3
5
  module CustomField
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BridgeAPI
2
4
  class Client
3
5
  module DataDump
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BridgeAPI
2
4
  class Client
3
5
  module Enrollment
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BridgeAPI
2
4
  class Client
3
5
  module Group
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BridgeAPI
2
4
  class Client
3
5
  module LearnerItem
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BridgeAPI
2
4
  class Client
3
5
  module LiveCourse
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BridgeAPI
2
4
  class Client
3
5
  module LiveCourseEnrollment
@@ -1,7 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BridgeAPI
2
4
  class Client
3
5
  module LiveCourseSession
4
-
5
6
  def get_live_course_sessions(live_course_id, params = {})
6
7
  get("#{API_PATH}#{AUTHOR_PATH}#{LIVE_COURSES_PATH}/#{live_course_id}#{SESSIONS_PATH}", params)
7
8
  end
@@ -35,4 +36,4 @@ module BridgeAPI
35
36
  end
36
37
  end
37
38
  end
38
- end
39
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BridgeAPI
2
4
  class Client
3
5
  module Manager
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BridgeAPI
2
4
  class Client
3
5
  module Program
@@ -10,4 +12,4 @@ module BridgeAPI
10
12
  end
11
13
  end
12
14
  end
13
- end
15
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BridgeAPI
2
4
  class Client
3
5
  # WARNING: the API endpoint for program enrollments is currently undocumented
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BridgeAPI
2
4
  class Client
3
5
  module Role
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BridgeAPI
2
4
  class Client
3
5
  module SubAccount
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BridgeAPI
2
4
  class Client
3
5
  module User
@@ -53,15 +55,14 @@ module BridgeAPI
53
55
  users = get_user(user_id, 'includes[]' => 'custom_fields')
54
56
  payload = []
55
57
  return payload if users.members.empty?
58
+
56
59
  existing_values = users.linked['custom_field_values']
57
60
  custom_field_values.each do |field_id, value|
58
61
  field_value_id = begin
59
62
  existing_values.find do |v|
60
- begin
61
- v['links']['custom_field']['id'] == field_id.to_s
62
- rescue StandardError
63
- false
64
- end
63
+ v['links']['custom_field']['id'] == field_id.to_s
64
+ rescue StandardError
65
+ false
65
66
  end['id']
66
67
  rescue StandardError
67
68
  nil
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BridgeAPI
2
- VERSION = '0.1.55'.freeze unless defined?(BridgeAPI::VERSION)
4
+ VERSION = '0.1.60' unless defined?(BridgeAPI::VERSION)
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  describe BridgeAPI::Client::CourseTemplate do
@@ -9,12 +11,10 @@ describe BridgeAPI::Client::CourseTemplate do
9
11
 
10
12
  it 'should create an account' do
11
13
  templates = @client.create_account(
12
- {
13
- sub_account: {
14
- locale: "en",
15
- subdomain: 'subdomain',
16
- name: 'subdomain'
17
- }
14
+ sub_account: {
15
+ locale: 'en',
16
+ subdomain: 'subdomain',
17
+ name: 'subdomain'
18
18
  }
19
19
  )
20
20
  expect(templates.status).to(eq(200))
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  describe BridgeAPI::Client::Affiliation do
@@ -6,7 +8,7 @@ describe BridgeAPI::Client::Affiliation do
6
8
  end
7
9
 
8
10
  it 'should list the affiliations' do
9
- response = @client.list_affiliations({item_id: 1, item_type: 'CourseTemple'})
11
+ response = @client.list_affiliations(item_id: 1, item_type: 'CourseTemple')
10
12
  expect(response.status).to(eq(204))
11
13
  end
12
14
 
@@ -14,5 +16,4 @@ describe BridgeAPI::Client::Affiliation do
14
16
  response = @client.share_affiliations_batch
15
17
  expect(response.status).to(eq(204))
16
18
  end
17
-
18
19
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  describe BridgeAPI::Client::CloneObject do
@@ -7,14 +9,12 @@ describe BridgeAPI::Client::CloneObject do
7
9
 
8
10
  it 'should clone an object' do
9
11
  response = @client.clone_objects(
10
- {
11
- clone_objects: [
12
- {
13
- source_type: 'CourseTemplate',
14
- source_id: 1
15
- }
16
- ]
17
- }
12
+ clone_objects: [
13
+ {
14
+ source_type: 'CourseTemplate',
15
+ source_id: 1
16
+ }
17
+ ]
18
18
  ).first
19
19
  expect(response['destination_id']).to(eq(1))
20
20
  end
@@ -23,5 +23,4 @@ describe BridgeAPI::Client::CloneObject do
23
23
  response = @client.clone_object_status(1).first
24
24
  expect(response['state']).to(eq('completed'))
25
25
  end
26
-
27
26
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  describe BridgeAPI::Client::CourseTemplate do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
  require 'byebug'
3
5
  describe BridgeAPI::Client::CustomField do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  describe BridgeAPI::Client::DataDump do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  describe BridgeAPI::Client::Enrollment do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  describe BridgeAPI::Client::Group do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  describe BridgeAPI::Client::LearnerItem do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  describe BridgeAPI::Client::LiveCourseEnrollment do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  describe BridgeAPI::Client::LiveCourseSession do
@@ -28,7 +30,7 @@ describe BridgeAPI::Client::LiveCourseSession do
28
30
  end
29
31
 
30
32
  it 'should delete a live course session in bridge' do
31
- response = @client.delete_live_course_session(1,5)
33
+ response = @client.delete_live_course_session(1, 5)
32
34
  expect(response.status).to eq(204)
33
35
  end
34
36
 
@@ -45,7 +47,7 @@ describe BridgeAPI::Client::LiveCourseSession do
45
47
  it 'should update web_conference' do
46
48
  params = {
47
49
  web_conference: {
48
- provider: "Bluejeans"
50
+ provider: 'Bluejeans'
49
51
  }
50
52
  }
51
53
  response = @client.update_web_conference(1, 5, params)
@@ -58,7 +60,7 @@ describe BridgeAPI::Client::LiveCourseSession do
58
60
  end
59
61
 
60
62
  it 'should get default web_conference config' do
61
- response = @client.get_default_web_conference({default_session: 1})
63
+ response = @client.get_default_web_conference(default_session: 1)
62
64
  expect(response.status).to eq(200)
63
65
  end
64
66
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  describe BridgeAPI::Client::LiveCourse do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  describe BridgeAPI::Client::User do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  describe BridgeAPI::Client::ProgramEnrollment do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  describe BridgeAPI::Client::Role do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  describe BridgeAPI::Client::SubAccount do
@@ -35,19 +37,19 @@ describe BridgeAPI::Client::SubAccount do
35
37
  end
36
38
 
37
39
  it 'should update the lti config' do
38
- params = {lti_tool: {consumer_key: "YoooKey", shared_secret: SecureRandom.hex(16)}}
40
+ params = { lti_tool: { consumer_key: 'YoooKey', shared_secret: SecureRandom.hex(16) } }
39
41
  response = @client.update_sub_account_lti_config(2, 15, params)
40
42
  expect(response.body['lti_tool']['id']).to(eq '20')
41
43
  end
42
44
 
43
45
  it 'should create new lti config' do
44
- params = {lti_tool: {consumer_key: "SomeKey", shared_secret: SecureRandom.hex(16)}}
45
- response = @client.create_sub_account_lti_config(2,params)
46
+ params = { lti_tool: { consumer_key: 'SomeKey', shared_secret: SecureRandom.hex(16) } }
47
+ response = @client.create_sub_account_lti_config(2, params)
46
48
  expect(response.status).to(eq 201)
47
49
  end
48
50
 
49
51
  it 'should delete a lti config' do
50
- response = @client.delete_lti_config(2,15)
52
+ response = @client.delete_lti_config(2, 15)
51
53
  expect(response.status).to(eq 204)
52
54
  end
53
55
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  describe BridgeAPI::Client::User do
@@ -37,5 +39,4 @@ describe BridgeAPI::Client::User do
37
39
  response = @client.add_user_to_role_batch(1)
38
40
  expect(response.length).to eq(7)
39
41
  end
40
-
41
42
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  describe BridgeAPI::Client do
3
4
  it 'should set the auth header to basic auth' do
@@ -12,20 +13,113 @@ describe BridgeAPI::Client do
12
13
 
13
14
  it 'should set the meta property on client' do
14
15
  client = BridgeAPI::Client.new(prefix: 'https://www.fake.com', token: 'test_token')
15
- users = client.get_all_users
16
+ users = client.get_all_users
16
17
  expect(users.meta['next']).to(eq('http://bridge.bridgeapp.com/api/author/users?after=eyJ0eXAiOiJKV1QiLCJhSDiQQ'))
17
18
  end
18
19
 
19
20
  it 'should set the linked property on client' do
20
21
  client = BridgeAPI::Client.new(prefix: 'https://www.fake.com', token: 'test_token')
21
- users = client.get_all_users
22
+ users = client.get_all_users
22
23
  expect(users.linked['custom_fields'][0]['id']).to(eq('1'))
23
24
  end
24
25
 
25
26
  it 'should enforce rate limits' do
26
- client = BridgeAPI::Client.new(prefix: 'https://www.fake.com', api_key: 'fake_token', api_secret: 'fake_secret')
27
+ client = BridgeAPI::Client.new(prefix: 'https://www.fake.com', api_key: 'fake_token', api_secret: 'fake_secret')
27
28
  BridgeAPI.enforce_rate_limits = true
28
- users = client.get_all_users
29
- users = client.get_all_users
29
+ users = client.get_all_users
30
+ users = client.get_all_users
31
+ end
32
+
33
+ describe 'initialize' do
34
+ it 'should set initial token when using a pool' do
35
+ expect_any_instance_of(BridgeAPI::Client).to receive(:initialize_from_token_pool).and_call_original
36
+ client = BridgeAPI::Client.new(prefix: 'https://www.fake.com', api_keys: { 'key1' => 'secret1', 'key2' => 'secret2' })
37
+ expect(client.config[:api_key]).to eq('key1')
38
+ expect(client.config[:api_secret]).to eq('secret1')
39
+ end
40
+
41
+ it 'should not call initialize_from_token_pool for a single token' do
42
+ expect_any_instance_of(BridgeAPI::Client).to_not receive(:initialize_from_token_pool)
43
+ BridgeAPI::Client.new(prefix: 'https://www.fake.com', api_key: 'fake_token', api_secret: 'fake_secret')
44
+ end
45
+ end
46
+
47
+ describe 'has_token_pool?' do
48
+ it 'should return true for api keys' do
49
+ config = { prefix: 'https://www.fake.com', api_keys: { 'key1' => 'secret1', 'key2' => 'secret2' } }
50
+ client = BridgeAPI::Client.new(config)
51
+ expect(client.has_token_pool?(config)).to be_truthy
52
+ end
53
+
54
+ it 'should return true for tokens' do
55
+ config = { prefix: 'https://www.fake.com', api_tokens: %w[token1 token2] }
56
+ client = BridgeAPI::Client.new(config)
57
+ expect(client.has_token_pool?(config)).to be_truthy
58
+ end
59
+
60
+ it 'should return false for single key' do
61
+ config = { prefix: 'https://www.fake.com', api_key: 'fake_token', api_secret: 'fake_secret' }
62
+ client = BridgeAPI::Client.new(config)
63
+ expect(client.has_token_pool?(config)).to be_falsey
64
+ end
65
+ end
66
+
67
+ describe 'initialize_from_token_pool' do
68
+ it 'should take first from api keys' do
69
+ config = { prefix: 'https://www.fake.com', api_keys: { 'key1' => 'secret1', 'key2' => 'secret2' } }
70
+ client = BridgeAPI::Client.new(config)
71
+ new_config = client.initialize_from_token_pool(config)
72
+ expect(new_config[:api_key]).to eq('key1')
73
+ expect(new_config[:api_secret]).to eq('secret1')
74
+ end
75
+
76
+ it 'should take first from tokens' do
77
+ config = { prefix: 'https://www.fake.com', api_tokens: %w[token1 token2] }
78
+ client = BridgeAPI::Client.new(config)
79
+ new_config = client.initialize_from_token_pool(config)
80
+ expect(new_config[:token]).to eq('token1')
81
+ end
82
+ end
83
+
84
+ describe 'get_next_key' do
85
+ it 'should find next key' do
86
+ keys = { 'key1' => 'secret1', 'key2' => 'secret2' }
87
+ config = { prefix: 'https://www.fake.com', api_keys: keys }
88
+ client = BridgeAPI::Client.new(config)
89
+ expect(client.get_next_key(keys.keys, 'key1')).to eq('key2')
90
+ expect(client.get_next_key(keys.keys, 'key2')).to eq('key1')
91
+ expect(client.get_next_key(keys.keys, 'non-existent-key')).to eq('key1')
92
+ end
93
+ end
94
+
95
+ describe 'rotate_token!' do
96
+ context 'with api key pool' do
97
+ it 'should rotate to the next key' do
98
+ keys = { 'key1' => 'secret1', 'key2' => 'secret2' }
99
+ config = { prefix: 'https://www.fake.com', api_keys: keys }
100
+ client = BridgeAPI::Client.new(config)
101
+ expect(client.config[:api_key]).to eq('key1')
102
+ expect(client.config[:api_secret]).to eq('secret1')
103
+ client.rotate_token!
104
+ expect(client.config[:api_key]).to eq('key2')
105
+ expect(client.config[:api_secret]).to eq('secret2')
106
+ client.rotate_token!
107
+ expect(client.config[:api_key]).to eq('key1')
108
+ expect(client.config[:api_secret]).to eq('secret1')
109
+ end
110
+ end
111
+
112
+ context 'with token pool' do
113
+ it 'should rotate to the next key' do
114
+ keys = %w[token1 token2]
115
+ config = { prefix: 'https://www.fake.com', api_tokens: keys }
116
+ client = BridgeAPI::Client.new(config)
117
+ expect(client.config[:token]).to eq('token1')
118
+ client.rotate_token!
119
+ expect(client.config[:token]).to eq('token2')
120
+ client.rotate_token!
121
+ expect(client.config[:token]).to eq('token1')
122
+ end
123
+ end
30
124
  end
31
125
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'sinatra/base'
2
4
  require 'tilt'
3
5
 
@@ -257,7 +259,7 @@ class FakeBridge < Sinatra::Base
257
259
  get_json_data 200, 'accounts.json'
258
260
  end
259
261
 
260
- #Roles
262
+ # Roles
261
263
  get %r{/api/author/roles} do
262
264
  get_json_data 200, 'roles.json'
263
265
  end
@@ -274,7 +276,7 @@ class FakeBridge < Sinatra::Base
274
276
  get_json_data 204, 'roles.json'
275
277
  end
276
278
 
277
- #Affiliations
279
+ # Affiliations
278
280
  get %r{/api/author/affiliated_sub_accounts} do
279
281
  get_json_data 204, 'affiliations.json'
280
282
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bridge_api'
2
4
  require 'rspec'
3
5
  require 'webmock/rspec'
@@ -9,7 +11,7 @@ RSpec.configure do |config|
9
11
 
10
12
  config.before(:each) do
11
13
  WebMock.disable_net_connect!
12
- WebMock.stub_request(:any, /api\/.*/).to_rack(FakeBridge)
14
+ WebMock.stub_request(:any, %r{api/.*}).to_rack(FakeBridge)
13
15
  end
14
16
  end
15
17
 
metadata CHANGED
@@ -1,35 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bridge_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.55
4
+ version: 0.1.60
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jay Shaffer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-05-02 00:00:00.000000000 Z
11
+ date: 2019-05-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: 1.0.0
20
17
  - - "~>"
21
18
  - !ruby/object:Gem::Version
22
19
  version: '1.0'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.0.0
23
23
  type: :development
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- version: 1.0.0
30
27
  - - "~>"
31
28
  - !ruby/object:Gem::Version
32
29
  version: '1.0'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.0.0
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: byebug
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -104,22 +104,22 @@ dependencies:
104
104
  name: tilt
105
105
  requirement: !ruby/object:Gem::Requirement
106
106
  requirements:
107
- - - "~>"
108
- - !ruby/object:Gem::Version
109
- version: '1.3'
110
107
  - - ">="
111
108
  - !ruby/object:Gem::Version
112
109
  version: 1.3.4
110
+ - - "~>"
111
+ - !ruby/object:Gem::Version
112
+ version: '1.3'
113
113
  type: :development
114
114
  prerelease: false
115
115
  version_requirements: !ruby/object:Gem::Requirement
116
116
  requirements:
117
- - - "~>"
118
- - !ruby/object:Gem::Version
119
- version: '1.3'
120
117
  - - ">="
121
118
  - !ruby/object:Gem::Version
122
119
  version: 1.3.4
120
+ - - "~>"
121
+ - !ruby/object:Gem::Version
122
+ version: '1.3'
123
123
  - !ruby/object:Gem::Dependency
124
124
  name: webmock
125
125
  requirement: !ruby/object:Gem::Requirement
@@ -297,7 +297,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
297
297
  - !ruby/object:Gem::Version
298
298
  version: '0'
299
299
  requirements: []
300
- rubygems_version: 3.0.3
300
+ rubyforge_project:
301
+ rubygems_version: 2.7.3
301
302
  signing_key:
302
303
  specification_version: 4
303
304
  summary: Bridge API