modal 1.4.3.dev2__tar.gz → 1.4.3.dev4__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.dev2 → modal-1.4.3.dev4}/PKG-INFO +1 -2
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/blob_utils.py +73 -6
- modal-1.4.3.dev4/modal/cli/_help.py +264 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/_traceback.py +1 -2
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/app.py +74 -58
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/billing.py +27 -21
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/bootstrap.py +15 -11
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/changelog.py +21 -18
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/cluster.py +12 -11
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/config.py +13 -8
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/container.py +57 -43
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/dashboard.py +6 -2
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/dict.py +59 -24
- modal-1.4.3.dev4/modal/cli/entry_point.py +132 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/environment.py +26 -19
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/launch.py +39 -15
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/network_file_system.py +56 -45
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/profile.py +12 -8
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/queues.py +62 -40
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/run.py +72 -35
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/secret.py +38 -16
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/shell.py +98 -99
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/token.py +27 -31
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/utils.py +12 -5
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/volume.py +89 -80
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/client.pyi +2 -2
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/functions.pyi +6 -6
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/mount.py +37 -27
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/volume.py +3 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal.egg-info/PKG-INFO +1 -2
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal.egg-info/SOURCES.txt +1 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal.egg-info/requires.txt +0 -1
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_docs/gen_cli_docs.py +1 -1
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_version/__init__.py +1 -1
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/pyproject.toml +0 -1
- modal-1.4.3.dev2/modal/cli/entry_point.py +0 -145
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/LICENSE +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/README.md +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/__init__.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/__main__.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_billing.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_clustered_functions.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_clustered_functions.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_container_entrypoint.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_functions.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_grpc_client.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_ipython.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_load_context.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_location.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_logs.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_object.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_output/__init__.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_output/manager.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_output/pty.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_output/rich.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_output/status.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_partial_function.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_resolver.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_resources.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/__init__.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/asgi.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/execution_context.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/telemetry.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_serialization.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_server.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_traceback.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_tunnel.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_tunnel.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_type_manager.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/__init__.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/app_utils.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/async_utils.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/browser_utils.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/deprecation.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/docker_utils.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/function_utils.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/git_utils.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/hash_utils.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/http_utils.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/logger.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/mount_utils.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/name_utils.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/package_utils.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/shell_utils.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/task_command_router_client.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/time_utils.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_vendor/__init__.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_vendor/tblib.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_vendor/version.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_watcher.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/app.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/app.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/billing.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/builder/2023.12.txt +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/builder/2024.04.txt +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/builder/2024.10.txt +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/builder/2025.06.txt +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/builder/README.md +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/builder/base-images.json +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/call_graph.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/__init__.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/_download.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/import_refs.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/logo.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/programs/__init__.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/programs/vscode.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/selector.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/client.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cls.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cls.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/config.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/container_process.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/container_process.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/dict.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/dict.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/environments.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/environments.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/exception.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/experimental/__init__.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/experimental/flash.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/experimental/flash.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/experimental/ipython.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/file_io.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/file_io.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/file_pattern_matcher.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/functions.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/image.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/image.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/io_streams.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/io_streams.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/mount.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/network_file_system.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/network_file_system.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/object.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/object.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/output.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/parallel_map.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/parallel_map.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/partial_function.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/partial_function.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/proxy.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/proxy.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/py.typed +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/queue.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/queue.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/retries.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/runner.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/runner.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/running_app.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/sandbox.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/sandbox.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/sandbox_fs.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/sandbox_fs.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/schedule.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/scheduler_placement.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/secret.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/secret.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/server.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/server.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/serving.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/serving.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/snapshot.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/snapshot.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/stream_type.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/token_flow.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/token_flow.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/volume.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal.egg-info/top_level.txt +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_docs/__init__.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/__init__.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/api_grpc.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/api_pb2.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/api_pb2.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/py.typed +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_version/__main__.py +0 -0
- {modal-1.4.3.dev2 → modal-1.4.3.dev4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modal
|
|
3
|
-
Version: 1.4.3.
|
|
3
|
+
Version: 1.4.3.dev4
|
|
4
4
|
Summary: Python client library for Modal
|
|
5
5
|
Author-email: Modal Labs <support@modal.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -25,7 +25,6 @@ Requires-Dist: protobuf!=4.24.0,<7.0,>=3.19
|
|
|
25
25
|
Requires-Dist: rich>=12.0.0
|
|
26
26
|
Requires-Dist: synchronicity~=0.12.1
|
|
27
27
|
Requires-Dist: toml
|
|
28
|
-
Requires-Dist: typer>=0.9
|
|
29
28
|
Requires-Dist: types-certifi
|
|
30
29
|
Requires-Dist: types-toml
|
|
31
30
|
Requires-Dist: watchfiles
|
|
@@ -6,7 +6,7 @@ import os
|
|
|
6
6
|
import platform
|
|
7
7
|
import time
|
|
8
8
|
from collections.abc import AsyncIterator
|
|
9
|
-
from contextlib import AbstractContextManager, contextmanager
|
|
9
|
+
from contextlib import AbstractContextManager, asynccontextmanager, contextmanager
|
|
10
10
|
from io import BytesIO, FileIO
|
|
11
11
|
from pathlib import Path, PurePosixPath
|
|
12
12
|
from typing import (
|
|
@@ -21,11 +21,13 @@ from typing import (
|
|
|
21
21
|
)
|
|
22
22
|
from urllib.parse import urlparse
|
|
23
23
|
|
|
24
|
+
from typing_extensions import Self
|
|
25
|
+
|
|
24
26
|
from modal_proto import api_pb2
|
|
25
27
|
from modal_proto.modal_api_grpc import ModalClientModal
|
|
26
28
|
|
|
27
29
|
from ..exception import ExecutionError
|
|
28
|
-
from .async_utils import TaskContext, retry
|
|
30
|
+
from .async_utils import TaskContext, asyncnullcontext, retry
|
|
29
31
|
from .hash_utils import UploadHashes, get_upload_hashes
|
|
30
32
|
from .http_utils import ClientSessionRegistry
|
|
31
33
|
from .logger import logger
|
|
@@ -54,10 +56,60 @@ DEFAULT_SEGMENT_CHUNK_SIZE = 2**24
|
|
|
54
56
|
# TODO(dano): remove this once we stop requiring md5 for blobs
|
|
55
57
|
MULTIPART_UPLOAD_THRESHOLD = 1024**3
|
|
56
58
|
|
|
59
|
+
# Memory budget for multipart uploads.
|
|
60
|
+
MULTIPART_INFLIGHT_BYTES_MAX = 2 * 1024**3 # 2 GiB
|
|
61
|
+
MULTIPART_INFLIGHT_BYTES_MIN = 256 * 1024 * 1024 # 256 MiB
|
|
62
|
+
MULTIPART_INFLIGHT_MEMORY_FRACTION = 0.5 # 50% of RAM
|
|
63
|
+
|
|
64
|
+
|
|
57
65
|
# For block based storage like volumefs2: the size of a block
|
|
58
66
|
BLOCK_SIZE: int = 8 * 1024 * 1024
|
|
59
67
|
|
|
60
68
|
|
|
69
|
+
class _ByteBudget:
|
|
70
|
+
"""Limits total in-flight bytes across concurrent operations."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, total: int):
|
|
73
|
+
self._total = total
|
|
74
|
+
self._available = total
|
|
75
|
+
self._cond = asyncio.Condition()
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_system_memory(cls) -> Self:
|
|
79
|
+
return cls(_get_multipart_inflight_budget())
|
|
80
|
+
|
|
81
|
+
@asynccontextmanager
|
|
82
|
+
async def acquire(self, n: int):
|
|
83
|
+
async with self._cond:
|
|
84
|
+
# Wait until enough budget is free. If a single part exceeds the
|
|
85
|
+
# total budget, allow it through once nothing else is in-flight.
|
|
86
|
+
while self._available < min(n, self._total):
|
|
87
|
+
await self._cond.wait()
|
|
88
|
+
self._available -= n
|
|
89
|
+
try:
|
|
90
|
+
yield
|
|
91
|
+
finally:
|
|
92
|
+
async with self._cond:
|
|
93
|
+
self._available += n
|
|
94
|
+
self._cond.notify_all()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _get_multipart_inflight_budget() -> int:
|
|
98
|
+
"""Return a byte budget for concurrent part uploads, scaled to available system memory."""
|
|
99
|
+
try:
|
|
100
|
+
import psutil
|
|
101
|
+
|
|
102
|
+
available = psutil.virtual_memory().available
|
|
103
|
+
except Exception:
|
|
104
|
+
try:
|
|
105
|
+
available = os.sysconf("SC_AVPHYS_PAGES") * os.sysconf("SC_PAGE_SIZE")
|
|
106
|
+
except (AttributeError, ValueError, OSError):
|
|
107
|
+
return MULTIPART_INFLIGHT_BYTES_MIN
|
|
108
|
+
|
|
109
|
+
budget = int(available * MULTIPART_INFLIGHT_MEMORY_FRACTION)
|
|
110
|
+
return max(MULTIPART_INFLIGHT_BYTES_MIN, min(budget, MULTIPART_INFLIGHT_BYTES_MAX))
|
|
111
|
+
|
|
112
|
+
|
|
61
113
|
@retry(n_attempts=3, base_delay=0.3, attempt_timeout=None)
|
|
62
114
|
async def _upload_to_s3_url(
|
|
63
115
|
upload_url,
|
|
@@ -116,6 +168,7 @@ async def perform_multipart_upload(
|
|
|
116
168
|
completion_url: str,
|
|
117
169
|
upload_chunk_size: int = DEFAULT_SEGMENT_CHUNK_SIZE,
|
|
118
170
|
progress_report_cb: Optional[Callable] = None,
|
|
171
|
+
byte_budget: Optional[_ByteBudget] = None,
|
|
119
172
|
) -> None:
|
|
120
173
|
from .bytes_io_segment_payload import BytesIOSegmentPayload
|
|
121
174
|
|
|
@@ -133,6 +186,14 @@ async def perform_multipart_upload(
|
|
|
133
186
|
filename = data_file.name
|
|
134
187
|
data_file_readers = [open(filename, "rb") for _ in range(len(part_urls))]
|
|
135
188
|
|
|
189
|
+
async def _upload_part(part_url: str, part_payload: "BytesIOSegmentPayload") -> str:
|
|
190
|
+
# BytesIOSegmentPayload limits the amount of memory used, but here we
|
|
191
|
+
# acquire some extra for additional copies for various buffers based on
|
|
192
|
+
# empirical testing.
|
|
193
|
+
bytes_to_acquire = 4 * part_payload.chunk_size
|
|
194
|
+
async with byte_budget.acquire(bytes_to_acquire) if byte_budget else asyncnullcontext():
|
|
195
|
+
return await _upload_to_s3_url(part_url, payload=part_payload, content_type=None)
|
|
196
|
+
|
|
136
197
|
for part_number, (data_file_rdr, part_url) in enumerate(zip(data_file_readers, part_urls), start=1):
|
|
137
198
|
part_length_bytes = min(num_bytes_left, max_part_size)
|
|
138
199
|
part_payload = BytesIOSegmentPayload(
|
|
@@ -142,7 +203,7 @@ async def perform_multipart_upload(
|
|
|
142
203
|
chunk_size=upload_chunk_size,
|
|
143
204
|
progress_report_cb=progress_report_cb,
|
|
144
205
|
)
|
|
145
|
-
upload_coros.append(
|
|
206
|
+
upload_coros.append(_upload_part(part_url, part_payload))
|
|
146
207
|
num_bytes_left -= part_length_bytes
|
|
147
208
|
file_offset += part_length_bytes
|
|
148
209
|
|
|
@@ -211,7 +272,11 @@ async def _blob_upload_with_fallback(
|
|
|
211
272
|
|
|
212
273
|
|
|
213
274
|
async def _blob_upload(
|
|
214
|
-
upload_hashes: UploadHashes,
|
|
275
|
+
upload_hashes: UploadHashes,
|
|
276
|
+
data: Union[bytes, BinaryIO],
|
|
277
|
+
stub,
|
|
278
|
+
progress_report_cb: Optional[Callable] = None,
|
|
279
|
+
byte_budget: Optional[_ByteBudget] = None,
|
|
215
280
|
) -> tuple[str, bool, int]:
|
|
216
281
|
if isinstance(data, bytes):
|
|
217
282
|
data = BytesIO(data)
|
|
@@ -236,6 +301,7 @@ async def _blob_upload(
|
|
|
236
301
|
completion_url=part.completion_url,
|
|
237
302
|
upload_chunk_size=DEFAULT_SEGMENT_CHUNK_SIZE,
|
|
238
303
|
progress_report_cb=progress_report_cb,
|
|
304
|
+
byte_budget=byte_budget,
|
|
239
305
|
)
|
|
240
306
|
|
|
241
307
|
blob_id, r2_failed, r2_throughput_bytes_s = await _blob_upload_with_fallback(
|
|
@@ -304,9 +370,10 @@ async def blob_upload_file(
|
|
|
304
370
|
progress_report_cb: Optional[Callable] = None,
|
|
305
371
|
sha256_hex: Optional[str] = None,
|
|
306
372
|
md5_hex: Optional[str] = None,
|
|
373
|
+
byte_budget: Optional[_ByteBudget] = None,
|
|
307
374
|
) -> str:
|
|
308
375
|
upload_hashes = get_upload_hashes(file_obj, sha256_hex=sha256_hex, md5_hex=md5_hex)
|
|
309
|
-
blob_id, _, _ = await _blob_upload(upload_hashes, file_obj, stub, progress_report_cb)
|
|
376
|
+
blob_id, _, _ = await _blob_upload(upload_hashes, file_obj, stub, progress_report_cb, byte_budget=byte_budget)
|
|
310
377
|
return blob_id
|
|
311
378
|
|
|
312
379
|
|
|
@@ -650,7 +717,7 @@ def use_md5(url: str) -> bool:
|
|
|
650
717
|
host = urlparse(url).netloc.split(":")[0]
|
|
651
718
|
if host.endswith(".amazonaws.com") or host.endswith(".r2.cloudflarestorage.com"):
|
|
652
719
|
return True
|
|
653
|
-
elif host in ["127.0.0.1", "localhost", "172.21.0.1"]:
|
|
720
|
+
elif host in ["127.0.0.1", "localhost", "172.20.0.1", "172.21.0.1"]:
|
|
654
721
|
return False
|
|
655
722
|
else:
|
|
656
723
|
raise Exception(f"Unknown S3 host: {host}")
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Copyright Modal Labs 2026
|
|
2
|
+
"""Custom help formatting for the Modal CLI."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import inspect
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.markdown import Markdown
|
|
14
|
+
from rich.padding import Padding
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
from rich.text import Text
|
|
18
|
+
|
|
19
|
+
_HEADING_STYLE = "bold bright_green"
|
|
20
|
+
_COMMAND_NAME_STYLE = ""
|
|
21
|
+
_COMMAND_DESC_STYLE = "dim"
|
|
22
|
+
_OPTION_FLAG_STYLE = "green"
|
|
23
|
+
_OPTION_METAVAR_STYLE = "dim"
|
|
24
|
+
_ERROR_STYLE = "red"
|
|
25
|
+
|
|
26
|
+
_MAX_HELP_WIDTH = 80
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def use_rich_style() -> bool:
|
|
30
|
+
"""Whether help output should be rendered in the rich style."""
|
|
31
|
+
env = os.environ.get("MODAL_RICH_CLI") # TODO move to config
|
|
32
|
+
if env in ("0", "1"):
|
|
33
|
+
return env == "1"
|
|
34
|
+
return sys.stdout.isatty()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _make_console(file: Optional[Any] = None) -> Console:
|
|
38
|
+
return Console(file=file, highlight=False)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _render_usage(cmd: click.Command, ctx: click.Context, console: Console) -> None:
|
|
42
|
+
pieces = " ".join(cmd.collect_usage_pieces(ctx))
|
|
43
|
+
usage = Text()
|
|
44
|
+
usage.append("Usage: ", style=_HEADING_STYLE)
|
|
45
|
+
usage.append(f"{ctx.command_path} {pieces}".rstrip())
|
|
46
|
+
console.print(usage)
|
|
47
|
+
console.print()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _render_help_text(cmd: click.Command, console: Console) -> None:
|
|
51
|
+
text = cmd.help or cmd.short_help or ""
|
|
52
|
+
if not text:
|
|
53
|
+
return
|
|
54
|
+
console.print(Padding(Markdown(inspect.cleandoc(text)), (0, 2)))
|
|
55
|
+
console.print()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _option_label(param: click.Parameter, ctx: click.Context) -> Text:
|
|
59
|
+
text = Text()
|
|
60
|
+
for i, opt in enumerate(param.opts):
|
|
61
|
+
if i > 0:
|
|
62
|
+
text.append(", ")
|
|
63
|
+
text.append(opt, style=_OPTION_FLAG_STYLE)
|
|
64
|
+
if param.secondary_opts:
|
|
65
|
+
text.append(" / ")
|
|
66
|
+
for i, opt in enumerate(param.secondary_opts):
|
|
67
|
+
if i > 0:
|
|
68
|
+
text.append(", ")
|
|
69
|
+
text.append(opt, style=_OPTION_FLAG_STYLE)
|
|
70
|
+
if not getattr(param, "is_flag", False) and not getattr(param, "count", False):
|
|
71
|
+
text.append(" ")
|
|
72
|
+
text.append(param.make_metavar(ctx), style=_OPTION_METAVAR_STYLE)
|
|
73
|
+
return text
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _render_options(cmd: click.Command, ctx: click.Context, console: Console) -> None:
|
|
77
|
+
rows: list[tuple[Text, str]] = []
|
|
78
|
+
for param in cmd.get_params(ctx):
|
|
79
|
+
rec = param.get_help_record(ctx)
|
|
80
|
+
if rec is None: # skips arguments and hidden options
|
|
81
|
+
continue
|
|
82
|
+
rows.append((_option_label(param, ctx), rec[1] or ""))
|
|
83
|
+
if not rows:
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
console.print(Text("Options", style=_HEADING_STYLE))
|
|
87
|
+
table = Table(box=None, show_header=False, pad_edge=False, padding=(0, 2))
|
|
88
|
+
table.add_column(no_wrap=True) # styling lives in the Text cells
|
|
89
|
+
table.add_column(overflow="fold")
|
|
90
|
+
for label, help_str in rows:
|
|
91
|
+
table.add_row(label, help_str)
|
|
92
|
+
console.print(table)
|
|
93
|
+
console.print()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _render_epilog(cmd: click.Command, console: Console) -> None:
|
|
97
|
+
if cmd.epilog:
|
|
98
|
+
console.print(cmd.epilog)
|
|
99
|
+
console.print()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _group_commands_by_panel(group: click.Group) -> dict[str, list[tuple[str, click.Command]]]:
|
|
103
|
+
"""Bucket visible subcommands, preserving registration order."""
|
|
104
|
+
panels: dict[str, list[tuple[str, click.Command]]] = {}
|
|
105
|
+
for name, sub in group.commands.items():
|
|
106
|
+
if sub.hidden:
|
|
107
|
+
continue
|
|
108
|
+
panels.setdefault(getattr(sub, "panel", None) or "Commands", []).append((name, sub))
|
|
109
|
+
return panels
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _render_commands(group: click.Group, console: Console) -> None:
|
|
113
|
+
panels = _group_commands_by_panel(group)
|
|
114
|
+
if not panels:
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
# We want the name / description columns to be the same widths across groups
|
|
118
|
+
name_width = max(len(name) for items in panels.values() for name, _ in items)
|
|
119
|
+
table_width = min(_MAX_HELP_WIDTH, console.width)
|
|
120
|
+
|
|
121
|
+
for panel_name, items in panels.items():
|
|
122
|
+
console.print(Text(panel_name.ljust(table_width), style=f"{_HEADING_STYLE} underline"))
|
|
123
|
+
_render_command_table(console, items, name_width, table_width)
|
|
124
|
+
console.print()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def build_command_table(name_width: int, table_width: Optional[int] = None) -> Table:
|
|
128
|
+
kwargs: dict[str, Any] = {"box": None, "show_header": False, "pad_edge": False, "padding": (0, 2)}
|
|
129
|
+
if table_width is not None:
|
|
130
|
+
kwargs["width"] = table_width
|
|
131
|
+
table = Table(**kwargs)
|
|
132
|
+
table.add_column(style=_COMMAND_NAME_STYLE, no_wrap=True, width=name_width)
|
|
133
|
+
# ratio=1 claims any extra width for the description column so col1 stays
|
|
134
|
+
# fixed at name_width across panels — otherwise Rich distributes slack into
|
|
135
|
+
# both columns and the name column grows in panels with shorter content.
|
|
136
|
+
table.add_column(style=_COMMAND_DESC_STYLE, overflow="fold", ratio=1)
|
|
137
|
+
return table
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _render_command_table(
|
|
141
|
+
console: Console,
|
|
142
|
+
items: list[tuple[str, click.Command]],
|
|
143
|
+
name_width: int,
|
|
144
|
+
table_width: int,
|
|
145
|
+
) -> None:
|
|
146
|
+
table = build_command_table(name_width, table_width)
|
|
147
|
+
for name, sub in items:
|
|
148
|
+
table.add_row(name, sub.get_short_help_str(limit=80))
|
|
149
|
+
console.print(table)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# -- Public Command / Group subclasses ---------------------------------------
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ModalCommand(click.Command):
|
|
156
|
+
"""click.Command that renders --help with custom rich output."""
|
|
157
|
+
|
|
158
|
+
def __init__(self, *args: Any, panel: Optional[str] = None, **kwargs: Any) -> None:
|
|
159
|
+
super().__init__(*args, **kwargs)
|
|
160
|
+
self.panel = panel
|
|
161
|
+
|
|
162
|
+
def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
163
|
+
if not use_rich_style():
|
|
164
|
+
return super().format_help(ctx, formatter)
|
|
165
|
+
console = _make_console()
|
|
166
|
+
with console.capture() as capture:
|
|
167
|
+
_render_usage(self, ctx, console)
|
|
168
|
+
_render_help_text(self, console)
|
|
169
|
+
_render_options(self, ctx, console)
|
|
170
|
+
_render_epilog(self, console)
|
|
171
|
+
formatter.write(capture.get())
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class ModalGroup(click.Group):
|
|
175
|
+
"""click.Group whose subcommands and subgroups inherit `ModalCommand`."""
|
|
176
|
+
|
|
177
|
+
command_class = ModalCommand
|
|
178
|
+
group_class = type # nested @group.group() reuses the enclosing class
|
|
179
|
+
|
|
180
|
+
def __init__(self, *args: Any, panel: Optional[str] = None, **kwargs: Any) -> None:
|
|
181
|
+
# Default to showing help when a group is invoked with no subcommand.
|
|
182
|
+
# Callers can opt out by passing no_args_is_help=False explicitly.
|
|
183
|
+
kwargs.setdefault("no_args_is_help", True)
|
|
184
|
+
super().__init__(*args, **kwargs)
|
|
185
|
+
self.panel = panel
|
|
186
|
+
|
|
187
|
+
def add_command(
|
|
188
|
+
self,
|
|
189
|
+
cmd: click.Command,
|
|
190
|
+
name: Optional[str] = None,
|
|
191
|
+
*,
|
|
192
|
+
panel: Optional[str] = None,
|
|
193
|
+
hidden: Optional[bool] = None,
|
|
194
|
+
) -> None:
|
|
195
|
+
super().add_command(cmd, name)
|
|
196
|
+
if panel is not None:
|
|
197
|
+
cmd.panel = panel # type: ignore[attr-defined]
|
|
198
|
+
if hidden is not None:
|
|
199
|
+
cmd.hidden = hidden
|
|
200
|
+
|
|
201
|
+
def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
202
|
+
# Replaces click's single flat "Commands:" section with one section per
|
|
203
|
+
# panel so the simple-style help output still preserves grouping.
|
|
204
|
+
for panel_name, items in _group_commands_by_panel(self).items():
|
|
205
|
+
rows = [(name, sub.get_short_help_str(limit=80)) for name, sub in items]
|
|
206
|
+
with formatter.section(panel_name):
|
|
207
|
+
formatter.write_dl(rows)
|
|
208
|
+
|
|
209
|
+
def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
210
|
+
if not use_rich_style():
|
|
211
|
+
return super().format_help(ctx, formatter)
|
|
212
|
+
console = _make_console()
|
|
213
|
+
with console.capture() as capture:
|
|
214
|
+
_render_usage(self, ctx, console)
|
|
215
|
+
_render_help_text(self, console)
|
|
216
|
+
_render_options(self, ctx, console)
|
|
217
|
+
_render_commands(self, console)
|
|
218
|
+
_render_epilog(self, console)
|
|
219
|
+
formatter.write(capture.get())
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# -- Error rendering ---------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _render_click_exception(exc: click.ClickException, file: Any) -> None:
|
|
226
|
+
console = _make_console(file if file is not None else sys.stderr)
|
|
227
|
+
title = "Error"
|
|
228
|
+
|
|
229
|
+
if isinstance(exc, click.UsageError) and exc.ctx is not None:
|
|
230
|
+
ctx = exc.ctx
|
|
231
|
+
console.print(ctx.get_usage())
|
|
232
|
+
if ctx.command.get_help_option(ctx) is not None:
|
|
233
|
+
option = ctx.help_option_names[0] if ctx.help_option_names else "--help"
|
|
234
|
+
console.print(f"Try [bold]'{ctx.command_path} {option}'[/bold] for help.")
|
|
235
|
+
|
|
236
|
+
console.print(
|
|
237
|
+
Panel(
|
|
238
|
+
Text(exc.format_message()),
|
|
239
|
+
title=title,
|
|
240
|
+
title_align="left",
|
|
241
|
+
border_style=_ERROR_STYLE,
|
|
242
|
+
expand=True,
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
_orig_click_show = click.ClickException.show
|
|
248
|
+
_orig_usage_show = click.UsageError.show
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _click_show(self: click.ClickException, file: Any = None) -> None:
|
|
252
|
+
if not use_rich_style():
|
|
253
|
+
return _orig_click_show(self, file)
|
|
254
|
+
_render_click_exception(self, file)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _usage_show(self: click.UsageError, file: Any = None) -> None:
|
|
258
|
+
if not use_rich_style():
|
|
259
|
+
return _orig_usage_show(self, file)
|
|
260
|
+
_render_click_exception(self, file)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
click.ClickException.show = _click_show # type: ignore[method-assign]
|
|
264
|
+
click.UsageError.show = _usage_show # type: ignore[method-assign]
|
|
@@ -154,9 +154,8 @@ def setup_rich_traceback() -> None:
|
|
|
154
154
|
import click
|
|
155
155
|
import grpclib
|
|
156
156
|
import synchronicity
|
|
157
|
-
import typer
|
|
158
157
|
|
|
159
|
-
install(suppress=[synchronicity, grpclib, click
|
|
158
|
+
install(suppress=[synchronicity, grpclib, click], extra_lines=1)
|
|
160
159
|
|
|
161
160
|
|
|
162
161
|
def highlight_modal_warnings() -> None:
|
|
@@ -8,11 +8,9 @@ from typing import Optional, Union, get_args
|
|
|
8
8
|
|
|
9
9
|
import click
|
|
10
10
|
import rich
|
|
11
|
-
import typer
|
|
12
11
|
from click import UsageError
|
|
13
12
|
from rich.table import Column
|
|
14
13
|
from rich.text import Text
|
|
15
|
-
from typer import Argument
|
|
16
14
|
|
|
17
15
|
from modal._object import _get_environment_name
|
|
18
16
|
from modal._traceback import print_server_warnings
|
|
@@ -27,20 +25,18 @@ from modal_proto import api_pb2
|
|
|
27
25
|
|
|
28
26
|
from .._logs import _FETCH_LIMIT, _MAX_FETCH_RANGE, LogsFilters
|
|
29
27
|
from .._utils.time_utils import locale_tz, timestamp_to_localized_str
|
|
28
|
+
from ._help import ModalGroup
|
|
30
29
|
from .utils import (
|
|
31
|
-
ENV_OPTION,
|
|
32
|
-
YES_OPTION,
|
|
33
30
|
confirm_or_suggest_yes,
|
|
34
31
|
display_table,
|
|
32
|
+
env_option,
|
|
35
33
|
fetch_app_logs,
|
|
36
34
|
stream_app_logs,
|
|
37
35
|
tail_app_logs,
|
|
36
|
+
yes_option,
|
|
38
37
|
)
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
NAME_OPTION = typer.Option("", "-n", "--name", help="Deprecated: Pass App name as a positional argument")
|
|
42
|
-
|
|
43
|
-
app_cli = typer.Typer(name="app", help="Manage deployed and running apps.", no_args_is_help=True)
|
|
39
|
+
app_cli = ModalGroup(name="app", help="Manage deployed and running apps.")
|
|
44
40
|
|
|
45
41
|
APP_STATE_TO_MESSAGE = {
|
|
46
42
|
api_pb2.APP_STATE_DEPLOYED: Text("deployed", style="green"),
|
|
@@ -101,9 +97,11 @@ async def resolve_app_identifier(
|
|
|
101
97
|
|
|
102
98
|
|
|
103
99
|
@app_cli.command("list")
|
|
100
|
+
@env_option
|
|
101
|
+
@click.option("--json", is_flag=True, default=False)
|
|
104
102
|
@synchronizer.create_blocking
|
|
105
|
-
async def list_(env: Optional[str] =
|
|
106
|
-
"""List
|
|
103
|
+
async def list_(env: Optional[str] = None, json: bool = False):
|
|
104
|
+
"""List Apps that are running, deployed or recently stopped."""
|
|
107
105
|
env = ensure_env(env)
|
|
108
106
|
client = await _Client.from_env()
|
|
109
107
|
|
|
@@ -176,38 +174,43 @@ _SOURCE_OPTIONS = {
|
|
|
176
174
|
|
|
177
175
|
|
|
178
176
|
@app_cli.command("logs", no_args_is_help=True)
|
|
177
|
+
@click.argument("app_identifier")
|
|
178
|
+
@click.option("-f", "--follow", is_flag=True, default=False, help="Stream log output until App stops")
|
|
179
|
+
@click.option(
|
|
180
|
+
"--since",
|
|
181
|
+
default=None,
|
|
182
|
+
help="Start of time range. Accepts ISO 8601 datetime or relative time, e.g. '1d' (1 day ago), '2h', '30m', etc.",
|
|
183
|
+
)
|
|
184
|
+
@click.option("--until", default=None, help="End of time range; accepts same argument types as --since")
|
|
185
|
+
@click.option("-n", "--tail", default=None, type=int, help="Show only the last N log entries")
|
|
186
|
+
@click.option("--search", default=None, help="Filter by search text")
|
|
187
|
+
@click.option("--function", "function_id", default="", help="Filter by Function ID (fu-*)")
|
|
188
|
+
@click.option("--function-call", "function_call_id", default="", help="Filter by FunctionCall ID (fc-*)")
|
|
189
|
+
@click.option("--container", "container_id", default="", help="Filter by Container ID (ta-*)")
|
|
190
|
+
@click.option("-s", "--source", default=None, help="Filter by source: 'stdout', 'stderr', or 'system'")
|
|
191
|
+
@click.option("--timestamps", is_flag=True, default=False, help="Prefix each line with its timestamp")
|
|
192
|
+
@click.option("--show-function-id", is_flag=True, default=False, help="Prefix each line with its Function ID")
|
|
193
|
+
@click.option("--show-function-call-id", is_flag=True, default=False, help="Prefix each line with its FunctionCall ID")
|
|
194
|
+
@click.option("--show-container-id", is_flag=True, default=False, help="Prefix each line with its Container ID")
|
|
195
|
+
@env_option
|
|
179
196
|
@synchronizer.create_blocking
|
|
180
197
|
async def logs(
|
|
181
|
-
app_identifier: str
|
|
182
|
-
follow: bool =
|
|
183
|
-
since: Optional[str] =
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
tail: Optional[int] = typer.Option(None, "--tail", "-n", help="Show only the last N log entries"),
|
|
196
|
-
search: Optional[str] = typer.Option(None, "--search", help="Filter by search text"),
|
|
197
|
-
function_id: Optional[str] = typer.Option("", "--function", help="Filter by Function ID (fu-*)"),
|
|
198
|
-
function_call_id: Optional[str] = typer.Option("", "--function-call", help="Filter by FunctionCall ID (fc-*)"),
|
|
199
|
-
container_id: Optional[str] = typer.Option("", "--container", help="Filter by Container ID (ta-*)"),
|
|
200
|
-
source: Optional[str] = typer.Option(
|
|
201
|
-
None, "--source", "-s", help="Filter by source: 'stdout', 'stderr', or 'system'"
|
|
202
|
-
),
|
|
203
|
-
timestamps: bool = typer.Option(False, "--timestamps", help="Prefix each line with its timestamp"),
|
|
204
|
-
show_function_id: bool = typer.Option(False, "--show-function-id", help="Prefix each line with its Function ID"),
|
|
205
|
-
show_function_call_id: bool = typer.Option(
|
|
206
|
-
False, "--show-function-call-id", help="Prefix each line with its FunctionCall ID"
|
|
207
|
-
),
|
|
208
|
-
show_container_id: bool = typer.Option(False, "--show-container-id", help="Prefix each line with its Container ID"),
|
|
198
|
+
app_identifier: str,
|
|
199
|
+
follow: bool = False,
|
|
200
|
+
since: Optional[str] = None,
|
|
201
|
+
until: Optional[str] = None,
|
|
202
|
+
tail: Optional[int] = None,
|
|
203
|
+
search: Optional[str] = None,
|
|
204
|
+
function_id: str = "",
|
|
205
|
+
function_call_id: str = "",
|
|
206
|
+
container_id: str = "",
|
|
207
|
+
source: Optional[str] = None,
|
|
208
|
+
timestamps: bool = False,
|
|
209
|
+
show_function_id: bool = False,
|
|
210
|
+
show_function_call_id: bool = False,
|
|
211
|
+
show_container_id: bool = False,
|
|
209
212
|
*,
|
|
210
|
-
env: Optional[str] =
|
|
213
|
+
env: Optional[str] = None,
|
|
211
214
|
):
|
|
212
215
|
"""Fetch or stream App logs.
|
|
213
216
|
|
|
@@ -265,8 +268,6 @@ async def logs(
|
|
|
265
268
|
```
|
|
266
269
|
|
|
267
270
|
"""
|
|
268
|
-
if not app_identifier:
|
|
269
|
-
raise UsageError("Either an App ID or name must be provided.")
|
|
270
271
|
|
|
271
272
|
if follow and (since or until or tail):
|
|
272
273
|
raise UsageError("--follow cannot be combined with --since, --until, or --tail.")
|
|
@@ -350,12 +351,15 @@ async def logs(
|
|
|
350
351
|
|
|
351
352
|
|
|
352
353
|
@app_cli.command("rollback", no_args_is_help=True, context_settings={"ignore_unknown_options": True})
|
|
354
|
+
@click.argument("app_identifier")
|
|
355
|
+
@click.argument("version", default="")
|
|
356
|
+
@env_option
|
|
353
357
|
@synchronizer.create_blocking
|
|
354
358
|
async def rollback(
|
|
355
|
-
app_identifier: str
|
|
356
|
-
version: str =
|
|
359
|
+
app_identifier: str,
|
|
360
|
+
version: str = "",
|
|
357
361
|
*,
|
|
358
|
-
env: Optional[str] =
|
|
362
|
+
env: Optional[str] = None,
|
|
359
363
|
):
|
|
360
364
|
"""Redeploy a previous version of an App.
|
|
361
365
|
|
|
@@ -404,16 +408,20 @@ async def rollback(
|
|
|
404
408
|
|
|
405
409
|
|
|
406
410
|
@app_cli.command("rollover", no_args_is_help=True)
|
|
411
|
+
@click.argument("app_identifier")
|
|
412
|
+
@click.option(
|
|
413
|
+
"--strategy",
|
|
414
|
+
default="rolling",
|
|
415
|
+
type=click.Choice(get_args(DEPLOYMENT_STRATEGY_TYPE)),
|
|
416
|
+
help="Strategy for rollover",
|
|
417
|
+
)
|
|
418
|
+
@env_option
|
|
407
419
|
@synchronizer.create_blocking
|
|
408
420
|
async def rollover(
|
|
409
|
-
app_identifier: str
|
|
421
|
+
app_identifier: str,
|
|
422
|
+
strategy: str = "rolling",
|
|
410
423
|
*,
|
|
411
|
-
|
|
412
|
-
"rolling",
|
|
413
|
-
help="Strategy for rollover",
|
|
414
|
-
click_type=click.Choice(get_args(DEPLOYMENT_STRATEGY_TYPE)),
|
|
415
|
-
),
|
|
416
|
-
env: Optional[str] = ENV_OPTION,
|
|
424
|
+
env: Optional[str] = None,
|
|
417
425
|
):
|
|
418
426
|
"""Redeploy an App to get new containers without code changes.
|
|
419
427
|
|
|
@@ -467,12 +475,15 @@ async def rollover(
|
|
|
467
475
|
|
|
468
476
|
|
|
469
477
|
@app_cli.command("stop", no_args_is_help=True)
|
|
478
|
+
@click.argument("app_identifier")
|
|
479
|
+
@yes_option
|
|
480
|
+
@env_option
|
|
470
481
|
@synchronizer.create_blocking
|
|
471
482
|
async def stop(
|
|
472
|
-
app_identifier: str
|
|
483
|
+
app_identifier: str,
|
|
473
484
|
*,
|
|
474
|
-
yes: bool =
|
|
475
|
-
env: Optional[str] =
|
|
485
|
+
yes: bool = False,
|
|
486
|
+
env: Optional[str] = None,
|
|
476
487
|
):
|
|
477
488
|
"""Permanently stop an App and terminate its running containers."""
|
|
478
489
|
env = ensure_env(env)
|
|
@@ -510,11 +521,14 @@ async def stop(
|
|
|
510
521
|
|
|
511
522
|
|
|
512
523
|
@app_cli.command("history", no_args_is_help=True)
|
|
524
|
+
@click.argument("app_identifier")
|
|
525
|
+
@env_option
|
|
526
|
+
@click.option("--json", is_flag=True, default=False)
|
|
513
527
|
@synchronizer.create_blocking
|
|
514
528
|
async def history(
|
|
515
|
-
app_identifier: str
|
|
529
|
+
app_identifier: str,
|
|
516
530
|
*,
|
|
517
|
-
env: Optional[str] =
|
|
531
|
+
env: Optional[str] = None,
|
|
518
532
|
json: bool = False,
|
|
519
533
|
):
|
|
520
534
|
"""Show an App's deployment history.
|
|
@@ -588,11 +602,13 @@ async def history(
|
|
|
588
602
|
|
|
589
603
|
|
|
590
604
|
@app_cli.command("dashboard", no_args_is_help=True)
|
|
605
|
+
@click.argument("app_identifier")
|
|
606
|
+
@env_option
|
|
591
607
|
@synchronizer.create_blocking
|
|
592
608
|
async def dashboard(
|
|
593
|
-
app_identifier: str
|
|
609
|
+
app_identifier: str,
|
|
594
610
|
*,
|
|
595
|
-
env: Optional[str] =
|
|
611
|
+
env: Optional[str] = None,
|
|
596
612
|
):
|
|
597
613
|
"""Open an App's dashboard page in your web browser.
|
|
598
614
|
|