modal 1.3.6.dev12__tar.gz → 1.3.6.dev13__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.3.6.dev12 → modal-1.3.6.dev13}/PKG-INFO +1 -1
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_functions.py +9 -1
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/mount_utils.py +20 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/app.py +4 -1
- modal-1.3.6.dev13/modal/cli/container.py +264 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/client.pyi +2 -2
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/functions.pyi +6 -6
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/image.py +35 -8
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/image.pyi +40 -4
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/runner.py +19 -9
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/sandbox.py +5 -2
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_version/__init__.py +1 -1
- modal-1.3.6.dev12/modal/cli/container.py +0 -117
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/LICENSE +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/README.md +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/__init__.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/__main__.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_billing.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_clustered_functions.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_clustered_functions.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_container_entrypoint.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_grpc_client.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_ipython.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_load_context.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_location.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_logs.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_object.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_output/__init__.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_output/manager.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_output/pty.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_output/rich.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_output/status.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_partial_function.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_resolver.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_resources.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_runtime/__init__.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_runtime/asgi.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_runtime/execution_context.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_runtime/telemetry.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_serialization.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_server.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_traceback.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_tunnel.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_tunnel.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_type_manager.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/__init__.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/app_utils.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/async_utils.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/blob_utils.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/browser_utils.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/deprecation.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/docker_utils.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/function_utils.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/git_utils.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/hash_utils.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/http_utils.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/logger.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/name_utils.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/package_utils.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/shell_utils.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/task_command_router_client.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_utils/time_utils.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_vendor/__init__.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_vendor/tblib.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_vendor/version.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/_watcher.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/app.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/app.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/billing.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/builder/2023.12.txt +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/builder/2024.04.txt +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/builder/2024.10.txt +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/builder/2025.06.txt +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/builder/README.md +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/builder/base-images.json +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/call_graph.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/__init__.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/_download.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/_traceback.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/billing.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/changelog.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/cluster.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/config.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/dashboard.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/dict.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/entry_point.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/environment.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/import_refs.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/launch.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/network_file_system.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/profile.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/programs/__init__.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/programs/vscode.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/queues.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/run.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/secret.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/selector.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/shell.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/token.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/utils.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cli/volume.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/client.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cls.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/cls.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/config.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/container_process.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/container_process.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/dict.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/dict.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/environments.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/environments.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/exception.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/experimental/__init__.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/experimental/flash.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/experimental/flash.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/experimental/ipython.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/file_io.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/file_io.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/file_pattern_matcher.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/functions.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/gpu.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/io_streams.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/io_streams.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/mount.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/mount.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/network_file_system.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/network_file_system.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/object.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/object.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/output.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/parallel_map.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/parallel_map.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/partial_function.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/partial_function.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/proxy.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/proxy.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/py.typed +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/queue.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/queue.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/retries.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/runner.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/running_app.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/sandbox.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/sandbox_fs.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/sandbox_fs.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/schedule.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/scheduler_placement.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/secret.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/secret.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/server.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/server.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/serving.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/serving.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/snapshot.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/snapshot.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/stream_type.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/token_flow.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/token_flow.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/volume.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal/volume.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal.egg-info/requires.txt +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal.egg-info/top_level.txt +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_docs/__init__.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_proto/__init__.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_proto/api_grpc.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_proto/api_pb2.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_proto/api_pb2.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_proto/py.typed +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/modal_version/__main__.py +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/pyproject.toml +0 -0
- {modal-1.3.6.dev12 → modal-1.3.6.dev13}/setup.cfg +0 -0
|
@@ -56,7 +56,7 @@ from ._utils.function_utils import (
|
|
|
56
56
|
is_async,
|
|
57
57
|
)
|
|
58
58
|
from ._utils.grpc_utils import Retry, RetryWarningMessage
|
|
59
|
-
from ._utils.mount_utils import validate_network_file_systems, validate_volumes
|
|
59
|
+
from ._utils.mount_utils import validate_network_file_systems, validate_volumes, validate_volumes_by_object_id
|
|
60
60
|
from .call_graph import InputInfo, _reconstruct_call_graph
|
|
61
61
|
from .client import _Client
|
|
62
62
|
from .cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
|
|
@@ -966,6 +966,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
966
966
|
if image._metadata is not None:
|
|
967
967
|
mount_client_dependencies = image._metadata.image_builder_version > "2024.10"
|
|
968
968
|
|
|
969
|
+
# Validate that the same volume (by object_id) isn't mounted at multiple paths
|
|
970
|
+
# This validation happens here (at load time) because volumes need to be hydrated
|
|
971
|
+
# to have their object_id set.
|
|
972
|
+
validate_volumes_by_object_id(validated_volumes_no_cloud_buckets)
|
|
973
|
+
|
|
969
974
|
# Relies on dicts being ordered (true as of Python 3.6).
|
|
970
975
|
volume_mounts = [
|
|
971
976
|
api_pb2.VolumeMount(
|
|
@@ -1235,6 +1240,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1235
1240
|
assert parent is not None and parent.is_hydrated
|
|
1236
1241
|
|
|
1237
1242
|
if options:
|
|
1243
|
+
# Validate that the same volume (by object_id) isn't mounted at multiple paths
|
|
1244
|
+
validate_volumes_by_object_id(options.validated_volumes)
|
|
1245
|
+
|
|
1238
1246
|
volume_mounts = [
|
|
1239
1247
|
api_pb2.VolumeMount(
|
|
1240
1248
|
mount_path=path,
|
|
@@ -101,3 +101,23 @@ def validate_only_modal_volumes(
|
|
|
101
101
|
raise InvalidError(f"{caller_name} only supports volumes that are modal.Volume")
|
|
102
102
|
|
|
103
103
|
return validated_volumes
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def validate_volumes_by_object_id(validated_volumes: Sequence[tuple[str, _Volume]]) -> None:
|
|
107
|
+
"""Validate that the same volume (by object_id) is not mounted at multiple paths.
|
|
108
|
+
|
|
109
|
+
This validation happens at load time after volumes are hydrated and have object_ids.
|
|
110
|
+
"""
|
|
111
|
+
object_id_to_paths: dict[str, list[str]] = {}
|
|
112
|
+
for path, volume in validated_volumes:
|
|
113
|
+
if not volume.is_hydrated:
|
|
114
|
+
# This should never happen since this function is only called at load time
|
|
115
|
+
raise RuntimeError(f"Internal error: Volume at '{path}' is not hydrated when validating mounts")
|
|
116
|
+
object_id_to_paths.setdefault(volume.object_id, []).append(path)
|
|
117
|
+
|
|
118
|
+
for object_id, paths in object_id_to_paths.items():
|
|
119
|
+
if len(paths) > 1:
|
|
120
|
+
conflicting = ", ".join(sorted(paths))
|
|
121
|
+
raise InvalidError(
|
|
122
|
+
f"The same Volume cannot be mounted in multiple locations for the same function: {conflicting}"
|
|
123
|
+
)
|
|
@@ -137,13 +137,14 @@ def logs(
|
|
|
137
137
|
"--until",
|
|
138
138
|
help="End of time range; accepts same argument types as --since",
|
|
139
139
|
),
|
|
140
|
+
tail: Optional[int] = typer.Option(None, "--tail", "-n", help="Show only the last N log entries"),
|
|
140
141
|
search: Optional[str] = typer.Option(None, "--search", help="Filter by search text"),
|
|
141
142
|
function_id: Optional[str] = typer.Option("", "--function", help="Filter by Function ID (fu-*)"),
|
|
142
143
|
function_call_id: Optional[str] = typer.Option("", "--function-call", help="Filter by FunctionCall ID (fc-*)"),
|
|
144
|
+
container_id: Optional[str] = typer.Option("", "--container", help="Filter by Container ID (ta-*)"),
|
|
143
145
|
source: Optional[str] = typer.Option(
|
|
144
146
|
None, "--source", "-s", help="Filter by source: 'stdout', 'stderr', or 'system'"
|
|
145
147
|
),
|
|
146
|
-
tail: Optional[int] = typer.Option(None, "--tail", "-n", help="Show only the last N log entries"),
|
|
147
148
|
timestamps: bool = typer.Option(False, "--timestamps", help="Prefix each line with its timestamp"),
|
|
148
149
|
show_function_id: bool = typer.Option(False, "--show-function-id", help="Prefix each line with its Function ID"),
|
|
149
150
|
show_function_call_id: bool = typer.Option(
|
|
@@ -236,12 +237,14 @@ def logs(
|
|
|
236
237
|
source=source_fd,
|
|
237
238
|
function_id=function_id or "",
|
|
238
239
|
function_call_id=function_call_id or "",
|
|
240
|
+
task_id=container_id or "",
|
|
239
241
|
search_text=search or "",
|
|
240
242
|
)
|
|
241
243
|
|
|
242
244
|
if follow:
|
|
243
245
|
stream_app_logs(
|
|
244
246
|
app_id,
|
|
247
|
+
task_id=container_id or "",
|
|
245
248
|
show_timestamps=timestamps,
|
|
246
249
|
follow=True,
|
|
247
250
|
prefix_fields=prefix_fields,
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Copyright Modal Labs 2022
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from typing import Optional, Union
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from click import UsageError
|
|
7
|
+
from rich.table import Column
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
|
|
10
|
+
from modal._logs import _FETCH_LIMIT, _MAX_FETCH_RANGE, LogsFilters
|
|
11
|
+
from modal._object import _get_environment_name
|
|
12
|
+
from modal._output.pty import get_pty_info
|
|
13
|
+
from modal._utils.async_utils import synchronizer
|
|
14
|
+
from modal._utils.time_utils import timestamp_to_localized_str
|
|
15
|
+
from modal.cli.app import _DEFAULT_LOGS_TAIL, _SOURCE_OPTIONS, _parse_time_arg
|
|
16
|
+
from modal.cli.utils import ENV_OPTION, display_table, fetch_app_logs, is_tty, stream_app_logs, tail_app_logs
|
|
17
|
+
from modal.client import _Client
|
|
18
|
+
from modal.config import config
|
|
19
|
+
from modal.container_process import _ContainerProcess
|
|
20
|
+
from modal.environments import ensure_env
|
|
21
|
+
from modal.exception import InvalidError
|
|
22
|
+
from modal.stream_type import StreamType
|
|
23
|
+
from modal_proto import api_pb2
|
|
24
|
+
|
|
25
|
+
container_cli = typer.Typer(name="container", help="Manage and connect to running containers.", no_args_is_help=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@container_cli.command("list")
|
|
29
|
+
@synchronizer.create_blocking
|
|
30
|
+
async def list_(env: Optional[str] = ENV_OPTION, json: bool = False, app_id: str = ""):
|
|
31
|
+
"""List all containers that are currently running."""
|
|
32
|
+
env = ensure_env(env)
|
|
33
|
+
client = await _Client.from_env()
|
|
34
|
+
environment_name = _get_environment_name(env)
|
|
35
|
+
res: api_pb2.TaskListResponse = await client.stub.TaskList(
|
|
36
|
+
api_pb2.TaskListRequest(environment_name=environment_name, app_id=app_id)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
column_names: list[Union[Column, str]] = [
|
|
40
|
+
Column("Container ID", min_width=29),
|
|
41
|
+
Column("App ID", min_width=25),
|
|
42
|
+
"App Name",
|
|
43
|
+
"Start Time",
|
|
44
|
+
]
|
|
45
|
+
rows: list[list[Union[Text, str]]] = []
|
|
46
|
+
res.tasks.sort(key=lambda task: task.started_at, reverse=True)
|
|
47
|
+
for task_stats in res.tasks:
|
|
48
|
+
rows.append(
|
|
49
|
+
[
|
|
50
|
+
task_stats.task_id,
|
|
51
|
+
task_stats.app_id,
|
|
52
|
+
task_stats.app_description,
|
|
53
|
+
timestamp_to_localized_str(task_stats.started_at, json) if task_stats.started_at else "Pending",
|
|
54
|
+
]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
display_table(column_names, rows, json=json, title=f"Active Containers in environment: {environment_name}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@container_cli.command("logs", no_args_is_help=True)
|
|
61
|
+
@synchronizer.create_blocking
|
|
62
|
+
async def logs(
|
|
63
|
+
container_id: str = typer.Argument(help="Container ID"),
|
|
64
|
+
follow: bool = typer.Option(False, "-f", "--follow", help="Stream log output until Container stops"),
|
|
65
|
+
all_logs: bool = typer.Option(False, "--all", help="Show all logs for the container"),
|
|
66
|
+
since: Optional[str] = typer.Option(
|
|
67
|
+
None,
|
|
68
|
+
"--since",
|
|
69
|
+
help=(
|
|
70
|
+
"Start of time range. Accepts ISO 8601 datetime or relative time, e.g. '1d' (1 day ago), '2h', '30m', etc."
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
until: Optional[str] = typer.Option(
|
|
74
|
+
None,
|
|
75
|
+
"--until",
|
|
76
|
+
help="End of time range; accepts same argument types as --since",
|
|
77
|
+
),
|
|
78
|
+
tail: Optional[int] = typer.Option(None, "--tail", "-n", help="Show only the last N log entries"),
|
|
79
|
+
search: Optional[str] = typer.Option(None, "--search", help="Filter by search text"),
|
|
80
|
+
source: Optional[str] = typer.Option(
|
|
81
|
+
None, "--source", "-s", help="Filter by source: 'stdout', 'stderr', or 'system'"
|
|
82
|
+
),
|
|
83
|
+
timestamps: bool = typer.Option(False, "--timestamps", help="Prefix each line with its timestamp"),
|
|
84
|
+
):
|
|
85
|
+
"""Fetch or stream logs for a specific container.
|
|
86
|
+
|
|
87
|
+
By default, this command fetches the last 100 log entries and exits. Use ``-f`` to
|
|
88
|
+
live-stream logs from a running container instead. Fetch and follow are mutually exclusive.
|
|
89
|
+
|
|
90
|
+
**Examples:**
|
|
91
|
+
|
|
92
|
+
Get recent logs for a container:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
modal container logs ta-123456
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Follow (stream) logs from a running container:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
modal container logs ta-123456 -f
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Fetch logs from the last 2 hours:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
modal container logs ta-123456 --since 2h
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Fetch logs in a specific time range:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
modal container logs ta-123456 --since 2026-03-01T05:00:00 --until 2026-03-01T08:00:00
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Fetch all container logs:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
modal container logs ta-123456 --all
|
|
120
|
+
```
|
|
121
|
+
"""
|
|
122
|
+
task_id, sandbox_id = None, None
|
|
123
|
+
if container_id.startswith("sb-"):
|
|
124
|
+
sandbox_id = container_id
|
|
125
|
+
elif container_id.startswith("ta-"):
|
|
126
|
+
task_id = container_id
|
|
127
|
+
else:
|
|
128
|
+
raise InvalidError(f"Invalid container ID: {container_id}")
|
|
129
|
+
|
|
130
|
+
if follow and (since or until or tail):
|
|
131
|
+
raise UsageError("--follow cannot be combined with --since, --until, or --tail.")
|
|
132
|
+
|
|
133
|
+
if tail is not None and tail <= 0:
|
|
134
|
+
raise UsageError("--tail value must be positive.")
|
|
135
|
+
|
|
136
|
+
if tail is not None and tail > _FETCH_LIMIT:
|
|
137
|
+
raise UsageError(f"--tail value must not exceed {_FETCH_LIMIT}.")
|
|
138
|
+
|
|
139
|
+
if all_logs and (since or until or tail):
|
|
140
|
+
raise UsageError("--all cannot be combined with --since, --until, or --tail.")
|
|
141
|
+
|
|
142
|
+
if all_logs and follow:
|
|
143
|
+
raise UsageError("--all cannot be combined with --follow.")
|
|
144
|
+
|
|
145
|
+
if source is not None:
|
|
146
|
+
if source not in _SOURCE_OPTIONS:
|
|
147
|
+
raise UsageError(f"Invalid source: '{source}'. Must be 'stdout', 'stderr', or 'system'.")
|
|
148
|
+
source_fd = _SOURCE_OPTIONS[source]
|
|
149
|
+
else:
|
|
150
|
+
source_fd = api_pb2.FILE_DESCRIPTOR_UNSPECIFIED
|
|
151
|
+
|
|
152
|
+
log_filters = LogsFilters(
|
|
153
|
+
source=source_fd,
|
|
154
|
+
task_id=task_id or "",
|
|
155
|
+
sandbox_id=sandbox_id or "",
|
|
156
|
+
search_text=search or "",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if follow:
|
|
160
|
+
await stream_app_logs.aio(
|
|
161
|
+
task_id=task_id,
|
|
162
|
+
sandbox_id=sandbox_id,
|
|
163
|
+
show_timestamps=timestamps,
|
|
164
|
+
follow=True,
|
|
165
|
+
filters=log_filters,
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
# Resolve the app_id for the container.
|
|
169
|
+
client = await _Client.from_env()
|
|
170
|
+
|
|
171
|
+
if sandbox_id:
|
|
172
|
+
sb_resp = await client.stub.SandboxGetTaskId(api_pb2.SandboxGetTaskIdRequest(sandbox_id=sandbox_id))
|
|
173
|
+
task_id = sb_resp.task_id
|
|
174
|
+
|
|
175
|
+
task_info_resp = await client.stub.TaskGetInfo(api_pb2.TaskGetInfoRequest(task_id=task_id))
|
|
176
|
+
app_id = task_info_resp.app_id
|
|
177
|
+
|
|
178
|
+
now = datetime.now(timezone.utc)
|
|
179
|
+
if all_logs:
|
|
180
|
+
since_dt = datetime.fromtimestamp(task_info_resp.info.started_at, timezone.utc)
|
|
181
|
+
if task_info_resp.info.finished_at:
|
|
182
|
+
until_dt = datetime.fromtimestamp(task_info_resp.info.finished_at, timezone.utc)
|
|
183
|
+
else:
|
|
184
|
+
until_dt = now
|
|
185
|
+
else:
|
|
186
|
+
since_dt = _parse_time_arg(since, default=now) if since else None
|
|
187
|
+
until_dt = _parse_time_arg(until, default=now) if until else None
|
|
188
|
+
|
|
189
|
+
if since_dt is not None and until_dt is not None and since_dt >= until_dt:
|
|
190
|
+
raise UsageError("--since must be before --until.")
|
|
191
|
+
|
|
192
|
+
if since_dt is not None:
|
|
193
|
+
effective_until = until_dt or now
|
|
194
|
+
if effective_until - since_dt > _MAX_FETCH_RANGE:
|
|
195
|
+
raise UsageError(f"Log fetch time range cannot exceed {_MAX_FETCH_RANGE.days} days.")
|
|
196
|
+
|
|
197
|
+
if all_logs or (since and tail is None):
|
|
198
|
+
# Range mode: --since without --tail fetches everything in the range.
|
|
199
|
+
await fetch_app_logs.aio(
|
|
200
|
+
app_id,
|
|
201
|
+
since_dt,
|
|
202
|
+
until_dt or now,
|
|
203
|
+
show_timestamps=timestamps,
|
|
204
|
+
filters=log_filters,
|
|
205
|
+
)
|
|
206
|
+
else:
|
|
207
|
+
# Tail mode: single fetch with limit.
|
|
208
|
+
# --since is a hard floor, --until shifts the anchor.
|
|
209
|
+
effective_tail = tail if tail is not None else _DEFAULT_LOGS_TAIL
|
|
210
|
+
await tail_app_logs.aio(
|
|
211
|
+
app_id,
|
|
212
|
+
effective_tail,
|
|
213
|
+
show_timestamps=timestamps,
|
|
214
|
+
since=since_dt,
|
|
215
|
+
until=until_dt,
|
|
216
|
+
filters=log_filters,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@container_cli.command("exec")
|
|
221
|
+
@synchronizer.create_blocking
|
|
222
|
+
async def exec(
|
|
223
|
+
pty: Optional[bool] = typer.Option(default=None, help="Run the command using a PTY."),
|
|
224
|
+
container_id: str = typer.Argument(help="Container ID"),
|
|
225
|
+
command: list[str] = typer.Argument(
|
|
226
|
+
help="A command to run inside the container.\n\n"
|
|
227
|
+
"To pass command-line flags or options, add `--` before the start of your commands. "
|
|
228
|
+
"For example: `modal container exec <id> -- /bin/bash -c 'echo hi'`"
|
|
229
|
+
),
|
|
230
|
+
):
|
|
231
|
+
"""Execute a command in a container."""
|
|
232
|
+
|
|
233
|
+
if pty is None:
|
|
234
|
+
pty = is_tty()
|
|
235
|
+
|
|
236
|
+
client = await _Client.from_env()
|
|
237
|
+
|
|
238
|
+
req = api_pb2.ContainerExecRequest(
|
|
239
|
+
task_id=container_id,
|
|
240
|
+
command=command,
|
|
241
|
+
pty_info=get_pty_info(shell=True) if pty else None,
|
|
242
|
+
runtime_debug=config.get("function_runtime_debug"),
|
|
243
|
+
)
|
|
244
|
+
res: api_pb2.ContainerExecResponse = await client.stub.ContainerExec(req)
|
|
245
|
+
|
|
246
|
+
if pty:
|
|
247
|
+
await _ContainerProcess(res.exec_id, container_id, client).attach()
|
|
248
|
+
else:
|
|
249
|
+
# TODO: redirect stderr to its own stream?
|
|
250
|
+
await _ContainerProcess(
|
|
251
|
+
res.exec_id, container_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT
|
|
252
|
+
).wait()
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@container_cli.command("stop")
|
|
256
|
+
@synchronizer.create_blocking
|
|
257
|
+
async def stop(container_id: str = typer.Argument(help="Container ID")):
|
|
258
|
+
"""Stop a currently-running container and reassign its in-progress inputs.
|
|
259
|
+
|
|
260
|
+
This will send the container a SIGINT signal that Modal will handle.
|
|
261
|
+
"""
|
|
262
|
+
client = await _Client.from_env()
|
|
263
|
+
request = api_pb2.ContainerStopRequest(task_id=container_id)
|
|
264
|
+
await client.stub.ContainerStop(request)
|
|
@@ -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.3.6.
|
|
38
|
+
version: str = "1.3.6.dev13",
|
|
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.3.6.
|
|
174
|
+
version: str = "1.3.6.dev13",
|
|
175
175
|
):
|
|
176
176
|
"""mdmd:hidden
|
|
177
177
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -408,7 +408,7 @@ class Function(
|
|
|
408
408
|
|
|
409
409
|
_call_generator: ___call_generator_spec
|
|
410
410
|
|
|
411
|
-
class __remote_spec(typing_extensions.Protocol[
|
|
411
|
+
class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
|
|
412
412
|
def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
|
|
413
413
|
"""Calls the function remotely, executing it with the given arguments and returning the execution's result."""
|
|
414
414
|
...
|
|
@@ -417,7 +417,7 @@ class Function(
|
|
|
417
417
|
"""Calls the function remotely, executing it with the given arguments and returning the execution's result."""
|
|
418
418
|
...
|
|
419
419
|
|
|
420
|
-
remote: __remote_spec[modal._functions.
|
|
420
|
+
remote: __remote_spec[modal._functions.ReturnType, modal._functions.P]
|
|
421
421
|
|
|
422
422
|
class __remote_gen_spec(typing_extensions.Protocol):
|
|
423
423
|
def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
|
|
@@ -444,7 +444,7 @@ class Function(
|
|
|
444
444
|
"""
|
|
445
445
|
...
|
|
446
446
|
|
|
447
|
-
class ___experimental_spawn_spec(typing_extensions.Protocol[
|
|
447
|
+
class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
|
|
448
448
|
def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
|
|
449
449
|
"""[Experimental] Calls the function with the given arguments, without waiting for the results.
|
|
450
450
|
|
|
@@ -467,7 +467,7 @@ class Function(
|
|
|
467
467
|
"""
|
|
468
468
|
...
|
|
469
469
|
|
|
470
|
-
_experimental_spawn: ___experimental_spawn_spec[modal._functions.
|
|
470
|
+
_experimental_spawn: ___experimental_spawn_spec[modal._functions.ReturnType, modal._functions.P]
|
|
471
471
|
|
|
472
472
|
class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER]):
|
|
473
473
|
def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> None: ...
|
|
@@ -475,7 +475,7 @@ class Function(
|
|
|
475
475
|
|
|
476
476
|
_spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P]
|
|
477
477
|
|
|
478
|
-
class __spawn_spec(typing_extensions.Protocol[
|
|
478
|
+
class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
|
|
479
479
|
def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
|
|
480
480
|
"""Calls the function with the given arguments, without waiting for the results.
|
|
481
481
|
|
|
@@ -496,7 +496,7 @@ class Function(
|
|
|
496
496
|
"""
|
|
497
497
|
...
|
|
498
498
|
|
|
499
|
-
spawn: __spawn_spec[modal._functions.
|
|
499
|
+
spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P]
|
|
500
500
|
|
|
501
501
|
def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
|
|
502
502
|
"""Return the inner Python object wrapped by this Modal Function."""
|
|
@@ -40,7 +40,7 @@ from ._utils.docker_utils import (
|
|
|
40
40
|
find_dockerignore_file,
|
|
41
41
|
)
|
|
42
42
|
from ._utils.function_utils import FunctionInfo
|
|
43
|
-
from ._utils.mount_utils import validate_only_modal_volumes
|
|
43
|
+
from ._utils.mount_utils import validate_only_modal_volumes, validate_volumes_by_object_id
|
|
44
44
|
from .client import _Client
|
|
45
45
|
from .cloud_bucket_mount import _CloudBucketMount
|
|
46
46
|
from .config import config, logger, user_config_path
|
|
@@ -434,7 +434,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
434
434
|
self._serve_mounts = other._serve_mounts
|
|
435
435
|
self._deferred_mounts = other._deferred_mounts
|
|
436
436
|
self._added_python_source_set = other._added_python_source_set
|
|
437
|
-
self._is_empty =
|
|
437
|
+
self._is_empty = False
|
|
438
438
|
|
|
439
439
|
def _get_metadata(self) -> Optional[Message]:
|
|
440
440
|
return self._metadata
|
|
@@ -626,6 +626,9 @@ class _Image(_Object, type_prefix="im"):
|
|
|
626
626
|
build_function_id = ""
|
|
627
627
|
_build_function = None
|
|
628
628
|
|
|
629
|
+
# Validate that the same volume (by object_id) isn't mounted at multiple paths
|
|
630
|
+
validate_volumes_by_object_id(validated_volumes)
|
|
631
|
+
|
|
629
632
|
# Relies on dicts being ordered (true as of Python 3.6).
|
|
630
633
|
volume_mounts = [
|
|
631
634
|
api_pb2.VolumeMount(
|
|
@@ -724,12 +727,6 @@ class _Image(_Object, type_prefix="im"):
|
|
|
724
727
|
)
|
|
725
728
|
return obj
|
|
726
729
|
|
|
727
|
-
@staticmethod
|
|
728
|
-
def _from_scratch() -> "_Image":
|
|
729
|
-
image = _Image.from_registry("scratch")
|
|
730
|
-
image._is_empty = True
|
|
731
|
-
return image
|
|
732
|
-
|
|
733
730
|
def _copy_mount(self, mount: _Mount, remote_path: Union[str, Path] = ".") -> "_Image":
|
|
734
731
|
"""mdmd:hidden
|
|
735
732
|
Internal
|
|
@@ -2179,6 +2176,36 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2179
2176
|
force_build=force_build,
|
|
2180
2177
|
)
|
|
2181
2178
|
|
|
2179
|
+
@staticmethod
|
|
2180
|
+
def from_scratch(force_build: bool = False) -> "_Image":
|
|
2181
|
+
"""Create an empty Image, equivalent to `FROM scratch` in Docker.
|
|
2182
|
+
|
|
2183
|
+
The resulting Image has no operating system, shell, or package manager. It is
|
|
2184
|
+
primarily useful as a lightweight filesystem to mount into a Sandbox via
|
|
2185
|
+
`Sandbox.mount_image`.
|
|
2186
|
+
|
|
2187
|
+
Note that since this Image doesn't contain Python or other standard OS utilities,
|
|
2188
|
+
higher-level Image build steps like `pip_install` cannot be chained onto it. It also
|
|
2189
|
+
cannot be used for `modal.Function` execution, which requires a Python interpreter.
|
|
2190
|
+
|
|
2191
|
+
**Example**
|
|
2192
|
+
|
|
2193
|
+
```python notest
|
|
2194
|
+
image = modal.Image.from_scratch().add_local_file(local_path, "/bin/my_binary", copy=True)
|
|
2195
|
+
```
|
|
2196
|
+
"""
|
|
2197
|
+
|
|
2198
|
+
def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
|
|
2199
|
+
return DockerfileSpec(commands=["FROM scratch"], context_files={})
|
|
2200
|
+
|
|
2201
|
+
image = _Image._from_args(
|
|
2202
|
+
dockerfile_function=build_dockerfile,
|
|
2203
|
+
force_build=force_build,
|
|
2204
|
+
_namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
|
|
2205
|
+
)
|
|
2206
|
+
image._is_empty = True
|
|
2207
|
+
return image
|
|
2208
|
+
|
|
2182
2209
|
@staticmethod
|
|
2183
2210
|
def debian_slim(python_version: Optional[str] = None, force_build: bool = False) -> "_Image":
|
|
2184
2211
|
"""Default image, based on the official `python` Docker images."""
|
|
@@ -187,8 +187,6 @@ class _Image(modal._object._Object):
|
|
|
187
187
|
_namespace: int = 1,
|
|
188
188
|
_do_assert_no_mount_layers: bool = True,
|
|
189
189
|
): ...
|
|
190
|
-
@staticmethod
|
|
191
|
-
def _from_scratch() -> _Image: ...
|
|
192
190
|
def _copy_mount(self, mount: modal.mount._Mount, remote_path: typing.Union[str, pathlib.Path] = ".") -> _Image:
|
|
193
191
|
"""mdmd:hidden
|
|
194
192
|
Internal
|
|
@@ -901,6 +899,26 @@ class _Image(modal._object._Object):
|
|
|
901
899
|
"""
|
|
902
900
|
...
|
|
903
901
|
|
|
902
|
+
@staticmethod
|
|
903
|
+
def from_scratch(force_build: bool = False) -> _Image:
|
|
904
|
+
"""Create an empty Image, equivalent to `FROM scratch` in Docker.
|
|
905
|
+
|
|
906
|
+
The resulting Image has no operating system, shell, or package manager. It is
|
|
907
|
+
primarily useful as a lightweight filesystem to mount into a Sandbox via
|
|
908
|
+
`Sandbox.mount_image`.
|
|
909
|
+
|
|
910
|
+
Note that since this Image doesn't contain Python or other standard OS utilities,
|
|
911
|
+
higher-level Image build steps like `pip_install` cannot be chained onto it. It also
|
|
912
|
+
cannot be used for `modal.Function` execution, which requires a Python interpreter.
|
|
913
|
+
|
|
914
|
+
**Example**
|
|
915
|
+
|
|
916
|
+
```python notest
|
|
917
|
+
image = modal.Image.from_scratch().add_local_file(local_path, "/bin/my_binary", copy=True)
|
|
918
|
+
```
|
|
919
|
+
"""
|
|
920
|
+
...
|
|
921
|
+
|
|
904
922
|
@staticmethod
|
|
905
923
|
def debian_slim(python_version: typing.Optional[str] = None, force_build: bool = False) -> _Image:
|
|
906
924
|
"""Default image, based on the official `python` Docker images."""
|
|
@@ -1109,8 +1127,6 @@ class Image(modal.object.Object):
|
|
|
1109
1127
|
_namespace: int = 1,
|
|
1110
1128
|
_do_assert_no_mount_layers: bool = True,
|
|
1111
1129
|
): ...
|
|
1112
|
-
@staticmethod
|
|
1113
|
-
def _from_scratch() -> Image: ...
|
|
1114
1130
|
def _copy_mount(self, mount: modal.mount.Mount, remote_path: typing.Union[str, pathlib.Path] = ".") -> Image:
|
|
1115
1131
|
"""mdmd:hidden
|
|
1116
1132
|
Internal
|
|
@@ -1879,6 +1895,26 @@ class Image(modal.object.Object):
|
|
|
1879
1895
|
"""
|
|
1880
1896
|
...
|
|
1881
1897
|
|
|
1898
|
+
@staticmethod
|
|
1899
|
+
def from_scratch(force_build: bool = False) -> Image:
|
|
1900
|
+
"""Create an empty Image, equivalent to `FROM scratch` in Docker.
|
|
1901
|
+
|
|
1902
|
+
The resulting Image has no operating system, shell, or package manager. It is
|
|
1903
|
+
primarily useful as a lightweight filesystem to mount into a Sandbox via
|
|
1904
|
+
`Sandbox.mount_image`.
|
|
1905
|
+
|
|
1906
|
+
Note that since this Image doesn't contain Python or other standard OS utilities,
|
|
1907
|
+
higher-level Image build steps like `pip_install` cannot be chained onto it. It also
|
|
1908
|
+
cannot be used for `modal.Function` execution, which requires a Python interpreter.
|
|
1909
|
+
|
|
1910
|
+
**Example**
|
|
1911
|
+
|
|
1912
|
+
```python notest
|
|
1913
|
+
image = modal.Image.from_scratch().add_local_file(local_path, "/bin/my_binary", copy=True)
|
|
1914
|
+
```
|
|
1915
|
+
"""
|
|
1916
|
+
...
|
|
1917
|
+
|
|
1882
1918
|
@staticmethod
|
|
1883
1919
|
def debian_slim(python_version: typing.Optional[str] = None, force_build: bool = False) -> Image:
|
|
1884
1920
|
"""Default image, based on the official `python` Docker images."""
|
|
@@ -214,23 +214,32 @@ async def _stop_and_wait_for_containers(
|
|
|
214
214
|
if container.enqueued_at and container.enqueued_at < deployed_at
|
|
215
215
|
]
|
|
216
216
|
|
|
217
|
+
stopped_ids = set()
|
|
218
|
+
|
|
217
219
|
async def stop_containers(container_ids: list[str]):
|
|
218
220
|
sem = asyncio.Semaphore(32)
|
|
219
221
|
|
|
220
222
|
async def stop_one_container(tid: str):
|
|
223
|
+
if tid in stopped_ids:
|
|
224
|
+
return
|
|
225
|
+
|
|
221
226
|
async with sem:
|
|
222
227
|
await client.stub.ContainerStop(api_pb2.ContainerStopRequest(task_id=tid))
|
|
228
|
+
stopped_ids.add(tid)
|
|
223
229
|
|
|
224
|
-
|
|
230
|
+
stop_tasks = [stop_one_container(tid) for tid in container_ids if tid not in stopped_ids]
|
|
231
|
+
if not stop_tasks:
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
results = await asyncio.gather(*stop_tasks, return_exceptions=True)
|
|
225
235
|
exceptions = [r for r in results if isinstance(r, BaseException)]
|
|
226
236
|
if exceptions:
|
|
227
237
|
raise exceptions[0]
|
|
228
238
|
|
|
229
|
-
async def poll_until_stopped(
|
|
230
|
-
await stop_containers(initial_ids)
|
|
231
|
-
await asyncio.sleep(WAIT_FOR_CONTAINER_STOP_SLEEP_INTERVAL)
|
|
232
|
-
|
|
239
|
+
async def poll_until_stopped():
|
|
233
240
|
while ids := await get_old_container_ids():
|
|
241
|
+
# Just in case there are new ids from `get_old_container_ids`, we call `stop_containers` again,
|
|
242
|
+
# which will no-op for containers that was already stopped.
|
|
234
243
|
await stop_containers(ids)
|
|
235
244
|
await asyncio.sleep(WAIT_FOR_CONTAINER_STOP_SLEEP_INTERVAL)
|
|
236
245
|
|
|
@@ -239,12 +248,13 @@ async def _stop_and_wait_for_containers(
|
|
|
239
248
|
return
|
|
240
249
|
|
|
241
250
|
output = OutputManager.get()
|
|
242
|
-
output.print("
|
|
251
|
+
output.print("🧹 Terminating running containers")
|
|
252
|
+
await stop_containers(ids)
|
|
243
253
|
|
|
244
254
|
try:
|
|
245
|
-
await asyncio.wait_for(poll_until_stopped(
|
|
255
|
+
await asyncio.wait_for(poll_until_stopped(), timeout=WAIT_FOR_CONTAINER_STOP_TIMEOUT)
|
|
246
256
|
except asyncio.TimeoutError:
|
|
247
|
-
raise asyncio.TimeoutError(f"Containers did not
|
|
257
|
+
raise asyncio.TimeoutError(f"Containers did not terminate in under {WAIT_FOR_CONTAINER_STOP_TIMEOUT} seconds.")
|
|
248
258
|
|
|
249
259
|
|
|
250
260
|
def _validate_deployment_strategy(strategy: str) -> DEPLOYMENT_STRATEGY_TYPE:
|
|
@@ -294,7 +304,7 @@ async def _publish_app(
|
|
|
294
304
|
)
|
|
295
305
|
except Exception as exc:
|
|
296
306
|
warnings.warn(
|
|
297
|
-
f"App updated successfully, but containers did not all
|
|
307
|
+
f"App updated successfully, but containers did not all terminate. {exc}",
|
|
298
308
|
UserWarning,
|
|
299
309
|
)
|
|
300
310
|
|
|
@@ -30,7 +30,7 @@ from ._resolver import Resolver
|
|
|
30
30
|
from ._resources import convert_fn_config_to_resources_config
|
|
31
31
|
from ._utils.async_utils import TaskContext, synchronize_api
|
|
32
32
|
from ._utils.deprecation import deprecation_warning
|
|
33
|
-
from ._utils.mount_utils import validate_network_file_systems, validate_volumes
|
|
33
|
+
from ._utils.mount_utils import validate_network_file_systems, validate_volumes, validate_volumes_by_object_id
|
|
34
34
|
from ._utils.name_utils import check_object_name
|
|
35
35
|
from ._utils.task_command_router_client import TaskCommandRouterClient
|
|
36
36
|
from .client import _Client
|
|
@@ -214,6 +214,9 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
214
214
|
async def _load(
|
|
215
215
|
self: _Sandbox, resolver: Resolver, load_context: LoadContext, _existing_object_id: Optional[str]
|
|
216
216
|
):
|
|
217
|
+
# Validate that the same volume (by object_id) isn't mounted at multiple paths
|
|
218
|
+
validate_volumes_by_object_id(validated_volumes)
|
|
219
|
+
|
|
217
220
|
# Relies on dicts being ordered (true as of Python 3.6).
|
|
218
221
|
volume_mounts = [
|
|
219
222
|
api_pb2.VolumeMount(
|
|
@@ -739,7 +742,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
739
742
|
"The `Sandbox._experimental_mount_image()` method is deprecated. Use `Sandbox.mount_image()` instead.",
|
|
740
743
|
)
|
|
741
744
|
if image is None:
|
|
742
|
-
image = _Image.
|
|
745
|
+
image = _Image.from_scratch()
|
|
743
746
|
await self.mount_image(path, image)
|
|
744
747
|
|
|
745
748
|
async def snapshot_directory(self, path: Union[PurePosixPath, str]) -> _Image:
|