letify 2.6.1__tar.gz → 2.6.2__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 (210) hide show
  1. {letify-2.6.1 → letify-2.6.2}/PKG-INFO +1 -1
  2. {letify-2.6.1 → letify-2.6.2}/docs/SPEC.md +1 -1
  3. {letify-2.6.1 → letify-2.6.2}/letify/__init__.py +1 -1
  4. {letify-2.6.1 → letify-2.6.2}/letify/protocol/worker.py +37 -2
  5. {letify-2.6.1 → letify-2.6.2}/letify-core/Cargo.lock +3 -3
  6. {letify-2.6.1 → letify-2.6.2}/letify-core/Cargo.toml +1 -1
  7. {letify-2.6.1 → letify-2.6.2}/letify-ext/package-lock.json +2 -2
  8. {letify-2.6.1 → letify-2.6.2}/letify-ext/package.json +1 -1
  9. {letify-2.6.1 → letify-2.6.2}/pyproject.toml +1 -1
  10. {letify-2.6.1 → letify-2.6.2}/tests/test_data_streaming.py +73 -0
  11. {letify-2.6.1 → letify-2.6.2}/.github/workflows/ci.yml +0 -0
  12. {letify-2.6.1 → letify-2.6.2}/.github/workflows/docs.yml +0 -0
  13. {letify-2.6.1 → letify-2.6.2}/.github/workflows/publish.yml +0 -0
  14. {letify-2.6.1 → letify-2.6.2}/.gitignore +0 -0
  15. {letify-2.6.1 → letify-2.6.2}/.researchtree +0 -0
  16. {letify-2.6.1 → letify-2.6.2}/CLAUDE.md +0 -0
  17. {letify-2.6.1 → letify-2.6.2}/LICENSE +0 -0
  18. {letify-2.6.1 → letify-2.6.2}/PROJECT.md +0 -0
  19. {letify-2.6.1 → letify-2.6.2}/README.md +0 -0
  20. {letify-2.6.1 → letify-2.6.2}/docs/COMPONENT.md +0 -0
  21. {letify-2.6.1 → letify-2.6.2}/docs/INTENT.md +0 -0
  22. {letify-2.6.1 → letify-2.6.2}/docs/NETWORK.md +0 -0
  23. {letify-2.6.1 → letify-2.6.2}/docs/guide/01-getting-started.md +0 -0
  24. {letify-2.6.1 → letify-2.6.2}/docs/guide/02-providers.md +0 -0
  25. {letify-2.6.1 → letify-2.6.2}/docs/guide/03-execution-modes.md +0 -0
  26. {letify-2.6.1 → letify-2.6.2}/docs/guide/04-environments-and-data.md +0 -0
  27. {letify-2.6.1 → letify-2.6.2}/docs/guide/05-concurrency.md +0 -0
  28. {letify-2.6.1 → letify-2.6.2}/docs/guide/06-cost.md +0 -0
  29. {letify-2.6.1 → letify-2.6.2}/docs/guide/07-troubleshooting.md +0 -0
  30. {letify-2.6.1 → letify-2.6.2}/docs/guide/08-releasing.md +0 -0
  31. {letify-2.6.1 → letify-2.6.2}/docs/guide/README.md +0 -0
  32. {letify-2.6.1 → letify-2.6.2}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
  33. {letify-2.6.1 → letify-2.6.2}/docs/guide/ko/02-/355/224/204/353/241/234/353/260/224/354/235/264/353/215/224.md" +0 -0
  34. {letify-2.6.1 → letify-2.6.2}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
  35. {letify-2.6.1 → letify-2.6.2}/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
  36. {letify-2.6.1 → letify-2.6.2}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
  37. {letify-2.6.1 → letify-2.6.2}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
  38. {letify-2.6.1 → letify-2.6.2}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
  39. {letify-2.6.1 → letify-2.6.2}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
  40. {letify-2.6.1 → letify-2.6.2}/docs/guide/ko/README.md +0 -0
  41. {letify-2.6.1 → letify-2.6.2}/docs/locales/README_ko.md +0 -0
  42. {letify-2.6.1 → letify-2.6.2}/docs/mkdocs.yml +0 -0
  43. {letify-2.6.1 → letify-2.6.2}/examples/README.md +0 -0
  44. {letify-2.6.1 → letify-2.6.2}/examples/nvfp4-lora/.letify.example +0 -0
  45. {letify-2.6.1 → letify-2.6.2}/examples/nvfp4-lora/00_smoke.py +0 -0
  46. {letify-2.6.1 → letify-2.6.2}/examples/nvfp4-lora/01_sweep.py +0 -0
  47. {letify-2.6.1 → letify-2.6.2}/examples/nvfp4-lora/02_resume.py +0 -0
  48. {letify-2.6.1 → letify-2.6.2}/examples/nvfp4-lora/03_watch.py +0 -0
  49. {letify-2.6.1 → letify-2.6.2}/examples/nvfp4-lora/README.md +0 -0
  50. {letify-2.6.1 → letify-2.6.2}/examples/nvfp4-lora/README_ko.md +0 -0
  51. {letify-2.6.1 → letify-2.6.2}/examples/nvfp4-lora/common.py +0 -0
  52. {letify-2.6.1 → letify-2.6.2}/examples/nvfp4-lora/recipe.py +0 -0
  53. {letify-2.6.1 → letify-2.6.2}/letify/cli.py +0 -0
  54. {letify-2.6.1 → letify-2.6.2}/letify/config/__init__.py +0 -0
  55. {letify-2.6.1 → letify-2.6.2}/letify/config/inventory.py +0 -0
  56. {letify-2.6.1 → letify-2.6.2}/letify/config/login.py +0 -0
  57. {letify-2.6.1 → letify-2.6.2}/letify/config/schema.py +0 -0
  58. {letify-2.6.1 → letify-2.6.2}/letify/config/secrets.py +0 -0
  59. {letify-2.6.1 → letify-2.6.2}/letify/config/writer.py +0 -0
  60. {letify-2.6.1 → letify-2.6.2}/letify/declare/__init__.py +0 -0
  61. {letify-2.6.1 → letify-2.6.2}/letify/declare/cache.py +0 -0
  62. {letify-2.6.1 → letify-2.6.2}/letify/declare/env.py +0 -0
  63. {letify-2.6.1 → letify-2.6.2}/letify/declare/function.py +0 -0
  64. {letify-2.6.1 → letify-2.6.2}/letify/declare/instance.py +0 -0
  65. {letify-2.6.1 → letify-2.6.2}/letify/declare/reduce.py +0 -0
  66. {letify-2.6.1 → letify-2.6.2}/letify/declare/replicas.py +0 -0
  67. {letify-2.6.1 → letify-2.6.2}/letify/errors.py +0 -0
  68. {letify-2.6.1 → letify-2.6.2}/letify/install.py +0 -0
  69. {letify-2.6.1 → letify-2.6.2}/letify/launcher.py +0 -0
  70. {letify-2.6.1 → letify-2.6.2}/letify/paths.py +0 -0
  71. {letify-2.6.1 → letify-2.6.2}/letify/protocol/__init__.py +0 -0
  72. {letify-2.6.1 → letify-2.6.2}/letify/protocol/codec.py +0 -0
  73. {letify-2.6.1 → letify-2.6.2}/letify/protocol/driver.py +0 -0
  74. {letify-2.6.1 → letify-2.6.2}/letify/protocol/handle.py +0 -0
  75. {letify-2.6.1 → letify-2.6.2}/letify/protocol/wire.py +0 -0
  76. {letify-2.6.1 → letify-2.6.2}/letify/providers/__init__.py +0 -0
  77. {letify-2.6.1 → letify-2.6.2}/letify/providers/base.py +0 -0
  78. {letify-2.6.1 → letify-2.6.2}/letify/providers/colab.py +0 -0
  79. {letify-2.6.1 → letify-2.6.2}/letify/providers/colab_files.py +0 -0
  80. {letify-2.6.1 → letify-2.6.2}/letify/providers/colab_keepalive.py +0 -0
  81. {letify-2.6.1 → letify-2.6.2}/letify/providers/elice.py +0 -0
  82. {letify-2.6.1 → letify-2.6.2}/letify/providers/kaggle.py +0 -0
  83. {letify-2.6.1 → letify-2.6.2}/letify/providers/kaggle_adapter.py +0 -0
  84. {letify-2.6.1 → letify-2.6.2}/letify/providers/local.py +0 -0
  85. {letify-2.6.1 → letify-2.6.2}/letify/providers/modal.py +0 -0
  86. {letify-2.6.1 → letify-2.6.2}/letify/providers/modal_adapter.py +0 -0
  87. {letify-2.6.1 → letify-2.6.2}/letify/providers/naming.py +0 -0
  88. {letify-2.6.1 → letify-2.6.2}/letify/providers/shell.py +0 -0
  89. {letify-2.6.1 → letify-2.6.2}/letify/providers/tunnel.py +0 -0
  90. {letify-2.6.1 → letify-2.6.2}/letify/providers/usage.py +0 -0
  91. {letify-2.6.1 → letify-2.6.2}/letify/remoting/__init__.py +0 -0
  92. {letify-2.6.1 → letify-2.6.2}/letify/remoting/capability.py +0 -0
  93. {letify-2.6.1 → letify-2.6.2}/letify/remoting/device/__init__.py +0 -0
  94. {letify-2.6.1 → letify-2.6.2}/letify/remoting/device/client.py +0 -0
  95. {letify-2.6.1 → letify-2.6.2}/letify/remoting/device/cuda.py +0 -0
  96. {letify-2.6.1 → letify-2.6.2}/letify/remoting/device/executor.py +0 -0
  97. {letify-2.6.1 → letify-2.6.2}/letify/remoting/device/frames.py +0 -0
  98. {letify-2.6.1 → letify-2.6.2}/letify/remoting/device/guard.py +0 -0
  99. {letify-2.6.1 → letify-2.6.2}/letify/remoting/device/tensor.py +0 -0
  100. {letify-2.6.1 → letify-2.6.2}/letify/remoting/device/trace.py +0 -0
  101. {letify-2.6.1 → letify-2.6.2}/letify/remoting/device/value.py +0 -0
  102. {letify-2.6.1 → letify-2.6.2}/letify/remoting/loader.py +0 -0
  103. {letify-2.6.1 → letify-2.6.2}/letify/remoting/probe.py +0 -0
  104. {letify-2.6.1 → letify-2.6.2}/letify/render.py +0 -0
  105. {letify-2.6.1 → letify-2.6.2}/letify/runtime/__init__.py +0 -0
  106. {letify-2.6.1 → letify-2.6.2}/letify/runtime/bootstrap.py +0 -0
  107. {letify-2.6.1 → letify-2.6.2}/letify/runtime/broker.py +0 -0
  108. {letify-2.6.1 → letify-2.6.2}/letify/runtime/channel.py +0 -0
  109. {letify-2.6.1 → letify-2.6.2}/letify/runtime/lease.py +0 -0
  110. {letify-2.6.1 → letify-2.6.2}/letify/runtime/pool.py +0 -0
  111. {letify-2.6.1 → letify-2.6.2}/letify/runtime/session.py +0 -0
  112. {letify-2.6.1 → letify-2.6.2}/letify/runtime/telemetry.py +0 -0
  113. {letify-2.6.1 → letify-2.6.2}/letify/store/__init__.py +0 -0
  114. {letify-2.6.1 → letify-2.6.2}/letify/store/backends/__init__.py +0 -0
  115. {letify-2.6.1 → letify-2.6.2}/letify/store/backends/filesystem.py +0 -0
  116. {letify-2.6.1 → letify-2.6.2}/letify/store/backends/google_auth.py +0 -0
  117. {letify-2.6.1 → letify-2.6.2}/letify/store/backends/layout.py +0 -0
  118. {letify-2.6.1 → letify-2.6.2}/letify/store/backends/objects.py +0 -0
  119. {letify-2.6.1 → letify-2.6.2}/letify/store/cas.py +0 -0
  120. {letify-2.6.1 → letify-2.6.2}/letify/store/pathdata.py +0 -0
  121. {letify-2.6.1 → letify-2.6.2}/letify/store/sendorder.py +0 -0
  122. {letify-2.6.1 → letify-2.6.2}/letify/store/volume.py +0 -0
  123. {letify-2.6.1 → letify-2.6.2}/letify/stubs.py +0 -0
  124. {letify-2.6.1 → letify-2.6.2}/letify/tools.py +0 -0
  125. {letify-2.6.1 → letify-2.6.2}/letify/transport/__init__.py +0 -0
  126. {letify-2.6.1 → letify-2.6.2}/letify/transport/agent.py +0 -0
  127. {letify-2.6.1 → letify-2.6.2}/letify/transport/announce.py +0 -0
  128. {letify-2.6.1 → letify-2.6.2}/letify/transport/link.py +0 -0
  129. {letify-2.6.1 → letify-2.6.2}/letify/transport/nat.py +0 -0
  130. {letify-2.6.1 → letify-2.6.2}/letify/transport/pipeline.py +0 -0
  131. {letify-2.6.1 → letify-2.6.2}/letify/transport/portmap.py +0 -0
  132. {letify-2.6.1 → letify-2.6.2}/letify/transport/probe.py +0 -0
  133. {letify-2.6.1 → letify-2.6.2}/letify/transport/quic.py +0 -0
  134. {letify-2.6.1 → letify-2.6.2}/letify/transport/rendezvous.py +0 -0
  135. {letify-2.6.1 → letify-2.6.2}/letify/transport/setup.py +0 -0
  136. {letify-2.6.1 → letify-2.6.2}/letify/transport/sshopts.py +0 -0
  137. {letify-2.6.1 → letify-2.6.2}/letify/transport/strategies.py +0 -0
  138. {letify-2.6.1 → letify-2.6.2}/letify-core/.gitignore +0 -0
  139. {letify-2.6.1 → letify-2.6.2}/letify-core/build.py +0 -0
  140. {letify-2.6.1 → letify-2.6.2}/letify-core/letify-agent/Cargo.toml +0 -0
  141. {letify-2.6.1 → letify-2.6.2}/letify-core/letify-agent/src/driver.rs +0 -0
  142. {letify-2.6.1 → letify-2.6.2}/letify-core/letify-agent/src/main.rs +0 -0
  143. {letify-2.6.1 → letify-2.6.2}/letify-core/letify-driver/Cargo.toml +0 -0
  144. {letify-2.6.1 → letify-2.6.2}/letify-core/letify-driver/src/client.rs +0 -0
  145. {letify-2.6.1 → letify-2.6.2}/letify-core/letify-driver/src/exports.rs +0 -0
  146. {letify-2.6.1 → letify-2.6.2}/letify-core/letify-driver/src/image.rs +0 -0
  147. {letify-2.6.1 → letify-2.6.2}/letify-core/letify-driver/src/lib.rs +0 -0
  148. {letify-2.6.1 → letify-2.6.2}/letify-core/letify-driver/src/status.rs +0 -0
  149. {letify-2.6.1 → letify-2.6.2}/letify-core/letify-driver/src/table.rs +0 -0
  150. {letify-2.6.1 → letify-2.6.2}/letify-core/letify-quic/Cargo.toml +0 -0
  151. {letify-2.6.1 → letify-2.6.2}/letify-core/letify-quic/src/main.rs +0 -0
  152. {letify-2.6.1 → letify-2.6.2}/letify-core/letify-wire/Cargo.toml +0 -0
  153. {letify-2.6.1 → letify-2.6.2}/letify-core/letify-wire/src/lib.rs +0 -0
  154. {letify-2.6.1 → letify-2.6.2}/letify-core/letify-wire/tests/throughput.rs +0 -0
  155. {letify-2.6.1 → letify-2.6.2}/letify-core/rust-toolchain.toml +0 -0
  156. {letify-2.6.1 → letify-2.6.2}/letify-ext/.vscodeignore +0 -0
  157. {letify-2.6.1 → letify-2.6.2}/letify-ext/LICENSE +0 -0
  158. {letify-2.6.1 → letify-2.6.2}/letify-ext/README.md +0 -0
  159. {letify-2.6.1 → letify-2.6.2}/letify-ext/media/icon.svg +0 -0
  160. {letify-2.6.1 → letify-2.6.2}/letify-ext/src/cli.ts +0 -0
  161. {letify-2.6.1 → letify-2.6.2}/letify-ext/src/extension.ts +0 -0
  162. {letify-2.6.1 → letify-2.6.2}/letify-ext/src/history.ts +0 -0
  163. {letify-2.6.1 → letify-2.6.2}/letify-ext/src/model.ts +0 -0
  164. {letify-2.6.1 → letify-2.6.2}/letify-ext/src/poll.ts +0 -0
  165. {letify-2.6.1 → letify-2.6.2}/letify-ext/src/render.ts +0 -0
  166. {letify-2.6.1 → letify-2.6.2}/letify-ext/test/cli.test.ts +0 -0
  167. {letify-2.6.1 → letify-2.6.2}/letify-ext/test/manifest.test.ts +0 -0
  168. {letify-2.6.1 → letify-2.6.2}/letify-ext/test/model.test.ts +0 -0
  169. {letify-2.6.1 → letify-2.6.2}/letify-ext/test/regressions.test.ts +0 -0
  170. {letify-2.6.1 → letify-2.6.2}/letify-ext/tsconfig.json +0 -0
  171. {letify-2.6.1 → letify-2.6.2}/letify_providers.py +0 -0
  172. {letify-2.6.1 → letify-2.6.2}/scripts/sync-main.sh +0 -0
  173. {letify-2.6.1 → letify-2.6.2}/scripts/version.py +0 -0
  174. {letify-2.6.1 → letify-2.6.2}/tests/conftest.py +0 -0
  175. {letify-2.6.1 → letify-2.6.2}/tests/fake_kaggle_adapter.py +0 -0
  176. {letify-2.6.1 → letify-2.6.2}/tests/fake_modal_adapter.py +0 -0
  177. {letify-2.6.1 → letify-2.6.2}/tests/test_cli.py +0 -0
  178. {letify-2.6.1 → letify-2.6.2}/tests/test_cli_output.py +0 -0
  179. {letify-2.6.1 → letify-2.6.2}/tests/test_colab_keepalive.py +0 -0
  180. {letify-2.6.1 → letify-2.6.2}/tests/test_config.py +0 -0
  181. {letify-2.6.1 → letify-2.6.2}/tests/test_core.py +0 -0
  182. {letify-2.6.1 → letify-2.6.2}/tests/test_data.py +0 -0
  183. {letify-2.6.1 → letify-2.6.2}/tests/test_declare.py +0 -0
  184. {letify-2.6.1 → letify-2.6.2}/tests/test_device.py +0 -0
  185. {letify-2.6.1 → letify-2.6.2}/tests/test_distributed.py +0 -0
  186. {letify-2.6.1 → letify-2.6.2}/tests/test_editor_sessions.py +0 -0
  187. {letify-2.6.1 → letify-2.6.2}/tests/test_elice.py +0 -0
  188. {letify-2.6.1 → letify-2.6.2}/tests/test_frames.py +0 -0
  189. {letify-2.6.1 → letify-2.6.2}/tests/test_install.py +0 -0
  190. {letify-2.6.1 → letify-2.6.2}/tests/test_kaggle.py +0 -0
  191. {letify-2.6.1 → letify-2.6.2}/tests/test_kaggle_cookie.py +0 -0
  192. {letify-2.6.1 → letify-2.6.2}/tests/test_kaggle_link.py +0 -0
  193. {letify-2.6.1 → letify-2.6.2}/tests/test_link_log.py +0 -0
  194. {letify-2.6.1 → letify-2.6.2}/tests/test_notebook.py +0 -0
  195. {letify-2.6.1 → letify-2.6.2}/tests/test_parallel_placement.py +0 -0
  196. {letify-2.6.1 → letify-2.6.2}/tests/test_paths.py +0 -0
  197. {letify-2.6.1 → letify-2.6.2}/tests/test_portmap.py +0 -0
  198. {letify-2.6.1 → letify-2.6.2}/tests/test_protocol.py +0 -0
  199. {letify-2.6.1 → letify-2.6.2}/tests/test_providers.py +0 -0
  200. {letify-2.6.1 → letify-2.6.2}/tests/test_quic.py +0 -0
  201. {letify-2.6.1 → letify-2.6.2}/tests/test_remoting.py +0 -0
  202. {letify-2.6.1 → letify-2.6.2}/tests/test_render.py +0 -0
  203. {letify-2.6.1 → letify-2.6.2}/tests/test_runtime.py +0 -0
  204. {letify-2.6.1 → letify-2.6.2}/tests/test_ship.py +0 -0
  205. {letify-2.6.1 → letify-2.6.2}/tests/test_store.py +0 -0
  206. {letify-2.6.1 → letify-2.6.2}/tests/test_strategies.py +0 -0
  207. {letify-2.6.1 → letify-2.6.2}/tests/test_stubs.py +0 -0
  208. {letify-2.6.1 → letify-2.6.2}/tests/test_transport.py +0 -0
  209. {letify-2.6.1 → letify-2.6.2}/tests/test_tunnel_setup.py +0 -0
  210. {letify-2.6.1 → letify-2.6.2}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: letify
3
- Version: 2.6.1
3
+ Version: 2.6.2
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
@@ -1261,7 +1261,7 @@ A `stat` cannot take that snapshot first, because it is on a data loader's hot p
1261
1261
 
1262
1262
  #### When a blob does not arrive <!-- id: project-data-streaming-failures -->
1263
1263
 
1264
- - **A blob that never arrives.** A `data_want` unanswered for `data_wait_timeout` seconds on the account, 600 by default, fails that open with `RuntimeFailure` naming the relative path and the digest. The call is not killed, because the body may handle it; the failure is infrastructure, so a retry of the call is allowed.
1264
+ - **A blob that never arrives.** A `data_want` whose blob has not grown for `data_wait_timeout` seconds on the account, 600 by default, fails that open with `RuntimeFailure` naming the relative path, the digest, and how much of the blob had arrived. The call is not killed, because the body may handle it; the failure is infrastructure, so a retry of the call is allowed. The timeout counts time without progress rather than total time, because a blob large enough to need longer than the timeout at the link's rate would otherwise fail however healthy the transfer was: 11.3 GB needs 19.3 MiB/s to land inside 600 s, and a Modal path measured at 45.3 MiB/s for one blob alone did not reach that behind 37 other files. The worker reads the size of the partial it is writing, so progress is the bytes on its own disk and not an estimate.
1265
1265
  - **A sender that stops.** The background sender raising leaves every blob still queued unsendable. letify says so once, `letify: data sending stopped after <n> of <n> files, <n> not sent: <error>`, and tells the runtime, which wakes every reader waiting on one of those blobs with `RuntimeFailure` naming the file and that reason, and fails a reader that arrives later at once. Waiting it out instead costs `data_wait_timeout` once for every blob still queued, which is how a hundred queued shards become an hour of silence.
1266
1266
  - **A client that disconnects mid stream.** The session ends as any lost channel ends it. Every blocked reader is woken with `RuntimeFailure`, the call ends with it, and no partial file is ever placed or read. A `.partial` file is removed when the worker next starts.
1267
1267
  - **A call that finishes before the background transfer.** The transfer is cancelled, both the channel sender and any bucket upload not yet started. A blob already committed to the cache stays and counts as held for the next call, and a partial one is removed. The write-back and the `data_evict` request follow the cancellation, in that order.
@@ -66,7 +66,7 @@ distributed: Final = Host.distributed
66
66
  # The enum class stays importable from letify.declare.instance for letify's own use.
67
67
  del Host
68
68
 
69
- __version__ = "2.6.1"
69
+ __version__ = "2.6.2"
70
70
 
71
71
 
72
72
  def fetch(tensor): # type: ignore[no-untyped-def]
@@ -1269,6 +1269,31 @@ def _op_data_failed(request):
1269
1269
  return {"ok": True, "value": True}
1270
1270
 
1271
1271
 
1272
+ def _data_received(blobs, digest):
1273
+ """Bytes of this blob that are on the runtime's disk, however they are arriving.
1274
+
1275
+ Spec "When a blob does not arrive": the wait counts time without progress, and this is
1276
+ what progress means. Reads the partial a single put writes and the one a parallel put
1277
+ fills, so neither shape looks stalled while it grows.
1278
+ """
1279
+ total = 0
1280
+ final = _data_file(blobs, digest)
1281
+ directory = os.path.dirname(final)
1282
+ name = os.path.basename(final)
1283
+ try:
1284
+ entries = os.listdir(directory)
1285
+ except OSError:
1286
+ return 0
1287
+ for entry in entries:
1288
+ if entry != name and not entry.startswith(name + "."):
1289
+ continue
1290
+ try:
1291
+ total = max(total, os.stat(os.path.join(directory, entry)).st_size)
1292
+ except OSError:
1293
+ continue
1294
+ return total
1295
+
1296
+
1272
1297
  def _data_want(digests, blocking=False):
1273
1298
  """Ask the local process for these blobs, in this order. Nothing replies to it."""
1274
1299
  if not digests:
@@ -1353,6 +1378,7 @@ def _data_wait(path):
1353
1378
  state["first"] = os.path.basename(path)
1354
1379
  deadline = started + state["wait"]
1355
1380
  _data_want([digest], True)
1381
+ received = _data_received(state["blobs"], digest)
1356
1382
  try:
1357
1383
  with _DATA_READY:
1358
1384
  while path in state["pending"] and state["dir"] in _DATA_CALLS:
@@ -1361,11 +1387,20 @@ def _data_wait(path):
1361
1387
  "%s was never sent: %s"
1362
1388
  % (os.path.basename(path), state["failed"])
1363
1389
  )
1390
+ # Spec "When a blob does not arrive": the wait counts time without
1391
+ # progress, because a blob needing longer than the timeout at the link's
1392
+ # rate would otherwise fail however healthy its transfer was.
1393
+ landed = _data_received(state["blobs"], digest)
1394
+ if landed > received:
1395
+ received = landed
1396
+ deadline = time.monotonic() + state["wait"]
1364
1397
  left = deadline - time.monotonic()
1365
1398
  if left <= 0:
1366
1399
  raise _DataFailure(
1367
- "the file blob %s of %s did not arrive within %.0f s"
1368
- % (digest, os.path.basename(path), state["wait"])
1400
+ "the file blob %s of %s did not arrive within %.0f s, "
1401
+ "%.1f MiB of %.1f MiB arrived"
1402
+ % (digest, os.path.basename(path), state["wait"],
1403
+ received / 1048576.0, size / 1048576.0)
1369
1404
  )
1370
1405
  _DATA_READY.wait(min(left, 0.5))
1371
1406
  finally:
@@ -138,7 +138,7 @@ dependencies = [
138
138
 
139
139
  [[package]]
140
140
  name = "letify-agent"
141
- version = "2.6.1"
141
+ version = "2.6.2"
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.6.1"
149
+ version = "2.6.2"
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.6.1"
167
+ version = "2.6.2"
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.6.1"
11
+ version = "2.6.2"
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.6.1",
3
+ "version": "2.6.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "letify-status",
9
- "version": "2.6.1",
9
+ "version": "2.6.2",
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.6.1",
5
+ "version": "2.6.2",
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.6.1"
3
+ version = "2.6.2"
4
4
  description = "Declarations that become infrastructure."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -15,6 +15,7 @@ from __future__ import annotations
15
15
 
16
16
  import collections
17
17
  import os
18
+ import pickle
18
19
  import re
19
20
  import tempfile
20
21
  import threading
@@ -1684,3 +1685,75 @@ def test_a_sender_that_stops_fails_the_readers_waiting_on_it(
1684
1685
  assert "was never sent" in str(failure.value), failure.value
1685
1686
  err = capsys.readouterr().err
1686
1687
  assert "data sending stopped after" in err, err
1688
+
1689
+
1690
+ def test_a_blob_still_arriving_does_not_time_out(let, cpu, project, monkeypatch) -> None:
1691
+ """Spec "When a blob does not arrive": the timeout counts time without progress.
1692
+
1693
+ KICT's 11.3 GB table failed after 600 s on a Modal path measured at 45.3 MiB/s for a
1694
+ blob on its own. 11.3 GB needs 19.3 MiB/s to land inside 600 s, so a healthy transfer
1695
+ of a blob that large always failed, and the line said 0.0 MiB/s because the rate counts
1696
+ only the blobs that completed.
1697
+ """
1698
+ from letify.store import pathdata
1699
+
1700
+ big = project / "slow.bin"
1701
+ big.write_bytes(b"s" * (6 << 20))
1702
+ monkeypatch.setattr(pathdata, "FIRST_WAVE_MIB", 0.0)
1703
+ monkeypatch.setattr(pathdata, "FIRST_WAVE_FILES", 0)
1704
+ monkeypatch.setattr(pathdata, "CHUNK", 1 << 20)
1705
+ # Each piece lands well inside the wait, and the whole blob takes longer than it.
1706
+ monkeypatch.setattr(pathdata, "WAIT_TIMEOUT_S", 4.0)
1707
+ def piecewise(runtime, pipeline, blobs, digest, local, size, meter):
1708
+ with open(local, "rb") as handle:
1709
+ offset = 0
1710
+ while offset < size:
1711
+ piece = handle.read(1 << 20)
1712
+ if not piece:
1713
+ break
1714
+ pipeline.send(
1715
+ {
1716
+ "op": "data_put",
1717
+ "dir": blobs,
1718
+ "digest": digest,
1719
+ "offset": offset,
1720
+ "chunk": pickle.PickleBuffer(piece),
1721
+ "last": offset + len(piece) >= size,
1722
+ }
1723
+ )
1724
+ offset += len(piece)
1725
+ time.sleep(3.0)
1726
+
1727
+ monkeypatch.setattr(pathdata, "_put", piecewise)
1728
+
1729
+ # retries=0, because a retry finds the blob already there and hides the timeout.
1730
+ @let.function(device=cpu, host=letify.remote, retries=0)
1731
+ def read_it(path) -> int:
1732
+ return len(path.read_bytes())
1733
+
1734
+ assert read_it(path=big) == 6 << 20
1735
+
1736
+
1737
+ def test_a_blob_that_never_starts_still_times_out_and_says_how_far_it_got(
1738
+ let, cpu, project, monkeypatch
1739
+ ) -> None:
1740
+ """Spec "When a blob does not arrive": the timeout still catches a blob that stalls."""
1741
+ from letify.store import pathdata
1742
+
1743
+ big = project / "stalled.bin"
1744
+ big.write_bytes(b"t" * (2 << 20))
1745
+ monkeypatch.setattr(pathdata, "FIRST_WAVE_MIB", 0.0)
1746
+ monkeypatch.setattr(pathdata, "FIRST_WAVE_FILES", 0)
1747
+ monkeypatch.setattr(pathdata, "WAIT_TIMEOUT_S", 4.0)
1748
+ # Nothing is ever sent, so nothing ever grows.
1749
+ monkeypatch.setattr(pathdata.Stream, "_send", lambda self, digest, meter: None)
1750
+
1751
+ @let.function(device=cpu, host=letify.remote)
1752
+ def read_it(path) -> int:
1753
+ return len(path.read_bytes())
1754
+
1755
+ with pytest.raises(RuntimeFailure) as failure:
1756
+ read_it(path=big)
1757
+ message = str(failure.value)
1758
+ assert "did not arrive" in message, message
1759
+ assert "arrived" in message, message
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes