modal 0.67.16__py3-none-any.whl → 0.67.20__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.
@@ -6,7 +6,7 @@ import inspect
6
6
  import itertools
7
7
  import time
8
8
  import typing
9
- from collections.abc import AsyncGenerator, Awaitable, Iterable, Iterator
9
+ from collections.abc import AsyncGenerator, AsyncIterable, Awaitable, Iterable, Iterator
10
10
  from contextlib import asynccontextmanager
11
11
  from dataclasses import dataclass
12
12
  from typing import (
@@ -484,14 +484,17 @@ class aclosing(typing.Generic[T]): # noqa
484
484
  await self.agen.aclose()
485
485
 
486
486
 
487
- async def sync_or_async_iter(iter: Union[Iterable[T], AsyncGenerator[T, None]]) -> AsyncGenerator[T, None]:
487
+ async def sync_or_async_iter(iter: Union[Iterable[T], AsyncIterable[T]]) -> AsyncGenerator[T, None]:
488
488
  if hasattr(iter, "__aiter__"):
489
489
  agen = typing.cast(AsyncGenerator[T, None], iter)
490
490
  try:
491
491
  async for item in agen:
492
492
  yield item
493
493
  finally:
494
- await agen.aclose()
494
+ if hasattr(agen, "aclose"):
495
+ # All AsyncGenerator's have an aclose method
496
+ # but some AsyncIterable's don't necessarily
497
+ await agen.aclose()
495
498
  else:
496
499
  assert hasattr(iter, "__iter__"), "sync_or_async_iter requires an Iterable or AsyncGenerator"
497
500
  # This intentionally could block the event loop for the duration of calling __iter__ and __next__,
@@ -5,6 +5,7 @@ import hashlib
5
5
  import io
6
6
  import os
7
7
  import platform
8
+ import time
8
9
  from collections.abc import AsyncIterator
9
10
  from contextlib import AbstractContextManager, contextmanager
10
11
  from pathlib import Path, PurePosixPath
@@ -289,11 +290,18 @@ async def _blob_upload(
289
290
 
290
291
 
291
292
  async def blob_upload(payload: bytes, stub: ModalClientModal) -> str:
293
+ size_mib = len(payload) / 1024 / 1024
294
+ logger.debug(f"Uploading large blob of size {size_mib:.2f} MiB")
295
+ t0 = time.time()
292
296
  if isinstance(payload, str):
293
297
  logger.warning("Blob uploading string, not bytes - auto-encoding as utf8")
294
298
  payload = payload.encode("utf8")
295
299
  upload_hashes = get_upload_hashes(payload)
296
- return await _blob_upload(upload_hashes, payload, stub)
300
+ blob_id = await _blob_upload(upload_hashes, payload, stub)
301
+ dur_s = max(time.time() - t0, 0.001) # avoid division by zero
302
+ throughput_mib_s = (size_mib) / dur_s
303
+ logger.debug(f"Uploaded large blob of size {size_mib:.2f} MiB ({throughput_mib_s:.2f} MiB/s)." f" {blob_id}")
304
+ return blob_id
297
305
 
298
306
 
299
307
  async def blob_upload_file(
@@ -318,11 +326,17 @@ async def _download_from_url(download_url: str) -> bytes:
318
326
 
319
327
 
320
328
  async def blob_download(blob_id: str, stub: ModalClientModal) -> bytes:
321
- # convenience function reading all of the downloaded file into memory
329
+ """Convenience function for reading all of the downloaded file into memory."""
330
+ logger.debug(f"Downloading large blob {blob_id}")
331
+ t0 = time.time()
322
332
  req = api_pb2.BlobGetRequest(blob_id=blob_id)
323
333
  resp = await retry_transient_errors(stub.BlobGet, req)
324
-
325
- return await _download_from_url(resp.download_url)
334
+ data = await _download_from_url(resp.download_url)
335
+ size_mib = len(data) / 1024 / 1024
336
+ dur_s = max(time.time() - t0, 0.001) # avoid division by zero
337
+ throughput_mib_s = size_mib / dur_s
338
+ logger.debug(f"Downloaded large blob {blob_id} of size {size_mib:.2f} MiB ({throughput_mib_s:.2f} MiB/s)")
339
+ return data
326
340
 
327
341
 
328
342
  async def blob_iter(blob_id: str, stub: ModalClientModal) -> AsyncIterator[bytes]:
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.67.16"
29
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.20"
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.67.16"
84
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.20"
85
85
  ): ...
86
86
  def is_closed(self) -> bool: ...
87
87
  @property
modal/io_streams.py CHANGED
@@ -313,6 +313,11 @@ class _StreamReader(Generic[T]):
313
313
  else:
314
314
  return cast(T, value)
315
315
 
316
+ async def aclose(self):
317
+ """mdmd:hidden"""
318
+ if self._stream:
319
+ await self._stream.aclose()
320
+
316
321
 
317
322
  MAX_BUFFER_SIZE = 2 * 1024 * 1024
318
323
 
modal/io_streams.pyi CHANGED
@@ -37,6 +37,7 @@ class _StreamReader(typing.Generic[T]):
37
37
  def _get_logs_by_line(self) -> collections.abc.AsyncGenerator[typing.Optional[bytes], None]: ...
38
38
  def __aiter__(self) -> collections.abc.AsyncIterator[T]: ...
39
39
  async def __anext__(self) -> T: ...
40
+ async def aclose(self): ...
40
41
 
41
42
  class _StreamWriter:
42
43
  def __init__(
@@ -103,6 +104,8 @@ class StreamReader(typing.Generic[T]):
103
104
  def __aiter__(self) -> collections.abc.AsyncIterator[T]: ...
104
105
  def __next__(self) -> T: ...
105
106
  async def __anext__(self) -> T: ...
107
+ def close(self): ...
108
+ async def aclose(self): ...
106
109
 
107
110
  class StreamWriter:
108
111
  def __init__(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.67.16
3
+ Version: 0.67.20
4
4
  Summary: Python client library for Modal
5
5
  Author: Modal Labs
6
6
  Author-email: support@modal.com
@@ -21,7 +21,7 @@ Requires-Dist: fastapi
21
21
  Requires-Dist: grpclib (==0.4.7)
22
22
  Requires-Dist: protobuf (!=4.24.0,<6.0,>=3.19)
23
23
  Requires-Dist: rich (>=12.0.0)
24
- Requires-Dist: synchronicity (~=0.9.3)
24
+ Requires-Dist: synchronicity (~=0.9.4)
25
25
  Requires-Dist: toml
26
26
  Requires-Dist: typer (>=0.9)
27
27
  Requires-Dist: types-certifi
@@ -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=VMg_aIuo_LOEe2ttxBHEND3PLhTp5lo-onH4wELhIyY,16375
22
- modal/client.pyi,sha256=IJfwJhllWxKJfV38qk43KNNHSoo3HQAhUu6uqrIsCOo,7354
22
+ modal/client.pyi,sha256=X7veyeLTIPn_1EkLl0uiD1cfRP4ZgF2aci_ULiCgrNo,7354
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=F2jk5zFCAA8h-GfM0dbdBG3Mu5wiG9k9Z9JLYRYuT2Q,24758
@@ -38,8 +38,8 @@ modal/functions.pyi,sha256=fifvDS5GDEYmXjko1UGZrKqmhfnQn6GRwCblM9hrRWo,25107
38
38
  modal/gpu.py,sha256=r4rL6uH3UJIQthzYvfWauXNyh01WqCPtKZCmmSX1fd4,6881
39
39
  modal/image.py,sha256=ZIC8tgjJnqWamN4sZ0Gch3x2VmcM671MWfRLR5SMmoc,79423
40
40
  modal/image.pyi,sha256=JjicLNuaBsfuPZ_xo_eN0zKZkDrEm2alYg-szENhJjM,24591
41
- modal/io_streams.py,sha256=4pF2HumRK1pVnrx6S9UwGqJn69rQqyQqpe5X_nifny0,14943
42
- modal/io_streams.pyi,sha256=An766S3JKP78b2A4RphjdVNR73yblDc5uG_xp5--6k4,4715
41
+ modal/io_streams.py,sha256=YfKAlWQAxzPCHE0-wVlAlX5vldrpfKMdr9ggL0c5VJo,15063
42
+ modal/io_streams.pyi,sha256=bCCVSxkMcosYd8O3PQDDwJw7TQ8JEcnYonLJ5t27TQs,4804
43
43
  modal/mount.py,sha256=_N_fd5NX_eWwmb_xh_X_28nNHW9upEDXDyXixZWnUiQ,27730
44
44
  modal/mount.pyi,sha256=3e4nkXUeeVmUmOyK8Tiyk_EQlHeWruN3yGJVnmDUVrI,9761
45
45
  modal/network_file_system.py,sha256=mwtYp25XtFaiGpSG7U0KSkiTzJWrxgGTcoxfPZ9yGR0,14141
@@ -81,8 +81,8 @@ modal/_runtime/telemetry.py,sha256=T1RoAGyjBDr1swiM6pPsGRSITm7LI5FDK18oNXxY08U,5
81
81
  modal/_runtime/user_code_imports.py,sha256=q_3JOYqCPDcVFZWCHEjyEqj8yzdFsQ49HzeqYmFDLbk,14521
82
82
  modal/_utils/__init__.py,sha256=waLjl5c6IPDhSsdWAm9Bji4e2PVxamYABKAze6CHVXY,28
83
83
  modal/_utils/app_utils.py,sha256=88BT4TPLWfYAQwKTHcyzNQRHg8n9B-QE2UyJs96iV-0,108
84
- modal/_utils/async_utils.py,sha256=CYXogDVqqUtSe-DVP2A3F-6KztjPZaW6ez2lrYBCW_Y,24917
85
- modal/_utils/blob_utils.py,sha256=XJpFr6SfNoODZEjyGm7WH9R3pKsK4yqpzZOFR3YX6fc,15856
84
+ modal/_utils/async_utils.py,sha256=9ubwMkwiDB4gzOYG2jL9j7Fs-5dxHjcifZe3r7JRg-k,25091
85
+ modal/_utils/blob_utils.py,sha256=0k_qUpO5GHnz538wjRhyRw4NdJ5O322N7QSilIu32jw,16601
86
86
  modal/_utils/function_utils.py,sha256=SkT5emqGJ8NNASk0BlBmgDfDBUYAkUM851K74qCHL98,24641
87
87
  modal/_utils/grpc_testing.py,sha256=iqM9n5M0cWUUIIWNaEDer_pIfPnzXdZBO4L8FVbNepQ,8309
88
88
  modal/_utils/grpc_utils.py,sha256=PPB5ay-vXencXNIWPVw5modr3EH7gfq2QPcO5YJ1lMU,7737
@@ -142,13 +142,13 @@ modal_global_objects/mounts/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0
142
142
  modal_global_objects/mounts/modal_client_package.py,sha256=W0E_yShsRojPzWm6LtIQqNVolapdnrZkm2hVEQuZK_4,767
143
143
  modal_global_objects/mounts/python_standalone.py,sha256=_vTEX3PECUsatzhDs8lyJmDK0LbFetT1sJB6MIDfFAo,1870
144
144
  modal_proto/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
145
- modal_proto/api.proto,sha256=1zr0aMwKHYgPfkvAjQ5hCRiWbJcuVXxwiuOlLpDU46Y,77644
146
- modal_proto/api_grpc.py,sha256=S7h8xe-msb3-Q8oSd7DUoB46z-dcRhsXGb6LjFCLNFI,99013
147
- modal_proto/api_pb2.py,sha256=PLRmPloiKDiiziRzbZrzF3Cr-Cn5uCv9USwc7nC_eeg,282809
148
- modal_proto/api_pb2.pyi,sha256=LAs_gmu_GYKzfa_8dxXiXsNejnWyWjX5hQ-6MuJmww0,378389
149
- modal_proto/api_pb2_grpc.py,sha256=g7EfCSir3xStPPjJOU2U668zz6cGdN6u7SxvTTwU9aU,214126
150
- modal_proto/api_pb2_grpc.pyi,sha256=9GhLZVRm69Qhyj_jmGqEGv1rD37Tzj6E6hGzKV08u48,49961
151
- modal_proto/modal_api_grpc.py,sha256=en48QTR5fwA7x0twtlsqLKRjjDEAKVoh6EeSznQfQ3U,13236
145
+ modal_proto/api.proto,sha256=I36DzPZ4fs045HqQCEdgEiiJr1Dcd6T6fFA1RbQW7aE,78014
146
+ modal_proto/api_grpc.py,sha256=cQOfwiGd2Nyj9esTgtu39EK1QKGZJXharISgWG7_lQ8,99814
147
+ modal_proto/api_pb2.py,sha256=rk-8yxgiqsevrf8ZOQl7Elr5xKKz0nYdI46Yd6S0iJI,284335
148
+ modal_proto/api_pb2.pyi,sha256=FCMnndhn2lqdFo1kr49PjfOQzxacPg2UHM9hGpatdS8,380051
149
+ modal_proto/api_pb2_grpc.py,sha256=VTrD72cWvA2SscVVpR7w0swwGJ2O6pY-uASamt9Qm7M,215824
150
+ modal_proto/api_pb2_grpc.pyi,sha256=SbWtkTeCztsSOz8WaM07UZ5fxfKn7nQG4Dxbp5PQUz8,50356
151
+ modal_proto/modal_api_grpc.py,sha256=AUfZ2n1xOaCv-NzMp5w91fgsQi5zs79IDLH7RGOg1-o,13340
152
152
  modal_proto/modal_options_grpc.py,sha256=qJ1cuwA54oRqrdTyPTbvfhFZYd9HhJKK5UCwt523r3Y,120
153
153
  modal_proto/options.proto,sha256=a-siq4swVbZPfaFRXAipRZzGP2bq8OsdUvjlyzAeodQ,488
154
154
  modal_proto/options_grpc.py,sha256=M18X3d-8F_cNYSVM3I25dUTO5rZ0rd-vCCfynfh13Nc,125
@@ -159,10 +159,10 @@ modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0y
159
159
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
160
160
  modal_version/__init__.py,sha256=3IY-AWLH55r35_mQXIaut0jrJvoPuf1NZJBQQfSbPuo,470
161
161
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
162
- modal_version/_version_generated.py,sha256=YjVTHXgFgY_BKue5ypqLKUgLAOu2RSFr5ZWkU08vbx4,149
163
- modal-0.67.16.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
164
- modal-0.67.16.dist-info/METADATA,sha256=1mxQSL4rxUlG1ThKNOsKPg3DOWUK-HafKMRkUO9ibXg,2329
165
- modal-0.67.16.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
166
- modal-0.67.16.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
167
- modal-0.67.16.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
168
- modal-0.67.16.dist-info/RECORD,,
162
+ modal_version/_version_generated.py,sha256=m1_NoPmjChEXPSIuHS00ITMgr-hCNtEJ_3c9yzP9NXA,149
163
+ modal-0.67.20.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
164
+ modal-0.67.20.dist-info/METADATA,sha256=lAIzSrk4e-LdS1QRA2QVbQnNGI9OXySsTjBxIDUBiMk,2329
165
+ modal-0.67.20.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
166
+ modal-0.67.20.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
167
+ modal-0.67.20.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
168
+ modal-0.67.20.dist-info/RECORD,,
modal_proto/api.proto CHANGED
@@ -2146,6 +2146,18 @@ message SandboxListResponse {
2146
2146
  repeated SandboxInfo sandboxes = 1;
2147
2147
  }
2148
2148
 
2149
+ message SandboxSnapshotFsRequest {
2150
+ string sandbox_id = 1;
2151
+ float timeout = 2;
2152
+ }
2153
+
2154
+ message SandboxSnapshotFsResponse {
2155
+ string image_id = 1;
2156
+ GenericResult result = 2;
2157
+ // Metadata may be empty since we may skip it for performance reasons.
2158
+ ImageMetadata image_metadata = 3;
2159
+ }
2160
+
2149
2161
  message SandboxStdinWriteRequest {
2150
2162
  string sandbox_id = 1;
2151
2163
  bytes input = 2;
@@ -2745,6 +2757,7 @@ service ModalClient {
2745
2757
  rpc SandboxGetTaskId(SandboxGetTaskIdRequest) returns (SandboxGetTaskIdResponse); // needed for modal container exec
2746
2758
  rpc SandboxGetTunnels(SandboxGetTunnelsRequest) returns (SandboxGetTunnelsResponse);
2747
2759
  rpc SandboxList(SandboxListRequest) returns (SandboxListResponse);
2760
+ rpc SandboxSnapshotFs(SandboxSnapshotFsRequest) returns (SandboxSnapshotFsResponse);
2748
2761
  rpc SandboxStdinWrite(SandboxStdinWriteRequest) returns (SandboxStdinWriteResponse);
2749
2762
  rpc SandboxTagsSet(SandboxTagsSetRequest) returns (google.protobuf.Empty);
2750
2763
  rpc SandboxTerminate(SandboxTerminateRequest) returns (SandboxTerminateResponse);
modal_proto/api_grpc.py CHANGED
@@ -389,6 +389,10 @@ class ModalClientBase(abc.ABC):
389
389
  async def SandboxList(self, stream: 'grpclib.server.Stream[modal_proto.api_pb2.SandboxListRequest, modal_proto.api_pb2.SandboxListResponse]') -> None:
390
390
  pass
391
391
 
392
+ @abc.abstractmethod
393
+ async def SandboxSnapshotFs(self, stream: 'grpclib.server.Stream[modal_proto.api_pb2.SandboxSnapshotFsRequest, modal_proto.api_pb2.SandboxSnapshotFsResponse]') -> None:
394
+ pass
395
+
392
396
  @abc.abstractmethod
393
397
  async def SandboxStdinWrite(self, stream: 'grpclib.server.Stream[modal_proto.api_pb2.SandboxStdinWriteRequest, modal_proto.api_pb2.SandboxStdinWriteResponse]') -> None:
394
398
  pass
@@ -1093,6 +1097,12 @@ class ModalClientBase(abc.ABC):
1093
1097
  modal_proto.api_pb2.SandboxListRequest,
1094
1098
  modal_proto.api_pb2.SandboxListResponse,
1095
1099
  ),
1100
+ '/modal.client.ModalClient/SandboxSnapshotFs': grpclib.const.Handler(
1101
+ self.SandboxSnapshotFs,
1102
+ grpclib.const.Cardinality.UNARY_UNARY,
1103
+ modal_proto.api_pb2.SandboxSnapshotFsRequest,
1104
+ modal_proto.api_pb2.SandboxSnapshotFsResponse,
1105
+ ),
1096
1106
  '/modal.client.ModalClient/SandboxStdinWrite': grpclib.const.Handler(
1097
1107
  self.SandboxStdinWrite,
1098
1108
  grpclib.const.Cardinality.UNARY_UNARY,
@@ -1873,6 +1883,12 @@ class ModalClientStub:
1873
1883
  modal_proto.api_pb2.SandboxListRequest,
1874
1884
  modal_proto.api_pb2.SandboxListResponse,
1875
1885
  )
1886
+ self.SandboxSnapshotFs = grpclib.client.UnaryUnaryMethod(
1887
+ channel,
1888
+ '/modal.client.ModalClient/SandboxSnapshotFs',
1889
+ modal_proto.api_pb2.SandboxSnapshotFsRequest,
1890
+ modal_proto.api_pb2.SandboxSnapshotFsResponse,
1891
+ )
1876
1892
  self.SandboxStdinWrite = grpclib.client.UnaryUnaryMethod(
1877
1893
  channel,
1878
1894
  '/modal.client.ModalClient/SandboxStdinWrite',