modAL 1.2.7.dev12__py3-none-any.whl → 1.2.7.dev13__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/_functions.py CHANGED
@@ -38,6 +38,7 @@ from ._utils.async_utils import (
38
38
  aclosing,
39
39
  async_merge,
40
40
  callable_to_agen,
41
+ deprecate_aio_usage,
41
42
  synchronizer,
42
43
  warn_if_generator_is_not_consumed,
43
44
  )
@@ -96,6 +97,7 @@ from .volume import _Volume
96
97
  if TYPE_CHECKING:
97
98
  import modal.app
98
99
  import modal.cls
100
+ import modal.functions
99
101
 
100
102
  MAX_INTERNAL_FAILURE_COUNT = 8
101
103
  TERMINAL_STATUSES = (
@@ -2025,8 +2027,11 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
2025
2027
  assert self._client and self._client.stub
2026
2028
  await self._client.stub.FunctionCallCancel(request)
2027
2029
 
2028
- @staticmethod
2029
- async def from_id(function_call_id: str, client: Optional[_Client] = None) -> "_FunctionCall[Any]":
2030
+ @deprecate_aio_usage((2025, 11, 14), "FunctionCall.from_id")
2031
+ @classmethod
2032
+ def from_id(
2033
+ cls, function_call_id: str, client: Optional["modal.client.Client"] = None
2034
+ ) -> "modal.functions.FunctionCall[Any]":
2030
2035
  """Instantiate a FunctionCall object from an existing ID.
2031
2036
 
2032
2037
  Examples:
@@ -2037,7 +2042,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
2037
2042
  fc_id = fc.object_id
2038
2043
 
2039
2044
  # Later, use the ID to re-instantiate the FunctionCall object
2040
- fc = _FunctionCall.from_id(fc_id)
2045
+ fc = FunctionCall.from_id(fc_id)
2041
2046
  result = fc.get()
2042
2047
  ```
2043
2048
 
@@ -2045,6 +2050,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
2045
2050
  if you no longer have access to the original object returned from `Function.spawn`.
2046
2051
 
2047
2052
  """
2053
+ _client = typing.cast(_Client, synchronizer._translate_in(client))
2048
2054
 
2049
2055
  async def _load(
2050
2056
  self: _FunctionCall, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
@@ -2053,10 +2059,10 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
2053
2059
  self._hydrate(function_call_id, load_context.client, None)
2054
2060
 
2055
2061
  rep = f"FunctionCall.from_id({function_call_id!r})"
2056
-
2057
- return _FunctionCall._from_loader(
2058
- _load, rep, hydrate_lazily=True, load_context_overrides=LoadContext(client=client)
2062
+ impl_instance = _FunctionCall._from_loader(
2063
+ _load, rep, hydrate_lazily=True, load_context_overrides=LoadContext(client=_client)
2059
2064
  )
2065
+ return typing.cast("modal.functions.FunctionCall[Any]", synchronizer._translate_out(impl_instance))
2060
2066
 
2061
2067
  @staticmethod
2062
2068
  async def gather(*function_calls: "_FunctionCall[T]") -> typing.Sequence[T]:
modal/_ipython.py CHANGED
@@ -2,10 +2,20 @@
2
2
  import sys
3
3
 
4
4
 
5
- def is_notebook(stdout=None):
6
- ipykernel_iostream = sys.modules.get("ipykernel.iostream")
7
- if ipykernel_iostream is None:
5
+ def is_interactive_ipython():
6
+ """
7
+ Detect if we're running in an interactive IPython session.
8
+
9
+ Returns True for IPython shells (including Jupyter notebooks), False otherwise.
10
+ """
11
+ try:
12
+ # Check if IPython is available and get the current instance
13
+ ipython = sys.modules.get("IPython")
14
+ if ipython is None:
15
+ return False
16
+
17
+ # Try to get the active IPython instance
18
+ shell = ipython.get_ipython()
19
+ return shell is not None
20
+ except Exception:
8
21
  return False
9
- if stdout is None:
10
- stdout = sys.stdout
11
- return isinstance(stdout, ipykernel_iostream.OutStream)
modal/_object.py CHANGED
@@ -1,4 +1,5 @@
1
1
  # Copyright Modal Labs 2022
2
+ import contextlib
2
3
  import typing
3
4
  import uuid
4
5
  from collections.abc import Awaitable, Hashable, Sequence
@@ -350,3 +351,18 @@ def live_method_gen(method):
350
351
  yield item
351
352
 
352
353
  return wrapped
354
+
355
+
356
+ def live_method_contextmanager(method):
357
+ # make sure a wrapped function returning an async context manager
358
+ # will not require both an `await func.aio()` and `async with`
359
+ # which would have been the case if it was wrapped in live_method
360
+
361
+ @wraps(method)
362
+ @contextlib.asynccontextmanager
363
+ async def wrapped(self, *args, **kwargs):
364
+ await self.hydrate()
365
+ async with method(self, *args, **kwargs) as ctx:
366
+ yield ctx
367
+
368
+ return wrapped
@@ -5,8 +5,10 @@ import contextlib
5
5
  import functools
6
6
  import inspect
7
7
  import itertools
8
+ import os
8
9
  import sys
9
10
  import time
11
+ import types
10
12
  import typing
11
13
  import warnings
12
14
  from collections.abc import AsyncGenerator, AsyncIterable, Awaitable, Iterable, Iterator
@@ -24,10 +26,14 @@ from typing import (
24
26
 
25
27
  import synchronicity
26
28
  from synchronicity.async_utils import Runner
29
+ from synchronicity.combined_types import MethodWithAio
27
30
  from synchronicity.exceptions import NestedEventLoops
28
31
  from typing_extensions import ParamSpec, assert_type
29
32
 
30
- from ..exception import InvalidError
33
+ from modal._ipython import is_interactive_ipython
34
+ from modal._utils.deprecation import deprecation_warning
35
+
36
+ from ..exception import AsyncUsageWarning, InvalidError
31
37
  from .logger import logger
32
38
 
33
39
  T = TypeVar("T")
@@ -38,7 +44,285 @@ if sys.platform == "win32":
38
44
  # quick workaround for deadlocks on shutdown - need to investigate further
39
45
  asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
40
46
 
41
- synchronizer = synchronicity.Synchronizer()
47
+
48
+ def rewrite_sync_to_async(code_line: str, original_func: Callable) -> tuple[bool, str]:
49
+ """
50
+ Rewrite a blocking call to use async/await syntax.
51
+
52
+ Handles four patterns:
53
+ 1. __aiter__: for x in obj -> async for x in obj
54
+ 2. __aenter__: with obj as x -> async with obj as x
55
+ 3. Async generators in for loops: for x in obj.method(...) -> async for x in obj.method(...)
56
+ 4. Regular methods: obj.method() -> await obj.method.aio()
57
+
58
+ Args:
59
+ code_line: The line of code containing the blocking call
60
+ original_func: The original function object being called
61
+
62
+ Returns:
63
+ A tuple of (success, rewritten_code):
64
+ - success: True if the pattern was found and rewritten, False if falling back to generic
65
+ - rewritten_code: The rewritten code or a generic suggestion
66
+ """
67
+ import re
68
+
69
+ func_name = original_func.__name__ # type: ignore
70
+
71
+ # Check if this is an async generator function
72
+ is_async_gen = inspect.isasyncgenfunction(original_func)
73
+
74
+ # Handle __aiter__ pattern: for x in obj -> async for x in obj
75
+ if func_name == "__aiter__" and code_line.startswith("for "):
76
+ suggestion = code_line.replace("for ", "async for ", 1)
77
+ return (True, suggestion)
78
+
79
+ # Handle __aenter__ pattern: with obj as x -> async with obj as x
80
+ if func_name == "__aenter__" and code_line.startswith("with "):
81
+ suggestion = code_line.replace("with ", "async with ", 1)
82
+ return (True, suggestion)
83
+
84
+ # Handle __setitem__ pattern: dct['key'] = value -> suggest alternative
85
+ if func_name == "__setitem__":
86
+ # Try to extract the object and key from the bracket syntax
87
+ setitem_match = re.match(r"(\w+)\[([^\]]+)\]\s*=\s*(.+)", code_line.strip())
88
+ if setitem_match:
89
+ obj, key, value = setitem_match.groups()
90
+ suggestion = (
91
+ f"You can't use `{obj}[{key}] = {value}` syntax asynchronously - "
92
+ f"there may be an alternative api, e.g. {obj}.put.aio({key}, {value})"
93
+ )
94
+ return (False, suggestion)
95
+ return (False, f"await ...{func_name}.aio(...)")
96
+
97
+ # Handle __getitem__ pattern: dct['key'] -> suggest alternative
98
+ if func_name == "__getitem__":
99
+ # Try to extract the object and key from the bracket syntax
100
+ getitem_match = re.match(r"(\w+)\[([^\]]+)\]$", code_line.strip())
101
+ if getitem_match:
102
+ obj, key = getitem_match.groups()
103
+ suggestion = (
104
+ f"You can't use `{obj}[{key}]` syntax asynchronously - "
105
+ f"there may be an alternative api, e.g. {obj}.get.aio({key})"
106
+ )
107
+ return (False, suggestion)
108
+ return (False, f"await ...{func_name}.aio(...)")
109
+
110
+ # Handle async generator methods in for loops: for x in obj.method(...) -> async for x in obj.method(...)
111
+ if is_async_gen and code_line.strip().startswith("for "):
112
+ # Pattern: for <var> in <expr>.<method>(<args>):
113
+ for_pattern = rf"(for\s+\w+\s+in\s+.*\.){re.escape(func_name)}(\s*\()"
114
+ for_match = re.search(for_pattern, code_line)
115
+
116
+ if for_match:
117
+ # Just replace "for" with "async for" - no .aio() needed for async generators
118
+ suggestion = code_line.replace("for ", "async for ", 1)
119
+ return (True, suggestion)
120
+
121
+ # Handle regular method calls and property access
122
+ # First check if it's a property access (no parentheses after the name)
123
+ property_pattern = rf"\.{re.escape(func_name)}(?!\s*\()"
124
+ property_match = re.search(property_pattern, code_line)
125
+
126
+ if property_match:
127
+ # This is a property access, rewrite to use await without .aio()
128
+ # Find the start of the expression (skip statement keywords and assignments)
129
+ statement_start = 0
130
+ prefix_match = re.match(r"^(\s*(?:\w+\s*=|return|yield|raise)\s+)", code_line)
131
+ if prefix_match:
132
+ statement_start = len(prefix_match.group(1))
133
+
134
+ before_expr = code_line[:statement_start]
135
+ after_prefix = code_line[statement_start:]
136
+
137
+ # Just add await before the expression for properties
138
+ suggestion = before_expr + "await " + after_prefix.lstrip()
139
+ return (True, suggestion)
140
+
141
+ # Try to find a method call (with parentheses)
142
+ method_pattern = rf"\.{re.escape(func_name)}\s*\("
143
+ method_match = re.search(method_pattern, code_line)
144
+
145
+ if not method_match:
146
+ # Can't find the function call or property
147
+ return (False, f"await ...{func_name}.aio(...)")
148
+
149
+ # Safety check: don't attempt rewrite for complex expressions
150
+ unsafe_keywords = ["if", "elif", "while", "and", "or", "not", "in", "is", "for"]
151
+
152
+ # Check if line contains control flow keywords (might be too complex)
153
+ for keyword in unsafe_keywords:
154
+ if re.search(rf"\b{keyword}\b", code_line):
155
+ # Fall back to generic suggestion for complex expressions
156
+ return (False, f"await ...{func_name}.aio(...)")
157
+
158
+ # Find the start of the object expression that leads to the method call
159
+ # We need to find where the object/chain starts, e.g., in "2 * foo.bar.method()" we want "foo"
160
+ # Work backwards from the method match to find the start of the identifier chain
161
+ method_start = method_match.start()
162
+
163
+ # Find the start of the identifier chain (the object being called)
164
+ # Walk backwards to find identifiers and dots that form the chain
165
+ expr_start = method_start
166
+ i = method_start - 1
167
+ while i >= 0:
168
+ c = code_line[i]
169
+ if c.isalnum() or c == "_" or c == ".":
170
+ expr_start = i
171
+ i -= 1
172
+ elif c.isspace():
173
+ # Skip whitespace within the chain (though unusual)
174
+ i -= 1
175
+ else:
176
+ # Found a non-identifier character, stop
177
+ break
178
+
179
+ # Now expr_start points to the start of the object chain (e.g., "foo" in "foo.method()")
180
+ # But we need to check if the identifier we found is actually a keyword like return/yield/raise
181
+ # In that case, skip over it and find the actual object
182
+ before_obj = code_line[:expr_start]
183
+ obj_and_rest = code_line[expr_start:]
184
+
185
+ # Check if what we found starts with a statement keyword
186
+ keyword_match = re.match(r"^(return|yield|raise)\s+", obj_and_rest)
187
+ if keyword_match:
188
+ # The "object" we found is actually a keyword, adjust to skip it
189
+ keyword_len = len(keyword_match.group(0))
190
+ before_obj = code_line[: expr_start + keyword_len]
191
+ obj_and_rest = code_line[expr_start + keyword_len :]
192
+
193
+ # Add .aio() after the method name and await before the object
194
+ rewritten_expr = re.sub(rf"(\.{re.escape(func_name)})\s*\(", r"\1.aio(", obj_and_rest, count=1)
195
+ suggestion = before_obj + "await " + rewritten_expr
196
+
197
+ return (True, suggestion)
198
+
199
+
200
+ @dataclass
201
+ class _CallFrame:
202
+ """Simple dataclass to hold call frame information."""
203
+
204
+ filename: str
205
+ lineno: int
206
+ line: Optional[str]
207
+
208
+
209
+ def _extract_user_call_frame():
210
+ """
211
+ Extract the call frame from user code by filtering out frames from synchronicity and asyncio.
212
+
213
+ Returns a _CallFrame with the filename, line number, and source line, or None if not found.
214
+ """
215
+ import linecache
216
+ import os
217
+
218
+ # Get the current call stack
219
+ stack = inspect.stack()
220
+
221
+ # Get the absolute path of this module to filter it out
222
+ this_file = os.path.abspath(__file__)
223
+
224
+ # Filter out frames from synchronicity, asyncio, and this module
225
+ for frame_info in stack:
226
+ filename = frame_info.filename
227
+ # Skip frames from synchronicity, asyncio packages, and this module
228
+ # Use path separators to ensure we're matching packages, not just filenames containing these words
229
+ if (
230
+ os.path.sep + "synchronicity" + os.path.sep in filename
231
+ or os.path.sep + "asyncio" + os.path.sep in filename
232
+ or os.path.abspath(filename) == this_file
233
+ ):
234
+ continue
235
+
236
+ # Found a user frame
237
+ line = linecache.getline(filename, frame_info.lineno)
238
+ return _CallFrame(filename=filename, lineno=frame_info.lineno, line=line if line else None)
239
+
240
+ # Fallback if we can't find a suitable frame
241
+ return None
242
+
243
+
244
+ def _blocking_in_async_warning(original_func: types.FunctionType):
245
+ if is_interactive_ipython():
246
+ # in notebooks or interactive sessions where sync usage is expected
247
+ # even if it's actually running in an event loop
248
+ return
249
+
250
+ import warnings
251
+
252
+ # Skip warnings for __aexit__ and __anext__ - the __aenter__ and __aiter__ warnings are sufficient
253
+ if original_func:
254
+ func_name = getattr(original_func, "__name__", str(original_func))
255
+ if func_name in ("__aexit__", "__anext__"):
256
+ # These dunders would typically already have caused a warning on the __aenter__ or __aiter__ respectively
257
+ return
258
+
259
+ # Extract the call frame from the stack
260
+ call_frame = _extract_user_call_frame()
261
+
262
+ # Build detailed warning message with location and function first
263
+ message_parts = [
264
+ "A blocking Modal interface is being used in an async context.",
265
+ "\n\nThis may cause performance issues or bugs.",
266
+ " Consider rewriting to use Modal's async interfaces:",
267
+ "\nhttps://modal.com/docs/guide/async",
268
+ ]
269
+
270
+ # Generate intelligent suggestion based on the context
271
+ suggestion = None
272
+ code_line = None
273
+
274
+ if original_func and call_frame and call_frame.line:
275
+ code_line = call_frame.line.strip()
276
+ # Use the unified rewrite function for all patterns
277
+ _, suggestion = rewrite_sync_to_async(code_line, original_func)
278
+
279
+ # Add suggestion in "change X to Y" format
280
+ if suggestion and code_line:
281
+ # this is a bit ugly, but the warnings formatter will show the offending source line
282
+ # on the last line regardless what we do, so we add this to not make it look out of place
283
+ message_parts.append(f"\n\nSuggested rewrite:\n {suggestion}\n\nOriginal line:")
284
+
285
+ # Use warn_explicit to provide precise location information from the call frame
286
+ if call_frame:
287
+ # Extract module name from filename, or use a default
288
+ module_name = os.path.splitext(os.path.basename(call_frame.filename))[0]
289
+
290
+ warnings.warn_explicit(
291
+ "".join(message_parts),
292
+ AsyncUsageWarning,
293
+ filename=call_frame.filename,
294
+ lineno=call_frame.lineno,
295
+ module=module_name,
296
+ )
297
+ else:
298
+ # Fallback to regular warn if no frame information available
299
+ warnings.warn("".join(message_parts), AsyncUsageWarning)
300
+
301
+
302
+ def _safe_blocking_in_async_warning(original_func: types.FunctionType):
303
+ """
304
+ Safety wrapper around _blocking_in_async_warning to ensure it never raises exceptions.
305
+
306
+ This is non-critical functionality (just a warning), so we don't want it to break user code.
307
+ However, if the warning has been configured to be treated as an error (via filterwarnings),
308
+ we should let that propagate.
309
+ """
310
+ from ..config import config
311
+
312
+ if not config.get("async_warnings"):
313
+ return
314
+ try:
315
+ _blocking_in_async_warning(original_func)
316
+ except AsyncUsageWarning:
317
+ # Re-raise the warning if it's been configured as an error
318
+ raise
319
+ except Exception:
320
+ # Silently ignore any other errors in the warning system
321
+ # We don't want the warning mechanism itself to cause problems
322
+ pass
323
+
324
+
325
+ synchronizer = synchronicity.Synchronizer(blocking_in_async_callback=_safe_blocking_in_async_warning)
42
326
 
43
327
 
44
328
  def synchronize_api(obj, target_module=None):
@@ -389,6 +673,7 @@ class _WarnIfGeneratorIsNotConsumed:
389
673
  self.function_name = function_name
390
674
  self.iterated = False
391
675
  self.warned = False
676
+ self.__wrapped__ = gen
392
677
 
393
678
  def __aiter__(self):
394
679
  self.iterated = True
@@ -897,3 +1182,26 @@ async def async_chain(*generators: AsyncGenerator[T, None]) -> AsyncGenerator[T,
897
1182
  logger.exception(f"Error closing async generator: {e}")
898
1183
  if first_exception is not None:
899
1184
  raise first_exception
1185
+
1186
+
1187
+ def deprecate_aio_usage(deprecation_date: tuple[int, int, int], readable_sync_call: str):
1188
+ # Note: Currently only works on methods, not top level functions
1189
+ def deco(sync_implementation):
1190
+ if isinstance(sync_implementation, classmethod):
1191
+ sync_implementation = sync_implementation.__func__
1192
+ is_classmethod = True
1193
+ else:
1194
+ is_classmethod = False
1195
+
1196
+ async def _async_proxy(*args, **kwargs):
1197
+ deprecation_warning(
1198
+ deprecation_date,
1199
+ f"""The async constructor {readable_sync_call}.aio(...) will be deprecated in a future version of Modal.
1200
+ Please use {readable_sync_call}(...) instead (it doesn't perform any IO, and is safe in async contexts)
1201
+ """,
1202
+ )
1203
+ return sync_implementation(*args, **kwargs)
1204
+
1205
+ return MethodWithAio(sync_implementation, _async_proxy, synchronizer, is_classmethod=is_classmethod)
1206
+
1207
+ return deco
@@ -45,8 +45,11 @@ def patch_mock_servicer(cls):
45
45
  Also patches all unimplemented abstract methods in a mock servicer with default error implementations.
46
46
  """
47
47
 
48
- async def fallback(self, stream) -> None:
49
- raise GRPCError(Status.UNIMPLEMENTED, "Not implemented in mock servicer " + repr(cls))
48
+ def fallback(name: str):
49
+ async def _fallback(self, stream) -> None:
50
+ raise GRPCError(Status.UNIMPLEMENTED, f"{name} not implemented in mock servicer " + repr(cls))
51
+
52
+ return _fallback
50
53
 
51
54
  @contextlib.contextmanager
52
55
  def intercept(servicer):
@@ -85,7 +88,7 @@ def patch_mock_servicer(cls):
85
88
  for name in dir(cls):
86
89
  method = getattr(cls, name)
87
90
  if getattr(method, "__isabstractmethod__", False):
88
- setattr(cls, name, patch_grpc_method(name, fallback))
91
+ setattr(cls, name, patch_grpc_method(name, fallback(name)))
89
92
  elif name[0].isupper() and inspect.isfunction(method):
90
93
  setattr(cls, name, patch_grpc_method(name, method))
91
94
 
modal/app.py CHANGED
@@ -21,7 +21,7 @@ from synchronicity.async_wrap import asynccontextmanager
21
21
  from modal_proto import api_pb2
22
22
 
23
23
  from ._functions import _Function
24
- from ._ipython import is_notebook
24
+ from ._ipython import is_interactive_ipython
25
25
  from ._load_context import LoadContext
26
26
  from ._object import _get_environment_name, _Object
27
27
  from ._partial_function import (
@@ -509,9 +509,10 @@ class _App:
509
509
  if old_function is function:
510
510
  return # already added the same exact instance, ignore
511
511
 
512
- # In notebooks, re-registering the same function with the same app will cause a named collision.
513
- # This is common notebook coding behavior, so we hide the warning.
514
- if not is_notebook():
512
+ # In a notebook or interactive REPL it would be relatively normal to rerun a cell that
513
+ # registers a function multiple times (i.e. as you iterate on the Function definition),
514
+ # and we don't want to warn about a collision in that case.
515
+ if not is_interactive_ipython():
515
516
  logger.warning(
516
517
  f"Warning: function name '{function.tag}' collision!"
517
518
  " Overriding existing function "
modal/client.py CHANGED
@@ -71,14 +71,15 @@ class _Client:
71
71
  _client_from_env_lock: ClassVar[Optional[asyncio.Lock]] = None
72
72
  _cancellation_context: TaskContext
73
73
  _cancellation_context_event_loop: Optional[asyncio.AbstractEventLoop] = None
74
- _stub: Optional[modal_api_grpc.ModalClientModal]
74
+ _stub: Optional[modal_api_grpc.ModalClientModal] = None
75
75
  _auth_token_manager: Optional[_AuthTokenManager] = None
76
76
  _snapshotted: bool = False
77
+ client_type: "api_pb2.ClientType.ValueType"
77
78
 
78
79
  def __init__(
79
80
  self,
80
81
  server_url: str,
81
- client_type: int,
82
+ client_type: "api_pb2.ClientType.ValueType",
82
83
  credentials: Optional[tuple[str, str]],
83
84
  version: str = __version__,
84
85
  ):
modal/client.pyi CHANGED
@@ -26,13 +26,14 @@ class _Client:
26
26
  _stub: typing.Optional[modal_proto.modal_api_grpc.ModalClientModal]
27
27
  _auth_token_manager: typing.Optional[modal._utils.auth_token_manager._AuthTokenManager]
28
28
  _snapshotted: bool
29
+ client_type: int
29
30
 
30
31
  def __init__(
31
32
  self,
32
33
  server_url: str,
33
34
  client_type: int,
34
35
  credentials: typing.Optional[tuple[str, str]],
35
- version: str = "1.2.7.dev12",
36
+ version: str = "1.2.7.dev13",
36
37
  ):
37
38
  """mdmd:hidden
38
39
  The Modal client object is not intended to be instantiated directly by users.
@@ -155,13 +156,14 @@ class Client:
155
156
  _stub: typing.Optional[modal_proto.modal_api_grpc.ModalClientModal]
156
157
  _auth_token_manager: typing.Optional[modal._utils.auth_token_manager._AuthTokenManager]
157
158
  _snapshotted: bool
159
+ client_type: int
158
160
 
159
161
  def __init__(
160
162
  self,
161
163
  server_url: str,
162
164
  client_type: int,
163
165
  credentials: typing.Optional[tuple[str, str]],
164
- version: str = "1.2.7.dev12",
166
+ version: str = "1.2.7.dev13",
165
167
  ):
166
168
  """mdmd:hidden
167
169
  The Modal client object is not intended to be instantiated directly by users.
modal/config.py CHANGED
@@ -263,6 +263,7 @@ _SETTINGS = {
263
263
  ),
264
264
  "dev_suffix": _Setting("", transform=_enforce_suffix_rules),
265
265
  "max_throttle_wait": _Setting(None, transform=lambda x: int(x) if x else None),
266
+ "async_warnings": _Setting(False, transform=_to_boolean), # Feature flag for async API warnings
266
267
  }
267
268
 
268
269
 
modal/exception.py CHANGED
@@ -252,6 +252,10 @@ class ServerWarning(UserWarning):
252
252
  """Warning originating from the Modal server and re-issued in client code."""
253
253
 
254
254
 
255
+ class AsyncUsageWarning(UserWarning):
256
+ """Warning emitted when a blocking Modal interface is used in an async context."""
257
+
258
+
255
259
  class InternalFailure(Error):
256
260
  """Retriable internal error."""
257
261
 
modal/functions.pyi CHANGED
@@ -407,7 +407,7 @@ class Function(
407
407
 
408
408
  _call_generator: ___call_generator_spec
409
409
 
410
- class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
410
+ class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
411
411
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
412
412
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
413
413
  ...
@@ -416,7 +416,7 @@ class Function(
416
416
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
417
417
  ...
418
418
 
419
- remote: __remote_spec[modal._functions.P, modal._functions.ReturnType]
419
+ remote: __remote_spec[modal._functions.ReturnType, modal._functions.P]
420
420
 
421
421
  class __remote_gen_spec(typing_extensions.Protocol):
422
422
  def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
@@ -443,7 +443,7 @@ class Function(
443
443
  """
444
444
  ...
445
445
 
446
- class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
446
+ class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
447
447
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
448
448
  """[Experimental] Calls the function with the given arguments, without waiting for the results.
449
449
 
@@ -466,7 +466,7 @@ class Function(
466
466
  """
467
467
  ...
468
468
 
469
- _experimental_spawn: ___experimental_spawn_spec[modal._functions.P, modal._functions.ReturnType]
469
+ _experimental_spawn: ___experimental_spawn_spec[modal._functions.ReturnType, modal._functions.P]
470
470
 
471
471
  class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER]):
472
472
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> None: ...
@@ -474,7 +474,7 @@ class Function(
474
474
 
475
475
  _spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P]
476
476
 
477
- class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
477
+ class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
478
478
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
479
479
  """Calls the function with the given arguments, without waiting for the results.
480
480
 
@@ -495,7 +495,7 @@ class Function(
495
495
  """
496
496
  ...
497
497
 
498
- spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType]
498
+ spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P]
499
499
 
500
500
  def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
501
501
  """Return the inner Python object wrapped by this Modal Function."""
@@ -864,7 +864,7 @@ class FunctionCall(typing.Generic[modal._functions.ReturnType], modal.object.Obj
864
864
  fc_id = fc.object_id
865
865
 
866
866
  # Later, use the ID to re-instantiate the FunctionCall object
867
- fc = _FunctionCall.from_id(fc_id)
867
+ fc = FunctionCall.from_id(fc_id)
868
868
  result = fc.get()
869
869
  ```
870
870
 
@@ -873,27 +873,7 @@ class FunctionCall(typing.Generic[modal._functions.ReturnType], modal.object.Obj
873
873
  """
874
874
  ...
875
875
 
876
- async def aio(
877
- self, /, function_call_id: str, client: typing.Optional[modal.client.Client] = None
878
- ) -> FunctionCall[typing.Any]:
879
- """Instantiate a FunctionCall object from an existing ID.
880
-
881
- Examples:
882
-
883
- ```python notest
884
- # Spawn a FunctionCall and keep track of its object ID
885
- fc = my_func.spawn()
886
- fc_id = fc.object_id
887
-
888
- # Later, use the ID to re-instantiate the FunctionCall object
889
- fc = _FunctionCall.from_id(fc_id)
890
- result = fc.get()
891
- ```
892
-
893
- Note that it's only necessary to re-instantiate the `FunctionCall` with this method
894
- if you no longer have access to the original object returned from `Function.spawn`.
895
- """
896
- ...
876
+ async def aio(self, /, function_call_id: str, client: typing.Optional[modal.client.Client] = None): ...
897
877
 
898
878
  from_id: typing.ClassVar[__from_id_spec]
899
879
 
modal/image.py CHANGED
@@ -21,6 +21,7 @@ from typing import (
21
21
  get_args,
22
22
  )
23
23
 
24
+ import typing_extensions
24
25
  from google.protobuf.message import Message
25
26
  from grpclib.exceptions import StreamTerminatedError
26
27
  from typing_extensions import Self
@@ -32,7 +33,7 @@ from ._load_context import LoadContext
32
33
  from ._object import _Object, live_method_gen
33
34
  from ._resolver import Resolver
34
35
  from ._serialization import get_preferred_payload_format, serialize
35
- from ._utils.async_utils import synchronize_api
36
+ from ._utils.async_utils import deprecate_aio_usage, synchronize_api, synchronizer
36
37
  from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
37
38
  from ._utils.docker_utils import (
38
39
  extract_copy_command_patterns,
@@ -63,6 +64,7 @@ from .volume import _Volume
63
64
 
64
65
  if typing.TYPE_CHECKING:
65
66
  import modal._functions
67
+ import modal.client
66
68
 
67
69
  # This is used for both type checking and runtime validation
68
70
  ImageBuilderVersion = Literal["2023.12", "2024.04", "2024.10", "2025.06", "PREVIEW"]
@@ -866,22 +868,25 @@ class _Image(_Object, type_prefix="im"):
866
868
  img._added_python_source_set |= set(modules)
867
869
  return img
868
870
 
869
- @staticmethod
870
- async def from_id(image_id: str, client: Optional[_Client] = None) -> "_Image":
871
+ @deprecate_aio_usage((2025, 11, 14), "Image.from_id")
872
+ @classmethod
873
+ def from_id(cls, image_id: str, client: Optional["modal.client.Client"] = None) -> typing_extensions.Self:
871
874
  """Construct an Image from an id and look up the Image result.
872
875
 
873
876
  The ID of an Image object can be accessed using `.object_id`.
874
877
  """
878
+ _client = typing.cast(_Client, synchronizer._translate_in(client))
875
879
 
876
880
  async def _load(self: _Image, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]):
877
881
  resp = await load_context.client.stub.ImageFromId(api_pb2.ImageFromIdRequest(image_id=image_id))
878
882
  self._hydrate(resp.image_id, load_context.client, resp.metadata)
879
883
 
880
884
  rep = f"Image.from_id({image_id!r})"
881
- obj = _Image._from_loader(_load, rep, load_context_overrides=LoadContext(client=client))
885
+
886
+ obj = _Image._from_loader(_load, rep, load_context_overrides=LoadContext(client=_client))
882
887
  obj._object_id = image_id
883
888
 
884
- return obj
889
+ return typing.cast(typing_extensions.Self, synchronizer._translate_out(obj))
885
890
 
886
891
  async def build(self, app: "modal.app._App") -> "_Image":
887
892
  """Eagerly build an image.
modal/image.pyi CHANGED
@@ -124,6 +124,8 @@ async def _image_await_build_result(
124
124
  image_id: str, client: modal.client._Client
125
125
  ) -> modal_proto.api_pb2.ImageJoinStreamingResponse: ...
126
126
 
127
+ SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
128
+
127
129
  class _Image(modal._object._Object):
128
130
  """Base class for container images to run functions in.
129
131
 
@@ -311,13 +313,17 @@ class _Image(modal._object._Object):
311
313
  """
312
314
  ...
313
315
 
314
- @staticmethod
315
- async def from_id(image_id: str, client: typing.Optional[modal.client._Client] = None) -> _Image:
316
- """Construct an Image from an id and look up the Image result.
316
+ class __from_id_spec(typing_extensions.Protocol[SUPERSELF]):
317
+ def __call__(self, /, image_id: str, client: typing.Optional[modal.client.Client] = None) -> SUPERSELF:
318
+ """Construct an Image from an id and look up the Image result.
317
319
 
318
- The ID of an Image object can be accessed using `.object_id`.
319
- """
320
- ...
320
+ The ID of an Image object can be accessed using `.object_id`.
321
+ """
322
+ ...
323
+
324
+ async def aio(self, /, image_id: str, client: typing.Optional[modal.client.Client] = None): ...
325
+
326
+ from_id: typing.ClassVar[__from_id_spec[typing_extensions.Self]]
321
327
 
322
328
  async def build(self, app: modal.app._App) -> _Image:
323
329
  """Eagerly build an image.
@@ -1035,8 +1041,6 @@ class _Image(modal._object._Object):
1035
1041
  """mdmd:hidden"""
1036
1042
  ...
1037
1043
 
1038
- SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
1039
-
1040
1044
  class Image(modal.object.Object):
1041
1045
  """Base class for container images to run functions in.
1042
1046
 
@@ -1228,22 +1232,17 @@ class Image(modal.object.Object):
1228
1232
  """
1229
1233
  ...
1230
1234
 
1231
- class __from_id_spec(typing_extensions.Protocol):
1232
- def __call__(self, /, image_id: str, client: typing.Optional[modal.client.Client] = None) -> Image:
1235
+ class __from_id_spec(typing_extensions.Protocol[SUPERSELF]):
1236
+ def __call__(self, /, image_id: str, client: typing.Optional[modal.client.Client] = None) -> SUPERSELF:
1233
1237
  """Construct an Image from an id and look up the Image result.
1234
1238
 
1235
1239
  The ID of an Image object can be accessed using `.object_id`.
1236
1240
  """
1237
1241
  ...
1238
1242
 
1239
- async def aio(self, /, image_id: str, client: typing.Optional[modal.client.Client] = None) -> Image:
1240
- """Construct an Image from an id and look up the Image result.
1241
-
1242
- The ID of an Image object can be accessed using `.object_id`.
1243
- """
1244
- ...
1243
+ async def aio(self, /, image_id: str, client: typing.Optional[modal.client.Client] = None): ...
1245
1244
 
1246
- from_id: typing.ClassVar[__from_id_spec]
1245
+ from_id: typing.ClassVar[__from_id_spec[typing_extensions.Self]]
1247
1246
 
1248
1247
  class __build_spec(typing_extensions.Protocol):
1249
1248
  def __call__(self, /, app: modal.app.App) -> Image:
modal/snapshot.py CHANGED
@@ -1,12 +1,15 @@
1
1
  # Copyright Modal Labs 2024
2
- from typing import Optional
2
+ from typing import Optional, cast
3
3
 
4
+ import typing_extensions
5
+
6
+ import modal.client
4
7
  from modal_proto import api_pb2
5
8
 
6
9
  from ._load_context import LoadContext
7
10
  from ._object import _Object
8
11
  from ._resolver import Resolver
9
- from ._utils.async_utils import synchronize_api
12
+ from ._utils.async_utils import deprecate_aio_usage, synchronize_api, synchronizer
10
13
  from .client import _Client
11
14
 
12
15
 
@@ -19,27 +22,35 @@ class _SandboxSnapshot(_Object, type_prefix="sn"):
19
22
  the original Sandbox at the time the snapshot was taken.
20
23
  """
21
24
 
22
- @staticmethod
23
- async def from_id(sandbox_snapshot_id: str, client: Optional[_Client] = None):
25
+ @deprecate_aio_usage((2025, 12, 5), "SandboxSnapshot.from_id")
26
+ @classmethod
27
+ def from_id(
28
+ cls, sandbox_snapshot_id: str, client: Optional["modal.client.Client"] = None
29
+ ) -> typing_extensions.Self:
24
30
  """
25
31
  Construct a `SandboxSnapshot` object from a sandbox snapshot ID.
26
32
  """
27
- # TODO: remove this - from_id constructor should not do io:
28
- client = client or await _Client.from_env()
33
+ _client = cast(_Client, synchronizer._translate_in(client))
29
34
 
30
35
  async def _load(
31
36
  self: _SandboxSnapshot, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
32
37
  ):
33
- await load_context.client.stub.SandboxSnapshotGet(
38
+ # hydration doesn't actually do much apart from validating the existance of the id
39
+ # which is implicitly done by trying to start a sandbox from the snapshot as well
40
+ resp: api_pb2.SandboxSnapshotGetResponse = await load_context.client.stub.SandboxSnapshotGet(
34
41
  api_pb2.SandboxSnapshotGetRequest(snapshot_id=sandbox_snapshot_id)
35
42
  )
43
+ self._hydrate(resp.snapshot_id, load_context.client, None)
36
44
 
37
45
  rep = "SandboxSnapshot()"
38
- obj = _SandboxSnapshot._from_loader(_load, rep, load_context_overrides=LoadContext(client=client))
39
- # TODO: should this be a _Object._new_hydrated instead?
40
- obj._hydrate(sandbox_snapshot_id, client, None)
41
-
42
- return obj
46
+ obj = _SandboxSnapshot._from_loader(
47
+ _load, rep, load_context_overrides=LoadContext(client=_client), hydrate_lazily=True
48
+ )
49
+ # Setting the object id directly is a bit hacky, but
50
+ # it avoids hydrating the object fully if it's going
51
+ # to be used only for its object id anyway
52
+ obj._object_id = sandbox_snapshot_id
53
+ return cast(typing_extensions.Self, synchronizer._translate_out(obj))
43
54
 
44
55
 
45
56
  SandboxSnapshot = synchronize_api(_SandboxSnapshot)
modal/snapshot.pyi CHANGED
@@ -4,6 +4,8 @@ import modal.object
4
4
  import typing
5
5
  import typing_extensions
6
6
 
7
+ SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
8
+
7
9
  class _SandboxSnapshot(modal._object._Object):
8
10
  """> Sandbox memory snapshots are in **early preview**.
9
11
 
@@ -11,10 +13,16 @@ class _SandboxSnapshot(modal._object._Object):
11
13
  `._experimental_snapshot()` on a Sandbox instance. This includes both the filesystem and memory state of
12
14
  the original Sandbox at the time the snapshot was taken.
13
15
  """
14
- @staticmethod
15
- async def from_id(sandbox_snapshot_id: str, client: typing.Optional[modal.client._Client] = None):
16
- """Construct a `SandboxSnapshot` object from a sandbox snapshot ID."""
17
- ...
16
+ class __from_id_spec(typing_extensions.Protocol[SUPERSELF]):
17
+ def __call__(
18
+ self, /, sandbox_snapshot_id: str, client: typing.Optional[modal.client.Client] = None
19
+ ) -> SUPERSELF:
20
+ """Construct a `SandboxSnapshot` object from a sandbox snapshot ID."""
21
+ ...
22
+
23
+ async def aio(self, /, sandbox_snapshot_id: str, client: typing.Optional[modal.client.Client] = None): ...
24
+
25
+ from_id: typing.ClassVar[__from_id_spec[typing_extensions.Self]]
18
26
 
19
27
  class SandboxSnapshot(modal.object.Object):
20
28
  """> Sandbox memory snapshots are in **early preview**.
@@ -27,13 +35,13 @@ class SandboxSnapshot(modal.object.Object):
27
35
  """mdmd:hidden"""
28
36
  ...
29
37
 
30
- class __from_id_spec(typing_extensions.Protocol):
31
- def __call__(self, /, sandbox_snapshot_id: str, client: typing.Optional[modal.client.Client] = None):
38
+ class __from_id_spec(typing_extensions.Protocol[SUPERSELF]):
39
+ def __call__(
40
+ self, /, sandbox_snapshot_id: str, client: typing.Optional[modal.client.Client] = None
41
+ ) -> SUPERSELF:
32
42
  """Construct a `SandboxSnapshot` object from a sandbox snapshot ID."""
33
43
  ...
34
44
 
35
- async def aio(self, /, sandbox_snapshot_id: str, client: typing.Optional[modal.client.Client] = None):
36
- """Construct a `SandboxSnapshot` object from a sandbox snapshot ID."""
37
- ...
45
+ async def aio(self, /, sandbox_snapshot_id: str, client: typing.Optional[modal.client.Client] = None): ...
38
46
 
39
- from_id: typing.ClassVar[__from_id_spec]
47
+ from_id: typing.ClassVar[__from_id_spec[typing_extensions.Self]]
modal/volume.py CHANGED
@@ -8,6 +8,7 @@ import multiprocessing
8
8
  import os
9
9
  import platform
10
10
  import re
11
+ import sys
11
12
  import time
12
13
  import typing
13
14
  from collections.abc import AsyncGenerator, AsyncIterator, Generator, Sequence
@@ -39,6 +40,7 @@ from ._object import (
39
40
  _get_environment_name,
40
41
  _Object,
41
42
  live_method,
43
+ live_method_contextmanager,
42
44
  live_method_gen,
43
45
  )
44
46
  from ._resolver import Resolver
@@ -643,7 +645,7 @@ class _Volume(_Object, type_prefix="vo"):
643
645
  return [entry async for entry in self.iterdir(path, recursive=recursive)]
644
646
 
645
647
  @live_method_gen
646
- async def read_file(self, path: str) -> AsyncIterator[bytes]:
648
+ async def read_file(self, path: str) -> AsyncGenerator[bytes, None]:
647
649
  """
648
650
  Read a file from the modal.Volume.
649
651
 
@@ -816,8 +818,9 @@ class _Volume(_Object, type_prefix="vo"):
816
818
  )
817
819
  await self._client.stub.VolumeCopyFiles2(request, retry=Retry(base_delay=1))
818
820
 
819
- @live_method
820
- async def batch_upload(self, force: bool = False) -> "_AbstractVolumeUploadContextManager":
821
+ @live_method_contextmanager
822
+ @asynccontextmanager
823
+ async def batch_upload(self, force: bool = False) -> AsyncGenerator["_AbstractVolumeUploadContextManager", None]:
821
824
  """
822
825
  Initiate a batched upload to a volume.
823
826
 
@@ -838,9 +841,15 @@ class _Volume(_Object, type_prefix="vo"):
838
841
  if self._read_only:
839
842
  raise InvalidError("Read-only Volume can not be written to")
840
843
 
841
- return _AbstractVolumeUploadContextManager.resolve(
844
+ version_context_manager = _AbstractVolumeUploadContextManager.resolve(
842
845
  self._metadata.version, self.object_id, self._client, force=force
843
846
  )
847
+ await version_context_manager.__aenter__()
848
+ try:
849
+ yield version_context_manager
850
+ finally:
851
+ exc_type, exc_value, traceback = sys.exc_info()
852
+ await version_context_manager.__aexit__(exc_type, exc_value, traceback)
844
853
 
845
854
  @live_method
846
855
  async def _instance_delete(self):
modal/volume.pyi CHANGED
@@ -597,7 +597,7 @@ class _Volume(modal._object._Object):
597
597
  """
598
598
  ...
599
599
 
600
- def read_file(self, path: str) -> collections.abc.AsyncIterator[bytes]:
600
+ def read_file(self, path: str) -> collections.abc.AsyncGenerator[bytes, None]:
601
601
  """Read a file from the modal.Volume.
602
602
 
603
603
  Note - this function is primarily intended to be used outside of a Modal App.
@@ -666,7 +666,7 @@ class _Volume(modal._object._Object):
666
666
  """
667
667
  ...
668
668
 
669
- async def batch_upload(self, force: bool = False) -> _AbstractVolumeUploadContextManager:
669
+ def batch_upload(self, force: bool = False) -> typing.AsyncContextManager[_AbstractVolumeUploadContextManager]:
670
670
  """Initiate a batched upload to a volume.
671
671
 
672
672
  To allow overwriting existing files, set `force` to `True` (you cannot overwrite existing directories with
@@ -1033,7 +1033,7 @@ class Volume(modal.object.Object):
1033
1033
  listdir: __listdir_spec
1034
1034
 
1035
1035
  class __read_file_spec(typing_extensions.Protocol):
1036
- def __call__(self, /, path: str) -> typing.Iterator[bytes]:
1036
+ def __call__(self, /, path: str) -> typing.Generator[bytes, None, None]:
1037
1037
  """Read a file from the modal.Volume.
1038
1038
 
1039
1039
  Note - this function is primarily intended to be used outside of a Modal App.
@@ -1052,7 +1052,7 @@ class Volume(modal.object.Object):
1052
1052
  """
1053
1053
  ...
1054
1054
 
1055
- def aio(self, /, path: str) -> collections.abc.AsyncIterator[bytes]:
1055
+ def aio(self, /, path: str) -> collections.abc.AsyncGenerator[bytes, None]:
1056
1056
  """Read a file from the modal.Volume.
1057
1057
 
1058
1058
  Note - this function is primarily intended to be used outside of a Modal App.
@@ -1189,7 +1189,9 @@ class Volume(modal.object.Object):
1189
1189
  copy_files: __copy_files_spec
1190
1190
 
1191
1191
  class __batch_upload_spec(typing_extensions.Protocol):
1192
- def __call__(self, /, force: bool = False) -> AbstractVolumeUploadContextManager:
1192
+ def __call__(
1193
+ self, /, force: bool = False
1194
+ ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[AbstractVolumeUploadContextManager]:
1193
1195
  """Initiate a batched upload to a volume.
1194
1196
 
1195
1197
  To allow overwriting existing files, set `force` to `True` (you cannot overwrite existing directories with
@@ -1208,7 +1210,7 @@ class Volume(modal.object.Object):
1208
1210
  """
1209
1211
  ...
1210
1212
 
1211
- async def aio(self, /, force: bool = False) -> AbstractVolumeUploadContextManager:
1213
+ def aio(self, /, force: bool = False) -> typing.AsyncContextManager[AbstractVolumeUploadContextManager]:
1212
1214
  """Initiate a batched upload to a volume.
1213
1215
 
1214
1216
  To allow overwriting existing files, set `force` to `True` (you cannot overwrite existing directories with
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.7.dev12
3
+ Version: 1.2.7.dev13
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -4,12 +4,12 @@ modal/_billing.py,sha256=C1jUN9f_1WqozSZAt9EOk1nImXUdiLrgFeeAu3R23cI,3012
4
4
  modal/_clustered_functions.py,sha256=7amiOxErtlciWeBJBL2KvaxerUomJpTAJ3qElflFHQA,2884
5
5
  modal/_clustered_functions.pyi,sha256=JmYwAGOLEnD5AF-gYF9O5tu-SgGjeoJz-X1j48b1Ijg,1157
6
6
  modal/_container_entrypoint.py,sha256=aatoboeM3fe9w7DVdCgVOhW4iW9EwduZwiVS3ri0HgA,19787
7
- modal/_functions.py,sha256=VqhEE8cctHkonWvtdXOCYnW06TvXPTV1ONeIMoR1SFA,91262
7
+ modal/_functions.py,sha256=7mw9Te71sMhIoOEKcl9lo2DRhs6bWoXn7jeJdKhjY4I,91611
8
8
  modal/_grpc_client.py,sha256=t3Ubaul6YD2-pt0lTPLz2nW1z25VuUfmyGDHewO0jx0,6929
9
- modal/_ipython.py,sha256=TW1fkVOmZL3YYqdS2YlM1hqpf654Yf8ZyybHdBnlhSw,301
9
+ modal/_ipython.py,sha256=Bu9fc3hm3xt2O47qLe_d-Mu2jS81LwMsmXNJ1sawBA0,575
10
10
  modal/_load_context.py,sha256=P7bMvu-_E1YXWhunYU46EqQX78r4D3b05YGz9blSsy8,3786
11
11
  modal/_location.py,sha256=joiX-0ZeutEUDTrrqLF1GHXCdVLF-rHzstocbMcd_-k,366
12
- modal/_object.py,sha256=kRTugj_eB4lD6bcVIo57hQ12Dho8KEdCWhURrOd6KVE,13232
12
+ modal/_object.py,sha256=6h5a-dcHRKpctpUIyVvQfaygJ5v0fIJAo4n4cFpODys,13729
13
13
  modal/_output.py,sha256=pKRLDyoEXjLe-oV12-hb6gYOpdUmNzrq7FQna-maC70,26832
14
14
  modal/_partial_function.py,sha256=xRaU_A5SE30jywgU_-S1kV2y9WUrm0S906T5OWpS2Vg,38684
15
15
  modal/_pty.py,sha256=E58MQ8d5-wkbMatRKpQR-G9FdbCRcZGiZxOpGy__VuY,1481
@@ -21,32 +21,32 @@ modal/_tunnel.py,sha256=bbjVuWiDyC3m_NaVBpgoNQte-NM1FUtDVQsMobQbhc8,6578
21
21
  modal/_tunnel.pyi,sha256=FoedJzHymbPk4Bmnbszm6uhay2rMq1JS9mIiHELYf3k,13804
22
22
  modal/_type_manager.py,sha256=DWjgmjYJuOagw2erin506UUbG2H5UzZCFEekS-7hmfA,9087
23
23
  modal/_watcher.py,sha256=K6LYnlmSGQB4tWWI9JADv-tvSvQ1j522FwT71B51CX8,3584
24
- modal/app.py,sha256=xyTCQwLVNBoKENy0aKZIoK7TVegvwXz984VOoKTs-PE,59466
24
+ modal/app.py,sha256=RNtwU9xibABT7m7c6idX49wtEi0HWNuJvsTi9R5RHeo,59571
25
25
  modal/app.pyi,sha256=WR0nGSUh8-8OYRHFqEnijGoPp_IbawNY1JW_0pozXsE,50857
26
26
  modal/billing.py,sha256=zmQ3bcCJlwa4KD1IA_QgdWpm1pn13c-7qfy79iEauYI,195
27
27
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
28
- modal/client.py,sha256=k1CqQ_0t4-vHylkqYZgYCnClg-FT0QrTPAKivL01A_k,14354
29
- modal/client.pyi,sha256=fCTgoqk8EYlqyQokJOKNDKhIj5HcasgzAPrL4KTE7Bg,14431
28
+ modal/client.py,sha256=CXnyJYLhY--xaCkjv3tpjYDca8AuXGQG1IiaXQVX4y4,14436
29
+ modal/client.pyi,sha256=0iz_8ZlCD4j-mRt3uF7rsiuaHNhfIiIaODwD4NQLg-A,14473
30
30
  modal/cloud_bucket_mount.py,sha256=cvVj5Rt4nDHoQbTneoJPTaAcnl_Ab_mrsjnnzd0v9OI,6013
31
31
  modal/cloud_bucket_mount.pyi,sha256=vXusGMOUk9HjCs4lhxteuCK4xLQclPfugGWKo7LECEA,7539
32
32
  modal/cls.py,sha256=Cmqt1vOw0-01ktjJBOrdzvdyDbHpjKMTaZ2eEy_8YUE,41694
33
33
  modal/cls.pyi,sha256=ZsJBwboZJ95wqgQUFi2VB6etbDXrmMPwJ61c-r8T2kY,27262
34
- modal/config.py,sha256=byOUVfUibX9Ixrhbz5z4HDcgn-8S5OFicD6xGUErqZs,13415
34
+ modal/config.py,sha256=JvNf4jr5xRLlFgIp4l38z1CJQyDxOHc_DnPOAYCJGvE,13516
35
35
  modal/container_process.py,sha256=xMJ93EeBcI-BoGDv78-a0UEbvSza-ye_TFi5fpGAxbw,16777
36
36
  modal/container_process.pyi,sha256=jyV3WIkTn2NsXqAFf5Jseciz2mdq_sjO9zQMoJnquIg,8211
37
37
  modal/dict.py,sha256=9tyzgvLLribY5dJAC5n8_kiF5GNZTr0QD35LuEtYgC4,22664
38
38
  modal/dict.pyi,sha256=1g6wUpLESOsSecE9oLiu61wg9wOhl5t1e0jX0qHqkJQ,32381
39
39
  modal/environments.py,sha256=wMiMNpPkvIl_BdxRb81CY9Djzhczp_y4GEYKWp0JyBc,6135
40
40
  modal/environments.pyi,sha256=bMIxvwahckgaolGD3-hP2iovRHDXewvgbVs_nx4ze0o,3808
41
- modal/exception.py,sha256=YPIa5uL1ug6SNJ9YvI4xefS5J6jGisdZwTBD4OWzpSo,10309
41
+ modal/exception.py,sha256=uKUHnCfMTkYeDwTBpv3AvAr3hdJnQZJjkSyaL1vUgWA,10436
42
42
  modal/file_io.py,sha256=dx90C5L8cQv2Z7pVVPaQmE0R3pUOGv2h-wn1qNNXbGQ,19818
43
43
  modal/file_io.pyi,sha256=up9OhV2Y54n-X7nlfV6j_eBwJ8D2cNMEswZ1iddrEQw,15877
44
44
  modal/file_pattern_matcher.py,sha256=A_Kdkej6q7YQyhM_2-BvpFmPqJ0oHb54B6yf9VqvPVE,8116
45
45
  modal/functions.py,sha256=kcNHvqeGBxPI7Cgd57NIBBghkfbeFJzXO44WW0jSmao,325
46
- modal/functions.pyi,sha256=gtBInyEElako585pVRplfXskzg2UAIvlUQcHjMUvbqM,37160
46
+ modal/functions.pyi,sha256=fkjxeYAqh6sX9C84gjUGoLVgHT1IfNB_07h0fpmIyWw,36465
47
47
  modal/gpu.py,sha256=Fe5ORvVPDIstSq1xjmM6OoNgLYFWvogP9r5BgmD3hYg,6769
48
- modal/image.py,sha256=AHvMjgEGuFScT4rPJJy0jCV4d2TFcjZu7wy5miCC4cE,107886
49
- modal/image.pyi,sha256=hVmhjP4KT0DmOuo70kAYvqCrvytUcRSrouhvBGXqo_A,78149
48
+ modal/image.py,sha256=sPaYxx4Dwdod1FjVooDaDDcPbcyyBXY4ChpbQvWe5mc,108197
49
+ modal/image.pyi,sha256=UMwAaYu4feSDzJS9l-5xKYP_yaJm18bY_s3pREOQF6A,78249
50
50
  modal/io_streams.py,sha256=g6H6QAl48qZtN5OY6WIIVwF9TXBj3jMNYCgn7RYYrVg,30697
51
51
  modal/io_streams.pyi,sha256=aBxCcbHTprL08FLvPlkzu4bypCTT7xUdelNe5kc7OZY,18506
52
52
  modal/mount.py,sha256=5ps0p8dik2McxetRfHakLouzOvBrQPYBFCUCrgNlXik,33856
@@ -77,13 +77,13 @@ modal/secret.py,sha256=XOHgwhjPJpzsIe7eo3DGAQ66hvyepc232UEEAREnjtk,18203
77
77
  modal/secret.pyi,sha256=wzGS_qOE0YwmoXvxWT5FyzKlTYS4wPFblbqDSuXRnoY,21016
78
78
  modal/serving.py,sha256=rfzj23Q4hPSMpc3vj_cQg3awkZULu4hLtRs83cOEB-w,4291
79
79
  modal/serving.pyi,sha256=eNqF_bhO_JQ0EDpqvSjSUdNs48vv7AcPXinp7GEs5wI,1966
80
- modal/snapshot.py,sha256=HiXLbJAZiQv2cEQ4ur8M3AVbI_ZSL-eoteVfCL77lUw,1640
81
- modal/snapshot.pyi,sha256=zks3WMIDoiegU0ZIDVacVhEVU4bmh0m7Hynu7WTqaRw,1699
80
+ modal/snapshot.py,sha256=vIKxA1XRRsH8cFcpSUqC9M8NccKIaYaDpmiEC2clNks,2297
81
+ modal/snapshot.pyi,sha256=vMYDx1s9Ds4qtH-PXgB_9RzVuS5wsTchVcclR5Q-mmM,2014
82
82
  modal/stream_type.py,sha256=A6320qoAAWhEfwOCZfGtymQTu5AfLfJXXgARqooTPvY,417
83
83
  modal/token_flow.py,sha256=tLvBE9OT50p8AnYYh4b5MvBMQc1vV1-6C0GsmyFDfxw,7626
84
84
  modal/token_flow.pyi,sha256=xxMmxKU2_Z8aLhQazqhnBng8PVPKaPgZXKnUuS5WykI,2406
85
- modal/volume.py,sha256=ayTo-z_f0zTQJVI6p31w73v35Kj6q-HumIEW7jV2MGQ,52335
86
- modal/volume.pyi,sha256=N4EKjbbpUIwwQG-p6j-DgaJamX-fQGu5Sw59rJ5DGek,53761
85
+ modal/volume.py,sha256=vej3tMfs0ayarcp2Y3wsDnStJdnQ6u2U8VKYQi7YTn8,52733
86
+ modal/volume.pyi,sha256=lrfm3p_IvWysX0OItHpsti7v9yJN1JA1XtGQrXOT1AM,53915
87
87
  modal/_runtime/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
88
88
  modal/_runtime/asgi.py,sha256=AOcduIlijmlxhXVWo7AIUhigo-bqm6nDkHj4Q4JLy6o,22607
89
89
  modal/_runtime/container_io_manager.py,sha256=EQF1aAtzb0A2chhr0B8es_G0rawbBUKZaFu0-p6dsQU,52102
@@ -96,7 +96,7 @@ modal/_runtime/user_code_event_loop.py,sha256=HwOuDyCZLOakr_DG7p2MrcscjSQNheIhBB
96
96
  modal/_runtime/user_code_imports.py,sha256=_6uHDfTpqCcspHpgjNV14uMyiGdWkAHU7zqHfUx9uzU,25997
97
97
  modal/_utils/__init__.py,sha256=waLjl5c6IPDhSsdWAm9Bji4e2PVxamYABKAze6CHVXY,28
98
98
  modal/_utils/app_utils.py,sha256=88BT4TPLWfYAQwKTHcyzNQRHg8n9B-QE2UyJs96iV-0,108
99
- modal/_utils/async_utils.py,sha256=PnwHfaHcqjLwMnlGC2kn8nqubxpXcSKcy4jHx6477dg,30625
99
+ modal/_utils/async_utils.py,sha256=2KQs7W2OwU6-5wxCoYbkt8_sLocDDQSyyBbb1gulZ20,43322
100
100
  modal/_utils/auth_token_manager.py,sha256=QsDokl1c1dN9m6Z9N-Se-8cY3laphByjJtNdRXUTieI,5151
101
101
  modal/_utils/blob_utils.py,sha256=L4lEL-rvZwoxCxrGaa8zWVoaHjhq1eQfO4SLrvt4g8k,23376
102
102
  modal/_utils/bytes_io_segment_payload.py,sha256=vaXPq8b52-x6G2hwE7SrjS58pg_aRm7gV3bn3yjmTzQ,4261
@@ -104,7 +104,7 @@ modal/_utils/deprecation.py,sha256=-Bgg7jZdcJU8lROy18YyVnQYbM8hue-hVmwJqlWAGH0,5
104
104
  modal/_utils/docker_utils.py,sha256=h1uETghR40mp_y3fSWuZAfbIASH1HMzuphJHghAL6DU,3722
105
105
  modal/_utils/function_utils.py,sha256=uoOcn_pyl6fOVHr_Q6ci1_5eJqo90so7G9izfs-T-As,28272
106
106
  modal/_utils/git_utils.py,sha256=qtUU6JAttF55ZxYq51y55OR58B0tDPZsZWK5dJe6W5g,3182
107
- modal/_utils/grpc_testing.py,sha256=H1zHqthv19eGPJz2HKXDyWXWGSqO4BRsxah3L5Xaa8A,8619
107
+ modal/_utils/grpc_testing.py,sha256=fZLyfMA5VYmIo-dUCcKQrVCH3ZKPT7K4_6ww373jt0Y,8697
108
108
  modal/_utils/grpc_utils.py,sha256=2Q88G0u22q4yo91Ud1SfxPF9YoR7C-YDU2e__KTMNp8,16279
109
109
  modal/_utils/hash_utils.py,sha256=zg3J6OGxTFGSFri1qQ12giDz90lWk8bzaxCTUCRtiX4,3034
110
110
  modal/_utils/http_utils.py,sha256=yeTFsXYr0rYMEhB7vBP7audG9Uc7OLhzKBANFDZWVt0,2451
@@ -158,7 +158,7 @@ modal/experimental/__init__.py,sha256=GOuo3t4-Le0ZVddiHIOw0MQxkdTJge-bVbhDvgnblm
158
158
  modal/experimental/flash.py,sha256=GJZwSr-4OHlP6gCIZdcbsSb8D8WX9xJwMphjwcfsRoU,31262
159
159
  modal/experimental/flash.pyi,sha256=W62a0AhmLUuH9a0n8Y3-DYhyEEFH3-bRUQFphOQfKHg,16418
160
160
  modal/experimental/ipython.py,sha256=TrCfmol9LGsRZMeDoeMPx3Hv3BFqQhYnmD_iH0pqdhk,2904
161
- modal-1.2.7.dev12.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
161
+ modal-1.2.7.dev13.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
162
162
  modal_docs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
163
163
  modal_docs/gen_cli_docs.py,sha256=c1yfBS_x--gL5bs0N4ihMwqwX8l3IBWSkBAKNNIi6bQ,3801
164
164
  modal_docs/gen_reference_docs.py,sha256=d_CQUGQ0rfw28u75I2mov9AlS773z9rG40-yq5o7g2U,6359
@@ -180,10 +180,10 @@ modal_proto/task_command_router_pb2.py,sha256=2Y8wRvnq4_7RiQ-5ZAPIgXaxrHv_MXzgS5
180
180
  modal_proto/task_command_router_pb2.pyi,sha256=yI4tFteErLlonFA2HyaWtONfhY_VGSe7KCWFIspvmkE,16323
181
181
  modal_proto/task_command_router_pb2_grpc.py,sha256=q2VWfK2-4uDcPaPLhAqPZkgxhuQRYnWmwUcnEbPbGuk,13895
182
182
  modal_proto/task_command_router_pb2_grpc.pyi,sha256=e1MM10VGUgDkm041v53QeTglGQPzTpA0FUsFtezjp74,4536
183
- modal_version/__init__.py,sha256=rFvO4YkopMGR4cQf3a1rGgiW7XIPrL2INupCBz_QS1g,121
183
+ modal_version/__init__.py,sha256=xODVrqo8G7Gk8Jq6O1pQPDO4Cq1T80Fzo0ksqa7pEN0,121
184
184
  modal_version/__main__.py,sha256=nLUbubxKx7GXNzJIkTB7M7_u8080LyLM0IS0rzFkC_c,119
185
- modal-1.2.7.dev12.dist-info/METADATA,sha256=hK-tSsEsRYfwg5lzDUJAJrf0YalSSw1msHt_6cj-Xx8,2493
186
- modal-1.2.7.dev12.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
187
- modal-1.2.7.dev12.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
188
- modal-1.2.7.dev12.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
189
- modal-1.2.7.dev12.dist-info/RECORD,,
185
+ modal-1.2.7.dev13.dist-info/METADATA,sha256=bc4BKkJwEQBggwxs9gCaVlt6jWvf9eJNx4pRPeESlYk,2493
186
+ modal-1.2.7.dev13.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
187
+ modal-1.2.7.dev13.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
188
+ modal-1.2.7.dev13.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
189
+ modal-1.2.7.dev13.dist-info/RECORD,,
modal_version/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2025
2
2
  """Supplies the current version of the modal client library."""
3
3
 
4
- __version__ = "1.2.7.dev12"
4
+ __version__ = "1.2.7.dev13"