boxd 0.2.10.dev615__tar.gz → 0.2.10.dev616__tar.gz

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.
Files changed (41) hide show
  1. {boxd-0.2.10.dev615/src/boxd.egg-info → boxd-0.2.10.dev616}/PKG-INFO +1 -1
  2. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/pyproject.toml +1 -1
  3. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/models.py +10 -3
  4. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/resources/machines.py +36 -12
  5. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616/src/boxd.egg-info}/PKG-INFO +1 -1
  6. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/tests/test_namespaces.py +26 -0
  7. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/LICENSE +0 -0
  8. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/README.md +0 -0
  9. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/setup.cfg +0 -0
  10. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/__init__.py +0 -0
  11. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_client.py +0 -0
  12. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_credentials.py +0 -0
  13. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_generated/__init__.py +0 -0
  14. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_generated/api_pb2.py +0 -0
  15. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_generated/api_pb2_grpc.py +0 -0
  16. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_mappers.py +0 -0
  17. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_requests.py +0 -0
  18. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_streaming.py +0 -0
  19. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_transport.py +0 -0
  20. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_urls.py +0 -0
  21. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_version_check.py +0 -0
  22. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/errors.py +0 -0
  23. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/resources/__init__.py +0 -0
  24. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/resources/account.py +0 -0
  25. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/resources/credentials.py +0 -0
  26. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/resources/disks.py +0 -0
  27. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/resources/domains.py +0 -0
  28. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/resources/orgs.py +0 -0
  29. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/resources/snapshots.py +0 -0
  30. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/resources/vars.py +0 -0
  31. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd.egg-info/SOURCES.txt +0 -0
  32. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd.egg-info/dependency_links.txt +0 -0
  33. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd.egg-info/requires.txt +0 -0
  34. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd.egg-info/top_level.txt +0 -0
  35. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/tests/test_credentials.py +0 -0
  36. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/tests/test_mappers.py +0 -0
  37. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/tests/test_requests.py +0 -0
  38. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/tests/test_streaming.py +0 -0
  39. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/tests/test_transport.py +0 -0
  40. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/tests/test_urls.py +0 -0
  41. {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/tests/test_version_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boxd
3
- Version: 0.2.10.dev615
3
+ Version: 0.2.10.dev616
4
4
  Summary: Python SDK for the boxd cloud VM platform
5
5
  Author: Azin
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "boxd"
3
- version = "0.2.10.dev615"
3
+ version = "0.2.10.dev616"
4
4
  description = "Python SDK for the boxd cloud VM platform"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -172,10 +172,17 @@ class Machine(BaseModel):
172
172
 
173
173
 
174
174
  class ExecResult(BaseModel):
175
- """A one-shot `machines.exec`."""
175
+ """A one-shot `machines.exec`.
176
176
 
177
- stdout: str
178
- stderr: str
177
+ `stdout`/`stderr` are `str`, UTF-8-decoded, unless `encoding="buffer"`
178
+ was passed to `exec`, in which case they're the raw `bytes` instead. The
179
+ default decode is lossy — any invalid byte sequence is replaced with
180
+ U+FFFD — so binary output (or output you need byte-exact) should use
181
+ `encoding="buffer"` or `stream_exec`, which is always byte-exact.
182
+ """
183
+
184
+ stdout: str | bytes
185
+ stderr: str | bytes
179
186
  exit_code: int
180
187
 
181
188
  @property
@@ -158,7 +158,26 @@ def _exec_timeout(timeout: float | None):
158
158
  raise
159
159
 
160
160
 
161
- def _collect_exec(chunks) -> ExecResult:
161
+ def _finish_exec(
162
+ stdout: bytes, stderr: bytes, exit_code: int, encoding: str
163
+ ) -> ExecResult:
164
+ """Build the final `ExecResult`, honoring `encoding`.
165
+
166
+ `"buffer"` returns the raw bytes untouched — byte-exact, same as
167
+ `stream_exec`. Anything else (default `"utf-8"`) decodes lossily: any
168
+ invalid byte sequence is replaced with U+FFFD, so binary output should
169
+ use `"buffer"` instead (see `ExecResult`).
170
+ """
171
+ if encoding == "buffer":
172
+ return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
173
+ return ExecResult(
174
+ stdout=stdout.decode("utf-8", errors="replace"),
175
+ stderr=stderr.decode("utf-8", errors="replace"),
176
+ exit_code=exit_code,
177
+ )
178
+
179
+
180
+ def _collect_exec(chunks, encoding: str = "utf-8") -> ExecResult:
162
181
  """Fold an `Exec` response stream into a one-shot result."""
163
182
  stdout: list[bytes] = []
164
183
  stderr: list[bytes] = []
@@ -167,11 +186,7 @@ def _collect_exec(chunks) -> ExecResult:
167
186
  if chunk.data:
168
187
  (stderr if chunk.is_stderr else stdout).append(chunk.data)
169
188
  exit_code = chunk.exit_code
170
- return ExecResult(
171
- stdout=b"".join(stdout).decode("utf-8", errors="replace"),
172
- stderr=b"".join(stderr).decode("utf-8", errors="replace"),
173
- exit_code=exit_code,
174
- )
189
+ return _finish_exec(b"".join(stdout), b"".join(stderr), exit_code, encoding)
175
190
 
176
191
 
177
192
  def _ready_check(machine: Machine, name: str) -> bool:
@@ -485,6 +500,7 @@ class Machines(Namespace):
485
500
  cols: int = 0,
486
501
  rows: int = 0,
487
502
  timeout: float | None = None,
503
+ encoding: Literal["utf-8", "buffer"] = "utf-8",
488
504
  ) -> ExecResult:
489
505
  """Run one command and collect its output.
490
506
 
@@ -495,6 +511,11 @@ class Machines(Namespace):
495
511
  fails fast (the remote `cd` errors and the command never runs)
496
512
  instead of silently running in the default directory.
497
513
 
514
+ `stdout`/`stderr` are UTF-8-decoded by default, which silently
515
+ replaces any invalid byte sequence with U+FFFD — pass
516
+ `encoding="buffer"` for byte-exact `bytes` output instead (e.g.
517
+ `head -c 4096 /dev/urandom`).
518
+
498
519
  `timeout` is in seconds and gives up on the call; the server kills
499
520
  the remote process's whole process group (SIGTERM, then SIGKILL
500
521
  after a short grace period if it hasn't exited) rather than leaving
@@ -505,7 +526,8 @@ class Machines(Namespace):
505
526
  )
506
527
  with _exec_timeout(timeout):
507
528
  return _collect_exec(
508
- self._transport.bidi("Exec", iter([init]), timeout=timeout)
529
+ self._transport.bidi("Exec", iter([init]), timeout=timeout),
530
+ encoding=encoding,
509
531
  )
510
532
 
511
533
  def stream_exec(
@@ -1119,6 +1141,7 @@ class AsyncMachines(Namespace):
1119
1141
  cols: int = 0,
1120
1142
  rows: int = 0,
1121
1143
  timeout: float | None = None,
1144
+ encoding: Literal["utf-8", "buffer"] = "utf-8",
1122
1145
  ) -> ExecResult:
1123
1146
  """Run one command and collect its output.
1124
1147
 
@@ -1129,6 +1152,11 @@ class AsyncMachines(Namespace):
1129
1152
  fails fast (the remote `cd` errors and the command never runs)
1130
1153
  instead of silently running in the default directory.
1131
1154
 
1155
+ `stdout`/`stderr` are UTF-8-decoded by default, which silently
1156
+ replaces any invalid byte sequence with U+FFFD — pass
1157
+ `encoding="buffer"` for byte-exact `bytes` output instead (e.g.
1158
+ `head -c 4096 /dev/urandom`).
1159
+
1132
1160
  `timeout` is in seconds and gives up on the call; the server kills
1133
1161
  the remote process's whole process group (SIGTERM, then SIGKILL
1134
1162
  after a short grace period if it hasn't exited) rather than leaving
@@ -1150,11 +1178,7 @@ class AsyncMachines(Namespace):
1150
1178
  if chunk.data:
1151
1179
  (stderr if chunk.is_stderr else stdout).append(chunk.data)
1152
1180
  exit_code = chunk.exit_code
1153
- return ExecResult(
1154
- stdout=b"".join(stdout).decode("utf-8", errors="replace"),
1155
- stderr=b"".join(stderr).decode("utf-8", errors="replace"),
1156
- exit_code=exit_code,
1157
- )
1181
+ return _finish_exec(b"".join(stdout), b"".join(stderr), exit_code, encoding)
1158
1182
 
1159
1183
  def stream_exec(
1160
1184
  self,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boxd
3
- Version: 0.2.10.dev615
3
+ Version: 0.2.10.dev616
4
4
  Summary: Python SDK for the boxd cloud VM platform
5
5
  Author: Azin
6
6
  License-Expression: MIT
@@ -186,6 +186,24 @@ def test_exec_folds_the_stream_into_one_result():
186
186
  assert transport.sent[0].vm_id == "alpha"
187
187
 
188
188
 
189
+ def test_exec_default_encoding_corrupts_invalid_utf8():
190
+ # BOX-280: the default string mode is lossy — invalid bytes become
191
+ # U+FFFD and the byte length changes. Documents the failure this issue
192
+ # is about, alongside the encoding="buffer" escape hatch below.
193
+ transport = FakeTransport([[api_pb2.ExecChunk(data=b"\x00\x01\xff\xfe", exit_code=0)]])
194
+ result = Machines(transport).exec("alpha", "printf '\\000\\001\\377\\376'")
195
+ assert result.stdout == "\x00\x01��"
196
+ assert isinstance(result.stdout, str)
197
+
198
+
199
+ def test_exec_buffer_encoding_is_byte_exact():
200
+ raw = bytes(range(256))
201
+ transport = FakeTransport([[api_pb2.ExecChunk(data=raw, exit_code=0)]])
202
+ result = Machines(transport).exec("alpha", "cat", encoding="buffer")
203
+ assert result.stdout == raw
204
+ assert isinstance(result.stdout, bytes)
205
+
206
+
189
207
  def test_exec_prefixes_env_assignments():
190
208
  transport = FakeTransport([[api_pb2.ExecChunk(exit_code=0)]])
191
209
  Machines(transport).exec("alpha", "env", env={"MODE": "a b"})
@@ -672,6 +690,14 @@ async def test_async_exec_folds_the_stream():
672
690
  assert result.exit_code == 0
673
691
 
674
692
 
693
+ async def test_async_exec_buffer_encoding_is_byte_exact():
694
+ raw = bytes(range(256))
695
+ transport = FakeAsyncTransport([[api_pb2.ExecChunk(data=raw, exit_code=0)]])
696
+ result = await AsyncMachines(transport).exec("alpha", "cat", encoding="buffer")
697
+ assert result.stdout == raw
698
+ assert isinstance(result.stdout, bytes)
699
+
700
+
675
701
  async def test_async_list_returns_a_plain_list():
676
702
  transport = FakeAsyncTransport(
677
703
  [api_pb2.ListVmsResponse(vms=[a_vm("alpha"), a_vm("beta")])]
File without changes
File without changes
File without changes