modal 1.1.1.dev41__py3-none-any.whl → 1.1.2__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/__main__.py +1 -2
- modal/_container_entrypoint.py +18 -7
- modal/_functions.py +135 -13
- modal/_object.py +13 -2
- modal/_partial_function.py +8 -8
- modal/_runtime/asgi.py +3 -2
- modal/_runtime/container_io_manager.py +20 -14
- modal/_runtime/container_io_manager.pyi +38 -13
- modal/_runtime/execution_context.py +18 -2
- modal/_runtime/execution_context.pyi +4 -1
- modal/_runtime/gpu_memory_snapshot.py +158 -54
- modal/_utils/blob_utils.py +83 -24
- modal/_utils/function_utils.py +4 -3
- modal/_utils/time_utils.py +28 -4
- modal/app.py +8 -4
- modal/app.pyi +8 -8
- modal/cli/dict.py +14 -11
- modal/cli/entry_point.py +9 -3
- modal/cli/launch.py +102 -4
- modal/cli/profile.py +1 -0
- modal/cli/programs/launch_instance_ssh.py +94 -0
- modal/cli/programs/run_marimo.py +95 -0
- modal/cli/queues.py +49 -19
- modal/cli/secret.py +45 -18
- modal/cli/volume.py +14 -16
- modal/client.pyi +2 -10
- modal/cls.py +12 -2
- modal/cls.pyi +9 -1
- modal/config.py +7 -7
- modal/dict.py +206 -12
- modal/dict.pyi +358 -4
- modal/experimental/__init__.py +130 -0
- modal/file_io.py +1 -1
- modal/file_io.pyi +2 -2
- modal/file_pattern_matcher.py +25 -16
- modal/functions.pyi +111 -11
- modal/image.py +9 -3
- modal/image.pyi +7 -7
- modal/mount.py +20 -13
- modal/mount.pyi +16 -3
- modal/network_file_system.py +8 -2
- modal/object.pyi +3 -0
- modal/parallel_map.py +346 -101
- modal/parallel_map.pyi +108 -0
- modal/proxy.py +2 -1
- modal/queue.py +199 -9
- modal/queue.pyi +357 -3
- modal/sandbox.py +6 -5
- modal/sandbox.pyi +17 -14
- modal/secret.py +196 -3
- modal/secret.pyi +372 -0
- modal/volume.py +239 -23
- modal/volume.pyi +405 -10
- {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/METADATA +2 -2
- {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/RECORD +68 -66
- modal_docs/mdmd/mdmd.py +11 -1
- modal_proto/api.proto +37 -10
- modal_proto/api_grpc.py +32 -0
- modal_proto/api_pb2.py +627 -597
- modal_proto/api_pb2.pyi +107 -19
- modal_proto/api_pb2_grpc.py +67 -2
- modal_proto/api_pb2_grpc.pyi +24 -8
- modal_proto/modal_api_grpc.py +2 -0
- modal_version/__init__.py +1 -1
- {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/WHEEL +0 -0
- {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/entry_points.txt +0 -0
- {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/licenses/LICENSE +0 -0
- {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/top_level.txt +0 -0
modal/volume.py
CHANGED
|
@@ -25,14 +25,21 @@ from typing import (
|
|
|
25
25
|
|
|
26
26
|
from google.protobuf.message import Message
|
|
27
27
|
from grpclib import GRPCError, Status
|
|
28
|
+
from synchronicity import classproperty
|
|
28
29
|
from synchronicity.async_wrap import asynccontextmanager
|
|
29
30
|
|
|
30
31
|
import modal.exception
|
|
31
32
|
import modal_proto.api_pb2
|
|
32
|
-
from modal.exception import InvalidError, VolumeUploadTimeoutError
|
|
33
|
+
from modal.exception import AlreadyExistsError, InvalidError, NotFoundError, VolumeUploadTimeoutError
|
|
33
34
|
from modal_proto import api_pb2
|
|
34
35
|
|
|
35
|
-
from ._object import
|
|
36
|
+
from ._object import (
|
|
37
|
+
EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
|
|
38
|
+
_get_environment_name,
|
|
39
|
+
_Object,
|
|
40
|
+
live_method,
|
|
41
|
+
live_method_gen,
|
|
42
|
+
)
|
|
36
43
|
from ._resolver import Resolver
|
|
37
44
|
from ._utils.async_utils import (
|
|
38
45
|
TaskContext,
|
|
@@ -55,7 +62,7 @@ from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
|
|
|
55
62
|
from ._utils.grpc_utils import retry_transient_errors
|
|
56
63
|
from ._utils.http_utils import ClientSessionRegistry
|
|
57
64
|
from ._utils.name_utils import check_object_name
|
|
58
|
-
from ._utils.time_utils import timestamp_to_localized_dt
|
|
65
|
+
from ._utils.time_utils import as_timestamp, timestamp_to_localized_dt
|
|
59
66
|
from .client import _Client
|
|
60
67
|
from .config import logger
|
|
61
68
|
|
|
@@ -106,6 +113,179 @@ class VolumeInfo:
|
|
|
106
113
|
created_by: Optional[str]
|
|
107
114
|
|
|
108
115
|
|
|
116
|
+
class _VolumeManager:
|
|
117
|
+
"""Namespace with methods for managing named Volume objects."""
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
async def create(
|
|
121
|
+
name: str, # Name to use for the new Volume
|
|
122
|
+
*,
|
|
123
|
+
version: Optional[int] = None, # Experimental: Configure the backend VolumeFS version
|
|
124
|
+
allow_existing: bool = False, # If True, no-op when the Volume already exists
|
|
125
|
+
environment_name: Optional[str] = None, # Uses active environment if not specified
|
|
126
|
+
client: Optional[_Client] = None, # Optional client with Modal credentials
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Create a new Volume object.
|
|
129
|
+
|
|
130
|
+
**Examples:**
|
|
131
|
+
|
|
132
|
+
```python notest
|
|
133
|
+
modal.Volume.objects.create("my-volume")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Volumes will be created in the active environment, or another one can be specified:
|
|
137
|
+
|
|
138
|
+
```python notest
|
|
139
|
+
modal.Volume.objects.create("my-volume", environment_name="dev")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
By default, an error will be raised if the Volume already exists, but passing
|
|
143
|
+
`allow_existing=True` will make the creation attempt a no-op in this case.
|
|
144
|
+
|
|
145
|
+
```python notest
|
|
146
|
+
modal.Volume.objects.create("my-volume", allow_existing=True)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Note that this method does not return a local instance of the Volume. You can use
|
|
150
|
+
`modal.Volume.from_name` to perform a lookup after creation.
|
|
151
|
+
|
|
152
|
+
Added in v1.1.2.
|
|
153
|
+
|
|
154
|
+
"""
|
|
155
|
+
check_object_name(name, "Volume")
|
|
156
|
+
client = await _Client.from_env() if client is None else client
|
|
157
|
+
object_creation_type = (
|
|
158
|
+
api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING
|
|
159
|
+
if allow_existing
|
|
160
|
+
else api_pb2.OBJECT_CREATION_TYPE_CREATE_FAIL_IF_EXISTS
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if version is not None and version not in {1, 2}:
|
|
164
|
+
raise InvalidError("VolumeFS version must be either 1 or 2")
|
|
165
|
+
|
|
166
|
+
req = api_pb2.VolumeGetOrCreateRequest(
|
|
167
|
+
deployment_name=name,
|
|
168
|
+
environment_name=_get_environment_name(environment_name),
|
|
169
|
+
object_creation_type=object_creation_type,
|
|
170
|
+
version=version,
|
|
171
|
+
)
|
|
172
|
+
try:
|
|
173
|
+
await retry_transient_errors(client.stub.VolumeGetOrCreate, req)
|
|
174
|
+
except GRPCError as exc:
|
|
175
|
+
if exc.status == Status.ALREADY_EXISTS and not allow_existing:
|
|
176
|
+
raise AlreadyExistsError(exc.message)
|
|
177
|
+
else:
|
|
178
|
+
raise
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
async def list(
|
|
182
|
+
*,
|
|
183
|
+
max_objects: Optional[int] = None, # Limit requests to this size
|
|
184
|
+
created_before: Optional[Union[datetime, str]] = None, # Limit based on creation date
|
|
185
|
+
environment_name: str = "", # Uses active environment if not specified
|
|
186
|
+
client: Optional[_Client] = None, # Optional client with Modal credentials
|
|
187
|
+
) -> list["_Volume"]:
|
|
188
|
+
"""Return a list of hydrated Volume objects.
|
|
189
|
+
|
|
190
|
+
**Examples:**
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
volumes = modal.Volume.objects.list()
|
|
194
|
+
print([v.name for v in volumes])
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Volumes will be retreived from the active environment, or another one can be specified:
|
|
198
|
+
|
|
199
|
+
```python notest
|
|
200
|
+
dev_volumes = modal.Volume.objects.list(environment_name="dev")
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
By default, all named Volumes are returned, newest to oldest. It's also possible to limit the
|
|
204
|
+
number of results and to filter by creation date:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
volumes = modal.Volume.objects.list(max_objects=10, created_before="2025-01-01")
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Added in v1.1.2.
|
|
211
|
+
|
|
212
|
+
"""
|
|
213
|
+
client = await _Client.from_env() if client is None else client
|
|
214
|
+
if max_objects is not None and max_objects < 0:
|
|
215
|
+
raise InvalidError("max_objects cannot be negative")
|
|
216
|
+
|
|
217
|
+
items: list[api_pb2.VolumeListItem] = []
|
|
218
|
+
|
|
219
|
+
async def retrieve_page(created_before: float) -> bool:
|
|
220
|
+
max_page_size = 100 if max_objects is None else min(100, max_objects - len(items))
|
|
221
|
+
pagination = api_pb2.ListPagination(max_objects=max_page_size, created_before=created_before)
|
|
222
|
+
req = api_pb2.VolumeListRequest(
|
|
223
|
+
environment_name=_get_environment_name(environment_name), pagination=pagination
|
|
224
|
+
)
|
|
225
|
+
resp = await retry_transient_errors(client.stub.VolumeList, req)
|
|
226
|
+
items.extend(resp.items)
|
|
227
|
+
finished = (len(resp.items) < max_page_size) or (max_objects is not None and len(items) >= max_objects)
|
|
228
|
+
return finished
|
|
229
|
+
|
|
230
|
+
finished = await retrieve_page(as_timestamp(created_before))
|
|
231
|
+
while True:
|
|
232
|
+
if finished:
|
|
233
|
+
break
|
|
234
|
+
finished = await retrieve_page(items[-1].metadata.creation_info.created_at)
|
|
235
|
+
|
|
236
|
+
volumes = [
|
|
237
|
+
_Volume._new_hydrated(
|
|
238
|
+
item.volume_id,
|
|
239
|
+
client,
|
|
240
|
+
item.metadata,
|
|
241
|
+
is_another_app=True,
|
|
242
|
+
rep=_Volume._repr(item.label, environment_name),
|
|
243
|
+
)
|
|
244
|
+
for item in items
|
|
245
|
+
]
|
|
246
|
+
return volumes[:max_objects] if max_objects is not None else volumes
|
|
247
|
+
|
|
248
|
+
@staticmethod
|
|
249
|
+
async def delete(
|
|
250
|
+
name: str, # Name of the Volume to delete
|
|
251
|
+
*,
|
|
252
|
+
allow_missing: bool = False, # If True, don't raise an error if the Volume doesn't exist
|
|
253
|
+
environment_name: Optional[str] = None, # Uses active environment if not specified
|
|
254
|
+
client: Optional[_Client] = None, # Optional client with Modal credentials
|
|
255
|
+
):
|
|
256
|
+
"""Delete a named Volume.
|
|
257
|
+
|
|
258
|
+
Warning: This deletes an *entire Volume*, not just a specific file.
|
|
259
|
+
Deletion is irreversible and will affect any Apps currently using the Volume.
|
|
260
|
+
|
|
261
|
+
**Examples:**
|
|
262
|
+
|
|
263
|
+
```python notest
|
|
264
|
+
await modal.Volume.objects.delete("my-volume")
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Volumes will be deleted from the active environment, or another one can be specified:
|
|
268
|
+
|
|
269
|
+
```python notest
|
|
270
|
+
await modal.Volume.objects.delete("my-volume", environment_name="dev")
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Added in v1.1.2.
|
|
274
|
+
|
|
275
|
+
"""
|
|
276
|
+
try:
|
|
277
|
+
obj = await _Volume.from_name(name, environment_name=environment_name).hydrate(client)
|
|
278
|
+
except NotFoundError:
|
|
279
|
+
if not allow_missing:
|
|
280
|
+
raise
|
|
281
|
+
else:
|
|
282
|
+
req = api_pb2.VolumeDeleteRequest(volume_id=obj.object_id)
|
|
283
|
+
await retry_transient_errors(obj._client.stub.VolumeDelete, req)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
VolumeManager = synchronize_api(_VolumeManager)
|
|
287
|
+
|
|
288
|
+
|
|
109
289
|
class _Volume(_Object, type_prefix="vo"):
|
|
110
290
|
"""A writeable volume that can be used to share files between one or more Modal functions.
|
|
111
291
|
|
|
@@ -152,6 +332,14 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
152
332
|
_metadata: "typing.Optional[api_pb2.VolumeMetadata]"
|
|
153
333
|
_read_only: bool = False
|
|
154
334
|
|
|
335
|
+
@classproperty
|
|
336
|
+
def objects(cls) -> _VolumeManager:
|
|
337
|
+
return _VolumeManager
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def name(self) -> Optional[str]:
|
|
341
|
+
return self._name
|
|
342
|
+
|
|
155
343
|
def read_only(self) -> "_Volume":
|
|
156
344
|
"""Configure Volume to mount as read-only.
|
|
157
345
|
|
|
@@ -181,10 +369,6 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
181
369
|
obj = _Volume._from_loader(_load, "Volume()", hydrate_lazily=True, deps=lambda: [self])
|
|
182
370
|
return obj
|
|
183
371
|
|
|
184
|
-
@property
|
|
185
|
-
def name(self) -> Optional[str]:
|
|
186
|
-
return self._name
|
|
187
|
-
|
|
188
372
|
def _hydrate_metadata(self, metadata: Optional[Message]):
|
|
189
373
|
if metadata:
|
|
190
374
|
assert isinstance(metadata, api_pb2.VolumeMetadata)
|
|
@@ -255,7 +439,8 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
255
439
|
response = await resolver.client.stub.VolumeGetOrCreate(req)
|
|
256
440
|
self._hydrate(response.volume_id, resolver.client, response.metadata)
|
|
257
441
|
|
|
258
|
-
|
|
442
|
+
rep = _Volume._repr(name, environment_name)
|
|
443
|
+
return _Volume._from_loader(_load, rep, hydrate_lazily=True, name=name)
|
|
259
444
|
|
|
260
445
|
@classmethod
|
|
261
446
|
@asynccontextmanager
|
|
@@ -264,7 +449,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
264
449
|
client: Optional[_Client] = None,
|
|
265
450
|
environment_name: Optional[str] = None,
|
|
266
451
|
version: "typing.Optional[modal_proto.api_pb2.VolumeFsVersion.ValueType]" = None,
|
|
267
|
-
_heartbeat_sleep: float = EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
|
|
452
|
+
_heartbeat_sleep: float = EPHEMERAL_OBJECT_HEARTBEAT_SLEEP, # mdmd:line-hidden
|
|
268
453
|
) -> AsyncGenerator["_Volume", None]:
|
|
269
454
|
"""Creates a new ephemeral volume within a context manager:
|
|
270
455
|
|
|
@@ -291,7 +476,13 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
291
476
|
async with TaskContext() as tc:
|
|
292
477
|
request = api_pb2.VolumeHeartbeatRequest(volume_id=response.volume_id)
|
|
293
478
|
tc.infinite_loop(lambda: client.stub.VolumeHeartbeat(request), sleep=_heartbeat_sleep)
|
|
294
|
-
yield cls._new_hydrated(
|
|
479
|
+
yield cls._new_hydrated(
|
|
480
|
+
response.volume_id,
|
|
481
|
+
client,
|
|
482
|
+
response.metadata,
|
|
483
|
+
is_another_app=True,
|
|
484
|
+
rep="modal.Volume.ephemeral()",
|
|
485
|
+
)
|
|
295
486
|
|
|
296
487
|
@staticmethod
|
|
297
488
|
async def lookup(
|
|
@@ -341,6 +532,22 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
341
532
|
client: Optional[_Client] = None,
|
|
342
533
|
environment_name: Optional[str] = None,
|
|
343
534
|
version: "typing.Optional[modal_proto.api_pb2.VolumeFsVersion.ValueType]" = None,
|
|
535
|
+
) -> str:
|
|
536
|
+
"""mdmd:hidden"""
|
|
537
|
+
deprecation_warning(
|
|
538
|
+
(2025, 8, 13),
|
|
539
|
+
"The undocumented `modal.Volume.create_deployed` method is deprecated and will be removed "
|
|
540
|
+
"in a future release. It can be replaced with `modal.Volume.objects.create`.",
|
|
541
|
+
)
|
|
542
|
+
return await _Volume._create_deployed(deployment_name, namespace, client, environment_name, version)
|
|
543
|
+
|
|
544
|
+
@staticmethod
|
|
545
|
+
async def _create_deployed(
|
|
546
|
+
deployment_name: str,
|
|
547
|
+
namespace=None, # mdmd:line-hidden
|
|
548
|
+
client: Optional[_Client] = None,
|
|
549
|
+
environment_name: Optional[str] = None,
|
|
550
|
+
version: "typing.Optional[modal_proto.api_pb2.VolumeFsVersion.ValueType]" = None,
|
|
344
551
|
) -> str:
|
|
345
552
|
"""mdmd:hidden"""
|
|
346
553
|
check_object_name(deployment_name, "Volume")
|
|
@@ -377,7 +584,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
377
584
|
|
|
378
585
|
@live_method
|
|
379
586
|
async def commit(self):
|
|
380
|
-
"""Commit changes to
|
|
587
|
+
"""Commit changes to a mounted volume.
|
|
381
588
|
|
|
382
589
|
If successful, the changes made are now persisted in durable storage and available to other containers accessing
|
|
383
590
|
the volume.
|
|
@@ -646,9 +853,20 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
646
853
|
|
|
647
854
|
@staticmethod
|
|
648
855
|
async def delete(name: str, client: Optional[_Client] = None, environment_name: Optional[str] = None):
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
856
|
+
"""mdmd:hidden
|
|
857
|
+
Delete a named Volume.
|
|
858
|
+
|
|
859
|
+
Warning: This deletes an *entire Volume*, not just a specific file.
|
|
860
|
+
Deletion is irreversible and will affect any Apps currently using the Volume.
|
|
861
|
+
|
|
862
|
+
DEPRECATED: This method is deprecated; we recommend using `modal.Volume.objects.delete` instead.
|
|
863
|
+
|
|
864
|
+
"""
|
|
865
|
+
deprecation_warning(
|
|
866
|
+
(2025, 8, 6),
|
|
867
|
+
"`modal.Volume.delete` is deprecated; we recommend using `modal.Volume.objects.delete` instead.",
|
|
868
|
+
)
|
|
869
|
+
await _Volume.objects.delete(name, environment_name=environment_name, client=client)
|
|
652
870
|
|
|
653
871
|
@staticmethod
|
|
654
872
|
async def rename(
|
|
@@ -988,9 +1206,9 @@ class _VolumeUploadContextManager2(_AbstractVolumeUploadContextManager):
|
|
|
988
1206
|
for file_spec in file_specs:
|
|
989
1207
|
blocks = [
|
|
990
1208
|
api_pb2.VolumePutFiles2Request.Block(
|
|
991
|
-
contents_sha256=
|
|
1209
|
+
contents_sha256=block.contents_sha256, put_response=put_responses.get(block.contents_sha256)
|
|
992
1210
|
)
|
|
993
|
-
for
|
|
1211
|
+
for block in file_spec.blocks
|
|
994
1212
|
]
|
|
995
1213
|
files.append(
|
|
996
1214
|
api_pb2.VolumePutFiles2Request.File(
|
|
@@ -1047,7 +1265,7 @@ async def _put_missing_blocks(
|
|
|
1047
1265
|
# TODO(dflemstr): Type is `api_pb2.VolumePutFiles2Response.MissingBlock` but synchronicity gets confused
|
|
1048
1266
|
# by the nested class (?)
|
|
1049
1267
|
missing_block,
|
|
1050
|
-
) ->
|
|
1268
|
+
) -> tuple[bytes, bytes]:
|
|
1051
1269
|
# Lazily import to keep the eager loading time of this module down
|
|
1052
1270
|
from ._utils.bytes_io_segment_payload import BytesIOSegmentPayload
|
|
1053
1271
|
|
|
@@ -1056,9 +1274,7 @@ async def _put_missing_blocks(
|
|
|
1056
1274
|
file_spec = file_specs[missing_block.file_index]
|
|
1057
1275
|
# TODO(dflemstr): What if the underlying file has changed here in the meantime; should we check the
|
|
1058
1276
|
# hash again just to be sure?
|
|
1059
|
-
|
|
1060
|
-
block_start = missing_block.block_index * BLOCK_SIZE
|
|
1061
|
-
block_length = min(BLOCK_SIZE, file_spec.size - block_start)
|
|
1277
|
+
block = file_spec.blocks[missing_block.block_index]
|
|
1062
1278
|
|
|
1063
1279
|
if file_spec.path not in file_progresses:
|
|
1064
1280
|
file_task_id = progress_cb(name=file_spec.path, size=file_spec.size)
|
|
@@ -1082,8 +1298,8 @@ async def _put_missing_blocks(
|
|
|
1082
1298
|
with file_spec.source() as source_fp:
|
|
1083
1299
|
payload = BytesIOSegmentPayload(
|
|
1084
1300
|
source_fp,
|
|
1085
|
-
|
|
1086
|
-
|
|
1301
|
+
block.start,
|
|
1302
|
+
block.end - block.start,
|
|
1087
1303
|
# limit chunk size somewhat to not keep event loop busy for too long
|
|
1088
1304
|
chunk_size=256 * 1024,
|
|
1089
1305
|
progress_report_cb=task_progress_cb,
|
|
@@ -1095,7 +1311,7 @@ async def _put_missing_blocks(
|
|
|
1095
1311
|
if len(file_progress.pending_blocks) == 0:
|
|
1096
1312
|
task_progress_cb(complete=True)
|
|
1097
1313
|
|
|
1098
|
-
return
|
|
1314
|
+
return block.contents_sha256, resp_data
|
|
1099
1315
|
|
|
1100
1316
|
tasks = [asyncio.create_task(put_missing_block(missing_block)) for missing_block in missing_blocks]
|
|
1101
1317
|
for task_result in asyncio.as_completed(tasks):
|