letify 1.2.3__tar.gz → 1.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {letify-1.2.3 → letify-1.2.4}/PKG-INFO +1 -1
- {letify-1.2.3 → letify-1.2.4}/letify/__init__.py +1 -1
- {letify-1.2.3 → letify-1.2.4}/letify/transport/pipeline.py +90 -35
- {letify-1.2.3 → letify-1.2.4}/letify-core/Cargo.lock +3 -3
- {letify-1.2.3 → letify-1.2.4}/letify-core/Cargo.toml +1 -1
- {letify-1.2.3 → letify-1.2.4}/letify-ext/package-lock.json +2 -2
- {letify-1.2.3 → letify-1.2.4}/letify-ext/package.json +1 -1
- {letify-1.2.3 → letify-1.2.4}/pyproject.toml +1 -1
- {letify-1.2.3 → letify-1.2.4}/tests/test_link_log.py +31 -0
- {letify-1.2.3 → letify-1.2.4}/.github/workflows/ci.yml +0 -0
- {letify-1.2.3 → letify-1.2.4}/.github/workflows/docs.yml +0 -0
- {letify-1.2.3 → letify-1.2.4}/.github/workflows/publish.yml +0 -0
- {letify-1.2.3 → letify-1.2.4}/.gitignore +0 -0
- {letify-1.2.3 → letify-1.2.4}/.researchtree +0 -0
- {letify-1.2.3 → letify-1.2.4}/LICENSE +0 -0
- {letify-1.2.3 → letify-1.2.4}/README.md +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/guide/01-getting-started.md +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/guide/02-providers.md +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/guide/03-execution-modes.md +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/guide/04-environments-and-data.md +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/guide/05-concurrency.md +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/guide/06-cost.md +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/guide/07-troubleshooting.md +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/guide/08-releasing.md +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/guide/README.md +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/guide/ko/02-/355/224/204/353/241/234/353/260/224/354/235/264/353/215/224.md" +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/guide/ko/04-/355/231/230/352/262/275/352/263/274-/353/215/260/354/235/264/355/204/260.md" +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/guide/ko/README.md +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/locales/README_ko.md +0 -0
- {letify-1.2.3 → letify-1.2.4}/docs/mkdocs.yml +0 -0
- {letify-1.2.3 → letify-1.2.4}/examples/README.md +0 -0
- {letify-1.2.3 → letify-1.2.4}/examples/nvfp4-lora/.letify.example +0 -0
- {letify-1.2.3 → letify-1.2.4}/examples/nvfp4-lora/00_smoke.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/examples/nvfp4-lora/01_sweep.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/examples/nvfp4-lora/02_resume.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/examples/nvfp4-lora/03_watch.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/examples/nvfp4-lora/README.md +0 -0
- {letify-1.2.3 → letify-1.2.4}/examples/nvfp4-lora/README_ko.md +0 -0
- {letify-1.2.3 → letify-1.2.4}/examples/nvfp4-lora/common.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/examples/nvfp4-lora/recipe.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/cli.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/config/__init__.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/config/inventory.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/config/login.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/config/schema.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/config/secrets.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/config/writer.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/declare/__init__.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/declare/cache.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/declare/env.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/declare/function.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/declare/instance.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/errors.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/install.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/launcher.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/paths.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/protocol/__init__.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/protocol/codec.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/protocol/driver.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/protocol/handle.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/protocol/wire.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/protocol/worker.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/providers/__init__.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/providers/base.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/providers/colab.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/providers/colab_files.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/providers/colab_keepalive.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/providers/elice.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/providers/kaggle.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/providers/kaggle_adapter.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/providers/local.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/providers/modal.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/providers/modal_adapter.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/providers/naming.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/providers/shell.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/providers/tunnel.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/providers/usage.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/remoting/__init__.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/remoting/capability.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/remoting/device/__init__.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/remoting/device/client.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/remoting/device/cuda.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/remoting/device/executor.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/remoting/device/frames.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/remoting/device/guard.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/remoting/device/tensor.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/remoting/device/trace.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/remoting/device/value.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/remoting/loader.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/remoting/probe.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/render.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/runtime/__init__.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/runtime/bootstrap.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/runtime/channel.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/runtime/lease.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/runtime/pool.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/runtime/session.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/runtime/telemetry.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/store/__init__.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/store/backends/__init__.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/store/backends/filesystem.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/store/backends/google_auth.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/store/backends/layout.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/store/backends/objects.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/store/cas.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/store/pathdata.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/store/sendorder.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/store/volume.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/stubs.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/tools.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/transport/__init__.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/transport/agent.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/transport/announce.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/transport/link.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/transport/nat.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/transport/probe.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/transport/rendezvous.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/transport/setup.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/transport/sshopts.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify/transport/strategies.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-core/.gitignore +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-core/build.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-core/letify-agent/Cargo.toml +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-core/letify-agent/src/driver.rs +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-core/letify-agent/src/main.rs +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-core/letify-driver/Cargo.toml +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-core/letify-driver/src/client.rs +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-core/letify-driver/src/exports.rs +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-core/letify-driver/src/image.rs +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-core/letify-driver/src/lib.rs +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-core/letify-driver/src/status.rs +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-core/letify-driver/src/table.rs +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-core/letify-wire/Cargo.toml +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-core/letify-wire/src/lib.rs +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-core/letify-wire/tests/throughput.rs +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-core/rust-toolchain.toml +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-ext/.vscodeignore +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-ext/LICENSE +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-ext/README.md +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-ext/media/icon.svg +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-ext/src/cli.ts +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-ext/src/extension.ts +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-ext/src/history.ts +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-ext/src/model.ts +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-ext/src/poll.ts +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-ext/src/render.ts +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-ext/test/cli.test.ts +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-ext/test/manifest.test.ts +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-ext/test/model.test.ts +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-ext/test/regressions.test.ts +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify-ext/tsconfig.json +0 -0
- {letify-1.2.3 → letify-1.2.4}/letify_providers.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/scripts/sync-main.sh +0 -0
- {letify-1.2.3 → letify-1.2.4}/scripts/version.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/conftest.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/fake_kaggle_adapter.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/fake_modal_adapter.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_cli.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_cli_output.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_colab_keepalive.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_config.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_core.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_data.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_data_streaming.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_declare.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_device.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_editor_sessions.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_elice.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_frames.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_install.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_kaggle.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_kaggle_cookie.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_notebook.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_paths.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_protocol.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_providers.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_remoting.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_render.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_runtime.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_ship.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_store.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_strategies.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_stubs.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_transport.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_tunnel_setup.py +0 -0
- {letify-1.2.3 → letify-1.2.4}/tests/test_version.py +0 -0
|
@@ -40,6 +40,15 @@ DEFAULT_MIN_MIB_PER_S = 10.0
|
|
|
40
40
|
DEFAULT_MAX_RTT_MS = 300.0
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
class _BelowFloor(ProviderUnavailable):
|
|
44
|
+
"""Every connected strategy measured below the floor.
|
|
45
|
+
|
|
46
|
+
Spec "Choosing a link": this does not end the race, because a strategy still
|
|
47
|
+
connecting may clear the floor. The race raises it only once nothing is left to wait
|
|
48
|
+
for, and it reaches the caller as the ``ProviderUnavailable`` it is.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
43
52
|
@dataclass(frozen=True)
|
|
44
53
|
class LinkFloor:
|
|
45
54
|
"""The slowest link an account accepts. A probe below this fails the connection."""
|
|
@@ -402,43 +411,86 @@ class Pipeline:
|
|
|
402
411
|
|
|
403
412
|
probed = {i for i, s in enumerate(applicable) if getattr(s, "probed", True)}
|
|
404
413
|
deadline = time.monotonic() + self.timeout
|
|
414
|
+
held = {s.name for s in applicable if not getattr(s, "probed", True)}
|
|
415
|
+
#: Strategies already offered to _choose, so a later round considers only new ones.
|
|
416
|
+
seen: set[int] = set()
|
|
417
|
+
rejected: _BelowFloor | None = None
|
|
418
|
+
|
|
419
|
+
# Spec "Choosing a link": the race is not over until a link is accepted. A round
|
|
420
|
+
# that ends with every connected strategy below the floor drops the grace period
|
|
421
|
+
# and waits out the rest of the timeout for the strategies still connecting.
|
|
422
|
+
while True:
|
|
423
|
+
with condition:
|
|
424
|
+
while len(outcomes) < len(applicable):
|
|
425
|
+
fresh = [
|
|
426
|
+
index
|
|
427
|
+
for index, outcome in outcomes.items()
|
|
428
|
+
if index not in seen and not isinstance(outcome, Exception)
|
|
429
|
+
]
|
|
430
|
+
if state["first"] is None and probed <= outcomes.keys() and fresh:
|
|
431
|
+
# Every probed strategy failed, so an unprobed one is all that is left.
|
|
432
|
+
break
|
|
433
|
+
now = time.monotonic()
|
|
434
|
+
limit = deadline if state["first"] is None else state["first"] + self.grace
|
|
435
|
+
if now >= limit:
|
|
436
|
+
break
|
|
437
|
+
condition.wait(limit - now)
|
|
438
|
+
settled = dict(outcomes)
|
|
439
|
+
|
|
440
|
+
connected = []
|
|
441
|
+
for index, strategy in enumerate(applicable):
|
|
442
|
+
if index in seen or index not in settled:
|
|
443
|
+
continue
|
|
444
|
+
seen.add(index)
|
|
445
|
+
outcome = settled[index]
|
|
446
|
+
if isinstance(outcome, Exception):
|
|
447
|
+
reasons.append(f"{strategy.name}: {outcome}")
|
|
448
|
+
else:
|
|
449
|
+
connected.append(outcome)
|
|
450
|
+
|
|
451
|
+
if connected:
|
|
452
|
+
connected.sort(key=lambda link: link.rank)
|
|
453
|
+
try:
|
|
454
|
+
chosen, measured = self._choose(connected, reasons, held)
|
|
455
|
+
except _BelowFloor as below:
|
|
456
|
+
rejected = below
|
|
457
|
+
else:
|
|
458
|
+
with condition:
|
|
459
|
+
state["decided"] = True
|
|
460
|
+
for index, event in enumerate(cancels):
|
|
461
|
+
if index not in seen:
|
|
462
|
+
event.set()
|
|
463
|
+
self._report_unsettled(applicable, seen, reasons)
|
|
464
|
+
first = state["first_name"]
|
|
465
|
+
if first is not None and first != chosen.strategy:
|
|
466
|
+
self._say(f"switching from {first} to {chosen.strategy}")
|
|
467
|
+
return chosen, measured
|
|
468
|
+
|
|
469
|
+
if len(seen) == len(applicable) or time.monotonic() >= deadline:
|
|
470
|
+
break
|
|
471
|
+
# Nothing acceptable yet, so the strategies still connecting get the rest of
|
|
472
|
+
# the timeout rather than the grace period that a rejected link started.
|
|
473
|
+
with condition:
|
|
474
|
+
state["first"] = None
|
|
475
|
+
state["first_name"] = None
|
|
476
|
+
|
|
405
477
|
with condition:
|
|
406
|
-
while len(outcomes) < len(applicable):
|
|
407
|
-
connected = [i for i, o in outcomes.items() if not isinstance(o, Exception)]
|
|
408
|
-
if state["first"] is None and probed <= outcomes.keys() and connected:
|
|
409
|
-
# Every probed strategy failed, so an unprobed one is all that is left.
|
|
410
|
-
break
|
|
411
|
-
now = time.monotonic()
|
|
412
|
-
limit = deadline if state["first"] is None else state["first"] + self.grace
|
|
413
|
-
if now >= limit:
|
|
414
|
-
break
|
|
415
|
-
condition.wait(limit - now)
|
|
416
478
|
state["decided"] = True
|
|
417
|
-
settled = dict(outcomes)
|
|
418
479
|
for index, event in enumerate(cancels):
|
|
419
|
-
if index not in
|
|
480
|
+
if index not in seen:
|
|
420
481
|
event.set()
|
|
421
|
-
|
|
422
|
-
|
|
482
|
+
self._report_unsettled(applicable, seen, reasons)
|
|
483
|
+
if rejected is not None:
|
|
484
|
+
raise rejected
|
|
485
|
+
self._say("no strategy connected")
|
|
486
|
+
raise ProviderUnavailable("shell", self._explain(reasons))
|
|
487
|
+
|
|
488
|
+
def _report_unsettled(self, applicable: list[Any], seen: set[int], reasons: list[str]) -> None:
|
|
489
|
+
"""Name every strategy that had not connected when the choice was made."""
|
|
423
490
|
for index, strategy in enumerate(applicable):
|
|
424
|
-
|
|
425
|
-
if outcome is None:
|
|
491
|
+
if index not in seen:
|
|
426
492
|
reasons.append(f"{strategy.name}: did not connect in time")
|
|
427
493
|
self._say(f"{strategy.name} timed out: not connected when the choice was made")
|
|
428
|
-
elif isinstance(outcome, Exception):
|
|
429
|
-
reasons.append(f"{strategy.name}: {outcome}")
|
|
430
|
-
else:
|
|
431
|
-
connected.append(outcome)
|
|
432
|
-
if not connected:
|
|
433
|
-
self._say("no strategy connected")
|
|
434
|
-
raise ProviderUnavailable("shell", self._explain(reasons))
|
|
435
|
-
connected.sort(key=lambda link: link.rank)
|
|
436
|
-
held = {s.name for s in applicable if not getattr(s, "probed", True)}
|
|
437
|
-
chosen, measured = self._choose(connected, reasons, held)
|
|
438
|
-
first = state["first_name"]
|
|
439
|
-
if first is not None and first != chosen.strategy:
|
|
440
|
-
self._say(f"switching from {first} to {chosen.strategy}")
|
|
441
|
-
return chosen, measured
|
|
442
494
|
|
|
443
495
|
def _measure(self, link: Any) -> ProbeResult | None:
|
|
444
496
|
"""Probe a link and print the numbers. None when the link cannot carry the probe."""
|
|
@@ -470,7 +522,7 @@ class Pipeline:
|
|
|
470
522
|
measured = None
|
|
471
523
|
if measured is None and floored_out:
|
|
472
524
|
_close(link)
|
|
473
|
-
raise
|
|
525
|
+
raise _BelowFloor("shell", self._explain(reasons))
|
|
474
526
|
if link.strategy in held:
|
|
475
527
|
self._fall_back(link)
|
|
476
528
|
else:
|
|
@@ -480,7 +532,8 @@ class Pipeline:
|
|
|
480
532
|
below = f"{link.strategy}: below the floor: {', '.join(floored)}"
|
|
481
533
|
self._say(f"rejected {below}")
|
|
482
534
|
_close(link)
|
|
483
|
-
|
|
535
|
+
reasons.append(below)
|
|
536
|
+
raise _BelowFloor("shell", self._explain(reasons))
|
|
484
537
|
self._say(f"chose {link.strategy}: only one connected")
|
|
485
538
|
return link, measured
|
|
486
539
|
|
|
@@ -502,7 +555,9 @@ class Pipeline:
|
|
|
502
555
|
floored = self.floor.violations(measured)
|
|
503
556
|
if floored:
|
|
504
557
|
self._say(f"rejected {link.strategy}: below the floor: {', '.join(floored)}")
|
|
505
|
-
|
|
558
|
+
below = f"{link.strategy}: below the floor: {', '.join(floored)}"
|
|
559
|
+
floored_out.append(below)
|
|
560
|
+
reasons.append(below)
|
|
506
561
|
_close(link)
|
|
507
562
|
else:
|
|
508
563
|
above_floor.append((link, measured))
|
|
@@ -534,8 +589,8 @@ class Pipeline:
|
|
|
534
589
|
for link in unprobed:
|
|
535
590
|
_close(link)
|
|
536
591
|
self._say("every probe was below the floor")
|
|
537
|
-
raise
|
|
538
|
-
"shell", self._explain([*reasons, "every probe was below the floor"
|
|
592
|
+
raise _BelowFloor(
|
|
593
|
+
"shell", self._explain([*reasons, "every probe was below the floor"])
|
|
539
594
|
)
|
|
540
595
|
elif unprobed:
|
|
541
596
|
chosen, measured = unprobed[0], None
|
|
@@ -10,7 +10,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|
|
10
10
|
|
|
11
11
|
[[package]]
|
|
12
12
|
name = "letify-agent"
|
|
13
|
-
version = "1.2.
|
|
13
|
+
version = "1.2.4"
|
|
14
14
|
dependencies = [
|
|
15
15
|
"letify-wire",
|
|
16
16
|
"libloading",
|
|
@@ -18,14 +18,14 @@ dependencies = [
|
|
|
18
18
|
|
|
19
19
|
[[package]]
|
|
20
20
|
name = "letify-driver"
|
|
21
|
-
version = "1.2.
|
|
21
|
+
version = "1.2.4"
|
|
22
22
|
dependencies = [
|
|
23
23
|
"letify-wire",
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
[[package]]
|
|
27
27
|
name = "letify-wire"
|
|
28
|
-
version = "1.2.
|
|
28
|
+
version = "1.2.4"
|
|
29
29
|
|
|
30
30
|
[[package]]
|
|
31
31
|
name = "libloading"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letify-status",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "letify-status",
|
|
9
|
-
"version": "1.2.
|
|
9
|
+
"version": "1.2.4",
|
|
10
10
|
"license": "SEE LICENSE IN LICENSE",
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@types/node": "^18.19.0",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "letify-status",
|
|
3
3
|
"displayName": "letify Status",
|
|
4
4
|
"description": "letify quota and GPU activity in the status bar, read from the letify CLI.",
|
|
5
|
-
"version": "1.2.
|
|
5
|
+
"version": "1.2.4",
|
|
6
6
|
"publisher": "darkpyonix",
|
|
7
7
|
"license": "SEE LICENSE IN LICENSE",
|
|
8
8
|
"repository": {
|
|
@@ -6,9 +6,11 @@ Strategies and probes are the fakes from conftest, so delays and numbers are con
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import pytest
|
|
9
10
|
from conftest import FakeProbe, FakeStrategy
|
|
10
11
|
|
|
11
12
|
import letify
|
|
13
|
+
from letify.errors import ProviderUnavailable
|
|
12
14
|
from letify.transport.pipeline import Fingerprint, LinkCache, LinkFloor, Pipeline
|
|
13
15
|
from letify.transport.probe import ProbeResult
|
|
14
16
|
|
|
@@ -192,3 +194,32 @@ def test_a_quiet_launcher_prints_no_connection_lines(config_file, capsys) -> Non
|
|
|
192
194
|
letify.Launcher(config_file(body), home=False, announce=False).provider("lab").connect()
|
|
193
195
|
captured = capsys.readouterr()
|
|
194
196
|
assert (captured.out, captured.err) == ("", "")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# -- Spec: Choosing a link, the race is not over until a link is accepted ----------
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_a_strategy_rejected_by_the_floor_does_not_end_the_race() -> None:
|
|
203
|
+
# The reported Colab run: tcp_punch connected at 10.7 s and was rejected by the floor,
|
|
204
|
+
# and tailcat connected 2 s later and was closed unprobed, so the connection failed
|
|
205
|
+
# although tailcat was acceptable. Spec "Choosing a link".
|
|
206
|
+
floor = LinkFloor(min_bps=10 * MIB, max_rtt_ms=300.0)
|
|
207
|
+
punch = FakeStrategy("tcp_punch", 2, delay=0.05, result=result(8.1, 0.2, rtt=138.0))
|
|
208
|
+
tailcat = FakeStrategy("tailcat", 3, delay=0.45, result=result(24.0, 20.0))
|
|
209
|
+
|
|
210
|
+
chosen = pipeline([punch, tailcat], grace=0.2, floor=floor).connect()
|
|
211
|
+
|
|
212
|
+
assert chosen.strategy == "tailcat"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_the_connection_still_fails_when_the_later_strategy_is_also_below_the_floor() -> None:
|
|
216
|
+
# Spec "Choosing a link": waiting longer is not accepting less. Every strategy settles
|
|
217
|
+
# below the floor, so the connection fails and names both rejections.
|
|
218
|
+
floor = LinkFloor(min_bps=10 * MIB, max_rtt_ms=300.0)
|
|
219
|
+
punch = FakeStrategy("tcp_punch", 2, delay=0.05, result=result(8.1, 0.2, rtt=138.0))
|
|
220
|
+
tailcat = FakeStrategy("tailcat", 3, delay=0.45, result=result(4.0, 0.1))
|
|
221
|
+
|
|
222
|
+
with pytest.raises(ProviderUnavailable) as raised:
|
|
223
|
+
pipeline([punch, tailcat], grace=0.2, floor=floor).connect()
|
|
224
|
+
|
|
225
|
+
assert "tcp_punch" in str(raised.value) and "tailcat" in str(raised.value)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{letify-1.2.3 → letify-1.2.4}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md"
RENAMED
|
File without changes
|
|
File without changes
|
{letify-1.2.3 → letify-1.2.4}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md"
RENAMED
|
File without changes
|
|
File without changes
|
{letify-1.2.3 → letify-1.2.4}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md"
RENAMED
|
File without changes
|
|
File without changes
|
{letify-1.2.3 → letify-1.2.4}/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
|