modAL 1.2.7.dev7__tar.gz → 1.2.7.dev10__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.2.7.dev7 → modal-1.2.7.dev10}/PKG-INFO +1 -1
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_container_entrypoint.py +0 -1
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/blob_utils.py +5 -4
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/grpc_utils.py +0 -2
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/package_utils.py +0 -1
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/task_command_router_client.py +14 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/client.pyi +2 -2
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/file_io.py +22 -10
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/image.py +1 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/io_streams.py +10 -7
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/sandbox.py +44 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/sandbox.pyi +39 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_version/__init__.py +1 -1
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/LICENSE +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/README.md +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/__init__.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/__main__.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_billing.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_clustered_functions.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_clustered_functions.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_functions.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_grpc_client.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_ipython.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_load_context.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_location.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_object.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_output.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_partial_function.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_pty.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_resolver.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_resources.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_runtime/__init__.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_runtime/asgi.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_runtime/execution_context.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_runtime/telemetry.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_serialization.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_traceback.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_tunnel.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_tunnel.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_type_manager.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/__init__.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/app_utils.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/async_utils.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/deprecation.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/docker_utils.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/function_utils.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/git_utils.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/hash_utils.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/http_utils.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/logger.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/mount_utils.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/name_utils.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/shell_utils.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_utils/time_utils.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_vendor/__init__.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_vendor/tblib.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/_watcher.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/app.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/app.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/billing.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/builder/2023.12.txt +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/builder/2024.04.txt +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/builder/2024.10.txt +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/builder/2025.06.txt +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/builder/README.md +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/builder/base-images.json +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/call_graph.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/__init__.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/_download.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/_traceback.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/app.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/cluster.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/config.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/container.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/dict.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/entry_point.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/environment.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/import_refs.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/launch.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/network_file_system.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/profile.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/programs/__init__.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/programs/vscode.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/queues.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/run.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/secret.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/shell.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/token.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/utils.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cli/volume.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/client.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cls.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/cls.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/config.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/container_process.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/container_process.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/dict.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/dict.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/environments.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/environments.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/exception.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/experimental/__init__.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/experimental/flash.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/experimental/flash.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/experimental/ipython.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/file_io.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/file_pattern_matcher.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/functions.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/functions.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/gpu.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/image.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/io_streams.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/mount.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/mount.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/network_file_system.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/network_file_system.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/object.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/object.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/output.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/parallel_map.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/parallel_map.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/partial_function.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/partial_function.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/proxy.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/proxy.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/py.typed +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/queue.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/queue.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/retries.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/runner.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/runner.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/running_app.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/schedule.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/scheduler_placement.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/secret.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/secret.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/serving.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/serving.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/snapshot.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/snapshot.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/stream_type.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/token_flow.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/token_flow.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/volume.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal/volume.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal.egg-info/requires.txt +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal.egg-info/top_level.txt +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_docs/__init__.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_proto/__init__.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_proto/api.proto +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_proto/api_grpc.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_proto/api_pb2.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_proto/api_pb2.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_proto/py.typed +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_proto/task_command_router.proto +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/modal_version/__main__.py +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/pyproject.toml +0 -0
- {modal-1.2.7.dev7 → modal-1.2.7.dev10}/setup.cfg +0 -0
|
@@ -152,12 +152,13 @@ async def perform_multipart_upload(
|
|
|
152
152
|
part_etags = await TaskContext.gather(*upload_coros)
|
|
153
153
|
|
|
154
154
|
# The body of the complete_multipart_upload command needs some data in xml format:
|
|
155
|
-
|
|
155
|
+
completion_parts = ["<CompleteMultipartUpload>"]
|
|
156
156
|
for part_number, etag in enumerate(part_etags, 1):
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
completion_parts.append(f"""<Part>\n<PartNumber>{part_number}</PartNumber>\n<ETag>"{etag}"</ETag>\n</Part>""")
|
|
158
|
+
completion_parts.append("</CompleteMultipartUpload>")
|
|
159
|
+
completion_body = "\n".join(completion_parts)
|
|
159
160
|
|
|
160
|
-
# etag of combined object should be md5 hex of
|
|
161
|
+
# etag of combined object should be md5 hex of concatenated md5 *bytes* from parts + `-{num_parts}`
|
|
161
162
|
bin_hash_parts = [bytes.fromhex(etag) for etag in part_etags]
|
|
162
163
|
|
|
163
164
|
expected_multipart_etag = hashlib.md5(b"".join(bin_hash_parts)).hexdigest() + f"-{len(part_etags)}"
|
|
@@ -16,8 +16,6 @@ from typing import Any, Optional, Sequence, TypeVar
|
|
|
16
16
|
import grpclib.client
|
|
17
17
|
import grpclib.config
|
|
18
18
|
import grpclib.events
|
|
19
|
-
import grpclib.protocol
|
|
20
|
-
import grpclib.stream
|
|
21
19
|
from google.protobuf.message import Message
|
|
22
20
|
from google.protobuf.symbol_database import SymbolDatabase
|
|
23
21
|
from grpclib import GRPCError, Status
|
|
@@ -521,3 +521,17 @@ class TaskCommandRouterClient:
|
|
|
521
521
|
await sleep_and_update_delay_and_num_retries_remaining(e)
|
|
522
522
|
else:
|
|
523
523
|
raise ConnectionError(str(e))
|
|
524
|
+
|
|
525
|
+
async def mount_image(self, request: sr_pb2.TaskMountDirectoryRequest):
|
|
526
|
+
with grpc_error_converter():
|
|
527
|
+
return await call_with_retries_on_transient_errors(
|
|
528
|
+
lambda: self._call_with_auth_retry(self._stub.TaskMountDirectory, request)
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
async def snapshot_directory(
|
|
532
|
+
self, request: sr_pb2.TaskSnapshotDirectoryRequest
|
|
533
|
+
) -> sr_pb2.TaskSnapshotDirectoryResponse:
|
|
534
|
+
with grpc_error_converter():
|
|
535
|
+
return await call_with_retries_on_transient_errors(
|
|
536
|
+
lambda: self._call_with_auth_retry(self._stub.TaskSnapshotDirectory, request)
|
|
537
|
+
)
|
|
@@ -32,7 +32,7 @@ class _Client:
|
|
|
32
32
|
server_url: str,
|
|
33
33
|
client_type: int,
|
|
34
34
|
credentials: typing.Optional[tuple[str, str]],
|
|
35
|
-
version: str = "1.2.7.
|
|
35
|
+
version: str = "1.2.7.dev10",
|
|
36
36
|
):
|
|
37
37
|
"""mdmd:hidden
|
|
38
38
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -163,7 +163,7 @@ class Client:
|
|
|
163
163
|
server_url: str,
|
|
164
164
|
client_type: int,
|
|
165
165
|
credentials: typing.Optional[tuple[str, str]],
|
|
166
|
-
version: str = "1.2.7.
|
|
166
|
+
version: str = "1.2.7.dev10",
|
|
167
167
|
):
|
|
168
168
|
"""mdmd:hidden
|
|
169
169
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -190,7 +190,7 @@ class _FileIO(Generic[T]):
|
|
|
190
190
|
|
|
191
191
|
async def _wait(self, exec_id: str) -> bytes:
|
|
192
192
|
# The logic here is similar to how output is read from `exec`
|
|
193
|
-
|
|
193
|
+
output_buffer = io.BytesIO()
|
|
194
194
|
completed = False
|
|
195
195
|
retries_remaining = 10
|
|
196
196
|
while not completed:
|
|
@@ -201,7 +201,7 @@ class _FileIO(Generic[T]):
|
|
|
201
201
|
break
|
|
202
202
|
if isinstance(data, Exception):
|
|
203
203
|
raise data
|
|
204
|
-
|
|
204
|
+
output_buffer.write(data)
|
|
205
205
|
except (GRPCError, StreamTerminatedError) as exc:
|
|
206
206
|
if retries_remaining > 0:
|
|
207
207
|
retries_remaining -= 1
|
|
@@ -212,7 +212,7 @@ class _FileIO(Generic[T]):
|
|
|
212
212
|
elif isinstance(exc, StreamTerminatedError):
|
|
213
213
|
continue
|
|
214
214
|
raise
|
|
215
|
-
return
|
|
215
|
+
return output_buffer.getvalue()
|
|
216
216
|
|
|
217
217
|
def _validate_type(self, data: Union[bytes, str]) -> None:
|
|
218
218
|
if self._binary and isinstance(data, str):
|
|
@@ -421,10 +421,22 @@ class _FileIO(Generic[T]):
|
|
|
421
421
|
task_id=self._task_id,
|
|
422
422
|
),
|
|
423
423
|
)
|
|
424
|
+
|
|
425
|
+
def end_of_event(item: bytes, buffer: io.BytesIO, boundary_token: bytes) -> bool:
|
|
426
|
+
if not item.endswith(b"\n"):
|
|
427
|
+
return False
|
|
428
|
+
boundary_token_size = len(boundary_token)
|
|
429
|
+
if buffer.tell() < boundary_token_size:
|
|
430
|
+
return False
|
|
431
|
+
buffer.seek(-boundary_token_size, io.SEEK_END)
|
|
432
|
+
if buffer.read(boundary_token_size) == boundary_token:
|
|
433
|
+
return True
|
|
434
|
+
return False
|
|
435
|
+
|
|
424
436
|
async with TaskContext() as tc:
|
|
425
437
|
tc.create_task(self._consume_watch_output(resp.exec_id))
|
|
426
438
|
|
|
427
|
-
|
|
439
|
+
item_buffer = io.BytesIO()
|
|
428
440
|
while True:
|
|
429
441
|
if len(self._watch_output_buffer) > 0:
|
|
430
442
|
item = self._watch_output_buffer.pop(0)
|
|
@@ -432,12 +444,12 @@ class _FileIO(Generic[T]):
|
|
|
432
444
|
break
|
|
433
445
|
if isinstance(item, Exception):
|
|
434
446
|
raise item
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
#
|
|
438
|
-
if
|
|
447
|
+
item_buffer.write(item)
|
|
448
|
+
assert isinstance(item, bytes)
|
|
449
|
+
# Single events may span multiple messages so we need to check for a special event boundary token
|
|
450
|
+
if end_of_event(item, item_buffer, boundary_token=b"\n\n"):
|
|
439
451
|
try:
|
|
440
|
-
event_json = json.loads(
|
|
452
|
+
event_json = json.loads(item_buffer.getvalue().strip().decode())
|
|
441
453
|
event = FileWatchEvent(
|
|
442
454
|
type=FileWatchEventType(event_json["event_type"]),
|
|
443
455
|
paths=event_json["paths"],
|
|
@@ -447,7 +459,7 @@ class _FileIO(Generic[T]):
|
|
|
447
459
|
except (json.JSONDecodeError, KeyError, ValueError):
|
|
448
460
|
# skip invalid events
|
|
449
461
|
pass
|
|
450
|
-
|
|
462
|
+
item_buffer = io.BytesIO()
|
|
451
463
|
else:
|
|
452
464
|
await asyncio.sleep(0.1)
|
|
453
465
|
|
|
@@ -23,7 +23,7 @@ from grpclib.exceptions import GRPCError, StreamTerminatedError
|
|
|
23
23
|
from modal.exception import ClientClosed, ExecTimeoutError, InvalidError
|
|
24
24
|
from modal_proto import api_pb2
|
|
25
25
|
|
|
26
|
-
from ._utils.async_utils import aclosing, synchronize_api
|
|
26
|
+
from ._utils.async_utils import aclosing, synchronize_api, synchronizer
|
|
27
27
|
from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES
|
|
28
28
|
from ._utils.task_command_router_client import TaskCommandRouterClient
|
|
29
29
|
from .client import _Client
|
|
@@ -153,17 +153,17 @@ class _StreamReaderThroughServer(Generic[T]):
|
|
|
153
153
|
"""Fetch the entire contents of the stream until EOF."""
|
|
154
154
|
logger.debug(f"{self._object_id} StreamReader fd={self._file_descriptor} read starting")
|
|
155
155
|
if self._text:
|
|
156
|
-
|
|
156
|
+
buffer = io.StringIO()
|
|
157
157
|
async for message in _decode_bytes_stream_to_str(self._get_logs()):
|
|
158
|
-
|
|
158
|
+
buffer.write(message)
|
|
159
159
|
logger.debug(f"{self._object_id} StreamReader fd={self._file_descriptor} read completed after EOF")
|
|
160
|
-
return cast(T,
|
|
160
|
+
return cast(T, buffer.getvalue())
|
|
161
161
|
else:
|
|
162
|
-
|
|
162
|
+
buffer = io.BytesIO()
|
|
163
163
|
async for message in self._get_logs():
|
|
164
|
-
|
|
164
|
+
buffer.write(message)
|
|
165
165
|
logger.debug(f"{self._object_id} StreamReader fd={self._file_descriptor} read completed after EOF")
|
|
166
|
-
return cast(T,
|
|
166
|
+
return cast(T, buffer.getvalue())
|
|
167
167
|
|
|
168
168
|
async def _consume_container_process_stream(self):
|
|
169
169
|
"""Consume the container process stream and store messages in the buffer."""
|
|
@@ -566,6 +566,9 @@ class _StreamReader(Generic[T]):
|
|
|
566
566
|
task_id: Optional[str] = None,
|
|
567
567
|
) -> None:
|
|
568
568
|
"""mdmd:hidden"""
|
|
569
|
+
# we can remove this once we ensure no constructors use async code
|
|
570
|
+
assert asyncio.get_running_loop() == synchronizer._get_loop(start=False)
|
|
571
|
+
|
|
569
572
|
if by_line and not text:
|
|
570
573
|
raise ValueError("line-buffering is only supported when text=True")
|
|
571
574
|
|
|
@@ -6,6 +6,7 @@ import time
|
|
|
6
6
|
import uuid
|
|
7
7
|
from collections.abc import AsyncGenerator, Collection, Sequence
|
|
8
8
|
from dataclasses import dataclass
|
|
9
|
+
from pathlib import PurePosixPath
|
|
9
10
|
from typing import TYPE_CHECKING, Any, AsyncIterator, Literal, Optional, Union, overload
|
|
10
11
|
|
|
11
12
|
from ._pty import get_pty_info
|
|
@@ -600,6 +601,49 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
600
601
|
|
|
601
602
|
return image
|
|
602
603
|
|
|
604
|
+
async def _experimental_mount_image(self, path: Union[PurePosixPath, str], image: Optional[_Image]):
|
|
605
|
+
"""Mount an Image at a path in the Sandbox filesystem."""
|
|
606
|
+
|
|
607
|
+
image_id = None
|
|
608
|
+
|
|
609
|
+
if image:
|
|
610
|
+
if not image._object_id:
|
|
611
|
+
# FIXME
|
|
612
|
+
raise InvalidError("Image has not been built.")
|
|
613
|
+
image_id = image._object_id
|
|
614
|
+
else:
|
|
615
|
+
image_id = "" # empty string indicates mount an empty dir
|
|
616
|
+
|
|
617
|
+
task_id = await self._get_task_id()
|
|
618
|
+
if (command_router_client := await self._get_command_router_client(task_id)) is None:
|
|
619
|
+
raise InvalidError("Mounting directories requires direct Sandbox control - please contact Modal support.")
|
|
620
|
+
|
|
621
|
+
posix_path = PurePosixPath(path)
|
|
622
|
+
if not posix_path.is_absolute():
|
|
623
|
+
raise InvalidError(f"Mount path must be absolute; got: {posix_path}")
|
|
624
|
+
path_bytes = posix_path.as_posix().encode("utf8")
|
|
625
|
+
|
|
626
|
+
req = sr_pb2.TaskMountDirectoryRequest(task_id=task_id, path=path_bytes, image_id=image_id)
|
|
627
|
+
await command_router_client.mount_image(req)
|
|
628
|
+
|
|
629
|
+
async def _experimental_snapshot_directory(self, path: Union[PurePosixPath, str]) -> _Image:
|
|
630
|
+
"""Snapshot local changes to a previously mounted Image, creating a new Image."""
|
|
631
|
+
|
|
632
|
+
task_id = await self._get_task_id()
|
|
633
|
+
if (command_router_client := await self._get_command_router_client(task_id)) is None:
|
|
634
|
+
raise InvalidError(
|
|
635
|
+
"Snapshotting directories requires direct Sandbox control - please contact Modal support."
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
posix_path = PurePosixPath(path)
|
|
639
|
+
if not posix_path.is_absolute():
|
|
640
|
+
raise InvalidError(f"Snapshot path must be absolute; got: {posix_path}")
|
|
641
|
+
path_bytes = posix_path.as_posix().encode("utf8")
|
|
642
|
+
|
|
643
|
+
req = sr_pb2.TaskSnapshotDirectoryRequest(task_id=task_id, path=path_bytes)
|
|
644
|
+
res = await command_router_client.snapshot_directory(req)
|
|
645
|
+
return _Image._new_hydrated(res.image_id, self._client, None)
|
|
646
|
+
|
|
603
647
|
# Live handle methods
|
|
604
648
|
|
|
605
649
|
async def wait(self, raise_on_termination: bool = True):
|
|
@@ -22,6 +22,7 @@ import modal.stream_type
|
|
|
22
22
|
import modal.volume
|
|
23
23
|
import modal_proto.api_pb2
|
|
24
24
|
import os
|
|
25
|
+
import pathlib
|
|
25
26
|
import typing
|
|
26
27
|
import typing_extensions
|
|
27
28
|
|
|
@@ -254,6 +255,18 @@ class _Sandbox(modal._object._Object):
|
|
|
254
255
|
"""
|
|
255
256
|
...
|
|
256
257
|
|
|
258
|
+
async def _experimental_mount_image(
|
|
259
|
+
self, path: typing.Union[pathlib.PurePosixPath, str], image: typing.Optional[modal.image._Image]
|
|
260
|
+
):
|
|
261
|
+
"""Mount an Image at a path in the Sandbox filesystem."""
|
|
262
|
+
...
|
|
263
|
+
|
|
264
|
+
async def _experimental_snapshot_directory(
|
|
265
|
+
self, path: typing.Union[pathlib.PurePosixPath, str]
|
|
266
|
+
) -> modal.image._Image:
|
|
267
|
+
"""Snapshot local changes to a previously mounted Image, creating a new Image."""
|
|
268
|
+
...
|
|
269
|
+
|
|
257
270
|
async def wait(self, raise_on_termination: bool = True):
|
|
258
271
|
"""Wait for the Sandbox to finish running."""
|
|
259
272
|
...
|
|
@@ -820,6 +833,32 @@ class Sandbox(modal.object.Object):
|
|
|
820
833
|
|
|
821
834
|
snapshot_filesystem: __snapshot_filesystem_spec[typing_extensions.Self]
|
|
822
835
|
|
|
836
|
+
class ___experimental_mount_image_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
837
|
+
def __call__(
|
|
838
|
+
self, /, path: typing.Union[pathlib.PurePosixPath, str], image: typing.Optional[modal.image.Image]
|
|
839
|
+
):
|
|
840
|
+
"""Mount an Image at a path in the Sandbox filesystem."""
|
|
841
|
+
...
|
|
842
|
+
|
|
843
|
+
async def aio(
|
|
844
|
+
self, /, path: typing.Union[pathlib.PurePosixPath, str], image: typing.Optional[modal.image.Image]
|
|
845
|
+
):
|
|
846
|
+
"""Mount an Image at a path in the Sandbox filesystem."""
|
|
847
|
+
...
|
|
848
|
+
|
|
849
|
+
_experimental_mount_image: ___experimental_mount_image_spec[typing_extensions.Self]
|
|
850
|
+
|
|
851
|
+
class ___experimental_snapshot_directory_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
852
|
+
def __call__(self, /, path: typing.Union[pathlib.PurePosixPath, str]) -> modal.image.Image:
|
|
853
|
+
"""Snapshot local changes to a previously mounted Image, creating a new Image."""
|
|
854
|
+
...
|
|
855
|
+
|
|
856
|
+
async def aio(self, /, path: typing.Union[pathlib.PurePosixPath, str]) -> modal.image.Image:
|
|
857
|
+
"""Snapshot local changes to a previously mounted Image, creating a new Image."""
|
|
858
|
+
...
|
|
859
|
+
|
|
860
|
+
_experimental_snapshot_directory: ___experimental_snapshot_directory_spec[typing_extensions.Self]
|
|
861
|
+
|
|
823
862
|
class __wait_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
824
863
|
def __call__(self, /, raise_on_termination: bool = True):
|
|
825
864
|
"""Wait for the Sandbox to finish running."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|