modal 1.4.4.dev9__tar.gz → 1.4.4.dev10__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.4.dev9 → modal-1.4.4.dev10}/PKG-INFO +1 -1
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/task_command_router_client.py +154 -38
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/app.pyi +1 -1
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/client.pyi +2 -2
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/container_process.py +5 -5
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/functions.pyi +1 -1
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/image.pyi +1 -1
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/io_streams.py +130 -51
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/io_streams.pyi +179 -24
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/sandbox.py +80 -6
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/sandbox.pyi +64 -9
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_version/__init__.py +1 -1
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/LICENSE +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/README.md +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/__init__.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/__main__.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_billing.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_clustered_functions.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_clustered_functions.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_container_entrypoint.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_environments.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_function_variants.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_functions.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_grpc_client.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_ipython.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_load_context.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_location.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_logs.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_object.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_output/__init__.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_output/manager.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_output/pty.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_output/rich.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_output/status.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_partial_function.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_resolver.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_resources.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_runtime/__init__.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_runtime/asgi.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_runtime/execution_context.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_runtime/telemetry.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_serialization.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_server.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_traceback.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_tunnel.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_tunnel.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_type_manager.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/__init__.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/app_utils.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/async_utils.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/blob_utils.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/browser_utils.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/deprecation.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/docker_utils.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/function_utils.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/git_utils.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/hash_utils.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/http_utils.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/logger.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/mount_utils.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/name_utils.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/package_utils.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/shell_utils.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_utils/time_utils.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_vendor/__init__.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_vendor/tblib.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_vendor/version.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/_watcher.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/app.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/billing.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/builder/2023.12.txt +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/builder/2024.04.txt +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/builder/2024.10.txt +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/builder/2025.06.txt +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/builder/README.md +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/builder/base-images.json +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/call_graph.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/__init__.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/_download.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/_help.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/_traceback.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/app.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/billing.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/bootstrap.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/changelog.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/cluster.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/config.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/container.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/dashboard.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/dict.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/entry_point.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/environment.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/import_refs.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/launch.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/logo.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/network_file_system.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/profile.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/programs/__init__.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/programs/vscode.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/queues.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/run.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/secret.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/selector.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/shell.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/token.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/utils.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cli/volume.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/client.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cls.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/cls.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/config.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/container_process.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/dict.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/dict.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/environments.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/environments.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/exception.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/experimental/__init__.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/experimental/flash.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/experimental/flash.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/experimental/ipython.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/file_io.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/file_io.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/file_pattern_matcher.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/functions.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/image.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/mount.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/mount.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/network_file_system.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/network_file_system.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/object.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/object.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/output.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/parallel_map.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/parallel_map.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/partial_function.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/partial_function.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/proxy.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/proxy.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/py.typed +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/queue.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/queue.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/retries.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/runner.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/runner.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/running_app.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/sandbox_fs.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/sandbox_fs.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/schedule.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/scheduler_placement.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/secret.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/secret.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/server.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/server.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/serving.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/serving.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/snapshot.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/snapshot.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/stream_type.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/token_flow.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/token_flow.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/volume.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal/volume.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal.egg-info/requires.txt +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal.egg-info/top_level.txt +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_docs/__init__.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_proto/__init__.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_proto/api_grpc.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_proto/api_pb2.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_proto/api_pb2.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_proto/py.typed +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/modal_version/__main__.py +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/pyproject.toml +0 -0
- {modal-1.4.4.dev9 → modal-1.4.4.dev10}/setup.cfg +0 -0
|
@@ -9,7 +9,7 @@ import typing
|
|
|
9
9
|
import urllib.parse
|
|
10
10
|
import weakref
|
|
11
11
|
from contextlib import suppress
|
|
12
|
-
from typing import AsyncGenerator, Optional
|
|
12
|
+
from typing import AsyncGenerator, Callable, Optional, TypeVar
|
|
13
13
|
|
|
14
14
|
import grpclib.client
|
|
15
15
|
import grpclib.config
|
|
@@ -146,6 +146,10 @@ async def call_with_retries_on_transient_errors(
|
|
|
146
146
|
await sleep_and_advance(e)
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
_StdioReq = TypeVar("_StdioReq")
|
|
150
|
+
_StdioResp = TypeVar("_StdioResp", sr_pb2.TaskExecStdioReadResponse, sr_pb2.SandboxStdioReadV2Response)
|
|
151
|
+
|
|
152
|
+
|
|
149
153
|
async def fetch_command_router_access(server_client, task_id: str) -> api_pb2.TaskGetCommandRouterAccessResponse:
|
|
150
154
|
"""Fetch direct command router access info from Modal server."""
|
|
151
155
|
return await server_client.stub.TaskGetCommandRouterAccess(
|
|
@@ -383,7 +387,7 @@ class TaskCommandRouterClient:
|
|
|
383
387
|
file_descriptor: "api_pb2.FileDescriptor.ValueType",
|
|
384
388
|
deadline: Optional[float] = None,
|
|
385
389
|
) -> AsyncGenerator[sr_pb2.TaskExecStdioReadResponse, None]:
|
|
386
|
-
"""Stream stdout/stderr batches from
|
|
390
|
+
"""Stream stdout/stderr batches from an exec'd command, retrying on transient errors.
|
|
387
391
|
|
|
388
392
|
Args:
|
|
389
393
|
task_id: The task ID of the task running the exec'd command.
|
|
@@ -413,6 +417,39 @@ class TaskCommandRouterClient:
|
|
|
413
417
|
async for item in stream:
|
|
414
418
|
yield item
|
|
415
419
|
|
|
420
|
+
async def sandbox_stdio_read(
|
|
421
|
+
self,
|
|
422
|
+
task_id: str,
|
|
423
|
+
# Quotes around the type required for protobuf 3.19.
|
|
424
|
+
file_descriptor: "api_pb2.FileDescriptor.ValueType",
|
|
425
|
+
) -> AsyncGenerator[sr_pb2.SandboxStdioReadV2Response, None]:
|
|
426
|
+
"""Stream stdout/stderr batches from a V2 sandbox.
|
|
427
|
+
|
|
428
|
+
Serves both live reads and post-exit reads.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
task_id: The task ID hosting the V2 sandbox.
|
|
432
|
+
file_descriptor: The file descriptor to read from (stdout or stderr).
|
|
433
|
+
Returns:
|
|
434
|
+
AsyncGenerator[sr_pb2.SandboxStdioReadV2Response, None]: A stream of stdout/stderr batches.
|
|
435
|
+
Raises:
|
|
436
|
+
Errors: If retries are exhausted on transient errors or if there is
|
|
437
|
+
an error from the RPC itself.
|
|
438
|
+
"""
|
|
439
|
+
if file_descriptor == api_pb2.FILE_DESCRIPTOR_STDOUT:
|
|
440
|
+
sr_fd = sr_pb2.SANDBOX_STDIO_FILE_DESCRIPTOR_STDOUT
|
|
441
|
+
elif file_descriptor == api_pb2.FILE_DESCRIPTOR_STDERR:
|
|
442
|
+
sr_fd = sr_pb2.SANDBOX_STDIO_FILE_DESCRIPTOR_STDERR
|
|
443
|
+
elif file_descriptor == api_pb2.FILE_DESCRIPTOR_INFO or file_descriptor == api_pb2.FILE_DESCRIPTOR_UNSPECIFIED:
|
|
444
|
+
raise ValueError(f"Unsupported file descriptor: {file_descriptor}")
|
|
445
|
+
else:
|
|
446
|
+
raise ValueError(f"Invalid file descriptor: {file_descriptor}")
|
|
447
|
+
|
|
448
|
+
with grpc_error_converter():
|
|
449
|
+
async with aclosing(self._stream_sandbox_stdio(task_id, sr_fd)) as stream:
|
|
450
|
+
async for item in stream:
|
|
451
|
+
yield item
|
|
452
|
+
|
|
416
453
|
async def exec_stdin_write(
|
|
417
454
|
self, task_id: str, exec_id: str, offset: int, data: bytes, eof: bool
|
|
418
455
|
) -> sr_pb2.TaskExecStdinWriteResponse:
|
|
@@ -434,6 +471,25 @@ class TaskCommandRouterClient:
|
|
|
434
471
|
lambda: self._call_with_auth_retry(self._stub.TaskExecStdinWrite, request)
|
|
435
472
|
)
|
|
436
473
|
|
|
474
|
+
async def sandbox_stdin_write_v2(
|
|
475
|
+
self, task_id: str, offset: int, data: bytes, eof: bool
|
|
476
|
+
) -> sr_pb2.SandboxStdinWriteV2Response:
|
|
477
|
+
"""Write to the stdin stream of a V2 sandbox's entrypoint process.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
task_id: The task ID of the V2 sandbox.
|
|
481
|
+
offset: The offset to start writing to.
|
|
482
|
+
eof: Whether to close the stdin stream after writing the data.
|
|
483
|
+
Raises:
|
|
484
|
+
Other errors: If retries are exhausted on transient errors or if there's an error
|
|
485
|
+
from the RPC itself.
|
|
486
|
+
"""
|
|
487
|
+
request = sr_pb2.SandboxStdinWriteV2Request(task_id=task_id, offset=offset, data=data, eof=eof)
|
|
488
|
+
with grpc_error_converter():
|
|
489
|
+
return await call_with_retries_on_transient_errors(
|
|
490
|
+
lambda: self._call_with_auth_retry(self._stub.SandboxStdinWriteV2, request)
|
|
491
|
+
)
|
|
492
|
+
|
|
437
493
|
async def exec_poll(
|
|
438
494
|
self, task_id: str, exec_id: str, deadline: Optional[float] = None
|
|
439
495
|
) -> sr_pb2.TaskExecPollResponse:
|
|
@@ -558,16 +614,23 @@ class TaskCommandRouterClient:
|
|
|
558
614
|
return await func(*args, **kwargs, metadata=self._get_metadata())
|
|
559
615
|
raise
|
|
560
616
|
|
|
561
|
-
async def
|
|
617
|
+
async def _stream_stdio_with_retries(
|
|
562
618
|
self,
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
619
|
+
*,
|
|
620
|
+
stub_method: "grpclib.client.UnaryStreamMethod[_StdioReq, _StdioResp]",
|
|
621
|
+
request_factory: Callable[[int], _StdioReq],
|
|
622
|
+
deadline_label: str,
|
|
567
623
|
deadline: Optional[float] = None,
|
|
568
|
-
) -> AsyncGenerator[
|
|
569
|
-
"""
|
|
570
|
-
|
|
624
|
+
) -> AsyncGenerator[_StdioResp, None]:
|
|
625
|
+
"""Drive a streaming-stdio RPC with offset bookkeeping, transient-error
|
|
626
|
+
retries, and JWT-refresh auth retries.
|
|
627
|
+
|
|
628
|
+
Shared by [`_stream_stdio`] (exec stdio) and [`_stream_sandbox_stdio`]
|
|
629
|
+
(V2 sandbox top-level stdio); both response types have a ``bytes data``
|
|
630
|
+
field that this helper uses to advance the offset. For V2 sandbox
|
|
631
|
+
responses (which carry ``starting_offset``), the offset is rebased off
|
|
632
|
+
the first chunk of each attempt so transient reconnects don't miss
|
|
633
|
+
bytes.
|
|
571
634
|
"""
|
|
572
635
|
offset = 0
|
|
573
636
|
delay_secs = self.stream_stdio_retry_delay_secs
|
|
@@ -581,7 +644,7 @@ class TaskCommandRouterClient:
|
|
|
581
644
|
nonlocal delay_secs, num_retries_remaining
|
|
582
645
|
logger.debug(f"Retrying stdio read with delay {delay_secs}s due to error: {e}")
|
|
583
646
|
if deadline is not None and deadline - time.monotonic() <= delay_secs:
|
|
584
|
-
raise ExecTimeoutError(f"Deadline exceeded while streaming stdio for
|
|
647
|
+
raise ExecTimeoutError(f"Deadline exceeded while streaming stdio for {deadline_label}")
|
|
585
648
|
|
|
586
649
|
await asyncio.sleep(delay_secs)
|
|
587
650
|
delay_secs *= delay_factor
|
|
@@ -590,18 +653,14 @@ class TaskCommandRouterClient:
|
|
|
590
653
|
while True:
|
|
591
654
|
timeout = max(0, deadline - time.monotonic()) if deadline is not None else None
|
|
592
655
|
try:
|
|
593
|
-
stream =
|
|
656
|
+
stream = stub_method.open(timeout=timeout, metadata=self._get_metadata())
|
|
594
657
|
async with stream as s:
|
|
595
|
-
req =
|
|
596
|
-
task_id=task_id,
|
|
597
|
-
exec_id=exec_id,
|
|
598
|
-
offset=offset,
|
|
599
|
-
file_descriptor=file_descriptor,
|
|
600
|
-
)
|
|
658
|
+
req = request_factory(offset)
|
|
601
659
|
|
|
602
660
|
# Auth retry is scoped to a single refresh per streaming attempt. While auth metadata is
|
|
603
661
|
# sent on request start, UNAUTHENTICATED may sometimes surface during iteration,
|
|
604
662
|
# so we handle it at both send and receive boundaries.
|
|
663
|
+
is_first_chunk_of_attempt = True
|
|
605
664
|
try:
|
|
606
665
|
await s.send_message(req, end=True)
|
|
607
666
|
async for item in s:
|
|
@@ -610,6 +669,11 @@ class TaskCommandRouterClient:
|
|
|
610
669
|
did_auth_retry = False
|
|
611
670
|
# Reset retry backoff after any successful chunk.
|
|
612
671
|
delay_secs = self.stream_stdio_retry_delay_secs
|
|
672
|
+
# Track it so transient reconnects request the
|
|
673
|
+
# correct next byte.
|
|
674
|
+
if is_first_chunk_of_attempt and isinstance(item, sr_pb2.SandboxStdioReadV2Response):
|
|
675
|
+
offset = item.starting_offset
|
|
676
|
+
is_first_chunk_of_attempt = False
|
|
613
677
|
offset += len(item.data)
|
|
614
678
|
yield item
|
|
615
679
|
except GRPCError as exc:
|
|
@@ -652,6 +716,63 @@ class TaskCommandRouterClient:
|
|
|
652
716
|
else:
|
|
653
717
|
raise ConnectionError(str(e))
|
|
654
718
|
|
|
719
|
+
async def _stream_stdio(
|
|
720
|
+
self,
|
|
721
|
+
task_id: str,
|
|
722
|
+
exec_id: str,
|
|
723
|
+
# Quotes around the type required for protobuf 3.19.
|
|
724
|
+
file_descriptor: "sr_pb2.TaskExecStdioFileDescriptor.ValueType",
|
|
725
|
+
deadline: Optional[float] = None,
|
|
726
|
+
) -> AsyncGenerator[sr_pb2.TaskExecStdioReadResponse, None]:
|
|
727
|
+
"""Stream exec stdio from the task, retrying on transient errors.
|
|
728
|
+
Raises ExecTimeoutError if the deadline is exceeded.
|
|
729
|
+
"""
|
|
730
|
+
|
|
731
|
+
def request_factory(offset: int) -> sr_pb2.TaskExecStdioReadRequest:
|
|
732
|
+
return sr_pb2.TaskExecStdioReadRequest(
|
|
733
|
+
task_id=task_id,
|
|
734
|
+
exec_id=exec_id,
|
|
735
|
+
offset=offset,
|
|
736
|
+
file_descriptor=file_descriptor,
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
async with aclosing(
|
|
740
|
+
self._stream_stdio_with_retries(
|
|
741
|
+
stub_method=self._stub.TaskExecStdioRead,
|
|
742
|
+
request_factory=request_factory,
|
|
743
|
+
deadline_label=f"exec {exec_id}",
|
|
744
|
+
deadline=deadline,
|
|
745
|
+
)
|
|
746
|
+
) as stream:
|
|
747
|
+
async for item in stream:
|
|
748
|
+
yield item
|
|
749
|
+
|
|
750
|
+
async def _stream_sandbox_stdio(
|
|
751
|
+
self,
|
|
752
|
+
task_id: str,
|
|
753
|
+
# Quotes around the type required for protobuf 3.19.
|
|
754
|
+
file_descriptor: "sr_pb2.SandboxStdioFileDescriptor.ValueType",
|
|
755
|
+
) -> AsyncGenerator[sr_pb2.SandboxStdioReadV2Response, None]:
|
|
756
|
+
"""Stream V2 sandbox top-level stdio from the task, retrying on transient errors."""
|
|
757
|
+
|
|
758
|
+
def request_factory(offset: int) -> sr_pb2.SandboxStdioReadV2Request:
|
|
759
|
+
return sr_pb2.SandboxStdioReadV2Request(
|
|
760
|
+
task_id=task_id,
|
|
761
|
+
offset=offset,
|
|
762
|
+
file_descriptor=file_descriptor,
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
async with aclosing(
|
|
766
|
+
self._stream_stdio_with_retries(
|
|
767
|
+
stub_method=self._stub.SandboxStdioReadV2,
|
|
768
|
+
request_factory=request_factory,
|
|
769
|
+
deadline_label=f"sandbox {task_id}",
|
|
770
|
+
deadline=None,
|
|
771
|
+
)
|
|
772
|
+
) as stream:
|
|
773
|
+
async for item in stream:
|
|
774
|
+
yield item
|
|
775
|
+
|
|
655
776
|
async def mount_image(self, request: sr_pb2.TaskMountDirectoryRequest):
|
|
656
777
|
with grpc_error_converter():
|
|
657
778
|
return await call_with_retries_on_transient_errors(
|
|
@@ -664,21 +785,10 @@ class TaskCommandRouterClient:
|
|
|
664
785
|
lambda: self._call_with_auth_retry(self._stub.TaskUnmountDirectory, request)
|
|
665
786
|
)
|
|
666
787
|
|
|
667
|
-
async def
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
return await call_with_retries_on_transient_errors(
|
|
672
|
-
lambda: self._call_with_auth_retry(self._stub.TaskSnapshotDirectory, request, **kwargs)
|
|
673
|
-
)
|
|
674
|
-
|
|
675
|
-
async def snapshot_filesystem(
|
|
676
|
-
self, request: sr_pb2.TaskSnapshotFilesystemRequest, *, timeout: float, **kwargs
|
|
677
|
-
) -> sr_pb2.TaskSnapshotFilesystemResponse:
|
|
678
|
-
# Compute the overall deadline once; each retry attempt passes the
|
|
679
|
-
# remaining budget as the per-call gRPC timeout so we honor the
|
|
680
|
-
# caller-specified `timeout` across retries instead of giving each
|
|
681
|
-
# attempt a fresh full window.
|
|
788
|
+
async def _snapshot_with_deadline(self, rpc, request, *, timeout: float, **kwargs):
|
|
789
|
+
# helper method for snapshot_directory and snapshot_filesystem to handle grpc
|
|
790
|
+
# deadlines in a consistent way, converting any error to TimeoutError after passing
|
|
791
|
+
# the total deadline budget
|
|
682
792
|
timeout_deadline = time.monotonic() + timeout
|
|
683
793
|
|
|
684
794
|
def call():
|
|
@@ -687,12 +797,8 @@ class TaskCommandRouterClient:
|
|
|
687
797
|
# doesn't matter which exception type this is
|
|
688
798
|
# as it will be caught by the catch all below
|
|
689
799
|
raise ModalTimeoutError("Timeout expired")
|
|
800
|
+
return self._call_with_auth_retry(rpc, request, timeout=call_timeout, **kwargs)
|
|
690
801
|
|
|
691
|
-
return self._call_with_auth_retry(
|
|
692
|
-
self._stub.TaskSnapshotFilesystem, request, timeout=call_timeout, **kwargs
|
|
693
|
-
)
|
|
694
|
-
|
|
695
|
-
# Any failure observed at or after the deadline is treated as a timeout
|
|
696
802
|
try:
|
|
697
803
|
with grpc_error_converter():
|
|
698
804
|
return await call_with_retries_on_transient_errors(
|
|
@@ -704,3 +810,13 @@ class TaskCommandRouterClient:
|
|
|
704
810
|
if time.monotonic() >= timeout_deadline:
|
|
705
811
|
raise ModalTimeoutError("Timeout expired")
|
|
706
812
|
raise
|
|
813
|
+
|
|
814
|
+
async def snapshot_directory(
|
|
815
|
+
self, request: sr_pb2.TaskSnapshotDirectoryRequest, *, timeout: float, **kwargs
|
|
816
|
+
) -> sr_pb2.TaskSnapshotDirectoryResponse:
|
|
817
|
+
return await self._snapshot_with_deadline(self._stub.TaskSnapshotDirectory, request, timeout=timeout, **kwargs)
|
|
818
|
+
|
|
819
|
+
async def snapshot_filesystem(
|
|
820
|
+
self, request: sr_pb2.TaskSnapshotFilesystemRequest, *, timeout: float, **kwargs
|
|
821
|
+
) -> sr_pb2.TaskSnapshotFilesystemResponse:
|
|
822
|
+
return await self._snapshot_with_deadline(self._stub.TaskSnapshotFilesystem, request, timeout=timeout, **kwargs)
|
|
@@ -55,7 +55,7 @@ def check_sequence(items: typing.Sequence[typing.Any], item_type: type[typing.An
|
|
|
55
55
|
|
|
56
56
|
CLS_T = typing.TypeVar("CLS_T", bound="type[typing.Any]")
|
|
57
57
|
|
|
58
|
-
P =
|
|
58
|
+
P = typing.ParamSpec("P")
|
|
59
59
|
|
|
60
60
|
ReturnType = typing.TypeVar("ReturnType")
|
|
61
61
|
|
|
@@ -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.4.
|
|
38
|
+
version: str = "1.4.4.dev10",
|
|
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.4.
|
|
178
|
+
version: str = "1.4.4.dev10",
|
|
179
179
|
):
|
|
180
180
|
"""mdmd:hidden
|
|
181
181
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -13,9 +13,9 @@ from .config import logger
|
|
|
13
13
|
from .exception import ExecTimeoutError, InteractiveTimeoutError, InvalidError
|
|
14
14
|
from .io_streams import (
|
|
15
15
|
_StreamReader,
|
|
16
|
-
|
|
16
|
+
_StreamReaderThroughSandboxExecCommandRouterParams,
|
|
17
17
|
_StreamWriter,
|
|
18
|
-
|
|
18
|
+
_StreamWriterThroughCommandRouterSandboxExecParams,
|
|
19
19
|
)
|
|
20
20
|
from .stream_type import StreamType
|
|
21
21
|
|
|
@@ -58,7 +58,7 @@ class _ContainerProcess(Generic[T]):
|
|
|
58
58
|
self._by_line = by_line
|
|
59
59
|
self._task_id = task_id
|
|
60
60
|
self._stdout = _StreamReader[T](
|
|
61
|
-
|
|
61
|
+
_StreamReaderThroughSandboxExecCommandRouterParams(
|
|
62
62
|
file_descriptor=api_pb2.FILE_DESCRIPTOR_STDOUT,
|
|
63
63
|
task_id=self._task_id,
|
|
64
64
|
object_id=process_id,
|
|
@@ -70,7 +70,7 @@ class _ContainerProcess(Generic[T]):
|
|
|
70
70
|
by_line=by_line,
|
|
71
71
|
)
|
|
72
72
|
self._stderr = _StreamReader[T](
|
|
73
|
-
|
|
73
|
+
_StreamReaderThroughSandboxExecCommandRouterParams(
|
|
74
74
|
file_descriptor=api_pb2.FILE_DESCRIPTOR_STDERR,
|
|
75
75
|
task_id=self._task_id,
|
|
76
76
|
object_id=process_id,
|
|
@@ -82,7 +82,7 @@ class _ContainerProcess(Generic[T]):
|
|
|
82
82
|
by_line=by_line,
|
|
83
83
|
)
|
|
84
84
|
self._stdin = _StreamWriter(
|
|
85
|
-
|
|
85
|
+
_StreamWriterThroughCommandRouterSandboxExecParams(
|
|
86
86
|
task_id=self._task_id,
|
|
87
87
|
object_id=process_id,
|
|
88
88
|
command_router_client=self._command_router_client,
|
|
@@ -27,7 +27,7 @@ import typing_extensions
|
|
|
27
27
|
|
|
28
28
|
ReturnType_INNER = typing.TypeVar("ReturnType_INNER", covariant=True)
|
|
29
29
|
|
|
30
|
-
P_INNER =
|
|
30
|
+
P_INNER = typing.ParamSpec("P_INNER")
|
|
31
31
|
|
|
32
32
|
class Function(
|
|
33
33
|
typing.Generic[modal._functions.P, modal._functions.ReturnType, modal._functions.OriginalReturnType],
|
|
@@ -4,7 +4,8 @@ import codecs
|
|
|
4
4
|
import contextlib
|
|
5
5
|
import io
|
|
6
6
|
import sys
|
|
7
|
-
from
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable
|
|
8
9
|
from dataclasses import dataclass
|
|
9
10
|
from typing import (
|
|
10
11
|
TYPE_CHECKING,
|
|
@@ -231,9 +232,9 @@ class _StreamReaderThroughServerParams:
|
|
|
231
232
|
|
|
232
233
|
|
|
233
234
|
@dataclass(frozen=True)
|
|
234
|
-
class
|
|
235
|
-
"""Parameters for a ``_StreamReader`` that reads
|
|
236
|
-
directly from the worker via the task command router."""
|
|
235
|
+
class _StreamReaderThroughSandboxExecCommandRouterParams:
|
|
236
|
+
"""Parameters for a ``_StreamReader`` that reads sandbox-exec stdio
|
|
237
|
+
directly from the worker via the task command router (``exec_stdio_read``)."""
|
|
237
238
|
|
|
238
239
|
file_descriptor: "api_pb2.FileDescriptor.ValueType"
|
|
239
240
|
task_id: str
|
|
@@ -242,10 +243,50 @@ class _StreamReaderThroughCommandRouterParams:
|
|
|
242
243
|
deadline: Optional[float]
|
|
243
244
|
|
|
244
245
|
|
|
245
|
-
|
|
246
|
-
|
|
246
|
+
@dataclass(frozen=True)
|
|
247
|
+
class _StreamReaderThroughSandboxCommandRouterParams:
|
|
248
|
+
"""Parameters for a ``_StreamReader`` that reads a V2 sandbox's
|
|
249
|
+
stdio directly from the worker via the task command router
|
|
250
|
+
(``sandbox_stdio_read``)."""
|
|
251
|
+
|
|
252
|
+
file_descriptor: "api_pb2.FileDescriptor.ValueType"
|
|
253
|
+
sandbox_id: str
|
|
254
|
+
# Lazily fetches ``(task_id, command_router_client)`` the first time the
|
|
255
|
+
# stream is iterated. Captures the sandbox handle so we only mint a JWT
|
|
256
|
+
# and open a connection to the worker when stdio is actually read.
|
|
257
|
+
resolve_router: Callable[[], Awaitable[tuple[str, TaskCommandRouterClient]]]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
_StreamReaderThroughCommandRouterParams = (
|
|
261
|
+
_StreamReaderThroughSandboxExecCommandRouterParams | _StreamReaderThroughSandboxCommandRouterParams
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
async def _stdio_stream_from_sandbox_command_router(
|
|
266
|
+
params: _StreamReaderThroughSandboxCommandRouterParams,
|
|
267
|
+
) -> AsyncGenerator[bytes, None]:
|
|
268
|
+
"""Stream raw bytes from a V2 sandbox's primary stdio via ``sandbox_stdio_read``."""
|
|
269
|
+
task_id, command_router_client = await params.resolve_router()
|
|
270
|
+
first_chunk = True
|
|
271
|
+
async with aclosing(command_router_client.sandbox_stdio_read(task_id, params.file_descriptor)) as stream:
|
|
272
|
+
async for item in stream:
|
|
273
|
+
if len(item.data) == 0:
|
|
274
|
+
raise ValueError("Received empty message streaming stdio from sandbox.")
|
|
275
|
+
if first_chunk:
|
|
276
|
+
first_chunk = False
|
|
277
|
+
if item.starting_offset > 0:
|
|
278
|
+
logger.warning(
|
|
279
|
+
f"V2 sandbox {params.sandbox_id} stdio: dropped first "
|
|
280
|
+
f"{item.starting_offset} bytes; only the most recent portion "
|
|
281
|
+
f"of output is retained."
|
|
282
|
+
)
|
|
283
|
+
yield item.data
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
async def _stdio_stream_from_sandbox_exec_command_router(
|
|
287
|
+
params: _StreamReaderThroughSandboxExecCommandRouterParams,
|
|
247
288
|
) -> AsyncGenerator[bytes, None]:
|
|
248
|
-
"""Stream raw bytes from
|
|
289
|
+
"""Stream raw bytes from a V2 sandbox-exec'd process via ``exec_stdio_read``."""
|
|
249
290
|
async with aclosing(
|
|
250
291
|
params.command_router_client.exec_stdio_read(
|
|
251
292
|
params.task_id, params.object_id, params.file_descriptor, params.deadline
|
|
@@ -254,9 +295,7 @@ async def _stdio_stream_from_command_router(
|
|
|
254
295
|
try:
|
|
255
296
|
async for item in stream:
|
|
256
297
|
if len(item.data) == 0:
|
|
257
|
-
# This is an error.
|
|
258
298
|
raise ValueError("Received empty message streaming stdio from sandbox.")
|
|
259
|
-
|
|
260
299
|
yield item.data
|
|
261
300
|
except ExecTimeoutError:
|
|
262
301
|
logger.debug(f"Deadline exceeded while streaming stdio for exec {params.object_id}")
|
|
@@ -265,20 +304,22 @@ async def _stdio_stream_from_command_router(
|
|
|
265
304
|
return
|
|
266
305
|
|
|
267
306
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
307
|
+
def _stdio_stream_from_command_router(
|
|
308
|
+
params: _StreamReaderThroughCommandRouterParams,
|
|
309
|
+
) -> AsyncGenerator[bytes, None]:
|
|
310
|
+
"""Dispatch between the V2-sandbox primary stdio and the V2-sandbox-exec
|
|
311
|
+
stdio streams, both of which yield raw bytes."""
|
|
312
|
+
if isinstance(params, _StreamReaderThroughSandboxCommandRouterParams):
|
|
313
|
+
return _stdio_stream_from_sandbox_command_router(params)
|
|
314
|
+
return _stdio_stream_from_sandbox_exec_command_router(params)
|
|
272
315
|
|
|
273
|
-
This implementation is used for non-text streams.
|
|
274
|
-
"""
|
|
275
316
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
317
|
+
class _BytesStreamReaderThroughCommandRouter:
|
|
318
|
+
"""StreamReader that yields raw bytes from the router-backed stdio source
|
|
319
|
+
(either V2 sandbox top-level stdio or V2 sandbox-exec stdio)."""
|
|
320
|
+
|
|
321
|
+
def __init__(self, params: _StreamReaderThroughCommandRouterParams) -> None:
|
|
280
322
|
self._params = params
|
|
281
|
-
self._stream = None
|
|
282
323
|
|
|
283
324
|
@property
|
|
284
325
|
def file_descriptor(self) -> int:
|
|
@@ -300,18 +341,10 @@ class _BytesStreamReaderThroughCommandRouter:
|
|
|
300
341
|
|
|
301
342
|
|
|
302
343
|
class _TextStreamReaderThroughCommandRouter:
|
|
303
|
-
"""
|
|
304
|
-
|
|
305
|
-
that hosts the sandbox.
|
|
344
|
+
"""StreamReader that yields UTF-8-decoded text from the router-backed
|
|
345
|
+
stdio source."""
|
|
306
346
|
|
|
307
|
-
|
|
308
|
-
"""
|
|
309
|
-
|
|
310
|
-
def __init__(
|
|
311
|
-
self,
|
|
312
|
-
params: _StreamReaderThroughCommandRouterParams,
|
|
313
|
-
by_line: bool,
|
|
314
|
-
) -> None:
|
|
347
|
+
def __init__(self, params: _StreamReaderThroughCommandRouterParams, by_line: bool) -> None:
|
|
315
348
|
self._params = params
|
|
316
349
|
self._by_line = by_line
|
|
317
350
|
|
|
@@ -513,15 +546,28 @@ class _StreamWriterThroughServerParams:
|
|
|
513
546
|
|
|
514
547
|
|
|
515
548
|
@dataclass(frozen=True)
|
|
516
|
-
class
|
|
517
|
-
"""Parameters for a ``_StreamWriter`` that writes
|
|
518
|
-
directly to the worker via the task command
|
|
549
|
+
class _StreamWriterThroughCommandRouterSandboxExecParams:
|
|
550
|
+
"""Parameters for a ``_StreamWriter`` that writes the stdin of a process
|
|
551
|
+
spawned via ``sb.exec(...)`` directly to the worker via the task command
|
|
552
|
+
router."""
|
|
519
553
|
|
|
520
554
|
task_id: str
|
|
521
555
|
object_id: str
|
|
522
556
|
command_router_client: TaskCommandRouterClient
|
|
523
557
|
|
|
524
558
|
|
|
559
|
+
@dataclass(frozen=True)
|
|
560
|
+
class _StreamWriterThroughCommandRouterSandboxParams:
|
|
561
|
+
"""Parameters for a ``_StreamWriter`` that writes a V2 sandbox entrypoint's
|
|
562
|
+
stdin directly to the worker via the task command router.
|
|
563
|
+
"""
|
|
564
|
+
|
|
565
|
+
# Lazily fetches ``(task_id, command_router_client)`` the first time the
|
|
566
|
+
# writer drains. Captures the sandbox handle so we only mint a JWT and
|
|
567
|
+
# open a connection to the worker when stdin is actually written.
|
|
568
|
+
resolve_router: Callable[[], Awaitable[tuple[str, TaskCommandRouterClient]]]
|
|
569
|
+
|
|
570
|
+
|
|
525
571
|
class _StreamWriterThroughServer:
|
|
526
572
|
"""Provides an interface to buffer and write to a sandbox stream (`stdin`) via the server."""
|
|
527
573
|
|
|
@@ -584,14 +630,18 @@ class _StreamWriterThroughServer:
|
|
|
584
630
|
raise ValueError(str(exc))
|
|
585
631
|
|
|
586
632
|
|
|
587
|
-
class
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
self.
|
|
593
|
-
self.
|
|
594
|
-
self._offset = 0
|
|
633
|
+
class _StreamWriterThroughCommandRouterBuffer(ABC):
|
|
634
|
+
"""Buffering/draining logic for stream writers that flush data
|
|
635
|
+
to the task command router."""
|
|
636
|
+
|
|
637
|
+
def __init__(self) -> None:
|
|
638
|
+
self._buffer: bytearray = bytearray()
|
|
639
|
+
self._is_closed: bool = False
|
|
640
|
+
self._offset: int = 0
|
|
641
|
+
|
|
642
|
+
@abstractmethod
|
|
643
|
+
async def stdin_write(self, data: bytes, eof: bool) -> None:
|
|
644
|
+
"""Write the given chunk (with optional EOF) to the command router."""
|
|
595
645
|
|
|
596
646
|
def write(self, data: Union[bytes, bytearray, memoryview, str]) -> None:
|
|
597
647
|
if self._is_closed:
|
|
@@ -613,29 +663,58 @@ class _StreamWriterThroughCommandRouter:
|
|
|
613
663
|
# NB: There's no need to prevent writing eof twice, because the command router will ignore the second EOF.
|
|
614
664
|
if self._buffer or eof:
|
|
615
665
|
data = bytes(self._buffer)
|
|
616
|
-
await self.
|
|
617
|
-
task_id=self._task_id, exec_id=self._object_id, offset=self._offset, data=data, eof=eof
|
|
618
|
-
)
|
|
666
|
+
await self.stdin_write(data, eof)
|
|
619
667
|
# Only clear the buffer after writing the data to the command router is successful.
|
|
620
668
|
# This allows the client to retry drain() in the event of an exception (though
|
|
621
|
-
#
|
|
622
|
-
# not do this).
|
|
669
|
+
# the underlying write call already retries on transient errors, so most users will
|
|
670
|
+
# probably not do this).
|
|
623
671
|
self._buffer.clear()
|
|
624
672
|
self._offset += len(data)
|
|
625
673
|
|
|
626
674
|
|
|
675
|
+
class _StreamWriterThroughCommandRouterSandboxExec(_StreamWriterThroughCommandRouterBuffer):
|
|
676
|
+
def __init__(self, params: _StreamWriterThroughCommandRouterSandboxExecParams) -> None:
|
|
677
|
+
super().__init__()
|
|
678
|
+
self._object_id = params.object_id
|
|
679
|
+
self._command_router_client = params.command_router_client
|
|
680
|
+
self._task_id = params.task_id
|
|
681
|
+
|
|
682
|
+
async def stdin_write(self, data: bytes, eof: bool) -> None:
|
|
683
|
+
await self._command_router_client.exec_stdin_write(
|
|
684
|
+
task_id=self._task_id, exec_id=self._object_id, offset=self._offset, data=data, eof=eof
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
class _StreamWriterThroughCommandRouterSandbox(_StreamWriterThroughCommandRouterBuffer):
|
|
689
|
+
"""Write a V2 sandbox entrypoint's stdin directly to the worker
|
|
690
|
+
via the task command router."""
|
|
691
|
+
|
|
692
|
+
def __init__(self, params: _StreamWriterThroughCommandRouterSandboxParams) -> None:
|
|
693
|
+
super().__init__()
|
|
694
|
+
self._resolve_router = params.resolve_router
|
|
695
|
+
|
|
696
|
+
async def stdin_write(self, data: bytes, eof: bool) -> None:
|
|
697
|
+
task_id, client = await self._resolve_router()
|
|
698
|
+
await client.sandbox_stdin_write_v2(task_id=task_id, offset=self._offset, data=data, eof=eof)
|
|
699
|
+
|
|
700
|
+
|
|
627
701
|
class _StreamWriter:
|
|
628
702
|
"""Provides an interface to buffer and write logs to a sandbox or container process stream (`stdin`)."""
|
|
629
703
|
|
|
630
704
|
def __init__(
|
|
631
705
|
self,
|
|
632
|
-
params: Union[
|
|
706
|
+
params: Union[
|
|
707
|
+
_StreamWriterThroughServerParams,
|
|
708
|
+
_StreamWriterThroughCommandRouterSandboxExecParams,
|
|
709
|
+
_StreamWriterThroughCommandRouterSandboxParams,
|
|
710
|
+
],
|
|
633
711
|
) -> None:
|
|
634
712
|
"""mdmd:hidden"""
|
|
635
|
-
if isinstance(params,
|
|
636
|
-
self._impl =
|
|
713
|
+
if isinstance(params, _StreamWriterThroughCommandRouterSandboxExecParams):
|
|
714
|
+
self._impl = _StreamWriterThroughCommandRouterSandboxExec(params)
|
|
715
|
+
elif isinstance(params, _StreamWriterThroughCommandRouterSandboxParams):
|
|
716
|
+
self._impl = _StreamWriterThroughCommandRouterSandbox(params)
|
|
637
717
|
else:
|
|
638
|
-
# Sandbox stdin is written via the server.
|
|
639
718
|
self._impl = _StreamWriterThroughServer(params)
|
|
640
719
|
|
|
641
720
|
def write(self, data: Union[bytes, bytearray, memoryview, str]) -> None:
|