modAL 1.4.2__tar.gz → 1.4.2.dev1__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 → modal-1.4.2.dev1}/PKG-INFO +1 -1
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_functions.py +3 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_runtime/asgi.py +2 -5
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/grpc_utils.py +5 -29
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/rand_pb_testing.py +1 -3
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/sandbox_fs_utils.py +0 -37
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/task_command_router_client.py +1 -11
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/app.py +4 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/app.pyi +4 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/app.py +79 -163
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/container.py +2 -20
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/entry_point.py +24 -8
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/selector.py +4 -12
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/utils.py +17 -10
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/client.py +0 -7
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/client.pyi +10 -14
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/dict.py +5 -5
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/dict.pyi +8 -8
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/exception.py +0 -6
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/file_io.py +2 -2
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/functions.pyi +7 -6
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/image.py +0 -2
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/image.pyi +0 -2
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/queue.py +5 -5
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/queue.pyi +8 -8
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/sandbox.py +18 -90
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/sandbox.pyi +27 -87
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/sandbox_fs.py +7 -44
- modal-1.4.2.dev1/modal/sandbox_fs.pyi +627 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/secret.py +5 -5
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/secret.pyi +8 -8
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/volume.py +5 -5
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/volume.pyi +8 -8
- {modal-1.4.2 → modal-1.4.2.dev1}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.4.2 → modal-1.4.2.dev1}/modal.egg-info/SOURCES.txt +0 -2
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_docs/mdmd/mdmd.py +13 -30
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_proto/api_grpc.py +0 -80
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_proto/api_pb2.py +1067 -1185
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_proto/api_pb2.pyi +5 -305
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_proto/api_pb2_grpc.py +0 -166
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_proto/api_pb2_grpc.pyi +0 -52
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_proto/modal_api_grpc.py +0 -5
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_version/__init__.py +1 -1
- {modal-1.4.2 → modal-1.4.2.dev1}/pyproject.toml +3 -3
- modal-1.4.2/modal/cli/bootstrap.py +0 -136
- modal-1.4.2/modal/cli/logo.py +0 -70
- modal-1.4.2/modal/sandbox_fs.pyi +0 -672
- {modal-1.4.2 → modal-1.4.2.dev1}/LICENSE +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/README.md +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/__init__.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/__main__.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_billing.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_clustered_functions.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_clustered_functions.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_container_entrypoint.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_grpc_client.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_ipython.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_load_context.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_location.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_logs.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_object.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_output/__init__.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_output/manager.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_output/pty.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_output/rich.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_output/status.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_partial_function.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_resolver.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_resources.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_runtime/__init__.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_runtime/execution_context.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_runtime/telemetry.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_serialization.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_server.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_traceback.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_tunnel.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_tunnel.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_type_manager.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/__init__.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/app_utils.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/async_utils.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/blob_utils.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/browser_utils.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/deprecation.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/docker_utils.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/function_utils.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/git_utils.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/hash_utils.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/http_utils.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/logger.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/mount_utils.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/name_utils.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/package_utils.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/shell_utils.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_utils/time_utils.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_vendor/__init__.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_vendor/tblib.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_vendor/version.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/_watcher.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/billing.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/builder/2023.12.txt +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/builder/2024.04.txt +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/builder/2024.10.txt +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/builder/2025.06.txt +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/builder/README.md +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/builder/base-images.json +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/call_graph.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/__init__.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/_download.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/_traceback.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/billing.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/changelog.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/cluster.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/config.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/dashboard.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/dict.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/environment.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/import_refs.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/launch.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/network_file_system.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/profile.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/programs/__init__.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/programs/vscode.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/queues.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/run.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/secret.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/shell.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/token.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cli/volume.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cls.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/cls.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/config.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/container_process.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/container_process.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/environments.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/environments.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/experimental/__init__.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/experimental/flash.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/experimental/flash.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/experimental/ipython.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/file_io.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/file_pattern_matcher.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/functions.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/io_streams.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/io_streams.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/mount.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/mount.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/network_file_system.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/network_file_system.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/object.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/object.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/output.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/parallel_map.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/parallel_map.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/partial_function.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/partial_function.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/proxy.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/proxy.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/py.typed +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/retries.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/runner.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/runner.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/running_app.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/schedule.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/scheduler_placement.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/server.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/server.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/serving.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/serving.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/snapshot.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/snapshot.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/stream_type.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/token_flow.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal/token_flow.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal.egg-info/requires.txt +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal.egg-info/top_level.txt +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_docs/__init__.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_proto/__init__.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_proto/py.typed +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/modal_version/__main__.py +0 -0
- {modal-1.4.2 → modal-1.4.2.dev1}/setup.cfg +0 -0
|
@@ -700,6 +700,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
700
700
|
ephemeral_disk: Optional[int] = None,
|
|
701
701
|
include_source: bool = True,
|
|
702
702
|
experimental_options: Optional[dict[str, str]] = None,
|
|
703
|
+
_experimental_proxy_ip: Optional[str] = None,
|
|
703
704
|
restrict_output: bool = False,
|
|
704
705
|
http_config: Optional[api_pb2.HTTPConfig] = None,
|
|
705
706
|
) -> "_Function":
|
|
@@ -1040,6 +1041,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1040
1041
|
# ---
|
|
1041
1042
|
_experimental_group_size=cluster_size or 0, # Experimental: Clustered functions
|
|
1042
1043
|
_experimental_concurrent_cancellations=True,
|
|
1044
|
+
_experimental_proxy_ip=_experimental_proxy_ip,
|
|
1043
1045
|
# --- These are deprecated in favor of autoscaler_settings
|
|
1044
1046
|
warm_pool_size=min_containers or 0,
|
|
1045
1047
|
concurrency_limit=max_containers or 0,
|
|
@@ -1082,6 +1084,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1082
1084
|
_experimental_group_size=function_definition._experimental_group_size,
|
|
1083
1085
|
_experimental_buffer_containers=function_definition._experimental_buffer_containers,
|
|
1084
1086
|
_experimental_custom_scaling=function_definition._experimental_custom_scaling,
|
|
1087
|
+
_experimental_proxy_ip=function_definition._experimental_proxy_ip,
|
|
1085
1088
|
snapshot_debug=function_definition.snapshot_debug,
|
|
1086
1089
|
runtime_perf_record=function_definition.runtime_perf_record,
|
|
1087
1090
|
function_schema=function_schema,
|
|
@@ -474,11 +474,8 @@ async def _proxy_lifespan_request(base_url, scope, receive, send) -> None:
|
|
|
474
474
|
auto_decompress=False,
|
|
475
475
|
read_bufsize=1024 * 1024, # 1 MiB
|
|
476
476
|
connector=aiohttp.TCPConnector(
|
|
477
|
-
limit=1000
|
|
478
|
-
|
|
479
|
-
# from stale pooled connections.
|
|
480
|
-
force_close=True,
|
|
481
|
-
),
|
|
477
|
+
limit=1000
|
|
478
|
+
), # 100 is the default max, but 1000 is the max for `@modal.concurrent`.
|
|
482
479
|
# Note: these values will need to be kept in sync.
|
|
483
480
|
**(
|
|
484
481
|
# These options were introduced in aiohttp 3.9, and we can remove the
|
|
@@ -216,11 +216,7 @@ def create_channel(
|
|
|
216
216
|
for k, v in metadata.items():
|
|
217
217
|
event.metadata[k] = v
|
|
218
218
|
|
|
219
|
-
|
|
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]})")
|
|
219
|
+
logger.debug(f"Sending request to {event.method_name}")
|
|
224
220
|
|
|
225
221
|
grpclib.events.listen(channel, grpclib.events.SendRequest, send_request)
|
|
226
222
|
|
|
@@ -292,14 +288,12 @@ def process_exception_before_retry(
|
|
|
292
288
|
n_retries: int,
|
|
293
289
|
delay: float,
|
|
294
290
|
idempotency_key: str,
|
|
295
|
-
rpc_elapsed: float,
|
|
296
291
|
):
|
|
297
292
|
"""Process exception before retry, used by `_retry_transient_errors`."""
|
|
298
293
|
with suppress_tb_frame():
|
|
299
294
|
if final_attempt:
|
|
300
295
|
logger.debug(
|
|
301
|
-
f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} {
|
|
302
|
-
f"for {fn_name} ({idempotency_key[:8]})"
|
|
296
|
+
f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} for {fn_name} ({idempotency_key[:8]})"
|
|
303
297
|
)
|
|
304
298
|
if isinstance(exc, OSError):
|
|
305
299
|
raise ConnectionError(str(exc))
|
|
@@ -316,10 +310,7 @@ def process_exception_before_retry(
|
|
|
316
310
|
# we handle in the retry logic once we drop this check!
|
|
317
311
|
raise exc
|
|
318
312
|
|
|
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
|
-
)
|
|
313
|
+
logger.debug(f"Retryable failure {repr(exc)} {n_retries=} {delay=} for {fn_name} ({idempotency_key[:8]})")
|
|
323
314
|
|
|
324
315
|
|
|
325
316
|
async def _retry_transient_errors(
|
|
@@ -382,7 +373,6 @@ async def _retry_transient_errors(
|
|
|
382
373
|
else:
|
|
383
374
|
timeout = None
|
|
384
375
|
|
|
385
|
-
attempt_started_at = time.monotonic()
|
|
386
376
|
try:
|
|
387
377
|
with suppress_tb_frame():
|
|
388
378
|
return await fn_callable(req, metadata=attempt_metadata, timeout=timeout)
|
|
@@ -419,13 +409,7 @@ async def _retry_transient_errors(
|
|
|
419
409
|
|
|
420
410
|
with suppress_tb_frame():
|
|
421
411
|
process_exception_before_retry(
|
|
422
|
-
exc,
|
|
423
|
-
final_attempt,
|
|
424
|
-
fn.name,
|
|
425
|
-
n_retries,
|
|
426
|
-
server_delay,
|
|
427
|
-
idempotency_key,
|
|
428
|
-
time.monotonic() - attempt_started_at,
|
|
412
|
+
exc, final_attempt, fn.name, n_retries, server_delay, idempotency_key
|
|
429
413
|
)
|
|
430
414
|
|
|
431
415
|
now = time.time()
|
|
@@ -454,15 +438,7 @@ async def _retry_transient_errors(
|
|
|
454
438
|
final_attempt = False
|
|
455
439
|
|
|
456
440
|
with suppress_tb_frame():
|
|
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
|
-
)
|
|
441
|
+
process_exception_before_retry(exc, final_attempt, fn.name, n_retries, delay, idempotency_key)
|
|
466
442
|
|
|
467
443
|
n_retries += 1
|
|
468
444
|
|
|
@@ -52,9 +52,8 @@ def _fill(msg, desc: Descriptor, rand: Random) -> None:
|
|
|
52
52
|
if hasattr(field, "is_repeated"):
|
|
53
53
|
is_repeated = field.is_repeated # type: ignore
|
|
54
54
|
else:
|
|
55
|
-
is_repeated = field.label == FieldDescriptor.LABEL_REPEATED
|
|
55
|
+
is_repeated = field.label == FieldDescriptor.LABEL_REPEATED
|
|
56
56
|
if is_message:
|
|
57
|
-
assert field.message_type is not None
|
|
58
57
|
msg_field = getattr(msg, field.name)
|
|
59
58
|
if is_repeated:
|
|
60
59
|
num = rand.randint(0, 2)
|
|
@@ -65,7 +64,6 @@ def _fill(msg, desc: Descriptor, rand: Random) -> None:
|
|
|
65
64
|
_fill(msg_field, field.message_type, rand)
|
|
66
65
|
else:
|
|
67
66
|
if field.type == FieldDescriptor.TYPE_ENUM:
|
|
68
|
-
assert field.enum_type is not None
|
|
69
67
|
enum_values = [x.number for x in field.enum_type.values]
|
|
70
68
|
generator = lambda rand: rand.choice(enum_values) # noqa: E731
|
|
71
69
|
|
|
@@ -18,7 +18,6 @@ from ..exception import (
|
|
|
18
18
|
SandboxFilesystemIsADirectoryError,
|
|
19
19
|
SandboxFilesystemNotADirectoryError,
|
|
20
20
|
SandboxFilesystemNotFoundError,
|
|
21
|
-
SandboxFilesystemPathAlreadyExistsError,
|
|
22
21
|
SandboxFilesystemPermissionError,
|
|
23
22
|
ServiceError,
|
|
24
23
|
)
|
|
@@ -189,42 +188,6 @@ def raise_remove_error(returncode: int, stderr: Union[str, bytes], remote_path:
|
|
|
189
188
|
raise SandboxFilesystemError(f"Operation on '{remote_path}' failed with exit code {returncode}")
|
|
190
189
|
|
|
191
190
|
|
|
192
|
-
def make_make_directory_command(remote_path: str, create_parents: bool) -> str:
|
|
193
|
-
"""Build the JSON command string for a MakeDirectory operation.
|
|
194
|
-
|
|
195
|
-
The returned JSON must match the `Command` enum in the modal-sandbox-fs-tools
|
|
196
|
-
Rust crate (crates/modal-sandbox-fs-tools/src/lib.rs). Treat changes to
|
|
197
|
-
this schema like protobuf changes: fields must not be removed or renamed,
|
|
198
|
-
only added with backwards-compatible defaults.
|
|
199
|
-
"""
|
|
200
|
-
return json.dumps({"MakeDirectory": {"path": remote_path, "parents": create_parents}})
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
def raise_make_directory_error(returncode: int, stderr: Union[str, bytes], remote_path: str) -> NoReturn:
|
|
204
|
-
if payload := try_parse_error_payload(stderr):
|
|
205
|
-
logger.debug(
|
|
206
|
-
f"sandbox-fs-tools make_directory error: path={remote_path}, "
|
|
207
|
-
f"error_kind={payload.error_kind}, message={payload.message}, detail={payload.detail}"
|
|
208
|
-
)
|
|
209
|
-
if payload.error_kind == "NotFound":
|
|
210
|
-
raise SandboxFilesystemNotFoundError(f"{payload.message}: {remote_path}")
|
|
211
|
-
if payload.error_kind == "PathAlreadyExists":
|
|
212
|
-
raise SandboxFilesystemPathAlreadyExistsError(f"{payload.message}: {remote_path}")
|
|
213
|
-
if payload.error_kind == "NotDirectory":
|
|
214
|
-
raise SandboxFilesystemNotADirectoryError(f"{payload.message}: {remote_path}")
|
|
215
|
-
if payload.error_kind == "PermissionDenied":
|
|
216
|
-
raise SandboxFilesystemPermissionError(f"{payload.message}: {remote_path}")
|
|
217
|
-
if payload.error_kind == "NotSupported":
|
|
218
|
-
raise InvalidError(
|
|
219
|
-
f"{payload.message}: {remote_path} - this operation is not supported for CloudBucketMounts"
|
|
220
|
-
)
|
|
221
|
-
raise SandboxFilesystemError(payload.message)
|
|
222
|
-
|
|
223
|
-
if stderr_text := _stderr_to_text(stderr):
|
|
224
|
-
logger.debug(f"Unstructured modal-sandbox-fs-tools stderr: {stderr_text}")
|
|
225
|
-
raise SandboxFilesystemError(f"Operation on '{remote_path}' failed with exit code {returncode}")
|
|
226
|
-
|
|
227
|
-
|
|
228
191
|
def validate_absolute_remote_path(remote_path: str, operation: str) -> None:
|
|
229
192
|
if not PurePosixPath(remote_path).is_absolute():
|
|
230
193
|
raise InvalidError(f"Sandbox.filesystem.{operation}() currently only supports absolute remote_path values")
|
|
@@ -126,11 +126,8 @@ async def fetch_command_router_access_v2(
|
|
|
126
126
|
server_client, sandbox_id: str
|
|
127
127
|
) -> api_pb2.SandboxGetCommandRouterAccessResponse:
|
|
128
128
|
"""Fetch direct command router access info from Modal server for a V2 sandbox."""
|
|
129
|
-
assert server_client._auth_token_manager
|
|
130
|
-
auth_token = await server_client._auth_token_manager.get_token()
|
|
131
129
|
return await server_client.stub.SandboxGetCommandRouterAccess(
|
|
132
130
|
api_pb2.SandboxGetCommandRouterAccessRequest(sandbox_id=sandbox_id),
|
|
133
|
-
metadata=[("x-modal-auth-token", auth_token)],
|
|
134
131
|
)
|
|
135
132
|
|
|
136
133
|
|
|
@@ -505,8 +502,7 @@ class TaskCommandRouterClient:
|
|
|
505
502
|
jwt, url = v1_resp.jwt, v1_resp.url
|
|
506
503
|
|
|
507
504
|
# Ensure the server URL remains stable for the lifetime of this client.
|
|
508
|
-
|
|
509
|
-
logger.warning("Task router URL changed during session")
|
|
505
|
+
assert url == self._server_url, "Task router URL changed during session"
|
|
510
506
|
self._jwt = jwt
|
|
511
507
|
self._jwt_exp = _parse_jwt_expiration(jwt)
|
|
512
508
|
|
|
@@ -620,12 +616,6 @@ class TaskCommandRouterClient:
|
|
|
620
616
|
lambda: self._call_with_auth_retry(self._stub.TaskMountDirectory, request)
|
|
621
617
|
)
|
|
622
618
|
|
|
623
|
-
async def unmount_image(self, request: sr_pb2.TaskUnmountDirectoryRequest):
|
|
624
|
-
with grpc_error_converter():
|
|
625
|
-
return await call_with_retries_on_transient_errors(
|
|
626
|
-
lambda: self._call_with_auth_retry(self._stub.TaskUnmountDirectory, request)
|
|
627
|
-
)
|
|
628
|
-
|
|
629
619
|
async def snapshot_directory(
|
|
630
620
|
self, request: sr_pb2.TaskSnapshotDirectoryRequest
|
|
631
621
|
) -> sr_pb2.TaskSnapshotDirectoryResponse:
|
|
@@ -753,6 +753,7 @@ class _App:
|
|
|
753
753
|
include_source: Optional[bool] = None,
|
|
754
754
|
experimental_options: Optional[dict[str, Any]] = None,
|
|
755
755
|
# Parameters below here are experimental. Use with caution!
|
|
756
|
+
_experimental_proxy_ip: Optional[str] = None, # IP address of proxy
|
|
756
757
|
_experimental_restrict_output: bool = False, # Don't use pickle for return values
|
|
757
758
|
# Parameters below here are deprecated. Please update your code as suggested
|
|
758
759
|
max_inputs: Optional[int] = None, # Replaced with `single_use_containers`
|
|
@@ -913,6 +914,7 @@ class _App:
|
|
|
913
914
|
rdma=rdma,
|
|
914
915
|
include_source=include_source if include_source is not None else local_state.include_source_default,
|
|
915
916
|
experimental_options={k: str(v) for k, v in (experimental_options or {}).items()},
|
|
917
|
+
_experimental_proxy_ip=_experimental_proxy_ip,
|
|
916
918
|
restrict_output=_experimental_restrict_output,
|
|
917
919
|
)
|
|
918
920
|
|
|
@@ -966,6 +968,7 @@ class _App:
|
|
|
966
968
|
include_source: Optional[bool] = None, # When `False`, don't automatically add the App source to the container.
|
|
967
969
|
experimental_options: Optional[dict[str, Any]] = None,
|
|
968
970
|
# Parameters below here are experimental. Use with caution!
|
|
971
|
+
_experimental_proxy_ip: Optional[str] = None, # IP address of proxy
|
|
969
972
|
_experimental_restrict_output: bool = False, # Don't use pickle for return values
|
|
970
973
|
# Parameters below here are deprecated. Please update your code as suggested
|
|
971
974
|
max_inputs: Optional[int] = None, # Replaced with `single_use_containers`
|
|
@@ -1112,6 +1115,7 @@ class _App:
|
|
|
1112
1115
|
rdma=rdma,
|
|
1113
1116
|
include_source=include_source if include_source is not None else local_state.include_source_default,
|
|
1114
1117
|
experimental_options={k: str(v) for k, v in (experimental_options or {}).items()},
|
|
1118
|
+
_experimental_proxy_ip=_experimental_proxy_ip,
|
|
1115
1119
|
restrict_output=_experimental_restrict_output,
|
|
1116
1120
|
)
|
|
1117
1121
|
|
|
@@ -497,6 +497,7 @@ class _App:
|
|
|
497
497
|
i6pn: typing.Optional[bool] = None,
|
|
498
498
|
include_source: typing.Optional[bool] = None,
|
|
499
499
|
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
500
|
+
_experimental_proxy_ip: typing.Optional[str] = None,
|
|
500
501
|
_experimental_restrict_output: bool = False,
|
|
501
502
|
max_inputs: typing.Optional[int] = None,
|
|
502
503
|
) -> _FunctionDecoratorType:
|
|
@@ -544,6 +545,7 @@ class _App:
|
|
|
544
545
|
i6pn: typing.Optional[bool] = None,
|
|
545
546
|
include_source: typing.Optional[bool] = None,
|
|
546
547
|
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
548
|
+
_experimental_proxy_ip: typing.Optional[str] = None,
|
|
547
549
|
_experimental_restrict_output: bool = False,
|
|
548
550
|
max_inputs: typing.Optional[int] = None,
|
|
549
551
|
) -> collections.abc.Callable[[typing.Union[CLS_T, modal._partial_function._PartialFunction]], CLS_T]:
|
|
@@ -1213,6 +1215,7 @@ class App:
|
|
|
1213
1215
|
i6pn: typing.Optional[bool] = None,
|
|
1214
1216
|
include_source: typing.Optional[bool] = None,
|
|
1215
1217
|
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
1218
|
+
_experimental_proxy_ip: typing.Optional[str] = None,
|
|
1216
1219
|
_experimental_restrict_output: bool = False,
|
|
1217
1220
|
max_inputs: typing.Optional[int] = None,
|
|
1218
1221
|
) -> _FunctionDecoratorType:
|
|
@@ -1260,6 +1263,7 @@ class App:
|
|
|
1260
1263
|
i6pn: typing.Optional[bool] = None,
|
|
1261
1264
|
include_source: typing.Optional[bool] = None,
|
|
1262
1265
|
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
1266
|
+
_experimental_proxy_ip: typing.Optional[str] = None,
|
|
1263
1267
|
_experimental_restrict_output: bool = False,
|
|
1264
1268
|
max_inputs: typing.Optional[int] = None,
|
|
1265
1269
|
) -> collections.abc.Callable[[typing.Union[CLS_T, modal.partial_function.PartialFunction]], CLS_T]:
|
|
@@ -20,22 +20,13 @@ 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
|
|
24
23
|
from modal.output import OutputManager
|
|
25
24
|
from modal.runner import DEPLOYMENT_STRATEGY_TYPE, _stop_and_wait_for_containers
|
|
26
25
|
from modal_proto import api_pb2
|
|
27
26
|
|
|
28
27
|
from .._logs import _FETCH_LIMIT, _MAX_FETCH_RANGE, LogsFilters
|
|
29
28
|
from .._utils.time_utils import locale_tz, timestamp_to_localized_str
|
|
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
|
-
)
|
|
29
|
+
from .utils import ENV_OPTION, display_table, fetch_app_logs, get_app_id_from_name, stream_app_logs, tail_app_logs
|
|
39
30
|
|
|
40
31
|
APP_IDENTIFIER = Argument("", help="App name or ID")
|
|
41
32
|
NAME_OPTION = typer.Option("", "-n", "--name", help="Deprecated: Pass App name as a positional argument")
|
|
@@ -54,50 +45,12 @@ APP_STATE_TO_MESSAGE = {
|
|
|
54
45
|
}
|
|
55
46
|
|
|
56
47
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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()
|
|
48
|
+
@synchronizer.create_blocking
|
|
49
|
+
async def get_app_id(app_identifier: str, env: Optional[str], client: Optional[_Client] = None) -> str:
|
|
50
|
+
"""Resolve an app_identifier that may be a name or an ID into an ID."""
|
|
77
51
|
if re.match(r"^ap-[a-zA-Z0-9]{22}$", app_identifier):
|
|
78
|
-
|
|
79
|
-
|
|
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)
|
|
52
|
+
return app_identifier
|
|
53
|
+
return await get_app_id_from_name.aio(app_identifier, env, client)
|
|
101
54
|
|
|
102
55
|
|
|
103
56
|
@app_cli.command("list")
|
|
@@ -176,8 +129,7 @@ _SOURCE_OPTIONS = {
|
|
|
176
129
|
|
|
177
130
|
|
|
178
131
|
@app_cli.command("logs", no_args_is_help=True)
|
|
179
|
-
|
|
180
|
-
async def logs(
|
|
132
|
+
def logs(
|
|
181
133
|
app_identifier: str = APP_IDENTIFIER,
|
|
182
134
|
follow: bool = typer.Option(False, "-f", "--follow", help="Stream log output until App stops"),
|
|
183
135
|
since: Optional[str] = typer.Option(
|
|
@@ -277,7 +229,7 @@ async def logs(
|
|
|
277
229
|
if tail is not None and tail > _FETCH_LIMIT:
|
|
278
230
|
raise UsageError(f"--tail value must not exceed {_FETCH_LIMIT}.")
|
|
279
231
|
|
|
280
|
-
app_id
|
|
232
|
+
app_id = get_app_id(app_identifier, env)
|
|
281
233
|
|
|
282
234
|
if source is not None:
|
|
283
235
|
if source not in _SOURCE_OPTIONS:
|
|
@@ -303,7 +255,7 @@ async def logs(
|
|
|
303
255
|
)
|
|
304
256
|
|
|
305
257
|
if follow:
|
|
306
|
-
|
|
258
|
+
stream_app_logs(
|
|
307
259
|
app_id,
|
|
308
260
|
task_id=container_id or "",
|
|
309
261
|
show_timestamps=timestamps,
|
|
@@ -326,7 +278,7 @@ async def logs(
|
|
|
326
278
|
|
|
327
279
|
if since and tail is None:
|
|
328
280
|
# Range mode: --since without --tail fetches everything in the range.
|
|
329
|
-
|
|
281
|
+
fetch_app_logs(
|
|
330
282
|
app_id,
|
|
331
283
|
since_dt,
|
|
332
284
|
until_dt or now,
|
|
@@ -338,7 +290,7 @@ async def logs(
|
|
|
338
290
|
# Tail mode: single fetch with limit.
|
|
339
291
|
# --since is a hard floor, --until shifts the anchor.
|
|
340
292
|
effective_tail = tail if tail is not None else _DEFAULT_LOGS_TAIL
|
|
341
|
-
|
|
293
|
+
tail_app_logs(
|
|
342
294
|
app_id,
|
|
343
295
|
effective_tail,
|
|
344
296
|
show_timestamps=timestamps,
|
|
@@ -386,125 +338,29 @@ async def rollback(
|
|
|
386
338
|
"""
|
|
387
339
|
env = ensure_env(env)
|
|
388
340
|
client = await _Client.from_env()
|
|
389
|
-
app_id
|
|
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
|
-
|
|
341
|
+
app_id = await get_app_id.aio(app_identifier, env, client)
|
|
394
342
|
if not version:
|
|
395
343
|
version_number = -1
|
|
396
344
|
else:
|
|
397
345
|
if m := re.match(r"v(\d+)", version):
|
|
398
346
|
version_number = int(m.group(1))
|
|
399
347
|
else:
|
|
400
|
-
raise UsageError(f"Invalid version
|
|
348
|
+
raise UsageError(f"Invalid version specifer: {version}")
|
|
401
349
|
req = api_pb2.AppRollbackRequest(app_id=app_id, version=version_number)
|
|
402
350
|
await client.stub.AppRollback(req)
|
|
403
351
|
rich.print("[green]✓[/green] Deployment rollback successful!")
|
|
404
352
|
|
|
405
353
|
|
|
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
|
-
|
|
469
354
|
@app_cli.command("stop", no_args_is_help=True)
|
|
470
355
|
@synchronizer.create_blocking
|
|
471
356
|
async def stop(
|
|
472
357
|
app_identifier: str = APP_IDENTIFIER,
|
|
473
358
|
*,
|
|
474
|
-
yes: bool = YES_OPTION,
|
|
475
359
|
env: Optional[str] = ENV_OPTION,
|
|
476
360
|
):
|
|
477
|
-
"""
|
|
478
|
-
env = ensure_env(env)
|
|
361
|
+
"""Stop an app."""
|
|
479
362
|
client = await _Client.from_env()
|
|
480
|
-
app_id
|
|
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)
|
|
363
|
+
app_id = await get_app_id.aio(app_identifier, env)
|
|
508
364
|
req = api_pb2.AppStopRequest(app_id=app_id, source=api_pb2.APP_STOP_SOURCE_CLI)
|
|
509
365
|
await client.stub.AppStop(req)
|
|
510
366
|
|
|
@@ -517,7 +373,7 @@ async def history(
|
|
|
517
373
|
env: Optional[str] = ENV_OPTION,
|
|
518
374
|
json: bool = False,
|
|
519
375
|
):
|
|
520
|
-
"""Show
|
|
376
|
+
"""Show App deployment history, for a currently deployed app
|
|
521
377
|
|
|
522
378
|
**Examples:**
|
|
523
379
|
|
|
@@ -527,7 +383,7 @@ async def history(
|
|
|
527
383
|
modal app history ap-123456
|
|
528
384
|
```
|
|
529
385
|
|
|
530
|
-
Get the history for
|
|
386
|
+
Get the history for a currently deployed App based on its name:
|
|
531
387
|
|
|
532
388
|
```
|
|
533
389
|
modal app history my-app
|
|
@@ -536,7 +392,7 @@ async def history(
|
|
|
536
392
|
"""
|
|
537
393
|
env = ensure_env(env)
|
|
538
394
|
client = await _Client.from_env()
|
|
539
|
-
app_id
|
|
395
|
+
app_id = await get_app_id.aio(app_identifier, env, client)
|
|
540
396
|
resp = await client.stub.AppDeploymentHistory(api_pb2.AppDeploymentHistoryRequest(app_id=app_id))
|
|
541
397
|
|
|
542
398
|
columns = [
|
|
@@ -611,6 +467,66 @@ async def dashboard(
|
|
|
611
467
|
```
|
|
612
468
|
"""
|
|
613
469
|
client = await _Client.from_env()
|
|
614
|
-
app_id
|
|
470
|
+
app_id = await get_app_id.aio(app_identifier, env, client)
|
|
471
|
+
|
|
615
472
|
url = f"https://modal.com/id/{app_id}"
|
|
616
473
|
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]")
|