basecamp-sdk 0.10.0 → 0.12.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 +28 -8
- data/lib/basecamp/config.rb +4 -3
- data/lib/basecamp/generated/metadata.json +178 -17
- data/lib/basecamp/generated/services/account_service.rb +1 -1
- data/lib/basecamp/generated/services/automation_service.rb +1 -1
- data/lib/basecamp/generated/services/bookmarks_service.rb +49 -0
- data/lib/basecamp/generated/services/boosts_service.rb +3 -3
- data/lib/basecamp/generated/services/calendars_service.rb +30 -0
- 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 +2 -2
- 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/drafts_service.rb +21 -0
- data/lib/basecamp/generated/services/events_service.rb +1 -1
- data/lib/basecamp/generated/services/everything_service.rb +69 -46
- 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 +2 -2
- data/lib/basecamp/generated/services/messages_service.rb +2 -2
- data/lib/basecamp/generated/services/my_assignments_service.rb +34 -3
- data/lib/basecamp/generated/services/my_notes_service.rb +28 -0
- data/lib/basecamp/generated/services/my_notifications_service.rb +2 -2
- data/lib/basecamp/generated/services/people_service.rb +8 -8
- data/lib/basecamp/generated/services/projects_service.rb +2 -2
- data/lib/basecamp/generated/services/recordings_service.rb +2 -2
- data/lib/basecamp/generated/services/reports_service.rb +5 -5
- data/lib/basecamp/generated/services/schedules_service.rb +4 -4
- 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 +4 -4
- 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 +19 -2
- data/lib/basecamp/generated/services/todosets_service.rb +1 -1
- data/lib/basecamp/generated/services/tools_service.rb +1 -1
- 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 +313 -34
- data/lib/basecamp/http.rb +47 -19
- 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/version.rb +2 -2
- data/scripts/generate-services.rb +10 -7
- metadata +9 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Auto-generated from OpenAPI spec. Do not edit manually.
|
|
4
|
-
# Generated: 2026-07-
|
|
4
|
+
# Generated: 2026-07-31T15:59:31Z
|
|
5
5
|
|
|
6
6
|
require "json"
|
|
7
7
|
require "time"
|
|
@@ -255,6 +255,62 @@ module Basecamp
|
|
|
255
255
|
end
|
|
256
256
|
end
|
|
257
257
|
|
|
258
|
+
# Bookmark
|
|
259
|
+
class Bookmark
|
|
260
|
+
include TypeHelpers
|
|
261
|
+
attr_accessor :created_at, :id, :recording, :updated_at
|
|
262
|
+
|
|
263
|
+
# @return [Array<Symbol>]
|
|
264
|
+
def self.required_fields
|
|
265
|
+
%i[created_at id recording updated_at].freeze
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def initialize(data = {})
|
|
269
|
+
@created_at = parse_datetime(data["created_at"])
|
|
270
|
+
@id = parse_integer(data["id"])
|
|
271
|
+
@recording = parse_type(data["recording"], "Recording")
|
|
272
|
+
@updated_at = parse_datetime(data["updated_at"])
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def to_h
|
|
276
|
+
{
|
|
277
|
+
"created_at" => @created_at,
|
|
278
|
+
"id" => @id,
|
|
279
|
+
"recording" => @recording,
|
|
280
|
+
"updated_at" => @updated_at,
|
|
281
|
+
}.compact
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def to_json(*args)
|
|
285
|
+
to_h.to_json(*args)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# BookmarkStatus
|
|
290
|
+
class BookmarkStatus
|
|
291
|
+
include TypeHelpers
|
|
292
|
+
attr_accessor :bookmarked
|
|
293
|
+
|
|
294
|
+
# @return [Array<Symbol>]
|
|
295
|
+
def self.required_fields
|
|
296
|
+
%i[bookmarked].freeze
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def initialize(data = {})
|
|
300
|
+
@bookmarked = parse_boolean(data["bookmarked"])
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def to_h
|
|
304
|
+
{
|
|
305
|
+
"bookmarked" => @bookmarked,
|
|
306
|
+
}.compact
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def to_json(*args)
|
|
310
|
+
to_h.to_json(*args)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
258
314
|
# Boost
|
|
259
315
|
class Boost
|
|
260
316
|
include TypeHelpers
|
|
@@ -342,6 +398,72 @@ module Basecamp
|
|
|
342
398
|
end
|
|
343
399
|
end
|
|
344
400
|
|
|
401
|
+
# Calendar
|
|
402
|
+
class Calendar
|
|
403
|
+
include TypeHelpers
|
|
404
|
+
attr_accessor :app_url, :color, :created_at, :id, :name, :schedule_url, :type, :updated_at, :url
|
|
405
|
+
|
|
406
|
+
# @return [Array<Symbol>]
|
|
407
|
+
def self.required_fields
|
|
408
|
+
%i[app_url color created_at id name schedule_url type updated_at url].freeze
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def initialize(data = {})
|
|
412
|
+
@app_url = data["app_url"]
|
|
413
|
+
@color = data["color"]
|
|
414
|
+
@created_at = parse_datetime(data["created_at"])
|
|
415
|
+
@id = parse_integer(data["id"])
|
|
416
|
+
@name = data["name"]
|
|
417
|
+
@schedule_url = data["schedule_url"]
|
|
418
|
+
@type = data["type"]
|
|
419
|
+
@updated_at = parse_datetime(data["updated_at"])
|
|
420
|
+
@url = data["url"]
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def to_h
|
|
424
|
+
{
|
|
425
|
+
"app_url" => @app_url,
|
|
426
|
+
"color" => @color,
|
|
427
|
+
"created_at" => @created_at,
|
|
428
|
+
"id" => @id,
|
|
429
|
+
"name" => @name,
|
|
430
|
+
"schedule_url" => @schedule_url,
|
|
431
|
+
"type" => @type,
|
|
432
|
+
"updated_at" => @updated_at,
|
|
433
|
+
"url" => @url,
|
|
434
|
+
}.compact
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def to_json(*args)
|
|
438
|
+
to_h.to_json(*args)
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# CalendarAttributes
|
|
443
|
+
class CalendarAttributes
|
|
444
|
+
include TypeHelpers
|
|
445
|
+
attr_accessor :color
|
|
446
|
+
|
|
447
|
+
# @return [Array<Symbol>]
|
|
448
|
+
def self.required_fields
|
|
449
|
+
%i[color].freeze
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def initialize(data = {})
|
|
453
|
+
@color = data["color"]
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def to_h
|
|
457
|
+
{
|
|
458
|
+
"color" => @color,
|
|
459
|
+
}.compact
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def to_json(*args)
|
|
463
|
+
to_h.to_json(*args)
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
345
467
|
# Campfire
|
|
346
468
|
class Campfire
|
|
347
469
|
include TypeHelpers
|
|
@@ -1351,6 +1473,107 @@ module Basecamp
|
|
|
1351
1473
|
end
|
|
1352
1474
|
end
|
|
1353
1475
|
|
|
1476
|
+
# Draft
|
|
1477
|
+
class Draft
|
|
1478
|
+
include TypeHelpers
|
|
1479
|
+
attr_accessor :app_url, :bucket, :created_at, :excerpt, :id, :parent, :scheduled_posting_at, :title, :type, :updated_at
|
|
1480
|
+
|
|
1481
|
+
# @return [Array<Symbol>]
|
|
1482
|
+
def self.required_fields
|
|
1483
|
+
%i[app_url bucket created_at excerpt id parent scheduled_posting_at title type updated_at].freeze
|
|
1484
|
+
end
|
|
1485
|
+
|
|
1486
|
+
def initialize(data = {})
|
|
1487
|
+
@app_url = data["app_url"]
|
|
1488
|
+
@bucket = parse_type(data["bucket"], "DraftBucket")
|
|
1489
|
+
@created_at = parse_datetime(data["created_at"])
|
|
1490
|
+
@excerpt = data["excerpt"]
|
|
1491
|
+
@id = parse_integer(data["id"])
|
|
1492
|
+
@parent = data["parent"]
|
|
1493
|
+
@scheduled_posting_at = data["scheduled_posting_at"]
|
|
1494
|
+
@title = data["title"]
|
|
1495
|
+
@type = data["type"]
|
|
1496
|
+
@updated_at = parse_datetime(data["updated_at"])
|
|
1497
|
+
end
|
|
1498
|
+
|
|
1499
|
+
def to_h
|
|
1500
|
+
{
|
|
1501
|
+
"app_url" => @app_url,
|
|
1502
|
+
"bucket" => @bucket,
|
|
1503
|
+
"created_at" => @created_at,
|
|
1504
|
+
"excerpt" => @excerpt,
|
|
1505
|
+
"id" => @id,
|
|
1506
|
+
"parent" => @parent,
|
|
1507
|
+
"scheduled_posting_at" => @scheduled_posting_at,
|
|
1508
|
+
"title" => @title,
|
|
1509
|
+
"type" => @type,
|
|
1510
|
+
"updated_at" => @updated_at,
|
|
1511
|
+
}.reject { |k, v| v.nil? && !["scheduled_posting_at"].include?(k) }
|
|
1512
|
+
end
|
|
1513
|
+
|
|
1514
|
+
def to_json(*args)
|
|
1515
|
+
to_h.to_json(*args)
|
|
1516
|
+
end
|
|
1517
|
+
end
|
|
1518
|
+
|
|
1519
|
+
# DraftBucket
|
|
1520
|
+
class DraftBucket
|
|
1521
|
+
include TypeHelpers
|
|
1522
|
+
attr_accessor :app_url, :id, :name
|
|
1523
|
+
|
|
1524
|
+
# @return [Array<Symbol>]
|
|
1525
|
+
def self.required_fields
|
|
1526
|
+
%i[app_url id name].freeze
|
|
1527
|
+
end
|
|
1528
|
+
|
|
1529
|
+
def initialize(data = {})
|
|
1530
|
+
@app_url = data["app_url"]
|
|
1531
|
+
@id = parse_integer(data["id"])
|
|
1532
|
+
@name = data["name"]
|
|
1533
|
+
end
|
|
1534
|
+
|
|
1535
|
+
def to_h
|
|
1536
|
+
{
|
|
1537
|
+
"app_url" => @app_url,
|
|
1538
|
+
"id" => @id,
|
|
1539
|
+
"name" => @name,
|
|
1540
|
+
}.compact
|
|
1541
|
+
end
|
|
1542
|
+
|
|
1543
|
+
def to_json(*args)
|
|
1544
|
+
to_h.to_json(*args)
|
|
1545
|
+
end
|
|
1546
|
+
end
|
|
1547
|
+
|
|
1548
|
+
# DraftParent
|
|
1549
|
+
class DraftParent
|
|
1550
|
+
include TypeHelpers
|
|
1551
|
+
attr_accessor :app_url, :id, :title
|
|
1552
|
+
|
|
1553
|
+
# @return [Array<Symbol>]
|
|
1554
|
+
def self.required_fields
|
|
1555
|
+
%i[app_url id title].freeze
|
|
1556
|
+
end
|
|
1557
|
+
|
|
1558
|
+
def initialize(data = {})
|
|
1559
|
+
@app_url = data["app_url"]
|
|
1560
|
+
@id = parse_integer(data["id"])
|
|
1561
|
+
@title = data["title"]
|
|
1562
|
+
end
|
|
1563
|
+
|
|
1564
|
+
def to_h
|
|
1565
|
+
{
|
|
1566
|
+
"app_url" => @app_url,
|
|
1567
|
+
"id" => @id,
|
|
1568
|
+
"title" => @title,
|
|
1569
|
+
}.compact
|
|
1570
|
+
end
|
|
1571
|
+
|
|
1572
|
+
def to_json(*args)
|
|
1573
|
+
to_h.to_json(*args)
|
|
1574
|
+
end
|
|
1575
|
+
end
|
|
1576
|
+
|
|
1354
1577
|
# Event
|
|
1355
1578
|
class Event
|
|
1356
1579
|
include TypeHelpers
|
|
@@ -1414,39 +1637,6 @@ module Basecamp
|
|
|
1414
1637
|
end
|
|
1415
1638
|
end
|
|
1416
1639
|
|
|
1417
|
-
# EverythingBoost
|
|
1418
|
-
class EverythingBoost
|
|
1419
|
-
include TypeHelpers
|
|
1420
|
-
attr_accessor :booster, :content, :created_at, :id, :recording
|
|
1421
|
-
|
|
1422
|
-
# @return [Array<Symbol>]
|
|
1423
|
-
def self.required_fields
|
|
1424
|
-
%i[booster content created_at id recording].freeze
|
|
1425
|
-
end
|
|
1426
|
-
|
|
1427
|
-
def initialize(data = {})
|
|
1428
|
-
@booster = parse_type(data["booster"], "Person")
|
|
1429
|
-
@content = data["content"]
|
|
1430
|
-
@created_at = parse_datetime(data["created_at"])
|
|
1431
|
-
@id = parse_integer(data["id"])
|
|
1432
|
-
@recording = parse_type(data["recording"], "Recording")
|
|
1433
|
-
end
|
|
1434
|
-
|
|
1435
|
-
def to_h
|
|
1436
|
-
{
|
|
1437
|
-
"booster" => @booster,
|
|
1438
|
-
"content" => @content,
|
|
1439
|
-
"created_at" => @created_at,
|
|
1440
|
-
"id" => @id,
|
|
1441
|
-
"recording" => @recording,
|
|
1442
|
-
}.compact
|
|
1443
|
-
end
|
|
1444
|
-
|
|
1445
|
-
def to_json(*args)
|
|
1446
|
-
to_h.to_json(*args)
|
|
1447
|
-
end
|
|
1448
|
-
end
|
|
1449
|
-
|
|
1450
1640
|
# EverythingFile
|
|
1451
1641
|
class EverythingFile
|
|
1452
1642
|
include TypeHelpers
|
|
@@ -1529,6 +1719,31 @@ module Basecamp
|
|
|
1529
1719
|
end
|
|
1530
1720
|
end
|
|
1531
1721
|
|
|
1722
|
+
# FieldKeyedErrors
|
|
1723
|
+
class FieldKeyedErrors
|
|
1724
|
+
include TypeHelpers
|
|
1725
|
+
attr_accessor :errors
|
|
1726
|
+
|
|
1727
|
+
# @return [Array<Symbol>]
|
|
1728
|
+
def self.required_fields
|
|
1729
|
+
%i[errors].freeze
|
|
1730
|
+
end
|
|
1731
|
+
|
|
1732
|
+
def initialize(data = {})
|
|
1733
|
+
@errors = parse_type(data["errors"], "FieldErrorMap")
|
|
1734
|
+
end
|
|
1735
|
+
|
|
1736
|
+
def to_h
|
|
1737
|
+
{
|
|
1738
|
+
"errors" => @errors,
|
|
1739
|
+
}.compact
|
|
1740
|
+
end
|
|
1741
|
+
|
|
1742
|
+
def to_json(*args)
|
|
1743
|
+
to_h.to_json(*args)
|
|
1744
|
+
end
|
|
1745
|
+
end
|
|
1746
|
+
|
|
1532
1747
|
# Forward
|
|
1533
1748
|
class Forward
|
|
1534
1749
|
include TypeHelpers
|
|
@@ -2312,6 +2527,70 @@ module Basecamp
|
|
|
2312
2527
|
end
|
|
2313
2528
|
end
|
|
2314
2529
|
|
|
2530
|
+
# MyNote
|
|
2531
|
+
class MyNote
|
|
2532
|
+
include TypeHelpers
|
|
2533
|
+
attr_accessor :app_url, :content, :content_attachments, :created_at, :id, :type, :updated_at, :url
|
|
2534
|
+
|
|
2535
|
+
# @return [Array<Symbol>]
|
|
2536
|
+
def self.required_fields
|
|
2537
|
+
%i[app_url content content_attachments created_at id type updated_at url].freeze
|
|
2538
|
+
end
|
|
2539
|
+
|
|
2540
|
+
def initialize(data = {})
|
|
2541
|
+
@app_url = data["app_url"]
|
|
2542
|
+
@content = data["content"]
|
|
2543
|
+
@content_attachments = parse_array(data["content_attachments"], "RichTextAttachment")
|
|
2544
|
+
@created_at = data["created_at"]
|
|
2545
|
+
@id = data["id"]
|
|
2546
|
+
@type = data["type"]
|
|
2547
|
+
@updated_at = data["updated_at"]
|
|
2548
|
+
@url = data["url"]
|
|
2549
|
+
end
|
|
2550
|
+
|
|
2551
|
+
def to_h
|
|
2552
|
+
{
|
|
2553
|
+
"app_url" => @app_url,
|
|
2554
|
+
"content" => @content,
|
|
2555
|
+
"content_attachments" => @content_attachments,
|
|
2556
|
+
"created_at" => @created_at,
|
|
2557
|
+
"id" => @id,
|
|
2558
|
+
"type" => @type,
|
|
2559
|
+
"updated_at" => @updated_at,
|
|
2560
|
+
"url" => @url,
|
|
2561
|
+
}.reject { |k, v| v.nil? && !["created_at", "id", "updated_at"].include?(k) }
|
|
2562
|
+
end
|
|
2563
|
+
|
|
2564
|
+
def to_json(*args)
|
|
2565
|
+
to_h.to_json(*args)
|
|
2566
|
+
end
|
|
2567
|
+
end
|
|
2568
|
+
|
|
2569
|
+
# MyNoteAttributes
|
|
2570
|
+
class MyNoteAttributes
|
|
2571
|
+
include TypeHelpers
|
|
2572
|
+
attr_accessor :content
|
|
2573
|
+
|
|
2574
|
+
# @return [Array<Symbol>]
|
|
2575
|
+
def self.required_fields
|
|
2576
|
+
%i[content].freeze
|
|
2577
|
+
end
|
|
2578
|
+
|
|
2579
|
+
def initialize(data = {})
|
|
2580
|
+
@content = data["content"]
|
|
2581
|
+
end
|
|
2582
|
+
|
|
2583
|
+
def to_h
|
|
2584
|
+
{
|
|
2585
|
+
"content" => @content,
|
|
2586
|
+
}.compact
|
|
2587
|
+
end
|
|
2588
|
+
|
|
2589
|
+
def to_json(*args)
|
|
2590
|
+
to_h.to_json(*args)
|
|
2591
|
+
end
|
|
2592
|
+
end
|
|
2593
|
+
|
|
2315
2594
|
# Notification
|
|
2316
2595
|
class Notification
|
|
2317
2596
|
include TypeHelpers
|
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,8 +383,30 @@ module Basecamp
|
|
|
377
383
|
end
|
|
378
384
|
end
|
|
379
385
|
|
|
380
|
-
noun =
|
|
381
|
-
raise last_error || Basecamp::ApiError.new("Request failed after #{
|
|
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")
|
|
382
410
|
end
|
|
383
411
|
|
|
384
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
|