code-sandboxes 1.9.31__tar.gz → 1.9.32__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.
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/PKG-INFO +1 -1
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/__version__.py +1 -1
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/daytona_sandbox.py +19 -1
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/adapters/daytona.py +78 -44
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/bases.py +16 -2
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/conformance.py +67 -8
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/spec.py +18 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/environments/specification.mdx +2 -2
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/providers/daytona.mdx +10 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_daytona.py +38 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_bases.py +19 -8
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_conformance.py +42 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_daytona_builder.py +108 -11
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_managed_builders.py +3 -2
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_spec.py +12 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/.github/workflows/build.yml +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/.github/workflows/environments-live.yml +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/.github/workflows/py-code-style.yml +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/.github/workflows/py-tests.yml +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/.github/workflows/py-typing.yml +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/.github/workflows/reusable-python.yml +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/.gitignore +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/.licenserc.yaml +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/.pre-commit-config.yaml +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/.vscode/settings.json +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/CHANGELOG.md +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/Dockerfile +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/LICENSE +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/Makefile +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/README.md +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/__init__.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/base.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/bridge_mount.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/builds.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/cli.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/client.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/cloudflare_sandbox.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/commands.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/console.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/contents.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/coreweave_sandbox.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/docker_sandbox.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/e2b_sandbox.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/__init__.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/__main__.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/accounts.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/adapters/__init__.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/adapters/datalayer.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/adapters/e2b.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/adapters/managed.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/adapters/modal.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/attest.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/breaker.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/build_secrets.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/builders.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/canonical.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/contract.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/doctor/__init__.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/doctor/build.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/doctor/report.schema.json +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/errors.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/files.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/image_import.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/lifecycle.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/policy.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/redact.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/resolve.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/resolve_conda.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/schema.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/eval_sandbox.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/exceptions.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/filesystem.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/google_colab.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/interfaces.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/jupyter_server_sandbox.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/kaggle.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/kaggle_execute.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/kaggle_live.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/kaggle_sandbox.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/lifecycle.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/manage.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/modal_sandbox.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/models.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/monty_sandbox.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/provider_ingress.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/providers.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docker/Dockerfile +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/.gitignore +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/LICENSE +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/Makefile +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/README.md +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/babel.config.js +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/api-reference/_category_.yml +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/api-reference/index.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/cli/_category_.yml +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/cli/index.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/contribute/_category_.yml +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/contribute/index.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/environments/_category_.yml +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/environments/contract.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/environments/index.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/environments/spike-2026-09.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/examples/_category_.yml +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/examples/index.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/guide/_category_.yml +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/guide/index.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/index.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/install/_category_.yml +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/install/index.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/provider-ingress.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/providers/_category_.yml +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/providers/cloudflare.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/providers/coreweave.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/providers/datalayer.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/providers/docker.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/providers/e2b.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/providers/eval.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/providers/google-colab.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/providers/index.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/providers/jupyter-server.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/providers/kaggle.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/providers/modal.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docs/providers/monty.mdx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/docusaurus.config.js +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/package.json +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/sidebars.js +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/src/components/HomepageRow1.js +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/src/components/HomepageRow1.module.css +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/src/components/HomepageRow2.js +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/src/components/HomepageRow2.module.css +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/src/components/HomepageRow3.js +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/src/components/HomepageRow3.module.css +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/src/css/custom.css +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/src/pages/index.module.css +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/src/pages/markdown-page.md +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/src/theme/CustomDocItem.tsx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/src/theme/IconsTitle.tsx +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/bricks.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/building-construction.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/cloud-sun.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/cloud.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/collaboration.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/copy-left.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/datalayer/logo.png +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/datalayer/logo.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/datalayer/personas/business.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/datalayer/personas/devops.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/datalayer/project/project-line.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/favicon.ico +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/jupyter.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/open-source.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/react-js.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/react-js2.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/ringed-planet.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/robot.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/rocket.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/shield.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/sparkles.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/streamlit.gif +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/super-hero.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/target.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/docs/static/img/thumbs-up.svg +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/README.md +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/exec/Makefile +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/exec/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/exec/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/exec/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/exec/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/exec/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/exec/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/exec/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/exec/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/exec/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/exec/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/exec/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/exec/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/repl/Makefile +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/repl/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/repl/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/repl/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/repl/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/repl/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/repl/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/repl/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/repl/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/repl/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/repl/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/repl/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/examples/repl/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/pyproject.toml +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/schemas/environment-v1alpha1.json +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/__init__.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/conftest.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_bridge_mount.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_builds.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_cli_exec.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_cli_repl.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_client.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_client_contents.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_client_files.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_client_outcome_error_output.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_cloudflare.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_commands_envelope.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_console.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_contents_contract.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_contents_live_matrix.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_coreweave.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_e2b.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_accounts.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_attest.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_breaker.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_build_secrets.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_builders.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_canonical.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_constraints.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_contract.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_cross_variant_report.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_datalayer_builder.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_dockerfile_source.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_doctor.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_e2b_builder.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_errors.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_image_import.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_lifecycle.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_live_matrix.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_modal_builder.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_organization_policy.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_redact.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_resolve.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_resolve_conda.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_environment_spec_organization_policy.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_eval.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_factory.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_google_colab.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_integration.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_jupyter_server.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_jupyter_server_streaming.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_kaggle.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_kaggle_execute.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_kaggle_live.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_kaggle_session.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_kernel_client_compatibility.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_lifecycle_vocabulary.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_manage.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_manage_datalayer.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_modal_google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_modal_session.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_models.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_output_line_fidelity.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_provider_catalog_names.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_provider_ingress.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_providers.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_sandboxes.py +0 -0
- {code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/tests/test_the_kill_relaunch_drill.py +0 -0
|
@@ -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
|
{code_sandboxes-1.9.31 → code_sandboxes-1.9.32}/code_sandboxes/environments/adapters/daytona.py
RENAMED
|
@@ -69,18 +69,13 @@ 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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
"""
|
|
@@ -118,10 +113,10 @@ from ..resolve_conda import (
|
|
|
118
113
|
micromamba_bootstrap_command,
|
|
119
114
|
micromamba_install_command,
|
|
120
115
|
)
|
|
121
|
-
from ..spec import GPU_SIZE_CLASSES, Environment
|
|
116
|
+
from ..spec import GPU_SIZE_CLASSES, Environment, EnvironmentSpec
|
|
122
117
|
from .managed import ManagedBuilder
|
|
123
118
|
|
|
124
|
-
__all__ = ["Builder"]
|
|
119
|
+
__all__ = ["DAYTONA_GPUS", "Builder", "daytona_gpu"]
|
|
125
120
|
|
|
126
121
|
#: Tags Daytona refuses for a snapshot's source image: each moves.
|
|
127
122
|
MOVING_TAGS = ("latest", "lts", "stable")
|
|
@@ -148,6 +143,26 @@ _CPU_RESOURCES: dict[str, dict[str, int]] = {
|
|
|
148
143
|
}
|
|
149
144
|
|
|
150
145
|
|
|
146
|
+
#: The GPUs Daytona offers, as its SDK's `GpuType` names them (E2-17). Spelled
|
|
147
|
+
#: out rather than read from the SDK, because `validate` runs where the SDK may
|
|
148
|
+
#: not be installed; a test holds the two together.
|
|
149
|
+
DAYTONA_GPUS: tuple[str, ...] = ("H100", "H200", "RTX-PRO-6000", "RTX-4090", "RTX-5090")
|
|
150
|
+
|
|
151
|
+
#: A GPU snapshot's disk when the spec gives no hint: the CUDA base alone is
|
|
152
|
+
#: about 15 GB, and Daytona's own default would not hold it with room to work.
|
|
153
|
+
_GPU_DISK_GI = 50
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def daytona_gpu(accelerator_type: str) -> str | None:
|
|
157
|
+
"""Daytona's name for an accelerator type, or `None` when Daytona has no such GPU.
|
|
158
|
+
|
|
159
|
+
`h100`, `H100` and `rtx_4090` name what Daytona calls `H100` and
|
|
160
|
+
`RTX-4090`; a `T4` or an `A100-80GB` is Modal's vocabulary, not Daytona's.
|
|
161
|
+
"""
|
|
162
|
+
name = accelerator_type.strip().upper().replace("_", "-")
|
|
163
|
+
return name if name in DAYTONA_GPUS else None
|
|
164
|
+
|
|
165
|
+
|
|
151
166
|
def _pip_lock_command(*, authored: bool) -> str:
|
|
152
167
|
"""Install the pip lock: `sync` to it, or `install` it over an authored Dockerfile.
|
|
153
168
|
|
|
@@ -204,8 +219,8 @@ class Builder(ManagedBuilder):
|
|
|
204
219
|
#: 2026-09-17.
|
|
205
220
|
forbidden_instructions = ()
|
|
206
221
|
dependency_formats = ("requirements", "pyproject", "conda")
|
|
207
|
-
#: Daytona runs GPUs, on its own hardware and the owner's account (E2-17)
|
|
208
|
-
#:
|
|
222
|
+
#: Daytona runs GPUs, on its own hardware and the owner's account (E2-17),
|
|
223
|
+
#: baked into the snapshot with the rest of its resources.
|
|
209
224
|
gpu = True
|
|
210
225
|
#: E0-04's spike found only a registry login for the private base, never
|
|
211
226
|
#: a per-step arbitrary named secret (E3-05): `buildSecrets` is refused.
|
|
@@ -300,15 +315,7 @@ class Builder(ManagedBuilder):
|
|
|
300
315
|
field="spec.compatibility.regions",
|
|
301
316
|
)
|
|
302
317
|
)
|
|
303
|
-
|
|
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
|
-
#
|
|
318
|
+
findings.extend(self._accelerator_findings(environment.spec))
|
|
312
319
|
# `spec.buildSecrets` needs no check of its own here: `supports_build_secrets
|
|
313
320
|
# = False` above (E3-05, merged since this branch started) makes
|
|
314
321
|
# `ManagedBuilder._own_findings` refuse it before this method is
|
|
@@ -330,18 +337,6 @@ class Builder(ManagedBuilder):
|
|
|
330
337
|
(E0-04).
|
|
331
338
|
"""
|
|
332
339
|
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
340
|
sdk = self._daytona_sdk()
|
|
346
341
|
client = self._client(sdk)
|
|
347
342
|
name = f"dl-{request.environment.metadata.name}-v{request.version}-{request.build_uid}"
|
|
@@ -437,7 +432,7 @@ class Builder(ManagedBuilder):
|
|
|
437
432
|
logged.append(line)
|
|
438
433
|
self._log(line)
|
|
439
434
|
|
|
440
|
-
resources = self._resources(sdk, request.size_class)
|
|
435
|
+
resources = self._resources(sdk, request.size_class, spec)
|
|
441
436
|
# A snapshot is region-scoped, and the region that scopes it is
|
|
442
437
|
# Daytona's, not this platform's. `validate` already refuses
|
|
443
438
|
# more than one, so the first is the only one.
|
|
@@ -505,10 +500,40 @@ class Builder(ManagedBuilder):
|
|
|
505
500
|
)
|
|
506
501
|
return sdk.Image.from_dockerfile(str(dockerfile))
|
|
507
502
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
503
|
+
@staticmethod
|
|
504
|
+
def _accelerator_findings(spec: EnvironmentSpec) -> list[CapabilityFinding]:
|
|
505
|
+
"""A GPU Daytona does not offer, refused before any build (E2-17)."""
|
|
506
|
+
accelerator = spec.resources.accelerator
|
|
507
|
+
if accelerator == "none" or daytona_gpu(accelerator.type):
|
|
508
|
+
return []
|
|
509
|
+
return [
|
|
510
|
+
CapabilityFinding(
|
|
511
|
+
code="DL_ENV_CAPABILITY_UNSUPPORTED",
|
|
512
|
+
message=(
|
|
513
|
+
f"Daytona has no GPU called `{accelerator.type}`; it offers "
|
|
514
|
+
+ ", ".join(DAYTONA_GPUS)
|
|
515
|
+
),
|
|
516
|
+
field="spec.resources.accelerator.type",
|
|
517
|
+
)
|
|
518
|
+
]
|
|
519
|
+
|
|
520
|
+
def _resources(self, sdk: Any, size_class: str, spec: EnvironmentSpec) -> Any:
|
|
521
|
+
"""What the snapshot bakes in: a class's CPU, or the spec's GPU (§11.3 item 4, E2-17)."""
|
|
522
|
+
accelerator = spec.resources.accelerator
|
|
523
|
+
if size_class not in GPU_SIZE_CLASSES or accelerator == "none":
|
|
524
|
+
shape = _CPU_RESOURCES.get(size_class, _CPU_RESOURCES["small"])
|
|
525
|
+
return sdk.Resources(cpu=shape["cpu"], memory=shape["memory"], disk=shape["disk"])
|
|
526
|
+
# D-4 gives the GPU classes no CPU or memory of their own (E4-11 has
|
|
527
|
+
# them, for Datalayer's nodes): the GPU is what the class is for, and
|
|
528
|
+
# Daytona sizes the rest of the machine for it unless the spec hints.
|
|
529
|
+
hints = spec.resources.hints
|
|
530
|
+
return sdk.Resources(
|
|
531
|
+
cpu=round(hints.cpu) if hints.cpu else None,
|
|
532
|
+
memory=round(hints.memory_gi) if hints.memory_gi else None,
|
|
533
|
+
disk=round(hints.disk_gi) if hints.disk_gi else _GPU_DISK_GI,
|
|
534
|
+
gpu=accelerator.count,
|
|
535
|
+
gpu_type=sdk.GpuType(daytona_gpu(accelerator.type)),
|
|
536
|
+
)
|
|
512
537
|
|
|
513
538
|
def _register_base_pull(self, client: Any, resolved_base: str) -> str | None:
|
|
514
539
|
"""A private registry entry for this build's base pull (D-17, D-18).
|
|
@@ -657,20 +682,29 @@ class Builder(ManagedBuilder):
|
|
|
657
682
|
"lock pinned, and an artifact carries neither",
|
|
658
683
|
detail={"variant": self.variant},
|
|
659
684
|
)
|
|
660
|
-
from ..conformance import expected_packages, run_core_tier
|
|
685
|
+
from ..conformance import expected_packages, run_accelerator_check, run_core_tier
|
|
661
686
|
|
|
662
687
|
snapshot = artifact.provider_artifact_id or artifact.immutable_reference
|
|
663
688
|
sandbox = self._smoke_test_sandbox(snapshot)
|
|
664
689
|
self._log(f"Launching {snapshot} to smoke-test it")
|
|
665
690
|
try:
|
|
666
691
|
sandbox.start()
|
|
667
|
-
|
|
692
|
+
result = run_core_tier(
|
|
668
693
|
sandbox,
|
|
669
694
|
python_version=environment.spec.language.version,
|
|
670
695
|
expected_packages=expected_packages(environment, lock_text or ""),
|
|
671
696
|
secret_values=tuple(secret_values),
|
|
672
697
|
restart=lambda: self._restart(sandbox),
|
|
673
698
|
)
|
|
699
|
+
accelerator = environment.spec.resources.accelerator
|
|
700
|
+
if accelerator != "none":
|
|
701
|
+
# A GPU version is only the version its spec describes when
|
|
702
|
+
# its GPUs are visible and its CUDA is the spec's (check 11,
|
|
703
|
+
# E2-17): the core tier alone passes on a machine with none.
|
|
704
|
+
result.checks.append(
|
|
705
|
+
run_accelerator_check(sandbox, cuda=accelerator.cuda, count=accelerator.count)
|
|
706
|
+
)
|
|
707
|
+
return result
|
|
674
708
|
except EnvironmentsError:
|
|
675
709
|
raise
|
|
676
710
|
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
|
|
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
|
-
|
|
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(
|
|
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['
|
|
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
|
-
|
|
645
|
+
gpus = answer.get("gpus") or []
|
|
646
|
+
if answer.get("returncode") != 0 or not gpus:
|
|
620
647
|
problems.append("no GPU is visible")
|
|
621
|
-
|
|
622
|
-
problems.append(f"
|
|
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`
|
|
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
|
|
|
@@ -25,14 +25,25 @@ DIGEST = "sha256:" + "a" * 64
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
@pytest.mark.parametrize("variant", VARIANTS)
|
|
28
|
-
def
|
|
28
|
+
def test_the_2026_09_channel_of_python_cuda_resolves_the_digest_its_release_pushed(
|
|
29
29
|
variant: str,
|
|
30
30
|
) -> None:
|
|
31
|
-
"""PLAN_ENV.md, E2-17:
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
"""PLAN_ENV.md, E2-17: released 2026-09-18, one image for every variant,
|
|
32
|
+
carrying the CUDA 12.8 toolkit a spec's `accelerator.cuda` may ask for."""
|
|
33
|
+
base = APPROVED_BASES["datalayer/python-cuda"]
|
|
34
|
+
assert resolve_base(base.ref, "2026.09", variant) == (
|
|
35
|
+
"sha256:a6374a2d4ff07c8a8fe0a71ee605964f93319894cc4152c4e2a1af6258ac9e6a"
|
|
36
|
+
)
|
|
37
|
+
assert (base.accelerator, base.cuda) == (True, "12.8")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize("variant", VARIANTS)
|
|
41
|
+
def test_a_channel_with_no_digest_yet_is_refused_rather_than_made_up(variant: str) -> None:
|
|
42
|
+
"""A channel is approved before its release pushes it: nothing may be made up."""
|
|
43
|
+
ref = "datalayer/python-next"
|
|
44
|
+
bases = {ref: ApprovedBase(ref=ref, python_versions=("3.13",), channels={"2026.10": {}})}
|
|
34
45
|
with pytest.raises(BaseChannelUnpublishedError) as refused:
|
|
35
|
-
resolve_base(ref, "2026.
|
|
46
|
+
resolve_base(ref, "2026.10", variant, bases)
|
|
36
47
|
error = refused.value
|
|
37
48
|
assert isinstance(error, EnvironmentsError)
|
|
38
49
|
assert error.code is errors.ARTIFACT_MISSING
|
|
@@ -40,9 +51,9 @@ def test_the_2026_09_channel_of_python_cuda_has_no_digest_until_it_is_pushed(
|
|
|
40
51
|
assert error.detail == {
|
|
41
52
|
"reason": "base_channel_unpublished",
|
|
42
53
|
"base": ref,
|
|
43
|
-
"channel": "2026.
|
|
54
|
+
"channel": "2026.10",
|
|
44
55
|
"variant": variant,
|
|
45
|
-
"repository": "environments/base/"
|
|
56
|
+
"repository": "environments/base/python-next",
|
|
46
57
|
}
|
|
47
58
|
assert "sha256:" not in str(error)
|
|
48
59
|
assert error.to_body()["code"] == "DL_ENV_ARTIFACT_MISSING"
|
|
@@ -127,7 +138,7 @@ def test_the_2026_09_channel_of_python_cpu_pins_apt_to_its_snapshot() -> None:
|
|
|
127
138
|
way — left on 2026-09-14's id after the channel moved to 2026-09-16's.
|
|
128
139
|
"""
|
|
129
140
|
assert channel_snapshot("datalayer/python-cpu", "2026.09") == "20260916T120000Z"
|
|
130
|
-
assert channel_snapshot("datalayer/python-cuda", "2026.09") == ""
|
|
141
|
+
assert channel_snapshot("datalayer/python-cuda", "2026.09") == "20260918T150000Z"
|
|
131
142
|
assert channel_snapshot("datalayer/nothing", "2026.09") == ""
|
|
132
143
|
|
|
133
144
|
|
|
@@ -303,6 +303,48 @@ def test_check_eleven_gates_when_a_gpu_version_sees_its_gpu() -> None:
|
|
|
303
303
|
assert gpu.gating and gpu.passed and result.passed
|
|
304
304
|
|
|
305
305
|
|
|
306
|
+
def test_check_eleven_reads_the_images_cuda_not_the_drivers() -> None:
|
|
307
|
+
"""`nvidia-smi` says the newest CUDA the host's driver runs, which the image
|
|
308
|
+
does not choose: a 12.8 image on a 13.0 driver is what its spec asked for
|
|
309
|
+
(found on 2026-09-18, E2-17)."""
|
|
310
|
+
sandbox = ScriptedSandbox(
|
|
311
|
+
{11: {"returncode": 0, "gpus": ["H100"], "driver": "13.0", "cuda": "12.8"}}
|
|
312
|
+
)
|
|
313
|
+
result = run_extended_tier(sandbox, accelerator_requested=True, cuda_version="12.8")
|
|
314
|
+
assert by_id(result, 11).passed, by_id(result, 11).detail
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def test_check_eleven_refuses_a_driver_older_than_the_image() -> None:
|
|
318
|
+
sandbox = ScriptedSandbox(
|
|
319
|
+
{11: {"returncode": 0, "gpus": ["H100"], "driver": "12.4", "cuda": "12.8"}}
|
|
320
|
+
)
|
|
321
|
+
gpu = by_id(run_extended_tier(sandbox, accelerator_requested=True), 11)
|
|
322
|
+
assert not gpu.passed
|
|
323
|
+
assert "the driver runs CUDA up to 12.4, older than the image's 12.8" in gpu.detail
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def test_check_eleven_counts_the_gpus_asked_for() -> None:
|
|
327
|
+
sandbox = ScriptedSandbox({11: {"returncode": 0, "gpus": ["H100"], "cuda": "12.8"}})
|
|
328
|
+
gpu = by_id(run_extended_tier(sandbox, accelerator_requested=True, accelerator_count=2), 11)
|
|
329
|
+
assert not gpu.passed
|
|
330
|
+
assert "1 GPU(s) visible, not the 2 asked for" in gpu.detail
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def test_check_eleven_alone_is_what_a_smoke_test_adds() -> None:
|
|
334
|
+
from code_sandboxes.environments.conformance import run_accelerator_check
|
|
335
|
+
|
|
336
|
+
passed = run_accelerator_check(
|
|
337
|
+
ScriptedSandbox({11: {"returncode": 0, "gpus": ["H100", "H100"], "cuda": "12.8.93"}}),
|
|
338
|
+
cuda="12.8",
|
|
339
|
+
count=2,
|
|
340
|
+
)
|
|
341
|
+
assert passed.id == "conformance:11" and passed.gating and passed.passed
|
|
342
|
+
failed = run_accelerator_check(
|
|
343
|
+
ScriptedSandbox({11: {"returncode": 9, "gpus": []}}), cuda="12.8"
|
|
344
|
+
)
|
|
345
|
+
assert failed.gating and not failed.passed
|
|
346
|
+
|
|
347
|
+
|
|
306
348
|
def test_a_cpu_version_never_gates_on_the_gpu_check() -> None:
|
|
307
349
|
"""No accelerator asked for: check 11 is the trivial recorded pass, and
|
|
308
350
|
the extended tier still gates nothing."""
|