modal 1.0.3.dev10__py3-none-any.whl → 1.2.3.dev7__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 +0 -2
- modal/__main__.py +3 -4
- modal/_billing.py +80 -0
- modal/_clustered_functions.py +7 -3
- modal/_clustered_functions.pyi +15 -3
- modal/_container_entrypoint.py +51 -69
- modal/_functions.py +508 -240
- modal/_grpc_client.py +171 -0
- modal/_load_context.py +105 -0
- modal/_object.py +81 -21
- modal/_output.py +58 -45
- modal/_partial_function.py +48 -73
- modal/_pty.py +7 -3
- modal/_resolver.py +26 -46
- modal/_runtime/asgi.py +4 -3
- modal/_runtime/container_io_manager.py +358 -220
- modal/_runtime/container_io_manager.pyi +296 -101
- modal/_runtime/execution_context.py +18 -2
- modal/_runtime/execution_context.pyi +64 -7
- modal/_runtime/gpu_memory_snapshot.py +262 -57
- modal/_runtime/user_code_imports.py +28 -58
- modal/_serialization.py +90 -6
- modal/_traceback.py +42 -1
- modal/_tunnel.pyi +380 -12
- modal/_utils/async_utils.py +84 -29
- modal/_utils/auth_token_manager.py +111 -0
- modal/_utils/blob_utils.py +181 -58
- modal/_utils/deprecation.py +19 -0
- modal/_utils/function_utils.py +91 -47
- modal/_utils/grpc_utils.py +89 -66
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/name_utils.py +17 -3
- modal/_utils/task_command_router_client.py +536 -0
- modal/_utils/time_utils.py +34 -6
- modal/app.py +256 -88
- modal/app.pyi +909 -92
- modal/billing.py +5 -0
- modal/builder/2025.06.txt +18 -0
- modal/builder/PREVIEW.txt +18 -0
- modal/builder/base-images.json +58 -0
- modal/cli/_download.py +19 -3
- modal/cli/_traceback.py +3 -2
- modal/cli/app.py +4 -4
- modal/cli/cluster.py +15 -7
- modal/cli/config.py +5 -3
- modal/cli/container.py +7 -6
- modal/cli/dict.py +22 -16
- modal/cli/entry_point.py +12 -5
- modal/cli/environment.py +5 -4
- modal/cli/import_refs.py +3 -3
- modal/cli/launch.py +102 -5
- modal/cli/network_file_system.py +11 -12
- modal/cli/profile.py +3 -2
- modal/cli/programs/launch_instance_ssh.py +94 -0
- modal/cli/programs/run_jupyter.py +1 -1
- modal/cli/programs/run_marimo.py +95 -0
- modal/cli/programs/vscode.py +1 -1
- modal/cli/queues.py +57 -26
- modal/cli/run.py +91 -23
- modal/cli/secret.py +48 -22
- modal/cli/token.py +7 -8
- modal/cli/utils.py +4 -7
- modal/cli/volume.py +31 -25
- modal/client.py +15 -85
- modal/client.pyi +183 -62
- modal/cloud_bucket_mount.py +5 -3
- modal/cloud_bucket_mount.pyi +197 -5
- modal/cls.py +200 -126
- modal/cls.pyi +446 -68
- modal/config.py +29 -11
- modal/container_process.py +319 -19
- modal/container_process.pyi +190 -20
- modal/dict.py +290 -71
- modal/dict.pyi +835 -83
- modal/environments.py +15 -27
- modal/environments.pyi +46 -24
- modal/exception.py +14 -2
- modal/experimental/__init__.py +194 -40
- modal/experimental/flash.py +618 -0
- modal/experimental/flash.pyi +380 -0
- modal/experimental/ipython.py +11 -7
- modal/file_io.py +29 -36
- modal/file_io.pyi +251 -53
- modal/file_pattern_matcher.py +56 -16
- modal/functions.pyi +673 -92
- modal/gpu.py +1 -1
- modal/image.py +528 -176
- modal/image.pyi +1572 -145
- modal/io_streams.py +458 -128
- modal/io_streams.pyi +433 -52
- modal/mount.py +216 -151
- modal/mount.pyi +225 -78
- modal/network_file_system.py +45 -62
- modal/network_file_system.pyi +277 -56
- modal/object.pyi +93 -17
- modal/parallel_map.py +942 -129
- modal/parallel_map.pyi +294 -15
- modal/partial_function.py +0 -2
- modal/partial_function.pyi +234 -19
- modal/proxy.py +17 -8
- modal/proxy.pyi +36 -3
- modal/queue.py +270 -65
- modal/queue.pyi +817 -57
- modal/runner.py +115 -101
- modal/runner.pyi +205 -49
- modal/sandbox.py +512 -136
- modal/sandbox.pyi +845 -111
- modal/schedule.py +1 -1
- modal/secret.py +300 -70
- modal/secret.pyi +589 -34
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +11 -8
- modal/snapshot.pyi +25 -4
- modal/token_flow.py +4 -4
- modal/token_flow.pyi +28 -8
- modal/volume.py +416 -158
- modal/volume.pyi +1117 -121
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +10 -9
- modal-1.2.3.dev7.dist-info/RECORD +195 -0
- modal_docs/mdmd/mdmd.py +17 -4
- modal_proto/api.proto +534 -79
- modal_proto/api_grpc.py +337 -1
- modal_proto/api_pb2.py +1522 -968
- modal_proto/api_pb2.pyi +1619 -134
- modal_proto/api_pb2_grpc.py +699 -4
- modal_proto/api_pb2_grpc.pyi +226 -14
- modal_proto/modal_api_grpc.py +175 -154
- modal_proto/sandbox_router.proto +145 -0
- modal_proto/sandbox_router_grpc.py +105 -0
- modal_proto/sandbox_router_pb2.py +149 -0
- modal_proto/sandbox_router_pb2.pyi +333 -0
- modal_proto/sandbox_router_pb2_grpc.py +203 -0
- modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
- modal_proto/task_command_router.proto +144 -0
- modal_proto/task_command_router_grpc.py +105 -0
- modal_proto/task_command_router_pb2.py +149 -0
- modal_proto/task_command_router_pb2.pyi +333 -0
- modal_proto/task_command_router_pb2_grpc.py +203 -0
- modal_proto/task_command_router_pb2_grpc.pyi +75 -0
- modal_version/__init__.py +1 -1
- modal/requirements/PREVIEW.txt +0 -16
- modal/requirements/base-images.json +0 -26
- modal-1.0.3.dev10.dist-info/RECORD +0 -179
- 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/{requirements → builder}/2023.12.312.txt +0 -0
- /modal/{requirements → builder}/2023.12.txt +0 -0
- /modal/{requirements → builder}/2024.04.txt +0 -0
- /modal/{requirements → builder}/2024.10.txt +0 -0
- /modal/{requirements → builder}/README.md +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
modal/volume.py
CHANGED
|
@@ -11,6 +11,7 @@ import time
|
|
|
11
11
|
import typing
|
|
12
12
|
from collections.abc import AsyncGenerator, AsyncIterator, Generator, Sequence
|
|
13
13
|
from dataclasses import dataclass
|
|
14
|
+
from datetime import datetime
|
|
14
15
|
from io import BytesIO
|
|
15
16
|
from pathlib import Path, PurePosixPath
|
|
16
17
|
from typing import (
|
|
@@ -24,13 +25,22 @@ from typing import (
|
|
|
24
25
|
|
|
25
26
|
from google.protobuf.message import Message
|
|
26
27
|
from grpclib import GRPCError, Status
|
|
28
|
+
from synchronicity import classproperty
|
|
27
29
|
from synchronicity.async_wrap import asynccontextmanager
|
|
28
30
|
|
|
31
|
+
import modal.exception
|
|
29
32
|
import modal_proto.api_pb2
|
|
30
|
-
from modal.exception import InvalidError, VolumeUploadTimeoutError
|
|
33
|
+
from modal.exception import AlreadyExistsError, InvalidError, NotFoundError, VolumeUploadTimeoutError
|
|
31
34
|
from modal_proto import api_pb2
|
|
32
35
|
|
|
33
|
-
from .
|
|
36
|
+
from ._load_context import LoadContext
|
|
37
|
+
from ._object import (
|
|
38
|
+
EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
|
|
39
|
+
_get_environment_name,
|
|
40
|
+
_Object,
|
|
41
|
+
live_method,
|
|
42
|
+
live_method_gen,
|
|
43
|
+
)
|
|
34
44
|
from ._resolver import Resolver
|
|
35
45
|
from ._utils.async_utils import (
|
|
36
46
|
TaskContext,
|
|
@@ -45,15 +55,15 @@ from ._utils.blob_utils import (
|
|
|
45
55
|
BLOCK_SIZE,
|
|
46
56
|
FileUploadSpec,
|
|
47
57
|
FileUploadSpec2,
|
|
48
|
-
blob_iter,
|
|
49
58
|
blob_upload_file,
|
|
50
59
|
get_file_upload_spec_from_fileobj,
|
|
51
60
|
get_file_upload_spec_from_path,
|
|
52
61
|
)
|
|
53
|
-
from ._utils.deprecation import deprecation_warning
|
|
54
|
-
from ._utils.grpc_utils import
|
|
62
|
+
from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
|
|
63
|
+
from ._utils.grpc_utils import Retry
|
|
55
64
|
from ._utils.http_utils import ClientSessionRegistry
|
|
56
65
|
from ._utils.name_utils import check_object_name
|
|
66
|
+
from ._utils.time_utils import as_timestamp, timestamp_to_localized_dt
|
|
57
67
|
from .client import _Client
|
|
58
68
|
from .config import logger
|
|
59
69
|
|
|
@@ -92,6 +102,191 @@ class FileEntry:
|
|
|
92
102
|
)
|
|
93
103
|
|
|
94
104
|
|
|
105
|
+
@dataclass
|
|
106
|
+
class VolumeInfo:
|
|
107
|
+
"""Information about the Volume object."""
|
|
108
|
+
|
|
109
|
+
# This dataclass should be limited to information that is unchanging over the lifetime of the Volume,
|
|
110
|
+
# since it is transmitted from the server when the object is hydrated and could be stale when accessed.
|
|
111
|
+
|
|
112
|
+
name: Optional[str]
|
|
113
|
+
created_at: datetime
|
|
114
|
+
created_by: Optional[str]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class _VolumeManager:
|
|
118
|
+
"""Namespace with methods for managing named Volume objects."""
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
async def create(
|
|
122
|
+
name: str, # Name to use for the new Volume
|
|
123
|
+
*,
|
|
124
|
+
version: Optional[int] = None, # Experimental: Configure the backend VolumeFS version
|
|
125
|
+
allow_existing: bool = False, # If True, no-op when the Volume already exists
|
|
126
|
+
environment_name: Optional[str] = None, # Uses active environment if not specified
|
|
127
|
+
client: Optional[_Client] = None, # Optional client with Modal credentials
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Create a new Volume object.
|
|
130
|
+
|
|
131
|
+
**Examples:**
|
|
132
|
+
|
|
133
|
+
```python notest
|
|
134
|
+
modal.Volume.objects.create("my-volume")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Volumes will be created in the active environment, or another one can be specified:
|
|
138
|
+
|
|
139
|
+
```python notest
|
|
140
|
+
modal.Volume.objects.create("my-volume", environment_name="dev")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
By default, an error will be raised if the Volume already exists, but passing
|
|
144
|
+
`allow_existing=True` will make the creation attempt a no-op in this case.
|
|
145
|
+
|
|
146
|
+
```python notest
|
|
147
|
+
modal.Volume.objects.create("my-volume", allow_existing=True)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Note that this method does not return a local instance of the Volume. You can use
|
|
151
|
+
`modal.Volume.from_name` to perform a lookup after creation.
|
|
152
|
+
|
|
153
|
+
Added in v1.1.2.
|
|
154
|
+
|
|
155
|
+
"""
|
|
156
|
+
check_object_name(name, "Volume")
|
|
157
|
+
client = await _Client.from_env() if client is None else client
|
|
158
|
+
object_creation_type = (
|
|
159
|
+
api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING
|
|
160
|
+
if allow_existing
|
|
161
|
+
else api_pb2.OBJECT_CREATION_TYPE_CREATE_FAIL_IF_EXISTS
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if version is not None and version not in {1, 2}:
|
|
165
|
+
raise InvalidError("VolumeFS version must be either 1 or 2")
|
|
166
|
+
|
|
167
|
+
req = api_pb2.VolumeGetOrCreateRequest(
|
|
168
|
+
deployment_name=name,
|
|
169
|
+
environment_name=_get_environment_name(environment_name),
|
|
170
|
+
object_creation_type=object_creation_type,
|
|
171
|
+
version=version,
|
|
172
|
+
)
|
|
173
|
+
try:
|
|
174
|
+
await client.stub.VolumeGetOrCreate(req)
|
|
175
|
+
except GRPCError as exc:
|
|
176
|
+
if exc.status == Status.ALREADY_EXISTS and not allow_existing:
|
|
177
|
+
raise AlreadyExistsError(exc.message)
|
|
178
|
+
else:
|
|
179
|
+
raise
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
async def list(
|
|
183
|
+
*,
|
|
184
|
+
max_objects: Optional[int] = None, # Limit requests to this size
|
|
185
|
+
created_before: Optional[Union[datetime, str]] = None, # Limit based on creation date
|
|
186
|
+
environment_name: str = "", # Uses active environment if not specified
|
|
187
|
+
client: Optional[_Client] = None, # Optional client with Modal credentials
|
|
188
|
+
) -> list["_Volume"]:
|
|
189
|
+
"""Return a list of hydrated Volume objects.
|
|
190
|
+
|
|
191
|
+
**Examples:**
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
volumes = modal.Volume.objects.list()
|
|
195
|
+
print([v.name for v in volumes])
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Volumes will be retreived from the active environment, or another one can be specified:
|
|
199
|
+
|
|
200
|
+
```python notest
|
|
201
|
+
dev_volumes = modal.Volume.objects.list(environment_name="dev")
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
By default, all named Volumes are returned, newest to oldest. It's also possible to limit the
|
|
205
|
+
number of results and to filter by creation date:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
volumes = modal.Volume.objects.list(max_objects=10, created_before="2025-01-01")
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Added in v1.1.2.
|
|
212
|
+
|
|
213
|
+
"""
|
|
214
|
+
client = await _Client.from_env() if client is None else client
|
|
215
|
+
if max_objects is not None and max_objects < 0:
|
|
216
|
+
raise InvalidError("max_objects cannot be negative")
|
|
217
|
+
|
|
218
|
+
items: list[api_pb2.VolumeListItem] = []
|
|
219
|
+
|
|
220
|
+
async def retrieve_page(created_before: float) -> bool:
|
|
221
|
+
max_page_size = 100 if max_objects is None else min(100, max_objects - len(items))
|
|
222
|
+
pagination = api_pb2.ListPagination(max_objects=max_page_size, created_before=created_before)
|
|
223
|
+
req = api_pb2.VolumeListRequest(
|
|
224
|
+
environment_name=_get_environment_name(environment_name), pagination=pagination
|
|
225
|
+
)
|
|
226
|
+
resp = await client.stub.VolumeList(req)
|
|
227
|
+
items.extend(resp.items)
|
|
228
|
+
finished = (len(resp.items) < max_page_size) or (max_objects is not None and len(items) >= max_objects)
|
|
229
|
+
return finished
|
|
230
|
+
|
|
231
|
+
finished = await retrieve_page(as_timestamp(created_before))
|
|
232
|
+
while True:
|
|
233
|
+
if finished:
|
|
234
|
+
break
|
|
235
|
+
finished = await retrieve_page(items[-1].metadata.creation_info.created_at)
|
|
236
|
+
|
|
237
|
+
volumes = [
|
|
238
|
+
_Volume._new_hydrated(
|
|
239
|
+
item.volume_id,
|
|
240
|
+
client,
|
|
241
|
+
item.metadata,
|
|
242
|
+
is_another_app=True,
|
|
243
|
+
rep=_Volume._repr(item.label, environment_name),
|
|
244
|
+
)
|
|
245
|
+
for item in items
|
|
246
|
+
]
|
|
247
|
+
return volumes[:max_objects] if max_objects is not None else volumes
|
|
248
|
+
|
|
249
|
+
@staticmethod
|
|
250
|
+
async def delete(
|
|
251
|
+
name: str, # Name of the Volume to delete
|
|
252
|
+
*,
|
|
253
|
+
allow_missing: bool = False, # If True, don't raise an error if the Volume doesn't exist
|
|
254
|
+
environment_name: Optional[str] = None, # Uses active environment if not specified
|
|
255
|
+
client: Optional[_Client] = None, # Optional client with Modal credentials
|
|
256
|
+
):
|
|
257
|
+
"""Delete a named Volume.
|
|
258
|
+
|
|
259
|
+
Warning: This deletes an *entire Volume*, not just a specific file.
|
|
260
|
+
Deletion is irreversible and will affect any Apps currently using the Volume.
|
|
261
|
+
|
|
262
|
+
**Examples:**
|
|
263
|
+
|
|
264
|
+
```python notest
|
|
265
|
+
await modal.Volume.objects.delete("my-volume")
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Volumes will be deleted from the active environment, or another one can be specified:
|
|
269
|
+
|
|
270
|
+
```python notest
|
|
271
|
+
await modal.Volume.objects.delete("my-volume", environment_name="dev")
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Added in v1.1.2.
|
|
275
|
+
|
|
276
|
+
"""
|
|
277
|
+
try:
|
|
278
|
+
obj = await _Volume.from_name(name, environment_name=environment_name).hydrate(client)
|
|
279
|
+
except NotFoundError:
|
|
280
|
+
if not allow_missing:
|
|
281
|
+
raise
|
|
282
|
+
else:
|
|
283
|
+
req = api_pb2.VolumeDeleteRequest(volume_id=obj.object_id)
|
|
284
|
+
await obj._client.stub.VolumeDelete(req)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
VolumeManager = synchronize_api(_VolumeManager)
|
|
288
|
+
|
|
289
|
+
|
|
95
290
|
class _Volume(_Object, type_prefix="vo"):
|
|
96
291
|
"""A writeable volume that can be used to share files between one or more Modal functions.
|
|
97
292
|
|
|
@@ -136,6 +331,61 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
136
331
|
|
|
137
332
|
_lock: Optional[asyncio.Lock] = None
|
|
138
333
|
_metadata: "typing.Optional[api_pb2.VolumeMetadata]"
|
|
334
|
+
_read_only: bool = False
|
|
335
|
+
|
|
336
|
+
@classproperty
|
|
337
|
+
def objects(cls) -> _VolumeManager:
|
|
338
|
+
return _VolumeManager
|
|
339
|
+
|
|
340
|
+
@property
|
|
341
|
+
def name(self) -> Optional[str]:
|
|
342
|
+
return self._name
|
|
343
|
+
|
|
344
|
+
def read_only(self) -> "_Volume":
|
|
345
|
+
"""Configure Volume to mount as read-only.
|
|
346
|
+
|
|
347
|
+
**Example**
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
import modal
|
|
351
|
+
|
|
352
|
+
volume = modal.Volume.from_name("my-volume", create_if_missing=True)
|
|
353
|
+
|
|
354
|
+
@app.function(volumes={"/mnt/items": volume.read_only()})
|
|
355
|
+
def f():
|
|
356
|
+
with open("/mnt/items/my-file.txt") as f:
|
|
357
|
+
return f.read()
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
The Volume is mounted as a read-only volume in a function. Any file system write operation into the
|
|
361
|
+
mounted volume will result in an error.
|
|
362
|
+
|
|
363
|
+
Added in v1.0.5.
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
async def _load(
|
|
367
|
+
new_volume: _Volume, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
368
|
+
):
|
|
369
|
+
new_volume._initialize_from_other(self)
|
|
370
|
+
new_volume._read_only = True
|
|
371
|
+
|
|
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
|
+
)
|
|
379
|
+
return obj
|
|
380
|
+
|
|
381
|
+
def _hydrate_metadata(self, metadata: Optional[Message]):
|
|
382
|
+
if metadata:
|
|
383
|
+
assert isinstance(metadata, api_pb2.VolumeMetadata)
|
|
384
|
+
self._metadata = metadata
|
|
385
|
+
self._name = metadata.name
|
|
386
|
+
|
|
387
|
+
def _get_metadata(self) -> Optional[Message]:
|
|
388
|
+
return self._metadata
|
|
139
389
|
|
|
140
390
|
async def _get_lock(self):
|
|
141
391
|
# To (mostly*) prevent multiple concurrent operations on the same volume, which can cause problems under
|
|
@@ -151,20 +401,29 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
151
401
|
self._lock = asyncio.Lock()
|
|
152
402
|
return self._lock
|
|
153
403
|
|
|
404
|
+
@property
|
|
405
|
+
def _is_v1(self) -> bool:
|
|
406
|
+
return self._metadata.version in [
|
|
407
|
+
None,
|
|
408
|
+
api_pb2.VolumeFsVersion.VOLUME_FS_VERSION_UNSPECIFIED,
|
|
409
|
+
api_pb2.VolumeFsVersion.VOLUME_FS_VERSION_V1,
|
|
410
|
+
]
|
|
411
|
+
|
|
154
412
|
@staticmethod
|
|
155
413
|
def from_name(
|
|
156
414
|
name: str,
|
|
157
415
|
*,
|
|
158
|
-
namespace=
|
|
416
|
+
namespace=None, # mdmd:line-hidden
|
|
159
417
|
environment_name: Optional[str] = None,
|
|
160
418
|
create_if_missing: bool = False,
|
|
161
419
|
version: "typing.Optional[modal_proto.api_pb2.VolumeFsVersion.ValueType]" = None,
|
|
420
|
+
client: Optional[_Client] = None,
|
|
162
421
|
) -> "_Volume":
|
|
163
422
|
"""Reference a Volume by name, creating if necessary.
|
|
164
423
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
424
|
+
This is a lazy method that defers hydrating the local
|
|
425
|
+
object with metadata from Modal servers until the first
|
|
426
|
+
time is is actually used.
|
|
168
427
|
|
|
169
428
|
```python
|
|
170
429
|
vol = modal.Volume.from_name("my-volume", create_if_missing=True)
|
|
@@ -178,36 +437,28 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
178
437
|
```
|
|
179
438
|
"""
|
|
180
439
|
check_object_name(name, "Volume")
|
|
440
|
+
warn_if_passing_namespace(namespace, "modal.Volume.from_name")
|
|
181
441
|
|
|
182
|
-
async def _load(
|
|
442
|
+
async def _load(
|
|
443
|
+
self: _Volume, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
444
|
+
):
|
|
183
445
|
req = api_pb2.VolumeGetOrCreateRequest(
|
|
184
446
|
deployment_name=name,
|
|
185
|
-
|
|
186
|
-
environment_name=_get_environment_name(environment_name, resolver),
|
|
447
|
+
environment_name=load_context.environment_name,
|
|
187
448
|
object_creation_type=(api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING if create_if_missing else None),
|
|
188
449
|
version=version,
|
|
189
450
|
)
|
|
190
|
-
response = await
|
|
191
|
-
self._hydrate(response.volume_id,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def _get_metadata(self) -> Optional[Message]:
|
|
202
|
-
return self._metadata
|
|
203
|
-
|
|
204
|
-
@property
|
|
205
|
-
def _is_v1(self) -> bool:
|
|
206
|
-
return self._metadata.version in [
|
|
207
|
-
None,
|
|
208
|
-
api_pb2.VolumeFsVersion.VOLUME_FS_VERSION_UNSPECIFIED,
|
|
209
|
-
api_pb2.VolumeFsVersion.VOLUME_FS_VERSION_V1,
|
|
210
|
-
]
|
|
451
|
+
response = await load_context.client.stub.VolumeGetOrCreate(req)
|
|
452
|
+
self._hydrate(response.volume_id, load_context.client, response.metadata)
|
|
453
|
+
|
|
454
|
+
rep = _Volume._repr(name, environment_name)
|
|
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
|
+
)
|
|
211
462
|
|
|
212
463
|
@classmethod
|
|
213
464
|
@asynccontextmanager
|
|
@@ -216,7 +467,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
216
467
|
client: Optional[_Client] = None,
|
|
217
468
|
environment_name: Optional[str] = None,
|
|
218
469
|
version: "typing.Optional[modal_proto.api_pb2.VolumeFsVersion.ValueType]" = None,
|
|
219
|
-
_heartbeat_sleep: float = EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
|
|
470
|
+
_heartbeat_sleep: float = EPHEMERAL_OBJECT_HEARTBEAT_SLEEP, # mdmd:line-hidden
|
|
220
471
|
) -> AsyncGenerator["_Volume", None]:
|
|
221
472
|
"""Creates a new ephemeral volume within a context manager:
|
|
222
473
|
|
|
@@ -243,80 +494,74 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
243
494
|
async with TaskContext() as tc:
|
|
244
495
|
request = api_pb2.VolumeHeartbeatRequest(volume_id=response.volume_id)
|
|
245
496
|
tc.infinite_loop(lambda: client.stub.VolumeHeartbeat(request), sleep=_heartbeat_sleep)
|
|
246
|
-
yield cls._new_hydrated(
|
|
497
|
+
yield cls._new_hydrated(
|
|
498
|
+
response.volume_id,
|
|
499
|
+
client,
|
|
500
|
+
response.metadata,
|
|
501
|
+
is_another_app=True,
|
|
502
|
+
rep="modal.Volume.ephemeral()",
|
|
503
|
+
)
|
|
247
504
|
|
|
248
505
|
@staticmethod
|
|
249
|
-
async def
|
|
250
|
-
|
|
251
|
-
namespace=
|
|
506
|
+
async def create_deployed(
|
|
507
|
+
deployment_name: str,
|
|
508
|
+
namespace=None, # mdmd:line-hidden
|
|
252
509
|
client: Optional[_Client] = None,
|
|
253
510
|
environment_name: Optional[str] = None,
|
|
254
|
-
create_if_missing: bool = False,
|
|
255
511
|
version: "typing.Optional[modal_proto.api_pb2.VolumeFsVersion.ValueType]" = None,
|
|
256
|
-
) ->
|
|
257
|
-
"""mdmd:hidden
|
|
258
|
-
Lookup a named Volume.
|
|
259
|
-
|
|
260
|
-
DEPRECATED: This method is deprecated in favor of `modal.Volume.from_name`.
|
|
261
|
-
|
|
262
|
-
In contrast to `modal.Volume.from_name`, this is an eager method
|
|
263
|
-
that will hydrate the local object with metadata from Modal servers.
|
|
264
|
-
|
|
265
|
-
```python notest
|
|
266
|
-
vol = modal.Volume.from_name("my-volume")
|
|
267
|
-
print(vol.listdir("/"))
|
|
268
|
-
```
|
|
269
|
-
"""
|
|
512
|
+
) -> str:
|
|
513
|
+
"""mdmd:hidden"""
|
|
270
514
|
deprecation_warning(
|
|
271
|
-
(2025,
|
|
272
|
-
"`modal.Volume.
|
|
273
|
-
" It can be replaced with `modal.Volume.
|
|
274
|
-
"\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
|
|
275
|
-
)
|
|
276
|
-
obj = _Volume.from_name(
|
|
277
|
-
name,
|
|
278
|
-
namespace=namespace,
|
|
279
|
-
environment_name=environment_name,
|
|
280
|
-
create_if_missing=create_if_missing,
|
|
281
|
-
version=version,
|
|
515
|
+
(2025, 8, 13),
|
|
516
|
+
"The undocumented `modal.Volume.create_deployed` method is deprecated and will be removed "
|
|
517
|
+
"in a future release. It can be replaced with `modal.Volume.objects.create`.",
|
|
282
518
|
)
|
|
283
|
-
|
|
284
|
-
client = await _Client.from_env()
|
|
285
|
-
resolver = Resolver(client=client)
|
|
286
|
-
await resolver.load(obj)
|
|
287
|
-
return obj
|
|
519
|
+
return await _Volume._create_deployed(deployment_name, namespace, client, environment_name, version)
|
|
288
520
|
|
|
289
521
|
@staticmethod
|
|
290
|
-
async def
|
|
522
|
+
async def _create_deployed(
|
|
291
523
|
deployment_name: str,
|
|
292
|
-
namespace=
|
|
524
|
+
namespace=None, # mdmd:line-hidden
|
|
293
525
|
client: Optional[_Client] = None,
|
|
294
526
|
environment_name: Optional[str] = None,
|
|
295
527
|
version: "typing.Optional[modal_proto.api_pb2.VolumeFsVersion.ValueType]" = None,
|
|
296
528
|
) -> str:
|
|
297
529
|
"""mdmd:hidden"""
|
|
298
530
|
check_object_name(deployment_name, "Volume")
|
|
531
|
+
warn_if_passing_namespace(namespace, "modal.Volume.create_deployed")
|
|
299
532
|
if client is None:
|
|
300
533
|
client = await _Client.from_env()
|
|
301
534
|
request = api_pb2.VolumeGetOrCreateRequest(
|
|
302
535
|
deployment_name=deployment_name,
|
|
303
|
-
namespace=namespace,
|
|
304
536
|
environment_name=_get_environment_name(environment_name),
|
|
305
537
|
object_creation_type=api_pb2.OBJECT_CREATION_TYPE_CREATE_FAIL_IF_EXISTS,
|
|
306
538
|
version=version,
|
|
307
539
|
)
|
|
308
|
-
resp = await
|
|
540
|
+
resp = await client.stub.VolumeGetOrCreate(request)
|
|
309
541
|
return resp.volume_id
|
|
310
542
|
|
|
543
|
+
@live_method
|
|
544
|
+
async def info(self) -> VolumeInfo:
|
|
545
|
+
"""Return information about the Volume object."""
|
|
546
|
+
metadata = self._get_metadata()
|
|
547
|
+
if not metadata:
|
|
548
|
+
return VolumeInfo()
|
|
549
|
+
creation_info = metadata.creation_info
|
|
550
|
+
return VolumeInfo(
|
|
551
|
+
name=metadata.name or None,
|
|
552
|
+
created_at=timestamp_to_localized_dt(creation_info.created_at),
|
|
553
|
+
created_by=creation_info.created_by or None,
|
|
554
|
+
)
|
|
555
|
+
|
|
311
556
|
@live_method
|
|
312
557
|
async def _do_reload(self, lock=True):
|
|
313
558
|
async with (await self._get_lock()) if lock else asyncnullcontext():
|
|
314
559
|
req = api_pb2.VolumeReloadRequest(volume_id=self.object_id)
|
|
315
|
-
_ = await
|
|
560
|
+
_ = await self._client.stub.VolumeReload(req)
|
|
316
561
|
|
|
317
562
|
@live_method
|
|
318
563
|
async def commit(self):
|
|
319
|
-
"""Commit changes to
|
|
564
|
+
"""Commit changes to a mounted volume.
|
|
320
565
|
|
|
321
566
|
If successful, the changes made are now persisted in durable storage and available to other containers accessing
|
|
322
567
|
the volume.
|
|
@@ -325,7 +570,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
325
570
|
req = api_pb2.VolumeCommitRequest(volume_id=self.object_id)
|
|
326
571
|
try:
|
|
327
572
|
# TODO(gongy): only apply indefinite retries on 504 status.
|
|
328
|
-
resp = await
|
|
573
|
+
resp = await self._client.stub.VolumeCommit(req, retry=Retry(max_retries=90))
|
|
329
574
|
if not resp.skip_reload:
|
|
330
575
|
# Reload changes on successful commit.
|
|
331
576
|
await self._do_reload(lock=False)
|
|
@@ -400,10 +645,14 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
400
645
|
return [entry async for entry in self.iterdir(path, recursive=recursive)]
|
|
401
646
|
|
|
402
647
|
@live_method_gen
|
|
403
|
-
def read_file(self, path: str) -> AsyncIterator[bytes]:
|
|
648
|
+
async def read_file(self, path: str) -> AsyncIterator[bytes]:
|
|
404
649
|
"""
|
|
405
650
|
Read a file from the modal.Volume.
|
|
406
651
|
|
|
652
|
+
Note - this function is primarily intended to be used outside of a Modal App.
|
|
653
|
+
For more information on downloading files from a Modal Volume, see
|
|
654
|
+
[the guide](https://modal.com/docs/guide/volumes).
|
|
655
|
+
|
|
407
656
|
**Example:**
|
|
408
657
|
|
|
409
658
|
```python notest
|
|
@@ -414,32 +663,17 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
414
663
|
print(len(data)) # == 1024 * 1024
|
|
415
664
|
```
|
|
416
665
|
"""
|
|
417
|
-
return self._read_file1(path) if self._is_v1 else self._read_file2(path)
|
|
418
|
-
|
|
419
|
-
async def _read_file1(self, path: str) -> AsyncIterator[bytes]:
|
|
420
|
-
req = api_pb2.VolumeGetFileRequest(volume_id=self.object_id, path=path)
|
|
421
|
-
try:
|
|
422
|
-
response = await retry_transient_errors(self._client.stub.VolumeGetFile, req)
|
|
423
|
-
except GRPCError as exc:
|
|
424
|
-
raise FileNotFoundError(exc.message) if exc.status == Status.NOT_FOUND else exc
|
|
425
|
-
# TODO(Jonathon): use ranged requests.
|
|
426
|
-
if response.WhichOneof("data_oneof") == "data":
|
|
427
|
-
yield response.data
|
|
428
|
-
return
|
|
429
|
-
else:
|
|
430
|
-
async for data in blob_iter(response.data_blob_id, self._client.stub):
|
|
431
|
-
yield data
|
|
432
|
-
|
|
433
|
-
async def _read_file2(self, path: str) -> AsyncIterator[bytes]:
|
|
434
666
|
req = api_pb2.VolumeGetFile2Request(volume_id=self.object_id, path=path)
|
|
435
667
|
|
|
436
668
|
try:
|
|
437
|
-
response = await
|
|
438
|
-
except
|
|
439
|
-
raise FileNotFoundError(exc.
|
|
669
|
+
response = await self._client.stub.VolumeGetFile2(req)
|
|
670
|
+
except modal.exception.NotFoundError as exc:
|
|
671
|
+
raise FileNotFoundError(exc.args[0])
|
|
440
672
|
|
|
673
|
+
@retry(n_attempts=5, base_delay=0.1, timeout=None)
|
|
441
674
|
async def read_block(block_url: str) -> bytes:
|
|
442
675
|
async with ClientSessionRegistry.get_session().get(block_url) as get_response:
|
|
676
|
+
get_response.raise_for_status()
|
|
443
677
|
return await get_response.content.read()
|
|
444
678
|
|
|
445
679
|
async def iter_urls() -> AsyncGenerator[str]:
|
|
@@ -455,59 +689,53 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
455
689
|
|
|
456
690
|
@live_method
|
|
457
691
|
async def read_file_into_fileobj(
|
|
458
|
-
self,
|
|
692
|
+
self,
|
|
693
|
+
path: str,
|
|
694
|
+
fileobj: typing.IO[bytes],
|
|
695
|
+
progress_cb: Optional[Callable[..., Any]] = None,
|
|
459
696
|
) -> int:
|
|
460
697
|
"""mdmd:hidden
|
|
461
698
|
Read volume file into file-like IO object.
|
|
462
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:
|
|
463
711
|
if progress_cb is None:
|
|
464
712
|
|
|
465
713
|
def progress_cb(*_, **__):
|
|
466
714
|
pass
|
|
467
715
|
|
|
468
|
-
if
|
|
469
|
-
|
|
470
|
-
else:
|
|
471
|
-
return await self._read_file_into_fileobj2(path, fileobj, progress_cb)
|
|
472
|
-
|
|
473
|
-
async def _read_file_into_fileobj1(
|
|
474
|
-
self, path: str, fileobj: typing.IO[bytes], progress_cb: Callable[..., Any]
|
|
475
|
-
) -> int:
|
|
476
|
-
num_bytes_written = 0
|
|
477
|
-
|
|
478
|
-
async for chunk in self._read_file1(path):
|
|
479
|
-
num_chunk_bytes_written = 0
|
|
480
|
-
|
|
481
|
-
while num_chunk_bytes_written < len(chunk):
|
|
482
|
-
# TODO(dflemstr): this is a small write, but nonetheless might block the event loop for some time:
|
|
483
|
-
n = fileobj.write(chunk)
|
|
484
|
-
num_chunk_bytes_written += n
|
|
485
|
-
progress_cb(advance=n)
|
|
486
|
-
|
|
487
|
-
num_bytes_written += len(chunk)
|
|
716
|
+
if concurrency is None:
|
|
717
|
+
concurrency = multiprocessing.cpu_count()
|
|
488
718
|
|
|
489
|
-
return num_bytes_written
|
|
490
|
-
|
|
491
|
-
async def _read_file_into_fileobj2(
|
|
492
|
-
self, path: str, fileobj: typing.IO[bytes], progress_cb: Callable[..., Any]
|
|
493
|
-
) -> int:
|
|
494
719
|
req = api_pb2.VolumeGetFile2Request(volume_id=self.object_id, path=path)
|
|
495
720
|
|
|
496
721
|
try:
|
|
497
|
-
response = await
|
|
498
|
-
except
|
|
499
|
-
raise FileNotFoundError(exc.
|
|
722
|
+
response = await self._client.stub.VolumeGetFile2(req)
|
|
723
|
+
except modal.exception.NotFoundError as exc:
|
|
724
|
+
raise FileNotFoundError(exc.args[0])
|
|
725
|
+
|
|
726
|
+
if download_semaphore is None:
|
|
727
|
+
download_semaphore = asyncio.Semaphore(concurrency)
|
|
500
728
|
|
|
501
|
-
# TODO(dflemstr): Sane default limit? Make configurable?
|
|
502
|
-
download_semaphore = asyncio.Semaphore(multiprocessing.cpu_count())
|
|
503
729
|
write_lock = asyncio.Lock()
|
|
504
730
|
start_pos = fileobj.tell()
|
|
505
731
|
|
|
732
|
+
@retry(n_attempts=5, base_delay=0.1, timeout=None)
|
|
506
733
|
async def download_block(idx, url) -> int:
|
|
507
734
|
block_start_pos = start_pos + idx * BLOCK_SIZE
|
|
508
735
|
num_bytes_written = 0
|
|
509
736
|
|
|
510
737
|
async with download_semaphore, ClientSessionRegistry.get_session().get(url) as get_response:
|
|
738
|
+
get_response.raise_for_status()
|
|
511
739
|
async for chunk in get_response.content.iter_any():
|
|
512
740
|
num_chunk_bytes_written = 0
|
|
513
741
|
|
|
@@ -535,15 +763,21 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
535
763
|
@live_method
|
|
536
764
|
async def remove_file(self, path: str, recursive: bool = False) -> None:
|
|
537
765
|
"""Remove a file or directory from a volume."""
|
|
538
|
-
if self.
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
766
|
+
if self._read_only:
|
|
767
|
+
raise InvalidError("Read-only Volume can not be written to")
|
|
768
|
+
|
|
769
|
+
try:
|
|
770
|
+
if self._is_v1:
|
|
771
|
+
req = api_pb2.VolumeRemoveFileRequest(volume_id=self.object_id, path=path, recursive=recursive)
|
|
772
|
+
await self._client.stub.VolumeRemoveFile(req)
|
|
773
|
+
else:
|
|
774
|
+
req = api_pb2.VolumeRemoveFile2Request(volume_id=self.object_id, path=path, recursive=recursive)
|
|
775
|
+
await self._client.stub.VolumeRemoveFile2(req)
|
|
776
|
+
except modal.exception.NotFoundError as exc:
|
|
777
|
+
raise FileNotFoundError(exc.args[0])
|
|
544
778
|
|
|
545
779
|
@live_method
|
|
546
|
-
async def copy_files(self, src_paths: Sequence[str], dst_path: str) -> None:
|
|
780
|
+
async def copy_files(self, src_paths: Sequence[str], dst_path: str, recursive: bool = False) -> None:
|
|
547
781
|
"""
|
|
548
782
|
Copy files within the volume from src_paths to dst_path.
|
|
549
783
|
The semantics of the copy operation follow those of the UNIX cp command.
|
|
@@ -567,8 +801,22 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
567
801
|
like `os.rename()` and then `commit()` the volume. The `copy_files()` method is useful when you don't have
|
|
568
802
|
the volume mounted as a filesystem, e.g. when running a script on your local computer.
|
|
569
803
|
"""
|
|
570
|
-
|
|
571
|
-
|
|
804
|
+
if self._read_only:
|
|
805
|
+
raise InvalidError("Read-only Volume can not be written to")
|
|
806
|
+
|
|
807
|
+
if self._is_v1:
|
|
808
|
+
if recursive:
|
|
809
|
+
raise ValueError("`recursive` is not supported for V1 volumes")
|
|
810
|
+
|
|
811
|
+
request = api_pb2.VolumeCopyFilesRequest(
|
|
812
|
+
volume_id=self.object_id, src_paths=src_paths, dst_path=dst_path, recursive=recursive
|
|
813
|
+
)
|
|
814
|
+
await self._client.stub.VolumeCopyFiles(request, retry=Retry(base_delay=1))
|
|
815
|
+
else:
|
|
816
|
+
request = api_pb2.VolumeCopyFiles2Request(
|
|
817
|
+
volume_id=self.object_id, src_paths=src_paths, dst_path=dst_path, recursive=recursive
|
|
818
|
+
)
|
|
819
|
+
await self._client.stub.VolumeCopyFiles2(request, retry=Retry(base_delay=1))
|
|
572
820
|
|
|
573
821
|
@live_method
|
|
574
822
|
async def batch_upload(self, force: bool = False) -> "_AbstractVolumeUploadContextManager":
|
|
@@ -589,21 +837,33 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
589
837
|
batch.put_file(io.BytesIO(b"some data"), "/foobar")
|
|
590
838
|
```
|
|
591
839
|
"""
|
|
840
|
+
if self._read_only:
|
|
841
|
+
raise InvalidError("Read-only Volume can not be written to")
|
|
842
|
+
|
|
592
843
|
return _AbstractVolumeUploadContextManager.resolve(
|
|
593
844
|
self._metadata.version, self.object_id, self._client, force=force
|
|
594
845
|
)
|
|
595
846
|
|
|
596
847
|
@live_method
|
|
597
848
|
async def _instance_delete(self):
|
|
598
|
-
await
|
|
599
|
-
self._client.stub.VolumeDelete, api_pb2.VolumeDeleteRequest(volume_id=self.object_id)
|
|
600
|
-
)
|
|
849
|
+
await self._client.stub.VolumeDelete(api_pb2.VolumeDeleteRequest(volume_id=self.object_id))
|
|
601
850
|
|
|
602
851
|
@staticmethod
|
|
603
852
|
async def delete(name: str, client: Optional[_Client] = None, environment_name: Optional[str] = None):
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
853
|
+
"""mdmd:hidden
|
|
854
|
+
Delete a named Volume.
|
|
855
|
+
|
|
856
|
+
Warning: This deletes an *entire Volume*, not just a specific file.
|
|
857
|
+
Deletion is irreversible and will affect any Apps currently using the Volume.
|
|
858
|
+
|
|
859
|
+
DEPRECATED: This method is deprecated; we recommend using `modal.Volume.objects.delete` instead.
|
|
860
|
+
|
|
861
|
+
"""
|
|
862
|
+
deprecation_warning(
|
|
863
|
+
(2025, 8, 6),
|
|
864
|
+
"`modal.Volume.delete` is deprecated; we recommend using `modal.Volume.objects.delete` instead.",
|
|
865
|
+
)
|
|
866
|
+
await _Volume.objects.delete(name, environment_name=environment_name, client=client)
|
|
607
867
|
|
|
608
868
|
@staticmethod
|
|
609
869
|
async def rename(
|
|
@@ -615,7 +875,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
615
875
|
):
|
|
616
876
|
obj = await _Volume.from_name(old_name, environment_name=environment_name).hydrate(client)
|
|
617
877
|
req = api_pb2.VolumeRenameRequest(volume_id=obj.object_id, name=new_name)
|
|
618
|
-
await
|
|
878
|
+
await obj._client.stub.VolumeRename(req)
|
|
619
879
|
|
|
620
880
|
|
|
621
881
|
Volume = synchronize_api(_Volume)
|
|
@@ -716,7 +976,7 @@ class _VolumeUploadContextManager(_AbstractVolumeUploadContextManager):
|
|
|
716
976
|
disallow_overwrite_existing_files=not self._force,
|
|
717
977
|
)
|
|
718
978
|
try:
|
|
719
|
-
await
|
|
979
|
+
await self._client.stub.VolumePutFiles(request, retry=Retry(base_delay=1))
|
|
720
980
|
except GRPCError as exc:
|
|
721
981
|
raise FileExistsError(exc.message) if exc.status == Status.ALREADY_EXISTS else exc
|
|
722
982
|
|
|
@@ -776,7 +1036,7 @@ class _VolumeUploadContextManager(_AbstractVolumeUploadContextManager):
|
|
|
776
1036
|
remote_filename = file_spec.mount_filename
|
|
777
1037
|
progress_task_id = self._progress_cb(name=remote_filename, size=file_spec.size)
|
|
778
1038
|
request = api_pb2.MountPutFileRequest(sha256_hex=file_spec.sha256_hex)
|
|
779
|
-
response = await
|
|
1039
|
+
response = await self._client.stub.MountPutFile(request, retry=Retry(base_delay=1))
|
|
780
1040
|
|
|
781
1041
|
start_time = time.monotonic()
|
|
782
1042
|
if not response.exists:
|
|
@@ -800,7 +1060,7 @@ class _VolumeUploadContextManager(_AbstractVolumeUploadContextManager):
|
|
|
800
1060
|
self._progress_cb(task_id=progress_task_id, complete=True)
|
|
801
1061
|
|
|
802
1062
|
while (time.monotonic() - start_time) < VOLUME_PUT_FILE_CLIENT_TIMEOUT:
|
|
803
|
-
response = await
|
|
1063
|
+
response = await self._client.stub.MountPutFile(request2, retry=Retry(base_delay=1))
|
|
804
1064
|
if response.exists:
|
|
805
1065
|
break
|
|
806
1066
|
|
|
@@ -943,9 +1203,9 @@ class _VolumeUploadContextManager2(_AbstractVolumeUploadContextManager):
|
|
|
943
1203
|
for file_spec in file_specs:
|
|
944
1204
|
blocks = [
|
|
945
1205
|
api_pb2.VolumePutFiles2Request.Block(
|
|
946
|
-
contents_sha256=
|
|
1206
|
+
contents_sha256=block.contents_sha256, put_response=put_responses.get(block.contents_sha256)
|
|
947
1207
|
)
|
|
948
|
-
for
|
|
1208
|
+
for block in file_spec.blocks
|
|
949
1209
|
]
|
|
950
1210
|
files.append(
|
|
951
1211
|
api_pb2.VolumePutFiles2Request.File(
|
|
@@ -960,7 +1220,7 @@ class _VolumeUploadContextManager2(_AbstractVolumeUploadContextManager):
|
|
|
960
1220
|
)
|
|
961
1221
|
|
|
962
1222
|
try:
|
|
963
|
-
response = await
|
|
1223
|
+
response = await self._client.stub.VolumePutFiles2(request, retry=Retry(base_delay=1))
|
|
964
1224
|
except GRPCError as exc:
|
|
965
1225
|
raise FileExistsError(exc.message) if exc.status == Status.ALREADY_EXISTS else exc
|
|
966
1226
|
|
|
@@ -1002,7 +1262,7 @@ async def _put_missing_blocks(
|
|
|
1002
1262
|
# TODO(dflemstr): Type is `api_pb2.VolumePutFiles2Response.MissingBlock` but synchronicity gets confused
|
|
1003
1263
|
# by the nested class (?)
|
|
1004
1264
|
missing_block,
|
|
1005
|
-
) ->
|
|
1265
|
+
) -> tuple[bytes, bytes]:
|
|
1006
1266
|
# Lazily import to keep the eager loading time of this module down
|
|
1007
1267
|
from ._utils.bytes_io_segment_payload import BytesIOSegmentPayload
|
|
1008
1268
|
|
|
@@ -1011,9 +1271,7 @@ async def _put_missing_blocks(
|
|
|
1011
1271
|
file_spec = file_specs[missing_block.file_index]
|
|
1012
1272
|
# TODO(dflemstr): What if the underlying file has changed here in the meantime; should we check the
|
|
1013
1273
|
# hash again just to be sure?
|
|
1014
|
-
|
|
1015
|
-
block_start = missing_block.block_index * BLOCK_SIZE
|
|
1016
|
-
block_length = min(BLOCK_SIZE, file_spec.size - block_start)
|
|
1274
|
+
block = file_spec.blocks[missing_block.block_index]
|
|
1017
1275
|
|
|
1018
1276
|
if file_spec.path not in file_progresses:
|
|
1019
1277
|
file_task_id = progress_cb(name=file_spec.path, size=file_spec.size)
|
|
@@ -1023,7 +1281,7 @@ async def _put_missing_blocks(
|
|
|
1023
1281
|
file_progress.pending_blocks.add(missing_block.block_index)
|
|
1024
1282
|
task_progress_cb = functools.partial(progress_cb, task_id=file_progress.task_id)
|
|
1025
1283
|
|
|
1026
|
-
@retry(n_attempts=
|
|
1284
|
+
@retry(n_attempts=11, base_delay=0.5, timeout=None)
|
|
1027
1285
|
async def put_missing_block_attempt(payload: BytesIOSegmentPayload) -> bytes:
|
|
1028
1286
|
with payload.reset_on_error(subtract_progress=True):
|
|
1029
1287
|
async with ClientSessionRegistry.get_session().put(
|
|
@@ -1037,8 +1295,8 @@ async def _put_missing_blocks(
|
|
|
1037
1295
|
with file_spec.source() as source_fp:
|
|
1038
1296
|
payload = BytesIOSegmentPayload(
|
|
1039
1297
|
source_fp,
|
|
1040
|
-
|
|
1041
|
-
|
|
1298
|
+
block.start,
|
|
1299
|
+
block.end - block.start,
|
|
1042
1300
|
# limit chunk size somewhat to not keep event loop busy for too long
|
|
1043
1301
|
chunk_size=256 * 1024,
|
|
1044
1302
|
progress_report_cb=task_progress_cb,
|
|
@@ -1050,7 +1308,7 @@ async def _put_missing_blocks(
|
|
|
1050
1308
|
if len(file_progress.pending_blocks) == 0:
|
|
1051
1309
|
task_progress_cb(complete=True)
|
|
1052
1310
|
|
|
1053
|
-
return
|
|
1311
|
+
return block.contents_sha256, resp_data
|
|
1054
1312
|
|
|
1055
1313
|
tasks = [asyncio.create_task(put_missing_block(missing_block)) for missing_block in missing_blocks]
|
|
1056
1314
|
for task_result in asyncio.as_completed(tasks):
|