modal 1.1.5.dev66__py3-none-any.whl → 1.3.1.dev8__py3-none-any.whl
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.
Potentially problematic release.
This version of modal might be problematic. Click here for more details.
- modal/__init__.py +4 -4
- modal/__main__.py +4 -29
- modal/_billing.py +84 -0
- modal/_clustered_functions.py +1 -3
- modal/_container_entrypoint.py +33 -208
- modal/_functions.py +171 -138
- modal/_grpc_client.py +191 -0
- modal/_ipython.py +16 -6
- modal/_load_context.py +106 -0
- modal/_object.py +72 -21
- modal/_output.py +12 -14
- modal/_partial_function.py +31 -4
- modal/_resolver.py +44 -57
- modal/_runtime/container_io_manager.py +30 -28
- modal/_runtime/container_io_manager.pyi +42 -44
- modal/_runtime/gpu_memory_snapshot.py +9 -7
- modal/_runtime/user_code_event_loop.py +80 -0
- modal/_runtime/user_code_imports.py +236 -10
- modal/_serialization.py +2 -1
- modal/_traceback.py +4 -13
- modal/_tunnel.py +16 -11
- modal/_tunnel.pyi +25 -3
- modal/_utils/async_utils.py +337 -10
- modal/_utils/auth_token_manager.py +1 -4
- modal/_utils/blob_utils.py +29 -22
- modal/_utils/function_utils.py +20 -21
- modal/_utils/grpc_testing.py +6 -3
- modal/_utils/grpc_utils.py +223 -64
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/name_utils.py +2 -3
- modal/_utils/package_utils.py +0 -1
- modal/_utils/rand_pb_testing.py +8 -1
- modal/_utils/task_command_router_client.py +524 -0
- modal/_vendor/cloudpickle.py +144 -48
- modal/app.py +285 -105
- modal/app.pyi +216 -53
- modal/billing.py +5 -0
- modal/builder/2025.06.txt +6 -3
- modal/builder/PREVIEW.txt +2 -1
- modal/builder/base-images.json +4 -2
- modal/cli/_download.py +19 -3
- modal/cli/cluster.py +4 -2
- modal/cli/config.py +3 -1
- modal/cli/container.py +5 -4
- modal/cli/dict.py +5 -2
- modal/cli/entry_point.py +26 -2
- modal/cli/environment.py +2 -16
- modal/cli/launch.py +1 -76
- modal/cli/network_file_system.py +5 -20
- modal/cli/programs/run_jupyter.py +1 -1
- modal/cli/programs/vscode.py +1 -1
- modal/cli/queues.py +5 -4
- modal/cli/run.py +24 -204
- modal/cli/secret.py +1 -2
- modal/cli/shell.py +375 -0
- modal/cli/utils.py +1 -13
- modal/cli/volume.py +11 -17
- modal/client.py +16 -125
- modal/client.pyi +94 -144
- modal/cloud_bucket_mount.py +3 -1
- modal/cloud_bucket_mount.pyi +4 -0
- modal/cls.py +101 -64
- modal/cls.pyi +9 -8
- modal/config.py +21 -1
- modal/container_process.py +288 -12
- modal/container_process.pyi +99 -38
- modal/dict.py +72 -33
- modal/dict.pyi +88 -57
- modal/environments.py +16 -8
- modal/environments.pyi +6 -2
- modal/exception.py +154 -16
- modal/experimental/__init__.py +24 -53
- modal/experimental/flash.py +161 -74
- modal/experimental/flash.pyi +97 -49
- modal/file_io.py +50 -92
- modal/file_io.pyi +117 -89
- modal/functions.pyi +70 -87
- modal/image.py +82 -47
- modal/image.pyi +51 -30
- modal/io_streams.py +500 -149
- modal/io_streams.pyi +279 -189
- modal/mount.py +60 -46
- modal/mount.pyi +41 -17
- modal/network_file_system.py +19 -11
- modal/network_file_system.pyi +72 -39
- modal/object.pyi +114 -22
- modal/parallel_map.py +42 -44
- modal/parallel_map.pyi +9 -17
- modal/partial_function.pyi +4 -2
- modal/proxy.py +14 -6
- modal/proxy.pyi +10 -2
- modal/queue.py +45 -38
- modal/queue.pyi +88 -52
- modal/runner.py +96 -96
- modal/runner.pyi +44 -27
- modal/sandbox.py +225 -107
- modal/sandbox.pyi +226 -60
- modal/secret.py +58 -56
- modal/secret.pyi +28 -13
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +29 -15
- modal/snapshot.pyi +18 -10
- modal/token_flow.py +1 -1
- modal/token_flow.pyi +4 -6
- modal/volume.py +102 -55
- modal/volume.pyi +125 -66
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/METADATA +10 -9
- modal-1.3.1.dev8.dist-info/RECORD +189 -0
- modal_proto/api.proto +141 -70
- modal_proto/api_grpc.py +42 -26
- modal_proto/api_pb2.py +1123 -1103
- modal_proto/api_pb2.pyi +331 -83
- modal_proto/api_pb2_grpc.py +80 -48
- modal_proto/api_pb2_grpc.pyi +26 -18
- modal_proto/modal_api_grpc.py +175 -174
- modal_proto/task_command_router.proto +164 -0
- modal_proto/task_command_router_grpc.py +138 -0
- modal_proto/task_command_router_pb2.py +180 -0
- modal_proto/{sandbox_router_pb2.pyi → task_command_router_pb2.pyi} +148 -57
- modal_proto/task_command_router_pb2_grpc.py +272 -0
- modal_proto/task_command_router_pb2_grpc.pyi +100 -0
- modal_version/__init__.py +1 -1
- modal_version/__main__.py +1 -1
- modal/cli/programs/launch_instance_ssh.py +0 -94
- modal/cli/programs/run_marimo.py +0 -95
- modal-1.1.5.dev66.dist-info/RECORD +0 -191
- modal_proto/modal_options_grpc.py +0 -3
- modal_proto/options.proto +0 -19
- modal_proto/options_grpc.py +0 -3
- modal_proto/options_pb2.py +0 -35
- modal_proto/options_pb2.pyi +0 -20
- modal_proto/options_pb2_grpc.py +0 -4
- modal_proto/options_pb2_grpc.pyi +0 -7
- modal_proto/sandbox_router.proto +0 -125
- modal_proto/sandbox_router_grpc.py +0 -89
- modal_proto/sandbox_router_pb2.py +0 -128
- modal_proto/sandbox_router_pb2_grpc.py +0 -169
- modal_proto/sandbox_router_pb2_grpc.pyi +0 -63
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/WHEEL +0 -0
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/entry_points.txt +0 -0
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/licenses/LICENSE +0 -0
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/top_level.txt +0 -0
modal/volume.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Copyright Modal Labs 2023
|
|
2
2
|
import asyncio
|
|
3
|
+
import builtins
|
|
3
4
|
import concurrent.futures
|
|
4
5
|
import enum
|
|
5
6
|
import functools
|
|
@@ -7,6 +8,7 @@ import multiprocessing
|
|
|
7
8
|
import os
|
|
8
9
|
import platform
|
|
9
10
|
import re
|
|
11
|
+
import sys
|
|
10
12
|
import time
|
|
11
13
|
import typing
|
|
12
14
|
from collections.abc import AsyncGenerator, AsyncIterator, Generator, Sequence
|
|
@@ -24,20 +26,21 @@ from typing import (
|
|
|
24
26
|
)
|
|
25
27
|
|
|
26
28
|
from google.protobuf.message import Message
|
|
27
|
-
from grpclib import GRPCError, Status
|
|
28
29
|
from synchronicity import classproperty
|
|
29
30
|
from synchronicity.async_wrap import asynccontextmanager
|
|
30
31
|
|
|
31
32
|
import modal.exception
|
|
32
33
|
import modal_proto.api_pb2
|
|
33
|
-
from modal.exception import AlreadyExistsError, InvalidError, NotFoundError, VolumeUploadTimeoutError
|
|
34
|
+
from modal.exception import AlreadyExistsError, ConflictError, InvalidError, NotFoundError, VolumeUploadTimeoutError
|
|
34
35
|
from modal_proto import api_pb2
|
|
35
36
|
|
|
37
|
+
from ._load_context import LoadContext
|
|
36
38
|
from ._object import (
|
|
37
39
|
EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
|
|
38
40
|
_get_environment_name,
|
|
39
41
|
_Object,
|
|
40
42
|
live_method,
|
|
43
|
+
live_method_contextmanager,
|
|
41
44
|
live_method_gen,
|
|
42
45
|
)
|
|
43
46
|
from ._resolver import Resolver
|
|
@@ -59,7 +62,7 @@ from ._utils.blob_utils import (
|
|
|
59
62
|
get_file_upload_spec_from_path,
|
|
60
63
|
)
|
|
61
64
|
from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
|
|
62
|
-
from ._utils.grpc_utils import
|
|
65
|
+
from ._utils.grpc_utils import Retry
|
|
63
66
|
from ._utils.http_utils import ClientSessionRegistry
|
|
64
67
|
from ._utils.name_utils import check_object_name
|
|
65
68
|
from ._utils.time_utils import as_timestamp, timestamp_to_localized_dt
|
|
@@ -170,11 +173,9 @@ class _VolumeManager:
|
|
|
170
173
|
version=version,
|
|
171
174
|
)
|
|
172
175
|
try:
|
|
173
|
-
await
|
|
174
|
-
except
|
|
175
|
-
if
|
|
176
|
-
raise AlreadyExistsError(exc.message)
|
|
177
|
-
else:
|
|
176
|
+
await client.stub.VolumeGetOrCreate(req)
|
|
177
|
+
except AlreadyExistsError:
|
|
178
|
+
if not allow_existing:
|
|
178
179
|
raise
|
|
179
180
|
|
|
180
181
|
@staticmethod
|
|
@@ -184,7 +185,7 @@ class _VolumeManager:
|
|
|
184
185
|
created_before: Optional[Union[datetime, str]] = None, # Limit based on creation date
|
|
185
186
|
environment_name: str = "", # Uses active environment if not specified
|
|
186
187
|
client: Optional[_Client] = None, # Optional client with Modal credentials
|
|
187
|
-
) -> list["_Volume"]:
|
|
188
|
+
) -> builtins.list["_Volume"]:
|
|
188
189
|
"""Return a list of hydrated Volume objects.
|
|
189
190
|
|
|
190
191
|
**Examples:**
|
|
@@ -222,7 +223,7 @@ class _VolumeManager:
|
|
|
222
223
|
req = api_pb2.VolumeListRequest(
|
|
223
224
|
environment_name=_get_environment_name(environment_name), pagination=pagination
|
|
224
225
|
)
|
|
225
|
-
resp = await
|
|
226
|
+
resp = await client.stub.VolumeList(req)
|
|
226
227
|
items.extend(resp.items)
|
|
227
228
|
finished = (len(resp.items) < max_page_size) or (max_objects is not None and len(items) >= max_objects)
|
|
228
229
|
return finished
|
|
@@ -280,7 +281,7 @@ class _VolumeManager:
|
|
|
280
281
|
raise
|
|
281
282
|
else:
|
|
282
283
|
req = api_pb2.VolumeDeleteRequest(volume_id=obj.object_id)
|
|
283
|
-
await
|
|
284
|
+
await obj._client.stub.VolumeDelete(req)
|
|
284
285
|
|
|
285
286
|
|
|
286
287
|
VolumeManager = synchronize_api(_VolumeManager)
|
|
@@ -362,11 +363,19 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
362
363
|
Added in v1.0.5.
|
|
363
364
|
"""
|
|
364
365
|
|
|
365
|
-
async def _load(
|
|
366
|
+
async def _load(
|
|
367
|
+
new_volume: _Volume, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
368
|
+
):
|
|
366
369
|
new_volume._initialize_from_other(self)
|
|
367
370
|
new_volume._read_only = True
|
|
368
371
|
|
|
369
|
-
obj = _Volume._from_loader(
|
|
372
|
+
obj = _Volume._from_loader(
|
|
373
|
+
_load,
|
|
374
|
+
"Volume()",
|
|
375
|
+
hydrate_lazily=True,
|
|
376
|
+
deps=lambda: [self],
|
|
377
|
+
load_context_overrides=self._load_context_overrides,
|
|
378
|
+
)
|
|
370
379
|
return obj
|
|
371
380
|
|
|
372
381
|
def _hydrate_metadata(self, metadata: Optional[Message]):
|
|
@@ -408,6 +417,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
408
417
|
environment_name: Optional[str] = None,
|
|
409
418
|
create_if_missing: bool = False,
|
|
410
419
|
version: "typing.Optional[modal_proto.api_pb2.VolumeFsVersion.ValueType]" = None,
|
|
420
|
+
client: Optional[_Client] = None,
|
|
411
421
|
) -> "_Volume":
|
|
412
422
|
"""Reference a Volume by name, creating if necessary.
|
|
413
423
|
|
|
@@ -429,18 +439,26 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
429
439
|
check_object_name(name, "Volume")
|
|
430
440
|
warn_if_passing_namespace(namespace, "modal.Volume.from_name")
|
|
431
441
|
|
|
432
|
-
async def _load(
|
|
442
|
+
async def _load(
|
|
443
|
+
self: _Volume, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
444
|
+
):
|
|
433
445
|
req = api_pb2.VolumeGetOrCreateRequest(
|
|
434
446
|
deployment_name=name,
|
|
435
|
-
environment_name=
|
|
447
|
+
environment_name=load_context.environment_name,
|
|
436
448
|
object_creation_type=(api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING if create_if_missing else None),
|
|
437
449
|
version=version,
|
|
438
450
|
)
|
|
439
|
-
response = await
|
|
440
|
-
self._hydrate(response.volume_id,
|
|
451
|
+
response = await load_context.client.stub.VolumeGetOrCreate(req)
|
|
452
|
+
self._hydrate(response.volume_id, load_context.client, response.metadata)
|
|
441
453
|
|
|
442
454
|
rep = _Volume._repr(name, environment_name)
|
|
443
|
-
return _Volume._from_loader(
|
|
455
|
+
return _Volume._from_loader(
|
|
456
|
+
_load,
|
|
457
|
+
rep,
|
|
458
|
+
hydrate_lazily=True,
|
|
459
|
+
name=name,
|
|
460
|
+
load_context_overrides=LoadContext(client=client, environment_name=environment_name),
|
|
461
|
+
)
|
|
444
462
|
|
|
445
463
|
@classmethod
|
|
446
464
|
@asynccontextmanager
|
|
@@ -519,7 +537,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
519
537
|
object_creation_type=api_pb2.OBJECT_CREATION_TYPE_CREATE_FAIL_IF_EXISTS,
|
|
520
538
|
version=version,
|
|
521
539
|
)
|
|
522
|
-
resp = await
|
|
540
|
+
resp = await client.stub.VolumeGetOrCreate(request)
|
|
523
541
|
return resp.volume_id
|
|
524
542
|
|
|
525
543
|
@live_method
|
|
@@ -539,7 +557,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
539
557
|
async def _do_reload(self, lock=True):
|
|
540
558
|
async with (await self._get_lock()) if lock else asyncnullcontext():
|
|
541
559
|
req = api_pb2.VolumeReloadRequest(volume_id=self.object_id)
|
|
542
|
-
_ = await
|
|
560
|
+
_ = await self._client.stub.VolumeReload(req)
|
|
543
561
|
|
|
544
562
|
@live_method
|
|
545
563
|
async def commit(self):
|
|
@@ -552,12 +570,12 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
552
570
|
req = api_pb2.VolumeCommitRequest(volume_id=self.object_id)
|
|
553
571
|
try:
|
|
554
572
|
# TODO(gongy): only apply indefinite retries on 504 status.
|
|
555
|
-
resp = await
|
|
573
|
+
resp = await self._client.stub.VolumeCommit(req, retry=Retry(max_retries=90))
|
|
556
574
|
if not resp.skip_reload:
|
|
557
575
|
# Reload changes on successful commit.
|
|
558
576
|
await self._do_reload(lock=False)
|
|
559
|
-
except
|
|
560
|
-
raise RuntimeError(exc
|
|
577
|
+
except (ConflictError, NotFoundError) as exc:
|
|
578
|
+
raise RuntimeError(str(exc))
|
|
561
579
|
|
|
562
580
|
@live_method
|
|
563
581
|
async def reload(self):
|
|
@@ -570,11 +588,12 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
570
588
|
"""
|
|
571
589
|
try:
|
|
572
590
|
await self._do_reload()
|
|
573
|
-
except
|
|
591
|
+
except (NotFoundError, ConflictError) as exc:
|
|
574
592
|
# TODO(staffan): This is brittle and janky, as it relies on specific paths and error messages which can
|
|
575
593
|
# change server-side at any time. Consider returning the open files directly in the error emitted from the
|
|
576
594
|
# server.
|
|
577
|
-
|
|
595
|
+
message = str(exc)
|
|
596
|
+
if "there are open files preventing the operation" in message:
|
|
578
597
|
# Attempt to identify what open files are problematic and include information about the first (to avoid
|
|
579
598
|
# really verbose errors) open file in the error message to help troubleshooting.
|
|
580
599
|
# This is best-effort and not necessarily bulletproof, as the view of open files inside the container
|
|
@@ -582,9 +601,8 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
582
601
|
vol_path = f"/__modal/volumes/{self.object_id}"
|
|
583
602
|
annotation = _open_files_error_annotation(vol_path)
|
|
584
603
|
if annotation:
|
|
585
|
-
raise RuntimeError(f"{
|
|
586
|
-
|
|
587
|
-
raise RuntimeError(exc.message) if exc.status in (Status.FAILED_PRECONDITION, Status.NOT_FOUND) else exc
|
|
604
|
+
raise RuntimeError(f"{message}: {annotation}")
|
|
605
|
+
raise RuntimeError(message)
|
|
588
606
|
|
|
589
607
|
@live_method_gen
|
|
590
608
|
async def iterdir(self, path: str, *, recursive: bool = True) -> AsyncIterator[FileEntry]:
|
|
@@ -627,7 +645,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
627
645
|
return [entry async for entry in self.iterdir(path, recursive=recursive)]
|
|
628
646
|
|
|
629
647
|
@live_method_gen
|
|
630
|
-
async def read_file(self, path: str) ->
|
|
648
|
+
async def read_file(self, path: str) -> AsyncGenerator[bytes, None]:
|
|
631
649
|
"""
|
|
632
650
|
Read a file from the modal.Volume.
|
|
633
651
|
|
|
@@ -648,13 +666,14 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
648
666
|
req = api_pb2.VolumeGetFile2Request(volume_id=self.object_id, path=path)
|
|
649
667
|
|
|
650
668
|
try:
|
|
651
|
-
response = await
|
|
669
|
+
response = await self._client.stub.VolumeGetFile2(req)
|
|
652
670
|
except modal.exception.NotFoundError as exc:
|
|
653
671
|
raise FileNotFoundError(exc.args[0])
|
|
654
672
|
|
|
655
673
|
@retry(n_attempts=5, base_delay=0.1, timeout=None)
|
|
656
674
|
async def read_block(block_url: str) -> bytes:
|
|
657
675
|
async with ClientSessionRegistry.get_session().get(block_url) as get_response:
|
|
676
|
+
get_response.raise_for_status()
|
|
658
677
|
return await get_response.content.read()
|
|
659
678
|
|
|
660
679
|
async def iter_urls() -> AsyncGenerator[str]:
|
|
@@ -670,25 +689,43 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
670
689
|
|
|
671
690
|
@live_method
|
|
672
691
|
async def read_file_into_fileobj(
|
|
673
|
-
self,
|
|
692
|
+
self,
|
|
693
|
+
path: str,
|
|
694
|
+
fileobj: typing.IO[bytes],
|
|
695
|
+
progress_cb: Optional[Callable[..., Any]] = None,
|
|
674
696
|
) -> int:
|
|
675
697
|
"""mdmd:hidden
|
|
676
698
|
Read volume file into file-like IO object.
|
|
677
699
|
"""
|
|
700
|
+
return await self._read_file_into_fileobj(path, fileobj, progress_cb=progress_cb)
|
|
701
|
+
|
|
702
|
+
@live_method
|
|
703
|
+
async def _read_file_into_fileobj(
|
|
704
|
+
self,
|
|
705
|
+
path: str,
|
|
706
|
+
fileobj: typing.IO[bytes],
|
|
707
|
+
concurrency: Optional[int] = None,
|
|
708
|
+
download_semaphore: Optional[asyncio.Semaphore] = None,
|
|
709
|
+
progress_cb: Optional[Callable[..., Any]] = None,
|
|
710
|
+
) -> int:
|
|
678
711
|
if progress_cb is None:
|
|
679
712
|
|
|
680
713
|
def progress_cb(*_, **__):
|
|
681
714
|
pass
|
|
682
715
|
|
|
716
|
+
if concurrency is None:
|
|
717
|
+
concurrency = multiprocessing.cpu_count()
|
|
718
|
+
|
|
683
719
|
req = api_pb2.VolumeGetFile2Request(volume_id=self.object_id, path=path)
|
|
684
720
|
|
|
685
721
|
try:
|
|
686
|
-
response = await
|
|
722
|
+
response = await self._client.stub.VolumeGetFile2(req)
|
|
687
723
|
except modal.exception.NotFoundError as exc:
|
|
688
724
|
raise FileNotFoundError(exc.args[0])
|
|
689
725
|
|
|
690
|
-
|
|
691
|
-
|
|
726
|
+
if download_semaphore is None:
|
|
727
|
+
download_semaphore = asyncio.Semaphore(concurrency)
|
|
728
|
+
|
|
692
729
|
write_lock = asyncio.Lock()
|
|
693
730
|
start_pos = fileobj.tell()
|
|
694
731
|
|
|
@@ -698,6 +735,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
698
735
|
num_bytes_written = 0
|
|
699
736
|
|
|
700
737
|
async with download_semaphore, ClientSessionRegistry.get_session().get(url) as get_response:
|
|
738
|
+
get_response.raise_for_status()
|
|
701
739
|
async for chunk in get_response.content.iter_any():
|
|
702
740
|
num_chunk_bytes_written = 0
|
|
703
741
|
|
|
@@ -731,10 +769,10 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
731
769
|
try:
|
|
732
770
|
if self._is_v1:
|
|
733
771
|
req = api_pb2.VolumeRemoveFileRequest(volume_id=self.object_id, path=path, recursive=recursive)
|
|
734
|
-
await
|
|
772
|
+
await self._client.stub.VolumeRemoveFile(req)
|
|
735
773
|
else:
|
|
736
774
|
req = api_pb2.VolumeRemoveFile2Request(volume_id=self.object_id, path=path, recursive=recursive)
|
|
737
|
-
await
|
|
775
|
+
await self._client.stub.VolumeRemoveFile2(req)
|
|
738
776
|
except modal.exception.NotFoundError as exc:
|
|
739
777
|
raise FileNotFoundError(exc.args[0])
|
|
740
778
|
|
|
@@ -773,15 +811,16 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
773
811
|
request = api_pb2.VolumeCopyFilesRequest(
|
|
774
812
|
volume_id=self.object_id, src_paths=src_paths, dst_path=dst_path, recursive=recursive
|
|
775
813
|
)
|
|
776
|
-
await
|
|
814
|
+
await self._client.stub.VolumeCopyFiles(request, retry=Retry(base_delay=1))
|
|
777
815
|
else:
|
|
778
816
|
request = api_pb2.VolumeCopyFiles2Request(
|
|
779
817
|
volume_id=self.object_id, src_paths=src_paths, dst_path=dst_path, recursive=recursive
|
|
780
818
|
)
|
|
781
|
-
await
|
|
819
|
+
await self._client.stub.VolumeCopyFiles2(request, retry=Retry(base_delay=1))
|
|
782
820
|
|
|
783
|
-
@
|
|
784
|
-
|
|
821
|
+
@live_method_contextmanager
|
|
822
|
+
@asynccontextmanager
|
|
823
|
+
async def batch_upload(self, force: bool = False) -> AsyncGenerator["_AbstractVolumeUploadContextManager", None]:
|
|
785
824
|
"""
|
|
786
825
|
Initiate a batched upload to a volume.
|
|
787
826
|
|
|
@@ -802,15 +841,19 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
802
841
|
if self._read_only:
|
|
803
842
|
raise InvalidError("Read-only Volume can not be written to")
|
|
804
843
|
|
|
805
|
-
|
|
844
|
+
version_context_manager = _AbstractVolumeUploadContextManager.resolve(
|
|
806
845
|
self._metadata.version, self.object_id, self._client, force=force
|
|
807
846
|
)
|
|
847
|
+
await version_context_manager.__aenter__()
|
|
848
|
+
try:
|
|
849
|
+
yield version_context_manager
|
|
850
|
+
finally:
|
|
851
|
+
exc_type, exc_value, traceback = sys.exc_info()
|
|
852
|
+
await version_context_manager.__aexit__(exc_type, exc_value, traceback)
|
|
808
853
|
|
|
809
854
|
@live_method
|
|
810
855
|
async def _instance_delete(self):
|
|
811
|
-
await
|
|
812
|
-
self._client.stub.VolumeDelete, api_pb2.VolumeDeleteRequest(volume_id=self.object_id)
|
|
813
|
-
)
|
|
856
|
+
await self._client.stub.VolumeDelete(api_pb2.VolumeDeleteRequest(volume_id=self.object_id))
|
|
814
857
|
|
|
815
858
|
@staticmethod
|
|
816
859
|
async def delete(name: str, client: Optional[_Client] = None, environment_name: Optional[str] = None):
|
|
@@ -839,7 +882,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
839
882
|
):
|
|
840
883
|
obj = await _Volume.from_name(old_name, environment_name=environment_name).hydrate(client)
|
|
841
884
|
req = api_pb2.VolumeRenameRequest(volume_id=obj.object_id, name=new_name)
|
|
842
|
-
await
|
|
885
|
+
await obj._client.stub.VolumeRename(req)
|
|
843
886
|
|
|
844
887
|
|
|
845
888
|
Volume = synchronize_api(_Volume)
|
|
@@ -940,9 +983,9 @@ class _VolumeUploadContextManager(_AbstractVolumeUploadContextManager):
|
|
|
940
983
|
disallow_overwrite_existing_files=not self._force,
|
|
941
984
|
)
|
|
942
985
|
try:
|
|
943
|
-
await
|
|
944
|
-
except
|
|
945
|
-
raise FileExistsError(exc
|
|
986
|
+
await self._client.stub.VolumePutFiles(request, retry=Retry(base_delay=1))
|
|
987
|
+
except AlreadyExistsError as exc:
|
|
988
|
+
raise FileExistsError(str(exc))
|
|
946
989
|
|
|
947
990
|
def put_file(
|
|
948
991
|
self,
|
|
@@ -1000,7 +1043,7 @@ class _VolumeUploadContextManager(_AbstractVolumeUploadContextManager):
|
|
|
1000
1043
|
remote_filename = file_spec.mount_filename
|
|
1001
1044
|
progress_task_id = self._progress_cb(name=remote_filename, size=file_spec.size)
|
|
1002
1045
|
request = api_pb2.MountPutFileRequest(sha256_hex=file_spec.sha256_hex)
|
|
1003
|
-
response = await
|
|
1046
|
+
response = await self._client.stub.MountPutFile(request, retry=Retry(base_delay=1))
|
|
1004
1047
|
|
|
1005
1048
|
start_time = time.monotonic()
|
|
1006
1049
|
if not response.exists:
|
|
@@ -1020,11 +1063,15 @@ class _VolumeUploadContextManager(_AbstractVolumeUploadContextManager):
|
|
|
1020
1063
|
logger.debug(
|
|
1021
1064
|
f"Uploading file {file_spec.source_description} to {remote_filename} ({file_spec.size} bytes)"
|
|
1022
1065
|
)
|
|
1023
|
-
|
|
1066
|
+
if file_spec.content is None:
|
|
1067
|
+
content = await asyncio.to_thread(file_spec.read_content)
|
|
1068
|
+
else:
|
|
1069
|
+
content = file_spec.content
|
|
1070
|
+
request2 = api_pb2.MountPutFileRequest(data=content, sha256_hex=file_spec.sha256_hex)
|
|
1024
1071
|
self._progress_cb(task_id=progress_task_id, complete=True)
|
|
1025
1072
|
|
|
1026
1073
|
while (time.monotonic() - start_time) < VOLUME_PUT_FILE_CLIENT_TIMEOUT:
|
|
1027
|
-
response = await
|
|
1074
|
+
response = await self._client.stub.MountPutFile(request2, retry=Retry(base_delay=1))
|
|
1028
1075
|
if response.exists:
|
|
1029
1076
|
break
|
|
1030
1077
|
|
|
@@ -1184,9 +1231,9 @@ class _VolumeUploadContextManager2(_AbstractVolumeUploadContextManager):
|
|
|
1184
1231
|
)
|
|
1185
1232
|
|
|
1186
1233
|
try:
|
|
1187
|
-
response = await
|
|
1188
|
-
except
|
|
1189
|
-
raise FileExistsError(exc
|
|
1234
|
+
response = await self._client.stub.VolumePutFiles2(request, retry=Retry(base_delay=1))
|
|
1235
|
+
except AlreadyExistsError as exc:
|
|
1236
|
+
raise FileExistsError(str(exc))
|
|
1190
1237
|
|
|
1191
1238
|
if not response.missing_blocks:
|
|
1192
1239
|
break
|
|
@@ -1245,7 +1292,7 @@ async def _put_missing_blocks(
|
|
|
1245
1292
|
file_progress.pending_blocks.add(missing_block.block_index)
|
|
1246
1293
|
task_progress_cb = functools.partial(progress_cb, task_id=file_progress.task_id)
|
|
1247
1294
|
|
|
1248
|
-
@retry(n_attempts=
|
|
1295
|
+
@retry(n_attempts=11, base_delay=0.5, timeout=None)
|
|
1249
1296
|
async def put_missing_block_attempt(payload: BytesIOSegmentPayload) -> bytes:
|
|
1250
1297
|
with payload.reset_on_error(subtract_progress=True):
|
|
1251
1298
|
async with ClientSessionRegistry.get_session().put(
|