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 +4 -4
- data/README.md +29 -0
- data/lib/runapi/core/account.rb +44 -0
- data/lib/runapi/core/client.rb +30 -0
- data/lib/runapi/core/constants.rb +1 -1
- data/lib/runapi/core/errors.rb +1 -6
- data/lib/runapi/core/files.rb +65 -0
- data/lib/runapi/core/http_client.rb +35 -4
- data/lib/runapi/core/multipart_body.rb +24 -0
- data/lib/runapi/core/resource_helpers.rb +1 -1
- data/lib/runapi/core/types.rb +1 -4
- data/lib/runapi/core/version.rb +1 -1
- data/lib/runapi/core.rb +4 -0
- metadata +14 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b0f298f55ee5728667cb83a8db61391cd20bdc64582faec2207d5db9ce79c384
|
|
4
|
+
data.tar.gz: fe0c644ac7dd1377e0c4b0c0fb4b545afc0ff175ac83c3c3886b5219d93aed4a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/runapi/core/errors.rb
CHANGED
|
@@ -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 = [
|
|
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
|
-
|
|
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
|
-
[
|
|
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
|
data/lib/runapi/core/types.rb
CHANGED
|
@@ -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 = [
|
|
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
|
data/lib/runapi/core/version.rb
CHANGED
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
|
+
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
|
|
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:
|
|
79
|
+
summary: RunAPI Core Ruby SDK
|
|
71
80
|
test_files: []
|