letify 2.8.2__tar.gz → 2.8.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-2.8.2 → letify-2.8.3}/PKG-INFO +1 -1
- {letify-2.8.2 → letify-2.8.3}/docs/SPEC.md +2 -2
- {letify-2.8.2 → letify-2.8.3}/letify/__init__.py +1 -1
- {letify-2.8.2 → letify-2.8.3}/letify/providers/colab.py +39 -5
- {letify-2.8.2 → letify-2.8.3}/letify/runtime/bootstrap.py +4 -2
- {letify-2.8.2 → letify-2.8.3}/letify/runtime/session.py +10 -1
- {letify-2.8.2 → letify-2.8.3}/letify-core/Cargo.lock +3 -3
- {letify-2.8.2 → letify-2.8.3}/letify-core/Cargo.toml +1 -1
- {letify-2.8.2 → letify-2.8.3}/letify-ext/package-lock.json +2 -2
- {letify-2.8.2 → letify-2.8.3}/letify-ext/package.json +1 -1
- {letify-2.8.2 → letify-2.8.3}/pyproject.toml +1 -1
- {letify-2.8.2 → letify-2.8.3}/tests/conftest.py +1 -1
- {letify-2.8.2 → letify-2.8.3}/tests/test_drive_store.py +26 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_providers.py +72 -3
- {letify-2.8.2 → letify-2.8.3}/tests/test_runtime.py +75 -0
- {letify-2.8.2 → letify-2.8.3}/.github/workflows/ci.yml +0 -0
- {letify-2.8.2 → letify-2.8.3}/.github/workflows/docs.yml +0 -0
- {letify-2.8.2 → letify-2.8.3}/.github/workflows/publish.yml +0 -0
- {letify-2.8.2 → letify-2.8.3}/.gitignore +0 -0
- {letify-2.8.2 → letify-2.8.3}/.researchtree +0 -0
- {letify-2.8.2 → letify-2.8.3}/CLAUDE.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/LICENSE +0 -0
- {letify-2.8.2 → letify-2.8.3}/PROJECT.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/README.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/docs/COMPONENT.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/docs/INTENT.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/docs/NETWORK.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/docs/guide/01-getting-started.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/docs/guide/02-providers.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/docs/guide/03-execution-modes.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/docs/guide/04-environments-and-data.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/docs/guide/05-concurrency.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/docs/guide/06-cost.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/docs/guide/07-troubleshooting.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/docs/guide/08-releasing.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/docs/guide/README.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
- {letify-2.8.2 → letify-2.8.3}/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.2 → letify-2.8.3}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
- {letify-2.8.2 → letify-2.8.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-2.8.2 → letify-2.8.3}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
- {letify-2.8.2 → letify-2.8.3}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
- {letify-2.8.2 → letify-2.8.3}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
- {letify-2.8.2 → letify-2.8.3}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
- {letify-2.8.2 → letify-2.8.3}/docs/guide/ko/README.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/docs/locales/README_ko.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/docs/mkdocs.yml +0 -0
- {letify-2.8.2 → letify-2.8.3}/examples/README.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/examples/nvfp4-lora/.letify.example +0 -0
- {letify-2.8.2 → letify-2.8.3}/examples/nvfp4-lora/00_smoke.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/examples/nvfp4-lora/01_sweep.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/examples/nvfp4-lora/02_resume.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/examples/nvfp4-lora/03_watch.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/examples/nvfp4-lora/README.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/examples/nvfp4-lora/README_ko.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/examples/nvfp4-lora/common.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/examples/nvfp4-lora/recipe.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/cli.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/config/__init__.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/config/inventory.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/config/login.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/config/schema.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/config/secrets.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/config/writer.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/declare/__init__.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/declare/cache.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/declare/env.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/declare/function.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/declare/instance.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/declare/reduce.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/declare/replicas.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/errors.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/install.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/launcher.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/paths.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/protocol/__init__.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/protocol/codec.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/protocol/driver.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/protocol/handle.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/protocol/wire.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/protocol/worker.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/providers/__init__.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/providers/base.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/providers/colab_files.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/providers/colab_keepalive.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/providers/elice.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/providers/kaggle.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/providers/kaggle_adapter.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/providers/local.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/providers/modal.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/providers/modal_adapter.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/providers/naming.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/providers/shell.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/providers/tunnel.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/providers/usage.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/remoting/__init__.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/remoting/capability.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/remoting/device/__init__.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/remoting/device/client.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/remoting/device/cuda.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/remoting/device/executor.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/remoting/device/frames.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/remoting/device/guard.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/remoting/device/tensor.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/remoting/device/trace.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/remoting/device/value.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/remoting/loader.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/remoting/probe.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/render.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/runtime/__init__.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/runtime/broker.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/runtime/channel.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/runtime/lease.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/runtime/pool.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/runtime/telemetry.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/store/__init__.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/store/backends/__init__.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/store/backends/drive.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/store/backends/filesystem.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/store/backends/google_auth.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/store/backends/layout.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/store/backends/objects.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/store/cas.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/store/handoff.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/store/pathdata.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/store/sendorder.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/store/volume.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/stubs.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/tools.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/transport/__init__.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/transport/agent.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/transport/announce.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/transport/link.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/transport/nat.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/transport/pipeline.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/transport/portmap.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/transport/probe.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/transport/quic.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/transport/rendezvous.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/transport/setup.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/transport/sshopts.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify/transport/strategies.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-core/.gitignore +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-core/build.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-core/letify-agent/Cargo.toml +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-core/letify-agent/src/driver.rs +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-core/letify-agent/src/main.rs +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-core/letify-driver/Cargo.toml +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-core/letify-driver/src/client.rs +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-core/letify-driver/src/exports.rs +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-core/letify-driver/src/image.rs +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-core/letify-driver/src/lib.rs +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-core/letify-driver/src/status.rs +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-core/letify-driver/src/table.rs +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-core/letify-quic/Cargo.toml +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-core/letify-quic/src/main.rs +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-core/letify-wire/Cargo.toml +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-core/letify-wire/src/lib.rs +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-core/letify-wire/tests/throughput.rs +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-core/rust-toolchain.toml +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-ext/.vscodeignore +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-ext/LICENSE +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-ext/README.md +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-ext/media/icon.svg +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-ext/src/cli.ts +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-ext/src/extension.ts +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-ext/src/history.ts +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-ext/src/model.ts +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-ext/src/poll.ts +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-ext/src/render.ts +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-ext/test/cli.test.ts +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-ext/test/manifest.test.ts +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-ext/test/model.test.ts +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-ext/test/regressions.test.ts +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify-ext/tsconfig.json +0 -0
- {letify-2.8.2 → letify-2.8.3}/letify_providers.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/scripts/sync-main.sh +0 -0
- {letify-2.8.2 → letify-2.8.3}/scripts/version.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/fake_kaggle_adapter.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/fake_modal_adapter.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_cli.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_cli_output.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_colab_keepalive.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_config.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_core.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_data.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_data_streaming.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_declare.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_device.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_distributed.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_drive_handoff.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_editor_sessions.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_elice.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_frames.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_install.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_kaggle.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_kaggle_cookie.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_kaggle_link.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_link_log.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_notebook.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_parallel_placement.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_paths.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_portmap.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_protocol.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_quic.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_remoting.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_render.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_ship.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_store.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_strategies.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_stubs.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_transport.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_tunnel_setup.py +0 -0
- {letify-2.8.2 → letify-2.8.3}/tests/test_version.py +0 -0
|
@@ -1162,7 +1162,7 @@ The archive is `<workspace root>/env-archives/<env key>-<platform>.tar`, uncompr
|
|
|
1162
1162
|
|
|
1163
1163
|
The client's inbound link is the weak one, so the store path is slower than the build in both directions. Compression costs more than it saves on the volume, so the archive is not compressed.
|
|
1164
1164
|
|
|
1165
|
-
After a successful sync and interpreter check, the runtime says `caching the environment on the workspace root, <size>`, runs tar from the project directory's parent into a temporary name beside the destination, and renames it into place, so a concurrent session never reads a half written archive. Nothing is sent to the client, and no measured rate gates the pack, because the transfer stays inside the provider.
|
|
1165
|
+
After a successful sync and interpreter check, the runtime says `caching the environment on the workspace root, <size>`, runs tar from the project directory's parent into a temporary name beside the destination, and renames it into place, so a concurrent session never reads a half written archive. The temporary name carries 16 random hex digits, `<archive>.<hex>.partial`, not the process id, because every Modal sandbox runs its worker under the same small pid, so two sessions packing the same environment at once wrote one file and one of them failed with tar exit status 2. A pack that fails for any reason removes its temporary file, says `letify: <runtime>: not caching the environment on the workspace root (<reason>)`, and the session goes on: the environment is already built and checked, so a failed cache never fails the session or its call. Nothing is sent to the client, and no measured rate gates the pack, because the transfer stays inside the provider.
|
|
1166
1166
|
|
|
1167
1167
|
At the start of `install_env`, when that archive exists on the runtime, the runtime says `restoring the environment from the workspace root`, extracts it into the project directory's parent, and validates the interpreter with the same check an archive restore uses. The restore refuses a member whose path escapes the destination and allows the absolute symlinks a `.venv` needs. A directory an earlier session left under the same name is removed first. When validation fails, the runtime removes the extracted tree, syncs, and replaces the archive. `Runtime.env_source` is `"workspace"` after this restore. This path is taken before and instead of the store path, and such a provider never uses the client store or the bucket store for its environment.
|
|
1168
1168
|
|
|
@@ -1931,7 +1931,7 @@ A jump host that letify cannot log in to, or whose `authorized_keys` it cannot e
|
|
|
1931
1931
|
|
|
1932
1932
|
> Colab is a `Shell` whose rendezvous is `colab exec`. It has no forward SSH, and its fallback is `colab exec` with the Colab file API.
|
|
1933
1933
|
|
|
1934
|
-
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.
|
|
1934
|
+
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. `colab new` runs through the CLI's own Python with a request timeout of 600 s instead of google-auth's 120 s. google-auth binds `google.auth.transport.requests._DEFAULT_TIMEOUT` into `AuthorizedSession.request`'s signature when the module loads, so setting the module value alone changes nothing: the script sets it to 600 and wraps `AuthorizedSession.request` to default `timeout` to it, then calls the CLI's entry point `colab_cli.cli:main`. That is needed because the assignment request for an A100 can take longer than 120 s to answer and the CLI then fails with `ReadTimeout` while the server goes on assigning the machine. letify's own limit on `colab new` stays 900 s.
|
|
1935
1935
|
|
|
1936
1936
|
**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.
|
|
1937
1937
|
|
|
@@ -131,6 +131,26 @@ UNASSIGN_SCRIPT = (
|
|
|
131
131
|
)
|
|
132
132
|
|
|
133
133
|
|
|
134
|
+
#: Run in the CLI's own Python: ``colab`` with google-auth's request timeout raised to 600 s.
|
|
135
|
+
#: The CLI passes no timeout, so a slow A100 assignment fails at the 120 s default while the
|
|
136
|
+
#: server goes on assigning. google-auth binds the 120 s default into the signature of
|
|
137
|
+
#: ``AuthorizedSession.request`` when it is defined, so setting the module value alone changes
|
|
138
|
+
#: nothing and the method is wrapped as well. Spec "Colab", the Colab CLI paragraph.
|
|
139
|
+
NEW_SCRIPT = (
|
|
140
|
+
"import sys\n"
|
|
141
|
+
"import google.auth.transport.requests as requests\n"
|
|
142
|
+
"requests._DEFAULT_TIMEOUT = 600\n"
|
|
143
|
+
"_request = requests.AuthorizedSession.request\n"
|
|
144
|
+
"def _slow_request(self, *args, **kwargs):\n"
|
|
145
|
+
" kwargs.setdefault('timeout', requests._DEFAULT_TIMEOUT)\n"
|
|
146
|
+
" return _request(self, *args, **kwargs)\n"
|
|
147
|
+
"requests.AuthorizedSession.request = _slow_request\n"
|
|
148
|
+
"from colab_cli.cli import main\n"
|
|
149
|
+
"sys.argv = ['colab', *sys.argv[1:]]\n"
|
|
150
|
+
"sys.exit(main())\n"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
134
154
|
def _parse_assignments(listing: str) -> list[tuple[str, str]]:
|
|
135
155
|
"""The ``(name, endpoint)`` pairs in ``colab sessions`` output, ``?`` for no local state."""
|
|
136
156
|
found = []
|
|
@@ -275,8 +295,21 @@ class Colab(Shell):
|
|
|
275
295
|
|
|
276
296
|
# -- the CLI -------------------------------------------------------------
|
|
277
297
|
|
|
278
|
-
def _cli(
|
|
279
|
-
|
|
298
|
+
def _cli(
|
|
299
|
+
self,
|
|
300
|
+
*args: str,
|
|
301
|
+
timeout: float | None = None,
|
|
302
|
+
stdin: str | None = None,
|
|
303
|
+
script: str | None = None,
|
|
304
|
+
) -> str:
|
|
305
|
+
"""Run the CLI; with ``script``, run that code in the CLI's Python with ``args`` as argv."""
|
|
306
|
+
if script is None:
|
|
307
|
+
command = [*self._colab(), *args]
|
|
308
|
+
else:
|
|
309
|
+
uv = tools.find_uv()
|
|
310
|
+
if uv is None:
|
|
311
|
+
raise ProviderUnavailable(self.kind, tools.missing_uv_message())
|
|
312
|
+
command = [*tools.python_command(tools.COLAB, uv), "-c", script, *args]
|
|
280
313
|
result = subprocess.run(
|
|
281
314
|
command,
|
|
282
315
|
input=stdin,
|
|
@@ -286,9 +319,10 @@ class Colab(Shell):
|
|
|
286
319
|
env=self._env(),
|
|
287
320
|
)
|
|
288
321
|
if result.returncode != 0:
|
|
322
|
+
shown = ["<script>" if part == script else part for part in command]
|
|
289
323
|
raise RuntimeFailure(
|
|
290
|
-
f"`{' '.join(
|
|
291
|
-
command=" ".join(
|
|
324
|
+
f"`{' '.join(shown)}` exited {result.returncode}",
|
|
325
|
+
command=" ".join(shown),
|
|
292
326
|
stderr=(result.stderr or "").strip(),
|
|
293
327
|
)
|
|
294
328
|
return result.stdout
|
|
@@ -482,7 +516,7 @@ class Colab(Shell):
|
|
|
482
516
|
file=sys.stderr,
|
|
483
517
|
)
|
|
484
518
|
try:
|
|
485
|
-
self._cli(*args, timeout=900)
|
|
519
|
+
self._cli(*args, timeout=900, script=NEW_SCRIPT)
|
|
486
520
|
except BaseException:
|
|
487
521
|
# A timed out `colab new` can still leave a server-side assignment behind.
|
|
488
522
|
if before is not None:
|
|
@@ -281,13 +281,15 @@ def workspace_pack_source(root: str, archive: str) -> str:
|
|
|
281
281
|
Spec "Volumes on a persistent runtime": runs ``tar`` from the directory's parent into a
|
|
282
282
|
temporary name and renames it into place, so a concurrent session never reads a half
|
|
283
283
|
written archive. Uncompressed, because gzip costs more than it saves on a local volume.
|
|
284
|
+
The temporary name carries random hex, not the process id, because every Modal sandbox
|
|
285
|
+
runs its worker under the same pid. A failed pack removes its temporary file.
|
|
284
286
|
"""
|
|
285
287
|
return (
|
|
286
|
-
"import os, subprocess\n"
|
|
288
|
+
"import os, secrets, subprocess\n"
|
|
287
289
|
f"_root = os.path.expanduser({root!r}).rstrip('/')\n"
|
|
288
290
|
f"_archive = os.path.expanduser({archive!r})\n"
|
|
289
291
|
"os.makedirs(os.path.dirname(_archive), exist_ok=True)\n"
|
|
290
|
-
"_partial = _archive + '.' +
|
|
292
|
+
"_partial = _archive + '.' + secrets.token_hex(8) + '.partial'\n"
|
|
291
293
|
"try:\n"
|
|
292
294
|
" subprocess.run(['tar', '-cf', _partial, '-C', os.path.dirname(_root),\n"
|
|
293
295
|
" os.path.basename(_root)], check=True, capture_output=True)\n"
|
|
@@ -826,7 +826,16 @@ class Runtime:
|
|
|
826
826
|
"caching the environment on the workspace root"
|
|
827
827
|
+ (f", {size / 1_000_000_000:.1f} GB" if size else "")
|
|
828
828
|
)
|
|
829
|
-
|
|
829
|
+
source = bootstrap.workspace_pack_source(root, self._workspace_archive())
|
|
830
|
+
try:
|
|
831
|
+
self.eval(source, timeout=3600)
|
|
832
|
+
except Exception as error:
|
|
833
|
+
# The environment is built and checked, so a failed cache must not fail the call.
|
|
834
|
+
lines = str(error).strip().splitlines()
|
|
835
|
+
reason = f"{type(error).__name__}: {lines[0]}" if lines else type(error).__name__
|
|
836
|
+
if len(reason) > 200:
|
|
837
|
+
reason = reason[:197] + "..."
|
|
838
|
+
self._say(f"not caching the environment on the workspace root ({reason})")
|
|
830
839
|
|
|
831
840
|
def _environment_volumes(self) -> tuple[Volume, ...]:
|
|
832
841
|
"""Explicit archive volumes, or the automatic cache where the project does not survive.
|
|
@@ -138,7 +138,7 @@ dependencies = [
|
|
|
138
138
|
|
|
139
139
|
[[package]]
|
|
140
140
|
name = "letify-agent"
|
|
141
|
-
version = "2.8.
|
|
141
|
+
version = "2.8.3"
|
|
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.3"
|
|
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.3"
|
|
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.3",
|
|
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.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": "2.8.
|
|
5
|
+
"version": "2.8.3",
|
|
6
6
|
"publisher": "darkpyonix",
|
|
7
7
|
"license": "SEE LICENSE IN LICENSE",
|
|
8
8
|
"repository": {
|
|
@@ -2040,7 +2040,7 @@ class FakeColabCli:
|
|
|
2040
2040
|
self.unassigned: list[str] = []
|
|
2041
2041
|
|
|
2042
2042
|
def __call__(self, command):
|
|
2043
|
-
if "python" in command and "-c" in command:
|
|
2043
|
+
if "python" in command and "-c" in command and "new" not in command:
|
|
2044
2044
|
self.unassigned.append(command[-1])
|
|
2045
2045
|
self.assignments = [a for a in self.assignments if a[1] != command[-1]]
|
|
2046
2046
|
return FakeCompleted()
|
|
@@ -43,6 +43,32 @@ def declare_drive(server: Any, **settings: Any) -> None:
|
|
|
43
43
|
paths.config_path().write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
#: Tests that pin the gate's own rates and arithmetic, so they see the real constants.
|
|
47
|
+
REAL_GATE = {
|
|
48
|
+
"test_the_gate_estimates_a_restore_as_size_over_the_download_and_extraction_rates",
|
|
49
|
+
"test_the_gate_packs_only_when_the_build_took_twice_the_estimated_restore",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.fixture(autouse=True)
|
|
54
|
+
def pack_whatever_the_build_time(
|
|
55
|
+
request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Make the estimated restore negligible, so a session packs however fast it built.
|
|
58
|
+
|
|
59
|
+
A test session builds in one or two seconds, and whether that clears twice the
|
|
60
|
+
estimated restore of its small tree depends on the machine. A test that needs the
|
|
61
|
+
pack would otherwise pass or fail by timing. Tests in ``REAL_GATE`` and tests that set
|
|
62
|
+
the rates themselves see their own values.
|
|
63
|
+
"""
|
|
64
|
+
if request.node.name in REAL_GATE:
|
|
65
|
+
return
|
|
66
|
+
from letify.runtime import session
|
|
67
|
+
|
|
68
|
+
monkeypatch.setattr(session, "DRIVE_DOWNLOAD_BPS", 1e18)
|
|
69
|
+
monkeypatch.setattr(session, "EXTRACT_BPS", 1e18)
|
|
70
|
+
|
|
71
|
+
|
|
46
72
|
def drive_options(server: Any, **extra: Any) -> dict[str, Any]:
|
|
47
73
|
"""Account options of an ephemeral account, after the machine has declared Drive."""
|
|
48
74
|
declare_drive(server)
|
|
@@ -398,6 +398,9 @@ COLAB_CLI = [
|
|
|
398
398
|
]
|
|
399
399
|
|
|
400
400
|
|
|
401
|
+
COLAB_PYTHON = [*COLAB_CLI[:-1], "python"]
|
|
402
|
+
|
|
403
|
+
|
|
401
404
|
def test_a_missing_uv_says_how_to_get_it(isolated_home, patch_which) -> None:
|
|
402
405
|
# The Colab CLI runs through uv, so uv is the one thing that has to be installed.
|
|
403
406
|
patch_which(tools_module, present=False)
|
|
@@ -414,7 +417,9 @@ def test_creating_a_colab_session_names_the_accelerator_the_cli_accepts(
|
|
|
414
417
|
recorder = patch_run(colab_module)
|
|
415
418
|
provider = provider_of(Colab, "colab_a")
|
|
416
419
|
provider.create_session(provider.RTX_PRO_6000, "letify-g4-1")
|
|
417
|
-
assert recorder.command == [
|
|
420
|
+
assert recorder.command == [
|
|
421
|
+
*COLAB_PYTHON, "-c", colab_module.NEW_SCRIPT, "new", "-s", "letify-g4-1", "--gpu", "G4"
|
|
422
|
+
]
|
|
418
423
|
|
|
419
424
|
|
|
420
425
|
def test_creating_a_colab_session_for_a_tpu_asks_for_a_tpu(
|
|
@@ -424,7 +429,9 @@ def test_creating_a_colab_session_for_a_tpu_asks_for_a_tpu(
|
|
|
424
429
|
recorder = patch_run(colab_module)
|
|
425
430
|
provider = provider_of(Colab, "colab_a")
|
|
426
431
|
provider.create_session(provider.v5e1, "letify-v5e1-1")
|
|
427
|
-
assert recorder.command == [
|
|
432
|
+
assert recorder.command == [
|
|
433
|
+
*COLAB_PYTHON, "-c", colab_module.NEW_SCRIPT, "new", "-s", "letify-v5e1-1", "--tpu", "v5e1"
|
|
434
|
+
]
|
|
428
435
|
|
|
429
436
|
|
|
430
437
|
def test_colab_offers_a_cpu_instance_under_the_name_local_uses(isolated_home) -> None:
|
|
@@ -441,7 +448,69 @@ def test_creating_a_colab_session_for_the_cpu_asks_for_no_accelerator(
|
|
|
441
448
|
recorder = patch_run(colab_module)
|
|
442
449
|
provider = provider_of(Colab, "colab_a")
|
|
443
450
|
provider.create_session(provider.cpu, "letify-cpu-1")
|
|
444
|
-
assert recorder.command == [
|
|
451
|
+
assert recorder.command == [
|
|
452
|
+
*COLAB_PYTHON, "-c", colab_module.NEW_SCRIPT, "new", "-s", "letify-cpu-1"
|
|
453
|
+
]
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def test_colab_new_raises_the_request_timeout_to_600_s_and_keeps_the_900_s_limit(
|
|
457
|
+
isolated_home, patch_which, patch_run
|
|
458
|
+
) -> None:
|
|
459
|
+
# Spec "Colab": google-auth's 120 s default is too short for an A100 assignment.
|
|
460
|
+
patch_which(tools_module, present=True)
|
|
461
|
+
recorder = patch_run(colab_module)
|
|
462
|
+
provider = provider_of(Colab, "colab_a")
|
|
463
|
+
provider.create_session(provider.A100, "letify-a100-1")
|
|
464
|
+
call = next(c for c in recorder.calls if "new" in c["command"])
|
|
465
|
+
assert "_DEFAULT_TIMEOUT = 600" in colab_module.NEW_SCRIPT
|
|
466
|
+
assert "colab_cli.cli" in colab_module.NEW_SCRIPT
|
|
467
|
+
assert call["timeout"] == 900
|
|
468
|
+
assert call["command"][-5:] == ["new", "-s", "letify-a100-1", "--gpu", "A100"]
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def test_the_new_script_sets_the_timeout_before_calling_the_cli_and_exits_with_its_code() -> None:
|
|
472
|
+
# Run the script with a stub colab_cli, to see the order and the exit code.
|
|
473
|
+
import subprocess as sp
|
|
474
|
+
import tempfile
|
|
475
|
+
from pathlib import Path
|
|
476
|
+
|
|
477
|
+
with tempfile.TemporaryDirectory() as root:
|
|
478
|
+
stub = Path(root, "stub")
|
|
479
|
+
(stub / "colab_cli").mkdir(parents=True)
|
|
480
|
+
(stub / "colab_cli" / "__init__.py").write_text("")
|
|
481
|
+
(stub / "colab_cli" / "cli.py").write_text(
|
|
482
|
+
"import sys\n"
|
|
483
|
+
"import google.auth.transport.requests as r\n"
|
|
484
|
+
"def main():\n"
|
|
485
|
+
" print(r._DEFAULT_TIMEOUT, r.AuthorizedSession().request('GET', 'u'), sys.argv)\n"
|
|
486
|
+
" raise SystemExit(3)\n"
|
|
487
|
+
)
|
|
488
|
+
google = Path(root, "stub", "google", "auth", "transport")
|
|
489
|
+
google.mkdir(parents=True)
|
|
490
|
+
for d in (google.parent.parent, google.parent, google):
|
|
491
|
+
(d / "__init__.py").write_text("")
|
|
492
|
+
(google / "requests.py").write_text(
|
|
493
|
+
"_DEFAULT_TIMEOUT = 120\n"
|
|
494
|
+
"class AuthorizedSession:\n"
|
|
495
|
+
" def request(self, method, url, timeout=_DEFAULT_TIMEOUT):\n"
|
|
496
|
+
" return timeout\n"
|
|
497
|
+
)
|
|
498
|
+
done = sp.run(
|
|
499
|
+
[sys.executable, "-c", colab_module.NEW_SCRIPT, "new", "-s", "x"],
|
|
500
|
+
capture_output=True, text=True, env={"PYTHONPATH": str(stub)},
|
|
501
|
+
)
|
|
502
|
+
assert done.returncode == 3, done.stderr
|
|
503
|
+
assert done.stdout.strip() == "600 600 ['colab', 'new', '-s', 'x']"
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def test_a_failed_colab_new_raises_with_the_command_and_stderr(
|
|
507
|
+
isolated_home, patch_which, patch_run
|
|
508
|
+
) -> None:
|
|
509
|
+
provider = _colab_with(patch_which, patch_run, FakeColabCli(new_fails=True))
|
|
510
|
+
with pytest.raises(letify.RuntimeFailure) as caught:
|
|
511
|
+
provider.create_session(provider.A100, "letify-a100-1")
|
|
512
|
+
assert "ReadTimeout" in caught.value.stderr
|
|
513
|
+
assert "new -s letify-a100-1 --gpu A100" in caught.value.command
|
|
445
514
|
|
|
446
515
|
|
|
447
516
|
def test_a_failing_cli_command_names_the_command_and_the_end_of_its_stderr(
|
|
@@ -2706,6 +2706,81 @@ def test_a_persistent_provider_that_builds_its_venv_elsewhere_archives_on_its_wo
|
|
|
2706
2706
|
assert "caching the environment on the workspace root" in err, err
|
|
2707
2707
|
|
|
2708
2708
|
|
|
2709
|
+
def test_the_workspace_pack_names_its_temporary_file_without_the_process_id(
|
|
2710
|
+
tmp_path: Path, monkeypatch
|
|
2711
|
+
) -> None:
|
|
2712
|
+
# Spec "Volumes on a persistent runtime": every Modal sandbox runs its worker under the
|
|
2713
|
+
# same small pid, so the temporary name carries 16 random hex digits instead.
|
|
2714
|
+
import re
|
|
2715
|
+
import subprocess
|
|
2716
|
+
|
|
2717
|
+
project = tmp_path / "proj"
|
|
2718
|
+
project.mkdir()
|
|
2719
|
+
(project / "a.txt").write_text("a")
|
|
2720
|
+
archive = tmp_path / "vol" / "env.tar"
|
|
2721
|
+
seen: list[str] = []
|
|
2722
|
+
real = subprocess.run
|
|
2723
|
+
|
|
2724
|
+
def record(cmd, *args, **kwargs):
|
|
2725
|
+
seen.append(cmd[2])
|
|
2726
|
+
return real(cmd, *args, **kwargs)
|
|
2727
|
+
|
|
2728
|
+
monkeypatch.setattr(subprocess, "run", record)
|
|
2729
|
+
source = bootstrap.workspace_pack_source(str(project), str(archive))
|
|
2730
|
+
for _ in range(2):
|
|
2731
|
+
exec(source, {})
|
|
2732
|
+
assert len(seen) == 2 and seen[0] != seen[1]
|
|
2733
|
+
for name in seen:
|
|
2734
|
+
assert re.fullmatch(re.escape(str(archive)) + r"\.[0-9a-f]{16}\.partial", name), name
|
|
2735
|
+
assert archive.is_file()
|
|
2736
|
+
assert not list(archive.parent.glob("*.partial"))
|
|
2737
|
+
|
|
2738
|
+
|
|
2739
|
+
def test_a_pack_that_fails_removes_its_temporary_file(tmp_path: Path) -> None:
|
|
2740
|
+
# Spec "Volumes on a persistent runtime": a failed pack leaves no partial behind.
|
|
2741
|
+
project = tmp_path / "proj"
|
|
2742
|
+
project.mkdir()
|
|
2743
|
+
archive = tmp_path / "vol" / "env.tar"
|
|
2744
|
+
source = bootstrap.workspace_pack_source(str(project), str(archive)).replace(
|
|
2745
|
+
"['tar', '-cf', _partial,", "['sh', '-c', 'echo x > \"$0\"; exit 2', _partial,"
|
|
2746
|
+
)
|
|
2747
|
+
with pytest.raises(subprocess.CalledProcessError):
|
|
2748
|
+
exec(source, {})
|
|
2749
|
+
assert not list(archive.parent.glob("*.partial")), "a temporary archive was left behind"
|
|
2750
|
+
assert not archive.exists()
|
|
2751
|
+
|
|
2752
|
+
|
|
2753
|
+
def test_a_failed_workspace_pack_does_not_fail_the_session(
|
|
2754
|
+
uv_project: Path, tmp_path: Path, monkeypatch, capsys
|
|
2755
|
+
) -> None:
|
|
2756
|
+
# Spec "Volumes on a persistent runtime": the environment is already built, so a failed
|
|
2757
|
+
# cache is said and the session goes on.
|
|
2758
|
+
provider, volume = _elsewhere_provider(tmp_path)
|
|
2759
|
+
env = Env()
|
|
2760
|
+
real = bootstrap.workspace_pack_source
|
|
2761
|
+
|
|
2762
|
+
def failing(root: str, archive: str) -> str:
|
|
2763
|
+
return real(root, archive).replace(
|
|
2764
|
+
"['tar', '-cf', _partial,", "['sh', '-c', 'echo x > \"$0\"; exit 2', _partial,"
|
|
2765
|
+
)
|
|
2766
|
+
|
|
2767
|
+
monkeypatch.setattr(bootstrap, "workspace_pack_source", failing)
|
|
2768
|
+
instance = Instance(provider, gpu=None)._placed("remote")
|
|
2769
|
+
runtime = provider.start(instance, env, name="lab-1", volumes=(volume,))
|
|
2770
|
+
try:
|
|
2771
|
+
assert runtime.env_source == "sync"
|
|
2772
|
+
assert runtime.eval("__letify_value__ = 6 * 7") == 42
|
|
2773
|
+
archive = _workspace_archive(env, runtime.platform)
|
|
2774
|
+
finally:
|
|
2775
|
+
runtime.shutdown()
|
|
2776
|
+
err = capsys.readouterr().err
|
|
2777
|
+
assert (
|
|
2778
|
+
"not caching the environment on the workspace root (RemoteError: CalledProcessError" in err
|
|
2779
|
+
), err
|
|
2780
|
+
assert not archive.exists()
|
|
2781
|
+
assert not list(archive.parent.glob("*.partial"))
|
|
2782
|
+
|
|
2783
|
+
|
|
2709
2784
|
def test_a_second_session_restores_the_environment_from_the_workspace_root(
|
|
2710
2785
|
uv_project: Path, tmp_path: Path, monkeypatch, capsys
|
|
2711
2786
|
) -> None:
|
|
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.2 → letify-2.8.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-2.8.2 → letify-2.8.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-2.8.2 → letify-2.8.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-2.8.2 → letify-2.8.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
|