bearcat 1.4.12 → 1.5.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bearcat.gemspec +9 -3
- data/lib/bearcat/api_array.rb +2 -0
- data/lib/bearcat/client/account_reports.rb +6 -14
- data/lib/bearcat/client/accounts.rb +20 -56
- data/lib/bearcat/client/analytics.rb +4 -7
- data/lib/bearcat/client/assignment_groups.rb +7 -19
- data/lib/bearcat/client/assignments.rb +15 -39
- data/lib/bearcat/client/blueprint_courses.rb +14 -20
- data/lib/bearcat/client/calendar_events.rb +9 -17
- data/lib/bearcat/client/canvas_files.rb +0 -2
- data/lib/bearcat/client/conferences.rb +3 -8
- data/lib/bearcat/client/conversations.rb +3 -8
- data/lib/bearcat/client/courses.rb +15 -45
- data/lib/bearcat/client/custom_gradebook_columns.rb +11 -25
- data/lib/bearcat/client/discussions.rb +11 -37
- data/lib/bearcat/client/enrollments.rb +9 -29
- data/lib/bearcat/client/external_tools.rb +9 -36
- data/lib/bearcat/client/file_helper.rb +3 -3
- data/lib/bearcat/client/files.rb +2 -4
- data/lib/bearcat/client/folders.rb +13 -59
- data/lib/bearcat/client/group_categories.rb +8 -17
- data/lib/bearcat/client/group_memberships.rb +6 -14
- data/lib/bearcat/client/groups.rb +9 -24
- data/lib/bearcat/client/module_items.rb +9 -17
- data/lib/bearcat/client/modules.rb +10 -20
- data/lib/bearcat/client/outcome_groups.rb +2 -4
- data/lib/bearcat/client/outcomes.rb +4 -7
- data/lib/bearcat/client/pages.rb +8 -23
- data/lib/bearcat/client/progresses.rb +2 -3
- data/lib/bearcat/client/quizzes.rb +15 -35
- data/lib/bearcat/client/reports.rb +8 -19
- data/lib/bearcat/client/roles.rb +1 -1
- data/lib/bearcat/client/rubric.rb +8 -20
- data/lib/bearcat/client/rubric_assessment.rb +5 -11
- data/lib/bearcat/client/rubric_association.rb +6 -12
- data/lib/bearcat/client/search.rb +2 -4
- data/lib/bearcat/client/sections.rb +11 -26
- data/lib/bearcat/client/submissions.rb +48 -44
- data/lib/bearcat/client/tabs.rb +4 -7
- data/lib/bearcat/client/users.rb +17 -44
- data/lib/bearcat/client.rb +72 -44
- data/lib/bearcat/client_module.rb +101 -0
- data/lib/bearcat/rate_limiting/redis_script.rb +164 -0
- data/lib/bearcat/rate_limiting.rb +70 -0
- data/lib/bearcat/spec_helpers.rb +62 -31
- data/lib/bearcat/version.rb +1 -1
- data/lib/bearcat.rb +8 -21
- data/lib/catalogcat/api_array.rb +18 -0
- data/lib/catalogcat/client/catalogs.rb +21 -0
- data/lib/catalogcat/client/certificates.rb +9 -0
- data/lib/catalogcat/client/courses.rb +25 -0
- data/lib/catalogcat/client/orders.rb +9 -0
- data/lib/catalogcat/client.rb +39 -0
- data/lib/catalogcat/version.rb +3 -0
- data/lib/catalogcat.rb +14 -0
- data/spec/bearcat/client/canvas_files_spec.rb +1 -2
- data/spec/bearcat/client/content_migrations_spec.rb +2 -2
- data/spec/bearcat/client/courses_spec.rb +2 -4
- data/spec/bearcat/{group_memberships_spec.rb → client/group_memberships_spec.rb} +0 -0
- data/spec/bearcat/client/learning_outcomes_spec.rb +2 -2
- data/spec/bearcat/client/submissions_spec.rb +14 -0
- data/spec/bearcat/client_spec.rb +1 -4
- data/spec/bearcat/rate_limiting_spec.rb +62 -0
- data/spec/bearcat/stub_bearcat_spec.rb +15 -0
- data/spec/helper.rb +1 -0
- metadata +196 -181
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'digest/sha1'
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
# Modified from https://github.com/Shopify/wolverine/blob/master/lib/wolverine/script.rb
|
6
|
+
|
7
|
+
module Bearcat
|
8
|
+
module RateLimiting
|
9
|
+
# {RedisScript} represents a lua script in the filesystem. It loads the script
|
10
|
+
# from disk and handles talking to redis to execute it. Error handling
|
11
|
+
# is handled by {LuaError}.
|
12
|
+
class RedisScript
|
13
|
+
|
14
|
+
# Loads the script file from disk and calculates its +SHA1+ sum.
|
15
|
+
#
|
16
|
+
# @param file [Pathname] the full path to the indicated file
|
17
|
+
def initialize(file)
|
18
|
+
@file = Pathname.new(file)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Passes the script and supplied arguments to redis for evaulation.
|
22
|
+
# It first attempts to use a script redis has already cached by using
|
23
|
+
# the +EVALSHA+ command, but falls back to providing the full script
|
24
|
+
# text via +EVAL+ if redis has not seen this script before. Future
|
25
|
+
# invocations will then use +EVALSHA+ without erroring.
|
26
|
+
#
|
27
|
+
# @param redis [Redis] the redis connection to run against
|
28
|
+
# @param args [*Objects] the arguments to the script
|
29
|
+
# @return [Object] the value passed back by redis after script execution
|
30
|
+
# @raise [LuaError] if the script failed to compile of encountered a
|
31
|
+
# runtime error
|
32
|
+
def call(redis, *args)
|
33
|
+
t = Time.now
|
34
|
+
begin
|
35
|
+
redis.evalsha(digest, *args)
|
36
|
+
rescue => e
|
37
|
+
e.message =~ /NOSCRIPT/ ? redis.eval(content, *args) : raise
|
38
|
+
end
|
39
|
+
rescue => e
|
40
|
+
if LuaError.intercepts?(e)
|
41
|
+
raise LuaError.new(e, @file, content)
|
42
|
+
else
|
43
|
+
raise
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def content
|
48
|
+
@content ||= load_lua(@file)
|
49
|
+
end
|
50
|
+
|
51
|
+
def digest
|
52
|
+
@digest ||= Digest::SHA1.hexdigest content
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def script_path
|
58
|
+
@file.basename
|
59
|
+
# Rails.root + 'app/redis_lua'
|
60
|
+
end
|
61
|
+
|
62
|
+
def relative_path
|
63
|
+
@path ||= @file.relative_path_from(script_path)
|
64
|
+
end
|
65
|
+
|
66
|
+
def load_lua(file)
|
67
|
+
TemplateContext.new(script_path).template(script_path + file)
|
68
|
+
end
|
69
|
+
|
70
|
+
class TemplateContext
|
71
|
+
def initialize(script_path)
|
72
|
+
@script_path = script_path
|
73
|
+
end
|
74
|
+
|
75
|
+
def template(pathname)
|
76
|
+
@partial_templates ||= {}
|
77
|
+
ERB.new(File.read(pathname)).result binding
|
78
|
+
end
|
79
|
+
|
80
|
+
# helper method to include a lua partial within another lua script
|
81
|
+
#
|
82
|
+
# @param relative_path [String] the relative path to the script from
|
83
|
+
# `script_path`
|
84
|
+
def include_partial(relative_path)
|
85
|
+
unless @partial_templates.has_key? relative_path
|
86
|
+
@partial_templates[relative_path] = nil
|
87
|
+
template( Pathname.new("#{@script_path}/#{relative_path}") )
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Reformats errors raised by redis representing failures while executing
|
93
|
+
# a lua script. The default errors have confusing messages and backtraces,
|
94
|
+
# and a type of +RuntimeError+. This class improves the message and
|
95
|
+
# modifies the backtrace to include the lua script itself in a reasonable
|
96
|
+
# way.
|
97
|
+
class LuaError < StandardError
|
98
|
+
PATTERN = /ERR Error (compiling|running) script \(.*?\): .*?:(\d+): (.*)/
|
99
|
+
WOLVERINE_LIB_PATH = File.expand_path('../../', __FILE__)
|
100
|
+
CONTEXT_LINE_NUMBER = 2
|
101
|
+
|
102
|
+
attr_reader :error, :file, :content
|
103
|
+
|
104
|
+
# Is this error one that should be reformatted?
|
105
|
+
#
|
106
|
+
# @param error [StandardError] the original error raised by redis
|
107
|
+
# @return [Boolean] is this an error that should be reformatted?
|
108
|
+
def self.intercepts? error
|
109
|
+
error.message =~ PATTERN
|
110
|
+
end
|
111
|
+
|
112
|
+
# Initialize a new {LuaError} from an existing redis error, adjusting
|
113
|
+
# the message and backtrace in the process.
|
114
|
+
#
|
115
|
+
# @param error [StandardError] the original error raised by redis
|
116
|
+
# @param file [Pathname] full path to the lua file the error ocurred in
|
117
|
+
# @param content [String] lua file content the error ocurred in
|
118
|
+
def initialize error, file, content
|
119
|
+
@error = error
|
120
|
+
@file = file
|
121
|
+
@content = content
|
122
|
+
|
123
|
+
@error.message =~ PATTERN
|
124
|
+
_stage, line_number, message = $1, $2, $3
|
125
|
+
error_context = generate_error_context(content, line_number.to_i)
|
126
|
+
|
127
|
+
super "#{message}\n\n#{error_context}\n\n"
|
128
|
+
set_backtrace generate_backtrace file, line_number
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def generate_error_context(content, line_number)
|
134
|
+
lines = content.lines.to_a
|
135
|
+
beginning_line_number = [1, line_number - CONTEXT_LINE_NUMBER].max
|
136
|
+
ending_line_number = [lines.count, line_number + CONTEXT_LINE_NUMBER].min
|
137
|
+
line_number_width = ending_line_number.to_s.length
|
138
|
+
|
139
|
+
(beginning_line_number..ending_line_number).map do |number|
|
140
|
+
indicator = number == line_number ? '=>' : ' '
|
141
|
+
formatted_number = "%#{line_number_width}d" % number
|
142
|
+
" #{indicator} #{formatted_number}: #{lines[number - 1]}"
|
143
|
+
end.join.chomp
|
144
|
+
end
|
145
|
+
|
146
|
+
def generate_backtrace(file, line_number)
|
147
|
+
pre_wolverine = backtrace_before_entering_wolverine(@error.backtrace)
|
148
|
+
index_of_first_wolverine_line = (@error.backtrace.size - pre_wolverine.size - 1)
|
149
|
+
pre_wolverine.unshift(@error.backtrace[index_of_first_wolverine_line])
|
150
|
+
pre_wolverine.unshift("#{file}:#{line_number}")
|
151
|
+
pre_wolverine
|
152
|
+
end
|
153
|
+
|
154
|
+
def backtrace_before_entering_wolverine(backtrace)
|
155
|
+
backtrace.reverse.take_while { |line| ! line_from_wolverine(line) }.reverse
|
156
|
+
end
|
157
|
+
|
158
|
+
def line_from_wolverine(line)
|
159
|
+
line.split(':').first.include?(WOLVERINE_LIB_PATH)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
require_relative 'rate_limiting/redis_script'
|
3
|
+
|
4
|
+
module Bearcat
|
5
|
+
module RateLimiting
|
6
|
+
LEAK_RATE = 10
|
7
|
+
GUESS_COST = 60
|
8
|
+
MAXIMUM = 600
|
9
|
+
|
10
|
+
class RateLimiter
|
11
|
+
attr_reader :namespace
|
12
|
+
|
13
|
+
def initialize(namespace: "bearcat", **kwargs)
|
14
|
+
@namespace = namespace
|
15
|
+
@params = kwargs
|
16
|
+
end
|
17
|
+
|
18
|
+
def apply(key, guess: GUESS_COST, on_sleep: nil, max_sleep: 15)
|
19
|
+
current = increment("#{key}", 0, guess)
|
20
|
+
cost = guess
|
21
|
+
remaining = MAXIMUM - current[:count]
|
22
|
+
|
23
|
+
if remaining < 0
|
24
|
+
tts = -remaining / LEAK_RATE
|
25
|
+
tts = [tts, max_sleep].min if max_sleep
|
26
|
+
on_sleep.call(tts) if on_sleep
|
27
|
+
sleep tts
|
28
|
+
end
|
29
|
+
|
30
|
+
cost = yield
|
31
|
+
ensure
|
32
|
+
increment(key, (cost || 0), -guess)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class RedisLimiter < RateLimiter
|
37
|
+
INCR = RedisScript.new(Pathname.new(__FILE__) + "../rate_limiting/increment_bucket.lua")
|
38
|
+
|
39
|
+
def increment(key, amount, pending_amount = 0)
|
40
|
+
ts = Time.now.to_f
|
41
|
+
|
42
|
+
redis do |r|
|
43
|
+
actual, timestamp = INCR.call(r, ["#{key}|reported"], [amount, ts, LEAK_RATE, MAXIMUM + 300])
|
44
|
+
{
|
45
|
+
count: actual.to_f,
|
46
|
+
timestamp: timestamp.to_f,
|
47
|
+
}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def checkin_known(key, remaining_time)
|
52
|
+
redis do |r|
|
53
|
+
r.mapped_hmset("#{key}|reported", {
|
54
|
+
count: MAXIMUM + 200 - remaining_time,
|
55
|
+
timestamp: Time.now.to_f,
|
56
|
+
})
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def redis(&blk)
|
61
|
+
# TODO Add bearcat-native ConnectionPool instead of relying on application or Sidekiq to do so
|
62
|
+
if @params[:redis]
|
63
|
+
@params[:redis].call(blk)
|
64
|
+
else
|
65
|
+
::Sidekiq.redis(&blk)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/bearcat/spec_helpers.rb
CHANGED
@@ -16,36 +16,10 @@ module Bearcat::SpecHelpers
|
|
16
16
|
# Accepts optional keyword parameters to interpolate specific values into the URL.
|
17
17
|
# Returns a mostly-normal Webmock stub (:to_return has been overridden to allow :body to be set to a Hash)
|
18
18
|
def stub_bearcat(endpoint, prefix: nil, method: :auto, **kwargs)
|
19
|
-
|
20
|
-
when Symbol
|
21
|
-
ruby_method = Bearcat::Client.instance_method(endpoint)
|
22
|
-
match = SOURCE_REGEX.match(ruby_method.source)
|
23
|
-
bits = []
|
24
|
-
url = match[:url].gsub(/\/$/, '')
|
25
|
-
lend = 0
|
26
|
-
url.scan(/#\{(?<key>.*?)\}/) do |key|
|
27
|
-
m = Regexp.last_match
|
28
|
-
between = url[lend..m.begin(0)-1]
|
29
|
-
bits << between if between.present? && (m.begin(0) > lend)
|
30
|
-
lend = m.end(0)
|
31
|
-
bits << (kwargs[m[:key].to_sym] || /\w+/)
|
32
|
-
end
|
33
|
-
between = url[lend..-1]
|
34
|
-
bits << between if between.present?
|
35
|
-
|
36
|
-
bits.map do |bit|
|
37
|
-
next bit.source if bit.is_a?(Regexp)
|
38
|
-
bit = bit.canvas_id if bit.respond_to?(:canvas_id)
|
39
|
-
Regexp.escape(bit.to_s)
|
40
|
-
end.join
|
41
|
-
when String
|
42
|
-
Regexp.escape(endpoint)
|
43
|
-
when Regexp
|
44
|
-
endpoint.source
|
45
|
-
end
|
19
|
+
cfg = _bearcat_resolve_config(endpoint, method: method)
|
46
20
|
|
47
|
-
url = Regexp.escape(
|
48
|
-
stub = stub_request(
|
21
|
+
url = Regexp.escape(_bearcat_resolve_prefix(prefix)) + cfg[:url]
|
22
|
+
stub = stub_request(cfg[:method], Regexp.new(url))
|
49
23
|
|
50
24
|
# Override the to_return method to accept a Hash as body:
|
51
25
|
stub.define_singleton_method(:to_return, ->(*resps, &blk) {
|
@@ -72,7 +46,64 @@ module Bearcat::SpecHelpers
|
|
72
46
|
|
73
47
|
private
|
74
48
|
|
75
|
-
def
|
49
|
+
def _bearcat_resolve_config(endpoint, url_context, method: :auto)
|
50
|
+
case endpoint
|
51
|
+
when Symbol
|
52
|
+
if (cfg = Bearcat::Client.registered_endpoints[endpoint]).present?
|
53
|
+
bits = []
|
54
|
+
url = cfg[:url]
|
55
|
+
lend = 0
|
56
|
+
url.scan(/:(?<key>\w+)/) do |key|
|
57
|
+
m = Regexp.last_match
|
58
|
+
between = url[lend..m.begin(0)-1]
|
59
|
+
bits << between if between.present? && (m.begin(0) > lend)
|
60
|
+
lend = m.end(0)
|
61
|
+
bits << (url_context[m[:key].to_sym] || /\w+/)
|
62
|
+
end
|
63
|
+
between = url[lend..-1]
|
64
|
+
bits << between if between.present?
|
65
|
+
|
66
|
+
url = bits.map do |bit|
|
67
|
+
next bit.source if bit.is_a?(Regexp)
|
68
|
+
bit = bit.canvas_id if bit.respond_to?(:canvas_id)
|
69
|
+
Regexp.escape(bit.to_s)
|
70
|
+
end.join
|
71
|
+
|
72
|
+
{ method: method == :auto ? cfg[:method] : method, url: url }
|
73
|
+
else
|
74
|
+
ruby_method = Bearcat::Client.instance_method(endpoint)
|
75
|
+
match = SOURCE_REGEX.match(ruby_method.source)
|
76
|
+
bits = []
|
77
|
+
url = match[:url].gsub(/\/$/, '')
|
78
|
+
lend = 0
|
79
|
+
url.scan(/#\{(?<key>.*?)\}/) do |key|
|
80
|
+
m = Regexp.last_match
|
81
|
+
between = url[lend..m.begin(0)-1]
|
82
|
+
bits << between if between.present? && (m.begin(0) > lend)
|
83
|
+
lend = m.end(0)
|
84
|
+
bits << (url_context[m[:key].to_sym] || /\w+/)
|
85
|
+
end
|
86
|
+
between = url[lend..-1]
|
87
|
+
bits << between if between.present?
|
88
|
+
|
89
|
+
url = bits.map do |bit|
|
90
|
+
next bit.source if bit.is_a?(Regexp)
|
91
|
+
bit = bit.canvas_id if bit.respond_to?(:canvas_id)
|
92
|
+
Regexp.escape(bit.to_s)
|
93
|
+
end.join
|
94
|
+
|
95
|
+
{ method: method == :auto ? (match ? match[:method].to_sym : :get) : method, url: url }
|
96
|
+
end
|
97
|
+
when String
|
98
|
+
raise "Cannot use method :auto when passing string endpoint" if method == :auto
|
99
|
+
{ method: method, url: Regexp.escape(endpoint) }
|
100
|
+
when Regexp
|
101
|
+
raise "Cannot use method :auto when passing regex endpoint" if method == :auto
|
102
|
+
{ method: method, url: Regexp.escape(endpoint) }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def _bearcat_resolve_prefix(prefix)
|
76
107
|
if prefix == true
|
77
108
|
prefix = canvas_api_client if defined? canvas_api_client
|
78
109
|
prefix = canvas_sync_client if defined? canvas_sync_client
|
@@ -90,4 +121,4 @@ module Bearcat::SpecHelpers
|
|
90
121
|
prefix
|
91
122
|
end
|
92
123
|
end
|
93
|
-
end
|
124
|
+
end
|
data/lib/bearcat/version.rb
CHANGED
data/lib/bearcat.rb
CHANGED
@@ -4,24 +4,23 @@ require 'bearcat/client'
|
|
4
4
|
module Bearcat
|
5
5
|
class << self
|
6
6
|
require 'logger'
|
7
|
-
attr_writer :
|
8
|
-
:min_sleep_seconds, :logger, :master_rate_limit, :
|
9
|
-
:rate_limit_threshold
|
7
|
+
attr_writer :rate_limit_min, :rate_limits, :max_sleep_seconds,
|
8
|
+
:min_sleep_seconds, :logger, :master_rate_limit, :rate_limiter
|
10
9
|
|
11
10
|
def configure
|
12
11
|
yield self if block_given?
|
13
12
|
end
|
14
13
|
|
15
|
-
def
|
16
|
-
@
|
14
|
+
def rate_limiter
|
15
|
+
@rate_limiter
|
17
16
|
end
|
18
17
|
|
19
|
-
def
|
20
|
-
@
|
18
|
+
def rate_limit_min
|
19
|
+
@rate_limit_min ||= 175
|
21
20
|
end
|
22
21
|
|
23
|
-
def
|
24
|
-
@
|
22
|
+
def min_sleep_seconds
|
23
|
+
@min_sleep_seconds ||= 5
|
25
24
|
end
|
26
25
|
|
27
26
|
def max_sleep_seconds
|
@@ -32,23 +31,11 @@ module Bearcat
|
|
32
31
|
@master_rate_limit ||= false
|
33
32
|
end
|
34
33
|
|
35
|
-
def master_mutex
|
36
|
-
@master_mutex ||= Mutex.new
|
37
|
-
end
|
38
|
-
|
39
|
-
def rate_limit_threshold
|
40
|
-
@rate_limit_threshold ||= 125
|
41
|
-
end
|
42
|
-
|
43
34
|
def logger
|
44
35
|
return @logger if defined? @logger
|
45
36
|
@logger = Logger.new(STDOUT)
|
46
37
|
@logger.level = Logger::DEBUG
|
47
38
|
@logger
|
48
39
|
end
|
49
|
-
|
50
|
-
def min_sleep_seconds
|
51
|
-
@min_sleep_seconds ||= 5
|
52
|
-
end
|
53
40
|
end
|
54
41
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'bearcat/api_array'
|
2
|
+
|
3
|
+
module Catalogcat
|
4
|
+
class ApiArray < Bearcat::ApiArray
|
5
|
+
def self.array_key(response)
|
6
|
+
key = nil
|
7
|
+
if response.env[:method] == :get
|
8
|
+
path = response.env[:url].path
|
9
|
+
key = 'courses' if path =~ %r{.*/courses}
|
10
|
+
key = 'course' if path =~ %r{.*/courses/[0-9]*}
|
11
|
+
key = 'catalogs' if path =~ %r{.*/catalogs}
|
12
|
+
key = 'enrollments' if path =~ %r{.*/enrollments}
|
13
|
+
key = 'order' if path =~ %r{.*/order/[0-9]*}
|
14
|
+
end
|
15
|
+
key.present? ? key : super(response)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Catalogcat
|
2
|
+
class Client < Footrest::Client
|
3
|
+
module Catalogs
|
4
|
+
def list_catalogs(params = {})
|
5
|
+
get('/api/v1/catalogs', params)
|
6
|
+
end
|
7
|
+
|
8
|
+
def applicants(params = {})
|
9
|
+
get('/api/v1/applicants', params)
|
10
|
+
end
|
11
|
+
|
12
|
+
def completed_certificates(params = {})
|
13
|
+
get('/api/v1/completed_certificates', params)
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_order(id, params = {})
|
17
|
+
get("/api/v1/orders/#{id}", params)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Catalogcat
|
2
|
+
class Client < Footrest::Client
|
3
|
+
module Courses
|
4
|
+
def list_courses(page = 1, params = {})
|
5
|
+
get("/api/v1/courses?page=#{page}&per_page=100", params)
|
6
|
+
end
|
7
|
+
|
8
|
+
def create_course(params = {})
|
9
|
+
post('/api/v1/courses', params)
|
10
|
+
end
|
11
|
+
|
12
|
+
def update_course(id, params = {})
|
13
|
+
put("/api/v1/courses/#{id}", params)
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_course(id, params = {})
|
17
|
+
get("/api/v1/courses/#{id}", params)
|
18
|
+
end
|
19
|
+
|
20
|
+
def delete_course(course)
|
21
|
+
delete("/api/v1/courses/#{course}")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'footrest/client'
|
2
|
+
|
3
|
+
module Catalogcat
|
4
|
+
class Client < Footrest::Client
|
5
|
+
require 'catalogcat/api_array' # monkey patch
|
6
|
+
|
7
|
+
Dir[File.join(__dir__, 'client', '*.rb')].each do |mod|
|
8
|
+
mname = File.basename(mod, '.*').camelize
|
9
|
+
require mod
|
10
|
+
include "Catalogcat::Client::#{mname}".constantize
|
11
|
+
end
|
12
|
+
|
13
|
+
# Override Footrest connection to use Token authorization
|
14
|
+
# rubocop:disable Naming/AccessorMethodName
|
15
|
+
def set_connection(config)
|
16
|
+
super
|
17
|
+
connection.builder.insert(Footrest::RaiseFootrestErrors, ExtendedRaiseFootrestErrors)
|
18
|
+
connection.builder.delete(Footrest::RaiseFootrestErrors)
|
19
|
+
connection.headers[:authorization].sub! 'Bearer', 'Token'
|
20
|
+
end
|
21
|
+
# rubocop:enable Naming/AccessorMethodName
|
22
|
+
|
23
|
+
# Override Footrest request for ApiArray support
|
24
|
+
def request(method, &block)
|
25
|
+
response = connection.send(method, &block)
|
26
|
+
raise Footrest::HttpError::ErrorBase, response if response.status >= 400
|
27
|
+
|
28
|
+
Catalogcat::ApiArray.process_response(response, self)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class ExtendedRaiseFootrestErrors < Footrest::RaiseFootrestErrors
|
33
|
+
def on_complete(response)
|
34
|
+
super
|
35
|
+
key = response[:status].to_i
|
36
|
+
raise ERROR_MAP[key.floor(-2)], response if key >= 400
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/catalogcat.rb
ADDED
@@ -13,8 +13,7 @@ describe Bearcat::Client::CanvasFiles do
|
|
13
13
|
stub_request(:post, "https://upload-url.invalid/").
|
14
14
|
to_return(status: 302, headers: {'Location' => 'https://confirm-upload.invalid/confirm?param=true'})
|
15
15
|
|
16
|
-
|
17
|
-
with(:body => {"param" => ["true"]}).to_return(json_response('canvas_files', 'upload_success.json'))
|
16
|
+
stub_get(@client, '/confirm').with(query: { param: true }).to_return(json_response('canvas_files', 'upload_success.json'))
|
18
17
|
|
19
18
|
response = @client.upload_file('my/upload/path', fixture('bearcat.jpg'))
|
20
19
|
expect(response['id']).to eq 123
|
@@ -7,12 +7,12 @@ describe Bearcat::Client::ContentMigrations do
|
|
7
7
|
|
8
8
|
it 'uploads a file' do
|
9
9
|
stub_post(@client, "/my/upload/path").
|
10
|
-
|
10
|
+
to_return(json_response('content_migration_files/response.json'))
|
11
11
|
|
12
12
|
stub_request(:post, "http://host/files_api").
|
13
13
|
to_return(status: 302, headers: {'Location' => 'https://confirm-upload.invalid/confirm?param=true'})
|
14
14
|
|
15
|
-
|
15
|
+
stub_get(@client, '/confirm').with(query: { param: true }).to_return(json_response('content_migration_files/upload_success.json'))
|
16
16
|
|
17
17
|
opts = {migration_type: 'canvas_cartridge_importer', pre_attachment: {name: 'cc.imscc', size: '2034'}}
|
18
18
|
response = @client.upload_content_package('my/upload/path', fixture('cc.imscc'), opts)
|
@@ -49,8 +49,7 @@ describe Bearcat::Client::Sections do
|
|
49
49
|
stub_request(:post, "http://host/files_api").
|
50
50
|
to_return(status: 302, headers: {'Location' => 'https://confirm-upload.invalid/confirm?param=true'})
|
51
51
|
|
52
|
-
|
53
|
-
with(:body => {"param" => ["true"]}).to_return(json_response('content_migration_files', 'upload_success.json'))
|
52
|
+
stub_get(@client, '/confirm').with(query: { param: true }).to_return(json_response('content_migration_files', 'upload_success.json'))
|
54
53
|
|
55
54
|
opts = {'migration_type' => 'canvas_cartridge_importer', 'pre_attachment[name]' => 'cc.imscc', 'pre_attachment[size]' => '2034'}
|
56
55
|
response = @client.create_content_migration('659', fixture('cc.imscc'), opts)
|
@@ -66,8 +65,7 @@ describe Bearcat::Client::Sections do
|
|
66
65
|
stub_request(:post, "http://host/files_api").
|
67
66
|
to_return(status: 302, headers: {'Location' => 'https://confirm-upload.invalid/confirm?param=true'})
|
68
67
|
|
69
|
-
|
70
|
-
with(:body => {"param" => ["true"]}).to_return(json_response('content_migration_files', 'upload_success.json'))
|
68
|
+
stub_get(@client, '/confirm').with(query: { param: true }).to_return(json_response('content_migration_files', 'upload_success.json'))
|
71
69
|
|
72
70
|
opts = {'migration_type' => 'canvas_cartridge_importer', 'pre_attachment[name]' => 'cc.imscc', 'pre_attachment[size]' => '2034'}
|
73
71
|
content_migration_response, file_upload_response = @client.create_content_migration_with_both_responses('659', fixture('cc.imscc'), opts)
|
File without changes
|
@@ -8,7 +8,7 @@ describe Bearcat::Client::LearningOutcomes do
|
|
8
8
|
end
|
9
9
|
|
10
10
|
it "returns an individual learning outcome" do
|
11
|
-
stub_get(@client, "/api/v1/outcomes/1").to_return(json_response("
|
11
|
+
stub_get(@client, "/api/v1/outcomes/1").to_return(json_response("learning_outcome.json"))
|
12
12
|
outcome = @client.learning_outcome(1)
|
13
13
|
outcome["id"].should == 1
|
14
14
|
outcome["context_type"].should == "Course"
|
@@ -16,7 +16,7 @@ describe Bearcat::Client::LearningOutcomes do
|
|
16
16
|
end
|
17
17
|
|
18
18
|
it "updates an individual learning outcome" do
|
19
|
-
stub_put(@client, "/api/v1/outcomes/1").to_return(json_response("
|
19
|
+
stub_put(@client, "/api/v1/outcomes/1").to_return(json_response("learning_outcome.json"))
|
20
20
|
outcome = @client.update_learning_outcome(1)
|
21
21
|
outcome["id"].should == 1
|
22
22
|
outcome["context_type"].should == "Course"
|
@@ -45,6 +45,13 @@ describe Bearcat::Client::Submissions do
|
|
45
45
|
response['id'].should == 8444510
|
46
46
|
end
|
47
47
|
|
48
|
+
it "submits multiple files" do
|
49
|
+
@client.stub(:upload_file).and_return({})
|
50
|
+
stub_post(@client, '/api/v1/courses/1/assignments/2/submissions').to_return(json_response('submissions', 'submission.json'))
|
51
|
+
response = @client.course_file_upload_submission(1, 2, 3, [fixture('bearcat.jpg'), fixture('file.csv')])
|
52
|
+
response['id'].should == 8444510
|
53
|
+
end
|
54
|
+
|
48
55
|
it "updates grades" do
|
49
56
|
params = {"grade_data[123]" => "19"}
|
50
57
|
stub_post(@client, "/api/v1/courses/1/assignments/1/submissions/update_grades").with(body: {"grade_data"=>["19"]}).to_return(json_response("progress.json"))
|
@@ -56,6 +63,13 @@ describe Bearcat::Client::Submissions do
|
|
56
63
|
|
57
64
|
context 'section' do
|
58
65
|
it "submits a file" do
|
66
|
+
@client.stub(:upload_file).and_return({})
|
67
|
+
stub_post(@client, '/api/v1/sections/1/assignments/2/submissions').to_return(json_response('submissions', 'submission.json'))
|
68
|
+
response = @client.section_file_upload_submission(1, 2, 3, [fixture('bearcat.jpg'), fixture('file.csv')])
|
69
|
+
response['id'].should == 8444510
|
70
|
+
end
|
71
|
+
|
72
|
+
it "submits multiple files" do
|
59
73
|
@client.stub(:upload_file).and_return({})
|
60
74
|
stub_post(@client, '/api/v1/sections/1/assignments/2/submissions').to_return(json_response('submissions', 'submission.json'))
|
61
75
|
response = @client.section_file_upload_submission(1, 2, 3, fixture('bearcat.jpg'))
|
data/spec/bearcat/client_spec.rb
CHANGED
@@ -1,16 +1,13 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
describe Bearcat::Client do
|
4
|
-
|
5
4
|
it "sets the domain" do
|
6
5
|
client = Bearcat::Client.new(domain: "http://canvas.instructure.com")
|
7
6
|
client.config[:domain].should == "http://canvas.instructure.com"
|
8
|
-
|
9
7
|
end
|
10
8
|
|
11
9
|
it "sets the authtoken" do
|
12
10
|
client = Bearcat::Client.new(token: "test_token")
|
13
11
|
client.config[:token].should == "test_token"
|
14
12
|
end
|
15
|
-
|
16
|
-
end
|
13
|
+
end
|