boxd 0.2.10__tar.gz → 0.2.10.dev615__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/src/boxd.egg-info → boxd-0.2.10.dev615}/PKG-INFO +31 -3
  2. {boxd-0.2.10 → boxd-0.2.10.dev615}/README.md +30 -2
  3. {boxd-0.2.10 → boxd-0.2.10.dev615}/pyproject.toml +1 -1
  4. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/resources/machines.py +23 -4
  5. {boxd-0.2.10 → boxd-0.2.10.dev615/src/boxd.egg-info}/PKG-INFO +31 -3
  6. {boxd-0.2.10 → boxd-0.2.10.dev615}/tests/test_namespaces.py +6 -0
  7. {boxd-0.2.10 → boxd-0.2.10.dev615}/LICENSE +0 -0
  8. {boxd-0.2.10 → boxd-0.2.10.dev615}/setup.cfg +0 -0
  9. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/__init__.py +0 -0
  10. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_client.py +0 -0
  11. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_credentials.py +0 -0
  12. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_generated/__init__.py +0 -0
  13. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_generated/api_pb2.py +0 -0
  14. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_generated/api_pb2_grpc.py +0 -0
  15. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_mappers.py +0 -0
  16. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_requests.py +0 -0
  17. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_streaming.py +0 -0
  18. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_transport.py +0 -0
  19. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_urls.py +0 -0
  20. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_version_check.py +0 -0
  21. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/errors.py +0 -0
  22. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/models.py +0 -0
  23. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/resources/__init__.py +0 -0
  24. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/resources/account.py +0 -0
  25. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/resources/credentials.py +0 -0
  26. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/resources/disks.py +0 -0
  27. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/resources/domains.py +0 -0
  28. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/resources/orgs.py +0 -0
  29. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/resources/snapshots.py +0 -0
  30. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/resources/vars.py +0 -0
  31. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd.egg-info/SOURCES.txt +0 -0
  32. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd.egg-info/dependency_links.txt +0 -0
  33. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd.egg-info/requires.txt +0 -0
  34. {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd.egg-info/top_level.txt +0 -0
  35. {boxd-0.2.10 → boxd-0.2.10.dev615}/tests/test_credentials.py +0 -0
  36. {boxd-0.2.10 → boxd-0.2.10.dev615}/tests/test_mappers.py +0 -0
  37. {boxd-0.2.10 → boxd-0.2.10.dev615}/tests/test_requests.py +0 -0
  38. {boxd-0.2.10 → boxd-0.2.10.dev615}/tests/test_streaming.py +0 -0
  39. {boxd-0.2.10 → boxd-0.2.10.dev615}/tests/test_transport.py +0 -0
  40. {boxd-0.2.10 → boxd-0.2.10.dev615}/tests/test_urls.py +0 -0
  41. {boxd-0.2.10 → boxd-0.2.10.dev615}/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
3
+ Version: 0.2.10.dev615
4
4
  Summary: Python SDK for the boxd cloud VM platform
5
5
  Author: Azin
6
6
  License-Expression: MIT
@@ -358,6 +358,7 @@ result.success # bool
358
358
 
359
359
  boxd.machines.exec(id, ["echo", "a b"]) # a list is quoted for you
360
360
  boxd.machines.exec(id, "env", env={"FOO": "bar"})
361
+ boxd.machines.exec(id, "make", cwd="/srv/app") # run from a working directory
361
362
  boxd.machines.exec(id, "cargo build", timeout=30) # seconds
362
363
 
363
364
  # Under a PTY, stderr merges into stdout and `stderr` comes back empty.
@@ -365,9 +366,22 @@ boxd.machines.exec(id, "top -b -n1", tty=True, cols=120, rows=40)
365
366
  ```
366
367
 
367
368
  `command` takes a list of argv — shell-quoted for you — or a ready-made command
368
- line as a string. `timeout` gives up on the call; whatever it started inside the
369
+ line as a string. `cwd` runs the command from that directory; a `cwd` that
370
+ doesn't exist fails fast instead of silently running in the default
371
+ directory. `timeout` gives up on the call; whatever it started inside the
369
372
  machine may well still be running.
370
373
 
374
+ **`result.stdout`/`result.stderr` are UTF-8-decoded with `errors="replace"`:**
375
+ any byte sequence that isn't valid UTF-8 is silently replaced with `U+FFFD`,
376
+ changing the length with no error raised. Fine for text output; corrupts
377
+ binary output. For binary or byte-exact output, read from `stream_exec`
378
+ instead — its chunks are raw `bytes`, never decoded:
379
+
380
+ ```python
381
+ with boxd.machines.stream_exec(id, command="cat file.bin") as stream:
382
+ data = b"".join(stream) # byte-exact
383
+ ```
384
+
371
385
  For anything interactive, `stream_exec` gives you a live session — the one
372
386
  handle in the SDK, because a bidirectional stream really is stateful:
373
387
 
@@ -420,13 +434,17 @@ from pathlib import Path
420
434
 
421
435
  written = boxd.machines.files.upload(id, "/app/config.json", '{"debug": true}')
422
436
  boxd.machines.files.upload(id, "/app/data.bin", Path("local.bin").read_bytes())
437
+ boxd.machines.files.upload(id, "notes.txt", "relative to /home/boxd")
423
438
  data = boxd.machines.files.download(id, "/app/output.json") # bytes
424
439
  ```
425
440
 
426
441
  `upload` takes `str` or `bytes`, streams it in chunks so large files are fine,
427
442
  and returns the number of bytes the machine confirmed it wrote. `download`
428
443
  streams too and returns the whole file as `bytes` — there is no size cap, but
429
- it is all held in memory.
444
+ it is all held in memory. Paths are either absolute (`/app/config.json`) or
445
+ relative to the VM user's home directory (`/home/boxd`) — a bare filename
446
+ like `"notes.txt"` resolves to `/home/boxd/notes.txt`, same as `"./notes.txt"`
447
+ or `"sub/notes.txt"` would.
430
448
 
431
449
  ### Ports and proxies
432
450
 
@@ -468,6 +486,16 @@ with one. `create` answers as soon as the route is accepted, so it confirms the
468
486
  subdomain and the port it was pointed at; `list()` reports the full domain and
469
487
  the resolved port.
470
488
 
489
+ `port_mode="auto"` detection runs continuously for the whole life of the
490
+ machine, not just once at boot: whenever the app binds one of a curated list
491
+ of common ports (80, 443, 8080, 8000, 3000, 5000, 5173), that becomes a
492
+ candidate, and it's adopted once roughly 750ms have passed with no *different*
493
+ candidate port seen (an already-adopted port won't be replaced again more
494
+ than once every 5s). So a redeploy or restart that moves your app onto a new
495
+ port is picked up on its own within a few seconds — no action needed. If your
496
+ app doesn't listen on one of those ports, or you don't want to wait out the
497
+ detection window, call `set_port` with the port explicitly instead of `"auto"`.
498
+
471
499
  ### Checkpoints
472
500
 
473
501
  Per-machine captures, restored in place. They are deleted with the machine.
@@ -326,6 +326,7 @@ result.success # bool
326
326
 
327
327
  boxd.machines.exec(id, ["echo", "a b"]) # a list is quoted for you
328
328
  boxd.machines.exec(id, "env", env={"FOO": "bar"})
329
+ boxd.machines.exec(id, "make", cwd="/srv/app") # run from a working directory
329
330
  boxd.machines.exec(id, "cargo build", timeout=30) # seconds
330
331
 
331
332
  # Under a PTY, stderr merges into stdout and `stderr` comes back empty.
@@ -333,9 +334,22 @@ boxd.machines.exec(id, "top -b -n1", tty=True, cols=120, rows=40)
333
334
  ```
334
335
 
335
336
  `command` takes a list of argv — shell-quoted for you — or a ready-made command
336
- line as a string. `timeout` gives up on the call; whatever it started inside the
337
+ line as a string. `cwd` runs the command from that directory; a `cwd` that
338
+ doesn't exist fails fast instead of silently running in the default
339
+ directory. `timeout` gives up on the call; whatever it started inside the
337
340
  machine may well still be running.
338
341
 
342
+ **`result.stdout`/`result.stderr` are UTF-8-decoded with `errors="replace"`:**
343
+ any byte sequence that isn't valid UTF-8 is silently replaced with `U+FFFD`,
344
+ changing the length with no error raised. Fine for text output; corrupts
345
+ binary output. For binary or byte-exact output, read from `stream_exec`
346
+ instead — its chunks are raw `bytes`, never decoded:
347
+
348
+ ```python
349
+ with boxd.machines.stream_exec(id, command="cat file.bin") as stream:
350
+ data = b"".join(stream) # byte-exact
351
+ ```
352
+
339
353
  For anything interactive, `stream_exec` gives you a live session — the one
340
354
  handle in the SDK, because a bidirectional stream really is stateful:
341
355
 
@@ -388,13 +402,17 @@ from pathlib import Path
388
402
 
389
403
  written = boxd.machines.files.upload(id, "/app/config.json", '{"debug": true}')
390
404
  boxd.machines.files.upload(id, "/app/data.bin", Path("local.bin").read_bytes())
405
+ boxd.machines.files.upload(id, "notes.txt", "relative to /home/boxd")
391
406
  data = boxd.machines.files.download(id, "/app/output.json") # bytes
392
407
  ```
393
408
 
394
409
  `upload` takes `str` or `bytes`, streams it in chunks so large files are fine,
395
410
  and returns the number of bytes the machine confirmed it wrote. `download`
396
411
  streams too and returns the whole file as `bytes` — there is no size cap, but
397
- it is all held in memory.
412
+ it is all held in memory. Paths are either absolute (`/app/config.json`) or
413
+ relative to the VM user's home directory (`/home/boxd`) — a bare filename
414
+ like `"notes.txt"` resolves to `/home/boxd/notes.txt`, same as `"./notes.txt"`
415
+ or `"sub/notes.txt"` would.
398
416
 
399
417
  ### Ports and proxies
400
418
 
@@ -436,6 +454,16 @@ with one. `create` answers as soon as the route is accepted, so it confirms the
436
454
  subdomain and the port it was pointed at; `list()` reports the full domain and
437
455
  the resolved port.
438
456
 
457
+ `port_mode="auto"` detection runs continuously for the whole life of the
458
+ machine, not just once at boot: whenever the app binds one of a curated list
459
+ of common ports (80, 443, 8080, 8000, 3000, 5000, 5173), that becomes a
460
+ candidate, and it's adopted once roughly 750ms have passed with no *different*
461
+ candidate port seen (an already-adopted port won't be replaced again more
462
+ than once every 5s). So a redeploy or restart that moves your app onto a new
463
+ port is picked up on its own within a few seconds — no action needed. If your
464
+ app doesn't listen on one of those ports, or you don't want to wait out the
465
+ detection window, call `set_port` with the port explicitly instead of `"auto"`.
466
+
439
467
  ### Checkpoints
440
468
 
441
469
  Per-machine captures, restored in place. They are deleted with the machine.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "boxd"
3
- version = "0.2.10"
3
+ version = "0.2.10.dev615"
4
4
  description = "Python SDK for the boxd cloud VM platform"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -119,6 +119,7 @@ def _exec_init(
119
119
  tty: bool,
120
120
  cols: int,
121
121
  rows: int,
122
+ cwd: str | None = None,
122
123
  ) -> api_pb2.ExecChunk:
123
124
  """The first chunk of an `Exec` stream: command, PTY flags, geometry."""
124
125
  line = req.join_command(command)
@@ -127,6 +128,12 @@ def _exec_init(
127
128
  # shell, so prefix assignments do the job.
128
129
  prefix = " ".join(f"{k}={shlex.quote(v)}" for k, v in env.items())
129
130
  line = f"{prefix} {line}"
131
+ if cwd:
132
+ # Same reasoning as `env`: no cwd field on ExecChunk, so `cd` ahead of
133
+ # the command through the same shell. `&&` means a `cwd` that doesn't
134
+ # exist fails fast with `cd`'s own error instead of silently running
135
+ # in the default directory (BOX-292).
136
+ line = f"cd {shlex.quote(cwd)} && {line}"
130
137
  return req.exec_chunk(
131
138
  machine_id=machine_id, command=line, tty=tty, cols=cols, rows=rows
132
139
  )
@@ -473,6 +480,7 @@ class Machines(Namespace):
473
480
  command: str | Sequence[str],
474
481
  *,
475
482
  env: Mapping[str, str] | None = None,
483
+ cwd: str | None = None,
476
484
  tty: bool = False,
477
485
  cols: int = 0,
478
486
  rows: int = 0,
@@ -483,13 +491,17 @@ class Machines(Namespace):
483
491
  With `tty` the command runs under a PTY, which merges stderr into
484
492
  stdout — `stderr` comes back empty and everything lands in `stdout`.
485
493
 
494
+ `cwd` runs the command in that directory; a `cwd` that doesn't exist
495
+ fails fast (the remote `cd` errors and the command never runs)
496
+ instead of silently running in the default directory.
497
+
486
498
  `timeout` is in seconds and gives up on the call; the server kills
487
499
  the remote process's whole process group (SIGTERM, then SIGKILL
488
500
  after a short grace period if it hasn't exited) rather than leaving
489
501
  it running with no one listening.
490
502
  """
491
503
  init = _exec_init(
492
- machine_id, command, env=env, tty=tty, cols=cols, rows=rows
504
+ machine_id, command, env=env, tty=tty, cols=cols, rows=rows, cwd=cwd
493
505
  )
494
506
  with _exec_timeout(timeout):
495
507
  return _collect_exec(
@@ -502,6 +514,7 @@ class Machines(Namespace):
502
514
  *,
503
515
  command: str | Sequence[str],
504
516
  env: Mapping[str, str] | None = None,
517
+ cwd: str | None = None,
505
518
  tty: bool = False,
506
519
  cols: int = 0,
507
520
  rows: int = 0,
@@ -514,7 +527,7 @@ class Machines(Namespace):
514
527
  "a PTY shell needs stdin open"
515
528
  )
516
529
  init = _exec_init(
517
- machine_id, command, env=env, tty=tty, cols=cols, rows=rows
530
+ machine_id, command, env=env, tty=tty, cols=cols, rows=rows, cwd=cwd
518
531
  )
519
532
  return ExecStream(self._transport, init, close_stdin=close_stdin)
520
533
 
@@ -1101,6 +1114,7 @@ class AsyncMachines(Namespace):
1101
1114
  command: str | Sequence[str],
1102
1115
  *,
1103
1116
  env: Mapping[str, str] | None = None,
1117
+ cwd: str | None = None,
1104
1118
  tty: bool = False,
1105
1119
  cols: int = 0,
1106
1120
  rows: int = 0,
@@ -1111,13 +1125,17 @@ class AsyncMachines(Namespace):
1111
1125
  With `tty` the command runs under a PTY, which merges stderr into
1112
1126
  stdout — `stderr` comes back empty and everything lands in `stdout`.
1113
1127
 
1128
+ `cwd` runs the command in that directory; a `cwd` that doesn't exist
1129
+ fails fast (the remote `cd` errors and the command never runs)
1130
+ instead of silently running in the default directory.
1131
+
1114
1132
  `timeout` is in seconds and gives up on the call; the server kills
1115
1133
  the remote process's whole process group (SIGTERM, then SIGKILL
1116
1134
  after a short grace period if it hasn't exited) rather than leaving
1117
1135
  it running with no one listening.
1118
1136
  """
1119
1137
  init = _exec_init(
1120
- machine_id, command, env=env, tty=tty, cols=cols, rows=rows
1138
+ machine_id, command, env=env, tty=tty, cols=cols, rows=rows, cwd=cwd
1121
1139
  )
1122
1140
 
1123
1141
  async def once():
@@ -1144,6 +1162,7 @@ class AsyncMachines(Namespace):
1144
1162
  *,
1145
1163
  command: str | Sequence[str],
1146
1164
  env: Mapping[str, str] | None = None,
1165
+ cwd: str | None = None,
1147
1166
  tty: bool = False,
1148
1167
  cols: int = 0,
1149
1168
  rows: int = 0,
@@ -1156,7 +1175,7 @@ class AsyncMachines(Namespace):
1156
1175
  "a PTY shell needs stdin open"
1157
1176
  )
1158
1177
  init = _exec_init(
1159
- machine_id, command, env=env, tty=tty, cols=cols, rows=rows
1178
+ machine_id, command, env=env, tty=tty, cols=cols, rows=rows, cwd=cwd
1160
1179
  )
1161
1180
  return AsyncExecStream(self._transport, init, close_stdin=close_stdin)
1162
1181
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boxd
3
- Version: 0.2.10
3
+ Version: 0.2.10.dev615
4
4
  Summary: Python SDK for the boxd cloud VM platform
5
5
  Author: Azin
6
6
  License-Expression: MIT
@@ -358,6 +358,7 @@ result.success # bool
358
358
 
359
359
  boxd.machines.exec(id, ["echo", "a b"]) # a list is quoted for you
360
360
  boxd.machines.exec(id, "env", env={"FOO": "bar"})
361
+ boxd.machines.exec(id, "make", cwd="/srv/app") # run from a working directory
361
362
  boxd.machines.exec(id, "cargo build", timeout=30) # seconds
362
363
 
363
364
  # Under a PTY, stderr merges into stdout and `stderr` comes back empty.
@@ -365,9 +366,22 @@ boxd.machines.exec(id, "top -b -n1", tty=True, cols=120, rows=40)
365
366
  ```
366
367
 
367
368
  `command` takes a list of argv — shell-quoted for you — or a ready-made command
368
- line as a string. `timeout` gives up on the call; whatever it started inside the
369
+ line as a string. `cwd` runs the command from that directory; a `cwd` that
370
+ doesn't exist fails fast instead of silently running in the default
371
+ directory. `timeout` gives up on the call; whatever it started inside the
369
372
  machine may well still be running.
370
373
 
374
+ **`result.stdout`/`result.stderr` are UTF-8-decoded with `errors="replace"`:**
375
+ any byte sequence that isn't valid UTF-8 is silently replaced with `U+FFFD`,
376
+ changing the length with no error raised. Fine for text output; corrupts
377
+ binary output. For binary or byte-exact output, read from `stream_exec`
378
+ instead — its chunks are raw `bytes`, never decoded:
379
+
380
+ ```python
381
+ with boxd.machines.stream_exec(id, command="cat file.bin") as stream:
382
+ data = b"".join(stream) # byte-exact
383
+ ```
384
+
371
385
  For anything interactive, `stream_exec` gives you a live session — the one
372
386
  handle in the SDK, because a bidirectional stream really is stateful:
373
387
 
@@ -420,13 +434,17 @@ from pathlib import Path
420
434
 
421
435
  written = boxd.machines.files.upload(id, "/app/config.json", '{"debug": true}')
422
436
  boxd.machines.files.upload(id, "/app/data.bin", Path("local.bin").read_bytes())
437
+ boxd.machines.files.upload(id, "notes.txt", "relative to /home/boxd")
423
438
  data = boxd.machines.files.download(id, "/app/output.json") # bytes
424
439
  ```
425
440
 
426
441
  `upload` takes `str` or `bytes`, streams it in chunks so large files are fine,
427
442
  and returns the number of bytes the machine confirmed it wrote. `download`
428
443
  streams too and returns the whole file as `bytes` — there is no size cap, but
429
- it is all held in memory.
444
+ it is all held in memory. Paths are either absolute (`/app/config.json`) or
445
+ relative to the VM user's home directory (`/home/boxd`) — a bare filename
446
+ like `"notes.txt"` resolves to `/home/boxd/notes.txt`, same as `"./notes.txt"`
447
+ or `"sub/notes.txt"` would.
430
448
 
431
449
  ### Ports and proxies
432
450
 
@@ -468,6 +486,16 @@ with one. `create` answers as soon as the route is accepted, so it confirms the
468
486
  subdomain and the port it was pointed at; `list()` reports the full domain and
469
487
  the resolved port.
470
488
 
489
+ `port_mode="auto"` detection runs continuously for the whole life of the
490
+ machine, not just once at boot: whenever the app binds one of a curated list
491
+ of common ports (80, 443, 8080, 8000, 3000, 5000, 5173), that becomes a
492
+ candidate, and it's adopted once roughly 750ms have passed with no *different*
493
+ candidate port seen (an already-adopted port won't be replaced again more
494
+ than once every 5s). So a redeploy or restart that moves your app onto a new
495
+ port is picked up on its own within a few seconds — no action needed. If your
496
+ app doesn't listen on one of those ports, or you don't want to wait out the
497
+ detection window, call `set_port` with the port explicitly instead of `"auto"`.
498
+
471
499
  ### Checkpoints
472
500
 
473
501
  Per-machine captures, restored in place. They are deleted with the machine.
@@ -192,6 +192,12 @@ def test_exec_prefixes_env_assignments():
192
192
  assert transport.sent[0].command == "MODE='a b' env"
193
193
 
194
194
 
195
+ def test_exec_cwd_prepends_a_cd():
196
+ transport = FakeTransport([[api_pb2.ExecChunk(exit_code=0)]])
197
+ Machines(transport).exec("alpha", "pwd", cwd="/opt/my app")
198
+ assert transport.sent[0].command == "cd '/opt/my app' && pwd"
199
+
200
+
195
201
  def test_exec_reports_a_nonzero_exit():
196
202
  transport = FakeTransport([[api_pb2.ExecChunk(exit_code=3)]])
197
203
  result = Machines(transport).exec("alpha", "false")
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