Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,11 @@ Unknown response fields are kept in `.raw`.

Omitted arguments are not sent. `update(ttl_seconds=None)` sends JSON `null` to
turn auto-stop off; leave it out (the default, `usedock.UNSET`) to keep the
current value. `resume` does not take this: the API ignores `ttl_seconds` on
resume today. `exec`, like `create` and `branch`, accepts `idempotency_key=`.
current value. `resume(ttl_seconds=...)` works the same way: a number sets
auto-stop (counted from ready), `None` clears it, and omitting it keeps the
current value. `files.read` and `files.list` raise `file_too_large` (before
sending) when the path makes the command exceed the 64 KB exec limit.
`exec`, like `create` and `branch`, accepts `idempotency_key=`.

The API has no file routes, so `files.*` run one `exec` call: content travels
as base64, paths are single-quoted, and the command must fit the API's 64 KB
Expand Down
36 changes: 26 additions & 10 deletions src/usedock/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,21 +242,23 @@ def resume(
cpus: Optional[int] = None,
memory_mib: Optional[int] = None,
disk_gib: Optional[int] = None,
ttl_seconds: Optional[int] = None,
ttl_seconds: Optional[int] = UNSET,
environment: Optional[str] = None,
no_env: Optional[bool] = None,
) -> DockState:
data = self._c._request(
"POST",
f"/v1/docks/{_seg(dock_id)}/resume",
body=_body(
cpus=cpus,
memoryMiB=memory_mib,
diskGiB=disk_gib,
ttlSeconds=ttl_seconds,
environment=environment,
noEnv=no_env,
),
body={
**_body(
cpus=cpus,
memoryMiB=memory_mib,
diskGiB=disk_gib,
environment=environment,
noEnv=no_env,
),
**({} if ttl_seconds is UNSET else {"ttlSeconds": ttl_seconds}),
},
)
return DockState.from_dict(data)

Expand Down Expand Up @@ -321,6 +323,17 @@ def _run(self, dock_id: str, command: str, what: str) -> ExecResult:
)
return result

@staticmethod
def _check_size(command: str, what: str) -> None:
size = len(command.encode("utf-8"))
if size > MAX_COMMAND_BYTES:
raise DockError(
"file_too_large",
0,
f"{what}: the exec command would be {size} bytes; the limit is "
f"{MAX_COMMAND_BYTES} (the path is too long)",
)

def write(self, dock_id: str, path: str, content: Union[str, bytes]) -> str:
raw = content.encode("utf-8") if isinstance(content, str) else content
if len(raw) > MAX_WRITE_BYTES:
Expand Down Expand Up @@ -349,7 +362,9 @@ def write(self, dock_id: str, path: str, content: Union[str, bytes]) -> str:
return path

def read_bytes(self, dock_id: str, path: str) -> bytes:
result = self._run(dock_id, f"base64 -w0 -- {shell_quote(path)}", f"read {path}")
command = f"base64 -w0 -- {shell_quote(path)}"
self._check_size(command, f"read {path}")
result = self._run(dock_id, command, f"read {path}")
return base64.b64decode(result.stdout)

def read(self, dock_id: str, path: str) -> str:
Expand All @@ -367,6 +382,7 @@ def list(self, dock_id: str, path: str) -> List[str]:
'find -H "$p" -mindepth 1 -maxdepth 1 -exec sh -c '
'\'for f; do printf "%s" "${f##*/}" | base64 -w0; echo; done\' sh {} +'
)
self._check_size(command, f"list {path}")
result = self._run(dock_id, command, f"list {path}")
return [
base64.b64decode(line).decode("utf-8", errors="replace")
Expand Down
20 changes: 20 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,3 +621,23 @@ def test_other_codes_stay_plain_dock_error() -> None:
with pytest.raises(DockError) as info:
client.docks.get("d")
assert type(info.value) is DockError


def test_resume_ttl_unset_null_and_number() -> None:
client, seen = recorder(ok(DOCK, 202))
client.docks.resume("d1")
client.docks.resume("d1", ttl_seconds=None)
client.docks.resume("d1", ttl_seconds=600, cpus=2)
assert body_of(seen[0]) == {}
assert body_of(seen[1]) == {"ttlSeconds": None}
assert body_of(seen[2]) == {"cpus": 2, "ttlSeconds": 600}


@pytest.mark.parametrize("op", ["read", "read_bytes", "list"])
def test_files_read_list_over_command_limit_raise_before_sending(op: str) -> None:
client, seen = recorder(exec_ok())
path = "/w/" + "茅" * 40000 # multibyte: UTF-8 bytes, not characters, are counted
with pytest.raises(DockError) as info:
getattr(client.docks.files, op)("d1", path)
assert (info.value.code, info.value.status) == ("file_too_large", 0)
assert len(seen) == 0
Loading