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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d41385ab7c36a096b4b54f56a0b60af4c6ab42e561e4bf147bdd850b77c8e230
4
- data.tar.gz: e93de2347867e1f40afcee749d1e49a7873fffd4f656200bf5305e0fef813b29
3
+ metadata.gz: 9b770f1606323f83fb028ebb44d51e0d2500664a6d88ec9209dc227807ace197
4
+ data.tar.gz: 418cf25fbdc65f25db5e9d816565cd20a7f2e3d0c06c5a147dcf896c38d2ec9e
5
5
  SHA512:
6
- metadata.gz: 8f33657f9328f7a786aa33925a96ab89786f655dd0f83db71f80b9fb9472c06bd2400a15b5637e964a00fc146a1dbae0f86b49157939f1df983f52838ec42380
7
- data.tar.gz: 2db82e6a78234d1f12f6e54a139d490df8f0f5205135a23b8946435f75f7120e863659d3c3fdeb6ae3e57a7cb94b55a32a7ba4914aa1ad0c7ebfba08f701d59e
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.13.0?** Read [MIGRATING.md](../MIGRATING.md#ruby) before you bump the version — Ruby carries eleven breaks nothing catches at load time ten with no signal at all, and one that raises only on a record where the field is populated.
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
 
@@ -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
@@ -12,5 +12,6 @@ module Basecamp
12
12
  API = "api_error"
13
13
  AMBIGUOUS = "ambiguous"
14
14
  VALIDATION = "validation"
15
+ LIMIT_EXCEEDED = "limit_exceeded"
15
16
  end
16
17
  end
@@ -13,5 +13,6 @@ module Basecamp
13
13
  API = 7
14
14
  AMBIGUOUS = 8
15
15
  VALIDATION = 9
16
+ LIMIT_EXCEEDED = 10
16
17
  end
17
18
  end
@@ -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-06T00:45:34Z",
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-06T00:45:34Z
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"]["email_address"]}"
19
+ # puts "Identity: #{auth["identity"]["id"]}"
11
20
  # auth["accounts"].each do |account|
12
- # puts "Account: #{account["name"]} (#{account["id"]})"
21
+ # puts "Account: #{account["name"]} (#{account["href"]})"
13
22
  # end
14
23
  class AuthorizationService < BaseService
15
24
  # Gets authorization information for the current user.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Basecamp
4
- VERSION = "0.13.0"
4
+ VERSION = "0.14.0"
5
5
  API_VERSION = "2026-08-05"
6
6
  end
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
- @openapi = JSON.parse(File.read(openapi_path))
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
- @openapi = JSON.parse(File.read(openapi_path))
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
 
@@ -131,7 +131,8 @@ if __FILE__ == $PROGRAM_NAME
131
131
  puts ' module Types'
132
132
  puts ' include TypeHelpers'
133
133
 
134
- schemas = JSON.parse(File.read(openapi_path))['components']['schemas'] || {}
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.13.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-07 00:00:00.000000000 Z
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