modal 1.4.2.dev5__tar.gz → 1.4.2.dev7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/PKG-INFO +1 -1
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/grpc_utils.py +29 -5
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/task_command_router_client.py +2 -1
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/app.py +163 -79
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/container.py +20 -2
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/utils.py +10 -17
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/client.pyi +2 -2
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/dict.py +5 -5
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/dict.pyi +8 -8
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/image.py +2 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/image.pyi +2 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/queue.py +5 -5
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/queue.pyi +8 -8
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/sandbox_fs.py +6 -6
- modal-1.4.2.dev7/modal/sandbox_fs.pyi +591 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/secret.py +5 -5
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/secret.pyi +8 -8
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/volume.py +5 -5
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/volume.pyi +8 -8
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_docs/mdmd/mdmd.py +30 -13
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/api_pb2.py +656 -656
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/api_pb2.pyi +18 -4
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_version/__init__.py +1 -1
- modal-1.4.2.dev5/modal/sandbox_fs.pyi +0 -627
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/LICENSE +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/README.md +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/__init__.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/__main__.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_billing.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_clustered_functions.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_clustered_functions.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_container_entrypoint.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_functions.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_grpc_client.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_ipython.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_load_context.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_location.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_logs.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_object.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_output/__init__.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_output/manager.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_output/pty.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_output/rich.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_output/status.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_partial_function.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_resolver.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_resources.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/__init__.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/asgi.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/execution_context.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/telemetry.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_serialization.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_server.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_traceback.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_tunnel.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_tunnel.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_type_manager.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/__init__.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/app_utils.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/async_utils.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/blob_utils.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/browser_utils.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/deprecation.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/docker_utils.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/function_utils.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/git_utils.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/hash_utils.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/http_utils.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/logger.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/mount_utils.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/name_utils.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/package_utils.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/shell_utils.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/time_utils.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_vendor/__init__.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_vendor/tblib.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_vendor/version.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_watcher.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/app.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/app.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/billing.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/builder/2023.12.txt +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/builder/2024.04.txt +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/builder/2024.10.txt +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/builder/2025.06.txt +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/builder/README.md +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/builder/base-images.json +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/call_graph.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/__init__.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/_download.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/_traceback.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/billing.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/changelog.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/cluster.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/config.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/dashboard.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/dict.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/entry_point.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/environment.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/import_refs.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/launch.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/network_file_system.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/profile.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/programs/__init__.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/programs/vscode.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/queues.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/run.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/secret.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/selector.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/shell.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/token.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/volume.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/client.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cls.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cls.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/config.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/container_process.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/container_process.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/environments.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/environments.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/exception.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/experimental/__init__.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/experimental/flash.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/experimental/flash.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/experimental/ipython.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/file_io.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/file_io.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/file_pattern_matcher.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/functions.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/functions.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/io_streams.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/io_streams.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/mount.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/mount.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/network_file_system.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/network_file_system.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/object.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/object.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/output.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/parallel_map.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/parallel_map.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/partial_function.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/partial_function.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/proxy.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/proxy.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/py.typed +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/retries.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/runner.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/runner.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/running_app.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/sandbox.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/sandbox.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/schedule.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/scheduler_placement.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/server.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/server.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/serving.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/serving.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/snapshot.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/snapshot.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/stream_type.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/token_flow.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/token_flow.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal.egg-info/requires.txt +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal.egg-info/top_level.txt +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_docs/__init__.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/__init__.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/api_grpc.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/py.typed +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_version/__main__.py +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/pyproject.toml +0 -0
- {modal-1.4.2.dev5 → modal-1.4.2.dev7}/setup.cfg +0 -0
|
@@ -216,7 +216,11 @@ def create_channel(
|
|
|
216
216
|
for k, v in metadata.items():
|
|
217
217
|
event.metadata[k] = v
|
|
218
218
|
|
|
219
|
-
|
|
219
|
+
idempotency_key = typing.cast(Optional[str], event.metadata.get("x-idempotency-key"))
|
|
220
|
+
if idempotency_key is None:
|
|
221
|
+
logger.debug(f"Sending request to {event.method_name}")
|
|
222
|
+
else:
|
|
223
|
+
logger.debug(f"Sending request to {event.method_name} ({idempotency_key[:8]})")
|
|
220
224
|
|
|
221
225
|
grpclib.events.listen(channel, grpclib.events.SendRequest, send_request)
|
|
222
226
|
|
|
@@ -288,12 +292,14 @@ def process_exception_before_retry(
|
|
|
288
292
|
n_retries: int,
|
|
289
293
|
delay: float,
|
|
290
294
|
idempotency_key: str,
|
|
295
|
+
rpc_elapsed: float,
|
|
291
296
|
):
|
|
292
297
|
"""Process exception before retry, used by `_retry_transient_errors`."""
|
|
293
298
|
with suppress_tb_frame():
|
|
294
299
|
if final_attempt:
|
|
295
300
|
logger.debug(
|
|
296
|
-
f"Final attempt failed with {repr(exc)} {n_retries=} {delay=}
|
|
301
|
+
f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} {rpc_elapsed=:0.2f}s "
|
|
302
|
+
f"for {fn_name} ({idempotency_key[:8]})"
|
|
297
303
|
)
|
|
298
304
|
if isinstance(exc, OSError):
|
|
299
305
|
raise ConnectionError(str(exc))
|
|
@@ -310,7 +316,10 @@ def process_exception_before_retry(
|
|
|
310
316
|
# we handle in the retry logic once we drop this check!
|
|
311
317
|
raise exc
|
|
312
318
|
|
|
313
|
-
logger.debug(
|
|
319
|
+
logger.debug(
|
|
320
|
+
f"Retryable failure {repr(exc)} {n_retries=} {delay=} {rpc_elapsed=:0.2f}s "
|
|
321
|
+
f"for {fn_name} ({idempotency_key[:8]})"
|
|
322
|
+
)
|
|
314
323
|
|
|
315
324
|
|
|
316
325
|
async def _retry_transient_errors(
|
|
@@ -373,6 +382,7 @@ async def _retry_transient_errors(
|
|
|
373
382
|
else:
|
|
374
383
|
timeout = None
|
|
375
384
|
|
|
385
|
+
attempt_started_at = time.monotonic()
|
|
376
386
|
try:
|
|
377
387
|
with suppress_tb_frame():
|
|
378
388
|
return await fn_callable(req, metadata=attempt_metadata, timeout=timeout)
|
|
@@ -409,7 +419,13 @@ async def _retry_transient_errors(
|
|
|
409
419
|
|
|
410
420
|
with suppress_tb_frame():
|
|
411
421
|
process_exception_before_retry(
|
|
412
|
-
exc,
|
|
422
|
+
exc,
|
|
423
|
+
final_attempt,
|
|
424
|
+
fn.name,
|
|
425
|
+
n_retries,
|
|
426
|
+
server_delay,
|
|
427
|
+
idempotency_key,
|
|
428
|
+
time.monotonic() - attempt_started_at,
|
|
413
429
|
)
|
|
414
430
|
|
|
415
431
|
now = time.time()
|
|
@@ -438,7 +454,15 @@ async def _retry_transient_errors(
|
|
|
438
454
|
final_attempt = False
|
|
439
455
|
|
|
440
456
|
with suppress_tb_frame():
|
|
441
|
-
process_exception_before_retry(
|
|
457
|
+
process_exception_before_retry(
|
|
458
|
+
exc,
|
|
459
|
+
final_attempt,
|
|
460
|
+
fn.name,
|
|
461
|
+
n_retries,
|
|
462
|
+
delay,
|
|
463
|
+
idempotency_key,
|
|
464
|
+
time.monotonic() - attempt_started_at,
|
|
465
|
+
)
|
|
442
466
|
|
|
443
467
|
n_retries += 1
|
|
444
468
|
|
|
@@ -502,7 +502,8 @@ class TaskCommandRouterClient:
|
|
|
502
502
|
jwt, url = v1_resp.jwt, v1_resp.url
|
|
503
503
|
|
|
504
504
|
# Ensure the server URL remains stable for the lifetime of this client.
|
|
505
|
-
|
|
505
|
+
if url != self._server_url:
|
|
506
|
+
logger.warning("Task router URL changed during session")
|
|
506
507
|
self._jwt = jwt
|
|
507
508
|
self._jwt_exp = _parse_jwt_expiration(jwt)
|
|
508
509
|
|
|
@@ -20,13 +20,22 @@ from modal._utils.async_utils import synchronizer
|
|
|
20
20
|
from modal._utils.browser_utils import open_url_and_display
|
|
21
21
|
from modal.client import _Client
|
|
22
22
|
from modal.environments import ensure_env
|
|
23
|
+
from modal.exception import InvalidError, NotFoundError
|
|
23
24
|
from modal.output import OutputManager
|
|
24
25
|
from modal.runner import DEPLOYMENT_STRATEGY_TYPE, _stop_and_wait_for_containers
|
|
25
26
|
from modal_proto import api_pb2
|
|
26
27
|
|
|
27
28
|
from .._logs import _FETCH_LIMIT, _MAX_FETCH_RANGE, LogsFilters
|
|
28
29
|
from .._utils.time_utils import locale_tz, timestamp_to_localized_str
|
|
29
|
-
from .utils import
|
|
30
|
+
from .utils import (
|
|
31
|
+
ENV_OPTION,
|
|
32
|
+
YES_OPTION,
|
|
33
|
+
confirm_or_suggest_yes,
|
|
34
|
+
display_table,
|
|
35
|
+
fetch_app_logs,
|
|
36
|
+
stream_app_logs,
|
|
37
|
+
tail_app_logs,
|
|
38
|
+
)
|
|
30
39
|
|
|
31
40
|
APP_IDENTIFIER = Argument("", help="App name or ID")
|
|
32
41
|
NAME_OPTION = typer.Option("", "-n", "--name", help="Deprecated: Pass App name as a positional argument")
|
|
@@ -45,12 +54,50 @@ APP_STATE_TO_MESSAGE = {
|
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
async def resolve_app_identifier(
|
|
58
|
+
app_identifier: str, env: Optional[str], client: Optional[_Client] = None
|
|
59
|
+
) -> tuple[str, str, api_pb2.AppLifecycle]: # Return app_id, environment_name, lifecycle
|
|
60
|
+
"""Handle an App ID or an App name and return context about the App it points at.
|
|
61
|
+
|
|
62
|
+
When a name is provided, we may retrieve either a currently deployed App or an App that
|
|
63
|
+
was recently stopped (if no other App with that name has been deployed since).
|
|
64
|
+
It is up to callers of this function to decide whether it's valid to use the App ID
|
|
65
|
+
based on the lifecycle returned and their specific operations.
|
|
66
|
+
|
|
67
|
+
Can also raise a NotFoundError if the argument matches the App ID regex but the App
|
|
68
|
+
doesn't exist on the backend, or if there is no currently deployed or recently stopped App
|
|
69
|
+
with that name.
|
|
70
|
+
|
|
71
|
+
The function also always returns a valid environment name for any name-based lookups,
|
|
72
|
+
which may reflect the server-defined default environment when the provided argument was null.
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
if client is None:
|
|
76
|
+
client = await _Client.from_env()
|
|
51
77
|
if re.match(r"^ap-[a-zA-Z0-9]{22}$", app_identifier):
|
|
52
|
-
return
|
|
53
|
-
|
|
78
|
+
# Identifier is an App ID. This is unambiguous, so we can make the request and return
|
|
79
|
+
# the lifecycle. AppGetLifecycle will raise NotFoundError if the ID doesn't point at an App.
|
|
80
|
+
# If we return, it's a real App, but it's up to the caller to decide what to do based on
|
|
81
|
+
# the App's current state as conveyed by the lifecycle. We do propagate a NotFoundError
|
|
82
|
+
# from the server if the App ID doesn't actually exist.
|
|
83
|
+
request = api_pb2.AppGetLifecycleRequest(app_id=app_identifier)
|
|
84
|
+
resp = await client.stub.AppGetLifecycle(request)
|
|
85
|
+
return app_identifier, "", resp.lifecycle
|
|
86
|
+
else:
|
|
87
|
+
# Identifier is treated as a name, which may or may not point at a currently deployed App
|
|
88
|
+
# (inside a specific environment)
|
|
89
|
+
request = api_pb2.AppGetByDeploymentNameRequest(name=app_identifier, environment_name=env or "")
|
|
90
|
+
resp = await client.stub.AppGetByDeploymentName(request)
|
|
91
|
+
if resp.app_id:
|
|
92
|
+
# App is currently deployed
|
|
93
|
+
return resp.app_id, resp.environment_name, resp.lifecycle
|
|
94
|
+
elif resp.previous_app_id:
|
|
95
|
+
# An App with this name was recently stopped. Return the ID of the stopped App
|
|
96
|
+
# and let callers decide what to do based on the lifecycle.
|
|
97
|
+
return resp.previous_app_id, resp.environment_name, resp.lifecycle
|
|
98
|
+
else:
|
|
99
|
+
msg = f"No App with name '{app_identifier}' found in the '{resp.environment_name}' environment."
|
|
100
|
+
raise NotFoundError(msg)
|
|
54
101
|
|
|
55
102
|
|
|
56
103
|
@app_cli.command("list")
|
|
@@ -129,7 +176,8 @@ _SOURCE_OPTIONS = {
|
|
|
129
176
|
|
|
130
177
|
|
|
131
178
|
@app_cli.command("logs", no_args_is_help=True)
|
|
132
|
-
|
|
179
|
+
@synchronizer.create_blocking
|
|
180
|
+
async def logs(
|
|
133
181
|
app_identifier: str = APP_IDENTIFIER,
|
|
134
182
|
follow: bool = typer.Option(False, "-f", "--follow", help="Stream log output until App stops"),
|
|
135
183
|
since: Optional[str] = typer.Option(
|
|
@@ -229,7 +277,7 @@ def logs(
|
|
|
229
277
|
if tail is not None and tail > _FETCH_LIMIT:
|
|
230
278
|
raise UsageError(f"--tail value must not exceed {_FETCH_LIMIT}.")
|
|
231
279
|
|
|
232
|
-
app_id =
|
|
280
|
+
app_id, _, _ = await resolve_app_identifier(app_identifier, env)
|
|
233
281
|
|
|
234
282
|
if source is not None:
|
|
235
283
|
if source not in _SOURCE_OPTIONS:
|
|
@@ -255,7 +303,7 @@ def logs(
|
|
|
255
303
|
)
|
|
256
304
|
|
|
257
305
|
if follow:
|
|
258
|
-
stream_app_logs(
|
|
306
|
+
await stream_app_logs.aio(
|
|
259
307
|
app_id,
|
|
260
308
|
task_id=container_id or "",
|
|
261
309
|
show_timestamps=timestamps,
|
|
@@ -278,7 +326,7 @@ def logs(
|
|
|
278
326
|
|
|
279
327
|
if since and tail is None:
|
|
280
328
|
# Range mode: --since without --tail fetches everything in the range.
|
|
281
|
-
fetch_app_logs(
|
|
329
|
+
await fetch_app_logs.aio(
|
|
282
330
|
app_id,
|
|
283
331
|
since_dt,
|
|
284
332
|
until_dt or now,
|
|
@@ -290,7 +338,7 @@ def logs(
|
|
|
290
338
|
# Tail mode: single fetch with limit.
|
|
291
339
|
# --since is a hard floor, --until shifts the anchor.
|
|
292
340
|
effective_tail = tail if tail is not None else _DEFAULT_LOGS_TAIL
|
|
293
|
-
tail_app_logs(
|
|
341
|
+
await tail_app_logs.aio(
|
|
294
342
|
app_id,
|
|
295
343
|
effective_tail,
|
|
296
344
|
show_timestamps=timestamps,
|
|
@@ -338,29 +386,125 @@ async def rollback(
|
|
|
338
386
|
"""
|
|
339
387
|
env = ensure_env(env)
|
|
340
388
|
client = await _Client.from_env()
|
|
341
|
-
app_id = await
|
|
389
|
+
app_id, environment_name, lifecycle = await resolve_app_identifier(app_identifier, env, client)
|
|
390
|
+
if lifecycle.app_state != api_pb2.APP_STATE_DEPLOYED:
|
|
391
|
+
env_suffix = f" in the '{environment_name}' environment" if environment_name else ""
|
|
392
|
+
raise InvalidError(f"App '{app_identifier}' is not deployed{env_suffix}.")
|
|
393
|
+
|
|
342
394
|
if not version:
|
|
343
395
|
version_number = -1
|
|
344
396
|
else:
|
|
345
397
|
if m := re.match(r"v(\d+)", version):
|
|
346
398
|
version_number = int(m.group(1))
|
|
347
399
|
else:
|
|
348
|
-
raise UsageError(f"Invalid version
|
|
400
|
+
raise UsageError(f"Invalid version specifier: {version}")
|
|
349
401
|
req = api_pb2.AppRollbackRequest(app_id=app_id, version=version_number)
|
|
350
402
|
await client.stub.AppRollback(req)
|
|
351
403
|
rich.print("[green]✓[/green] Deployment rollback successful!")
|
|
352
404
|
|
|
353
405
|
|
|
406
|
+
@app_cli.command("rollover", no_args_is_help=True)
|
|
407
|
+
@synchronizer.create_blocking
|
|
408
|
+
async def rollover(
|
|
409
|
+
app_identifier: str = APP_IDENTIFIER,
|
|
410
|
+
*,
|
|
411
|
+
strategy: str = typer.Option(
|
|
412
|
+
"rolling",
|
|
413
|
+
help="Strategy for rollover",
|
|
414
|
+
click_type=click.Choice(get_args(DEPLOYMENT_STRATEGY_TYPE)),
|
|
415
|
+
),
|
|
416
|
+
env: Optional[str] = ENV_OPTION,
|
|
417
|
+
):
|
|
418
|
+
"""Redeploy an App to get new containers without code changes.
|
|
419
|
+
|
|
420
|
+
A rollover replaces existing containers with fresh ones built from the same
|
|
421
|
+
App version — useful for refreshing containers without changing your code.
|
|
422
|
+
The rollover appears as a new entry in the App's deployment history.
|
|
423
|
+
|
|
424
|
+
**Examples:**
|
|
425
|
+
|
|
426
|
+
Rollover an App using a rolling deployment. Running containers are now considered
|
|
427
|
+
outdated and will be gracefully replaced by new ones.
|
|
428
|
+
|
|
429
|
+
```
|
|
430
|
+
modal app rollover my-app
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Rollover an App by terminating any running containers. Inputs on the queue will
|
|
434
|
+
start new containers.
|
|
435
|
+
|
|
436
|
+
```
|
|
437
|
+
modal app rollover my-app --strategy recreate
|
|
438
|
+
```
|
|
439
|
+
"""
|
|
440
|
+
env = ensure_env(env)
|
|
441
|
+
client = await _Client.from_env()
|
|
442
|
+
|
|
443
|
+
app_id, environment_name, lifecycle = await resolve_app_identifier(app_identifier, env, client)
|
|
444
|
+
if lifecycle.app_state != api_pb2.APP_STATE_DEPLOYED:
|
|
445
|
+
env_suffix = f" in the '{environment_name}' environment" if environment_name else ""
|
|
446
|
+
raise InvalidError(f"App '{app_identifier}' is not deployed{env_suffix}.")
|
|
447
|
+
|
|
448
|
+
output_mgr = OutputManager.get()
|
|
449
|
+
output_mgr.print(f"🔨 Starting app rollover with {strategy} strategy")
|
|
450
|
+
t0 = time.monotonic()
|
|
451
|
+
|
|
452
|
+
req = api_pb2.AppRolloverRequest(app_id=app_id)
|
|
453
|
+
response = await client.stub.AppRollover(req)
|
|
454
|
+
print_server_warnings(response.server_warnings)
|
|
455
|
+
|
|
456
|
+
if strategy == "recreate":
|
|
457
|
+
try:
|
|
458
|
+
await _stop_and_wait_for_containers(client, app_id, response.deployed_at, env)
|
|
459
|
+
except Exception as exc:
|
|
460
|
+
warnings.warn(f"App updated successfully, but containers did not all terminate. {exc}", UserWarning)
|
|
461
|
+
output_mgr.print(f"\nView Deployment: [magenta]{response.url}[/magenta]")
|
|
462
|
+
sys.exit(1)
|
|
463
|
+
|
|
464
|
+
duration = time.monotonic() - t0
|
|
465
|
+
output_mgr.step_completed(f"Rollover completed in {duration:.3f}s with {strategy} strategy! 🎉")
|
|
466
|
+
output_mgr.print(f"\nView Deployment: [magenta]{response.url}[/magenta]")
|
|
467
|
+
|
|
468
|
+
|
|
354
469
|
@app_cli.command("stop", no_args_is_help=True)
|
|
355
470
|
@synchronizer.create_blocking
|
|
356
471
|
async def stop(
|
|
357
472
|
app_identifier: str = APP_IDENTIFIER,
|
|
358
473
|
*,
|
|
474
|
+
yes: bool = YES_OPTION,
|
|
359
475
|
env: Optional[str] = ENV_OPTION,
|
|
360
476
|
):
|
|
361
|
-
"""
|
|
477
|
+
"""Permanently stop an App and terminate its running containers."""
|
|
478
|
+
env = ensure_env(env)
|
|
362
479
|
client = await _Client.from_env()
|
|
363
|
-
app_id = await
|
|
480
|
+
app_id, environment_name, lifecycle = await resolve_app_identifier(app_identifier, env, client)
|
|
481
|
+
|
|
482
|
+
if lifecycle.app_state == api_pb2.APP_STATE_STOPPED:
|
|
483
|
+
msg = "App is already stopped."
|
|
484
|
+
if lifecycle.stopped_at:
|
|
485
|
+
stopped_at = timestamp_to_localized_str(lifecycle.stopped_at)
|
|
486
|
+
verb = "Stopped" if lifecycle.stopped_by else "Finished"
|
|
487
|
+
attribution = f" by '{lifecycle.stopped_by}'" if lifecycle.stopped_by else ""
|
|
488
|
+
msg += f" ({verb} at {stopped_at}{attribution})."
|
|
489
|
+
raise SystemExit(msg)
|
|
490
|
+
|
|
491
|
+
if not yes:
|
|
492
|
+
res = await client.stub.TaskList(api_pb2.TaskListRequest(app_id=app_id))
|
|
493
|
+
num_containers = len(res.tasks)
|
|
494
|
+
|
|
495
|
+
if environment_name:
|
|
496
|
+
msg = f"Are you sure you want to stop App '{app_identifier}' in the '{environment_name}' environment?"
|
|
497
|
+
else:
|
|
498
|
+
msg = f"Are you sure you want to stop App '{app_identifier}'?"
|
|
499
|
+
|
|
500
|
+
if num_containers:
|
|
501
|
+
msg += (
|
|
502
|
+
f" This will immediately terminate {num_containers} running"
|
|
503
|
+
f" container{'s' if num_containers != 1 else ''}."
|
|
504
|
+
)
|
|
505
|
+
else:
|
|
506
|
+
msg += " No containers are currently running."
|
|
507
|
+
confirm_or_suggest_yes(msg)
|
|
364
508
|
req = api_pb2.AppStopRequest(app_id=app_id, source=api_pb2.APP_STOP_SOURCE_CLI)
|
|
365
509
|
await client.stub.AppStop(req)
|
|
366
510
|
|
|
@@ -373,7 +517,7 @@ async def history(
|
|
|
373
517
|
env: Optional[str] = ENV_OPTION,
|
|
374
518
|
json: bool = False,
|
|
375
519
|
):
|
|
376
|
-
"""Show App deployment history
|
|
520
|
+
"""Show an App's deployment history.
|
|
377
521
|
|
|
378
522
|
**Examples:**
|
|
379
523
|
|
|
@@ -383,7 +527,7 @@ async def history(
|
|
|
383
527
|
modal app history ap-123456
|
|
384
528
|
```
|
|
385
529
|
|
|
386
|
-
Get the history for
|
|
530
|
+
Get the history for an App based on its name:
|
|
387
531
|
|
|
388
532
|
```
|
|
389
533
|
modal app history my-app
|
|
@@ -392,7 +536,7 @@ async def history(
|
|
|
392
536
|
"""
|
|
393
537
|
env = ensure_env(env)
|
|
394
538
|
client = await _Client.from_env()
|
|
395
|
-
app_id = await
|
|
539
|
+
app_id, _, _ = await resolve_app_identifier(app_identifier, env, client)
|
|
396
540
|
resp = await client.stub.AppDeploymentHistory(api_pb2.AppDeploymentHistoryRequest(app_id=app_id))
|
|
397
541
|
|
|
398
542
|
columns = [
|
|
@@ -467,66 +611,6 @@ async def dashboard(
|
|
|
467
611
|
```
|
|
468
612
|
"""
|
|
469
613
|
client = await _Client.from_env()
|
|
470
|
-
app_id = await
|
|
471
|
-
|
|
614
|
+
app_id, _, _ = await resolve_app_identifier(app_identifier, env, client)
|
|
472
615
|
url = f"https://modal.com/id/{app_id}"
|
|
473
616
|
open_url_and_display(url, "App dashboard")
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
@app_cli.command("rollover", no_args_is_help=True, context_settings={"ignore_unknown_options": True})
|
|
477
|
-
@synchronizer.create_blocking
|
|
478
|
-
async def rollover(
|
|
479
|
-
app_identifier: str = APP_IDENTIFIER,
|
|
480
|
-
*,
|
|
481
|
-
strategy: str = typer.Option(
|
|
482
|
-
"rolling",
|
|
483
|
-
help="Strategy for rollover",
|
|
484
|
-
click_type=click.Choice(get_args(DEPLOYMENT_STRATEGY_TYPE)),
|
|
485
|
-
),
|
|
486
|
-
env: Optional[str] = ENV_OPTION,
|
|
487
|
-
):
|
|
488
|
-
"""Rollover an App.
|
|
489
|
-
|
|
490
|
-
A rollover replaces existing containers with fresh ones built from the same
|
|
491
|
-
App version — useful for refreshing containers without changing your code.
|
|
492
|
-
The rollover appears as a new entry in the App's deployment history.
|
|
493
|
-
|
|
494
|
-
**Examples:**
|
|
495
|
-
|
|
496
|
-
Rollover an App using a rolling deployment. Running containers are now considered
|
|
497
|
-
outdated and new containers will replace them.
|
|
498
|
-
|
|
499
|
-
```
|
|
500
|
-
modal app rollover my-app
|
|
501
|
-
```
|
|
502
|
-
|
|
503
|
-
Rollover an App by termatining all running containers. Inputs on the queue will
|
|
504
|
-
start new containers.
|
|
505
|
-
|
|
506
|
-
```
|
|
507
|
-
modal app rollover my-app --strategy recreate
|
|
508
|
-
```
|
|
509
|
-
"""
|
|
510
|
-
env = ensure_env(env)
|
|
511
|
-
output_mgr = OutputManager.get()
|
|
512
|
-
output_mgr.print(f"🔨 Starting app rollover with {strategy} strategy")
|
|
513
|
-
t0 = time.monotonic()
|
|
514
|
-
|
|
515
|
-
client = await _Client.from_env()
|
|
516
|
-
app_id = await get_app_id.aio(app_identifier, env, client)
|
|
517
|
-
|
|
518
|
-
req = api_pb2.AppRolloverRequest(app_id=app_id)
|
|
519
|
-
response = await client.stub.AppRollover(req)
|
|
520
|
-
print_server_warnings(response.server_warnings)
|
|
521
|
-
|
|
522
|
-
if strategy == "recreate":
|
|
523
|
-
try:
|
|
524
|
-
await _stop_and_wait_for_containers(client, app_id, response.deployed_at, env)
|
|
525
|
-
except Exception as exc:
|
|
526
|
-
warnings.warn(f"App updated successfully, but containers did not all terminate. {exc}", UserWarning)
|
|
527
|
-
output_mgr.print(f"\nView Deployment: [magenta]{response.url}[/magenta]")
|
|
528
|
-
sys.exit(1)
|
|
529
|
-
|
|
530
|
-
duration = time.monotonic() - t0
|
|
531
|
-
output_mgr.step_completed(f"Rollover completed in {duration:.3f}s with {strategy} strategy! 🎉")
|
|
532
|
-
output_mgr.print(f"\nView Deployment: [magenta]{response.url}[/magenta]")
|
|
@@ -14,7 +14,16 @@ from modal._output.pty import get_pty_info
|
|
|
14
14
|
from modal._utils.async_utils import synchronizer
|
|
15
15
|
from modal._utils.time_utils import timestamp_to_localized_str
|
|
16
16
|
from modal.cli.app import _DEFAULT_LOGS_TAIL, _SOURCE_OPTIONS, _parse_time_arg
|
|
17
|
-
from modal.cli.utils import
|
|
17
|
+
from modal.cli.utils import (
|
|
18
|
+
ENV_OPTION,
|
|
19
|
+
YES_OPTION,
|
|
20
|
+
confirm_or_suggest_yes,
|
|
21
|
+
display_table,
|
|
22
|
+
fetch_app_logs,
|
|
23
|
+
is_tty,
|
|
24
|
+
stream_app_logs,
|
|
25
|
+
tail_app_logs,
|
|
26
|
+
)
|
|
18
27
|
from modal.client import _Client
|
|
19
28
|
from modal.config import config
|
|
20
29
|
from modal.container_process import _ContainerProcess
|
|
@@ -285,11 +294,20 @@ async def exec(
|
|
|
285
294
|
|
|
286
295
|
@container_cli.command("stop")
|
|
287
296
|
@synchronizer.create_blocking
|
|
288
|
-
async def stop(
|
|
297
|
+
async def stop(
|
|
298
|
+
container_id: str = typer.Argument(help="Container ID"),
|
|
299
|
+
*,
|
|
300
|
+
yes: bool = YES_OPTION,
|
|
301
|
+
):
|
|
289
302
|
"""Stop a currently-running container and reassign its in-progress inputs.
|
|
290
303
|
|
|
291
304
|
This will send the container a SIGINT signal that Modal will handle.
|
|
292
305
|
"""
|
|
293
306
|
client = await _Client.from_env()
|
|
307
|
+
resp = await client.stub.TaskGetInfo(api_pb2.TaskGetInfoRequest(task_id=container_id))
|
|
308
|
+
if resp.info.finished_at:
|
|
309
|
+
raise SystemExit(f"Container '{container_id}' is already stopped.")
|
|
310
|
+
if not yes:
|
|
311
|
+
confirm_or_suggest_yes(f"Are you sure you want to stop container '{container_id}'?")
|
|
294
312
|
request = api_pb2.ContainerStopRequest(task_id=container_id)
|
|
295
313
|
await client.stub.ContainerStop(request)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
2
|
import asyncio
|
|
3
3
|
import io
|
|
4
|
+
import sys
|
|
4
5
|
from collections.abc import Sequence
|
|
5
6
|
from contextlib import nullcontext
|
|
6
7
|
from csv import writer as csv_writer
|
|
@@ -12,14 +13,11 @@ import typer
|
|
|
12
13
|
from rich.table import Column, Table
|
|
13
14
|
from rich.text import Text
|
|
14
15
|
|
|
15
|
-
from modal_proto import api_pb2
|
|
16
|
-
|
|
17
16
|
from .._logs import LogsFilters, fetch_logs, tail_logs
|
|
18
17
|
from .._output.pty import _build_log_prefix, get_app_logs_loop
|
|
19
18
|
from .._utils.async_utils import synchronizer
|
|
20
19
|
from ..client import _Client
|
|
21
|
-
from ..
|
|
22
|
-
from ..exception import InvalidError, NotFoundError
|
|
20
|
+
from ..exception import InvalidError
|
|
23
21
|
from ..output import OutputManager
|
|
24
22
|
|
|
25
23
|
|
|
@@ -125,19 +123,6 @@ async def fetch_app_logs(
|
|
|
125
123
|
await _drain_batches(output_mgr, batches, prefix_fields or [], filters.search_text)
|
|
126
124
|
|
|
127
125
|
|
|
128
|
-
@synchronizer.create_blocking
|
|
129
|
-
async def get_app_id_from_name(name: str, env: Optional[str], client: Optional[_Client] = None) -> str:
|
|
130
|
-
if client is None:
|
|
131
|
-
client = await _Client.from_env()
|
|
132
|
-
env_name = ensure_env(env)
|
|
133
|
-
request = api_pb2.AppGetByDeploymentNameRequest(name=name, environment_name=env_name)
|
|
134
|
-
resp = await client.stub.AppGetByDeploymentName(request)
|
|
135
|
-
if not resp.app_id:
|
|
136
|
-
env_comment = f" in the '{env_name}' environment" if env_name else ""
|
|
137
|
-
raise NotFoundError(f"Could not find a deployed app named '{name}'{env_comment}.")
|
|
138
|
-
return resp.app_id
|
|
139
|
-
|
|
140
|
-
|
|
141
126
|
def _plain(text: Union[Text, str]) -> str:
|
|
142
127
|
return text.plain if isinstance(text, Text) else text
|
|
143
128
|
|
|
@@ -185,3 +170,11 @@ Otherwise, raises an error if the workspace has multiple environments.
|
|
|
185
170
|
ENV_OPTION = typer.Option(None, "-e", "--env", help=ENV_OPTION_HELP)
|
|
186
171
|
|
|
187
172
|
YES_OPTION = typer.Option(False, "-y", "--yes", help="Run without pausing for confirmation.")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def confirm_or_suggest_yes(msg: str) -> None:
|
|
176
|
+
"""Prompt for confirmation, or abort with a hint to use --yes if stdin is not a TTY."""
|
|
177
|
+
if not sys.stdin.isatty():
|
|
178
|
+
typer.echo(f"{msg} [y/N]: ")
|
|
179
|
+
raise SystemExit("Aborted: no interactive terminal detected. Rerun with --yes (-y) to skip confirmation.")
|
|
180
|
+
typer.confirm(msg, default=False, abort=True)
|
|
@@ -35,7 +35,7 @@ class _Client:
|
|
|
35
35
|
server_url: str,
|
|
36
36
|
client_type: int,
|
|
37
37
|
credentials: typing.Optional[tuple[str, str]],
|
|
38
|
-
version: str = "1.4.2.
|
|
38
|
+
version: str = "1.4.2.dev7",
|
|
39
39
|
):
|
|
40
40
|
"""mdmd:hidden
|
|
41
41
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -171,7 +171,7 @@ class Client:
|
|
|
171
171
|
server_url: str,
|
|
172
172
|
client_type: int,
|
|
173
173
|
credentials: typing.Optional[tuple[str, str]],
|
|
174
|
-
version: str = "1.4.2.
|
|
174
|
+
version: str = "1.4.2.dev7",
|
|
175
175
|
):
|
|
176
176
|
"""mdmd:hidden
|
|
177
177
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -84,8 +84,8 @@ class DictInfo:
|
|
|
84
84
|
class _DictManager:
|
|
85
85
|
"""Namespace with methods for managing named Dict objects."""
|
|
86
86
|
|
|
87
|
-
@staticmethod
|
|
88
87
|
async def create(
|
|
88
|
+
self,
|
|
89
89
|
name: str, # Name to use for the new Dict
|
|
90
90
|
*,
|
|
91
91
|
allow_existing: bool = False, # If True, no-op when the Dict already exists
|
|
@@ -137,8 +137,8 @@ class _DictManager:
|
|
|
137
137
|
if not allow_existing:
|
|
138
138
|
raise
|
|
139
139
|
|
|
140
|
-
@staticmethod
|
|
141
140
|
async def list(
|
|
141
|
+
self,
|
|
142
142
|
*,
|
|
143
143
|
max_objects: Optional[int] = None, # Limit results to this size
|
|
144
144
|
created_before: Optional[Union[datetime, str]] = None, # Limit based on creation date
|
|
@@ -205,8 +205,8 @@ class _DictManager:
|
|
|
205
205
|
]
|
|
206
206
|
return dicts[:max_objects] if max_objects is not None else dicts
|
|
207
207
|
|
|
208
|
-
@staticmethod
|
|
209
208
|
async def delete(
|
|
209
|
+
self,
|
|
210
210
|
name: str, # Name of the Dict to delete
|
|
211
211
|
*,
|
|
212
212
|
allow_missing: bool = False, # If True, don't raise an error if the Dict doesn't exist
|
|
@@ -299,8 +299,8 @@ class _Dict(_Object, type_prefix="di"):
|
|
|
299
299
|
|
|
300
300
|
@classproperty
|
|
301
301
|
@classmethod
|
|
302
|
-
def objects(cls) ->
|
|
303
|
-
return _DictManager
|
|
302
|
+
def objects(cls) -> _DictManager:
|
|
303
|
+
return _DictManager()
|
|
304
304
|
|
|
305
305
|
@property
|
|
306
306
|
def name(self) -> Optional[str]:
|
|
@@ -44,8 +44,8 @@ class DictInfo:
|
|
|
44
44
|
|
|
45
45
|
class _DictManager:
|
|
46
46
|
"""Namespace with methods for managing named Dict objects."""
|
|
47
|
-
@staticmethod
|
|
48
47
|
async def create(
|
|
48
|
+
self,
|
|
49
49
|
name: str,
|
|
50
50
|
*,
|
|
51
51
|
allow_existing: bool = False,
|
|
@@ -80,8 +80,8 @@ class _DictManager:
|
|
|
80
80
|
"""
|
|
81
81
|
...
|
|
82
82
|
|
|
83
|
-
@staticmethod
|
|
84
83
|
async def list(
|
|
84
|
+
self,
|
|
85
85
|
*,
|
|
86
86
|
max_objects: typing.Optional[int] = None,
|
|
87
87
|
created_before: typing.Union[datetime.datetime, str, None] = None,
|
|
@@ -114,8 +114,8 @@ class _DictManager:
|
|
|
114
114
|
"""
|
|
115
115
|
...
|
|
116
116
|
|
|
117
|
-
@staticmethod
|
|
118
117
|
async def delete(
|
|
118
|
+
self,
|
|
119
119
|
name: str,
|
|
120
120
|
*,
|
|
121
121
|
allow_missing: bool = False,
|
|
@@ -224,7 +224,7 @@ class DictManager:
|
|
|
224
224
|
"""
|
|
225
225
|
...
|
|
226
226
|
|
|
227
|
-
create:
|
|
227
|
+
create: __create_spec
|
|
228
228
|
|
|
229
229
|
class __list_spec(typing_extensions.Protocol):
|
|
230
230
|
def __call__(
|
|
@@ -297,7 +297,7 @@ class DictManager:
|
|
|
297
297
|
"""
|
|
298
298
|
...
|
|
299
299
|
|
|
300
|
-
list:
|
|
300
|
+
list: __list_spec
|
|
301
301
|
|
|
302
302
|
class __delete_spec(typing_extensions.Protocol):
|
|
303
303
|
def __call__(
|
|
@@ -360,7 +360,7 @@ class DictManager:
|
|
|
360
360
|
"""
|
|
361
361
|
...
|
|
362
362
|
|
|
363
|
-
delete:
|
|
363
|
+
delete: __delete_spec
|
|
364
364
|
|
|
365
365
|
class _Dict(modal._object._Object):
|
|
366
366
|
"""Distributed dictionary for storage in Modal apps.
|
|
@@ -413,7 +413,7 @@ class _Dict(modal._object._Object):
|
|
|
413
413
|
|
|
414
414
|
@synchronicity.classproperty
|
|
415
415
|
@classmethod
|
|
416
|
-
def objects(cls) ->
|
|
416
|
+
def objects(cls) -> _DictManager: ...
|
|
417
417
|
@property
|
|
418
418
|
def name(self) -> typing.Optional[str]: ...
|
|
419
419
|
def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
|
|
@@ -655,7 +655,7 @@ class Dict(modal.object.Object):
|
|
|
655
655
|
|
|
656
656
|
@synchronicity.classproperty
|
|
657
657
|
@classmethod
|
|
658
|
-
def objects(cls) ->
|
|
658
|
+
def objects(cls) -> DictManager: ...
|
|
659
659
|
@property
|
|
660
660
|
def name(self) -> typing.Optional[str]: ...
|
|
661
661
|
def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
|