basecamp-sdk 0.13.0 → 0.14.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 +2 -1
- data/lib/basecamp/error.rb +1 -0
- data/lib/basecamp/error_code.rb +1 -0
- data/lib/basecamp/exit_code.rb +1 -0
- data/lib/basecamp/generated/metadata.json +12 -1
- data/lib/basecamp/generated/services/uploads_service.rb +18 -0
- data/lib/basecamp/generated/types.rb +77 -1
- data/lib/basecamp/http.rb +6 -0
- data/lib/basecamp/limit_exceeded_error.rb +22 -0
- data/lib/basecamp/services/authorization_service.rb +11 -2
- data/lib/basecamp/version.rb +1 -1
- data/lib/basecamp.rb +4 -0
- data/scripts/generate-metadata.rb +3 -1
- data/scripts/generate-services.rb +4 -2
- data/scripts/generate-types.rb +2 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b770f1606323f83fb028ebb44d51e0d2500664a6d88ec9209dc227807ace197
|
|
4
|
+
data.tar.gz: 418cf25fbdc65f25db5e9d816565cd20a7f2e3d0c06c5a147dcf896c38d2ec9e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 82caf3dad37f93e3b9ce65ad2da90b798efa466d0a2fc8e25d63ea84778357a436fe0da8615bbde3f03c3836eb9077dee20b35775ab283900437e575a64aa99e
|
|
7
|
+
data.tar.gz: c17f87a5bac84d66e89edf3725fb3321fa768b31d868d8214f1dbf8326a6361f73dd5acf355c4a22712a9434fe5514d0b750edd2eb8d6ce15468a0867f44419d
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Official Ruby SDK for the [Basecamp API](https://github.com/basecamp/bc3-api).
|
|
4
4
|
|
|
5
|
-
**Upgrading to v0.
|
|
5
|
+
**Upgrading to v0.14.0?** Read [MIGRATING.md](../MIGRATING.md) before you bump the version — nothing catches either break at load time. `uploads.list_versions` entries now carry the version event's keys, with the file nested under `"upload"` — code reading `version["filename"]` was getting nil and now has a real place to look — and every 507 reports `limit_exceeded` instead of a retryable `api_error`, so a `when` falling through to an else arm reroutes storage, project and webhook limits silently. Coming from v0.12.0 or earlier, read v0.13.0's section too.
|
|
6
6
|
|
|
7
7
|
## Requirements
|
|
8
8
|
|
|
@@ -480,6 +480,7 @@ end
|
|
|
480
480
|
| `ValidationError` | Invalid request data (400, 422) |
|
|
481
481
|
| `RateLimitError` | Rate limit exceeded (429) |
|
|
482
482
|
| `NetworkError` | Connection failures |
|
|
483
|
+
| `LimitExceededError` | Account limit reached (507) — file storage, projects, webhooks |
|
|
483
484
|
|
|
484
485
|
### Validation Errors
|
|
485
486
|
|
data/lib/basecamp/error.rb
CHANGED
|
@@ -79,6 +79,7 @@ module Basecamp
|
|
|
79
79
|
when ErrorCode::API then ExitCode::API
|
|
80
80
|
when ErrorCode::AMBIGUOUS then ExitCode::AMBIGUOUS
|
|
81
81
|
when ErrorCode::VALIDATION then ExitCode::VALIDATION
|
|
82
|
+
when ErrorCode::LIMIT_EXCEEDED then ExitCode::LIMIT_EXCEEDED
|
|
82
83
|
else ExitCode::API
|
|
83
84
|
end
|
|
84
85
|
end
|
data/lib/basecamp/error_code.rb
CHANGED
data/lib/basecamp/exit_code.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://basecamp.com/schemas/sdk-metadata.json",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"generated": "2026-08-
|
|
4
|
+
"generated": "2026-08-07T11:12:00Z",
|
|
5
5
|
"operations": {
|
|
6
6
|
"GetAccount": {
|
|
7
7
|
"retry": {
|
|
@@ -3148,6 +3148,17 @@
|
|
|
3148
3148
|
"maxPageSize": 50
|
|
3149
3149
|
}
|
|
3150
3150
|
},
|
|
3151
|
+
"CreateUploadVersion": {
|
|
3152
|
+
"retry": {
|
|
3153
|
+
"maxAttempts": 2,
|
|
3154
|
+
"baseDelayMs": 1000,
|
|
3155
|
+
"backoff": "exponential",
|
|
3156
|
+
"retryOn": [
|
|
3157
|
+
429,
|
|
3158
|
+
503
|
|
3159
|
+
]
|
|
3160
|
+
}
|
|
3161
|
+
},
|
|
3151
3162
|
"GetVault": {
|
|
3152
3163
|
"retry": {
|
|
3153
3164
|
"maxAttempts": 3,
|
|
@@ -37,6 +37,24 @@ module Basecamp
|
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# Replace an upload's file with a new version
|
|
41
|
+
# @param upload_id [Integer] upload id ID
|
|
42
|
+
# @param attachable_sgid [String] attachable sgid
|
|
43
|
+
# @param base_name [String, nil] Omit to keep the uploaded file's own name. Sending "" also keeps it.
|
|
44
|
+
# @param description [String, nil] Presence-aware: omit to carry the previous version's description forward,
|
|
45
|
+
# send "" to clear it, send a value to set it.
|
|
46
|
+
# @param notify [String, nil] Who to notify: "default", "everyone", or "custom" (the people in subscriptions).
|
|
47
|
+
#
|
|
48
|
+
# Omit both this and subscriptions to notify nobody. A subscriptions array sent
|
|
49
|
+
# without notify is read as "custom".
|
|
50
|
+
# @param subscriptions [Array, nil] People to notify about the replacement and subscribe to the upload.
|
|
51
|
+
# @return [Hash] response data
|
|
52
|
+
def create_version(upload_id:, attachable_sgid:, base_name: nil, description: nil, notify: nil, subscriptions: nil)
|
|
53
|
+
with_operation(service: "uploads", operation: "create_version", is_mutation: true, resource_id: upload_id) do
|
|
54
|
+
http_post("/uploads/#{upload_id}/versions.json", body: compact_params(attachable_sgid: attachable_sgid, base_name: base_name, description: description, notify: notify, subscriptions: subscriptions)).json
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
40
58
|
# List uploads in a vault
|
|
41
59
|
# @param vault_id [Integer] vault id ID
|
|
42
60
|
# @param page [Integer, nil] Page number for paginating through results. Defaults to 1. A positive value selects exactly that page, not a starting offset; see SPEC section 8.
|
|
@@ -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-08-
|
|
4
|
+
# Generated: 2026-08-07T11:12:00Z
|
|
5
5
|
|
|
6
6
|
require "json"
|
|
7
7
|
require "time"
|
|
@@ -5063,6 +5063,82 @@ module Basecamp
|
|
|
5063
5063
|
end
|
|
5064
5064
|
end
|
|
5065
5065
|
|
|
5066
|
+
# UploadVersion
|
|
5067
|
+
class UploadVersion
|
|
5068
|
+
include TypeHelpers
|
|
5069
|
+
attr_accessor :action, :created_at, :creator, :id, :recording_id, :boosts_count, :boosts_url, :details, :upload
|
|
5070
|
+
|
|
5071
|
+
# @return [Array<Symbol>]
|
|
5072
|
+
def self.required_fields
|
|
5073
|
+
%i[action created_at creator id recording_id].freeze
|
|
5074
|
+
end
|
|
5075
|
+
|
|
5076
|
+
def initialize(data = {})
|
|
5077
|
+
@action = data["action"]
|
|
5078
|
+
@created_at = parse_datetime(data["created_at"])
|
|
5079
|
+
@creator = parse_type(data["creator"], "Person")
|
|
5080
|
+
@id = parse_integer(data["id"])
|
|
5081
|
+
@recording_id = parse_integer(data["recording_id"])
|
|
5082
|
+
@boosts_count = parse_integer(data["boosts_count"])
|
|
5083
|
+
@boosts_url = data["boosts_url"]
|
|
5084
|
+
@details = parse_type(data["details"], "EventDetails")
|
|
5085
|
+
@upload = parse_type(data["upload"], "UploadVersionFile")
|
|
5086
|
+
end
|
|
5087
|
+
|
|
5088
|
+
def to_h
|
|
5089
|
+
{
|
|
5090
|
+
"action" => @action,
|
|
5091
|
+
"created_at" => @created_at,
|
|
5092
|
+
"creator" => @creator,
|
|
5093
|
+
"id" => @id,
|
|
5094
|
+
"recording_id" => @recording_id,
|
|
5095
|
+
"boosts_count" => @boosts_count,
|
|
5096
|
+
"boosts_url" => @boosts_url,
|
|
5097
|
+
"details" => @details,
|
|
5098
|
+
"upload" => @upload,
|
|
5099
|
+
}.compact
|
|
5100
|
+
end
|
|
5101
|
+
|
|
5102
|
+
def to_json(*args)
|
|
5103
|
+
to_h.to_json(*args)
|
|
5104
|
+
end
|
|
5105
|
+
end
|
|
5106
|
+
|
|
5107
|
+
# UploadVersionFile
|
|
5108
|
+
class UploadVersionFile
|
|
5109
|
+
include TypeHelpers
|
|
5110
|
+
attr_accessor :app_download_url, :current, :download_url, :filename, :byte_size, :content_type
|
|
5111
|
+
|
|
5112
|
+
# @return [Array<Symbol>]
|
|
5113
|
+
def self.required_fields
|
|
5114
|
+
%i[app_download_url current download_url filename].freeze
|
|
5115
|
+
end
|
|
5116
|
+
|
|
5117
|
+
def initialize(data = {})
|
|
5118
|
+
@app_download_url = data["app_download_url"]
|
|
5119
|
+
@current = parse_boolean(data["current"])
|
|
5120
|
+
@download_url = data["download_url"]
|
|
5121
|
+
@filename = data["filename"]
|
|
5122
|
+
@byte_size = parse_integer(data["byte_size"])
|
|
5123
|
+
@content_type = data["content_type"]
|
|
5124
|
+
end
|
|
5125
|
+
|
|
5126
|
+
def to_h
|
|
5127
|
+
{
|
|
5128
|
+
"app_download_url" => @app_download_url,
|
|
5129
|
+
"current" => @current,
|
|
5130
|
+
"download_url" => @download_url,
|
|
5131
|
+
"filename" => @filename,
|
|
5132
|
+
"byte_size" => @byte_size,
|
|
5133
|
+
"content_type" => @content_type,
|
|
5134
|
+
}.compact
|
|
5135
|
+
end
|
|
5136
|
+
|
|
5137
|
+
def to_json(*args)
|
|
5138
|
+
to_h.to_json(*args)
|
|
5139
|
+
end
|
|
5140
|
+
end
|
|
5141
|
+
|
|
5066
5142
|
# Vault
|
|
5067
5143
|
class Vault
|
|
5068
5144
|
include TypeHelpers
|
data/lib/basecamp/http.rb
CHANGED
|
@@ -632,6 +632,12 @@ module Basecamp
|
|
|
632
632
|
Basecamp.compose_validation_message(Basecamp.parse_error_message(body), field_errors) || "Validation failed"
|
|
633
633
|
)
|
|
634
634
|
Basecamp::ValidationError.new(message, http_status: status, field_errors: field_errors)
|
|
635
|
+
when 507
|
|
636
|
+
# A 5xx status carrying a client fact: the account is out of storage, or
|
|
637
|
+
# at its webhook ceiling. Retrying cannot satisfy it, so this is decided
|
|
638
|
+
# before the 5xx arms below.
|
|
639
|
+
message = Security.truncate(Basecamp.parse_error_message(body) || "Account limit reached")
|
|
640
|
+
Basecamp::LimitExceededError.new(message)
|
|
635
641
|
when 500
|
|
636
642
|
Basecamp::ApiError.new("Server error (500)", http_status: 500, retryable: true)
|
|
637
643
|
when 502, 503, 504
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Basecamp
|
|
4
|
+
# Raised when an account limit blocks the request (HTTP 507) — file storage
|
|
5
|
+
# exhausted, or a webhook ceiling reached.
|
|
6
|
+
#
|
|
7
|
+
# Never retryable: no amount of backoff frees storage or raises a plan limit.
|
|
8
|
+
# That is the whole reason this is not an ApiError, which a 507 would
|
|
9
|
+
# otherwise become through the 5xx catch-all.
|
|
10
|
+
class LimitExceededError < Error
|
|
11
|
+
def initialize(message = "Account limit reached", hint: nil, cause: nil)
|
|
12
|
+
super(
|
|
13
|
+
code: ErrorCode::LIMIT_EXCEEDED,
|
|
14
|
+
message: message,
|
|
15
|
+
hint: hint,
|
|
16
|
+
http_status: 507,
|
|
17
|
+
retryable: false,
|
|
18
|
+
cause: cause
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -5,11 +5,20 @@ module Basecamp
|
|
|
5
5
|
# Service for authorization operations.
|
|
6
6
|
# This is the only service that doesn't require an account context.
|
|
7
7
|
#
|
|
8
|
+
# The document's shape depends on which issuer served it. Discovery selects a
|
|
9
|
+
# BC5 issuer whenever one is advertised, and a BC5 issuer serves its *own*
|
|
10
|
+
# document (+app/views/api/authorizations/show.json.jbuilder+), which is not
|
|
11
|
+
# Launchpad's: it carries +identity.id+ and nothing else of the identity, no
|
|
12
|
+
# +product+ or +app_href+ on accounts, an RFC 8707 +resource+ indicator
|
|
13
|
+
# instead, a top-level +scope+ for BC3-issued tokens, and +expires_at+ as
|
|
14
|
+
# integer epoch seconds rather than ISO-8601. Only +identity.id+,
|
|
15
|
+
# +accounts[].id+, +accounts[].name+ and +accounts[].href+ are common to both.
|
|
16
|
+
#
|
|
8
17
|
# @example Get authorization info
|
|
9
18
|
# auth = client.authorization.get
|
|
10
|
-
# puts "Identity: #{auth["identity"]["
|
|
19
|
+
# puts "Identity: #{auth["identity"]["id"]}"
|
|
11
20
|
# auth["accounts"].each do |account|
|
|
12
|
-
# puts "Account: #{account["name"]} (#{account["
|
|
21
|
+
# puts "Account: #{account["name"]} (#{account["href"]})"
|
|
13
22
|
# end
|
|
14
23
|
class AuthorizationService < BaseService
|
|
15
24
|
# Gets authorization information for the current user.
|
data/lib/basecamp/version.rb
CHANGED
data/lib/basecamp.rb
CHANGED
|
@@ -132,6 +132,10 @@ module Basecamp
|
|
|
132
132
|
NotFoundError.new(message: message)
|
|
133
133
|
when 429
|
|
134
134
|
RateLimitError.new(retry_after: retry_after)
|
|
135
|
+
when 507
|
|
136
|
+
# Decided before the 5xx arms: a 507 is an account limit, not a
|
|
137
|
+
# transient server failure, and no retry can satisfy it.
|
|
138
|
+
LimitExceededError.new(Security.truncate(message))
|
|
135
139
|
when 500
|
|
136
140
|
ApiError.new("Server error (500)", http_status: 500, retryable: true)
|
|
137
141
|
when 502, 503, 504
|
|
@@ -14,7 +14,9 @@ class MetadataExtractor
|
|
|
14
14
|
METHODS = %w[get post put patch delete].freeze
|
|
15
15
|
|
|
16
16
|
def initialize(openapi_path)
|
|
17
|
-
|
|
17
|
+
# Read as UTF-8 regardless of process locale (LC_ALL=C would otherwise read
|
|
18
|
+
# as US-ASCII and JSON.parse dies on the spec's multibyte characters)
|
|
19
|
+
@openapi = JSON.parse(File.read(openapi_path, encoding: 'UTF-8'))
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def extract
|
|
@@ -65,7 +65,7 @@ class ServiceGenerator
|
|
|
65
65
|
},
|
|
66
66
|
'Files' => {
|
|
67
67
|
'Attachments' => %w[CreateAttachment],
|
|
68
|
-
'Uploads' => %w[GetUpload UpdateUpload ListUploads CreateUpload ListUploadVersions],
|
|
68
|
+
'Uploads' => %w[GetUpload UpdateUpload ListUploads CreateUpload ListUploadVersions CreateUploadVersion],
|
|
69
69
|
'Vaults' => %w[GetVault UpdateVault ListVaults CreateVault],
|
|
70
70
|
'Documents' => %w[GetDocument ReplaceDocument ListDocuments CreateDocument],
|
|
71
71
|
'CloudFiles' => %w[GetCloudFile CreateCloudFile UpdateCloudFile],
|
|
@@ -217,6 +217,7 @@ class ServiceGenerator
|
|
|
217
217
|
'ListUploads' => 'list',
|
|
218
218
|
'CreateUpload' => 'create',
|
|
219
219
|
'ListUploadVersions' => 'list_versions',
|
|
220
|
+
'CreateUploadVersion' => 'create_version',
|
|
220
221
|
'GetMessage' => 'get',
|
|
221
222
|
'UpdateMessage' => 'update',
|
|
222
223
|
'CreateMessage' => 'create',
|
|
@@ -320,7 +321,8 @@ class ServiceGenerator
|
|
|
320
321
|
].freeze
|
|
321
322
|
|
|
322
323
|
def initialize(openapi_path)
|
|
323
|
-
|
|
324
|
+
# UTF-8 regardless of process locale — see generate-metadata.rb
|
|
325
|
+
@openapi = JSON.parse(File.read(openapi_path, encoding: 'UTF-8'))
|
|
324
326
|
@schemas = @openapi.dig('components', 'schemas') || {}
|
|
325
327
|
end
|
|
326
328
|
|
data/scripts/generate-types.rb
CHANGED
|
@@ -131,7 +131,8 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
131
131
|
puts ' module Types'
|
|
132
132
|
puts ' include TypeHelpers'
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
# UTF-8 regardless of process locale — see generate-metadata.rb
|
|
135
|
+
schemas = JSON.parse(File.read(openapi_path, encoding: 'UTF-8'))['components']['schemas'] || {}
|
|
135
136
|
sorted = schemas.keys.sort
|
|
136
137
|
|
|
137
138
|
sorted.each do |name|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: basecamp-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Basecamp
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -235,6 +235,7 @@ files:
|
|
|
235
235
|
- lib/basecamp/generated/types.rb
|
|
236
236
|
- lib/basecamp/hooks.rb
|
|
237
237
|
- lib/basecamp/http.rb
|
|
238
|
+
- lib/basecamp/limit_exceeded_error.rb
|
|
238
239
|
- lib/basecamp/list_enumerator.rb
|
|
239
240
|
- lib/basecamp/list_meta.rb
|
|
240
241
|
- lib/basecamp/logger_hooks.rb
|