letify 2.3.0__tar.gz → 2.4.0__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.
Files changed (200) hide show
  1. {letify-2.3.0 → letify-2.4.0}/PKG-INFO +1 -1
  2. {letify-2.3.0 → letify-2.4.0}/letify/__init__.py +1 -1
  3. {letify-2.3.0 → letify-2.4.0}/letify/providers/base.py +11 -0
  4. {letify-2.3.0 → letify-2.4.0}/letify/providers/kaggle.py +27 -2
  5. {letify-2.3.0 → letify-2.4.0}/letify/transport/pipeline.py +44 -0
  6. {letify-2.3.0 → letify-2.4.0}/letify-core/Cargo.lock +3 -3
  7. {letify-2.3.0 → letify-2.4.0}/letify-core/Cargo.toml +1 -1
  8. {letify-2.3.0 → letify-2.4.0}/letify-ext/package-lock.json +2 -2
  9. {letify-2.3.0 → letify-2.4.0}/letify-ext/package.json +1 -1
  10. {letify-2.3.0 → letify-2.4.0}/pyproject.toml +1 -1
  11. {letify-2.3.0 → letify-2.4.0}/tests/test_kaggle.py +62 -0
  12. {letify-2.3.0 → letify-2.4.0}/tests/test_transport.py +32 -0
  13. {letify-2.3.0 → letify-2.4.0}/.github/workflows/ci.yml +0 -0
  14. {letify-2.3.0 → letify-2.4.0}/.github/workflows/docs.yml +0 -0
  15. {letify-2.3.0 → letify-2.4.0}/.github/workflows/publish.yml +0 -0
  16. {letify-2.3.0 → letify-2.4.0}/.gitignore +0 -0
  17. {letify-2.3.0 → letify-2.4.0}/.researchtree +0 -0
  18. {letify-2.3.0 → letify-2.4.0}/LICENSE +0 -0
  19. {letify-2.3.0 → letify-2.4.0}/README.md +0 -0
  20. {letify-2.3.0 → letify-2.4.0}/docs/guide/01-getting-started.md +0 -0
  21. {letify-2.3.0 → letify-2.4.0}/docs/guide/02-providers.md +0 -0
  22. {letify-2.3.0 → letify-2.4.0}/docs/guide/03-execution-modes.md +0 -0
  23. {letify-2.3.0 → letify-2.4.0}/docs/guide/04-environments-and-data.md +0 -0
  24. {letify-2.3.0 → letify-2.4.0}/docs/guide/05-concurrency.md +0 -0
  25. {letify-2.3.0 → letify-2.4.0}/docs/guide/06-cost.md +0 -0
  26. {letify-2.3.0 → letify-2.4.0}/docs/guide/07-troubleshooting.md +0 -0
  27. {letify-2.3.0 → letify-2.4.0}/docs/guide/08-releasing.md +0 -0
  28. {letify-2.3.0 → letify-2.4.0}/docs/guide/README.md +0 -0
  29. {letify-2.3.0 → letify-2.4.0}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
  30. {letify-2.3.0 → letify-2.4.0}/docs/guide/ko/02-/355/224/204/353/241/234/353/260/224/354/235/264/353/215/224.md" +0 -0
  31. {letify-2.3.0 → letify-2.4.0}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
  32. {letify-2.3.0 → letify-2.4.0}/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
  33. {letify-2.3.0 → letify-2.4.0}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
  34. {letify-2.3.0 → letify-2.4.0}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
  35. {letify-2.3.0 → letify-2.4.0}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
  36. {letify-2.3.0 → letify-2.4.0}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
  37. {letify-2.3.0 → letify-2.4.0}/docs/guide/ko/README.md +0 -0
  38. {letify-2.3.0 → letify-2.4.0}/docs/locales/README_ko.md +0 -0
  39. {letify-2.3.0 → letify-2.4.0}/docs/mkdocs.yml +0 -0
  40. {letify-2.3.0 → letify-2.4.0}/examples/README.md +0 -0
  41. {letify-2.3.0 → letify-2.4.0}/examples/nvfp4-lora/.letify.example +0 -0
  42. {letify-2.3.0 → letify-2.4.0}/examples/nvfp4-lora/00_smoke.py +0 -0
  43. {letify-2.3.0 → letify-2.4.0}/examples/nvfp4-lora/01_sweep.py +0 -0
  44. {letify-2.3.0 → letify-2.4.0}/examples/nvfp4-lora/02_resume.py +0 -0
  45. {letify-2.3.0 → letify-2.4.0}/examples/nvfp4-lora/03_watch.py +0 -0
  46. {letify-2.3.0 → letify-2.4.0}/examples/nvfp4-lora/README.md +0 -0
  47. {letify-2.3.0 → letify-2.4.0}/examples/nvfp4-lora/README_ko.md +0 -0
  48. {letify-2.3.0 → letify-2.4.0}/examples/nvfp4-lora/common.py +0 -0
  49. {letify-2.3.0 → letify-2.4.0}/examples/nvfp4-lora/recipe.py +0 -0
  50. {letify-2.3.0 → letify-2.4.0}/letify/cli.py +0 -0
  51. {letify-2.3.0 → letify-2.4.0}/letify/config/__init__.py +0 -0
  52. {letify-2.3.0 → letify-2.4.0}/letify/config/inventory.py +0 -0
  53. {letify-2.3.0 → letify-2.4.0}/letify/config/login.py +0 -0
  54. {letify-2.3.0 → letify-2.4.0}/letify/config/schema.py +0 -0
  55. {letify-2.3.0 → letify-2.4.0}/letify/config/secrets.py +0 -0
  56. {letify-2.3.0 → letify-2.4.0}/letify/config/writer.py +0 -0
  57. {letify-2.3.0 → letify-2.4.0}/letify/declare/__init__.py +0 -0
  58. {letify-2.3.0 → letify-2.4.0}/letify/declare/cache.py +0 -0
  59. {letify-2.3.0 → letify-2.4.0}/letify/declare/env.py +0 -0
  60. {letify-2.3.0 → letify-2.4.0}/letify/declare/function.py +0 -0
  61. {letify-2.3.0 → letify-2.4.0}/letify/declare/instance.py +0 -0
  62. {letify-2.3.0 → letify-2.4.0}/letify/errors.py +0 -0
  63. {letify-2.3.0 → letify-2.4.0}/letify/install.py +0 -0
  64. {letify-2.3.0 → letify-2.4.0}/letify/launcher.py +0 -0
  65. {letify-2.3.0 → letify-2.4.0}/letify/paths.py +0 -0
  66. {letify-2.3.0 → letify-2.4.0}/letify/protocol/__init__.py +0 -0
  67. {letify-2.3.0 → letify-2.4.0}/letify/protocol/codec.py +0 -0
  68. {letify-2.3.0 → letify-2.4.0}/letify/protocol/driver.py +0 -0
  69. {letify-2.3.0 → letify-2.4.0}/letify/protocol/handle.py +0 -0
  70. {letify-2.3.0 → letify-2.4.0}/letify/protocol/wire.py +0 -0
  71. {letify-2.3.0 → letify-2.4.0}/letify/protocol/worker.py +0 -0
  72. {letify-2.3.0 → letify-2.4.0}/letify/providers/__init__.py +0 -0
  73. {letify-2.3.0 → letify-2.4.0}/letify/providers/colab.py +0 -0
  74. {letify-2.3.0 → letify-2.4.0}/letify/providers/colab_files.py +0 -0
  75. {letify-2.3.0 → letify-2.4.0}/letify/providers/colab_keepalive.py +0 -0
  76. {letify-2.3.0 → letify-2.4.0}/letify/providers/elice.py +0 -0
  77. {letify-2.3.0 → letify-2.4.0}/letify/providers/kaggle_adapter.py +0 -0
  78. {letify-2.3.0 → letify-2.4.0}/letify/providers/local.py +0 -0
  79. {letify-2.3.0 → letify-2.4.0}/letify/providers/modal.py +0 -0
  80. {letify-2.3.0 → letify-2.4.0}/letify/providers/modal_adapter.py +0 -0
  81. {letify-2.3.0 → letify-2.4.0}/letify/providers/naming.py +0 -0
  82. {letify-2.3.0 → letify-2.4.0}/letify/providers/shell.py +0 -0
  83. {letify-2.3.0 → letify-2.4.0}/letify/providers/tunnel.py +0 -0
  84. {letify-2.3.0 → letify-2.4.0}/letify/providers/usage.py +0 -0
  85. {letify-2.3.0 → letify-2.4.0}/letify/remoting/__init__.py +0 -0
  86. {letify-2.3.0 → letify-2.4.0}/letify/remoting/capability.py +0 -0
  87. {letify-2.3.0 → letify-2.4.0}/letify/remoting/device/__init__.py +0 -0
  88. {letify-2.3.0 → letify-2.4.0}/letify/remoting/device/client.py +0 -0
  89. {letify-2.3.0 → letify-2.4.0}/letify/remoting/device/cuda.py +0 -0
  90. {letify-2.3.0 → letify-2.4.0}/letify/remoting/device/executor.py +0 -0
  91. {letify-2.3.0 → letify-2.4.0}/letify/remoting/device/frames.py +0 -0
  92. {letify-2.3.0 → letify-2.4.0}/letify/remoting/device/guard.py +0 -0
  93. {letify-2.3.0 → letify-2.4.0}/letify/remoting/device/tensor.py +0 -0
  94. {letify-2.3.0 → letify-2.4.0}/letify/remoting/device/trace.py +0 -0
  95. {letify-2.3.0 → letify-2.4.0}/letify/remoting/device/value.py +0 -0
  96. {letify-2.3.0 → letify-2.4.0}/letify/remoting/loader.py +0 -0
  97. {letify-2.3.0 → letify-2.4.0}/letify/remoting/probe.py +0 -0
  98. {letify-2.3.0 → letify-2.4.0}/letify/render.py +0 -0
  99. {letify-2.3.0 → letify-2.4.0}/letify/runtime/__init__.py +0 -0
  100. {letify-2.3.0 → letify-2.4.0}/letify/runtime/bootstrap.py +0 -0
  101. {letify-2.3.0 → letify-2.4.0}/letify/runtime/channel.py +0 -0
  102. {letify-2.3.0 → letify-2.4.0}/letify/runtime/lease.py +0 -0
  103. {letify-2.3.0 → letify-2.4.0}/letify/runtime/pool.py +0 -0
  104. {letify-2.3.0 → letify-2.4.0}/letify/runtime/session.py +0 -0
  105. {letify-2.3.0 → letify-2.4.0}/letify/runtime/telemetry.py +0 -0
  106. {letify-2.3.0 → letify-2.4.0}/letify/store/__init__.py +0 -0
  107. {letify-2.3.0 → letify-2.4.0}/letify/store/backends/__init__.py +0 -0
  108. {letify-2.3.0 → letify-2.4.0}/letify/store/backends/filesystem.py +0 -0
  109. {letify-2.3.0 → letify-2.4.0}/letify/store/backends/google_auth.py +0 -0
  110. {letify-2.3.0 → letify-2.4.0}/letify/store/backends/layout.py +0 -0
  111. {letify-2.3.0 → letify-2.4.0}/letify/store/backends/objects.py +0 -0
  112. {letify-2.3.0 → letify-2.4.0}/letify/store/cas.py +0 -0
  113. {letify-2.3.0 → letify-2.4.0}/letify/store/pathdata.py +0 -0
  114. {letify-2.3.0 → letify-2.4.0}/letify/store/sendorder.py +0 -0
  115. {letify-2.3.0 → letify-2.4.0}/letify/store/volume.py +0 -0
  116. {letify-2.3.0 → letify-2.4.0}/letify/stubs.py +0 -0
  117. {letify-2.3.0 → letify-2.4.0}/letify/tools.py +0 -0
  118. {letify-2.3.0 → letify-2.4.0}/letify/transport/__init__.py +0 -0
  119. {letify-2.3.0 → letify-2.4.0}/letify/transport/agent.py +0 -0
  120. {letify-2.3.0 → letify-2.4.0}/letify/transport/announce.py +0 -0
  121. {letify-2.3.0 → letify-2.4.0}/letify/transport/link.py +0 -0
  122. {letify-2.3.0 → letify-2.4.0}/letify/transport/nat.py +0 -0
  123. {letify-2.3.0 → letify-2.4.0}/letify/transport/portmap.py +0 -0
  124. {letify-2.3.0 → letify-2.4.0}/letify/transport/probe.py +0 -0
  125. {letify-2.3.0 → letify-2.4.0}/letify/transport/quic.py +0 -0
  126. {letify-2.3.0 → letify-2.4.0}/letify/transport/rendezvous.py +0 -0
  127. {letify-2.3.0 → letify-2.4.0}/letify/transport/setup.py +0 -0
  128. {letify-2.3.0 → letify-2.4.0}/letify/transport/sshopts.py +0 -0
  129. {letify-2.3.0 → letify-2.4.0}/letify/transport/strategies.py +0 -0
  130. {letify-2.3.0 → letify-2.4.0}/letify-core/.gitignore +0 -0
  131. {letify-2.3.0 → letify-2.4.0}/letify-core/build.py +0 -0
  132. {letify-2.3.0 → letify-2.4.0}/letify-core/letify-agent/Cargo.toml +0 -0
  133. {letify-2.3.0 → letify-2.4.0}/letify-core/letify-agent/src/driver.rs +0 -0
  134. {letify-2.3.0 → letify-2.4.0}/letify-core/letify-agent/src/main.rs +0 -0
  135. {letify-2.3.0 → letify-2.4.0}/letify-core/letify-driver/Cargo.toml +0 -0
  136. {letify-2.3.0 → letify-2.4.0}/letify-core/letify-driver/src/client.rs +0 -0
  137. {letify-2.3.0 → letify-2.4.0}/letify-core/letify-driver/src/exports.rs +0 -0
  138. {letify-2.3.0 → letify-2.4.0}/letify-core/letify-driver/src/image.rs +0 -0
  139. {letify-2.3.0 → letify-2.4.0}/letify-core/letify-driver/src/lib.rs +0 -0
  140. {letify-2.3.0 → letify-2.4.0}/letify-core/letify-driver/src/status.rs +0 -0
  141. {letify-2.3.0 → letify-2.4.0}/letify-core/letify-driver/src/table.rs +0 -0
  142. {letify-2.3.0 → letify-2.4.0}/letify-core/letify-quic/Cargo.toml +0 -0
  143. {letify-2.3.0 → letify-2.4.0}/letify-core/letify-quic/src/main.rs +0 -0
  144. {letify-2.3.0 → letify-2.4.0}/letify-core/letify-wire/Cargo.toml +0 -0
  145. {letify-2.3.0 → letify-2.4.0}/letify-core/letify-wire/src/lib.rs +0 -0
  146. {letify-2.3.0 → letify-2.4.0}/letify-core/letify-wire/tests/throughput.rs +0 -0
  147. {letify-2.3.0 → letify-2.4.0}/letify-core/rust-toolchain.toml +0 -0
  148. {letify-2.3.0 → letify-2.4.0}/letify-ext/.vscodeignore +0 -0
  149. {letify-2.3.0 → letify-2.4.0}/letify-ext/LICENSE +0 -0
  150. {letify-2.3.0 → letify-2.4.0}/letify-ext/README.md +0 -0
  151. {letify-2.3.0 → letify-2.4.0}/letify-ext/media/icon.svg +0 -0
  152. {letify-2.3.0 → letify-2.4.0}/letify-ext/src/cli.ts +0 -0
  153. {letify-2.3.0 → letify-2.4.0}/letify-ext/src/extension.ts +0 -0
  154. {letify-2.3.0 → letify-2.4.0}/letify-ext/src/history.ts +0 -0
  155. {letify-2.3.0 → letify-2.4.0}/letify-ext/src/model.ts +0 -0
  156. {letify-2.3.0 → letify-2.4.0}/letify-ext/src/poll.ts +0 -0
  157. {letify-2.3.0 → letify-2.4.0}/letify-ext/src/render.ts +0 -0
  158. {letify-2.3.0 → letify-2.4.0}/letify-ext/test/cli.test.ts +0 -0
  159. {letify-2.3.0 → letify-2.4.0}/letify-ext/test/manifest.test.ts +0 -0
  160. {letify-2.3.0 → letify-2.4.0}/letify-ext/test/model.test.ts +0 -0
  161. {letify-2.3.0 → letify-2.4.0}/letify-ext/test/regressions.test.ts +0 -0
  162. {letify-2.3.0 → letify-2.4.0}/letify-ext/tsconfig.json +0 -0
  163. {letify-2.3.0 → letify-2.4.0}/letify_providers.py +0 -0
  164. {letify-2.3.0 → letify-2.4.0}/scripts/sync-main.sh +0 -0
  165. {letify-2.3.0 → letify-2.4.0}/scripts/version.py +0 -0
  166. {letify-2.3.0 → letify-2.4.0}/tests/conftest.py +0 -0
  167. {letify-2.3.0 → letify-2.4.0}/tests/fake_kaggle_adapter.py +0 -0
  168. {letify-2.3.0 → letify-2.4.0}/tests/fake_modal_adapter.py +0 -0
  169. {letify-2.3.0 → letify-2.4.0}/tests/test_cli.py +0 -0
  170. {letify-2.3.0 → letify-2.4.0}/tests/test_cli_output.py +0 -0
  171. {letify-2.3.0 → letify-2.4.0}/tests/test_colab_keepalive.py +0 -0
  172. {letify-2.3.0 → letify-2.4.0}/tests/test_config.py +0 -0
  173. {letify-2.3.0 → letify-2.4.0}/tests/test_core.py +0 -0
  174. {letify-2.3.0 → letify-2.4.0}/tests/test_data.py +0 -0
  175. {letify-2.3.0 → letify-2.4.0}/tests/test_data_streaming.py +0 -0
  176. {letify-2.3.0 → letify-2.4.0}/tests/test_declare.py +0 -0
  177. {letify-2.3.0 → letify-2.4.0}/tests/test_device.py +0 -0
  178. {letify-2.3.0 → letify-2.4.0}/tests/test_editor_sessions.py +0 -0
  179. {letify-2.3.0 → letify-2.4.0}/tests/test_elice.py +0 -0
  180. {letify-2.3.0 → letify-2.4.0}/tests/test_frames.py +0 -0
  181. {letify-2.3.0 → letify-2.4.0}/tests/test_install.py +0 -0
  182. {letify-2.3.0 → letify-2.4.0}/tests/test_kaggle_cookie.py +0 -0
  183. {letify-2.3.0 → letify-2.4.0}/tests/test_kaggle_link.py +0 -0
  184. {letify-2.3.0 → letify-2.4.0}/tests/test_link_log.py +0 -0
  185. {letify-2.3.0 → letify-2.4.0}/tests/test_notebook.py +0 -0
  186. {letify-2.3.0 → letify-2.4.0}/tests/test_parallel_placement.py +0 -0
  187. {letify-2.3.0 → letify-2.4.0}/tests/test_paths.py +0 -0
  188. {letify-2.3.0 → letify-2.4.0}/tests/test_portmap.py +0 -0
  189. {letify-2.3.0 → letify-2.4.0}/tests/test_protocol.py +0 -0
  190. {letify-2.3.0 → letify-2.4.0}/tests/test_providers.py +0 -0
  191. {letify-2.3.0 → letify-2.4.0}/tests/test_quic.py +0 -0
  192. {letify-2.3.0 → letify-2.4.0}/tests/test_remoting.py +0 -0
  193. {letify-2.3.0 → letify-2.4.0}/tests/test_render.py +0 -0
  194. {letify-2.3.0 → letify-2.4.0}/tests/test_runtime.py +0 -0
  195. {letify-2.3.0 → letify-2.4.0}/tests/test_ship.py +0 -0
  196. {letify-2.3.0 → letify-2.4.0}/tests/test_store.py +0 -0
  197. {letify-2.3.0 → letify-2.4.0}/tests/test_strategies.py +0 -0
  198. {letify-2.3.0 → letify-2.4.0}/tests/test_stubs.py +0 -0
  199. {letify-2.3.0 → letify-2.4.0}/tests/test_tunnel_setup.py +0 -0
  200. {letify-2.3.0 → letify-2.4.0}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: letify
3
- Version: 2.3.0
3
+ Version: 2.4.0
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
@@ -62,7 +62,7 @@ remote: Final = Host.remote
62
62
  # The enum class stays importable from letify.declare.instance for letify's own use.
63
63
  del Host
64
64
 
65
- __version__ = "2.3.0"
65
+ __version__ = "2.4.0"
66
66
 
67
67
 
68
68
  def fetch(tensor): # type: ignore[no-untyped-def]
@@ -383,6 +383,17 @@ class Provider(abc.ABC):
383
383
  """
384
384
  return None
385
385
 
386
+ @property
387
+ def fallback_mib_per_s(self) -> float:
388
+ """What this provider's own fallback carries, or 0 when it is not known.
389
+
390
+ Spec "A floor rejection never picks something slower": a link the floor rejected
391
+ is kept when it beats this, because refusing a slow link only makes sense when
392
+ what happens instead is better.
393
+ """
394
+ value = self.config.option("fallback_mib_per_s")
395
+ return max(0.0, float(value)) if isinstance(value, (int, float)) else 0.0
396
+
386
397
  @property
387
398
  def any_accelerator(self) -> bool:
388
399
  """Whether a card other than the one asked for is accepted, from ``any_accelerator``."""
@@ -103,6 +103,9 @@ FIRESTORE_DOCUMENT = (
103
103
  #: runnable. A freshly created empty notebook cancels its own session because it has nothing
104
104
  #: to run, so the session is started with one committed cell.
105
105
  NOTEBOOK_TITLE = "letify runtime"
106
+ #: What the kernel bridge carries, measured sending 64 MiB on a live account. The
107
+ #: floor compares a link against this rather than refusing one that beats it.
108
+ BRIDGE_MIB_PER_S = 2.0
106
109
  NOTEBOOK_BODY = json.dumps(
107
110
  {
108
111
  "cells": [
@@ -1112,7 +1115,9 @@ class Kaggle(Provider):
1112
1115
  usage_unit = "GPU hours"
1113
1116
  usage_source = "the weekly accelerator quota the official Kaggle CLI's quota command reads"
1114
1117
 
1115
- default_workspace = "/kaggle/working/letify"
1118
+ # Spec "Kaggle runtimes": /kaggle/working is the notebook's output directory on its
1119
+ # own 19.5 GB device, while /kaggle is the overlay with about 1 TB free.
1120
+ default_workspace = "/kaggle/letify"
1116
1121
 
1117
1122
  def account_note(self) -> str | None:
1118
1123
  """How the account's cookie is doing, for `letify providers`, with no network call.
@@ -1319,6 +1324,7 @@ class Kaggle(Provider):
1319
1324
  return LinkFloor(
1320
1325
  min_bps=float(min_mib) * MIB if isinstance(min_mib, (int, float)) else default.min_bps,
1321
1326
  max_rtt_ms=float(max_rtt) if isinstance(max_rtt, (int, float)) else default.max_rtt_ms,
1327
+ fallback_bps=self.fallback_mib_per_s * MIB,
1322
1328
  )
1323
1329
 
1324
1330
  @property
@@ -1339,7 +1345,15 @@ class Kaggle(Provider):
1339
1345
 
1340
1346
  try:
1341
1347
  link = self.link_over(bridge, name=name)
1342
- except Exception:
1348
+ except Exception as exc:
1349
+ # Never silent: a session that quietly changed channel is one nobody can
1350
+ # account for. Spec "A floor rejection never picks something slower".
1351
+ from ..transport.announce import printer
1352
+
1353
+ printer(bool(self.__dict__.get("announce", True)))(
1354
+ f"{self.alias}: the kernel bridge is carrying {name} at about "
1355
+ f"{self.fallback_mib_per_s:.1f} MiB/s, because no link was chosen: {exc}"
1356
+ )
1343
1357
  return bridge
1344
1358
  self._links[name] = link
1345
1359
  return PersistentChannel(
@@ -1347,6 +1361,17 @@ class Kaggle(Provider):
1347
1361
  name=name,
1348
1362
  )
1349
1363
 
1364
+ @property
1365
+ def fallback_mib_per_s(self) -> float:
1366
+ """The kernel bridge, measured at 2.0 MiB/s sending 64 MiB on a live account.
1367
+
1368
+ Spec "A floor rejection never picks something slower": without this a punched link
1369
+ at down 8.1 MiB/s was refused for being under the 10 MiB/s floor and 26 GB went
1370
+ over the 2 MiB/s bridge instead.
1371
+ """
1372
+ value = self.config.option("fallback_mib_per_s")
1373
+ return max(0.0, float(value)) if isinstance(value, (int, float)) else BRIDGE_MIB_PER_S
1374
+
1350
1375
  @property
1351
1376
  def transfer_connection_cost_s(self) -> float:
1352
1377
  """A punch, which is what one more stream costs here. Measured at about 10 s."""
@@ -61,11 +61,39 @@ class LinkFloor:
61
61
 
62
62
  min_bps: float
63
63
  max_rtt_ms: float
64
+ #: What the provider's own fallback carries, when it is known. A link the floor
65
+ #: rejected is kept when it beats this. Spec "A floor rejection never picks something
66
+ #: slower".
67
+ fallback_bps: float = 0.0
64
68
 
65
69
  @classmethod
66
70
  def default(cls) -> LinkFloor:
67
71
  return cls(min_bps=DEFAULT_MIN_MIB_PER_S * MIB, max_rtt_ms=DEFAULT_MAX_RTT_MS)
68
72
 
73
+ def kept_below(self, result: ProbeResult, floored: list[str]) -> str | None:
74
+ """Why a link the floor rejected is used anyway, or None when it is not.
75
+
76
+ Spec "A floor rejection never picks something slower".
77
+ """
78
+ if not floored or not self.beats_fallback(result):
79
+ return None
80
+ return (
81
+ f"kept below the floor, because the fallback carries "
82
+ f"{self.fallback_bps / MIB:.1f} MiB/s"
83
+ )
84
+
85
+ def beats_fallback(self, result: ProbeResult) -> bool:
86
+ """Whether this probe is faster than the path that would be used instead.
87
+
88
+ Spec "A floor rejection never picks something slower": the floor refuses a slow
89
+ link so a slow link is not used, which is only right when what happens instead is
90
+ better. A provider with no measurable fallback has nothing to compare against, so
91
+ the answer is False and the rejection stands.
92
+ """
93
+ if not self.fallback_bps:
94
+ return False
95
+ return result.upload_bps > self.fallback_bps and result.download_bps > self.fallback_bps
96
+
69
97
  def violations(self, result: ProbeResult) -> list[str]:
70
98
  """Each direction, or the round trip, that the probe failed to clear."""
71
99
  found = _below(
@@ -312,6 +340,10 @@ class Pipeline:
312
340
  return link
313
341
  if measured is not None:
314
342
  floored = self.floor.violations(measured)
343
+ kept = self.floor.kept_below(measured, floored)
344
+ if kept:
345
+ self._say(f"{link.strategy} {kept}")
346
+ floored = []
315
347
  if floored:
316
348
  self._say(f"rejected {link.strategy}: below the floor: {', '.join(floored)}")
317
349
  _close(link)
@@ -389,6 +421,10 @@ class Pipeline:
389
421
  _close(link)
390
422
  return None
391
423
  floored = self.floor.violations(measured)
424
+ kept = self.floor.kept_below(measured, floored)
425
+ if kept:
426
+ self._say(f"cached {name} {kept}")
427
+ floored = []
392
428
  if floored:
393
429
  self._say(f"cached {name} rejected: below the floor: {', '.join(floored)}")
394
430
  reasons.append(f"{name}: below the floor: {', '.join(floored)}")
@@ -573,6 +609,10 @@ class Pipeline:
573
609
  else:
574
610
  if measured is not None:
575
611
  floored = self.floor.violations(measured)
612
+ kept = self.floor.kept_below(measured, floored)
613
+ if kept:
614
+ self._say(f"{link.strategy} {kept}")
615
+ floored = []
576
616
  if floored:
577
617
  below = f"{link.strategy}: below the floor: {', '.join(floored)}"
578
618
  self._say(f"rejected {below}")
@@ -598,6 +638,10 @@ class Pipeline:
598
638
  above_floor: list[tuple[Any, ProbeResult]] = []
599
639
  for link, measured in probed:
600
640
  floored = self.floor.violations(measured)
641
+ kept = self.floor.kept_below(measured, floored)
642
+ if kept:
643
+ self._say(f"{link.strategy} {kept}")
644
+ floored = []
601
645
  if floored:
602
646
  self._say(f"rejected {link.strategy}: below the floor: {', '.join(floored)}")
603
647
  below = f"{link.strategy}: below the floor: {', '.join(floored)}"
@@ -138,7 +138,7 @@ dependencies = [
138
138
 
139
139
  [[package]]
140
140
  name = "letify-agent"
141
- version = "2.3.0"
141
+ version = "2.4.0"
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.3.0"
149
+ version = "2.4.0"
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.3.0"
167
+ version = "2.4.0"
168
168
 
169
169
  [[package]]
170
170
  name = "libc"
@@ -8,7 +8,7 @@ resolver = "3"
8
8
  members = ["letify-wire", "letify-driver", "letify-agent", "letify-quic"]
9
9
 
10
10
  [workspace.package]
11
- version = "2.3.0"
11
+ version = "2.4.0"
12
12
  edition = "2024"
13
13
  license = "MIT"
14
14
  repository = "https://github.com/DarkPyonix/letify"
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "letify-status",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "letify-status",
9
- "version": "2.3.0",
9
+ "version": "2.4.0",
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.3.0",
5
+ "version": "2.4.0",
6
6
  "publisher": "darkpyonix",
7
7
  "license": "SEE LICENSE IN LICENSE",
8
8
  "repository": {
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "letify"
3
- version = "2.3.0"
3
+ version = "2.4.0"
4
4
  description = "Declarations that become infrastructure."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1232,3 +1232,65 @@ def test_a_kaggle_account_is_asked_for_its_sessions(launcher_from, fake_kaggle_c
1232
1232
  row = rows.get("kaggle_a")
1233
1233
  assert row is not None, rows
1234
1234
  assert row.get("reason") != "session discovery is not supported"
1235
+
1236
+
1237
+ def test_the_kaggle_workspace_is_not_under_the_output_directory() -> None:
1238
+ """Spec "Kaggle": /kaggle/working is the output directory, on its own 19.5 GB device.
1239
+
1240
+ Measured on a live session: /kaggle/working has 19.5 GB against Kaggle's 20 GB output
1241
+ limit, while /kaggle is the overlay with 1069.9 GB free. A venv and a call's blobs are
1242
+ not the notebook's output, and a 26 GB dataset does not fit on the smaller one.
1243
+ /kaggle/temp, which looks like the obvious scratch path, does not exist on the image.
1244
+ """
1245
+ provider = kaggle_provider()
1246
+ assert provider.workspace_root == "/kaggle/letify"
1247
+ assert "/kaggle/working" not in provider.workspace_root
1248
+
1249
+
1250
+ def test_a_kaggle_account_can_still_name_its_own_workspace(config_file) -> None:
1251
+ """Spec: an account that wants another path sets workspace."""
1252
+ from conftest import provider_of
1253
+
1254
+ from letify.providers import Kaggle
1255
+
1256
+ provider = provider_of(Kaggle, "kaggle_a", workspace="/kaggle/working/letify")
1257
+ assert provider.workspace_root == "/kaggle/working/letify"
1258
+
1259
+
1260
+ def test_the_kaggle_floor_knows_what_the_bridge_carries() -> None:
1261
+ """Spec "A floor rejection never picks something slower": the bridge is 2.0 MiB/s."""
1262
+ from letify.transport.pipeline import MIB
1263
+
1264
+ provider = kaggle_provider()
1265
+ assert provider.fallback_mib_per_s == 2.0
1266
+ assert provider.link_floor.fallback_bps == 2.0 * MIB
1267
+
1268
+
1269
+ def test_an_account_can_say_what_its_bridge_carries(config_file) -> None:
1270
+ """Spec: an account overrides it with fallback_mib_per_s."""
1271
+ from conftest import provider_of
1272
+
1273
+ from letify.providers import Kaggle
1274
+
1275
+ provider = provider_of(Kaggle, "kaggle_a", fallback_mib_per_s=5)
1276
+ assert provider.fallback_mib_per_s == 5.0
1277
+
1278
+
1279
+ def test_falling_back_to_the_bridge_says_so(monkeypatch, capsys) -> None:
1280
+ """Spec: taking the fallback is never silent.
1281
+
1282
+ A live session spent 25 minutes on the bridge with no line saying which channel it was
1283
+ on, because the fallback was reached through a bare `except Exception: return bridge`.
1284
+ """
1285
+ provider = kaggle_provider()
1286
+ provider.announce = True
1287
+
1288
+ def refuse(channel, name=None):
1289
+ raise RuntimeError("every strategy was below the floor")
1290
+
1291
+ monkeypatch.setattr(provider, "link_over", refuse)
1292
+ bridge = object()
1293
+ assert provider.channel_over(bridge, name="k-1") is bridge
1294
+ err = capsys.readouterr().err
1295
+ assert "the kernel bridge is carrying k-1" in err
1296
+ assert "2.0 MiB/s" in err
@@ -901,3 +901,35 @@ def test_a_platform_without_the_kernel_copy_still_splices(monkeypatch) -> None:
901
901
  left.close()
902
902
  near.close()
903
903
  thread.join(10)
904
+
905
+
906
+ def test_a_link_below_the_floor_is_kept_when_the_fallback_is_slower(isolated_home, capsys) -> None:
907
+ """Spec "A floor rejection never picks something slower".
908
+
909
+ A live Kaggle session refused a punched link at down 8.1 MiB/s for being under the
910
+ 10 MiB/s floor and then carried 26 GB over the 2 MiB/s kernel bridge instead, which is
911
+ the opposite of what the floor is for.
912
+ """
913
+ floor = LinkFloor(min_bps=10 * MIB, max_rtt_ms=300.0, fallback_bps=2 * MIB)
914
+ punch = FakeStrategy("tcp_punch", 2, result=result(8.1, 8.1, rtt=190.0))
915
+ link = pipeline([punch], floor=floor).connect()
916
+ assert link.strategy == "tcp_punch"
917
+ err = capsys.readouterr().err
918
+ assert "kept below the floor" in err
919
+ assert "2.0 MiB/s" in err
920
+
921
+
922
+ def test_a_link_slower_than_the_fallback_is_still_refused(isolated_home) -> None:
923
+ """Spec: only a link that beats the fallback is kept; a slower one is given up."""
924
+ floor = LinkFloor(min_bps=10 * MIB, max_rtt_ms=300.0, fallback_bps=2 * MIB)
925
+ punch = FakeStrategy("tcp_punch", 2, result=result(8.1, 0.5, rtt=190.0))
926
+ with pytest.raises(letify.ProviderUnavailable):
927
+ pipeline([punch], floor=floor).connect()
928
+
929
+
930
+ def test_a_provider_with_no_known_fallback_keeps_failing_below_the_floor(isolated_home) -> None:
931
+ """Spec: a provider with no measurable fallback has nothing to compare against."""
932
+ floor = LinkFloor(min_bps=10 * MIB, max_rtt_ms=300.0)
933
+ punch = FakeStrategy("tcp_punch", 2, result=result(8.1, 8.1, rtt=190.0))
934
+ with pytest.raises(letify.ProviderUnavailable):
935
+ pipeline([punch], floor=floor).connect()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes