modAL 1.6.3.dev5__tar.gz → 1.6.3.dev6__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.
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/PKG-INFO +1 -1
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/grpc_testing.py +3 -1
- modal-1.6.3.dev6/modal/builder/PREVIEW.txt +18 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/client.pyi +2 -2
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/_common.py +17 -2
- modal-1.6.3.dev6/modal/sandbox/_impl_persistent.py +470 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/_impl_v1.py +10 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/_impl_v2.py +10 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/_sandbox.py +21 -3
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/sandbox.pyi +38 -2
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal.egg-info/SOURCES.txt +1 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_version/__init__.py +1 -1
- modal-1.6.3.dev5/modal/builder/PREVIEW.txt +0 -21
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/LICENSE +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/README.md +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/__init__.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/__main__.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_billing.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_cluster.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_cluster.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_container_entrypoint.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_environments.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_function_variants.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_functions.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_grpc_client.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_image.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_ipython.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_load_context.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_location.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_logs.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_logs_manager.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_logs_manager.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_metrics.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_metrics.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_object.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_outbound_policy.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_outbound_policy.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_output/__init__.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_output/manager.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_output/pty.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_output/rich.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_output/status.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_partial_function.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_resolver.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_resources.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/__init__.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/asgi.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/execution_context.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/telemetry.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_serialization.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_server.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_server.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_supports_logs.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_traceback.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_tunnel.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_tunnel.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_type_manager.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/__init__.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/app_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/async_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/blob_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/browser_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/curl_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/deprecation.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/display_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/docker_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/function_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/git_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/hash_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/http_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/idle_countdown.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/logger.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/mount_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/name_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/package_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/proxy_support.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/shell_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/time_utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_vendor/__init__.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_vendor/tblib.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_vendor/version.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_watcher.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_workspace.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/app.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/app.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/billing.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/billing.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/builder/2023.12.txt +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/builder/2024.04.txt +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/builder/2024.10.txt +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/builder/2025.06.txt +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/builder/README.md +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/builder/base-images.json +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/call_graph.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/__init__.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/_download.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/_help.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/_logs.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/_stats.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/_traceback.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/app.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/billing.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/changelog.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/config.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/container.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/curl.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/dashboard.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/dict.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/endpoint.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/entry_point.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/environment.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/function.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/image.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/import_refs.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/logo.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/network_file_system.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/profile.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/queues.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/run.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/secret.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/selector.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/server.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/shell.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/skills.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/token.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/utils.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/volume.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/workspace.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/client.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cls.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cls.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cluster.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cluster.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/config.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/container_process.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/container_process.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/dict.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/dict.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/environments.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/environments.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/exception.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/experimental/__init__.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/experimental/_flash_container_entry.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/experimental/flash.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/experimental/flash.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/experimental/flash_server.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/experimental/flash_server.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/experimental/ipython.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/file_pattern_matcher.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/functions.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/functions.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/image.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/image.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/io_streams.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/io_streams.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/mount.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/mount.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/network_file_system.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/network_file_system.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/object.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/object.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/output.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/parallel_map.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/parallel_map.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/partial_function.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/partial_function.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/proxy.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/proxy.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/py.typed +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/queue.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/queue.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/retries.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/runner.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/runner.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/running_app.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/__init__.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/_filesystem.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/_sidecar.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/_snapshot.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/_task_command_router_client.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/sandbox.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/schedule.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/scheduler_placement.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/secret.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/secret.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/server.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/server.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/serving.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/serving.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/stream_type.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/token_flow.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/token_flow.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/types.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/volume.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/volume.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/workspace.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/workspace.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal.egg-info/requires.txt +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal.egg-info/top_level.txt +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/__init__.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/mdmd/types.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/reference_intro.md +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/__init__.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/api_grpc.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/api_pb2.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/api_pb2.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/py.typed +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_version/__main__.py +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/pyproject.toml +0 -0
- {modal-1.6.3.dev5 → modal-1.6.3.dev6}/setup.cfg +0 -0
|
@@ -116,7 +116,7 @@ class InterceptionContext:
|
|
|
116
116
|
"""Adds one response payload to an expected queue of responses for a method.
|
|
117
117
|
|
|
118
118
|
These responses will be used once each instead of calling the MockServicer's
|
|
119
|
-
implementation of the method.
|
|
119
|
+
implementation of the method. An exception payload is raised instead of being sent.
|
|
120
120
|
|
|
121
121
|
The interception context will throw an exception on exit if not all of the added
|
|
122
122
|
responses have been consumed.
|
|
@@ -184,6 +184,8 @@ class InterceptionContext:
|
|
|
184
184
|
async def responder(servicer_self, stream):
|
|
185
185
|
await stream.recv_message() # get the input message so we can track that
|
|
186
186
|
for msg in next_response_messages:
|
|
187
|
+
if isinstance(msg, Exception):
|
|
188
|
+
raise msg
|
|
187
189
|
await stream.send_message(msg)
|
|
188
190
|
|
|
189
191
|
return responder
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
aiohappyeyeballs==2.7.1
|
|
2
|
+
aiohttp==3.14.5
|
|
3
|
+
aiosignal==1.4.0
|
|
4
|
+
async-timeout==5.0.1 ; python_version < "3.11"
|
|
5
|
+
attrs==26.1.0
|
|
6
|
+
cbor2==5.9.0
|
|
7
|
+
certifi==2026.10.19
|
|
8
|
+
frozenlist==1.8.0
|
|
9
|
+
grpclib==0.4.9
|
|
10
|
+
h2==4.4.1
|
|
11
|
+
hpack==4.2.0
|
|
12
|
+
hyperframe==6.1.0
|
|
13
|
+
idna==3.20
|
|
14
|
+
multidict==7.1.0
|
|
15
|
+
propcache==0.5.4
|
|
16
|
+
protobuf==7.36.2
|
|
17
|
+
typing_extensions==4.16.0
|
|
18
|
+
yarl==1.25.1
|
|
@@ -80,7 +80,7 @@ class _Client:
|
|
|
80
80
|
server_url: str,
|
|
81
81
|
client_type: int,
|
|
82
82
|
credentials: typing.Optional[tuple[str, str]],
|
|
83
|
-
version: str = "1.6.3.
|
|
83
|
+
version: str = "1.6.3.dev6",
|
|
84
84
|
*,
|
|
85
85
|
oauth_credentials: typing.Optional[_OAuthCredentials] = None,
|
|
86
86
|
):
|
|
@@ -290,7 +290,7 @@ class Client:
|
|
|
290
290
|
server_url: str,
|
|
291
291
|
client_type: int,
|
|
292
292
|
credentials: typing.Optional[tuple[str, str]],
|
|
293
|
-
version: str = "1.6.3.
|
|
293
|
+
version: str = "1.6.3.dev6",
|
|
294
294
|
*,
|
|
295
295
|
oauth_credentials: typing.Optional[_OAuthCredentials] = None,
|
|
296
296
|
):
|
|
@@ -386,7 +386,7 @@ class SandboxCreateArgs:
|
|
|
386
386
|
secrets: Collection[_Secret] | None = None
|
|
387
387
|
env: dict[str, str | None] | None = None
|
|
388
388
|
name: str | None = None
|
|
389
|
-
timeout: int = 300
|
|
389
|
+
timeout: int | None = 300
|
|
390
390
|
idle_timeout: int | None = None
|
|
391
391
|
workdir: str | None = None
|
|
392
392
|
gpu: str | None = None
|
|
@@ -417,6 +417,7 @@ class SandboxCreateArgs:
|
|
|
417
417
|
custom_domain: str | None = None
|
|
418
418
|
include_oidc_identity_token: bool = False
|
|
419
419
|
runtime: SandboxRuntime | None = None
|
|
420
|
+
persistent: bool = False
|
|
420
421
|
|
|
421
422
|
|
|
422
423
|
class SandboxCreateCommon:
|
|
@@ -428,6 +429,8 @@ class SandboxCreateCommon:
|
|
|
428
429
|
|
|
429
430
|
def __init__(self, create_args: SandboxCreateArgs, *, secrets: Sequence[_Secret]) -> None:
|
|
430
431
|
a = create_args
|
|
432
|
+
if a.timeout is None:
|
|
433
|
+
raise InvalidError("`timeout=None` is only supported for experimental persistent Sandboxes.")
|
|
431
434
|
_validate_exec_args(a.args)
|
|
432
435
|
if a.name is not None:
|
|
433
436
|
check_object_name(a.name, "Sandbox")
|
|
@@ -452,6 +455,7 @@ class SandboxCreateCommon:
|
|
|
452
455
|
raise InvalidError("`inbound_cidr_allowlist` cannot be used when `block_network` is enabled")
|
|
453
456
|
|
|
454
457
|
self.create_args = a
|
|
458
|
+
self.timeout = a.timeout
|
|
455
459
|
self.secrets = secrets
|
|
456
460
|
self.image = a.image or _default_image
|
|
457
461
|
self.network_file_systems = validate_network_file_systems(a.network_file_systems)
|
|
@@ -503,7 +507,7 @@ class SandboxCreateCommon:
|
|
|
503
507
|
secret_ids=[secret.object_id for secret in self.secrets],
|
|
504
508
|
# Ephemeral disk requests are not supported on Sandboxes.
|
|
505
509
|
resources=convert_fn_config_to_resources_config(cpu=a.cpu, memory=a.memory, gpu=a.gpu, ephemeral_disk=None),
|
|
506
|
-
timeout_secs=
|
|
510
|
+
timeout_secs=self.timeout,
|
|
507
511
|
idle_timeout_secs=a.idle_timeout,
|
|
508
512
|
workdir=a.workdir,
|
|
509
513
|
cloud_provider_str=a.cloud if a.cloud else None, # pyright: ignore[reportArgumentType]
|
|
@@ -637,6 +641,10 @@ class SandboxImpl(Protocol):
|
|
|
637
641
|
"""
|
|
638
642
|
...
|
|
639
643
|
|
|
644
|
+
def clone(self) -> "SandboxImpl":
|
|
645
|
+
"""Create an implementation of the same type for another handle to the same Sandbox."""
|
|
646
|
+
...
|
|
647
|
+
|
|
640
648
|
@classmethod
|
|
641
649
|
async def lookup_by_name(
|
|
642
650
|
cls, client: _Client, req: api_pb2.SandboxGetFromNameRequest
|
|
@@ -674,6 +682,10 @@ class SandboxImpl(Protocol):
|
|
|
674
682
|
|
|
675
683
|
async def terminate(self) -> None: ...
|
|
676
684
|
|
|
685
|
+
async def pause(self) -> None: ...
|
|
686
|
+
|
|
687
|
+
async def resume(self) -> None: ...
|
|
688
|
+
|
|
677
689
|
async def close(self) -> None:
|
|
678
690
|
"""Release connections held for the Sandbox once in-flight operations complete."""
|
|
679
691
|
...
|
|
@@ -907,6 +919,9 @@ async def snapshot_filesystem_via_tcr(
|
|
|
907
919
|
return _Image._new_hydrated(res.image_id, client, None)
|
|
908
920
|
|
|
909
921
|
|
|
922
|
+
_PERSISTENT_ONLY_MESSAGE = "`{}` requires a persistent Sandbox, created with `_experimental_persistent=True`."
|
|
923
|
+
|
|
924
|
+
|
|
910
925
|
async def mount_image_via_tcr(
|
|
911
926
|
impl: SandboxImpl,
|
|
912
927
|
path: PurePosixPath | str,
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
# Copyright Modal Labs 2026
|
|
2
|
+
"""Persistent Sandboxes, managed by the `PersistentSandbox*` control-plane RPCs.
|
|
3
|
+
|
|
4
|
+
A persistent Sandbox runs as a sequence of regular V2 Sandboxes, its sessions. The control plane starts each
|
|
5
|
+
session from the exit snapshot of the previous one.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import copy
|
|
10
|
+
import dataclasses
|
|
11
|
+
import time
|
|
12
|
+
import weakref
|
|
13
|
+
from collections.abc import Awaitable, Callable, Collection, Sequence
|
|
14
|
+
from pathlib import PurePosixPath
|
|
15
|
+
from typing import TYPE_CHECKING, Any, Literal, TypeVar
|
|
16
|
+
|
|
17
|
+
from grpclib import GRPCError, Status
|
|
18
|
+
|
|
19
|
+
from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
|
|
20
|
+
|
|
21
|
+
from .._image import _Image
|
|
22
|
+
from .._outbound_policy import _OutboundPolicy
|
|
23
|
+
from .._resolver import Resolver
|
|
24
|
+
from .._tunnel import Tunnel
|
|
25
|
+
from ..client import _Client
|
|
26
|
+
from ..cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
|
|
27
|
+
from ..config import logger
|
|
28
|
+
from ..container_process import _ContainerProcess
|
|
29
|
+
from ..exception import ClientClosed, InvalidError, SandboxTerminatedError
|
|
30
|
+
from ..io_streams import (
|
|
31
|
+
StreamReader,
|
|
32
|
+
StreamWriter,
|
|
33
|
+
_StreamReaderThroughSandboxCommandRouterParams,
|
|
34
|
+
_StreamWriterThroughCommandRouterSandboxParams,
|
|
35
|
+
)
|
|
36
|
+
from ..secret import _resolvable_secrets, _Secret, _secret_sources
|
|
37
|
+
from ..stream_type import StreamType
|
|
38
|
+
from ._common import SandboxCreateArgs, SandboxCreateCommon, SandboxImpl, command_router, sandbox_create_context
|
|
39
|
+
from ._impl_v2 import SandboxImplV2, auth_metadata
|
|
40
|
+
from ._snapshot import _SandboxSnapshot
|
|
41
|
+
from ._task_command_router_client import TaskCommandRouterClient
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
import modal.app
|
|
45
|
+
|
|
46
|
+
_T = TypeVar("_T")
|
|
47
|
+
|
|
48
|
+
# Errors caused by the caller rather than by the session, which a new session can't fix.
|
|
49
|
+
_NON_RECOVERABLE_ERRORS = (InvalidError, ClientClosed)
|
|
50
|
+
|
|
51
|
+
# Lifetime limits of each session. A session that hits one is paused, and resumed on next use.
|
|
52
|
+
PERSISTENT_SANDBOX_SESSION_TIMEOUT = 60 * 60
|
|
53
|
+
PERSISTENT_SANDBOX_SESSION_IDLE_TIMEOUT = 5 * 60
|
|
54
|
+
|
|
55
|
+
# How long to retry resuming while the previous session is still saving its exit snapshot.
|
|
56
|
+
_RESUME_RETRY_TIMEOUT = 120.0
|
|
57
|
+
_RESUME_RETRY_INTERVAL = 1.0
|
|
58
|
+
|
|
59
|
+
# How often `wait()` checks whether the persistent Sandbox has been terminated.
|
|
60
|
+
_WAIT_POLL_INTERVAL = 1.0
|
|
61
|
+
|
|
62
|
+
_TERMINATED_RESULT = api_pb2.GenericResult(status=api_pb2.GenericResult.GENERIC_STATUS_TERMINATED)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SandboxImplPersistent:
|
|
66
|
+
"""Backs a persistent Sandbox, whose object ID is the ID of its first session.
|
|
67
|
+
|
|
68
|
+
Operations are forwarded to the current session. When one fails and the Sandbox hasn't been terminated
|
|
69
|
+
through this handle, the control plane is asked for the running session, resuming the Sandbox from its
|
|
70
|
+
latest exit snapshot if there is none. The operation is then retried once on the new session.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
client: _Client,
|
|
76
|
+
sandbox_id: str,
|
|
77
|
+
*,
|
|
78
|
+
result: api_pb2.GenericResult | None = None,
|
|
79
|
+
persistent_sandbox_id: str = "",
|
|
80
|
+
session: SandboxImplV2 | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
self.client = client
|
|
83
|
+
self.sandbox_id = sandbox_id
|
|
84
|
+
self.result = result
|
|
85
|
+
self._persistent_sandbox_id = persistent_sandbox_id
|
|
86
|
+
self._session = session if session is not None else SandboxImplV2(client, sandbox_id)
|
|
87
|
+
self._replace_lock: asyncio.Lock | None = None
|
|
88
|
+
self._terminated = result is not None
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def _delegate(self) -> SandboxImplV2:
|
|
92
|
+
return self._session
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def _client(self) -> _Client:
|
|
96
|
+
return self.client
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
async def create(
|
|
100
|
+
cls, create_args: SandboxCreateArgs, *, app: "modal.app._App | None", client: _Client | None
|
|
101
|
+
) -> "tuple[SandboxImpl, api_pb2.SandboxHandleMetadata]":
|
|
102
|
+
if create_args.timeout is not None or create_args.idle_timeout is not None:
|
|
103
|
+
raise InvalidError("Persistent Sandboxes require `timeout=None` and don't support `idle_timeout`.")
|
|
104
|
+
create_args = dataclasses.replace(
|
|
105
|
+
create_args,
|
|
106
|
+
timeout=PERSISTENT_SANDBOX_SESSION_TIMEOUT,
|
|
107
|
+
idle_timeout=PERSISTENT_SANDBOX_SESSION_IDLE_TIMEOUT,
|
|
108
|
+
)
|
|
109
|
+
if create_args.name is not None:
|
|
110
|
+
raise InvalidError("Named persistent Sandboxes are not supported yet.")
|
|
111
|
+
if create_args.gpu is not None or create_args.network_file_systems or create_args.pty_info is not None:
|
|
112
|
+
raise InvalidError("GPUs, network file systems and PTYs are not supported by persistent Sandboxes yet.")
|
|
113
|
+
if create_args.enable_snapshot:
|
|
114
|
+
raise InvalidError("Memory snapshots are not supported by persistent Sandboxes.")
|
|
115
|
+
if create_args.env or create_args.tags:
|
|
116
|
+
raise InvalidError("`env` and `tags` are not supported by persistent Sandboxes yet.")
|
|
117
|
+
|
|
118
|
+
secrets = create_args.secrets or []
|
|
119
|
+
common = SandboxCreateCommon(create_args, secrets=_resolvable_secrets(secrets))
|
|
120
|
+
async with sandbox_create_context(app, client) as (client, load_context):
|
|
121
|
+
await common.load_dependencies(Resolver(), load_context, load_ephemeral_bucket_secrets=False)
|
|
122
|
+
cloud_bucket_mount_protos, cloud_bucket_credentials = cloud_bucket_mounts_to_proto(
|
|
123
|
+
common.cloud_bucket_mounts, split_ephemeral_credentials=True
|
|
124
|
+
)
|
|
125
|
+
if cloud_bucket_credentials or any(source.HasField("env") for source in _secret_sources(secrets)):
|
|
126
|
+
raise InvalidError("Ephemeral credentials are not supported by persistent Sandboxes yet.")
|
|
127
|
+
req = api_pb2.PersistentSandboxCreateRequest(
|
|
128
|
+
app_id=load_context.app_id or "",
|
|
129
|
+
definition=common.definition(cloud_bucket_mounts=cloud_bucket_mount_protos),
|
|
130
|
+
)
|
|
131
|
+
resp = await client._stub.PersistentSandboxCreate(req, metadata=await auth_metadata(client))
|
|
132
|
+
app_id = load_context.app_id or ""
|
|
133
|
+
impl = cls(client, resp.sandbox_id, persistent_sandbox_id=resp.persistent_sandbox_id)
|
|
134
|
+
return impl, api_pb2.SandboxHandleMetadata(app_id=app_id)
|
|
135
|
+
|
|
136
|
+
# Persistent Sandboxes are only created through `create`, and can't be looked up yet.
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
async def lookup_by_name(cls, *args: Any, **kwargs: Any) -> api_pb2.SandboxGetFromNameResponse:
|
|
140
|
+
raise NotImplementedError
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
async def lookup_by_id(cls, *args: Any, **kwargs: Any) -> api_pb2.SandboxWaitResponse:
|
|
144
|
+
raise NotImplementedError
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
async def list_page(cls, *args: Any, **kwargs: Any) -> api_pb2.SandboxListResponse:
|
|
148
|
+
raise NotImplementedError
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
async def restore_from_memory_snapshot(
|
|
152
|
+
cls, *args: Any, **kwargs: Any
|
|
153
|
+
) -> "tuple[SandboxImpl, api_pb2.SandboxHandleMetadata]":
|
|
154
|
+
raise NotImplementedError
|
|
155
|
+
|
|
156
|
+
def clone(self) -> "SandboxImplPersistent":
|
|
157
|
+
return copy.copy(self)
|
|
158
|
+
|
|
159
|
+
def make_stdio(self, check_open: Callable[[], None]) -> tuple[StreamReader[str], StreamReader[str], StreamWriter]:
|
|
160
|
+
"""Create stdio streams of the entrypoint of whichever session is current when they connect."""
|
|
161
|
+
weak_self = weakref.ref(self)
|
|
162
|
+
|
|
163
|
+
async def resolve_router() -> tuple[str, TaskCommandRouterClient]:
|
|
164
|
+
this = weak_self()
|
|
165
|
+
if this is None:
|
|
166
|
+
raise RuntimeError("Sandbox was garbage collected before its stdio reader connected")
|
|
167
|
+
return await command_router(this)
|
|
168
|
+
|
|
169
|
+
def reader(file_descriptor: "api_pb2.FileDescriptor.ValueType") -> StreamReader[str]:
|
|
170
|
+
return StreamReader(
|
|
171
|
+
_StreamReaderThroughSandboxCommandRouterParams(
|
|
172
|
+
file_descriptor=file_descriptor, sandbox_id=self.sandbox_id, resolve_router=resolve_router
|
|
173
|
+
),
|
|
174
|
+
by_line=True,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
stdin = StreamWriter(
|
|
178
|
+
_StreamWriterThroughCommandRouterSandboxParams(resolve_router=resolve_router, check_open=check_open)
|
|
179
|
+
)
|
|
180
|
+
return reader(api_pb2.FILE_DESCRIPTOR_STDOUT), reader(api_pb2.FILE_DESCRIPTOR_STDERR), stdin
|
|
181
|
+
|
|
182
|
+
# Session replacement
|
|
183
|
+
|
|
184
|
+
async def _run(self, op: Callable[[SandboxImplV2], Awaitable[_T]]) -> _T:
|
|
185
|
+
"""Run `op` on the session, switching to the running session and retrying once if it fails."""
|
|
186
|
+
session = self._delegate
|
|
187
|
+
try:
|
|
188
|
+
return await op(session)
|
|
189
|
+
except _NON_RECOVERABLE_ERRORS:
|
|
190
|
+
raise
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
if self._terminated:
|
|
193
|
+
# A terminated persistent Sandbox is stopped for good, so it isn't resumed on use.
|
|
194
|
+
raise
|
|
195
|
+
replacement = await self._replace_session(session)
|
|
196
|
+
if replacement is None:
|
|
197
|
+
raise
|
|
198
|
+
logger.debug(f"Retrying operation on persistent Sandbox {self._persistent_sandbox_id} after: {exc!r}")
|
|
199
|
+
return await op(replacement)
|
|
200
|
+
|
|
201
|
+
async def _get_running_session_id(self) -> str:
|
|
202
|
+
req = api_pb2.PersistentSandboxGetRequest(persistent_sandbox_id=self._persistent_sandbox_id)
|
|
203
|
+
resp = await self._client._stub.PersistentSandboxGet(req, metadata=await auth_metadata(self._client))
|
|
204
|
+
return resp.sandbox_id
|
|
205
|
+
|
|
206
|
+
async def _resume_session(self) -> str:
|
|
207
|
+
"""Resume the persistent Sandbox, returning the ID of its running session."""
|
|
208
|
+
req = api_pb2.PersistentSandboxResumeRequest(persistent_sandbox_id=self._persistent_sandbox_id)
|
|
209
|
+
deadline = time.monotonic() + _RESUME_RETRY_TIMEOUT
|
|
210
|
+
while True:
|
|
211
|
+
try:
|
|
212
|
+
resp = await self._client._stub.PersistentSandboxResume(req, metadata=await auth_metadata(self._client))
|
|
213
|
+
return resp.sandbox_id
|
|
214
|
+
except GRPCError as exc:
|
|
215
|
+
# UNAVAILABLE means the previous session is still pausing or another resume is in progress.
|
|
216
|
+
if exc.status != Status.UNAVAILABLE or time.monotonic() > deadline:
|
|
217
|
+
raise
|
|
218
|
+
await asyncio.sleep(_RESUME_RETRY_INTERVAL)
|
|
219
|
+
|
|
220
|
+
async def _replace_session(self, failed: SandboxImplV2 | None, *, resume: bool = True) -> SandboxImplV2 | None:
|
|
221
|
+
"""Switch from the `failed` session, or return None if it is still the running session."""
|
|
222
|
+
# Lazily create the lock on the event loop that first uses the Sandbox.
|
|
223
|
+
if self._replace_lock is None:
|
|
224
|
+
self._replace_lock = asyncio.Lock()
|
|
225
|
+
async with self._replace_lock:
|
|
226
|
+
if self._session is not failed:
|
|
227
|
+
# A concurrent operation has already replaced the session.
|
|
228
|
+
return self._session
|
|
229
|
+
|
|
230
|
+
session_id = await self._get_running_session_id()
|
|
231
|
+
if failed is not None and session_id == failed.sandbox_id:
|
|
232
|
+
return None
|
|
233
|
+
if not session_id:
|
|
234
|
+
if not resume:
|
|
235
|
+
return None
|
|
236
|
+
session_id = await self._resume_session()
|
|
237
|
+
logger.debug(f"Switching persistent Sandbox {self._persistent_sandbox_id} to session {session_id}")
|
|
238
|
+
replacement = SandboxImplV2(self.client, session_id)
|
|
239
|
+
self._session = replacement
|
|
240
|
+
if failed is not None:
|
|
241
|
+
await failed.close()
|
|
242
|
+
return replacement
|
|
243
|
+
|
|
244
|
+
# Operations
|
|
245
|
+
|
|
246
|
+
async def close(self) -> None:
|
|
247
|
+
await self._session.close()
|
|
248
|
+
|
|
249
|
+
def _set_terminated_result(self) -> None:
|
|
250
|
+
self.result = _TERMINATED_RESULT
|
|
251
|
+
|
|
252
|
+
async def wait(self, raise_on_termination: bool) -> None:
|
|
253
|
+
# A persistent Sandbox keeps running across sessions, until it is terminated.
|
|
254
|
+
while not self._terminated:
|
|
255
|
+
await asyncio.sleep(_WAIT_POLL_INTERVAL)
|
|
256
|
+
self._set_terminated_result()
|
|
257
|
+
if raise_on_termination:
|
|
258
|
+
raise SandboxTerminatedError()
|
|
259
|
+
|
|
260
|
+
async def poll(self) -> None:
|
|
261
|
+
if self._terminated:
|
|
262
|
+
self._set_terminated_result()
|
|
263
|
+
else:
|
|
264
|
+
self.result = None
|
|
265
|
+
|
|
266
|
+
async def terminate(self) -> None:
|
|
267
|
+
req = api_pb2.PersistentSandboxTerminateRequest(persistent_sandbox_id=self._persistent_sandbox_id)
|
|
268
|
+
await self._client._stub.PersistentSandboxTerminate(req, metadata=await auth_metadata(self._client))
|
|
269
|
+
self._terminated = True
|
|
270
|
+
self._set_terminated_result()
|
|
271
|
+
|
|
272
|
+
async def pause(self) -> None:
|
|
273
|
+
"""Stop the current session, which saves its exit snapshot for the next `resume`."""
|
|
274
|
+
self._check_not_terminated()
|
|
275
|
+
session = self._delegate
|
|
276
|
+
await session.terminate()
|
|
277
|
+
await session.wait(raise_on_termination=False)
|
|
278
|
+
|
|
279
|
+
async def resume(self) -> None:
|
|
280
|
+
"""Start a session from the latest exit snapshot, unless one is already running."""
|
|
281
|
+
self._check_not_terminated()
|
|
282
|
+
session = self._delegate
|
|
283
|
+
session_id = await self._resume_session()
|
|
284
|
+
if session_id != session.sandbox_id:
|
|
285
|
+
self._session = SandboxImplV2(self.client, session_id)
|
|
286
|
+
await session.close()
|
|
287
|
+
|
|
288
|
+
def _check_not_terminated(self) -> None:
|
|
289
|
+
if self._terminated:
|
|
290
|
+
raise InvalidError(f"Persistent Sandbox {self._persistent_sandbox_id} has been terminated.")
|
|
291
|
+
|
|
292
|
+
async def tunnels(self, timeout: int) -> dict[int, Tunnel]:
|
|
293
|
+
return await self._run(lambda d: d.tunnels(timeout))
|
|
294
|
+
|
|
295
|
+
async def get_task_id(
|
|
296
|
+
self, raise_if_task_complete: bool = False, timeout: float | None = None, retry_transient: bool = False
|
|
297
|
+
) -> str:
|
|
298
|
+
return await self._run(
|
|
299
|
+
lambda d: d.get_task_id(
|
|
300
|
+
raise_if_task_complete=raise_if_task_complete, timeout=timeout, retry_transient=retry_transient
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
async def resolve_task_id_for_logs(self) -> str:
|
|
305
|
+
return await self._delegate.resolve_task_id_for_logs()
|
|
306
|
+
|
|
307
|
+
async def get_exit_snapshot(self, timeout: float | None) -> _Image:
|
|
308
|
+
return await self._delegate.get_exit_snapshot(timeout)
|
|
309
|
+
|
|
310
|
+
async def snapshot_memory(self) -> _SandboxSnapshot:
|
|
311
|
+
raise NotImplementedError("Memory snapshots aren't supported for persistent Sandboxes.")
|
|
312
|
+
|
|
313
|
+
async def get_command_router_client(self, task_id: str) -> TaskCommandRouterClient:
|
|
314
|
+
# The task ID belongs to the current delegate, so this can't be retried on a replacement.
|
|
315
|
+
return await self._delegate.get_command_router_client(task_id)
|
|
316
|
+
|
|
317
|
+
async def get_tags(self) -> dict[str, str]:
|
|
318
|
+
raise NotImplementedError("Tags aren't supported for persistent Sandboxes yet.")
|
|
319
|
+
|
|
320
|
+
async def set_tags(self, tags: dict[str, str]) -> None:
|
|
321
|
+
raise NotImplementedError("Tags aren't supported for persistent Sandboxes yet.")
|
|
322
|
+
|
|
323
|
+
async def set_name(self, name: str) -> None:
|
|
324
|
+
raise InvalidError("The name of a persistent Sandbox can't be changed.")
|
|
325
|
+
|
|
326
|
+
async def create_connect_token(
|
|
327
|
+
self, req: api_pb2.SandboxCreateConnectTokenRequest
|
|
328
|
+
) -> api_pb2.SandboxCreateConnectTokenResponse:
|
|
329
|
+
def create(d: SandboxImplV2) -> Awaitable[api_pb2.SandboxCreateConnectTokenResponse]:
|
|
330
|
+
delegate_req = api_pb2.SandboxCreateConnectTokenRequest()
|
|
331
|
+
delegate_req.CopyFrom(req)
|
|
332
|
+
delegate_req.sandbox_id = d.sandbox_id
|
|
333
|
+
return d.create_connect_token(delegate_req)
|
|
334
|
+
|
|
335
|
+
return await self._run(create)
|
|
336
|
+
|
|
337
|
+
async def wait_until_ready(self, timeout: int) -> None:
|
|
338
|
+
await self._run(lambda d: d.wait_until_ready(timeout))
|
|
339
|
+
|
|
340
|
+
async def set_network_access(self, network_access: api_pb2.NetworkAccess) -> None:
|
|
341
|
+
raise NotImplementedError("Updating network access isn't supported for persistent Sandboxes yet.")
|
|
342
|
+
|
|
343
|
+
async def update_outbound_policy(self, outbound_policy: _OutboundPolicy) -> None:
|
|
344
|
+
raise NotImplementedError("Updating the outbound policy isn't supported for persistent Sandboxes yet.")
|
|
345
|
+
|
|
346
|
+
async def snapshot_filesystem(self, timeout: int, *, ttl: int | None, container_id: str = "") -> _Image:
|
|
347
|
+
return await self._run(lambda d: d.snapshot_filesystem(timeout, ttl=ttl, container_id=container_id))
|
|
348
|
+
|
|
349
|
+
async def mount_image(
|
|
350
|
+
self,
|
|
351
|
+
path: PurePosixPath | str,
|
|
352
|
+
image_id: str,
|
|
353
|
+
*,
|
|
354
|
+
encryption_key: bytes | None,
|
|
355
|
+
container_id: str = "",
|
|
356
|
+
) -> None:
|
|
357
|
+
raise NotImplementedError("Mounting images isn't supported for persistent Sandboxes yet.")
|
|
358
|
+
|
|
359
|
+
async def unmount_image(self, path: PurePosixPath | str, *, container_id: str = "") -> None:
|
|
360
|
+
raise NotImplementedError("Mounting images isn't supported for persistent Sandboxes yet.")
|
|
361
|
+
|
|
362
|
+
async def snapshot_directory(
|
|
363
|
+
self,
|
|
364
|
+
path: PurePosixPath | str,
|
|
365
|
+
*,
|
|
366
|
+
timeout: int,
|
|
367
|
+
ttl: int | None,
|
|
368
|
+
encryption_key: bytes | None,
|
|
369
|
+
container_id: str = "",
|
|
370
|
+
) -> _Image:
|
|
371
|
+
return await self._run(
|
|
372
|
+
lambda d: d.snapshot_directory(
|
|
373
|
+
path, timeout=timeout, ttl=ttl, encryption_key=encryption_key, container_id=container_id
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
async def reload_volumes(self, *, timeout: int, container_id: str = "") -> None:
|
|
378
|
+
await self._run(lambda d: d.reload_volumes(timeout=timeout, container_id=container_id))
|
|
379
|
+
|
|
380
|
+
async def exec(
|
|
381
|
+
self,
|
|
382
|
+
*args: str,
|
|
383
|
+
pty_info: api_pb2.PTYInfo | None,
|
|
384
|
+
stdout: StreamType,
|
|
385
|
+
stderr: StreamType,
|
|
386
|
+
timeout: int | None,
|
|
387
|
+
workdir: str | None,
|
|
388
|
+
env: dict[str, str | None] | None,
|
|
389
|
+
secrets: Collection[_Secret] | None,
|
|
390
|
+
text: bool,
|
|
391
|
+
bufsize: Literal[-1, 1],
|
|
392
|
+
container_id: str | None,
|
|
393
|
+
) -> _ContainerProcess[bytes] | _ContainerProcess[str]:
|
|
394
|
+
return await self._run(
|
|
395
|
+
lambda d: d.exec(
|
|
396
|
+
*args,
|
|
397
|
+
pty_info=pty_info,
|
|
398
|
+
stdout=stdout,
|
|
399
|
+
stderr=stderr,
|
|
400
|
+
timeout=timeout,
|
|
401
|
+
workdir=workdir,
|
|
402
|
+
env=env,
|
|
403
|
+
secrets=secrets,
|
|
404
|
+
text=text,
|
|
405
|
+
bufsize=bufsize,
|
|
406
|
+
container_id=container_id,
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Sidecar containers
|
|
411
|
+
|
|
412
|
+
def validate_sidecar_create(
|
|
413
|
+
self,
|
|
414
|
+
*,
|
|
415
|
+
cloud_bucket_mounts: Sequence[tuple[str, _CloudBucketMount]],
|
|
416
|
+
has_proxy: bool,
|
|
417
|
+
include_oidc_identity_token: bool,
|
|
418
|
+
) -> None:
|
|
419
|
+
self._delegate.validate_sidecar_create(
|
|
420
|
+
cloud_bucket_mounts=cloud_bucket_mounts,
|
|
421
|
+
has_proxy=has_proxy,
|
|
422
|
+
include_oidc_identity_token=include_oidc_identity_token,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
async def create_sidecar(
|
|
426
|
+
self,
|
|
427
|
+
*args: str,
|
|
428
|
+
name: str,
|
|
429
|
+
image_id: str,
|
|
430
|
+
secret_ids: Sequence[str],
|
|
431
|
+
secret_sources: Sequence[api_pb2.SecretSource],
|
|
432
|
+
workdir: str | None,
|
|
433
|
+
volume_mounts: Sequence[api_pb2.VolumeMount],
|
|
434
|
+
cloud_bucket_mounts: Sequence[tuple[str, _CloudBucketMount]],
|
|
435
|
+
network_access: api_pb2.NetworkAccess,
|
|
436
|
+
include_oidc_identity_token: bool,
|
|
437
|
+
proxy_id: str | None,
|
|
438
|
+
pty_info: api_pb2.PTYInfo | None,
|
|
439
|
+
memory_reserve_consume_mib: int | None,
|
|
440
|
+
) -> tuple[str, str]:
|
|
441
|
+
return await self._run(
|
|
442
|
+
lambda d: d.create_sidecar(
|
|
443
|
+
*args,
|
|
444
|
+
name=name,
|
|
445
|
+
image_id=image_id,
|
|
446
|
+
secret_ids=secret_ids,
|
|
447
|
+
secret_sources=secret_sources,
|
|
448
|
+
workdir=workdir,
|
|
449
|
+
volume_mounts=volume_mounts,
|
|
450
|
+
cloud_bucket_mounts=cloud_bucket_mounts,
|
|
451
|
+
network_access=network_access,
|
|
452
|
+
include_oidc_identity_token=include_oidc_identity_token,
|
|
453
|
+
proxy_id=proxy_id,
|
|
454
|
+
pty_info=pty_info,
|
|
455
|
+
memory_reserve_consume_mib=memory_reserve_consume_mib,
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
async def get_sidecar(self, name: str, include_terminated: bool) -> sr_pb2.TaskContainerInfo:
|
|
460
|
+
return await self._run(lambda d: d.get_sidecar(name, include_terminated))
|
|
461
|
+
|
|
462
|
+
async def list_sidecars(self, include_terminated: bool) -> list[sr_pb2.TaskContainerInfo]:
|
|
463
|
+
return await self._run(lambda d: d.list_sidecars(include_terminated))
|
|
464
|
+
|
|
465
|
+
async def wait_sidecar(self, container_id: str, timeout: float) -> api_pb2.GenericResult | None:
|
|
466
|
+
# Sidecar containers don't carry over to a replacement delegate.
|
|
467
|
+
return await self._delegate.wait_sidecar(container_id, timeout)
|
|
468
|
+
|
|
469
|
+
async def terminate_sidecar(self, container_id: str) -> None:
|
|
470
|
+
await self._delegate.terminate_sidecar(container_id)
|
|
@@ -40,6 +40,7 @@ from ..io_streams import (
|
|
|
40
40
|
from ..secret import _Secret
|
|
41
41
|
from ..stream_type import StreamType
|
|
42
42
|
from ._common import (
|
|
43
|
+
_PERSISTENT_ONLY_MESSAGE,
|
|
43
44
|
_TASK_ID_POLL_INTERVAL,
|
|
44
45
|
ExitSnapshotPoll,
|
|
45
46
|
SandboxCreateArgs,
|
|
@@ -124,6 +125,9 @@ class SandboxImplV1:
|
|
|
124
125
|
def _client(self) -> _Client:
|
|
125
126
|
return self.client
|
|
126
127
|
|
|
128
|
+
def clone(self) -> "SandboxImplV1":
|
|
129
|
+
return type(self)(self.client, self.sandbox_id, result=self.result)
|
|
130
|
+
|
|
127
131
|
@classmethod
|
|
128
132
|
async def create(
|
|
129
133
|
cls, create_args: SandboxCreateArgs, *, app: "modal.app._App | None", client: _Client | None
|
|
@@ -289,6 +293,12 @@ class SandboxImplV1:
|
|
|
289
293
|
req = api_pb2.SandboxTerminateRequest(sandbox_id=self.sandbox_id)
|
|
290
294
|
await self.client._stub.SandboxTerminate(req)
|
|
291
295
|
|
|
296
|
+
async def pause(self) -> None:
|
|
297
|
+
raise NotImplementedError(_PERSISTENT_ONLY_MESSAGE.format("_experimental_pause()"))
|
|
298
|
+
|
|
299
|
+
async def resume(self) -> None:
|
|
300
|
+
raise NotImplementedError(_PERSISTENT_ONLY_MESSAGE.format("_experimental_resume()"))
|
|
301
|
+
|
|
292
302
|
async def snapshot_filesystem(self, timeout: int, *, ttl: int | None, container_id: str = "") -> _Image:
|
|
293
303
|
if not container_id and os.environ.get("MODAL_USE_LEGACY_FILESYSTEM_SNAPSHOT") == "1":
|
|
294
304
|
if ttl != DEFAULT_SNAPSHOT_TTL:
|
|
@@ -32,6 +32,7 @@ from ..io_streams import (
|
|
|
32
32
|
from ..secret import _resolvable_secrets, _Secret, _secret_sources
|
|
33
33
|
from ..stream_type import StreamType
|
|
34
34
|
from ._common import (
|
|
35
|
+
_PERSISTENT_ONLY_MESSAGE,
|
|
35
36
|
_TASK_ID_POLL_INTERVAL,
|
|
36
37
|
ExitSnapshotPoll,
|
|
37
38
|
SandboxCreateArgs,
|
|
@@ -147,6 +148,9 @@ class SandboxImplV2:
|
|
|
147
148
|
def _client(self) -> _Client:
|
|
148
149
|
return self.client
|
|
149
150
|
|
|
151
|
+
def clone(self) -> "SandboxImplV2":
|
|
152
|
+
return type(self)(self.client, self.sandbox_id, result=self.result)
|
|
153
|
+
|
|
150
154
|
@classmethod
|
|
151
155
|
async def create(
|
|
152
156
|
cls, create_args: SandboxCreateArgs, *, app: "modal.app._App | None", client: _Client | None
|
|
@@ -351,6 +355,12 @@ class SandboxImplV2:
|
|
|
351
355
|
req = api_pb2.SandboxTerminateRequest(sandbox_id=self.sandbox_id)
|
|
352
356
|
await self.client._stub.SandboxTerminateV2(req, metadata=await self._metadata())
|
|
353
357
|
|
|
358
|
+
async def pause(self) -> None:
|
|
359
|
+
raise NotImplementedError(_PERSISTENT_ONLY_MESSAGE.format("_experimental_pause()"))
|
|
360
|
+
|
|
361
|
+
async def resume(self) -> None:
|
|
362
|
+
raise NotImplementedError(_PERSISTENT_ONLY_MESSAGE.format("_experimental_resume()"))
|
|
363
|
+
|
|
354
364
|
def _create_sidecar_via_control_plane(self) -> bool:
|
|
355
365
|
# Opt out via the `use_control_plane_sidecar_create` config setting to create
|
|
356
366
|
# sidecars over the task command router instead.
|