modal 0.68.11__py3-none-any.whl → 0.68.14__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.
modal/_ipython.py CHANGED
@@ -1,21 +1,11 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import sys
3
- import warnings
4
-
5
- ipy_outstream = None
6
- try:
7
- with warnings.catch_warnings():
8
- warnings.simplefilter("ignore")
9
- import ipykernel.iostream
10
-
11
- ipy_outstream = ipykernel.iostream.OutStream
12
- except ImportError:
13
- pass
14
3
 
15
4
 
16
5
  def is_notebook(stdout=None):
17
- if ipy_outstream is None:
6
+ ipykernel_iostream = sys.modules.get("ipykernel.iostream")
7
+ if ipykernel_iostream is None:
18
8
  return False
19
9
  if stdout is None:
20
10
  stdout = sys.stdout
21
- return isinstance(stdout, ipy_outstream)
11
+ return isinstance(stdout, ipykernel_iostream.OutStream)
@@ -21,7 +21,7 @@ from modal_proto.modal_api_grpc import ModalClientModal
21
21
  from ..exception import ExecutionError
22
22
  from .async_utils import TaskContext, retry
23
23
  from .grpc_utils import retry_transient_errors
24
- from .hash_utils import UploadHashes, get_sha256_hex, get_upload_hashes
24
+ from .hash_utils import UploadHashes, get_upload_hashes
25
25
  from .http_utils import ClientSessionRegistry
26
26
  from .logger import logger
27
27
 
@@ -38,6 +38,11 @@ BLOB_MAX_PARALLELISM = 10
38
38
  # read ~16MiB chunks by default
39
39
  DEFAULT_SEGMENT_CHUNK_SIZE = 2**24
40
40
 
41
+ # Files larger than this will be multipart uploaded. The server might request multipart upload for smaller files as
42
+ # well, but the limit will never be raised.
43
+ # TODO(dano): remove this once we stop requiring md5 for blobs
44
+ MULTIPART_UPLOAD_THRESHOLD = 1024**3
45
+
41
46
 
42
47
  class BytesIOSegmentPayload(BytesIOPayload):
43
48
  """Modified bytes payload for concurrent sends of chunks from the same file.
@@ -305,9 +310,13 @@ async def blob_upload(payload: bytes, stub: ModalClientModal) -> str:
305
310
 
306
311
 
307
312
  async def blob_upload_file(
308
- file_obj: BinaryIO, stub: ModalClientModal, progress_report_cb: Optional[Callable] = None
313
+ file_obj: BinaryIO,
314
+ stub: ModalClientModal,
315
+ progress_report_cb: Optional[Callable] = None,
316
+ sha256_hex: Optional[str] = None,
317
+ md5_hex: Optional[str] = None,
309
318
  ) -> str:
310
- upload_hashes = get_upload_hashes(file_obj)
319
+ upload_hashes = get_upload_hashes(file_obj, sha256_hex=sha256_hex, md5_hex=md5_hex)
311
320
  return await _blob_upload(upload_hashes, file_obj, stub, progress_report_cb)
312
321
 
313
322
 
@@ -366,6 +375,7 @@ class FileUploadSpec:
366
375
  use_blob: bool
367
376
  content: Optional[bytes] # typically None if using blob, required otherwise
368
377
  sha256_hex: str
378
+ md5_hex: str
369
379
  mode: int # file permission bits (last 12 bits of st_mode)
370
380
  size: int
371
381
 
@@ -383,13 +393,15 @@ def _get_file_upload_spec(
383
393
  fp.seek(0)
384
394
 
385
395
  if size >= LARGE_FILE_LIMIT:
396
+ # TODO(dano): remove the placeholder md5 once we stop requiring md5 for blobs
397
+ md5_hex = "baadbaadbaadbaadbaadbaadbaadbaad" if size > MULTIPART_UPLOAD_THRESHOLD else None
386
398
  use_blob = True
387
399
  content = None
388
- sha256_hex = get_sha256_hex(fp)
400
+ hashes = get_upload_hashes(fp, md5_hex=md5_hex)
389
401
  else:
390
402
  use_blob = False
391
403
  content = fp.read()
392
- sha256_hex = get_sha256_hex(content)
404
+ hashes = get_upload_hashes(content)
393
405
 
394
406
  return FileUploadSpec(
395
407
  source=source,
@@ -397,7 +409,8 @@ def _get_file_upload_spec(
397
409
  mount_filename=mount_filename.as_posix(),
398
410
  use_blob=use_blob,
399
411
  content=content,
400
- sha256_hex=sha256_hex,
412
+ sha256_hex=hashes.sha256_hex(),
413
+ md5_hex=hashes.md5_hex(),
401
414
  mode=mode & 0o7777,
402
415
  size=size,
403
416
  )
@@ -3,14 +3,14 @@ import base64
3
3
  import dataclasses
4
4
  import hashlib
5
5
  import time
6
- from typing import BinaryIO, Callable, Union
6
+ from typing import BinaryIO, Callable, Optional, Sequence, Union
7
7
 
8
8
  from modal.config import logger
9
9
 
10
10
  HASH_CHUNK_SIZE = 65536
11
11
 
12
12
 
13
- def _update(hashers: list[Callable[[bytes], None]], data: Union[bytes, BinaryIO]) -> None:
13
+ def _update(hashers: Sequence[Callable[[bytes], None]], data: Union[bytes, BinaryIO]) -> None:
14
14
  if isinstance(data, bytes):
15
15
  for hasher in hashers:
16
16
  hasher(data)
@@ -57,15 +57,44 @@ class UploadHashes:
57
57
  md5_base64: str
58
58
  sha256_base64: str
59
59
 
60
+ def md5_hex(self) -> str:
61
+ return base64.b64decode(self.md5_base64).hex()
60
62
 
61
- def get_upload_hashes(data: Union[bytes, BinaryIO]) -> UploadHashes:
63
+ def sha256_hex(self) -> str:
64
+ return base64.b64decode(self.sha256_base64).hex()
65
+
66
+
67
+ def get_upload_hashes(
68
+ data: Union[bytes, BinaryIO], sha256_hex: Optional[str] = None, md5_hex: Optional[str] = None
69
+ ) -> UploadHashes:
62
70
  t0 = time.monotonic()
63
- md5 = hashlib.md5()
64
- sha256 = hashlib.sha256()
65
- _update([md5.update, sha256.update], data)
71
+ hashers = {}
72
+
73
+ if not sha256_hex:
74
+ sha256 = hashlib.sha256()
75
+ hashers["sha256"] = sha256
76
+ if not md5_hex:
77
+ md5 = hashlib.md5()
78
+ hashers["md5"] = md5
79
+
80
+ if hashers:
81
+ updaters = [h.update for h in hashers.values()]
82
+ _update(updaters, data)
83
+
84
+ if sha256_hex:
85
+ sha256_base64 = base64.b64encode(bytes.fromhex(sha256_hex)).decode("ascii")
86
+ else:
87
+ sha256_base64 = base64.b64encode(hashers["sha256"].digest()).decode("ascii")
88
+
89
+ if md5_hex:
90
+ md5_base64 = base64.b64encode(bytes.fromhex(md5_hex)).decode("ascii")
91
+ else:
92
+ md5_base64 = base64.b64encode(hashers["md5"].digest()).decode("ascii")
93
+
66
94
  hashes = UploadHashes(
67
- md5_base64=base64.b64encode(md5.digest()).decode("ascii"),
68
- sha256_base64=base64.b64encode(sha256.digest()).decode("ascii"),
95
+ md5_base64=md5_base64,
96
+ sha256_base64=sha256_base64,
69
97
  )
70
- logger.debug("get_upload_hashes took %.3fs", time.monotonic() - t0)
98
+
99
+ logger.debug("get_upload_hashes took %.3fs (%s)", time.monotonic() - t0, hashers.keys())
71
100
  return hashes
modal/client.pyi CHANGED
@@ -26,7 +26,7 @@ class _Client:
26
26
  _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
27
27
 
28
28
  def __init__(
29
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.11"
29
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.14"
30
30
  ): ...
31
31
  def is_closed(self) -> bool: ...
32
32
  @property
@@ -81,7 +81,7 @@ class Client:
81
81
  _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
82
82
 
83
83
  def __init__(
84
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.11"
84
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.14"
85
85
  ): ...
86
86
  def is_closed(self) -> bool: ...
87
87
  @property
modal/mount.py CHANGED
@@ -483,7 +483,9 @@ class _Mount(_Object, type_prefix="mo"):
483
483
  logger.debug(f"Creating blob file for {file_spec.source_description} ({file_spec.size} bytes)")
484
484
  async with blob_upload_concurrency:
485
485
  with file_spec.source() as fp:
486
- blob_id = await blob_upload_file(fp, resolver.client.stub)
486
+ blob_id = await blob_upload_file(
487
+ fp, resolver.client.stub, sha256_hex=file_spec.sha256_hex, md5_hex=file_spec.md5_hex
488
+ )
487
489
  logger.debug(f"Uploading blob file {file_spec.source_description} as {remote_filename}")
488
490
  request2 = api_pb2.MountPutFileRequest(data_blob_id=blob_id, sha256_hex=file_spec.sha256_hex)
489
491
  else:
@@ -245,7 +245,10 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
245
245
  if data_size > LARGE_FILE_LIMIT:
246
246
  progress_task_id = progress_cb(name=remote_path, size=data_size)
247
247
  blob_id = await blob_upload_file(
248
- fp, self._client.stub, progress_report_cb=functools.partial(progress_cb, progress_task_id)
248
+ fp,
249
+ self._client.stub,
250
+ progress_report_cb=functools.partial(progress_cb, progress_task_id),
251
+ sha256_hex=sha_hash,
249
252
  )
250
253
  req = api_pb2.SharedVolumePutFileRequest(
251
254
  shared_volume_id=self.object_id,
modal/volume.py CHANGED
@@ -632,7 +632,11 @@ class _VolumeUploadContextManager:
632
632
  logger.debug(f"Creating blob file for {file_spec.source_description} ({file_spec.size} bytes)")
633
633
  with file_spec.source() as fp:
634
634
  blob_id = await blob_upload_file(
635
- fp, self._client.stub, functools.partial(self._progress_cb, progress_task_id)
635
+ fp,
636
+ self._client.stub,
637
+ functools.partial(self._progress_cb, progress_task_id),
638
+ sha256_hex=file_spec.sha256_hex,
639
+ md5_hex=file_spec.md5_hex,
636
640
  )
637
641
  logger.debug(f"Uploading blob file {file_spec.source_description} as {remote_filename}")
638
642
  request2 = api_pb2.MountPutFileRequest(data_blob_id=blob_id, sha256_hex=file_spec.sha256_hex)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.68.11
3
+ Version: 0.68.14
4
4
  Summary: Python client library for Modal
5
5
  Author: Modal Labs
6
6
  Author-email: support@modal.com
@@ -3,7 +3,7 @@ modal/__main__.py,sha256=scYhGFqh8OJcVDo-VOxIT6CCwxOgzgflYWMnIZiMRqE,2871
3
3
  modal/_clustered_functions.py,sha256=kTf-9YBXY88NutC1akI-gCbvf01RhMPCw-zoOI_YIUE,2700
4
4
  modal/_clustered_functions.pyi,sha256=vllkegc99A0jrUOWa8mdlSbdp6uz36TsHhGxysAOpaQ,771
5
5
  modal/_container_entrypoint.py,sha256=wk10vA5vRZZsVwQ0yINOLd0i-NwH7x6XbhTslumvGjo,28910
6
- modal/_ipython.py,sha256=HF_DYy0e0qM9WnGDmTY30s1RxzGya9GeORCauCEpRaE,450
6
+ modal/_ipython.py,sha256=TW1fkVOmZL3YYqdS2YlM1hqpf654Yf8ZyybHdBnlhSw,301
7
7
  modal/_location.py,sha256=S3lSxIU3h9HkWpkJ3Pwo0pqjIOSB1fjeSgUsY3x7eec,1202
8
8
  modal/_output.py,sha256=0fWX_KQwhER--U81ys16CL-pA5A-LN20C0EZjElKGJQ,25410
9
9
  modal/_proxy_tunnel.py,sha256=gnKyCfmVB7x2d1A6c-JDysNIP3kEFxmXzhcXhPrzPn0,1906
@@ -19,7 +19,7 @@ modal/app.py,sha256=EJ7FUN6rWnSwLJoYJh8nmKg_t-8hdN8_rt0OrkP7JvQ,46084
19
19
  modal/app.pyi,sha256=BE5SlR5tRECuc6-e2lUuOknDdov3zxgZ4N0AsLb5ZVQ,25270
20
20
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
21
21
  modal/client.py,sha256=nyPjfromWBeOyurexpFP2QLQNk822RPggMCLyX9j1jA,15247
22
- modal/client.pyi,sha256=wnmsE-wrmK3qkFH1a5PoT45B11cFC5T3gLLO9KceJNU,7280
22
+ modal/client.pyi,sha256=4x5j0yBlE_sZf8cS2eChs-pYuxr-_SsRiKrf1fSJmfA,7280
23
23
  modal/cloud_bucket_mount.py,sha256=G7T7jWLD0QkmrfKR75mSTwdUZ2xNfj7pkVqb4ipmxmI,5735
24
24
  modal/cloud_bucket_mount.pyi,sha256=CEi7vrH3kDUF4LAy4qP6tfImy2UJuFRcRbsgRNM1wo8,1403
25
25
  modal/cls.py,sha256=ONnrfZ2vPcaY2JuKypPiBA9eTiyg8Qfg-Ull40nn9zs,30956
@@ -42,9 +42,9 @@ modal/image.py,sha256=cQ6WP1xHXZT_nY8z3aEFiGwKzrTV0yxi3Ab8JzF91eo,79653
42
42
  modal/image.pyi,sha256=PIKH6JBA4L5TfdJrQu3pm2ykyIITmiP920TpP8cdyQA,24585
43
43
  modal/io_streams.py,sha256=QkQiizKRzd5bnbKQsap31LJgBYlAnj4-XkV_50xPYX0,15079
44
44
  modal/io_streams.pyi,sha256=bCCVSxkMcosYd8O3PQDDwJw7TQ8JEcnYonLJ5t27TQs,4804
45
- modal/mount.py,sha256=liaid5p42o0OKnzoocJJ_oCovDVderk3-JuCTa5pqtA,27656
45
+ modal/mount.py,sha256=7FJrS-QkRJGndKuvRnMz452wfUcbLpd_UEnmFgQCKQQ,27770
46
46
  modal/mount.pyi,sha256=3e4nkXUeeVmUmOyK8Tiyk_EQlHeWruN3yGJVnmDUVrI,9761
47
- modal/network_file_system.py,sha256=NKZgh_p8MyJyyJgP92lhRgTmwA3kOPw7m8AbYlchhCE,14530
47
+ modal/network_file_system.py,sha256=kwwQLCJVO086FTiAWSF_jz9BkqijZLpSbEYXpFvS0Ik,14600
48
48
  modal/network_file_system.pyi,sha256=8mHKXuRkxHPazF6ljIW7g4M5aVqLSl6eKUPLgDCug5c,7901
49
49
  modal/object.py,sha256=HZs3N59C6JxlMuPQWJYvrWV1FEEkH9txUovVDorVUbs,9763
50
50
  modal/object.pyi,sha256=MO78H9yFSE5i1gExPEwyyQzLdlshkcGHN1aQ0ylyvq0,8802
@@ -73,7 +73,7 @@ modal/serving.pyi,sha256=ncV-9jY_vZYFnGs5ZnMb3ffrX8LmcLdIMHBC56xRbtE,1711
73
73
  modal/stream_type.py,sha256=A6320qoAAWhEfwOCZfGtymQTu5AfLfJXXgARqooTPvY,417
74
74
  modal/token_flow.py,sha256=LcgSce_MSQ2p7j55DPwpVRpiAtCDe8GRSEwzO7muNR8,6774
75
75
  modal/token_flow.pyi,sha256=gOYtYujrWt_JFZeiI8EmfahXPx5GCR5Na-VaPQcWgEY,1937
76
- modal/volume.py,sha256=IISuMeXq9MoSkhXg8Q6JG0F-2n9NTkWk0xGuJB8l3d8,29159
76
+ modal/volume.py,sha256=PGzbninvRU-IhSwJgM2jZKzD8llRhZhadsOxZ-YNwaM,29316
77
77
  modal/volume.pyi,sha256=St0mDiaojfep6Bs4sBbkRJmeacYHF6lh6FKOWGmheHA,11182
78
78
  modal/_runtime/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
79
79
  modal/_runtime/asgi.py,sha256=GvuxZqWnIHMIR-Bx5f7toCQlkERaJO8CHjTPNM9IFIw,21537
@@ -84,11 +84,11 @@ modal/_runtime/user_code_imports.py,sha256=4fI0F9OIaNOcO_S4Tx2JcnYwZwZq6JdhMAkUz
84
84
  modal/_utils/__init__.py,sha256=waLjl5c6IPDhSsdWAm9Bji4e2PVxamYABKAze6CHVXY,28
85
85
  modal/_utils/app_utils.py,sha256=88BT4TPLWfYAQwKTHcyzNQRHg8n9B-QE2UyJs96iV-0,108
86
86
  modal/_utils/async_utils.py,sha256=9ubwMkwiDB4gzOYG2jL9j7Fs-5dxHjcifZe3r7JRg-k,25091
87
- modal/_utils/blob_utils.py,sha256=0k_qUpO5GHnz538wjRhyRw4NdJ5O322N7QSilIu32jw,16601
87
+ modal/_utils/blob_utils.py,sha256=1D_dXspFdsVxkW3gsYH-wJUUHiCpYvlwhmCgYZZgN9k,17237
88
88
  modal/_utils/function_utils.py,sha256=LgcveUUb4XU_dWxtqgK_3ujZBvS3cGVzcDOkljyFZ2w,25066
89
89
  modal/_utils/grpc_testing.py,sha256=H1zHqthv19eGPJz2HKXDyWXWGSqO4BRsxah3L5Xaa8A,8619
90
90
  modal/_utils/grpc_utils.py,sha256=PPB5ay-vXencXNIWPVw5modr3EH7gfq2QPcO5YJ1lMU,7737
91
- modal/_utils/hash_utils.py,sha256=LOWJ9U5Eaye2ZIGQOdEk8RN8ExPFovGQisyuGv1PXU0,2236
91
+ modal/_utils/hash_utils.py,sha256=zg3J6OGxTFGSFri1qQ12giDz90lWk8bzaxCTUCRtiX4,3034
92
92
  modal/_utils/http_utils.py,sha256=VKXYNPJtrSwZ1ttcXVGQUWmn8cLAXiOTv05g2ac3GbU,2179
93
93
  modal/_utils/logger.py,sha256=ePzdudrtx9jJCjuO6-bcL_kwUJfi4AwloUmIiNtqkY0,1330
94
94
  modal/_utils/mount_utils.py,sha256=J-FRZbPQv1i__Tob-FIpbB1oXWpFLAwZiB4OCiJpFG0,3206
@@ -161,10 +161,10 @@ modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0y
161
161
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
162
162
  modal_version/__init__.py,sha256=RT6zPoOdFO99u5Wcxxaoir4ZCuPTbQ22cvzFAXl3vUY,470
163
163
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
164
- modal_version/_version_generated.py,sha256=iMmZstd6JhPFScB-dfN5XinMS8GWtT-kQD4yTA3FQr4,149
165
- modal-0.68.11.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
166
- modal-0.68.11.dist-info/METADATA,sha256=xrxk7D7aRQurLJimfndYZrKyYvfhEFqBVT_o7q7Mxgk,2329
167
- modal-0.68.11.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
168
- modal-0.68.11.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
169
- modal-0.68.11.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
170
- modal-0.68.11.dist-info/RECORD,,
164
+ modal_version/_version_generated.py,sha256=SWqJPFBaGp3gDChTb6Zx02oI_iQyIRtKpGgcw69Yegs,149
165
+ modal-0.68.14.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
166
+ modal-0.68.14.dist-info/METADATA,sha256=gHSGyvKb5gQiL1YbO3mRis3T8En0t19ug9mfOY4HYos,2329
167
+ modal-0.68.14.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
168
+ modal-0.68.14.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
169
+ modal-0.68.14.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
170
+ modal-0.68.14.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2024
2
2
 
3
3
  # Note: Reset this value to -1 whenever you make a minor `0.X` release of the client.
4
- build_number = 11 # git: 51e786f
4
+ build_number = 14 # git: 45ed247