letify 2.10.1__tar.gz → 2.10.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.10.1 → letify-2.10.3}/PKG-INFO +1 -1
- {letify-2.10.1 → letify-2.10.3}/docs/SPEC.md +2 -1
- {letify-2.10.1 → letify-2.10.3}/letify/__init__.py +1 -1
- {letify-2.10.1 → letify-2.10.3}/letify/protocol/worker.py +12 -9
- {letify-2.10.1 → letify-2.10.3}/letify/providers/kaggle.py +10 -0
- {letify-2.10.1 → letify-2.10.3}/letify-core/Cargo.lock +2 -2
- {letify-2.10.1 → letify-2.10.3}/letify-core/Cargo.toml +1 -1
- {letify-2.10.1 → letify-2.10.3}/letify-ext/package-lock.json +2 -2
- {letify-2.10.1 → letify-2.10.3}/letify-ext/package.json +1 -1
- {letify-2.10.1 → letify-2.10.3}/pyproject.toml +1 -1
- {letify-2.10.1 → letify-2.10.3}/tests/test_kaggle.py +34 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_run_takeover.py +110 -0
- {letify-2.10.1 → letify-2.10.3}/.github/workflows/ci.yml +0 -0
- {letify-2.10.1 → letify-2.10.3}/.github/workflows/docs.yml +0 -0
- {letify-2.10.1 → letify-2.10.3}/.github/workflows/publish-crates.yml +0 -0
- {letify-2.10.1 → letify-2.10.3}/.github/workflows/publish.yml +0 -0
- {letify-2.10.1 → letify-2.10.3}/.github/workflows/reserve-crates.yml +0 -0
- {letify-2.10.1 → letify-2.10.3}/.gitignore +0 -0
- {letify-2.10.1 → letify-2.10.3}/.researchtree +0 -0
- {letify-2.10.1 → letify-2.10.3}/CLAUDE.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/LICENSE +0 -0
- {letify-2.10.1 → letify-2.10.3}/PROJECT.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/README.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/docs/COMPONENT.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/docs/INTENT.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/docs/NETWORK.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/docs/guide/01-getting-started.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/docs/guide/02-providers.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/docs/guide/03-execution-modes.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/docs/guide/04-environments-and-data.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/docs/guide/05-concurrency.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/docs/guide/06-cost.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/docs/guide/07-troubleshooting.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/docs/guide/08-releasing.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/docs/guide/README.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
- {letify-2.10.1 → letify-2.10.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.10.1 → letify-2.10.3}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
- {letify-2.10.1 → letify-2.10.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.10.1 → letify-2.10.3}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
- {letify-2.10.1 → letify-2.10.3}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
- {letify-2.10.1 → letify-2.10.3}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
- {letify-2.10.1 → letify-2.10.3}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
- {letify-2.10.1 → letify-2.10.3}/docs/guide/ko/README.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/docs/locales/README_ko.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/docs/mkdocs.yml +0 -0
- {letify-2.10.1 → letify-2.10.3}/examples/README.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/examples/nvfp4-lora/.letify.example +0 -0
- {letify-2.10.1 → letify-2.10.3}/examples/nvfp4-lora/00_smoke.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/examples/nvfp4-lora/01_sweep.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/examples/nvfp4-lora/02_resume.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/examples/nvfp4-lora/03_watch.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/examples/nvfp4-lora/README.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/examples/nvfp4-lora/README_ko.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/examples/nvfp4-lora/common.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/examples/nvfp4-lora/recipe.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/cli.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/config/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/config/inventory.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/config/login.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/config/schema.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/config/secrets.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/config/writer.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/declare/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/declare/cache.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/declare/env.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/declare/exit.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/declare/function.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/declare/instance.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/declare/reduce.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/declare/replicas.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/errors.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/install.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/launcher.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/paths.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/protocol/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/protocol/codec.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/protocol/driver.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/protocol/handle.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/protocol/wire.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/providers/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/providers/base.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/providers/colab.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/providers/colab_files.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/providers/colab_keepalive.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/providers/elice.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/providers/kaggle_adapter.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/providers/local.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/providers/modal.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/providers/modal_adapter.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/providers/naming.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/providers/shell.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/providers/tunnel.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/providers/usage.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/remoting/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/remoting/device/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/remoting/device/client.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/remoting/device/cuda.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/remoting/device/executor.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/remoting/device/frames.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/remoting/device/guard.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/remoting/device/tensor.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/remoting/device/trace.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/remoting/device/value.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/render.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/runtime/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/runtime/bootstrap.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/runtime/broker.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/runtime/channel.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/runtime/control.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/runtime/lease.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/runtime/pool.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/runtime/session.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/runtime/takeover.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/runtime/telemetry.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/runtime/workers.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/store/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/store/backends/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/store/backends/drive.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/store/backends/filesystem.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/store/backends/google_auth.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/store/backends/layout.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/store/backends/objects.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/store/cas.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/store/handoff.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/store/pathdata.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/store/sendorder.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/store/volume.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/stubs.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/tools.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/transport/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/transport/agent.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/transport/announce.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/transport/link.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/transport/nat.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/transport/pipeline.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/transport/portmap.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/transport/probe.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/transport/quic.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/transport/rendezvous.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/transport/setup.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/transport/sshopts.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify/transport/strategies.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-core/.gitignore +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-core/build.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-core/letify/Cargo.toml +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-core/letify/LICENSE +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-core/letify/README.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-core/letify/src/lib.rs +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-core/letify-quic/Cargo.toml +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-core/letify-quic/src/main.rs +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-core/rust-toolchain.toml +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-ext/.vscodeignore +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-ext/LICENSE +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-ext/README.md +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-ext/media/icon.svg +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-ext/src/cli.ts +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-ext/src/extension.ts +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-ext/src/history.ts +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-ext/src/model.ts +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-ext/src/poll.ts +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-ext/src/render.ts +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-ext/test/cli.test.ts +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-ext/test/manifest.test.ts +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-ext/test/model.test.ts +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-ext/test/regressions.test.ts +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify-ext/tsconfig.json +0 -0
- {letify-2.10.1 → letify-2.10.3}/letify_providers.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/scripts/sync-main.sh +0 -0
- {letify-2.10.1 → letify-2.10.3}/scripts/version.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/conftest.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/fake_kaggle_adapter.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/fake_modal_adapter.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_cli.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_cli_output.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_colab_keepalive.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_config.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_core.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_data.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_data_streaming.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_declare.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_device.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_device_compile.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_distributed.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_drive_handoff.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_drive_store.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_editor_sessions.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_elice.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_frames.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_install.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_kaggle_cookie.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_kaggle_link.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_link_log.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_notebook.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_parallel_placement.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_paths.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_portmap.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_protocol.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_providers.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_punch_sockets.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_quic.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_remoting.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_render.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_run_control.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_runtime.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_shell_workers.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_ship.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_store.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_strategies.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_stubs.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_transport.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_tunnel_setup.py +0 -0
- {letify-2.10.1 → letify-2.10.3}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: letify
|
|
3
|
-
Version: 2.10.
|
|
3
|
+
Version: 2.10.3
|
|
4
4
|
Summary: Declarations that become infrastructure.
|
|
5
5
|
Project-URL: Homepage, https://github.com/DarkPyonix/letify
|
|
6
6
|
Project-URL: Documentation, https://github.com/DarkPyonix/letify/tree/main/docs
|
|
@@ -281,6 +281,7 @@ Not opening a link is a decision of this project, not a rule of Kaggle's. Two so
|
|
|
281
281
|
- **Token input and notebook owner.** `letify login kaggle <alias>` accepts `--key` or hidden input asking for the API token exactly as kaggle.com shows it, including `KGAT_`. It requires that prefix and a nonempty body with no whitespace; it does not strip or reformat the token. Length is not fixed because the measured 32-character body establishes a working token, not a universal length contract. A malformed token is refused with a message naming what to paste, before any cookie verification or file write. The token is not verified online at login. A renewal asks for each credential rather than reusing what is on disk, and a blank answer keeps the stored one. The two credentials expire on different clocks: the cookie is a thirty day web session and the token lasts until it is rotated, so renewing one must not demand the other. `letify login kaggle <alias> --replace` therefore prompts for the token and then the cookie, each saying that blank keeps what is stored, and writes only what was answered. A blank answer with nothing stored is refused, naming what to paste. Silently reusing a stored credential would make a renewal a no-op with no way to replace a credential that stopped working. Login retains `--username` or a username prompt and stores it in `username` beside the credentials with mode 0600. This is only the owner for `kernels delete -y <owner>/<slug>`, not an authentication credential. Explicit input avoids another authenticated lookup and does not mistake a cookie display name for an owner. The owner is not read from an authenticated call. Reading an existing `access_token` file strips leading and trailing whitespace, including a trailing newline, before CLI calls or reuse by login when `--key` is omitted. Explicit `--key` and prompted token input still reject whitespace rather than reformatting it, so adding an owner with `--username` does not require pasting the token again. The owner is a public Kaggle username, not a secret, and its file uses the same owner-only permissions as the account directory. Login refuses a missing or blank owner before cookie verification or any file write. Nothing is written until both credentials, the owner and cookie verification pass. Before any session starts or notebook is created, `open_channel` requires a readable, nonempty `username` file. An incomplete account is refused with `ConfigError` naming the login command, so a run cannot reach teardown without a notebook owner.
|
|
282
282
|
- **The cookie carries an ``exp`` claim, but that claim is not proof the cookie still works.** The `CLIENT-TOKEN` cookie is an `alg:none` JWT whose `exp` claim was thirty days after the login that minted it, in every case observed. But a live account has been seen with over nine days left on that claim while every internal call, including the plain `users.UsersService/GetCurrentUser` read, answered as a refused, anonymous session: Kaggle invalidates a cookie server side, for reasons this project has not pinned down, without the claim changing to say so. So `exp` is useful for exactly one direction: a cookie past its `exp` is certainly dead. It says nothing about a cookie before its `exp`, which may be live or may already be refused.
|
|
283
283
|
- **Two checks, for two costs.** An offline check reads only the claim, costs no network call, and is what `letify providers` shows as the account's `exp claim says N days left (not checked live; see letify usage)`, or `EXPIRED` once `exp` has passed. An online check additionally calls `GetCurrentUser`, the same call the login proved the cookie with, and is made once per session start (`open_channel`) and once per `letify usage` read, each result trusted for sixty seconds so starting several runtimes together costs one round trip rather than one per runtime. A run whose cookie fails either check is refused with `ConfigError`: missing or past `exp` says exactly that; refused online says "the Kaggle cookie has not expired, but Kaggle no longer accepts it", which is the one fact that check establishes, never a guess at why. A run is also refused when fewer than one hour is left on the `exp` claim, since starting a session the claim says the cookie cannot outlive risks it dying mid run with no way to tell that apart from any other lost runtime; this offline guard and the online check both have to pass.
|
|
284
|
+
- **A refused session can answer HTTP 400 with an empty body.** When the `XSRF-TOKEN` cookie in the stored cookie is no longer the one Kaggle issued for the session, `GetCurrentUser` answers HTTP 400, 401 or 403 instead of an empty account. The online check treats those three statuses as Kaggle refusing the cookie and reports `the Kaggle cookie was refused (HTTP <status>)` with the instruction to log in again. Any other HTTP status, such as 500, and any transport failure is not an answer about the cookie: the check reports `the Kaggle cookie could not be checked: HTTP <status>` or the exception class name, and the run fails as an infrastructure failure rather than as a bad cookie.
|
|
284
285
|
- The online check is not made on every call into a running session, only at the two points above, so it is not the cost of every request a declared function makes.
|
|
285
286
|
- Everything after that registration is what every other provider does: the session builds the declared environment with `uv sync`, enters a workspace root, transfers files and checks the worker's interpreter. Kaggle gets no exemption from any of it.
|
|
286
287
|
|
|
@@ -1089,7 +1090,7 @@ A call has exactly one owner at a time, and the session still ends with the call
|
|
|
1089
1090
|
|
|
1090
1091
|
1. **The registry keeps an orphan.** The record also holds `project`, the project root the call was sent from, and `lease_deadline`, the Unix time at which the runtime's lease ends unless renewed, rewritten at each renewal. It also holds what a second process needs to reach the call and finish it: `door`, the path of the worker's socket below; `config`, the project `config.toml` that names the account; and `call_dir`, `blobs` and `outputs`, the runtime paths and the output locations to write back, each location stored relative to `project` when it lies inside it. No secret is among them. A record whose owner pid is gone is no longer removed when read while its `lease_deadline` is in the future: `letify runs` lists it with `owner` `gone` and the seconds left, and it can be taken over. After the deadline it is removed as stale.
|
|
1091
1092
|
2. **The worker keeps a second door.** While a call runs, the worker listens on a Unix socket on the runtime, `<workspace root>/.letify/run-<run id>.sock`, mode 0600. When that path is longer than 100 bytes, the limit of a Unix socket address, the worker and the relay reach the socket through `/proc/self/fd/<n>`, a descriptor of its directory, so it stays at that path; where `/proc` is missing the worker reports that the run has no second door. A channel that closes while a call runs, with no shutdown frame before it, does not end the worker: the body keeps running until the lease deadline passes. A shutdown frame is different: it is the owner ending the run on purpose, as [A deliberate shutdown](#run-shutdown) describes. Output the body makes while no channel is open is kept, the last 256 KiB per stream, and is sent to the next owner first. A body that returns while no channel is open keeps its result for a new owner until the lease deadline; on a provider with no lease the worker ends then, as before.
|
|
1092
|
-
3. **The new owner connects.** `letify takeover <run>` reads the record, opens a channel to the same runtime through the record's provider and runtime name, and starts a relay to that socket instead of a new worker. It then sends `adopt` with the run id. The worker answers once with the call's state, which is `running` or `finished`, and from then on sends the call's output and its reply on the new channel, the reply as the last piece of the same answer. The old channel, if still open, is told `moved` on the call's stream
|
|
1093
|
+
3. **The new owner connects.** `letify takeover <run>` reads the record, opens a channel to the same runtime through the record's provider and runtime name, and starts a relay to that socket instead of a new worker. It then sends `adopt` with the run id. The worker answers once with the call's state, which is `running` or `finished`, and from then on sends the call's output and its reply on the new channel, the reply as the last piece of the same answer. The old channel, if still open, is told `moved` on the call's stream, after the worker has switched the call's output to the new channel, so everything the body prints once the old owner knows of the move reaches the new owner. It answers every later request on it with `moved`. A door takes one connection at a time and no request but `adopt` before it, and a second owner can adopt after the first one's connection has closed. The adoption is atomic with respect to the old channel: if the old channel closes while the adoption is under way, the worker decides what that close means only after the adoption has finished, sees that the call has moved, and does nothing. The new owner's channel is then never marked as having lost its input, and no exit hook runs for the close.
|
|
1093
1094
|
4. **The old owner lets go.** When the old owner is alive, takeover first asks it through its control socket to release the run. Its waiting call raises `OwnershipMoved` naming the run and the new owner's pid, it stops renewing that runtime's lease, and it drops the runtime from its pool without stopping it. A `keep_alive` block in the old owner no longer holds that session. An old owner that does not answer within 5 s is treated as gone.
|
|
1094
1095
|
5. **The call ends at the new owner.** `letify takeover` prints the body's output as it arrives. When the call returns it prints the `repr` of the value, or with `--result PATH` writes the value with pickle to `PATH`, and exits 0. When the body raises it prints the traceback and exits 1. With `--json` the last line is one object with `run`, `ok`, `value_repr` or `error`, and `result_path`. The call's outputs are written back under the record's `project`, not the current directory. The session is then ended.
|
|
1095
1096
|
6. **The same from Python.** `letify.takeover(run)` does the same in the calling process and returns the value or raises the body's exception, as a call does. It holds no handle: it returns once the call has ended and the session is gone. A darkpyonix kernel uses this to adopt a run started elsewhere.
|
|
@@ -3549,11 +3549,12 @@ def _reader_ended(door, shutdown=False):
|
|
|
3549
3549
|
Spec "A deliberate shutdown": a shutdown frame on the channel that owns a running call
|
|
3550
3550
|
ends the body and the worker. End of input alone leaves the body for a takeover.
|
|
3551
3551
|
"""
|
|
3552
|
-
if door is None and _TAKE["moved"]:
|
|
3553
|
-
# The call was adopted away; this channel closing is the old owner leaving.
|
|
3554
|
-
return
|
|
3555
3552
|
during = False
|
|
3556
3553
|
with _TAKE_COND:
|
|
3554
|
+
if door is None and _TAKE["moved"]:
|
|
3555
|
+
# The call was adopted away; this channel closing is the old owner leaving. Read
|
|
3556
|
+
# under the lock, because an adoption in progress holds it until it has moved.
|
|
3557
|
+
return
|
|
3557
3558
|
if door is None or (_TAKE["owner"] is door and _TAKE["live"]):
|
|
3558
3559
|
during = _TAKE["run"] is not None
|
|
3559
3560
|
if door is not None or during:
|
|
@@ -3702,12 +3703,6 @@ def _door_adopt(door, stream, request):
|
|
|
3702
3703
|
moved = _moved_outcome()
|
|
3703
3704
|
if _TAKE["owner"] is not None:
|
|
3704
3705
|
moved["last"] = True
|
|
3705
|
-
# Told on a thread, because a former owner that has stopped reading can block the write.
|
|
3706
|
-
told = threading.Thread(
|
|
3707
|
-
target=_answer, args=(former, former_stream, moved), daemon=True
|
|
3708
|
-
)
|
|
3709
|
-
told.start()
|
|
3710
|
-
told.join(2.0)
|
|
3711
3706
|
state = {
|
|
3712
3707
|
"run": run,
|
|
3713
3708
|
"state": "finished" if _TAKE["finished"] else "running",
|
|
@@ -3728,6 +3723,14 @@ def _door_adopt(door, stream, request):
|
|
|
3728
3723
|
)
|
|
3729
3724
|
_SENDER = door.sender
|
|
3730
3725
|
_TAKE_COND.notify_all()
|
|
3726
|
+
# Told last, after the output has been switched, so that nothing the body prints from
|
|
3727
|
+
# the moment the former owner learns of the move is sent to a channel that has stopped
|
|
3728
|
+
# reading. A thread, because a former owner that has stopped reading can block the write.
|
|
3729
|
+
told = threading.Thread(
|
|
3730
|
+
target=_answer, args=(former, former_stream, moved), daemon=True
|
|
3731
|
+
)
|
|
3732
|
+
told.start()
|
|
3733
|
+
told.join(2.0)
|
|
3731
3734
|
|
|
3732
3735
|
|
|
3733
3736
|
def _deliver_call(stream, outcome):
|
|
@@ -89,6 +89,9 @@ RETIRED = {
|
|
|
89
89
|
KAGGLE_INTERNAL = "https://www.kaggle.com/api/i/"
|
|
90
90
|
KERNELS_SERVICE = "kernels.KernelsService/"
|
|
91
91
|
USERS_SERVICE = "users.UsersService/"
|
|
92
|
+
#: HTTP statuses ``GetCurrentUser`` answers for a session Kaggle no longer accepts. A stale
|
|
93
|
+
#: ``XSRF-TOKEN`` cookie is answered with 400 and an empty body.
|
|
94
|
+
REFUSING_STATUSES = frozenset({400, 401, 403})
|
|
92
95
|
|
|
93
96
|
#: The host that routes to a session's Jupyter server, and the language id for Python.
|
|
94
97
|
JUPYTER_PROXY_HOST = "https://kkb-production.jupyter-proxy.kaggle.net"
|
|
@@ -256,6 +259,13 @@ def verify_cookie(cookie: str) -> str:
|
|
|
256
259
|
try:
|
|
257
260
|
with urlopen(request) as response:
|
|
258
261
|
body = json.loads(response.read())
|
|
262
|
+
except urllib.error.HTTPError as exc:
|
|
263
|
+
if exc.code in REFUSING_STATUSES:
|
|
264
|
+
raise ValueError(
|
|
265
|
+
f"the Kaggle cookie was refused (HTTP {exc.code}); log in to kaggle.com and "
|
|
266
|
+
f"copy a fresh cookie"
|
|
267
|
+
) from None
|
|
268
|
+
raise ValueError(f"the Kaggle cookie could not be checked: HTTP {exc.code}") from None
|
|
259
269
|
except (urllib.error.URLError, OSError, ValueError) as exc:
|
|
260
270
|
raise ValueError(f"the Kaggle cookie could not be checked: {type(exc).__name__}") from None
|
|
261
271
|
name = body.get("displayName") or (body.get("user") or {}).get("displayName")
|
|
@@ -138,11 +138,11 @@ dependencies = [
|
|
|
138
138
|
|
|
139
139
|
[[package]]
|
|
140
140
|
name = "letify"
|
|
141
|
-
version = "2.10.
|
|
141
|
+
version = "2.10.3"
|
|
142
142
|
|
|
143
143
|
[[package]]
|
|
144
144
|
name = "letify-quic"
|
|
145
|
-
version = "2.10.
|
|
145
|
+
version = "2.10.3"
|
|
146
146
|
dependencies = [
|
|
147
147
|
"quinn",
|
|
148
148
|
"rcgen",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letify-status",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "letify-status",
|
|
9
|
-
"version": "2.10.
|
|
9
|
+
"version": "2.10.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.10.
|
|
5
|
+
"version": "2.10.3",
|
|
6
6
|
"publisher": "darkpyonix",
|
|
7
7
|
"license": "SEE LICENSE IN LICENSE",
|
|
8
8
|
"repository": {
|
|
@@ -1759,3 +1759,37 @@ def test_a_worker_source_that_arrives_whole_still_runs() -> None:
|
|
|
1759
1759
|
# The worker announces itself and then reads, so closing its input ends it cleanly.
|
|
1760
1760
|
assert b"arrived" not in done.stderr, done.stderr[-2000:]
|
|
1761
1761
|
assert done.stdout, "the worker said nothing, so it never started"
|
|
1762
|
+
|
|
1763
|
+
|
|
1764
|
+
def _http_error(code: int):
|
|
1765
|
+
import io
|
|
1766
|
+
import urllib.error
|
|
1767
|
+
|
|
1768
|
+
return urllib.error.HTTPError("https://www.kaggle.com/", code, "x", {}, io.BytesIO(b""))
|
|
1769
|
+
|
|
1770
|
+
|
|
1771
|
+
@pytest.mark.parametrize("code", [400, 401, 403])
|
|
1772
|
+
def test_an_http_error_that_refuses_the_session_reads_as_a_refused_cookie(
|
|
1773
|
+
monkeypatch, code
|
|
1774
|
+
) -> None:
|
|
1775
|
+
"""Spec "Kaggle account": 400, 401 and 403 from GetCurrentUser mean the cookie is refused."""
|
|
1776
|
+
from letify.providers import kaggle as kaggle_module
|
|
1777
|
+
|
|
1778
|
+
def refuse(request, timeout=None):
|
|
1779
|
+
raise _http_error(code)
|
|
1780
|
+
|
|
1781
|
+
monkeypatch.setattr(kaggle_module, "urlopen", refuse)
|
|
1782
|
+
with pytest.raises(ValueError, match=rf"was refused \(HTTP {code}\)"):
|
|
1783
|
+
kaggle_module.verify_cookie(make_cookie())
|
|
1784
|
+
|
|
1785
|
+
|
|
1786
|
+
def test_a_server_error_during_the_cookie_check_names_its_status(monkeypatch) -> None:
|
|
1787
|
+
"""Spec "Kaggle account": any other status is a failed check, with the status shown."""
|
|
1788
|
+
from letify.providers import kaggle as kaggle_module
|
|
1789
|
+
|
|
1790
|
+
def fail(request, timeout=None):
|
|
1791
|
+
raise _http_error(503)
|
|
1792
|
+
|
|
1793
|
+
monkeypatch.setattr(kaggle_module, "urlopen", fail)
|
|
1794
|
+
with pytest.raises(ValueError, match="could not be checked: HTTP 503"):
|
|
1795
|
+
kaggle_module.verify_cookie(make_cookie())
|
|
@@ -289,6 +289,8 @@ def test_the_new_owner_takes_the_record_and_removes_it_when_the_call_ends(world,
|
|
|
289
289
|
until(lambda: any(r["pid"] == os.getpid() for r in control.list_runs()), "the new record")
|
|
290
290
|
row = next(r for r in control.list_runs() if r["pid"] == os.getpid())
|
|
291
291
|
assert row["run"] == owner.run
|
|
292
|
+
# The body returns only once the old owner has let go, which is after the adoption.
|
|
293
|
+
owner.answer()
|
|
292
294
|
owner.finish_body()
|
|
293
295
|
taking.join()
|
|
294
296
|
assert control.list_runs(orphans=True) == []
|
|
@@ -742,3 +744,111 @@ def test_detaching_a_persistent_channel_sends_no_shutdown(monkeypatch) -> None:
|
|
|
742
744
|
channel.detach()
|
|
743
745
|
assert wire.SHUTDOWN not in sent
|
|
744
746
|
assert sent == []
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
#: The old owner's channel closes while the worker is still adopting the call to the new owner.
|
|
750
|
+
#: The adoption holds the call's lock, so the close has to wait for it and then see the move.
|
|
751
|
+
RACE_PROBE = textwrap.dedent(
|
|
752
|
+
"""
|
|
753
|
+
import json, sys, threading, time
|
|
754
|
+
from pathlib import Path
|
|
755
|
+
|
|
756
|
+
from letify.protocol import worker
|
|
757
|
+
|
|
758
|
+
out = sys.argv[1]
|
|
759
|
+
text = worker.SOURCE.rstrip()
|
|
760
|
+
assert text.endswith("_serve()")
|
|
761
|
+
space = {"__name__": "probe"}
|
|
762
|
+
exec(compile(text[: -len("_serve()")], "worker", "exec"), space)
|
|
763
|
+
hooks = []
|
|
764
|
+
space["_run_exit_hooks"] = lambda reason: hooks.append(reason)
|
|
765
|
+
take, cond = space["_TAKE"], space["_TAKE_COND"]
|
|
766
|
+
take.update(run="abcdef012345", live=True, moved=False)
|
|
767
|
+
new_owner = object()
|
|
768
|
+
with cond:
|
|
769
|
+
closing = threading.Thread(target=space["_reader_ended"], args=(None, False))
|
|
770
|
+
closing.start()
|
|
771
|
+
time.sleep(0.5)
|
|
772
|
+
# The adoption finishes under the lock, after the close began.
|
|
773
|
+
take.update(owner=new_owner, moved=True, live=True, ended_input=False)
|
|
774
|
+
closing.join(30)
|
|
775
|
+
Path(out).write_text(json.dumps({
|
|
776
|
+
"live": take["live"], "ended_input": take["ended_input"], "hooks": hooks,
|
|
777
|
+
}))
|
|
778
|
+
"""
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def test_the_old_owners_channel_closing_during_the_adoption_leaves_the_new_owner_live(
|
|
783
|
+
tmp_path,
|
|
784
|
+
) -> None:
|
|
785
|
+
script = tmp_path / "race.py"
|
|
786
|
+
script.write_text(RACE_PROBE, encoding="utf-8")
|
|
787
|
+
out = tmp_path / "race.out"
|
|
788
|
+
root = str(Path(letify.__file__).resolve().parent.parent)
|
|
789
|
+
subprocess.run(
|
|
790
|
+
[sys.executable, str(script), str(out)],
|
|
791
|
+
check=True, timeout=60, env={**os.environ, "PYTHONPATH": root},
|
|
792
|
+
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
793
|
+
)
|
|
794
|
+
assert json.loads(out.read_text(encoding="utf-8")) == {
|
|
795
|
+
"live": True, "ended_input": False, "hooks": [],
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
#: Adopts a call with stand-in channels and records whether the worker already sends output
|
|
800
|
+
#: to the new owner at the moment the old owner is told the call has moved.
|
|
801
|
+
ORDER_PROBE = textwrap.dedent(
|
|
802
|
+
"""
|
|
803
|
+
import json, sys
|
|
804
|
+
from pathlib import Path
|
|
805
|
+
|
|
806
|
+
from letify.protocol import worker
|
|
807
|
+
|
|
808
|
+
out = sys.argv[1]
|
|
809
|
+
text = worker.SOURCE.rstrip()
|
|
810
|
+
assert text.endswith("_serve()")
|
|
811
|
+
space = {"__name__": "probe"}
|
|
812
|
+
exec(compile(text[: -len("_serve()")], "worker", "exec"), space)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
class Channel:
|
|
816
|
+
def message(self, *args):
|
|
817
|
+
pass
|
|
818
|
+
|
|
819
|
+
def frame(self, *args):
|
|
820
|
+
pass
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
class Connection:
|
|
824
|
+
def settimeout(self, value):
|
|
825
|
+
pass
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
class Door:
|
|
829
|
+
sender = Channel()
|
|
830
|
+
connection = Connection()
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
door, former = Door(), Channel()
|
|
834
|
+
seen = []
|
|
835
|
+
space["_answer"] = lambda sender, stream, outcome: seen.append(space["_SENDER"] is door.sender)
|
|
836
|
+
space["_SENDER"] = former
|
|
837
|
+
space["_TAKE"].update(run="abcdef012345", live=True, stream=3)
|
|
838
|
+
space["_door_adopt"](door, 7, {"run": "abcdef012345", "owner": 1234})
|
|
839
|
+
Path(out).write_text(json.dumps(seen))
|
|
840
|
+
"""
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def test_output_after_the_old_owner_is_told_the_call_moved_goes_to_the_new_owner(tmp_path) -> None:
|
|
845
|
+
script = tmp_path / "order.py"
|
|
846
|
+
script.write_text(ORDER_PROBE, encoding="utf-8")
|
|
847
|
+
out = tmp_path / "order.out"
|
|
848
|
+
root = str(Path(letify.__file__).resolve().parent.parent)
|
|
849
|
+
subprocess.run(
|
|
850
|
+
[sys.executable, str(script), str(out)],
|
|
851
|
+
check=True, timeout=60, env={**os.environ, "PYTHONPATH": root},
|
|
852
|
+
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
853
|
+
)
|
|
854
|
+
assert json.loads(out.read_text(encoding="utf-8")) == [True]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|