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.
- {boxd-0.2.10/src/boxd.egg-info → boxd-0.2.10.dev615}/PKG-INFO +31 -3
- {boxd-0.2.10 → boxd-0.2.10.dev615}/README.md +30 -2
- {boxd-0.2.10 → boxd-0.2.10.dev615}/pyproject.toml +1 -1
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/resources/machines.py +23 -4
- {boxd-0.2.10 → boxd-0.2.10.dev615/src/boxd.egg-info}/PKG-INFO +31 -3
- {boxd-0.2.10 → boxd-0.2.10.dev615}/tests/test_namespaces.py +6 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/LICENSE +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/setup.cfg +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/__init__.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_client.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_credentials.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_generated/__init__.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_generated/api_pb2.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_generated/api_pb2_grpc.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_mappers.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_requests.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_streaming.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_transport.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_urls.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/_version_check.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/errors.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/models.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/resources/__init__.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/resources/account.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/resources/credentials.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/resources/disks.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/resources/domains.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/resources/orgs.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/resources/snapshots.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd/resources/vars.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd.egg-info/SOURCES.txt +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd.egg-info/dependency_links.txt +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd.egg-info/requires.txt +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/src/boxd.egg-info/top_level.txt +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/tests/test_credentials.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/tests/test_mappers.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/tests/test_requests.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/tests/test_streaming.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/tests/test_transport.py +0 -0
- {boxd-0.2.10 → boxd-0.2.10.dev615}/tests/test_urls.py +0 -0
- {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. `
|
|
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. `
|
|
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.
|
|
@@ -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. `
|
|
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
|
|
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
|