modal 0.74.58__py3-none-any.whl → 0.74.59__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/cli/_download.py CHANGED
@@ -1,5 +1,6 @@
1
1
  # Copyright Modal Labs 2023
2
2
  import asyncio
3
+ import functools
3
4
  import os
4
5
  import shutil
5
6
  import sys
@@ -70,21 +71,30 @@ async def _volume_download(
70
71
  if is_pipe:
71
72
  if entry.type == FileEntryType.FILE:
72
73
  progress_task_id = progress_cb(name=entry.path, size=entry.size)
74
+ file_progress_cb = functools.partial(progress_cb, task_id=progress_task_id)
75
+
73
76
  async for chunk in volume.read_file(entry.path):
74
77
  sys.stdout.buffer.write(chunk)
75
- progress_cb(task_id=progress_task_id, advance=len(chunk))
76
- progress_cb(task_id=progress_task_id, complete=True)
78
+ file_progress_cb(advance=len(chunk))
79
+
80
+ file_progress_cb(complete=True)
77
81
  else:
78
82
  if entry.type == FileEntryType.FILE:
79
83
  progress_task_id = progress_cb(name=entry.path, size=entry.size)
80
84
  output_path.parent.mkdir(parents=True, exist_ok=True)
85
+ file_progress_cb = functools.partial(progress_cb, task_id=progress_task_id)
86
+
81
87
  with output_path.open("wb") as fp:
82
- b = 0
83
- async for chunk in volume.read_file(entry.path):
84
- b += fp.write(chunk)
85
- progress_cb(task_id=progress_task_id, advance=len(chunk))
88
+ if isinstance(volume, _Volume):
89
+ b = await volume.read_file_into_fileobj(entry.path, fp, file_progress_cb)
90
+ else:
91
+ b = 0
92
+ async for chunk in volume.read_file(entry.path):
93
+ b += fp.write(chunk)
94
+ file_progress_cb(advance=len(chunk))
95
+
86
96
  logger.debug(f"Wrote {b} bytes to {output_path}")
87
- progress_cb(task_id=progress_task_id, complete=True)
97
+ file_progress_cb(complete=True)
88
98
  elif entry.type == FileEntryType.DIRECTORY:
89
99
  output_path.mkdir(parents=True, exist_ok=True)
90
100
  finally:
modal/client.pyi CHANGED
@@ -27,7 +27,7 @@ class _Client:
27
27
  _snapshotted: bool
28
28
 
29
29
  def __init__(
30
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.74.58"
30
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.74.59"
31
31
  ): ...
32
32
  def is_closed(self) -> bool: ...
33
33
  @property
@@ -86,7 +86,7 @@ class Client:
86
86
  _snapshotted: bool
87
87
 
88
88
  def __init__(
89
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.74.58"
89
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.74.59"
90
90
  ): ...
91
91
  def is_closed(self) -> bool: ...
92
92
  @property
modal/volume.py CHANGED
@@ -463,6 +463,96 @@ class _Volume(_Object, type_prefix="vo"):
463
463
  yield value
464
464
 
465
465
 
466
+ @live_method
467
+ async def read_file_into_fileobj(
468
+ self,
469
+ path: str,
470
+ fileobj: typing.IO[bytes],
471
+ progress_cb: Optional[Callable[..., Any]] = None
472
+ ) -> int:
473
+ """mdmd:hidden
474
+ Read volume file into file-like IO object.
475
+ """
476
+ if progress_cb is None:
477
+ def progress_cb(*_, **__):
478
+ pass
479
+
480
+ if self._is_v1:
481
+ return await self._read_file_into_fileobj1(path, fileobj, progress_cb)
482
+ else:
483
+ return await self._read_file_into_fileobj2(path, fileobj, progress_cb)
484
+
485
+
486
+ async def _read_file_into_fileobj1(
487
+ self,
488
+ path: str,
489
+ fileobj: typing.IO[bytes],
490
+ progress_cb: Callable[..., Any]
491
+ ) -> int:
492
+ num_bytes_written = 0
493
+
494
+ async for chunk in self._read_file1(path):
495
+ num_chunk_bytes_written = 0
496
+
497
+ while num_chunk_bytes_written < len(chunk):
498
+ # TODO(dflemstr): this is a small write, but nonetheless might block the event loop for some time:
499
+ n = fileobj.write(chunk)
500
+ num_chunk_bytes_written += n
501
+ progress_cb(advance=n)
502
+
503
+ num_bytes_written += len(chunk)
504
+
505
+ return num_bytes_written
506
+
507
+
508
+ async def _read_file_into_fileobj2(
509
+ self,
510
+ path: str,
511
+ fileobj: typing.IO[bytes],
512
+ progress_cb: Callable[..., Any]
513
+ ) -> int:
514
+ req = api_pb2.VolumeGetFile2Request(volume_id=self.object_id, path=path)
515
+
516
+ try:
517
+ response = await retry_transient_errors(self._client.stub.VolumeGetFile2, req)
518
+ except GRPCError as exc:
519
+ raise FileNotFoundError(exc.message) if exc.status == Status.NOT_FOUND else exc
520
+
521
+ # TODO(dflemstr): Sane default limit? Make configurable?
522
+ download_semaphore = asyncio.Semaphore(multiprocessing.cpu_count())
523
+ write_lock = asyncio.Lock()
524
+ start_pos = fileobj.tell()
525
+
526
+ async def download_block(idx, url) -> int:
527
+ block_start_pos = start_pos + idx * BLOCK_SIZE
528
+ num_bytes_written = 0
529
+
530
+ async with download_semaphore, ClientSessionRegistry.get_session().get(url) as get_response:
531
+ async for chunk in get_response.content:
532
+ num_chunk_bytes_written = 0
533
+
534
+ while num_chunk_bytes_written < len(chunk):
535
+ async with write_lock:
536
+ fileobj.seek(block_start_pos + num_bytes_written + num_chunk_bytes_written)
537
+ # TODO(dflemstr): this is a small write, but nonetheless might block the event loop for some
538
+ # time:
539
+ n = fileobj.write(chunk)
540
+
541
+ num_chunk_bytes_written += n
542
+ progress_cb(advance=n)
543
+
544
+ num_bytes_written += len(chunk)
545
+
546
+ return num_bytes_written
547
+
548
+ coros = [download_block(idx, url) for idx, url in enumerate(response.get_urls)]
549
+
550
+ total_size = sum(await asyncio.gather(*coros))
551
+ fileobj.seek(start_pos + total_size)
552
+
553
+ return total_size
554
+
555
+
466
556
  @live_method
467
557
  async def remove_file(self, path: str, recursive: bool = False) -> None:
468
558
  """Remove a file or directory from a volume."""
modal/volume.pyi CHANGED
@@ -87,6 +87,18 @@ class _Volume(modal._object._Object):
87
87
  def read_file(self, path: str) -> collections.abc.AsyncIterator[bytes]: ...
88
88
  def _read_file1(self, path: str) -> collections.abc.AsyncIterator[bytes]: ...
89
89
  def _read_file2(self, path: str) -> collections.abc.AsyncIterator[bytes]: ...
90
+ async def read_file_into_fileobj(
91
+ self,
92
+ path: str,
93
+ fileobj: typing.IO[bytes],
94
+ progress_cb: typing.Optional[collections.abc.Callable[..., typing.Any]] = None,
95
+ ) -> int: ...
96
+ async def _read_file_into_fileobj1(
97
+ self, path: str, fileobj: typing.IO[bytes], progress_cb: collections.abc.Callable[..., typing.Any]
98
+ ) -> int: ...
99
+ async def _read_file_into_fileobj2(
100
+ self, path: str, fileobj: typing.IO[bytes], progress_cb: collections.abc.Callable[..., typing.Any]
101
+ ) -> int: ...
90
102
  async def remove_file(self, path: str, recursive: bool = False) -> None: ...
91
103
  async def copy_files(self, src_paths: collections.abc.Sequence[str], dst_path: str) -> None: ...
92
104
  async def batch_upload(self, force: bool = False) -> _AbstractVolumeUploadContextManager: ...
@@ -234,6 +246,44 @@ class Volume(modal.object.Object):
234
246
 
235
247
  _read_file2: ___read_file2_spec[typing_extensions.Self]
236
248
 
249
+ class __read_file_into_fileobj_spec(typing_extensions.Protocol[SUPERSELF]):
250
+ def __call__(
251
+ self,
252
+ /,
253
+ path: str,
254
+ fileobj: typing.IO[bytes],
255
+ progress_cb: typing.Optional[collections.abc.Callable[..., typing.Any]] = None,
256
+ ) -> int: ...
257
+ async def aio(
258
+ self,
259
+ /,
260
+ path: str,
261
+ fileobj: typing.IO[bytes],
262
+ progress_cb: typing.Optional[collections.abc.Callable[..., typing.Any]] = None,
263
+ ) -> int: ...
264
+
265
+ read_file_into_fileobj: __read_file_into_fileobj_spec[typing_extensions.Self]
266
+
267
+ class ___read_file_into_fileobj1_spec(typing_extensions.Protocol[SUPERSELF]):
268
+ def __call__(
269
+ self, /, path: str, fileobj: typing.IO[bytes], progress_cb: collections.abc.Callable[..., typing.Any]
270
+ ) -> int: ...
271
+ async def aio(
272
+ self, /, path: str, fileobj: typing.IO[bytes], progress_cb: collections.abc.Callable[..., typing.Any]
273
+ ) -> int: ...
274
+
275
+ _read_file_into_fileobj1: ___read_file_into_fileobj1_spec[typing_extensions.Self]
276
+
277
+ class ___read_file_into_fileobj2_spec(typing_extensions.Protocol[SUPERSELF]):
278
+ def __call__(
279
+ self, /, path: str, fileobj: typing.IO[bytes], progress_cb: collections.abc.Callable[..., typing.Any]
280
+ ) -> int: ...
281
+ async def aio(
282
+ self, /, path: str, fileobj: typing.IO[bytes], progress_cb: collections.abc.Callable[..., typing.Any]
283
+ ) -> int: ...
284
+
285
+ _read_file_into_fileobj2: ___read_file_into_fileobj2_spec[typing_extensions.Self]
286
+
237
287
  class __remove_file_spec(typing_extensions.Protocol[SUPERSELF]):
238
288
  def __call__(self, /, path: str, recursive: bool = False) -> None: ...
239
289
  async def aio(self, /, path: str, recursive: bool = False) -> None: ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 0.74.58
3
+ Version: 0.74.59
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -22,7 +22,7 @@ modal/app.py,sha256=xojuGZv4LaQwZU5ntj7WbmMjeNuB9Gll8Mzqe2LyiEs,51323
22
22
  modal/app.pyi,sha256=zNwR1_2LpmQc9AhemuAeVdk90XNYDw9keOkXAwAATeA,28732
23
23
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
24
24
  modal/client.py,sha256=o-aQThHpvDHUzg_kUafyhWzACViUBhY2WLZ2EitnSHA,16787
25
- modal/client.pyi,sha256=s35e9PyIePi0LYczptJLzW634xvENbqmQwwyVmWGqqc,8385
25
+ modal/client.pyi,sha256=DHS5JgIcHrXAE9ADaeYMxYw_IcuSVQbDo8oaRZly784,8385
26
26
  modal/cloud_bucket_mount.py,sha256=YOe9nnvSr4ZbeCn587d7_VhE9IioZYRvF9VYQTQux08,5914
27
27
  modal/cloud_bucket_mount.pyi,sha256=30T3K1a89l6wzmEJ_J9iWv9SknoGqaZDx59Xs-ZQcmk,1607
28
28
  modal/cls.py,sha256=aHoMEWMZUN7bOezs3tRPxzS1FP3gTxZBORVjbPmtxyg,35338
@@ -78,8 +78,8 @@ modal/snapshot.pyi,sha256=dIEBdTPb7O3VwkQ8TMPjfyU17RLuS9i0DnACxxHy8X4,676
78
78
  modal/stream_type.py,sha256=A6320qoAAWhEfwOCZfGtymQTu5AfLfJXXgARqooTPvY,417
79
79
  modal/token_flow.py,sha256=0_4KabXKsuE4OXTJ1OuLOtA-b1sesShztMZkkRFK7tA,7605
80
80
  modal/token_flow.pyi,sha256=ILbRv6JsZq-jK8jcJM7eB74e0PsbzwBm7hyPcV9lBlQ,2121
81
- modal/volume.py,sha256=4njGgpmC7bVAxwDgn88XJNtaCk4mrz1qLcHBXXCXklg,41085
82
- modal/volume.pyi,sha256=-rcgdhGCaQV5v--hgkABcuYH1VbtElKPUC4AnxonNtk,18970
81
+ modal/volume.py,sha256=DFkdjipvjmWvedd1FL_CjIwyfT7QN-VxusJ9O3fVCw4,44206
82
+ modal/volume.pyi,sha256=9hPIMRBzGZycVL8uRfGpjSmNu_pCbkGAOyrnE86bU2Y,21113
83
83
  modal/_runtime/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
84
84
  modal/_runtime/asgi.py,sha256=_2xSTsDD27Cit7xnMs4lzkJA2wzer2_N4Oa3BkXFzVA,22521
85
85
  modal/_runtime/container_io_manager.py,sha256=6j0jO2-s9ShckM4SK45OapoQxWW9HQwQjFaBkXPJPwU,44763
@@ -115,7 +115,7 @@ modal/_vendor/a2wsgi_wsgi.py,sha256=Q1AsjpV_Q_vzQsz_cSqmP9jWzsGsB-ARFU6vpQYml8k,
115
115
  modal/_vendor/cloudpickle.py,sha256=avxOIgNKqL9KyPNuIOVQzBm0D1l9ipeB4RrcUMUGmeQ,55216
116
116
  modal/_vendor/tblib.py,sha256=g1O7QUDd3sDoLd8YPFltkXkih7r_fyZOjgmGuligv3s,9722
117
117
  modal/cli/__init__.py,sha256=6FRleWQxBDT19y7OayO4lBOzuL6Bs9r0rLINYYYbHwQ,769
118
- modal/cli/_download.py,sha256=t6BXZwjTd9MgznDvbsV8rp0FZWggdzC-lUAGZU4xx1g,3984
118
+ modal/cli/_download.py,sha256=tV8JFkncTtQKh85bSguQg6AW5aRRlynf-rvyN7ruigc,4337
119
119
  modal/cli/_traceback.py,sha256=4ywtmFcmPnY3tqb4-3fA061N2tRiM01xs8fSagtkwhE,7293
120
120
  modal/cli/app.py,sha256=87LWg3bTQQIHFOqs8iiJYD_X03omXBZ6lFYR0rMJV-I,8433
121
121
  modal/cli/cluster.py,sha256=EBDhkzfOtPSbwknYdYPBGYvRAwl4Gm7OJkD6_zxrcus,3106
@@ -146,7 +146,7 @@ modal/requirements/2024.10.txt,sha256=qD-5cVIVM9wXesJ6JC89Ew-3m2KjEElUz3jaw_MddR
146
146
  modal/requirements/PREVIEW.txt,sha256=qD-5cVIVM9wXesJ6JC89Ew-3m2KjEElUz3jaw_MddRo,296
147
147
  modal/requirements/README.md,sha256=9tK76KP0Uph7O0M5oUgsSwEZDj5y-dcUPsnpR0Sc-Ik,854
148
148
  modal/requirements/base-images.json,sha256=57vMSqzMbLBxw5tFWSaMiIkkVEps4JfX5PAtXGnkS4U,740
149
- modal-0.74.58.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
149
+ modal-0.74.59.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
150
150
  modal_docs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
151
151
  modal_docs/gen_cli_docs.py,sha256=c1yfBS_x--gL5bs0N4ihMwqwX8l3IBWSkBAKNNIi6bQ,3801
152
152
  modal_docs/gen_reference_docs.py,sha256=d_CQUGQ0rfw28u75I2mov9AlS773z9rG40-yq5o7g2U,6359
@@ -171,9 +171,9 @@ modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0y
171
171
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
172
172
  modal_version/__init__.py,sha256=m94xZNWIjH8oUtJk4l9xfovzDJede2o7X-q0MHVECtM,470
173
173
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
174
- modal_version/_version_generated.py,sha256=ty9vlBouwII9j47zM8FqtqP8f3FZEMvofJhfF5qojIY,149
175
- modal-0.74.58.dist-info/METADATA,sha256=8v0JtnopQExL5NG0TFZwr7Q8NY9tYSsGUOH_XTYa-qU,2451
176
- modal-0.74.58.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
177
- modal-0.74.58.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
178
- modal-0.74.58.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
179
- modal-0.74.58.dist-info/RECORD,,
174
+ modal_version/_version_generated.py,sha256=gO9v_hSDkfPZyxi6-dUheFZ5pAFiRJ0MvryhC8RNw8w,149
175
+ modal-0.74.59.dist-info/METADATA,sha256=FjOM1ctCE-exDej2RbPnPh3Pcm5HjkV4M7lgk8vFjBY,2451
176
+ modal-0.74.59.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
177
+ modal-0.74.59.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
178
+ modal-0.74.59.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
179
+ modal-0.74.59.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2025
2
2
 
3
3
  # Note: Reset this value to -1 whenever you make a minor `0.X` release of the client.
4
- build_number = 58 # git: ea8f826
4
+ build_number = 59 # git: 57679cb