letify 2.5.0__tar.gz → 2.5.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.5.0 → letify-2.5.1}/PKG-INFO +1 -1
- {letify-2.5.0 → letify-2.5.1}/docs/SPEC.md +2 -2
- {letify-2.5.0 → letify-2.5.1}/letify/__init__.py +1 -1
- {letify-2.5.0 → letify-2.5.1}/letify/protocol/worker.py +6 -1
- {letify-2.5.0 → letify-2.5.1}/letify/providers/kaggle.py +5 -1
- {letify-2.5.0 → letify-2.5.1}/letify/providers/kaggle_adapter.py +13 -2
- {letify-2.5.0 → letify-2.5.1}/letify-core/Cargo.lock +3 -3
- {letify-2.5.0 → letify-2.5.1}/letify-core/Cargo.toml +1 -1
- {letify-2.5.0 → letify-2.5.1}/letify-ext/package-lock.json +2 -2
- {letify-2.5.0 → letify-2.5.1}/letify-ext/package.json +1 -1
- {letify-2.5.0 → letify-2.5.1}/pyproject.toml +1 -1
- {letify-2.5.0 → letify-2.5.1}/tests/test_kaggle.py +67 -0
- {letify-2.5.0 → letify-2.5.1}/.github/workflows/ci.yml +0 -0
- {letify-2.5.0 → letify-2.5.1}/.github/workflows/docs.yml +0 -0
- {letify-2.5.0 → letify-2.5.1}/.github/workflows/publish.yml +0 -0
- {letify-2.5.0 → letify-2.5.1}/.gitignore +0 -0
- {letify-2.5.0 → letify-2.5.1}/.researchtree +0 -0
- {letify-2.5.0 → letify-2.5.1}/CLAUDE.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/LICENSE +0 -0
- {letify-2.5.0 → letify-2.5.1}/PROJECT.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/README.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/docs/COMPONENT.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/docs/INTENT.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/docs/NETWORK.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/docs/guide/01-getting-started.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/docs/guide/02-providers.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/docs/guide/03-execution-modes.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/docs/guide/04-environments-and-data.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/docs/guide/05-concurrency.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/docs/guide/06-cost.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/docs/guide/07-troubleshooting.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/docs/guide/08-releasing.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/docs/guide/README.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
- {letify-2.5.0 → letify-2.5.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.5.0 → letify-2.5.1}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
- {letify-2.5.0 → letify-2.5.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.5.0 → letify-2.5.1}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
- {letify-2.5.0 → letify-2.5.1}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
- {letify-2.5.0 → letify-2.5.1}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
- {letify-2.5.0 → letify-2.5.1}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
- {letify-2.5.0 → letify-2.5.1}/docs/guide/ko/README.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/docs/locales/README_ko.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/docs/mkdocs.yml +0 -0
- {letify-2.5.0 → letify-2.5.1}/examples/README.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/examples/nvfp4-lora/.letify.example +0 -0
- {letify-2.5.0 → letify-2.5.1}/examples/nvfp4-lora/00_smoke.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/examples/nvfp4-lora/01_sweep.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/examples/nvfp4-lora/02_resume.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/examples/nvfp4-lora/03_watch.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/examples/nvfp4-lora/README.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/examples/nvfp4-lora/README_ko.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/examples/nvfp4-lora/common.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/examples/nvfp4-lora/recipe.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/cli.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/config/__init__.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/config/inventory.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/config/login.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/config/schema.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/config/secrets.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/config/writer.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/declare/__init__.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/declare/cache.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/declare/env.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/declare/function.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/declare/instance.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/declare/reduce.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/declare/replicas.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/errors.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/install.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/launcher.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/paths.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/protocol/__init__.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/protocol/codec.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/protocol/driver.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/protocol/handle.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/protocol/wire.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/providers/__init__.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/providers/base.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/providers/colab.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/providers/colab_files.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/providers/colab_keepalive.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/providers/elice.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/providers/local.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/providers/modal.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/providers/modal_adapter.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/providers/naming.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/providers/shell.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/providers/tunnel.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/providers/usage.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/remoting/__init__.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/remoting/capability.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/remoting/device/__init__.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/remoting/device/client.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/remoting/device/cuda.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/remoting/device/executor.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/remoting/device/frames.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/remoting/device/guard.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/remoting/device/tensor.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/remoting/device/trace.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/remoting/device/value.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/remoting/loader.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/remoting/probe.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/render.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/runtime/__init__.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/runtime/bootstrap.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/runtime/broker.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/runtime/channel.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/runtime/lease.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/runtime/pool.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/runtime/session.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/runtime/telemetry.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/store/__init__.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/store/backends/__init__.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/store/backends/filesystem.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/store/backends/google_auth.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/store/backends/layout.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/store/backends/objects.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/store/cas.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/store/pathdata.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/store/sendorder.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/store/volume.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/stubs.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/tools.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/transport/__init__.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/transport/agent.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/transport/announce.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/transport/link.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/transport/nat.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/transport/pipeline.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/transport/portmap.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/transport/probe.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/transport/quic.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/transport/rendezvous.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/transport/setup.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/transport/sshopts.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify/transport/strategies.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-core/.gitignore +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-core/build.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-core/letify-agent/Cargo.toml +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-core/letify-agent/src/driver.rs +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-core/letify-agent/src/main.rs +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-core/letify-driver/Cargo.toml +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-core/letify-driver/src/client.rs +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-core/letify-driver/src/exports.rs +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-core/letify-driver/src/image.rs +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-core/letify-driver/src/lib.rs +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-core/letify-driver/src/status.rs +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-core/letify-driver/src/table.rs +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-core/letify-quic/Cargo.toml +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-core/letify-quic/src/main.rs +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-core/letify-wire/Cargo.toml +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-core/letify-wire/src/lib.rs +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-core/letify-wire/tests/throughput.rs +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-core/rust-toolchain.toml +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-ext/.vscodeignore +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-ext/LICENSE +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-ext/README.md +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-ext/media/icon.svg +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-ext/src/cli.ts +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-ext/src/extension.ts +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-ext/src/history.ts +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-ext/src/model.ts +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-ext/src/poll.ts +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-ext/src/render.ts +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-ext/test/cli.test.ts +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-ext/test/manifest.test.ts +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-ext/test/model.test.ts +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-ext/test/regressions.test.ts +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify-ext/tsconfig.json +0 -0
- {letify-2.5.0 → letify-2.5.1}/letify_providers.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/scripts/sync-main.sh +0 -0
- {letify-2.5.0 → letify-2.5.1}/scripts/version.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/conftest.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/fake_kaggle_adapter.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/fake_modal_adapter.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_cli.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_cli_output.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_colab_keepalive.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_config.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_core.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_data.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_data_streaming.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_declare.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_device.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_distributed.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_editor_sessions.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_elice.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_frames.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_install.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_kaggle_cookie.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_kaggle_link.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_link_log.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_notebook.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_parallel_placement.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_paths.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_portmap.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_protocol.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_providers.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_quic.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_remoting.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_render.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_runtime.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_ship.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_store.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_strategies.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_stubs.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_transport.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_tunnel_setup.py +0 -0
- {letify-2.5.0 → letify-2.5.1}/tests/test_version.py +0 -0
|
@@ -269,7 +269,7 @@ letify starts the session itself, on a notebook created fresh for that run alone
|
|
|
269
269
|
The URL is split into the server base, the URL without its query string, and the token, the `token` query parameter when present. A routed proxy URL carries no query, so the token is in the path and every REST request goes to `<base>/api/...` with no `token` query and no `Authorization` header, through the standard library HTTP client. The full URL and the token never appear in a message, a log or a command line; a message names only the host.
|
|
270
270
|
|
|
271
271
|
1. **Start.** `open_channel` reads the account's cookie, refusing the run with a `ConfigError` when it is missing, past its `exp`, or within one hour of it. It creates a fresh notebook for this run (see "Kaggle session token chain" below), started with the session's internet access enabled (`compute.internet.isEnabled` in `CommitAndRun`), because the environment step downloads from PyPI and the uv installer from astral.sh; Kaggle grants that access only to a phone-verified account, and a run it refuses fails with Kaggle's own error. It runs the token chain to get the routed URL, waits for `<base>/api/status` to answer, then creates one kernel with `POST <base>/api/kernels` and body `{"name": "python3"}` and keeps its id for the runtime. `stop` deletes the kernel with `DELETE <base>/api/kernels/<id>`, cancels the run, then deletes the notebook through the official CLI only. All three are best effort, and a notebook neither delete reaches is reported with a warning naming the notebook, never the cookie, so it can be deleted by hand on kaggle.com.
|
|
272
|
-
2. **The worker.** The session carries one worker for the life of the runtime, not one program per request. The Kaggle adapter, `letify/providers/kaggle_adapter.py`, is a bridge process started by `uv run --no-project --python 3.13 --with "jupyter-kernel-client<1" python -P`, and it receives the URL and the kernel id in the environment variables `LETIFY_JUPYTER_URL` and `LETIFY_KERNEL_ID`. It executes one cell in that kernel which runs until the runtime ends, and that cell carries a shim rather than the bootstrap stub of Channels. A kernel answers `input()` and nothing else: the cell's `sys.stdin` is a `TextIOWrapper` whose `buffer` reads empty instead of raising `input_request`, so a worker reading binary from standard input would read nothing at all. The shim therefore reads base64 lines with `input()`, decodes each one, and writes the bytes into a pipe the worker reads as its standard input, with the worker's own standard output going back out as `stream` messages. The bridge moves the lines on two threads, one per kernel channel, and never on one: what it reads on its standard input one thread hands to the cell by answering `input_request`, and every `stream` message the cell writes the other thread copies to its standard output. One thread is required rather than a convenience. Answering an `input_request` from the same loop that reads the cell's output blocks that loop in a standard-input read, and while it blocks no `stream` message is delivered. The worker announces itself with a `stream` message, so a single-loop bridge holds that hello behind an `input_request` the channel does not answer until it has seen the hello, which is a deadlock, and the same deadlock returns on every large response, where the cell reads the next request while the worker is still writing output. The letify process never imports `jupyter_kernel_client`.
|
|
272
|
+
2. **The worker.** The session carries one worker for the life of the runtime, not one program per request. The Kaggle adapter, `letify/providers/kaggle_adapter.py`, is a bridge process started by `uv run --no-project --python 3.13 --with "jupyter-kernel-client<1" python -P`, and it receives the URL and the kernel id in the environment variables `LETIFY_JUPYTER_URL` and `LETIFY_KERNEL_ID`. It executes one cell in that kernel which runs until the runtime ends, and that cell carries a shim rather than the bootstrap stub of Channels. A kernel answers `input()` and nothing else: the cell's `sys.stdin` is a `TextIOWrapper` whose `buffer` reads empty instead of raising `input_request`, so a worker reading binary from standard input would read nothing at all. The shim therefore reads base64 lines with `input()`, decodes each one, and writes the bytes into a pipe the worker reads as its standard input, with the worker's own standard output going back out as `stream` messages. A line that does not decode is reported on standard error as `the letify shim could not decode a line of <length> characters` before the shim stops, because a line truncated in transit otherwise ends the shim silently, closes the worker's standard input, and leaves the worker compiling whatever arrived. The client writes at most 36 KiB of bytes per line, so no line exceeds 48 KiB of base64 and the worker source crosses as several lines rather than as one reply of 145 KiB; this is the limit Channels already states for a Modal sandbox, for the same reason. The bridge moves the lines on two threads, one per kernel channel, and never on one: what it reads on its standard input one thread hands to the cell by answering `input_request`, and every `stream` message the cell writes the other thread copies to its standard output. One thread is required rather than a convenience. Answering an `input_request` from the same loop that reads the cell's output blocks that loop in a standard-input read, and while it blocks no `stream` message is delivered. The worker announces itself with a `stream` message, so a single-loop bridge holds that hello behind an `input_request` the channel does not answer until it has seen the hello, which is a deadlock, and the same deadlock returns on every large response, where the cell reads the next request while the worker is still writing output. The letify process never imports `jupyter_kernel_client`.
|
|
273
273
|
|
|
274
274
|
The channel is a `FramedChannel` with `text_frames` set, so the worker writes each frame as a base64 line, as the Modal sandbox already does for the same reason: a kernel's `stream` messages carry text, not bytes. `persistent_channel` is therefore true, which is what gives Kaggle an object table, a blob table that keeps a large argument across calls, and the option of `host="local"` if a link ever makes it worth using. The bridge's standard input is an unbuffered pipe, and a blocking write into a full pipe returns short when a signal arrives while it waits, as `SIGCHLD` does when a process with a handler for it, such as one where PyTorch has forked data loader workers, sees an earlier worker exit. So the channel writes each base64 line until every byte of it is in the pipe, and counts the piece as sent only then.
|
|
275
275
|
|
|
@@ -656,7 +656,7 @@ A **persistent channel** keeps one worker process alive behind a pipe pair. Mess
|
|
|
656
656
|
|
|
657
657
|
A **one-shot channel** can only run a command and collect its output. Every call starts a fresh process, so nothing persists. It exists because some transports offer nothing more, and it refuses the operations that need persistence rather than pretending.
|
|
658
658
|
|
|
659
|
-
The worker source cannot be sent on standard input as a script, because `python -` reads to end of file before compiling anything and the pipe has to stay open for requests. A small bootstrap stub passed with `-c` reads one line holding a decimal byte count from `sys.stdin.buffer`, reads that many bytes of UTF-8 worker source, executes them, and leaves standard input where it was. From then on both pipes carry frames only.
|
|
659
|
+
The worker source cannot be sent on standard input as a script, because `python -` reads to end of file before compiling anything and the pipe has to stay open for requests. A small bootstrap stub passed with `-c` reads one line holding a decimal byte count from `sys.stdin.buffer`, reads that many bytes of UTF-8 worker source, executes them, and leaves standard input where it was. From then on both pipes carry frames only. The stub compares what it read against the count and raises `SystemExit` with `the letify worker source arrived short: <read> of <expected> bytes` rather than compiling a truncated program, because a program cut anywhere fails as a `SyntaxError` at whatever construct the cut landed inside, which names the symptom and hides the cause.
|
|
660
660
|
|
|
661
661
|
### Frames <!-- id: frames -->
|
|
662
662
|
|
|
@@ -40,7 +40,12 @@ BOOTSTRAP = (
|
|
|
40
40
|
"import sys;"
|
|
41
41
|
"b=sys.stdin.buffer;"
|
|
42
42
|
"n=int(b.readline());"
|
|
43
|
-
"
|
|
43
|
+
"s=b.read(n);"
|
|
44
|
+
# Spec "Channels": a source cut anywhere compiles as whatever construct the cut landed
|
|
45
|
+
# inside, or runs and exits without serving, so the count is checked before it is run.
|
|
46
|
+
"len(s)==n or sys.exit("
|
|
47
|
+
"'the letify worker source arrived short: %d of %d bytes'%(len(s),n));"
|
|
48
|
+
"exec(compile(s.decode(),'letify-worker','exec'))"
|
|
44
49
|
)
|
|
45
50
|
|
|
46
51
|
_BODY = r'''
|
|
@@ -1006,7 +1006,11 @@ class KaggleChannel(FramedChannel):
|
|
|
1006
1006
|
text_frames = True
|
|
1007
1007
|
|
|
1008
1008
|
#: Bytes handed to the bridge per write, matching the Modal channel.
|
|
1009
|
-
|
|
1009
|
+
#: Bytes per base64 line. Spec "Kaggle Jupyter Server session": a kernel carries one
|
|
1010
|
+
#: line as one input reply, so the 108 KB worker source went as one reply of 145 KiB.
|
|
1011
|
+
#: 36 KiB is a multiple of three, so every line decodes on its own, and it is the limit
|
|
1012
|
+
#: Channels already states for a Modal sandbox, for the same reason.
|
|
1013
|
+
WRITE_LIMIT = 36 * 1024
|
|
1010
1014
|
|
|
1011
1015
|
def __init__(
|
|
1012
1016
|
self, command: list[str], env: dict[str, str], *, name: str, session: Session
|
|
@@ -34,7 +34,12 @@ STUB = (
|
|
|
34
34
|
"import sys;"
|
|
35
35
|
"b=sys.stdin.buffer;"
|
|
36
36
|
"n=int(b.readline());"
|
|
37
|
-
"
|
|
37
|
+
"s=b.read(n);"
|
|
38
|
+
# Spec "Channels": a source cut anywhere compiles as whatever construct the cut landed
|
|
39
|
+
# inside, or runs and exits without serving, so the count is checked before it is run.
|
|
40
|
+
"len(s)==n or sys.exit("
|
|
41
|
+
"'the letify worker source arrived short: %d of %d bytes'%(len(s),n));"
|
|
42
|
+
"exec(compile(s.decode(),'letify-worker','exec'))"
|
|
38
43
|
)
|
|
39
44
|
|
|
40
45
|
#: The cell that keeps the worker alive. It reads base64 lines with input(), because that is
|
|
@@ -67,7 +72,13 @@ while True:
|
|
|
67
72
|
try:
|
|
68
73
|
_worker.stdin.write(base64.b64decode(_text))
|
|
69
74
|
_worker.stdin.flush()
|
|
70
|
-
except
|
|
75
|
+
except ValueError:
|
|
76
|
+
sys.stderr.write(
|
|
77
|
+
"the letify shim could not decode a line of %%d characters\\n" %% len(_text)
|
|
78
|
+
)
|
|
79
|
+
sys.stderr.flush()
|
|
80
|
+
break
|
|
81
|
+
except OSError:
|
|
71
82
|
break
|
|
72
83
|
|
|
73
84
|
try:
|
|
@@ -138,7 +138,7 @@ dependencies = [
|
|
|
138
138
|
|
|
139
139
|
[[package]]
|
|
140
140
|
name = "letify-agent"
|
|
141
|
-
version = "2.5.
|
|
141
|
+
version = "2.5.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.5.
|
|
149
|
+
version = "2.5.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.5.
|
|
167
|
+
version = "2.5.1"
|
|
168
168
|
|
|
169
169
|
[[package]]
|
|
170
170
|
name = "libc"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letify-status",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "letify-status",
|
|
9
|
-
"version": "2.5.
|
|
9
|
+
"version": "2.5.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.5.
|
|
5
|
+
"version": "2.5.1",
|
|
6
6
|
"publisher": "darkpyonix",
|
|
7
7
|
"license": "SEE LICENSE IN LICENSE",
|
|
8
8
|
"repository": {
|
|
@@ -1649,3 +1649,70 @@ def test_a_gap_that_never_clears_stops_blocking_the_account(
|
|
|
1649
1649
|
time.sleep = monkeypatched
|
|
1650
1650
|
|
|
1651
1651
|
assert sum(slept) <= 40.0, f"waited {sum(slept):.0f} s, over four times the interval"
|
|
1652
|
+
|
|
1653
|
+
|
|
1654
|
+
def test_the_stub_refuses_a_worker_source_that_arrived_short() -> None:
|
|
1655
|
+
"""Spec "Channels": a cut program fails as a SyntaxError that hides the cause.
|
|
1656
|
+
|
|
1657
|
+
KICT reported a Kaggle session dying with "unterminated triple-quoted string literal"
|
|
1658
|
+
inside the worker source. A truncated source compiles as whatever construct the cut
|
|
1659
|
+
landed inside, so the stub checks the count it was given instead.
|
|
1660
|
+
"""
|
|
1661
|
+
import subprocess
|
|
1662
|
+
|
|
1663
|
+
from letify.protocol.worker import BOOTSTRAP, source
|
|
1664
|
+
|
|
1665
|
+
body = source().encode("utf-8")
|
|
1666
|
+
# The count says the whole source; the pipe carries all but the last 284 bytes.
|
|
1667
|
+
done = subprocess.run(
|
|
1668
|
+
[sys.executable, "-u", "-c", BOOTSTRAP],
|
|
1669
|
+
input=b"%d\n" % len(body) + body[:-284],
|
|
1670
|
+
capture_output=True,
|
|
1671
|
+
timeout=120,
|
|
1672
|
+
)
|
|
1673
|
+
assert b"arrived short" in done.stderr, done.stderr[-2000:]
|
|
1674
|
+
assert str(len(body)).encode() in done.stderr
|
|
1675
|
+
assert b"SyntaxError" not in done.stderr
|
|
1676
|
+
|
|
1677
|
+
|
|
1678
|
+
def test_the_shim_says_which_line_it_could_not_decode() -> None:
|
|
1679
|
+
"""Spec "Kaggle Jupyter Server session": a line cut in transit ended the shim silently.
|
|
1680
|
+
|
|
1681
|
+
It closed the worker's standard input, so the worker compiled whatever had arrived and
|
|
1682
|
+
the only report was the worker's own SyntaxError.
|
|
1683
|
+
"""
|
|
1684
|
+
import subprocess
|
|
1685
|
+
|
|
1686
|
+
from letify.providers.kaggle_adapter import SHIM, STUB
|
|
1687
|
+
|
|
1688
|
+
cell = SHIM % {"stub": STUB}
|
|
1689
|
+
done = subprocess.run(
|
|
1690
|
+
[sys.executable, "-u", "-c", cell],
|
|
1691
|
+
# Five base64 characters, which is not a whole group, so decoding raises.
|
|
1692
|
+
input=b"abcde\n",
|
|
1693
|
+
capture_output=True,
|
|
1694
|
+
timeout=120,
|
|
1695
|
+
)
|
|
1696
|
+
assert b"could not decode a line" in done.stderr, done.stderr[-2000:]
|
|
1697
|
+
assert b"of 5 characters" in done.stderr
|
|
1698
|
+
|
|
1699
|
+
|
|
1700
|
+
def test_a_kernel_line_stays_inside_the_limit_a_kernel_carries() -> None:
|
|
1701
|
+
"""Spec "Kaggle Jupyter Server session": at most 36 KiB per line, so no line is 145 KiB.
|
|
1702
|
+
|
|
1703
|
+
The worker source is about 108 KB, which went as one base64 reply of about 145 KiB.
|
|
1704
|
+
Channels states the same 36 KiB limit for a Modal sandbox, for the same reason.
|
|
1705
|
+
"""
|
|
1706
|
+
import base64
|
|
1707
|
+
|
|
1708
|
+
from letify.protocol.worker import source
|
|
1709
|
+
from letify.providers.kaggle import KaggleChannel
|
|
1710
|
+
|
|
1711
|
+
assert KaggleChannel.WRITE_LIMIT <= 36 * 1024
|
|
1712
|
+
# A multiple of three bytes, so every line decodes on its own.
|
|
1713
|
+
assert KaggleChannel.WRITE_LIMIT % 3 == 0
|
|
1714
|
+
# 48 KiB of base64 plus the newline, which is the bound Channels states for Modal.
|
|
1715
|
+
assert len(base64.b64encode(b"x" * KaggleChannel.WRITE_LIMIT)) <= 48 * 1024
|
|
1716
|
+
assert len(source().encode("utf-8")) > KaggleChannel.WRITE_LIMIT, (
|
|
1717
|
+
"the source must cross as several lines, or this pins nothing"
|
|
1718
|
+
)
|
|
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.5.0 → letify-2.5.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.5.0 → letify-2.5.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.5.0 → letify-2.5.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.5.0 → letify-2.5.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
|
|
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
|