e2b 0.2.0 → 0.3.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: d76a27510ea8da9f113ba2efb9aecf7c06377d20a409153491f6a918ee506a72
4
- data.tar.gz: c0b1dae2a55304bbddd9211a54dea1ba964be0dcf73557a479f936627ebd67c3
3
+ metadata.gz: 103831a7280b1f523e4a7299820e40b328d0fd840f65850f42336f8c8b0e23ee
4
+ data.tar.gz: d079861b11c20bc1e2280badc7e7e9032810a736a7bbca02a433802be27b2053
5
5
  SHA512:
6
- metadata.gz: 8777bb85409fab9f15c62c7d8d39856c7901616e90cc150914b78deaf4e71ba32540545dced53513cfee7641fa234815254ff529f5ebd41f533513696af436de
7
- data.tar.gz: b1bd243b3741b8cba7860ffc36f0398ed5b049cb82482d65a1fea0f5da55071d3664602b3144ff7cbb06c4c90708e991ae565c9ba032ced51dac0f4e0bd6e2fb
6
+ metadata.gz: 9fa1e68c98160a18f31afb994e66fc55d20647cc0769f80afbc8ab6fccc45df2c2e7d0f0033a37dd3e7a1571d373b0cd92060f98f6ab675241aa61309e0e70d9
7
+ data.tar.gz: 759cffe0f051858f7f851f2f7599d495b24ca6abad1d313bed0cc96a7794e2a47eecacfdd6ecc81577d6fba18c9094824ec235022b8746047d630dce697ace0c
data/README.md CHANGED
@@ -17,10 +17,14 @@ Aligned with the [official E2B SDKs](https://github.com/e2b-dev/E2B) (Python/JS)
17
17
 
18
18
  ## Installation
19
19
 
20
- Add to your Gemfile:
20
+ ```bash
21
+ gem install e2b
22
+ ```
23
+
24
+ Or add to your Gemfile:
21
25
 
22
26
  ```ruby
23
- gem 'e2b', git: 'https://github.com/ya-luotao/e2b-ruby.git'
27
+ gem 'e2b'
24
28
  ```
25
29
 
26
30
  Then:
@@ -10,6 +10,8 @@ module E2B
10
10
  #
11
11
  # Handles authentication, request/response processing, and error handling.
12
12
  class HttpClient
13
+ DetailedResponse = Struct.new(:body, :headers, keyword_init: true)
14
+
13
15
  # Default request timeout in seconds
14
16
  DEFAULT_TIMEOUT = 120
15
17
 
@@ -19,11 +21,13 @@ module E2B
19
21
  # Initialize a new HTTP client
20
22
  #
21
23
  # @param base_url [String] Base URL for API requests
22
- # @param api_key [String] API key for authentication
24
+ # @param api_key [String, nil] API key for authentication
25
+ # @param access_token [String, nil] Access token for bearer authentication
23
26
  # @param logger [Logger, nil] Optional logger
24
- def initialize(base_url:, api_key:, logger: nil)
27
+ def initialize(base_url:, api_key: nil, access_token: nil, logger: nil)
25
28
  @base_url = base_url.end_with?("/") ? base_url : "#{base_url}/"
26
29
  @api_key = api_key
30
+ @access_token = access_token
27
31
  @logger = logger
28
32
  @connection = build_connection
29
33
  end
@@ -34,8 +38,8 @@ module E2B
34
38
  # @param params [Hash] Query parameters
35
39
  # @param timeout [Integer] Request timeout in seconds
36
40
  # @return [Hash, Array, String] Parsed response body
37
- def get(path, params: {}, timeout: DEFAULT_TIMEOUT)
38
- handle_response do
41
+ def get(path, params: {}, timeout: DEFAULT_TIMEOUT, detailed: false)
42
+ handle_response(detailed: detailed) do
39
43
  @connection.get(normalize_path(path)) do |req|
40
44
  req.params = params
41
45
  req.options.timeout = timeout
@@ -49,8 +53,8 @@ module E2B
49
53
  # @param body [Hash, nil] Request body
50
54
  # @param timeout [Integer] Request timeout in seconds
51
55
  # @return [Hash, Array, String] Parsed response body
52
- def post(path, body: nil, timeout: DEFAULT_TIMEOUT)
53
- handle_response do
56
+ def post(path, body: nil, timeout: DEFAULT_TIMEOUT, detailed: false)
57
+ handle_response(detailed: detailed) do
54
58
  @connection.post(normalize_path(path)) do |req|
55
59
  req.body = body.to_json if body
56
60
  req.options.timeout = timeout
@@ -64,8 +68,8 @@ module E2B
64
68
  # @param body [Hash, nil] Request body
65
69
  # @param timeout [Integer] Request timeout in seconds
66
70
  # @return [Hash, Array, String] Parsed response body
67
- def put(path, body: nil, timeout: DEFAULT_TIMEOUT)
68
- handle_response do
71
+ def put(path, body: nil, timeout: DEFAULT_TIMEOUT, detailed: false)
72
+ handle_response(detailed: detailed) do
69
73
  @connection.put(normalize_path(path)) do |req|
70
74
  req.body = body.to_json if body
71
75
  req.options.timeout = timeout
@@ -76,11 +80,13 @@ module E2B
76
80
  # Perform a DELETE request
77
81
  #
78
82
  # @param path [String] API endpoint path
83
+ # @param body [Hash, nil] Request body
79
84
  # @param timeout [Integer] Request timeout in seconds
80
85
  # @return [Hash, Array, String, nil] Parsed response body
81
- def delete(path, timeout: DEFAULT_TIMEOUT)
82
- handle_response do
86
+ def delete(path, body: nil, timeout: DEFAULT_TIMEOUT, detailed: false)
87
+ handle_response(detailed: detailed) do
83
88
  @connection.delete(normalize_path(path)) do |req|
89
+ req.body = body.to_json if body
84
90
  req.options.timeout = timeout
85
91
  end
86
92
  end
@@ -100,22 +106,31 @@ module E2B
100
106
  conn.response :json, content_type: /\bjson$/
101
107
  conn.adapter Faraday.default_adapter
102
108
 
103
- conn.headers["X-API-Key"] = @api_key
109
+ conn.headers["X-API-Key"] = @api_key if @api_key && !@api_key.empty?
110
+ conn.headers["Authorization"] = "Bearer #{@access_token}" if @access_token && !@access_token.empty?
104
111
  conn.headers["Content-Type"] = "application/json"
105
112
  conn.headers["Accept"] = "application/json"
106
113
  conn.headers["User-Agent"] = "e2b-ruby-sdk/#{E2B::VERSION}"
107
114
  end
108
115
  end
109
116
 
110
- def handle_response
117
+ def handle_response(detailed: false)
111
118
  response = yield
112
119
  handle_error(response) unless response.success?
113
120
 
114
- body = response.body
121
+ parsed_body = parse_body(response.body, response.headers)
122
+ return DetailedResponse.new(body: parsed_body, headers: response.headers.to_h) if detailed
123
+
124
+ parsed_body
125
+ rescue Faraday::TimeoutError => e
126
+ raise E2B::TimeoutError, "Request timed out: #{e.message}"
127
+ rescue Faraday::ConnectionFailed => e
128
+ raise E2B::E2BError, "Connection failed: #{e.message}"
129
+ end
115
130
 
116
- # If body is a string but should be JSON, try to parse it
131
+ def parse_body(body, headers)
117
132
  if body.is_a?(String) && !body.empty?
118
- content_type = response.headers["content-type"] rescue "unknown"
133
+ content_type = headers["content-type"] rescue "unknown"
119
134
  if content_type&.include?("json") || body.start_with?("{", "[")
120
135
  begin
121
136
  return JSON.parse(body)
@@ -126,10 +141,6 @@ module E2B
126
141
  end
127
142
 
128
143
  body
129
- rescue Faraday::TimeoutError => e
130
- raise E2B::TimeoutError, "Request timed out: #{e.message}"
131
- rescue Faraday::ConnectionFailed => e
132
- raise E2B::E2BError, "Connection failed: #{e.message}"
133
144
  end
134
145
 
135
146
  def handle_error(response)
data/lib/e2b/client.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rubygems/version"
4
+
3
5
  module E2B
4
6
  # Client for interacting with the E2B API
5
7
  #
@@ -14,6 +16,8 @@ module E2B
14
16
  # @example Using Sandbox directly (recommended, matches official SDK)
15
17
  # sandbox = E2B::Sandbox.create(template: "base", api_key: "your-key")
16
18
  class Client
19
+ include SandboxHelpers
20
+
17
21
  # @return [Configuration] Client configuration
18
22
  attr_reader :config
19
23
 
@@ -30,10 +34,11 @@ module E2B
30
34
  @http_client = API::HttpClient.new(
31
35
  base_url: @config.api_url,
32
36
  api_key: @config.api_key,
37
+ access_token: @config.access_token,
33
38
  logger: @config.logger
34
39
  )
35
40
 
36
- @domain = Sandbox::DEFAULT_DOMAIN
41
+ @domain = @config.domain
37
42
  end
38
43
 
39
44
  # Create a new sandbox
@@ -44,7 +49,9 @@ module E2B
44
49
  # @param metadata [Hash, nil] Custom metadata
45
50
  # @param envs [Hash{String => String}, nil] Environment variables
46
51
  # @return [Sandbox] The created sandbox instance
47
- def create(template: "base", timeout: nil, timeout_ms: nil, metadata: nil, envs: nil, **_opts)
52
+ def create(template: "base", timeout: nil, timeout_ms: nil, metadata: nil, envs: nil,
53
+ secure: true, allow_internet_access: true, network: nil,
54
+ lifecycle: nil, auto_pause: nil, mcp: nil, request_timeout: nil, **_opts)
48
55
  # Support both seconds and milliseconds for backward compat
49
56
  timeout_seconds = if timeout
50
57
  timeout
@@ -53,22 +60,37 @@ module E2B
53
60
  else
54
61
  (@config.sandbox_timeout_ms / 1000).to_i
55
62
  end
63
+ template = resolved_template(template, mcp: mcp)
64
+ lifecycle = normalized_lifecycle(lifecycle: lifecycle, auto_pause: auto_pause)
56
65
 
57
66
  body = {
58
67
  templateID: template,
59
- timeout: timeout_seconds
68
+ timeout: timeout_seconds,
69
+ secure: secure,
70
+ allow_internet_access: allow_internet_access,
71
+ autoPause: lifecycle[:on_timeout] == "pause"
60
72
  }
61
73
  body[:metadata] = metadata if metadata
62
74
  body[:envVars] = envs if envs
75
+ body[:mcp] = mcp if mcp
76
+ body[:network] = network if network
77
+ if body[:autoPause]
78
+ body[:autoResume] = { enabled: lifecycle[:auto_resume] }
79
+ end
63
80
 
64
- response = @http_client.post("/sandboxes", body: body, timeout: 120)
81
+ response = @http_client.post("/sandboxes", body: body, timeout: request_timeout || @config.request_timeout || 120)
82
+ ensure_supported_envd_version!(response, @http_client)
65
83
 
66
- Sandbox.new(
84
+ sandbox = Sandbox.new(
67
85
  sandbox_data: response,
68
86
  http_client: @http_client,
69
87
  api_key: @config.api_key,
70
88
  domain: @domain
71
89
  )
90
+
91
+ start_mcp_gateway(sandbox, mcp) if mcp
92
+
93
+ sandbox
72
94
  end
73
95
 
74
96
  # Connect to an existing sandbox
@@ -77,12 +99,9 @@ module E2B
77
99
  # @param timeout [Integer, nil] Timeout in seconds
78
100
  # @return [Sandbox]
79
101
  def connect(sandbox_id, timeout: nil)
80
- if timeout
81
- response = @http_client.post("/sandboxes/#{sandbox_id}/connect",
82
- body: { timeout: timeout })
83
- else
84
- response = @http_client.get("/sandboxes/#{sandbox_id}")
85
- end
102
+ timeout_seconds = timeout || ((@config.sandbox_timeout_ms || (Sandbox::DEFAULT_TIMEOUT * 1000)) / 1000).to_i
103
+ response = @http_client.post("/sandboxes/#{sandbox_id}/connect",
104
+ body: { timeout: timeout_seconds })
86
105
 
87
106
  Sandbox.new(
88
107
  sandbox_data: response,
@@ -112,29 +131,18 @@ module E2B
112
131
  # @param metadata [Hash, nil] Filter by metadata
113
132
  # @param state [String, nil] Filter by state
114
133
  # @param limit [Integer] Maximum results
115
- # @return [Array<Sandbox>]
116
- def list(metadata: nil, state: nil, limit: 100)
117
- params = { limit: limit }
118
- params[:metadata] = metadata.to_json if metadata
119
- params[:state] = state if state
120
-
121
- response = @http_client.get("/v2/sandboxes", params: params)
122
-
123
- sandboxes = if response.is_a?(Array)
124
- response
125
- elsif response.is_a?(Hash)
126
- response["sandboxes"] || response[:sandboxes] || []
127
- else
128
- []
129
- end
130
- Array(sandboxes).map do |sandbox_data|
131
- Sandbox.new(
132
- sandbox_data: sandbox_data,
133
- http_client: @http_client,
134
- api_key: @config.api_key,
135
- domain: @domain
136
- )
137
- end
134
+ # @return [SandboxPaginator]
135
+ def list(metadata: nil, state: nil, limit: 100, next_token: nil)
136
+ query = {}
137
+ query[:metadata] = metadata if metadata
138
+ query[:state] = state if state
139
+
140
+ SandboxPaginator.new(
141
+ http_client: @http_client,
142
+ query: query.empty? ? nil : query,
143
+ limit: limit,
144
+ next_token: next_token
145
+ )
138
146
  end
139
147
 
140
148
  # Kill a sandbox
@@ -170,8 +178,8 @@ module E2B
170
178
  # @param timeout [Integer, nil] New timeout in seconds
171
179
  # @return [Sandbox]
172
180
  def resume(sandbox_id, timeout: nil)
173
- body = {}
174
- body[:timeout] = timeout if timeout
181
+ timeout_seconds = timeout || ((@config.sandbox_timeout_ms || (Sandbox::DEFAULT_TIMEOUT * 1000)) / 1000).to_i
182
+ body = { timeout: timeout_seconds }
175
183
 
176
184
  response = @http_client.post("/sandboxes/#{sandbox_id}/connect", body: body)
177
185
 
@@ -183,6 +191,41 @@ module E2B
183
191
  )
184
192
  end
185
193
 
194
+ # Create a snapshot from an existing sandbox.
195
+ #
196
+ # @param sandbox_id [String] Source sandbox ID
197
+ # @return [Models::SnapshotInfo]
198
+ def create_snapshot(sandbox_id)
199
+ response = @http_client.post("/sandboxes/#{sandbox_id}/snapshots")
200
+ Models::SnapshotInfo.from_hash(response)
201
+ end
202
+
203
+ # List snapshots for the team, optionally filtered by source sandbox.
204
+ #
205
+ # @param sandbox_id [String, nil] Filter snapshots by source sandbox ID
206
+ # @param limit [Integer] Maximum results per page
207
+ # @param next_token [String, nil] Pagination token
208
+ # @return [SnapshotPaginator]
209
+ def list_snapshots(sandbox_id: nil, limit: 100, next_token: nil)
210
+ SnapshotPaginator.new(
211
+ http_client: @http_client,
212
+ sandbox_id: sandbox_id,
213
+ limit: limit,
214
+ next_token: next_token
215
+ )
216
+ end
217
+
218
+ # Delete a snapshot template.
219
+ #
220
+ # @param snapshot_id [String] Snapshot identifier
221
+ # @return [Boolean]
222
+ def delete_snapshot(snapshot_id)
223
+ @http_client.delete("/templates/#{snapshot_id}")
224
+ true
225
+ rescue NotFoundError
226
+ false
227
+ end
228
+
186
229
  private
187
230
 
188
231
  def resolve_config(config_or_options)
@@ -16,12 +16,12 @@ module E2B
16
16
  # config.request_timeout = 120
17
17
  # end
18
18
  class Configuration
19
- # Default API base URL
20
- DEFAULT_API_URL = "https://api.e2b.app"
21
-
22
19
  # Default domain
23
20
  DEFAULT_DOMAIN = "e2b.app"
24
21
 
22
+ # Default API base URL
23
+ DEFAULT_API_URL = "https://api.#{DEFAULT_DOMAIN}"
24
+
25
25
  # Default request timeout in seconds
26
26
  DEFAULT_REQUEST_TIMEOUT = 60
27
27
 
@@ -32,7 +32,7 @@ module E2B
32
32
  DEFAULT_TIMEOUT_MS = 300_000
33
33
 
34
34
  # Default sandbox timeout in milliseconds (backward compat)
35
- DEFAULT_SANDBOX_TIMEOUT_MS = 3_600_000
35
+ DEFAULT_SANDBOX_TIMEOUT_MS = 300_000
36
36
 
37
37
  # Maximum sandbox timeout (24 hours for Pro)
38
38
  MAX_SANDBOX_TIMEOUT_MS = 86_400_000
@@ -90,11 +90,11 @@ module E2B
90
90
  @api_key = api_key || ENV["E2B_API_KEY"]
91
91
  @access_token = access_token || ENV["E2B_ACCESS_TOKEN"]
92
92
  @domain = domain || ENV["E2B_DOMAIN"] || DEFAULT_DOMAIN
93
- @api_url = api_url || ENV["E2B_API_URL"] || DEFAULT_API_URL
93
+ @debug = debug || ENV["E2B_DEBUG"]&.downcase == "true"
94
+ @api_url = api_url || ENV["E2B_API_URL"] || self.class.default_api_url(@domain, debug: @debug)
94
95
  @request_timeout = request_timeout
95
96
  @timeout_ms = timeout_ms
96
97
  @sandbox_timeout_ms = sandbox_timeout_ms
97
- @debug = debug || ENV["E2B_DEBUG"]&.downcase == "true"
98
98
  @default_template = nil
99
99
  @logger = nil
100
100
  end
@@ -115,5 +115,11 @@ module E2B
115
115
  def valid?
116
116
  (!@api_key.nil? && !@api_key.empty?) || (!@access_token.nil? && !@access_token.empty?)
117
117
  end
118
+
119
+ def self.default_api_url(domain, debug: false)
120
+ return "http://localhost:3000" if debug
121
+
122
+ "https://api.#{domain}"
123
+ end
118
124
  end
119
125
  end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "shellwords"
5
+
6
+ module E2B
7
+ module DockerfileParser
8
+ module_function
9
+
10
+ def parse(dockerfile_content_or_path, template_builder)
11
+ instructions = parse_instructions(read_dockerfile(dockerfile_content_or_path))
12
+ from_instructions = instructions.select { |instruction| instruction[:keyword] == "FROM" }
13
+
14
+ raise TemplateError, "Multi-stage Dockerfiles are not supported" if from_instructions.length > 1
15
+ raise TemplateError, "Dockerfile must contain a FROM instruction" if from_instructions.empty?
16
+
17
+ base_image = normalize_base_image(from_instructions.first[:value])
18
+ user_changed = false
19
+ workdir_changed = false
20
+
21
+ template_builder.set_user("root")
22
+ template_builder.set_workdir("/")
23
+
24
+ instructions.each do |instruction|
25
+ keyword = instruction[:keyword]
26
+ value = instruction[:value]
27
+
28
+ case keyword
29
+ when "FROM"
30
+ next
31
+ when "RUN"
32
+ handle_run(value, template_builder)
33
+ when "COPY", "ADD"
34
+ handle_copy(value, template_builder)
35
+ when "WORKDIR"
36
+ handle_workdir(value, template_builder)
37
+ workdir_changed = true
38
+ when "USER"
39
+ handle_user(value, template_builder)
40
+ user_changed = true
41
+ when "ENV", "ARG"
42
+ handle_env(value, keyword, template_builder)
43
+ when "CMD", "ENTRYPOINT"
44
+ handle_cmd(value, template_builder)
45
+ end
46
+ end
47
+
48
+ template_builder.set_user("user") unless user_changed
49
+ template_builder.set_workdir("/home/user") unless workdir_changed
50
+
51
+ base_image
52
+ end
53
+
54
+ def read_dockerfile(dockerfile_content_or_path)
55
+ if File.file?(dockerfile_content_or_path)
56
+ File.read(dockerfile_content_or_path)
57
+ else
58
+ dockerfile_content_or_path
59
+ end
60
+ rescue StandardError
61
+ dockerfile_content_or_path
62
+ end
63
+
64
+ def parse_instructions(content)
65
+ instructions = []
66
+ current = +""
67
+
68
+ content.each_line do |raw_line|
69
+ line = raw_line.chomp
70
+ next if line.strip.empty? || line.lstrip.start_with?("#")
71
+
72
+ if line.rstrip.end_with?("\\")
73
+ current << line.rstrip.sub(/\\\s*\z/, "")
74
+ current << " "
75
+ next
76
+ end
77
+
78
+ current << line
79
+ instruction = current.strip
80
+ current = +""
81
+ next if instruction.empty?
82
+
83
+ keyword, value = instruction.split(/\s+/, 2)
84
+ instructions << { keyword: keyword.upcase, value: value.to_s.strip }
85
+ end
86
+
87
+ unless current.strip.empty?
88
+ keyword, value = current.strip.split(/\s+/, 2)
89
+ instructions << { keyword: keyword.upcase, value: value.to_s.strip }
90
+ end
91
+
92
+ instructions
93
+ end
94
+
95
+ def normalize_base_image(value)
96
+ value.sub(/\s+as\s+.+\z/i, "").strip
97
+ end
98
+
99
+ def handle_run(value, template_builder)
100
+ return if value.strip.empty?
101
+
102
+ template_builder.run_cmd(value.strip.gsub(/\s+/, " "))
103
+ end
104
+
105
+ def handle_copy(value, template_builder)
106
+ return if value.strip.empty?
107
+
108
+ parts = Shellwords.split(value)
109
+ user = nil
110
+ non_flag_parts = []
111
+
112
+ parts.each do |part|
113
+ if part.start_with?("--chown=")
114
+ user = part.delete_prefix("--chown=")
115
+ elsif !part.start_with?("--")
116
+ non_flag_parts << part
117
+ end
118
+ end
119
+
120
+ return unless non_flag_parts.length >= 2
121
+
122
+ src = non_flag_parts.first
123
+ dest = non_flag_parts.last
124
+ template_builder.copy(src, dest, user: user)
125
+ end
126
+
127
+ def handle_workdir(value, template_builder)
128
+ return if value.strip.empty?
129
+
130
+ template_builder.set_workdir(value.strip)
131
+ end
132
+
133
+ def handle_user(value, template_builder)
134
+ return if value.strip.empty?
135
+
136
+ template_builder.set_user(value.strip)
137
+ end
138
+
139
+ def handle_env(value, keyword, template_builder)
140
+ return if value.strip.empty?
141
+
142
+ parts = Shellwords.split(value)
143
+ envs = {}
144
+
145
+ if parts.length == 1
146
+ parse_single_env_part(parts.first, keyword, envs)
147
+ elsif parts.length == 2 && !(parts[0].include?("=") && parts[1].include?("="))
148
+ envs[parts[0]] = parts[1]
149
+ else
150
+ parts.each { |part| parse_single_env_part(part, keyword, envs) }
151
+ end
152
+
153
+ template_builder.set_envs(envs) unless envs.empty?
154
+ end
155
+
156
+ def parse_single_env_part(part, keyword, envs)
157
+ equal_index = part.index("=")
158
+ if equal_index && equal_index.positive?
159
+ envs[part[0...equal_index]] = part[(equal_index + 1)..]
160
+ elsif keyword == "ARG" && !part.strip.empty?
161
+ envs[part.strip] = ""
162
+ end
163
+ end
164
+
165
+ def handle_cmd(value, template_builder)
166
+ return if value.strip.empty?
167
+
168
+ command = value.strip
169
+ begin
170
+ parsed = JSON.parse(command)
171
+ command = parsed.join(" ") if parsed.is_a?(Array)
172
+ rescue JSON::ParserError
173
+ nil
174
+ end
175
+
176
+ template_builder.set_start_cmd(command, E2B.wait_for_timeout(20_000))
177
+ end
178
+ end
179
+ end
data/lib/e2b/errors.rb CHANGED
@@ -49,7 +49,30 @@ module E2B
49
49
  class NotEnoughSpaceError < E2BError; end
50
50
 
51
51
  # Error raised for template-related failures
52
- class TemplateError < E2BError; end
52
+ class TemplateError < E2BError
53
+ attr_reader :source_location
54
+
55
+ def initialize(message = nil, status_code: nil, headers: {}, source_location: nil)
56
+ @source_location = source_location
57
+ super(message, status_code: status_code, headers: headers)
58
+ set_backtrace([source_location]) if source_location
59
+ end
60
+ end
61
+
62
+ # Error raised when a template build fails or returns an invalid status
63
+ class BuildError < E2BError
64
+ attr_reader :step, :source_location
65
+
66
+ def initialize(message = nil, status_code: nil, headers: {}, step: nil, source_location: nil)
67
+ @step = step
68
+ @source_location = source_location
69
+ super(message, status_code: status_code, headers: headers)
70
+ set_backtrace([source_location]) if source_location
71
+ end
72
+ end
73
+
74
+ # Error raised when file upload fails during a template build
75
+ class FileUploadError < BuildError; end
53
76
 
54
77
  # Error raised when a command exits with non-zero exit code
55
78
  #
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E2B
4
+ module Models
5
+ class BuildInfo
6
+ attr_reader :alias_name, :name, :tags, :template_id, :build_id, :build_step_origins
7
+
8
+ def self.from_hash(data)
9
+ new(
10
+ alias_name: data["alias"] || data[:alias],
11
+ name: data["name"] || data[:name],
12
+ tags: data["tags"] || data[:tags] || [],
13
+ template_id: data["templateID"] || data["template_id"] || data[:templateID],
14
+ build_id: data["buildID"] || data["build_id"] || data[:buildID],
15
+ build_step_origins: data["buildStepOrigins"] || data["build_step_origins"] || data[:buildStepOrigins] || []
16
+ )
17
+ end
18
+
19
+ def initialize(alias_name:, name:, tags:, template_id:, build_id:, build_step_origins: [])
20
+ @alias_name = alias_name
21
+ @name = name
22
+ @tags = tags || []
23
+ @template_id = template_id
24
+ @build_id = build_id
25
+ @build_step_origins = Array(build_step_origins).compact
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E2B
4
+ module Models
5
+ class BuildStatusReason
6
+ attr_reader :message, :step, :log_entries
7
+
8
+ def self.from_hash(data)
9
+ return nil if data.nil?
10
+
11
+ new(
12
+ message: data["message"] || data[:message],
13
+ step: data["step"] || data[:step],
14
+ log_entries: Array(data["logEntries"] || data["log_entries"] || data[:logEntries]).map do |entry|
15
+ TemplateLogEntry.from_hash(entry)
16
+ end
17
+ )
18
+ end
19
+
20
+ def initialize(message:, step: nil, log_entries: [])
21
+ @message = message
22
+ @step = step
23
+ @log_entries = log_entries || []
24
+ end
25
+ end
26
+ end
27
+ end