modal 1.4.2.dev13__tar.gz → 1.4.2.dev15__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.4.2.dev13 → modal-1.4.2.dev15}/PKG-INFO +1 -1
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/sandbox_fs_utils.py +37 -0
- modal-1.4.2.dev15/modal/cli/bootstrap.py +136 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/entry_point.py +8 -24
- modal-1.4.2.dev15/modal/cli/logo.py +70 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/selector.py +12 -4
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/client.pyi +2 -2
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/exception.py +6 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/sandbox.py +36 -5
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/sandbox.pyi +24 -6
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/sandbox_fs.py +38 -1
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/sandbox_fs.pyi +84 -3
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal.egg-info/SOURCES.txt +2 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/api_grpc.py +80 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/api_pb2.py +863 -749
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/api_pb2.pyi +273 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/api_pb2_grpc.py +166 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/api_pb2_grpc.pyi +52 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/modal_api_grpc.py +5 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_version/__init__.py +1 -1
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/LICENSE +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/README.md +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/__init__.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/__main__.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_billing.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_clustered_functions.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_clustered_functions.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_container_entrypoint.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_functions.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_grpc_client.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_ipython.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_load_context.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_location.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_logs.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_object.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_output/__init__.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_output/manager.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_output/pty.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_output/rich.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_output/status.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_partial_function.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_resolver.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_resources.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/__init__.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/asgi.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/execution_context.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/telemetry.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_serialization.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_server.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_traceback.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_tunnel.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_tunnel.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_type_manager.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/__init__.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/app_utils.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/async_utils.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/blob_utils.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/browser_utils.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/deprecation.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/docker_utils.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/function_utils.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/git_utils.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/hash_utils.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/http_utils.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/logger.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/mount_utils.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/name_utils.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/package_utils.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/shell_utils.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/task_command_router_client.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/time_utils.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_vendor/__init__.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_vendor/tblib.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_vendor/version.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_watcher.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/app.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/app.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/billing.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/builder/2023.12.txt +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/builder/2024.04.txt +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/builder/2024.10.txt +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/builder/2025.06.txt +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/builder/README.md +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/builder/base-images.json +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/call_graph.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/__init__.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/_download.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/_traceback.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/app.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/billing.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/changelog.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/cluster.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/config.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/container.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/dashboard.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/dict.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/environment.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/import_refs.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/launch.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/network_file_system.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/profile.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/programs/__init__.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/programs/vscode.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/queues.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/run.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/secret.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/shell.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/token.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/utils.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/volume.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/client.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cls.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cls.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/config.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/container_process.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/container_process.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/dict.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/dict.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/environments.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/environments.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/experimental/__init__.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/experimental/flash.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/experimental/flash.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/experimental/ipython.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/file_io.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/file_io.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/file_pattern_matcher.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/functions.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/functions.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/image.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/image.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/io_streams.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/io_streams.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/mount.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/mount.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/network_file_system.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/network_file_system.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/object.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/object.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/output.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/parallel_map.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/parallel_map.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/partial_function.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/partial_function.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/proxy.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/proxy.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/py.typed +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/queue.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/queue.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/retries.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/runner.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/runner.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/running_app.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/schedule.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/scheduler_placement.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/secret.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/secret.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/server.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/server.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/serving.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/serving.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/snapshot.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/snapshot.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/stream_type.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/token_flow.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/token_flow.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/volume.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/volume.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal.egg-info/requires.txt +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal.egg-info/top_level.txt +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_docs/__init__.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/__init__.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/py.typed +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_version/__main__.py +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/pyproject.toml +0 -0
- {modal-1.4.2.dev13 → modal-1.4.2.dev15}/setup.cfg +0 -0
|
@@ -18,6 +18,7 @@ from ..exception import (
|
|
|
18
18
|
SandboxFilesystemIsADirectoryError,
|
|
19
19
|
SandboxFilesystemNotADirectoryError,
|
|
20
20
|
SandboxFilesystemNotFoundError,
|
|
21
|
+
SandboxFilesystemPathAlreadyExistsError,
|
|
21
22
|
SandboxFilesystemPermissionError,
|
|
22
23
|
ServiceError,
|
|
23
24
|
)
|
|
@@ -188,6 +189,42 @@ def raise_remove_error(returncode: int, stderr: Union[str, bytes], remote_path:
|
|
|
188
189
|
raise SandboxFilesystemError(f"Operation on '{remote_path}' failed with exit code {returncode}")
|
|
189
190
|
|
|
190
191
|
|
|
192
|
+
def make_make_directory_command(remote_path: str, create_parents: bool) -> str:
|
|
193
|
+
"""Build the JSON command string for a MakeDirectory operation.
|
|
194
|
+
|
|
195
|
+
The returned JSON must match the `Command` enum in the modal-sandbox-fs-tools
|
|
196
|
+
Rust crate (crates/modal-sandbox-fs-tools/src/lib.rs). Treat changes to
|
|
197
|
+
this schema like protobuf changes: fields must not be removed or renamed,
|
|
198
|
+
only added with backwards-compatible defaults.
|
|
199
|
+
"""
|
|
200
|
+
return json.dumps({"MakeDirectory": {"path": remote_path, "parents": create_parents}})
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def raise_make_directory_error(returncode: int, stderr: Union[str, bytes], remote_path: str) -> NoReturn:
|
|
204
|
+
if payload := try_parse_error_payload(stderr):
|
|
205
|
+
logger.debug(
|
|
206
|
+
f"sandbox-fs-tools make_directory error: path={remote_path}, "
|
|
207
|
+
f"error_kind={payload.error_kind}, message={payload.message}, detail={payload.detail}"
|
|
208
|
+
)
|
|
209
|
+
if payload.error_kind == "NotFound":
|
|
210
|
+
raise SandboxFilesystemNotFoundError(f"{payload.message}: {remote_path}")
|
|
211
|
+
if payload.error_kind == "PathAlreadyExists":
|
|
212
|
+
raise SandboxFilesystemPathAlreadyExistsError(f"{payload.message}: {remote_path}")
|
|
213
|
+
if payload.error_kind == "NotDirectory":
|
|
214
|
+
raise SandboxFilesystemNotADirectoryError(f"{payload.message}: {remote_path}")
|
|
215
|
+
if payload.error_kind == "PermissionDenied":
|
|
216
|
+
raise SandboxFilesystemPermissionError(f"{payload.message}: {remote_path}")
|
|
217
|
+
if payload.error_kind == "NotSupported":
|
|
218
|
+
raise InvalidError(
|
|
219
|
+
f"{payload.message}: {remote_path} - this operation is not supported for CloudBucketMounts"
|
|
220
|
+
)
|
|
221
|
+
raise SandboxFilesystemError(payload.message)
|
|
222
|
+
|
|
223
|
+
if stderr_text := _stderr_to_text(stderr):
|
|
224
|
+
logger.debug(f"Unstructured modal-sandbox-fs-tools stderr: {stderr_text}")
|
|
225
|
+
raise SandboxFilesystemError(f"Operation on '{remote_path}' failed with exit code {returncode}")
|
|
226
|
+
|
|
227
|
+
|
|
191
228
|
def validate_absolute_remote_path(remote_path: str, operation: str) -> None:
|
|
192
229
|
if not PurePosixPath(remote_path).is_absolute():
|
|
193
230
|
raise InvalidError(f"Sandbox.filesystem.{operation}() currently only supports absolute remote_path values")
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Copyright Modal Labs 2026
|
|
2
|
+
import io
|
|
3
|
+
import shutil
|
|
4
|
+
import zipfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from modal._utils.async_utils import synchronizer
|
|
12
|
+
from modal._utils.http_utils import _http_client_with_tls
|
|
13
|
+
from modal.client import _Client
|
|
14
|
+
from modal.config import logger
|
|
15
|
+
from modal.output import OutputManager
|
|
16
|
+
from modal_proto import api_pb2
|
|
17
|
+
|
|
18
|
+
from .logo import GREEN, print_logo
|
|
19
|
+
from .selector import Selector
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@synchronizer.create_blocking
|
|
23
|
+
async def bootstrap(
|
|
24
|
+
name: Optional[str] = typer.Argument(None, help="The name of the template to load."),
|
|
25
|
+
output: str = typer.Option(".", "-o", "--output", help="Location for storing the template."),
|
|
26
|
+
force: bool = typer.Option(False, "--force", help="Overwrite the output directory if it already exists."),
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Initialize a sample Modal App."""
|
|
29
|
+
client = await _Client.from_env()
|
|
30
|
+
resp = await client.stub.TemplateList(api_pb2.TemplateListRequest())
|
|
31
|
+
names = sorted([item.name for item in resp.items])
|
|
32
|
+
|
|
33
|
+
output_manager = OutputManager.get()
|
|
34
|
+
print_logo()
|
|
35
|
+
|
|
36
|
+
if name is None:
|
|
37
|
+
if not names:
|
|
38
|
+
output_manager.print("No templates available. Try updating modal.")
|
|
39
|
+
raise typer.Exit(1)
|
|
40
|
+
try:
|
|
41
|
+
selector = Selector(names, title="Select a template", highlight_style=f"bold {GREEN}")
|
|
42
|
+
name = selector.run()
|
|
43
|
+
except Exception:
|
|
44
|
+
output_manager.print("Available templates:")
|
|
45
|
+
for n in names:
|
|
46
|
+
output_manager.print(f" - {n}")
|
|
47
|
+
output_manager.print("\nRun `modal bootstrap <name>` to select a template.")
|
|
48
|
+
raise typer.Exit(0)
|
|
49
|
+
|
|
50
|
+
item = next((item for item in resp.items if item.name == name), None)
|
|
51
|
+
if item is None:
|
|
52
|
+
output_manager.print(f"Unknown template: {name}")
|
|
53
|
+
output_manager.print(f"Available templates: {', '.join(names)}")
|
|
54
|
+
raise typer.Exit(1)
|
|
55
|
+
|
|
56
|
+
template_root = Path(output)
|
|
57
|
+
template_root.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
template_written_to_cwd = template_root.resolve() == Path.cwd().resolve()
|
|
59
|
+
|
|
60
|
+
dest = template_root / name
|
|
61
|
+
if dest.exists() and not force:
|
|
62
|
+
raise click.UsageError(f"Output path '{dest}' already exists. Use --force to overwrite.")
|
|
63
|
+
|
|
64
|
+
# Download the repo archive
|
|
65
|
+
ref = item.ref or "main"
|
|
66
|
+
archive_url = item.repo.rstrip("/") + f"/archive/refs/heads/{ref}.zip"
|
|
67
|
+
|
|
68
|
+
with output_manager.status(f"Downloading template '{name}'..."):
|
|
69
|
+
try:
|
|
70
|
+
session = _http_client_with_tls(timeout=30)
|
|
71
|
+
try:
|
|
72
|
+
async with session.get(archive_url) as resp_http:
|
|
73
|
+
resp_http.raise_for_status()
|
|
74
|
+
data = await resp_http.read()
|
|
75
|
+
finally:
|
|
76
|
+
await session.close()
|
|
77
|
+
except Exception as e:
|
|
78
|
+
output_manager.print(f"Failed to download template from {archive_url}: {e}")
|
|
79
|
+
raise typer.Exit(1)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
zf = zipfile.ZipFile(io.BytesIO(data))
|
|
83
|
+
all_names = zf.namelist()
|
|
84
|
+
root_prefix = all_names[0].split("/")[0] + "/"
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.debug(f"Failed to parse template archive: {e}")
|
|
87
|
+
output_manager.print(f"Unable to fetch template '{name}'.")
|
|
88
|
+
raise typer.Exit(1)
|
|
89
|
+
|
|
90
|
+
with zf:
|
|
91
|
+
template_prefix = root_prefix + name + "/"
|
|
92
|
+
logger.debug(f"Zip root: {root_prefix!r}, template: {template_prefix!r}, entries: {len(all_names)}")
|
|
93
|
+
for entry in all_names[:20]:
|
|
94
|
+
logger.debug(f" {entry}")
|
|
95
|
+
|
|
96
|
+
members = [m for m in all_names if m.startswith(template_prefix) and m != template_prefix]
|
|
97
|
+
logger.debug(f"Matched {len(members)} members under {template_prefix!r}")
|
|
98
|
+
|
|
99
|
+
if not members:
|
|
100
|
+
output_manager.print(f"Template '{name}' not found in repository.")
|
|
101
|
+
raise typer.Exit(1)
|
|
102
|
+
|
|
103
|
+
if dest.exists() and force:
|
|
104
|
+
shutil.rmtree(dest)
|
|
105
|
+
|
|
106
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
|
|
108
|
+
resolved_dest = dest.resolve()
|
|
109
|
+
for member in members:
|
|
110
|
+
rel_path = member[len(template_prefix) :]
|
|
111
|
+
out_path = (dest / rel_path).resolve()
|
|
112
|
+
if not out_path.is_relative_to(resolved_dest):
|
|
113
|
+
logger.debug(f"Zip entry escapes destination: {member!r}")
|
|
114
|
+
output_manager.print(f"Unable to fetch template '{name}'.")
|
|
115
|
+
raise typer.Exit(1)
|
|
116
|
+
if member.endswith("/"):
|
|
117
|
+
out_path.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
else:
|
|
119
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
out_path.write_bytes(zf.read(member))
|
|
121
|
+
|
|
122
|
+
output_manager.print(f"[{GREEN}]✓[/{GREEN}] Template '{name}' written to {dest}", highlight=False)
|
|
123
|
+
output_manager.print(f"[{GREEN}]→[/{GREEN}] To see it in action, run the following command:")
|
|
124
|
+
amp = " [dim white]&&[/dim white] "
|
|
125
|
+
if template_written_to_cwd:
|
|
126
|
+
steps = []
|
|
127
|
+
else:
|
|
128
|
+
steps = [f"[#ff8de6]cd {template_root}[/#ff8de6]"]
|
|
129
|
+
steps.extend(
|
|
130
|
+
[
|
|
131
|
+
f"[{GREEN}]modal deploy -m {name}.app[/{GREEN}]",
|
|
132
|
+
f"[#91c8ef]python -m {name}.try[/#91c8ef]",
|
|
133
|
+
]
|
|
134
|
+
)
|
|
135
|
+
command = amp.join(steps)
|
|
136
|
+
output_manager.print(command)
|
|
@@ -11,6 +11,7 @@ from modal.output import OutputManager
|
|
|
11
11
|
from . import run, shell as shell_module
|
|
12
12
|
from .app import app_cli
|
|
13
13
|
from .billing import billing_cli
|
|
14
|
+
from .bootstrap import bootstrap
|
|
14
15
|
from .changelog import changelog
|
|
15
16
|
from .cluster import cluster_cli
|
|
16
17
|
from .config import config_cli
|
|
@@ -19,6 +20,7 @@ from .dashboard import dashboard
|
|
|
19
20
|
from .dict import dict_cli
|
|
20
21
|
from .environment import environment_cli
|
|
21
22
|
from .launch import launch_cli
|
|
23
|
+
from .logo import print_logo
|
|
22
24
|
from .network_file_system import nfs_cli
|
|
23
25
|
from .profile import profile_cli
|
|
24
26
|
from .queues import queue_cli
|
|
@@ -88,28 +90,7 @@ def check_path():
|
|
|
88
90
|
@synchronizer.create_blocking
|
|
89
91
|
async def setup(profile: Optional[str] = None):
|
|
90
92
|
check_path()
|
|
91
|
-
|
|
92
|
-
art = """
|
|
93
|
-
############# #############
|
|
94
|
-
#### ## #### ##
|
|
95
|
-
## ## ## ## ## ##
|
|
96
|
-
## ## ## ## ## ##
|
|
97
|
-
## ## #### ## ##
|
|
98
|
-
## ############# ## ##
|
|
99
|
-
## ## #### ## ##
|
|
100
|
-
## ## ## ## ## ##
|
|
101
|
-
## ## ## ## ## ##
|
|
102
|
-
## ## ## ## ## ##
|
|
103
|
-
## ## ## ## ## ##
|
|
104
|
-
## ## ## ## #############
|
|
105
|
-
## ## ## ## ## ##
|
|
106
|
-
## ## ## ## ## ##
|
|
107
|
-
## ## ## ## ## ##
|
|
108
|
-
#### ## #### ##
|
|
109
|
-
############# #############
|
|
110
|
-
"""
|
|
111
|
-
|
|
112
|
-
OutputManager.get().print(art, highlight=False, style="green")
|
|
93
|
+
print_logo()
|
|
113
94
|
|
|
114
95
|
# Fetch a new token (same as `modal token new` but redirect to /home once finishes)
|
|
115
96
|
await _new_token(profile=profile, next_url="/home")
|
|
@@ -145,8 +126,11 @@ entrypoint_cli_typer.add_typer(billing_cli, rich_help_panel="Observability")
|
|
|
145
126
|
entrypoint_cli_typer.command("changelog", rich_help_panel="Observability")(changelog)
|
|
146
127
|
entrypoint_cli_typer.command("dashboard", rich_help_panel="Observability")(dashboard)
|
|
147
128
|
|
|
148
|
-
#
|
|
149
|
-
entrypoint_cli_typer.command("setup", help="
|
|
129
|
+
# Onboarding
|
|
130
|
+
entrypoint_cli_typer.command("setup", help="Get started using Modal.", rich_help_panel="Onboarding")(setup)
|
|
131
|
+
entrypoint_cli_typer.command("bootstrap", help="Initialize a sample Modal App.", rich_help_panel="Onboarding")(
|
|
132
|
+
bootstrap
|
|
133
|
+
)
|
|
150
134
|
|
|
151
135
|
# Special handling for modal run, which is more complicated
|
|
152
136
|
entrypoint_cli = typer.main.get_command(entrypoint_cli_typer)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Copyright Modal Labs 2026
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from modal.output import OutputManager
|
|
5
|
+
|
|
6
|
+
GREEN = "#7FEE64"
|
|
7
|
+
|
|
8
|
+
ASCII_LOGO = """
|
|
9
|
+
############# #############
|
|
10
|
+
#### ## #### ##
|
|
11
|
+
## ## ## ## ## ##
|
|
12
|
+
## ## ## ## ## ##
|
|
13
|
+
## ## #### ## ##
|
|
14
|
+
## ############# ## ##
|
|
15
|
+
## ## #### ## ##
|
|
16
|
+
## ## ## ## ## ##
|
|
17
|
+
## ## ## ## ## ##
|
|
18
|
+
## ## ## ## ## ##
|
|
19
|
+
## ## ## ## ## ##
|
|
20
|
+
## ## ## ## #############
|
|
21
|
+
## ## ## ## ## ##
|
|
22
|
+
## ## ## ## ## ##
|
|
23
|
+
## ## ## ## ## ##
|
|
24
|
+
#### ## #### ##
|
|
25
|
+
############# #############
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
UNICODE_LOGO_LARGE = """
|
|
29
|
+
█████████████ █████████████
|
|
30
|
+
████ ██ ████ ██
|
|
31
|
+
██ ██ ██ ██ ██ ██
|
|
32
|
+
██ ██ ██ ██ ██ ██
|
|
33
|
+
██ ██ ████ ██ ██
|
|
34
|
+
██ █████████████ ██ ██
|
|
35
|
+
██ ██ ████ ██ ██
|
|
36
|
+
██ ██ ██ ██ ██ ██
|
|
37
|
+
██ ██ ██ ██ ██ ██
|
|
38
|
+
██ ██ ██ ██ ██ ██
|
|
39
|
+
██ ██ ██ ██ ██ ██
|
|
40
|
+
██ ██ ██ ██ █████████████
|
|
41
|
+
██ ██ ██ ██ ██ ██
|
|
42
|
+
██ ██ ██ ██ ██ ██
|
|
43
|
+
██ ██ ██ ██ ██ ██
|
|
44
|
+
████ ██ ████ ██
|
|
45
|
+
█████████████ █████████████
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
UNICODE_LOGO_SMALL = """
|
|
49
|
+
▟█▀▀▀▀▜▖ ▗█▛▀▀▀▀▙
|
|
50
|
+
▟▘▝▙ ▜▖▗▛ ▜▖ ▝▙
|
|
51
|
+
▟▘ ▝▙▄▄▄▄█▛ ▜▖ ▝▙
|
|
52
|
+
▟▘ ▟▘ ▗▛▜▖ ▜▖ ▝▙
|
|
53
|
+
▟▘ ▟▘ ▗▛ ▜▖ ▜▖ ▝▙
|
|
54
|
+
▟▘ ▟▘ ▗▛ ▜▖ ▜▄▄▄▄▟▙
|
|
55
|
+
▝▙ ▟▘ ▗▛ ▜▖ ▗▛ ▟▘
|
|
56
|
+
▝▙▟▘ ▗▛ ▜▄▛ ▟▘
|
|
57
|
+
▝▀▀▀▀▀▀ ▀▀▀▀▀▀▘
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_logo():
|
|
62
|
+
if sys.stdout.encoding and sys.stdout.encoding.lower().startswith("utf"):
|
|
63
|
+
return UNICODE_LOGO_SMALL
|
|
64
|
+
else:
|
|
65
|
+
return ASCII_LOGO
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def print_logo():
|
|
69
|
+
if sys.stdout.isatty():
|
|
70
|
+
OutputManager.get().print(get_logo(), highlight=False, style=GREEN)
|
|
@@ -19,6 +19,13 @@ def _cbreak_terminal():
|
|
|
19
19
|
old_settings = termios.tcgetattr(fd)
|
|
20
20
|
try:
|
|
21
21
|
tty.setcbreak(fd, termios.TCSADRAIN)
|
|
22
|
+
# Disable ISIG so Ctrl-C is delivered as \x03 to os.read() rather than
|
|
23
|
+
# generating an asynchronous SIGINT. This lets _input_loop raise
|
|
24
|
+
# KeyboardInterrupt synchronously, ensuring context-manager cleanup
|
|
25
|
+
# (cursor visibility, terminal echo) completes without interruption.
|
|
26
|
+
attrs = termios.tcgetattr(fd)
|
|
27
|
+
attrs[3] &= ~termios.ISIG
|
|
28
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, attrs)
|
|
22
29
|
yield
|
|
23
30
|
finally:
|
|
24
31
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
@@ -44,10 +51,10 @@ class Selector:
|
|
|
44
51
|
exiting.
|
|
45
52
|
"""
|
|
46
53
|
|
|
47
|
-
def __init__(self, options: list[str], title: str = "Select an option"):
|
|
54
|
+
def __init__(self, options: list[str], title: str = "Select an option", highlight_style: str = "bold green"):
|
|
48
55
|
if not options:
|
|
49
56
|
raise ValueError("options must not be empty")
|
|
50
|
-
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
57
|
+
if not sys.stdin.isatty() or not sys.stdout.isatty() or Console().is_dumb_terminal:
|
|
51
58
|
raise RuntimeError("Interactive selection requires a TTY")
|
|
52
59
|
# Fail fast on platforms without termios (e.g. Windows).
|
|
53
60
|
import termios # noqa: F401
|
|
@@ -55,6 +62,7 @@ class Selector:
|
|
|
55
62
|
self.options = options
|
|
56
63
|
self.title = title
|
|
57
64
|
self.selected = 0
|
|
65
|
+
self.highlight_style = highlight_style
|
|
58
66
|
|
|
59
67
|
# -- state manipulation ---------------------------------------------------
|
|
60
68
|
|
|
@@ -75,8 +83,8 @@ class Selector:
|
|
|
75
83
|
text.append(f"{self.title}\n\n", style="bold")
|
|
76
84
|
for i, opt in enumerate(self.options):
|
|
77
85
|
if i == self.selected:
|
|
78
|
-
text.append(" > ", style=
|
|
79
|
-
text.append(f"{opt}\n", style=
|
|
86
|
+
text.append(" > ", style=self.highlight_style)
|
|
87
|
+
text.append(f"{opt}\n", style=self.highlight_style)
|
|
80
88
|
else:
|
|
81
89
|
text.append(f" {opt}\n")
|
|
82
90
|
return text
|
|
@@ -35,7 +35,7 @@ class _Client:
|
|
|
35
35
|
server_url: str,
|
|
36
36
|
client_type: int,
|
|
37
37
|
credentials: typing.Optional[tuple[str, str]],
|
|
38
|
-
version: str = "1.4.2.
|
|
38
|
+
version: str = "1.4.2.dev15",
|
|
39
39
|
):
|
|
40
40
|
"""mdmd:hidden
|
|
41
41
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -175,7 +175,7 @@ class Client:
|
|
|
175
175
|
server_url: str,
|
|
176
176
|
client_type: int,
|
|
177
177
|
credentials: typing.Optional[tuple[str, str]],
|
|
178
|
-
version: str = "1.4.2.
|
|
178
|
+
version: str = "1.4.2.dev15",
|
|
179
179
|
):
|
|
180
180
|
"""mdmd:hidden
|
|
181
181
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -379,3 +379,9 @@ class SandboxFilesystemFileTooLargeError(SandboxFilesystemError):
|
|
|
379
379
|
"""Raised when a file exceeds the maximum allowed size for a read operation in the sandbox."""
|
|
380
380
|
|
|
381
381
|
pass
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class SandboxFilesystemPathAlreadyExistsError(SandboxFilesystemError):
|
|
385
|
+
"""Raised when a path already exists and the operation requires it to be absent."""
|
|
386
|
+
|
|
387
|
+
pass
|
|
@@ -1110,7 +1110,12 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1110
1110
|
|
|
1111
1111
|
req = api_pb2.SandboxGetTunnelsRequest(sandbox_id=self.object_id, timeout=timeout)
|
|
1112
1112
|
stub = self._client.stub
|
|
1113
|
-
|
|
1113
|
+
if self._is_v2:
|
|
1114
|
+
assert self._client._auth_token_manager
|
|
1115
|
+
auth_token = await self._client._auth_token_manager.get_token()
|
|
1116
|
+
resp = await stub.SandboxGetTunnelsV2(req, metadata=[("x-modal-auth-token", auth_token)])
|
|
1117
|
+
else:
|
|
1118
|
+
resp = await stub.SandboxGetTunnels(req)
|
|
1114
1119
|
|
|
1115
1120
|
# If we couldn't get the tunnels in time, report the timeout.
|
|
1116
1121
|
if resp.result.status == api_pb2.GenericResult.GENERIC_STATUS_TIMEOUT:
|
|
@@ -1179,7 +1184,12 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1179
1184
|
This is a no-op if the Sandbox has already finished running."""
|
|
1180
1185
|
req = api_pb2.SandboxTerminateRequest(sandbox_id=self.object_id)
|
|
1181
1186
|
stub = self._client.stub
|
|
1182
|
-
|
|
1187
|
+
if self._is_v2:
|
|
1188
|
+
assert self._client._auth_token_manager
|
|
1189
|
+
auth_token = await self._client._auth_token_manager.get_token()
|
|
1190
|
+
await stub.SandboxTerminateV2(req, metadata=[("x-modal-auth-token", auth_token)])
|
|
1191
|
+
else:
|
|
1192
|
+
await stub.SandboxTerminate(req)
|
|
1183
1193
|
if wait:
|
|
1184
1194
|
await self.wait(raise_on_termination=False)
|
|
1185
1195
|
return self.returncode
|
|
@@ -1208,7 +1218,12 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1208
1218
|
while not self._task_id:
|
|
1209
1219
|
req = api_pb2.SandboxGetTaskIdRequest(sandbox_id=self.object_id)
|
|
1210
1220
|
stub = self._client.stub
|
|
1211
|
-
|
|
1221
|
+
if self._is_v2:
|
|
1222
|
+
assert self._client._auth_token_manager
|
|
1223
|
+
auth_token = await self._client._auth_token_manager.get_token()
|
|
1224
|
+
resp = await stub.SandboxGetTaskIdV2(req, metadata=[("x-modal-auth-token", auth_token)])
|
|
1225
|
+
else:
|
|
1226
|
+
resp = await stub.SandboxGetTaskId(req)
|
|
1212
1227
|
if not resp.task_id and raise_if_task_complete and resp.HasField("task_result"):
|
|
1213
1228
|
msg = resp.task_result.exception or "Sandbox already finished"
|
|
1214
1229
|
raise Error(msg)
|
|
@@ -1632,14 +1647,30 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1632
1647
|
return await ls(path, self._client, task_id)
|
|
1633
1648
|
|
|
1634
1649
|
async def mkdir(self, path: str, parents: bool = False) -> None:
|
|
1635
|
-
"""[Alpha] Create a new directory in the Sandbox.
|
|
1650
|
+
"""[Alpha] Create a new directory in the Sandbox.
|
|
1651
|
+
|
|
1652
|
+
.. deprecated:: 2026-04-15
|
|
1653
|
+
"""
|
|
1636
1654
|
self._ensure_v1("mkdir")
|
|
1655
|
+
deprecation_warning(
|
|
1656
|
+
(2026, 4, 15),
|
|
1657
|
+
"`Sandbox.mkdir()` is deprecated. Use the `Sandbox.filesystem` APIs instead.",
|
|
1658
|
+
pending=True,
|
|
1659
|
+
)
|
|
1637
1660
|
task_id = await self._get_task_id()
|
|
1638
1661
|
return await mkdir(path, self._client, task_id, parents)
|
|
1639
1662
|
|
|
1640
1663
|
async def rm(self, path: str, recursive: bool = False) -> None:
|
|
1641
|
-
"""[Alpha] Remove a file or directory in the Sandbox.
|
|
1664
|
+
"""[Alpha] Remove a file or directory in the Sandbox.
|
|
1665
|
+
|
|
1666
|
+
.. deprecated:: 2026-04-15
|
|
1667
|
+
"""
|
|
1642
1668
|
self._ensure_v1("rm")
|
|
1669
|
+
deprecation_warning(
|
|
1670
|
+
(2026, 4, 15),
|
|
1671
|
+
"`Sandbox.rm()` is deprecated. Use the `Sandbox.filesystem` APIs instead.",
|
|
1672
|
+
pending=True,
|
|
1673
|
+
)
|
|
1643
1674
|
task_id = await self._get_task_id()
|
|
1644
1675
|
return await rm(path, self._client, task_id, recursive)
|
|
1645
1676
|
|
|
@@ -617,11 +617,17 @@ class _Sandbox(modal._object._Object):
|
|
|
617
617
|
...
|
|
618
618
|
|
|
619
619
|
async def mkdir(self, path: str, parents: bool = False) -> None:
|
|
620
|
-
"""[Alpha] Create a new directory in the Sandbox.
|
|
620
|
+
"""[Alpha] Create a new directory in the Sandbox.
|
|
621
|
+
|
|
622
|
+
.. deprecated:: 2026-04-15
|
|
623
|
+
"""
|
|
621
624
|
...
|
|
622
625
|
|
|
623
626
|
async def rm(self, path: str, recursive: bool = False) -> None:
|
|
624
|
-
"""[Alpha] Remove a file or directory in the Sandbox.
|
|
627
|
+
"""[Alpha] Remove a file or directory in the Sandbox.
|
|
628
|
+
|
|
629
|
+
.. deprecated:: 2026-04-15
|
|
630
|
+
"""
|
|
625
631
|
...
|
|
626
632
|
|
|
627
633
|
def watch(
|
|
@@ -1911,22 +1917,34 @@ class Sandbox(modal.object.Object):
|
|
|
1911
1917
|
|
|
1912
1918
|
class __mkdir_spec(typing_extensions.Protocol):
|
|
1913
1919
|
def __call__(self, /, path: str, parents: bool = False) -> None:
|
|
1914
|
-
"""[Alpha] Create a new directory in the Sandbox.
|
|
1920
|
+
"""[Alpha] Create a new directory in the Sandbox.
|
|
1921
|
+
|
|
1922
|
+
.. deprecated:: 2026-04-15
|
|
1923
|
+
"""
|
|
1915
1924
|
...
|
|
1916
1925
|
|
|
1917
1926
|
async def aio(self, /, path: str, parents: bool = False) -> None:
|
|
1918
|
-
"""[Alpha] Create a new directory in the Sandbox.
|
|
1927
|
+
"""[Alpha] Create a new directory in the Sandbox.
|
|
1928
|
+
|
|
1929
|
+
.. deprecated:: 2026-04-15
|
|
1930
|
+
"""
|
|
1919
1931
|
...
|
|
1920
1932
|
|
|
1921
1933
|
mkdir: __mkdir_spec
|
|
1922
1934
|
|
|
1923
1935
|
class __rm_spec(typing_extensions.Protocol):
|
|
1924
1936
|
def __call__(self, /, path: str, recursive: bool = False) -> None:
|
|
1925
|
-
"""[Alpha] Remove a file or directory in the Sandbox.
|
|
1937
|
+
"""[Alpha] Remove a file or directory in the Sandbox.
|
|
1938
|
+
|
|
1939
|
+
.. deprecated:: 2026-04-15
|
|
1940
|
+
"""
|
|
1926
1941
|
...
|
|
1927
1942
|
|
|
1928
1943
|
async def aio(self, /, path: str, recursive: bool = False) -> None:
|
|
1929
|
-
"""[Alpha] Remove a file or directory in the Sandbox.
|
|
1944
|
+
"""[Alpha] Remove a file or directory in the Sandbox.
|
|
1945
|
+
|
|
1946
|
+
.. deprecated:: 2026-04-15
|
|
1947
|
+
"""
|
|
1930
1948
|
...
|
|
1931
1949
|
|
|
1932
1950
|
rm: __rm_spec
|
|
@@ -11,9 +11,11 @@ from typing import Union, cast
|
|
|
11
11
|
from ._utils.async_utils import synchronize_api
|
|
12
12
|
from ._utils.logger import logger
|
|
13
13
|
from ._utils.sandbox_fs_utils import (
|
|
14
|
+
make_make_directory_command,
|
|
14
15
|
make_read_file_command,
|
|
15
16
|
make_remove_command,
|
|
16
17
|
make_write_file_command,
|
|
18
|
+
raise_make_directory_error,
|
|
17
19
|
raise_read_file_error,
|
|
18
20
|
raise_remove_error,
|
|
19
21
|
raise_write_file_error,
|
|
@@ -338,7 +340,7 @@ class _SandboxFilesystem:
|
|
|
338
340
|
To remove a directory and all its contents:
|
|
339
341
|
|
|
340
342
|
```python fixture:sandbox
|
|
341
|
-
sandbox.
|
|
343
|
+
sandbox.filesystem.make_directory("/tmp/mydir/subdir")
|
|
342
344
|
sandbox.filesystem.remove("/tmp/mydir", recursive=True)
|
|
343
345
|
```
|
|
344
346
|
"""
|
|
@@ -351,5 +353,40 @@ class _SandboxFilesystem:
|
|
|
351
353
|
if returncode != 0:
|
|
352
354
|
raise_remove_error(returncode, stderr, remote_path)
|
|
353
355
|
|
|
356
|
+
async def make_directory(self, remote_path: str, *, create_parents: bool = True) -> None:
|
|
357
|
+
"""Create a new directory in the Sandbox.
|
|
358
|
+
|
|
359
|
+
`remote_path` must be an absolute path in the Sandbox.
|
|
360
|
+
|
|
361
|
+
When `create_parents` is `True` (the default), any missing parent directories are created and the call is
|
|
362
|
+
idempotent (succeeds silently if the directory already exists). When `create_parents` is `False`, the
|
|
363
|
+
immediate parent directory must already exist and the path must not already exist.
|
|
364
|
+
|
|
365
|
+
**Raises**
|
|
366
|
+
|
|
367
|
+
- `SandboxFilesystemNotFoundError`: the parent directory does not exist and `create_parents` is `False`.
|
|
368
|
+
- `SandboxFilesystemPathAlreadyExistsError`: the path already exists.
|
|
369
|
+
- `SandboxFilesystemNotADirectoryError`: a path component is not a directory.
|
|
370
|
+
- `SandboxFilesystemPermissionError`: creation is not permitted.
|
|
371
|
+
- `InvalidError`: the operation is not supported by the mount.
|
|
372
|
+
- `SandboxFilesystemError`: the command fails for any other reason.
|
|
373
|
+
|
|
374
|
+
**Usage**
|
|
375
|
+
|
|
376
|
+
```python fixture:sandbox
|
|
377
|
+
sandbox.filesystem.make_directory("/tmp/a/b/c")
|
|
378
|
+
```
|
|
379
|
+
"""
|
|
380
|
+
validate_absolute_remote_path(remote_path, "make_directory")
|
|
381
|
+
|
|
382
|
+
with translate_exec_errors("make_directory", remote_path):
|
|
383
|
+
process = await self._sandbox.exec(
|
|
384
|
+
_SANDBOX_FS_TOOLS_PATH, make_make_directory_command(remote_path, create_parents)
|
|
385
|
+
)
|
|
386
|
+
stderr, returncode = await asyncio.gather(process.stderr.read(), process.wait())
|
|
387
|
+
|
|
388
|
+
if returncode != 0:
|
|
389
|
+
raise_make_directory_error(returncode, stderr, remote_path)
|
|
390
|
+
|
|
354
391
|
|
|
355
392
|
SandboxFilesystem = synchronize_api(_SandboxFilesystem)
|