modal 1.4.3.dev27__tar.gz → 1.4.3.dev28__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.dev27 → modal-1.4.3.dev28}/PKG-INFO +1 -1
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_environments.py +2 -2
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_functions.py +1 -1
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_grpc_client.py +7 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_object.py +13 -13
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_resolver.py +3 -5
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_runtime/user_code_imports.py +2 -2
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/task_command_router_client.py +54 -5
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/queues.py +1 -1
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/secret.py +1 -1
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/client.pyi +2 -2
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cls.py +2 -2
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/dict.py +4 -4
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/mount.py +1 -1
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/network_file_system.py +2 -2
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/object.pyi +6 -6
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/proxy.py +1 -1
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/queue.py +4 -4
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/sandbox.py +29 -4
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/sandbox.pyi +19 -3
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/secret.py +2 -2
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/volume.py +4 -4
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_proto/task_command_router_grpc.py +32 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_proto/task_command_router_pb2.py +85 -35
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_proto/task_command_router_pb2.pyi +95 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_proto/task_command_router_pb2_grpc.py +70 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_proto/task_command_router_pb2_grpc.pyi +32 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_version/__init__.py +1 -1
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/LICENSE +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/README.md +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/__init__.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/__main__.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_billing.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_clustered_functions.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_clustered_functions.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_container_entrypoint.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_function_variants.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_ipython.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_load_context.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_location.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_logs.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_output/__init__.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_output/manager.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_output/pty.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_output/rich.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_output/status.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_partial_function.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_resources.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_runtime/__init__.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_runtime/asgi.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_runtime/execution_context.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_runtime/telemetry.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_serialization.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_server.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_traceback.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_tunnel.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_tunnel.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_type_manager.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/__init__.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/app_utils.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/async_utils.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/blob_utils.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/browser_utils.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/deprecation.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/docker_utils.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/function_utils.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/git_utils.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/hash_utils.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/http_utils.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/logger.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/mount_utils.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/name_utils.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/package_utils.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/shell_utils.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_utils/time_utils.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_vendor/__init__.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_vendor/tblib.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_vendor/version.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/_watcher.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/app.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/app.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/billing.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/builder/2023.12.txt +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/builder/2024.04.txt +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/builder/2024.10.txt +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/builder/2025.06.txt +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/builder/README.md +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/builder/base-images.json +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/call_graph.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/__init__.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/_download.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/_help.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/_traceback.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/app.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/billing.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/bootstrap.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/changelog.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/cluster.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/config.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/container.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/dashboard.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/dict.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/entry_point.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/environment.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/import_refs.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/launch.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/logo.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/network_file_system.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/profile.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/programs/__init__.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/programs/vscode.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/run.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/selector.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/shell.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/token.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/utils.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cli/volume.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/client.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/cls.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/config.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/container_process.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/container_process.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/dict.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/environments.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/environments.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/exception.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/experimental/__init__.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/experimental/flash.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/experimental/flash.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/experimental/ipython.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/file_io.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/file_io.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/file_pattern_matcher.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/functions.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/functions.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/image.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/image.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/io_streams.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/io_streams.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/mount.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/network_file_system.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/object.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/output.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/parallel_map.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/parallel_map.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/partial_function.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/partial_function.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/proxy.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/py.typed +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/queue.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/retries.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/runner.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/runner.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/running_app.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/sandbox_fs.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/sandbox_fs.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/schedule.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/scheduler_placement.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/secret.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/server.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/server.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/serving.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/serving.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/snapshot.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/snapshot.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/stream_type.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/token_flow.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/token_flow.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal/volume.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal.egg-info/requires.txt +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal.egg-info/top_level.txt +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_docs/__init__.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_proto/__init__.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_proto/api_grpc.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_proto/api_pb2.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_proto/api_pb2.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_proto/py.typed +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/modal_version/__main__.py +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/pyproject.toml +0 -0
- {modal-1.4.3.dev27 → modal-1.4.3.dev28}/setup.cfg +0 -0
|
@@ -75,7 +75,7 @@ class _EnvironmentManager:
|
|
|
75
75
|
item.environment_id,
|
|
76
76
|
client,
|
|
77
77
|
metadata,
|
|
78
|
-
|
|
78
|
+
skip_reload=True,
|
|
79
79
|
rep=f"Environment.from_name({item.name!r})",
|
|
80
80
|
)
|
|
81
81
|
environments.append(env)
|
|
@@ -351,7 +351,7 @@ class _Environment(_Object, type_prefix="en"):
|
|
|
351
351
|
return _Environment._from_loader(
|
|
352
352
|
_load,
|
|
353
353
|
repr,
|
|
354
|
-
|
|
354
|
+
skip_reload=True,
|
|
355
355
|
hydrate_lazily=True,
|
|
356
356
|
name=name,
|
|
357
357
|
load_context_overrides=LoadContext(client=client),
|
|
@@ -1242,7 +1242,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1242
1242
|
)
|
|
1243
1243
|
rep = f"modal.Function.from_name('{app_name}', '{name}'{environment_rep})"
|
|
1244
1244
|
return cls._from_loader(
|
|
1245
|
-
_load_remote, rep,
|
|
1245
|
+
_load_remote, rep, skip_reload=True, hydrate_lazily=True, load_context_overrides=load_context_overrides
|
|
1246
1246
|
)
|
|
1247
1247
|
|
|
1248
1248
|
@classmethod
|
|
@@ -44,6 +44,10 @@ _STATUS_TO_EXCEPTION: dict[Status, type[exception._GRPCErrorWrapper]] = {
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
class grpc_error_converter:
|
|
47
|
+
def __init__(self, *, expect_timeouts: bool = False):
|
|
48
|
+
"""expect_timeouts: if this is set we convert cancelled/deadline_exceeded statuses to exception.TimeoutError"""
|
|
49
|
+
self._expect_timeouts = expect_timeouts
|
|
50
|
+
|
|
47
51
|
def __enter__(self):
|
|
48
52
|
pass
|
|
49
53
|
|
|
@@ -52,6 +56,9 @@ class grpc_error_converter:
|
|
|
52
56
|
use_full_traceback = config.get("traceback")
|
|
53
57
|
with suppress_tb_frame():
|
|
54
58
|
if isinstance(exc, GRPCError):
|
|
59
|
+
if self._expect_timeouts and exc.status in (Status.CANCELLED, Status.DEADLINE_EXCEEDED):
|
|
60
|
+
raise exception.TimeoutError("Timeout expired")
|
|
61
|
+
|
|
55
62
|
modal_exc = _STATUS_TO_EXCEPTION[exc.status](exc.message)
|
|
56
63
|
modal_exc._grpc_message = exc.message
|
|
57
64
|
modal_exc._grpc_status = exc.status
|
|
@@ -82,7 +82,7 @@ class _Object:
|
|
|
82
82
|
_load: Optional[Callable[[Self, Resolver, LoadContext, Optional[str]], Awaitable[None]]] = None
|
|
83
83
|
_preload: Optional[Callable[[Self, Resolver, LoadContext, Optional[str]], Awaitable[None]]]
|
|
84
84
|
_rep: str
|
|
85
|
-
|
|
85
|
+
_skip_reload: bool
|
|
86
86
|
_hydrate_lazily: bool
|
|
87
87
|
_deps: Optional[Callable[..., Sequence["_Object"]]]
|
|
88
88
|
_deduplication_key: Optional[Callable[[], Awaitable[Hashable]]] = None
|
|
@@ -113,9 +113,9 @@ class _Object:
|
|
|
113
113
|
self,
|
|
114
114
|
rep: str,
|
|
115
115
|
load: Optional[Callable[[Self, Resolver, LoadContext, Optional[str]], Awaitable[None]]] = None,
|
|
116
|
-
is_another_app: bool = False,
|
|
117
116
|
preload: Optional[Callable[[Self, Resolver, LoadContext, Optional[str]], Awaitable[None]]] = None,
|
|
118
117
|
hydrate_lazily: bool = False,
|
|
118
|
+
skip_reload: bool = False,
|
|
119
119
|
deps: Optional[Callable[..., Sequence["_Object"]]] = None,
|
|
120
120
|
deduplication_key: Optional[Callable[[], Awaitable[Hashable]]] = None,
|
|
121
121
|
name: Optional[str] = None,
|
|
@@ -123,10 +123,10 @@ class _Object:
|
|
|
123
123
|
load_context_overrides: Optional[LoadContext] = None,
|
|
124
124
|
):
|
|
125
125
|
self._local_uuid = str(uuid.uuid4())
|
|
126
|
+
self._rep = rep
|
|
126
127
|
self._load = load
|
|
127
128
|
self._preload = preload
|
|
128
|
-
self.
|
|
129
|
-
self._is_another_app = is_another_app
|
|
129
|
+
self._skip_reload = skip_reload
|
|
130
130
|
self._hydrate_lazily = hydrate_lazily
|
|
131
131
|
self._deps = deps
|
|
132
132
|
self._deduplication_key = deduplication_key
|
|
@@ -218,7 +218,7 @@ class _Object:
|
|
|
218
218
|
cls,
|
|
219
219
|
load: Callable[[Self, Resolver, LoadContext, Optional[str]], Awaitable[None]],
|
|
220
220
|
rep: str,
|
|
221
|
-
|
|
221
|
+
skip_reload: bool = False,
|
|
222
222
|
preload: Optional[Callable[[Self, Resolver, LoadContext, Optional[str]], Awaitable[None]]] = None,
|
|
223
223
|
hydrate_lazily: bool = False,
|
|
224
224
|
deps: Optional[Callable[..., Sequence["_Object"]]] = None,
|
|
@@ -232,12 +232,12 @@ class _Object:
|
|
|
232
232
|
obj._init(
|
|
233
233
|
rep,
|
|
234
234
|
load,
|
|
235
|
-
|
|
236
|
-
preload,
|
|
237
|
-
hydrate_lazily,
|
|
238
|
-
deps,
|
|
239
|
-
deduplication_key,
|
|
240
|
-
name,
|
|
235
|
+
skip_reload=skip_reload,
|
|
236
|
+
preload=preload,
|
|
237
|
+
hydrate_lazily=hydrate_lazily,
|
|
238
|
+
deps=deps,
|
|
239
|
+
deduplication_key=deduplication_key,
|
|
240
|
+
name=name,
|
|
241
241
|
load_context_overrides=load_context_overrides,
|
|
242
242
|
)
|
|
243
243
|
return obj
|
|
@@ -268,7 +268,7 @@ class _Object:
|
|
|
268
268
|
object_id: str,
|
|
269
269
|
client: _Client,
|
|
270
270
|
handle_metadata: Optional[Message],
|
|
271
|
-
|
|
271
|
+
skip_reload: bool = False,
|
|
272
272
|
rep: Optional[str] = None,
|
|
273
273
|
) -> Self:
|
|
274
274
|
obj_cls: type[Self]
|
|
@@ -287,7 +287,7 @@ class _Object:
|
|
|
287
287
|
# Instantiate provider
|
|
288
288
|
obj = _Object.__new__(obj_cls)
|
|
289
289
|
rep = rep or f"modal.{obj_cls.__name__.strip('_')}.from_id({object_id!r})"
|
|
290
|
-
obj._init(rep,
|
|
290
|
+
obj._init(rep, skip_reload=skip_reload)
|
|
291
291
|
obj._hydrate(object_id, client, handle_metadata)
|
|
292
292
|
|
|
293
293
|
return obj
|
|
@@ -44,8 +44,7 @@ class Resolver:
|
|
|
44
44
|
*,
|
|
45
45
|
existing_object_id: Optional[str] = None,
|
|
46
46
|
):
|
|
47
|
-
if obj._is_hydrated and obj.
|
|
48
|
-
# No need to reload this, it won't typically change
|
|
47
|
+
if obj._is_hydrated and obj._skip_reload:
|
|
49
48
|
if obj.local_uuid not in self._local_uuid_to_future:
|
|
50
49
|
# a bit dumb - but we still need to store a reference to the object here
|
|
51
50
|
# to be able to include all referenced objects when setting up the app
|
|
@@ -89,12 +88,11 @@ class Resolver:
|
|
|
89
88
|
await obj._load(obj, self, load_context, existing_object_id)
|
|
90
89
|
|
|
91
90
|
# Check that the id of functions didn't change
|
|
92
|
-
# Persisted refs are ignored because their life cycle is managed independently.
|
|
93
91
|
if (
|
|
94
|
-
not
|
|
95
|
-
and existing_object_id is not None
|
|
92
|
+
existing_object_id is not None
|
|
96
93
|
and existing_object_id.startswith("fu-")
|
|
97
94
|
and obj.object_id != existing_object_id
|
|
95
|
+
and not obj._skip_reload
|
|
98
96
|
):
|
|
99
97
|
raise Exception(
|
|
100
98
|
f"Tried creating an object using existing id {existing_object_id} "
|
|
@@ -570,7 +570,7 @@ def import_class_service(
|
|
|
570
570
|
service_function_hydration_data.object_id,
|
|
571
571
|
_client,
|
|
572
572
|
service_function_hydration_data.function_handle_metadata,
|
|
573
|
-
|
|
573
|
+
skip_reload=True, # this skips re-loading the function, which is required since it doesn't have a loader
|
|
574
574
|
)
|
|
575
575
|
_cls = modal.cls._Cls.from_local(cls_or_user_cls, active_app, _service_function)
|
|
576
576
|
# hydration of the class itself - just sets the id and triggers some side effects
|
|
@@ -625,7 +625,7 @@ def import_server_service(
|
|
|
625
625
|
service_function_hydration_data.object_id,
|
|
626
626
|
_client,
|
|
627
627
|
service_function_hydration_data.function_handle_metadata,
|
|
628
|
-
|
|
628
|
+
skip_reload=True, # this skips re-loading the function, which is required since it doesn't have a loader
|
|
629
629
|
)
|
|
630
630
|
|
|
631
631
|
_server = modal._server._Server._from_local(cls_or_user_cls, active_app, _service_function)
|
|
@@ -5,6 +5,7 @@ import json
|
|
|
5
5
|
import socket
|
|
6
6
|
import ssl
|
|
7
7
|
import time
|
|
8
|
+
import typing
|
|
8
9
|
import urllib.parse
|
|
9
10
|
import weakref
|
|
10
11
|
from contextlib import suppress
|
|
@@ -16,7 +17,7 @@ from grpclib import GRPCError, Status
|
|
|
16
17
|
from grpclib.exceptions import StreamTerminatedError
|
|
17
18
|
|
|
18
19
|
from modal.config import logger
|
|
19
|
-
from modal.exception import ExecTimeoutError
|
|
20
|
+
from modal.exception import ExecTimeoutError, TimeoutError as ModalTimeoutError
|
|
20
21
|
from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
|
|
21
22
|
from modal_proto.task_command_router_grpc import TaskCommandRouterStub
|
|
22
23
|
|
|
@@ -72,13 +73,24 @@ async def call_with_retries_on_transient_errors(
|
|
|
72
73
|
base_delay_secs: float = 0.01,
|
|
73
74
|
delay_factor: float = 2,
|
|
74
75
|
max_retries: Optional[int] = 10,
|
|
76
|
+
exclude_status_codes: Optional[list[Status]] = None,
|
|
77
|
+
expect_timeouts: bool = False, # when True we convert TimeoutError to exception.TimeoutError and don't retry"""
|
|
75
78
|
):
|
|
76
79
|
"""Call func() with transient error retries and exponential backoff.
|
|
77
80
|
|
|
78
81
|
Authentication retries are expected to be handled by the caller.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
exclude_status_codes: gRPC status codes to exclude from retry logic even if
|
|
85
|
+
they are in RETRYABLE_GRPC_STATUS_CODES. Use this to let certain errors
|
|
86
|
+
(e.g. DEADLINE_EXCEEDED) propagate immediately rather than being retried.
|
|
79
87
|
"""
|
|
80
88
|
delay_secs = base_delay_secs
|
|
81
89
|
num_retries = 0
|
|
90
|
+
exclude_status_codes = exclude_status_codes or []
|
|
91
|
+
|
|
92
|
+
def is_retryable_status(status: Status) -> bool:
|
|
93
|
+
return status in RETRYABLE_GRPC_STATUS_CODES and status not in exclude_status_codes
|
|
82
94
|
|
|
83
95
|
async def sleep_and_update_delay_and_num_retries_remaining(e: Exception):
|
|
84
96
|
nonlocal delay_secs, num_retries
|
|
@@ -91,7 +103,7 @@ async def call_with_retries_on_transient_errors(
|
|
|
91
103
|
try:
|
|
92
104
|
return await func()
|
|
93
105
|
except GRPCError as e:
|
|
94
|
-
if (max_retries is None or num_retries < max_retries) and e.status
|
|
106
|
+
if (max_retries is None or num_retries < max_retries) and is_retryable_status(e.status):
|
|
95
107
|
await sleep_and_update_delay_and_num_retries_remaining(e)
|
|
96
108
|
else:
|
|
97
109
|
raise e
|
|
@@ -108,7 +120,16 @@ async def call_with_retries_on_transient_errors(
|
|
|
108
120
|
await sleep_and_update_delay_and_num_retries_remaining(e)
|
|
109
121
|
else:
|
|
110
122
|
raise e
|
|
111
|
-
except
|
|
123
|
+
except asyncio.TimeoutError as e:
|
|
124
|
+
if expect_timeouts:
|
|
125
|
+
# grpclib raises TimeoutError for the client-side timeout/deadlines
|
|
126
|
+
raise ModalTimeoutError("Timeout expired")
|
|
127
|
+
|
|
128
|
+
if max_retries is None or num_retries < max_retries:
|
|
129
|
+
await sleep_and_update_delay_and_num_retries_remaining(e)
|
|
130
|
+
else:
|
|
131
|
+
raise ConnectionError(str(e))
|
|
132
|
+
except OSError as e:
|
|
112
133
|
if max_retries is None or num_retries < max_retries:
|
|
113
134
|
await sleep_and_update_delay_and_num_retries_remaining(e)
|
|
114
135
|
else:
|
|
@@ -186,6 +207,15 @@ class TaskCommandRouterClient:
|
|
|
186
207
|
closed_error_message="Unable to perform operation on a detached sandbox",
|
|
187
208
|
)
|
|
188
209
|
|
|
210
|
+
async def send_request(event: grpclib.events.SendRequest) -> None:
|
|
211
|
+
idempotency_key = typing.cast(Optional[str], event.metadata.get("x-idempotency-key"))
|
|
212
|
+
if idempotency_key is None:
|
|
213
|
+
logger.debug(f"Sending request to {event.method_name}")
|
|
214
|
+
else:
|
|
215
|
+
logger.debug(f"Sending request to {event.method_name} ({idempotency_key[:8]})")
|
|
216
|
+
|
|
217
|
+
grpclib.events.listen(channel, grpclib.events.SendRequest, send_request)
|
|
218
|
+
|
|
189
219
|
try:
|
|
190
220
|
await _connect_channel(channel)
|
|
191
221
|
except socket.gaierror as exc:
|
|
@@ -618,9 +648,28 @@ class TaskCommandRouterClient:
|
|
|
618
648
|
)
|
|
619
649
|
|
|
620
650
|
async def snapshot_directory(
|
|
621
|
-
self, request: sr_pb2.TaskSnapshotDirectoryRequest
|
|
651
|
+
self, request: sr_pb2.TaskSnapshotDirectoryRequest, **kwargs
|
|
622
652
|
) -> sr_pb2.TaskSnapshotDirectoryResponse:
|
|
623
653
|
with grpc_error_converter():
|
|
624
654
|
return await call_with_retries_on_transient_errors(
|
|
625
|
-
lambda: self._call_with_auth_retry(self._stub.TaskSnapshotDirectory, request)
|
|
655
|
+
lambda: self._call_with_auth_retry(self._stub.TaskSnapshotDirectory, request, **kwargs)
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
async def snapshot_filesystem(
|
|
659
|
+
self, request: sr_pb2.TaskSnapshotFilesystemRequest, *, timeout: Optional[int] = None, **kwargs
|
|
660
|
+
) -> sr_pb2.TaskSnapshotFilesystemResponse:
|
|
661
|
+
expect_timeouts = timeout is not None
|
|
662
|
+
with grpc_error_converter(expect_timeouts=expect_timeouts):
|
|
663
|
+
# note: TaskSnapshotFilesystem has a timeout concept with multiple variants:
|
|
664
|
+
# * Normally it would time out on the client side inside of grpclib - this causes an asyncio.TimeoutError
|
|
665
|
+
# * It also propagates the timeout to the server however, which could potentially trigger, in particular
|
|
666
|
+
# if an in-progress requests is idempotently rejoined as a retry. These errors appear to be propagated
|
|
667
|
+
# as Status.CANCELLED at the time of writing this, but we may change the backend to return
|
|
668
|
+
# DEADLINE_EXCEEDED in the future, so we want to make the code compatible with both here:
|
|
669
|
+
return await call_with_retries_on_transient_errors(
|
|
670
|
+
lambda: self._call_with_auth_retry(
|
|
671
|
+
self._stub.TaskSnapshotFilesystem, request, timeout=timeout, **kwargs
|
|
672
|
+
),
|
|
673
|
+
exclude_status_codes=[Status.DEADLINE_EXCEEDED, Status.CANCELLED],
|
|
674
|
+
expect_timeouts=expect_timeouts, # client wanted a timeout - raise as such immediately
|
|
626
675
|
)
|
|
@@ -106,7 +106,7 @@ async def list_(*, json: bool = False, env: Optional[str] = None):
|
|
|
106
106
|
break
|
|
107
107
|
finished = await retrieve_page(items[-1].metadata.creation_info.created_at)
|
|
108
108
|
|
|
109
|
-
queues = [_Queue._new_hydrated(item.queue_id, client, item.metadata,
|
|
109
|
+
queues = [_Queue._new_hydrated(item.queue_id, client, item.metadata, skip_reload=True) for item in items]
|
|
110
110
|
|
|
111
111
|
rows = []
|
|
112
112
|
for obj, resp_data in zip(queues, items):
|
|
@@ -55,7 +55,7 @@ async def list_(env: Optional[str] = None, json: bool = False):
|
|
|
55
55
|
break
|
|
56
56
|
finished = await retrieve_page(items[-1].metadata.creation_info.created_at)
|
|
57
57
|
|
|
58
|
-
secrets = [_Secret._new_hydrated(item.secret_id, client, item.metadata,
|
|
58
|
+
secrets = [_Secret._new_hydrated(item.secret_id, client, item.metadata, skip_reload=True) for item in items]
|
|
59
59
|
|
|
60
60
|
rows = []
|
|
61
61
|
for obj, resp_data in zip(secrets, items):
|
|
@@ -35,7 +35,7 @@ class _Client:
|
|
|
35
35
|
server_url: str,
|
|
36
36
|
client_type: int,
|
|
37
37
|
credentials: typing.Optional[tuple[str, str]],
|
|
38
|
-
version: str = "1.4.3.
|
|
38
|
+
version: str = "1.4.3.dev28",
|
|
39
39
|
):
|
|
40
40
|
"""mdmd:hidden
|
|
41
41
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -175,7 +175,7 @@ class Client:
|
|
|
175
175
|
server_url: str,
|
|
176
176
|
client_type: int,
|
|
177
177
|
credentials: typing.Optional[tuple[str, str]],
|
|
178
|
-
version: str = "1.4.3.
|
|
178
|
+
version: str = "1.4.3.dev28",
|
|
179
179
|
):
|
|
180
180
|
"""mdmd:hidden
|
|
181
181
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -519,7 +519,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
|
519
519
|
new_cls = _Cls._from_loader(
|
|
520
520
|
_load_from_base,
|
|
521
521
|
rep=f"{self._name}.{method_name}(...)",
|
|
522
|
-
|
|
522
|
+
skip_reload=True,
|
|
523
523
|
deps=lambda: [],
|
|
524
524
|
load_context_overrides=self._load_context_overrides,
|
|
525
525
|
hydrate_lazily=True,
|
|
@@ -680,7 +680,7 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
680
680
|
cls = cls._from_loader(
|
|
681
681
|
_load_remote,
|
|
682
682
|
rep,
|
|
683
|
-
|
|
683
|
+
skip_reload=True,
|
|
684
684
|
hydrate_lazily=True,
|
|
685
685
|
load_context_overrides=load_context_overrides,
|
|
686
686
|
)
|
|
@@ -198,7 +198,7 @@ class _DictManager:
|
|
|
198
198
|
item.dict_id,
|
|
199
199
|
client,
|
|
200
200
|
item.metadata,
|
|
201
|
-
|
|
201
|
+
skip_reload=True,
|
|
202
202
|
rep=_Dict._repr(item.name, environment_name),
|
|
203
203
|
)
|
|
204
204
|
for item in items
|
|
@@ -354,7 +354,7 @@ class _Dict(_Object, type_prefix="di"):
|
|
|
354
354
|
response.dict_id,
|
|
355
355
|
client,
|
|
356
356
|
response.metadata,
|
|
357
|
-
|
|
357
|
+
skip_reload=True,
|
|
358
358
|
rep="modal.Dict.ephemeral()",
|
|
359
359
|
)
|
|
360
360
|
|
|
@@ -393,7 +393,7 @@ class _Dict(_Object, type_prefix="di"):
|
|
|
393
393
|
return _Dict._from_loader(
|
|
394
394
|
_load,
|
|
395
395
|
rep,
|
|
396
|
-
|
|
396
|
+
skip_reload=True,
|
|
397
397
|
hydrate_lazily=True,
|
|
398
398
|
name=name,
|
|
399
399
|
load_context_overrides=LoadContext(environment_name=environment_name, client=client),
|
|
@@ -436,7 +436,7 @@ class _Dict(_Object, type_prefix="di"):
|
|
|
436
436
|
return _Dict._from_loader(
|
|
437
437
|
_load,
|
|
438
438
|
rep,
|
|
439
|
-
|
|
439
|
+
skip_reload=True,
|
|
440
440
|
hydrate_lazily=True,
|
|
441
441
|
load_context_overrides=LoadContext(client=client),
|
|
442
442
|
)
|
|
@@ -664,7 +664,7 @@ class _Mount(_Object, type_prefix="mo"):
|
|
|
664
664
|
return _Mount._from_loader(
|
|
665
665
|
_load,
|
|
666
666
|
"Mount()",
|
|
667
|
-
|
|
667
|
+
skip_reload=True,
|
|
668
668
|
hydrate_lazily=True,
|
|
669
669
|
load_context_overrides=LoadContext(environment_name=environment_name, client=client),
|
|
670
670
|
)
|
|
@@ -134,7 +134,7 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
|
|
|
134
134
|
return _NetworkFileSystem._from_loader(
|
|
135
135
|
_load,
|
|
136
136
|
"NetworkFileSystem()",
|
|
137
|
-
|
|
137
|
+
skip_reload=True,
|
|
138
138
|
hydrate_lazily=True,
|
|
139
139
|
load_context_overrides=LoadContext(environment_name=environment_name, client=client),
|
|
140
140
|
)
|
|
@@ -174,7 +174,7 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
|
|
|
174
174
|
response.shared_volume_id,
|
|
175
175
|
client,
|
|
176
176
|
None,
|
|
177
|
-
|
|
177
|
+
skip_reload=True,
|
|
178
178
|
rep="modal.NetworkFileSystem.ephemeral()",
|
|
179
179
|
)
|
|
180
180
|
|
|
@@ -24,7 +24,7 @@ class Object:
|
|
|
24
24
|
]
|
|
25
25
|
]
|
|
26
26
|
_rep: str
|
|
27
|
-
|
|
27
|
+
_skip_reload: bool
|
|
28
28
|
_hydrate_lazily: bool
|
|
29
29
|
_deps: typing.Optional[collections.abc.Callable[..., collections.abc.Sequence[Object]]]
|
|
30
30
|
_deduplication_key: typing.Optional[
|
|
@@ -60,7 +60,6 @@ class Object:
|
|
|
60
60
|
None,
|
|
61
61
|
]
|
|
62
62
|
] = None,
|
|
63
|
-
is_another_app: bool = False,
|
|
64
63
|
preload: typing.Optional[
|
|
65
64
|
collections.abc.Callable[
|
|
66
65
|
[
|
|
@@ -73,6 +72,7 @@ class Object:
|
|
|
73
72
|
]
|
|
74
73
|
] = None,
|
|
75
74
|
hydrate_lazily: bool = False,
|
|
75
|
+
skip_reload: bool = False,
|
|
76
76
|
deps: typing.Optional[collections.abc.Callable[..., collections.abc.Sequence[Object]]] = None,
|
|
77
77
|
deduplication_key: typing.Optional[collections.abc.Callable[[], collections.abc.Hashable]] = None,
|
|
78
78
|
name: typing.Optional[str] = None,
|
|
@@ -94,7 +94,6 @@ class Object:
|
|
|
94
94
|
collections.abc.Awaitable[None],
|
|
95
95
|
]
|
|
96
96
|
] = None,
|
|
97
|
-
is_another_app: bool = False,
|
|
98
97
|
preload: typing.Optional[
|
|
99
98
|
collections.abc.Callable[
|
|
100
99
|
[
|
|
@@ -107,6 +106,7 @@ class Object:
|
|
|
107
106
|
]
|
|
108
107
|
] = None,
|
|
109
108
|
hydrate_lazily: bool = False,
|
|
109
|
+
skip_reload: bool = False,
|
|
110
110
|
deps: typing.Optional[collections.abc.Callable[..., collections.abc.Sequence[Object]]] = None,
|
|
111
111
|
deduplication_key: typing.Optional[
|
|
112
112
|
collections.abc.Callable[[], collections.abc.Awaitable[collections.abc.Hashable]]
|
|
@@ -148,7 +148,7 @@ class Object:
|
|
|
148
148
|
None,
|
|
149
149
|
],
|
|
150
150
|
rep: str,
|
|
151
|
-
|
|
151
|
+
skip_reload: bool = False,
|
|
152
152
|
preload: typing.Optional[
|
|
153
153
|
collections.abc.Callable[
|
|
154
154
|
[
|
|
@@ -180,7 +180,7 @@ class Object:
|
|
|
180
180
|
collections.abc.Awaitable[None],
|
|
181
181
|
],
|
|
182
182
|
rep: str,
|
|
183
|
-
|
|
183
|
+
skip_reload: bool = False,
|
|
184
184
|
preload: typing.Optional[
|
|
185
185
|
collections.abc.Callable[
|
|
186
186
|
[
|
|
@@ -216,7 +216,7 @@ class Object:
|
|
|
216
216
|
object_id: str,
|
|
217
217
|
client: modal.client.Client,
|
|
218
218
|
handle_metadata: typing.Optional[google.protobuf.message.Message],
|
|
219
|
-
|
|
219
|
+
skip_reload: bool = False,
|
|
220
220
|
rep: typing.Optional[str] = None,
|
|
221
221
|
) -> typing_extensions.Self: ...
|
|
222
222
|
def _hydrate_from_other(self, other: typing_extensions.Self): ...
|
|
@@ -163,7 +163,7 @@ class _QueueManager:
|
|
|
163
163
|
item.queue_id,
|
|
164
164
|
client,
|
|
165
165
|
item.metadata,
|
|
166
|
-
|
|
166
|
+
skip_reload=True,
|
|
167
167
|
rep=_Queue._repr(item.name, environment_name),
|
|
168
168
|
)
|
|
169
169
|
for item in items
|
|
@@ -353,7 +353,7 @@ class _Queue(_Object, type_prefix="qu"):
|
|
|
353
353
|
async with TaskContext() as tc:
|
|
354
354
|
request = api_pb2.QueueHeartbeatRequest(queue_id=response.queue_id)
|
|
355
355
|
tc.infinite_loop(lambda: client.stub.QueueHeartbeat(request), sleep=_heartbeat_sleep)
|
|
356
|
-
yield cls._new_hydrated(response.queue_id, client, response.metadata,
|
|
356
|
+
yield cls._new_hydrated(response.queue_id, client, response.metadata, skip_reload=True)
|
|
357
357
|
|
|
358
358
|
@staticmethod
|
|
359
359
|
def from_name(
|
|
@@ -389,7 +389,7 @@ class _Queue(_Object, type_prefix="qu"):
|
|
|
389
389
|
return _Queue._from_loader(
|
|
390
390
|
_load,
|
|
391
391
|
rep,
|
|
392
|
-
|
|
392
|
+
skip_reload=True,
|
|
393
393
|
hydrate_lazily=True,
|
|
394
394
|
name=name,
|
|
395
395
|
load_context_overrides=LoadContext(environment_name=environment_name, client=client),
|
|
@@ -432,7 +432,7 @@ class _Queue(_Object, type_prefix="qu"):
|
|
|
432
432
|
return _Queue._from_loader(
|
|
433
433
|
_load,
|
|
434
434
|
rep,
|
|
435
|
-
|
|
435
|
+
skip_reload=True,
|
|
436
436
|
hydrate_lazily=True,
|
|
437
437
|
load_context_overrides=LoadContext(client=client),
|
|
438
438
|
)
|
|
@@ -74,7 +74,7 @@ _default_image: _Image = _Image.debian_slim()
|
|
|
74
74
|
# We need some bytes of overhead for the rest of the command line besides the args,
|
|
75
75
|
# e.g. 'runsc exec ...'. So we use 2**16 as the limit.
|
|
76
76
|
ARG_MAX_BYTES = 2**16
|
|
77
|
-
|
|
77
|
+
TTL_NO_EXPIRY_SENTINEL = -1
|
|
78
78
|
|
|
79
79
|
if TYPE_CHECKING:
|
|
80
80
|
import modal.app
|
|
@@ -955,9 +955,29 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
955
955
|
|
|
956
956
|
Returns an [`Image`](https://modal.com/docs/reference/modal.Image) object which
|
|
957
957
|
can be used to spawn a new Sandbox with the same filesystem.
|
|
958
|
+
|
|
959
|
+
`timeout` If the snapshot does not return within that window, the call is cancelled
|
|
960
|
+
and `modal.exception.TimeoutError` is raised.
|
|
958
961
|
"""
|
|
962
|
+
if os.environ.get("MODAL_USE_LEGACY_FILESYSTEM_SNAPSHOT") == "1":
|
|
963
|
+
return await self._legacy_snapshot_filesystem(timeout)
|
|
964
|
+
|
|
959
965
|
self._ensure_v1("snapshot_filesystem")
|
|
960
|
-
|
|
966
|
+
|
|
967
|
+
task_id = await self._get_task_id()
|
|
968
|
+
command_router_client = await self._get_command_router_client(task_id)
|
|
969
|
+
|
|
970
|
+
req = sr_pb2.TaskSnapshotFilesystemRequest(
|
|
971
|
+
task_id=task_id,
|
|
972
|
+
snapshot_id=str(uuid.uuid4()),
|
|
973
|
+
ttl_seconds=TTL_NO_EXPIRY_SENTINEL,
|
|
974
|
+
)
|
|
975
|
+
res = await command_router_client.snapshot_filesystem(req, timeout=float(timeout))
|
|
976
|
+
return _Image._new_hydrated(res.image_id, self._client, None)
|
|
977
|
+
|
|
978
|
+
async def _legacy_snapshot_filesystem(self, timeout: int = 55) -> _Image:
|
|
979
|
+
self._ensure_v1("snapshot_filesystem")
|
|
980
|
+
await self._get_task_id()
|
|
961
981
|
req = api_pb2.SandboxSnapshotFsRequest(sandbox_id=self.object_id, timeout=timeout)
|
|
962
982
|
resp = await self._client.stub.SandboxSnapshotFs(req)
|
|
963
983
|
|
|
@@ -1048,7 +1068,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1048
1068
|
async def snapshot_directory(self, path: Union[PurePosixPath, str]) -> _Image:
|
|
1049
1069
|
"""Snapshot a directory in a running Sandbox, creating a new Image with its content.
|
|
1050
1070
|
|
|
1051
|
-
Directory snapshots are currently persisted for 30 days after they were
|
|
1071
|
+
Directory snapshots are currently persisted for 30 days after they were created.
|
|
1052
1072
|
|
|
1053
1073
|
Usage:
|
|
1054
1074
|
```py notest
|
|
@@ -1071,7 +1091,12 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1071
1091
|
path_bytes = posix_path.as_posix().encode("utf8")
|
|
1072
1092
|
|
|
1073
1093
|
snapshot_id = str(uuid.uuid4())
|
|
1074
|
-
req = sr_pb2.TaskSnapshotDirectoryRequest(
|
|
1094
|
+
req = sr_pb2.TaskSnapshotDirectoryRequest(
|
|
1095
|
+
task_id=task_id,
|
|
1096
|
+
path=path_bytes,
|
|
1097
|
+
snapshot_id=snapshot_id,
|
|
1098
|
+
ttl_seconds=None, # pretending to be old client for now - set server decide on retention
|
|
1099
|
+
)
|
|
1075
1100
|
res = await command_router_client.snapshot_directory(req)
|
|
1076
1101
|
return _Image._new_hydrated(res.image_id, self._client, None)
|
|
1077
1102
|
|
|
@@ -386,9 +386,13 @@ class _Sandbox(modal._object._Object):
|
|
|
386
386
|
|
|
387
387
|
Returns an [`Image`](https://modal.com/docs/reference/modal.Image) object which
|
|
388
388
|
can be used to spawn a new Sandbox with the same filesystem.
|
|
389
|
+
|
|
390
|
+
`timeout` If the snapshot does not return within that window, the call is cancelled
|
|
391
|
+
and `modal.exception.TimeoutError` is raised.
|
|
389
392
|
"""
|
|
390
393
|
...
|
|
391
394
|
|
|
395
|
+
async def _legacy_snapshot_filesystem(self, timeout: int = 55) -> modal.image._Image: ...
|
|
392
396
|
async def mount_image(self, path: typing.Union[pathlib.PurePosixPath, str], image: modal.image._Image):
|
|
393
397
|
"""Mount an Image at a specified path in a running Sandbox.
|
|
394
398
|
|
|
@@ -426,7 +430,7 @@ class _Sandbox(modal._object._Object):
|
|
|
426
430
|
async def snapshot_directory(self, path: typing.Union[pathlib.PurePosixPath, str]) -> modal.image._Image:
|
|
427
431
|
"""Snapshot a directory in a running Sandbox, creating a new Image with its content.
|
|
428
432
|
|
|
429
|
-
Directory snapshots are currently persisted for 30 days after they were
|
|
433
|
+
Directory snapshots are currently persisted for 30 days after they were created.
|
|
430
434
|
|
|
431
435
|
Usage:
|
|
432
436
|
```py notest
|
|
@@ -1358,6 +1362,9 @@ class Sandbox(modal.object.Object):
|
|
|
1358
1362
|
|
|
1359
1363
|
Returns an [`Image`](https://modal.com/docs/reference/modal.Image) object which
|
|
1360
1364
|
can be used to spawn a new Sandbox with the same filesystem.
|
|
1365
|
+
|
|
1366
|
+
`timeout` If the snapshot does not return within that window, the call is cancelled
|
|
1367
|
+
and `modal.exception.TimeoutError` is raised.
|
|
1361
1368
|
"""
|
|
1362
1369
|
...
|
|
1363
1370
|
|
|
@@ -1366,11 +1373,20 @@ class Sandbox(modal.object.Object):
|
|
|
1366
1373
|
|
|
1367
1374
|
Returns an [`Image`](https://modal.com/docs/reference/modal.Image) object which
|
|
1368
1375
|
can be used to spawn a new Sandbox with the same filesystem.
|
|
1376
|
+
|
|
1377
|
+
`timeout` If the snapshot does not return within that window, the call is cancelled
|
|
1378
|
+
and `modal.exception.TimeoutError` is raised.
|
|
1369
1379
|
"""
|
|
1370
1380
|
...
|
|
1371
1381
|
|
|
1372
1382
|
snapshot_filesystem: __snapshot_filesystem_spec
|
|
1373
1383
|
|
|
1384
|
+
class ___legacy_snapshot_filesystem_spec(typing_extensions.Protocol):
|
|
1385
|
+
def __call__(self, /, timeout: int = 55) -> modal.image.Image: ...
|
|
1386
|
+
async def aio(self, /, timeout: int = 55) -> modal.image.Image: ...
|
|
1387
|
+
|
|
1388
|
+
_legacy_snapshot_filesystem: ___legacy_snapshot_filesystem_spec
|
|
1389
|
+
|
|
1374
1390
|
class __mount_image_spec(typing_extensions.Protocol):
|
|
1375
1391
|
def __call__(self, /, path: typing.Union[pathlib.PurePosixPath, str], image: modal.image.Image):
|
|
1376
1392
|
"""Mount an Image at a specified path in a running Sandbox.
|
|
@@ -1449,7 +1465,7 @@ class Sandbox(modal.object.Object):
|
|
|
1449
1465
|
def __call__(self, /, path: typing.Union[pathlib.PurePosixPath, str]) -> modal.image.Image:
|
|
1450
1466
|
"""Snapshot a directory in a running Sandbox, creating a new Image with its content.
|
|
1451
1467
|
|
|
1452
|
-
Directory snapshots are currently persisted for 30 days after they were
|
|
1468
|
+
Directory snapshots are currently persisted for 30 days after they were created.
|
|
1453
1469
|
|
|
1454
1470
|
Usage:
|
|
1455
1471
|
```py notest
|
|
@@ -1466,7 +1482,7 @@ class Sandbox(modal.object.Object):
|
|
|
1466
1482
|
async def aio(self, /, path: typing.Union[pathlib.PurePosixPath, str]) -> modal.image.Image:
|
|
1467
1483
|
"""Snapshot a directory in a running Sandbox, creating a new Image with its content.
|
|
1468
1484
|
|
|
1469
|
-
Directory snapshots are currently persisted for 30 days after they were
|
|
1485
|
+
Directory snapshots are currently persisted for 30 days after they were created.
|
|
1470
1486
|
|
|
1471
1487
|
Usage:
|
|
1472
1488
|
```py notest
|