globus_client 0.12.1 → 0.13.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 65d27626412e2eff8ff6514be3ece81fd5f27e471137fcfd8409e51cd6e0f105
4
- data.tar.gz: 321b1d89734daacb8ad54487836a68f1fdf1af931fad426d72ec35bc2f355ec8
3
+ metadata.gz: 9129e55c5e5e6f4f62fc0d8912eca9310eff036e1c0095d24cae07367947054c
4
+ data.tar.gz: 1265f2f6c1b68bb6d76634f7bd636ed920b1095fd3bfd5ab6702525d994bf295
5
5
  SHA512:
6
- metadata.gz: a4ce82aac85e8c00df5310cdb3b4f821cd1415ee0d525938562b20d97c86f330707691a8ee69dcbfeee53d0e20064f78fd902678d5aa4a0113cc7ce38f209c9a
7
- data.tar.gz: 75888830f1b0660f78d551eb3fd2a323f40fb21e5ac9a361c0f8ea22be0b32a11a014dd42719e4b25c33f5f497e2d1b6b296c8db15f5dc89abaed5d4f03ff5e1
6
+ metadata.gz: 407e55c907f990f0492898e636525c8238221167fabf6d0b4bccef0c9eb163f0d75b5bb3b05b58130be46bb823b809a524490f762cbd5b3c56143e2abf9fd7c4
7
+ data.tar.gz: e7300d3adbaa6b75d43cafe1faae5eccfc212f558ce7d38889438d3ceaac99becc55242d2a10f2ec3eba759d2038649fe2b80585e97cba4e58ebd215b4767594
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- globus_client (0.12.1)
4
+ globus_client (0.13.0)
5
5
  activesupport (>= 4.2, < 8)
6
6
  faraday
7
7
  faraday-retry
@@ -76,14 +76,14 @@ GEM
76
76
  unicode-display_width (>= 2.4.0, < 3.0)
77
77
  rubocop-ast (1.29.0)
78
78
  parser (>= 3.2.1.0)
79
- rubocop-capybara (2.18.0)
79
+ rubocop-capybara (2.19.0)
80
80
  rubocop (~> 1.41)
81
81
  rubocop-factory_bot (2.24.0)
82
82
  rubocop (~> 1.33)
83
83
  rubocop-performance (1.19.1)
84
84
  rubocop (>= 1.7.0, < 2.0)
85
85
  rubocop-ast (>= 0.4.0)
86
- rubocop-rspec (2.24.0)
86
+ rubocop-rspec (2.24.1)
87
87
  rubocop (~> 1.33)
88
88
  rubocop-capybara (~> 2.17)
89
89
  rubocop-factory_bot (~> 2.22)
data/api_test.rb CHANGED
@@ -27,7 +27,7 @@ Benchmark.bm(20) do |benchmark|
27
27
 
28
28
  benchmark.report("before_perms:") do
29
29
  # Not part of the public API but this allows us to test access changes
30
- @before_permissions = GlobusClient::Endpoint.new(GlobusClient.config, user_id:, path:).send(:access_rule)["permissions"]
30
+ @before_permissions = GlobusClient::Endpoint.new(GlobusClient.instance, user_id:, path:).send(:access_rule)["permissions"]
31
31
  end
32
32
 
33
33
  benchmark.report("has_files?:") do
@@ -48,7 +48,7 @@ Benchmark.bm(20) do |benchmark|
48
48
 
49
49
  benchmark.report("after_perms:") do
50
50
  # Not part of the public API but this allows us to test access changes
51
- @after_permissions = GlobusClient::Endpoint.new(GlobusClient.config, user_id:, path:).send(:access_rule)["permissions"]
51
+ @after_permissions = GlobusClient::Endpoint.new(GlobusClient.instance, user_id:, path:).send(:access_rule)["permissions"]
52
52
  end
53
53
 
54
54
  puts "User #{user_id} exists: #{@user_exists}"
@@ -17,6 +17,8 @@ class GlobusClient
17
17
  def token
18
18
  response = connection.post("/v2/oauth2/token", form_data)
19
19
 
20
+ UnexpectedResponse.call(response) unless response.success?
21
+
20
22
  JSON.parse(response.body)["access_token"]
21
23
  end
22
24
 
@@ -7,11 +7,11 @@ class GlobusClient
7
7
 
8
8
  FileInfo = Struct.new(:name, :size)
9
9
 
10
- # @param config [#token, #uploads_directory, #transfer_endpoint_id, #transfer_url, #auth_url] configuration for the gem
10
+ # @param client [GlobusClient] a configured instance of the GlobusClient
11
11
  # @param path [String] the path to operate on
12
12
  # @param user_id [String] a Globus user ID (e.g., a @stanford.edu email address)
13
- def initialize(config, path:, user_id:)
14
- @config = config
13
+ def initialize(client, path:, user_id:)
14
+ @client = client
15
15
  @user_id = user_id
16
16
  @path = path
17
17
  end
@@ -30,23 +30,12 @@ class GlobusClient
30
30
  def mkdir
31
31
  # transfer API does not support recursive directory creation
32
32
  paths.each do |path|
33
- response = connection.post("#{transfer_path}/mkdir") do |req|
34
- req.headers["Content-Type"] = "application/json"
35
- req.body = {
36
- DATA_TYPE: "mkdir",
37
- path:
38
- }.to_json
39
- end
40
-
41
- next if response.success?
42
-
43
- # Ignore error if directory already exists
44
- if response.status == 502
45
- error = JSON.parse(response.body)
46
- next if error["code"] == "ExternalError.MkdirFailed.Exists"
47
- end
48
-
49
- UnexpectedResponse.call(response)
33
+ client.post(
34
+ base_url: client.config.transfer_url,
35
+ path: "#{transfer_path}/mkdir",
36
+ body: {DATA_TYPE: "mkdir", path:},
37
+ expected_response: ->(resp) { resp.status == 502 && JSON.parse(resp.body)["code"] == "ExternalError.MkdirFailed.Exists" }
38
+ )
50
39
  end
51
40
  end
52
41
 
@@ -64,35 +53,18 @@ class GlobusClient
64
53
  def delete_access_rule
65
54
  raise(StandardError, "Access rule not found for #{path}") if !access_rule_id
66
55
 
67
- response = connection.delete("#{access_path}/#{access_rule_id}")
68
- return true if response.success?
69
-
70
- UnexpectedResponse.call(response)
56
+ client.delete(
57
+ base_url: client.config.transfer_url,
58
+ path: "#{access_path}/#{access_rule_id}"
59
+ )
71
60
  end
72
61
 
73
62
  private
74
63
 
75
- attr_reader :config, :path, :user_id
76
-
77
- def connection
78
- # faraday/retry is used here to catch Faraday::ConnectionFailed exceptions
79
- # see: https://github.com/sul-dlss/happy-heron/issues/3008
80
- @connection ||= Faraday.new(
81
- url: config.transfer_url,
82
- headers: {Authorization: "Bearer #{config.token}"}
83
- ) do |faraday|
84
- faraday.request :retry, {
85
- max: 10,
86
- interval: 0.05,
87
- interval_randomness: 0.5,
88
- backoff_factor: 2,
89
- exceptions: Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS + [Faraday::ConnectionFailed]
90
- }
91
- end
92
- end
64
+ attr_reader :client, :path, :user_id
93
65
 
94
66
  def globus_identity_id
95
- Identity.new(config).get_identity_id(user_id)
67
+ Identity.new(client).get_identity_id(user_id)
96
68
  end
97
69
 
98
70
  # Builds up a path from a list of path elements. E.g., input would look like:
@@ -102,7 +74,7 @@ class GlobusClient
102
74
  def paths
103
75
  @paths ||= path_segments.map.with_index do |_segment, index|
104
76
  File
105
- .join(config.uploads_directory, path_segments.slice(..index))
77
+ .join(client.config.uploads_directory, path_segments.slice(..index))
106
78
  .concat(PATH_SEPARATOR)
107
79
  end
108
80
  end
@@ -123,18 +95,21 @@ class GlobusClient
123
95
  # @param files [Array<FileInfo>] an array of FileInfo structs, each of which has a name and a size
124
96
  # @param return_presence [Boolean] if true, return a boolean to indicate if any files at all are present, short-circuiting the recursive operation
125
97
  def ls_path(filepath, files, return_presence: false)
126
- response = connection.get("#{transfer_path}/ls?path=#{CGI.escape(filepath)}")
127
- return UnexpectedResponse.call(response) unless response.success?
98
+ response = client.get(
99
+ base_url: client.config.transfer_url,
100
+ path: "#{transfer_path}/ls",
101
+ params: {path: filepath}
102
+ )
128
103
 
129
- data = JSON.parse(response.body)["DATA"]
130
- data
104
+ response["DATA"]
131
105
  .select { |object| object["type"] == "file" }
132
106
  .each do |file|
133
107
  return true if return_presence
134
108
 
135
109
  files << FileInfo.new("#{filepath}#{file["name"]}", file["size"])
136
110
  end
137
- data
111
+
112
+ response["DATA"]
138
113
  .select { |object| object["type"] == "dir" }
139
114
  .each do |dir|
140
115
  # NOTE: This allows the recursive method to short-circuit iff ls_path
@@ -149,62 +124,45 @@ class GlobusClient
149
124
  end
150
125
 
151
126
  def access_request(permissions:)
152
- response = if access_rule_id
153
- connection.put("#{access_path}/#{access_rule_id}") do |req|
154
- req.body = {
155
- DATA_TYPE: "access",
156
- permissions:
157
- }.to_json
158
- req.headers["Content-Type"] = "application/json"
159
- end
127
+ if access_rule_id
128
+ update_access_request(permissions:)
160
129
  else
161
- connection.post(access_path) do |req|
162
- req.body = {
130
+ client.post(
131
+ base_url: client.config.transfer_url,
132
+ path: access_path,
133
+ body: {
163
134
  DATA_TYPE: "access",
164
135
  principal_type: "identity",
165
136
  principal: globus_identity_id,
166
137
  path: full_path,
167
138
  permissions:,
168
139
  notify_email: user_id
169
- }.to_json
170
- req.headers["Content-Type"] = "application/json"
171
- end
140
+ }
141
+ )
172
142
  end
173
-
174
- return true if response.success?
175
-
176
- UnexpectedResponse.call(response)
177
143
  end
178
144
 
179
145
  def update_access_request(permissions:)
180
146
  raise(StandardError, "Access rule not found for #{path}") if !access_rule_id
181
147
 
182
- response = connection.put("#{access_path}/#{access_rule_id}") do |req|
183
- req.body = {
148
+ client.put(
149
+ base_url: client.config.transfer_url,
150
+ path: "#{access_path}/#{access_rule_id}",
151
+ body: {
184
152
  DATA_TYPE: "access",
185
153
  permissions:
186
- }.to_json
187
- req.headers["Content-Type"] = "application/json"
188
- end
189
-
190
- return true if response.success?
191
-
192
- UnexpectedResponse.call(response)
154
+ }
155
+ )
193
156
  end
194
157
 
195
158
  def access_rule
196
- response = connection.get(access_list_path) do |req|
197
- req.headers["Content-Type"] = "application/json"
198
- end
199
-
200
- # debugging of Globus responses
201
- response_body = JSON.parse(response.body)
202
-
203
- UnexpectedResponse.call(response, message: "Response is missing DATA in: #{response_body}") unless response.success? && response_body.key?("DATA")
159
+ response = client.get(
160
+ base_url: client.config.transfer_url,
161
+ path: access_list_path,
162
+ content_type: "application/json"
163
+ )
204
164
 
205
- JSON
206
- .parse(response.body)["DATA"]
207
- .find { |acl| acl["path"] == full_path }
165
+ response.fetch("DATA").find { |acl| acl["path"] == full_path }
208
166
  end
209
167
 
210
168
  def access_rule_id
@@ -212,15 +170,15 @@ class GlobusClient
212
170
  end
213
171
 
214
172
  def transfer_path
215
- "/v0.10/operation/endpoint/#{config.transfer_endpoint_id}"
173
+ "/v0.10/operation/endpoint/#{client.config.transfer_endpoint_id}"
216
174
  end
217
175
 
218
176
  def access_path
219
- "/v0.10/endpoint/#{config.transfer_endpoint_id}/access"
177
+ "/v0.10/endpoint/#{client.config.transfer_endpoint_id}/access"
220
178
  end
221
179
 
222
180
  def access_list_path
223
- "/v0.10/endpoint/#{config.transfer_endpoint_id}/access_list"
181
+ "/v0.10/endpoint/#{client.config.transfer_endpoint_id}/access_list"
224
182
  end
225
183
  end
226
184
  end
@@ -3,19 +3,20 @@
3
3
  class GlobusClient
4
4
  # Lookup of a Globus identity ID
5
5
  class Identity
6
- def initialize(config)
7
- @config = config
6
+ def initialize(client)
7
+ @client = client
8
8
  end
9
9
 
10
10
  # @param user_id [String] the username in the form of an email addresss
11
11
  # @return [Hash] id and status of Globus identity
12
12
  def get_identity(user_id)
13
- @email = user_id
14
- response = lookup_identity
15
- UnexpectedResponse.call(response) unless response.success?
13
+ response = client.get(
14
+ base_url: client.config.auth_url,
15
+ path: "/v2/api/identities",
16
+ params: {usernames: user_id}
17
+ )
16
18
 
17
- data = JSON.parse(response.body)
18
- extract_id(data)
19
+ response["identities"].find { |id| id["username"] == user_id }
19
20
  end
20
21
 
21
22
  # @param user_id [String] the username in the form of an email addresss
@@ -32,23 +33,6 @@ class GlobusClient
32
33
 
33
34
  private
34
35
 
35
- attr_reader :config
36
-
37
- def connection
38
- Faraday.new(url: config.auth_url)
39
- end
40
-
41
- def lookup_identity
42
- id_endpoint = "/v2/api/identities"
43
- connection.get(id_endpoint) do |req|
44
- req.params["usernames"] = @email
45
- req.headers["Authorization"] = "Bearer #{config.token}"
46
- end
47
- end
48
-
49
- def extract_id(data)
50
- identities = data["identities"]
51
- identities.find { |id| id["username"] == @email }
52
- end
36
+ attr_reader :client
53
37
  end
54
38
  end
@@ -26,7 +26,7 @@ class GlobusClient
26
26
  # https://docs.globus.org/api/transfer/file_operations/#errors
27
27
  # https://docs.globus.org/api/transfer/acl/#common_errors
28
28
  # https://docs.globus.org/api/auth/reference/
29
- def self.call(response, message: "")
29
+ def self.call(response)
30
30
  case response.status
31
31
  when 400
32
32
  raise BadRequestError, "Invalid path or another error with the request: #{response.body}"
@@ -37,11 +37,11 @@ class GlobusClient
37
37
  when 404
38
38
  raise ResourceNotFound, "Endpoint ID not found or resource does not exist: #{response.body}"
39
39
  when 502
40
- raise EndpointError, "Other error with endpoint: #{response.status} #{response.body}. #{message}"
40
+ raise EndpointError, "Other error with endpoint: #{response.status} #{response.body}."
41
41
  when 503
42
42
  raise ServiceUnavailable, "The service is down for maintenance."
43
43
  else
44
- raise StandardError, "Unexpected response: #{response.status} #{response.body}. #{message}"
44
+ raise StandardError, "Unexpected response: #{response.status} #{response.body}."
45
45
  end
46
46
  end
47
47
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class GlobusClient
4
- VERSION = "0.12.1"
4
+ VERSION = "0.13.0"
5
5
  end
data/lib/globus_client.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/module/delegation"
4
+ require "active_support/core_ext/object/blank"
4
5
  require "faraday"
5
6
  require "faraday/retry"
6
7
  require "ostruct"
@@ -24,15 +25,15 @@ class GlobusClient
24
25
  def configure(client_id:, client_secret:, uploads_directory:, transfer_endpoint_id:, transfer_url: default_transfer_url, auth_url: default_auth_url)
25
26
  instance.config = OpenStruct.new(
26
27
  # For the initial token, use a dummy value to avoid hitting any APIs
27
- # during configuration, allowing the `TokenWrapper` to handle auto-magic
28
- # token refreshing. Why not immediately get a valid token? Our apps
28
+ # during configuration, allowing `with_token_refresh_when_unauthorized` to handle
29
+ # auto-magic token refreshing. Why not immediately get a valid token? Our apps
29
30
  # commonly invoke client `.configure` methods in the initializer in all
30
31
  # application environments, even those that are never expected to
31
32
  # connect to production APIs, such as local development machines.
32
33
  #
33
34
  # NOTE: `nil` and blank string cannot be used as dummy values here as
34
35
  # they lead to a malformed request to be sent, which triggers an
35
- # exception not rescued by `TokenWrapper`
36
+ # exception not rescued by `with_token_refresh_when_unauthorized`
36
37
  token: "a temporary dummy token to avoid hitting the API before it is needed",
37
38
  client_id:,
38
39
  client_secret:,
@@ -46,7 +47,7 @@ class GlobusClient
46
47
  end
47
48
 
48
49
  delegate :config, :disallow_writes, :delete_access_rule, :file_count, :list_files, :mkdir, :total_size,
49
- :user_valid?, :get_filenames, :has_files?, to: :instance
50
+ :user_valid?, :get_filenames, :has_files?, :delete, :get, :post, :put, to: :instance
50
51
 
51
52
  def default_transfer_url
52
53
  "https://transfer.api.globusonline.org"
@@ -59,70 +60,176 @@ class GlobusClient
59
60
 
60
61
  attr_accessor :config
61
62
 
63
+ # Send an authenticated GET request
64
+ # @param base_url [String] the base URL of the Globus API
65
+ # @param path [String] the path to the Globus API request
66
+ # @param params [Hash] params to get to the API
67
+ def get(base_url:, path:, params: {}, content_type: nil)
68
+ response = with_token_refresh_when_unauthorized do
69
+ connection(base_url).get(path, params) do |request|
70
+ request.headers["Authorization"] = "Bearer #{config.token}"
71
+ request.headers["Content-Type"] = content_type if content_type
72
+ end
73
+ end
74
+
75
+ UnexpectedResponse.call(response) unless response.success?
76
+
77
+ return nil if response.body.blank?
78
+
79
+ JSON.parse(response.body)
80
+ end
81
+
82
+ # Send an authenticated POST request
83
+ # @param base_url [String] the base URL of the Globus API
84
+ # @param path [String] the path to the Globus API request
85
+ # @param body [String] the body of the Globus API request
86
+ # @param expected_response [#call] an expected response handler to allow short-circuiting the unexpected response
87
+ def post(base_url:, path:, body:, expected_response: ->(resp) { false })
88
+ response = with_token_refresh_when_unauthorized do
89
+ connection(base_url).post(path) do |request|
90
+ request.headers["Authorization"] = "Bearer #{config.token}"
91
+ request.headers["Content-Type"] = "application/json"
92
+ request.body = body.to_json
93
+ end
94
+ end
95
+
96
+ UnexpectedResponse.call(response) unless response.success? || expected_response.call(response)
97
+
98
+ return nil if response.body.blank?
99
+
100
+ JSON.parse(response.body)
101
+ end
102
+
103
+ # Send an authenticated PUT request
104
+ # @param base_url [String] the base URL of the Globus API
105
+ # @param path [String] the path to the Globus API request
106
+ # @param body [String] the body of the Globus API request
107
+ def put(base_url:, path:, body:)
108
+ response = with_token_refresh_when_unauthorized do
109
+ connection(base_url).put(path) do |request|
110
+ request.headers["Authorization"] = "Bearer #{config.token}"
111
+ request.headers["Content-Type"] = "application/json"
112
+ request.body = body.to_json
113
+ end
114
+ end
115
+
116
+ UnexpectedResponse.call(response) unless response.success?
117
+
118
+ return nil if response.body.blank?
119
+
120
+ JSON.parse(response.body)
121
+ end
122
+
123
+ # Send an authenticated DELETE request
124
+ # @param base_url [String] the base URL of the Globus API
125
+ # @param path [String] the path to the Globus API request
126
+ def delete(base_url:, path:)
127
+ response = with_token_refresh_when_unauthorized do
128
+ connection(base_url).delete(path) do |request|
129
+ request.headers["Authorization"] = "Bearer #{config.token}"
130
+ end
131
+ end
132
+
133
+ UnexpectedResponse.call(response) unless response.success?
134
+
135
+ return nil if response.body.blank?
136
+
137
+ JSON.parse(response.body)
138
+ end
139
+
62
140
  def mkdir(...)
63
- TokenWrapper.refresh(config) do
64
- endpoint = Endpoint.new(config, ...)
141
+ Endpoint.new(self, ...).tap do |endpoint|
65
142
  endpoint.mkdir
66
143
  endpoint.allow_writes
67
144
  end
68
145
  end
69
146
 
70
147
  def disallow_writes(...)
71
- TokenWrapper.refresh(config) do
72
- endpoint = Endpoint.new(config, ...)
73
- endpoint.disallow_writes
74
- end
148
+ Endpoint
149
+ .new(self, ...)
150
+ .disallow_writes
75
151
  end
76
152
 
77
153
  def delete_access_rule(...)
78
- TokenWrapper.refresh(config) do
79
- endpoint = Endpoint.new(config, ...)
80
- endpoint.delete_access_rule
81
- end
154
+ Endpoint
155
+ .new(self, ...)
156
+ .delete_access_rule
82
157
  end
83
158
 
84
159
  # NOTE: Can't use the `...` (argument forwarding) operator here because we
85
160
  # want to route the keyword args to `Endpoint#new` and the block arg to
86
161
  # `Endpoint#list_files`
87
162
  def list_files(**keywords, &block)
88
- TokenWrapper.refresh(config) do
89
- endpoint = Endpoint.new(config, **keywords)
90
- endpoint.list_files(&block)
91
- end
163
+ Endpoint
164
+ .new(self, **keywords)
165
+ .list_files(&block)
92
166
  end
93
167
 
94
168
  def file_count(...)
95
- TokenWrapper.refresh(config) do
96
- endpoint = Endpoint.new(config, ...)
97
- endpoint.list_files { |files| return files.count }
98
- end
169
+ Endpoint
170
+ .new(self, ...)
171
+ .list_files { |files| return files.count }
99
172
  end
100
173
 
101
174
  def total_size(...)
102
- TokenWrapper.refresh(config) do
103
- endpoint = Endpoint.new(config, ...)
104
- endpoint.list_files { |files| return files.sum(&:size) }
105
- end
175
+ Endpoint
176
+ .new(self, ...)
177
+ .list_files { |files| return files.sum(&:size) }
106
178
  end
107
179
 
108
180
  def get_filenames(...)
109
- TokenWrapper.refresh(config) do
110
- endpoint = Endpoint.new(config, ...)
111
- endpoint.list_files { |files| return files.map(&:name) }
112
- end
181
+ Endpoint
182
+ .new(self, ...)
183
+ .list_files { |files| return files.map(&:name) }
113
184
  end
114
185
 
115
186
  def has_files?(...)
116
- TokenWrapper.refresh(config) do
117
- endpoint = Endpoint.new(config, ...)
118
- endpoint.has_files?
119
- end
187
+ Endpoint
188
+ .new(self, ...)
189
+ .has_files?
120
190
  end
121
191
 
122
192
  def user_valid?(...)
123
- TokenWrapper.refresh(config) do
124
- identity = Identity.new(config)
125
- identity.valid?(...)
193
+ Identity
194
+ .new(self)
195
+ .valid?(...)
196
+ end
197
+
198
+ private
199
+
200
+ def connection(base_url)
201
+ Faraday.new(url: base_url) do |conn|
202
+ conn.request :retry, {
203
+ max: 10,
204
+ interval: 0.05,
205
+ interval_randomness: 0.5,
206
+ backoff_factor: 2,
207
+ exceptions: Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS + [Faraday::ConnectionFailed]
208
+ }
126
209
  end
127
210
  end
211
+
212
+ # Wraps API operations to request new access token if expired.
213
+ # @yieldreturn response [Faraday::Response] the response to inspect
214
+ #
215
+ # @note You likely want to make sure you're wrapping a _single_ HTTP request in this
216
+ # method, because 1) all calls in the block will be retried from the top if there's
217
+ # an authN failure detected, and 2) only the response returned by the block will be
218
+ # inspected for authN failure.
219
+ # Related: consider that the client instance and its token will live across many
220
+ # invocations of the GlobusClient methods once the client is configured by a consuming application,
221
+ # since this class is a Singleton. Thus, a token may expire between any two calls (i.e. it
222
+ # isn't necessary for a set of operations to collectively take longer than the token lifetime for
223
+ # expiry to fall in the middle of that related set of HTTP calls).
224
+ def with_token_refresh_when_unauthorized
225
+ response = yield
226
+
227
+ # if unauthorized, token has likely expired. try to get a new token and then retry the same request(s).
228
+ if response.status == 401
229
+ config.token = Authenticator.token(config.client_id, config.client_secret, config.auth_url)
230
+ response = yield
231
+ end
232
+
233
+ response
234
+ end
128
235
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: globus_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.1
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Collier
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2023-09-22 00:00:00.000000000 Z
13
+ date: 2023-09-27 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -183,7 +183,6 @@ files:
183
183
  - lib/globus_client/authenticator.rb
184
184
  - lib/globus_client/endpoint.rb
185
185
  - lib/globus_client/identity.rb
186
- - lib/globus_client/token_wrapper.rb
187
186
  - lib/globus_client/unexpected_response.rb
188
187
  - lib/globus_client/version.rb
189
188
  homepage: https://github.com/sul-dlss/globus_client
@@ -208,7 +207,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
208
207
  - !ruby/object:Gem::Version
209
208
  version: '0'
210
209
  requirements: []
211
- rubygems_version: 3.4.13
210
+ rubygems_version: 3.4.19
212
211
  signing_key:
213
212
  specification_version: 4
214
213
  summary: Interface for interacting with the Globus API.
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class GlobusClient
4
- # Wraps API operations to request new access token if expired
5
- class TokenWrapper
6
- def self.refresh(config)
7
- yield
8
- rescue UnexpectedResponse::UnauthorizedError
9
- config.token = Authenticator.token(config.client_id, config.client_secret, config.auth_url)
10
- yield
11
- end
12
- end
13
- end