letify 2.10.2__tar.gz → 2.10.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. {letify-2.10.2 → letify-2.10.3}/PKG-INFO +1 -1
  2. {letify-2.10.2 → letify-2.10.3}/docs/SPEC.md +1 -0
  3. {letify-2.10.2 → letify-2.10.3}/letify/__init__.py +1 -1
  4. {letify-2.10.2 → letify-2.10.3}/letify/providers/kaggle.py +10 -0
  5. {letify-2.10.2 → letify-2.10.3}/letify-core/Cargo.lock +2 -2
  6. {letify-2.10.2 → letify-2.10.3}/letify-core/Cargo.toml +1 -1
  7. {letify-2.10.2 → letify-2.10.3}/letify-ext/package-lock.json +2 -2
  8. {letify-2.10.2 → letify-2.10.3}/letify-ext/package.json +1 -1
  9. {letify-2.10.2 → letify-2.10.3}/pyproject.toml +1 -1
  10. {letify-2.10.2 → letify-2.10.3}/tests/test_kaggle.py +34 -0
  11. {letify-2.10.2 → letify-2.10.3}/.github/workflows/ci.yml +0 -0
  12. {letify-2.10.2 → letify-2.10.3}/.github/workflows/docs.yml +0 -0
  13. {letify-2.10.2 → letify-2.10.3}/.github/workflows/publish-crates.yml +0 -0
  14. {letify-2.10.2 → letify-2.10.3}/.github/workflows/publish.yml +0 -0
  15. {letify-2.10.2 → letify-2.10.3}/.github/workflows/reserve-crates.yml +0 -0
  16. {letify-2.10.2 → letify-2.10.3}/.gitignore +0 -0
  17. {letify-2.10.2 → letify-2.10.3}/.researchtree +0 -0
  18. {letify-2.10.2 → letify-2.10.3}/CLAUDE.md +0 -0
  19. {letify-2.10.2 → letify-2.10.3}/LICENSE +0 -0
  20. {letify-2.10.2 → letify-2.10.3}/PROJECT.md +0 -0
  21. {letify-2.10.2 → letify-2.10.3}/README.md +0 -0
  22. {letify-2.10.2 → letify-2.10.3}/docs/COMPONENT.md +0 -0
  23. {letify-2.10.2 → letify-2.10.3}/docs/INTENT.md +0 -0
  24. {letify-2.10.2 → letify-2.10.3}/docs/NETWORK.md +0 -0
  25. {letify-2.10.2 → letify-2.10.3}/docs/guide/01-getting-started.md +0 -0
  26. {letify-2.10.2 → letify-2.10.3}/docs/guide/02-providers.md +0 -0
  27. {letify-2.10.2 → letify-2.10.3}/docs/guide/03-execution-modes.md +0 -0
  28. {letify-2.10.2 → letify-2.10.3}/docs/guide/04-environments-and-data.md +0 -0
  29. {letify-2.10.2 → letify-2.10.3}/docs/guide/05-concurrency.md +0 -0
  30. {letify-2.10.2 → letify-2.10.3}/docs/guide/06-cost.md +0 -0
  31. {letify-2.10.2 → letify-2.10.3}/docs/guide/07-troubleshooting.md +0 -0
  32. {letify-2.10.2 → letify-2.10.3}/docs/guide/08-releasing.md +0 -0
  33. {letify-2.10.2 → letify-2.10.3}/docs/guide/README.md +0 -0
  34. {letify-2.10.2 → letify-2.10.3}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
  35. {letify-2.10.2 → letify-2.10.3}/docs/guide/ko/02-/355/224/204/353/241/234/353/260/224/354/235/264/353/215/224.md" +0 -0
  36. {letify-2.10.2 → letify-2.10.3}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
  37. {letify-2.10.2 → letify-2.10.3}/docs/guide/ko/04-/355/231/230/352/262/275/352/263/274-/353/215/260/354/235/264/355/204/260.md" +0 -0
  38. {letify-2.10.2 → letify-2.10.3}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
  39. {letify-2.10.2 → letify-2.10.3}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
  40. {letify-2.10.2 → letify-2.10.3}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
  41. {letify-2.10.2 → letify-2.10.3}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
  42. {letify-2.10.2 → letify-2.10.3}/docs/guide/ko/README.md +0 -0
  43. {letify-2.10.2 → letify-2.10.3}/docs/locales/README_ko.md +0 -0
  44. {letify-2.10.2 → letify-2.10.3}/docs/mkdocs.yml +0 -0
  45. {letify-2.10.2 → letify-2.10.3}/examples/README.md +0 -0
  46. {letify-2.10.2 → letify-2.10.3}/examples/nvfp4-lora/.letify.example +0 -0
  47. {letify-2.10.2 → letify-2.10.3}/examples/nvfp4-lora/00_smoke.py +0 -0
  48. {letify-2.10.2 → letify-2.10.3}/examples/nvfp4-lora/01_sweep.py +0 -0
  49. {letify-2.10.2 → letify-2.10.3}/examples/nvfp4-lora/02_resume.py +0 -0
  50. {letify-2.10.2 → letify-2.10.3}/examples/nvfp4-lora/03_watch.py +0 -0
  51. {letify-2.10.2 → letify-2.10.3}/examples/nvfp4-lora/README.md +0 -0
  52. {letify-2.10.2 → letify-2.10.3}/examples/nvfp4-lora/README_ko.md +0 -0
  53. {letify-2.10.2 → letify-2.10.3}/examples/nvfp4-lora/common.py +0 -0
  54. {letify-2.10.2 → letify-2.10.3}/examples/nvfp4-lora/recipe.py +0 -0
  55. {letify-2.10.2 → letify-2.10.3}/letify/cli.py +0 -0
  56. {letify-2.10.2 → letify-2.10.3}/letify/config/__init__.py +0 -0
  57. {letify-2.10.2 → letify-2.10.3}/letify/config/inventory.py +0 -0
  58. {letify-2.10.2 → letify-2.10.3}/letify/config/login.py +0 -0
  59. {letify-2.10.2 → letify-2.10.3}/letify/config/schema.py +0 -0
  60. {letify-2.10.2 → letify-2.10.3}/letify/config/secrets.py +0 -0
  61. {letify-2.10.2 → letify-2.10.3}/letify/config/writer.py +0 -0
  62. {letify-2.10.2 → letify-2.10.3}/letify/declare/__init__.py +0 -0
  63. {letify-2.10.2 → letify-2.10.3}/letify/declare/cache.py +0 -0
  64. {letify-2.10.2 → letify-2.10.3}/letify/declare/env.py +0 -0
  65. {letify-2.10.2 → letify-2.10.3}/letify/declare/exit.py +0 -0
  66. {letify-2.10.2 → letify-2.10.3}/letify/declare/function.py +0 -0
  67. {letify-2.10.2 → letify-2.10.3}/letify/declare/instance.py +0 -0
  68. {letify-2.10.2 → letify-2.10.3}/letify/declare/reduce.py +0 -0
  69. {letify-2.10.2 → letify-2.10.3}/letify/declare/replicas.py +0 -0
  70. {letify-2.10.2 → letify-2.10.3}/letify/errors.py +0 -0
  71. {letify-2.10.2 → letify-2.10.3}/letify/install.py +0 -0
  72. {letify-2.10.2 → letify-2.10.3}/letify/launcher.py +0 -0
  73. {letify-2.10.2 → letify-2.10.3}/letify/paths.py +0 -0
  74. {letify-2.10.2 → letify-2.10.3}/letify/protocol/__init__.py +0 -0
  75. {letify-2.10.2 → letify-2.10.3}/letify/protocol/codec.py +0 -0
  76. {letify-2.10.2 → letify-2.10.3}/letify/protocol/driver.py +0 -0
  77. {letify-2.10.2 → letify-2.10.3}/letify/protocol/handle.py +0 -0
  78. {letify-2.10.2 → letify-2.10.3}/letify/protocol/wire.py +0 -0
  79. {letify-2.10.2 → letify-2.10.3}/letify/protocol/worker.py +0 -0
  80. {letify-2.10.2 → letify-2.10.3}/letify/providers/__init__.py +0 -0
  81. {letify-2.10.2 → letify-2.10.3}/letify/providers/base.py +0 -0
  82. {letify-2.10.2 → letify-2.10.3}/letify/providers/colab.py +0 -0
  83. {letify-2.10.2 → letify-2.10.3}/letify/providers/colab_files.py +0 -0
  84. {letify-2.10.2 → letify-2.10.3}/letify/providers/colab_keepalive.py +0 -0
  85. {letify-2.10.2 → letify-2.10.3}/letify/providers/elice.py +0 -0
  86. {letify-2.10.2 → letify-2.10.3}/letify/providers/kaggle_adapter.py +0 -0
  87. {letify-2.10.2 → letify-2.10.3}/letify/providers/local.py +0 -0
  88. {letify-2.10.2 → letify-2.10.3}/letify/providers/modal.py +0 -0
  89. {letify-2.10.2 → letify-2.10.3}/letify/providers/modal_adapter.py +0 -0
  90. {letify-2.10.2 → letify-2.10.3}/letify/providers/naming.py +0 -0
  91. {letify-2.10.2 → letify-2.10.3}/letify/providers/shell.py +0 -0
  92. {letify-2.10.2 → letify-2.10.3}/letify/providers/tunnel.py +0 -0
  93. {letify-2.10.2 → letify-2.10.3}/letify/providers/usage.py +0 -0
  94. {letify-2.10.2 → letify-2.10.3}/letify/remoting/__init__.py +0 -0
  95. {letify-2.10.2 → letify-2.10.3}/letify/remoting/device/__init__.py +0 -0
  96. {letify-2.10.2 → letify-2.10.3}/letify/remoting/device/client.py +0 -0
  97. {letify-2.10.2 → letify-2.10.3}/letify/remoting/device/cuda.py +0 -0
  98. {letify-2.10.2 → letify-2.10.3}/letify/remoting/device/executor.py +0 -0
  99. {letify-2.10.2 → letify-2.10.3}/letify/remoting/device/frames.py +0 -0
  100. {letify-2.10.2 → letify-2.10.3}/letify/remoting/device/guard.py +0 -0
  101. {letify-2.10.2 → letify-2.10.3}/letify/remoting/device/tensor.py +0 -0
  102. {letify-2.10.2 → letify-2.10.3}/letify/remoting/device/trace.py +0 -0
  103. {letify-2.10.2 → letify-2.10.3}/letify/remoting/device/value.py +0 -0
  104. {letify-2.10.2 → letify-2.10.3}/letify/render.py +0 -0
  105. {letify-2.10.2 → letify-2.10.3}/letify/runtime/__init__.py +0 -0
  106. {letify-2.10.2 → letify-2.10.3}/letify/runtime/bootstrap.py +0 -0
  107. {letify-2.10.2 → letify-2.10.3}/letify/runtime/broker.py +0 -0
  108. {letify-2.10.2 → letify-2.10.3}/letify/runtime/channel.py +0 -0
  109. {letify-2.10.2 → letify-2.10.3}/letify/runtime/control.py +0 -0
  110. {letify-2.10.2 → letify-2.10.3}/letify/runtime/lease.py +0 -0
  111. {letify-2.10.2 → letify-2.10.3}/letify/runtime/pool.py +0 -0
  112. {letify-2.10.2 → letify-2.10.3}/letify/runtime/session.py +0 -0
  113. {letify-2.10.2 → letify-2.10.3}/letify/runtime/takeover.py +0 -0
  114. {letify-2.10.2 → letify-2.10.3}/letify/runtime/telemetry.py +0 -0
  115. {letify-2.10.2 → letify-2.10.3}/letify/runtime/workers.py +0 -0
  116. {letify-2.10.2 → letify-2.10.3}/letify/store/__init__.py +0 -0
  117. {letify-2.10.2 → letify-2.10.3}/letify/store/backends/__init__.py +0 -0
  118. {letify-2.10.2 → letify-2.10.3}/letify/store/backends/drive.py +0 -0
  119. {letify-2.10.2 → letify-2.10.3}/letify/store/backends/filesystem.py +0 -0
  120. {letify-2.10.2 → letify-2.10.3}/letify/store/backends/google_auth.py +0 -0
  121. {letify-2.10.2 → letify-2.10.3}/letify/store/backends/layout.py +0 -0
  122. {letify-2.10.2 → letify-2.10.3}/letify/store/backends/objects.py +0 -0
  123. {letify-2.10.2 → letify-2.10.3}/letify/store/cas.py +0 -0
  124. {letify-2.10.2 → letify-2.10.3}/letify/store/handoff.py +0 -0
  125. {letify-2.10.2 → letify-2.10.3}/letify/store/pathdata.py +0 -0
  126. {letify-2.10.2 → letify-2.10.3}/letify/store/sendorder.py +0 -0
  127. {letify-2.10.2 → letify-2.10.3}/letify/store/volume.py +0 -0
  128. {letify-2.10.2 → letify-2.10.3}/letify/stubs.py +0 -0
  129. {letify-2.10.2 → letify-2.10.3}/letify/tools.py +0 -0
  130. {letify-2.10.2 → letify-2.10.3}/letify/transport/__init__.py +0 -0
  131. {letify-2.10.2 → letify-2.10.3}/letify/transport/agent.py +0 -0
  132. {letify-2.10.2 → letify-2.10.3}/letify/transport/announce.py +0 -0
  133. {letify-2.10.2 → letify-2.10.3}/letify/transport/link.py +0 -0
  134. {letify-2.10.2 → letify-2.10.3}/letify/transport/nat.py +0 -0
  135. {letify-2.10.2 → letify-2.10.3}/letify/transport/pipeline.py +0 -0
  136. {letify-2.10.2 → letify-2.10.3}/letify/transport/portmap.py +0 -0
  137. {letify-2.10.2 → letify-2.10.3}/letify/transport/probe.py +0 -0
  138. {letify-2.10.2 → letify-2.10.3}/letify/transport/quic.py +0 -0
  139. {letify-2.10.2 → letify-2.10.3}/letify/transport/rendezvous.py +0 -0
  140. {letify-2.10.2 → letify-2.10.3}/letify/transport/setup.py +0 -0
  141. {letify-2.10.2 → letify-2.10.3}/letify/transport/sshopts.py +0 -0
  142. {letify-2.10.2 → letify-2.10.3}/letify/transport/strategies.py +0 -0
  143. {letify-2.10.2 → letify-2.10.3}/letify-core/.gitignore +0 -0
  144. {letify-2.10.2 → letify-2.10.3}/letify-core/build.py +0 -0
  145. {letify-2.10.2 → letify-2.10.3}/letify-core/letify/Cargo.toml +0 -0
  146. {letify-2.10.2 → letify-2.10.3}/letify-core/letify/LICENSE +0 -0
  147. {letify-2.10.2 → letify-2.10.3}/letify-core/letify/README.md +0 -0
  148. {letify-2.10.2 → letify-2.10.3}/letify-core/letify/src/lib.rs +0 -0
  149. {letify-2.10.2 → letify-2.10.3}/letify-core/letify-quic/Cargo.toml +0 -0
  150. {letify-2.10.2 → letify-2.10.3}/letify-core/letify-quic/src/main.rs +0 -0
  151. {letify-2.10.2 → letify-2.10.3}/letify-core/rust-toolchain.toml +0 -0
  152. {letify-2.10.2 → letify-2.10.3}/letify-ext/.vscodeignore +0 -0
  153. {letify-2.10.2 → letify-2.10.3}/letify-ext/LICENSE +0 -0
  154. {letify-2.10.2 → letify-2.10.3}/letify-ext/README.md +0 -0
  155. {letify-2.10.2 → letify-2.10.3}/letify-ext/media/icon.svg +0 -0
  156. {letify-2.10.2 → letify-2.10.3}/letify-ext/src/cli.ts +0 -0
  157. {letify-2.10.2 → letify-2.10.3}/letify-ext/src/extension.ts +0 -0
  158. {letify-2.10.2 → letify-2.10.3}/letify-ext/src/history.ts +0 -0
  159. {letify-2.10.2 → letify-2.10.3}/letify-ext/src/model.ts +0 -0
  160. {letify-2.10.2 → letify-2.10.3}/letify-ext/src/poll.ts +0 -0
  161. {letify-2.10.2 → letify-2.10.3}/letify-ext/src/render.ts +0 -0
  162. {letify-2.10.2 → letify-2.10.3}/letify-ext/test/cli.test.ts +0 -0
  163. {letify-2.10.2 → letify-2.10.3}/letify-ext/test/manifest.test.ts +0 -0
  164. {letify-2.10.2 → letify-2.10.3}/letify-ext/test/model.test.ts +0 -0
  165. {letify-2.10.2 → letify-2.10.3}/letify-ext/test/regressions.test.ts +0 -0
  166. {letify-2.10.2 → letify-2.10.3}/letify-ext/tsconfig.json +0 -0
  167. {letify-2.10.2 → letify-2.10.3}/letify_providers.py +0 -0
  168. {letify-2.10.2 → letify-2.10.3}/scripts/sync-main.sh +0 -0
  169. {letify-2.10.2 → letify-2.10.3}/scripts/version.py +0 -0
  170. {letify-2.10.2 → letify-2.10.3}/tests/conftest.py +0 -0
  171. {letify-2.10.2 → letify-2.10.3}/tests/fake_kaggle_adapter.py +0 -0
  172. {letify-2.10.2 → letify-2.10.3}/tests/fake_modal_adapter.py +0 -0
  173. {letify-2.10.2 → letify-2.10.3}/tests/test_cli.py +0 -0
  174. {letify-2.10.2 → letify-2.10.3}/tests/test_cli_output.py +0 -0
  175. {letify-2.10.2 → letify-2.10.3}/tests/test_colab_keepalive.py +0 -0
  176. {letify-2.10.2 → letify-2.10.3}/tests/test_config.py +0 -0
  177. {letify-2.10.2 → letify-2.10.3}/tests/test_core.py +0 -0
  178. {letify-2.10.2 → letify-2.10.3}/tests/test_data.py +0 -0
  179. {letify-2.10.2 → letify-2.10.3}/tests/test_data_streaming.py +0 -0
  180. {letify-2.10.2 → letify-2.10.3}/tests/test_declare.py +0 -0
  181. {letify-2.10.2 → letify-2.10.3}/tests/test_device.py +0 -0
  182. {letify-2.10.2 → letify-2.10.3}/tests/test_device_compile.py +0 -0
  183. {letify-2.10.2 → letify-2.10.3}/tests/test_distributed.py +0 -0
  184. {letify-2.10.2 → letify-2.10.3}/tests/test_drive_handoff.py +0 -0
  185. {letify-2.10.2 → letify-2.10.3}/tests/test_drive_store.py +0 -0
  186. {letify-2.10.2 → letify-2.10.3}/tests/test_editor_sessions.py +0 -0
  187. {letify-2.10.2 → letify-2.10.3}/tests/test_elice.py +0 -0
  188. {letify-2.10.2 → letify-2.10.3}/tests/test_frames.py +0 -0
  189. {letify-2.10.2 → letify-2.10.3}/tests/test_install.py +0 -0
  190. {letify-2.10.2 → letify-2.10.3}/tests/test_kaggle_cookie.py +0 -0
  191. {letify-2.10.2 → letify-2.10.3}/tests/test_kaggle_link.py +0 -0
  192. {letify-2.10.2 → letify-2.10.3}/tests/test_link_log.py +0 -0
  193. {letify-2.10.2 → letify-2.10.3}/tests/test_notebook.py +0 -0
  194. {letify-2.10.2 → letify-2.10.3}/tests/test_parallel_placement.py +0 -0
  195. {letify-2.10.2 → letify-2.10.3}/tests/test_paths.py +0 -0
  196. {letify-2.10.2 → letify-2.10.3}/tests/test_portmap.py +0 -0
  197. {letify-2.10.2 → letify-2.10.3}/tests/test_protocol.py +0 -0
  198. {letify-2.10.2 → letify-2.10.3}/tests/test_providers.py +0 -0
  199. {letify-2.10.2 → letify-2.10.3}/tests/test_punch_sockets.py +0 -0
  200. {letify-2.10.2 → letify-2.10.3}/tests/test_quic.py +0 -0
  201. {letify-2.10.2 → letify-2.10.3}/tests/test_remoting.py +0 -0
  202. {letify-2.10.2 → letify-2.10.3}/tests/test_render.py +0 -0
  203. {letify-2.10.2 → letify-2.10.3}/tests/test_run_control.py +0 -0
  204. {letify-2.10.2 → letify-2.10.3}/tests/test_run_takeover.py +0 -0
  205. {letify-2.10.2 → letify-2.10.3}/tests/test_runtime.py +0 -0
  206. {letify-2.10.2 → letify-2.10.3}/tests/test_shell_workers.py +0 -0
  207. {letify-2.10.2 → letify-2.10.3}/tests/test_ship.py +0 -0
  208. {letify-2.10.2 → letify-2.10.3}/tests/test_store.py +0 -0
  209. {letify-2.10.2 → letify-2.10.3}/tests/test_strategies.py +0 -0
  210. {letify-2.10.2 → letify-2.10.3}/tests/test_stubs.py +0 -0
  211. {letify-2.10.2 → letify-2.10.3}/tests/test_transport.py +0 -0
  212. {letify-2.10.2 → letify-2.10.3}/tests/test_tunnel_setup.py +0 -0
  213. {letify-2.10.2 → letify-2.10.3}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: letify
3
- Version: 2.10.2
3
+ Version: 2.10.3
4
4
  Summary: Declarations that become infrastructure.
5
5
  Project-URL: Homepage, https://github.com/DarkPyonix/letify
6
6
  Project-URL: Documentation, https://github.com/DarkPyonix/letify/tree/main/docs
@@ -281,6 +281,7 @@ Not opening a link is a decision of this project, not a rule of Kaggle's. Two so
281
281
  - **Token input and notebook owner.** `letify login kaggle <alias>` accepts `--key` or hidden input asking for the API token exactly as kaggle.com shows it, including `KGAT_`. It requires that prefix and a nonempty body with no whitespace; it does not strip or reformat the token. Length is not fixed because the measured 32-character body establishes a working token, not a universal length contract. A malformed token is refused with a message naming what to paste, before any cookie verification or file write. The token is not verified online at login. A renewal asks for each credential rather than reusing what is on disk, and a blank answer keeps the stored one. The two credentials expire on different clocks: the cookie is a thirty day web session and the token lasts until it is rotated, so renewing one must not demand the other. `letify login kaggle <alias> --replace` therefore prompts for the token and then the cookie, each saying that blank keeps what is stored, and writes only what was answered. A blank answer with nothing stored is refused, naming what to paste. Silently reusing a stored credential would make a renewal a no-op with no way to replace a credential that stopped working. Login retains `--username` or a username prompt and stores it in `username` beside the credentials with mode 0600. This is only the owner for `kernels delete -y <owner>/<slug>`, not an authentication credential. Explicit input avoids another authenticated lookup and does not mistake a cookie display name for an owner. The owner is not read from an authenticated call. Reading an existing `access_token` file strips leading and trailing whitespace, including a trailing newline, before CLI calls or reuse by login when `--key` is omitted. Explicit `--key` and prompted token input still reject whitespace rather than reformatting it, so adding an owner with `--username` does not require pasting the token again. The owner is a public Kaggle username, not a secret, and its file uses the same owner-only permissions as the account directory. Login refuses a missing or blank owner before cookie verification or any file write. Nothing is written until both credentials, the owner and cookie verification pass. Before any session starts or notebook is created, `open_channel` requires a readable, nonempty `username` file. An incomplete account is refused with `ConfigError` naming the login command, so a run cannot reach teardown without a notebook owner.
282
282
  - **The cookie carries an ``exp`` claim, but that claim is not proof the cookie still works.** The `CLIENT-TOKEN` cookie is an `alg:none` JWT whose `exp` claim was thirty days after the login that minted it, in every case observed. But a live account has been seen with over nine days left on that claim while every internal call, including the plain `users.UsersService/GetCurrentUser` read, answered as a refused, anonymous session: Kaggle invalidates a cookie server side, for reasons this project has not pinned down, without the claim changing to say so. So `exp` is useful for exactly one direction: a cookie past its `exp` is certainly dead. It says nothing about a cookie before its `exp`, which may be live or may already be refused.
283
283
  - **Two checks, for two costs.** An offline check reads only the claim, costs no network call, and is what `letify providers` shows as the account's `exp claim says N days left (not checked live; see letify usage)`, or `EXPIRED` once `exp` has passed. An online check additionally calls `GetCurrentUser`, the same call the login proved the cookie with, and is made once per session start (`open_channel`) and once per `letify usage` read, each result trusted for sixty seconds so starting several runtimes together costs one round trip rather than one per runtime. A run whose cookie fails either check is refused with `ConfigError`: missing or past `exp` says exactly that; refused online says "the Kaggle cookie has not expired, but Kaggle no longer accepts it", which is the one fact that check establishes, never a guess at why. A run is also refused when fewer than one hour is left on the `exp` claim, since starting a session the claim says the cookie cannot outlive risks it dying mid run with no way to tell that apart from any other lost runtime; this offline guard and the online check both have to pass.
284
+ - **A refused session can answer HTTP 400 with an empty body.** When the `XSRF-TOKEN` cookie in the stored cookie is no longer the one Kaggle issued for the session, `GetCurrentUser` answers HTTP 400, 401 or 403 instead of an empty account. The online check treats those three statuses as Kaggle refusing the cookie and reports `the Kaggle cookie was refused (HTTP <status>)` with the instruction to log in again. Any other HTTP status, such as 500, and any transport failure is not an answer about the cookie: the check reports `the Kaggle cookie could not be checked: HTTP <status>` or the exception class name, and the run fails as an infrastructure failure rather than as a bad cookie.
284
285
  - The online check is not made on every call into a running session, only at the two points above, so it is not the cost of every request a declared function makes.
285
286
  - Everything after that registration is what every other provider does: the session builds the declared environment with `uv sync`, enters a workspace root, transfers files and checks the worker's interpreter. Kaggle gets no exemption from any of it.
286
287
 
@@ -70,7 +70,7 @@ distributed: Final = Host.distributed
70
70
  # The enum class stays importable from letify.declare.instance for letify's own use.
71
71
  del Host
72
72
 
73
- __version__ = "2.10.2"
73
+ __version__ = "2.10.3"
74
74
 
75
75
 
76
76
  def fetch(tensor): # type: ignore[no-untyped-def]
@@ -89,6 +89,9 @@ RETIRED = {
89
89
  KAGGLE_INTERNAL = "https://www.kaggle.com/api/i/"
90
90
  KERNELS_SERVICE = "kernels.KernelsService/"
91
91
  USERS_SERVICE = "users.UsersService/"
92
+ #: HTTP statuses ``GetCurrentUser`` answers for a session Kaggle no longer accepts. A stale
93
+ #: ``XSRF-TOKEN`` cookie is answered with 400 and an empty body.
94
+ REFUSING_STATUSES = frozenset({400, 401, 403})
92
95
 
93
96
  #: The host that routes to a session's Jupyter server, and the language id for Python.
94
97
  JUPYTER_PROXY_HOST = "https://kkb-production.jupyter-proxy.kaggle.net"
@@ -256,6 +259,13 @@ def verify_cookie(cookie: str) -> str:
256
259
  try:
257
260
  with urlopen(request) as response:
258
261
  body = json.loads(response.read())
262
+ except urllib.error.HTTPError as exc:
263
+ if exc.code in REFUSING_STATUSES:
264
+ raise ValueError(
265
+ f"the Kaggle cookie was refused (HTTP {exc.code}); log in to kaggle.com and "
266
+ f"copy a fresh cookie"
267
+ ) from None
268
+ raise ValueError(f"the Kaggle cookie could not be checked: HTTP {exc.code}") from None
259
269
  except (urllib.error.URLError, OSError, ValueError) as exc:
260
270
  raise ValueError(f"the Kaggle cookie could not be checked: {type(exc).__name__}") from None
261
271
  name = body.get("displayName") or (body.get("user") or {}).get("displayName")
@@ -138,11 +138,11 @@ dependencies = [
138
138
 
139
139
  [[package]]
140
140
  name = "letify"
141
- version = "2.10.2"
141
+ version = "2.10.3"
142
142
 
143
143
  [[package]]
144
144
  name = "letify-quic"
145
- version = "2.10.2"
145
+ version = "2.10.3"
146
146
  dependencies = [
147
147
  "quinn",
148
148
  "rcgen",
@@ -8,7 +8,7 @@ resolver = "3"
8
8
  members = ["letify", "letify-quic"]
9
9
 
10
10
  [workspace.package]
11
- version = "2.10.2"
11
+ version = "2.10.3"
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.10.2",
3
+ "version": "2.10.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "letify-status",
9
- "version": "2.10.2",
9
+ "version": "2.10.3",
10
10
  "license": "SEE LICENSE IN LICENSE",
11
11
  "devDependencies": {
12
12
  "@types/node": "^18.19.0",
@@ -2,7 +2,7 @@
2
2
  "name": "letify-status",
3
3
  "displayName": "letify Status",
4
4
  "description": "letify quota and GPU activity in the status bar, read from the letify CLI.",
5
- "version": "2.10.2",
5
+ "version": "2.10.3",
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.10.2"
3
+ version = "2.10.3"
4
4
  description = "Declarations that become infrastructure."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1759,3 +1759,37 @@ def test_a_worker_source_that_arrives_whole_still_runs() -> None:
1759
1759
  # The worker announces itself and then reads, so closing its input ends it cleanly.
1760
1760
  assert b"arrived" not in done.stderr, done.stderr[-2000:]
1761
1761
  assert done.stdout, "the worker said nothing, so it never started"
1762
+
1763
+
1764
+ def _http_error(code: int):
1765
+ import io
1766
+ import urllib.error
1767
+
1768
+ return urllib.error.HTTPError("https://www.kaggle.com/", code, "x", {}, io.BytesIO(b""))
1769
+
1770
+
1771
+ @pytest.mark.parametrize("code", [400, 401, 403])
1772
+ def test_an_http_error_that_refuses_the_session_reads_as_a_refused_cookie(
1773
+ monkeypatch, code
1774
+ ) -> None:
1775
+ """Spec "Kaggle account": 400, 401 and 403 from GetCurrentUser mean the cookie is refused."""
1776
+ from letify.providers import kaggle as kaggle_module
1777
+
1778
+ def refuse(request, timeout=None):
1779
+ raise _http_error(code)
1780
+
1781
+ monkeypatch.setattr(kaggle_module, "urlopen", refuse)
1782
+ with pytest.raises(ValueError, match=rf"was refused \(HTTP {code}\)"):
1783
+ kaggle_module.verify_cookie(make_cookie())
1784
+
1785
+
1786
+ def test_a_server_error_during_the_cookie_check_names_its_status(monkeypatch) -> None:
1787
+ """Spec "Kaggle account": any other status is a failed check, with the status shown."""
1788
+ from letify.providers import kaggle as kaggle_module
1789
+
1790
+ def fail(request, timeout=None):
1791
+ raise _http_error(503)
1792
+
1793
+ monkeypatch.setattr(kaggle_module, "urlopen", fail)
1794
+ with pytest.raises(ValueError, match="could not be checked: HTTP 503"):
1795
+ kaggle_module.verify_cookie(make_cookie())
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes