daytona 0.173.0 → 0.176.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: 83d12903b0910a3d69cd7042e642fe9cc748ee97ec605ae669e1537f433cbe32
4
- data.tar.gz: af534e94adbe126f60c62b8e27b1a7caa1e67230ab10e055e73c4092e13b2684
3
+ metadata.gz: 7535ad9431345bb3c463fe8bfbeb3e0f296fb5aefd6e8a2208376aa71893fb77
4
+ data.tar.gz: 74f70acd3c6cb193bc8d471e437345ea6ed68bf65d6b8735fc1d0826306c8a03
5
5
  SHA512:
6
- metadata.gz: b45b939dacd1b20b5864a90f0fdc02dcd2c4f34873b81148bd19de59c748d4b0f959b01136b3d1ad580b4f15fff7fef710c967f68cc66d5ce0dc9370fdbafbcd
7
- data.tar.gz: 69cc63a19e83148b9f36206cf6bc4b59de091105bba7ed4709d815743b5689b29ef9e9c85da45ed332b58039e19a89b7730070f5f3d2b44c3c8563e07926570a
6
+ metadata.gz: e80742f74b83913585be67091015a0b39c6bbd4acb1c7d0743e73bde841940fe9cdce49d4a466cead18bd54b550888ba873925b80cb034995ae4dc0f7bb986d3
7
+ data.tar.gz: b123fc8cfd3c45d122f750777194f493982256550bb93525c5cb981b795d0aaa8e2f5d707ce0e00bb4ca2c76fabbfcc464bb3370787c6243e184ea1c21fbf61e
data/.rubocop.yml CHANGED
@@ -14,3 +14,9 @@ Style/Documentation:
14
14
 
15
15
  Style/AccessorGrouping:
16
16
  EnforcedStyle: separated
17
+
18
+ # RSpec describe / context blocks naturally span the whole file; the
19
+ # stock 25-line cap forces noise instead of catching real complexity.
20
+ Metrics/BlockLength:
21
+ Exclude:
22
+ - 'spec/**/*'
@@ -409,6 +409,139 @@ module Daytona
409
409
  attr_reader :otel_state
410
410
  end
411
411
 
412
+ # Accessibility operations for computer use functionality.
413
+ class Accessibility
414
+ include Instrumentation
415
+
416
+ # @return [String] The ID of the sandbox
417
+ attr_reader :sandbox_id
418
+
419
+ # @return [DaytonaToolboxApiClient::ComputerUseApi] API client for sandbox operations
420
+ attr_reader :toolbox_api
421
+
422
+ # @param sandbox_id [String] The ID of the sandbox
423
+ # @param toolbox_api [DaytonaToolboxApiClient::ComputerUseApi] API client for sandbox operations
424
+ # @param otel_state [Daytona::OtelState, nil]
425
+ def initialize(sandbox_id:, toolbox_api:, otel_state: nil)
426
+ @sandbox_id = sandbox_id
427
+ @toolbox_api = toolbox_api
428
+ @otel_state = otel_state
429
+ end
430
+
431
+ # Fetches the AT-SPI accessibility tree.
432
+ #
433
+ # @param scope [String, nil] Tree scope to inspect: "focused", "pid", or "all"
434
+ # @param pid [Integer, nil] Process ID when scope is "pid"
435
+ # @param max_depth [Integer, nil] Maximum depth to descend; 0 returns only the root
436
+ # @return [DaytonaToolboxApiClient::AccessibilityTreeResponse] Accessibility tree response
437
+ # @raise [Daytona::Sdk::Error] If the operation fails
438
+ #
439
+ # @example
440
+ # tree = sandbox.computer_use.accessibility.get_tree(scope: "all", max_depth: 3)
441
+ # puts tree.root.name
442
+ def get_tree(scope: nil, pid: nil, max_depth: nil)
443
+ opts = {}
444
+ opts[:scope] = scope unless scope.nil?
445
+ opts[:pid] = pid unless pid.nil?
446
+ opts[:max_depth] = max_depth unless max_depth.nil?
447
+
448
+ toolbox_api.get_accessibility_tree(opts)
449
+ rescue StandardError => e
450
+ raise Sdk::Error, "Failed to get accessibility tree: #{e.message}"
451
+ end
452
+
453
+ # Finds AT-SPI accessibility nodes matching the provided filters.
454
+ #
455
+ # @param scope [String, nil] Search scope: "focused", "pid", or "all"
456
+ # @param pid [Integer, nil] Process ID when scope is "pid"
457
+ # @param role [String, nil] Accessibility role to match, such as "button"
458
+ # @param name [String, nil] Accessible name to match
459
+ # @param name_match [String, nil] Name match mode, such as "exact" or "substring"
460
+ # @param states [Array<String>, nil] Required accessibility states
461
+ # @param limit [Integer, nil] Maximum number of matches
462
+ # @return [DaytonaToolboxApiClient::AccessibilityNodesResponse] Matching accessibility nodes
463
+ # @raise [Daytona::Sdk::Error] If the operation fails
464
+ #
465
+ # @example
466
+ # buttons = sandbox.computer_use.accessibility.find_nodes(
467
+ # scope: "all",
468
+ # role: "button",
469
+ # name: "Submit",
470
+ # name_match: "substring"
471
+ # )
472
+ # puts buttons.matches.length
473
+ def find_nodes(scope: nil, pid: nil, role: nil, name: nil, name_match: nil, states: nil, limit: nil)
474
+ attrs = {}
475
+ attrs[:scope] = scope unless scope.nil?
476
+ attrs[:pid] = pid unless pid.nil?
477
+ attrs[:role] = role unless role.nil?
478
+ attrs[:name] = name unless name.nil?
479
+ attrs[:name_match] = name_match unless name_match.nil?
480
+ attrs[:states] = states unless states.nil?
481
+ attrs[:limit] = limit unless limit.nil?
482
+
483
+ request = DaytonaToolboxApiClient::FindAccessibilityNodesRequest.new(attrs)
484
+ toolbox_api.find_accessibility_nodes(request)
485
+ rescue StandardError => e
486
+ raise Sdk::Error, "Failed to find accessibility nodes: #{e.message}"
487
+ end
488
+
489
+ # Focuses an AT-SPI accessibility node.
490
+ #
491
+ # @param id [String] Accessibility node ID returned by get_tree or find_nodes
492
+ # @raise [Daytona::Sdk::Error] If the operation fails
493
+ #
494
+ # @example
495
+ # sandbox.computer_use.accessibility.focus_node(id: node.id)
496
+ def focus_node(id:)
497
+ request = DaytonaToolboxApiClient::AccessibilityNodeRequest.new(id:)
498
+ toolbox_api.focus_accessibility_node(request)
499
+ rescue StandardError => e
500
+ raise Sdk::Error, "Failed to focus accessibility node: #{e.message}"
501
+ end
502
+
503
+ # Invokes an AT-SPI accessibility node action.
504
+ #
505
+ # @param id [String] Accessibility node ID returned by get_tree or find_nodes
506
+ # @param action [String, nil] Action name to invoke, or nil for the primary action
507
+ # @raise [Daytona::Sdk::Error] If the operation fails
508
+ #
509
+ # @example
510
+ # sandbox.computer_use.accessibility.invoke_node(id: node.id, action: "click")
511
+ def invoke_node(id:, action: nil)
512
+ attrs = { id: }
513
+ attrs[:action] = action unless action.nil?
514
+
515
+ request = DaytonaToolboxApiClient::AccessibilityInvokeRequest.new(attrs)
516
+ toolbox_api.invoke_accessibility_node(request)
517
+ rescue StandardError => e
518
+ raise Sdk::Error, "Failed to invoke accessibility node: #{e.message}"
519
+ end
520
+
521
+ # Sets an AT-SPI accessibility node value.
522
+ #
523
+ # @param id [String] Accessibility node ID returned by get_tree or find_nodes
524
+ # @param value [String] Value to write to the node
525
+ # @raise [Daytona::Sdk::Error] If the operation fails
526
+ #
527
+ # @example
528
+ # sandbox.computer_use.accessibility.set_node_value(id: node.id, value: "hello")
529
+ def set_node_value(id:, value:)
530
+ request = DaytonaToolboxApiClient::AccessibilitySetValueRequest.new(id:, value:)
531
+ toolbox_api.set_accessibility_node_value(request)
532
+ rescue StandardError => e
533
+ raise Sdk::Error, "Failed to set accessibility node value: #{e.message}"
534
+ end
535
+
536
+ instrument :get_tree, :find_nodes, :focus_node, :invoke_node, :set_node_value,
537
+ component: 'Accessibility'
538
+
539
+ private
540
+
541
+ # @return [Daytona::OtelState, nil]
542
+ attr_reader :otel_state
543
+ end
544
+
412
545
  # Region coordinates for screenshot operations.
413
546
  class ScreenshotRegion
414
547
  # @return [Integer] X coordinate of the region
@@ -652,6 +785,9 @@ module Daytona
652
785
  # @return [Recording] Screen recording operations interface
653
786
  attr_reader :recording
654
787
 
788
+ # @return [Accessibility] Accessibility operations interface
789
+ attr_reader :accessibility
790
+
655
791
  # Initialize a new ComputerUse instance.
656
792
  #
657
793
  # @param sandbox_id [String] The ID of the sandbox
@@ -666,6 +802,7 @@ module Daytona
666
802
  @screenshot = Screenshot.new(sandbox_id:, toolbox_api:, otel_state:)
667
803
  @display = Display.new(sandbox_id:, toolbox_api:, otel_state:)
668
804
  @recording = Recording.new(sandbox_id:, toolbox_api:, otel_state:)
805
+ @accessibility = Accessibility.new(sandbox_id:, toolbox_api:, otel_state:)
669
806
  end
670
807
 
671
808
  # Starts all computer use processes (Xvfb, xfce4, x11vnc, novnc).
@@ -162,10 +162,17 @@ module Daytona
162
162
  # based on the sandbox working directory.
163
163
  # @param timeout [Integer] Timeout for the download operation in seconds. 0 means no timeout.
164
164
  # Default is 30 minutes.
165
+ # @param on_progress [Proc, nil] Optional callback invoked with a Daytona::DownloadProgress
166
+ # struct containing bytes_received (Integer) and total_bytes (Integer or nil).
167
+ # @param cancel_event [#set?, nil] Optional cancellation token (anything responding to +set?+;
168
+ # the standard library's +Concurrent::Event+ or a small ad-hoc object both work). When set
169
+ # during streaming, the next chunk raises Daytona::Sdk::Error and the underlying HTTP
170
+ # connection is torn down.
165
171
  # @yield [chunk] Yields each chunk of file content as it arrives
166
172
  # @yieldparam chunk [String] A binary string chunk of file content
167
173
  # @return [Enumerator, nil] An Enumerator yielding chunks if no block given, nil otherwise
168
- # @raise [Daytona::Sdk::Error] If the file does not exist or the operation fails
174
+ # @raise [Daytona::Sdk::Error] If the file does not exist, the operation fails, or
175
+ # +cancel_event+ is set during streaming
169
176
  #
170
177
  # @example Stream to a local file without loading into memory
171
178
  # File.open("local_copy.bin", "wb") do |f|
@@ -175,11 +182,12 @@ module Daytona
175
182
  # @example Collect chunks with an Enumerator
176
183
  # content = sandbox.fs.download_file_stream("workspace/data.json").reduce(:+)
177
184
  # puts content
178
- def download_file_stream(remote_path, timeout: 30 * 60, &)
179
- return enum_for(__method__, remote_path, timeout:) unless block_given?
185
+ def download_file_stream(remote_path, timeout: 30 * 60, on_progress: nil, cancel_event: nil, &)
186
+ return enum_for(__method__, remote_path, timeout:, on_progress:, cancel_event:) unless block_given?
180
187
 
181
188
  FileTransfer.stream_download(api_client: toolbox_api.api_client, remote_path: remote_path,
182
- timeout: timeout, &)
189
+ timeout: timeout, on_progress: on_progress,
190
+ cancel_event: cancel_event, &)
183
191
  nil
184
192
  rescue StandardError => e
185
193
  raise Sdk::Error, "Failed to download file: #{e.message}"
@@ -225,6 +233,36 @@ module Daytona
225
233
  raise Sdk::Error, "Failed to upload file: #{e.message}"
226
234
  end
227
235
 
236
+ # Streams +source+ to the Sandbox without buffering its contents in memory, with
237
+ # optional progress reporting.
238
+ #
239
+ # @param source [String, IO] A local file path or any IO-like object responding to
240
+ # +read(n)+. Strings that don't reference an existing file are uploaded as their
241
+ # raw bytes (still streamed, just from memory).
242
+ # @param remote_path [String] Destination path in the Sandbox.
243
+ # @param timeout [Integer] Timeout in seconds. 0 means no timeout. Default 30 minutes.
244
+ # @param on_progress [Proc, nil] Optional callback invoked with a
245
+ # +Daytona::UploadProgress+ struct as libcurl reports bytes actually uploaded.
246
+ # @param cancel_event [#set?, nil] Optional cancellation token. When set while
247
+ # staging a non-file source or during the libcurl upload, the operation raises
248
+ # Daytona::Sdk::Error and the in-progress upload is aborted (no destination file
249
+ # is left on the sandbox thanks to the daemon's atomic-rename behaviour).
250
+ # @return [void]
251
+ # @raise [Daytona::Sdk::Error] If the operation fails or +cancel_event+ is set.
252
+ #
253
+ # @example
254
+ # File.open("large.bin", "rb") do |f|
255
+ # sandbox.fs.upload_file_stream(f, "tmp/large.bin",
256
+ # on_progress: ->(p) { puts "#{p.bytes_sent} bytes sent" })
257
+ # end
258
+ def upload_file_stream(source, remote_path, timeout: 30 * 60, on_progress: nil, cancel_event: nil)
259
+ FileTransfer.stream_upload(api_client: toolbox_api.api_client, remote_path: remote_path,
260
+ source: source, timeout: timeout, on_progress: on_progress,
261
+ cancel_event: cancel_event)
262
+ rescue StandardError => e
263
+ raise Sdk::Error, "Failed to upload file: #{e.message}"
264
+ end
265
+
228
266
  # Uploads multiple files to the Sandbox. If files already exist at the destination paths,
229
267
  # they will be overwritten.
230
268
  #
@@ -4,11 +4,20 @@
4
4
  # frozen_string_literal: true
5
5
 
6
6
  require 'json'
7
+ require 'stringio'
8
+ require 'tempfile'
7
9
  require 'typhoeus'
8
10
 
9
11
  module Daytona
12
+ # Progress information for a streaming download.
13
+ DownloadProgress = Struct.new(:bytes_received, :total_bytes, keyword_init: true)
14
+
15
+ # Progress information for a streaming upload.
16
+ UploadProgress = Struct.new(:bytes_sent, keyword_init: true)
17
+
10
18
  class MultipartDownloadStreamParser
11
19
  attr_reader :error_message
20
+ attr_reader :part_total_bytes
12
21
  attr_writer :boundary_token
13
22
 
14
23
  def initialize(&on_file_chunk)
@@ -17,6 +26,7 @@ module Daytona
17
26
  @buffer = String.new.b
18
27
  @state = :preamble
19
28
  @part_name = nil
29
+ @part_total_bytes = nil
20
30
  @error_buffer = String.new.b
21
31
  end
22
32
 
@@ -62,14 +72,14 @@ module Daytona
62
72
  end
63
73
 
64
74
  def consume_headers?
65
- separator = "\r\n\r\n".b
66
- index = @buffer.index(separator)
75
+ index = @buffer.index("\r\n\r\n".b)
67
76
  return false unless index
68
77
 
69
78
  headers = @buffer.byteslice(0, index)
70
- @buffer = remaining_bytes(index + separator.bytesize)
71
- @part_name = headers[/Content-Disposition:\s*[^\r\n]*\bname="([^"]+)"/i, 1]
72
- raise Sdk::Error, 'Invalid multipart response' if @part_name.nil?
79
+ @buffer = remaining_bytes(index + 4)
80
+ @part_name = headers[/Content-Disposition:\s*[^\r\n]*\bname="([^"]+)"/i, 1] ||
81
+ raise(Sdk::Error, 'Invalid multipart response')
82
+ @part_total_bytes = headers[/Content-Length:\s*(\d+)/i, 1]&.to_i
73
83
 
74
84
  @state = :body
75
85
  true
@@ -124,16 +134,11 @@ module Daytona
124
134
  false
125
135
  end
126
136
 
127
- def remaining_bytes(offset)
128
- @buffer.byteslice(offset, @buffer.bytesize - offset) || String.new.b
129
- end
130
-
131
- def boundary
132
- "--#{@boundary_token}".b
133
- end
137
+ def remaining_bytes(offset) = @buffer.byteslice(offset, @buffer.bytesize - offset) || String.new.b
138
+ def boundary = "--#{@boundary_token}".b
134
139
  end
135
140
 
136
- module FileTransfer
141
+ module FileTransfer # rubocop:disable Metrics/ModuleLength
137
142
  def self.extract_multipart_boundary(content_type)
138
143
  match = content_type&.match(/boundary=(?:"([^"]+)"|([^;]+))/i)
139
144
  return unless match
@@ -141,9 +146,31 @@ module Daytona
141
146
  match.captures.compact.first
142
147
  end
143
148
 
144
- def self.stream_download(api_client:, remote_path:, timeout:, &) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
149
+ def self.assign_download_boundary(parser, content_type)
150
+ boundary = extract_multipart_boundary(content_type)
151
+ raise Sdk::Error, 'Missing multipart boundary in download response' unless boundary
152
+
153
+ parser.boundary_token = boundary
154
+ end
155
+
156
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
157
+ def self.stream_download(api_client:, remote_path:, timeout:, on_progress: nil, cancel_event: nil, &block)
145
158
  config = api_client.config
146
- parser = MultipartDownloadStreamParser.new(&)
159
+ bytes_received = 0
160
+ parser = nil
161
+ wrapped_block = proc do |chunk|
162
+ raise Sdk::Error, "Download cancelled: #{remote_path}" if cancel_event&.set?
163
+
164
+ if on_progress
165
+ bytes_received += chunk.bytesize
166
+ on_progress.call(DownloadProgress.new(
167
+ bytes_received: bytes_received,
168
+ total_bytes: parser&.part_total_bytes
169
+ ))
170
+ end
171
+ block.call(chunk)
172
+ end
173
+ parser = MultipartDownloadStreamParser.new(&wrapped_block)
147
174
  response = nil
148
175
 
149
176
  request = Typhoeus::Request.new(
@@ -160,13 +187,15 @@ module Daytona
160
187
  )
161
188
 
162
189
  request.on_headers do |stream_response|
163
- boundary = extract_multipart_boundary(stream_response.headers['Content-Type'])
164
- raise Sdk::Error, 'Missing multipart boundary in download response' unless boundary
165
-
166
- parser.boundary_token = boundary
190
+ assign_download_boundary(parser, stream_response.headers['Content-Type'])
167
191
  end
168
192
 
193
+ # Returning +:abort+ from the on_body callback tells libcurl to tear down the
194
+ # connection immediately, which is how cancellation actually severs the
195
+ # transfer rather than just stopping our own bookkeeping.
169
196
  request.on_body do |chunk|
197
+ next :abort if cancel_event&.set?
198
+
170
199
  parser << chunk
171
200
  end
172
201
 
@@ -177,8 +206,143 @@ module Daytona
177
206
 
178
207
  request.run
179
208
 
209
+ raise Sdk::Error, "Download cancelled: #{remote_path}" if cancel_event&.set?
180
210
  raise Sdk::Error, parser.error_message if parser.error_message
181
211
  raise Sdk::Error, "HTTP #{response.code}" if response && !response.success?
182
212
  end
213
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
214
+
215
+ # Uploads +source+ to /files/bulk-upload via Typhoeus (libcurl), which streams the
216
+ # request body straight from disk without buffering it in memory. Local file paths
217
+ # are uploaded directly; in-memory IOs/bytes are first drained to a tempfile so we
218
+ # have a stable file handle for libcurl.
219
+ #
220
+ # The daemon owns atomicity (writes to a sibling tempfile then renames), so a
221
+ # client-side abort just leaves no destination file at all.
222
+ #
223
+ # @param api_client The OpenAPI-generated toolbox API client (auth/base-url only).
224
+ # @param remote_path [String] Destination path in the sandbox.
225
+ # @param source [String, IO] Local file path or any IO-like object responding to +read(n)+.
226
+ # @param timeout [Integer] Typhoeus timeout in seconds (0 disables).
227
+ # @param on_progress [Proc, nil] Optional callback invoked with +Daytona::UploadProgress+
228
+ # as libcurl reports real network upload progress.
229
+ # @param cancel_event [#set?, nil] Optional cancellation token. Checked while staging
230
+ # non-file sources and during the libcurl transfer itself.
231
+ # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
232
+ def self.stream_upload(api_client:, remote_path:, source:, timeout:, on_progress: nil, cancel_event: nil)
233
+ with_upload_file(source, cancel_event, remote_path) do |upload_path|
234
+ config = api_client.config
235
+ progress_callback = upload_progress_callback(on_progress, cancel_event)
236
+ response = with_open_upload_file(upload_path) do |file|
237
+ upload_request(
238
+ api_client: api_client,
239
+ config: config,
240
+ remote_path: remote_path,
241
+ file: file,
242
+ timeout: timeout,
243
+ progress_callback: progress_callback
244
+ ).run
245
+ end
246
+ raise_upload_error(response, cancel_event, remote_path)
247
+ end
248
+ end
249
+ # rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
250
+
251
+ # Yields a path on disk that holds the source's bytes, ready for libcurl to stream.
252
+ # Local files are passed through unchanged; everything else is drained into a
253
+ # tempfile that gets unlinked when we return.
254
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
255
+ def self.with_upload_file(source, cancel_event, remote_path)
256
+ raise Sdk::Error, "Upload cancelled: #{remote_path}" if cancel_event&.set?
257
+
258
+ return yield(source) if source.is_a?(String) && File.exist?(source)
259
+
260
+ tmp = Tempfile.new(['daytona-upload-', File.extname(remote_path).to_s])
261
+ tmp.binmode
262
+ begin
263
+ drain_source_to(source, tmp, cancel_event, remote_path)
264
+ tmp.flush
265
+ tmp.close
266
+ yield(tmp.path)
267
+ ensure
268
+ tmp.close unless tmp.closed?
269
+ begin
270
+ tmp.unlink
271
+ rescue StandardError
272
+ # tempfile already gone, nothing to do
273
+ end
274
+ end
275
+ end
276
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
277
+
278
+ def self.drain_source_to(source, sink, cancel_event, remote_path)
279
+ io, owns_io = open_drain_source(source)
280
+ begin
281
+ while (chunk = io.read(64 * 1024))
282
+ break if chunk.empty?
283
+ raise Sdk::Error, "Upload cancelled: #{remote_path}" if cancel_event&.set?
284
+
285
+ sink.write(chunk)
286
+ end
287
+ ensure
288
+ io.close if owns_io && io.respond_to?(:close)
289
+ end
290
+ end
291
+
292
+ def self.with_open_upload_file(upload_path)
293
+ file = File.open(upload_path, 'rb')
294
+ yield(file)
295
+ ensure
296
+ file.close if file && !file.closed?
297
+ end
298
+
299
+ # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
300
+ def self.upload_request(api_client:, config:, remote_path:, file:, timeout:, progress_callback:)
301
+ Typhoeus::Request.new(
302
+ "#{config.base_url}/files/bulk-upload",
303
+ method: :post,
304
+ headers: api_client.default_headers.dup.tap { |h| h.delete('Content-Type') },
305
+ body: {
306
+ 'files[0].path' => remote_path,
307
+ 'files[0].file' => file
308
+ },
309
+ timeout: timeout,
310
+ ssl_verifypeer: config.verify_ssl,
311
+ ssl_verifyhost: config.verify_ssl_host ? 2 : 0,
312
+ noprogress: false,
313
+ progressfunction: progress_callback,
314
+ xferinfofunction: progress_callback
315
+ )
316
+ end
317
+ # rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
318
+
319
+ def self.upload_progress_callback(on_progress, cancel_event)
320
+ last_bytes_sent = -1
321
+
322
+ proc do |_clientp, _dltotal, _dlnow, _ultotal, ulnow|
323
+ next 1 if cancel_event&.set?
324
+
325
+ bytes_sent = ulnow.to_i
326
+ if on_progress && bytes_sent > last_bytes_sent
327
+ last_bytes_sent = bytes_sent
328
+ on_progress.call(UploadProgress.new(bytes_sent: bytes_sent))
329
+ end
330
+
331
+ 0
332
+ end
333
+ end
334
+
335
+ def self.raise_upload_error(response, _cancel_event, remote_path)
336
+ raise Sdk::Error, "Upload timed out: #{remote_path}" if response.timed_out?
337
+ raise Sdk::Error, "Upload cancelled: #{remote_path}" if response.return_code == :aborted_by_callback
338
+ raise Sdk::Error, "HTTP #{response.code}: #{response.body}" unless response.success?
339
+ end
340
+
341
+ def self.open_drain_source(source)
342
+ return [source, false] if source.respond_to?(:read)
343
+ return [StringIO.new(source.b), true] if source.is_a?(String)
344
+
345
+ raise Sdk::Error, "Unsupported upload source: #{source.class}"
346
+ end
183
347
  end
184
348
  end
data/lib/daytona/git.rb CHANGED
@@ -45,6 +45,8 @@ module Daytona
45
45
  # ])
46
46
  def add(path, files)
47
47
  toolbox_api.add_files(DaytonaToolboxApiClient::GitAddRequest.new(path:, files:))
48
+ rescue DaytonaToolboxApiClient::ApiError => e
49
+ raise map_api_error(e, 'Failed to add files')
48
50
  rescue StandardError => e
49
51
  raise Sdk::Error, "Failed to add files: #{e.message}"
50
52
  end
@@ -61,6 +63,8 @@ module Daytona
61
63
  # puts "Branches: #{response.branches}"
62
64
  def branches(path)
63
65
  toolbox_api.list_branches(path)
66
+ rescue DaytonaToolboxApiClient::ApiError => e
67
+ raise map_api_error(e, 'Failed to list branches')
64
68
  rescue StandardError => e
65
69
  raise Sdk::Error, "Failed to list branches: #{e.message}"
66
70
  end
@@ -114,6 +118,8 @@ module Daytona
114
118
  commit_id: commit_id
115
119
  )
116
120
  )
121
+ rescue DaytonaToolboxApiClient::ApiError => e
122
+ raise map_api_error(e, 'Failed to clone repository')
117
123
  rescue StandardError => e
118
124
  raise Sdk::Error, "Failed to clone repository: #{e.message}"
119
125
  end
@@ -146,6 +152,8 @@ module Daytona
146
152
  DaytonaToolboxApiClient::GitCommitRequest.new(path:, message:, author:, email:, allow_empty:)
147
153
  )
148
154
  GitCommitResponse.new(sha: response._hash)
155
+ rescue DaytonaToolboxApiClient::ApiError => e
156
+ raise map_api_error(e, 'Failed to commit changes')
149
157
  rescue StandardError => e
150
158
  raise Sdk::Error, "Failed to commit changes: #{e.message}"
151
159
  end
@@ -175,6 +183,8 @@ module Daytona
175
183
  toolbox_api.push_changes(
176
184
  DaytonaToolboxApiClient::GitRepoRequest.new(path:, username:, password:)
177
185
  )
186
+ rescue DaytonaToolboxApiClient::ApiError => e
187
+ raise map_api_error(e, 'Failed to push changes')
178
188
  rescue StandardError => e
179
189
  raise Sdk::Error, "Failed to push changes: #{e.message}"
180
190
  end
@@ -204,6 +214,8 @@ module Daytona
204
214
  toolbox_api.pull_changes(
205
215
  DaytonaToolboxApiClient::GitRepoRequest.new(path:, username:, password:)
206
216
  )
217
+ rescue DaytonaToolboxApiClient::ApiError => e
218
+ raise map_api_error(e, 'Failed to pull changes')
207
219
  rescue StandardError => e
208
220
  raise Sdk::Error, "Failed to pull changes: #{e.message}"
209
221
  end
@@ -222,6 +234,8 @@ module Daytona
222
234
  # puts "Commits behind: #{status.behind}"
223
235
  def status(path)
224
236
  toolbox_api.get_status(path)
237
+ rescue DaytonaToolboxApiClient::ApiError => e
238
+ raise map_api_error(e, 'Failed to get status')
225
239
  rescue StandardError => e
226
240
  raise Sdk::Error, "Failed to get status: #{e.message}"
227
241
  end
@@ -241,6 +255,8 @@ module Daytona
241
255
  toolbox_api.checkout_branch(
242
256
  DaytonaToolboxApiClient::GitCheckoutRequest.new(path:, branch:)
243
257
  )
258
+ rescue DaytonaToolboxApiClient::ApiError => e
259
+ raise map_api_error(e, 'Failed to checkout branch')
244
260
  rescue StandardError => e
245
261
  raise Sdk::Error, "Failed to checkout branch: #{e.message}"
246
262
  end
@@ -261,6 +277,8 @@ module Daytona
261
277
  toolbox_api.create_branch(
262
278
  DaytonaToolboxApiClient::GitBranchRequest.new(path:, name:)
263
279
  )
280
+ rescue DaytonaToolboxApiClient::ApiError => e
281
+ raise map_api_error(e, 'Failed to create branch')
264
282
  rescue StandardError => e
265
283
  raise Sdk::Error, "Failed to create branch: #{e.message}"
266
284
  end
@@ -280,6 +298,8 @@ module Daytona
280
298
  toolbox_api.delete_branch(
281
299
  DaytonaToolboxApiClient::GitDeleteBranchRequest.new(path:, name:)
282
300
  )
301
+ rescue DaytonaToolboxApiClient::ApiError => e
302
+ raise map_api_error(e, 'Failed to delete branch')
283
303
  rescue StandardError => e
284
304
  raise Sdk::Error, "Failed to delete branch: #{e.message}"
285
305
  end
@@ -292,5 +312,19 @@ module Daytona
292
312
 
293
313
  # @return [Daytona::OtelState, nil]
294
314
  attr_reader :otel_state
315
+
316
+ def map_api_error(api_error, prefix)
317
+ msg = "#{prefix}: #{api_error.message}"
318
+ case api_error.code
319
+ when 400 then Sdk::ValidationError.new(msg)
320
+ when 401 then Sdk::AuthenticationError.new(msg)
321
+ when 403 then Sdk::ForbiddenError.new(msg)
322
+ when 404 then Sdk::NotFoundError.new(msg)
323
+ when 409 then Sdk::ConflictError.new(msg)
324
+ when 429 then Sdk::RateLimitError.new(msg)
325
+ when 500..599 then Sdk::ServerError.new(msg)
326
+ else Sdk::Error.new(msg)
327
+ end
328
+ end
295
329
  end
296
330
  end
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Daytona
7
7
  module Sdk
8
- VERSION = '0.173.0'
8
+ VERSION = '0.176.0'
9
9
  end
10
10
  end
data/lib/daytona/sdk.rb CHANGED
@@ -43,6 +43,13 @@ module Daytona
43
43
  module Sdk
44
44
  class Error < StandardError; end
45
45
  class TimeoutError < Error; end
46
+ class AuthenticationError < Error; end
47
+ class ForbiddenError < Error; end
48
+ class NotFoundError < Error; end
49
+ class ConflictError < Error; end
50
+ class ValidationError < Error; end
51
+ class RateLimitError < Error; end
52
+ class ServerError < Error; end
46
53
 
47
54
  def self.logger = @logger ||= Logger.new($stdout, level: Logger::INFO)
48
55
  end
@@ -23,6 +23,7 @@ CLASSES_TO_DOCUMENT = [
23
23
  ['volume.rb', 'volume.mdx', 'Daytona::Volume'],
24
24
  ['object_storage.rb', 'object-storage.mdx', 'Daytona::ObjectStorage'],
25
25
  ['computer_use.rb', 'computer-use.mdx', 'Daytona::ComputerUse'],
26
+ ['computer_use.rb', 'computer-use.mdx', 'Daytona::ComputerUse::Accessibility'],
26
27
  ['snapshot_service.rb', 'snapshot.mdx', 'Daytona::SnapshotService'],
27
28
  ['volume_service.rb', 'volume-service.mdx', 'Daytona::VolumeService'],
28
29
  ['common/charts.rb', 'charts.mdx', 'Daytona::Chart'],
@@ -371,7 +372,11 @@ def generate_docs_for_class(file_path, output_filename, class_name)
371
372
 
372
373
  # Write to output file
373
374
  output_path = File.join(DOCS_OUTPUT_DIR, output_filename)
374
- File.write(output_path, final_content)
375
+ if File.exist?(output_path)
376
+ File.write(output_path, "#{File.read(output_path).rstrip}\n\n#{markdown_content}")
377
+ else
378
+ File.write(output_path, final_content)
379
+ end
375
380
 
376
381
  puts "✅ Generated: #{output_filename}"
377
382
  rescue StandardError => e
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: daytona
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.173.0
4
+ version: 0.176.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daytona Platforms Inc.
@@ -85,28 +85,28 @@ dependencies:
85
85
  requirements:
86
86
  - - '='
87
87
  - !ruby/object:Gem::Version
88
- version: 0.173.0
88
+ version: 0.176.0
89
89
  type: :runtime
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - '='
94
94
  - !ruby/object:Gem::Version
95
- version: 0.173.0
95
+ version: 0.176.0
96
96
  - !ruby/object:Gem::Dependency
97
97
  name: daytona_toolbox_api_client
98
98
  requirement: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - '='
101
101
  - !ruby/object:Gem::Version
102
- version: 0.173.0
102
+ version: 0.176.0
103
103
  type: :runtime
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - '='
108
108
  - !ruby/object:Gem::Version
109
- version: 0.173.0
109
+ version: 0.176.0
110
110
  - !ruby/object:Gem::Dependency
111
111
  name: dotenv
112
112
  requirement: !ruby/object:Gem::Requirement