modal 0.67.9__py3-none-any.whl → 0.67.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.
@@ -193,7 +193,7 @@ def get_user_class_instance(cls: typing.Union[type, modal.cls.Cls], args: tuple,
193
193
  if isinstance(cls, modal.cls.Cls):
194
194
  # globally @app.cls-decorated class
195
195
  modal_obj: modal.cls.Obj = cls(*args, **kwargs)
196
- modal_obj.entered = True # ugly but prevents .local() from triggering additional enter-logic
196
+ modal_obj._entered = True # ugly but prevents .local() from triggering additional enter-logic
197
197
  # TODO: unify lifecycle logic between .local() and container_entrypoint
198
198
  user_cls_instance = modal_obj._cached_user_cls_instance()
199
199
  else:
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.9"
29
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.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.67.9"
84
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.14"
85
85
  ): ...
86
86
  def is_closed(self) -> bool: ...
87
87
  @property
modal/cls.py CHANGED
@@ -78,7 +78,7 @@ class _Obj:
78
78
  All this class does is to return `Function` objects."""
79
79
 
80
80
  _functions: dict[str, _Function]
81
- _entered: bool
81
+ _has_entered: bool
82
82
  _user_cls_instance: Optional[Any] = None
83
83
  _construction_args: tuple[tuple, dict[str, Any]]
84
84
 
@@ -121,8 +121,7 @@ class _Obj:
121
121
  self._method_functions[method_name] = method
122
122
 
123
123
  # Used for construction local object lazily
124
- self._entered = False
125
- self._local_user_cls_instance = None
124
+ self._has_entered = False
126
125
  self._user_cls = user_cls
127
126
  self._construction_args = (args, kwargs) # used for lazy construction in case of explicit constructors
128
127
 
@@ -176,8 +175,8 @@ class _Obj:
176
175
 
177
176
  return self._user_cls_instance
178
177
 
179
- def enter(self):
180
- if not self._entered:
178
+ def _enter(self):
179
+ if not self._has_entered:
181
180
  if hasattr(self._user_cls_instance, "__enter__"):
182
181
  self._user_cls_instance.__enter__()
183
182
 
@@ -188,26 +187,26 @@ class _Obj:
188
187
  for enter_method in _find_callables_for_obj(self._user_cls_instance, method_flag).values():
189
188
  enter_method()
190
189
 
191
- self._entered = True
190
+ self._has_entered = True
192
191
 
193
192
  @property
194
- def entered(self):
195
- # needed because aenter is nowrap
196
- return self._entered
193
+ def _entered(self) -> bool:
194
+ # needed because _aenter is nowrap
195
+ return self._has_entered
197
196
 
198
- @entered.setter
199
- def entered(self, val):
200
- self._entered = val
197
+ @_entered.setter
198
+ def _entered(self, val: bool):
199
+ self._has_entered = val
201
200
 
202
201
  @synchronizer.nowrap
203
- async def aenter(self):
204
- if not self.entered:
202
+ async def _aenter(self):
203
+ if not self._entered: # use the property to get at the impl class
205
204
  user_cls_instance = self._cached_user_cls_instance()
206
205
  if hasattr(user_cls_instance, "__aenter__"):
207
206
  await user_cls_instance.__aenter__()
208
207
  elif hasattr(user_cls_instance, "__enter__"):
209
208
  user_cls_instance.__enter__()
210
- self.entered = True
209
+ self._has_entered = True
211
210
 
212
211
  def __getattr__(self, k):
213
212
  if k in self._method_functions:
modal/cls.pyi CHANGED
@@ -22,7 +22,7 @@ def _get_class_constructor_signature(user_cls: type) -> inspect.Signature: ...
22
22
 
23
23
  class _Obj:
24
24
  _functions: dict[str, modal.functions._Function]
25
- _entered: bool
25
+ _has_entered: bool
26
26
  _user_cls_instance: typing.Optional[typing.Any]
27
27
  _construction_args: tuple[tuple, dict[str, typing.Any]]
28
28
  _instance_service_function: typing.Optional[modal.functions._Function]
@@ -41,17 +41,17 @@ class _Obj:
41
41
  def _new_user_cls_instance(self): ...
42
42
  async def keep_warm(self, warm_pool_size: int) -> None: ...
43
43
  def _cached_user_cls_instance(self): ...
44
- def enter(self): ...
44
+ def _enter(self): ...
45
45
  @property
46
- def entered(self): ...
47
- @entered.setter
48
- def entered(self, val): ...
49
- async def aenter(self): ...
46
+ def _entered(self) -> bool: ...
47
+ @_entered.setter
48
+ def _entered(self, val: bool): ...
49
+ async def _aenter(self): ...
50
50
  def __getattr__(self, k): ...
51
51
 
52
52
  class Obj:
53
53
  _functions: dict[str, modal.functions.Function]
54
- _entered: bool
54
+ _has_entered: bool
55
55
  _user_cls_instance: typing.Optional[typing.Any]
56
56
  _construction_args: tuple[tuple, dict[str, typing.Any]]
57
57
  _instance_service_function: typing.Optional[modal.functions.Function]
@@ -76,12 +76,12 @@ class Obj:
76
76
  keep_warm: __keep_warm_spec
77
77
 
78
78
  def _cached_user_cls_instance(self): ...
79
- def enter(self): ...
79
+ def _enter(self): ...
80
80
  @property
81
- def entered(self): ...
82
- @entered.setter
83
- def entered(self, val): ...
84
- async def aenter(self): ...
81
+ def _entered(self) -> bool: ...
82
+ @_entered.setter
83
+ def _entered(self, val: bool): ...
84
+ async def _aenter(self): ...
85
85
  def __getattr__(self, k): ...
86
86
 
87
87
  class _Cls(modal.object._Object):
modal/config.py CHANGED
@@ -221,6 +221,7 @@ _SETTINGS = {
221
221
  "image_builder_version": _Setting(),
222
222
  "strict_parameters": _Setting(False, transform=_to_boolean), # For internal/experimental use
223
223
  "snapshot_debug": _Setting(False, transform=_to_boolean),
224
+ "client_retries": _Setting(False, transform=_to_boolean), # For internal testing.
224
225
  }
225
226
 
226
227
 
@@ -128,12 +128,16 @@ class _ContainerProcess(Generic[T]):
128
128
  on_connect = asyncio.Event()
129
129
 
130
130
  async def _write_to_fd_loop(stream: _StreamReader):
131
- async for line in stream:
131
+ # Don't skip empty messages so we can detect when the process has booted.
132
+ async for chunk in stream._get_logs(skip_empty_messages=False):
133
+ if chunk is None:
134
+ break
135
+
132
136
  if not on_connect.is_set():
133
137
  connecting_status.stop()
134
138
  on_connect.set()
135
139
 
136
- await write_to_fd(stream.file_descriptor, line.encode("utf-8"))
140
+ await write_to_fd(stream.file_descriptor, chunk)
137
141
 
138
142
  async def _handle_input(data: bytes, message_index: int):
139
143
  self.stdin.write(data)
modal/functions.py CHANGED
@@ -1,4 +1,5 @@
1
1
  # Copyright Modal Labs 2023
2
+ import dataclasses
2
3
  import inspect
3
4
  import textwrap
4
5
  import time
@@ -19,6 +20,7 @@ import typing_extensions
19
20
  from google.protobuf.message import Message
20
21
  from grpclib import GRPCError, Status
21
22
  from synchronicity.combined_types import MethodWithAio
23
+ from synchronicity.exceptions import UserCodeException
22
24
 
23
25
  from modal._utils.async_utils import aclosing
24
26
  from modal_proto import api_pb2
@@ -57,6 +59,7 @@ from .cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
57
59
  from .config import config
58
60
  from .exception import (
59
61
  ExecutionError,
62
+ FunctionTimeoutError,
60
63
  InvalidError,
61
64
  NotFoundError,
62
65
  OutputExpiredError,
@@ -79,7 +82,7 @@ from .parallel_map import (
79
82
  _SynchronizedQueue,
80
83
  )
81
84
  from .proxy import _Proxy
82
- from .retries import Retries
85
+ from .retries import Retries, RetryManager
83
86
  from .schedule import Schedule
84
87
  from .scheduler_placement import SchedulerPlacement
85
88
  from .secret import _Secret
@@ -91,15 +94,32 @@ if TYPE_CHECKING:
91
94
  import modal.partial_function
92
95
 
93
96
 
97
+ @dataclasses.dataclass
98
+ class _RetryContext:
99
+ function_call_invocation_type: "api_pb2.FunctionCallInvocationType.ValueType"
100
+ retry_policy: api_pb2.FunctionRetryPolicy
101
+ function_call_jwt: str
102
+ input_jwt: str
103
+ input_id: str
104
+ item: api_pb2.FunctionPutInputsItem
105
+
106
+
94
107
  class _Invocation:
95
108
  """Internal client representation of a single-input call to a Modal Function or Generator"""
96
109
 
97
110
  stub: ModalClientModal
98
111
 
99
- def __init__(self, stub: ModalClientModal, function_call_id: str, client: _Client):
112
+ def __init__(
113
+ self,
114
+ stub: ModalClientModal,
115
+ function_call_id: str,
116
+ client: _Client,
117
+ retry_context: Optional[_RetryContext] = None,
118
+ ):
100
119
  self.stub = stub
101
120
  self.client = client # Used by the deserializer.
102
121
  self.function_call_id = function_call_id # TODO: remove and use only input_id
122
+ self._retry_context = retry_context
103
123
 
104
124
  @staticmethod
105
125
  async def create(
@@ -125,7 +145,17 @@ class _Invocation:
125
145
  function_call_id = response.function_call_id
126
146
 
127
147
  if response.pipelined_inputs:
128
- return _Invocation(client.stub, function_call_id, client)
148
+ assert len(response.pipelined_inputs) == 1
149
+ input = response.pipelined_inputs[0]
150
+ retry_context = _RetryContext(
151
+ function_call_invocation_type=function_call_invocation_type,
152
+ retry_policy=response.retry_policy,
153
+ function_call_jwt=response.function_call_jwt,
154
+ input_jwt=input.input_jwt,
155
+ input_id=input.input_id,
156
+ item=item,
157
+ )
158
+ return _Invocation(client.stub, function_call_id, client, retry_context)
129
159
 
130
160
  request_put = api_pb2.FunctionPutInputsRequest(
131
161
  function_id=function_id, inputs=[item], function_call_id=function_call_id
@@ -137,7 +167,16 @@ class _Invocation:
137
167
  processed_inputs = inputs_response.inputs
138
168
  if not processed_inputs:
139
169
  raise Exception("Could not create function call - the input queue seems to be full")
140
- return _Invocation(client.stub, function_call_id, client)
170
+ input = inputs_response.inputs[0]
171
+ retry_context = _RetryContext(
172
+ function_call_invocation_type=function_call_invocation_type,
173
+ retry_policy=response.retry_policy,
174
+ function_call_jwt=response.function_call_jwt,
175
+ input_jwt=input.input_jwt,
176
+ input_id=input.input_id,
177
+ item=item,
178
+ )
179
+ return _Invocation(client.stub, function_call_id, client, retry_context)
141
180
 
142
181
  async def pop_function_call_outputs(
143
182
  self, timeout: Optional[float], clear_on_success: bool
@@ -173,13 +212,46 @@ class _Invocation:
173
212
  # return the last response to check for state of num_unfinished_inputs
174
213
  return response
175
214
 
176
- async def run_function(self) -> Any:
215
+ async def _retry_input(self) -> None:
216
+ ctx = self._retry_context
217
+ if not ctx:
218
+ raise ValueError("Cannot retry input when _retry_context is empty.")
219
+
220
+ item = api_pb2.FunctionRetryInputsItem(input_jwt=ctx.input_jwt, input=ctx.item.input)
221
+ request = api_pb2.FunctionRetryInputsRequest(function_call_jwt=ctx.function_call_jwt, inputs=[item])
222
+ await retry_transient_errors(
223
+ self.client.stub.FunctionRetryInputs,
224
+ request,
225
+ )
226
+
227
+ async def _get_single_output(self) -> Any:
177
228
  # waits indefinitely for a single result for the function, and clear the outputs buffer after
178
229
  item: api_pb2.FunctionGetOutputsItem = (
179
230
  await self.pop_function_call_outputs(timeout=None, clear_on_success=True)
180
231
  ).outputs[0]
181
232
  return await _process_result(item.result, item.data_format, self.stub, self.client)
182
233
 
234
+ async def run_function(self) -> Any:
235
+ # Use retry logic only if retry policy is specified and
236
+ ctx = self._retry_context
237
+ if (
238
+ not ctx
239
+ or not ctx.retry_policy
240
+ or ctx.retry_policy.retries == 0
241
+ or ctx.function_call_invocation_type != api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC
242
+ ):
243
+ return await self._get_single_output()
244
+
245
+ # User errors including timeouts are managed by the user specified retry policy.
246
+ user_retry_manager = RetryManager(ctx.retry_policy)
247
+
248
+ while True:
249
+ try:
250
+ return await self._get_single_output()
251
+ except (UserCodeException, FunctionTimeoutError) as exc:
252
+ await user_retry_manager.raise_or_sleep(exc)
253
+ await self._retry_input()
254
+
183
255
  async def poll_function(self, timeout: Optional[float] = None):
184
256
  """Waits up to timeout for a result from a function.
185
257
 
@@ -1225,13 +1297,18 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1225
1297
  yield item
1226
1298
 
1227
1299
  async def _call_function(self, args, kwargs) -> ReturnType:
1300
+ if config.get("client_retries"):
1301
+ function_call_invocation_type = api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC
1302
+ else:
1303
+ function_call_invocation_type = api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC_LEGACY
1228
1304
  invocation = await _Invocation.create(
1229
1305
  self,
1230
1306
  args,
1231
1307
  kwargs,
1232
1308
  client=self._client,
1233
- function_call_invocation_type=api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC_LEGACY,
1309
+ function_call_invocation_type=function_call_invocation_type,
1234
1310
  )
1311
+
1235
1312
  return await invocation.run_function()
1236
1313
 
1237
1314
  async def _call_function_nowait(
@@ -1348,12 +1425,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1348
1425
  if is_async(info.raw_f):
1349
1426
  # We want to run __aenter__ and fun in the same coroutine
1350
1427
  async def coro():
1351
- await obj.aenter()
1428
+ await obj._aenter()
1352
1429
  return await fun(*args, **kwargs)
1353
1430
 
1354
1431
  return coro() # type: ignore
1355
1432
  else:
1356
- obj.enter()
1433
+ obj._enter()
1357
1434
  return fun(*args, **kwargs)
1358
1435
 
1359
1436
  @synchronizer.no_input_translation
modal/functions.pyi CHANGED
@@ -26,11 +26,35 @@ import pathlib
26
26
  import typing
27
27
  import typing_extensions
28
28
 
29
+ class _RetryContext:
30
+ function_call_invocation_type: int
31
+ retry_policy: modal_proto.api_pb2.FunctionRetryPolicy
32
+ function_call_jwt: str
33
+ input_jwt: str
34
+ input_id: str
35
+ item: modal_proto.api_pb2.FunctionPutInputsItem
36
+
37
+ def __init__(
38
+ self,
39
+ function_call_invocation_type: int,
40
+ retry_policy: modal_proto.api_pb2.FunctionRetryPolicy,
41
+ function_call_jwt: str,
42
+ input_jwt: str,
43
+ input_id: str,
44
+ item: modal_proto.api_pb2.FunctionPutInputsItem,
45
+ ) -> None: ...
46
+ def __repr__(self): ...
47
+ def __eq__(self, other): ...
48
+
29
49
  class _Invocation:
30
50
  stub: modal_proto.modal_api_grpc.ModalClientModal
31
51
 
32
52
  def __init__(
33
- self, stub: modal_proto.modal_api_grpc.ModalClientModal, function_call_id: str, client: modal.client._Client
53
+ self,
54
+ stub: modal_proto.modal_api_grpc.ModalClientModal,
55
+ function_call_id: str,
56
+ client: modal.client._Client,
57
+ retry_context: typing.Optional[_RetryContext] = None,
34
58
  ): ...
35
59
  @staticmethod
36
60
  async def create(
@@ -39,6 +63,8 @@ class _Invocation:
39
63
  async def pop_function_call_outputs(
40
64
  self, timeout: typing.Optional[float], clear_on_success: bool
41
65
  ) -> modal_proto.api_pb2.FunctionGetOutputsResponse: ...
66
+ async def _retry_input(self) -> None: ...
67
+ async def _get_single_output(self) -> typing.Any: ...
42
68
  async def run_function(self) -> typing.Any: ...
43
69
  async def poll_function(self, timeout: typing.Optional[float] = None): ...
44
70
  def run_generator(self): ...
modal/io_streams.py CHANGED
@@ -224,7 +224,7 @@ class _StreamReader(Generic[T]):
224
224
 
225
225
  entry_id += 1
226
226
 
227
- async def _get_logs(self) -> AsyncGenerator[Optional[bytes], None]:
227
+ async def _get_logs(self, skip_empty_messages: bool = True) -> AsyncGenerator[Optional[bytes], None]:
228
228
  """Streams sandbox or process logs from the server to the reader.
229
229
 
230
230
  Logs returned by this method may contain partial or multiple lines at a time.
@@ -253,6 +253,11 @@ class _StreamReader(Generic[T]):
253
253
 
254
254
  async for message, entry_id in iterator:
255
255
  self._last_entry_id = entry_id
256
+ # Empty messages are sent when the process boots up. Don't yield them unless
257
+ # we're using the empty message to signal process liveness.
258
+ if skip_empty_messages and message == b"":
259
+ continue
260
+
256
261
  yield message
257
262
  if message is None:
258
263
  completed = True
modal/io_streams.pyi CHANGED
@@ -31,7 +31,9 @@ class _StreamReader(typing.Generic[T]):
31
31
  async def read(self) -> T: ...
32
32
  async def _consume_container_process_stream(self): ...
33
33
  def _stream_container_process(self) -> collections.abc.AsyncGenerator[tuple[typing.Optional[bytes], str], None]: ...
34
- def _get_logs(self) -> collections.abc.AsyncGenerator[typing.Optional[bytes], None]: ...
34
+ def _get_logs(
35
+ self, skip_empty_messages: bool = True
36
+ ) -> collections.abc.AsyncGenerator[typing.Optional[bytes], None]: ...
35
37
  def _get_logs_by_line(self) -> collections.abc.AsyncGenerator[typing.Optional[bytes], None]: ...
36
38
  def __aiter__(self) -> collections.abc.AsyncIterator[T]: ...
37
39
  async def __anext__(self) -> T: ...
@@ -82,8 +84,12 @@ class StreamReader(typing.Generic[T]):
82
84
  _stream_container_process: ___stream_container_process_spec
83
85
 
84
86
  class ___get_logs_spec(typing_extensions.Protocol):
85
- def __call__(self) -> typing.Generator[typing.Optional[bytes], None, None]: ...
86
- def aio(self) -> collections.abc.AsyncGenerator[typing.Optional[bytes], None]: ...
87
+ def __call__(
88
+ self, skip_empty_messages: bool = True
89
+ ) -> typing.Generator[typing.Optional[bytes], None, None]: ...
90
+ def aio(
91
+ self, skip_empty_messages: bool = True
92
+ ) -> collections.abc.AsyncGenerator[typing.Optional[bytes], None]: ...
87
93
 
88
94
  _get_logs: ___get_logs_spec
89
95
 
modal/partial_function.py CHANGED
@@ -155,7 +155,7 @@ def _find_partial_methods_for_user_cls(user_cls: type[Any], flags: int) -> dict[
155
155
  deprecation_error((2024, 2, 21), message)
156
156
 
157
157
  partial_functions: dict[str, PartialFunction] = {}
158
- for parent_cls in user_cls.mro():
158
+ for parent_cls in reversed(user_cls.mro()):
159
159
  if parent_cls is not object:
160
160
  for k, v in parent_cls.__dict__.items():
161
161
  if isinstance(v, PartialFunction):
modal/retries.py CHANGED
@@ -1,10 +1,14 @@
1
1
  # Copyright Modal Labs 2022
2
+ import asyncio
2
3
  from datetime import timedelta
3
4
 
4
5
  from modal_proto import api_pb2
5
6
 
6
7
  from .exception import InvalidError
7
8
 
9
+ MIN_INPUT_RETRY_DELAY_MS = 1000
10
+ MAX_INPUT_RETRY_DELAY_MS = 24 * 60 * 60 * 1000
11
+
8
12
 
9
13
  class Retries:
10
14
  """Adds a retry policy to a Modal function.
@@ -103,3 +107,37 @@ class Retries:
103
107
  initial_delay_ms=self.initial_delay // timedelta(milliseconds=1),
104
108
  max_delay_ms=self.max_delay // timedelta(milliseconds=1),
105
109
  )
110
+
111
+
112
+ class RetryManager:
113
+ """
114
+ Helper class to apply the specified retry policy.
115
+ """
116
+
117
+ def __init__(self, retry_policy: api_pb2.FunctionRetryPolicy):
118
+ self.retry_policy = retry_policy
119
+ self.attempt_count = 0
120
+
121
+ async def raise_or_sleep(self, exc: Exception):
122
+ """
123
+ Raises an exception if the maximum retry count has been reached, otherwise sleeps for calculated delay.
124
+ """
125
+ self.attempt_count += 1
126
+ if self.attempt_count > self.retry_policy.retries:
127
+ raise exc
128
+ delay_ms = self._retry_delay_ms(self.attempt_count, self.retry_policy)
129
+ await asyncio.sleep(delay_ms / 1000)
130
+
131
+ @staticmethod
132
+ def _retry_delay_ms(attempt_count: int, retry_policy: api_pb2.FunctionRetryPolicy) -> float:
133
+ """
134
+ Computes the amount of time to sleep before retrying based on the backend_coefficient and initial_delay_ms args.
135
+ """
136
+ if attempt_count < 1:
137
+ raise ValueError(f"Cannot compute retry delay. attempt_count must be at least 1, but was {attempt_count}")
138
+ delay_ms = retry_policy.initial_delay_ms * (retry_policy.backoff_coefficient ** (attempt_count - 1))
139
+ if delay_ms < MIN_INPUT_RETRY_DELAY_MS:
140
+ return MIN_INPUT_RETRY_DELAY_MS
141
+ if delay_ms > MAX_INPUT_RETRY_DELAY_MS:
142
+ return MAX_INPUT_RETRY_DELAY_MS
143
+ return delay_ms
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.67.9
3
+ Version: 0.67.14
4
4
  Summary: Python client library for Modal
5
5
  Author: Modal Labs
6
6
  Author-email: support@modal.com
@@ -19,13 +19,13 @@ 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=r--ghy9vX3h0MDTB7LKkpOu-FpjBuKUm1bPcMUJc-OU,7352
22
+ modal/client.pyi,sha256=GfMKvzgJgycbuVP2WPB33Dk5M7kg0kIdsvTiwmLS1mI,7354
23
23
  modal/cloud_bucket_mount.py,sha256=G7T7jWLD0QkmrfKR75mSTwdUZ2xNfj7pkVqb4ipmxmI,5735
24
24
  modal/cloud_bucket_mount.pyi,sha256=CEi7vrH3kDUF4LAy4qP6tfImy2UJuFRcRbsgRNM1wo8,1403
25
- modal/cls.py,sha256=fCtqbwu5j7LOnuPYKWPFzL7DTvls3CBvLjV0PCPpIfo,24708
26
- modal/cls.pyi,sha256=2NzoOL9cdxTMAy0Tay8KqFaCoOa8COxAcNB79mBOp20,8101
27
- modal/config.py,sha256=86vrmCPiH7PhrbC7-EDuU9yuUnqTpDGy1OEvGTdt7Fs,10923
28
- modal/container_process.py,sha256=c_jBPtyPeSxbIcbLfs_FzTrt-1eErtRSnsfxkDozFoY,5589
25
+ modal/cls.py,sha256=F2jk5zFCAA8h-GfM0dbdBG3Mu5wiG9k9Z9JLYRYuT2Q,24758
26
+ modal/cls.pyi,sha256=2_nbvSlkh2d0tfibTIxsThPiL0Xcrcosc5f_ET-i0sk,8147
27
+ modal/config.py,sha256=1KhNJkjYsJkX1V8RPPdRYPlM2HE-ZZs0JVSxbiXjmrw,11010
28
+ modal/container_process.py,sha256=YRCKjn56oqTtGjtLxpl_KSkOhYrcRitgF3LOI6o14Q4,5759
29
29
  modal/container_process.pyi,sha256=k2kClwaSzz11eci1pzFZgCm-ptXapHAyHTOENorlazA,2594
30
30
  modal/dict.py,sha256=RmJlEwFJOdSfAYcVa50hbbFccV8e7BvC5tc5g1HXF-c,12622
31
31
  modal/dict.pyi,sha256=2cYgOqBxYZih4BYgMV0c3rNPuxYR6-cB1GBXzFkHA5c,7265
@@ -33,13 +33,13 @@ modal/environments.py,sha256=5cgA-zbm6ngKLsRA19zSOgtgo9-BarJK3FJK0BiF2Lo,6505
33
33
  modal/environments.pyi,sha256=XalNpiPkAtHWAvOU2Cotq0ozmtl-Jv0FDsR8h9mr27Q,3521
34
34
  modal/exception.py,sha256=EBkdWVved2XEPsXaoPRu56xfxFFHL9iuqvUsdj42WDA,6392
35
35
  modal/experimental.py,sha256=jFuNbwrNHos47viMB9q-cHJSvf2RDxDdoEcss9plaZE,2302
36
- modal/functions.py,sha256=KOhlt716_HnBZw9hwtIzC5i1mBfjeuw9zy0zcWJtw-w,66913
37
- modal/functions.pyi,sha256=yAb8-EE-Y7bbZXC0P5lEvCvmNVgxZCYlZxdagGdC3_M,24325
36
+ modal/functions.py,sha256=Pwebl3aeEkNrniXCzuDdjfxgExykTWQo7o0VjFD4To8,69853
37
+ 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=gZPXD7unxRrxC4aroejKM1-pd4STg09JqLd0NRegAMs,14635
42
- modal/io_streams.pyi,sha256=QNvbZpm1qW6LkqCIcJ4Zprn_IW-ZVqt62KrpDP--_Co,4555
41
+ modal/io_streams.py,sha256=4pF2HumRK1pVnrx6S9UwGqJn69rQqyQqpe5X_nifny0,14943
42
+ modal/io_streams.pyi,sha256=An766S3JKP78b2A4RphjdVNR73yblDc5uG_xp5--6k4,4715
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
@@ -49,14 +49,14 @@ modal/object.pyi,sha256=MO78H9yFSE5i1gExPEwyyQzLdlshkcGHN1aQ0ylyvq0,8802
49
49
  modal/output.py,sha256=N0xf4qeudEaYrslzdAl35VKV8rapstgIM2e9wO8_iy0,1967
50
50
  modal/parallel_map.py,sha256=4aoMXIrlG3wl5Ifk2YDNOQkXsGRsm6Xbfm6WtJ2t3WY,16002
51
51
  modal/parallel_map.pyi,sha256=pOhT0P3DDYlwLx0fR3PTsecA7DI8uOdXC1N8i-ZkyOY,2328
52
- modal/partial_function.py,sha256=onHBLCbQLJJu1h9--L7hw_gmvEdbLm-pNSa4xxk7ydA,28205
52
+ modal/partial_function.py,sha256=938kcVJHcdGXKWsO7NE_FBxPldZ304a_GyhjxD79wHE,28215
53
53
  modal/partial_function.pyi,sha256=EafGOzZdEq-yE5bYRoMfnMqw-o8Hk_So8MRPDSB99_0,8982
54
54
  modal/proxy.py,sha256=ZrOsuQP7dSZFq1OrIxalNnt0Zvsnp1h86Th679sSL40,1417
55
55
  modal/proxy.pyi,sha256=UvygdOYneLTuoDY6hVaMNCyZ947Tmx93IdLjErUqkvM,368
56
56
  modal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
57
  modal/queue.py,sha256=q0SDkrN2e2CNxsHjhs6xMwf03LZdvuUFYL7SZPZpKps,18390
58
58
  modal/queue.pyi,sha256=di3ownBw4jc6d4X7ygXtbpjlUMOK69qyaD3lVsJbpoM,9900
59
- modal/retries.py,sha256=z4dYXdksUcjkefM3vGLkhCQ_m_TUPLJgC4uSYDzWSOU,3750
59
+ modal/retries.py,sha256=HKR2Q9aNPWkMjQ5nwobqYTuZaSuw0a8lI2zrtY5IW98,5230
60
60
  modal/runner.py,sha256=7obU-Gq1ocpBGCuR6pvn1T-D6ggg1T48qFo2TNUGWkU,24089
61
61
  modal/runner.pyi,sha256=RAtCvx_lXWjyFjIaZ3t9-X1c7rqpgAQlhl4Hww53OY8,5038
62
62
  modal/running_app.py,sha256=CshNvGDJtagOdKW54uYjY8HY73j2TpnsL9jkPFZAsfA,560
@@ -78,7 +78,7 @@ modal/_runtime/asgi.py,sha256=GvuxZqWnIHMIR-Bx5f7toCQlkERaJO8CHjTPNM9IFIw,21537
78
78
  modal/_runtime/container_io_manager.py,sha256=yVKSBBybfciDaady7NxOPVU5cm9qL690OgdZ4dZN1bs,44134
79
79
  modal/_runtime/execution_context.py,sha256=E6ofm6j1POXGPxS841X3V7JU6NheVb8OkQc7JpLq4Kg,2712
80
80
  modal/_runtime/telemetry.py,sha256=T1RoAGyjBDr1swiM6pPsGRSITm7LI5FDK18oNXxY08U,5163
81
- modal/_runtime/user_code_imports.py,sha256=_sTWN4w1WjrO9b9oZIb5QNY_giTx3G5L_ly2UdZcy28,14520
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
84
  modal/_utils/async_utils.py,sha256=CYXogDVqqUtSe-DVP2A3F-6KztjPZaW6ez2lrYBCW_Y,24917
@@ -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=p9KVwv9r1G7UnZfBzzZEBOK-_rV1N6IZLGBkzFzsXeA,148
163
- modal-0.67.9.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
164
- modal-0.67.9.dist-info/METADATA,sha256=9qjMkC-PPnHwVJEs-r6_GlW4f4-4XcRdq36REUp6ln8,2328
165
- modal-0.67.9.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
166
- modal-0.67.9.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
167
- modal-0.67.9.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
168
- modal-0.67.9.dist-info/RECORD,,
162
+ modal_version/_version_generated.py,sha256=C_bhmRQ5vhAyOuxu_gJXQnJ-QqoRX-Hrts9qMSeIr38,149
163
+ modal-0.67.14.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
164
+ modal-0.67.14.dist-info/METADATA,sha256=aumIF34KL_3jyBsm0akwFP5lgbjpt1thUxa9oahCh2Y,2329
165
+ modal-0.67.14.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
166
+ modal-0.67.14.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
167
+ modal-0.67.14.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
168
+ modal-0.67.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 = 9 # git: 89698e5
4
+ build_number = 14 # git: 48a82c7