code-sandboxes 1.9.31__tar.gz → 1.9.33__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 (266) hide show
  1. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/PKG-INFO +1 -1
  2. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/__version__.py +1 -1
  3. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/daytona_sandbox.py +19 -1
  4. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/adapters/daytona.py +124 -47
  5. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/bases.py +16 -2
  6. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/conformance.py +67 -8
  7. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/spec.py +18 -0
  8. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/environments/specification.mdx +2 -2
  9. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/providers/daytona.mdx +10 -0
  10. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_daytona.py +38 -0
  11. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_bases.py +19 -8
  12. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_conformance.py +42 -0
  13. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_daytona_builder.py +152 -11
  14. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_managed_builders.py +3 -2
  15. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_spec.py +12 -0
  16. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/.github/workflows/build.yml +0 -0
  17. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/.github/workflows/environments-live.yml +0 -0
  18. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/.github/workflows/py-code-style.yml +0 -0
  19. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/.github/workflows/py-tests.yml +0 -0
  20. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/.github/workflows/py-typing.yml +0 -0
  21. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/.github/workflows/reusable-python.yml +0 -0
  22. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/.gitignore +0 -0
  23. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/.licenserc.yaml +0 -0
  24. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/.pre-commit-config.yaml +0 -0
  25. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/.vscode/settings.json +0 -0
  26. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/CHANGELOG.md +0 -0
  27. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/Dockerfile +0 -0
  28. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/LICENSE +0 -0
  29. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/Makefile +0 -0
  30. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/README.md +0 -0
  31. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/__init__.py +0 -0
  32. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/base.py +0 -0
  33. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/bridge_mount.py +0 -0
  34. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/builds.py +0 -0
  35. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/cli.py +0 -0
  36. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/client.py +0 -0
  37. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/cloudflare_sandbox.py +0 -0
  38. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/commands.py +0 -0
  39. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/console.py +0 -0
  40. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/contents.py +0 -0
  41. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/coreweave_sandbox.py +0 -0
  42. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/datalayer_sandbox.py +0 -0
  43. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/docker_sandbox.py +0 -0
  44. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/e2b_sandbox.py +0 -0
  45. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/__init__.py +0 -0
  46. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/__main__.py +0 -0
  47. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/accounts.py +0 -0
  48. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/adapters/__init__.py +0 -0
  49. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/adapters/datalayer.py +0 -0
  50. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/adapters/e2b.py +0 -0
  51. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/adapters/managed.py +0 -0
  52. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/adapters/modal.py +0 -0
  53. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/attest.py +0 -0
  54. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/breaker.py +0 -0
  55. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/build_secrets.py +0 -0
  56. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/builders.py +0 -0
  57. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/canonical.py +0 -0
  58. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
  59. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
  60. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
  61. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/contract.py +0 -0
  62. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/doctor/__init__.py +0 -0
  63. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/doctor/build.py +0 -0
  64. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
  65. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/doctor/report.schema.json +0 -0
  66. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/errors.py +0 -0
  67. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/files.py +0 -0
  68. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/image_import.py +0 -0
  69. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/lifecycle.py +0 -0
  70. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/policy.py +0 -0
  71. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/redact.py +0 -0
  72. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/resolve.py +0 -0
  73. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/resolve_conda.py +0 -0
  74. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/environments/schema.py +0 -0
  75. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/eval_sandbox.py +0 -0
  76. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/exceptions.py +0 -0
  77. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/filesystem.py +0 -0
  78. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/google_colab.py +0 -0
  79. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/google_colab_sandbox.py +0 -0
  80. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/interfaces.py +0 -0
  81. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/jupyter_ingress.py +0 -0
  82. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/jupyter_server_sandbox.py +0 -0
  83. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/kaggle.py +0 -0
  84. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/kaggle_execute.py +0 -0
  85. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/kaggle_live.py +0 -0
  86. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/kaggle_sandbox.py +0 -0
  87. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/lifecycle.py +0 -0
  88. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/manage.py +0 -0
  89. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/modal_sandbox.py +0 -0
  90. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/models.py +0 -0
  91. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/monty_sandbox.py +0 -0
  92. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/provider_ingress.py +0 -0
  93. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/code_sandboxes/providers.py +0 -0
  94. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docker/Dockerfile +0 -0
  95. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/.gitignore +0 -0
  96. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/LICENSE +0 -0
  97. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/Makefile +0 -0
  98. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/README.md +0 -0
  99. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/babel.config.js +0 -0
  100. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/api-reference/_category_.yml +0 -0
  101. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/api-reference/index.mdx +0 -0
  102. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/cli/_category_.yml +0 -0
  103. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/cli/index.mdx +0 -0
  104. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/contribute/_category_.yml +0 -0
  105. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/contribute/index.mdx +0 -0
  106. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/environments/_category_.yml +0 -0
  107. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/environments/contract.mdx +0 -0
  108. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/environments/index.mdx +0 -0
  109. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/environments/spike-2026-09.mdx +0 -0
  110. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/examples/_category_.yml +0 -0
  111. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/examples/index.mdx +0 -0
  112. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/guide/_category_.yml +0 -0
  113. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/guide/index.mdx +0 -0
  114. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/index.mdx +0 -0
  115. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/install/_category_.yml +0 -0
  116. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/install/index.mdx +0 -0
  117. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/provider-ingress.mdx +0 -0
  118. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/providers/_category_.yml +0 -0
  119. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/providers/cloudflare.mdx +0 -0
  120. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/providers/coreweave.mdx +0 -0
  121. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/providers/datalayer.mdx +0 -0
  122. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/providers/docker.mdx +0 -0
  123. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/providers/e2b.mdx +0 -0
  124. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/providers/eval.mdx +0 -0
  125. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/providers/google-colab.mdx +0 -0
  126. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/providers/index.mdx +0 -0
  127. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/providers/jupyter-server.mdx +0 -0
  128. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/providers/kaggle.mdx +0 -0
  129. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/providers/modal.mdx +0 -0
  130. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docs/providers/monty.mdx +0 -0
  131. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/docusaurus.config.js +0 -0
  132. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/package.json +0 -0
  133. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/sidebars.js +0 -0
  134. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/src/components/HomepageRow1.js +0 -0
  135. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/src/components/HomepageRow1.module.css +0 -0
  136. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/src/components/HomepageRow2.js +0 -0
  137. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/src/components/HomepageRow2.module.css +0 -0
  138. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/src/components/HomepageRow3.js +0 -0
  139. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/src/components/HomepageRow3.module.css +0 -0
  140. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/src/css/custom.css +0 -0
  141. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/src/pages/index.module.css +0 -0
  142. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/src/pages/markdown-page.md +0 -0
  143. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/src/theme/CustomDocItem.tsx +0 -0
  144. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/src/theme/IconsTitle.tsx +0 -0
  145. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/bricks.svg +0 -0
  146. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/building-construction.svg +0 -0
  147. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/cloud-sun.svg +0 -0
  148. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/cloud.svg +0 -0
  149. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/collaboration.svg +0 -0
  150. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/copy-left.svg +0 -0
  151. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/datalayer/logo.png +0 -0
  152. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/datalayer/logo.svg +0 -0
  153. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/datalayer/personas/business.svg +0 -0
  154. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
  155. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/datalayer/personas/devops.svg +0 -0
  156. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/datalayer/project/project-line.svg +0 -0
  157. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/favicon.ico +0 -0
  158. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/jupyter.svg +0 -0
  159. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/open-source.svg +0 -0
  160. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/react-js.svg +0 -0
  161. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/react-js2.svg +0 -0
  162. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/ringed-planet.svg +0 -0
  163. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/robot.svg +0 -0
  164. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/rocket.svg +0 -0
  165. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/shield.svg +0 -0
  166. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/sparkles.svg +0 -0
  167. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/streamlit.gif +0 -0
  168. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/super-hero.svg +0 -0
  169. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/target.svg +0 -0
  170. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/docs/static/img/thumbs-up.svg +0 -0
  171. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/README.md +0 -0
  172. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/exec/Makefile +0 -0
  173. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/exec/cloudflare_sandbox_example.py +0 -0
  174. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/exec/coreweave_sandbox_example.py +0 -0
  175. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/exec/datalayer_sandbox_example.py +0 -0
  176. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/exec/daytona_sandbox_example.py +0 -0
  177. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/exec/docker_sandbox_example.py +0 -0
  178. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/exec/e2b_sandbox_example.py +0 -0
  179. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/exec/eval_sandbox_example.py +0 -0
  180. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/exec/google_colab_sandbox_example.py +0 -0
  181. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/exec/jupyter_server_sandbox_example.py +0 -0
  182. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/exec/kaggle_sandbox_example.py +0 -0
  183. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/exec/modal_sandbox_example.py +0 -0
  184. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/exec/monty_sandbox_example.py +0 -0
  185. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/repl/Makefile +0 -0
  186. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/repl/cloudflare_sandbox_example.py +0 -0
  187. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/repl/coreweave_sandbox_example.py +0 -0
  188. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/repl/datalayer_sandbox_example.py +0 -0
  189. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/repl/daytona_sandbox_example.py +0 -0
  190. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/repl/docker_sandbox_example.py +0 -0
  191. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/repl/e2b_sandbox_example.py +0 -0
  192. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/repl/eval_sandbox_example.py +0 -0
  193. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/repl/google_colab_sandbox_example.py +0 -0
  194. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/repl/jupyter_server_sandbox_example.py +0 -0
  195. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/repl/kaggle_sandbox_example.py +0 -0
  196. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/repl/modal_sandbox_example.py +0 -0
  197. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/examples/repl/monty_sandbox_example.py +0 -0
  198. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/pyproject.toml +0 -0
  199. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/schemas/environment-v1alpha1.json +0 -0
  200. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/__init__.py +0 -0
  201. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/conftest.py +0 -0
  202. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
  203. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_bridge_mount.py +0 -0
  204. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_builds.py +0 -0
  205. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_cli_exec.py +0 -0
  206. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_cli_repl.py +0 -0
  207. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_client.py +0 -0
  208. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_client_contents.py +0 -0
  209. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_client_files.py +0 -0
  210. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_client_outcome_error_output.py +0 -0
  211. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_cloudflare.py +0 -0
  212. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_commands_envelope.py +0 -0
  213. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_console.py +0 -0
  214. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_contents_contract.py +0 -0
  215. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_contents_live_matrix.py +0 -0
  216. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_coreweave.py +0 -0
  217. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_datalayer_sandbox.py +0 -0
  218. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_e2b.py +0 -0
  219. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_accounts.py +0 -0
  220. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_attest.py +0 -0
  221. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_breaker.py +0 -0
  222. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_build_secrets.py +0 -0
  223. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_builders.py +0 -0
  224. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_canonical.py +0 -0
  225. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_constraints.py +0 -0
  226. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_contract.py +0 -0
  227. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_cross_variant_report.py +0 -0
  228. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_datalayer_builder.py +0 -0
  229. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_dockerfile_source.py +0 -0
  230. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_doctor.py +0 -0
  231. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_e2b_builder.py +0 -0
  232. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_errors.py +0 -0
  233. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_image_import.py +0 -0
  234. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_lifecycle.py +0 -0
  235. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_live_matrix.py +0 -0
  236. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_modal_builder.py +0 -0
  237. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_organization_policy.py +0 -0
  238. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_redact.py +0 -0
  239. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_resolve.py +0 -0
  240. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_resolve_conda.py +0 -0
  241. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_environment_spec_organization_policy.py +0 -0
  242. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_eval.py +0 -0
  243. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
  244. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_factory.py +0 -0
  245. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_google_colab.py +0 -0
  246. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_integration.py +0 -0
  247. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_jupyter_ingress.py +0 -0
  248. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_jupyter_server.py +0 -0
  249. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_jupyter_server_streaming.py +0 -0
  250. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_kaggle.py +0 -0
  251. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_kaggle_execute.py +0 -0
  252. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_kaggle_live.py +0 -0
  253. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_kaggle_session.py +0 -0
  254. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_kernel_client_compatibility.py +0 -0
  255. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_lifecycle_vocabulary.py +0 -0
  256. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_manage.py +0 -0
  257. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_manage_datalayer.py +0 -0
  258. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_modal_google_colab_sandbox.py +0 -0
  259. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_modal_session.py +0 -0
  260. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_models.py +0 -0
  261. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_output_line_fidelity.py +0 -0
  262. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_provider_catalog_names.py +0 -0
  263. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_provider_ingress.py +0 -0
  264. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_providers.py +0 -0
  265. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_sandboxes.py +0 -0
  266. {code_sandboxes-1.9.31 → code_sandboxes-1.9.33}/tests/test_the_kill_relaunch_drill.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: code_sandboxes
3
- Version: 1.9.31
3
+ Version: 1.9.33
4
4
  Project-URL: Home, https://github.com/datalayer/code-sandboxes
5
5
  Author-email: Datalayer <info@datalayer.io>
6
6
  License: BSD 3-Clause License
@@ -3,4 +3,4 @@
3
3
 
4
4
  """Code Sandboxes."""
5
5
 
6
- __version__ = "1.9.31"
6
+ __version__ = "1.9.33"
@@ -510,6 +510,24 @@ class DaytonaSandbox(Sandbox):
510
510
  given = {key: value for key, value in settings.items() if value}
511
511
  return daytona.DaytonaConfig(**given) if given else None
512
512
 
513
+ def _snapshot_carries_a_gpu(self) -> bool:
514
+ """Whether the snapshot this sandbox starts from was built with a GPU (E2-17).
515
+
516
+ A GPU is baked into a snapshot with its other resources, so a sandbox
517
+ of one asks for a GPU without saying so, and Daytona refuses it unless
518
+ it is ephemeral, the same as one asked for with `gpu=`. The snapshot
519
+ record says: a GPU environment's artifact, launched by its id, reads
520
+ `gpu` there. One that cannot be read is taken to have none, and
521
+ Daytona's own refusal then says why.
522
+ """
523
+ if self._snapshot is None or self._daytona is None:
524
+ return False
525
+ try:
526
+ snapshot = self._daytona.snapshot.get(self._snapshot)
527
+ except Exception:
528
+ return False
529
+ return bool(getattr(snapshot, "gpu", 0))
530
+
513
531
  def _create_params(self, daytona: Any) -> Any: # noqa: C901
514
532
  """What to ask Daytona for, from the configuration of this sandbox.
515
533
 
@@ -534,7 +552,7 @@ class DaytonaSandbox(Sandbox):
534
552
  ]
535
553
 
536
554
  resources = self._resources(daytona)
537
- if _asks_for_a_gpu(resources):
555
+ if _asks_for_a_gpu(resources) or self._snapshot_carries_a_gpu():
538
556
  # Daytona will not create a GPU sandbox that outlives its stop:
539
557
  # "GPU sandboxes must be ephemeral; set autoDeleteInterval to 0".
540
558
  # It is a property of asking for a GPU at all, not of asking for
@@ -69,24 +69,20 @@ would make an artifact refuse to build for a reason that says nothing about
69
69
  what it does once actually launched. A real answer needs a launched
70
70
  sandbox, which is a smoke test's job (E1-14), not this builder's.
71
71
 
72
- **GPU classes are not built here.** `gpu = True` on this builder is a true
73
- capability (Daytona's own hardware runs one, D-20), and `validate` leaves a
74
- GPU size class buildable rather than refusing it — the constraint table of
75
- section 6 has nothing against it, and an existing test
76
- (`test_a_gpu_spec_is_buildable_on_modal_and_daytona`) already pins that
77
- answer. What actually stops a GPU build today is upstream: the CUDA base
78
- channel is E2-17's to publish, and `bases.py` has no digest to resolve
79
- `python-cuda` to yet, so a `BuildRequest` for one cannot be constructed in
80
- practice. `build()` itself still guards it explicitly, refusing plainly
81
- rather than baking an unsourced guess at a GPU type and count into a
82
- snapshot, in case that ever changes before the real numbers do (section 11.3
83
- item 7).
72
+ **A GPU is baked into the snapshot** (E2-17, D-20), from the spec's own
73
+ `resources.accelerator`: its `type` is one of Daytona's GPUs, checked at
74
+ `validate` so a GPU Daytona does not offer is refused before any build, and
75
+ its `count` is the number of them. CPU and memory come from the spec's hints,
76
+ or Daytona's own default for that GPU; the disk from the hints, or enough for
77
+ the CUDA base. A sandbox of a GPU snapshot is ephemeral, which Daytona
78
+ requires of every GPU sandbox (`DaytonaSandbox` reads the snapshot's `gpu`).
84
79
 
85
80
  @module code_sandboxes.environments.adapters.daytona
86
81
  """
87
82
 
88
83
  from __future__ import annotations
89
84
 
85
+ import contextlib
90
86
  import shlex
91
87
  import tempfile
92
88
  import uuid
@@ -118,10 +114,10 @@ from ..resolve_conda import (
118
114
  micromamba_bootstrap_command,
119
115
  micromamba_install_command,
120
116
  )
121
- from ..spec import GPU_SIZE_CLASSES, Environment
117
+ from ..spec import GPU_SIZE_CLASSES, Environment, EnvironmentSpec
122
118
  from .managed import ManagedBuilder
123
119
 
124
- __all__ = ["Builder"]
120
+ __all__ = ["DAYTONA_GPUS", "Builder", "daytona_gpu"]
125
121
 
126
122
  #: Tags Daytona refuses for a snapshot's source image: each moves.
127
123
  MOVING_TAGS = ("latest", "lts", "stable")
@@ -148,6 +144,31 @@ _CPU_RESOURCES: dict[str, dict[str, int]] = {
148
144
  }
149
145
 
150
146
 
147
+ #: The GPUs Daytona offers, as its SDK's `GpuType` names them (E2-17). Spelled
148
+ #: out rather than read from the SDK, because `validate` runs where the SDK may
149
+ #: not be installed; a test holds the two together.
150
+ DAYTONA_GPUS: tuple[str, ...] = ("H100", "H200", "RTX-PRO-6000", "RTX-4090", "RTX-5090")
151
+
152
+ #: A GPU snapshot's disk when the spec gives no hint: the CUDA base alone is
153
+ #: about 15 GB, and Daytona's own default would not hold it with room to work.
154
+ _GPU_DISK_GI = 50
155
+
156
+
157
+ def daytona_gpu(accelerator_type: str) -> str | None:
158
+ """Daytona's name for an accelerator type, or `None` when Daytona has no such GPU.
159
+
160
+ `h100`, `H100` and `rtx_4090` name what Daytona calls `H100` and
161
+ `RTX-4090`; a `T4` or an `A100-80GB` is Modal's vocabulary, not Daytona's.
162
+ """
163
+ name = accelerator_type.strip().upper().replace("_", "-")
164
+ return name if name in DAYTONA_GPUS else None
165
+
166
+
167
+ def _snapshot_name(request: BuildRequest) -> str:
168
+ """The name a build gives its snapshot, unique to the build (its uid is in it)."""
169
+ return f"dl-{request.environment.metadata.name}-v{request.version}-{request.build_uid}"
170
+
171
+
151
172
  def _pip_lock_command(*, authored: bool) -> str:
152
173
  """Install the pip lock: `sync` to it, or `install` it over an authored Dockerfile.
153
174
 
@@ -204,8 +225,8 @@ class Builder(ManagedBuilder):
204
225
  #: 2026-09-17.
205
226
  forbidden_instructions = ()
206
227
  dependency_formats = ("requirements", "pyproject", "conda")
207
- #: Daytona runs GPUs, on its own hardware and the owner's account (E2-17).
208
- #: This builder does not build one yet: see `_own_findings`.
228
+ #: Daytona runs GPUs, on its own hardware and the owner's account (E2-17),
229
+ #: baked into the snapshot with the rest of its resources.
209
230
  gpu = True
210
231
  #: E0-04's spike found only a registry login for the private base, never
211
232
  #: a per-step arbitrary named secret (E3-05): `buildSecrets` is refused.
@@ -230,6 +251,9 @@ class Builder(ManagedBuilder):
230
251
  self._daytona_sdk = daytona_sdk or _daytona_sdk
231
252
  self._registry_sdk = registry_sdk or _daytona_registry_sdk
232
253
  self._client_instance: Any = None
254
+ #: The builds `cancel` was asked to stop: one whose snapshot is only
255
+ #: finished after the cancel deletes it itself (E2-18).
256
+ self._cancelled: set[str] = set()
233
257
 
234
258
  def _provider_secrets(self) -> dict[str, str]:
235
259
  """The owner's Daytona secrets the build credential carries (D-8, E2-01).
@@ -300,15 +324,7 @@ class Builder(ManagedBuilder):
300
324
  field="spec.compatibility.regions",
301
325
  )
302
326
  )
303
- # A GPU class is left buildable here on purpose (`gpu = True`,
304
- # D-20): the CUDA base E2-17 has not published yet, so a GPU
305
- # `BuildRequest` cannot reach `build()` in practice — `bases.py`'s
306
- # own resolver has no digest to resolve `python-cuda` to, and
307
- # refuses first. `build()` itself still guards it explicitly (see
308
- # its own docstring), so a spec that somehow got a `resolved_base`
309
- # anyway is refused plainly rather than baking an unsourced guess
310
- # at a GPU type and count into a snapshot.
311
- #
327
+ findings.extend(self._accelerator_findings(environment.spec))
312
328
  # `spec.buildSecrets` needs no check of its own here: `supports_build_secrets
313
329
  # = False` above (E3-05, merged since this branch started) makes
314
330
  # `ManagedBuilder._own_findings` refuse it before this method is
@@ -330,21 +346,9 @@ class Builder(ManagedBuilder):
330
346
  (E0-04).
331
347
  """
332
348
  spec = request.environment.spec
333
- if request.size_class in GPU_SIZE_CLASSES:
334
- # `validate` leaves a GPU class buildable (`gpu = True`, D-20):
335
- # `bases.py` has no CUDA digest to resolve yet, so this cannot
336
- # be reached in practice — refused plainly here rather than
337
- # baking an unsourced guess at a GPU type and count into a
338
- # snapshot (E2-17 is what will give this real numbers).
339
- raise EnvironmentsError(
340
- CAPABILITY_UNSUPPORTED,
341
- f"Daytona runs `{request.size_class}` on its own GPUs, but the CUDA base "
342
- "and the GPU resource shape this needs are E2-17's, not built yet",
343
- detail={"variant": self.variant, "missing": "E2-17"},
344
- )
345
349
  sdk = self._daytona_sdk()
346
350
  client = self._client(sdk)
347
- name = f"dl-{request.environment.metadata.name}-v{request.version}-{request.build_uid}"
351
+ name = _snapshot_name(request)
348
352
 
349
353
  registry_id = self._register_base_pull(client, request.resolved_base)
350
354
  try:
@@ -437,7 +441,7 @@ class Builder(ManagedBuilder):
437
441
  logged.append(line)
438
442
  self._log(line)
439
443
 
440
- resources = self._resources(sdk, request.size_class)
444
+ resources = self._resources(sdk, request.size_class, spec)
441
445
  # A snapshot is region-scoped, and the region that scopes it is
442
446
  # Daytona's, not this platform's. `validate` already refuses
443
447
  # more than one, so the first is the only one.
@@ -474,6 +478,17 @@ class Builder(ManagedBuilder):
474
478
  # not control, whether the build above succeeded or not (D-18).
475
479
  self._unregister_base_pull(client, registry_id)
476
480
 
481
+ if request.build_uid in self._cancelled:
482
+ # Finished after the build was cancelled: nothing will record it,
483
+ # so it goes now rather than wait in the account for nobody.
484
+ self._log(f"The build was cancelled: deleting the snapshot {snapshot.id} it made")
485
+ with contextlib.suppress(EnvironmentsError):
486
+ self._delete_by_id(sdk, client, snapshot.id)
487
+ raise EnvironmentsError(
488
+ BUILD_FAILED,
489
+ "The build was cancelled, and the snapshot it made was deleted",
490
+ detail={"variant": self.variant, "name": name},
491
+ )
477
492
  return ArtifactReference(
478
493
  variant=self.variant,
479
494
  immutable_reference=snapshot.id,
@@ -505,10 +520,40 @@ class Builder(ManagedBuilder):
505
520
  )
506
521
  return sdk.Image.from_dockerfile(str(dockerfile))
507
522
 
508
- def _resources(self, sdk: Any, size_class: str) -> Any:
509
- """The CPU resources a size class bakes into the snapshot (§11.3 item 4)."""
510
- shape = _CPU_RESOURCES.get(size_class, _CPU_RESOURCES["small"])
511
- return sdk.Resources(cpu=shape["cpu"], memory=shape["memory"], disk=shape["disk"])
523
+ @staticmethod
524
+ def _accelerator_findings(spec: EnvironmentSpec) -> list[CapabilityFinding]:
525
+ """A GPU Daytona does not offer, refused before any build (E2-17)."""
526
+ accelerator = spec.resources.accelerator
527
+ if accelerator == "none" or daytona_gpu(accelerator.type):
528
+ return []
529
+ return [
530
+ CapabilityFinding(
531
+ code="DL_ENV_CAPABILITY_UNSUPPORTED",
532
+ message=(
533
+ f"Daytona has no GPU called `{accelerator.type}`; it offers "
534
+ + ", ".join(DAYTONA_GPUS)
535
+ ),
536
+ field="spec.resources.accelerator.type",
537
+ )
538
+ ]
539
+
540
+ def _resources(self, sdk: Any, size_class: str, spec: EnvironmentSpec) -> Any:
541
+ """What the snapshot bakes in: a class's CPU, or the spec's GPU (§11.3 item 4, E2-17)."""
542
+ accelerator = spec.resources.accelerator
543
+ if size_class not in GPU_SIZE_CLASSES or accelerator == "none":
544
+ shape = _CPU_RESOURCES.get(size_class, _CPU_RESOURCES["small"])
545
+ return sdk.Resources(cpu=shape["cpu"], memory=shape["memory"], disk=shape["disk"])
546
+ # D-4 gives the GPU classes no CPU or memory of their own (E4-11 has
547
+ # them, for Datalayer's nodes): the GPU is what the class is for, and
548
+ # Daytona sizes the rest of the machine for it unless the spec hints.
549
+ hints = spec.resources.hints
550
+ return sdk.Resources(
551
+ cpu=round(hints.cpu) if hints.cpu else None,
552
+ memory=round(hints.memory_gi) if hints.memory_gi else None,
553
+ disk=round(hints.disk_gi) if hints.disk_gi else _GPU_DISK_GI,
554
+ gpu=accelerator.count,
555
+ gpu_type=sdk.GpuType(daytona_gpu(accelerator.type)),
556
+ )
512
557
 
513
558
  def _register_base_pull(self, client: Any, resolved_base: str) -> str | None:
514
559
  """A private registry entry for this build's base pull (D-17, D-18).
@@ -610,8 +655,11 @@ class Builder(ManagedBuilder):
610
655
  a later build's snapshot that inherited it.
611
656
  """
612
657
  sdk = self._daytona_sdk()
613
- client = self._client(sdk)
614
- snapshot = artifact.provider_artifact_id or artifact.immutable_reference
658
+ self._delete_by_id(
659
+ sdk, self._client(sdk), artifact.provider_artifact_id or artifact.immutable_reference
660
+ )
661
+
662
+ def _delete_by_id(self, sdk: Any, client: Any, snapshot: str) -> None:
615
663
  try:
616
664
  client.snapshot.delete(snapshot)
617
665
  except sdk.DaytonaNotFoundError:
@@ -621,6 +669,26 @@ class Builder(ManagedBuilder):
621
669
  raise self._provider_error("delete the snapshot", error) from error
622
670
  self._log(f"Deleted the Daytona snapshot {snapshot}")
623
671
 
672
+ def cancel(self, request: BuildRequest) -> None:
673
+ """Stop a build: delete the snapshot it is making, now or once it is made (E2-18).
674
+
675
+ The build step calls this when the build is cancelled, and then lets
676
+ the build's own thread finish unheard, so a snapshot Daytona goes on
677
+ building is recorded by nobody. It is found by the name this build
678
+ gave it, which is unique to the build (the build uid is in it), and
679
+ deleted by its id. One not made yet is deleted by `build` itself, the
680
+ moment Daytona hands it back.
681
+ """
682
+ self._cancelled.add(request.build_uid)
683
+ sdk = self._daytona_sdk()
684
+ client = self._client(sdk)
685
+ try:
686
+ snapshot = client.snapshot.get(_snapshot_name(request))
687
+ except Exception:
688
+ return
689
+ self._log(f"The build was cancelled: deleting the snapshot {snapshot.id} it was making")
690
+ self._delete_by_id(sdk, client, snapshot.id)
691
+
624
692
  def smoke_test(
625
693
  self,
626
694
  artifact: ArtifactReference,
@@ -657,20 +725,29 @@ class Builder(ManagedBuilder):
657
725
  "lock pinned, and an artifact carries neither",
658
726
  detail={"variant": self.variant},
659
727
  )
660
- from ..conformance import expected_packages, run_core_tier
728
+ from ..conformance import expected_packages, run_accelerator_check, run_core_tier
661
729
 
662
730
  snapshot = artifact.provider_artifact_id or artifact.immutable_reference
663
731
  sandbox = self._smoke_test_sandbox(snapshot)
664
732
  self._log(f"Launching {snapshot} to smoke-test it")
665
733
  try:
666
734
  sandbox.start()
667
- return run_core_tier(
735
+ result = run_core_tier(
668
736
  sandbox,
669
737
  python_version=environment.spec.language.version,
670
738
  expected_packages=expected_packages(environment, lock_text or ""),
671
739
  secret_values=tuple(secret_values),
672
740
  restart=lambda: self._restart(sandbox),
673
741
  )
742
+ accelerator = environment.spec.resources.accelerator
743
+ if accelerator != "none":
744
+ # A GPU version is only the version its spec describes when
745
+ # its GPUs are visible and its CUDA is the spec's (check 11,
746
+ # E2-17): the core tier alone passes on a machine with none.
747
+ result.checks.append(
748
+ run_accelerator_check(sandbox, cuda=accelerator.cuda, count=accelerator.count)
749
+ )
750
+ return result
674
751
  except EnvironmentsError:
675
752
  raise
676
753
  except Exception as error:
@@ -59,6 +59,9 @@ class ApprovedBase(BaseModel):
59
59
  python_versions: tuple[str, ...]
60
60
  #: Whether the base carries CUDA, which a GPU size class requires.
61
61
  accelerator: bool = False
62
+ #: The CUDA toolkit the base pins, ``major.minor`` (E2-17): what a spec's
63
+ #: ``accelerator.cuda`` may ask for, and what check 11 reads back.
64
+ cuda: str | None = None
62
65
  #: Channel, then variant, to the digest resolution pins. A channel with no
63
66
  #: variant is approved and not yet published.
64
67
  channels: dict[str, dict[str, str]] = Field(default_factory=dict)
@@ -140,12 +143,23 @@ APPROVED_BASES: dict[str, ApprovedBase] = {
140
143
  },
141
144
  snapshots={"2026.09": "20260916T120000Z"},
142
145
  ),
143
- # E2-17: jupyter-python-cuda plus the same layer.
146
+ # E2-17: jupyter-python-cuda:0.3.1 (jupyter-python 0.2.2 and the CUDA
147
+ # 12.8 toolkit, pinned) plus the same layer, released 2026-09-18 to
148
+ # environments/base/python-cuda as `2026.09-bceb272088e4`. The doctor
149
+ # passes all fifteen checks and `nvcc` answers 12.8 without a GPU.
144
150
  ApprovedBase(
145
151
  ref="datalayer/python-cuda",
146
152
  python_versions=("3.13",),
147
153
  accelerator=True,
148
- channels={"2026.09": {}},
154
+ cuda="12.8",
155
+ channels={
156
+ "2026.09": dict.fromkeys(
157
+ ("datalayer", "e2b", "daytona", "modal"),
158
+ "sha256:a6374a2d4ff07c8a8fe0a71ee605964f93319894cc4152c4e2a1af6258ac9e6a",
159
+ )
160
+ },
161
+ # After its CUDA layer's own `apt-get update`, which ran that day.
162
+ snapshots={"2026.09": "20260918T150000Z"},
149
163
  ),
150
164
  )
151
165
  }
@@ -37,6 +37,7 @@ __all__ = [
37
37
  "cross_variant_packages",
38
38
  "drifted_from",
39
39
  "package_versions_of",
40
+ "run_accelerator_check",
40
41
  "run_conformance",
41
42
  "run_core_tier",
42
43
  "run_extended_tier",
@@ -596,30 +597,62 @@ def _egress(
596
597
  )
597
598
 
598
599
 
599
- def _gpu(sandbox: Sandbox, requested: bool, cuda: str | None, timeout: float | None) -> CheckResult:
600
+ def _gpu(
601
+ sandbox: Sandbox,
602
+ requested: bool,
603
+ cuda: str | None,
604
+ timeout: float | None,
605
+ count: int = 1,
606
+ ) -> CheckResult:
607
+ """Check 11: the GPUs asked for are visible, and CUDA is the spec's.
608
+
609
+ Two CUDA versions are read, and they answer different questions. The
610
+ image's own toolkit (`nvcc`, or the base's `CUDA_VERSION`) is what the
611
+ spec's `accelerator.cuda` names and the base channel pins. `nvidia-smi`'s
612
+ "CUDA Version" is the newest CUDA the host's *driver* runs, which the
613
+ image does not choose: comparing the spec with it failed a correct image
614
+ on any newer driver (found on 2026-09-18, E2-17). The driver only has to
615
+ be new enough for the toolkit.
616
+ """
600
617
  if not requested:
601
618
  return _result(11, True, gating=False, detail="no accelerator was requested")
602
619
  body = (
603
- "import re as _dl_re, subprocess as _dl_sp\n"
604
- "_dl_out = {'returncode': None, 'gpus': [], 'cuda': None}\n"
620
+ "import os as _dl_os, re as _dl_re, shutil as _dl_sh, subprocess as _dl_sp\n"
621
+ "_dl_out = {'returncode': None, 'gpus': [], 'driver': None, 'cuda': None}\n"
605
622
  "try:\n"
606
623
  " _dl_run = _dl_sp.run(['nvidia-smi'], capture_output=True, text=True, timeout=30)\n"
607
624
  " _dl_out['returncode'] = _dl_run.returncode\n"
608
625
  " _dl_match = _dl_re.search(r'CUDA Version:\\s*([0-9.]+)', _dl_run.stdout)\n"
609
- " _dl_out['cuda'] = _dl_match.group(1) if _dl_match else None\n"
626
+ " _dl_out['driver'] = _dl_match.group(1) if _dl_match else None\n"
610
627
  " _dl_names = _dl_sp.run(['nvidia-smi', '--query-gpu=name', '--format=csv,noheader'], "
611
628
  "capture_output=True, text=True, timeout=30)\n"
612
629
  " _dl_out['gpus'] = [_dl_l.strip() for _dl_l in _dl_names.stdout.splitlines() "
613
630
  "if _dl_l.strip()]\n"
614
631
  "except (OSError, _dl_sp.SubprocessError) as _dl_error:\n"
615
632
  " _dl_out['error'] = str(_dl_error)\n"
633
+ "try:\n"
634
+ " _dl_nvcc = _dl_sh.which('nvcc') or '/usr/local/cuda/bin/nvcc'\n"
635
+ " _dl_run = _dl_sp.run([_dl_nvcc, '--version'], capture_output=True, text=True, "
636
+ "timeout=30)\n"
637
+ " _dl_match = _dl_re.search(r'release ([0-9.]+),', _dl_run.stdout)\n"
638
+ " _dl_out['cuda'] = _dl_match.group(1) if _dl_match else None\n"
639
+ "except (OSError, _dl_sp.SubprocessError):\n"
640
+ " pass\n"
641
+ "_dl_out['cuda'] = _dl_out['cuda'] or _dl_os.environ.get('CUDA_VERSION') or None\n"
616
642
  )
617
643
  answer = probe(sandbox, _code(11, body, "_dl_out"), timeout=timeout)
618
644
  problems = []
619
- if answer.get("returncode") != 0 or not answer.get("gpus"):
645
+ gpus = answer.get("gpus") or []
646
+ if answer.get("returncode") != 0 or not gpus:
620
647
  problems.append("no GPU is visible")
621
- if cuda and not str(answer.get("cuda") or "").startswith(cuda):
622
- problems.append(f"CUDA is {answer.get('cuda')}, not {cuda}")
648
+ elif len(gpus) < count:
649
+ problems.append(f"{len(gpus)} GPU(s) visible, not the {count} asked for")
650
+ toolkit = str(answer.get("cuda") or "")
651
+ if cuda and not _version_is(toolkit, cuda):
652
+ problems.append(f"CUDA is {toolkit or None}, not {cuda}")
653
+ driver = str(answer.get("driver") or "")
654
+ if toolkit and driver and _version_tuple(driver) < _version_tuple(toolkit):
655
+ problems.append(f"the driver runs CUDA up to {driver}, older than the image's {toolkit}")
623
656
  # A GPU version gates on this (E2-17): a version that asked for an
624
657
  # accelerator and cannot see it, or sees the wrong CUDA, is not the
625
658
  # version its spec describes. A version that asked for none never
@@ -628,6 +661,31 @@ def _gpu(sandbox: Sandbox, requested: bool, cuda: str | None, timeout: float | N
628
661
  return _result(11, not problems, gating=True, detail="; ".join(problems) or None, actual=answer)
629
662
 
630
663
 
664
+ def _version_tuple(version: str) -> tuple[int, ...]:
665
+ return tuple(int(part) for part in re.findall(r"\d+", version))
666
+
667
+
668
+ def _version_is(version: str, asked: str) -> bool:
669
+ """Whether `version` is what `asked` names: `12` and `12.8` both name 12.8.3."""
670
+ wanted = _version_tuple(asked)
671
+ return bool(wanted) and _version_tuple(version)[: len(wanted)] == wanted
672
+
673
+
674
+ def run_accelerator_check(
675
+ sandbox: Sandbox,
676
+ *,
677
+ cuda: str | None = None,
678
+ count: int = 1,
679
+ timeout: float | None = 120.0,
680
+ ) -> CheckResult:
681
+ """Appendix B check 11 alone, gating, for a version that asked for an accelerator (E2-17).
682
+
683
+ What a builder's smoke test adds to the core tier for a GPU version: the
684
+ rest of the extended tier records, and this one decides.
685
+ """
686
+ return _guard(11, True, lambda: _gpu(sandbox, True, cuda, timeout, count))
687
+
688
+
631
689
  def _throughput(
632
690
  sandbox: Sandbox, contract: SandboxContract, minimum: float, timeout: float | None
633
691
  ) -> CheckResult:
@@ -694,6 +752,7 @@ def run_extended_tier(
694
752
  contract: SandboxContract = SANDBOX_CONTRACT_V1,
695
753
  accelerator_requested: bool = False,
696
754
  cuda_version: str | None = None,
755
+ accelerator_count: int = 1,
697
756
  egress_allowed: Sequence[str] = (),
698
757
  egress_blocked: Sequence[str] = (),
699
758
  cold_start_seconds: float | None = None,
@@ -711,7 +770,7 @@ def run_extended_tier(
711
770
  _guard(
712
771
  11,
713
772
  accelerator_requested,
714
- lambda: _gpu(sandbox, accelerator_requested, cuda_version, timeout),
773
+ lambda: _gpu(sandbox, accelerator_requested, cuda_version, timeout, accelerator_count),
715
774
  ),
716
775
  _guard(12, False, lambda: _throughput(sandbox, contract, minimum_mib_per_second, timeout)),
717
776
  _guard(13, False, lambda: _cold_start(cold_start_seconds, cold_start_budget)),
@@ -497,6 +497,12 @@ def _index_findings(field: str, url: str) -> list[SpecFinding]:
497
497
  return []
498
498
 
499
499
 
500
+ def _same_cuda(asked: str, carried: str) -> bool:
501
+ """Whether a spec's CUDA names the base's: `12` and `12.8` both name 12.8."""
502
+ asked_parts = asked.strip().split(".")
503
+ return carried.strip().split(".")[: len(asked_parts)] == asked_parts
504
+
505
+
500
506
  def parse_requirements_txt(text: str) -> list[str]:
501
507
  """The requirement lines of a `requirements.txt`, comments and blanks dropped.
502
508
 
@@ -960,6 +966,18 @@ def spec_findings(
960
966
  "spec.base.ref", f"`{base.ref}` has no CUDA; a GPU size class needs a CUDA base"
961
967
  )
962
968
  )
969
+ # The CUDA a spec asks for is the base's toolkit, which a channel pins
970
+ # (E2-17): a version that asks for another would build and then fail
971
+ # check 11 in every sandbox it starts.
972
+ cuda = resources.accelerator.cuda if wants_gpu else None
973
+ if cuda and base is not None and base.cuda and not _same_cuda(cuda, base.cuda):
974
+ findings.append(
975
+ SpecFinding(
976
+ "spec.resources.accelerator.cuda",
977
+ f"`{base.ref}` carries CUDA {base.cuda}, not {cuda}; ask for {base.cuda} "
978
+ "or leave `cuda` out",
979
+ )
980
+ )
963
981
 
964
982
  variants = spec.compatibility.variants
965
983
  if not variants.required:
@@ -37,12 +37,12 @@ A document is refused with `DL_ENV_SPEC_INVALID` when a field is wrong, and with
37
37
  | `commands.postInstall` | none | At most 32 non-empty commands, run after the packages are installed. |
38
38
  | `buildSecrets` | none | `id` is a Datalayer secret id, `dlsec_...`; `mountAs` is `env` or `file`; `name` is the variable or file name. Ids and names are unique. Secrets are injected into the build step that needs them and never written into the artifact. |
39
39
  | `resources.sizeClass` | `small` | `small`, `medium`, `large`, `gpu-small` or `gpu-large`. A GPU class needs an accelerator and a CUDA base; an accelerator needs a GPU class. Datalayer runs no GPU nodes yet, so a GPU class is built for Modal and Daytona, which bring their own GPUs; E2B has none. |
40
- | `resources.accelerator` | `none` | `none`, or `type`, `count` and `cuda`. |
40
+ | `resources.accelerator` | `none` | `none`, or `type`, `count` and `cuda`. `type` is the provider's own name for the GPU (Daytona: `H100`, `H200`, `RTX-PRO-6000`, `RTX-4090`, `RTX-5090`), and a GPU a variant does not offer is refused for it at `validate`. `cuda` is the base's toolkit: `datalayer/python-cuda:2026.09` carries 12.8, and another version is refused. |
41
41
  | `resources.hints` | none | `cpu`, `memoryGi`, `diskGi`: positive numbers, within the size class. |
42
42
  | `compatibility.variants.required` | `[datalayer]` | At least one of `datalayer`, `e2b`, `daytona`, `modal`. |
43
43
  | `compatibility.variants.optional` | none | The same variants, none of them also required. |
44
44
  | `compatibility.regions` | none | Region names. |
45
- | `build.source` | `packages` | `packages`; `dependencyFile` (a `requirements.txt`; a `pyproject.toml` whose own `uv.lock` is checked against Datalayer's protected pins and exported rather than re-resolved; or a conda `environment.yml`, resolved in its own `micromamba` solve into an explicit lock with the protected pins merged over its `pip:` layer); `dockerfile` (`build.dockerfile.content`, inline: its `FROM` names an approved base with its channel as the tag, which is what resolves, and what its own instructions install is not locked, as the lock's header says); and `image` (an existing image from a bootstrap registry allowlist, replacing the approved base). `packages`, a `requirements.txt`, a conda `environment.yml` and `dockerfile` build on the Datalayer and Daytona variants. A `pyproject.toml` does not build yet: its `uv.lock` has to lock Datalayer's `jupyter-server` fork, which no package index serves. `image` resolves but does not yet start as a sandbox, because the imported image has no Datalayer runtime layer; `image` from a private registry, and a `COPY` from a Dockerfile's build context, are refused. |
45
+ | `build.source` | `packages` | `packages`; `dependencyFile` (a `requirements.txt`; a `pyproject.toml` whose own `uv.lock` is checked against Datalayer's protected pins and exported rather than re-resolved; or a conda `environment.yml`, resolved in its own `micromamba` solve into an explicit lock with the protected pins merged over its `pip:` layer); `dockerfile` (`build.dockerfile.content`, inline: its `FROM` names an approved base with its channel as the tag, which is what resolves, and what its own instructions install is not locked, as the lock's header says); and `image` (an existing image from a bootstrap registry allowlist, replacing the approved base). `packages`, a `requirements.txt`, a `pyproject.toml` with its `uv.lock`, a conda `environment.yml` and `dockerfile` build on the Datalayer and Daytona variants. A `pyproject.toml`'s `uv.lock` has to lock Datalayer's `jupyter-server` fork, which no package index serves: the project takes it by URL from [the fork's release](https://github.com/datalayer-externals/jupyter-server/releases/tag/v2.21.0-datalayer.1), in `[tool.uv.sources]`, which is also what makes `uv` record its hash. `image` resolves but does not yet start as a sandbox, because the imported image has no Datalayer runtime layer; `image` from a private registry, and a `COPY` from a Dockerfile's build context, are refused. |
46
46
 
47
47
  ## The spec digest
48
48
 
@@ -270,6 +270,16 @@ for, and the `target` a build's snapshot lands in is the one a launch
270
270
  starts from — a version that changes size class or region rebuilds a
271
271
  second, distinct artifact rather than resizing the first.
272
272
 
273
+ **A GPU is part of the snapshot too.** A GPU class (`gpu-small`,
274
+ `gpu-large`) on the CUDA base bakes the spec's `accelerator` into it: its
275
+ `type`, one of Daytona's GPUs, and its `count`. CPU and memory come from
276
+ `resources.hints`, or Daytona's own size for that GPU, and the disk holds the
277
+ CUDA base. Every sandbox of a GPU snapshot is ephemeral, as Daytona requires:
278
+ `DaytonaSandbox` reads the snapshot's `gpu` and asks for it. The build's
279
+ smoke test adds check 11 to the core tier: the GPUs asked for are visible,
280
+ CUDA is the base's, and the host's driver is new enough for it. GPU compute
281
+ is billed by Daytona to the owner's organization, which needs GPU credits.
282
+
273
283
  Of the four variants, Daytona is the one whose build-time doctor check and
274
284
  live launch agree in full: the doctor is *not* run at build time (a
275
285
  build-time `RUN` step's PID 1 is Daytona's own build agent, not the
@@ -700,6 +700,44 @@ def test_a_snapshot_and_an_image_together_are_refused():
700
700
  sandbox._create_params(daytona)
701
701
 
702
702
 
703
+ class _SnapshotsNamed:
704
+ """`daytona.snapshot`, answering the records it was given by id."""
705
+
706
+ def __init__(self, **records) -> None:
707
+ self.records = records
708
+
709
+ def get(self, snapshot_id):
710
+ if snapshot_id not in self.records:
711
+ raise KeyError(snapshot_id)
712
+ return self.records[snapshot_id]
713
+
714
+
715
+ def test_a_sandbox_of_a_gpu_snapshot_is_ephemeral():
716
+ """E2-17: a GPU is baked into an Environment's snapshot, so its sandbox asks
717
+ for a GPU without `gpu=`, and Daytona refuses it unless it is ephemeral."""
718
+ import types
719
+
720
+ daytona = pytest.importorskip("daytona")
721
+ sandbox = _started(SandboxConfig(), snapshot="dl-gpu-v1")
722
+ sandbox._daytona = types.SimpleNamespace(
723
+ snapshot=_SnapshotsNamed(**{"dl-gpu-v1": types.SimpleNamespace(gpu=2)})
724
+ )
725
+ params = sandbox._create_params(daytona)
726
+ assert isinstance(params, daytona.CreateSandboxFromSnapshotParams)
727
+ assert params.auto_delete_interval == 0
728
+
729
+
730
+ def test_a_sandbox_of_a_cpu_snapshot_is_kept_as_asked():
731
+ import types
732
+
733
+ daytona = pytest.importorskip("daytona")
734
+ for records in ({"dl-geo-v3": types.SimpleNamespace(gpu=0)}, {}):
735
+ sandbox = _started(SandboxConfig(), snapshot="dl-geo-v3")
736
+ sandbox._daytona = types.SimpleNamespace(snapshot=_SnapshotsNamed(**records))
737
+ params = sandbox._create_params(daytona)
738
+ assert getattr(params, "auto_delete_interval", None) != 0
739
+
740
+
703
741
  def test_a_snapshot_alone_still_creates_from_the_snapshot():
704
742
  daytona = pytest.importorskip("daytona")
705
743