letify 2.8.0__tar.gz → 2.8.1__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-2.8.0 → letify-2.8.1}/PKG-INFO +1 -1
- {letify-2.8.0 → letify-2.8.1}/docs/SPEC.md +4 -0
- {letify-2.8.0 → letify-2.8.1}/letify/__init__.py +1 -1
- {letify-2.8.0 → letify-2.8.1}/letify/protocol/worker.py +5 -0
- {letify-2.8.0 → letify-2.8.1}/letify/providers/colab.py +100 -1
- {letify-2.8.0 → letify-2.8.1}/letify/store/handoff.py +14 -3
- {letify-2.8.0 → letify-2.8.1}/letify/tools.py +10 -1
- {letify-2.8.0 → letify-2.8.1}/letify-core/Cargo.lock +3 -3
- {letify-2.8.0 → letify-2.8.1}/letify-core/Cargo.toml +1 -1
- {letify-2.8.0 → letify-2.8.1}/letify-ext/package-lock.json +2 -2
- {letify-2.8.0 → letify-2.8.1}/letify-ext/package.json +1 -1
- {letify-2.8.0 → letify-2.8.1}/pyproject.toml +1 -1
- {letify-2.8.0 → letify-2.8.1}/tests/conftest.py +38 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_drive_handoff.py +74 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_providers.py +71 -1
- {letify-2.8.0 → letify-2.8.1}/.github/workflows/ci.yml +0 -0
- {letify-2.8.0 → letify-2.8.1}/.github/workflows/docs.yml +0 -0
- {letify-2.8.0 → letify-2.8.1}/.github/workflows/publish.yml +0 -0
- {letify-2.8.0 → letify-2.8.1}/.gitignore +0 -0
- {letify-2.8.0 → letify-2.8.1}/.researchtree +0 -0
- {letify-2.8.0 → letify-2.8.1}/CLAUDE.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/LICENSE +0 -0
- {letify-2.8.0 → letify-2.8.1}/PROJECT.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/README.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/COMPONENT.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/INTENT.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/NETWORK.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/guide/01-getting-started.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/guide/02-providers.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/guide/03-execution-modes.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/guide/04-environments-and-data.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/guide/05-concurrency.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/guide/06-cost.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/guide/07-troubleshooting.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/guide/08-releasing.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/guide/README.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/guide/ko/02-/355/224/204/353/241/234/353/260/224/354/235/264/353/215/224.md" +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
- {letify-2.8.0 → letify-2.8.1}/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-2.8.0 → letify-2.8.1}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/guide/ko/README.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/locales/README_ko.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/docs/mkdocs.yml +0 -0
- {letify-2.8.0 → letify-2.8.1}/examples/README.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/examples/nvfp4-lora/.letify.example +0 -0
- {letify-2.8.0 → letify-2.8.1}/examples/nvfp4-lora/00_smoke.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/examples/nvfp4-lora/01_sweep.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/examples/nvfp4-lora/02_resume.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/examples/nvfp4-lora/03_watch.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/examples/nvfp4-lora/README.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/examples/nvfp4-lora/README_ko.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/examples/nvfp4-lora/common.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/examples/nvfp4-lora/recipe.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/cli.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/config/__init__.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/config/inventory.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/config/login.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/config/schema.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/config/secrets.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/config/writer.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/declare/__init__.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/declare/cache.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/declare/env.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/declare/function.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/declare/instance.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/declare/reduce.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/declare/replicas.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/errors.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/install.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/launcher.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/paths.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/protocol/__init__.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/protocol/codec.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/protocol/driver.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/protocol/handle.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/protocol/wire.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/providers/__init__.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/providers/base.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/providers/colab_files.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/providers/colab_keepalive.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/providers/elice.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/providers/kaggle.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/providers/kaggle_adapter.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/providers/local.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/providers/modal.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/providers/modal_adapter.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/providers/naming.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/providers/shell.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/providers/tunnel.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/providers/usage.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/remoting/__init__.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/remoting/capability.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/remoting/device/__init__.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/remoting/device/client.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/remoting/device/cuda.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/remoting/device/executor.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/remoting/device/frames.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/remoting/device/guard.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/remoting/device/tensor.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/remoting/device/trace.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/remoting/device/value.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/remoting/loader.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/remoting/probe.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/render.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/runtime/__init__.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/runtime/bootstrap.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/runtime/broker.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/runtime/channel.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/runtime/lease.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/runtime/pool.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/runtime/session.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/runtime/telemetry.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/store/__init__.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/store/backends/__init__.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/store/backends/drive.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/store/backends/filesystem.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/store/backends/google_auth.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/store/backends/layout.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/store/backends/objects.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/store/cas.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/store/pathdata.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/store/sendorder.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/store/volume.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/stubs.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/transport/__init__.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/transport/agent.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/transport/announce.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/transport/link.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/transport/nat.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/transport/pipeline.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/transport/portmap.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/transport/probe.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/transport/quic.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/transport/rendezvous.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/transport/setup.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/transport/sshopts.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify/transport/strategies.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-core/.gitignore +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-core/build.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-core/letify-agent/Cargo.toml +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-core/letify-agent/src/driver.rs +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-core/letify-agent/src/main.rs +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-core/letify-driver/Cargo.toml +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-core/letify-driver/src/client.rs +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-core/letify-driver/src/exports.rs +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-core/letify-driver/src/image.rs +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-core/letify-driver/src/lib.rs +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-core/letify-driver/src/status.rs +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-core/letify-driver/src/table.rs +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-core/letify-quic/Cargo.toml +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-core/letify-quic/src/main.rs +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-core/letify-wire/Cargo.toml +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-core/letify-wire/src/lib.rs +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-core/letify-wire/tests/throughput.rs +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-core/rust-toolchain.toml +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-ext/.vscodeignore +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-ext/LICENSE +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-ext/README.md +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-ext/media/icon.svg +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-ext/src/cli.ts +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-ext/src/extension.ts +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-ext/src/history.ts +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-ext/src/model.ts +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-ext/src/poll.ts +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-ext/src/render.ts +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-ext/test/cli.test.ts +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-ext/test/manifest.test.ts +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-ext/test/model.test.ts +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-ext/test/regressions.test.ts +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify-ext/tsconfig.json +0 -0
- {letify-2.8.0 → letify-2.8.1}/letify_providers.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/scripts/sync-main.sh +0 -0
- {letify-2.8.0 → letify-2.8.1}/scripts/version.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/fake_kaggle_adapter.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/fake_modal_adapter.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_cli.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_cli_output.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_colab_keepalive.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_config.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_core.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_data.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_data_streaming.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_declare.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_device.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_distributed.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_drive_store.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_editor_sessions.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_elice.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_frames.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_install.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_kaggle.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_kaggle_cookie.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_kaggle_link.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_link_log.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_notebook.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_parallel_placement.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_paths.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_portmap.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_protocol.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_quic.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_remoting.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_render.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_runtime.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_ship.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_store.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_strategies.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_stubs.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_transport.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_tunnel_setup.py +0 -0
- {letify-2.8.0 → letify-2.8.1}/tests/test_version.py +0 -0
|
@@ -1565,6 +1565,8 @@ Both run in two places with the same result:
|
|
|
1565
1565
|
|
|
1566
1566
|
A body run on a one-shot channel, which has no stream back to the local process, cannot ask for a grant, so `stash` and `unstash` there raise `UnsupportedMode` naming the provider rather than reading the runtime's own configuration.
|
|
1567
1567
|
|
|
1568
|
+
The worker sets the environment variable `LETIFY_WORKER_PID` to its process id, so a process the body starts, such as one per GPU, inherits it without the grant hook. `stash` and `unstash` called there raise `UnsupportedMode` naming the handoff, saying the call must be made from the process letify runs the body in and not from a child process it started, and neither reads the configuration nor touches Drive.
|
|
1569
|
+
|
|
1568
1570
|
When the machine declares no Drive store, both raise `ConfigError` naming `drive_login` and `drive_project`. letify never falls back to another store or to the local disk. A Drive request that fails raises `RuntimeFailure` naming the Drive status and the name, and the function body decides whether to carry on.
|
|
1569
1571
|
|
|
1570
1572
|
## Environment
|
|
@@ -1914,6 +1916,8 @@ The key is per session. letify generates a new ed25519 key pair for each session
|
|
|
1914
1916
|
|
|
1915
1917
|
The Colab CLI runs as `uv tool run --from google-colab-cli colab`, with `jupyter-kernel-client<1` pinned, because release 0.6.0 of the CLI calls an API that jupyter-kernel-client 1.0 removed. `colab new` and `colab stop` manage the session. `Colab.sessions()` reads `colab sessions` and returns the first word of each listing line. The CLI prints a session as `[<name>] <id> | Hardware: <hardware> | Variant: <variant>`, so a first word in square brackets gives the name inside them. A line starting with `[colab]` is a message from the CLI, such as `[colab] No active sessions found on server.`, and names no session, so an account with no session returns an empty list.
|
|
1916
1918
|
|
|
1919
|
+
**An assignment with no session.** `colab new` can fail after Colab has already assigned a machine, for example with `ReadTimeout` after 120 s, and the machine then bills (14.2 compute units per hour for an A100) with no local session state. `colab sessions` lists it as `[?] <endpoint> | ...`, and `colab stop -s <name>` cannot reach it. `Colab.create_session` therefore lists the account's assignments before `colab new`. When `colab new` raises, for any reason, it lists them again, releases every `?` assignment whose endpoint was not in the first list, and re-raises the original error. An assignment that existed before the attempt is never released. Each release prints one line on standard error naming the endpoint. When the first or the second listing fails, one line on standard error names the endpoint check and the error, and the original error is still the one raised. An assignment is released by endpoint with `Client.unassign` of the Colab CLI, run as `uv tool run --from google-colab-cli python -c <script> <endpoint>` with the account's environment, because the CLI has no command for it. `Colab.orphans()` returns the endpoints of the `?` rows of `colab sessions` and `Colab.stop_orphan(endpoint)` releases one, so `letify stop --orphans` covers Colab as it covers Kaggle.
|
|
1920
|
+
|
|
1917
1921
|
**Runtime tools.** A runtime whose rendezvous runs a Python program on the machine, Colab and Kaggle both, installs tailcat lazily when the tailcat strategy requests its endpoint, within that strategy's race thread. The install belongs to the rendezvous and not to one provider, so a provider that leaves it out is a defect rather than a choice: without it the tailcat request carries no path, the remote side runs a bare `tailcat`, and the strategy fails with `FileNotFoundError` on a runtime image that ships no such binary. Target construction performs no remote installation. TCP punch rendezvous never waits for tailcat preparation, even if installation fails or takes its full 180 second timeout. A locally available `tailcat` or configured `tailcat_binary` requires the Linux release pinned by `letify.install.TAILCAT_VERSION`, `TAILCAT_RELEASE` and `TAILCAT_SHA256`. The runtime selects its own architecture, verifies the archive SHA-256, extracts only a regular `tailcat` member into `~/.letify/tools/tailcat/<version>/tailcat`, and verifies the cached binary against its recorded SHA-256 on reuse. Only tailcat requests carry that absolute path. Installation errors name the tool and verification failure and fail that strategy. Tailcat remains eligible because throughput is measured per link rather than inferred from another session. The shared account throughput floor still rejects slow links.
|
|
1918
1922
|
|
|
1919
1923
|
**Rendezvous scheduling.** Provider `colab exec` commands, including TCP punch rendezvous and tailcat preparation, do not acquire the wake lock. The lock protects concurrent wakes sharing CLI session state; rendezvous is a separate operation whose agreed dial time cannot wait behind a wake's 120 second timeout. Wakes acquire the account lock without waiting. If another wake holds it, the daemon records `wake_skipped` with reason `account_busy`, logs the skip and waits 60 seconds before trying again. Successful acquisition still holds the lock through the wake command so two wakes cannot collide. An already running wake cannot hold a lock needed by a connection.
|
|
@@ -1346,6 +1346,11 @@ _DRIVE_LOCK = threading.Condition()
|
|
|
1346
1346
|
#: ``stash`` or ``unstash`` takes them.
|
|
1347
1347
|
_DRIVE_ANSWERS = {}
|
|
1348
1348
|
|
|
1349
|
+
# A process the body starts inherits this and lacks ``_drive_ask``, which is how
|
|
1350
|
+
# ``stash`` and ``unstash`` tell it is a child of the body and refuse. Spec "Named handoffs
|
|
1351
|
+
# on the Drive store".
|
|
1352
|
+
os.environ["LETIFY_WORKER_PID"] = str(os.getpid())
|
|
1353
|
+
|
|
1349
1354
|
|
|
1350
1355
|
def _drive_ask(message):
|
|
1351
1356
|
"""Ask the client for a Drive grant. Nothing replies to this request.
|
|
@@ -121,6 +121,28 @@ def _access_token(stored: dict[str, Any]) -> str:
|
|
|
121
121
|
return str(json.loads(response.read())["access_token"])
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
#: Run in the CLI's own Python: release the server-side assignment at the endpoint in argv.
|
|
125
|
+
#: ``colab stop`` cannot do this, because it needs the local session state an assignment
|
|
126
|
+
#: made by a failed ``colab new`` never got. Spec "Colab", an assignment with no session.
|
|
127
|
+
UNASSIGN_SCRIPT = (
|
|
128
|
+
"import sys\n"
|
|
129
|
+
"from colab_cli.common import state\n"
|
|
130
|
+
"state.client.unassign(sys.argv[1])\n"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _parse_assignments(listing: str) -> list[tuple[str, str]]:
|
|
135
|
+
"""The ``(name, endpoint)`` pairs in ``colab sessions`` output, ``?`` for no local state."""
|
|
136
|
+
found = []
|
|
137
|
+
for line in listing.splitlines():
|
|
138
|
+
head = line.strip().split("|")[0].split()
|
|
139
|
+
if len(head) >= 2 and head[0].startswith("[") and head[0].endswith("]"):
|
|
140
|
+
name = head[0][1:-1]
|
|
141
|
+
if name.lower() != "colab":
|
|
142
|
+
found.append((name, head[1]))
|
|
143
|
+
return found
|
|
144
|
+
|
|
145
|
+
|
|
124
146
|
class Colab(Shell):
|
|
125
147
|
"""A Colab runtime for one Google account."""
|
|
126
148
|
|
|
@@ -316,6 +338,68 @@ class Colab(Shell):
|
|
|
316
338
|
names.append(word)
|
|
317
339
|
return names
|
|
318
340
|
|
|
341
|
+
def _assignments(self) -> list[tuple[str, str]]:
|
|
342
|
+
"""Every assignment the account holds, as ``(name, endpoint)``; ``?`` has no local state."""
|
|
343
|
+
return _parse_assignments(self._cli("sessions", timeout=120))
|
|
344
|
+
|
|
345
|
+
def _unassign(self, endpoint: str) -> None:
|
|
346
|
+
"""Release one assignment by endpoint, through the CLI's own client library."""
|
|
347
|
+
uv = tools.find_uv()
|
|
348
|
+
if uv is None:
|
|
349
|
+
raise ProviderUnavailable(self.kind, tools.missing_uv_message())
|
|
350
|
+
command = [*tools.python_command(tools.COLAB, uv), "-c", UNASSIGN_SCRIPT, endpoint]
|
|
351
|
+
result = subprocess.run(
|
|
352
|
+
command, capture_output=True, text=True, timeout=180, env=self._env()
|
|
353
|
+
)
|
|
354
|
+
if result.returncode != 0:
|
|
355
|
+
raise RuntimeFailure(
|
|
356
|
+
f"unassigning {endpoint} exited {result.returncode}",
|
|
357
|
+
command=" ".join(command[:-3]),
|
|
358
|
+
stderr=(result.stderr or "").strip(),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
def orphans(self) -> list[str]:
|
|
362
|
+
"""Endpoints of assignments the account holds that no local session names."""
|
|
363
|
+
return [endpoint for name, endpoint in self._assignments() if name == "?"]
|
|
364
|
+
|
|
365
|
+
def stop_orphan(self, ref: str) -> bool:
|
|
366
|
+
"""Unassign one endpoint `orphans` reported, and report whether it is gone."""
|
|
367
|
+
try:
|
|
368
|
+
self._unassign(ref)
|
|
369
|
+
except (RuntimeFailure, ProviderUnavailable, subprocess.TimeoutExpired):
|
|
370
|
+
return False
|
|
371
|
+
return True
|
|
372
|
+
|
|
373
|
+
def _release_leftovers(self, before: set[str]) -> None:
|
|
374
|
+
"""Unassign each `?` assignment that appeared since ``before``, best effort."""
|
|
375
|
+
try:
|
|
376
|
+
leftovers = [
|
|
377
|
+
endpoint
|
|
378
|
+
for name, endpoint in self._assignments()
|
|
379
|
+
if name == "?" and endpoint not in before
|
|
380
|
+
]
|
|
381
|
+
except (RuntimeFailure, ProviderUnavailable, subprocess.TimeoutExpired) as exc:
|
|
382
|
+
print(
|
|
383
|
+
f"letify: {self.alias}: the endpoint check after the failed 'colab new' "
|
|
384
|
+
f"could not list assignments ({exc}); an assignment may still be billing, "
|
|
385
|
+
"see 'letify stop --orphans'",
|
|
386
|
+
file=sys.stderr,
|
|
387
|
+
)
|
|
388
|
+
return
|
|
389
|
+
for endpoint in leftovers:
|
|
390
|
+
if self.stop_orphan(endpoint):
|
|
391
|
+
print(
|
|
392
|
+
f"letify: {self.alias}: released {endpoint}, assigned by the failed "
|
|
393
|
+
"'colab new'",
|
|
394
|
+
file=sys.stderr,
|
|
395
|
+
)
|
|
396
|
+
else:
|
|
397
|
+
print(
|
|
398
|
+
f"letify: {self.alias}: could not release {endpoint}, assigned by the "
|
|
399
|
+
"failed 'colab new'; run 'letify stop --orphans'",
|
|
400
|
+
file=sys.stderr,
|
|
401
|
+
)
|
|
402
|
+
|
|
319
403
|
# -- the pipeline ----------------------------------------------------------
|
|
320
404
|
|
|
321
405
|
def strategies(self) -> list[Strategy]:
|
|
@@ -388,7 +472,22 @@ class Colab(Shell):
|
|
|
388
472
|
args += ["--gpu", ALIASES.get(instance.gpu, instance.gpu)]
|
|
389
473
|
elif instance.tpu:
|
|
390
474
|
args += ["--tpu", instance.tpu]
|
|
391
|
-
|
|
475
|
+
try:
|
|
476
|
+
before = {endpoint for _, endpoint in self._assignments()}
|
|
477
|
+
except (RuntimeFailure, subprocess.TimeoutExpired) as exc:
|
|
478
|
+
before = None
|
|
479
|
+
print(
|
|
480
|
+
f"letify: {self.alias}: the endpoint check before 'colab new' could not "
|
|
481
|
+
f"list assignments ({exc}); a failed creation will not be cleaned up",
|
|
482
|
+
file=sys.stderr,
|
|
483
|
+
)
|
|
484
|
+
try:
|
|
485
|
+
self._cli(*args, timeout=900)
|
|
486
|
+
except BaseException:
|
|
487
|
+
# A timed out `colab new` can still leave a server-side assignment behind.
|
|
488
|
+
if before is not None:
|
|
489
|
+
self._release_leftovers(before)
|
|
490
|
+
raise
|
|
392
491
|
try:
|
|
393
492
|
self._start_keep_alive(name)
|
|
394
493
|
except BaseException:
|
|
@@ -35,6 +35,10 @@ from ..protocol import codec
|
|
|
35
35
|
#: so a body can tell it runs on a channel with no stream back to the local process.
|
|
36
36
|
ONE_SHOT_MARKER = "LETIFY_ONE_SHOT"
|
|
37
37
|
|
|
38
|
+
#: The environment variable the worker sets to its process id, so a child process of the
|
|
39
|
+
#: body, which inherits it but lacks the worker's grant hook, can say why it cannot ask.
|
|
40
|
+
WORKER_MARKER = "LETIFY_WORKER_PID"
|
|
41
|
+
|
|
38
42
|
#: The prefix of a handoff's ref, so handoffs share the folder with other refs.
|
|
39
43
|
REF_SPACE = "stash/"
|
|
40
44
|
|
|
@@ -198,7 +202,7 @@ def stash(name: str, path: str | os.PathLike[str]) -> str:
|
|
|
198
202
|
source = Path(path)
|
|
199
203
|
if not source.exists():
|
|
200
204
|
raise FileNotFoundError(f"cannot stash {str(source)!r}: it does not exist")
|
|
201
|
-
main = _worker_main()
|
|
205
|
+
main = _worker_main(name)
|
|
202
206
|
if main is not None:
|
|
203
207
|
return _stash_in_worker(main, name, source)
|
|
204
208
|
backend = drive_backend()
|
|
@@ -222,7 +226,7 @@ def unstash(name: str, path: str | os.PathLike[str]) -> Path | None:
|
|
|
222
226
|
"""
|
|
223
227
|
check_name(name)
|
|
224
228
|
target = Path(path)
|
|
225
|
-
main = _worker_main()
|
|
229
|
+
main = _worker_main(name)
|
|
226
230
|
if main is not None:
|
|
227
231
|
return _unstash_in_worker(main, name, target)
|
|
228
232
|
backend = drive_backend()
|
|
@@ -255,7 +259,7 @@ def _verify(name: str, archive: Path, digest: str) -> None:
|
|
|
255
259
|
# -- inside a shipped function ---------------------------------------------------------
|
|
256
260
|
|
|
257
261
|
|
|
258
|
-
def _worker_main() -> Any:
|
|
262
|
+
def _worker_main(name: str) -> Any:
|
|
259
263
|
"""The worker's ``__main__`` module when this runs inside a shipped function, else None.
|
|
260
264
|
|
|
261
265
|
The same test ``all_reduce`` makes: only the worker defines the hook that sends a request
|
|
@@ -264,6 +268,13 @@ def _worker_main() -> Any:
|
|
|
264
268
|
main = sys.modules.get("__main__")
|
|
265
269
|
if main is not None and hasattr(main, "_drive_ask"):
|
|
266
270
|
return main
|
|
271
|
+
worker_pid = os.environ.get(WORKER_MARKER)
|
|
272
|
+
if worker_pid is not None and worker_pid != str(os.getpid()):
|
|
273
|
+
raise UnsupportedMode(
|
|
274
|
+
f"handoff {name!r}: stash and unstash must be called from the process letify "
|
|
275
|
+
f"runs the body in, not from a child process it started (this is a child of "
|
|
276
|
+
f"worker process {worker_pid}). Call it in the body and pass the path to the child."
|
|
277
|
+
)
|
|
267
278
|
one_shot = os.environ.get(ONE_SHOT_MARKER)
|
|
268
279
|
if one_shot is not None:
|
|
269
280
|
raise UnsupportedMode(
|
|
@@ -19,7 +19,7 @@ from __future__ import annotations
|
|
|
19
19
|
import os
|
|
20
20
|
import shutil
|
|
21
21
|
import sys
|
|
22
|
-
from dataclasses import dataclass
|
|
22
|
+
from dataclasses import dataclass, replace
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
|
|
25
25
|
from .config.secrets import account_directory
|
|
@@ -111,6 +111,15 @@ def command(tool: Tool, uv: str) -> list[str]:
|
|
|
111
111
|
]
|
|
112
112
|
|
|
113
113
|
|
|
114
|
+
def python_command(tool: Tool, uv: str) -> list[str]:
|
|
115
|
+
"""The argument list that runs the Python of ``tool``'s own uv environment.
|
|
116
|
+
|
|
117
|
+
Code run this way can import the tool's package, which is how a call the tool's command
|
|
118
|
+
line does not offer reaches the tool's client library.
|
|
119
|
+
"""
|
|
120
|
+
return command(replace(tool, executable="python"), uv)
|
|
121
|
+
|
|
122
|
+
|
|
114
123
|
def _ensure_symlink(link_path: Path, target: Path) -> None:
|
|
115
124
|
link_path.parent.mkdir(parents=True, exist_ok=True)
|
|
116
125
|
if link_path.is_symlink() or link_path.exists():
|
|
@@ -138,7 +138,7 @@ dependencies = [
|
|
|
138
138
|
|
|
139
139
|
[[package]]
|
|
140
140
|
name = "letify-agent"
|
|
141
|
-
version = "2.8.
|
|
141
|
+
version = "2.8.1"
|
|
142
142
|
dependencies = [
|
|
143
143
|
"letify-wire",
|
|
144
144
|
"libloading",
|
|
@@ -146,7 +146,7 @@ dependencies = [
|
|
|
146
146
|
|
|
147
147
|
[[package]]
|
|
148
148
|
name = "letify-driver"
|
|
149
|
-
version = "2.8.
|
|
149
|
+
version = "2.8.1"
|
|
150
150
|
dependencies = [
|
|
151
151
|
"letify-wire",
|
|
152
152
|
]
|
|
@@ -164,7 +164,7 @@ dependencies = [
|
|
|
164
164
|
|
|
165
165
|
[[package]]
|
|
166
166
|
name = "letify-wire"
|
|
167
|
-
version = "2.8.
|
|
167
|
+
version = "2.8.1"
|
|
168
168
|
|
|
169
169
|
[[package]]
|
|
170
170
|
name = "libc"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letify-status",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "letify-status",
|
|
9
|
-
"version": "2.8.
|
|
9
|
+
"version": "2.8.1",
|
|
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": "2.8.
|
|
5
|
+
"version": "2.8.1",
|
|
6
6
|
"publisher": "darkpyonix",
|
|
7
7
|
"license": "SEE LICENSE IN LICENSE",
|
|
8
8
|
"repository": {
|
|
@@ -2016,3 +2016,41 @@ def kaggle_api_token(isolated_home):
|
|
|
2016
2016
|
write_secret("kaggle_a", "username", "irack000")
|
|
2017
2017
|
return write_secret("kaggle_a", "access_token", "KGAT_" + "a" * 32)
|
|
2018
2018
|
|
|
2019
|
+
|
|
2020
|
+
|
|
2021
|
+
class FakeColabCli:
|
|
2022
|
+
"""A Colab CLI that holds assignments, for ``patch_run`` as the ``result`` callable.
|
|
2023
|
+
|
|
2024
|
+
``sessions`` prints one ``[<name>] <endpoint>`` row per assignment, ``?`` for one with
|
|
2025
|
+
no local state. ``new`` can add an assignment and then fail, as a read timeout does.
|
|
2026
|
+
The unassign script is recognized by its ``python`` executable and removes the endpoint
|
|
2027
|
+
named by its last argument.
|
|
2028
|
+
"""
|
|
2029
|
+
|
|
2030
|
+
def __init__(self, assignments=(), new_adds=None, new_fails=False, list_fails_after=None):
|
|
2031
|
+
self.assignments = list(assignments)
|
|
2032
|
+
self.new_adds = new_adds
|
|
2033
|
+
self.new_fails = new_fails
|
|
2034
|
+
self.list_fails_after = list_fails_after
|
|
2035
|
+
self.listings = 0
|
|
2036
|
+
self.unassigned: list[str] = []
|
|
2037
|
+
|
|
2038
|
+
def __call__(self, command):
|
|
2039
|
+
if "python" in command and "-c" in command:
|
|
2040
|
+
self.unassigned.append(command[-1])
|
|
2041
|
+
self.assignments = [a for a in self.assignments if a[1] != command[-1]]
|
|
2042
|
+
return FakeCompleted()
|
|
2043
|
+
if "sessions" in command:
|
|
2044
|
+
self.listings += 1
|
|
2045
|
+
if self.list_fails_after is not None and self.listings > self.list_fails_after:
|
|
2046
|
+
return FakeCompleted(returncode=1, stderr="Read timed out")
|
|
2047
|
+
rows = [f"[{n}] {e} | Hardware: A100 | Variant: GPU\n" for n, e in self.assignments]
|
|
2048
|
+
return FakeCompleted(
|
|
2049
|
+
stdout="".join(rows) or "[colab] No active sessions found on server.\n"
|
|
2050
|
+
)
|
|
2051
|
+
if "new" in command:
|
|
2052
|
+
if self.new_adds:
|
|
2053
|
+
self.assignments.append(("?", self.new_adds))
|
|
2054
|
+
if self.new_fails:
|
|
2055
|
+
return FakeCompleted(returncode=1, stderr="ReadTimeout: Read timed out")
|
|
2056
|
+
return FakeCompleted()
|
|
@@ -425,3 +425,77 @@ def test_stash_and_unstash_on_a_one_shot_channel_raise_unsupported_mode_naming_t
|
|
|
425
425
|
assert len(value) == 2
|
|
426
426
|
assert all("colab_a" in message for message in value)
|
|
427
427
|
assert fake_drive.requests == []
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
# -- a child process of the body -------------------------------------------------------
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def test_stash_and_unstash_in_a_child_process_of_the_body_say_to_call_from_the_main_process(
|
|
434
|
+
fake_drive, launcher_from, tmp_path, worker_sees_this_checkout
|
|
435
|
+
) -> None:
|
|
436
|
+
"""Spec: a child the body starts has no grant hook, so it raises instead of reading config."""
|
|
437
|
+
declare_drive(fake_drive)
|
|
438
|
+
let = launcher_from(LOCAL)
|
|
439
|
+
source = tmp_path / "f.txt"
|
|
440
|
+
source.write_text("x")
|
|
441
|
+
|
|
442
|
+
@let.function(device=let.providers.a.CPU, host=letify.remote)
|
|
443
|
+
def from_children(path: str) -> dict:
|
|
444
|
+
import multiprocessing
|
|
445
|
+
import os
|
|
446
|
+
import subprocess
|
|
447
|
+
import sys
|
|
448
|
+
from pathlib import Path
|
|
449
|
+
|
|
450
|
+
import letify as inner
|
|
451
|
+
|
|
452
|
+
code = (
|
|
453
|
+
"import sys, letify\n"
|
|
454
|
+
"for call in (lambda: letify.stash('kid', sys.argv[1]),"
|
|
455
|
+
" lambda: letify.unstash('kid', sys.argv[1] + '.out')):\n"
|
|
456
|
+
" try:\n"
|
|
457
|
+
" call()\n"
|
|
458
|
+
" except letify.UnsupportedMode as exc:\n"
|
|
459
|
+
" print('UM:' + str(exc))\n"
|
|
460
|
+
)
|
|
461
|
+
run = subprocess.run(
|
|
462
|
+
[sys.executable, "-c", code, path], capture_output=True, text=True, check=True
|
|
463
|
+
)
|
|
464
|
+
helper = Path(path).parent / "spawned_helper.py"
|
|
465
|
+
helper.write_text(
|
|
466
|
+
"import letify\n"
|
|
467
|
+
"def unstash_it(queue, path):\n"
|
|
468
|
+
" try:\n"
|
|
469
|
+
" letify.unstash('spawned', path)\n"
|
|
470
|
+
" except letify.UnsupportedMode as exc:\n"
|
|
471
|
+
" queue.put(str(exc))\n"
|
|
472
|
+
" else:\n"
|
|
473
|
+
" queue.put('no error')\n"
|
|
474
|
+
)
|
|
475
|
+
sys.path.insert(0, str(helper.parent))
|
|
476
|
+
old = os.environ.get("PYTHONPATH", "")
|
|
477
|
+
os.environ["PYTHONPATH"] = str(helper.parent) + os.pathsep + old
|
|
478
|
+
import spawned_helper
|
|
479
|
+
|
|
480
|
+
context = multiprocessing.get_context("spawn")
|
|
481
|
+
queue = context.Queue()
|
|
482
|
+
process = context.Process(target=spawned_helper.unstash_it, args=(queue, path + ".spawn"))
|
|
483
|
+
process.start()
|
|
484
|
+
spawned = queue.get(timeout=120)
|
|
485
|
+
process.join()
|
|
486
|
+
return {
|
|
487
|
+
"subprocess": [x for x in run.stdout.splitlines() if x.startswith("UM:")],
|
|
488
|
+
"spawn": spawned,
|
|
489
|
+
"main_is_not_refused": inner.UnsupportedMode.__name__,
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
with let.keep_alive():
|
|
493
|
+
answer = from_children(str(source))
|
|
494
|
+
assert len(answer["subprocess"]) == 2
|
|
495
|
+
for message in answer["subprocess"] + [answer["spawn"]]:
|
|
496
|
+
assert "process letify runs the body in" in message
|
|
497
|
+
assert "child process" in message
|
|
498
|
+
assert "'kid'" in answer["subprocess"][0] and "'kid'" in answer["subprocess"][1]
|
|
499
|
+
assert "'spawned'" in answer["spawn"]
|
|
500
|
+
assert fake_drive.requests == []
|
|
501
|
+
|
|
@@ -20,7 +20,7 @@ from importlib import import_module
|
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
|
|
22
22
|
import pytest
|
|
23
|
-
from conftest import FakeCompleted, FakeResponse, provider_of
|
|
23
|
+
from conftest import FakeColabCli, FakeCompleted, FakeResponse, provider_of
|
|
24
24
|
|
|
25
25
|
import letify
|
|
26
26
|
from letify import providers
|
|
@@ -3066,3 +3066,73 @@ def test_a_data_channel_failure_names_the_host(isolated_home, fake_modal, capfd)
|
|
|
3066
3066
|
finally:
|
|
3067
3067
|
channel.close()
|
|
3068
3068
|
provider.stop(runtime)
|
|
3069
|
+
|
|
3070
|
+
|
|
3071
|
+
# -- Spec: Colab, an assignment with no session ---------------------------------
|
|
3072
|
+
|
|
3073
|
+
|
|
3074
|
+
def _colab_with(patch_which, patch_run, cli):
|
|
3075
|
+
patch_which(tools_module, present=True)
|
|
3076
|
+
patch_run(colab_module, result=cli)
|
|
3077
|
+
return provider_of(Colab, "colab_a")
|
|
3078
|
+
|
|
3079
|
+
|
|
3080
|
+
def test_a_failed_colab_new_that_left_an_assignment_unassigns_it_and_reraises(
|
|
3081
|
+
isolated_home, patch_which, patch_run, capsys
|
|
3082
|
+
) -> None:
|
|
3083
|
+
cli = FakeColabCli(new_adds="gpu-a100-new", new_fails=True)
|
|
3084
|
+
provider = _colab_with(patch_which, patch_run, cli)
|
|
3085
|
+
with pytest.raises(letify.RuntimeFailure, match=r"ReadTimeout|exited 1"):
|
|
3086
|
+
provider.create_session(provider.A100, "letify-a100-1")
|
|
3087
|
+
assert cli.unassigned == ["gpu-a100-new"]
|
|
3088
|
+
assert "gpu-a100-new" in capsys.readouterr().err
|
|
3089
|
+
|
|
3090
|
+
|
|
3091
|
+
def test_an_assignment_that_existed_before_a_failed_colab_new_is_left_alone(
|
|
3092
|
+
isolated_home, patch_which, patch_run
|
|
3093
|
+
) -> None:
|
|
3094
|
+
cli = FakeColabCli(assignments=[("?", "gpu-old")], new_fails=True)
|
|
3095
|
+
provider = _colab_with(patch_which, patch_run, cli)
|
|
3096
|
+
with pytest.raises(letify.RuntimeFailure):
|
|
3097
|
+
provider.create_session(provider.A100, "letify-a100-1")
|
|
3098
|
+
assert cli.unassigned == []
|
|
3099
|
+
|
|
3100
|
+
|
|
3101
|
+
def test_a_failed_colab_new_does_not_unassign_a_session_with_a_name(
|
|
3102
|
+
isolated_home, patch_which, patch_run
|
|
3103
|
+
) -> None:
|
|
3104
|
+
cli = FakeColabCli(new_fails=True)
|
|
3105
|
+
cli.assignments.append(("letify-other", "gpu-named"))
|
|
3106
|
+
provider = _colab_with(patch_which, patch_run, cli)
|
|
3107
|
+
with pytest.raises(letify.RuntimeFailure):
|
|
3108
|
+
provider.create_session(provider.A100, "letify-a100-1")
|
|
3109
|
+
assert cli.unassigned == []
|
|
3110
|
+
|
|
3111
|
+
|
|
3112
|
+
def test_a_failed_endpoint_check_is_reported_and_the_original_error_is_raised(
|
|
3113
|
+
isolated_home, patch_which, patch_run, capsys
|
|
3114
|
+
) -> None:
|
|
3115
|
+
cli = FakeColabCli(new_fails=True, list_fails_after=1)
|
|
3116
|
+
provider = _colab_with(patch_which, patch_run, cli)
|
|
3117
|
+
with pytest.raises(letify.RuntimeFailure, match=r"colab_a|new"):
|
|
3118
|
+
provider.create_session(provider.A100, "letify-a100-1")
|
|
3119
|
+
assert "endpoint check" in capsys.readouterr().err
|
|
3120
|
+
assert cli.unassigned == []
|
|
3121
|
+
|
|
3122
|
+
|
|
3123
|
+
def test_colab_orphans_are_the_endpoints_with_no_local_session(
|
|
3124
|
+
isolated_home, patch_which, patch_run
|
|
3125
|
+
) -> None:
|
|
3126
|
+
cli = FakeColabCli(assignments=[("?", "gpu-a"), ("letify-x", "gpu-b"), ("?", "gpu-c")])
|
|
3127
|
+
provider = _colab_with(patch_which, patch_run, cli)
|
|
3128
|
+
assert provider.orphans() == ["gpu-a", "gpu-c"]
|
|
3129
|
+
|
|
3130
|
+
|
|
3131
|
+
def test_stopping_a_colab_orphan_unassigns_its_endpoint(
|
|
3132
|
+
isolated_home, patch_which, patch_run
|
|
3133
|
+
) -> None:
|
|
3134
|
+
cli = FakeColabCli(assignments=[("?", "gpu-a")])
|
|
3135
|
+
provider = _colab_with(patch_which, patch_run, cli)
|
|
3136
|
+
assert provider.stop_orphan("gpu-a") is True
|
|
3137
|
+
assert cli.unassigned == ["gpu-a"]
|
|
3138
|
+
assert provider.orphans() == []
|
|
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
|
{letify-2.8.0 → letify-2.8.1}/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-2.8.0 → letify-2.8.1}/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-2.8.0 → letify-2.8.1}/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-2.8.0 → letify-2.8.1}/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
|