runapi-core 0.2.4 → 0.2.6

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: df4b7518746af7dc568a3fbda315ef3b1f9e683fb1a7919431c2654cafb54f53
4
- data.tar.gz: d2b57145c7d3d2855df5cd360e31a01614234a4d0ff9fde121ca7a1a7cd1ab7f
3
+ metadata.gz: b0f298f55ee5728667cb83a8db61391cd20bdc64582faec2207d5db9ce79c384
4
+ data.tar.gz: fe0c644ac7dd1377e0c4b0c0fb4b545afc0ff175ac83c3c3886b5219d93aed4a
5
5
  SHA512:
6
- metadata.gz: 0e7b6a5a46d3170cdc14a3ee1046d1ca5efc3a5a068150e909a6130cd04ad28701e9904e161bd1f064c8637a911410e52d34f05dbbb14355644eac24f6d8836e
7
- data.tar.gz: '0873f8ed32e8f8ac2aef057e784be881be9dc587abaa97eeb2773ab57309851dd97c59ed7005837e4794050a79fe301d4e98313d525e4945ff7375ea834cb18b'
6
+ metadata.gz: 5d473dc453e4dba6a452c54901ad2274453464de990d34c3c2c685a92d24cdb8fb9b07d6e57086913e560f034f11e5a5deea40f030bf1c1909a02724907ac4af
7
+ data.tar.gz: 47ef6424ffd81307922ef8adfc7c42286b6b3cd69527478b28f0674ac23332f212c10682564da8ad23bdd3b5dc0728ae7f0ce678f8814ae4f7e40736392ffdc8
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # RunAPI Core Ruby SDK
2
+
3
+ The RunAPI Core Ruby SDK provides shared authentication, HTTP, retry, error, and polling primitives for RunAPI model gems. Install `runapi-core` only when you are building SDK infrastructure or shared Ruby tooling; application code should normally install a concrete model gem such as `runapi-suno`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ gem install runapi-core
9
+ ```
10
+
11
+ ## Notes
12
+
13
+ Use the core gem for common client options, error classes, request helpers, and task polling behavior that model SDKs share. Public SDK docs live at https://runapi.ai/docs#runapi-sdks and the model catalog lives at https://runapi.ai/models.
14
+
15
+ ## File Upload
16
+
17
+ ```ruby
18
+ client = RunApi::NanoBanana::Client.new(api_key: ENV["RUNAPI_API_KEY"])
19
+
20
+ upload = client.files.create(source: {type: "url", url: "https://example.com/photo.jpg"})
21
+ puts upload.url
22
+ ```
23
+
24
+ > [!IMPORTANT]
25
+ > Uploaded file URLs expire 1 hour after creation. Pass them to a model promptly rather than storing them for later use.
26
+
27
+ ## License
28
+
29
+ Licensed under the Apache License, Version 2.0.
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RunApi
4
+ module Core
5
+ class Account
6
+ include RunApi::Core::ResourceHelpers
7
+
8
+ INFO_ENDPOINT = "/api/v1/me"
9
+ BALANCE_ENDPOINT = "/api/v1/me/balance"
10
+
11
+ class AccountRecord < RunApi::Core::BaseModel
12
+ required :id, Integer
13
+ required :name, String
14
+ end
15
+
16
+ class InfoResponse < RunApi::Core::BaseModel
17
+ required :id, Integer
18
+ required :name, String
19
+ required :email, String
20
+ required :account, AccountRecord
21
+ end
22
+
23
+ class BalanceResponse < RunApi::Core::BaseModel
24
+ required :balance_cents, Integer
25
+ required :paid_balance_cents, Integer
26
+ required :bonus_balance_cents, Integer
27
+ required :spent_cents_today, Integer
28
+ required :spent_cents_total, Integer
29
+ end
30
+
31
+ def initialize(http)
32
+ @http = http
33
+ end
34
+
35
+ def info(options: nil)
36
+ request(:get, INFO_ENDPOINT, options:, response_class: InfoResponse)
37
+ end
38
+
39
+ def balance(options: nil)
40
+ request(:get, BALANCE_ENDPOINT, options:, response_class: BalanceResponse)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RunApi
4
+ module Core
5
+ # Base class for every RunAPI client. Resolves the API key, builds the shared
6
+ # HTTP client, and exposes the Universal Resources (file upload, account) that
7
+ # are available on any client regardless of which model gem was required.
8
+ #
9
+ # Provider clients inherit from this and build their model resources from the
10
+ # protected +http+ reader.
11
+ class Client
12
+ # @return [Files] Temporary file upload operations.
13
+ attr_reader :files
14
+ # @return [Account] Account info and balance operations.
15
+ attr_reader :account
16
+
17
+ def initialize(api_key: nil, **options)
18
+ @api_key = Core::Auth.resolve_api_key(api_key)
19
+ client_options = Core::ClientOptions.new(api_key: @api_key, **options)
20
+ @http = client_options.http_client || Core::HttpClient.new(client_options)
21
+ @files = Files.new(@http)
22
+ @account = Account.new(@http)
23
+ end
24
+
25
+ protected
26
+
27
+ attr_reader :http
28
+ end
29
+ end
30
+ end
@@ -21,7 +21,7 @@ module RunApi
21
21
 
22
22
  IDEMPOTENT_METHODS = %w[GET HEAD PUT DELETE OPTIONS].freeze
23
23
 
24
- RETRYABLE_STATUS_CODES = [ 429, 500, 502, 503, 504 ].freeze
24
+ RETRYABLE_STATUS_CODES = [429, 500, 502, 503, 504].freeze
25
25
  end
26
26
  end
27
27
  end
@@ -39,15 +39,12 @@ module RunApi
39
39
  409 => "ConflictError",
40
40
  422 => "ValidationError",
41
41
  429 => "RateLimitError",
42
- 451 => "ValidationError",
43
- 455 => "ServiceUnavailableError",
44
42
  500 => "ServerError",
45
43
  501 => "ServerError",
46
44
  502 => "ServerError",
47
45
  503 => "ServiceUnavailableError",
48
46
  504 => "ServerError",
49
- 505 => "ServerError",
50
- 531 => "ServerError"
47
+ 505 => "ServerError"
51
48
  }.freeze
52
49
 
53
50
  DEFAULT_MESSAGES = {
@@ -61,8 +58,6 @@ module RunApi
61
58
  415 => "Unsupported media type",
62
59
  422 => "Validation failed",
63
60
  429 => "Too many requests",
64
- 451 => "Failed to fetch image",
65
- 455 => "Service unavailable (maintenance)",
66
61
  503 => "Service unavailable"
67
62
  }.freeze
68
63
 
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RunApi
4
+ module Core
5
+ class Files
6
+ include RunApi::Core::ResourceHelpers
7
+
8
+ ENDPOINT = "/api/v1/files"
9
+
10
+ class UploadResponse < RunApi::Core::BaseModel
11
+ required :file_name, String
12
+ required :url, String
13
+ required :size_bytes, Integer
14
+ required :mime_type, String
15
+ required :created_at, String
16
+ required :expires_at, String
17
+ end
18
+
19
+ RESPONSE_CLASS = UploadResponse
20
+
21
+ def initialize(http)
22
+ @http = http
23
+ end
24
+
25
+ def create(file: nil, source: nil, file_name: nil, options: nil)
26
+ validate_source!(file:, source:)
27
+
28
+ body = if file
29
+ multipart_body(file, file_name:)
30
+ else
31
+ compact_params(source:, file_name:)
32
+ end
33
+
34
+ request(:post, ENDPOINT, body:, options:)
35
+ end
36
+
37
+ private
38
+
39
+ def validate_source!(file:, source:)
40
+ source_count = [file, source].count { |value| !value.nil? }
41
+ return if source_count == 1
42
+
43
+ raise ArgumentError, "Exactly one source is required: file or source"
44
+ end
45
+
46
+ def multipart_body(file, file_name:)
47
+ path = file_path(file)
48
+ filename = file_name || File.basename(path)
49
+ Core::MultipartBody.new(
50
+ fields: compact_params(file_name: file_name),
51
+ files: {
52
+ file: Core::MultipartFile.new(path:, filename:)
53
+ }
54
+ )
55
+ end
56
+
57
+ def file_path(file)
58
+ return file if file.is_a?(String)
59
+ return file.path if file.respond_to?(:path)
60
+
61
+ raise ArgumentError, "file must be a file path or respond to :path"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -3,7 +3,7 @@
3
3
  module RunApi
4
4
  module Core
5
5
  class HttpClient
6
- STALE_CONNECTION_ERRORS = [ Errno::EPIPE, EOFError, IOError, OpenSSL::SSL::SSLError ].freeze
6
+ STALE_CONNECTION_ERRORS = [Errno::EPIPE, EOFError, IOError, OpenSSL::SSL::SSLError].freeze
7
7
 
8
8
  def initialize(options)
9
9
  @options = options
@@ -50,6 +50,8 @@ module RunApi
50
50
 
51
51
  raise error
52
52
  end
53
+ ensure
54
+ close_multipart_files(req)
53
55
  end
54
56
 
55
57
  private
@@ -69,16 +71,45 @@ module RunApi
69
71
  req = klass.new(uri.request_uri)
70
72
 
71
73
  req["Authorization"] = "Bearer #{@options.api_key}"
72
- req["Content-Type"] = "application/json"
73
74
  req["Accept"] = "application/json"
74
75
  req["User-Agent"] = Constants::SDK_USER_AGENT
75
76
 
76
77
  options&.headers&.each { |k, v| req[k.to_s] = v }
77
78
 
78
- req.body = JSON.generate(body) if body
79
+ if body.is_a?(MultipartBody)
80
+ req.set_form(multipart_parts(body), "multipart/form-data")
81
+ elsif body
82
+ req["Content-Type"] = "application/json"
83
+ req.body = JSON.generate(body)
84
+ end
79
85
  req
80
86
  end
81
87
 
88
+ def multipart_parts(body)
89
+ opened_files = []
90
+ field_parts = body.fields.map { |key, value| [key, value.to_s] }
91
+ file_parts = body.files.map do |key, file|
92
+ options = {filename: file.filename}
93
+ options[:content_type] = file.content_type if file.content_type
94
+ opened_files << File.open(file.path, "rb")
95
+ [key, opened_files.last, options]
96
+ end
97
+ field_parts + file_parts
98
+ rescue
99
+ opened_files.each { |file| file.close unless file.closed? }
100
+ raise
101
+ end
102
+
103
+ def close_multipart_files(request)
104
+ body_data = request&.instance_variable_get(:@body_data)
105
+ return unless body_data
106
+
107
+ body_data.each do |part|
108
+ file = part[1]
109
+ file.close if file.is_a?(File) && !file.closed?
110
+ end
111
+ end
112
+
82
113
  def retryable?(method, status)
83
114
  Constants::IDEMPOTENT_METHODS.include?(method.to_s.upcase) &&
84
115
  Constants::RETRYABLE_STATUS_CODES.include?(status)
@@ -91,7 +122,7 @@ module RunApi
91
122
 
92
123
  base = @options.retry_base_delay * (2**(attempt - 1))
93
124
  jitter = rand * base * 0.5
94
- [ base + jitter, @options.retry_max_delay ].min
125
+ [base + jitter, @options.retry_max_delay].min
95
126
  end
96
127
 
97
128
  def parse_body(body)
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RunApi
4
+ module Core
5
+ MultipartFile = Struct.new(:path, :filename, :content_type, keyword_init: true)
6
+
7
+ class MultipartBody
8
+ attr_reader :fields, :files
9
+
10
+ def initialize(fields: {}, files: {})
11
+ @fields = stringify_keys(fields)
12
+ @files = stringify_keys(files)
13
+ end
14
+
15
+ private
16
+
17
+ def stringify_keys(hash)
18
+ hash.each_with_object({}) do |(key, value), result|
19
+ result[key.to_s] = value
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -15,7 +15,7 @@ module RunApi
15
15
  @http.request(method, path)
16
16
  end
17
17
  else
18
- kwargs = { body: body }
18
+ kwargs = {body: body}
19
19
  kwargs[:options] = options if options
20
20
  @http.request(method, path, **kwargs)
21
21
  end
@@ -107,8 +107,6 @@ module RunApi
107
107
  # @return [String, nil] Current task status.
108
108
  # @!attribute [r] error
109
109
  # @return [String, nil] Error message if task failed.
110
- # @!attribute [r] data
111
- # @return [RunApi::Core::DynamicModel, nil] Task-specific result data.
112
110
  class TaskResponse < BaseModel
113
111
  module Status
114
112
  PENDING = "pending"
@@ -116,13 +114,12 @@ module RunApi
116
114
  COMPLETED = "completed"
117
115
  FAILED = "failed"
118
116
 
119
- ALL = [ PENDING, PROCESSING, COMPLETED, FAILED ].freeze
117
+ ALL = [PENDING, PROCESSING, COMPLETED, FAILED].freeze
120
118
  end
121
119
 
122
120
  optional :id, String
123
121
  optional :status, String
124
122
  optional :error, String
125
- optional :data, -> { DynamicModel }
126
123
  end
127
124
  end
128
125
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module RunApi
4
4
  module Core
5
- VERSION = "0.2.4"
5
+ VERSION = "0.2.6"
6
6
  end
7
7
 
8
8
  VERSION = Core::VERSION
data/lib/runapi/core.rb CHANGED
@@ -14,6 +14,10 @@ require_relative "core/base_model"
14
14
  require_relative "core/types"
15
15
  require_relative "core/errors"
16
16
  require_relative "core/auth"
17
+ require_relative "core/multipart_body"
17
18
  require_relative "core/http_client"
18
19
  require_relative "core/polling"
19
20
  require_relative "core/resource_helpers"
21
+ require_relative "core/files"
22
+ require_relative "core/account"
23
+ require_relative "core/client"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: runapi-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - RunAPI
@@ -23,22 +23,31 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '2.4'
26
- description: RunAPI core SDK for JavaScript, Ruby, and Go
26
+ description: The RunAPI Core Ruby SDK provides shared authentication, HTTP, retry,
27
+ error, and polling primitives for RunAPI model gems. Install `runapi-core` only
28
+ when you are building SDK infrastructure or shared Ruby tooling; application code
29
+ should normally install a concrete model gem such as `runapi-suno`.
27
30
  email:
28
31
  - contact@runapi.ai
29
32
  executables: []
30
33
  extensions: []
31
- extra_rdoc_files: []
34
+ extra_rdoc_files:
35
+ - README.md
32
36
  files:
33
37
  - LICENSE
38
+ - README.md
34
39
  - lib/runapi-core.rb
35
40
  - lib/runapi/core.rb
41
+ - lib/runapi/core/account.rb
36
42
  - lib/runapi/core/auth.rb
37
43
  - lib/runapi/core/base_model.rb
44
+ - lib/runapi/core/client.rb
38
45
  - lib/runapi/core/configuration.rb
39
46
  - lib/runapi/core/constants.rb
40
47
  - lib/runapi/core/errors.rb
48
+ - lib/runapi/core/files.rb
41
49
  - lib/runapi/core/http_client.rb
50
+ - lib/runapi/core/multipart_body.rb
42
51
  - lib/runapi/core/polling.rb
43
52
  - lib/runapi/core/resource_helpers.rb
44
53
  - lib/runapi/core/types.rb
@@ -48,7 +57,7 @@ licenses:
48
57
  - Apache-2.0
49
58
  metadata:
50
59
  homepage_uri: https://runapi.ai/models
51
- documentation_uri: https://github.com/runapi-ai/core-sdk/blob/main/README.md
60
+ documentation_uri: https://github.com/runapi-ai/core-sdk/blob/main/ruby/README.md
52
61
  source_code_uri: https://github.com/runapi-ai/core-sdk
53
62
  changelog_uri: https://github.com/runapi-ai/core-sdk/blob/main/CHANGELOG.md
54
63
  rdoc_options: []
@@ -67,5 +76,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
67
76
  requirements: []
68
77
  rubygems_version: 4.0.10
69
78
  specification_version: 4
70
- summary: Shared SDK primitives for RunAPI JavaScript, Ruby, and Go SDKs.
79
+ summary: RunAPI Core Ruby SDK
71
80
  test_files: []