code-sandboxes 1.4.4__tar.gz → 1.4.6__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 (195) hide show
  1. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/CHANGELOG.md +47 -0
  2. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/PKG-INFO +1 -1
  3. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/__version__.py +1 -1
  4. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/base.py +44 -0
  5. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/cloudflare_sandbox.py +2 -1
  6. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/coreweave_sandbox.py +2 -1
  7. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/datalayer_sandbox.py +9 -7
  8. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/daytona_sandbox.py +2 -1
  9. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/docker_sandbox.py +19 -1
  10. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/e2b_sandbox.py +2 -1
  11. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/eval_sandbox.py +2 -1
  12. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/google_colab_sandbox.py +14 -1
  13. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/jupyter_ingress.py +30 -0
  14. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/jupyter_server_sandbox.py +2 -1
  15. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/kaggle_sandbox.py +14 -1
  16. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/modal_sandbox.py +2 -1
  17. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/monty_sandbox.py +21 -1
  18. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +59 -47
  19. code_sandboxes-1.4.6/tests/test_every_variant_has_a_real_interrupt.py +262 -0
  20. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/.github/workflows/build.yml +0 -0
  21. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/.github/workflows/py-code-style.yml +0 -0
  22. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/.github/workflows/py-tests.yml +0 -0
  23. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/.github/workflows/py-typing.yml +0 -0
  24. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/.github/workflows/reusable-python.yml +0 -0
  25. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/.gitignore +0 -0
  26. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/.licenserc.yaml +0 -0
  27. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/.pre-commit-config.yaml +0 -0
  28. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/.vscode/settings.json +0 -0
  29. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/Dockerfile +0 -0
  30. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/LICENSE +0 -0
  31. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/Makefile +0 -0
  32. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/README.md +0 -0
  33. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/__init__.py +0 -0
  34. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/bridge_mount.py +0 -0
  35. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/builds.py +0 -0
  36. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/cli.py +0 -0
  37. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/client.py +0 -0
  38. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/commands.py +0 -0
  39. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/console.py +0 -0
  40. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/contents.py +0 -0
  41. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/exceptions.py +0 -0
  42. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/filesystem.py +0 -0
  43. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/google_colab.py +0 -0
  44. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/interfaces.py +0 -0
  45. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/kaggle.py +0 -0
  46. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/kaggle_execute.py +0 -0
  47. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/kaggle_live.py +0 -0
  48. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/lifecycle.py +0 -0
  49. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/manage.py +0 -0
  50. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/models.py +0 -0
  51. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/provider_ingress.py +0 -0
  52. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/code_sandboxes/providers.py +0 -0
  53. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docker/Dockerfile +0 -0
  54. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/.gitignore +0 -0
  55. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/LICENSE +0 -0
  56. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/Makefile +0 -0
  57. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/README.md +0 -0
  58. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/babel.config.js +0 -0
  59. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/api-reference/_category_.yml +0 -0
  60. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/api-reference/index.mdx +0 -0
  61. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/cli/_category_.yml +0 -0
  62. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/cli/index.mdx +0 -0
  63. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/contribute/_category_.yml +0 -0
  64. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/contribute/index.mdx +0 -0
  65. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/examples/_category_.yml +0 -0
  66. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/examples/index.mdx +0 -0
  67. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/guide/_category_.yml +0 -0
  68. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/guide/index.mdx +0 -0
  69. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/index.mdx +0 -0
  70. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/install/_category_.yml +0 -0
  71. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/install/index.mdx +0 -0
  72. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/provider-ingress.mdx +0 -0
  73. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/providers/_category_.yml +0 -0
  74. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/providers/cloudflare.mdx +0 -0
  75. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/providers/coreweave.mdx +0 -0
  76. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/providers/datalayer.mdx +0 -0
  77. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/providers/daytona.mdx +0 -0
  78. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/providers/docker.mdx +0 -0
  79. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/providers/e2b.mdx +0 -0
  80. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/providers/eval.mdx +0 -0
  81. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/providers/google-colab.mdx +0 -0
  82. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/providers/index.mdx +0 -0
  83. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/providers/jupyter-server.mdx +0 -0
  84. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/providers/kaggle.mdx +0 -0
  85. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/providers/modal.mdx +0 -0
  86. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docs/providers/monty.mdx +0 -0
  87. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/docusaurus.config.js +0 -0
  88. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/package.json +0 -0
  89. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/sidebars.js +0 -0
  90. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/src/components/HomepageRow1.js +0 -0
  91. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/src/components/HomepageRow1.module.css +0 -0
  92. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/src/components/HomepageRow2.js +0 -0
  93. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/src/components/HomepageRow2.module.css +0 -0
  94. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/src/components/HomepageRow3.js +0 -0
  95. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/src/components/HomepageRow3.module.css +0 -0
  96. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/src/css/custom.css +0 -0
  97. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/src/pages/index.module.css +0 -0
  98. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/src/pages/markdown-page.md +0 -0
  99. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/src/theme/CustomDocItem.tsx +0 -0
  100. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/src/theme/IconsTitle.tsx +0 -0
  101. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/bricks.svg +0 -0
  102. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/building-construction.svg +0 -0
  103. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/cloud-sun.svg +0 -0
  104. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/cloud.svg +0 -0
  105. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/collaboration.svg +0 -0
  106. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/copy-left.svg +0 -0
  107. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/datalayer/logo.png +0 -0
  108. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/datalayer/logo.svg +0 -0
  109. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/datalayer/personas/business.svg +0 -0
  110. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
  111. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/datalayer/personas/devops.svg +0 -0
  112. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/datalayer/project/project-line.svg +0 -0
  113. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/favicon.ico +0 -0
  114. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/jupyter.svg +0 -0
  115. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/open-source.svg +0 -0
  116. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/react-js.svg +0 -0
  117. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/react-js2.svg +0 -0
  118. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/ringed-planet.svg +0 -0
  119. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/robot.svg +0 -0
  120. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/rocket.svg +0 -0
  121. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/shield.svg +0 -0
  122. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/sparkles.svg +0 -0
  123. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/streamlit.gif +0 -0
  124. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/super-hero.svg +0 -0
  125. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/target.svg +0 -0
  126. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/docs/static/img/thumbs-up.svg +0 -0
  127. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/README.md +0 -0
  128. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/exec/Makefile +0 -0
  129. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/exec/cloudflare_sandbox_example.py +0 -0
  130. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/exec/coreweave_sandbox_example.py +0 -0
  131. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/exec/datalayer_sandbox_example.py +0 -0
  132. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/exec/daytona_sandbox_example.py +0 -0
  133. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/exec/docker_sandbox_example.py +0 -0
  134. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/exec/e2b_sandbox_example.py +0 -0
  135. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/exec/eval_sandbox_example.py +0 -0
  136. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/exec/google_colab_sandbox_example.py +0 -0
  137. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/exec/jupyter_server_sandbox_example.py +0 -0
  138. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/exec/kaggle_sandbox_example.py +0 -0
  139. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/exec/modal_sandbox_example.py +0 -0
  140. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/exec/monty_sandbox_example.py +0 -0
  141. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/repl/Makefile +0 -0
  142. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/repl/cloudflare_sandbox_example.py +0 -0
  143. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/repl/coreweave_sandbox_example.py +0 -0
  144. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/repl/datalayer_sandbox_example.py +0 -0
  145. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/repl/daytona_sandbox_example.py +0 -0
  146. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/repl/docker_sandbox_example.py +0 -0
  147. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/repl/e2b_sandbox_example.py +0 -0
  148. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/repl/eval_sandbox_example.py +0 -0
  149. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/repl/google_colab_sandbox_example.py +0 -0
  150. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/repl/jupyter_server_sandbox_example.py +0 -0
  151. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/repl/kaggle_sandbox_example.py +0 -0
  152. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/repl/modal_sandbox_example.py +0 -0
  153. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/examples/repl/monty_sandbox_example.py +0 -0
  154. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/pyproject.toml +0 -0
  155. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/__init__.py +0 -0
  156. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/conftest.py +0 -0
  157. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_bridge_mount.py +0 -0
  158. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_builds.py +0 -0
  159. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_cli_exec.py +0 -0
  160. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_cli_repl.py +0 -0
  161. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_client.py +0 -0
  162. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_client_contents.py +0 -0
  163. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_client_files.py +0 -0
  164. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_client_outcome_error_output.py +0 -0
  165. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_cloudflare.py +0 -0
  166. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_commands_envelope.py +0 -0
  167. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_console.py +0 -0
  168. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_contents_contract.py +0 -0
  169. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_contents_live_matrix.py +0 -0
  170. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_coreweave.py +0 -0
  171. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_datalayer_sandbox.py +0 -0
  172. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_daytona.py +0 -0
  173. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_e2b.py +0 -0
  174. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_eval.py +0 -0
  175. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_factory.py +0 -0
  176. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_google_colab.py +0 -0
  177. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_integration.py +0 -0
  178. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_jupyter_ingress.py +0 -0
  179. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_jupyter_server.py +0 -0
  180. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_jupyter_server_streaming.py +0 -0
  181. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_kaggle.py +0 -0
  182. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_kaggle_execute.py +0 -0
  183. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_kaggle_live.py +0 -0
  184. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_kaggle_session.py +0 -0
  185. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_kernel_client_compatibility.py +0 -0
  186. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_lifecycle_vocabulary.py +0 -0
  187. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_manage.py +0 -0
  188. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_modal_google_colab_sandbox.py +0 -0
  189. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_modal_session.py +0 -0
  190. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_models.py +0 -0
  191. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_output_line_fidelity.py +0 -0
  192. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_provider_catalog_names.py +0 -0
  193. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_provider_ingress.py +0 -0
  194. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_providers.py +0 -0
  195. {code_sandboxes-1.4.4 → code_sandboxes-1.4.6}/tests/test_sandboxes.py +0 -0
@@ -8,6 +8,53 @@
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ - **Every variant has a real interrupt now, and can be reached to give it.**
12
+ `Sandbox.interrupt` is two gates — `_executing_event` must be set, then
13
+ `_do_interrupt` must answer — and seven variants failed one of them, each in
14
+ a way that looked like success:
15
+
16
+ - `docker`, `google_colab`, `kaggle` and `monty` had no `_do_interrupt`, so
17
+ the base default ran: set a flag, return `True`, stop nothing. `True` means
18
+ "the interrupt was delivered", and nothing had been. `google_colab` and
19
+ `kaggle` did read the flag, but only *after* the run, to label a finished
20
+ result as `interrupted` — labelling a run is not stopping one.
21
+ - `cloudflare`, `coreweave`, `daytona`, `e2b` and `modal` answered honestly
22
+ (`False`: this provider takes no interrupt) but never marked their execution
23
+ window, so `interrupt()` returned at the first gate and their answer was
24
+ never reached — and `is_executing` was always False, which every status
25
+ above them believed.
26
+
27
+ Docker, Colab and Kaggle now interrupt for real, delegating to the
28
+ `JupyterKernelClient` that already knows how to authenticate to their server
29
+ rather than rebuilding the request. Monty answers `False` with its reason:
30
+ `feed_run` executes a snippet in one blocking call with no limits and no
31
+ callable hook, so there is no point at which a flag could be read.
32
+
33
+ The execution window is `@marks_execution` on `run_code` rather than a line
34
+ in each body, because it has to close on *every* path out — including the
35
+ early `return ExecutionResult(...)` each adapter uses for an infrastructure
36
+ failure — and a `finally` in the decorator cannot be forgotten in one branch
37
+ of one variant. Both invariants are asserted across the package with no
38
+ pinned exceptions left.
39
+
40
+ - A Datalayer sandbox says when it is running code, so an interrupt can reach
41
+ it. `Sandbox.interrupt` refuses before it delegates — `if not
42
+ self._executing_event.is_set(): return False` — and `run_code` never set that
43
+ event, so `is_executing` was always False, every interrupt was refused at the
44
+ door, and the refusal was reported as "no code was running" to a caller
45
+ watching a cell run.
46
+
47
+ This is the other half of the interrupt fix released in 1.4.4, and the
48
+ measurement said so: with `_do_interrupt` implemented and deployed, cancelling
49
+ a two-minute cell ten seconds in still left the kernel busy for **60.5
50
+ seconds**. Implementing the interrupt changed nothing while nothing could call
51
+ it. Both are needed, and the package's tests now hold them together.
52
+
53
+ Five variants — cloudflare, coreweave, daytona, e2b, modal — implement
54
+ `_do_interrupt` and never mark execution either, so their interrupts are
55
+ unreachable in the same way. Pinned as known rather than fixed blind, since
56
+ none can be measured from here.
57
+
11
58
  - `DatalayerSandbox` can be interrupted. It neither implemented `_do_interrupt`
12
59
  nor read the flag the base class sets, so the default ran instead: it sets a
13
60
  flag, returns `True`, and stops nothing. Everything above believed it —
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: code_sandboxes
3
- Version: 1.4.4
3
+ Version: 1.4.6
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.4.4"
6
+ __version__ = "1.4.6"
@@ -6,6 +6,7 @@
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import functools
9
10
  import threading
10
11
  import uuid
11
12
  from abc import ABC, abstractmethod
@@ -82,6 +83,49 @@ def generate_sandbox_name() -> str:
82
83
  DEFAULT_ENVIRONMENT = "ai-agents-env"
83
84
 
84
85
 
86
+ def marks_execution(run):
87
+ """Say, for the duration of the call, that this sandbox is running code.
88
+
89
+ `Sandbox.interrupt` refuses before it delegates::
90
+
91
+ if not self._executing_event.is_set():
92
+ return False
93
+ self._interrupt_requested.set()
94
+ return self._do_interrupt()
95
+
96
+ So a variant that never sets the event has an interrupt nothing can
97
+ reach, and an `is_executing` that is always False — every status and
98
+ every cancel decision above it silently wrong. `DatalayerSandbox` was in
99
+ exactly that state, and implementing `_do_interrupt` for it changed the
100
+ measurement on prod1 from 60.8 seconds to 60.5: a fix that reads as
101
+ complete and measures as nothing.
102
+
103
+ A decorator rather than a line in each body, because the window has to
104
+ close on *every* path out — including the several early `return
105
+ ExecutionResult(...)`s that each adapter uses for an infrastructure
106
+ failure — and a `finally` here cannot be forgotten in one branch of one
107
+ variant. It also gives the invariant something to test that is not a
108
+ grep for a line of source.
109
+
110
+ `_interrupt_requested` is cleared on the way in, so a stale request from
111
+ a previous call cannot stop this one, and deliberately *not* on the way
112
+ out: the cooperative variants read it after the run to report that the
113
+ execution was interrupted.
114
+ """
115
+
116
+ @functools.wraps(run)
117
+ def marked(self, *args, **kwargs):
118
+ self._interrupt_requested.clear()
119
+ self._executing_event.set()
120
+ try:
121
+ return run(self, *args, **kwargs)
122
+ finally:
123
+ self._executing_event.clear()
124
+
125
+ marked._marks_execution = True
126
+ return marked
127
+
128
+
85
129
  class Sandbox(ABC):
86
130
  """Abstract base class for code execution sandboxes.
87
131
 
@@ -43,7 +43,7 @@ from collections.abc import Iterator
43
43
  from typing import Any
44
44
  from urllib.parse import quote
45
45
 
46
- from .base import Sandbox
46
+ from .base import Sandbox, marks_execution
47
47
  from .exceptions import (
48
48
  SandboxConfigurationError,
49
49
  SandboxConnectionError,
@@ -370,6 +370,7 @@ class CloudflareSandbox(Sandbox):
370
370
  except Exception:
371
371
  return False
372
372
 
373
+ @marks_execution
373
374
  def run_code(
374
375
  self,
375
376
  code: str,
@@ -31,7 +31,7 @@ import math
31
31
  import time
32
32
  from typing import Any
33
33
 
34
- from .base import Sandbox
34
+ from .base import Sandbox, marks_execution
35
35
  from .exceptions import (
36
36
  SandboxConfigurationError,
37
37
  SandboxExecutionError,
@@ -521,6 +521,7 @@ class CoreWeaveSandbox(Sandbox):
521
521
  if self._info:
522
522
  self._info.status = SandboxStatus.STOPPED
523
523
 
524
+ @marks_execution
524
525
  def run_code(
525
526
  self,
526
527
  code: str,
@@ -17,7 +17,7 @@ from typing import TYPE_CHECKING, Any, Optional
17
17
  if TYPE_CHECKING:
18
18
  from .filesystem import SandboxFileHandle
19
19
 
20
- from .base import Sandbox
20
+ from .base import Sandbox, marks_execution
21
21
  from .contents import (
22
22
  FILESYSTEM_PRIMITIVES,
23
23
  LOCAL_BRIDGE_MOUNT,
@@ -761,6 +761,7 @@ class DatalayerSandbox(Sandbox):
761
761
  return None
762
762
  return 0
763
763
 
764
+ @marks_execution
764
765
  def run_code(
765
766
  self,
766
767
  code: str,
@@ -801,14 +802,15 @@ class DatalayerSandbox(Sandbox):
801
802
 
802
803
  started_at = time.time()
803
804
 
804
- # Set environment variables if provided
805
- if envs:
806
- env_code = "\n".join(f"import os; os.environ[{k!r}] = {v!r}" for k, v in envs.items())
807
- self._runtime.execute(env_code)
808
-
809
- # Execute the code
805
+ # The execution window is `@marks_execution`'s, not this body's — see
806
+ # its docstring for why the interrupt is refused without one.
810
807
  execution_timeout = timeout or self.config.timeout
811
808
  try:
809
+ # Set environment variables if provided
810
+ if envs:
811
+ env_code = "\n".join(f"import os; os.environ[{k!r}] = {v!r}" for k, v in envs.items())
812
+ self._runtime.execute(env_code)
813
+
812
814
  response = self._runtime.execute(code, timeout=execution_timeout)
813
815
  except Exception as e:
814
816
  # Infrastructure failure - couldn't execute the code
@@ -30,7 +30,7 @@ import textwrap
30
30
  import time
31
31
  from typing import Any
32
32
 
33
- from .base import Sandbox
33
+ from .base import Sandbox, marks_execution
34
34
  from .contents import (
35
35
  FILESYSTEM_PRIMITIVES,
36
36
  ContentAttachmentSpec,
@@ -701,6 +701,7 @@ class DaytonaSandbox(Sandbox):
701
701
  self._contexts[context.id] = existing
702
702
  return existing
703
703
 
704
+ @marks_execution
704
705
  def run_code( # noqa: C901
705
706
  self,
706
707
  code: str,
@@ -15,9 +15,12 @@ import tempfile
15
15
  import time
16
16
  import uuid
17
17
 
18
+ import logging
19
+
18
20
  import requests
19
21
 
20
- from .base import Sandbox
22
+ from .base import Sandbox, marks_execution
23
+ from .jupyter_ingress import interrupt_kernel_client
21
24
  from .exceptions import SandboxConfigurationError, SandboxNotStartedError
22
25
  from .interfaces import ISandboxClient
23
26
  from .models import (
@@ -34,6 +37,8 @@ from .models import (
34
37
  SandboxStatus,
35
38
  )
36
39
 
40
+ logger = logging.getLogger(__name__)
41
+
37
42
  DEFAULT_IMAGE = "code-sandboxes-jupyter:latest"
38
43
  DEFAULT_PORT = 8888
39
44
 
@@ -194,6 +199,18 @@ class DockerSandbox(Sandbox):
194
199
  """The underlying kernel client for this sandbox, if started."""
195
200
  return self._client
196
201
 
202
+ def _do_interrupt(self) -> bool:
203
+ """Interrupt the container's kernel, through the client that runs it.
204
+
205
+ This variant runs a Jupyter Server in a container and executes
206
+ through `jupyter-kernel-client`, so it can be interrupted — and it
207
+ had no `_do_interrupt` at all, so the base class's default ran: sets
208
+ a flag, returns `True`, stops nothing. A caller that cancelled a long
209
+ cell here was told the interrupt had been delivered while the
210
+ container went on computing.
211
+ """
212
+ return interrupt_kernel_client(self._client, variant="container's")
213
+
197
214
  def stop(self) -> None:
198
215
  if not self._started:
199
216
  return
@@ -225,6 +242,7 @@ class DockerSandbox(Sandbox):
225
242
  if self._info:
226
243
  self._info.status = SandboxStatus.STOPPED
227
244
 
245
+ @marks_execution
228
246
  def run_code(
229
247
  self,
230
248
  code: str,
@@ -28,7 +28,7 @@ import logging
28
28
  import time
29
29
  from typing import Any
30
30
 
31
- from .base import Sandbox
31
+ from .base import Sandbox, marks_execution
32
32
  from .contents import (
33
33
  FILESYSTEM_PRIMITIVES,
34
34
  ContentAttachmentSpec,
@@ -471,6 +471,7 @@ class E2BSandbox(Sandbox):
471
471
  self._contexts[context.id] = existing
472
472
  return existing
473
473
 
474
+ @marks_execution
474
475
  def run_code(
475
476
  self,
476
477
  code: str,
@@ -24,7 +24,7 @@ import uuid
24
24
  from contextlib import contextmanager, redirect_stderr, redirect_stdout
25
25
  from typing import Any, Optional
26
26
 
27
- from .base import Sandbox
27
+ from .base import Sandbox, marks_execution
28
28
  from .exceptions import SandboxNotStartedError
29
29
  from .models import (
30
30
  CodeError,
@@ -139,6 +139,7 @@ class EvalSandbox(Sandbox):
139
139
  self._execution_count[context.id] = 0
140
140
  return context
141
141
 
142
+ @marks_execution
142
143
  def run_code(
143
144
  self,
144
145
  code: str,
@@ -19,7 +19,8 @@ import logging
19
19
  import time
20
20
  import uuid
21
21
 
22
- from .base import Sandbox
22
+ from .base import Sandbox, marks_execution
23
+ from .jupyter_ingress import interrupt_kernel_client
23
24
  from .exceptions import SandboxConfigurationError, SandboxNotStartedError
24
25
  from .google_colab import GoogleColabKernelClient, parse_google_colab_channels_url
25
26
  from .interfaces import ISandboxClient
@@ -140,6 +141,17 @@ class GoogleColabSandbox(Sandbox):
140
141
  """Keep tool calling on the client side for Colab sandboxes."""
141
142
  return
142
143
 
144
+ def _do_interrupt(self) -> bool:
145
+ """Interrupt the Colab kernel, through the client that runs it.
146
+
147
+ There was no override here, so the base class's default ran: it sets
148
+ a flag, returns `True`, and stops nothing. This variant *does* read
149
+ that flag — but only after the fact, to label a result as
150
+ `interrupted`. Labelling a run is not stopping one: the notebook kept
151
+ executing, and the caller had been told the interrupt was delivered.
152
+ """
153
+ return interrupt_kernel_client(self._client, variant="Colab")
154
+
143
155
  def stop(self) -> None:
144
156
  if not self._started:
145
157
  return
@@ -154,6 +166,7 @@ class GoogleColabSandbox(Sandbox):
154
166
  if self._info:
155
167
  self._info.status = SandboxStatus.STOPPED
156
168
 
169
+ @marks_execution
157
170
  def run_code( # noqa: C901
158
171
  self,
159
172
  code: str,
@@ -6,11 +6,15 @@
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import logging
9
10
  import secrets
10
11
  import shlex
12
+ from typing import Any
11
13
 
12
14
  from .models import JupyterServerOptions
13
15
 
16
+ logger = logging.getLogger(__name__)
17
+
14
18
 
15
19
  def resolved_options(options: JupyterServerOptions | None) -> JupyterServerOptions:
16
20
  """Return options carrying a fresh Jupyter token when none was supplied."""
@@ -72,3 +76,29 @@ def websocket_url(http_url: str) -> str:
72
76
  if http_url.startswith("http://"):
73
77
  return "ws://" + http_url.removeprefix("http://")
74
78
  raise ValueError(f"Provider returned a non-HTTP ingress URL: {http_url!r}")
79
+
80
+
81
+ def interrupt_kernel_client(client: Any, *, variant: str) -> bool:
82
+ """Interrupt the kernel a `JupyterKernelClient` is connected to.
83
+
84
+ Every Jupyter-backed variant reaches its kernel through a client that
85
+ already knows how to authenticate to that server — Colab's proxy token
86
+ header and query parameter, Kaggle's signed proxy URL, Docker's Jupyter
87
+ token. `JupyterKernelClient.interrupt` uses exactly that, so this
88
+ delegates rather than rebuilding the request: a second copy of the auth
89
+ is a second thing to keep in step with the first.
90
+
91
+ The client's `interrupt` answers `None` and raises when it fails, so this
92
+ turns it into the boolean `_do_interrupt` owes its caller. It never
93
+ raises: a cancel that could not reach the kernel is a cancel that failed,
94
+ and the caller decides what that means.
95
+ """
96
+ if client is None:
97
+ return False
98
+ try:
99
+ client.interrupt()
100
+ except Exception as error: # noqa: BLE001 - a failed interrupt is an answer
101
+ logger.warning("The %s kernel could not be interrupted: %s", variant, error)
102
+ return False
103
+ return True
104
+
@@ -29,7 +29,7 @@ from urllib.parse import parse_qs, urlparse, urlunparse
29
29
 
30
30
  import requests
31
31
 
32
- from .base import Sandbox
32
+ from .base import Sandbox, marks_execution
33
33
  from .exceptions import SandboxConfigurationError, SandboxNotStartedError
34
34
  from .interfaces import ISandboxClient
35
35
  from .models import (
@@ -572,6 +572,7 @@ class JupyterServerSandbox(Sandbox):
572
572
  logger.warning(f"Failed to interrupt Jupyter kernel: {e}")
573
573
  return False
574
574
 
575
+ @marks_execution
575
576
  def run_code( # noqa: C901
576
577
  self,
577
578
  code: str,
@@ -33,7 +33,8 @@ from collections.abc import AsyncIterator, Iterator
33
33
  from pathlib import Path
34
34
  from typing import Any
35
35
 
36
- from .base import Sandbox
36
+ from .base import Sandbox, marks_execution
37
+ from .jupyter_ingress import interrupt_kernel_client
37
38
  from .exceptions import SandboxConfigurationError, SandboxNotStartedError
38
39
  from .interfaces import ISandboxClient
39
40
  from .kaggle import KaggleKernelClient, parse_kaggle_channels_url
@@ -261,6 +262,17 @@ class KaggleSandbox(Sandbox):
261
262
  except (OSError, ValueError):
262
263
  pass
263
264
 
265
+ def _do_interrupt(self) -> bool:
266
+ """Interrupt the Kaggle kernel, through the client that runs it.
267
+
268
+ There was no override here, so the base class's default ran: it sets
269
+ a flag, returns `True`, and stops nothing. This variant *does* read
270
+ that flag — but only after the fact, to label a result as
271
+ `interrupted`. Labelling a run is not stopping one: the notebook kept
272
+ executing, and the caller had been told the interrupt was delivered.
273
+ """
274
+ return interrupt_kernel_client(self._client, variant="Kaggle")
275
+
264
276
  def stop(self) -> None:
265
277
  if not self._started:
266
278
  return
@@ -277,6 +289,7 @@ class KaggleSandbox(Sandbox):
277
289
  if self._info:
278
290
  self._info.status = SandboxStatus.STOPPED
279
291
 
292
+ @marks_execution
280
293
  def run_code( # noqa: C901
281
294
  self,
282
295
  code: str,
@@ -25,7 +25,7 @@ import time
25
25
  import uuid
26
26
  from typing import Any
27
27
 
28
- from .base import Sandbox
28
+ from .base import Sandbox, marks_execution
29
29
  from .contents import (
30
30
  CREDENTIAL_DELIVERY_UNSUPPORTED,
31
31
  FILESYSTEM_PRIMITIVES,
@@ -472,6 +472,7 @@ class ModalSandbox(Sandbox):
472
472
  if self._info:
473
473
  self._info.status = SandboxStatus.STOPPED
474
474
 
475
+ @marks_execution
475
476
  def run_code( # noqa: C901
476
477
  self,
477
478
  code: str,
@@ -25,7 +25,7 @@ import time
25
25
  import uuid
26
26
  from typing import Any
27
27
 
28
- from .base import Sandbox
28
+ from .base import Sandbox, marks_execution
29
29
  from .exceptions import SandboxConfigurationError, SandboxNotStartedError
30
30
  from .models import (
31
31
  CodeError,
@@ -126,6 +126,25 @@ class MontySandbox(Sandbox):
126
126
  )
127
127
  self._started = True
128
128
 
129
+ def _do_interrupt(self) -> bool:
130
+ """Monty's REPL takes no interrupt; a timeout is the only stop.
131
+
132
+ `False` is the honest answer, and it is the point of writing this at
133
+ all. Without an override the base class's default ran — set a flag,
134
+ return `True` — so a caller that cancelled a long run here was told
135
+ the interrupt had been delivered while the interpreter went on.
136
+ Answering `False` lets the caller say "this could not be stopped"
137
+ instead of "this was stopped".
138
+
139
+ `MontyRepl.feed_run` executes the whole snippet in one blocking call.
140
+ It takes no limits (only `Monty.run` does) and its `print_callback`
141
+ is a `CollectStreams` object the interpreter writes into rather than
142
+ a callable it invokes, so there is no point at which a flag could be
143
+ read. If a later Monty exposes a step or callback hook, this is where
144
+ a cooperative interrupt goes.
145
+ """
146
+ return False
147
+
129
148
  def stop(self) -> None:
130
149
  if not self._started:
131
150
  return
@@ -142,6 +161,7 @@ class MontySandbox(Sandbox):
142
161
  return value.decode("utf-8", errors="replace")
143
162
  return str(value)
144
163
 
164
+ @marks_execution
145
165
  def run_code( # noqa: C901
146
166
  self,
147
167
  code: str,
@@ -4,13 +4,16 @@
4
4
  """A sandbox that cannot be interrupted, and said it had been.
5
5
 
6
6
  `Sandbox.interrupt` sets a flag and calls `_do_interrupt`, whose default
7
- "simply sets the interrupt flag" and returns `True`. A variant is entitled to
8
- that default only if it *reads* the flag — `google_colab` and `kaggle` poll
9
- `_interrupt_requested` and interrupt cooperatively. `datalayer_sandbox` did
10
- neither: no override, no read. So on a Datalayer runtime the interrupt was
11
- delivered nowhere and reported success, and everything above believed it:
12
- `agent_runtimes`' `interrupt_kernel` delegates straight to it, and so does
13
- the interrupt `execute_cell` registers for `tasks/cancel`.
7
+ "simply sets the interrupt flag" and returns `True`. `datalayer_sandbox` had
8
+ no override, so on a Datalayer runtime the interrupt was delivered nowhere
9
+ and reported success, and everything above believed it: `agent_runtimes`'
10
+ `interrupt_kernel` delegates straight to it, and so does the interrupt
11
+ `execute_cell` registers for `tasks/cancel`.
12
+
13
+ This variant, in detail. The invariants that hold for the whole package —
14
+ that no variant inherits the lying default, and that each marks the window
15
+ its interrupt is reachable through — are in
16
+ `test_every_variant_has_a_real_interrupt.py`.
14
17
 
15
18
  Measured on prod1 on 2026-09-07: a cell looping for two minutes, cancelled
16
19
  ten seconds in. `tasks/cancel` and `tasks/get` both answered `cancelled`,
@@ -51,51 +54,12 @@ def _sandbox(runtime):
51
54
  return made
52
55
 
53
56
 
54
- class TestEveryVariantCanBeInterrupted:
57
+ class TestItHasOneAtAll:
55
58
  def test_datalayer_overrides_it_like_the_others(self):
56
59
  """The assertion that would have caught this: it is about the class,
57
60
  not about a call, because the default *succeeds* silently."""
58
61
  assert "_do_interrupt" in DatalayerSandbox.__dict__
59
62
 
60
- def test_no_variant_quietly_acknowledges_an_interrupt(self):
61
- """Held across the package, so the next variant added without one
62
- fails here rather than on a runtime somebody is paying for.
63
-
64
- A variant may either override `_do_interrupt` or consult the flag the
65
- base sets; doing neither means `interrupt()` returns True having done
66
- nothing. `docker` and `monty` do neither today — pinned rather than
67
- hidden, so the set cannot grow silently and shrinks visibly when one
68
- is fixed.
69
- """
70
- import importlib
71
- import inspect
72
- import pkgutil
73
-
74
- import code_sandboxes
75
- from code_sandboxes.base import Sandbox
76
-
77
- deaf = set()
78
- for module in pkgutil.iter_modules(code_sandboxes.__path__):
79
- if not module.name.endswith("_sandbox"):
80
- continue
81
- loaded = importlib.import_module(f"code_sandboxes.{module.name}")
82
- reads_flag = "_interrupt_requested" in inspect.getsource(loaded)
83
- for name in dir(loaded):
84
- value = getattr(loaded, name)
85
- if (
86
- isinstance(value, type)
87
- and issubclass(value, Sandbox)
88
- and value is not Sandbox
89
- and value.__module__ == loaded.__name__
90
- and "_do_interrupt" not in value.__dict__
91
- and not reads_flag
92
- ):
93
- deaf.add(f"{module.name}.{name}")
94
-
95
- known = {"docker_sandbox.DockerSandbox", "monty_sandbox.MontySandbox"}
96
- assert deaf - known == set(), f"new variants cannot be interrupted: {deaf - known}"
97
- assert known - deaf == set(), f"fixed — drop from the pinned set: {known - deaf}"
98
-
99
63
 
100
64
  class TestWhatItDoes:
101
65
  def test_it_asks_the_runtime_s_own_client(self):
@@ -160,3 +124,51 @@ class TestTheChainReachesAKernel:
160
124
  from code_sandboxes.jupyter_server_sandbox import JupyterServerSandbox
161
125
 
162
126
  assert "_do_interrupt" in JupyterServerSandbox.__dict__
127
+
128
+ class TestTheInterruptIsReached:
129
+ """`_do_interrupt` was necessary and not sufficient, which the first
130
+ measurement after deploying it said plainly: still 60.5 seconds.
131
+
132
+ `Sandbox.interrupt` refuses before it delegates::
133
+
134
+ if not self._executing_event.is_set():
135
+ return False
136
+ self._interrupt_requested.set()
137
+ return self._do_interrupt()
138
+
139
+ `DatalayerSandbox.run_code` never set that event, so `is_executing` was
140
+ always False and every interrupt was refused at the door — and reported
141
+ as "no code was running" to a caller watching a cell run. Two guards,
142
+ both needed: one that the door opens, one that what is behind it works.
143
+ """
144
+
145
+ def test_run_code_says_that_code_is_running(self):
146
+ import threading
147
+
148
+ from code_sandboxes.datalayer_sandbox import DatalayerSandbox
149
+
150
+ seen = []
151
+
152
+ class _Runtime:
153
+ sandbox_client = None
154
+
155
+ def execute(self, code, timeout=None):
156
+ seen.append(sandbox._executing_event.is_set())
157
+ raise RuntimeError("stop here; the flag is what is under test")
158
+
159
+ sandbox = DatalayerSandbox.__new__(DatalayerSandbox)
160
+ sandbox._started = True
161
+ sandbox._runtime = _Runtime()
162
+ sandbox._executing_event = threading.Event()
163
+ sandbox._interrupt_requested = threading.Event()
164
+ sandbox._connect = lambda: None
165
+ sandbox._sandbox_id = "sb_test"
166
+
167
+ class _Config:
168
+ timeout = 30
169
+
170
+ sandbox.config = _Config()
171
+ sandbox.run_code("print(1)")
172
+
173
+ assert seen == [True], "the sandbox ran code without saying it was running"
174
+ assert not sandbox._executing_event.is_set(), "the flag outlived the execution"