modal 1.5.2.dev20__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.dev20 → modal-1.5.2.dev22}/PKG-INFO +1 -1
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/task_command_router_client.py +127 -3
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/_help.py +7 -1
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/container.py +10 -2
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/curl.py +6 -4
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/shell.py +6 -2
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/client.pyi +2 -2
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/container_process.py +13 -1
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/container_process.pyi +27 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/sandbox.py +73 -17
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/sandbox.pyi +126 -18
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/sandbox_fs.py +32 -80
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/sandbox_fs.pyi +24 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/types.py +7 -1
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/volume.py +26 -1
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/volume.pyi +26 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_docs/mdmd/mdmd.py +34 -11
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/api_grpc.py +64 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/api_pb2.py +955 -855
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/api_pb2.pyi +218 -4
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/api_pb2_grpc.py +133 -1
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/api_pb2_grpc.pyi +43 -3
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/modal_api_grpc.py +4 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_version/__init__.py +1 -1
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/LICENSE +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/README.md +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/__init__.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/__main__.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_billing.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_clustered_functions.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_clustered_functions.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_container_entrypoint.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_environments.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_function_variants.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_functions.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_grpc_client.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_image.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_ipython.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_load_context.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_location.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_logs.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_object.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_output/__init__.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_output/manager.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_output/pty.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_output/rich.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_output/status.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_partial_function.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_resolver.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_resources.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/__init__.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/asgi.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/execution_context.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/telemetry.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_serialization.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_server.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_traceback.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_tunnel.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_tunnel.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_type_manager.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/__init__.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/app_utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/async_utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/blob_utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/browser_utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/curl_utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/deprecation.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/docker_utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/function_utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/git_utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/hash_utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/http_utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/logger.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/mount_utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/name_utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/package_utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/proxy_support.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/shell_utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/time_utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_vendor/__init__.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_vendor/tblib.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_vendor/version.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_watcher.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_workspace.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/app.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/app.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/billing.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/billing.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/builder/2023.12.txt +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/builder/2024.04.txt +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/builder/2024.10.txt +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/builder/2025.06.txt +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/builder/README.md +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/builder/base-images.json +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/call_graph.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/__init__.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/_download.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/_traceback.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/app.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/billing.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/bootstrap.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/changelog.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/cluster.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/config.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/dashboard.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/dict.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/endpoint.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/entry_point.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/environment.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/image.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/import_refs.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/launch.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/logo.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/network_file_system.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/profile.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/programs/__init__.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/programs/vscode.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/queues.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/run.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/secret.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/selector.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/skills.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/token.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/utils.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/volume.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/workspace.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/client.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cls.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cls.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/config.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/dict.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/dict.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/environments.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/environments.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/exception.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/experimental/__init__.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/experimental/flash.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/experimental/flash.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/experimental/ipython.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/file_io.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/file_io.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/file_pattern_matcher.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/functions.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/functions.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/image.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/image.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/io_streams.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/io_streams.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/mount.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/mount.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/network_file_system.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/network_file_system.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/object.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/object.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/output.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/parallel_map.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/parallel_map.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/partial_function.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/partial_function.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/proxy.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/proxy.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/py.typed +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/queue.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/queue.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/retries.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/runner.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/runner.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/running_app.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/schedule.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/scheduler_placement.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/secret.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/secret.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/server.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/server.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/serving.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/serving.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/snapshot.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/snapshot.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/stream_type.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/token_flow.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/token_flow.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/workspace.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/workspace.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal.egg-info/requires.txt +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal.egg-info/top_level.txt +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_docs/__init__.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_docs/mdmd/types.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/__init__.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/py.typed +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_version/__main__.py +0 -0
- {modal-1.5.2.dev20 → modal-1.5.2.dev22}/pyproject.toml +0 -0
- {modal-1.5.2.dev20 → 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
|
|
|
@@ -269,15 +269,23 @@ async def _exec_impl(
|
|
|
269
269
|
pty: bool | None = None,
|
|
270
270
|
container_id: str = "",
|
|
271
271
|
command: tuple[str, ...] = (),
|
|
272
|
+
object_id_for_v2: str | None = None,
|
|
272
273
|
):
|
|
273
|
-
"""Execute a command in a container (implementation).
|
|
274
|
+
"""Execute a command in a container (implementation).
|
|
275
|
+
|
|
276
|
+
For tasks belonging to V2 sandboxes, `object_id_for_v2` must be set to the
|
|
277
|
+
sandbox ID.
|
|
278
|
+
"""
|
|
274
279
|
|
|
275
280
|
if pty is None:
|
|
276
281
|
pty = is_tty()
|
|
277
282
|
|
|
278
283
|
client = await _Client.from_env()
|
|
279
284
|
|
|
280
|
-
|
|
285
|
+
if object_id_for_v2 is not None:
|
|
286
|
+
command_router_client = await TaskCommandRouterClient.init_v2(client, object_id_for_v2, container_id)
|
|
287
|
+
else:
|
|
288
|
+
command_router_client = await TaskCommandRouterClient.init(client, container_id)
|
|
281
289
|
|
|
282
290
|
process_id = str(uuid.uuid4())
|
|
283
291
|
|
|
@@ -117,10 +117,12 @@ async def curl(curl_args: tuple[str, ...]):
|
|
|
117
117
|
All arguments are passed directly to `curl`, which must be installed locally.
|
|
118
118
|
|
|
119
119
|
Examples:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
modal curl https://user--my-app.us-west.modal.direct
|
|
123
|
+
modal curl -X GET https://user--my-app.us-west.modal.direct
|
|
124
|
+
```
|
|
125
|
+
|
|
124
126
|
"""
|
|
125
127
|
url = find_url(curl_args)
|
|
126
128
|
if url is None:
|
|
@@ -17,7 +17,7 @@ from ..functions import Function
|
|
|
17
17
|
from ..image import Image
|
|
18
18
|
from ..mount import _Mount
|
|
19
19
|
from ..runner import interactive_shell
|
|
20
|
-
from ..sandbox import _MAIN_CONTAINER_NAME, Sandbox
|
|
20
|
+
from ..sandbox import _MAIN_CONTAINER_NAME, Sandbox, _is_v2_sandbox_id
|
|
21
21
|
from ..secret import Secret
|
|
22
22
|
from ..stream_type import StreamType
|
|
23
23
|
from ..volume import Volume
|
|
@@ -135,6 +135,8 @@ def _start_shell_in_running_container(ref: str, cmd: str, pty: bool) -> None:
|
|
|
135
135
|
if container_name is not None and container_name != _MAIN_CONTAINER_NAME:
|
|
136
136
|
return _start_shell_in_sidecar_container(sandbox_id, container_name, cmd, pty)
|
|
137
137
|
|
|
138
|
+
object_id_for_v2: str | None = None
|
|
139
|
+
|
|
138
140
|
if _is_valid_modal_id(sandbox_id, "sb-"):
|
|
139
141
|
try:
|
|
140
142
|
sandbox = Sandbox.from_id(sandbox_id)
|
|
@@ -143,10 +145,12 @@ def _start_shell_in_running_container(ref: str, cmd: str, pty: bool) -> None:
|
|
|
143
145
|
raise ClickException(f"Sandbox '{sandbox_id}' not found (is it still running?)")
|
|
144
146
|
except Exception as e:
|
|
145
147
|
raise ClickException(f"Error connecting to Sandbox '{sandbox_id}': {str(e)}")
|
|
148
|
+
if _is_v2_sandbox_id(sandbox_id):
|
|
149
|
+
object_id_for_v2 = sandbox_id
|
|
146
150
|
|
|
147
151
|
assert _is_valid_modal_id(ref, "ta-")
|
|
148
152
|
try:
|
|
149
|
-
_exec_impl(container_id=ref, command=shlex.split(cmd), pty=pty)
|
|
153
|
+
_exec_impl(container_id=ref, command=shlex.split(cmd), pty=pty, object_id_for_v2=object_id_for_v2)
|
|
150
154
|
except NotFoundError:
|
|
151
155
|
raise ClickException(f"Container '{ref}' not found (is it still running?)")
|
|
152
156
|
except Exception as e:
|
|
@@ -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
|
|
@@ -861,9 +862,10 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
861
862
|
(e.g. `region="us-east-1"`).
|
|
862
863
|
|
|
863
864
|
V2 sandboxes created with this method are not currently returned by
|
|
864
|
-
`Sandbox.list()
|
|
865
|
-
|
|
866
|
-
use `Sandbox.from_id(sandbox.object_id)` to
|
|
865
|
+
`Sandbox.list()`. A named sandbox can be looked up with
|
|
866
|
+
`Sandbox._experimental_from_name(app_name, name)`; otherwise store
|
|
867
|
+
`sandbox.object_id` and use `Sandbox.from_id(sandbox.object_id)` to
|
|
868
|
+
reattach.
|
|
867
869
|
"""
|
|
868
870
|
from .app import _App
|
|
869
871
|
|
|
@@ -992,10 +994,12 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
992
994
|
),
|
|
993
995
|
)
|
|
994
996
|
|
|
997
|
+
tag_protos = [api_pb2.SandboxTag(tag_name=k, tag_value=v) for k, v in tags.items()] if tags else []
|
|
995
998
|
create_req = api_pb2.SandboxCreateV2Request(
|
|
996
999
|
app_id=load_context.app_id,
|
|
997
1000
|
definition=definition,
|
|
998
1001
|
ephemeral_secrets=api_pb2.StringMap(contents=env_dict) if env_dict else None,
|
|
1002
|
+
tags=tag_protos,
|
|
999
1003
|
)
|
|
1000
1004
|
assert load_context.client._auth_token_manager
|
|
1001
1005
|
auth_token = await load_context.client._auth_token_manager.get_token()
|
|
@@ -1186,6 +1190,44 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1186
1190
|
resp = await client.stub.SandboxGetFromName(req)
|
|
1187
1191
|
return _Sandbox._new_hydrated(resp.sandbox_id, client, None)
|
|
1188
1192
|
|
|
1193
|
+
@staticmethod
|
|
1194
|
+
async def _experimental_from_name(
|
|
1195
|
+
app_name: str,
|
|
1196
|
+
name: str,
|
|
1197
|
+
*,
|
|
1198
|
+
environment_name: str | None = None,
|
|
1199
|
+
client: _Client | None = None,
|
|
1200
|
+
) -> "_Sandbox":
|
|
1201
|
+
"""Get a running V2 Sandbox by name from a deployed App.
|
|
1202
|
+
|
|
1203
|
+
This looks up V2 sandboxes, ie sandboxes created via `modal.Sandbox._experimental_create`.
|
|
1204
|
+
|
|
1205
|
+
Args:
|
|
1206
|
+
app_name: Name of the deployed app to look up the sandbox under.
|
|
1207
|
+
name: Sandbox name to resolve.
|
|
1208
|
+
environment_name: Optional environment name for the lookup; defaults to the configured environment.
|
|
1209
|
+
client: Modal client to use for the RPC; defaults to `Client.from_env()` when omitted.
|
|
1210
|
+
|
|
1211
|
+
Returns:
|
|
1212
|
+
A `Sandbox` handle for the running sandbox.
|
|
1213
|
+
|
|
1214
|
+
Raises:
|
|
1215
|
+
NotFoundError: If no running sandbox exists with the given name.
|
|
1216
|
+
"""
|
|
1217
|
+
if client is None:
|
|
1218
|
+
client = await _Client.from_env()
|
|
1219
|
+
env_name = _get_environment_name(environment_name)
|
|
1220
|
+
|
|
1221
|
+
req = api_pb2.SandboxGetFromNameRequest(sandbox_name=name, app_name=app_name, environment_name=env_name)
|
|
1222
|
+
assert client._auth_token_manager
|
|
1223
|
+
auth_token = await client._auth_token_manager.get_token()
|
|
1224
|
+
resp = await client.stub.SandboxGetFromNameV2(req, metadata=[("x-modal-auth-token", auth_token)])
|
|
1225
|
+
|
|
1226
|
+
obj = _Sandbox._new_hydrated(resp.sandbox_id, client, None)
|
|
1227
|
+
obj._is_v2 = True
|
|
1228
|
+
obj._hydrate_metadata_v2()
|
|
1229
|
+
return obj
|
|
1230
|
+
|
|
1189
1231
|
@staticmethod
|
|
1190
1232
|
async def from_id(sandbox_id: str, client: _Client | None = None) -> "_Sandbox":
|
|
1191
1233
|
"""Construct a Sandbox from an id and look up the Sandbox result.
|
|
@@ -1228,22 +1270,27 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1228
1270
|
Returns:
|
|
1229
1271
|
Tags as a map from tag name to tag value.
|
|
1230
1272
|
"""
|
|
1231
|
-
self._ensure_v1("get_tags")
|
|
1232
1273
|
req = api_pb2.SandboxTagsGetRequest(sandbox_id=self.object_id)
|
|
1233
|
-
|
|
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)
|
|
1234
1281
|
|
|
1235
1282
|
return {tag.tag_name: tag.tag_value for tag in resp.tags}
|
|
1236
1283
|
|
|
1237
1284
|
async def set_tags(self, tags: dict[str, str], *, client: _Client | None = None) -> None:
|
|
1238
1285
|
"""Set tags (key-value pairs) on the Sandbox. Tags can be used to filter results in `Sandbox.list`.
|
|
1239
1286
|
|
|
1287
|
+
Setting tags replaces the Sandbox's entire tag set; passing an empty dict clears all tags.
|
|
1288
|
+
|
|
1240
1289
|
Args:
|
|
1241
1290
|
tags: Tag names and values to set on this sandbox.
|
|
1242
1291
|
client: Deprecated. Prefer setting the client when creating or re-attaching to the sandbox.
|
|
1243
1292
|
|
|
1244
1293
|
"""
|
|
1245
|
-
self._ensure_v1("set_tags")
|
|
1246
|
-
environment_name = _get_environment_name()
|
|
1247
1294
|
if client is not None:
|
|
1248
1295
|
deprecation_warning(
|
|
1249
1296
|
(2025, 9, 18),
|
|
@@ -1252,13 +1299,19 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1252
1299
|
)
|
|
1253
1300
|
|
|
1254
1301
|
tags_list = [api_pb2.SandboxTag(tag_name=name, tag_value=value) for name, value in tags.items()]
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
tags=tags_list
|
|
1260
|
-
|
|
1261
|
-
|
|
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)
|
|
1262
1315
|
|
|
1263
1316
|
async def _experimental_set_outbound_network_policy(
|
|
1264
1317
|
self,
|
|
@@ -2279,15 +2332,15 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
2279
2332
|
|
|
2280
2333
|
@staticmethod
|
|
2281
2334
|
async def _experimental_list(
|
|
2282
|
-
*, app_id: str | None = None, client: _Client | None = None
|
|
2335
|
+
*, app_id: str | None = None, tags: dict[str, str] | None = None, client: _Client | None = None
|
|
2283
2336
|
) -> AsyncGenerator["_Sandbox", None]:
|
|
2284
2337
|
"""List v2 Sandboxes in an App.
|
|
2285
2338
|
|
|
2286
2339
|
This function lists v2 sandboxes, ie sandboxes created via modal.Sandbox._experimental_create.
|
|
2287
|
-
Filtering based on tags is not yet supported.
|
|
2288
2340
|
|
|
2289
2341
|
Args:
|
|
2290
2342
|
app_id: The App to list Sandboxes under.
|
|
2343
|
+
tags: If set, only sandboxes containing at least these tags are returned.
|
|
2291
2344
|
client: Optional client to use for the session.
|
|
2292
2345
|
|
|
2293
2346
|
Yields:
|
|
@@ -2304,12 +2357,15 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
2304
2357
|
if client is None:
|
|
2305
2358
|
client = await _Client.from_env()
|
|
2306
2359
|
|
|
2360
|
+
tags_list = [api_pb2.SandboxTag(tag_name=name, tag_value=value) for name, value in tags.items()] if tags else []
|
|
2361
|
+
|
|
2307
2362
|
assert client._auth_token_manager
|
|
2308
2363
|
while True:
|
|
2309
2364
|
req = api_pb2.SandboxListRequest(
|
|
2310
2365
|
app_id=app_id,
|
|
2311
2366
|
before_timestamp=before_timestamp,
|
|
2312
2367
|
include_finished=False,
|
|
2368
|
+
tags=tags_list,
|
|
2313
2369
|
)
|
|
2314
2370
|
|
|
2315
2371
|
# Fetches a batch of sandboxes. SandboxListV2 authenticates via the
|