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 +12 -6
- modal/_ipython.py +16 -6
- modal/_object.py +16 -0
- modal/_utils/async_utils.py +310 -2
- modal/_utils/grpc_testing.py +6 -3
- modal/app.py +5 -4
- modal/client.py +3 -2
- modal/client.pyi +4 -2
- modal/config.py +1 -0
- modal/exception.py +4 -0
- modal/functions.pyi +8 -28
- modal/image.py +10 -5
- modal/image.pyi +16 -17
- modal/snapshot.py +23 -12
- modal/snapshot.pyi +18 -10
- modal/volume.py +13 -4
- modal/volume.pyi +8 -6
- {modal-1.2.7.dev12.dist-info → modal-1.2.7.dev13.dist-info}/METADATA +1 -1
- {modal-1.2.7.dev12.dist-info → modal-1.2.7.dev13.dist-info}/RECORD +24 -24
- modal_version/__init__.py +1 -1
- {modal-1.2.7.dev12.dist-info → modal-1.2.7.dev13.dist-info}/WHEEL +0 -0
- {modal-1.2.7.dev12.dist-info → modal-1.2.7.dev13.dist-info}/entry_points.txt +0 -0
- {modal-1.2.7.dev12.dist-info → modal-1.2.7.dev13.dist-info}/licenses/LICENSE +0 -0
- {modal-1.2.7.dev12.dist-info → modal-1.2.7.dev13.dist-info}/top_level.txt +0 -0
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
|
-
@
|
|
2029
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
6
|
-
|
|
7
|
-
if
|
|
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
|
modal/_utils/async_utils.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
modal/_utils/grpc_testing.py
CHANGED
|
@@ -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
|
-
|
|
49
|
-
|
|
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
|
|
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
|
|
513
|
-
#
|
|
514
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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[
|
|
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.
|
|
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[
|
|
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.
|
|
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[
|
|
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.
|
|
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 =
|
|
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
|
-
@
|
|
870
|
-
|
|
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
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
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) ->
|
|
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)
|
|
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
|
-
@
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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__(
|
|
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) ->
|
|
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
|
-
@
|
|
820
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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__(
|
|
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
|
-
|
|
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
|
|
@@ -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=
|
|
7
|
+
modal/_functions.py,sha256=7mw9Te71sMhIoOEKcl9lo2DRhs6bWoXn7jeJdKhjY4I,91611
|
|
8
8
|
modal/_grpc_client.py,sha256=t3Ubaul6YD2-pt0lTPLz2nW1z25VuUfmyGDHewO0jx0,6929
|
|
9
|
-
modal/_ipython.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
29
|
-
modal/client.pyi,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
46
|
+
modal/functions.pyi,sha256=fkjxeYAqh6sX9C84gjUGoLVgHT1IfNB_07h0fpmIyWw,36465
|
|
47
47
|
modal/gpu.py,sha256=Fe5ORvVPDIstSq1xjmM6OoNgLYFWvogP9r5BgmD3hYg,6769
|
|
48
|
-
modal/image.py,sha256=
|
|
49
|
-
modal/image.pyi,sha256=
|
|
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=
|
|
81
|
-
modal/snapshot.pyi,sha256=
|
|
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=
|
|
86
|
-
modal/volume.pyi,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
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=
|
|
183
|
+
modal_version/__init__.py,sha256=xODVrqo8G7Gk8Jq6O1pQPDO4Cq1T80Fzo0ksqa7pEN0,121
|
|
184
184
|
modal_version/__main__.py,sha256=nLUbubxKx7GXNzJIkTB7M7_u8080LyLM0IS0rzFkC_c,119
|
|
185
|
-
modal-1.2.7.
|
|
186
|
-
modal-1.2.7.
|
|
187
|
-
modal-1.2.7.
|
|
188
|
-
modal-1.2.7.
|
|
189
|
-
modal-1.2.7.
|
|
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
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|