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.
@@ -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 runtime._telemetry import instrument_imports
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(fun_name)
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 a raw class
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)
@@ -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
@@ -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):
@@ -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
- HASH_CHUNK_SIZE = 4096
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
- return UploadHashes(
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
- style="yellow",
186
- title=f"Modal Deprecation Warning ({date})" if date else "Modal Deprecation Warning",
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, DeprecationError, VersionError
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
- try:
141
- req = empty_pb2.Empty()
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
- pass # Will call ClientHello RPC and possibly raise AuthError or ConnectionError
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.67.43"
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.67.43"
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