aspire 0.1.0
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/.gitignore +59 -0
- data/.rbenv-gemsets +1 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Dockerfile +20 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +851 -0
- data/Rakefile +10 -0
- data/aspire.gemspec +40 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/entrypoint.sh +11 -0
- data/exe/build-cache +13 -0
- data/lib/aspire.rb +11 -0
- data/lib/aspire/api.rb +2 -0
- data/lib/aspire/api/base.rb +198 -0
- data/lib/aspire/api/json.rb +195 -0
- data/lib/aspire/api/linked_data.rb +214 -0
- data/lib/aspire/caching.rb +4 -0
- data/lib/aspire/caching/builder.rb +356 -0
- data/lib/aspire/caching/cache.rb +365 -0
- data/lib/aspire/caching/cache_entry.rb +296 -0
- data/lib/aspire/caching/cache_logger.rb +63 -0
- data/lib/aspire/caching/util.rb +210 -0
- data/lib/aspire/cli/cache_builder.rb +123 -0
- data/lib/aspire/cli/command.rb +20 -0
- data/lib/aspire/enumerator/base.rb +29 -0
- data/lib/aspire/enumerator/json_enumerator.rb +130 -0
- data/lib/aspire/enumerator/linked_data_uri_enumerator.rb +32 -0
- data/lib/aspire/enumerator/report_enumerator.rb +64 -0
- data/lib/aspire/exceptions.rb +36 -0
- data/lib/aspire/object.rb +7 -0
- data/lib/aspire/object/base.rb +155 -0
- data/lib/aspire/object/digitisation.rb +43 -0
- data/lib/aspire/object/factory.rb +87 -0
- data/lib/aspire/object/list.rb +590 -0
- data/lib/aspire/object/module.rb +36 -0
- data/lib/aspire/object/resource.rb +371 -0
- data/lib/aspire/object/time_period.rb +47 -0
- data/lib/aspire/object/user.rb +46 -0
- data/lib/aspire/properties.rb +20 -0
- data/lib/aspire/user_lookup.rb +103 -0
- data/lib/aspire/util.rb +185 -0
- data/lib/aspire/version.rb +3 -0
- data/lib/retry.rb +197 -0
- metadata +274 -0
data/Rakefile
ADDED
data/aspire.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'aspire/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'aspire'
|
9
|
+
spec.version = Aspire::VERSION
|
10
|
+
spec.authors = ['Lancaster University Library']
|
11
|
+
spec.email = ['library.dit@lancaster.ac.uk']
|
12
|
+
|
13
|
+
spec.summary = 'Ruby interface to the Talis Aspire API'
|
14
|
+
spec.description = 'This gem provides a Ruby interface for working with' \
|
15
|
+
'the Talis Aspire API.'
|
16
|
+
spec.homepage = 'https://github.com/lulibrary/aspire'
|
17
|
+
spec.license = 'MIT'
|
18
|
+
|
19
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
20
|
+
f.match(%r{^(test|spec|features)/})
|
21
|
+
end
|
22
|
+
spec.bindir = 'exe'
|
23
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
|
+
spec.require_paths = ['lib']
|
25
|
+
|
26
|
+
spec.add_dependency 'logglier', '~> 0.2'
|
27
|
+
spec.add_dependency 'loofah', '~> 2.0'
|
28
|
+
spec.add_dependency 'rest-client', '~> 2.0'
|
29
|
+
spec.add_dependency 'sentry-raven', '~> 2.6'
|
30
|
+
spec.add_dependency 'clamp', '~> 1.1'
|
31
|
+
spec.add_dependency 'dotenv', '~> 2.2'
|
32
|
+
|
33
|
+
spec.add_development_dependency 'byebug', '~> 9.0'
|
34
|
+
spec.add_development_dependency 'bundler', '~> 1.14'
|
35
|
+
spec.add_development_dependency 'dotenv', '~> 2.2'
|
36
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
37
|
+
spec.add_development_dependency 'rubocop', '~> 0.49'
|
38
|
+
spec.add_development_dependency 'minitest', '~> 5.0'
|
39
|
+
spec.add_development_dependency 'minitest-reporters', '~> 1.1'
|
40
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "aspire"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/entrypoint.sh
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
|
3
|
+
TIME_PERIOD=${TIME_PERIOD:-''}
|
4
|
+
STATUS=${STATUS:-''}
|
5
|
+
PRIVACY_CONTROL=${PRIVACY_CONTROL:-''}
|
6
|
+
LIST_URL=${LIST_URL:-''}
|
7
|
+
LANG=en_US.UTF-8
|
8
|
+
LANGUAGE=en_US.UTF-8
|
9
|
+
LC_ALL=en_US.UTF-8
|
10
|
+
|
11
|
+
build-cache -t "${TIME_PERIOD}" -s "${STATUS}" -p "${PRIVACY_CONTROL}" -l "${LIST_URL}" -f
|
data/exe/build-cache
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'aspire'
|
5
|
+
require 'raven'
|
6
|
+
|
7
|
+
unless ENV['SENTRY_DSN'].nil? || ENV['SENTRY_DSN'].empty?
|
8
|
+
Raven.configure do |config|
|
9
|
+
config.dsn = ENV['SENTRY_DSN']
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
Aspire::CLI::CacheBuilder.run
|
data/lib/aspire.rb
ADDED
data/lib/aspire/api.rb
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
require 'rest-client'
|
5
|
+
|
6
|
+
require 'retry'
|
7
|
+
|
8
|
+
module Aspire
|
9
|
+
module API
|
10
|
+
# The base class for Aspire API wrappers
|
11
|
+
class Base
|
12
|
+
# Domain names
|
13
|
+
TALIS_DOMAIN = 'talis.com'.freeze
|
14
|
+
ASPIRE_DOMAIN = "rl.#{TALIS_DOMAIN}".freeze
|
15
|
+
ASPIRE_AUTH_DOMAIN = "users.#{TALIS_DOMAIN}".freeze
|
16
|
+
|
17
|
+
# The default URL scheme
|
18
|
+
SCHEME = 'http'.freeze
|
19
|
+
|
20
|
+
# SSL options
|
21
|
+
SSL_OPTS = %i[ssl_ca_file ssl_ca_path ssl_cert_store].freeze
|
22
|
+
|
23
|
+
# @!attribute [rw] logger
|
24
|
+
# @return [Logger] a logger for activity logging
|
25
|
+
attr_accessor :logger
|
26
|
+
|
27
|
+
# @!attribute [rw] ssl
|
28
|
+
# @return [Hash] SSL options
|
29
|
+
attr_accessor :ssl
|
30
|
+
|
31
|
+
# @!attribute [rw] tenancy_code
|
32
|
+
# @return [String] the Aspire short tenancy code
|
33
|
+
attr_accessor :tenancy_code
|
34
|
+
|
35
|
+
# @!attribute [rw] timeout
|
36
|
+
# @return [Integer] the timeout period in seconds for API calls
|
37
|
+
attr_accessor :timeout
|
38
|
+
|
39
|
+
# Initialises a new API instance
|
40
|
+
# @param tenancy_code [String]
|
41
|
+
# @option opts [Logger] :logger the API activity logger
|
42
|
+
# @option opts [Integer] :timeout the API timeout in seconds
|
43
|
+
# @option opts [String] :ssl_ca_file the certificate authority filename
|
44
|
+
# @option opts [String] :ssl_ca_path the certificate authority directory
|
45
|
+
# @option opts [String] :ssl_cert_store the certificate authority store
|
46
|
+
def initialize(tenancy_code, **opts)
|
47
|
+
self.logger = opts[:logger]
|
48
|
+
self.tenancy_code = tenancy_code
|
49
|
+
self.timeout = opts[:timeout] || 0
|
50
|
+
# Retry options
|
51
|
+
initialize_retry(opts)
|
52
|
+
# SSL options
|
53
|
+
initialize_ssl(opts)
|
54
|
+
# Set the RestClient logger
|
55
|
+
RestClient.log = logger if logger
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns a full API URL from a partial path
|
59
|
+
# @abstract Subclasses must implement this method
|
60
|
+
# @param path [String] a full or partial API URL
|
61
|
+
# @return [String] the full API URL
|
62
|
+
def api_url(path)
|
63
|
+
path
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
# Calls an Aspire API endpoint and processes the response.
|
69
|
+
# Keyword parameters are passed directly to the REST client
|
70
|
+
# @return [(RestClient::Response, Hash)] the REST client response and
|
71
|
+
# parsed JSON data from the response
|
72
|
+
def call_api(**rest_options)
|
73
|
+
@retry.do do
|
74
|
+
res = RestClient::Request.execute(**rest_options)
|
75
|
+
json = res && !res.empty? ? ::JSON.parse(res.to_s) : nil
|
76
|
+
call_api_response(res) if respond_to?(:call_api_response)
|
77
|
+
[res, json]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns the HTTP headers for an API call
|
82
|
+
# @param headers [Hash] optional headers to add to the API call
|
83
|
+
# @param params [Hash] query string parameters for the API call
|
84
|
+
# @return [Hash] the HTTP headers
|
85
|
+
def call_rest_headers(headers, params)
|
86
|
+
rest_headers = {}.merge(headers || {})
|
87
|
+
rest_headers[:params] = params if params && !params.empty?
|
88
|
+
rest_headers
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns the REST client options for an API call
|
92
|
+
# @param path [String] the path of the API call
|
93
|
+
# @param headers [Hash<String, String>] HTTP headers for the API call
|
94
|
+
# @param options [Hash<String, Object>] options for the REST client
|
95
|
+
# @param params [Hash<String, String>] query string parameters
|
96
|
+
# @param payload [String, nil] the data to post to the API call
|
97
|
+
# @return [Hash] the REST client options
|
98
|
+
def call_rest_options(path, headers: nil, options: nil, params: nil,
|
99
|
+
payload: nil)
|
100
|
+
rest_headers = call_rest_headers(headers, params)
|
101
|
+
rest_options = {
|
102
|
+
headers: rest_headers,
|
103
|
+
url: api_url(path)
|
104
|
+
}
|
105
|
+
common_rest_options(rest_options)
|
106
|
+
rest_options[:payload] = payload if payload
|
107
|
+
rest_options.merge!(options) if options
|
108
|
+
rest_options[:method] ||= payload ? :post : :get
|
109
|
+
rest_options
|
110
|
+
end
|
111
|
+
|
112
|
+
# Sets the REST client options common to all API calls
|
113
|
+
# @param rest_options [Hash] the REST client options
|
114
|
+
# @return [Hash] the REST client options
|
115
|
+
def common_rest_options(rest_options)
|
116
|
+
SSL_OPTS.each { |opt| rest_options[opt] = ssl[opt] if ssl[opt] }
|
117
|
+
rest_options[:timeout] = timeout > 0 ? timeout : nil
|
118
|
+
rest_options
|
119
|
+
end
|
120
|
+
|
121
|
+
# Initialises retry options
|
122
|
+
# @param opts [Hash] the options hash
|
123
|
+
# @return [void]
|
124
|
+
def initialize_retry(opts)
|
125
|
+
@retry = Retry::Engine.new(delay: opts[:retry_delay] || 5,
|
126
|
+
exceptions: initialize_retry_exceptions,
|
127
|
+
handlers: initialize_retry_handlers,
|
128
|
+
tries: opts[:retries] || 5)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns a hash of retriable exceptions
|
132
|
+
# @return [Hash<Exception|Symbol, Boolean>] the retriable exceptions
|
133
|
+
def initialize_retry_exceptions
|
134
|
+
[
|
135
|
+
RestClient::ExceptionWithResponse,
|
136
|
+
RestClient::ServerBrokeConnection,
|
137
|
+
RestClient::Exceptions::Timeout
|
138
|
+
].push(*Retry::Exceptions::SOCKET_EXCEPTIONS)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns a hash of retry handlers
|
142
|
+
# @return [Hash<Exception|Symbol, Proc>] the retry handlers
|
143
|
+
def initialize_retry_handlers
|
144
|
+
{
|
145
|
+
:default => proc { |e, _t| log_exception(e) },
|
146
|
+
:retry => proc { |_e, t| logger.debug("Retrying (#{t} tries left)") },
|
147
|
+
RestClient::ExceptionWithResponse => proc do |e, _t|
|
148
|
+
log_exception(e, debug: "Response: #{e}")
|
149
|
+
# json = ::JSON.parse(response.to_s) if response && !response.empty?
|
150
|
+
raise Retry::StopRetry.new([e.response, nil])
|
151
|
+
end
|
152
|
+
}
|
153
|
+
end
|
154
|
+
|
155
|
+
# Sets the SSL options
|
156
|
+
# @param opts [Hash] the options hash
|
157
|
+
def initialize_ssl(opts)
|
158
|
+
self.ssl = {}
|
159
|
+
SSL_OPTS.each { |opt| ssl[opt] = opts[opt] }
|
160
|
+
end
|
161
|
+
|
162
|
+
# Logs an exception
|
163
|
+
# @param e [Exception] the exception
|
164
|
+
# @param debug [String] extra debugging message
|
165
|
+
def log_exception(e, debug: nil)
|
166
|
+
return unless logger
|
167
|
+
logger.error(e.to_s)
|
168
|
+
logger.debug(e.backtrace.join('\n'))
|
169
|
+
logger.debug(debug) if debug
|
170
|
+
end
|
171
|
+
|
172
|
+
# Returns a URI instance for a URL, treating URLs without schemes or path
|
173
|
+
# components as host names
|
174
|
+
# @param url [String] the URL
|
175
|
+
# @return [URI] the URI instance
|
176
|
+
def uri(url)
|
177
|
+
url = URI.parse(url) unless url.is_a?(URI)
|
178
|
+
if url.host.nil? && url.scheme.nil?
|
179
|
+
url.host = url.path
|
180
|
+
url.path = ''
|
181
|
+
end
|
182
|
+
url.scheme ||= 'http'
|
183
|
+
url
|
184
|
+
rescue URI::InvalidComponentError, URI::InvalidURIError
|
185
|
+
nil
|
186
|
+
end
|
187
|
+
|
188
|
+
# Returns the host name of a URL, treating URLs without schemes or path
|
189
|
+
# components as host names
|
190
|
+
# @param url [String] the URL
|
191
|
+
# @return [String] the host name of the URL
|
192
|
+
def uri_host(url)
|
193
|
+
url = uri(url)
|
194
|
+
url ? url.host : nil
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module Aspire
|
6
|
+
module API
|
7
|
+
# A wrapper class for the Aspire JSON API
|
8
|
+
class JSON < Base
|
9
|
+
# The default API root URL
|
10
|
+
API_ROOT = "https://#{ASPIRE_DOMAIN}".freeze
|
11
|
+
|
12
|
+
# The default authentication API root URL
|
13
|
+
API_ROOT_AUTH = "https://#{ASPIRE_AUTH_DOMAIN}/1/oauth/tokens".freeze
|
14
|
+
|
15
|
+
# @!attribute [rw] api_root
|
16
|
+
# @return [String] the base URL of the Aspire JSON APIs
|
17
|
+
attr_accessor :api_root
|
18
|
+
|
19
|
+
# @!attribute [rw] api_root_auth
|
20
|
+
# @return [String] the base URL of the Aspire Persona authentication API
|
21
|
+
attr_accessor :api_root_auth
|
22
|
+
|
23
|
+
# @!attribute [rw] api_version
|
24
|
+
# @return [Integer] the version of the Aspire JSON APIs
|
25
|
+
attr_accessor :api_version
|
26
|
+
|
27
|
+
# @!attribute [rw] rate_limit
|
28
|
+
# @return [Integer] the rate limit value from the most recent API call
|
29
|
+
attr_accessor :rate_limit
|
30
|
+
|
31
|
+
# @!attribute [rw] rate_remaining
|
32
|
+
# @return [Integer] the rate remaining value from the most recent API
|
33
|
+
# call (the number of calls remaining within the current limit period)
|
34
|
+
attr_accessor :rate_remaining
|
35
|
+
|
36
|
+
# @!attribute [rw] rate_reset
|
37
|
+
# @return [Integer] the rate reset value from the most recent API call
|
38
|
+
# (the time in seconds since the Epoch until the next limit period)
|
39
|
+
attr_accessor :rate_reset
|
40
|
+
|
41
|
+
# Initialises a new API instance
|
42
|
+
# @param api_client_id [String] the API client ID
|
43
|
+
# @param api_secret [String] the API secret associated with the client ID
|
44
|
+
# @param tenancy_code [String] the Aspire short tenancy code
|
45
|
+
# @param opts [Hash] API customisation options
|
46
|
+
# @option opts [String] :api_root the base URL of the Aspire JSON APIs
|
47
|
+
# @option opts [String] :api_root_auth the base URL of the Aspire Persona
|
48
|
+
# authentication API
|
49
|
+
# @option opts [Integer] :api_version the version of the Aspire JSON APIs
|
50
|
+
# @option opts [Logger] :logger a logger for activity logging
|
51
|
+
# @option opts [Integer] :timeout the API call timeout period in seconds
|
52
|
+
# @return [void]
|
53
|
+
def initialize(api_client_id = nil, api_secret = nil, tenancy_code = nil,
|
54
|
+
**opts)
|
55
|
+
super(tenancy_code, **opts)
|
56
|
+
@api_client_id = api_client_id
|
57
|
+
@api_secret = api_secret
|
58
|
+
@api_token = nil
|
59
|
+
self.api_root = opts[:api_root] || API_ROOT
|
60
|
+
self.api_root_auth = opts[:api_root_auth] || API_ROOT_AUTH
|
61
|
+
self.api_version = opts[:api_version] || 2
|
62
|
+
rate_limit
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns a full Aspire JSON API URL. Full URLs are returned as-is,
|
66
|
+
# partial endpoint paths are expanded with the API root, version and
|
67
|
+
# tenancy code.
|
68
|
+
# @param path [String] the full URL or partial endpoint path
|
69
|
+
# @return [String] the full JSON API URL
|
70
|
+
def api_url(path)
|
71
|
+
return path if path.include?('//')
|
72
|
+
"#{api_root}/#{api_version}/#{tenancy_code}/#{path}"
|
73
|
+
end
|
74
|
+
|
75
|
+
# Calls an Aspire JSON API method and returns the parsed JSON response
|
76
|
+
# Additional keyword parameters are passed as query string parameters
|
77
|
+
# to the API call.
|
78
|
+
# @param path [String] the path of the API call
|
79
|
+
# @param headers [Hash<String, String>] HTTP headers for the API call
|
80
|
+
# @param options [Hash<String, Object>] options for the REST client
|
81
|
+
# @param payload [String, nil] the data to post to the API call
|
82
|
+
# @return [Hash] the parsed JSON content from the API response
|
83
|
+
# @yield [response, data] Passes the REST client response and parsed JSON
|
84
|
+
# hash to the block
|
85
|
+
# @yieldparam [RestClient::Response] the REST client response
|
86
|
+
# @yieldparam [Hash] the parsed JSON data from the response
|
87
|
+
def call(path, headers: nil, options: nil, payload: nil, **params)
|
88
|
+
rest_options = call_rest_options(path,
|
89
|
+
headers: headers, options: options,
|
90
|
+
payload: payload, params: params)
|
91
|
+
response, data = call_api_with_auth(**rest_options)
|
92
|
+
yield(response, data) if block_given?
|
93
|
+
data
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
# Returns an Aspire OAuth API token. New tokens are retrieved from the
|
99
|
+
# Aspire Persona API and cached for subsequent API calls.
|
100
|
+
# @param refresh [Boolean] if true, force retrieval of a new token
|
101
|
+
# @return [String] the API token
|
102
|
+
def api_token(refresh = false)
|
103
|
+
# Return the cached token unless forcing a refresh
|
104
|
+
return @api_token unless @api_token.nil? || refresh
|
105
|
+
# Set the token to nil to indicate that there is no current valid token
|
106
|
+
# in case an exception is thrown by the API call.
|
107
|
+
@api_token = nil
|
108
|
+
# Get and return the API token
|
109
|
+
_response, data = call_api(**api_token_rest_options)
|
110
|
+
@api_token = data['access_token']
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns the HTTP Basic authentication token
|
114
|
+
# @return [String] the Basic authentication token
|
115
|
+
def api_token_authorization
|
116
|
+
Base64.strict_encode64("#{@api_client_id}:#{@api_secret}")
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns the HTTP headers for the token retrieval API call
|
120
|
+
def api_token_rest_headers
|
121
|
+
{
|
122
|
+
Authorization: "basic #{api_token_authorization}",
|
123
|
+
'Content-Type'.to_sym => 'application/x-www-form-urlencoded'
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
def api_token_rest_options
|
128
|
+
rest_options = {
|
129
|
+
headers: api_token_rest_headers,
|
130
|
+
payload: { grant_type: 'client_credentials' },
|
131
|
+
url: api_root_auth
|
132
|
+
}
|
133
|
+
common_rest_options(rest_options)
|
134
|
+
rest_options[:method] = :post
|
135
|
+
rest_options
|
136
|
+
end
|
137
|
+
|
138
|
+
# Returns true if the HTTP response is an authentication failure
|
139
|
+
# @param response [RestClient::Response] the REST client response
|
140
|
+
# @return [Boolean] true on authentication failure, false otherwise
|
141
|
+
def auth_failed(response)
|
142
|
+
response && response.code == 401
|
143
|
+
end
|
144
|
+
|
145
|
+
# Performs custom HTTP response processing
|
146
|
+
# @param response [RestClient::Response] the REST client response
|
147
|
+
# @return [void]
|
148
|
+
def call_api_response(response)
|
149
|
+
rate_limit(headers: response.headers)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Calls an authenticated Aspire API endpoint and processes the response.
|
153
|
+
# The call is made first with the currently-cached authentication token.
|
154
|
+
# If this fails due to authentication, the token is refreshed and the call
|
155
|
+
# is repeated once with the new token.
|
156
|
+
# @see (#call_api)
|
157
|
+
def call_api_with_auth(**rest_options)
|
158
|
+
refresh = false
|
159
|
+
loop do
|
160
|
+
token = api_token(refresh)
|
161
|
+
rest_options[:headers]['Authorization'] = "Bearer #{token}"
|
162
|
+
response, data = call_api(**rest_options)
|
163
|
+
# Stop if we have a valid response or we've already tried to refresh
|
164
|
+
# the token.
|
165
|
+
return response, data unless auth_failed(response) && !refresh
|
166
|
+
# The API token may have expired, try one more time with a new token
|
167
|
+
refresh = true
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# Sets API rate-limit parameters
|
172
|
+
# @param headers [Hash] the HTTP response headers
|
173
|
+
# @param limit [Integer] the default rate limit
|
174
|
+
# @param remaining [Integer] the default remaining count
|
175
|
+
# @param reset [Integer] the default reset period timestamp
|
176
|
+
# @return [nil]
|
177
|
+
def rate_limit(headers: {}, limit: nil, remaining: nil, reset: nil)
|
178
|
+
reset = rate_limit_header(:reset, headers, reset)
|
179
|
+
self.rate_limit = rate_limit_header(:limit, headers, limit)
|
180
|
+
self.rate_remaining = rate_limit_header(:remaining, headers, remaining)
|
181
|
+
self.rate_reset = reset ? Time(reset) : nil
|
182
|
+
end
|
183
|
+
|
184
|
+
# Returns the numeric value of a rate-limit header
|
185
|
+
# @param header [String, Symbol] the header (minus x_ratelimit_ prefix)
|
186
|
+
# @param headers [Hash] the HTTP response headers
|
187
|
+
# @param default [Integer, nil] the default value if the header is missing
|
188
|
+
# @return [Integer, nil] the numeric value of the header
|
189
|
+
def rate_limit_header(header, headers, default)
|
190
|
+
value = headers["x_ratelimit_#{header}".to_sym]
|
191
|
+
value ? value.to_i : default
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|