modal 0.67.43__py3-none-any.whl → 0.68.11__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/_container_entrypoint.py +4 -1
- modal/_runtime/container_io_manager.py +3 -0
- modal/_runtime/user_code_imports.py +4 -2
- modal/_traceback.py +16 -2
- modal/_utils/function_utils.py +5 -1
- modal/_utils/grpc_testing.py +6 -2
- modal/_utils/hash_utils.py +14 -2
- modal/cli/_traceback.py +11 -4
- modal/cli/run.py +0 -7
- modal/client.py +6 -37
- modal/client.pyi +2 -6
- modal/cls.py +132 -62
- modal/cls.pyi +13 -7
- modal/exception.py +20 -0
- modal/file_io.py +380 -0
- modal/file_io.pyi +185 -0
- modal/functions.py +33 -11
- modal/functions.pyi +5 -3
- modal/object.py +4 -2
- modal/partial_function.py +14 -10
- modal/partial_function.pyi +2 -2
- modal/runner.py +5 -4
- modal/runner.pyi +2 -1
- modal/sandbox.py +40 -0
- modal/sandbox.pyi +18 -0
- {modal-0.67.43.dist-info → modal-0.68.11.dist-info}/METADATA +2 -2
- {modal-0.67.43.dist-info → modal-0.68.11.dist-info}/RECORD +37 -35
- modal_docs/gen_reference_docs.py +1 -0
- modal_proto/api.proto +25 -1
- modal_proto/api_pb2.py +758 -718
- modal_proto/api_pb2.pyi +95 -10
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +1 -1
- {modal-0.67.43.dist-info → modal-0.68.11.dist-info}/LICENSE +0 -0
- {modal-0.67.43.dist-info → modal-0.68.11.dist-info}/WHEEL +0 -0
- {modal-0.67.43.dist-info → modal-0.68.11.dist-info}/entry_points.txt +0 -0
- {modal-0.67.43.dist-info → modal-0.68.11.dist-info}/top_level.txt +0 -0
modal/_container_entrypoint.py
CHANGED
@@ -6,7 +6,7 @@ from modal._runtime.user_code_imports import Service, import_class_service, impo
|
|
6
6
|
|
7
7
|
telemetry_socket = os.environ.get("MODAL_TELEMETRY_SOCKET")
|
8
8
|
if telemetry_socket:
|
9
|
-
from
|
9
|
+
from ._runtime.telemetry import instrument_imports
|
10
10
|
|
11
11
|
instrument_imports(telemetry_socket)
|
12
12
|
|
@@ -415,6 +415,9 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
|
|
415
415
|
|
416
416
|
_client: _Client = synchronizer._translate_in(client) # TODO(erikbern): ugly
|
417
417
|
|
418
|
+
# Call ContainerHello - currently a noop but might be used later for things
|
419
|
+
container_io_manager.hello()
|
420
|
+
|
418
421
|
with container_io_manager.heartbeats(is_snapshotting_function), UserCodeEventLoop() as event_loop:
|
419
422
|
# If this is a serialized function, fetch the definition from the server
|
420
423
|
if function_def.definition_type == api_pb2.Function.DEFINITION_TYPE_SERIALIZED:
|
@@ -335,6 +335,9 @@ class _ContainerIOManager:
|
|
335
335
|
"""Only used for tests."""
|
336
336
|
cls._singleton = None
|
337
337
|
|
338
|
+
async def hello(self):
|
339
|
+
await self._client.stub.ContainerHello(Empty())
|
340
|
+
|
338
341
|
async def _run_heartbeat_loop(self):
|
339
342
|
while 1:
|
340
343
|
t0 = time.monotonic()
|
@@ -269,10 +269,12 @@ def import_single_function_service(
|
|
269
269
|
# The cls decorator is in global scope
|
270
270
|
_cls = synchronizer._translate_in(cls)
|
271
271
|
user_defined_callable = _cls._callables[fun_name]
|
272
|
-
function = _cls._method_functions.get(
|
272
|
+
function = _cls._method_functions.get(
|
273
|
+
fun_name
|
274
|
+
) # bound to the class service function - there is no instance
|
273
275
|
active_app = _cls._app
|
274
276
|
else:
|
275
|
-
# This is
|
277
|
+
# This is non-decorated class
|
276
278
|
user_defined_callable = getattr(cls, fun_name)
|
277
279
|
else:
|
278
280
|
raise InvalidError(f"Invalid function qualname {qual_name}")
|
modal/_traceback.py
CHANGED
@@ -1,16 +1,21 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
|
-
"""Helper functions related to operating on traceback objects.
|
2
|
+
"""Helper functions related to operating on exceptions, warnings, and traceback objects.
|
3
3
|
|
4
4
|
Functions related to *displaying* tracebacks should go in `modal/cli/_traceback.py`
|
5
5
|
so that Rich is not a dependency of the container Client.
|
6
6
|
"""
|
7
|
+
|
7
8
|
import re
|
8
9
|
import sys
|
9
10
|
import traceback
|
11
|
+
import warnings
|
10
12
|
from types import TracebackType
|
11
|
-
from typing import Any, Optional
|
13
|
+
from typing import Any, Iterable, Optional
|
14
|
+
|
15
|
+
from modal_proto import api_pb2
|
12
16
|
|
13
17
|
from ._vendor.tblib import Traceback as TBLibTraceback
|
18
|
+
from .exception import ServerWarning
|
14
19
|
|
15
20
|
TBDictType = dict[str, Any]
|
16
21
|
LineCacheType = dict[tuple[str, str], str]
|
@@ -109,3 +114,12 @@ def print_exception(exc: Optional[type[BaseException]], value: Optional[BaseExce
|
|
109
114
|
if sys.version_info < (3, 11) and value is not None:
|
110
115
|
notes = getattr(value, "__notes__", [])
|
111
116
|
print(*notes, sep="\n", file=sys.stderr)
|
117
|
+
|
118
|
+
|
119
|
+
def print_server_warnings(server_warnings: Iterable[api_pb2.Warning]):
|
120
|
+
"""Issue a warning originating from the server with empty metadata about local origin.
|
121
|
+
|
122
|
+
When using the Modal CLI, these warnings should get caught and coerced into Rich panels.
|
123
|
+
"""
|
124
|
+
for warning in server_warnings:
|
125
|
+
warnings.warn_explicit(warning.message, ServerWarning, "<modal-server>", 0)
|
modal/_utils/function_utils.py
CHANGED
@@ -99,7 +99,11 @@ def get_function_type(is_generator: Optional[bool]) -> "api_pb2.Function.Functio
|
|
99
99
|
|
100
100
|
|
101
101
|
class FunctionInfo:
|
102
|
-
"""Class that helps us extract a bunch of information about a function.
|
102
|
+
"""Class that helps us extract a bunch of information about a locally defined function.
|
103
|
+
|
104
|
+
Used for populating the definition of a remote function, and for making .local() calls
|
105
|
+
on a host with the local definition available.
|
106
|
+
"""
|
103
107
|
|
104
108
|
raw_f: Optional[Callable[..., Any]] # if None - this is a "class service function"
|
105
109
|
function_name: str
|
modal/_utils/grpc_testing.py
CHANGED
@@ -50,7 +50,7 @@ def patch_mock_servicer(cls):
|
|
50
50
|
|
51
51
|
@contextlib.contextmanager
|
52
52
|
def intercept(servicer):
|
53
|
-
ctx = InterceptionContext()
|
53
|
+
ctx = InterceptionContext(servicer)
|
54
54
|
servicer.interception_context = ctx
|
55
55
|
yield ctx
|
56
56
|
ctx._assert_responses_consumed()
|
@@ -101,7 +101,8 @@ class ResponseNotConsumed(Exception):
|
|
101
101
|
|
102
102
|
|
103
103
|
class InterceptionContext:
|
104
|
-
def __init__(self):
|
104
|
+
def __init__(self, servicer):
|
105
|
+
self._servicer = servicer
|
105
106
|
self.calls: list[tuple[str, Any]] = [] # List[Tuple[method_name, message]]
|
106
107
|
self.custom_responses: dict[str, list[tuple[Callable[[Any], bool], list[Any]]]] = defaultdict(list)
|
107
108
|
self.custom_defaults: dict[str, Callable[["MockClientServicer", grpclib.server.Stream], Awaitable[None]]] = {}
|
@@ -149,6 +150,9 @@ class InterceptionContext:
|
|
149
150
|
raise KeyError(f"No message of that type in call list: {self.calls}")
|
150
151
|
|
151
152
|
def get_requests(self, method_name: str) -> list[Any]:
|
153
|
+
if not hasattr(self._servicer, method_name):
|
154
|
+
# we check this to prevent things like `assert ctx.get_requests("ASdfFunctionCreate") == 0` passing
|
155
|
+
raise ValueError(f"{method_name} not in MockServicer - did you spell it right?")
|
152
156
|
return [msg for _method_name, msg in self.calls if _method_name == method_name]
|
153
157
|
|
154
158
|
def _add_recv(self, method_name: str, msg):
|
modal/_utils/hash_utils.py
CHANGED
@@ -2,9 +2,12 @@
|
|
2
2
|
import base64
|
3
3
|
import dataclasses
|
4
4
|
import hashlib
|
5
|
+
import time
|
5
6
|
from typing import BinaryIO, Callable, Union
|
6
7
|
|
7
|
-
|
8
|
+
from modal.config import logger
|
9
|
+
|
10
|
+
HASH_CHUNK_SIZE = 65536
|
8
11
|
|
9
12
|
|
10
13
|
def _update(hashers: list[Callable[[bytes], None]], data: Union[bytes, BinaryIO]) -> None:
|
@@ -26,20 +29,26 @@ def _update(hashers: list[Callable[[bytes], None]], data: Union[bytes, BinaryIO]
|
|
26
29
|
|
27
30
|
|
28
31
|
def get_sha256_hex(data: Union[bytes, BinaryIO]) -> str:
|
32
|
+
t0 = time.monotonic()
|
29
33
|
hasher = hashlib.sha256()
|
30
34
|
_update([hasher.update], data)
|
35
|
+
logger.debug("get_sha256_hex took %.3fs", time.monotonic() - t0)
|
31
36
|
return hasher.hexdigest()
|
32
37
|
|
33
38
|
|
34
39
|
def get_sha256_base64(data: Union[bytes, BinaryIO]) -> str:
|
40
|
+
t0 = time.monotonic()
|
35
41
|
hasher = hashlib.sha256()
|
36
42
|
_update([hasher.update], data)
|
43
|
+
logger.debug("get_sha256_base64 took %.3fs", time.monotonic() - t0)
|
37
44
|
return base64.b64encode(hasher.digest()).decode("ascii")
|
38
45
|
|
39
46
|
|
40
47
|
def get_md5_base64(data: Union[bytes, BinaryIO]) -> str:
|
48
|
+
t0 = time.monotonic()
|
41
49
|
hasher = hashlib.md5()
|
42
50
|
_update([hasher.update], data)
|
51
|
+
logger.debug("get_md5_base64 took %.3fs", time.monotonic() - t0)
|
43
52
|
return base64.b64encode(hasher.digest()).decode("utf-8")
|
44
53
|
|
45
54
|
|
@@ -50,10 +59,13 @@ class UploadHashes:
|
|
50
59
|
|
51
60
|
|
52
61
|
def get_upload_hashes(data: Union[bytes, BinaryIO]) -> UploadHashes:
|
62
|
+
t0 = time.monotonic()
|
53
63
|
md5 = hashlib.md5()
|
54
64
|
sha256 = hashlib.sha256()
|
55
65
|
_update([md5.update, sha256.update], data)
|
56
|
-
|
66
|
+
hashes = UploadHashes(
|
57
67
|
md5_base64=base64.b64encode(md5.digest()).decode("ascii"),
|
58
68
|
sha256_base64=base64.b64encode(sha256.digest()).decode("ascii"),
|
59
69
|
)
|
70
|
+
logger.debug("get_upload_hashes took %.3fs", time.monotonic() - t0)
|
71
|
+
return hashes
|
modal/cli/_traceback.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# Copyright Modal Labs 2024
|
2
2
|
"""Helper functions related to displaying tracebacks in the CLI."""
|
3
|
+
|
3
4
|
import functools
|
4
5
|
import re
|
5
6
|
import warnings
|
@@ -11,7 +12,7 @@ from rich.syntax import Syntax
|
|
11
12
|
from rich.text import Text
|
12
13
|
from rich.traceback import PathHighlighter, Stack, Traceback, install
|
13
14
|
|
14
|
-
from ..exception import DeprecationError, PendingDeprecationError
|
15
|
+
from ..exception import DeprecationError, PendingDeprecationError, ServerWarning
|
15
16
|
|
16
17
|
|
17
18
|
@group()
|
@@ -165,7 +166,7 @@ def highlight_modal_deprecation_warnings() -> None:
|
|
165
166
|
base_showwarning = warnings.showwarning
|
166
167
|
|
167
168
|
def showwarning(warning, category, filename, lineno, file=None, line=None):
|
168
|
-
if issubclass(category, (DeprecationError, PendingDeprecationError)):
|
169
|
+
if issubclass(category, (DeprecationError, PendingDeprecationError, ServerWarning)):
|
169
170
|
content = str(warning)
|
170
171
|
if re.match(r"^\d{4}-\d{2}-\d{2}", content):
|
171
172
|
date = content[:10]
|
@@ -180,10 +181,16 @@ def highlight_modal_deprecation_warnings() -> None:
|
|
180
181
|
except OSError:
|
181
182
|
# e.g., when filename is "<unknown>"; raises FileNotFoundError on posix but OSError on windows
|
182
183
|
pass
|
184
|
+
if issubclass(category, ServerWarning):
|
185
|
+
title = "Modal Warning"
|
186
|
+
else:
|
187
|
+
title = "Modal Deprecation Warning"
|
188
|
+
if date:
|
189
|
+
title += f" ({date})"
|
183
190
|
panel = Panel(
|
184
191
|
message,
|
185
|
-
|
186
|
-
title=
|
192
|
+
border_style="yellow",
|
193
|
+
title=title,
|
187
194
|
title_align="left",
|
188
195
|
)
|
189
196
|
Console().print(panel)
|
modal/cli/run.py
CHANGED
@@ -13,8 +13,6 @@ from typing import Any, Callable, Optional, get_type_hints
|
|
13
13
|
|
14
14
|
import click
|
15
15
|
import typer
|
16
|
-
from rich.console import Console
|
17
|
-
from rich.panel import Panel
|
18
16
|
from typing_extensions import TypedDict
|
19
17
|
|
20
18
|
from .. import Cls
|
@@ -308,11 +306,6 @@ def deploy(
|
|
308
306
|
|
309
307
|
with enable_output():
|
310
308
|
res = deploy_app(app, name=name, environment_name=env or "", tag=tag)
|
311
|
-
if res.warnings:
|
312
|
-
console = Console()
|
313
|
-
for warning in res.warnings:
|
314
|
-
panel = Panel(warning, title="Warning", title_align="left", border_style="yellow")
|
315
|
-
console.print(panel, highlight=False)
|
316
309
|
|
317
310
|
if stream_logs:
|
318
311
|
stream_app_logs(app_id=res.app_id, app_logs_url=res.app_logs_url)
|
modal/client.py
CHANGED
@@ -16,23 +16,21 @@ from typing import (
|
|
16
16
|
import grpclib.client
|
17
17
|
from google.protobuf import empty_pb2
|
18
18
|
from google.protobuf.message import Message
|
19
|
-
from grpclib import GRPCError, Status
|
20
19
|
from synchronicity.async_wrap import asynccontextmanager
|
21
20
|
|
22
21
|
from modal._utils.async_utils import synchronizer
|
23
22
|
from modal_proto import api_grpc, api_pb2, modal_api_grpc
|
24
23
|
from modal_version import __version__
|
25
24
|
|
25
|
+
from ._traceback import print_server_warnings
|
26
26
|
from ._utils import async_utils
|
27
27
|
from ._utils.async_utils import TaskContext, synchronize_api
|
28
28
|
from ._utils.grpc_utils import connect_channel, create_channel, retry_transient_errors
|
29
29
|
from .config import _check_config, _is_remote, config, logger
|
30
|
-
from .exception import AuthError, ClientClosed, ConnectionError
|
30
|
+
from .exception import AuthError, ClientClosed, ConnectionError
|
31
31
|
|
32
32
|
HEARTBEAT_INTERVAL: float = config.get("heartbeat_interval")
|
33
33
|
HEARTBEAT_TIMEOUT: float = HEARTBEAT_INTERVAL + 0.1
|
34
|
-
CLIENT_CREATE_ATTEMPT_TIMEOUT: float = 4.0
|
35
|
-
CLIENT_CREATE_TOTAL_TIMEOUT: float = 15.0
|
36
34
|
|
37
35
|
|
38
36
|
def _get_metadata(client_type: int, credentials: Optional[tuple[str, str]], version: str) -> dict[str, str]:
|
@@ -137,32 +135,11 @@ class _Client:
|
|
137
135
|
async def hello(self):
|
138
136
|
"""Connect to server and retrieve version information; raise appropriate error for various failures."""
|
139
137
|
logger.debug(f"Client ({id(self)}): Starting")
|
140
|
-
|
141
|
-
|
142
|
-
resp = await retry_transient_errors(
|
143
|
-
self.stub.ClientHello,
|
144
|
-
req,
|
145
|
-
attempt_timeout=CLIENT_CREATE_ATTEMPT_TIMEOUT,
|
146
|
-
total_timeout=CLIENT_CREATE_TOTAL_TIMEOUT,
|
147
|
-
)
|
148
|
-
if resp.warning:
|
149
|
-
ALARM_EMOJI = chr(0x1F6A8)
|
150
|
-
warnings.warn_explicit(f"{ALARM_EMOJI} {resp.warning} {ALARM_EMOJI}", DeprecationError, "<unknown>", 0)
|
151
|
-
except GRPCError as exc:
|
152
|
-
if exc.status == Status.FAILED_PRECONDITION:
|
153
|
-
raise VersionError(
|
154
|
-
f"The client version ({self.version}) is too old. Please update (pip install --upgrade modal)."
|
155
|
-
)
|
156
|
-
else:
|
157
|
-
raise exc
|
138
|
+
resp = await retry_transient_errors(self.stub.ClientHello, empty_pb2.Empty())
|
139
|
+
print_server_warnings(resp.server_warnings)
|
158
140
|
|
159
141
|
async def __aenter__(self):
|
160
142
|
await self._open()
|
161
|
-
try:
|
162
|
-
await self.hello()
|
163
|
-
except BaseException:
|
164
|
-
await self._close()
|
165
|
-
raise
|
166
143
|
return self
|
167
144
|
|
168
145
|
async def __aexit__(self, exc_type, exc, tb):
|
@@ -178,7 +155,6 @@ class _Client:
|
|
178
155
|
client = cls(server_url, api_pb2.CLIENT_TYPE_CLIENT, credentials=None)
|
179
156
|
try:
|
180
157
|
await client._open()
|
181
|
-
# Skip client.hello
|
182
158
|
yield client
|
183
159
|
finally:
|
184
160
|
await client._close()
|
@@ -229,7 +205,6 @@ class _Client:
|
|
229
205
|
client = _Client(server_url, client_type, credentials)
|
230
206
|
await client._open()
|
231
207
|
async_utils.on_shutdown(client._close())
|
232
|
-
await client.hello()
|
233
208
|
cls._client_from_env = client
|
234
209
|
return client
|
235
210
|
|
@@ -252,11 +227,6 @@ class _Client:
|
|
252
227
|
credentials = (token_id, token_secret)
|
253
228
|
client = _Client(server_url, client_type, credentials)
|
254
229
|
await client._open()
|
255
|
-
try:
|
256
|
-
await client.hello()
|
257
|
-
except BaseException:
|
258
|
-
await client._close()
|
259
|
-
raise
|
260
230
|
async_utils.on_shutdown(client._close())
|
261
231
|
return client
|
262
232
|
|
@@ -265,8 +235,8 @@ class _Client:
|
|
265
235
|
"""mdmd:hidden
|
266
236
|
Check whether can the client can connect to this server with these credentials; raise if not.
|
267
237
|
"""
|
268
|
-
async with cls(server_url, api_pb2.CLIENT_TYPE_CLIENT, credentials):
|
269
|
-
|
238
|
+
async with cls(server_url, api_pb2.CLIENT_TYPE_CLIENT, credentials) as client:
|
239
|
+
client.hello() # Will call ClientHello RPC and possibly raise AuthError or ConnectionError
|
270
240
|
|
271
241
|
@classmethod
|
272
242
|
def set_env_client(cls, client: Optional["_Client"]):
|
@@ -316,7 +286,6 @@ class _Client:
|
|
316
286
|
self.set_env_client(None)
|
317
287
|
# TODO(elias): reset _cancellation_context in case ?
|
318
288
|
await self._open()
|
319
|
-
# intentionally not doing self.hello since we should already be authenticated etc.
|
320
289
|
|
321
290
|
async def _get_grpclib_method(self, method_name: str) -> Any:
|
322
291
|
# safely get grcplib method that is bound to a valid channel
|
modal/client.pyi
CHANGED
@@ -26,7 +26,7 @@ class _Client:
|
|
26
26
|
_stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
|
27
27
|
|
28
28
|
def __init__(
|
29
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.
|
29
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.11"
|
30
30
|
): ...
|
31
31
|
def is_closed(self) -> bool: ...
|
32
32
|
@property
|
@@ -81,7 +81,7 @@ class Client:
|
|
81
81
|
_stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
|
82
82
|
|
83
83
|
def __init__(
|
84
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.
|
84
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.11"
|
85
85
|
): ...
|
86
86
|
def is_closed(self) -> bool: ...
|
87
87
|
@property
|
@@ -194,7 +194,3 @@ class UnaryStreamWrapper(typing.Generic[RequestType, ResponseType]):
|
|
194
194
|
HEARTBEAT_INTERVAL: float
|
195
195
|
|
196
196
|
HEARTBEAT_TIMEOUT: float
|
197
|
-
|
198
|
-
CLIENT_CREATE_ATTEMPT_TIMEOUT: float
|
199
|
-
|
200
|
-
CLIENT_CREATE_TOTAL_TIMEOUT: float
|