sprites-ruby 0.1.0 → 0.2.1

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: 696d67a3d2015a55e8eb2e5b6179e37b2594eecbd49c4f355000fc246e45827b
4
- data.tar.gz: db6806ee207f88f94a251af9a5681d6c68b2910aa9b5f54d2db627f07bc55209
3
+ metadata.gz: 5da75311f037ca5acfc55a350835e258e792f06221d4f9fab135e7ba567676b0
4
+ data.tar.gz: c8ef0d1914539b8e618dea6aad2bf9641269d5396397950d1048a9295fc399f7
5
5
  SHA512:
6
- metadata.gz: fcd1a087a9373e8c3a4d2cd82f2acd6070e5a78645c8b0e893310f2913585f805f0f4486d9a6475d1a3e4cd2dc271893af8b3b175deddb0a4bea15a12ae8d897
7
- data.tar.gz: 1507c816cf3081bd407e391ab067160d04e4dd35cfd25a637a4e80ac7bad0bfecb880bb6f690f5058b4c2782980e99ad90644e87db5eb191039d11aff86d2a74
6
+ metadata.gz: cd6283f8bee73c8a9adcb7a039cef763d2109e980da95fd4ec9eccd852af89a89b4b894b5299c2d76afa87edb2bbb61edbe3c4bab5d17dab8169c0d9b037d523
7
+ data.tar.gz: 9e96fe5d15b8f974fce96a1ced6740aa83ba6a77f78abb7e2a682668b29c03b49c4cd7b4f5f7938c1c3073b1e84e5be9b37ce44864be736d1442f3bf9a887e14
data/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ ## 0.2.1
4
+
5
+ - Fix parsing error for empty API responses
6
+
7
+ ## 0.2.0
8
+
9
+ - Command execution via HTTP (`exec.create`)
10
+ - WebSocket exec for interactive sessions (`run`, `interactive`, `connect`)
11
+ - Session management (`exec.list`, `exec.attach`, `exec.kill`)
12
+ - `wait_until_warm` option for sprite creation
13
+
14
+ ## 0.1.0
15
+
16
+ - Initial release
17
+ - Sprites CRUD operations
18
+ - Checkpoints (create, list, retrieve, restore)
19
+ - Network policies (retrieve, update)
data/README.md CHANGED
@@ -1,13 +1,184 @@
1
- # Sprites Ruby
1
+ # Sprites
2
2
 
3
- Ruby client for the [Sprites](https://sprites.dev) API.
3
+ Ruby client for the [Sprites](https://sprites.dev) API - stateful sandbox environments.
4
4
 
5
5
  ## Installation
6
6
 
7
+ Add this line to your application's Gemfile:
8
+
7
9
  ```ruby
8
10
  gem "sprites-ruby"
9
11
  ```
10
12
 
13
+ ## Getting Started
14
+
15
+ Create a client
16
+
17
+ ```ruby
18
+ client = Sprites::Client.new(token: ENV["SPRITES_TOKEN"])
19
+ ```
20
+
21
+ Or configure globally
22
+
23
+ ```ruby
24
+ Sprites.configure do |config|
25
+ config.token = ENV["SPRITES_TOKEN"]
26
+ end
27
+
28
+ client = Sprites::Client.new
29
+ ```
30
+
31
+ ## Sprites
32
+
33
+ Create a sprite
34
+
35
+ ```ruby
36
+ sprite = client.sprites.create(name: "my-sprite")
37
+ ```
38
+
39
+ Create and wait until ready
40
+
41
+ ```ruby
42
+ sprite = client.sprites.create(name: "my-sprite", wait: true)
43
+ ```
44
+
45
+ List sprites
46
+
47
+ ```ruby
48
+ collection = client.sprites.list
49
+ collection.sprites.each { |s| puts s.name }
50
+ ```
51
+
52
+ Get a sprite
53
+
54
+ ```ruby
55
+ sprite = client.sprites.retrieve("my-sprite")
56
+ sprite.status # => "warm"
57
+ ```
58
+
59
+ Update a sprite
60
+
61
+ ```ruby
62
+ sprite = client.sprites.update("my-sprite", url_settings: { auth: "public" })
63
+ ```
64
+
65
+ Delete a sprite
66
+
67
+ ```ruby
68
+ client.sprites.delete("my-sprite")
69
+ ```
70
+
71
+ ## Command Execution
72
+
73
+ Run a command (HTTP, simple)
74
+
75
+ ```ruby
76
+ result = client.exec.create("my-sprite", command: "echo hello")
77
+ result[:output] # => "hello\n"
78
+ result[:exit_code] # => 0
79
+ ```
80
+
81
+ Run a command (WebSocket, streaming)
82
+
83
+ ```ruby
84
+ result = client.exec.run("my-sprite", ["ls", "-la"])
85
+ result.stdout
86
+ result.stderr
87
+ result.exit_code
88
+ ```
89
+
90
+ Interactive terminal session
91
+
92
+ ```ruby
93
+ client.exec.interactive("my-sprite", ["bash"])
94
+ ```
95
+
96
+ With custom I/O
97
+
98
+ ```ruby
99
+ client.exec.interactive("my-sprite", ["bash"], input: $stdin, output: $stdout)
100
+ ```
101
+
102
+ ## Sessions
103
+
104
+ List active sessions
105
+
106
+ ```ruby
107
+ sessions = client.exec.list("my-sprite")
108
+ sessions.each { |s| puts "#{s[:id]}: #{s[:command]}" }
109
+ ```
110
+
111
+ Attach to a session
112
+
113
+ ```ruby
114
+ client.exec.attach("my-sprite", session_id)
115
+ ```
116
+
117
+ Kill a session
118
+
119
+ ```ruby
120
+ client.exec.kill("my-sprite", session_id)
121
+ ```
122
+
123
+ With a specific signal
124
+
125
+ ```ruby
126
+ client.exec.kill("my-sprite", session_id, signal: "SIGKILL")
127
+ ```
128
+
129
+ ## Checkpoints
130
+
131
+ Create a checkpoint
132
+
133
+ ```ruby
134
+ client.checkpoints.create("my-sprite", comment: "before deploy")
135
+ ```
136
+
137
+ List checkpoints
138
+
139
+ ```ruby
140
+ checkpoints = client.checkpoints.list("my-sprite")
141
+ ```
142
+
143
+ Restore a checkpoint
144
+
145
+ ```ruby
146
+ client.checkpoints.restore("my-sprite", checkpoint_id)
147
+ ```
148
+
149
+ ## Network Policies
150
+
151
+ Get current policy
152
+
153
+ ```ruby
154
+ policy = client.policies.retrieve("my-sprite")
155
+ policy[:egress][:policy] # => "allow-all"
156
+ ```
157
+
158
+ Update policy
159
+
160
+ ```ruby
161
+ client.policies.update("my-sprite", egress: { policy: "block-all" })
162
+ ```
163
+
164
+ ## Low-Level WebSocket API
165
+
166
+ For advanced use cases, use `connect` directly
167
+
168
+ ```ruby
169
+ client.exec.connect("my-sprite", command: ["bash"], tty: true) do |task, session|
170
+ session.on_stdout { |data| print data }
171
+ session.on_stderr { |data| $stderr.print data }
172
+ session.on_exit { |code| puts "Exit: #{code}" }
173
+
174
+ task.async do
175
+ while (line = $stdin.gets)
176
+ session.write(line)
177
+ end
178
+ end
179
+ end
180
+ ```
181
+
11
182
  ## Development
12
183
 
13
184
  ```sh
@@ -4,24 +4,63 @@ require "faraday"
4
4
  require "json"
5
5
 
6
6
  module Sprites
7
+ # HTTP client for the Sprites API.
8
+ #
9
+ # @example
10
+ # client = Sprites::Client.new(token: "your-token")
11
+ # sprite = client.sprites.create(name: "my-sprite")
12
+ #
7
13
  class Client
14
+ # @return [String] the API token
15
+ attr_reader :token
16
+
17
+ # Create a new client.
18
+ #
19
+ # @param token [String, nil] API token (defaults to Sprites.configuration.token)
20
+ # @param base_url [String, nil] API base URL (defaults to https://api.sprites.dev)
8
21
  def initialize(token: nil, base_url: nil)
9
22
  @token = token || Sprites.configuration.token
10
23
  @base_url = base_url || Sprites.configuration.base_url
11
24
  end
12
25
 
26
+ # Access sprite operations.
27
+ #
28
+ # @return [Resources::Sprites]
13
29
  def sprites
14
30
  Resources::Sprites.new(self)
15
31
  end
16
32
 
33
+ # Access checkpoint operations.
34
+ #
35
+ # @return [Resources::Checkpoints]
17
36
  def checkpoints
18
37
  Resources::Checkpoints.new(self)
19
38
  end
20
39
 
40
+ # Access network policy operations.
41
+ #
42
+ # @return [Resources::Policies]
21
43
  def policies
22
44
  Resources::Policies.new(self)
23
45
  end
24
46
 
47
+ # Access command execution operations.
48
+ #
49
+ # @return [Resources::Exec]
50
+ def exec
51
+ Resources::Exec.new(self)
52
+ end
53
+
54
+ # @return [String] WebSocket URL derived from base URL
55
+ def websocket_url
56
+ @base_url.sub(/^http(s?)/) { "ws#{$1}" }
57
+ end
58
+
59
+ # @return [Array<Array<String>>] authorization headers for WebSocket connections
60
+ def auth_headers
61
+ [["authorization", "Bearer #{@token}"]]
62
+ end
63
+
25
64
  def get(path) = handle_response(connection.get(path))
26
65
 
27
66
  def post(path, body) = handle_response(connection.post(path, body.to_json, "Content-Type" => "application/json"))
@@ -65,7 +104,11 @@ module Sprites
65
104
  raise Error, body.strip
66
105
  end
67
106
 
68
- def parse_json(json) = JSON.parse(json, symbolize_names: true)
107
+ def parse_json(json)
108
+ return nil if json.to_s.empty?
109
+
110
+ JSON.parse(json, symbolize_names: true)
111
+ end
69
112
 
70
113
  def parse_ndjson(body) = body.each_line.map { parse_json(it) }
71
114
 
@@ -1,8 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sprites
4
+ # Paginated collection of sprites.
4
5
  class Collection
5
- attr_reader :sprites, :next_continuation_token
6
+ # @return [Array<Sprite>] sprites in this page
7
+ attr_reader :sprites
8
+
9
+ # @return [String, nil] token for fetching the next page
10
+ attr_reader :next_continuation_token
6
11
 
7
12
  def initialize(sprites:, has_more:, next_continuation_token:)
8
13
  @sprites = sprites
@@ -10,6 +15,9 @@ module Sprites
10
15
  @next_continuation_token = next_continuation_token
11
16
  end
12
17
 
18
+ # Check if more pages are available.
19
+ #
20
+ # @return [Boolean]
13
21
  def has_more?
14
22
  @has_more && !@next_continuation_token.nil?
15
23
  end
@@ -2,23 +2,45 @@
2
2
 
3
3
  module Sprites
4
4
  module Resources
5
+ # Checkpoint operations for sprite snapshots.
5
6
  class Checkpoints
6
7
  def initialize(client)
7
8
  @client = client
8
9
  end
9
10
 
11
+ # List all checkpoints for a sprite.
12
+ #
13
+ # @param sprite_name [String] sprite name
14
+ # @return [Array<Hash>] checkpoints
10
15
  def list(sprite_name)
11
16
  @client.get("/v1/sprites/#{sprite_name}/checkpoints")
12
17
  end
13
18
 
19
+ # Retrieve a checkpoint by ID.
20
+ #
21
+ # @param sprite_name [String] sprite name
22
+ # @param checkpoint_id [String] checkpoint ID
23
+ # @return [Hash] checkpoint details
14
24
  def retrieve(sprite_name, checkpoint_id)
15
25
  @client.get("/v1/sprites/#{sprite_name}/checkpoints/#{checkpoint_id}")
16
26
  end
17
27
 
28
+ # Create a new checkpoint.
29
+ #
30
+ # @param sprite_name [String] sprite name
31
+ # @param comment [String, nil] optional comment
32
+ # @yield [Hash] streaming NDJSON events
33
+ # @return [Array<Hash>] all events if no block given
18
34
  def create(sprite_name, comment: nil, &block)
19
35
  @client.post_stream("/v1/sprites/#{sprite_name}/checkpoint", { comment: }, &block)
20
36
  end
21
37
 
38
+ # Restore a sprite to a checkpoint.
39
+ #
40
+ # @param sprite_name [String] sprite name
41
+ # @param checkpoint_id [String] checkpoint ID
42
+ # @yield [Hash] streaming NDJSON events
43
+ # @return [Array<Hash>] all events if no block given
22
44
  def restore(sprite_name, checkpoint_id, &block)
23
45
  @client.post_stream("/v1/sprites/#{sprite_name}/checkpoints/#{checkpoint_id}/restore", {}, &block)
24
46
  end
@@ -0,0 +1,293 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async"
4
+ require "async/http/endpoint"
5
+ require "async/websocket/client"
6
+ require "io/console"
7
+ require "io/stream"
8
+ require "uri"
9
+
10
+ module Sprites
11
+ module Resources
12
+ # Command execution on sprites via HTTP and WebSocket.
13
+ class Exec
14
+ STREAM_STDIN = 0
15
+ STREAM_STDOUT = 1
16
+ STREAM_STDERR = 2
17
+ STREAM_EXIT = 3
18
+ STREAM_STDIN_EOF = 4
19
+
20
+ def initialize(client)
21
+ @client = client
22
+ end
23
+
24
+ # Execute a command via HTTP POST (non-streaming).
25
+ #
26
+ # @param sprite_name [String] sprite name
27
+ # @param command [String] command to execute
28
+ # @return [Hash] result with :exit_code and :output
29
+ def create(sprite_name, command:)
30
+ @client.post("/v1/sprites/#{sprite_name}/exec", { command: })
31
+ end
32
+
33
+ # List active exec sessions.
34
+ #
35
+ # @param sprite_name [String] sprite name
36
+ # @return [Array<Hash>] sessions with :id, :command, :tty, :is_active, etc.
37
+ def list(sprite_name)
38
+ @client.get("/v1/sprites/#{sprite_name}/exec")
39
+ end
40
+
41
+ # Run a command via WebSocket and return the result (blocking).
42
+ #
43
+ # @param sprite_name [String] sprite name
44
+ # @param command [Array<String>] command and arguments
45
+ # @param options [Hash] WebSocket options (:cols, :rows, :path)
46
+ # @return [Result] with stdout, stderr, and exit_code
47
+ def run(sprite_name, command, **options)
48
+ stdout = +""
49
+ stderr = +""
50
+ exit_code = nil
51
+
52
+ connect(sprite_name, command: command, **options) do |_task, session|
53
+ session.on_stdout { |data| stdout << data }
54
+ session.on_stderr { |data| stderr << data }
55
+ session.on_exit { |code| exit_code = code }
56
+ end
57
+
58
+ Result.new(stdout: stdout, stderr: stderr, exit_code: exit_code)
59
+ end
60
+
61
+ # Start an interactive terminal session wired to stdin/stdout.
62
+ #
63
+ # @param sprite_name [String] sprite name
64
+ # @param command [Array<String>] command and arguments
65
+ # @param input [IO] input stream (default: $stdin)
66
+ # @param output [IO] output stream (default: $stdout)
67
+ # @param options [Hash] WebSocket options (:cols, :rows)
68
+ def interactive(sprite_name, command, input: $stdin, output: $stdout, **options)
69
+ run_interactive(input: input, output: output) do |block|
70
+ connect(sprite_name, command: command, tty: true, **options, &block)
71
+ end
72
+ end
73
+
74
+ # Attach to an existing exec session.
75
+ #
76
+ # @param sprite_name [String] sprite name
77
+ # @param session_id [Integer] session ID from #list
78
+ # @param input [IO] input stream (default: $stdin)
79
+ # @param output [IO] output stream (default: $stdout)
80
+ def attach(sprite_name, session_id, input: $stdin, output: $stdout)
81
+ run_interactive(input: input, output: output) do |block|
82
+ connect(sprite_name, session_id: session_id, tty: true, &block)
83
+ end
84
+ end
85
+
86
+ # Kill an exec session.
87
+ #
88
+ # @param sprite_name [String] sprite name
89
+ # @param session_id [Integer] session ID from #list
90
+ # @param signal [String, nil] signal to send (default: SIGTERM)
91
+ # @param timeout [String, nil] timeout waiting for exit (default: 10s)
92
+ # @yield [Hash] streaming NDJSON events (signal, exited, complete)
93
+ # @return [Array<Hash>] all events if no block given
94
+ def kill(sprite_name, session_id, signal: nil, timeout: nil, &block)
95
+ body = {}
96
+ body[:signal] = signal if signal
97
+ body[:timeout] = timeout if timeout
98
+ @client.post_stream("/v1/sprites/#{sprite_name}/exec/#{session_id}/kill", body, &block)
99
+ end
100
+
101
+ # Connect to a WebSocket exec session.
102
+ #
103
+ # Yields the Async task and session for concurrent I/O.
104
+ #
105
+ # @param sprite_name [String] sprite name
106
+ # @param command [Array<String>, nil] command and arguments
107
+ # @param tty [Boolean] enable TTY mode (default: false)
108
+ # @param options [Hash] WebSocket options (:session_id, :cols, :rows, :path, :stdin)
109
+ # @yield [Async::Task, Session] task and session for callbacks
110
+ #
111
+ # @example
112
+ # client.exec.connect(sprite.name, command: ["bash"], tty: true) do |task, session|
113
+ # session.on_stdout { |data| print data }
114
+ # task.async do
115
+ # while (line = $stdin.gets)
116
+ # session.write(line)
117
+ # end
118
+ # end
119
+ # end
120
+ def connect(sprite_name, command: nil, tty: false, **options, &block)
121
+ url = build_websocket_url(sprite_name, command, tty: tty, **options)
122
+ endpoint = Async::HTTP::Endpoint.parse(url, alpn_protocols: ["http/1.1"])
123
+ headers = Protocol::HTTP::Headers.new
124
+ headers["authorization"] = "Bearer #{@client.token}"
125
+
126
+ Async do |task|
127
+ Async::WebSocket::Client.connect(endpoint, headers: headers) do |connection|
128
+ session = Session.new(connection, tty: tty)
129
+
130
+ # Run user's block and read loop concurrently
131
+ task.async { block.call(task, session) }
132
+ session.read_loop
133
+ end
134
+ end
135
+ end
136
+
137
+ private
138
+
139
+ def run_interactive(input:, output:)
140
+ input.raw!
141
+ stdin = IO::Stream::Buffered.wrap(input)
142
+
143
+ yield proc { |task, session|
144
+ session.on_stdout do |data|
145
+ output.write(data)
146
+ output.flush
147
+ end
148
+ session.on_stderr do |data|
149
+ output.write(data)
150
+ output.flush
151
+ end
152
+
153
+ input_task = task.async do
154
+ while (char = stdin.read(1))
155
+ session.write(char)
156
+ end
157
+ rescue IOError
158
+ # Connection closed
159
+ end
160
+
161
+ session.on_exit { |_code| input_task.stop }
162
+ }
163
+ ensure
164
+ input.cooked!
165
+ end
166
+
167
+ def build_websocket_url(sprite_name, command, tty:, session_id: nil, **options)
168
+ base = "#{@client.websocket_url}/v1/sprites/#{sprite_name}/exec"
169
+ base = "#{base}/#{session_id}" if session_id
170
+
171
+ params = []
172
+ Array(command).each { |arg| params << ["cmd", arg] } unless session_id
173
+ params << ["tty", "true"] if tty
174
+ params << ["stdin", "true"] if options.fetch(:stdin, true)
175
+ params << ["cols", options[:cols].to_s] if options[:cols]
176
+ params << ["rows", options[:rows].to_s] if options[:rows]
177
+ params << ["path", options[:path]] if options[:path]
178
+
179
+ query = params.empty? ? "" : "?#{URI.encode_www_form(params)}"
180
+ "#{base}#{query}"
181
+ end
182
+
183
+ # Result of a blocking command execution.
184
+ # @!attribute [r] stdout
185
+ # @return [String] standard output
186
+ # @!attribute [r] stderr
187
+ # @return [String] standard error
188
+ # @!attribute [r] exit_code
189
+ # @return [Integer] process exit code
190
+ Result = Data.define(:stdout, :stderr, :exit_code)
191
+
192
+ # WebSocket session for interactive command execution.
193
+ class Session
194
+ def initialize(connection, tty: false)
195
+ @connection = connection
196
+ @tty = tty
197
+ @callbacks = { stdout: [], stderr: [], exit: [] }
198
+ @exit_code = nil
199
+ end
200
+
201
+ # @return [Integer, nil] exit code once process exits
202
+ attr_reader :exit_code
203
+
204
+ # Register a callback for stdout data.
205
+ # @yield [String] stdout data
206
+ def on_stdout(&block) = @callbacks[:stdout] << block
207
+
208
+ # Register a callback for stderr data.
209
+ # @yield [String] stderr data
210
+ def on_stderr(&block) = @callbacks[:stderr] << block
211
+
212
+ # Register a callback for process exit.
213
+ # @yield [Integer] exit code
214
+ def on_exit(&block) = @callbacks[:exit] << block
215
+
216
+ # Write data to stdin.
217
+ # @param data [String] data to write
218
+ def write(data)
219
+ if @tty
220
+ @connection.write(Protocol::WebSocket::BinaryMessage.new(data))
221
+ else
222
+ message = [STREAM_STDIN].pack("C") + data
223
+ @connection.write(Protocol::WebSocket::BinaryMessage.new(message))
224
+ end
225
+ @connection.flush
226
+ end
227
+
228
+ # Signal end of stdin (non-TTY mode only).
229
+ def send_eof
230
+ return if @tty
231
+
232
+ message = [STREAM_STDIN_EOF].pack("C")
233
+ @connection.write(Protocol::WebSocket::BinaryMessage.new(message))
234
+ @connection.flush
235
+ end
236
+
237
+ # Close the WebSocket connection.
238
+ def close
239
+ @connection.close
240
+ end
241
+
242
+ def read_loop
243
+ loop do
244
+ message = @connection.read
245
+ break unless message
246
+
247
+ handle_message(message)
248
+ break if @exit_code
249
+ end
250
+ end
251
+
252
+ private
253
+
254
+ def handle_message(message)
255
+ data = message.respond_to?(:buffer) ? message.buffer : message.to_s
256
+
257
+ if data.start_with?("{")
258
+ handle_json_message(JSON.parse(data, symbolize_names: true))
259
+ elsif @tty
260
+ @callbacks[:stdout].each { |cb| cb.call(data) }
261
+ else
262
+ handle_binary_message(data)
263
+ end
264
+ end
265
+
266
+ def handle_json_message(parsed)
267
+ case parsed[:type]
268
+ when "exit"
269
+ @exit_code = parsed[:exit_code]
270
+ @callbacks[:exit].each { |cb| cb.call(@exit_code) }
271
+ end
272
+ end
273
+
274
+ def handle_binary_message(data)
275
+ return if data.nil? || data.empty?
276
+
277
+ stream_id = data.bytes[0]
278
+ payload = data[1..]
279
+
280
+ case stream_id
281
+ when STREAM_STDOUT
282
+ @callbacks[:stdout].each { |cb| cb.call(payload) }
283
+ when STREAM_STDERR
284
+ @callbacks[:stderr].each { |cb| cb.call(payload) }
285
+ when STREAM_EXIT
286
+ @exit_code = payload.unpack1("C")
287
+ @callbacks[:exit].each { |cb| cb.call(@exit_code) }
288
+ end
289
+ end
290
+ end
291
+ end
292
+ end
293
+ end
@@ -2,15 +2,25 @@
2
2
 
3
3
  module Sprites
4
4
  module Resources
5
+ # Network policy operations.
5
6
  class Policies
6
7
  def initialize(client)
7
8
  @client = client
8
9
  end
9
10
 
11
+ # Get current network policy for a sprite.
12
+ #
13
+ # @param sprite_name [String] sprite name
14
+ # @return [Hash] policy with :egress settings
10
15
  def retrieve(sprite_name)
11
16
  @client.get("/v1/sprites/#{sprite_name}/policies")
12
17
  end
13
18
 
19
+ # Update network policy for a sprite.
20
+ #
21
+ # @param sprite_name [String] sprite name
22
+ # @param attrs [Hash] policy attributes (:egress)
23
+ # @return [Hash] updated policy
14
24
  def update(sprite_name, **attrs)
15
25
  @client.post("/v1/sprites/#{sprite_name}/policies", attrs)
16
26
  end
@@ -2,23 +2,66 @@
2
2
 
3
3
  module Sprites
4
4
  module Resources
5
+ # Sprite CRUD operations.
5
6
  class Sprites
6
7
  def initialize(client)
7
8
  @client = client
8
9
  end
9
10
 
11
+ # List all sprites.
12
+ #
13
+ # @return [Collection] paginated collection of sprites
10
14
  def list
11
15
  @client.get("/v1/sprites") => { sprites:, has_more:, next_continuation_token: }
12
16
  sprites = sprites.map(&Sprite)
13
17
  Collection.new(sprites:, has_more:, next_continuation_token:)
14
18
  end
15
19
 
20
+ # Retrieve a sprite by name.
21
+ #
22
+ # @param name [String] sprite name
23
+ # @return [Sprite]
16
24
  def retrieve(name) = @client.get("/v1/sprites/#{name}").then(&Sprite)
17
25
 
18
- def create(name:) = @client.post("/v1/sprites", { name: }).then(&Sprite)
26
+ # Create a new sprite.
27
+ #
28
+ # @param name [String] sprite name
29
+ # @param wait [Boolean] block until sprite is warm (default: false)
30
+ # @param timeout [Integer] seconds to wait for warm status (default: 60)
31
+ # @return [Sprite]
32
+ def create(name:, wait: false, timeout: 60)
33
+ sprite = @client.post("/v1/sprites", { name: }).then(&Sprite)
34
+ wait ? wait_until_warm(sprite.name, timeout:) : sprite
35
+ end
36
+
37
+ # Poll until a sprite becomes warm.
38
+ #
39
+ # @param name [String] sprite name
40
+ # @param timeout [Integer] seconds to wait (default: 60)
41
+ # @return [Sprite]
42
+ # @raise [Error] if timeout is reached
43
+ def wait_until_warm(name, timeout: 60)
44
+ deadline = Time.now + timeout
45
+ loop do
46
+ sprite = retrieve(name)
47
+ return sprite if sprite.status == "warm"
48
+ raise Error, "Timed out waiting for sprite to become warm" if Time.now > deadline
49
+
50
+ sleep 0.5
51
+ end
52
+ end
19
53
 
54
+ # Update a sprite.
55
+ #
56
+ # @param name [String] sprite name
57
+ # @param attrs [Hash] attributes to update
58
+ # @return [Sprite]
20
59
  def update(name, **attrs) = @client.put("/v1/sprites/#{name}", attrs).then(&Sprite)
21
60
 
61
+ # Delete a sprite.
62
+ #
63
+ # @param name [String] sprite name
64
+ # @return [nil]
22
65
  def delete(name) = @client.delete("/v1/sprites/#{name}")
23
66
  end
24
67
  end
@@ -1,11 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sprites
4
+ # A sprite instance.
4
5
  class Sprite
6
+ # @api private
5
7
  def self.to_proc = ->(attrs) { new(**attrs) }
6
8
 
7
- attr_reader :id, :name, :status, :version, :url, :created_at, :updated_at,
8
- :organization, :url_settings, :environment_version
9
+ # @return [String] unique sprite ID
10
+ attr_reader :id
11
+
12
+ # @return [String] sprite name
13
+ attr_reader :name
14
+
15
+ # @return [String] status ("cold", "warm")
16
+ attr_reader :status
17
+
18
+ # @return [String, nil] sprite version
19
+ attr_reader :version
20
+
21
+ # @return [String] public URL for the sprite
22
+ attr_reader :url
23
+
24
+ # @return [String] creation timestamp
25
+ attr_reader :created_at
26
+
27
+ # @return [String] last update timestamp
28
+ attr_reader :updated_at
29
+
30
+ # @return [String] organization slug
31
+ attr_reader :organization
32
+
33
+ # @return [Hash] URL authentication settings
34
+ attr_reader :url_settings
35
+
36
+ # @return [String, nil] environment version
37
+ attr_reader :environment_version
9
38
 
10
39
  def initialize(id:, name:, status:, version:, url:, created_at:, updated_at:,
11
40
  organization:, url_settings:, environment_version:)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sprites
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/sprites.rb CHANGED
@@ -7,6 +7,7 @@ require_relative "sprites/sprite"
7
7
  require_relative "sprites/resources/sprites"
8
8
  require_relative "sprites/resources/checkpoints"
9
9
  require_relative "sprites/resources/policies"
10
+ require_relative "sprites/resources/exec"
10
11
  require_relative "sprites/client"
11
12
 
12
13
  module Sprites
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sprites-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ara Hacopian
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: async-websocket
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.30'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.30'
26
40
  description: Ruby client for the Sprites API - stateful sandbox environments
27
41
  email:
28
42
  - ara@hacopian.de
@@ -30,6 +44,7 @@ executables: []
30
44
  extensions: []
31
45
  extra_rdoc_files: []
32
46
  files:
47
+ - CHANGELOG.md
33
48
  - LICENSE
34
49
  - README.md
35
50
  - lib/sprites.rb
@@ -37,6 +52,7 @@ files:
37
52
  - lib/sprites/collection.rb
38
53
  - lib/sprites/configuration.rb
39
54
  - lib/sprites/resources/checkpoints.rb
55
+ - lib/sprites/resources/exec.rb
40
56
  - lib/sprites/resources/policies.rb
41
57
  - lib/sprites/resources/sprites.rb
42
58
  - lib/sprites/sprite.rb