runapi-core 0.2.5 → 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: 43b27ac4a9c29db093d7efcad4686efe7730aa9ddb906a956c0f2a4b353e0ccd
4
- data.tar.gz: a42105c4a366fa9a17642f519392ae6dd50978a32ae73fae6c62f38f8b02cb28
3
+ metadata.gz: b0f298f55ee5728667cb83a8db61391cd20bdc64582faec2207d5db9ce79c384
4
+ data.tar.gz: fe0c644ac7dd1377e0c4b0c0fb4b545afc0ff175ac83c3c3886b5219d93aed4a
5
5
  SHA512:
6
- metadata.gz: 9bee66f588446e79bbbf8eea48964bb2505bfee41c4dc9c68ce1622359e0a536e498a895c065ef8efb54c2ee41b5df19b8d7c55b8707d181c3585d2887cb29b5
7
- data.tar.gz: bb4dc1ff3c4f564c89bc20e4838263864704d54a80f315f093dba9d77e6efabce80bc4087748b816aec20ac0674141e30b1d3ee632e6d92b1f006bb4c07d2b3b
6
+ metadata.gz: 5d473dc453e4dba6a452c54901ad2274453464de990d34c3c2c685a92d24cdb8fb9b07d6e57086913e560f034f11e5a5deea40f030bf1c1909a02724907ac4af
7
+ data.tar.gz: 47ef6424ffd81307922ef8adfc7c42286b6b3cd69527478b28f0674ac23332f212c10682564da8ad23bdd3b5dc0728ae7f0ce678f8814ae4f7e40736392ffdc8
data/README.md CHANGED
@@ -12,6 +12,18 @@ gem install runapi-core
12
12
 
13
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
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
+
15
27
  ## License
16
28
 
17
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
@@ -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
@@ -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)
@@ -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
@@ -2,7 +2,7 @@
2
2
 
3
3
  module RunApi
4
4
  module Core
5
- VERSION = "0.2.5"
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.5
4
+ version: 0.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - RunAPI
@@ -38,12 +38,16 @@ files:
38
38
  - README.md
39
39
  - lib/runapi-core.rb
40
40
  - lib/runapi/core.rb
41
+ - lib/runapi/core/account.rb
41
42
  - lib/runapi/core/auth.rb
42
43
  - lib/runapi/core/base_model.rb
44
+ - lib/runapi/core/client.rb
43
45
  - lib/runapi/core/configuration.rb
44
46
  - lib/runapi/core/constants.rb
45
47
  - lib/runapi/core/errors.rb
48
+ - lib/runapi/core/files.rb
46
49
  - lib/runapi/core/http_client.rb
50
+ - lib/runapi/core/multipart_body.rb
47
51
  - lib/runapi/core/polling.rb
48
52
  - lib/runapi/core/resource_helpers.rb
49
53
  - lib/runapi/core/types.rb