modal 1.2.5.dev26__tar.gz → 1.2.5.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.2.5.dev26 → modal-1.2.5.dev28}/PKG-INFO +1 -1
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/blob_utils.py +18 -5
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/task_command_router_client.py +38 -67
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/client.pyi +2 -2
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/mount.py +14 -8
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/volume.py +5 -1
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_version/__init__.py +1 -1
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/LICENSE +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/README.md +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/__init__.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/__main__.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_billing.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_clustered_functions.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_clustered_functions.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_container_entrypoint.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_functions.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_grpc_client.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_ipython.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_load_context.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_location.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_object.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_output.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_partial_function.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_pty.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_resolver.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_resources.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/__init__.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/asgi.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/execution_context.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/telemetry.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_serialization.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_traceback.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_tunnel.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_tunnel.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_type_manager.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/__init__.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/app_utils.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/async_utils.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/deprecation.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/docker_utils.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/function_utils.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/git_utils.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/hash_utils.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/http_utils.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/logger.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/mount_utils.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/name_utils.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/package_utils.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/shell_utils.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/time_utils.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_vendor/__init__.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_vendor/tblib.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_watcher.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/app.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/app.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/billing.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/builder/2023.12.txt +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/builder/2024.04.txt +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/builder/2024.10.txt +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/builder/2025.06.txt +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/builder/README.md +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/builder/base-images.json +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/call_graph.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/__init__.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/_download.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/_traceback.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/app.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/cluster.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/config.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/container.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/dict.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/entry_point.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/environment.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/import_refs.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/launch.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/network_file_system.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/profile.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/programs/__init__.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/programs/launch_instance_ssh.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/programs/run_marimo.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/programs/vscode.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/queues.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/run.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/secret.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/shell.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/token.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/utils.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/volume.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/client.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cls.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cls.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/config.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/container_process.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/container_process.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/dict.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/dict.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/environments.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/environments.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/exception.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/experimental/__init__.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/experimental/flash.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/experimental/flash.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/experimental/ipython.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/file_io.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/file_io.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/file_pattern_matcher.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/functions.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/functions.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/gpu.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/image.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/image.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/io_streams.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/io_streams.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/mount.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/network_file_system.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/network_file_system.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/object.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/object.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/output.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/parallel_map.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/parallel_map.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/partial_function.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/partial_function.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/proxy.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/proxy.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/py.typed +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/queue.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/queue.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/retries.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/runner.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/runner.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/running_app.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/sandbox.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/sandbox.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/schedule.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/scheduler_placement.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/secret.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/secret.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/serving.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/serving.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/snapshot.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/snapshot.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/stream_type.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/token_flow.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/token_flow.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/volume.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal.egg-info/requires.txt +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal.egg-info/top_level.txt +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_docs/__init__.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/__init__.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/api.proto +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/api_grpc.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/api_pb2.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/api_pb2.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/py.typed +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/task_command_router.proto +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_version/__main__.py +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/pyproject.toml +0 -0
- {modal-1.2.5.dev26 → modal-1.2.5.dev28}/setup.cfg +0 -0
|
@@ -371,11 +371,17 @@ class FileUploadSpec:
|
|
|
371
371
|
mount_filename: str
|
|
372
372
|
|
|
373
373
|
use_blob: bool
|
|
374
|
-
content: Optional[bytes] # typically None if using blob, required otherwise
|
|
375
374
|
sha256_hex: str
|
|
376
375
|
md5_hex: str
|
|
377
376
|
mode: int # file permission bits (last 12 bits of st_mode)
|
|
378
377
|
size: int
|
|
378
|
+
content: Optional[bytes] = None # Set for very small files to avoid double-read
|
|
379
|
+
|
|
380
|
+
def read_content(self) -> bytes:
|
|
381
|
+
"""Read content from source."""
|
|
382
|
+
with self.source() as fp:
|
|
383
|
+
fp.seek(0)
|
|
384
|
+
return fp.read()
|
|
379
385
|
|
|
380
386
|
|
|
381
387
|
def _get_file_upload_spec(
|
|
@@ -384,6 +390,7 @@ def _get_file_upload_spec(
|
|
|
384
390
|
mount_filename: PurePosixPath,
|
|
385
391
|
mode: int,
|
|
386
392
|
) -> FileUploadSpec:
|
|
393
|
+
content = None
|
|
387
394
|
with source() as fp:
|
|
388
395
|
# Current position is ignored - we always upload from position 0
|
|
389
396
|
fp.seek(0, os.SEEK_END)
|
|
@@ -394,12 +401,18 @@ def _get_file_upload_spec(
|
|
|
394
401
|
# TODO(dano): remove the placeholder md5 once we stop requiring md5 for blobs
|
|
395
402
|
md5_hex = "baadbaadbaadbaadbaadbaadbaadbaad" if size > MULTIPART_UPLOAD_THRESHOLD else None
|
|
396
403
|
use_blob = True
|
|
397
|
-
content = None
|
|
398
404
|
hashes = get_upload_hashes(fp, md5_hex=md5_hex)
|
|
399
405
|
else:
|
|
400
406
|
use_blob = False
|
|
401
|
-
|
|
402
|
-
|
|
407
|
+
# For very small files (< 256 KiB), read content once and cache it
|
|
408
|
+
# This avoids double-read penalty while limiting memory usage
|
|
409
|
+
if size < 256 * 1024: # 256 KiB threshold
|
|
410
|
+
fp.seek(0)
|
|
411
|
+
content = fp.read()
|
|
412
|
+
hashes = get_upload_hashes(content)
|
|
413
|
+
else:
|
|
414
|
+
# For medium files (256 KiB - 4 MiB), compute hashes without caching content
|
|
415
|
+
hashes = get_upload_hashes(fp)
|
|
403
416
|
|
|
404
417
|
return FileUploadSpec(
|
|
405
418
|
source=source,
|
|
@@ -407,11 +420,11 @@ def _get_file_upload_spec(
|
|
|
407
420
|
source_is_path=isinstance(source_description, Path),
|
|
408
421
|
mount_filename=mount_filename.as_posix(),
|
|
409
422
|
use_blob=use_blob,
|
|
410
|
-
content=content,
|
|
411
423
|
sha256_hex=hashes.sha256_hex(),
|
|
412
424
|
md5_hex=hashes.md5_hex(),
|
|
413
425
|
mode=mode & 0o7777,
|
|
414
426
|
size=size,
|
|
427
|
+
content=content,
|
|
415
428
|
)
|
|
416
429
|
|
|
417
430
|
|
|
@@ -158,8 +158,10 @@ class TaskCommandRouterClient:
|
|
|
158
158
|
)
|
|
159
159
|
|
|
160
160
|
await connect_channel(channel)
|
|
161
|
+
loop = asyncio.get_running_loop()
|
|
162
|
+
jwt_refresh_lock = asyncio.Lock()
|
|
161
163
|
|
|
162
|
-
return cls(server_client, task_id, resp.url, resp.jwt, channel)
|
|
164
|
+
return cls(server_client, task_id, resp.url, resp.jwt, channel, loop, jwt_refresh_lock)
|
|
163
165
|
|
|
164
166
|
def __init__(
|
|
165
167
|
self,
|
|
@@ -168,12 +170,18 @@ class TaskCommandRouterClient:
|
|
|
168
170
|
server_url: str,
|
|
169
171
|
jwt: str,
|
|
170
172
|
channel: grpclib.client.Channel,
|
|
173
|
+
loop: asyncio.AbstractEventLoop,
|
|
174
|
+
jwt_refresh_lock: asyncio.Lock,
|
|
171
175
|
*,
|
|
172
176
|
stream_stdio_retry_delay_secs: float = 0.01,
|
|
173
177
|
stream_stdio_retry_delay_factor: float = 2,
|
|
174
178
|
stream_stdio_max_retries: int = 10,
|
|
175
179
|
) -> None:
|
|
176
180
|
"""Callers should not use this directly. Use TaskCommandRouterClient.try_init() instead."""
|
|
181
|
+
# Record the loop this instance is bound to so __del__ can safely schedule cleanup
|
|
182
|
+
# even if finalization happens from a different thread (e.g. via synchronicity).
|
|
183
|
+
self._loop = loop
|
|
184
|
+
|
|
177
185
|
# Attach bearer token on all requests to the worker-side router service.
|
|
178
186
|
self._server_client = server_client
|
|
179
187
|
self._task_id = task_id
|
|
@@ -187,12 +195,10 @@ class TaskCommandRouterClient:
|
|
|
187
195
|
|
|
188
196
|
# JWT refresh coordination
|
|
189
197
|
self._jwt_exp: Optional[float] = _parse_jwt_expiration(jwt)
|
|
190
|
-
|
|
191
|
-
self.
|
|
192
|
-
self._closed = False
|
|
198
|
+
# This is passed in as an argument to ensure it's created from within the correct event loop.
|
|
199
|
+
self._jwt_refresh_lock = jwt_refresh_lock
|
|
193
200
|
|
|
194
|
-
|
|
195
|
-
self._jwt_refresh_task = asyncio.create_task(self._jwt_refresh_loop())
|
|
201
|
+
self._closed = False
|
|
196
202
|
|
|
197
203
|
async def send_request(event: grpclib.events.SendRequest) -> None:
|
|
198
204
|
# This will get the most recent JWT for every request. No need to
|
|
@@ -205,29 +211,40 @@ class TaskCommandRouterClient:
|
|
|
205
211
|
self._stub = TaskCommandRouterStub(self._channel)
|
|
206
212
|
|
|
207
213
|
def __del__(self) -> None:
|
|
208
|
-
"""
|
|
209
|
-
|
|
214
|
+
"""Best-effort cleanup if the caller forgot to close().
|
|
215
|
+
|
|
216
|
+
This object is typically used through synchronicity wrappers, which means this finalizer
|
|
217
|
+
may run on a different thread than the event loop that owns the channel. Closing the
|
|
218
|
+
channel is therefore scheduled onto the owning loop using call_soon_threadsafe.
|
|
219
|
+
|
|
220
|
+
Use getattr in the event that attributes are not yet initialized or the
|
|
221
|
+
object is in a half-torn-down state.
|
|
222
|
+
"""
|
|
223
|
+
if getattr(self, "_closed", False):
|
|
224
|
+
return
|
|
225
|
+
self._closed = True
|
|
226
|
+
|
|
227
|
+
channel = getattr(self, "_channel", None)
|
|
228
|
+
if channel is None:
|
|
210
229
|
return
|
|
211
230
|
|
|
212
|
-
self
|
|
231
|
+
loop = getattr(self, "_loop", None)
|
|
213
232
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
233
|
+
if loop is not None and not loop.is_closed():
|
|
234
|
+
try:
|
|
235
|
+
loop.call_soon_threadsafe(channel.close)
|
|
236
|
+
except Exception:
|
|
237
|
+
# call_soon_threadsafe could throw if the loop is torn down
|
|
238
|
+
# after calling is_closed. This is safe to ignore, and we don't
|
|
239
|
+
# want to raise an exception from a destructor.
|
|
240
|
+
pass
|
|
218
241
|
|
|
219
242
|
async def close(self) -> None:
|
|
220
|
-
"""Close the client
|
|
243
|
+
"""Close the client."""
|
|
221
244
|
if self._closed:
|
|
222
245
|
return
|
|
223
246
|
|
|
224
247
|
self._closed = True
|
|
225
|
-
self._jwt_refresh_task.cancel()
|
|
226
|
-
try:
|
|
227
|
-
logger.debug(f"Waiting for JWT refresh task to complete for exec with task ID {self._task_id}")
|
|
228
|
-
await self._jwt_refresh_task
|
|
229
|
-
except asyncio.CancelledError:
|
|
230
|
-
pass
|
|
231
248
|
self._channel.close()
|
|
232
249
|
|
|
233
250
|
async def exec_start(self, request: sr_pb2.TaskExecStartRequest) -> sr_pb2.TaskExecStartResponse:
|
|
@@ -370,10 +387,7 @@ class TaskCommandRouterClient:
|
|
|
370
387
|
raise ExecTimeoutError(f"Deadline exceeded while waiting for exec {exec_id}")
|
|
371
388
|
|
|
372
389
|
async def _refresh_jwt(self) -> None:
|
|
373
|
-
"""Refresh JWT from the server and update internal state.
|
|
374
|
-
|
|
375
|
-
Concurrency-safe: only one refresh runs at a time.
|
|
376
|
-
"""
|
|
390
|
+
"""Refresh JWT from the server and update internal state."""
|
|
377
391
|
async with self._jwt_refresh_lock:
|
|
378
392
|
if self._closed:
|
|
379
393
|
return
|
|
@@ -394,8 +408,6 @@ class TaskCommandRouterClient:
|
|
|
394
408
|
assert resp.url == self._server_url, "Task router URL changed during session"
|
|
395
409
|
self._jwt = resp.jwt
|
|
396
410
|
self._jwt_exp = _parse_jwt_expiration(resp.jwt)
|
|
397
|
-
# Wake up the background loop to recompute its next sleep.
|
|
398
|
-
self._jwt_refresh_event.set()
|
|
399
411
|
|
|
400
412
|
async def _call_with_auth_retry(self, func, *args, **kwargs):
|
|
401
413
|
try:
|
|
@@ -407,47 +419,6 @@ class TaskCommandRouterClient:
|
|
|
407
419
|
return await func(*args, **kwargs)
|
|
408
420
|
raise
|
|
409
421
|
|
|
410
|
-
async def _jwt_refresh_loop(self) -> None:
|
|
411
|
-
"""Background task that refreshes JWT 30 seconds before expiration.
|
|
412
|
-
|
|
413
|
-
Uses an event to wake early when a manual refresh happens or token changes.
|
|
414
|
-
"""
|
|
415
|
-
while not self._closed:
|
|
416
|
-
try:
|
|
417
|
-
exp = self._jwt_exp
|
|
418
|
-
now = time.time()
|
|
419
|
-
if exp is None:
|
|
420
|
-
# Unknown expiration: re-check periodically or until event wakes us.
|
|
421
|
-
sleep_s = 60.0
|
|
422
|
-
else:
|
|
423
|
-
refresh_at = exp - 30.0
|
|
424
|
-
sleep_s = max(refresh_at - now, 0.0)
|
|
425
|
-
|
|
426
|
-
self._jwt_refresh_event.clear()
|
|
427
|
-
if sleep_s > 0:
|
|
428
|
-
try:
|
|
429
|
-
logger.debug(f"Waiting for JWT refresh for {sleep_s}s for exec with task ID {self._task_id}")
|
|
430
|
-
# Wait until it's time to refresh, unless woken early.
|
|
431
|
-
await asyncio.wait_for(self._jwt_refresh_event.wait(), timeout=sleep_s)
|
|
432
|
-
logger.debug(f"Stopped waiting for JWT refresh for exec with task ID {self._task_id}")
|
|
433
|
-
# Event fired (e.g., token changed) -> recompute timings.
|
|
434
|
-
continue
|
|
435
|
-
except asyncio.TimeoutError:
|
|
436
|
-
logger.debug(f"Done waiting for JWT refresh for exec with task ID {self._task_id}")
|
|
437
|
-
pass
|
|
438
|
-
|
|
439
|
-
# Time to refresh.
|
|
440
|
-
logger.debug(f"Refreshing JWT for exec with task ID {self._task_id}")
|
|
441
|
-
await self._refresh_jwt()
|
|
442
|
-
except asyncio.CancelledError:
|
|
443
|
-
logger.debug(f"Cancelled JWT refresh loop for exec with task ID {self._task_id}")
|
|
444
|
-
break
|
|
445
|
-
except Exception as e:
|
|
446
|
-
# Exceptions here can stem from non-transient errors against the server sending
|
|
447
|
-
# the TaskGetCommandRouterAccess RPC, for instance, if the task has finished.
|
|
448
|
-
logger.debug(f"Background JWT refresh failed for exec with task ID {self._task_id}: {e}")
|
|
449
|
-
break
|
|
450
|
-
|
|
451
422
|
async def _stream_stdio(
|
|
452
423
|
self,
|
|
453
424
|
task_id: str,
|
|
@@ -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.5.
|
|
35
|
+
version: str = "1.2.5.dev28",
|
|
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.5.
|
|
166
|
+
version: str = "1.2.5.dev28",
|
|
167
167
|
):
|
|
168
168
|
"""mdmd:hidden
|
|
169
169
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -466,16 +466,18 @@ class _Mount(_Object, type_prefix="mo"):
|
|
|
466
466
|
loop = asyncio.get_event_loop()
|
|
467
467
|
with concurrent.futures.ThreadPoolExecutor() as exe:
|
|
468
468
|
all_files = await loop.run_in_executor(exe, _select_files, entries)
|
|
469
|
+
logger.debug(f"Computing checksums for {len(all_files)} files using {exe._max_workers} worker threads")
|
|
469
470
|
|
|
470
|
-
|
|
471
|
+
# Yield FileUploadSpec objects lazily as they're consumed by async_map downstream.
|
|
472
|
+
# async_map's concurrency limit provides natural backpressure, so we don't need
|
|
473
|
+
# a separate semaphore here. This keeps memory bounded without creating all tasks upfront.
|
|
471
474
|
for local_filename, remote_filename in all_files:
|
|
472
|
-
logger.debug(f"Mounting {local_filename} as {remote_filename}")
|
|
473
|
-
futs.append(loop.run_in_executor(exe, get_file_upload_spec_from_path, local_filename, remote_filename))
|
|
474
|
-
|
|
475
|
-
logger.debug(f"Computing checksums for {len(futs)} files using {exe._max_workers} worker threads")
|
|
476
|
-
for fut in asyncio.as_completed(futs):
|
|
477
475
|
try:
|
|
478
|
-
|
|
476
|
+
logger.debug(f"Mounting {local_filename} as {remote_filename}")
|
|
477
|
+
file_spec = await loop.run_in_executor(
|
|
478
|
+
exe, get_file_upload_spec_from_path, local_filename, remote_filename
|
|
479
|
+
)
|
|
480
|
+
yield file_spec
|
|
479
481
|
except FileNotFoundError as exc:
|
|
480
482
|
# Can happen with temporary files (e.g. emacs will write temp files and delete them quickly)
|
|
481
483
|
logger.info(f"Ignoring file not found: {exc}")
|
|
@@ -547,7 +549,11 @@ class _Mount(_Object, type_prefix="mo"):
|
|
|
547
549
|
logger.debug(
|
|
548
550
|
f"Uploading file {file_spec.source_description} to {remote_filename} ({file_spec.size} bytes)"
|
|
549
551
|
)
|
|
550
|
-
|
|
552
|
+
if file_spec.content is None:
|
|
553
|
+
content = await asyncio.to_thread(file_spec.read_content)
|
|
554
|
+
else:
|
|
555
|
+
content = file_spec.content
|
|
556
|
+
request2 = api_pb2.MountPutFileRequest(data=content, sha256_hex=file_spec.sha256_hex)
|
|
551
557
|
|
|
552
558
|
start_time = time.monotonic()
|
|
553
559
|
while time.monotonic() - start_time < MOUNT_PUT_FILE_CLIENT_TIMEOUT:
|
|
@@ -1056,7 +1056,11 @@ class _VolumeUploadContextManager(_AbstractVolumeUploadContextManager):
|
|
|
1056
1056
|
logger.debug(
|
|
1057
1057
|
f"Uploading file {file_spec.source_description} to {remote_filename} ({file_spec.size} bytes)"
|
|
1058
1058
|
)
|
|
1059
|
-
|
|
1059
|
+
if file_spec.content is None:
|
|
1060
|
+
content = await asyncio.to_thread(file_spec.read_content)
|
|
1061
|
+
else:
|
|
1062
|
+
content = file_spec.content
|
|
1063
|
+
request2 = api_pb2.MountPutFileRequest(data=content, sha256_hex=file_spec.sha256_hex)
|
|
1060
1064
|
self._progress_cb(task_id=progress_task_id, complete=True)
|
|
1061
1065
|
|
|
1062
1066
|
while (time.monotonic() - start_time) < VOLUME_PUT_FILE_CLIENT_TIMEOUT:
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|