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.
- {boxd-0.2.10.dev615/src/boxd.egg-info → boxd-0.2.10.dev616}/PKG-INFO +1 -1
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/pyproject.toml +1 -1
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/models.py +10 -3
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/resources/machines.py +36 -12
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616/src/boxd.egg-info}/PKG-INFO +1 -1
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/tests/test_namespaces.py +26 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/LICENSE +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/README.md +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/setup.cfg +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/__init__.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_client.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_credentials.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_generated/__init__.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_generated/api_pb2.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_generated/api_pb2_grpc.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_mappers.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_requests.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_streaming.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_transport.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_urls.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/_version_check.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/errors.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/resources/__init__.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/resources/account.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/resources/credentials.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/resources/disks.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/resources/domains.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/resources/orgs.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/resources/snapshots.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd/resources/vars.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd.egg-info/SOURCES.txt +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd.egg-info/dependency_links.txt +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd.egg-info/requires.txt +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/src/boxd.egg-info/top_level.txt +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/tests/test_credentials.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/tests/test_mappers.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/tests/test_requests.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/tests/test_streaming.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/tests/test_transport.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/tests/test_urls.py +0 -0
- {boxd-0.2.10.dev615 → boxd-0.2.10.dev616}/tests/test_version_check.py +0 -0
|
@@ -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
|
|
178
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|