modal 0.67.42__py3-none-any.whl → 0.68.4__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 +3 -0
- modal/_runtime/container_io_manager.py +3 -0
- modal/_traceback.py +16 -2
- modal/cli/_traceback.py +11 -4
- modal/cli/container.py +16 -5
- modal/cli/run.py +24 -24
- modal/cli/utils.py +4 -0
- modal/client.py +6 -37
- modal/client.pyi +2 -6
- modal/cls.py +4 -4
- modal/container_process.py +10 -3
- modal/container_process.pyi +3 -3
- modal/exception.py +20 -0
- modal/file_io.py +380 -0
- modal/file_io.pyi +185 -0
- modal/functions.py +3 -0
- modal/functions.pyi +6 -6
- modal/partial_function.py +14 -10
- modal/partial_function.pyi +2 -2
- modal/runner.py +19 -7
- modal/runner.pyi +11 -4
- modal/sandbox.py +50 -3
- modal/sandbox.pyi +18 -0
- {modal-0.67.42.dist-info → modal-0.68.4.dist-info}/METADATA +2 -2
- {modal-0.67.42.dist-info → modal-0.68.4.dist-info}/RECORD +35 -33
- modal_docs/gen_reference_docs.py +1 -0
- modal_proto/api.proto +18 -1
- modal_proto/api_pb2.py +748 -718
- modal_proto/api_pb2.pyi +69 -10
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +1 -1
- {modal-0.67.42.dist-info → modal-0.68.4.dist-info}/LICENSE +0 -0
- {modal-0.67.42.dist-info → modal-0.68.4.dist-info}/WHEEL +0 -0
- {modal-0.67.42.dist-info → modal-0.68.4.dist-info}/entry_points.txt +0 -0
- {modal-0.67.42.dist-info → modal-0.68.4.dist-info}/top_level.txt +0 -0
modal/_container_entrypoint.py
CHANGED
@@ -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()
|
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/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/container.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
|
-
|
3
2
|
from typing import Optional, Union
|
4
3
|
|
5
4
|
import typer
|
@@ -8,12 +7,13 @@ from rich.text import Text
|
|
8
7
|
from modal._pty import get_pty_info
|
9
8
|
from modal._utils.async_utils import synchronizer
|
10
9
|
from modal._utils.grpc_utils import retry_transient_errors
|
11
|
-
from modal.cli.utils import ENV_OPTION, display_table, stream_app_logs, timestamp_to_local
|
10
|
+
from modal.cli.utils import ENV_OPTION, display_table, is_tty, stream_app_logs, timestamp_to_local
|
12
11
|
from modal.client import _Client
|
13
12
|
from modal.config import config
|
14
13
|
from modal.container_process import _ContainerProcess
|
15
14
|
from modal.environments import ensure_env
|
16
15
|
from modal.object import _get_environment_name
|
16
|
+
from modal.stream_type import StreamType
|
17
17
|
from modal_proto import api_pb2
|
18
18
|
|
19
19
|
container_cli = typer.Typer(name="container", help="Manage and connect to running containers.", no_args_is_help=True)
|
@@ -55,12 +55,19 @@ def logs(container_id: str = typer.Argument(help="Container ID")):
|
|
55
55
|
@container_cli.command("exec")
|
56
56
|
@synchronizer.create_blocking
|
57
57
|
async def exec(
|
58
|
+
pty: Optional[bool] = typer.Option(default=None, help="Run the command using a PTY."),
|
58
59
|
container_id: str = typer.Argument(help="Container ID"),
|
59
|
-
command: list[str] = typer.Argument(
|
60
|
-
|
60
|
+
command: list[str] = typer.Argument(
|
61
|
+
help="A command to run inside the container.\n\n"
|
62
|
+
"To pass command-line flags or options, add `--` before the start of your commands. "
|
63
|
+
"For example: `modal container exec <id> -- /bin/bash -c 'echo hi'`"
|
64
|
+
),
|
61
65
|
):
|
62
66
|
"""Execute a command in a container."""
|
63
67
|
|
68
|
+
if pty is None:
|
69
|
+
pty = is_tty()
|
70
|
+
|
64
71
|
client = await _Client.from_env()
|
65
72
|
|
66
73
|
req = api_pb2.ContainerExecRequest(
|
@@ -71,7 +78,11 @@ async def exec(
|
|
71
78
|
)
|
72
79
|
res: api_pb2.ContainerExecResponse = await client.stub.ContainerExec(req)
|
73
80
|
|
74
|
-
|
81
|
+
if pty:
|
82
|
+
await _ContainerProcess(res.exec_id, client).attach()
|
83
|
+
else:
|
84
|
+
# TODO: redirect stderr to its own stream?
|
85
|
+
await _ContainerProcess(res.exec_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT).wait()
|
75
86
|
|
76
87
|
|
77
88
|
@container_cli.command("stop")
|
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
|
@@ -29,7 +27,7 @@ from ..runner import deploy_app, interactive_shell, run_app
|
|
29
27
|
from ..serving import serve_app
|
30
28
|
from ..volume import Volume
|
31
29
|
from .import_refs import import_app, import_function
|
32
|
-
from .utils import ENV_OPTION, ENV_OPTION_HELP, stream_app_logs
|
30
|
+
from .utils import ENV_OPTION, ENV_OPTION_HELP, is_tty, stream_app_logs
|
33
31
|
|
34
32
|
|
35
33
|
class ParameterMetadata(TypedDict):
|
@@ -306,14 +304,7 @@ def deploy(
|
|
306
304
|
if name is None:
|
307
305
|
name = app.name
|
308
306
|
|
309
|
-
|
310
|
-
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
|
-
|
307
|
+
res = deploy_app(app, name=name, environment_name=env or "", tag=tag)
|
317
308
|
if stream_logs:
|
318
309
|
stream_app_logs(app_id=res.app_id, app_logs_url=res.app_logs_url)
|
319
310
|
|
@@ -392,40 +383,47 @@ def shell(
|
|
392
383
|
"Can be a single region or a comma-separated list to choose from (if not using REF)."
|
393
384
|
),
|
394
385
|
),
|
386
|
+
pty: Optional[bool] = typer.Option(default=None, help="Run the command using a PTY."),
|
395
387
|
):
|
396
|
-
"""Run
|
388
|
+
"""Run a command or interactive shell inside a Modal container.
|
397
389
|
|
398
|
-
**Examples:**
|
390
|
+
\b**Examples:**
|
399
391
|
|
400
|
-
|
392
|
+
\bStart an interactive shell inside the default Debian-based image:
|
401
393
|
|
402
|
-
```
|
394
|
+
\b```
|
403
395
|
modal shell
|
404
396
|
```
|
405
397
|
|
406
|
-
|
398
|
+
\bStart an interactive shell with the spec for `my_function` in your App
|
399
|
+
(uses the same image, volumes, mounts, etc.):
|
407
400
|
|
408
|
-
```
|
401
|
+
\b```
|
409
402
|
modal shell hello_world.py::my_function
|
410
403
|
```
|
411
404
|
|
412
|
-
|
405
|
+
\bOr, if you're using a [modal.Cls](/docs/reference/modal.Cls), you can refer to a `@modal.method` directly:
|
413
406
|
|
414
|
-
```
|
407
|
+
\b```
|
415
408
|
modal shell hello_world.py::MyClass.my_method
|
416
409
|
```
|
417
410
|
|
418
411
|
Start a `python` shell:
|
419
412
|
|
420
|
-
```
|
413
|
+
\b```
|
421
414
|
modal shell hello_world.py --cmd=python
|
422
415
|
```
|
416
|
+
|
417
|
+
\bRun a command with your function's spec and pipe the output to a file:
|
418
|
+
|
419
|
+
\b```
|
420
|
+
modal shell hello_world.py -c 'uv pip list' > env.txt
|
421
|
+
```
|
423
422
|
"""
|
424
423
|
env = ensure_env(env)
|
425
424
|
|
426
|
-
|
427
|
-
|
428
|
-
raise click.UsageError("`modal shell` can only be run from a terminal.")
|
425
|
+
if pty is None:
|
426
|
+
pty = is_tty()
|
429
427
|
|
430
428
|
if platform.system() == "Windows":
|
431
429
|
raise InvalidError("`modal shell` is currently not supported on Windows")
|
@@ -441,7 +439,7 @@ def shell(
|
|
441
439
|
):
|
442
440
|
from .container import exec
|
443
441
|
|
444
|
-
exec(container_id=container_or_function, command=shlex.split(cmd)
|
442
|
+
exec(container_id=container_or_function, command=shlex.split(cmd))
|
445
443
|
return
|
446
444
|
|
447
445
|
function = import_function(
|
@@ -461,6 +459,7 @@ def shell(
|
|
461
459
|
memory=function_spec.memory,
|
462
460
|
volumes=function_spec.volumes,
|
463
461
|
region=function_spec.scheduler_placement.proto.regions if function_spec.scheduler_placement else None,
|
462
|
+
pty=pty,
|
464
463
|
)
|
465
464
|
else:
|
466
465
|
modal_image = Image.from_registry(image, add_python=add_python) if image else None
|
@@ -474,6 +473,7 @@ def shell(
|
|
474
473
|
cloud=cloud,
|
475
474
|
volumes=volumes,
|
476
475
|
region=region.split(",") if region else [],
|
476
|
+
pty=pty,
|
477
477
|
)
|
478
478
|
|
479
479
|
# NB: invoking under bash makes --cmd a lot more flexible.
|
modal/cli/utils.py
CHANGED
@@ -77,6 +77,10 @@ def _plain(text: Union[Text, str]) -> str:
|
|
77
77
|
return text.plain if isinstance(text, Text) else text
|
78
78
|
|
79
79
|
|
80
|
+
def is_tty() -> bool:
|
81
|
+
return Console().is_terminal
|
82
|
+
|
83
|
+
|
80
84
|
def display_table(
|
81
85
|
columns: Sequence[Union[Column, str]],
|
82
86
|
rows: Sequence[Sequence[Union[Text, str]]],
|
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.4"
|
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.4"
|
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
|
modal/cls.py
CHANGED
@@ -14,15 +14,13 @@ from modal_proto import api_pb2
|
|
14
14
|
from ._resolver import Resolver
|
15
15
|
from ._resources import convert_fn_config_to_resources_config
|
16
16
|
from ._serialization import check_valid_cls_constructor_arg
|
17
|
+
from ._traceback import print_server_warnings
|
17
18
|
from ._utils.async_utils import synchronize_api, synchronizer
|
18
19
|
from ._utils.grpc_utils import retry_transient_errors
|
19
20
|
from ._utils.mount_utils import validate_volumes
|
20
21
|
from .client import _Client
|
21
22
|
from .exception import InvalidError, NotFoundError, VersionError
|
22
|
-
from .functions import
|
23
|
-
_Function,
|
24
|
-
_parse_retries,
|
25
|
-
)
|
23
|
+
from .functions import _Function, _parse_retries
|
26
24
|
from .gpu import GPU_T
|
27
25
|
from .object import _get_environment_name, _Object
|
28
26
|
from .partial_function import (
|
@@ -486,6 +484,8 @@ class _Cls(_Object, type_prefix="cs"):
|
|
486
484
|
else:
|
487
485
|
raise
|
488
486
|
|
487
|
+
print_server_warnings(response.server_warnings)
|
488
|
+
|
489
489
|
class_function_tag = f"{tag}.*" # special name of the base service function for the class
|
490
490
|
|
491
491
|
class_service_function = _Function.from_name(
|
modal/container_process.py
CHANGED
@@ -9,7 +9,7 @@ from ._utils.async_utils import TaskContext, synchronize_api
|
|
9
9
|
from ._utils.grpc_utils import retry_transient_errors
|
10
10
|
from ._utils.shell_utils import stream_from_stdin, write_to_fd
|
11
11
|
from .client import _Client
|
12
|
-
from .exception import InteractiveTimeoutError, InvalidError
|
12
|
+
from .exception import InteractiveTimeoutError, InvalidError, deprecation_error
|
13
13
|
from .io_streams import _StreamReader, _StreamWriter
|
14
14
|
from .stream_type import StreamType
|
15
15
|
|
@@ -114,11 +114,18 @@ class _ContainerProcess(Generic[T]):
|
|
114
114
|
self._returncode = resp.exit_code
|
115
115
|
return self._returncode
|
116
116
|
|
117
|
-
async def attach(self, *, pty: bool):
|
117
|
+
async def attach(self, *, pty: Optional[bool] = None):
|
118
118
|
if platform.system() == "Windows":
|
119
119
|
print("interactive exec is not currently supported on Windows.")
|
120
120
|
return
|
121
121
|
|
122
|
+
if pty is not None:
|
123
|
+
deprecation_error(
|
124
|
+
(2024, 12, 9),
|
125
|
+
"The `pty` argument to `modal.container_process.attach(pty=...)` is deprecated, "
|
126
|
+
"as only PTY mode is supported. Please remove the argument.",
|
127
|
+
)
|
128
|
+
|
122
129
|
from rich.console import Console
|
123
130
|
|
124
131
|
console = Console()
|
@@ -151,7 +158,7 @@ class _ContainerProcess(Generic[T]):
|
|
151
158
|
# time out if we can't connect to the server fast enough
|
152
159
|
await asyncio.wait_for(on_connect.wait(), timeout=60)
|
153
160
|
|
154
|
-
async with stream_from_stdin(_handle_input, use_raw_terminal=
|
161
|
+
async with stream_from_stdin(_handle_input, use_raw_terminal=True):
|
155
162
|
await stdout_task
|
156
163
|
await stderr_task
|
157
164
|
|
modal/container_process.pyi
CHANGED
@@ -34,7 +34,7 @@ class _ContainerProcess(typing.Generic[T]):
|
|
34
34
|
def returncode(self) -> int: ...
|
35
35
|
async def poll(self) -> typing.Optional[int]: ...
|
36
36
|
async def wait(self) -> int: ...
|
37
|
-
async def attach(self, *, pty: bool): ...
|
37
|
+
async def attach(self, *, pty: typing.Optional[bool] = None): ...
|
38
38
|
|
39
39
|
class ContainerProcess(typing.Generic[T]):
|
40
40
|
_process_id: typing.Optional[str]
|
@@ -76,7 +76,7 @@ class ContainerProcess(typing.Generic[T]):
|
|
76
76
|
wait: __wait_spec
|
77
77
|
|
78
78
|
class __attach_spec(typing_extensions.Protocol):
|
79
|
-
def __call__(self, *, pty: bool): ...
|
80
|
-
async def aio(self, *, pty: bool): ...
|
79
|
+
def __call__(self, *, pty: typing.Optional[bool] = None): ...
|
80
|
+
async def aio(self, *, pty: typing.Optional[bool] = None): ...
|
81
81
|
|
82
82
|
attach: __attach_spec
|
modal/exception.py
CHANGED
@@ -4,6 +4,9 @@ import signal
|
|
4
4
|
import sys
|
5
5
|
import warnings
|
6
6
|
from datetime import date
|
7
|
+
from typing import Iterable
|
8
|
+
|
9
|
+
from modal_proto import api_pb2
|
7
10
|
|
8
11
|
|
9
12
|
class Error(Exception):
|
@@ -107,6 +110,10 @@ class PendingDeprecationError(UserWarning):
|
|
107
110
|
"""Soon to be deprecated feature. Only used intermittently because of multi-repo concerns."""
|
108
111
|
|
109
112
|
|
113
|
+
class ServerWarning(UserWarning):
|
114
|
+
"""Warning originating from the Modal server and re-issued in client code."""
|
115
|
+
|
116
|
+
|
110
117
|
class _CliUserExecutionError(Exception):
|
111
118
|
"""mdmd:hidden
|
112
119
|
Private wrapper for exceptions during when importing or running stubs from the CLI.
|
@@ -213,3 +220,16 @@ class ModuleNotMountable(Exception):
|
|
213
220
|
|
214
221
|
class ClientClosed(Error):
|
215
222
|
pass
|
223
|
+
|
224
|
+
|
225
|
+
class FilesystemExecutionError(Error):
|
226
|
+
"""Raised when an unknown error is thrown during a container filesystem operation."""
|
227
|
+
|
228
|
+
|
229
|
+
def print_server_warnings(server_warnings: Iterable[api_pb2.Warning]):
|
230
|
+
# TODO(erikbern): move this to modal._utils.deprecation
|
231
|
+
for warning in server_warnings:
|
232
|
+
if warning.type == api_pb2.Warning.WARNING_TYPE_CLIENT_DEPRECATION:
|
233
|
+
warnings.warn_explicit(warning.message, DeprecationError, "<unknown>", 0)
|
234
|
+
else:
|
235
|
+
warnings.warn_explicit(warning.message, UserWarning, "<unknown>", 0)
|