letify 1.2.2__tar.gz → 1.2.3__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.
- {letify-1.2.2 → letify-1.2.3}/PKG-INFO +1 -1
- {letify-1.2.2 → letify-1.2.3}/letify/__init__.py +1 -1
- {letify-1.2.2 → letify-1.2.3}/letify/store/volume.py +6 -2
- {letify-1.2.2 → letify-1.2.3}/letify/transport/nat.py +42 -10
- {letify-1.2.2 → letify-1.2.3}/letify-core/Cargo.lock +3 -3
- {letify-1.2.2 → letify-1.2.3}/letify-core/Cargo.toml +1 -1
- {letify-1.2.2 → letify-1.2.3}/letify-ext/package-lock.json +2 -2
- {letify-1.2.2 → letify-1.2.3}/letify-ext/package.json +1 -1
- {letify-1.2.2 → letify-1.2.3}/pyproject.toml +1 -1
- {letify-1.2.2 → letify-1.2.3}/tests/conftest.py +7 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_paths.py +35 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_strategies.py +37 -0
- {letify-1.2.2 → letify-1.2.3}/.github/workflows/ci.yml +0 -0
- {letify-1.2.2 → letify-1.2.3}/.github/workflows/docs.yml +0 -0
- {letify-1.2.2 → letify-1.2.3}/.github/workflows/publish.yml +0 -0
- {letify-1.2.2 → letify-1.2.3}/.gitignore +0 -0
- {letify-1.2.2 → letify-1.2.3}/.researchtree +0 -0
- {letify-1.2.2 → letify-1.2.3}/LICENSE +0 -0
- {letify-1.2.2 → letify-1.2.3}/README.md +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/guide/01-getting-started.md +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/guide/02-providers.md +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/guide/03-execution-modes.md +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/guide/04-environments-and-data.md +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/guide/05-concurrency.md +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/guide/06-cost.md +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/guide/07-troubleshooting.md +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/guide/08-releasing.md +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/guide/README.md +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/guide/ko/02-/355/224/204/353/241/234/353/260/224/354/235/264/353/215/224.md" +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/guide/ko/04-/355/231/230/352/262/275/352/263/274-/353/215/260/354/235/264/355/204/260.md" +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/guide/ko/README.md +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/locales/README_ko.md +0 -0
- {letify-1.2.2 → letify-1.2.3}/docs/mkdocs.yml +0 -0
- {letify-1.2.2 → letify-1.2.3}/examples/README.md +0 -0
- {letify-1.2.2 → letify-1.2.3}/examples/nvfp4-lora/.letify.example +0 -0
- {letify-1.2.2 → letify-1.2.3}/examples/nvfp4-lora/00_smoke.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/examples/nvfp4-lora/01_sweep.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/examples/nvfp4-lora/02_resume.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/examples/nvfp4-lora/03_watch.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/examples/nvfp4-lora/README.md +0 -0
- {letify-1.2.2 → letify-1.2.3}/examples/nvfp4-lora/README_ko.md +0 -0
- {letify-1.2.2 → letify-1.2.3}/examples/nvfp4-lora/common.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/examples/nvfp4-lora/recipe.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/cli.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/config/__init__.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/config/inventory.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/config/login.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/config/schema.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/config/secrets.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/config/writer.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/declare/__init__.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/declare/cache.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/declare/env.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/declare/function.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/declare/instance.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/errors.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/install.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/launcher.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/paths.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/protocol/__init__.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/protocol/codec.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/protocol/driver.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/protocol/handle.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/protocol/wire.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/protocol/worker.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/providers/__init__.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/providers/base.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/providers/colab.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/providers/colab_files.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/providers/colab_keepalive.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/providers/elice.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/providers/kaggle.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/providers/kaggle_adapter.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/providers/local.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/providers/modal.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/providers/modal_adapter.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/providers/naming.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/providers/shell.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/providers/tunnel.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/providers/usage.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/remoting/__init__.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/remoting/capability.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/remoting/device/__init__.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/remoting/device/client.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/remoting/device/cuda.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/remoting/device/executor.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/remoting/device/frames.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/remoting/device/guard.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/remoting/device/tensor.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/remoting/device/trace.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/remoting/device/value.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/remoting/loader.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/remoting/probe.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/render.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/runtime/__init__.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/runtime/bootstrap.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/runtime/channel.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/runtime/lease.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/runtime/pool.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/runtime/session.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/runtime/telemetry.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/store/__init__.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/store/backends/__init__.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/store/backends/filesystem.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/store/backends/google_auth.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/store/backends/layout.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/store/backends/objects.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/store/cas.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/store/pathdata.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/store/sendorder.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/stubs.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/tools.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/transport/__init__.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/transport/agent.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/transport/announce.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/transport/link.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/transport/pipeline.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/transport/probe.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/transport/rendezvous.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/transport/setup.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/transport/sshopts.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify/transport/strategies.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-core/.gitignore +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-core/build.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-core/letify-agent/Cargo.toml +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-core/letify-agent/src/driver.rs +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-core/letify-agent/src/main.rs +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-core/letify-driver/Cargo.toml +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-core/letify-driver/src/client.rs +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-core/letify-driver/src/exports.rs +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-core/letify-driver/src/image.rs +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-core/letify-driver/src/lib.rs +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-core/letify-driver/src/status.rs +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-core/letify-driver/src/table.rs +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-core/letify-wire/Cargo.toml +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-core/letify-wire/src/lib.rs +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-core/letify-wire/tests/throughput.rs +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-core/rust-toolchain.toml +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-ext/.vscodeignore +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-ext/LICENSE +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-ext/README.md +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-ext/media/icon.svg +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-ext/src/cli.ts +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-ext/src/extension.ts +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-ext/src/history.ts +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-ext/src/model.ts +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-ext/src/poll.ts +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-ext/src/render.ts +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-ext/test/cli.test.ts +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-ext/test/manifest.test.ts +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-ext/test/model.test.ts +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-ext/test/regressions.test.ts +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify-ext/tsconfig.json +0 -0
- {letify-1.2.2 → letify-1.2.3}/letify_providers.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/scripts/sync-main.sh +0 -0
- {letify-1.2.2 → letify-1.2.3}/scripts/version.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/fake_kaggle_adapter.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/fake_modal_adapter.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_cli.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_cli_output.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_colab_keepalive.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_config.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_core.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_data.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_data_streaming.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_declare.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_device.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_editor_sessions.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_elice.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_frames.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_install.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_kaggle.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_kaggle_cookie.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_link_log.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_notebook.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_protocol.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_providers.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_remoting.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_render.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_runtime.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_ship.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_store.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_stubs.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_transport.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_tunnel_setup.py +0 -0
- {letify-1.2.2 → letify-1.2.3}/tests/test_version.py +0 -0
|
@@ -89,14 +89,18 @@ def environment_volume(provider: Provider) -> Volume:
|
|
|
89
89
|
"""The automatic environment store when an ephemeral declaration names no volumes.
|
|
90
90
|
|
|
91
91
|
Spec "Materializing into a runtime": reuse the account's data bucket, or keep the
|
|
92
|
-
archive on the client without requiring a bucket or a volume declaration.
|
|
92
|
+
archive on the client without requiring a bucket or a volume declaration. The client
|
|
93
|
+
store is the declared ``~/.letify/cache/storage/<name>`` root with the name
|
|
94
|
+
``environments``, so it stays under the single local root while still reading archives
|
|
95
|
+
an earlier install left under ``~/.cache/letify/environments``.
|
|
93
96
|
"""
|
|
97
|
+
from ..paths import storage_cache_directory
|
|
94
98
|
from .backends.filesystem import FilesystemBackend
|
|
95
99
|
from .pathdata import data_bucket
|
|
96
100
|
|
|
97
101
|
backend = data_bucket(provider)
|
|
98
102
|
if backend is None:
|
|
99
|
-
root =
|
|
103
|
+
root = storage_cache_directory("environments") / provider.kind / provider.alias
|
|
100
104
|
backend = FilesystemBackend(root)
|
|
101
105
|
return Volume(provider, "environments", _store=Store(backend))
|
|
102
106
|
|
|
@@ -382,11 +382,10 @@ def begin(request: dict):
|
|
|
382
382
|
stderr=subprocess.STDOUT,
|
|
383
383
|
text=True,
|
|
384
384
|
)
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
raise RuntimeError("tailcat serve exited without printing its address")
|
|
385
|
+
address = _match_then_drain(process, process.stdout, r"address:\s*(tc\S+)")
|
|
386
|
+
if address is None:
|
|
387
|
+
raise RuntimeError("tailcat serve exited without printing its address")
|
|
388
|
+
return {"address": address}, process.wait
|
|
390
389
|
if kind == "reverse_ssh":
|
|
391
390
|
# The directory is under the account's workspace root, which may start with ~.
|
|
392
391
|
key = Path(os.path.expanduser(request["key_directory"])) / "letify_reverse"
|
|
@@ -405,14 +404,47 @@ def begin(request: dict):
|
|
|
405
404
|
stderr=subprocess.PIPE,
|
|
406
405
|
text=True,
|
|
407
406
|
) # fmt: skip
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
raise RuntimeError("ssh -R exited without allocating a port")
|
|
407
|
+
allocated = _match_then_drain(process, process.stderr, r"Allocated port (\d+)")
|
|
408
|
+
if allocated is None:
|
|
409
|
+
raise RuntimeError("ssh -R exited without allocating a port")
|
|
410
|
+
return {"port": int(allocated)}, process.wait
|
|
413
411
|
raise ValueError(f"unknown rendezvous request {kind!r}")
|
|
414
412
|
|
|
415
413
|
|
|
414
|
+
def _match_then_drain(process, stream, pattern: str) -> str | None:
|
|
415
|
+
"""First capture of ``pattern`` in ``stream``, then keep draining it in the background.
|
|
416
|
+
|
|
417
|
+
A helper letify starts and reads one line from keeps writing afterwards. Its pipe holds
|
|
418
|
+
about 64 KiB, so once nobody reads it the helper blocks on its next write and stops
|
|
419
|
+
moving bytes. ``tailcat serve`` carries the SSH connection itself, so that stall shows
|
|
420
|
+
up as a link that connects and then measures far below the link underneath it. Reading
|
|
421
|
+
on and discarding keeps the writer running, and keeps ``process.wait`` from deadlocking
|
|
422
|
+
against a full pipe.
|
|
423
|
+
|
|
424
|
+
Returns None when the stream ends before the pattern appears.
|
|
425
|
+
"""
|
|
426
|
+
expression = re.compile(pattern)
|
|
427
|
+
found = None
|
|
428
|
+
for line in stream:
|
|
429
|
+
match = expression.search(line)
|
|
430
|
+
if match:
|
|
431
|
+
found = match.group(1)
|
|
432
|
+
break
|
|
433
|
+
if found is None:
|
|
434
|
+
return None
|
|
435
|
+
|
|
436
|
+
def drain() -> None:
|
|
437
|
+
try:
|
|
438
|
+
for _ in stream:
|
|
439
|
+
pass
|
|
440
|
+
except (OSError, ValueError):
|
|
441
|
+
# The process ended and the stream was closed under us, which is the normal end.
|
|
442
|
+
pass
|
|
443
|
+
|
|
444
|
+
threading.Thread(target=drain, name="letify-drain", daemon=True).start()
|
|
445
|
+
return found
|
|
446
|
+
|
|
447
|
+
|
|
416
448
|
def _authorize(public_key: str) -> None:
|
|
417
449
|
path = Path.home() / ".ssh" / "authorized_keys"
|
|
418
450
|
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
@@ -10,7 +10,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|
|
10
10
|
|
|
11
11
|
[[package]]
|
|
12
12
|
name = "letify-agent"
|
|
13
|
-
version = "1.2.
|
|
13
|
+
version = "1.2.3"
|
|
14
14
|
dependencies = [
|
|
15
15
|
"letify-wire",
|
|
16
16
|
"libloading",
|
|
@@ -18,14 +18,14 @@ dependencies = [
|
|
|
18
18
|
|
|
19
19
|
[[package]]
|
|
20
20
|
name = "letify-driver"
|
|
21
|
-
version = "1.2.
|
|
21
|
+
version = "1.2.3"
|
|
22
22
|
dependencies = [
|
|
23
23
|
"letify-wire",
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
[[package]]
|
|
27
27
|
name = "letify-wire"
|
|
28
|
-
version = "1.2.
|
|
28
|
+
version = "1.2.3"
|
|
29
29
|
|
|
30
30
|
[[package]]
|
|
31
31
|
name = "libloading"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letify-status",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "letify-status",
|
|
9
|
-
"version": "1.2.
|
|
9
|
+
"version": "1.2.3",
|
|
10
10
|
"license": "SEE LICENSE IN LICENSE",
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@types/node": "^18.19.0",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "letify-status",
|
|
3
3
|
"displayName": "letify Status",
|
|
4
4
|
"description": "letify quota and GPU activity in the status bar, read from the letify CLI.",
|
|
5
|
-
"version": "1.2.
|
|
5
|
+
"version": "1.2.3",
|
|
6
6
|
"publisher": "darkpyonix",
|
|
7
7
|
"license": "SEE LICENSE IN LICENSE",
|
|
8
8
|
"repository": {
|
|
@@ -158,6 +158,13 @@ def uv_project(tmp_path: Path, monkeypatch) -> Path:
|
|
|
158
158
|
subprocess.run([uv, "lock"], cwd=project, capture_output=True, check=True)
|
|
159
159
|
monkeypatch.chdir(project)
|
|
160
160
|
monkeypatch.setattr(bootstrap, "DEFAULT_WORKSPACE_ROOT", str(tmp_path / "runtime-workspace"))
|
|
161
|
+
# The automatic environment archive store lives under the user's own ~/.letify, so
|
|
162
|
+
# without this an archive a previous run published is found and a test that expects a
|
|
163
|
+
# sync sees "archive" instead. Spec "Materializing into a runtime".
|
|
164
|
+
from letify import paths
|
|
165
|
+
|
|
166
|
+
store = tmp_path / "storage"
|
|
167
|
+
monkeypatch.setattr(paths, "storage_cache_directory", lambda name: store / name)
|
|
161
168
|
return project
|
|
162
169
|
|
|
163
170
|
|
|
@@ -302,3 +302,38 @@ def test_compatibility_fallbacks_for_migrated_paths(
|
|
|
302
302
|
Colab(ProviderConfig("colab_compat", "colab", {}, 0))
|
|
303
303
|
# It should find the token via fallback
|
|
304
304
|
assert (acct / ".config" / "colab-cli" / "token.json").is_file()
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# -- Spec: Local machine, the filesystem storage backend root ----------------------
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def test_the_automatic_environment_store_sits_under_the_declared_cache(monkeypatch, tmp_path):
|
|
311
|
+
# Spec "Materializing into a runtime": the automatic archive store is the
|
|
312
|
+
# ~/.letify/cache/storage/<name> row with the name "environments", not a directory of
|
|
313
|
+
# its own under ~/.cache.
|
|
314
|
+
from letify.config.schema import ProviderConfig
|
|
315
|
+
from letify.providers.local import Local
|
|
316
|
+
from letify.store.volume import environment_volume
|
|
317
|
+
|
|
318
|
+
monkeypatch.setattr(Path, "home", staticmethod(lambda: tmp_path))
|
|
319
|
+
provider = Local(ProviderConfig("lab", "local", {}, 0))
|
|
320
|
+
volume = environment_volume(provider)
|
|
321
|
+
root = volume._store.backend.root
|
|
322
|
+
|
|
323
|
+
assert root == tmp_path / ".letify" / "cache" / "storage" / "environments" / "local" / "lab"
|
|
324
|
+
assert ".cache" not in root.parts
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def test_an_existing_environment_store_outside_the_root_is_still_read(monkeypatch, tmp_path):
|
|
328
|
+
# Spec "Materializing into a runtime": archives an earlier install left under
|
|
329
|
+
# ~/.cache/letify/environments are read rather than abandoned.
|
|
330
|
+
from letify.config.schema import ProviderConfig
|
|
331
|
+
from letify.providers.local import Local
|
|
332
|
+
from letify.store.volume import environment_volume
|
|
333
|
+
|
|
334
|
+
legacy = tmp_path / ".cache" / "letify" / "environments"
|
|
335
|
+
legacy.mkdir(parents=True)
|
|
336
|
+
monkeypatch.setattr(Path, "home", staticmethod(lambda: tmp_path))
|
|
337
|
+
provider = Local(ProviderConfig("lab", "local", {}, 0))
|
|
338
|
+
|
|
339
|
+
assert environment_volume(provider)._store.backend.root == legacy / "local" / "lab"
|
|
@@ -592,3 +592,40 @@ def test_the_remote_half_starts_no_sshd_when_an_ssh_server_already_answers(
|
|
|
592
592
|
finally:
|
|
593
593
|
server.close()
|
|
594
594
|
assert not any(str(binary) in command for command in recorder.commands)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _flooding_binary(directory, address: str, bytes_after: int) -> str:
|
|
598
|
+
"""An executable that prints ``address`` the way tailcat does, then floods its output."""
|
|
599
|
+
import os
|
|
600
|
+
import sys
|
|
601
|
+
from pathlib import Path
|
|
602
|
+
|
|
603
|
+
path = Path(directory) / "flooding_tailcat"
|
|
604
|
+
path.write_text(
|
|
605
|
+
f"#!{sys.executable}\n"
|
|
606
|
+
"import sys\n"
|
|
607
|
+
f"print('Server listening with new address: {address}', flush=True)\n"
|
|
608
|
+
f"sys.stdout.write('x' * {bytes_after})\n"
|
|
609
|
+
"sys.stdout.flush()\n"
|
|
610
|
+
)
|
|
611
|
+
path.chmod(0o755)
|
|
612
|
+
return os.fspath(path)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def test_the_remote_half_keeps_reading_what_tailcat_serve_prints_after_the_address(
|
|
616
|
+
tmp_path,
|
|
617
|
+
) -> None:
|
|
618
|
+
# A pipe holds about 64 KiB, so a megabyte is far past the point where an unread
|
|
619
|
+
# pipe blocks the writer and the tunnel stops moving bytes.
|
|
620
|
+
binary = _flooding_binary(tmp_path, "tcReal1", 1 << 20)
|
|
621
|
+
answer, serve = nat.begin({"kind": "tailcat", "ssh_port": 22, "binary": binary})
|
|
622
|
+
assert answer == {"address": "tcReal1"}
|
|
623
|
+
|
|
624
|
+
import threading
|
|
625
|
+
|
|
626
|
+
waiter = threading.Thread(target=serve, daemon=True)
|
|
627
|
+
waiter.start()
|
|
628
|
+
waiter.join(30.0)
|
|
629
|
+
assert not waiter.is_alive(), (
|
|
630
|
+
"tailcat serve never finished, so it blocked writing to a pipe nobody reads"
|
|
631
|
+
)
|
|
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
|
{letify-1.2.2 → letify-1.2.3}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md"
RENAMED
|
File without changes
|
|
File without changes
|
{letify-1.2.2 → letify-1.2.3}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md"
RENAMED
|
File without changes
|
|
File without changes
|
{letify-1.2.2 → letify-1.2.3}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md"
RENAMED
|
File without changes
|
|
File without changes
|
{letify-1.2.2 → letify-1.2.3}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md"
RENAMED
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|