modal 1.5.2.dev21__tar.gz → 1.5.2.dev22__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.5.2.dev21 → modal-1.5.2.dev22}/PKG-INFO +1 -1
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/task_command_router_client.py +127 -3
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/_help.py +7 -1
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/client.pyi +2 -2
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/container_process.py +13 -1
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/container_process.pyi +27 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/sandbox.py +26 -12
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/sandbox.pyi +12 -3
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/sandbox_fs.py +32 -80
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/sandbox_fs.pyi +24 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/api_grpc.py +32 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/api_pb2.py +928 -861
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/api_pb2.pyi +149 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/api_pb2_grpc.py +67 -1
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/api_pb2_grpc.pyi +23 -3
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/modal_api_grpc.py +2 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_version/__init__.py +1 -1
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/LICENSE +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/README.md +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/__init__.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/__main__.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_billing.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_clustered_functions.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_clustered_functions.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_container_entrypoint.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_environments.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_function_variants.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_functions.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_grpc_client.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_image.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_ipython.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_load_context.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_location.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_logs.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_object.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_output/__init__.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_output/manager.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_output/pty.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_output/rich.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_output/status.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_partial_function.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_resolver.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_resources.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/__init__.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/asgi.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/execution_context.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/telemetry.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_serialization.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_server.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_traceback.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_tunnel.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_tunnel.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_type_manager.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/__init__.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/app_utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/async_utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/blob_utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/browser_utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/curl_utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/deprecation.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/docker_utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/function_utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/git_utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/hash_utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/http_utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/logger.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/mount_utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/name_utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/package_utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/proxy_support.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/shell_utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/time_utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_vendor/__init__.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_vendor/tblib.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_vendor/version.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_watcher.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_workspace.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/app.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/app.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/billing.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/billing.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/builder/2023.12.txt +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/builder/2024.04.txt +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/builder/2024.10.txt +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/builder/2025.06.txt +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/builder/README.md +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/builder/base-images.json +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/call_graph.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/__init__.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/_download.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/_traceback.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/app.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/billing.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/bootstrap.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/changelog.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/cluster.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/config.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/container.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/curl.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/dashboard.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/dict.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/endpoint.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/entry_point.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/environment.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/image.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/import_refs.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/launch.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/logo.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/network_file_system.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/profile.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/programs/__init__.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/programs/vscode.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/queues.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/run.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/secret.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/selector.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/shell.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/skills.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/token.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/utils.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/volume.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/workspace.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/client.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cls.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cls.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/config.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/dict.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/dict.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/environments.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/environments.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/exception.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/experimental/__init__.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/experimental/flash.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/experimental/flash.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/experimental/ipython.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/file_io.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/file_io.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/file_pattern_matcher.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/functions.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/functions.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/image.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/image.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/io_streams.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/io_streams.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/mount.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/mount.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/network_file_system.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/network_file_system.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/object.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/object.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/output.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/parallel_map.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/parallel_map.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/partial_function.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/partial_function.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/proxy.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/proxy.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/py.typed +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/queue.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/queue.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/retries.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/runner.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/runner.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/running_app.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/schedule.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/scheduler_placement.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/secret.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/secret.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/server.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/server.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/serving.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/serving.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/snapshot.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/snapshot.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/stream_type.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/token_flow.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/token_flow.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/types.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/volume.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/volume.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/workspace.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/workspace.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal.egg-info/requires.txt +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal.egg-info/top_level.txt +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_docs/__init__.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_docs/mdmd/types.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/__init__.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/py.typed +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_version/__main__.py +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/pyproject.toml +0 -0
- {modal-1.5.2.dev21 → modal-1.5.2.dev22}/setup.cfg +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Copyright Modal Labs 2025
|
|
2
2
|
import asyncio
|
|
3
3
|
import base64
|
|
4
|
+
import io
|
|
4
5
|
import json
|
|
5
6
|
import socket
|
|
6
7
|
import ssl
|
|
@@ -8,9 +9,9 @@ import time
|
|
|
8
9
|
import typing
|
|
9
10
|
import urllib.parse
|
|
10
11
|
import weakref
|
|
11
|
-
from collections.abc import AsyncGenerator, Callable
|
|
12
|
+
from collections.abc import AsyncGenerator, AsyncIterable, Callable
|
|
12
13
|
from contextlib import suppress
|
|
13
|
-
from typing import TypeVar
|
|
14
|
+
from typing import BinaryIO, TypeVar
|
|
14
15
|
|
|
15
16
|
import grpclib.client
|
|
16
17
|
import grpclib.config
|
|
@@ -18,7 +19,7 @@ from grpclib import GRPCError, Status
|
|
|
18
19
|
from grpclib.exceptions import StreamTerminatedError
|
|
19
20
|
|
|
20
21
|
from modal.config import logger
|
|
21
|
-
from modal.exception import ExecTimeoutError, TimeoutError as ModalTimeoutError
|
|
22
|
+
from modal.exception import AuthError, ExecTimeoutError, InternalError, ServiceError, TimeoutError as ModalTimeoutError
|
|
22
23
|
from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
|
|
23
24
|
from modal_proto.task_command_router_grpc import TaskCommandRouterStub
|
|
24
25
|
|
|
@@ -27,6 +28,17 @@ from .._utils.grpc_utils import ModalChannel
|
|
|
27
28
|
from .async_utils import aclosing, retry
|
|
28
29
|
from .grpc_utils import RETRYABLE_GRPC_STATUS_CODES
|
|
29
30
|
|
|
31
|
+
STREAMING_STDIN_CHUNK_SIZE = 256 * 1024
|
|
32
|
+
|
|
33
|
+
_STREAMING_STDIN_RESUMABLE_EXCEPTIONS = (
|
|
34
|
+
AuthError,
|
|
35
|
+
InternalError,
|
|
36
|
+
ServiceError,
|
|
37
|
+
StreamTerminatedError,
|
|
38
|
+
OSError,
|
|
39
|
+
asyncio.TimeoutError,
|
|
40
|
+
)
|
|
41
|
+
|
|
30
42
|
|
|
31
43
|
@retry(n_attempts=34, base_delay=1, max_delay=10, attempt_timeout=10, total_timeout=310)
|
|
32
44
|
async def _connect_channel(channel: grpclib.client.Channel):
|
|
@@ -472,6 +484,118 @@ class TaskCommandRouterClient:
|
|
|
472
484
|
lambda: self._call_with_auth_retry(self._stub.TaskExecStdinWrite, request)
|
|
473
485
|
)
|
|
474
486
|
|
|
487
|
+
async def exec_stdin_write_stream(
|
|
488
|
+
self,
|
|
489
|
+
task_id: str,
|
|
490
|
+
exec_id: str,
|
|
491
|
+
source: BinaryIO,
|
|
492
|
+
chunk_size: int = STREAMING_STDIN_CHUNK_SIZE,
|
|
493
|
+
max_resume_attempts: int = 9,
|
|
494
|
+
) -> int:
|
|
495
|
+
"""Stream `source` into the exec's stdin, with bounded resume on transient failures.
|
|
496
|
+
|
|
497
|
+
Streams the full contents of `source` in one client-streaming RPC and
|
|
498
|
+
closes stdin (EOF) on success. On a resumable error, queries
|
|
499
|
+
`exec_stdin_status` for the server's canonical offset, seeks `source`
|
|
500
|
+
to that point, and reopens the stream.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
task_id: The task ID of the task running the exec'd command.
|
|
504
|
+
exec_id: The execution ID of the command to write to.
|
|
505
|
+
source: A seekable byte source (file, BytesIO, etc.).
|
|
506
|
+
chunk_size: Bytes per outbound message.
|
|
507
|
+
max_resume_attempts: Upper bound on resume retries before giving up.
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
The total number of bytes streamed into the exec's stdin.
|
|
511
|
+
"""
|
|
512
|
+
offset = 0
|
|
513
|
+
attempt = 0
|
|
514
|
+
while True:
|
|
515
|
+
|
|
516
|
+
async def _chunks():
|
|
517
|
+
while True:
|
|
518
|
+
chunk = source.read(chunk_size)
|
|
519
|
+
if not chunk:
|
|
520
|
+
return
|
|
521
|
+
yield chunk
|
|
522
|
+
|
|
523
|
+
try:
|
|
524
|
+
source.seek(offset)
|
|
525
|
+
await self._exec_stdin_write_stream(task_id, exec_id, offset, _chunks())
|
|
526
|
+
return source.tell()
|
|
527
|
+
except (*_STREAMING_STDIN_RESUMABLE_EXCEPTIONS, AttributeError) as e:
|
|
528
|
+
# StreamTerminatedError surfaces as AttributeError in grpclib<=0.4.7;
|
|
529
|
+
# see call_with_retries_on_transient_errors.
|
|
530
|
+
if isinstance(e, AttributeError) and "_write_appdata" not in str(e):
|
|
531
|
+
raise
|
|
532
|
+
attempt += 1
|
|
533
|
+
if attempt > max_resume_attempts:
|
|
534
|
+
raise
|
|
535
|
+
status = await self.exec_stdin_status(task_id=task_id, exec_id=exec_id)
|
|
536
|
+
if status.closed:
|
|
537
|
+
pos = source.tell()
|
|
538
|
+
if status.num_bytes_written == pos and source.seek(0, io.SEEK_END) == pos:
|
|
539
|
+
logger.debug(f"exec_stdin_write_stream completed but response was lost: {e}")
|
|
540
|
+
return pos
|
|
541
|
+
raise
|
|
542
|
+
offset = status.num_bytes_written
|
|
543
|
+
logger.debug(f"exec_stdin_write_stream resuming from offset {offset} after error: {e}")
|
|
544
|
+
|
|
545
|
+
async def _exec_stdin_write_stream(
|
|
546
|
+
self,
|
|
547
|
+
task_id: str,
|
|
548
|
+
exec_id: str,
|
|
549
|
+
start_offset: int,
|
|
550
|
+
chunks: AsyncIterable[bytes],
|
|
551
|
+
) -> sr_pb2.TaskExecStdinWriteStreamResponse | None:
|
|
552
|
+
"""Single client-streaming attempt: Start, Data chunks, then End (EOF).
|
|
553
|
+
|
|
554
|
+
Does not retry; `exec_stdin_write_stream` owns resume.
|
|
555
|
+
"""
|
|
556
|
+
with grpc_error_converter():
|
|
557
|
+
stream = self._stub.TaskExecStdinWriteStream.open(metadata=self._get_metadata())
|
|
558
|
+
async with stream as s:
|
|
559
|
+
start = sr_pb2.TaskExecStdinWriteStreamRequest(
|
|
560
|
+
start=sr_pb2.TaskExecStdinWriteStreamStart(
|
|
561
|
+
task_id=task_id,
|
|
562
|
+
exec_id=exec_id,
|
|
563
|
+
offset=start_offset,
|
|
564
|
+
),
|
|
565
|
+
)
|
|
566
|
+
await s.send_message(start)
|
|
567
|
+
async for data in chunks:
|
|
568
|
+
if not data:
|
|
569
|
+
continue
|
|
570
|
+
await s.send_message(sr_pb2.TaskExecStdinWriteStreamRequest(data=data))
|
|
571
|
+
# The server closes stdin only on this explicit End message. A
|
|
572
|
+
# stream that breaks before it leaves stdin open for resume.
|
|
573
|
+
await s.send_message(
|
|
574
|
+
sr_pb2.TaskExecStdinWriteStreamRequest(end=sr_pb2.TaskExecStdinWriteStreamEnd()),
|
|
575
|
+
end=True,
|
|
576
|
+
)
|
|
577
|
+
return await s.recv_message()
|
|
578
|
+
|
|
579
|
+
async def exec_stdin_status(self, task_id: str, exec_id: str) -> sr_pb2.TaskExecStdinStatusResponse:
|
|
580
|
+
"""Read the current stdin write status for an exec'd command, to support retries from the right offset.
|
|
581
|
+
|
|
582
|
+
Used by streaming clients to find the resume offset after a stream
|
|
583
|
+
failure. Evicts any in-flight stdin stream for the exec, so it is not
|
|
584
|
+
safe to call for read-only observability.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
task_id: The task ID of the task running the exec'd command.
|
|
588
|
+
exec_id: The execution ID of the command to query.
|
|
589
|
+
Raises:
|
|
590
|
+
Other errors: If retries are exhausted on transient errors or if there's an error
|
|
591
|
+
from the RPC itself.
|
|
592
|
+
"""
|
|
593
|
+
request = sr_pb2.TaskExecStdinStatusRequest(task_id=task_id, exec_id=exec_id)
|
|
594
|
+
with grpc_error_converter():
|
|
595
|
+
return await call_with_retries_on_transient_errors(
|
|
596
|
+
lambda: self._call_with_auth_retry(self._stub.TaskExecStdinStatus, request)
|
|
597
|
+
)
|
|
598
|
+
|
|
475
599
|
async def sandbox_stdin_write_v2(
|
|
476
600
|
self, task_id: str, offset: int, data: bytes, eof: bool
|
|
477
601
|
) -> sr_pb2.SandboxStdinWriteV2Response:
|
|
@@ -70,7 +70,13 @@ def _option_label(param: click.Parameter, ctx: click.Context) -> Text:
|
|
|
70
70
|
text.append(opt, style=_OPTION_FLAG_STYLE)
|
|
71
71
|
if not getattr(param, "is_flag", False) and not getattr(param, "count", False):
|
|
72
72
|
text.append(" ")
|
|
73
|
-
|
|
73
|
+
if "ctx" in inspect.signature(click.Parameter.make_metavar).parameters:
|
|
74
|
+
# click >= 8.2.0
|
|
75
|
+
metavar = param.make_metavar(ctx)
|
|
76
|
+
else:
|
|
77
|
+
# backwards compatibility
|
|
78
|
+
metavar = param.make_metavar() # type: ignore[call-arg]
|
|
79
|
+
text.append(metavar, style=_OPTION_METAVAR_STYLE)
|
|
74
80
|
return text
|
|
75
81
|
|
|
76
82
|
|
|
@@ -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.5.2.
|
|
38
|
+
version: str = "1.5.2.dev22",
|
|
39
39
|
):
|
|
40
40
|
"""mdmd:hidden
|
|
41
41
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -205,7 +205,7 @@ class Client:
|
|
|
205
205
|
server_url: str,
|
|
206
206
|
client_type: int,
|
|
207
207
|
credentials: typing.Optional[tuple[str, str]],
|
|
208
|
-
version: str = "1.5.2.
|
|
208
|
+
version: str = "1.5.2.dev22",
|
|
209
209
|
):
|
|
210
210
|
"""mdmd:hidden
|
|
211
211
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Copyright Modal Labs 2024
|
|
2
2
|
import asyncio
|
|
3
3
|
import platform
|
|
4
|
-
from typing import Generic, TypeVar
|
|
4
|
+
from typing import BinaryIO, Generic, TypeVar
|
|
5
5
|
|
|
6
6
|
from modal_proto import api_pb2
|
|
7
7
|
|
|
@@ -117,6 +117,18 @@ class _ContainerProcess(Generic[T]):
|
|
|
117
117
|
)
|
|
118
118
|
return self._returncode
|
|
119
119
|
|
|
120
|
+
async def _stdin_write_stream(self, source: BinaryIO) -> int:
|
|
121
|
+
"""mdmd:hidden
|
|
122
|
+
Stream `source` into the process's stdin and close it (EOF) on success.
|
|
123
|
+
|
|
124
|
+
Returns the total bytes streamed.
|
|
125
|
+
"""
|
|
126
|
+
return await self._command_router_client.exec_stdin_write_stream(
|
|
127
|
+
task_id=self._task_id,
|
|
128
|
+
exec_id=self._process_id,
|
|
129
|
+
source=source,
|
|
130
|
+
)
|
|
131
|
+
|
|
120
132
|
async def poll(self) -> int | None:
|
|
121
133
|
"""Check if the container process has finished running.
|
|
122
134
|
|
|
@@ -53,6 +53,14 @@ class _ContainerProcess(typing.Generic[T]):
|
|
|
53
53
|
|
|
54
54
|
@property
|
|
55
55
|
def returncode(self) -> int: ...
|
|
56
|
+
async def _stdin_write_stream(self, source: typing.BinaryIO) -> int:
|
|
57
|
+
"""mdmd:hidden
|
|
58
|
+
Stream `source` into the process's stdin and close it (EOF) on success.
|
|
59
|
+
|
|
60
|
+
Returns the total bytes streamed.
|
|
61
|
+
"""
|
|
62
|
+
...
|
|
63
|
+
|
|
56
64
|
async def poll(self) -> typing.Optional[int]:
|
|
57
65
|
"""Check if the container process has finished running.
|
|
58
66
|
|
|
@@ -105,6 +113,25 @@ class ContainerProcess(typing.Generic[T]):
|
|
|
105
113
|
@property
|
|
106
114
|
def returncode(self) -> int: ...
|
|
107
115
|
|
|
116
|
+
class ___stdin_write_stream_spec(typing_extensions.Protocol):
|
|
117
|
+
def __call__(self, /, source: typing.BinaryIO) -> int:
|
|
118
|
+
"""mdmd:hidden
|
|
119
|
+
Stream `source` into the process's stdin and close it (EOF) on success.
|
|
120
|
+
|
|
121
|
+
Returns the total bytes streamed.
|
|
122
|
+
"""
|
|
123
|
+
...
|
|
124
|
+
|
|
125
|
+
async def aio(self, /, source: typing.BinaryIO) -> int:
|
|
126
|
+
"""mdmd:hidden
|
|
127
|
+
Stream `source` into the process's stdin and close it (EOF) on success.
|
|
128
|
+
|
|
129
|
+
Returns the total bytes streamed.
|
|
130
|
+
"""
|
|
131
|
+
...
|
|
132
|
+
|
|
133
|
+
_stdin_write_stream: ___stdin_write_stream_spec
|
|
134
|
+
|
|
108
135
|
class __poll_spec(typing_extensions.Protocol):
|
|
109
136
|
def __call__(self, /) -> typing.Optional[int]:
|
|
110
137
|
"""Check if the container process has finished running.
|
|
@@ -816,6 +816,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
816
816
|
*args: str,
|
|
817
817
|
app: "modal.app._App | None" = None,
|
|
818
818
|
name: str | None = None,
|
|
819
|
+
tags: dict[str, str] | None = None,
|
|
819
820
|
image: _Image | None = None,
|
|
820
821
|
env: dict[str, str | None] | None = None,
|
|
821
822
|
secrets: Collection[_Secret] | None = None,
|
|
@@ -851,7 +852,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
851
852
|
`secret=...` or `oidc_auth_role_arn`), OIDC identity tokens, proxies, and
|
|
852
853
|
filesystem snapshots.
|
|
853
854
|
|
|
854
|
-
Features like
|
|
855
|
+
Features like memory snapshots, network file systems, GPUs, and custom
|
|
855
856
|
domains are not supported.
|
|
856
857
|
|
|
857
858
|
Set `i6pn=True` to enable private IPv6 networking so sandboxes in the same
|
|
@@ -993,10 +994,12 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
993
994
|
),
|
|
994
995
|
)
|
|
995
996
|
|
|
997
|
+
tag_protos = [api_pb2.SandboxTag(tag_name=k, tag_value=v) for k, v in tags.items()] if tags else []
|
|
996
998
|
create_req = api_pb2.SandboxCreateV2Request(
|
|
997
999
|
app_id=load_context.app_id,
|
|
998
1000
|
definition=definition,
|
|
999
1001
|
ephemeral_secrets=api_pb2.StringMap(contents=env_dict) if env_dict else None,
|
|
1002
|
+
tags=tag_protos,
|
|
1000
1003
|
)
|
|
1001
1004
|
assert load_context.client._auth_token_manager
|
|
1002
1005
|
auth_token = await load_context.client._auth_token_manager.get_token()
|
|
@@ -1267,22 +1270,27 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1267
1270
|
Returns:
|
|
1268
1271
|
Tags as a map from tag name to tag value.
|
|
1269
1272
|
"""
|
|
1270
|
-
self._ensure_v1("get_tags")
|
|
1271
1273
|
req = api_pb2.SandboxTagsGetRequest(sandbox_id=self.object_id)
|
|
1272
|
-
|
|
1274
|
+
stub = self._client.stub
|
|
1275
|
+
if self._is_v2:
|
|
1276
|
+
assert self._client._auth_token_manager
|
|
1277
|
+
auth_token = await self._client._auth_token_manager.get_token()
|
|
1278
|
+
resp = await stub.SandboxTagsGetV2(req, metadata=[("x-modal-auth-token", auth_token)])
|
|
1279
|
+
else:
|
|
1280
|
+
resp = await stub.SandboxTagsGet(req)
|
|
1273
1281
|
|
|
1274
1282
|
return {tag.tag_name: tag.tag_value for tag in resp.tags}
|
|
1275
1283
|
|
|
1276
1284
|
async def set_tags(self, tags: dict[str, str], *, client: _Client | None = None) -> None:
|
|
1277
1285
|
"""Set tags (key-value pairs) on the Sandbox. Tags can be used to filter results in `Sandbox.list`.
|
|
1278
1286
|
|
|
1287
|
+
Setting tags replaces the Sandbox's entire tag set; passing an empty dict clears all tags.
|
|
1288
|
+
|
|
1279
1289
|
Args:
|
|
1280
1290
|
tags: Tag names and values to set on this sandbox.
|
|
1281
1291
|
client: Deprecated. Prefer setting the client when creating or re-attaching to the sandbox.
|
|
1282
1292
|
|
|
1283
1293
|
"""
|
|
1284
|
-
self._ensure_v1("set_tags")
|
|
1285
|
-
environment_name = _get_environment_name()
|
|
1286
1294
|
if client is not None:
|
|
1287
1295
|
deprecation_warning(
|
|
1288
1296
|
(2025, 9, 18),
|
|
@@ -1291,13 +1299,19 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1291
1299
|
)
|
|
1292
1300
|
|
|
1293
1301
|
tags_list = [api_pb2.SandboxTag(tag_name=name, tag_value=value) for name, value in tags.items()]
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
tags=tags_list
|
|
1299
|
-
|
|
1300
|
-
|
|
1302
|
+
stub = self._client.stub
|
|
1303
|
+
if self._is_v2:
|
|
1304
|
+
assert self._client._auth_token_manager
|
|
1305
|
+
auth_token = await self._client._auth_token_manager.get_token()
|
|
1306
|
+
req = api_pb2.SandboxTagsSetRequest(sandbox_id=self.object_id, tags=tags_list)
|
|
1307
|
+
await stub.SandboxTagsSetV2(req, metadata=[("x-modal-auth-token", auth_token)])
|
|
1308
|
+
else:
|
|
1309
|
+
req = api_pb2.SandboxTagsSetRequest(
|
|
1310
|
+
environment_name=_get_environment_name(),
|
|
1311
|
+
sandbox_id=self.object_id,
|
|
1312
|
+
tags=tags_list,
|
|
1313
|
+
)
|
|
1314
|
+
await stub.SandboxTagsSet(req)
|
|
1301
1315
|
|
|
1302
1316
|
async def _experimental_set_outbound_network_policy(
|
|
1303
1317
|
self,
|
|
@@ -361,6 +361,7 @@ class _Sandbox(modal._object._Object):
|
|
|
361
361
|
*args: str,
|
|
362
362
|
app: typing.Optional[modal.app._App] = None,
|
|
363
363
|
name: typing.Optional[str] = None,
|
|
364
|
+
tags: typing.Optional[dict[str, str]] = None,
|
|
364
365
|
image: typing.Optional[modal._image._Image] = None,
|
|
365
366
|
env: typing.Optional[dict[str, typing.Optional[str]]] = None,
|
|
366
367
|
secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
|
|
@@ -399,7 +400,7 @@ class _Sandbox(modal._object._Object):
|
|
|
399
400
|
`secret=...` or `oidc_auth_role_arn`), OIDC identity tokens, proxies, and
|
|
400
401
|
filesystem snapshots.
|
|
401
402
|
|
|
402
|
-
Features like
|
|
403
|
+
Features like memory snapshots, network file systems, GPUs, and custom
|
|
403
404
|
domains are not supported.
|
|
404
405
|
|
|
405
406
|
Set `i6pn=True` to enable private IPv6 networking so sandboxes in the same
|
|
@@ -518,6 +519,8 @@ class _Sandbox(modal._object._Object):
|
|
|
518
519
|
async def set_tags(self, tags: dict[str, str], *, client: typing.Optional[modal.client._Client] = None) -> None:
|
|
519
520
|
"""Set tags (key-value pairs) on the Sandbox. Tags can be used to filter results in `Sandbox.list`.
|
|
520
521
|
|
|
522
|
+
Setting tags replaces the Sandbox's entire tag set; passing an empty dict clears all tags.
|
|
523
|
+
|
|
521
524
|
Args:
|
|
522
525
|
tags: Tag names and values to set on this sandbox.
|
|
523
526
|
client: Deprecated. Prefer setting the client when creating or re-attaching to the sandbox.
|
|
@@ -1711,6 +1714,7 @@ class Sandbox(modal.object.Object):
|
|
|
1711
1714
|
*args: str,
|
|
1712
1715
|
app: typing.Optional[modal.app.App] = None,
|
|
1713
1716
|
name: typing.Optional[str] = None,
|
|
1717
|
+
tags: typing.Optional[dict[str, str]] = None,
|
|
1714
1718
|
image: typing.Optional[modal.image.Image] = None,
|
|
1715
1719
|
env: typing.Optional[dict[str, typing.Optional[str]]] = None,
|
|
1716
1720
|
secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
|
|
@@ -1749,7 +1753,7 @@ class Sandbox(modal.object.Object):
|
|
|
1749
1753
|
`secret=...` or `oidc_auth_role_arn`), OIDC identity tokens, proxies, and
|
|
1750
1754
|
filesystem snapshots.
|
|
1751
1755
|
|
|
1752
|
-
Features like
|
|
1756
|
+
Features like memory snapshots, network file systems, GPUs, and custom
|
|
1753
1757
|
domains are not supported.
|
|
1754
1758
|
|
|
1755
1759
|
Set `i6pn=True` to enable private IPv6 networking so sandboxes in the same
|
|
@@ -1772,6 +1776,7 @@ class Sandbox(modal.object.Object):
|
|
|
1772
1776
|
*args: str,
|
|
1773
1777
|
app: typing.Optional[modal.app.App] = None,
|
|
1774
1778
|
name: typing.Optional[str] = None,
|
|
1779
|
+
tags: typing.Optional[dict[str, str]] = None,
|
|
1775
1780
|
image: typing.Optional[modal.image.Image] = None,
|
|
1776
1781
|
env: typing.Optional[dict[str, typing.Optional[str]]] = None,
|
|
1777
1782
|
secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
|
|
@@ -1810,7 +1815,7 @@ class Sandbox(modal.object.Object):
|
|
|
1810
1815
|
`secret=...` or `oidc_auth_role_arn`), OIDC identity tokens, proxies, and
|
|
1811
1816
|
filesystem snapshots.
|
|
1812
1817
|
|
|
1813
|
-
Features like
|
|
1818
|
+
Features like memory snapshots, network file systems, GPUs, and custom
|
|
1814
1819
|
domains are not supported.
|
|
1815
1820
|
|
|
1816
1821
|
Set `i6pn=True` to enable private IPv6 networking so sandboxes in the same
|
|
@@ -2035,6 +2040,8 @@ class Sandbox(modal.object.Object):
|
|
|
2035
2040
|
def __call__(self, /, tags: dict[str, str], *, client: typing.Optional[modal.client.Client] = None) -> None:
|
|
2036
2041
|
"""Set tags (key-value pairs) on the Sandbox. Tags can be used to filter results in `Sandbox.list`.
|
|
2037
2042
|
|
|
2043
|
+
Setting tags replaces the Sandbox's entire tag set; passing an empty dict clears all tags.
|
|
2044
|
+
|
|
2038
2045
|
Args:
|
|
2039
2046
|
tags: Tag names and values to set on this sandbox.
|
|
2040
2047
|
client: Deprecated. Prefer setting the client when creating or re-attaching to the sandbox.
|
|
@@ -2044,6 +2051,8 @@ class Sandbox(modal.object.Object):
|
|
|
2044
2051
|
async def aio(self, /, tags: dict[str, str], *, client: typing.Optional[modal.client.Client] = None) -> None:
|
|
2045
2052
|
"""Set tags (key-value pairs) on the Sandbox. Tags can be used to filter results in `Sandbox.list`.
|
|
2046
2053
|
|
|
2054
|
+
Setting tags replaces the Sandbox's entire tag set; passing an empty dict clears all tags.
|
|
2055
|
+
|
|
2047
2056
|
Args:
|
|
2048
2057
|
tags: Tag names and values to set on this sandbox.
|
|
2049
2058
|
client: Deprecated. Prefer setting the client when creating or re-attaching to the sandbox.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Copyright Modal Labs 2026
|
|
2
2
|
import asyncio
|
|
3
|
+
import io
|
|
3
4
|
import json
|
|
4
5
|
import os
|
|
5
6
|
import random
|
|
@@ -7,7 +8,7 @@ import string
|
|
|
7
8
|
import time
|
|
8
9
|
import weakref
|
|
9
10
|
from pathlib import Path
|
|
10
|
-
from typing import TYPE_CHECKING, AsyncIterator, Optional, Union, cast
|
|
11
|
+
from typing import TYPE_CHECKING, AsyncIterator, BinaryIO, Optional, Union, cast
|
|
11
12
|
|
|
12
13
|
if TYPE_CHECKING:
|
|
13
14
|
import modal.sandbox
|
|
@@ -33,7 +34,6 @@ from ._utils.sandbox_fs_utils import (
|
|
|
33
34
|
validate_absolute_remote_path,
|
|
34
35
|
)
|
|
35
36
|
from .exception import ConflictError
|
|
36
|
-
from .io_streams import TASK_COMMAND_ROUTER_MAX_BUFFER_SIZE
|
|
37
37
|
from .types import FileInfo, FileType, FileWatchEvent, FileWatchEventType
|
|
38
38
|
|
|
39
39
|
_SANDBOX_FS_TOOLS_PATH = "/__modal/.bin/modal-sandbox-fs-tools"
|
|
@@ -111,69 +111,8 @@ class _SandboxFilesystem:
|
|
|
111
111
|
validate_absolute_remote_path(remote_path, "copy_from_local")
|
|
112
112
|
|
|
113
113
|
t0 = time.monotonic()
|
|
114
|
-
total_bytes = 0
|
|
115
114
|
with open(local_path, "rb") as file_obj:
|
|
116
|
-
|
|
117
|
-
process = await self._container.exec(_SANDBOX_FS_TOOLS_PATH, make_write_file_command(remote_path))
|
|
118
|
-
try:
|
|
119
|
-
while True:
|
|
120
|
-
logger.debug(
|
|
121
|
-
f"sandbox copy_from_local('{local_path}', '{remote_path}'): reading "
|
|
122
|
-
f"{TASK_COMMAND_ROUTER_MAX_BUFFER_SIZE} bytes from {local_path} at offset {total_bytes}"
|
|
123
|
-
)
|
|
124
|
-
# TODO(saltzm): If this fails, the ContainerProcess will remain alive indefinitely since
|
|
125
|
-
# stdin will remain open. Unfortunately we can't just call write_eof either, since that
|
|
126
|
-
# would lead to a partially written file being persisted. We should catch exceptions
|
|
127
|
-
# from this and kill the ContainerProcess when we have a way to do this.
|
|
128
|
-
chunk = file_obj.read(TASK_COMMAND_ROUTER_MAX_BUFFER_SIZE)
|
|
129
|
-
if not chunk:
|
|
130
|
-
logger.debug(
|
|
131
|
-
f"sandbox copy_from_local('{local_path}', '{remote_path}'): read no data from "
|
|
132
|
-
f"{local_path}, finished reading file"
|
|
133
|
-
)
|
|
134
|
-
break
|
|
135
|
-
total_bytes += len(chunk)
|
|
136
|
-
logger.debug(
|
|
137
|
-
f"sandbox copy_from_local('{local_path}', '{remote_path}'): writing {len(chunk)} bytes "
|
|
138
|
-
f"from {local_path} to remote {remote_path}"
|
|
139
|
-
)
|
|
140
|
-
process.stdin.write(chunk)
|
|
141
|
-
await process.stdin.drain()
|
|
142
|
-
logger.debug(
|
|
143
|
-
f"sandbox copy_from_local('{local_path}', '{remote_path}'): finished writing "
|
|
144
|
-
f"{len(chunk)} bytes from {local_path} to remote {remote_path}"
|
|
145
|
-
)
|
|
146
|
-
logger.debug(
|
|
147
|
-
f"sandbox copy_from_local('{local_path}', '{remote_path}'): writing eof to remote {remote_path}"
|
|
148
|
-
)
|
|
149
|
-
process.stdin.write_eof()
|
|
150
|
-
await process.stdin.drain()
|
|
151
|
-
logger.debug(
|
|
152
|
-
f"sandbox copy_from_local('{local_path}', '{remote_path}'): finished writing eof to remote "
|
|
153
|
-
f"{remote_path}"
|
|
154
|
-
)
|
|
155
|
-
# When the FS tools binary exits early on an error, the worker
|
|
156
|
-
# reports the dropped stdin write as ConflictError.
|
|
157
|
-
except ConflictError:
|
|
158
|
-
pass
|
|
159
|
-
|
|
160
|
-
async def read_stderr():
|
|
161
|
-
stderr = await process.stderr.read()
|
|
162
|
-
logger.debug(f"sandbox copy_from_local('{local_path}', '{remote_path}'): finished reading stderr")
|
|
163
|
-
return stderr
|
|
164
|
-
|
|
165
|
-
async def wait_for_process_completion():
|
|
166
|
-
returncode = await process.wait()
|
|
167
|
-
logger.debug(
|
|
168
|
-
f"sandbox copy_from_local('{local_path}', '{remote_path}'): finished waiting for process "
|
|
169
|
-
"completion"
|
|
170
|
-
)
|
|
171
|
-
return returncode
|
|
172
|
-
|
|
173
|
-
stderr, returncode = await asyncio.gather(read_stderr(), wait_for_process_completion())
|
|
174
|
-
|
|
175
|
-
if returncode != 0:
|
|
176
|
-
raise_write_file_error(returncode, stderr, remote_path)
|
|
115
|
+
total_bytes = await self._exec_fs_tool_write(file_obj, remote_path, "copy_from_local")
|
|
177
116
|
|
|
178
117
|
dur_s = max(time.monotonic() - t0, 0.001)
|
|
179
118
|
_log_throughput(f"copy_from_local('{local_path}', '{remote_path}')", total_bytes, dur_s)
|
|
@@ -638,22 +577,7 @@ class _SandboxFilesystem:
|
|
|
638
577
|
raise TypeError("data must be bytes-like")
|
|
639
578
|
|
|
640
579
|
t0 = time.monotonic()
|
|
641
|
-
|
|
642
|
-
process = await self._container.exec(_SANDBOX_FS_TOOLS_PATH, make_write_file_command(remote_path))
|
|
643
|
-
try:
|
|
644
|
-
for offset in range(0, max(len(data), 1), TASK_COMMAND_ROUTER_MAX_BUFFER_SIZE):
|
|
645
|
-
process.stdin.write(data[offset : offset + TASK_COMMAND_ROUTER_MAX_BUFFER_SIZE])
|
|
646
|
-
await process.stdin.drain()
|
|
647
|
-
process.stdin.write_eof()
|
|
648
|
-
await process.stdin.drain()
|
|
649
|
-
# When the FS tools binary exits early on an error, the worker
|
|
650
|
-
# reports the dropped stdin write as ConflictError.
|
|
651
|
-
except ConflictError:
|
|
652
|
-
pass
|
|
653
|
-
stderr, returncode = await asyncio.gather(process.stderr.read(), process.wait())
|
|
654
|
-
|
|
655
|
-
if returncode != 0:
|
|
656
|
-
raise_write_file_error(returncode, stderr, remote_path)
|
|
580
|
+
await self._exec_fs_tool_write(io.BytesIO(data), remote_path, "write_bytes")
|
|
657
581
|
|
|
658
582
|
dur_s = max(time.monotonic() - t0, 0.001)
|
|
659
583
|
_log_throughput(f"write_bytes {remote_path}", len(data), dur_s)
|
|
@@ -686,5 +610,33 @@ class _SandboxFilesystem:
|
|
|
686
610
|
raise TypeError("data must be str")
|
|
687
611
|
await self.write_bytes(data.encode("utf-8"), remote_path)
|
|
688
612
|
|
|
613
|
+
async def _exec_fs_tool_write(self, source: BinaryIO, remote_path: str, op_name: str) -> int:
|
|
614
|
+
"""Exec the FS-tools write command and stream `source` into its stdin.
|
|
615
|
+
|
|
616
|
+
Returns the number of bytes streamed.
|
|
617
|
+
"""
|
|
618
|
+
with translate_exec_errors(op_name, remote_path):
|
|
619
|
+
process = await self._container.exec(_SANDBOX_FS_TOOLS_PATH, make_write_file_command(remote_path))
|
|
620
|
+
# TODO(saltzm): If streaming fails after resume attempts are exhausted, the
|
|
621
|
+
# ContainerProcess will remain alive indefinitely since stdin will remain open.
|
|
622
|
+
# We should catch exceptions from this and kill the ContainerProcess when we
|
|
623
|
+
# have a way to do this.
|
|
624
|
+
try:
|
|
625
|
+
total_bytes = await process._stdin_write_stream(source)
|
|
626
|
+
# When the FS tools binary exits early on an error, the worker
|
|
627
|
+
# reports the dropped stdin write as ConflictError.
|
|
628
|
+
except ConflictError:
|
|
629
|
+
# ConflictError can come from a failure in fs-tools or server-side.
|
|
630
|
+
# - if server-side, the process won't exit, so the gather below would hang forever -> raise
|
|
631
|
+
# - else if fs-tools, process will be closed, use raise_write_file_error below
|
|
632
|
+
if await process.poll() is None:
|
|
633
|
+
raise
|
|
634
|
+
total_bytes = source.tell()
|
|
635
|
+
stderr, returncode = await asyncio.gather(process.stderr.read(), process.wait())
|
|
636
|
+
|
|
637
|
+
if returncode != 0:
|
|
638
|
+
raise_write_file_error(returncode, stderr, remote_path)
|
|
639
|
+
return total_bytes
|
|
640
|
+
|
|
689
641
|
|
|
690
642
|
SandboxFilesystem = synchronize_api(_SandboxFilesystem)
|
|
@@ -348,6 +348,13 @@ class _SandboxFilesystem:
|
|
|
348
348
|
"""
|
|
349
349
|
...
|
|
350
350
|
|
|
351
|
+
async def _exec_fs_tool_write(self, source: typing.BinaryIO, remote_path: str, op_name: str) -> int:
|
|
352
|
+
"""Exec the FS-tools write command and stream `source` into its stdin.
|
|
353
|
+
|
|
354
|
+
Returns the number of bytes streamed.
|
|
355
|
+
"""
|
|
356
|
+
...
|
|
357
|
+
|
|
351
358
|
class SandboxFilesystem:
|
|
352
359
|
"""mdmd:namespace
|
|
353
360
|
Namespace for Sandbox filesystem APIs.
|
|
@@ -1041,3 +1048,20 @@ class SandboxFilesystem:
|
|
|
1041
1048
|
...
|
|
1042
1049
|
|
|
1043
1050
|
write_text: __write_text_spec
|
|
1051
|
+
|
|
1052
|
+
class ___exec_fs_tool_write_spec(typing_extensions.Protocol):
|
|
1053
|
+
def __call__(self, /, source: typing.BinaryIO, remote_path: str, op_name: str) -> int:
|
|
1054
|
+
"""Exec the FS-tools write command and stream `source` into its stdin.
|
|
1055
|
+
|
|
1056
|
+
Returns the number of bytes streamed.
|
|
1057
|
+
"""
|
|
1058
|
+
...
|
|
1059
|
+
|
|
1060
|
+
async def aio(self, /, source: typing.BinaryIO, remote_path: str, op_name: str) -> int:
|
|
1061
|
+
"""Exec the FS-tools write command and stream `source` into its stdin.
|
|
1062
|
+
|
|
1063
|
+
Returns the number of bytes streamed.
|
|
1064
|
+
"""
|
|
1065
|
+
...
|
|
1066
|
+
|
|
1067
|
+
_exec_fs_tool_write: ___exec_fs_tool_write_spec
|