bridge_api 0.1.55 → 0.1.60

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.
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