modal 0.73.94__py3-none-any.whl → 0.73.96__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.
@@ -385,6 +385,10 @@ class AsyncOrSyncIterable:
385
385
  except NestedEventLoops:
386
386
  raise InvalidError(self.nested_async_message)
387
387
 
388
+ async def aclose(self):
389
+ if hasattr(self._async_iterable, "aclose"):
390
+ await self._async_iterable.aclose()
391
+
388
392
 
389
393
  _shutdown_tasks = []
390
394
 
@@ -8,6 +8,7 @@ import typing
8
8
  import urllib.parse
9
9
  import uuid
10
10
  from collections.abc import AsyncIterator
11
+ from dataclasses import dataclass
11
12
  from typing import (
12
13
  Any,
13
14
  Optional,
@@ -68,6 +69,11 @@ RETRYABLE_GRPC_STATUS_CODES = [
68
69
  Status.INTERNAL,
69
70
  ]
70
71
 
72
+ @dataclass
73
+ class RetryWarningMessage:
74
+ message: str
75
+ warning_interval: int
76
+ errors_to_warn_for: typing.List[Status]
71
77
 
72
78
  def create_channel(
73
79
  server_url: str,
@@ -144,6 +150,7 @@ async def retry_transient_errors(
144
150
  attempt_timeout: Optional[float] = None, # timeout for each attempt
145
151
  total_timeout: Optional[float] = None, # timeout for the entire function call
146
152
  attempt_timeout_floor=2.0, # always have at least this much timeout (only for total_timeout)
153
+ retry_warning_message: Optional[RetryWarningMessage] = None
147
154
  ) -> ResponseType:
148
155
  """Retry on transient gRPC failures with back-off until max_retries is reached.
149
156
  If max_retries is None, retry forever."""
@@ -212,6 +219,9 @@ async def retry_transient_errors(
212
219
 
213
220
  n_retries += 1
214
221
 
222
+ if retry_warning_message and n_retries % retry_warning_message.warning_interval == 0:
223
+ logger.warning(retry_warning_message.message)
224
+
215
225
  await asyncio.sleep(delay)
216
226
  delay = min(delay * delay_factor, max_delay)
217
227
 
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.73.94"
30
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.73.96"
31
31
  ): ...
32
32
  def is_closed(self) -> bool: ...
33
33
  @property
@@ -85,7 +85,7 @@ class Client:
85
85
  _snapshotted: bool
86
86
 
87
87
  def __init__(
88
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.73.94"
88
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.73.96"
89
89
  ): ...
90
90
  def is_closed(self) -> bool: ...
91
91
  @property
modal/parallel_map.py CHANGED
@@ -5,7 +5,7 @@ import typing
5
5
  from dataclasses import dataclass
6
6
  from typing import Any, Callable, Optional
7
7
 
8
- from grpclib import GRPCError, Status
8
+ from grpclib import Status
9
9
 
10
10
  from modal._runtime.execution_context import current_input_id
11
11
  from modal._utils.async_utils import (
@@ -27,13 +27,17 @@ from modal._utils.function_utils import (
27
27
  _create_input,
28
28
  _process_result,
29
29
  )
30
- from modal._utils.grpc_utils import retry_transient_errors
30
+ from modal._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES, RetryWarningMessage, retry_transient_errors
31
31
  from modal.config import logger
32
32
  from modal_proto import api_pb2
33
33
 
34
34
  if typing.TYPE_CHECKING:
35
35
  import modal.client
36
36
 
37
+ # pump_inputs should retry if it receives any of the standard retryable codes plus RESOURCE_EXHAUSTED.
38
+ PUMP_INPUTS_RETRYABLE_GRPC_STATUS_CODES = RETRYABLE_GRPC_STATUS_CODES + [Status.RESOURCE_EXHAUSTED]
39
+ PUMP_INPUTS_MAX_RETRIES = 8
40
+ PUMP_INPUTS_MAX_RETRY_DELAY=15
37
41
 
38
42
  class _SynchronizedQueue:
39
43
  """mdmd:hidden"""
@@ -136,25 +140,19 @@ async def _map_invocation(
136
140
  logger.debug(
137
141
  f"Pushing {len(items)} inputs to server. Num queued inputs awaiting push is {input_queue.qsize()}."
138
142
  )
139
- while True:
140
- try:
141
- resp = await retry_transient_errors(
142
- client.stub.FunctionPutInputs,
143
- request,
144
- # with 8 retries we log the warning below about every 30 secondswhich isn't too spammy.
145
- max_retries=8,
146
- max_delay=15,
147
- additional_status_codes=[Status.RESOURCE_EXHAUSTED],
148
- )
149
- break
150
- except GRPCError as err:
151
- if err.status != Status.RESOURCE_EXHAUSTED:
152
- raise err
153
- logger.warning(
154
- f"Warning: map progress for function {function._function_name} is limited."
155
- " Common bottlenecks include slow iteration over results, or function backlogs."
156
- )
157
-
143
+ # with 8 retries we log the warning below about every 30 seconds which isn't too spammy.
144
+ retry_warning_message = RetryWarningMessage(
145
+ message=f"Warning: map progress for function {function._function_name} is limited."
146
+ " Common bottlenecks include slow iteration over results, or function backlogs.",
147
+ warning_interval=8,
148
+ errors_to_warn_for=[Status.RESOURCE_EXHAUSTED])
149
+ resp = await retry_transient_errors(
150
+ client.stub.FunctionPutInputs,
151
+ request,
152
+ max_retries=None,
153
+ max_delay=PUMP_INPUTS_MAX_RETRY_DELAY,
154
+ additional_status_codes=[Status.RESOURCE_EXHAUSTED],
155
+ retry_warning_message=retry_warning_message)
158
156
  count_update()
159
157
  for item in resp.inputs:
160
158
  pending_outputs.setdefault(item.input_id, 0)
@@ -312,7 +310,7 @@ def _map_sync(
312
310
 
313
311
  @warn_if_generator_is_not_consumed(function_name="Function.map.aio")
314
312
  async def _map_async(
315
- self,
313
+ self: "modal.functions.Function",
316
314
  *input_iterators: typing.Union[
317
315
  typing.Iterable[Any], typing.AsyncIterable[Any]
318
316
  ], # one input iterator per argument in the mapped-over function/generator
@@ -339,19 +337,20 @@ async def _map_async(
339
337
  async for args in streamer:
340
338
  await raw_input_queue.put.aio((args, kwargs))
341
339
  await raw_input_queue.put.aio(None) # end-of-input sentinel
342
-
343
- feed_input_task = asyncio.create_task(feed_queue())
344
-
345
- try:
346
- # note that `map()` and `map.aio()` are not synchronicity-wrapped, since
347
- # they accept executable code in the form of
348
- # iterators that we don't want to run inside the synchronicity thread.
349
- # Instead, we delegate to `._map()` with a safer Queue as input
350
- async with aclosing(self._map.aio(raw_input_queue, order_outputs, return_exceptions)) as map_output_stream:
351
- async for output in map_output_stream:
352
- yield output
353
- finally:
354
- feed_input_task.cancel() # should only be needed in case of exceptions
340
+ if False:
341
+ # make this a never yielding generator so we can async_merge it below
342
+ # this is important so any exception raised in feed_queue will be propagated
343
+ yield
344
+
345
+ # note that `map()` and `map.aio()` are not synchronicity-wrapped, since
346
+ # they accept executable code in the form of
347
+ # iterators that we don't want to run inside the synchronicity thread.
348
+ # Instead, we delegate to `._map()` with a safer Queue as input
349
+ async with aclosing(
350
+ async_merge(self._map.aio(raw_input_queue, order_outputs, return_exceptions), feed_queue())
351
+ ) as map_output_stream:
352
+ async for output in map_output_stream:
353
+ yield output
355
354
 
356
355
 
357
356
  def _for_each_sync(self, *input_iterators, kwargs={}, ignore_exceptions: bool = False):
modal/parallel_map.pyi CHANGED
@@ -2,6 +2,7 @@ import collections.abc
2
2
  import modal._functions
3
3
  import modal._utils.async_utils
4
4
  import modal.client
5
+ import modal.functions
5
6
  import typing
6
7
  import typing_extensions
7
8
 
@@ -52,7 +53,7 @@ def _map_sync(
52
53
  self, *input_iterators, kwargs={}, order_outputs: bool = True, return_exceptions: bool = False
53
54
  ) -> modal._utils.async_utils.AsyncOrSyncIterable: ...
54
55
  def _map_async(
55
- self,
56
+ self: modal.functions.Function,
56
57
  *input_iterators: typing.Union[typing.Iterable[typing.Any], typing.AsyncIterable[typing.Any]],
57
58
  kwargs={},
58
59
  order_outputs: bool = True,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: modal
3
- Version: 0.73.94
3
+ Version: 0.73.96
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=ojhuLZuNZAQ1OsbDH0k6G4pm1W7bOIvZfXbaKlvQ-Ao,45622
22
22
  modal/app.pyi,sha256=tZFbcsu20SuvfB2puxCyuXLFNJ9bQulzag55rVpgZmc,26827
23
23
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
24
24
  modal/client.py,sha256=j9D3hNis1lfhnz9lVFGgJgowbH3PaGUzNKgHPWYG778,15372
25
- modal/client.pyi,sha256=v8voss-x8xmD6S5y316pRZRpaf6A2UrrxrJlQ6qmb50,7593
25
+ modal/client.pyi,sha256=Fy8pLhqiVFbGOaLU5x0qZVC9VAu8qXIPyTbGiB09KK4,7593
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=JhDbaZZHN52lqA_roY1BCbcN9BvbkUcdXiM2Kg9lIc0,31717
@@ -54,8 +54,8 @@ modal/network_file_system.pyi,sha256=4N3eqMbTSlqmS8VV_aJK-uvrgJC8xnf_YtW5FHfRfc8
54
54
  modal/object.py,sha256=bTeskuY8JFrESjU4_UL_nTwYlBQdOLmVaOX3X6EMxsg,164
55
55
  modal/object.pyi,sha256=kyJkRQcVv3ct7zSAxvvXcuhBVeH914v80uSlqeS7cA4,5632
56
56
  modal/output.py,sha256=q4T9uHduunj4NwY-YSwkHGgjZlCXMuJbfQ5UFaAGRAc,1968
57
- modal/parallel_map.py,sha256=POBTyiWabe2e4qBNlsjjksiu1AAPEsNqI-mM8cgNFco,16042
58
- modal/parallel_map.pyi,sha256=-YKY_bVuQv8B4gtFrHnXtuNV0_JpmU9vqMJzR7beeCU,2524
57
+ modal/parallel_map.py,sha256=Mctp_HdfyG1aMCw2ohcywLqGEKIbSPNtI3VyvXgSGJc,16321
58
+ modal/parallel_map.pyi,sha256=CWLbHNo6bJMOHSGfbLLcIDh-3XT3jtUOG_jzuFYLqcw,2573
59
59
  modal/partial_function.py,sha256=uu8zvIV0Big0jiTlC4-VPL16dOScNB5jhfPeqxvvCrI,1117
60
60
  modal/partial_function.pyi,sha256=-MAK61qJRi6Wjym-Measz5_9moJurYrJfdi7uSQZa5M,4936
61
61
  modal/proxy.py,sha256=NrOevrWxG3G7-zlyRzG6BcIvop7AWLeyahZxitbBaOk,1418
@@ -93,14 +93,14 @@ modal/_runtime/telemetry.py,sha256=T1RoAGyjBDr1swiM6pPsGRSITm7LI5FDK18oNXxY08U,5
93
93
  modal/_runtime/user_code_imports.py,sha256=OTxf5BIwMOaHFplIxxyEAOsR-xM2f5kSiSOa9Ne2le0,14814
94
94
  modal/_utils/__init__.py,sha256=waLjl5c6IPDhSsdWAm9Bji4e2PVxamYABKAze6CHVXY,28
95
95
  modal/_utils/app_utils.py,sha256=88BT4TPLWfYAQwKTHcyzNQRHg8n9B-QE2UyJs96iV-0,108
96
- modal/_utils/async_utils.py,sha256=5PdDuI1aSwPOI4a3dIvW0DkPqGw6KZN6RtWE18Dzv1E,25079
96
+ modal/_utils/async_utils.py,sha256=aes4Me0GA3yurNjsb6f8qeLD5bQw8NOmzKQFUqLaqSk,25208
97
97
  modal/_utils/blob_utils.py,sha256=RB1G6T7eC1Poe-O45qYLaxwCr2jkM-Q6Nexk1J3wk_w,14505
98
98
  modal/_utils/bytes_io_segment_payload.py,sha256=uunxVJS4PE1LojF_UpURMzVK9GuvmYWRqQo_bxEj5TU,3385
99
99
  modal/_utils/deprecation.py,sha256=EXP1beU4pmEqEzWMLw6E3kUfNfpmNA_VOp6i0EHi93g,4856
100
100
  modal/_utils/docker_utils.py,sha256=h1uETghR40mp_y3fSWuZAfbIASH1HMzuphJHghAL6DU,3722
101
101
  modal/_utils/function_utils.py,sha256=Rmz8GJDie-RW_q2RcTwholEWixS2IQDPBsRBJ3f3ZvU,27302
102
102
  modal/_utils/grpc_testing.py,sha256=H1zHqthv19eGPJz2HKXDyWXWGSqO4BRsxah3L5Xaa8A,8619
103
- modal/_utils/grpc_utils.py,sha256=qYzsePrTZcuZ66GEJw718X0KrRJwp_wqspJNP_lInqM,7972
103
+ modal/_utils/grpc_utils.py,sha256=rERGLUYRVv1tOLxOkHD-c8fc_gXUhHfHshkr7NQCH7Q,8356
104
104
  modal/_utils/hash_utils.py,sha256=zg3J6OGxTFGSFri1qQ12giDz90lWk8bzaxCTUCRtiX4,3034
105
105
  modal/_utils/http_utils.py,sha256=yeTFsXYr0rYMEhB7vBP7audG9Uc7OLhzKBANFDZWVt0,2451
106
106
  modal/_utils/logger.py,sha256=ePzdudrtx9jJCjuO6-bcL_kwUJfi4AwloUmIiNtqkY0,1330
@@ -168,10 +168,10 @@ modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0y
168
168
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
169
169
  modal_version/__init__.py,sha256=wiJQ53c-OMs0Xf1UeXOxQ7FwlV1VzIjnX6o-pRYZ_Pk,470
170
170
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
171
- modal_version/_version_generated.py,sha256=DEfu-7ga9Ulk647AlqzbbmsRNCxfG9L_3Vi5naDfyyU,149
172
- modal-0.73.94.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
173
- modal-0.73.94.dist-info/METADATA,sha256=apvv3TUlTPcKiB8EgLNbA3MajsPR3hSJ60UBg-WAy3I,2452
174
- modal-0.73.94.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
175
- modal-0.73.94.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
176
- modal-0.73.94.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
177
- modal-0.73.94.dist-info/RECORD,,
171
+ modal_version/_version_generated.py,sha256=JzFwSulqLTXbV5v6KCgLju2IGDqyKLdFJjZJ995cnds,149
172
+ modal-0.73.96.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
173
+ modal-0.73.96.dist-info/METADATA,sha256=yE8ybxzFN_prnqz6-K_DzV40G8on9awGSYCrSvqY6mA,2452
174
+ modal-0.73.96.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
175
+ modal-0.73.96.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
176
+ modal-0.73.96.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
177
+ modal-0.73.96.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 = 94 # git: 3f8ec72
4
+ build_number = 96 # git: 9b83eca