daytona-sdk 0.125.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 +7 -0
- data/.rubocop.yml +16 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/README.md +39 -0
- data/Rakefile +12 -0
- data/lib/daytona/code_toolbox/sandbox_python_code_toolbox.rb +439 -0
- data/lib/daytona/code_toolbox/sandbox_ts_code_toolbox.rb +23 -0
- data/lib/daytona/common/charts.rb +298 -0
- data/lib/daytona/common/code_language.rb +11 -0
- data/lib/daytona/common/daytona.rb +206 -0
- data/lib/daytona/common/file_system.rb +23 -0
- data/lib/daytona/common/git.rb +16 -0
- data/lib/daytona/common/image.rb +493 -0
- data/lib/daytona/common/process.rb +141 -0
- data/lib/daytona/common/pty.rb +306 -0
- data/lib/daytona/common/resources.rb +31 -0
- data/lib/daytona/common/response.rb +28 -0
- data/lib/daytona/common/snapshot.rb +110 -0
- data/lib/daytona/computer_use.rb +549 -0
- data/lib/daytona/config.rb +53 -0
- data/lib/daytona/daytona.rb +278 -0
- data/lib/daytona/file_system.rb +359 -0
- data/lib/daytona/git.rb +287 -0
- data/lib/daytona/lsp_server.rb +130 -0
- data/lib/daytona/object_storage.rb +169 -0
- data/lib/daytona/process.rb +484 -0
- data/lib/daytona/sandbox.rb +376 -0
- data/lib/daytona/sdk/version.rb +7 -0
- data/lib/daytona/sdk.rb +45 -0
- data/lib/daytona/snapshot_service.rb +198 -0
- data/lib/daytona/util.rb +56 -0
- data/lib/daytona/volume.rb +43 -0
- data/lib/daytona/volume_service.rb +49 -0
- data/sig/daytona/sdk.rbs +6 -0
- metadata +149 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'pathname'
|
|
6
|
+
require 'shellwords'
|
|
7
|
+
|
|
8
|
+
module Daytona
|
|
9
|
+
class Context
|
|
10
|
+
attr_reader :source_path
|
|
11
|
+
attr_reader :archive_path
|
|
12
|
+
|
|
13
|
+
# @param source_path [String] The path to the source file or directory
|
|
14
|
+
# @param archive_path [String, nil] The path inside the archive file in object storage
|
|
15
|
+
def initialize(source_path:, archive_path: nil)
|
|
16
|
+
@source_path = source_path
|
|
17
|
+
@archive_path = archive_path
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Represents an image definition for a Daytona sandbox.
|
|
22
|
+
# Do not construct this class directly. Instead use one of its static factory methods,
|
|
23
|
+
# such as `Image.base()`, `Image.debian_slim()`, or `Image.from_dockerfile()`.
|
|
24
|
+
class Image # rubocop:disable Metrics/ClassLength
|
|
25
|
+
# @return [String, nil] The generated Dockerfile for the image
|
|
26
|
+
attr_reader :dockerfile
|
|
27
|
+
|
|
28
|
+
# @return [Array<Context>] List of context files for the image
|
|
29
|
+
attr_reader :context_list
|
|
30
|
+
|
|
31
|
+
# Supported Python series
|
|
32
|
+
SUPPORTED_PYTHON_SERIES = %w[3.9 3.10 3.11 3.12 3.13].freeze
|
|
33
|
+
LATEST_PYTHON_MICRO_VERSIONS = %w[3.9.22 3.10.17 3.11.12 3.12.10 3.13.3].freeze
|
|
34
|
+
|
|
35
|
+
# @param dockerfile [String, nil] The Dockerfile content
|
|
36
|
+
# @param context_list [Array<Context>] List of context files
|
|
37
|
+
def initialize(dockerfile: nil, context_list: [])
|
|
38
|
+
@dockerfile = dockerfile || ''
|
|
39
|
+
@context_list = context_list
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Adds commands to install packages using pip
|
|
43
|
+
#
|
|
44
|
+
# @param packages [Array<String>] The packages to install
|
|
45
|
+
# @param find_links [Array<String>, nil] The find-links to use
|
|
46
|
+
# @param index_url [String, nil] The index URL to use
|
|
47
|
+
# @param extra_index_urls [Array<String>, nil] The extra index URLs to use
|
|
48
|
+
# @param pre [Boolean] Whether to install pre-release packages
|
|
49
|
+
# @param extra_options [String] Additional options to pass to pip
|
|
50
|
+
# @return [Image] The image with the pip install commands added
|
|
51
|
+
#
|
|
52
|
+
# @example
|
|
53
|
+
# image = Image.debian_slim("3.12").pip_install("requests", "pandas")
|
|
54
|
+
def pip_install(*packages, find_links: nil, index_url: nil, extra_index_urls: nil, pre: false, extra_options: '') # rubocop:disable Metrics/ParameterLists
|
|
55
|
+
pkgs = flatten_str_args('pip_install', 'packages', packages)
|
|
56
|
+
return self if pkgs.empty?
|
|
57
|
+
|
|
58
|
+
extra_args = format_pip_install_args(find_links:, index_url:, extra_index_urls:, pre:, extra_options:)
|
|
59
|
+
@dockerfile += "RUN python -m pip install #{Shellwords.join(pkgs.sort)}#{extra_args}\n"
|
|
60
|
+
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Installs dependencies from a requirements.txt file
|
|
65
|
+
#
|
|
66
|
+
# @param requirements_txt [String] The path to the requirements.txt file
|
|
67
|
+
# @param find_links [Array<String>, nil] The find-links to use
|
|
68
|
+
# @param index_url [String, nil] The index URL to use
|
|
69
|
+
# @param extra_index_urls [Array<String>, nil] The extra index URLs to use
|
|
70
|
+
# @param pre [Boolean] Whether to install pre-release packages
|
|
71
|
+
# @param extra_options [String] Additional options to pass to pip
|
|
72
|
+
# @return [Image] The image with the pip install commands added
|
|
73
|
+
# @raise [Sdk::Error] If the requirements file does not exist
|
|
74
|
+
#
|
|
75
|
+
# @example
|
|
76
|
+
# image = Image.debian_slim("3.12").pip_install_from_requirements("requirements.txt")
|
|
77
|
+
def pip_install_from_requirements(requirements_txt, find_links: nil, index_url: nil, extra_index_urls: nil, # rubocop:disable Metrics/ParameterLists
|
|
78
|
+
pre: false, extra_options: '')
|
|
79
|
+
requirements_txt = File.expand_path(requirements_txt)
|
|
80
|
+
raise Sdk::Error, "Requirements file #{requirements_txt} does not exist" unless File.exist?(requirements_txt)
|
|
81
|
+
|
|
82
|
+
extra_args = format_pip_install_args(find_links:, index_url:, extra_index_urls:, pre:, extra_options:)
|
|
83
|
+
|
|
84
|
+
archive_path = ObjectStorage.compute_archive_base_path(requirements_txt)
|
|
85
|
+
@context_list << Context.new(source_path: requirements_txt, archive_path:)
|
|
86
|
+
@dockerfile += "COPY #{archive_path} /.requirements.txt\n"
|
|
87
|
+
@dockerfile += "RUN python -m pip install -r /.requirements.txt#{extra_args}\n"
|
|
88
|
+
|
|
89
|
+
self
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Installs dependencies from a pyproject.toml file
|
|
93
|
+
#
|
|
94
|
+
# @param pyproject_toml [String] The path to the pyproject.toml file
|
|
95
|
+
# @param optional_dependencies [Array<String>] The optional dependencies to install
|
|
96
|
+
# @param find_links [String, nil] The find-links to use
|
|
97
|
+
# @param index_url [String, nil] The index URL to use
|
|
98
|
+
# @param extra_index_url [String, nil] The extra index URL to use
|
|
99
|
+
# @param pre [Boolean] Whether to install pre-release packages
|
|
100
|
+
# @param extra_options [String] Additional options to pass to pip
|
|
101
|
+
# @return [Image] The image with the pip install commands added
|
|
102
|
+
# @raise [Sdk::Error] If pyproject.toml parsing is not supported
|
|
103
|
+
#
|
|
104
|
+
# @example
|
|
105
|
+
# image = Image.debian_slim("3.12").pip_install_from_pyproject("pyproject.toml", optional_dependencies: ["dev"])
|
|
106
|
+
def pip_install_from_pyproject(pyproject_toml, optional_dependencies: [], find_links: nil, index_url: nil, # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
|
|
107
|
+
extra_index_url: nil, pre: false, extra_options: '')
|
|
108
|
+
data = TOML.load_file(pyproject_toml)
|
|
109
|
+
dependencies = data.dig('project', 'dependencies')
|
|
110
|
+
|
|
111
|
+
unless dependencies
|
|
112
|
+
raise Sdk::Error, 'No [project.dependencies] section in pyproject.toml file. ' \
|
|
113
|
+
'See https://packaging.python.org/en/latest/guides/writing-pyproject-toml ' \
|
|
114
|
+
'for further file format guidelines.'
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
return unless optional_dependencies
|
|
118
|
+
|
|
119
|
+
optionals = data.dig('project', 'optional-dependencies')
|
|
120
|
+
optional_dependencies.each do |group|
|
|
121
|
+
dependencies.concat(optionals.fetch(group, []))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
pip_install(*dependencies, find_links:, index_url:, extra_index_urls: extra_index_url, pre:, extra_options:)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Adds a local file to the image
|
|
128
|
+
#
|
|
129
|
+
# @param local_path [String] The path to the local file
|
|
130
|
+
# @param remote_path [String] The path to the file in the image
|
|
131
|
+
# @return [Image] The image with the local file added
|
|
132
|
+
#
|
|
133
|
+
# @example
|
|
134
|
+
# image = Image.debian_slim("3.12").add_local_file("package.json", "/home/daytona/package.json")
|
|
135
|
+
def add_local_file(local_path, remote_path)
|
|
136
|
+
remote_path = "#{remote_path}/#{File.basename(local_path)}" if remote_path.end_with?('/')
|
|
137
|
+
|
|
138
|
+
local_path = File.expand_path(local_path)
|
|
139
|
+
archive_path = ObjectStorage.compute_archive_base_path(local_path)
|
|
140
|
+
@context_list << Context.new(source_path: local_path, archive_path: archive_path)
|
|
141
|
+
@dockerfile += "COPY #{archive_path} #{remote_path}\n"
|
|
142
|
+
|
|
143
|
+
self
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Adds a local directory to the image
|
|
147
|
+
#
|
|
148
|
+
# @param local_path [String] The path to the local directory
|
|
149
|
+
# @param remote_path [String] The path to the directory in the image
|
|
150
|
+
# @return [Image] The image with the local directory added
|
|
151
|
+
#
|
|
152
|
+
# @example
|
|
153
|
+
# image = Image.debian_slim("3.12").add_local_dir("src", "/home/daytona/src")
|
|
154
|
+
def add_local_dir(local_path, remote_path)
|
|
155
|
+
local_path = File.expand_path(local_path)
|
|
156
|
+
archive_path = ObjectStorage.compute_archive_base_path(local_path)
|
|
157
|
+
@context_list << Context.new(source_path: local_path, archive_path: archive_path)
|
|
158
|
+
@dockerfile += "COPY #{archive_path} #{remote_path}\n"
|
|
159
|
+
|
|
160
|
+
self
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Runs commands in the image
|
|
164
|
+
#
|
|
165
|
+
# @param commands [Array<String>] The commands to run
|
|
166
|
+
# @return [Image] The image with the commands added
|
|
167
|
+
#
|
|
168
|
+
# @example
|
|
169
|
+
# image = Image.debian_slim("3.12").run_commands('echo "Hello, world!"', 'echo "Hello again!"')
|
|
170
|
+
def run_commands(*commands)
|
|
171
|
+
commands.each do |command|
|
|
172
|
+
if command.is_a?(Array)
|
|
173
|
+
escaped = command.map { |c| c.gsub('"', '\\"').gsub("'", "\\'") }
|
|
174
|
+
@dockerfile += "RUN #{escaped.map { |c| "\"#{c}\"" }.join(' ')}\n"
|
|
175
|
+
else
|
|
176
|
+
@dockerfile += "RUN #{command}\n"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
self
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Sets environment variables in the image
|
|
184
|
+
#
|
|
185
|
+
# @param env_vars [Hash<String, String>] The environment variables to set
|
|
186
|
+
# @return [Image] The image with the environment variables added
|
|
187
|
+
#
|
|
188
|
+
# @example
|
|
189
|
+
# image = Image.debian_slim("3.12").env({"PROJECT_ROOT" => "/home/daytona"})
|
|
190
|
+
def env(env_vars)
|
|
191
|
+
non_str_keys = env_vars.reject { |_key, val| val.is_a?(String) }.keys
|
|
192
|
+
raise Sdk::Error, "Image ENV variables must be strings. Invalid keys: #{non_str_keys}" unless non_str_keys.empty?
|
|
193
|
+
|
|
194
|
+
env_vars.each do |key, val|
|
|
195
|
+
@dockerfile += "ENV #{key}=#{Shellwords.escape(val)}\n"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
self
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Sets the working directory in the image
|
|
202
|
+
#
|
|
203
|
+
# @param path [String] The path to the working directory
|
|
204
|
+
# @return [Image] The image with the working directory added
|
|
205
|
+
#
|
|
206
|
+
# @example
|
|
207
|
+
# image = Image.debian_slim("3.12").workdir("/home/daytona")
|
|
208
|
+
def workdir(path)
|
|
209
|
+
@dockerfile += "WORKDIR #{Shellwords.escape(path.to_s)}\n"
|
|
210
|
+
self
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Sets the entrypoint for the image
|
|
214
|
+
#
|
|
215
|
+
# @param entrypoint_commands [Array<String>] The commands to set as the entrypoint
|
|
216
|
+
# @return [Image] The image with the entrypoint added
|
|
217
|
+
#
|
|
218
|
+
# @example
|
|
219
|
+
# image = Image.debian_slim("3.12").entrypoint(["/bin/bash"])
|
|
220
|
+
def entrypoint(entrypoint_commands)
|
|
221
|
+
unless entrypoint_commands.is_a?(Array) && entrypoint_commands.all? { |x| x.is_a?(String) }
|
|
222
|
+
raise Sdk::Error, 'entrypoint_commands must be a list of strings.'
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
args_str = flatten_str_args('entrypoint', 'entrypoint_commands', entrypoint_commands)
|
|
226
|
+
args_str = args_str.map { |arg| "\"#{arg}\"" }.join(', ') if args_str.any?
|
|
227
|
+
@dockerfile += "ENTRYPOINT [#{args_str}]\n"
|
|
228
|
+
|
|
229
|
+
self
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Sets the default command for the image
|
|
233
|
+
#
|
|
234
|
+
# @param cmd [Array<String>] The commands to set as the default command
|
|
235
|
+
# @return [Image] The image with the default command added
|
|
236
|
+
#
|
|
237
|
+
# @example
|
|
238
|
+
# image = Image.debian_slim("3.12").cmd(["/bin/bash"])
|
|
239
|
+
def cmd(cmd)
|
|
240
|
+
unless cmd.is_a?(Array) && cmd.all? { |x| x.is_a?(String) }
|
|
241
|
+
raise Sdk::Error, 'Image CMD must be a list of strings.'
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
cmd_str = flatten_str_args('cmd', 'cmd', cmd)
|
|
245
|
+
cmd_str = cmd_str.map { |arg| "\"#{arg}\"" }.join(', ') if cmd_str.any?
|
|
246
|
+
@dockerfile += "CMD [#{cmd_str}]\n"
|
|
247
|
+
self
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Adds arbitrary Dockerfile-like commands to the image
|
|
251
|
+
#
|
|
252
|
+
# @param dockerfile_commands [Array<String>] The commands to add to the Dockerfile
|
|
253
|
+
# @param context_dir [String, nil] The path to the context directory
|
|
254
|
+
# @return [Image] The image with the Dockerfile commands added
|
|
255
|
+
#
|
|
256
|
+
# @example
|
|
257
|
+
# image = Image.debian_slim("3.12").dockerfile_commands(["RUN echo 'Hello, world!'"])
|
|
258
|
+
def dockerfile_commands(dockerfile_commands, context_dir: nil) # rubocop:disable Metrics/MethodLength
|
|
259
|
+
if context_dir
|
|
260
|
+
context_dir = File.expand_path(context_dir)
|
|
261
|
+
raise Sdk::Error, "Context directory #{context_dir} does not exist" unless Dir.exist?(context_dir)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Extract copy sources from dockerfile commands
|
|
265
|
+
extract_copy_sources(dockerfile_commands.join("\n"), context_dir || '').each do |context_path, original_path|
|
|
266
|
+
archive_base_path = context_path
|
|
267
|
+
if context_dir && !original_path.start_with?(context_dir)
|
|
268
|
+
archive_base_path = context_path.delete_prefix(context_dir)
|
|
269
|
+
end
|
|
270
|
+
@context_list << Context.new(source_path: context_path, archive_path: archive_base_path)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
@dockerfile += "#{dockerfile_commands.join("\n")}\n"
|
|
274
|
+
self
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
class << self
|
|
278
|
+
# Creates an Image from an existing Dockerfile
|
|
279
|
+
#
|
|
280
|
+
# @param path [String] The path to the Dockerfile
|
|
281
|
+
# @return [Image] The image with the Dockerfile added
|
|
282
|
+
#
|
|
283
|
+
# @example
|
|
284
|
+
# image = Image.from_dockerfile("Dockerfile")
|
|
285
|
+
def from_dockerfile(path) # rubocop:disable Metrics/AbcSize
|
|
286
|
+
path = Pathname.new(File.expand_path(path))
|
|
287
|
+
dockerfile = path.read
|
|
288
|
+
img = new(dockerfile: dockerfile)
|
|
289
|
+
|
|
290
|
+
# Remove dockerfile filename from path
|
|
291
|
+
path_prefix = path.to_s.delete_suffix(path.basename.to_s)
|
|
292
|
+
|
|
293
|
+
extract_copy_sources(dockerfile, path_prefix).each do |context_path, original_path|
|
|
294
|
+
archive_base_path = context_path
|
|
295
|
+
archive_base_path = context_path.delete_prefix(path_prefix) unless original_path.start_with?(path_prefix)
|
|
296
|
+
img.context_list << Context.new(source_path: context_path, archive_path: archive_base_path)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
img
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Creates an Image from an existing base image
|
|
303
|
+
#
|
|
304
|
+
# @param image [String] The base image to use
|
|
305
|
+
# @return [Image] The image with the base image added
|
|
306
|
+
#
|
|
307
|
+
# @example
|
|
308
|
+
# image = Image.base("python:3.12-slim-bookworm")
|
|
309
|
+
def base(image)
|
|
310
|
+
img = new
|
|
311
|
+
img.instance_variable_set(:@dockerfile, "FROM #{image}\n")
|
|
312
|
+
img
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Creates a Debian slim image based on the official Python Docker image
|
|
316
|
+
#
|
|
317
|
+
# @param python_version [String, nil] The Python version to use
|
|
318
|
+
# @return [Image] The image with the Debian slim image added
|
|
319
|
+
#
|
|
320
|
+
# @example
|
|
321
|
+
# image = Image.debian_slim("3.12")
|
|
322
|
+
def debian_slim(python_version = nil) # rubocop:disable Metrics/MethodLength
|
|
323
|
+
python_version = process_python_version(python_version)
|
|
324
|
+
img = new
|
|
325
|
+
commands = [
|
|
326
|
+
"FROM python:#{python_version}-slim-bookworm",
|
|
327
|
+
'RUN apt-get update',
|
|
328
|
+
'RUN apt-get install -y gcc gfortran build-essential',
|
|
329
|
+
'RUN pip install --upgrade pip',
|
|
330
|
+
# Set debian front-end to non-interactive to avoid users getting stuck with input prompts.
|
|
331
|
+
"RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections"
|
|
332
|
+
]
|
|
333
|
+
img.instance_variable_set(:@dockerfile, "#{commands.join("\n")}\n")
|
|
334
|
+
img
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
private
|
|
338
|
+
|
|
339
|
+
# Processes the Python version
|
|
340
|
+
#
|
|
341
|
+
# @param python_version [String, nil] The Python version to process
|
|
342
|
+
# @param allow_micro_granularity [Boolean] Whether to allow micro-level granularity
|
|
343
|
+
# @return [String] The processed Python version
|
|
344
|
+
def process_python_version(python_version = nil)
|
|
345
|
+
python_version ||= SUPPORTED_PYTHON_SERIES.last
|
|
346
|
+
|
|
347
|
+
unless SUPPORTED_PYTHON_SERIES.include?(python_version)
|
|
348
|
+
raise Sdk::Error, "Unsupported Python version: #{python_version}"
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
LATEST_PYTHON_MICRO_VERSIONS.select { |v| v.start_with?(python_version) }.last
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Extracts source files from COPY commands in a Dockerfile
|
|
355
|
+
#
|
|
356
|
+
# @param dockerfile_content [String] The content of the Dockerfile
|
|
357
|
+
# @param path_prefix [String] The path prefix to use for the sources
|
|
358
|
+
# @return [Array<Array<String>>] The list of the actual file path and its corresponding COPY-command source path
|
|
359
|
+
def extract_copy_sources(dockerfile_content, path_prefix = '') # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
|
360
|
+
sources = []
|
|
361
|
+
lines = dockerfile_content.split("\n")
|
|
362
|
+
|
|
363
|
+
lines.each do |line|
|
|
364
|
+
# Skip empty lines and comments
|
|
365
|
+
next if line.strip.empty? || line.strip.start_with?('#')
|
|
366
|
+
|
|
367
|
+
# Check if the line contains a COPY command (at the beginning of the line)
|
|
368
|
+
next unless line.match?(/^\s*COPY\s+(?!.*--from=)/i)
|
|
369
|
+
|
|
370
|
+
# Extract the sources from the COPY command
|
|
371
|
+
command_parts = parse_copy_command(line)
|
|
372
|
+
next unless command_parts
|
|
373
|
+
|
|
374
|
+
# Get source paths from the parsed command parts
|
|
375
|
+
command_parts['sources'].each do |source|
|
|
376
|
+
# Handle absolute and relative paths differently
|
|
377
|
+
full_path_pattern = if Pathname.new(source).absolute?
|
|
378
|
+
# Absolute path - use as is
|
|
379
|
+
source
|
|
380
|
+
else
|
|
381
|
+
# Relative path - add prefix
|
|
382
|
+
File.join(path_prefix, source)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# Handle glob patterns
|
|
386
|
+
matching_files = Dir.glob(full_path_pattern)
|
|
387
|
+
|
|
388
|
+
if matching_files.any?
|
|
389
|
+
matching_files.each { |matching_file| sources << [matching_file, source] }
|
|
390
|
+
else
|
|
391
|
+
# If no files match, include the pattern anyway
|
|
392
|
+
sources << [full_path_pattern, source]
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
sources
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Parses a COPY command to extract sources and destination
|
|
401
|
+
#
|
|
402
|
+
# @param line [String] The line to parse
|
|
403
|
+
# @return [Hash, nil] A hash containing the sources and destination, or nil if parsing fails
|
|
404
|
+
def parse_copy_command(line) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
|
405
|
+
# Remove initial "COPY" and strip whitespace
|
|
406
|
+
parts = line.strip[4..].strip
|
|
407
|
+
|
|
408
|
+
# Handle JSON array format: COPY ["src1", "src2", "dest"]
|
|
409
|
+
if parts.start_with?('[')
|
|
410
|
+
begin
|
|
411
|
+
# Parse the JSON-like array format
|
|
412
|
+
elements = Shellwords.split(parts.delete('[]'))
|
|
413
|
+
return nil if elements.length < 2
|
|
414
|
+
|
|
415
|
+
{ 'sources' => elements[0..-2], 'dest' => elements[-1] }
|
|
416
|
+
rescue StandardError
|
|
417
|
+
nil
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Handle regular format with possible flags
|
|
422
|
+
parts = Shellwords.split(parts)
|
|
423
|
+
|
|
424
|
+
# Extract flags like --chown, --chmod, --from
|
|
425
|
+
sources_start_idx = 0
|
|
426
|
+
parts.each_with_index do |part, i|
|
|
427
|
+
break unless part.start_with?('--')
|
|
428
|
+
|
|
429
|
+
# Skip the flag and its value if it has one
|
|
430
|
+
sources_start_idx = if !part.include?('=') && i + 1 < parts.length && !parts[i + 1].start_with?('--')
|
|
431
|
+
i + 2
|
|
432
|
+
else
|
|
433
|
+
i + 1
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# After skipping flags, we need at least one source and one destination
|
|
438
|
+
return nil if parts.length - sources_start_idx < 2
|
|
439
|
+
|
|
440
|
+
{ 'sources' => parts[sources_start_idx..-2], 'dest' => parts[-1] }
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
private
|
|
445
|
+
|
|
446
|
+
# Flattens a list of strings and arrays of strings into a single array of strings
|
|
447
|
+
#
|
|
448
|
+
# @param function_name [String] The name of the function that is being called
|
|
449
|
+
# @param arg_name [String] The name of the argument that is being passed
|
|
450
|
+
# @param args [Array] The list of arguments to flatten
|
|
451
|
+
# @return [Array<String>] A list of strings
|
|
452
|
+
def flatten_str_args(function_name, arg_name, args) # rubocop:disable Metrics/MethodLength
|
|
453
|
+
ret = []
|
|
454
|
+
args.each do |x|
|
|
455
|
+
case x
|
|
456
|
+
when String
|
|
457
|
+
ret << x
|
|
458
|
+
when Array
|
|
459
|
+
unless x.all? { |y| y.is_a?(String) }
|
|
460
|
+
raise Sdk::Error, "#{function_name}: #{arg_name} must only contain strings"
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
ret.concat(x)
|
|
464
|
+
|
|
465
|
+
else
|
|
466
|
+
raise Sdk::Error, "#{function_name}: #{arg_name} must only contain strings"
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
ret
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# Formats the arguments in a single string
|
|
473
|
+
#
|
|
474
|
+
# @param find_links [Array<String>, nil] The find-links to use
|
|
475
|
+
# @param index_url [String, nil] The index URL to use
|
|
476
|
+
# @param extra_index_urls [Array<String>, nil] The extra index URLs to use
|
|
477
|
+
# @param pre [Boolean] Whether to install pre-release packages
|
|
478
|
+
# @param extra_options [String] Additional options to pass to pip
|
|
479
|
+
# @return [String] The formatted arguments
|
|
480
|
+
def format_pip_install_args(find_links: nil, index_url: nil, extra_index_urls: nil, pre: false, extra_options: '') # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
481
|
+
extra_args = ''
|
|
482
|
+
find_links&.each { |find_link| extra_args += " --find-links #{Shellwords.escape(find_link)}" }
|
|
483
|
+
extra_args += " --index-url #{Shellwords.escape(index_url)}" if index_url
|
|
484
|
+
extra_index_urls&.each do |extra_index_url|
|
|
485
|
+
extra_args += " --extra-index-url #{Shellwords.escape(extra_index_url)}"
|
|
486
|
+
end
|
|
487
|
+
extra_args += ' --pre' if pre
|
|
488
|
+
extra_args += " #{extra_options.strip}" if extra_options && !extra_options.strip.empty?
|
|
489
|
+
|
|
490
|
+
extra_args
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Daytona
|
|
4
|
+
class ExecuteResponse
|
|
5
|
+
# @return [Integer] The exit code from the command execution
|
|
6
|
+
attr_reader :exit_code
|
|
7
|
+
|
|
8
|
+
# @return [String] The output from the command execution
|
|
9
|
+
attr_reader :result
|
|
10
|
+
|
|
11
|
+
# @return [ExecutionArtifacts, nil] Artifacts from the command execution
|
|
12
|
+
attr_reader :artifacts
|
|
13
|
+
|
|
14
|
+
# @return [Hash] Additional properties from the response
|
|
15
|
+
attr_reader :additional_properties
|
|
16
|
+
|
|
17
|
+
# Initialize a new ExecuteResponse
|
|
18
|
+
#
|
|
19
|
+
# @param exit_code [Integer] The exit code from the command execution
|
|
20
|
+
# @param result [String] The output from the command execution
|
|
21
|
+
# @param artifacts [ExecutionArtifacts, nil] Artifacts from the command execution
|
|
22
|
+
# @param additional_properties [Hash] Additional properties from the response
|
|
23
|
+
def initialize(exit_code:, result:, artifacts: nil, additional_properties: {})
|
|
24
|
+
@exit_code = exit_code
|
|
25
|
+
@result = result
|
|
26
|
+
@artifacts = artifacts
|
|
27
|
+
@additional_properties = additional_properties
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class ExecutionArtifacts
|
|
32
|
+
# @return [String] Standard output from the command, same as `result` in `ExecuteResponse`
|
|
33
|
+
attr_accessor :stdout
|
|
34
|
+
|
|
35
|
+
# @return [Array] List of chart metadata from matplotlib
|
|
36
|
+
attr_accessor :charts
|
|
37
|
+
|
|
38
|
+
# Initialize a new ExecutionArtifacts
|
|
39
|
+
#
|
|
40
|
+
# @param stdout [String] Standard output from the command
|
|
41
|
+
# @param charts [Array] List of chart metadata from matplotlib
|
|
42
|
+
def initialize(stdout = '', charts = [])
|
|
43
|
+
@stdout = stdout
|
|
44
|
+
@charts = charts
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class CodeRunParams
|
|
49
|
+
# @return [Array<String>, nil] Command line arguments
|
|
50
|
+
attr_accessor :argv
|
|
51
|
+
|
|
52
|
+
# @return [Hash<String, String>, nil] Environment variables
|
|
53
|
+
attr_accessor :env
|
|
54
|
+
|
|
55
|
+
# Initialize a new CodeRunParams
|
|
56
|
+
#
|
|
57
|
+
# @param argv [Array<String>, nil] Command line arguments
|
|
58
|
+
# @param env [Hash<String, String>, nil] Environment variables
|
|
59
|
+
def initialize(argv: nil, env: nil)
|
|
60
|
+
@argv = argv
|
|
61
|
+
@env = env
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class SessionExecuteRequest
|
|
66
|
+
# @return [String] The command to execute
|
|
67
|
+
attr_accessor :command
|
|
68
|
+
|
|
69
|
+
# @return [Boolean] Whether to execute the command asynchronously
|
|
70
|
+
attr_accessor :run_async
|
|
71
|
+
|
|
72
|
+
# Initialize a new SessionExecuteRequest
|
|
73
|
+
#
|
|
74
|
+
# @param command [String] The command to execute
|
|
75
|
+
# @param run_async [Boolean] Whether to execute the command asynchronously
|
|
76
|
+
def initialize(command:, run_async: false)
|
|
77
|
+
@command = command
|
|
78
|
+
@run_async = run_async
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class SessionExecuteResponse
|
|
83
|
+
# @return [String, nil] Unique identifier for the executed command
|
|
84
|
+
attr_reader :cmd_id
|
|
85
|
+
|
|
86
|
+
# @return [String, nil] The output from the command execution
|
|
87
|
+
attr_reader :output
|
|
88
|
+
|
|
89
|
+
# @return [String, nil] Standard output from the command
|
|
90
|
+
attr_reader :stdout
|
|
91
|
+
|
|
92
|
+
# @return [String, nil] Standard error from the command
|
|
93
|
+
attr_reader :stderr
|
|
94
|
+
|
|
95
|
+
# @return [Integer, nil] The exit code from the command execution
|
|
96
|
+
attr_reader :exit_code
|
|
97
|
+
|
|
98
|
+
# @return [Hash] Additional properties from the response
|
|
99
|
+
attr_reader :additional_properties
|
|
100
|
+
|
|
101
|
+
# Initialize a new SessionExecuteResponse
|
|
102
|
+
#
|
|
103
|
+
# @param opts [Hash] Options for the SessionExecuteResponse
|
|
104
|
+
# @param cmd_id [String, nil] Unique identifier for the executed command
|
|
105
|
+
# @param output [String, nil] The output from the command execution
|
|
106
|
+
# @param stdout [String, nil] Standard output from the command
|
|
107
|
+
# @param stderr [String, nil] Standard error from the command
|
|
108
|
+
# @param exit_code [Integer, nil] The exit code from the command execution
|
|
109
|
+
# @param additional_properties [Hash] Additional properties from the response
|
|
110
|
+
def initialize(opts = {})
|
|
111
|
+
@cmd_id = opts.fetch(:cmd_id, nil)
|
|
112
|
+
@output = opts.fetch(:output, nil)
|
|
113
|
+
@stdout = opts.fetch(:stdout, nil)
|
|
114
|
+
@stderr = opts.fetch(:stderr, nil)
|
|
115
|
+
@exit_code = opts.fetch(:exit_code, nil)
|
|
116
|
+
@additional_properties = opts.fetch(:additional_properties, {})
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
class SessionCommandLogsResponse
|
|
121
|
+
# @return [String, nil] The combined output from the command
|
|
122
|
+
attr_reader :output
|
|
123
|
+
|
|
124
|
+
# @return [String, nil] The stdout from the command
|
|
125
|
+
attr_reader :stdout
|
|
126
|
+
|
|
127
|
+
# @return [String, nil] The stderr from the command
|
|
128
|
+
attr_reader :stderr
|
|
129
|
+
|
|
130
|
+
# Initialize a new SessionCommandLogsResponse
|
|
131
|
+
#
|
|
132
|
+
# @param output [String, nil] The combined output from the command
|
|
133
|
+
# @param stdout [String, nil] The stdout from the command
|
|
134
|
+
# @param stderr [String, nil] The stderr from the command
|
|
135
|
+
def initialize(output: nil, stdout: nil, stderr: nil)
|
|
136
|
+
@output = output
|
|
137
|
+
@stdout = stdout
|
|
138
|
+
@stderr = stderr
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|