basecamp-sdk 0.9.0 → 0.11.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 +4 -4
- data/README.md +106 -10
- data/lib/basecamp/client.rb +13 -8
- data/lib/basecamp/config.rb +4 -3
- data/lib/basecamp/generated/metadata.json +280 -12
- data/lib/basecamp/generated/services/account_service.rb +1 -1
- data/lib/basecamp/generated/services/automation_service.rb +2 -2
- data/lib/basecamp/generated/services/boosts_service.rb +3 -3
- data/lib/basecamp/generated/services/campfires_service.rb +7 -7
- data/lib/basecamp/generated/services/card_columns_service.rb +1 -1
- data/lib/basecamp/generated/services/card_steps_service.rb +1 -1
- data/lib/basecamp/generated/services/card_tables_service.rb +1 -1
- data/lib/basecamp/generated/services/cards_service.rb +4 -4
- data/lib/basecamp/generated/services/checkins_service.rb +8 -8
- data/lib/basecamp/generated/services/client_approvals_service.rb +2 -2
- data/lib/basecamp/generated/services/client_correspondences_service.rb +2 -2
- data/lib/basecamp/generated/services/client_replies_service.rb +2 -2
- data/lib/basecamp/generated/services/comments_service.rb +2 -2
- data/lib/basecamp/generated/services/documents_service.rb +2 -2
- data/lib/basecamp/generated/services/events_service.rb +1 -1
- data/lib/basecamp/generated/services/everything_service.rb +169 -0
- data/lib/basecamp/generated/services/forwards_service.rb +5 -5
- data/lib/basecamp/generated/services/gauges_service.rb +3 -3
- data/lib/basecamp/generated/services/hill_charts_service.rb +1 -1
- data/lib/basecamp/generated/services/message_boards_service.rb +1 -1
- data/lib/basecamp/generated/services/message_types_service.rb +20 -15
- data/lib/basecamp/generated/services/messages_service.rb +2 -2
- data/lib/basecamp/generated/services/my_assignments_service.rb +5 -5
- data/lib/basecamp/generated/services/my_notifications_service.rb +16 -2
- data/lib/basecamp/generated/services/people_service.rb +9 -9
- data/lib/basecamp/generated/services/projects_service.rb +2 -2
- data/lib/basecamp/generated/services/recordings_service.rb +3 -3
- data/lib/basecamp/generated/services/reports_service.rb +5 -5
- data/lib/basecamp/generated/services/schedules_service.rb +13 -5
- data/lib/basecamp/generated/services/search_service.rb +2 -2
- data/lib/basecamp/generated/services/subscriptions_service.rb +1 -1
- data/lib/basecamp/generated/services/templates_service.rb +3 -3
- data/lib/basecamp/generated/services/timeline_service.rb +1 -1
- data/lib/basecamp/generated/services/timesheets_service.rb +5 -5
- data/lib/basecamp/generated/services/todolist_groups_service.rb +1 -1
- data/lib/basecamp/generated/services/todolists_service.rb +2 -2
- data/lib/basecamp/generated/services/todos_service.rb +2 -2
- data/lib/basecamp/generated/services/todosets_service.rb +1 -1
- data/lib/basecamp/generated/services/tools_service.rb +4 -3
- data/lib/basecamp/generated/services/uploads_service.rb +3 -3
- data/lib/basecamp/generated/services/vaults_service.rb +2 -2
- data/lib/basecamp/generated/services/webhooks_service.rb +2 -2
- data/lib/basecamp/generated/types.rb +374 -18
- data/lib/basecamp/http.rb +47 -18
- data/lib/basecamp/oauth/device_authorization.rb +37 -0
- data/lib/basecamp/oauth/device_flow.rb +873 -0
- data/lib/basecamp/oauth/device_flow_error.rb +46 -0
- data/lib/basecamp/oauth/discovery.rb +13 -11
- data/lib/basecamp/oauth/exchange.rb +44 -5
- data/lib/basecamp/oauth/fetcher.rb +626 -56
- data/lib/basecamp/oauth/oauth_error.rb +3 -1
- data/lib/basecamp/oauth/refresh_request.rb +8 -2
- data/lib/basecamp/oauth/resource.rb +13 -10
- data/lib/basecamp/oauth/token.rb +10 -3
- data/lib/basecamp/oauth.rb +47 -2
- data/lib/basecamp/services/cards_extensions.rb +67 -0
- data/lib/basecamp/version.rb +2 -2
- data/lib/basecamp.rb +5 -0
- data/scripts/generate-services.rb +24 -6
- data/scripts/generate-types.rb +29 -2
- metadata +7 -2
data/lib/basecamp/http.rb
CHANGED
|
@@ -55,9 +55,12 @@ module Basecamp
|
|
|
55
55
|
# Performs a GET request.
|
|
56
56
|
# @param path [String] URL path
|
|
57
57
|
# @param params [Hash] query parameters
|
|
58
|
+
# @param operation [String, nil] canonical operation ID; when given, the
|
|
59
|
+
# operation's declared retry block governs attempts (max as a ceiling on
|
|
60
|
+
# the configured cap) and retryable statuses (the declared retryOn set)
|
|
58
61
|
# @return [Response]
|
|
59
|
-
def get(path, params: {})
|
|
60
|
-
request(:get, path, params: params)
|
|
62
|
+
def get(path, params: {}, operation: nil)
|
|
63
|
+
request(:get, path, params: params, operation: operation)
|
|
61
64
|
end
|
|
62
65
|
|
|
63
66
|
# Performs a GET request to an absolute URL.
|
|
@@ -171,8 +174,8 @@ module Basecamp
|
|
|
171
174
|
# @param params [Hash] query parameters
|
|
172
175
|
# @yield [Hash] each item from the response
|
|
173
176
|
# @return [Enumerator] if no block given
|
|
174
|
-
def paginate(path, params: {}, &block)
|
|
175
|
-
return to_enum(:paginate, path, params: params) unless block
|
|
177
|
+
def paginate(path, params: {}, operation: nil, &block)
|
|
178
|
+
return to_enum(:paginate, path, params: params, operation: operation) unless block
|
|
176
179
|
|
|
177
180
|
base_url = build_url(path)
|
|
178
181
|
url = base_url
|
|
@@ -183,7 +186,7 @@ module Basecamp
|
|
|
183
186
|
break if page > @config.max_pages
|
|
184
187
|
|
|
185
188
|
@hooks.on_paginate(url, page)
|
|
186
|
-
response = get(url, params: page == 1 ? params : {})
|
|
189
|
+
response = get(url, params: page == 1 ? params : {}, operation: operation)
|
|
187
190
|
|
|
188
191
|
Security.check_body_size!(response.body, Security::MAX_RESPONSE_BODY_BYTES)
|
|
189
192
|
|
|
@@ -217,8 +220,8 @@ module Basecamp
|
|
|
217
220
|
# @param params [Hash] query parameters
|
|
218
221
|
# @yield [Hash] each item from the response
|
|
219
222
|
# @return [Enumerator] if no block given
|
|
220
|
-
def paginate_key(path, key:, params: {}, &block)
|
|
221
|
-
return to_enum(:paginate_key, path, key: key, params: params) unless block
|
|
223
|
+
def paginate_key(path, key:, params: {}, operation: nil, &block)
|
|
224
|
+
return to_enum(:paginate_key, path, key: key, params: params, operation: operation) unless block
|
|
222
225
|
|
|
223
226
|
base_url = build_url(path)
|
|
224
227
|
url = base_url
|
|
@@ -229,7 +232,7 @@ module Basecamp
|
|
|
229
232
|
break if page > @config.max_pages
|
|
230
233
|
|
|
231
234
|
@hooks.on_paginate(url, page)
|
|
232
|
-
response = get(url, params: page == 1 ? params : {})
|
|
235
|
+
response = get(url, params: page == 1 ? params : {}, operation: operation)
|
|
233
236
|
|
|
234
237
|
Security.check_body_size!(response.body, Security::MAX_RESPONSE_BODY_BYTES)
|
|
235
238
|
|
|
@@ -266,11 +269,11 @@ module Basecamp
|
|
|
266
269
|
# @param key [String] the key containing the array of paginated items
|
|
267
270
|
# @param params [Hash] query parameters
|
|
268
271
|
# @return [Hash] wrapper fields merged with key => Enumerator of all items
|
|
269
|
-
def paginate_wrapped(path, key:, params: {})
|
|
272
|
+
def paginate_wrapped(path, key:, params: {}, operation: nil)
|
|
270
273
|
base_url = build_url(path)
|
|
271
274
|
|
|
272
275
|
@hooks.on_paginate(base_url, 1)
|
|
273
|
-
first_response = get(base_url, params: params)
|
|
276
|
+
first_response = get(base_url, params: params, operation: operation)
|
|
274
277
|
Security.check_body_size!(first_response.body, Security::MAX_RESPONSE_BODY_BYTES)
|
|
275
278
|
|
|
276
279
|
begin
|
|
@@ -304,7 +307,7 @@ module Basecamp
|
|
|
304
307
|
end
|
|
305
308
|
|
|
306
309
|
@hooks.on_paginate(next_url, page)
|
|
307
|
-
response = get(next_url)
|
|
310
|
+
response = get(next_url, operation: operation)
|
|
308
311
|
Security.check_body_size!(response.body, Security::MAX_RESPONSE_BODY_BYTES)
|
|
309
312
|
|
|
310
313
|
begin
|
|
@@ -340,34 +343,37 @@ module Basecamp
|
|
|
340
343
|
end
|
|
341
344
|
end
|
|
342
345
|
|
|
343
|
-
def request(method, path, params: {}, body: nil, allow_cross_origin: false)
|
|
346
|
+
def request(method, path, params: {}, body: nil, allow_cross_origin: false, operation: nil)
|
|
344
347
|
url = build_url(path, allow_cross_origin: allow_cross_origin)
|
|
345
348
|
|
|
346
349
|
# Mutations don't retry on 429/5xx to avoid duplicating data
|
|
347
350
|
if method == :get
|
|
348
|
-
request_with_retry(method, url, params: params, allow_cross_origin: allow_cross_origin)
|
|
351
|
+
request_with_retry(method, url, params: params, allow_cross_origin: allow_cross_origin, operation: operation)
|
|
349
352
|
else
|
|
350
353
|
single_request(method, url, params: params, body: body, attempt: 1, allow_cross_origin: allow_cross_origin)
|
|
351
354
|
end
|
|
352
355
|
end
|
|
353
356
|
|
|
354
|
-
def request_with_retry(method, url, params: {}, allow_cross_origin: false)
|
|
357
|
+
def request_with_retry(method, url, params: {}, allow_cross_origin: false, operation: nil)
|
|
358
|
+
op_retry = operation && Http.operation_retry(operation)
|
|
359
|
+
caller_cap = [ @config.max_retries, 1 ].max
|
|
360
|
+
max_attempts = op_retry ? [ caller_cap, op_retry.fetch("maxAttempts") ].min : @config.max_retries
|
|
355
361
|
attempt = 0
|
|
356
362
|
last_error = nil
|
|
357
363
|
|
|
358
364
|
loop do
|
|
359
365
|
attempt += 1
|
|
360
|
-
break if attempt >
|
|
366
|
+
break if attempt > max_attempts
|
|
361
367
|
|
|
362
368
|
begin
|
|
363
369
|
return single_request(method, url, params: params, body: nil, attempt: attempt, allow_cross_origin: allow_cross_origin)
|
|
364
370
|
rescue Basecamp::RateLimitError, Basecamp::NetworkError, Basecamp::ApiError => e
|
|
365
|
-
raise e unless e
|
|
371
|
+
raise e unless retry_eligible?(e, op_retry)
|
|
366
372
|
|
|
367
373
|
last_error = e
|
|
368
374
|
|
|
369
375
|
# Don't sleep if this was the last attempt
|
|
370
|
-
break if attempt >=
|
|
376
|
+
break if attempt >= max_attempts
|
|
371
377
|
|
|
372
378
|
delay = calculate_delay(attempt, e.retry_after)
|
|
373
379
|
|
|
@@ -377,7 +383,30 @@ module Basecamp
|
|
|
377
383
|
end
|
|
378
384
|
end
|
|
379
385
|
|
|
380
|
-
|
|
386
|
+
noun = max_attempts == 1 ? "attempt" : "attempts"
|
|
387
|
+
raise last_error || Basecamp::ApiError.new("Request failed after #{max_attempts} #{noun}")
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# For a governed request (operation given), a status-bearing error retries
|
|
391
|
+
# exactly when the operation's declared retryOn set says so — the error
|
|
392
|
+
# taxonomy's retryable flag neither widens the set (500 is retryable in
|
|
393
|
+
# errors.rb but not declared) nor vetoes it. Status-less errors (network
|
|
394
|
+
# failures) and all ungoverned traffic keep the taxonomy's judgment.
|
|
395
|
+
def retry_eligible?(error, op_retry)
|
|
396
|
+
if op_retry && error.http_status
|
|
397
|
+
op_retry.fetch("retryOn").include?(error.http_status)
|
|
398
|
+
else
|
|
399
|
+
error.retryable?
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Memoized per-operation retry metadata, keyed by canonical operation ID.
|
|
404
|
+
# Benign-race memoization: concurrent first loads compute identical values.
|
|
405
|
+
def self.operation_retry(operation)
|
|
406
|
+
@operation_metadata ||= JSON.parse(
|
|
407
|
+
File.read(File.join(__dir__, "generated", "metadata.json"))
|
|
408
|
+
).fetch("operations").freeze
|
|
409
|
+
@operation_metadata.dig(operation, "retry")
|
|
381
410
|
end
|
|
382
411
|
|
|
383
412
|
def single_request(method, url, params:, body:, attempt:, retry_count: 0, allow_cross_origin: false)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Basecamp
|
|
4
|
+
module Oauth
|
|
5
|
+
# RFC 8628 device authorization response (the device/user code pair the user
|
|
6
|
+
# approves out of band).
|
|
7
|
+
#
|
|
8
|
+
# @attr device_code [String] The device verification code polled at the token
|
|
9
|
+
# endpoint.
|
|
10
|
+
# @attr user_code [String] The code the user enters at the verification URI.
|
|
11
|
+
# @attr verification_uri [String] Where the user goes to approve the request.
|
|
12
|
+
# @attr verification_uri_complete [String, nil] The verification URI with the
|
|
13
|
+
# user code pre-filled (optional; absent when the server omits it).
|
|
14
|
+
# @attr expires_in [Integer] Lifetime of the device/user codes in seconds.
|
|
15
|
+
# @attr interval [Integer] Minimum seconds to wait between token polls
|
|
16
|
+
# (defaults to 5 when the server omits it).
|
|
17
|
+
DeviceAuthorization = Data.define(
|
|
18
|
+
:device_code,
|
|
19
|
+
:user_code,
|
|
20
|
+
:verification_uri,
|
|
21
|
+
:verification_uri_complete,
|
|
22
|
+
:expires_in,
|
|
23
|
+
:interval
|
|
24
|
+
) do
|
|
25
|
+
def initialize(
|
|
26
|
+
device_code:,
|
|
27
|
+
user_code:,
|
|
28
|
+
verification_uri:,
|
|
29
|
+
expires_in:,
|
|
30
|
+
interval:,
|
|
31
|
+
verification_uri_complete: nil
|
|
32
|
+
)
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|