modal 1.4.3.dev19__tar.gz → 1.4.3.dev21__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.3.dev19 → modal-1.4.3.dev21}/PKG-INFO +1 -1
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_output/rich.py +4 -4
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/deprecation.py +5 -8
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/docker_utils.py +10 -1
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/task_command_router_client.py +14 -5
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/cluster.py +4 -1
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/container.py +3 -1
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/environment.py +2 -1
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/client.pyi +2 -2
- modal-1.4.3.dev21/modal/container_process.py +470 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/container_process.pyi +120 -11
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/exception.py +0 -5
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/file_io.py +0 -3
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/functions.pyi +6 -6
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/image.py +29 -1
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/image.pyi +54 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/io_streams.py +199 -69
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/io_streams.pyi +64 -125
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/sandbox.py +93 -50
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/sandbox.pyi +85 -27
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/api_pb2.py +600 -600
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/api_pb2.pyi +4 -1
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_version/__init__.py +1 -1
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/pyproject.toml +5 -0
- modal-1.4.3.dev19/modal/container_process.py +0 -224
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/LICENSE +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/README.md +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/__init__.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/__main__.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_billing.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_clustered_functions.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_clustered_functions.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_container_entrypoint.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_environments.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_functions.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_grpc_client.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_ipython.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_load_context.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_location.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_logs.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_object.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_output/__init__.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_output/manager.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_output/pty.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_output/status.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_partial_function.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_resolver.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_resources.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/__init__.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/asgi.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/execution_context.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/telemetry.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_serialization.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_server.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_traceback.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_tunnel.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_tunnel.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_type_manager.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/__init__.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/app_utils.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/async_utils.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/blob_utils.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/browser_utils.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/function_utils.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/git_utils.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/hash_utils.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/http_utils.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/logger.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/mount_utils.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/name_utils.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/package_utils.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/shell_utils.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/time_utils.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_vendor/__init__.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_vendor/tblib.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_vendor/version.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_watcher.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/app.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/app.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/billing.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/builder/2023.12.txt +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/builder/2024.04.txt +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/builder/2024.10.txt +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/builder/2025.06.txt +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/builder/README.md +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/builder/base-images.json +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/call_graph.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/__init__.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/_download.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/_help.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/_traceback.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/app.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/billing.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/bootstrap.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/changelog.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/config.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/dashboard.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/dict.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/entry_point.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/import_refs.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/launch.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/logo.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/network_file_system.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/profile.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/programs/__init__.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/programs/vscode.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/queues.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/run.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/secret.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/selector.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/shell.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/token.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/utils.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/volume.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/client.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cls.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cls.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/config.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/dict.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/dict.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/environments.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/environments.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/experimental/__init__.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/experimental/flash.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/experimental/flash.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/experimental/ipython.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/file_io.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/file_pattern_matcher.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/functions.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/mount.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/mount.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/network_file_system.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/network_file_system.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/object.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/object.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/output.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/parallel_map.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/parallel_map.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/partial_function.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/partial_function.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/proxy.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/proxy.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/py.typed +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/queue.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/queue.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/retries.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/runner.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/runner.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/running_app.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/sandbox_fs.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/sandbox_fs.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/schedule.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/scheduler_placement.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/secret.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/secret.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/server.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/server.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/serving.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/serving.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/snapshot.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/snapshot.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/stream_type.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/token_flow.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/token_flow.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/volume.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/volume.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal.egg-info/requires.txt +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal.egg-info/top_level.txt +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_docs/__init__.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/__init__.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/api_grpc.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/py.typed +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_version/__main__.py +0 -0
- {modal-1.4.3.dev19 → modal-1.4.3.dev21}/setup.cfg +0 -0
|
@@ -49,7 +49,7 @@ from modal._output.manager import (
|
|
|
49
49
|
)
|
|
50
50
|
from modal._utils.time_utils import timestamp_to_localized_str
|
|
51
51
|
from modal.config import logger
|
|
52
|
-
from modal.exception import DeprecationError,
|
|
52
|
+
from modal.exception import DeprecationError, ServerWarning
|
|
53
53
|
from modal_proto import api_pb2
|
|
54
54
|
|
|
55
55
|
if platform.system() == "Windows":
|
|
@@ -359,14 +359,14 @@ class RichOutputManager(OutputManager):
|
|
|
359
359
|
) -> None:
|
|
360
360
|
"""Display a warning, using rich formatting for Modal-specific warnings.
|
|
361
361
|
|
|
362
|
-
Modal warnings (DeprecationError,
|
|
362
|
+
Modal warnings (DeprecationError, ServerWarning) are shown
|
|
363
363
|
in a yellow-bordered panel with source context. Other warnings fall back to the
|
|
364
364
|
default Python warning display.
|
|
365
365
|
"""
|
|
366
366
|
# For non-Modal warnings, fall back to the default display
|
|
367
367
|
import modal
|
|
368
368
|
|
|
369
|
-
is_modal_warning = issubclass(category, (DeprecationError,
|
|
369
|
+
is_modal_warning = issubclass(category, (DeprecationError, ServerWarning))
|
|
370
370
|
filename_in_modal = modal.__path__ and Path(filename).is_relative_to(modal.__path__[0])
|
|
371
371
|
if not is_modal_warning and not filename_in_modal:
|
|
372
372
|
base_showwarning(warning, category, filename, lineno, file=None, line=None)
|
|
@@ -392,7 +392,7 @@ class RichOutputManager(OutputManager):
|
|
|
392
392
|
pass
|
|
393
393
|
|
|
394
394
|
# Build title
|
|
395
|
-
if issubclass(category,
|
|
395
|
+
if issubclass(category, DeprecationError):
|
|
396
396
|
title = "Modal Deprecation Warning"
|
|
397
397
|
else:
|
|
398
398
|
title = "Modal Warning"
|
|
@@ -7,7 +7,7 @@ from typing import Any, Callable, TypeVar
|
|
|
7
7
|
|
|
8
8
|
from typing_extensions import ParamSpec # Needed for Python 3.9
|
|
9
9
|
|
|
10
|
-
from ..exception import DeprecationError
|
|
10
|
+
from ..exception import DeprecationError
|
|
11
11
|
|
|
12
12
|
_INTERNAL_MODULES = ["modal", "synchronicity"]
|
|
13
13
|
|
|
@@ -21,12 +21,11 @@ def deprecation_error(deprecated_on: tuple[int, int, int], msg: str):
|
|
|
21
21
|
raise DeprecationError(f"Deprecated on {date(*deprecated_on)}: {msg}")
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def deprecation_warning(
|
|
25
|
-
deprecated_on: tuple[int, int, int], msg: str, *, pending: bool = False, show_source: bool = True
|
|
26
|
-
) -> None:
|
|
24
|
+
def deprecation_warning(deprecated_on: tuple[int, int, int], msg: str, *, show_source: bool = True) -> None:
|
|
27
25
|
"""Issue a Modal deprecation warning with source optionally attributed to user code.
|
|
28
26
|
|
|
29
|
-
See the implementation of the built-in
|
|
27
|
+
See the implementation of the built-in warnings.warn:
|
|
28
|
+
https://docs.python.org/3/library/warnings.html#available-functions.
|
|
30
29
|
"""
|
|
31
30
|
filename, lineno = "<unknown>", 0
|
|
32
31
|
if show_source:
|
|
@@ -42,10 +41,8 @@ def deprecation_warning(
|
|
|
42
41
|
# Use the defaults from above
|
|
43
42
|
pass
|
|
44
43
|
|
|
45
|
-
warning_cls = PendingDeprecationError if pending else DeprecationError
|
|
46
|
-
|
|
47
44
|
# This is a lower-level function that warnings.warn uses
|
|
48
|
-
warnings.warn_explicit(f"{date(*deprecated_on)}: {msg}",
|
|
45
|
+
warnings.warn_explicit(f"{date(*deprecated_on)}: {msg}", DeprecationError, filename, lineno)
|
|
49
46
|
|
|
50
47
|
|
|
51
48
|
P = ParamSpec("P")
|
|
@@ -40,10 +40,19 @@ def extract_copy_command_patterns(dockerfile_lines: Sequence[str]) -> list[str]:
|
|
|
40
40
|
|
|
41
41
|
# COPY --from=... commands reference external sources and do not need a context mount.
|
|
42
42
|
# https://docs.docker.com/reference/dockerfile/#copy---from
|
|
43
|
-
if
|
|
43
|
+
if any(p.startswith("--from=") for p in parts):
|
|
44
44
|
current_command = ""
|
|
45
45
|
continue
|
|
46
46
|
|
|
47
|
+
# Strip known COPY flags (--chmod, --chown, --link) before processing sources.
|
|
48
|
+
known_flag_prefixes = ("--chmod=", "--chown=", "--link=")
|
|
49
|
+
parts = [
|
|
50
|
+
p
|
|
51
|
+
for p in parts
|
|
52
|
+
# link has a special handling - it can be "--link" or like "--link=true"
|
|
53
|
+
if p != "--link" and not any(p.startswith(prefix) for prefix in known_flag_prefixes)
|
|
54
|
+
]
|
|
55
|
+
|
|
47
56
|
if len(parts) >= 2:
|
|
48
57
|
# Last part is destination, everything else is a mount source
|
|
49
58
|
sources = parts[:-1]
|
|
@@ -16,7 +16,7 @@ from grpclib import GRPCError, Status
|
|
|
16
16
|
from grpclib.exceptions import StreamTerminatedError
|
|
17
17
|
|
|
18
18
|
from modal.config import logger
|
|
19
|
-
from modal.exception import ExecTimeoutError
|
|
19
|
+
from modal.exception import ConflictError, ExecTimeoutError
|
|
20
20
|
from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
|
|
21
21
|
from modal_proto.task_command_router_grpc import TaskCommandRouterStub
|
|
22
22
|
|
|
@@ -196,12 +196,21 @@ class TaskCommandRouterClient:
|
|
|
196
196
|
return cls(server_client, task_id, url, jwt, channel, loop, jwt_refresh_lock, sandbox_id=sandbox_id)
|
|
197
197
|
|
|
198
198
|
@classmethod
|
|
199
|
-
async def
|
|
199
|
+
async def try_init(
|
|
200
200
|
cls,
|
|
201
201
|
server_client,
|
|
202
202
|
task_id: str,
|
|
203
|
-
) -> "TaskCommandRouterClient":
|
|
204
|
-
|
|
203
|
+
) -> Optional["TaskCommandRouterClient"]:
|
|
204
|
+
"""Attempt to initialize a TaskCommandRouterClient by fetching direct access.
|
|
205
|
+
|
|
206
|
+
Returns None if command router access is not enabled (FAILED_PRECONDITION).
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
resp = await fetch_command_router_access(server_client, task_id)
|
|
210
|
+
except ConflictError:
|
|
211
|
+
logger.debug(f"Command router access is not enabled for task {task_id}")
|
|
212
|
+
return None
|
|
213
|
+
|
|
205
214
|
logger.debug(f"Using command router access for task {task_id}")
|
|
206
215
|
return await cls._connect(server_client, task_id, resp.url, resp.jwt)
|
|
207
216
|
|
|
@@ -232,7 +241,7 @@ class TaskCommandRouterClient:
|
|
|
232
241
|
stream_stdio_retry_delay_factor: float = 2,
|
|
233
242
|
stream_stdio_max_retries: int = 10,
|
|
234
243
|
) -> None:
|
|
235
|
-
"""Callers should not use this directly. Use TaskCommandRouterClient.
|
|
244
|
+
"""Callers should not use this directly. Use TaskCommandRouterClient.try_init() instead."""
|
|
236
245
|
# Record the loop this instance is bound to so __del__ can safely schedule cleanup
|
|
237
246
|
# even if finalization happens from a different thread (e.g. via synchronicity).
|
|
238
247
|
self._loop = loop
|
|
@@ -16,6 +16,7 @@ from modal.cli.utils import display_table, env_option, is_tty
|
|
|
16
16
|
from modal.client import _Client
|
|
17
17
|
from modal.config import config
|
|
18
18
|
from modal.container_process import _ContainerProcess
|
|
19
|
+
from modal.exception import InvalidError
|
|
19
20
|
from modal.output import OutputManager
|
|
20
21
|
from modal.stream_type import StreamType
|
|
21
22
|
from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
|
|
@@ -78,7 +79,9 @@ async def shell(cluster_id: str, rank: int = 0):
|
|
|
78
79
|
|
|
79
80
|
pty = is_tty()
|
|
80
81
|
|
|
81
|
-
command_router_client = await TaskCommandRouterClient.
|
|
82
|
+
command_router_client = await TaskCommandRouterClient.try_init(client, task_id)
|
|
83
|
+
if command_router_client is None:
|
|
84
|
+
raise InvalidError(f"Command router access is not available for container {task_id}")
|
|
82
85
|
|
|
83
86
|
process_id = str(uuid.uuid4())
|
|
84
87
|
|
|
@@ -278,7 +278,9 @@ async def _exec_impl(
|
|
|
278
278
|
|
|
279
279
|
client = await _Client.from_env()
|
|
280
280
|
|
|
281
|
-
command_router_client = await TaskCommandRouterClient.
|
|
281
|
+
command_router_client = await TaskCommandRouterClient.try_init(client, container_id)
|
|
282
|
+
if command_router_client is None:
|
|
283
|
+
raise InvalidError(f"Command router access is not available for container {container_id}")
|
|
282
284
|
|
|
283
285
|
process_id = str(uuid.uuid4())
|
|
284
286
|
|
|
@@ -67,7 +67,8 @@ def list_(json: bool = False):
|
|
|
67
67
|
def create(name: str, restricted: bool = False):
|
|
68
68
|
check_environment_name(name)
|
|
69
69
|
Environment.objects.create(name, restricted=restricted)
|
|
70
|
-
|
|
70
|
+
prefix = "Restricted " if restricted else ""
|
|
71
|
+
rich.print(f"[green]✓[/green] {prefix}Environment created: {name}")
|
|
71
72
|
|
|
72
73
|
|
|
73
74
|
ENVIRONMENT_DELETE_HELP = """Delete an environment in the current workspace.
|
|
@@ -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.3.
|
|
38
|
+
version: str = "1.4.3.dev21",
|
|
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.3.
|
|
178
|
+
version: str = "1.4.3.dev21",
|
|
179
179
|
):
|
|
180
180
|
"""mdmd:hidden
|
|
181
181
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
# Copyright Modal Labs 2024
|
|
2
|
+
import asyncio
|
|
3
|
+
import platform
|
|
4
|
+
import time
|
|
5
|
+
from typing import Generic, Optional, TypeVar
|
|
6
|
+
|
|
7
|
+
from modal_proto import api_pb2
|
|
8
|
+
|
|
9
|
+
from ._utils.async_utils import TaskContext, synchronize_api
|
|
10
|
+
from ._utils.shell_utils import stream_from_stdin, write_to_fd
|
|
11
|
+
from ._utils.task_command_router_client import TaskCommandRouterClient
|
|
12
|
+
from .client import _Client
|
|
13
|
+
from .config import logger
|
|
14
|
+
from .exception import ExecTimeoutError, InteractiveTimeoutError, InvalidError
|
|
15
|
+
from .io_streams import _StreamReader, _StreamWriter
|
|
16
|
+
from .stream_type import StreamType
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T", str, bytes)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _ContainerProcessThroughServer(Generic[T]):
|
|
22
|
+
_process_id: Optional[str] = None
|
|
23
|
+
_stdout: _StreamReader[T]
|
|
24
|
+
_stderr: _StreamReader[T]
|
|
25
|
+
_stdin: _StreamWriter
|
|
26
|
+
_exec_deadline: Optional[float] = None
|
|
27
|
+
_text: bool
|
|
28
|
+
_by_line: bool
|
|
29
|
+
_returncode: Optional[int] = None
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
process_id: str,
|
|
34
|
+
task_id: str,
|
|
35
|
+
client: _Client,
|
|
36
|
+
stdout: StreamType = StreamType.PIPE,
|
|
37
|
+
stderr: StreamType = StreamType.PIPE,
|
|
38
|
+
exec_deadline: Optional[float] = None,
|
|
39
|
+
text: bool = True,
|
|
40
|
+
by_line: bool = False,
|
|
41
|
+
) -> None:
|
|
42
|
+
self._process_id = process_id
|
|
43
|
+
self._client = client
|
|
44
|
+
self._exec_deadline = exec_deadline
|
|
45
|
+
self._text = text
|
|
46
|
+
self._by_line = by_line
|
|
47
|
+
self._stdout = _StreamReader[T](
|
|
48
|
+
api_pb2.FILE_DESCRIPTOR_STDOUT,
|
|
49
|
+
process_id,
|
|
50
|
+
"container_process",
|
|
51
|
+
self._client,
|
|
52
|
+
stream_type=stdout,
|
|
53
|
+
text=text,
|
|
54
|
+
by_line=by_line,
|
|
55
|
+
deadline=exec_deadline,
|
|
56
|
+
task_id=task_id,
|
|
57
|
+
)
|
|
58
|
+
self._stderr = _StreamReader[T](
|
|
59
|
+
api_pb2.FILE_DESCRIPTOR_STDERR,
|
|
60
|
+
process_id,
|
|
61
|
+
"container_process",
|
|
62
|
+
self._client,
|
|
63
|
+
stream_type=stderr,
|
|
64
|
+
text=text,
|
|
65
|
+
by_line=by_line,
|
|
66
|
+
deadline=exec_deadline,
|
|
67
|
+
task_id=task_id,
|
|
68
|
+
)
|
|
69
|
+
self._stdin = _StreamWriter(process_id, "container_process", self._client)
|
|
70
|
+
|
|
71
|
+
def __repr__(self) -> str:
|
|
72
|
+
return f"ContainerProcess(process_id={self._process_id!r})"
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def stdout(self) -> _StreamReader[T]:
|
|
76
|
+
"""StreamReader for the container process's stdout stream."""
|
|
77
|
+
return self._stdout
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def stderr(self) -> _StreamReader[T]:
|
|
81
|
+
"""StreamReader for the container process's stderr stream."""
|
|
82
|
+
return self._stderr
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def stdin(self) -> _StreamWriter:
|
|
86
|
+
"""StreamWriter for the container process's stdin stream."""
|
|
87
|
+
return self._stdin
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def returncode(self) -> int:
|
|
91
|
+
if self._returncode is None:
|
|
92
|
+
raise InvalidError(
|
|
93
|
+
"You must call wait() before accessing the returncode. "
|
|
94
|
+
"To poll for the status of a running process, use poll() instead."
|
|
95
|
+
)
|
|
96
|
+
return self._returncode
|
|
97
|
+
|
|
98
|
+
async def poll(self) -> Optional[int]:
|
|
99
|
+
"""Check if the container process has finished running.
|
|
100
|
+
|
|
101
|
+
Returns `None` if the process is still running, else returns the exit code.
|
|
102
|
+
"""
|
|
103
|
+
assert self._process_id
|
|
104
|
+
if self._returncode is not None:
|
|
105
|
+
return self._returncode
|
|
106
|
+
if self._exec_deadline and time.monotonic() >= self._exec_deadline:
|
|
107
|
+
# TODO(matt): In the future, it would be nice to raise a ContainerExecTimeoutError to make it
|
|
108
|
+
# clear to the user that their sandbox terminated due to a timeout
|
|
109
|
+
self._returncode = -1
|
|
110
|
+
return self._returncode
|
|
111
|
+
|
|
112
|
+
req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=0)
|
|
113
|
+
resp = await self._client.stub.ContainerExecWait(req)
|
|
114
|
+
|
|
115
|
+
if resp.completed:
|
|
116
|
+
self._returncode = resp.exit_code
|
|
117
|
+
return self._returncode
|
|
118
|
+
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
async def _wait_for_completion(self) -> int:
|
|
122
|
+
assert self._process_id
|
|
123
|
+
while True:
|
|
124
|
+
req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=10)
|
|
125
|
+
resp = await self._client.stub.ContainerExecWait(req)
|
|
126
|
+
if resp.completed:
|
|
127
|
+
return resp.exit_code
|
|
128
|
+
|
|
129
|
+
async def wait(self) -> int:
|
|
130
|
+
"""Wait for the container process to finish running. Returns the exit code."""
|
|
131
|
+
if self._returncode is not None:
|
|
132
|
+
return self._returncode
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
timeout = None
|
|
136
|
+
if self._exec_deadline:
|
|
137
|
+
timeout = self._exec_deadline - time.monotonic()
|
|
138
|
+
if timeout <= 0:
|
|
139
|
+
raise TimeoutError()
|
|
140
|
+
self._returncode = await asyncio.wait_for(self._wait_for_completion(), timeout=timeout)
|
|
141
|
+
except (asyncio.TimeoutError, TimeoutError):
|
|
142
|
+
self._returncode = -1
|
|
143
|
+
logger.debug(f"ContainerProcess {self._process_id} wait completed with returncode {self._returncode}")
|
|
144
|
+
return self._returncode
|
|
145
|
+
|
|
146
|
+
async def attach(self):
|
|
147
|
+
"""mdmd:hidden"""
|
|
148
|
+
if platform.system() == "Windows":
|
|
149
|
+
print("interactive exec is not currently supported on Windows.") # noqa: T201
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
from .output import OutputManager
|
|
153
|
+
|
|
154
|
+
output = OutputManager.get()
|
|
155
|
+
connecting_status = output.status("Connecting...")
|
|
156
|
+
connecting_status.start()
|
|
157
|
+
on_connect = asyncio.Event()
|
|
158
|
+
|
|
159
|
+
async def _write_to_fd_loop(stream: _StreamReader):
|
|
160
|
+
# This is required to make modal shell to an existing task work,
|
|
161
|
+
# since that uses ContainerExec RPCs directly, but this is hacky.
|
|
162
|
+
#
|
|
163
|
+
# TODO(saltzm): Once we use the new exec path for that use case, this code can all be removed.
|
|
164
|
+
from .io_streams import _StreamReaderThroughServer
|
|
165
|
+
|
|
166
|
+
assert isinstance(stream._impl, _StreamReaderThroughServer)
|
|
167
|
+
stream_impl = stream._impl
|
|
168
|
+
# Don't skip empty messages so we can detect when the process has booted.
|
|
169
|
+
async for chunk in stream_impl._get_logs(skip_empty_messages=False):
|
|
170
|
+
if not on_connect.is_set():
|
|
171
|
+
connecting_status.stop()
|
|
172
|
+
on_connect.set()
|
|
173
|
+
|
|
174
|
+
await write_to_fd(stream.file_descriptor, chunk)
|
|
175
|
+
|
|
176
|
+
async def _handle_input(data: bytes, message_index: int):
|
|
177
|
+
self.stdin.write(data)
|
|
178
|
+
await self.stdin.drain()
|
|
179
|
+
|
|
180
|
+
async with TaskContext() as tc:
|
|
181
|
+
stdout_task = tc.create_task(_write_to_fd_loop(self.stdout))
|
|
182
|
+
stderr_task = tc.create_task(_write_to_fd_loop(self.stderr))
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
# time out if we can't connect to the server fast enough
|
|
186
|
+
await asyncio.wait_for(on_connect.wait(), timeout=60)
|
|
187
|
+
|
|
188
|
+
async with stream_from_stdin(_handle_input, use_raw_terminal=True):
|
|
189
|
+
await stdout_task
|
|
190
|
+
await stderr_task
|
|
191
|
+
|
|
192
|
+
# TODO: this doesn't work right now.
|
|
193
|
+
# if exit_status != 0:
|
|
194
|
+
# raise ExecutionError(f"Process exited with status code {exit_status}")
|
|
195
|
+
|
|
196
|
+
except (asyncio.TimeoutError, TimeoutError):
|
|
197
|
+
connecting_status.stop()
|
|
198
|
+
stdout_task.cancel()
|
|
199
|
+
stderr_task.cancel()
|
|
200
|
+
raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
async def _iter_stream_as_bytes(stream: _StreamReader[T]):
|
|
204
|
+
"""Yield raw bytes from a StreamReader regardless of text mode/backend."""
|
|
205
|
+
async for part in stream:
|
|
206
|
+
if isinstance(part, str):
|
|
207
|
+
yield part.encode("utf-8")
|
|
208
|
+
else:
|
|
209
|
+
yield part
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class _ContainerProcessThroughCommandRouter(Generic[T]):
|
|
213
|
+
"""
|
|
214
|
+
Container process implementation that works via direct communication with
|
|
215
|
+
the Modal worker where the container is running.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
def __init__(
|
|
219
|
+
self,
|
|
220
|
+
process_id: str,
|
|
221
|
+
client: _Client,
|
|
222
|
+
command_router_client: TaskCommandRouterClient,
|
|
223
|
+
task_id: str,
|
|
224
|
+
*,
|
|
225
|
+
stdout: StreamType = StreamType.PIPE,
|
|
226
|
+
stderr: StreamType = StreamType.PIPE,
|
|
227
|
+
exec_deadline: Optional[float] = None,
|
|
228
|
+
text: bool = True,
|
|
229
|
+
by_line: bool = False,
|
|
230
|
+
) -> None:
|
|
231
|
+
self._client = client
|
|
232
|
+
self._command_router_client = command_router_client
|
|
233
|
+
self._process_id = process_id
|
|
234
|
+
self._exec_deadline = exec_deadline
|
|
235
|
+
self._text = text
|
|
236
|
+
self._by_line = by_line
|
|
237
|
+
self._task_id = task_id
|
|
238
|
+
self._stdout = _StreamReader[T](
|
|
239
|
+
api_pb2.FILE_DESCRIPTOR_STDOUT,
|
|
240
|
+
process_id,
|
|
241
|
+
"container_process",
|
|
242
|
+
self._client,
|
|
243
|
+
stream_type=stdout,
|
|
244
|
+
text=text,
|
|
245
|
+
by_line=by_line,
|
|
246
|
+
deadline=exec_deadline,
|
|
247
|
+
command_router_client=self._command_router_client,
|
|
248
|
+
task_id=self._task_id,
|
|
249
|
+
)
|
|
250
|
+
self._stderr = _StreamReader[T](
|
|
251
|
+
api_pb2.FILE_DESCRIPTOR_STDERR,
|
|
252
|
+
process_id,
|
|
253
|
+
"container_process",
|
|
254
|
+
self._client,
|
|
255
|
+
stream_type=stderr,
|
|
256
|
+
text=text,
|
|
257
|
+
by_line=by_line,
|
|
258
|
+
deadline=exec_deadline,
|
|
259
|
+
command_router_client=self._command_router_client,
|
|
260
|
+
task_id=self._task_id,
|
|
261
|
+
)
|
|
262
|
+
self._stdin = _StreamWriter(
|
|
263
|
+
process_id,
|
|
264
|
+
"container_process",
|
|
265
|
+
self._client,
|
|
266
|
+
command_router_client=self._command_router_client,
|
|
267
|
+
task_id=self._task_id,
|
|
268
|
+
)
|
|
269
|
+
self._returncode = None
|
|
270
|
+
|
|
271
|
+
def __repr__(self) -> str:
|
|
272
|
+
return f"ContainerProcess(process_id={self._process_id!r})"
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def stdout(self) -> _StreamReader[T]:
|
|
276
|
+
return self._stdout
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def stderr(self) -> _StreamReader[T]:
|
|
280
|
+
return self._stderr
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def stdin(self) -> _StreamWriter:
|
|
284
|
+
return self._stdin
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def returncode(self) -> int:
|
|
288
|
+
if self._returncode is None:
|
|
289
|
+
raise InvalidError(
|
|
290
|
+
"You must call wait() before accessing the returncode. "
|
|
291
|
+
"To poll for the status of a running process, use poll() instead."
|
|
292
|
+
)
|
|
293
|
+
return self._returncode
|
|
294
|
+
|
|
295
|
+
async def poll(self) -> Optional[int]:
|
|
296
|
+
if self._returncode is not None:
|
|
297
|
+
return self._returncode
|
|
298
|
+
try:
|
|
299
|
+
resp = await self._command_router_client.exec_poll(self._task_id, self._process_id, self._exec_deadline)
|
|
300
|
+
which = resp.WhichOneof("exit_status")
|
|
301
|
+
if which is None:
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
if which == "code":
|
|
305
|
+
self._returncode = int(resp.code)
|
|
306
|
+
return self._returncode
|
|
307
|
+
elif which == "signal":
|
|
308
|
+
self._returncode = 128 + int(resp.signal)
|
|
309
|
+
return self._returncode
|
|
310
|
+
else:
|
|
311
|
+
logger.debug(f"ContainerProcess {self._process_id} exited with unexpected status: {which}")
|
|
312
|
+
raise InvalidError("Unexpected exit status")
|
|
313
|
+
except ExecTimeoutError:
|
|
314
|
+
logger.debug(f"ContainerProcess poll for {self._process_id} did not complete within deadline")
|
|
315
|
+
# TODO(saltzm): This is a weird API, but customers currently may rely on it. This
|
|
316
|
+
# should probably raise an ExecTimeoutError instead.
|
|
317
|
+
self._returncode = -1
|
|
318
|
+
return self._returncode
|
|
319
|
+
except Exception as e:
|
|
320
|
+
# Re-raise non-transient errors or errors resulting from exceeding retries on transient errors.
|
|
321
|
+
logger.warning(f"ContainerProcess poll for {self._process_id} failed: {e}")
|
|
322
|
+
raise
|
|
323
|
+
|
|
324
|
+
async def wait(self) -> int:
|
|
325
|
+
if self._returncode is not None:
|
|
326
|
+
return self._returncode
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
resp = await self._command_router_client.exec_wait(self._task_id, self._process_id, self._exec_deadline)
|
|
330
|
+
which = resp.WhichOneof("exit_status")
|
|
331
|
+
if which == "code":
|
|
332
|
+
self._returncode = int(resp.code)
|
|
333
|
+
elif which == "signal":
|
|
334
|
+
self._returncode = 128 + int(resp.signal)
|
|
335
|
+
else:
|
|
336
|
+
logger.debug(f"ContainerProcess {self._process_id} exited with unexpected status: {which}")
|
|
337
|
+
self._returncode = -1
|
|
338
|
+
raise InvalidError("Unexpected exit status")
|
|
339
|
+
except ExecTimeoutError:
|
|
340
|
+
logger.debug(f"ContainerProcess {self._process_id} did not complete within deadline")
|
|
341
|
+
# TODO(saltzm): This is a weird API, but customers currently may rely on it. This
|
|
342
|
+
# should be a ExecTimeoutError.
|
|
343
|
+
self._returncode = -1
|
|
344
|
+
|
|
345
|
+
return self._returncode
|
|
346
|
+
|
|
347
|
+
async def attach(self):
|
|
348
|
+
if platform.system() == "Windows":
|
|
349
|
+
print("interactive exec is not currently supported on Windows.") # noqa: T201
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
from .output import OutputManager
|
|
353
|
+
|
|
354
|
+
output = OutputManager.get()
|
|
355
|
+
connecting_status = output.status("Connecting...")
|
|
356
|
+
connecting_status.start()
|
|
357
|
+
on_connect = asyncio.Event()
|
|
358
|
+
|
|
359
|
+
async def _write_to_fd_loop(stream: _StreamReader[T]):
|
|
360
|
+
async for chunk in _iter_stream_as_bytes(stream):
|
|
361
|
+
if chunk is None:
|
|
362
|
+
break
|
|
363
|
+
|
|
364
|
+
if not on_connect.is_set():
|
|
365
|
+
connecting_status.stop()
|
|
366
|
+
on_connect.set()
|
|
367
|
+
|
|
368
|
+
await write_to_fd(stream.file_descriptor, chunk)
|
|
369
|
+
|
|
370
|
+
async def _handle_input(data: bytes, message_index: int):
|
|
371
|
+
self.stdin.write(data)
|
|
372
|
+
await self.stdin.drain()
|
|
373
|
+
|
|
374
|
+
async with TaskContext() as tc:
|
|
375
|
+
stdout_task = tc.create_task(_write_to_fd_loop(self.stdout))
|
|
376
|
+
stderr_task = tc.create_task(_write_to_fd_loop(self.stderr))
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
# Time out if we can't connect fast enough.
|
|
380
|
+
await asyncio.wait_for(on_connect.wait(), timeout=60)
|
|
381
|
+
|
|
382
|
+
async with stream_from_stdin(_handle_input, use_raw_terminal=True):
|
|
383
|
+
await stdout_task
|
|
384
|
+
await stderr_task
|
|
385
|
+
|
|
386
|
+
except (asyncio.TimeoutError, TimeoutError):
|
|
387
|
+
connecting_status.stop()
|
|
388
|
+
stdout_task.cancel()
|
|
389
|
+
stderr_task.cancel()
|
|
390
|
+
raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class _ContainerProcess(Generic[T]):
|
|
394
|
+
"""Represents a running process in a container."""
|
|
395
|
+
|
|
396
|
+
def __init__(
|
|
397
|
+
self,
|
|
398
|
+
process_id: str,
|
|
399
|
+
task_id: str,
|
|
400
|
+
client: _Client,
|
|
401
|
+
stdout: StreamType = StreamType.PIPE,
|
|
402
|
+
stderr: StreamType = StreamType.PIPE,
|
|
403
|
+
exec_deadline: Optional[float] = None,
|
|
404
|
+
text: bool = True,
|
|
405
|
+
by_line: bool = False,
|
|
406
|
+
command_router_client: Optional[TaskCommandRouterClient] = None,
|
|
407
|
+
) -> None:
|
|
408
|
+
if command_router_client is None:
|
|
409
|
+
self._impl = _ContainerProcessThroughServer(
|
|
410
|
+
process_id,
|
|
411
|
+
task_id,
|
|
412
|
+
client,
|
|
413
|
+
stdout=stdout,
|
|
414
|
+
stderr=stderr,
|
|
415
|
+
exec_deadline=exec_deadline,
|
|
416
|
+
text=text,
|
|
417
|
+
by_line=by_line,
|
|
418
|
+
)
|
|
419
|
+
else:
|
|
420
|
+
self._impl = _ContainerProcessThroughCommandRouter(
|
|
421
|
+
process_id,
|
|
422
|
+
client,
|
|
423
|
+
command_router_client,
|
|
424
|
+
task_id,
|
|
425
|
+
stdout=stdout,
|
|
426
|
+
stderr=stderr,
|
|
427
|
+
exec_deadline=exec_deadline,
|
|
428
|
+
text=text,
|
|
429
|
+
by_line=by_line,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
def __repr__(self) -> str:
|
|
433
|
+
return self._impl.__repr__()
|
|
434
|
+
|
|
435
|
+
@property
|
|
436
|
+
def stdout(self) -> _StreamReader[T]:
|
|
437
|
+
"""StreamReader for the container process's stdout stream."""
|
|
438
|
+
return self._impl.stdout
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
def stderr(self) -> _StreamReader[T]:
|
|
442
|
+
"""StreamReader for the container process's stderr stream."""
|
|
443
|
+
return self._impl.stderr
|
|
444
|
+
|
|
445
|
+
@property
|
|
446
|
+
def stdin(self) -> _StreamWriter:
|
|
447
|
+
"""StreamWriter for the container process's stdin stream."""
|
|
448
|
+
return self._impl.stdin
|
|
449
|
+
|
|
450
|
+
@property
|
|
451
|
+
def returncode(self) -> int:
|
|
452
|
+
return self._impl.returncode
|
|
453
|
+
|
|
454
|
+
async def poll(self) -> Optional[int]:
|
|
455
|
+
"""Check if the container process has finished running.
|
|
456
|
+
|
|
457
|
+
Returns `None` if the process is still running, else returns the exit code.
|
|
458
|
+
"""
|
|
459
|
+
return await self._impl.poll()
|
|
460
|
+
|
|
461
|
+
async def wait(self) -> int:
|
|
462
|
+
"""Wait for the container process to finish running. Returns the exit code."""
|
|
463
|
+
return await self._impl.wait()
|
|
464
|
+
|
|
465
|
+
async def attach(self):
|
|
466
|
+
"""mdmd:hidden"""
|
|
467
|
+
await self._impl.attach()
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
ContainerProcess = synchronize_api(_ContainerProcess)
|