letify 2.6.0__tar.gz → 2.6.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. {letify-2.6.0 → letify-2.6.1}/PKG-INFO +1 -1
  2. {letify-2.6.0 → letify-2.6.1}/docs/SPEC.md +2 -0
  3. {letify-2.6.0 → letify-2.6.1}/letify/__init__.py +1 -1
  4. {letify-2.6.0 → letify-2.6.1}/letify/runtime/bootstrap.py +12 -3
  5. {letify-2.6.0 → letify-2.6.1}/letify-core/Cargo.lock +3 -3
  6. {letify-2.6.0 → letify-2.6.1}/letify-core/Cargo.toml +1 -1
  7. {letify-2.6.0 → letify-2.6.1}/letify-ext/package-lock.json +2 -2
  8. {letify-2.6.0 → letify-2.6.1}/letify-ext/package.json +1 -1
  9. {letify-2.6.0 → letify-2.6.1}/pyproject.toml +1 -1
  10. {letify-2.6.0 → letify-2.6.1}/tests/test_runtime.py +41 -0
  11. {letify-2.6.0 → letify-2.6.1}/.github/workflows/ci.yml +0 -0
  12. {letify-2.6.0 → letify-2.6.1}/.github/workflows/docs.yml +0 -0
  13. {letify-2.6.0 → letify-2.6.1}/.github/workflows/publish.yml +0 -0
  14. {letify-2.6.0 → letify-2.6.1}/.gitignore +0 -0
  15. {letify-2.6.0 → letify-2.6.1}/.researchtree +0 -0
  16. {letify-2.6.0 → letify-2.6.1}/CLAUDE.md +0 -0
  17. {letify-2.6.0 → letify-2.6.1}/LICENSE +0 -0
  18. {letify-2.6.0 → letify-2.6.1}/PROJECT.md +0 -0
  19. {letify-2.6.0 → letify-2.6.1}/README.md +0 -0
  20. {letify-2.6.0 → letify-2.6.1}/docs/COMPONENT.md +0 -0
  21. {letify-2.6.0 → letify-2.6.1}/docs/INTENT.md +0 -0
  22. {letify-2.6.0 → letify-2.6.1}/docs/NETWORK.md +0 -0
  23. {letify-2.6.0 → letify-2.6.1}/docs/guide/01-getting-started.md +0 -0
  24. {letify-2.6.0 → letify-2.6.1}/docs/guide/02-providers.md +0 -0
  25. {letify-2.6.0 → letify-2.6.1}/docs/guide/03-execution-modes.md +0 -0
  26. {letify-2.6.0 → letify-2.6.1}/docs/guide/04-environments-and-data.md +0 -0
  27. {letify-2.6.0 → letify-2.6.1}/docs/guide/05-concurrency.md +0 -0
  28. {letify-2.6.0 → letify-2.6.1}/docs/guide/06-cost.md +0 -0
  29. {letify-2.6.0 → letify-2.6.1}/docs/guide/07-troubleshooting.md +0 -0
  30. {letify-2.6.0 → letify-2.6.1}/docs/guide/08-releasing.md +0 -0
  31. {letify-2.6.0 → letify-2.6.1}/docs/guide/README.md +0 -0
  32. {letify-2.6.0 → letify-2.6.1}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
  33. {letify-2.6.0 → letify-2.6.1}/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.0 → letify-2.6.1}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
  35. {letify-2.6.0 → letify-2.6.1}/docs/guide/ko/04-/355/231/230/352/262/275/352/263/274-/353/215/260/354/235/264/355/204/260.md" +0 -0
  36. {letify-2.6.0 → letify-2.6.1}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
  37. {letify-2.6.0 → letify-2.6.1}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
  38. {letify-2.6.0 → letify-2.6.1}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
  39. {letify-2.6.0 → letify-2.6.1}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
  40. {letify-2.6.0 → letify-2.6.1}/docs/guide/ko/README.md +0 -0
  41. {letify-2.6.0 → letify-2.6.1}/docs/locales/README_ko.md +0 -0
  42. {letify-2.6.0 → letify-2.6.1}/docs/mkdocs.yml +0 -0
  43. {letify-2.6.0 → letify-2.6.1}/examples/README.md +0 -0
  44. {letify-2.6.0 → letify-2.6.1}/examples/nvfp4-lora/.letify.example +0 -0
  45. {letify-2.6.0 → letify-2.6.1}/examples/nvfp4-lora/00_smoke.py +0 -0
  46. {letify-2.6.0 → letify-2.6.1}/examples/nvfp4-lora/01_sweep.py +0 -0
  47. {letify-2.6.0 → letify-2.6.1}/examples/nvfp4-lora/02_resume.py +0 -0
  48. {letify-2.6.0 → letify-2.6.1}/examples/nvfp4-lora/03_watch.py +0 -0
  49. {letify-2.6.0 → letify-2.6.1}/examples/nvfp4-lora/README.md +0 -0
  50. {letify-2.6.0 → letify-2.6.1}/examples/nvfp4-lora/README_ko.md +0 -0
  51. {letify-2.6.0 → letify-2.6.1}/examples/nvfp4-lora/common.py +0 -0
  52. {letify-2.6.0 → letify-2.6.1}/examples/nvfp4-lora/recipe.py +0 -0
  53. {letify-2.6.0 → letify-2.6.1}/letify/cli.py +0 -0
  54. {letify-2.6.0 → letify-2.6.1}/letify/config/__init__.py +0 -0
  55. {letify-2.6.0 → letify-2.6.1}/letify/config/inventory.py +0 -0
  56. {letify-2.6.0 → letify-2.6.1}/letify/config/login.py +0 -0
  57. {letify-2.6.0 → letify-2.6.1}/letify/config/schema.py +0 -0
  58. {letify-2.6.0 → letify-2.6.1}/letify/config/secrets.py +0 -0
  59. {letify-2.6.0 → letify-2.6.1}/letify/config/writer.py +0 -0
  60. {letify-2.6.0 → letify-2.6.1}/letify/declare/__init__.py +0 -0
  61. {letify-2.6.0 → letify-2.6.1}/letify/declare/cache.py +0 -0
  62. {letify-2.6.0 → letify-2.6.1}/letify/declare/env.py +0 -0
  63. {letify-2.6.0 → letify-2.6.1}/letify/declare/function.py +0 -0
  64. {letify-2.6.0 → letify-2.6.1}/letify/declare/instance.py +0 -0
  65. {letify-2.6.0 → letify-2.6.1}/letify/declare/reduce.py +0 -0
  66. {letify-2.6.0 → letify-2.6.1}/letify/declare/replicas.py +0 -0
  67. {letify-2.6.0 → letify-2.6.1}/letify/errors.py +0 -0
  68. {letify-2.6.0 → letify-2.6.1}/letify/install.py +0 -0
  69. {letify-2.6.0 → letify-2.6.1}/letify/launcher.py +0 -0
  70. {letify-2.6.0 → letify-2.6.1}/letify/paths.py +0 -0
  71. {letify-2.6.0 → letify-2.6.1}/letify/protocol/__init__.py +0 -0
  72. {letify-2.6.0 → letify-2.6.1}/letify/protocol/codec.py +0 -0
  73. {letify-2.6.0 → letify-2.6.1}/letify/protocol/driver.py +0 -0
  74. {letify-2.6.0 → letify-2.6.1}/letify/protocol/handle.py +0 -0
  75. {letify-2.6.0 → letify-2.6.1}/letify/protocol/wire.py +0 -0
  76. {letify-2.6.0 → letify-2.6.1}/letify/protocol/worker.py +0 -0
  77. {letify-2.6.0 → letify-2.6.1}/letify/providers/__init__.py +0 -0
  78. {letify-2.6.0 → letify-2.6.1}/letify/providers/base.py +0 -0
  79. {letify-2.6.0 → letify-2.6.1}/letify/providers/colab.py +0 -0
  80. {letify-2.6.0 → letify-2.6.1}/letify/providers/colab_files.py +0 -0
  81. {letify-2.6.0 → letify-2.6.1}/letify/providers/colab_keepalive.py +0 -0
  82. {letify-2.6.0 → letify-2.6.1}/letify/providers/elice.py +0 -0
  83. {letify-2.6.0 → letify-2.6.1}/letify/providers/kaggle.py +0 -0
  84. {letify-2.6.0 → letify-2.6.1}/letify/providers/kaggle_adapter.py +0 -0
  85. {letify-2.6.0 → letify-2.6.1}/letify/providers/local.py +0 -0
  86. {letify-2.6.0 → letify-2.6.1}/letify/providers/modal.py +0 -0
  87. {letify-2.6.0 → letify-2.6.1}/letify/providers/modal_adapter.py +0 -0
  88. {letify-2.6.0 → letify-2.6.1}/letify/providers/naming.py +0 -0
  89. {letify-2.6.0 → letify-2.6.1}/letify/providers/shell.py +0 -0
  90. {letify-2.6.0 → letify-2.6.1}/letify/providers/tunnel.py +0 -0
  91. {letify-2.6.0 → letify-2.6.1}/letify/providers/usage.py +0 -0
  92. {letify-2.6.0 → letify-2.6.1}/letify/remoting/__init__.py +0 -0
  93. {letify-2.6.0 → letify-2.6.1}/letify/remoting/capability.py +0 -0
  94. {letify-2.6.0 → letify-2.6.1}/letify/remoting/device/__init__.py +0 -0
  95. {letify-2.6.0 → letify-2.6.1}/letify/remoting/device/client.py +0 -0
  96. {letify-2.6.0 → letify-2.6.1}/letify/remoting/device/cuda.py +0 -0
  97. {letify-2.6.0 → letify-2.6.1}/letify/remoting/device/executor.py +0 -0
  98. {letify-2.6.0 → letify-2.6.1}/letify/remoting/device/frames.py +0 -0
  99. {letify-2.6.0 → letify-2.6.1}/letify/remoting/device/guard.py +0 -0
  100. {letify-2.6.0 → letify-2.6.1}/letify/remoting/device/tensor.py +0 -0
  101. {letify-2.6.0 → letify-2.6.1}/letify/remoting/device/trace.py +0 -0
  102. {letify-2.6.0 → letify-2.6.1}/letify/remoting/device/value.py +0 -0
  103. {letify-2.6.0 → letify-2.6.1}/letify/remoting/loader.py +0 -0
  104. {letify-2.6.0 → letify-2.6.1}/letify/remoting/probe.py +0 -0
  105. {letify-2.6.0 → letify-2.6.1}/letify/render.py +0 -0
  106. {letify-2.6.0 → letify-2.6.1}/letify/runtime/__init__.py +0 -0
  107. {letify-2.6.0 → letify-2.6.1}/letify/runtime/broker.py +0 -0
  108. {letify-2.6.0 → letify-2.6.1}/letify/runtime/channel.py +0 -0
  109. {letify-2.6.0 → letify-2.6.1}/letify/runtime/lease.py +0 -0
  110. {letify-2.6.0 → letify-2.6.1}/letify/runtime/pool.py +0 -0
  111. {letify-2.6.0 → letify-2.6.1}/letify/runtime/session.py +0 -0
  112. {letify-2.6.0 → letify-2.6.1}/letify/runtime/telemetry.py +0 -0
  113. {letify-2.6.0 → letify-2.6.1}/letify/store/__init__.py +0 -0
  114. {letify-2.6.0 → letify-2.6.1}/letify/store/backends/__init__.py +0 -0
  115. {letify-2.6.0 → letify-2.6.1}/letify/store/backends/filesystem.py +0 -0
  116. {letify-2.6.0 → letify-2.6.1}/letify/store/backends/google_auth.py +0 -0
  117. {letify-2.6.0 → letify-2.6.1}/letify/store/backends/layout.py +0 -0
  118. {letify-2.6.0 → letify-2.6.1}/letify/store/backends/objects.py +0 -0
  119. {letify-2.6.0 → letify-2.6.1}/letify/store/cas.py +0 -0
  120. {letify-2.6.0 → letify-2.6.1}/letify/store/pathdata.py +0 -0
  121. {letify-2.6.0 → letify-2.6.1}/letify/store/sendorder.py +0 -0
  122. {letify-2.6.0 → letify-2.6.1}/letify/store/volume.py +0 -0
  123. {letify-2.6.0 → letify-2.6.1}/letify/stubs.py +0 -0
  124. {letify-2.6.0 → letify-2.6.1}/letify/tools.py +0 -0
  125. {letify-2.6.0 → letify-2.6.1}/letify/transport/__init__.py +0 -0
  126. {letify-2.6.0 → letify-2.6.1}/letify/transport/agent.py +0 -0
  127. {letify-2.6.0 → letify-2.6.1}/letify/transport/announce.py +0 -0
  128. {letify-2.6.0 → letify-2.6.1}/letify/transport/link.py +0 -0
  129. {letify-2.6.0 → letify-2.6.1}/letify/transport/nat.py +0 -0
  130. {letify-2.6.0 → letify-2.6.1}/letify/transport/pipeline.py +0 -0
  131. {letify-2.6.0 → letify-2.6.1}/letify/transport/portmap.py +0 -0
  132. {letify-2.6.0 → letify-2.6.1}/letify/transport/probe.py +0 -0
  133. {letify-2.6.0 → letify-2.6.1}/letify/transport/quic.py +0 -0
  134. {letify-2.6.0 → letify-2.6.1}/letify/transport/rendezvous.py +0 -0
  135. {letify-2.6.0 → letify-2.6.1}/letify/transport/setup.py +0 -0
  136. {letify-2.6.0 → letify-2.6.1}/letify/transport/sshopts.py +0 -0
  137. {letify-2.6.0 → letify-2.6.1}/letify/transport/strategies.py +0 -0
  138. {letify-2.6.0 → letify-2.6.1}/letify-core/.gitignore +0 -0
  139. {letify-2.6.0 → letify-2.6.1}/letify-core/build.py +0 -0
  140. {letify-2.6.0 → letify-2.6.1}/letify-core/letify-agent/Cargo.toml +0 -0
  141. {letify-2.6.0 → letify-2.6.1}/letify-core/letify-agent/src/driver.rs +0 -0
  142. {letify-2.6.0 → letify-2.6.1}/letify-core/letify-agent/src/main.rs +0 -0
  143. {letify-2.6.0 → letify-2.6.1}/letify-core/letify-driver/Cargo.toml +0 -0
  144. {letify-2.6.0 → letify-2.6.1}/letify-core/letify-driver/src/client.rs +0 -0
  145. {letify-2.6.0 → letify-2.6.1}/letify-core/letify-driver/src/exports.rs +0 -0
  146. {letify-2.6.0 → letify-2.6.1}/letify-core/letify-driver/src/image.rs +0 -0
  147. {letify-2.6.0 → letify-2.6.1}/letify-core/letify-driver/src/lib.rs +0 -0
  148. {letify-2.6.0 → letify-2.6.1}/letify-core/letify-driver/src/status.rs +0 -0
  149. {letify-2.6.0 → letify-2.6.1}/letify-core/letify-driver/src/table.rs +0 -0
  150. {letify-2.6.0 → letify-2.6.1}/letify-core/letify-quic/Cargo.toml +0 -0
  151. {letify-2.6.0 → letify-2.6.1}/letify-core/letify-quic/src/main.rs +0 -0
  152. {letify-2.6.0 → letify-2.6.1}/letify-core/letify-wire/Cargo.toml +0 -0
  153. {letify-2.6.0 → letify-2.6.1}/letify-core/letify-wire/src/lib.rs +0 -0
  154. {letify-2.6.0 → letify-2.6.1}/letify-core/letify-wire/tests/throughput.rs +0 -0
  155. {letify-2.6.0 → letify-2.6.1}/letify-core/rust-toolchain.toml +0 -0
  156. {letify-2.6.0 → letify-2.6.1}/letify-ext/.vscodeignore +0 -0
  157. {letify-2.6.0 → letify-2.6.1}/letify-ext/LICENSE +0 -0
  158. {letify-2.6.0 → letify-2.6.1}/letify-ext/README.md +0 -0
  159. {letify-2.6.0 → letify-2.6.1}/letify-ext/media/icon.svg +0 -0
  160. {letify-2.6.0 → letify-2.6.1}/letify-ext/src/cli.ts +0 -0
  161. {letify-2.6.0 → letify-2.6.1}/letify-ext/src/extension.ts +0 -0
  162. {letify-2.6.0 → letify-2.6.1}/letify-ext/src/history.ts +0 -0
  163. {letify-2.6.0 → letify-2.6.1}/letify-ext/src/model.ts +0 -0
  164. {letify-2.6.0 → letify-2.6.1}/letify-ext/src/poll.ts +0 -0
  165. {letify-2.6.0 → letify-2.6.1}/letify-ext/src/render.ts +0 -0
  166. {letify-2.6.0 → letify-2.6.1}/letify-ext/test/cli.test.ts +0 -0
  167. {letify-2.6.0 → letify-2.6.1}/letify-ext/test/manifest.test.ts +0 -0
  168. {letify-2.6.0 → letify-2.6.1}/letify-ext/test/model.test.ts +0 -0
  169. {letify-2.6.0 → letify-2.6.1}/letify-ext/test/regressions.test.ts +0 -0
  170. {letify-2.6.0 → letify-2.6.1}/letify-ext/tsconfig.json +0 -0
  171. {letify-2.6.0 → letify-2.6.1}/letify_providers.py +0 -0
  172. {letify-2.6.0 → letify-2.6.1}/scripts/sync-main.sh +0 -0
  173. {letify-2.6.0 → letify-2.6.1}/scripts/version.py +0 -0
  174. {letify-2.6.0 → letify-2.6.1}/tests/conftest.py +0 -0
  175. {letify-2.6.0 → letify-2.6.1}/tests/fake_kaggle_adapter.py +0 -0
  176. {letify-2.6.0 → letify-2.6.1}/tests/fake_modal_adapter.py +0 -0
  177. {letify-2.6.0 → letify-2.6.1}/tests/test_cli.py +0 -0
  178. {letify-2.6.0 → letify-2.6.1}/tests/test_cli_output.py +0 -0
  179. {letify-2.6.0 → letify-2.6.1}/tests/test_colab_keepalive.py +0 -0
  180. {letify-2.6.0 → letify-2.6.1}/tests/test_config.py +0 -0
  181. {letify-2.6.0 → letify-2.6.1}/tests/test_core.py +0 -0
  182. {letify-2.6.0 → letify-2.6.1}/tests/test_data.py +0 -0
  183. {letify-2.6.0 → letify-2.6.1}/tests/test_data_streaming.py +0 -0
  184. {letify-2.6.0 → letify-2.6.1}/tests/test_declare.py +0 -0
  185. {letify-2.6.0 → letify-2.6.1}/tests/test_device.py +0 -0
  186. {letify-2.6.0 → letify-2.6.1}/tests/test_distributed.py +0 -0
  187. {letify-2.6.0 → letify-2.6.1}/tests/test_editor_sessions.py +0 -0
  188. {letify-2.6.0 → letify-2.6.1}/tests/test_elice.py +0 -0
  189. {letify-2.6.0 → letify-2.6.1}/tests/test_frames.py +0 -0
  190. {letify-2.6.0 → letify-2.6.1}/tests/test_install.py +0 -0
  191. {letify-2.6.0 → letify-2.6.1}/tests/test_kaggle.py +0 -0
  192. {letify-2.6.0 → letify-2.6.1}/tests/test_kaggle_cookie.py +0 -0
  193. {letify-2.6.0 → letify-2.6.1}/tests/test_kaggle_link.py +0 -0
  194. {letify-2.6.0 → letify-2.6.1}/tests/test_link_log.py +0 -0
  195. {letify-2.6.0 → letify-2.6.1}/tests/test_notebook.py +0 -0
  196. {letify-2.6.0 → letify-2.6.1}/tests/test_parallel_placement.py +0 -0
  197. {letify-2.6.0 → letify-2.6.1}/tests/test_paths.py +0 -0
  198. {letify-2.6.0 → letify-2.6.1}/tests/test_portmap.py +0 -0
  199. {letify-2.6.0 → letify-2.6.1}/tests/test_protocol.py +0 -0
  200. {letify-2.6.0 → letify-2.6.1}/tests/test_providers.py +0 -0
  201. {letify-2.6.0 → letify-2.6.1}/tests/test_quic.py +0 -0
  202. {letify-2.6.0 → letify-2.6.1}/tests/test_remoting.py +0 -0
  203. {letify-2.6.0 → letify-2.6.1}/tests/test_render.py +0 -0
  204. {letify-2.6.0 → letify-2.6.1}/tests/test_ship.py +0 -0
  205. {letify-2.6.0 → letify-2.6.1}/tests/test_store.py +0 -0
  206. {letify-2.6.0 → letify-2.6.1}/tests/test_strategies.py +0 -0
  207. {letify-2.6.0 → letify-2.6.1}/tests/test_stubs.py +0 -0
  208. {letify-2.6.0 → letify-2.6.1}/tests/test_transport.py +0 -0
  209. {letify-2.6.0 → letify-2.6.1}/tests/test_tunnel_setup.py +0 -0
  210. {letify-2.6.0 → letify-2.6.1}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: letify
3
- Version: 2.6.0
3
+ Version: 2.6.1
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
@@ -1498,6 +1498,8 @@ For an ephemeral provider that archives its environment, the default managed Pyt
1498
1498
 
1499
1499
  Otherwise the sync step sets `UV_CACHE_DIR`, `UV_PYTHON_INSTALL_DIR` and `UV_TOOL_DIR` under `<workspace root>` for `uv sync` and `uv pip install`, unconditionally: not only when the provider's `persistence` is `persistent`, and not only when it sets no `env_root`. An `Env.vars` entry naming one of the three wins over this rule for that variable.
1500
1500
 
1501
+ `UV_PYTHON_INSTALL_DIR` is the exception: it follows the project directory whenever the provider sets an `env_root`, because the interpreter belongs with the `.venv` it serves and a workspace root is not always a filesystem one can be unpacked on. Modal's workspace root is a Modal volume, measured as supporting no hard link at all: a link inside it fails with `EPERM` and a link from it to the sandbox disk with `EXDEV`. So `uv sync --python 3.13` on a sandbox whose image carries 3.12 downloaded a managed interpreter into the volume and failed with `error: Operation not permitted (os error 1)`, which named neither the directory nor the reason. The archive case already placed it there, for its own reason, so this makes the two agree.
1502
+
1501
1503
  On a runtime with a persistent workspace root the cache outlives a container rebuild along with the projects built from it. On an ephemeral provider the cache is filled once per runtime wherever it lives and discarded with it, but it is still written under the workspace root rather than the home directory, because the workspace root may be the only writable path.
1502
1504
 
1503
1505
  letify never deletes from the cache. The first sync on a runtime fills `<workspace root>/uv-cache` once, and a cache uv already had elsewhere is left in place. `uv cache prune` run with the same `UV_CACHE_DIR` removes entries no lock file needs any more; a file still hard linked from a `.venv` keeps its disk blocks until that `.venv` is removed as well.
@@ -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.0"
69
+ __version__ = "2.6.1"
70
70
 
71
71
 
72
72
  def fetch(tensor): # type: ignore[no-untyped-def]
@@ -357,7 +357,9 @@ def sync_source(
357
357
  It raises ``RuntimeError`` with ``uv could not be installed on <name>`` or ``uv sync
358
358
  failed on <name>`` so the local side can name the step that failed.
359
359
  """
360
- root = root or project_dir(DEFAULT_WORKSPACE_ROOT, env)
360
+ # Derived from the workspace root when it is given, so the two cannot disagree: the
361
+ # managed Python's directory is chosen by whether the project lies inside it.
362
+ root = root or project_dir(workspace or DEFAULT_WORKSPACE_ROOT, env)
361
363
  workspace = workspace or DEFAULT_WORKSPACE_ROOT
362
364
  encoded = {key: base64.b64encode(value).decode() for key, value in files.items()}
363
365
  sync_args = sync_command(env)[1:]
@@ -400,8 +402,15 @@ def sync_source(
400
402
  ]
401
403
  declared = dict(env.variables)
402
404
  directories = uv_env(workspace)
403
- if archive:
404
- # Keep an interpreter uv downloads inside the tree sent to the next VM.
405
+ inside = root.rstrip("/") == workspace.rstrip("/") or root.startswith(
406
+ workspace.rstrip("/") + "/"
407
+ )
408
+ if archive or not inside:
409
+ # Keep an interpreter uv downloads inside the tree sent to the next VM, and, when
410
+ # the project directory is off the workspace root, beside the .venv it serves: a
411
+ # workspace root is not always a filesystem an interpreter can be unpacked on.
412
+ # Spec "Remote paths letify writes": Modal's is a volume that carries no hard
413
+ # link, so unpacking one there fails with Operation not permitted.
405
414
  directories["UV_PYTHON_INSTALL_DIR"] = f"{root.rstrip('/')}/.letify-python"
406
415
  for key, value in directories.items():
407
416
  if key not in declared:
@@ -138,7 +138,7 @@ dependencies = [
138
138
 
139
139
  [[package]]
140
140
  name = "letify-agent"
141
- version = "2.6.0"
141
+ version = "2.6.1"
142
142
  dependencies = [
143
143
  "letify-wire",
144
144
  "libloading",
@@ -146,7 +146,7 @@ dependencies = [
146
146
 
147
147
  [[package]]
148
148
  name = "letify-driver"
149
- version = "2.6.0"
149
+ version = "2.6.1"
150
150
  dependencies = [
151
151
  "letify-wire",
152
152
  ]
@@ -164,7 +164,7 @@ dependencies = [
164
164
 
165
165
  [[package]]
166
166
  name = "letify-wire"
167
- version = "2.6.0"
167
+ version = "2.6.1"
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.0"
11
+ version = "2.6.1"
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.0",
3
+ "version": "2.6.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "letify-status",
9
- "version": "2.6.0",
9
+ "version": "2.6.1",
10
10
  "license": "SEE LICENSE IN LICENSE",
11
11
  "devDependencies": {
12
12
  "@types/node": "^18.19.0",
@@ -2,7 +2,7 @@
2
2
  "name": "letify-status",
3
3
  "displayName": "letify Status",
4
4
  "description": "letify quota and GPU activity in the status bar, read from the letify CLI.",
5
- "version": "2.6.0",
5
+ "version": "2.6.1",
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.0"
3
+ version = "2.6.1"
4
4
  description = "Declarations that become infrastructure."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -2523,3 +2523,44 @@ def test_a_lock_naming_cloudpickle_alone_is_accepted(tmp_path) -> None:
2523
2523
  (tmp_path / "uv.lock").write_text('version = 1\n\n[[package]]\nname = "cloudpickle"\n')
2524
2524
  files = bootstrap.project_files(Env(lock=str(tmp_path / "uv.lock")))
2525
2525
  assert b"cloudpickle" in files["uv.lock"]
2526
+
2527
+
2528
+ def test_the_managed_python_goes_with_the_venv_when_the_provider_sets_an_env_root() -> None:
2529
+ """Spec "Remote paths letify writes": a workspace root is not always unpackable.
2530
+
2531
+ Modal's workspace root is a Modal volume, which supports no hard link at all: a link
2532
+ inside it fails with EPERM and one to the sandbox disk with EXDEV. So uv downloading a
2533
+ managed interpreter into it failed with "Operation not permitted (os error 1)", naming
2534
+ neither the directory nor the reason. Reproduced on a live Modal T4 sandbox: the same
2535
+ sync with that directory on the sandbox disk succeeds.
2536
+ """
2537
+ from letify.declare.env import Env
2538
+ from letify.runtime import bootstrap
2539
+
2540
+ env = Env()
2541
+ project = "/root/.letify-env/project/abc123"
2542
+ files = {"pyproject.toml": b"[project]\n", "uv.lock": b""}
2543
+ # A persistent provider archives nothing, which is the path that was left on the volume.
2544
+ source = bootstrap.sync_source(
2545
+ env, files, root=project, name="sandbox", workspace="/letify", archive=False
2546
+ )
2547
+ assert f"UV_PYTHON_INSTALL_DIR'] = os.path.expanduser('{project}/.letify-python')" in source
2548
+ # The cache and the tool directory stay on the workspace root, which outlives a sandbox
2549
+ # and was measured to carry them.
2550
+ assert "UV_CACHE_DIR'] = os.path.expanduser('/letify/uv-cache')" in source
2551
+ assert "UV_TOOL_DIR'] = os.path.expanduser('/letify/uv-tool')" in source
2552
+
2553
+
2554
+ def test_the_managed_python_stays_on_the_workspace_root_without_an_env_root() -> None:
2555
+ """Spec "Remote paths letify writes": the workspace root is the only writable path
2556
+ some accounts have, so nothing moves where the venv is already under it."""
2557
+ from letify.declare.env import Env
2558
+ from letify.runtime import bootstrap
2559
+
2560
+ env = Env()
2561
+ project = "/workspace/project/abc123"
2562
+ files = {"pyproject.toml": b"[project]\n", "uv.lock": b""}
2563
+ source = bootstrap.sync_source(
2564
+ env, files, root=project, name="lab", workspace="/workspace", archive=False
2565
+ )
2566
+ assert "UV_PYTHON_INSTALL_DIR'] = os.path.expanduser('/workspace/uv-python')" in source
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes