bearcat 1.0.0 → 1.5.24
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.
- checksums.yaml +7 -0
- data/bearcat.gemspec +15 -5
- data/lib/badgrcat/api_array.rb +25 -0
- data/lib/badgrcat/client/methods.rb +54 -0
- data/lib/badgrcat/client.rb +53 -0
- data/lib/badgrcat/version.rb +3 -0
- data/lib/bearcat/api_array.rb +132 -65
- data/lib/bearcat/client/account_reports.rb +6 -14
- data/lib/bearcat/client/accounts.rb +18 -6
- data/lib/bearcat/client/analytics.rb +12 -0
- data/lib/bearcat/client/assignment_groups.rb +15 -0
- data/lib/bearcat/client/assignments.rb +17 -9
- data/lib/bearcat/client/blueprint_courses.rb +25 -0
- 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/content_exports.rb +39 -0
- data/lib/bearcat/client/content_migrations.rb +54 -0
- data/lib/bearcat/client/conversations.rb +3 -8
- data/lib/bearcat/client/courses.rb +25 -14
- data/lib/bearcat/client/custom_gradebook_columns.rb +21 -0
- data/lib/bearcat/client/discussions.rb +10 -4
- data/lib/bearcat/client/enrollments.rb +9 -25
- data/lib/bearcat/client/external_tools.rb +18 -0
- data/lib/bearcat/client/file_helper.rb +36 -30
- data/lib/bearcat/client/files.rb +9 -0
- data/lib/bearcat/client/folders.rb +24 -0
- data/lib/bearcat/client/graph_ql.rb +17 -0
- data/lib/bearcat/client/group_categories.rb +18 -0
- data/lib/bearcat/client/group_memberships.rb +14 -0
- data/lib/bearcat/client/groups.rb +10 -2
- data/lib/bearcat/client/learning_outcomes.rb +17 -0
- data/lib/bearcat/client/logins.rb +20 -0
- data/lib/bearcat/client/module_items.rb +18 -0
- data/lib/bearcat/client/modules.rb +12 -7
- data/lib/bearcat/client/o_auth2.rb +18 -9
- data/lib/bearcat/client/outcome_groups.rb +2 -4
- data/lib/bearcat/client/outcome_imports.rb +48 -0
- data/lib/bearcat/client/outcomes.rb +4 -7
- data/lib/bearcat/client/pages.rb +15 -0
- data/lib/bearcat/client/progresses.rb +9 -0
- data/lib/bearcat/client/quizzes.rb +13 -9
- data/lib/bearcat/client/reports.rb +37 -17
- data/lib/bearcat/client/roles.rb +15 -0
- data/lib/bearcat/client/rubric.rb +17 -0
- data/lib/bearcat/client/rubric_assessment.rb +13 -0
- data/lib/bearcat/client/rubric_association.rb +13 -0
- data/lib/bearcat/client/search.rb +9 -0
- data/lib/bearcat/client/sections.rb +10 -17
- data/lib/bearcat/client/sis_imports.rb +6 -12
- data/lib/bearcat/client/submissions.rb +53 -21
- data/lib/bearcat/client/tabs.rb +12 -0
- data/lib/bearcat/client/users.rb +32 -13
- data/lib/bearcat/client.rb +111 -45
- data/lib/bearcat/client_module.rb +103 -0
- data/lib/bearcat/rate_limiting/increment_bucket.lua +33 -0
- data/lib/bearcat/rate_limiting/redis_script.rb +164 -0
- data/lib/bearcat/rate_limiting.rb +69 -0
- data/lib/bearcat/redis_connection.rb +106 -0
- data/lib/bearcat/spec_helpers.rb +125 -0
- data/lib/bearcat/version.rb +1 -1
- data/lib/bearcat.rb +49 -0
- data/lib/catalogcat/api_array.rb +22 -0
- data/lib/catalogcat/client/catalogs.rb +21 -0
- data/lib/catalogcat/client/certificates.rb +17 -0
- data/lib/catalogcat/client/courses.rb +25 -0
- data/lib/catalogcat/client/email_domain_sets.rb +17 -0
- data/lib/catalogcat/client/enrollments.rb +25 -0
- data/lib/catalogcat/client/orders.rb +13 -0
- data/lib/catalogcat/client/user_registrations.rb +9 -0
- data/lib/catalogcat/client.rb +26 -0
- data/lib/catalogcat/version.rb +3 -0
- data/lib/catalogcat.rb +14 -0
- data/spec/bearcat/api_array_spec.rb +112 -0
- data/spec/bearcat/client/accounts_spec.rb +71 -1
- data/spec/bearcat/client/analytics_spec.rb +22 -0
- data/spec/bearcat/client/assignment_groups_spec.rb +47 -0
- data/spec/bearcat/client/assignments_spec.rb +43 -0
- data/spec/bearcat/client/blueprint_courses_spec.rb +43 -0
- data/spec/bearcat/client/canvas_files_spec.rb +1 -2
- data/spec/bearcat/client/content_exports_spec.rb +68 -0
- data/spec/bearcat/client/content_migrations_spec.rb +94 -0
- data/spec/bearcat/client/courses_spec.rb +81 -2
- data/spec/bearcat/client/custom_gradebook_columns_spec.rb +66 -0
- data/spec/bearcat/client/discussions_spec.rb +73 -0
- data/spec/bearcat/client/enrollments_spec.rb +10 -0
- data/spec/bearcat/client/external_tools_spec.rb +106 -0
- data/spec/bearcat/client/files_spec.rb +15 -0
- data/spec/bearcat/client/folders_spec.rb +18 -0
- data/spec/bearcat/client/graph_ql_spec.rb +35 -0
- data/spec/bearcat/client/group_categories_spec.rb +45 -0
- data/spec/bearcat/client/group_membership_spec.rb +14 -0
- data/spec/bearcat/client/group_memberships_spec.rb +36 -0
- data/spec/bearcat/client/groups_spec.rb +46 -0
- data/spec/bearcat/client/learning_outcomes_spec.rb +25 -0
- data/spec/bearcat/client/module_items_spec.rb +60 -0
- data/spec/bearcat/client/modules_spec.rb +38 -1
- data/spec/bearcat/client/o_auth2_spec.rb +3 -3
- data/spec/bearcat/client/pages_spec.rb +17 -0
- data/spec/bearcat/client/quizzes_spec.rb +41 -4
- data/spec/bearcat/client/reports_spec.rb +40 -1
- data/spec/bearcat/client/roles_spec.rb +24 -0
- data/spec/bearcat/client/rubric_assessment_spec.rb +47 -0
- data/spec/bearcat/client/rubric_association_spec.rb +39 -0
- data/spec/bearcat/client/rubric_spec.rb +45 -0
- data/spec/bearcat/client/search_spec.rb +16 -0
- data/spec/bearcat/client/sections_spec.rb +12 -0
- data/spec/bearcat/client/submissions_spec.rb +47 -2
- data/spec/bearcat/client/users_spec.rb +43 -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/fixtures/access_token.json +3 -0
- data/spec/fixtures/account_admin_create.json +14 -0
- data/spec/fixtures/account_admin_delete.json +15 -0
- data/spec/fixtures/account_admins.json +54 -0
- data/spec/fixtures/account_courses.json +48 -0
- data/spec/fixtures/account_grading_standards.json +20 -0
- data/spec/fixtures/account_groups.json +42 -0
- data/spec/fixtures/account_role.json +34 -0
- data/spec/fixtures/account_roles.json +35 -0
- data/spec/fixtures/account_sis_imports.json +39 -0
- data/spec/fixtures/account_sub_accounts.json +17 -0
- data/spec/fixtures/accounts.json +13 -0
- data/spec/fixtures/assignment.json +32 -0
- data/spec/fixtures/assignment_group.json +7 -0
- data/spec/fixtures/assignment_groups.json +16 -0
- data/spec/fixtures/blueprint_migration.json +12 -0
- data/spec/fixtures/blueprint_subscriptions.json +5 -0
- data/spec/fixtures/blueprint_template.json +7 -0
- data/spec/fixtures/blueprint_update_assocations_success.json +3 -0
- data/spec/fixtures/communication_channels.json +10 -0
- data/spec/fixtures/content_export.json +9 -0
- data/spec/fixtures/content_migration_files/content_migration.json +13 -0
- data/spec/fixtures/course_copy.json +18 -0
- data/spec/fixtures/course_files.json +38 -0
- data/spec/fixtures/course_folder.json +21 -0
- data/spec/fixtures/course_folders.json +44 -0
- data/spec/fixtures/course_grading_standards.json +20 -0
- data/spec/fixtures/course_settings.json +33 -0
- data/spec/fixtures/create_course_discussion.json +44 -0
- data/spec/fixtures/created_group.json +37 -0
- data/spec/fixtures/created_group_category.json +15 -0
- data/spec/fixtures/created_group_membership.json +8 -0
- data/spec/fixtures/created_module.json +13 -0
- data/spec/fixtures/custom_gradebook_columns/column_data.json +4 -0
- data/spec/fixtures/custom_gradebook_columns/custom_gradebook_column.json +7 -0
- data/spec/fixtures/custom_gradebook_columns/custom_gradebook_columns.json +16 -0
- data/spec/fixtures/custom_gradebook_columns/gradebook_column_progress.json +14 -0
- data/spec/fixtures/dashboard.json +6 -0
- data/spec/fixtures/delete_course.json +3 -0
- data/spec/fixtures/delete_group_category.json +3 -0
- data/spec/fixtures/deleted_group.json +37 -0
- data/spec/fixtures/department_level_participation.json +73 -0
- data/spec/fixtures/department_level_statistics.json +10 -0
- data/spec/fixtures/discussion_entries.json +21 -0
- data/spec/fixtures/discussion_entry_replies.json +21 -0
- data/spec/fixtures/discussion_topic.json +49 -0
- data/spec/fixtures/discussion_topics.json +51 -0
- data/spec/fixtures/edited_group.json +129 -0
- data/spec/fixtures/edited_group_category.json +15 -0
- data/spec/fixtures/enrollment_terms.json +1 -1
- data/spec/fixtures/external_tool.json +55 -0
- data/spec/fixtures/external_tools.json +57 -0
- data/spec/fixtures/file.csv +5 -0
- data/spec/fixtures/gradebook_history.json +52 -0
- data/spec/fixtures/graph_ql_scores.json +33 -0
- data/spec/fixtures/group.json +15 -0
- data/spec/fixtures/group_categories.json +28 -0
- data/spec/fixtures/group_category.json +13 -0
- data/spec/fixtures/group_category_groups.json +20 -0
- data/spec/fixtures/group_membership.json +7 -0
- data/spec/fixtures/learning_outcome.json +32 -0
- data/spec/fixtures/merge_user.json +8 -0
- data/spec/fixtures/module.json +15 -0
- data/spec/fixtures/module_item.json +15 -0
- data/spec/fixtures/module_items.json +47 -0
- data/spec/fixtures/ok.json +3 -0
- data/spec/fixtures/outcome_result.json +13 -0
- data/spec/fixtures/pages.json +40 -0
- data/spec/fixtures/progress.json +13 -0
- data/spec/fixtures/quizzes/course_quiz_questions.json +59 -0
- data/spec/fixtures/quizzes/quiz_assignment_override.json +31 -0
- data/spec/fixtures/reactivate_enrollment.json +20 -0
- data/spec/fixtures/rubric.json +13 -0
- data/spec/fixtures/rubric_assessment.json +32 -0
- data/spec/fixtures/rubric_association.json +13 -0
- data/spec/fixtures/search_find_recipients.json +10 -0
- data/spec/fixtures/update_section.json +1 -1
- data/spec/fixtures/user_details.json +16 -0
- data/spec/fixtures/user_logins.json +9 -0
- data/spec/helper.rb +2 -0
- metadata +336 -43
@@ -0,0 +1,103 @@
|
|
1
|
+
module Bearcat
|
2
|
+
class Client < Footrest::Client
|
3
|
+
module ClientModule
|
4
|
+
ARG_REGEX = /:(\w+)/
|
5
|
+
attr_reader :_registered_endpoints
|
6
|
+
|
7
|
+
def prefix(prefix)
|
8
|
+
past_prefix = @current_prefix
|
9
|
+
@current_prefix = (past_prefix || '') + prefix
|
10
|
+
yield
|
11
|
+
ensure
|
12
|
+
@current_prefix = past_prefix
|
13
|
+
end
|
14
|
+
|
15
|
+
def endpoint(method, identifier, url = "", defaults: {}, &blk)
|
16
|
+
if @current_prefix && !url.start_with?('/')
|
17
|
+
url = url[2..-1] if url.start_with?('./')
|
18
|
+
url = @current_prefix + url
|
19
|
+
end
|
20
|
+
|
21
|
+
args = url.to_enum(:scan, ARG_REGEX).map { Regexp.last_match }
|
22
|
+
arg_names = args.map{|m| m[1]}
|
23
|
+
|
24
|
+
@_registered_endpoints ||= {}
|
25
|
+
@_registered_endpoints[identifier] = { symbol: identifier, method: method, url: url }
|
26
|
+
|
27
|
+
# TODO: Consider generating the method using class_eval and a template - this will improve runtime performance
|
28
|
+
# signature_bits = []
|
29
|
+
# logical_bits = []
|
30
|
+
# args.each do |m|
|
31
|
+
# name = [1]
|
32
|
+
# end
|
33
|
+
|
34
|
+
# interpolated_url = url.gsub(ARG_REGEX) do |m|
|
35
|
+
# '#{' + m[1] + '}'
|
36
|
+
# end
|
37
|
+
|
38
|
+
# class_eval <<~RUBY
|
39
|
+
# def #{identifier}(*args, **kwargs)
|
40
|
+
# parameters = {
|
41
|
+
|
42
|
+
# }
|
43
|
+
|
44
|
+
# #{method}(#{interpolated_url})
|
45
|
+
# end
|
46
|
+
# RUBY
|
47
|
+
|
48
|
+
define_method(identifier) do |*args, **kwargs|
|
49
|
+
url_arguments = {}
|
50
|
+
parameters = { }.with_indifferent_access
|
51
|
+
parameters.merge!(defaults)
|
52
|
+
|
53
|
+
args.each_with_index do |v, i|
|
54
|
+
if arg_names[i]
|
55
|
+
url_arguments[arg_names[i]] = v
|
56
|
+
elsif i == arg_names.count && v.is_a?(Hash)
|
57
|
+
parameters.merge!(v)
|
58
|
+
else
|
59
|
+
raise ArgumentError, "Too many arguments passed to #{identifier}" unless arg_names[i]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
parameters.merge!(kwargs)
|
64
|
+
|
65
|
+
preq = BearcatRequest.new(url, url_arguments, parameters)
|
66
|
+
yield preq if block_given?
|
67
|
+
|
68
|
+
arg_names.each do |an|
|
69
|
+
preq.arguments[an] = preq.parameters.delete(an) unless preq.arguments.key?(an)
|
70
|
+
raise ArgumentError, "Missing argument #{an}" unless preq.arguments.key?(an)
|
71
|
+
end
|
72
|
+
|
73
|
+
send(method, preq.interpoated_url, preq.parameters)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def context_types(types, &blk)
|
78
|
+
types.each(&blk)
|
79
|
+
end
|
80
|
+
|
81
|
+
%i[get post delete put patch head].each do |mthd|
|
82
|
+
define_method(mthd) do |*args, **kwargs, &blk|
|
83
|
+
endpoint(mthd, *args, **kwargs, &blk)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
BearcatRequest = Struct.new(:url, :arguments, :parameters) do
|
88
|
+
def interpoated_url
|
89
|
+
url.gsub(ARG_REGEX) do |_|
|
90
|
+
m = Regexp.last_match
|
91
|
+
val = arguments[m[1]]
|
92
|
+
val = val.canvas_id if val.respond_to?(:canvas_id)
|
93
|
+
val = val.id if val.respond_to?(:id)
|
94
|
+
val = val['id'].presence || val[:id].presence || val if val.is_a?(Hash)
|
95
|
+
val = val.to_s if val.present?
|
96
|
+
val = URI.encode_www_form_component(val) if val.is_a?(String)
|
97
|
+
val
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
local cache_key = KEYS[1]
|
2
|
+
|
3
|
+
local amount = tonumber(ARGV[1])
|
4
|
+
local current_time = tonumber(ARGV[2])
|
5
|
+
local outflow = tonumber(ARGV[3])
|
6
|
+
local maximum = tonumber(ARGV[4])
|
7
|
+
|
8
|
+
local leak = function(count, last_touched, current_time, outflow)
|
9
|
+
if count > 0 then
|
10
|
+
local timespan = current_time - last_touched
|
11
|
+
local loss = outflow * timespan
|
12
|
+
if loss > 0 then
|
13
|
+
count = count - loss
|
14
|
+
end
|
15
|
+
end
|
16
|
+
return count, last_touched
|
17
|
+
end
|
18
|
+
|
19
|
+
local count, last_touched = unpack(redis.call('HMGET', cache_key, 'count', 'timestamp'))
|
20
|
+
count = tonumber(count or 0)
|
21
|
+
last_touched = tonumber(last_touched or current_time)
|
22
|
+
|
23
|
+
count, last_touched = leak(count, last_touched, current_time, outflow)
|
24
|
+
if count < 0 then count = 0 end
|
25
|
+
|
26
|
+
count = count + amount
|
27
|
+
if count < 0 then count = 0 end
|
28
|
+
if count > maximum then count = maximum end
|
29
|
+
|
30
|
+
redis.call('HMSET', cache_key, 'count', count, 'timestamp', current_time)
|
31
|
+
redis.call('EXPIRE', cache_key, 600)
|
32
|
+
|
33
|
+
return { tostring(count), tostring(current_time) }
|
@@ -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,69 @@
|
|
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
|
+
if @params[:redis]
|
62
|
+
@params[:redis].call(blk)
|
63
|
+
else
|
64
|
+
::Bearcat.redis(&blk)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "connection_pool"
|
4
|
+
require "redis"
|
5
|
+
require "uri"
|
6
|
+
|
7
|
+
# Adapted from Sidekiq 6.x Implementation
|
8
|
+
|
9
|
+
module Bearcat
|
10
|
+
module RedisConnection
|
11
|
+
KNOWN_REDIS_URL_ENVS = %w[REDIS_URL REDISTOGO_URL REDISCLOUD_URL].freeze
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def configured?(prefix, explicit: false)
|
15
|
+
determine_redis_provider(prefix: prefix, explicit: true)
|
16
|
+
end
|
17
|
+
|
18
|
+
def create(options = {})
|
19
|
+
symbolized_options = options.transform_keys(&:to_sym)
|
20
|
+
|
21
|
+
if !symbolized_options[:url] && (u = determine_redis_provider(prefix: options[:env_prefix]))
|
22
|
+
symbolized_options[:url] = u
|
23
|
+
end
|
24
|
+
|
25
|
+
size = if symbolized_options[:size]
|
26
|
+
symbolized_options[:size]
|
27
|
+
elsif symbolized_options[:env_prefix] && (v = ENV["#{symbolized_options[:env_prefix]}_REDIS_POOL_SIZE"])
|
28
|
+
Integer(v)
|
29
|
+
elsif ENV["RAILS_MAX_THREADS"]
|
30
|
+
Integer(ENV["RAILS_MAX_THREADS"])
|
31
|
+
else
|
32
|
+
5
|
33
|
+
end
|
34
|
+
|
35
|
+
pool_timeout = symbolized_options[:pool_timeout] || 1
|
36
|
+
|
37
|
+
ConnectionPool.new(timeout: pool_timeout, size: size) do
|
38
|
+
namespace = symbolized_options[:namespace]
|
39
|
+
client = Redis.new client_opts(symbolized_options)
|
40
|
+
|
41
|
+
if namespace
|
42
|
+
begin
|
43
|
+
require "redis/namespace"
|
44
|
+
Redis::Namespace.new(namespace, redis: client)
|
45
|
+
rescue LoadError
|
46
|
+
Rails.logger.error("Your Redis configuration uses the namespace '#{namespace}' but the redis-namespace gem is not included in the Gemfile." \
|
47
|
+
"Add the gem to your Gemfile to continue using a namespace. Otherwise, remove the namespace parameter.")
|
48
|
+
exit(-127)
|
49
|
+
end
|
50
|
+
else
|
51
|
+
client
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def client_opts(options)
|
59
|
+
opts = options.dup
|
60
|
+
if opts[:namespace]
|
61
|
+
opts.delete(:namespace)
|
62
|
+
end
|
63
|
+
|
64
|
+
opts.delete(:size)
|
65
|
+
opts.delete(:env_prefix)
|
66
|
+
|
67
|
+
if opts[:network_timeout]
|
68
|
+
opts[:timeout] = opts[:network_timeout]
|
69
|
+
opts.delete(:network_timeout)
|
70
|
+
end
|
71
|
+
|
72
|
+
opts[:reconnect_attempts] ||= 1
|
73
|
+
|
74
|
+
opts
|
75
|
+
end
|
76
|
+
|
77
|
+
def determine_redis_provider(prefix: nil, explicit: false)
|
78
|
+
vars = []
|
79
|
+
|
80
|
+
if prefix.present?
|
81
|
+
if (ptr = ENV["#{prefix}_REDIS_PROVIDER"]).present?
|
82
|
+
return ENV[ptr]
|
83
|
+
else
|
84
|
+
vars.push(*KNOWN_REDIS_URL_ENVS.map { |e| ENV["#{prefix}_#{e}"] })
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
if !explicit || !prefix.present?
|
89
|
+
if (ptr = ENV["REDIS_PROVIDER"]).present?
|
90
|
+
vars << ptr # Intentionally not a return
|
91
|
+
else
|
92
|
+
vars.push(*KNOWN_REDIS_URL_ENVS)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
vars.select!(&:present?)
|
97
|
+
|
98
|
+
vars.each do |e|
|
99
|
+
return ENV[e] if ENV[e].present?
|
100
|
+
end
|
101
|
+
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'bearcat'
|
2
|
+
|
3
|
+
# Bearcat RSpec Helpers
|
4
|
+
# Can be included in your spec_helper.rb file by using:
|
5
|
+
# - `require 'bearcat/spec_helpers'`
|
6
|
+
# And
|
7
|
+
# - `config.include Bearcat::SpecHelpers`
|
8
|
+
# This helper requires `gem 'method_source'` in your test group
|
9
|
+
module Bearcat::SpecHelpers
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
SOURCE_REGEX = /(?<method>get|post|delete|put)\((?<quote>\\?('|"))(?<url>.*)\k<quote>/
|
13
|
+
|
14
|
+
# Helper method to Stub Bearcat requests.
|
15
|
+
# Automagically parses the Bearcat method source to determine the correct URL to stub.
|
16
|
+
# Accepts optional keyword parameters to interpolate specific values into the URL.
|
17
|
+
# Returns a mostly-normal Webmock stub (:to_return has been overridden to allow :body to be set to a Hash)
|
18
|
+
def stub_bearcat(endpoint, prefix: nil, method: :auto, **kwargs)
|
19
|
+
cfg = _bearcat_resolve_config(endpoint, kwargs, method: method)
|
20
|
+
|
21
|
+
url = Regexp.escape(_bearcat_resolve_prefix(prefix)) + cfg[:url]
|
22
|
+
url += "?" if url.end_with?('/')
|
23
|
+
stub = stub_request(cfg[:method], Regexp.new(url))
|
24
|
+
|
25
|
+
# Override the to_return method to accept a Hash as body:
|
26
|
+
stub.define_singleton_method(:to_return, ->(*resps, &blk) {
|
27
|
+
if blk
|
28
|
+
super do |*args|
|
29
|
+
resp = blk.call(*args)
|
30
|
+
resp[:headers] ||= {}
|
31
|
+
resp[:headers]["Content-Type"] ||= "application/json"
|
32
|
+
resp[:body] = resp[:body].to_json
|
33
|
+
resp
|
34
|
+
end
|
35
|
+
else
|
36
|
+
resps.map do |resp|
|
37
|
+
resp[:headers] ||= {}
|
38
|
+
resp[:headers]["Content-Type"] ||= "application/json"
|
39
|
+
resp[:body] = resp[:body].to_json
|
40
|
+
end
|
41
|
+
super(*resps)
|
42
|
+
end
|
43
|
+
})
|
44
|
+
|
45
|
+
stub
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def _bearcat_resolve_config(endpoint, url_context, method: :auto)
|
51
|
+
case endpoint
|
52
|
+
when Symbol
|
53
|
+
if (cfg = Bearcat::Client.registered_endpoints[endpoint]).present?
|
54
|
+
bits = []
|
55
|
+
url = cfg[:url]
|
56
|
+
lend = 0
|
57
|
+
url.scan(/:(?<key>\w+)/) do |key|
|
58
|
+
m = Regexp.last_match
|
59
|
+
between = url[lend..m.begin(0)-1]
|
60
|
+
bits << between if between.present? && (m.begin(0) > lend)
|
61
|
+
lend = m.end(0)
|
62
|
+
bits << (url_context[m[:key].to_sym] || /[\w:]+/)
|
63
|
+
end
|
64
|
+
between = url[lend..-1]
|
65
|
+
bits << between if between.present?
|
66
|
+
|
67
|
+
url = bits.map do |bit|
|
68
|
+
next bit.source if bit.is_a?(Regexp)
|
69
|
+
bit = bit.canvas_id if bit.respond_to?(:canvas_id)
|
70
|
+
Regexp.escape(bit.to_s)
|
71
|
+
end.join
|
72
|
+
|
73
|
+
{ method: method == :auto ? cfg[:method] : method, url: url }
|
74
|
+
else
|
75
|
+
ruby_method = Bearcat::Client.instance_method(endpoint)
|
76
|
+
match = SOURCE_REGEX.match(ruby_method.source)
|
77
|
+
bits = []
|
78
|
+
url = match[:url].gsub(/\/$/, '')
|
79
|
+
lend = 0
|
80
|
+
url.scan(/#\{(?<key>.*?)\}/) do |key|
|
81
|
+
m = Regexp.last_match
|
82
|
+
between = url[lend..m.begin(0)-1]
|
83
|
+
bits << between if between.present? && (m.begin(0) > lend)
|
84
|
+
lend = m.end(0)
|
85
|
+
bits << (url_context[m[:key].to_sym] || /[\w:]+/)
|
86
|
+
end
|
87
|
+
between = url[lend..-1]
|
88
|
+
bits << between if between.present?
|
89
|
+
|
90
|
+
url = bits.map do |bit|
|
91
|
+
next bit.source if bit.is_a?(Regexp)
|
92
|
+
bit = bit.canvas_id if bit.respond_to?(:canvas_id)
|
93
|
+
Regexp.escape(bit.to_s)
|
94
|
+
end.join
|
95
|
+
|
96
|
+
{ method: method == :auto ? (match ? match[:method].to_sym : :get) : method, url: url }
|
97
|
+
end
|
98
|
+
when String
|
99
|
+
raise "Cannot use method :auto when passing string endpoint" if method == :auto
|
100
|
+
{ method: method, url: Regexp.escape(endpoint) }
|
101
|
+
when Regexp
|
102
|
+
raise "Cannot use method :auto when passing regex endpoint" if method == :auto
|
103
|
+
{ method: method, url: Regexp.escape(endpoint) }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def _bearcat_resolve_prefix(prefix)
|
108
|
+
if prefix == true
|
109
|
+
prefix = canvas_api_client if defined? canvas_api_client
|
110
|
+
prefix = canvas_sync_client if defined? canvas_sync_client
|
111
|
+
end
|
112
|
+
prefix = case prefix
|
113
|
+
when nil
|
114
|
+
''
|
115
|
+
when false
|
116
|
+
''
|
117
|
+
when true
|
118
|
+
raise "stub_bearcat() prefix: set to true, but neither canvas_(sync|api)_client are defined"
|
119
|
+
when Bearcat::Client
|
120
|
+
prefix.prefix
|
121
|
+
when String
|
122
|
+
prefix
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
data/lib/bearcat/version.rb
CHANGED
data/lib/bearcat.rb
CHANGED
@@ -1,2 +1,51 @@
|
|
1
1
|
require 'bearcat/version'
|
2
2
|
require 'bearcat/client'
|
3
|
+
require 'bearcat/redis_connection'
|
4
|
+
|
5
|
+
module Bearcat
|
6
|
+
class << self
|
7
|
+
require 'logger'
|
8
|
+
attr_accessor :master_rate_limit
|
9
|
+
attr_writer :rate_limit_min, :rate_limits, :max_sleep_seconds,
|
10
|
+
:min_sleep_seconds, :logger, :rate_limiter
|
11
|
+
|
12
|
+
def configure
|
13
|
+
yield self if block_given?
|
14
|
+
end
|
15
|
+
|
16
|
+
def rate_limiter
|
17
|
+
@rate_limiter
|
18
|
+
end
|
19
|
+
|
20
|
+
def rate_limit_min
|
21
|
+
@rate_limit_min ||= 175
|
22
|
+
end
|
23
|
+
|
24
|
+
def min_sleep_seconds
|
25
|
+
@min_sleep_seconds ||= 5
|
26
|
+
end
|
27
|
+
|
28
|
+
def max_sleep_seconds
|
29
|
+
@max_sleep_seconds ||= 60
|
30
|
+
end
|
31
|
+
|
32
|
+
def logger
|
33
|
+
return @logger if defined? @logger
|
34
|
+
@logger = Logger.new(STDOUT)
|
35
|
+
@logger.level = Logger::DEBUG
|
36
|
+
@logger
|
37
|
+
end
|
38
|
+
|
39
|
+
def redis_pool
|
40
|
+
require 'bearcat/redis_connection'
|
41
|
+
@redis_pool ||= Bearcat::RedisConnection.create(env_prefix: "BEARCAT")
|
42
|
+
end
|
43
|
+
|
44
|
+
def redis
|
45
|
+
raise ArgumentError, "requires a block" unless block_given?
|
46
|
+
redis_pool.with do |conn|
|
47
|
+
yield conn
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,22 @@
|
|
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
|
+
key = 'orders' if path =~ %r{.*/orders}
|
15
|
+
key = 'completed_certificates' if path =~ %r{.*/completed_certificates}
|
16
|
+
key = 'user_registrations' if path =~ %r{.*/user_registrations}
|
17
|
+
key = 'email_domain_sets' if path =~ %r{.*/email_domain_sets}
|
18
|
+
end
|
19
|
+
key.present? ? key : super(response)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|