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/_utils/blob_utils.py
CHANGED
|
@@ -4,6 +4,7 @@ import dataclasses
|
|
|
4
4
|
import hashlib
|
|
5
5
|
import os
|
|
6
6
|
import platform
|
|
7
|
+
import random
|
|
7
8
|
import time
|
|
8
9
|
from collections.abc import AsyncIterator
|
|
9
10
|
from contextlib import AbstractContextManager, contextmanager
|
|
@@ -26,7 +27,6 @@ from modal_proto.modal_api_grpc import ModalClientModal
|
|
|
26
27
|
|
|
27
28
|
from ..exception import ExecutionError
|
|
28
29
|
from .async_utils import TaskContext, retry
|
|
29
|
-
from .grpc_utils import retry_transient_errors
|
|
30
30
|
from .hash_utils import UploadHashes, get_upload_hashes
|
|
31
31
|
from .http_utils import ClientSessionRegistry
|
|
32
32
|
from .logger import logger
|
|
@@ -37,12 +37,15 @@ if TYPE_CHECKING:
|
|
|
37
37
|
# Max size for function inputs and outputs.
|
|
38
38
|
MAX_OBJECT_SIZE_BYTES = 2 * 1024 * 1024 # 2 MiB
|
|
39
39
|
|
|
40
|
+
# Max size for async function inputs and outputs.
|
|
41
|
+
MAX_ASYNC_OBJECT_SIZE_BYTES = 8 * 1024 # 8 KiB
|
|
42
|
+
|
|
40
43
|
# If a file is LARGE_FILE_LIMIT bytes or larger, it's uploaded to blob store (s3) instead of going through grpc
|
|
41
44
|
# It will also make sure to chunk the hash calculation to avoid reading the entire file into memory
|
|
42
45
|
LARGE_FILE_LIMIT = 4 * 1024 * 1024 # 4 MiB
|
|
43
46
|
|
|
44
47
|
# Max parallelism during map calls
|
|
45
|
-
BLOB_MAX_PARALLELISM =
|
|
48
|
+
BLOB_MAX_PARALLELISM = 20
|
|
46
49
|
|
|
47
50
|
# read ~16MiB chunks by default
|
|
48
51
|
DEFAULT_SEGMENT_CHUNK_SIZE = 2**24
|
|
@@ -55,6 +58,8 @@ MULTIPART_UPLOAD_THRESHOLD = 1024**3
|
|
|
55
58
|
# For block based storage like volumefs2: the size of a block
|
|
56
59
|
BLOCK_SIZE: int = 8 * 1024 * 1024
|
|
57
60
|
|
|
61
|
+
HEALTHY_R2_UPLOAD_PERCENTAGE = 0.95
|
|
62
|
+
|
|
58
63
|
|
|
59
64
|
@retry(n_attempts=5, base_delay=0.5, timeout=None)
|
|
60
65
|
async def _upload_to_s3_url(
|
|
@@ -79,7 +84,7 @@ async def _upload_to_s3_url(
|
|
|
79
84
|
) as resp:
|
|
80
85
|
# S3 signal to slow down request rate.
|
|
81
86
|
if resp.status == 503:
|
|
82
|
-
logger.
|
|
87
|
+
logger.debug("Received SlowDown signal from S3, sleeping for 1 second before retrying.")
|
|
83
88
|
await asyncio.sleep(1)
|
|
84
89
|
|
|
85
90
|
if resp.status != 200:
|
|
@@ -182,9 +187,37 @@ def get_content_length(data: BinaryIO) -> int:
|
|
|
182
187
|
return content_length - pos
|
|
183
188
|
|
|
184
189
|
|
|
190
|
+
async def _blob_upload_with_fallback(
|
|
191
|
+
items, blob_ids: list[str], callback, content_length: int
|
|
192
|
+
) -> tuple[str, bool, int]:
|
|
193
|
+
r2_throughput_bytes_s = 0
|
|
194
|
+
r2_failed = False
|
|
195
|
+
for idx, (item, blob_id) in enumerate(zip(items, blob_ids)):
|
|
196
|
+
# We want to default to R2 95% of the time and S3 5% of the time.
|
|
197
|
+
# To ensure the failure path is continuously exercised.
|
|
198
|
+
if idx == 0 and len(items) > 1 and random.random() > HEALTHY_R2_UPLOAD_PERCENTAGE:
|
|
199
|
+
continue
|
|
200
|
+
try:
|
|
201
|
+
if blob_id.endswith(":r2"):
|
|
202
|
+
t0 = time.monotonic_ns()
|
|
203
|
+
await callback(item)
|
|
204
|
+
dt_ns = time.monotonic_ns() - t0
|
|
205
|
+
r2_throughput_bytes_s = (content_length * 1_000_000_000) // max(dt_ns, 1)
|
|
206
|
+
else:
|
|
207
|
+
await callback(item)
|
|
208
|
+
return blob_id, r2_failed, r2_throughput_bytes_s
|
|
209
|
+
except Exception as _:
|
|
210
|
+
if blob_id.endswith(":r2"):
|
|
211
|
+
r2_failed = True
|
|
212
|
+
# Ignore all errors except the last one, since we're out of fallback options.
|
|
213
|
+
if idx == len(items) - 1:
|
|
214
|
+
raise
|
|
215
|
+
raise ExecutionError("Failed to upload blob")
|
|
216
|
+
|
|
217
|
+
|
|
185
218
|
async def _blob_upload(
|
|
186
219
|
upload_hashes: UploadHashes, data: Union[bytes, BinaryIO], stub, progress_report_cb: Optional[Callable] = None
|
|
187
|
-
) -> str:
|
|
220
|
+
) -> tuple[str, bool, int]:
|
|
188
221
|
if isinstance(data, bytes):
|
|
189
222
|
data = BytesIO(data)
|
|
190
223
|
|
|
@@ -195,19 +228,26 @@ async def _blob_upload(
|
|
|
195
228
|
content_sha256_base64=upload_hashes.sha256_base64,
|
|
196
229
|
content_length=content_length,
|
|
197
230
|
)
|
|
198
|
-
resp = await
|
|
199
|
-
|
|
200
|
-
|
|
231
|
+
resp = await stub.BlobCreate(req)
|
|
232
|
+
|
|
233
|
+
if resp.WhichOneof("upload_types_oneof") == "multiparts":
|
|
234
|
+
|
|
235
|
+
async def upload_multipart_upload(part):
|
|
236
|
+
return await perform_multipart_upload(
|
|
237
|
+
data,
|
|
238
|
+
content_length=content_length,
|
|
239
|
+
max_part_size=part.part_length,
|
|
240
|
+
part_urls=part.upload_urls,
|
|
241
|
+
completion_url=part.completion_url,
|
|
242
|
+
upload_chunk_size=DEFAULT_SEGMENT_CHUNK_SIZE,
|
|
243
|
+
progress_report_cb=progress_report_cb,
|
|
244
|
+
)
|
|
201
245
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
246
|
+
blob_id, r2_failed, r2_throughput_bytes_s = await _blob_upload_with_fallback(
|
|
247
|
+
resp.multiparts.items,
|
|
248
|
+
resp.blob_ids,
|
|
249
|
+
upload_multipart_upload,
|
|
205
250
|
content_length=content_length,
|
|
206
|
-
max_part_size=resp.multipart.part_length,
|
|
207
|
-
part_urls=resp.multipart.upload_urls,
|
|
208
|
-
completion_url=resp.multipart.completion_url,
|
|
209
|
-
upload_chunk_size=DEFAULT_SEGMENT_CHUNK_SIZE,
|
|
210
|
-
progress_report_cb=progress_report_cb,
|
|
211
251
|
)
|
|
212
252
|
else:
|
|
213
253
|
from .bytes_io_segment_payload import BytesIOSegmentPayload
|
|
@@ -215,34 +255,54 @@ async def _blob_upload(
|
|
|
215
255
|
payload = BytesIOSegmentPayload(
|
|
216
256
|
data, segment_start=0, segment_length=content_length, progress_report_cb=progress_report_cb
|
|
217
257
|
)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
258
|
+
|
|
259
|
+
async def upload_to_s3_url(url):
|
|
260
|
+
return await _upload_to_s3_url(
|
|
261
|
+
url,
|
|
262
|
+
payload,
|
|
263
|
+
# for single part uploads, we use server side md5 checksums
|
|
264
|
+
content_md5_b64=upload_hashes.md5_base64,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
blob_id, r2_failed, r2_throughput_bytes_s = await _blob_upload_with_fallback(
|
|
268
|
+
resp.upload_urls.items,
|
|
269
|
+
resp.blob_ids,
|
|
270
|
+
upload_to_s3_url,
|
|
271
|
+
content_length=content_length,
|
|
223
272
|
)
|
|
224
273
|
|
|
225
274
|
if progress_report_cb:
|
|
226
275
|
progress_report_cb(complete=True)
|
|
227
276
|
|
|
228
|
-
return blob_id
|
|
277
|
+
return blob_id, r2_failed, r2_throughput_bytes_s
|
|
229
278
|
|
|
230
279
|
|
|
231
|
-
async def
|
|
280
|
+
async def blob_upload_with_r2_failure_info(payload: bytes, stub: ModalClientModal) -> tuple[str, bool, int]:
|
|
232
281
|
size_mib = len(payload) / 1024 / 1024
|
|
233
282
|
logger.debug(f"Uploading large blob of size {size_mib:.2f} MiB")
|
|
234
283
|
t0 = time.time()
|
|
235
284
|
if isinstance(payload, str):
|
|
236
|
-
logger.
|
|
285
|
+
logger.debug("Blob uploading string, not bytes - auto-encoding as utf8")
|
|
237
286
|
payload = payload.encode("utf8")
|
|
238
287
|
upload_hashes = get_upload_hashes(payload)
|
|
239
|
-
blob_id = await _blob_upload(upload_hashes, payload, stub)
|
|
288
|
+
blob_id, r2_failed, r2_throughput_bytes_s = await _blob_upload(upload_hashes, payload, stub)
|
|
240
289
|
dur_s = max(time.time() - t0, 0.001) # avoid division by zero
|
|
241
290
|
throughput_mib_s = (size_mib) / dur_s
|
|
242
|
-
logger.debug(
|
|
291
|
+
logger.debug(
|
|
292
|
+
f"Uploaded large blob of size {size_mib:.2f} MiB ({throughput_mib_s:.2f} MiB/s, total {dur_s:.2f}s). {blob_id}"
|
|
293
|
+
)
|
|
294
|
+
return blob_id, r2_failed, r2_throughput_bytes_s
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
async def blob_upload(payload: bytes, stub: ModalClientModal) -> str:
|
|
298
|
+
blob_id, _, _ = await blob_upload_with_r2_failure_info(payload, stub)
|
|
243
299
|
return blob_id
|
|
244
300
|
|
|
245
301
|
|
|
302
|
+
async def format_blob_data(data: bytes, api_stub: ModalClientModal) -> dict[str, Any]:
|
|
303
|
+
return {"data_blob_id": await blob_upload(data, api_stub)} if len(data) > MAX_OBJECT_SIZE_BYTES else {"data": data}
|
|
304
|
+
|
|
305
|
+
|
|
246
306
|
async def blob_upload_file(
|
|
247
307
|
file_obj: BinaryIO,
|
|
248
308
|
stub: ModalClientModal,
|
|
@@ -251,7 +311,8 @@ async def blob_upload_file(
|
|
|
251
311
|
md5_hex: Optional[str] = None,
|
|
252
312
|
) -> str:
|
|
253
313
|
upload_hashes = get_upload_hashes(file_obj, sha256_hex=sha256_hex, md5_hex=md5_hex)
|
|
254
|
-
|
|
314
|
+
blob_id, _, _ = await _blob_upload(upload_hashes, file_obj, stub, progress_report_cb)
|
|
315
|
+
return blob_id
|
|
255
316
|
|
|
256
317
|
|
|
257
318
|
@retry(n_attempts=5, base_delay=0.1, timeout=None)
|
|
@@ -259,7 +320,7 @@ async def _download_from_url(download_url: str) -> bytes:
|
|
|
259
320
|
async with ClientSessionRegistry.get_session().get(download_url) as s3_resp:
|
|
260
321
|
# S3 signal to slow down request rate.
|
|
261
322
|
if s3_resp.status == 503:
|
|
262
|
-
logger.
|
|
323
|
+
logger.debug("Received SlowDown signal from S3, sleeping for 1 second before retrying.")
|
|
263
324
|
await asyncio.sleep(1)
|
|
264
325
|
|
|
265
326
|
if s3_resp.status != 200:
|
|
@@ -273,23 +334,25 @@ async def blob_download(blob_id: str, stub: ModalClientModal) -> bytes:
|
|
|
273
334
|
logger.debug(f"Downloading large blob {blob_id}")
|
|
274
335
|
t0 = time.time()
|
|
275
336
|
req = api_pb2.BlobGetRequest(blob_id=blob_id)
|
|
276
|
-
resp = await
|
|
337
|
+
resp = await stub.BlobGet(req)
|
|
277
338
|
data = await _download_from_url(resp.download_url)
|
|
278
339
|
size_mib = len(data) / 1024 / 1024
|
|
279
340
|
dur_s = max(time.time() - t0, 0.001) # avoid division by zero
|
|
280
341
|
throughput_mib_s = size_mib / dur_s
|
|
281
|
-
logger.debug(
|
|
342
|
+
logger.debug(
|
|
343
|
+
f"Downloaded large blob {blob_id} of size {size_mib:.2f} MiB ({throughput_mib_s:.2f} MiB/s, total {dur_s:.2f}s)"
|
|
344
|
+
)
|
|
282
345
|
return data
|
|
283
346
|
|
|
284
347
|
|
|
285
348
|
async def blob_iter(blob_id: str, stub: ModalClientModal) -> AsyncIterator[bytes]:
|
|
286
349
|
req = api_pb2.BlobGetRequest(blob_id=blob_id)
|
|
287
|
-
resp = await
|
|
350
|
+
resp = await stub.BlobGet(req)
|
|
288
351
|
download_url = resp.download_url
|
|
289
352
|
async with ClientSessionRegistry.get_session().get(download_url) as s3_resp:
|
|
290
353
|
# S3 signal to slow down request rate.
|
|
291
354
|
if s3_resp.status == 503:
|
|
292
|
-
logger.
|
|
355
|
+
logger.debug("Received SlowDown signal from S3, sleeping for 1 second before retrying.")
|
|
293
356
|
await asyncio.sleep(1)
|
|
294
357
|
|
|
295
358
|
if s3_resp.status != 200:
|
|
@@ -380,20 +443,31 @@ def get_file_upload_spec_from_fileobj(fp: BinaryIO, mount_filename: PurePosixPat
|
|
|
380
443
|
mode,
|
|
381
444
|
)
|
|
382
445
|
|
|
446
|
+
|
|
383
447
|
_FileUploadSource2 = Callable[[], ContextManager[BinaryIO]]
|
|
384
448
|
|
|
449
|
+
|
|
450
|
+
@dataclasses.dataclass
|
|
451
|
+
class FileUploadBlock:
|
|
452
|
+
# The start (byte offset, inclusive) of the block within the file
|
|
453
|
+
start: int
|
|
454
|
+
# The end (byte offset, exclusive) of the block, after having removed any trailing zeroes
|
|
455
|
+
end: int
|
|
456
|
+
# Raw (unencoded 32 byte) SHA256 sum of the block, not including trailing zeroes
|
|
457
|
+
contents_sha256: bytes
|
|
458
|
+
|
|
459
|
+
|
|
385
460
|
@dataclasses.dataclass
|
|
386
461
|
class FileUploadSpec2:
|
|
387
462
|
source: _FileUploadSource2
|
|
388
463
|
source_description: Union[str, Path]
|
|
389
464
|
|
|
390
465
|
path: str
|
|
391
|
-
#
|
|
392
|
-
|
|
466
|
+
# 8MiB file blocks
|
|
467
|
+
blocks: list[FileUploadBlock]
|
|
393
468
|
mode: int # file permission bits (last 12 bits of st_mode)
|
|
394
469
|
size: int
|
|
395
470
|
|
|
396
|
-
|
|
397
471
|
@staticmethod
|
|
398
472
|
async def from_path(
|
|
399
473
|
filename: Path,
|
|
@@ -416,7 +490,6 @@ class FileUploadSpec2:
|
|
|
416
490
|
hash_semaphore,
|
|
417
491
|
)
|
|
418
492
|
|
|
419
|
-
|
|
420
493
|
@staticmethod
|
|
421
494
|
async def from_fileobj(
|
|
422
495
|
source_fp: Union[BinaryIO, BytesIO],
|
|
@@ -426,6 +499,7 @@ class FileUploadSpec2:
|
|
|
426
499
|
) -> "FileUploadSpec2":
|
|
427
500
|
try:
|
|
428
501
|
fileno = source_fp.fileno()
|
|
502
|
+
|
|
429
503
|
def source():
|
|
430
504
|
new_fd = os.dup(fileno)
|
|
431
505
|
fp = os.fdopen(new_fd, "rb")
|
|
@@ -436,6 +510,7 @@ class FileUploadSpec2:
|
|
|
436
510
|
# `.fileno()` not available; assume BytesIO-like type
|
|
437
511
|
source_fp = cast(BytesIO, source_fp)
|
|
438
512
|
buffer = source_fp.getbuffer()
|
|
513
|
+
|
|
439
514
|
def source():
|
|
440
515
|
return BytesIO(buffer)
|
|
441
516
|
|
|
@@ -447,7 +522,6 @@ class FileUploadSpec2:
|
|
|
447
522
|
hash_semaphore,
|
|
448
523
|
)
|
|
449
524
|
|
|
450
|
-
|
|
451
525
|
@staticmethod
|
|
452
526
|
async def _create(
|
|
453
527
|
source: _FileUploadSource2,
|
|
@@ -461,53 +535,102 @@ class FileUploadSpec2:
|
|
|
461
535
|
source_fp.seek(0, os.SEEK_END)
|
|
462
536
|
size = source_fp.tell()
|
|
463
537
|
|
|
464
|
-
|
|
538
|
+
blocks = await _gather_blocks(source, size, hash_semaphore)
|
|
465
539
|
|
|
466
540
|
return FileUploadSpec2(
|
|
467
541
|
source=source,
|
|
468
542
|
source_description=source_description,
|
|
469
543
|
path=mount_filename.as_posix(),
|
|
470
|
-
|
|
544
|
+
blocks=blocks,
|
|
471
545
|
mode=mode & 0o7777,
|
|
472
546
|
size=size,
|
|
473
547
|
)
|
|
474
548
|
|
|
475
549
|
|
|
476
|
-
async def
|
|
550
|
+
async def _gather_blocks(
|
|
477
551
|
source: _FileUploadSource2,
|
|
478
552
|
size: int,
|
|
479
553
|
hash_semaphore: asyncio.Semaphore,
|
|
480
|
-
) -> list[
|
|
554
|
+
) -> list[FileUploadBlock]:
|
|
481
555
|
def ceildiv(a: int, b: int) -> int:
|
|
482
556
|
return -(a // -b)
|
|
483
557
|
|
|
484
558
|
num_blocks = ceildiv(size, BLOCK_SIZE)
|
|
485
559
|
|
|
486
|
-
def
|
|
487
|
-
|
|
488
|
-
|
|
560
|
+
async def gather_block(block_idx: int) -> FileUploadBlock:
|
|
561
|
+
async with hash_semaphore:
|
|
562
|
+
return await asyncio.to_thread(_gather_block, source, block_idx)
|
|
489
563
|
|
|
490
|
-
|
|
491
|
-
|
|
564
|
+
tasks = (gather_block(idx) for idx in range(num_blocks))
|
|
565
|
+
return await asyncio.gather(*tasks)
|
|
492
566
|
|
|
493
|
-
num_bytes_read = 0
|
|
494
|
-
while num_bytes_read < BLOCK_SIZE:
|
|
495
|
-
chunk = block_fp.read(BLOCK_SIZE - num_bytes_read)
|
|
496
567
|
|
|
497
|
-
|
|
498
|
-
|
|
568
|
+
def _gather_block(source: _FileUploadSource2, block_idx: int) -> FileUploadBlock:
|
|
569
|
+
start = block_idx * BLOCK_SIZE
|
|
570
|
+
end = _find_end_of_block(source, start, start + BLOCK_SIZE)
|
|
571
|
+
contents_sha256 = _hash_range_sha256(source, start, end)
|
|
572
|
+
return FileUploadBlock(start=start, end=end, contents_sha256=contents_sha256)
|
|
499
573
|
|
|
500
|
-
num_bytes_read += len(chunk)
|
|
501
|
-
sha256_hash.update(chunk)
|
|
502
574
|
|
|
503
|
-
|
|
575
|
+
def _hash_range_sha256(source: _FileUploadSource2, start, end):
|
|
576
|
+
sha256_hash = hashlib.sha256()
|
|
577
|
+
range_size = end - start
|
|
504
578
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
579
|
+
with source() as fp:
|
|
580
|
+
fp.seek(start)
|
|
581
|
+
|
|
582
|
+
num_bytes_read = 0
|
|
583
|
+
while num_bytes_read < range_size:
|
|
584
|
+
chunk = fp.read(range_size - num_bytes_read)
|
|
585
|
+
|
|
586
|
+
if not chunk:
|
|
587
|
+
break
|
|
588
|
+
|
|
589
|
+
num_bytes_read += len(chunk)
|
|
590
|
+
sha256_hash.update(chunk)
|
|
591
|
+
|
|
592
|
+
return sha256_hash.digest()
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _find_end_of_block(source: _FileUploadSource2, start: int, end: int) -> Optional[int]:
|
|
596
|
+
"""Finds the appropriate end of a block, which is the index of the byte just past the last non-zero byte in the
|
|
597
|
+
block.
|
|
598
|
+
|
|
599
|
+
>>> _find_end_of_block(lambda: BytesIO(b"abc123\0\0\0"), 0, 1024)
|
|
600
|
+
6
|
|
601
|
+
>>> _find_end_of_block(lambda: BytesIO(b"abc123\0\0\0"), 3, 1024)
|
|
602
|
+
6
|
|
603
|
+
>>> _find_end_of_block(lambda: BytesIO(b"abc123\0\0\0"), 0, 3)
|
|
604
|
+
4
|
|
605
|
+
>>> _find_end_of_block(lambda: BytesIO(b"abc123\0\0\0a"), 0, 9)
|
|
606
|
+
6
|
|
607
|
+
>>> _find_end_of_block(lambda: BytesIO(b"\0\0\0"), 0, 3)
|
|
608
|
+
0
|
|
609
|
+
>>> _find_end_of_block(lambda: BytesIO(b"\0\0\0\0\0\0"), 3, 6)
|
|
610
|
+
3
|
|
611
|
+
>>> _find_end_of_block(lambda: BytesIO(b""), 0, 1024)
|
|
612
|
+
0
|
|
613
|
+
"""
|
|
614
|
+
size = end - start
|
|
615
|
+
new_end = start
|
|
508
616
|
|
|
509
|
-
|
|
510
|
-
|
|
617
|
+
with source() as block_fp:
|
|
618
|
+
block_fp.seek(start)
|
|
619
|
+
|
|
620
|
+
num_bytes_read = 0
|
|
621
|
+
while num_bytes_read < size:
|
|
622
|
+
chunk = block_fp.read(size - num_bytes_read)
|
|
623
|
+
|
|
624
|
+
if not chunk:
|
|
625
|
+
break
|
|
626
|
+
|
|
627
|
+
stripped_chunk = chunk.rstrip(b"\0")
|
|
628
|
+
if stripped_chunk:
|
|
629
|
+
new_end = start + num_bytes_read + len(stripped_chunk)
|
|
630
|
+
|
|
631
|
+
num_bytes_read += len(chunk)
|
|
632
|
+
|
|
633
|
+
return new_end
|
|
511
634
|
|
|
512
635
|
|
|
513
636
|
def use_md5(url: str) -> bool:
|
modal/_utils/deprecation.py
CHANGED
|
@@ -122,3 +122,22 @@ def warn_on_renamed_autoscaler_settings(func: Callable[P, R]) -> Callable[P, R]:
|
|
|
122
122
|
return func(*args, **kwargs)
|
|
123
123
|
|
|
124
124
|
return wrapper
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def warn_if_passing_namespace(
|
|
128
|
+
namespace: Any,
|
|
129
|
+
resource_name: str,
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Issue deprecation warning for namespace parameter if non-None value is passed.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
namespace: The namespace parameter value (may be None or actual value)
|
|
135
|
+
resource_name: Name of the resource type for the warning message
|
|
136
|
+
"""
|
|
137
|
+
if namespace is not None:
|
|
138
|
+
deprecation_warning(
|
|
139
|
+
(2025, 6, 30),
|
|
140
|
+
f"The `namespace` parameter for `{resource_name}` is deprecated and will be"
|
|
141
|
+
" removed in a future release. It is no longer needed, so can be removed"
|
|
142
|
+
" from your code.",
|
|
143
|
+
)
|