modal 0.67.33__py3-none-any.whl → 0.67.43__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/_runtime/container_io_manager.py +2 -2
- modal/cli/_traceback.py +8 -3
- modal/cli/app.py +1 -1
- modal/cli/container.py +16 -5
- modal/cli/network_file_system.py +23 -2
- modal/cli/run.py +23 -14
- modal/cli/utils.py +4 -0
- modal/client.py +1 -1
- modal/client.pyi +2 -2
- modal/container_process.py +10 -3
- modal/container_process.pyi +3 -3
- modal/functions.py +2 -2
- modal/functions.pyi +4 -4
- modal/io_streams.py +1 -1
- modal/network_file_system.py +6 -0
- modal/network_file_system.pyi +20 -0
- modal/queue.py +33 -27
- modal/runner.py +14 -3
- modal/runner.pyi +9 -3
- modal/sandbox.py +46 -14
- modal/sandbox.pyi +10 -3
- {modal-0.67.33.dist-info → modal-0.67.43.dist-info}/METADATA +1 -1
- {modal-0.67.33.dist-info → modal-0.67.43.dist-info}/RECORD +35 -35
- modal_proto/api.proto +26 -1
- modal_proto/api_grpc.py +16 -0
- modal_proto/api_pb2.py +743 -710
- modal_proto/api_pb2.pyi +88 -4
- modal_proto/api_pb2_grpc.py +34 -1
- modal_proto/api_pb2_grpc.pyi +13 -3
- modal_proto/modal_api_grpc.py +1 -0
- modal_version/_version_generated.py +1 -1
- {modal-0.67.33.dist-info → modal-0.67.43.dist-info}/LICENSE +0 -0
- {modal-0.67.33.dist-info → modal-0.67.43.dist-info}/WHEEL +0 -0
- {modal-0.67.33.dist-info → modal-0.67.43.dist-info}/entry_points.txt +0 -0
- {modal-0.67.33.dist-info → modal-0.67.43.dist-info}/top_level.txt +0 -0
@@ -908,7 +908,7 @@ class _ContainerIOManager:
|
|
908
908
|
if self.checkpoint_id:
|
909
909
|
logger.debug(f"Checkpoint ID: {self.checkpoint_id} (Memory Snapshot ID)")
|
910
910
|
else:
|
911
|
-
|
911
|
+
raise ValueError("No checkpoint ID provided for memory snapshot")
|
912
912
|
|
913
913
|
# Pause heartbeats since they keep the client connection open which causes the snapshotter to crash
|
914
914
|
async with self.heartbeat_condition:
|
@@ -918,7 +918,7 @@ class _ContainerIOManager:
|
|
918
918
|
self.heartbeat_condition.notify_all()
|
919
919
|
|
920
920
|
await self._client.stub.ContainerCheckpoint(
|
921
|
-
api_pb2.ContainerCheckpointRequest(checkpoint_id=self.checkpoint_id
|
921
|
+
api_pb2.ContainerCheckpointRequest(checkpoint_id=self.checkpoint_id)
|
922
922
|
)
|
923
923
|
|
924
924
|
await self._client._close(prep_for_restore=True)
|
modal/cli/_traceback.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Copyright Modal Labs 2024
|
2
2
|
"""Helper functions related to displaying tracebacks in the CLI."""
|
3
3
|
import functools
|
4
|
+
import re
|
4
5
|
import warnings
|
5
6
|
from typing import Optional
|
6
7
|
|
@@ -166,8 +167,12 @@ def highlight_modal_deprecation_warnings() -> None:
|
|
166
167
|
def showwarning(warning, category, filename, lineno, file=None, line=None):
|
167
168
|
if issubclass(category, (DeprecationError, PendingDeprecationError)):
|
168
169
|
content = str(warning)
|
169
|
-
|
170
|
-
|
170
|
+
if re.match(r"^\d{4}-\d{2}-\d{2}", content):
|
171
|
+
date = content[:10]
|
172
|
+
message = content[11:].strip()
|
173
|
+
else:
|
174
|
+
date = ""
|
175
|
+
message = content
|
171
176
|
try:
|
172
177
|
with open(filename, encoding="utf-8", errors="replace") as code_file:
|
173
178
|
source = code_file.readlines()[lineno - 1].strip()
|
@@ -178,7 +183,7 @@ def highlight_modal_deprecation_warnings() -> None:
|
|
178
183
|
panel = Panel(
|
179
184
|
message,
|
180
185
|
style="yellow",
|
181
|
-
title=f"Modal Deprecation Warning ({date})",
|
186
|
+
title=f"Modal Deprecation Warning ({date})" if date else "Modal Deprecation Warning",
|
182
187
|
title_align="left",
|
183
188
|
)
|
184
189
|
Console().print(panel)
|
modal/cli/app.py
CHANGED
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/network_file_system.py
CHANGED
@@ -10,7 +10,7 @@ from grpclib import GRPCError, Status
|
|
10
10
|
from rich.console import Console
|
11
11
|
from rich.syntax import Syntax
|
12
12
|
from rich.table import Table
|
13
|
-
from typer import Typer
|
13
|
+
from typer import Argument, Typer
|
14
14
|
|
15
15
|
import modal
|
16
16
|
from modal._location import display_location
|
@@ -18,7 +18,7 @@ from modal._output import OutputManager, ProgressHandler
|
|
18
18
|
from modal._utils.async_utils import synchronizer
|
19
19
|
from modal._utils.grpc_utils import retry_transient_errors
|
20
20
|
from modal.cli._download import _volume_download
|
21
|
-
from modal.cli.utils import ENV_OPTION, display_table, timestamp_to_local
|
21
|
+
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table, timestamp_to_local
|
22
22
|
from modal.client import _Client
|
23
23
|
from modal.environments import ensure_env
|
24
24
|
from modal.network_file_system import _NetworkFileSystem
|
@@ -217,3 +217,24 @@ async def rm(
|
|
217
217
|
if exc.status in (Status.NOT_FOUND, Status.INVALID_ARGUMENT):
|
218
218
|
raise UsageError(exc.message)
|
219
219
|
raise
|
220
|
+
|
221
|
+
|
222
|
+
@nfs_cli.command(
|
223
|
+
name="delete",
|
224
|
+
help="Delete a named, persistent modal.NetworkFileSystem.",
|
225
|
+
rich_help_panel="Management",
|
226
|
+
)
|
227
|
+
@synchronizer.create_blocking
|
228
|
+
async def delete(
|
229
|
+
nfs_name: str = Argument(help="Name of the modal.NetworkFileSystem to be deleted. Case sensitive"),
|
230
|
+
yes: bool = YES_OPTION,
|
231
|
+
env: Optional[str] = ENV_OPTION,
|
232
|
+
):
|
233
|
+
if not yes:
|
234
|
+
typer.confirm(
|
235
|
+
f"Are you sure you want to irrevocably delete the modal.NetworkFileSystem '{nfs_name}'?",
|
236
|
+
default=False,
|
237
|
+
abort=True,
|
238
|
+
)
|
239
|
+
|
240
|
+
await _NetworkFileSystem.delete(label=nfs_name, environment_name=env)
|
modal/cli/run.py
CHANGED
@@ -29,7 +29,7 @@ from ..runner import deploy_app, interactive_shell, run_app
|
|
29
29
|
from ..serving import serve_app
|
30
30
|
from ..volume import Volume
|
31
31
|
from .import_refs import import_app, import_function
|
32
|
-
from .utils import ENV_OPTION, ENV_OPTION_HELP, stream_app_logs
|
32
|
+
from .utils import ENV_OPTION, ENV_OPTION_HELP, is_tty, stream_app_logs
|
33
33
|
|
34
34
|
|
35
35
|
class ParameterMetadata(TypedDict):
|
@@ -392,40 +392,47 @@ def shell(
|
|
392
392
|
"Can be a single region or a comma-separated list to choose from (if not using REF)."
|
393
393
|
),
|
394
394
|
),
|
395
|
+
pty: Optional[bool] = typer.Option(default=None, help="Run the command using a PTY."),
|
395
396
|
):
|
396
|
-
"""Run
|
397
|
+
"""Run a command or interactive shell inside a Modal container.
|
397
398
|
|
398
|
-
**Examples:**
|
399
|
+
\b**Examples:**
|
399
400
|
|
400
|
-
|
401
|
+
\bStart an interactive shell inside the default Debian-based image:
|
401
402
|
|
402
|
-
```
|
403
|
+
\b```
|
403
404
|
modal shell
|
404
405
|
```
|
405
406
|
|
406
|
-
|
407
|
+
\bStart an interactive shell with the spec for `my_function` in your App
|
408
|
+
(uses the same image, volumes, mounts, etc.):
|
407
409
|
|
408
|
-
```
|
410
|
+
\b```
|
409
411
|
modal shell hello_world.py::my_function
|
410
412
|
```
|
411
413
|
|
412
|
-
|
414
|
+
\bOr, if you're using a [modal.Cls](/docs/reference/modal.Cls), you can refer to a `@modal.method` directly:
|
413
415
|
|
414
|
-
```
|
416
|
+
\b```
|
415
417
|
modal shell hello_world.py::MyClass.my_method
|
416
418
|
```
|
417
419
|
|
418
420
|
Start a `python` shell:
|
419
421
|
|
420
|
-
```
|
422
|
+
\b```
|
421
423
|
modal shell hello_world.py --cmd=python
|
422
424
|
```
|
425
|
+
|
426
|
+
\bRun a command with your function's spec and pipe the output to a file:
|
427
|
+
|
428
|
+
\b```
|
429
|
+
modal shell hello_world.py -c 'uv pip list' > env.txt
|
430
|
+
```
|
423
431
|
"""
|
424
432
|
env = ensure_env(env)
|
425
433
|
|
426
|
-
|
427
|
-
|
428
|
-
raise click.UsageError("`modal shell` can only be run from a terminal.")
|
434
|
+
if pty is None:
|
435
|
+
pty = is_tty()
|
429
436
|
|
430
437
|
if platform.system() == "Windows":
|
431
438
|
raise InvalidError("`modal shell` is currently not supported on Windows")
|
@@ -441,7 +448,7 @@ def shell(
|
|
441
448
|
):
|
442
449
|
from .container import exec
|
443
450
|
|
444
|
-
exec(container_id=container_or_function, command=shlex.split(cmd)
|
451
|
+
exec(container_id=container_or_function, command=shlex.split(cmd))
|
445
452
|
return
|
446
453
|
|
447
454
|
function = import_function(
|
@@ -461,6 +468,7 @@ def shell(
|
|
461
468
|
memory=function_spec.memory,
|
462
469
|
volumes=function_spec.volumes,
|
463
470
|
region=function_spec.scheduler_placement.proto.regions if function_spec.scheduler_placement else None,
|
471
|
+
pty=pty,
|
464
472
|
)
|
465
473
|
else:
|
466
474
|
modal_image = Image.from_registry(image, add_python=add_python) if image else None
|
@@ -474,6 +482,7 @@ def shell(
|
|
474
482
|
cloud=cloud,
|
475
483
|
volumes=volumes,
|
476
484
|
region=region.split(",") if region else [],
|
485
|
+
pty=pty,
|
477
486
|
)
|
478
487
|
|
479
488
|
# 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
@@ -147,7 +147,7 @@ class _Client:
|
|
147
147
|
)
|
148
148
|
if resp.warning:
|
149
149
|
ALARM_EMOJI = chr(0x1F6A8)
|
150
|
-
warnings.
|
150
|
+
warnings.warn_explicit(f"{ALARM_EMOJI} {resp.warning} {ALARM_EMOJI}", DeprecationError, "<unknown>", 0)
|
151
151
|
except GRPCError as exc:
|
152
152
|
if exc.status == Status.FAILED_PRECONDITION:
|
153
153
|
raise VersionError(
|
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.
|
29
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.43"
|
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.
|
84
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.43"
|
85
85
|
): ...
|
86
86
|
def is_closed(self) -> bool: ...
|
87
87
|
@property
|
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/functions.py
CHANGED
@@ -347,7 +347,7 @@ class _FunctionSpec:
|
|
347
347
|
volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]]
|
348
348
|
gpus: Union[GPU_T, list[GPU_T]] # TODO(irfansharif): Somehow assert that it's the first kind, in sandboxes
|
349
349
|
cloud: Optional[str]
|
350
|
-
cpu: Optional[float]
|
350
|
+
cpu: Optional[Union[float, tuple[float, float]]]
|
351
351
|
memory: Optional[Union[int, tuple[int, int]]]
|
352
352
|
ephemeral_disk: Optional[int]
|
353
353
|
scheduler_placement: Optional[SchedulerPlacement]
|
@@ -448,7 +448,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
448
448
|
batch_max_size: Optional[int] = None,
|
449
449
|
batch_wait_ms: Optional[int] = None,
|
450
450
|
container_idle_timeout: Optional[int] = None,
|
451
|
-
cpu: Optional[float] = None,
|
451
|
+
cpu: Optional[Union[float, tuple[float, float]]] = None,
|
452
452
|
keep_warm: Optional[int] = None, # keep_warm=True is equivalent to keep_warm=1
|
453
453
|
cloud: Optional[str] = None,
|
454
454
|
scheduler_placement: Optional[SchedulerPlacement] = None,
|
modal/functions.pyi
CHANGED
@@ -96,7 +96,7 @@ class _FunctionSpec:
|
|
96
96
|
]
|
97
97
|
gpus: typing.Union[None, bool, str, modal.gpu._GPUConfig, list[typing.Union[None, bool, str, modal.gpu._GPUConfig]]]
|
98
98
|
cloud: typing.Optional[str]
|
99
|
-
cpu: typing.
|
99
|
+
cpu: typing.Union[float, tuple[float, float], None]
|
100
100
|
memory: typing.Union[int, tuple[int, int], None]
|
101
101
|
ephemeral_disk: typing.Optional[int]
|
102
102
|
scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement]
|
@@ -117,7 +117,7 @@ class _FunctionSpec:
|
|
117
117
|
None, bool, str, modal.gpu._GPUConfig, list[typing.Union[None, bool, str, modal.gpu._GPUConfig]]
|
118
118
|
],
|
119
119
|
cloud: typing.Optional[str],
|
120
|
-
cpu: typing.
|
120
|
+
cpu: typing.Union[float, tuple[float, float], None],
|
121
121
|
memory: typing.Union[int, tuple[int, int], None],
|
122
122
|
ephemeral_disk: typing.Optional[int],
|
123
123
|
scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement],
|
@@ -180,7 +180,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
|
|
180
180
|
batch_max_size: typing.Optional[int] = None,
|
181
181
|
batch_wait_ms: typing.Optional[int] = None,
|
182
182
|
container_idle_timeout: typing.Optional[int] = None,
|
183
|
-
cpu: typing.
|
183
|
+
cpu: typing.Union[float, tuple[float, float], None] = None,
|
184
184
|
keep_warm: typing.Optional[int] = None,
|
185
185
|
cloud: typing.Optional[str] = None,
|
186
186
|
scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
|
@@ -348,7 +348,7 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
348
348
|
batch_max_size: typing.Optional[int] = None,
|
349
349
|
batch_wait_ms: typing.Optional[int] = None,
|
350
350
|
container_idle_timeout: typing.Optional[int] = None,
|
351
|
-
cpu: typing.
|
351
|
+
cpu: typing.Union[float, tuple[float, float], None] = None,
|
352
352
|
keep_warm: typing.Optional[int] = None,
|
353
353
|
cloud: typing.Optional[str] = None,
|
354
354
|
scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
|
modal/io_streams.py
CHANGED
@@ -184,7 +184,7 @@ class _StreamReader(Generic[T]):
|
|
184
184
|
|
185
185
|
async for message in iterator:
|
186
186
|
if self._stream_type == StreamType.STDOUT and message:
|
187
|
-
print(message, end="")
|
187
|
+
print(message.decode("utf-8"), end="")
|
188
188
|
elif self._stream_type == StreamType.PIPE:
|
189
189
|
self._container_process_buffer.append(message)
|
190
190
|
if message is None:
|
modal/network_file_system.py
CHANGED
@@ -221,6 +221,12 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
|
|
221
221
|
resp = await retry_transient_errors(client.stub.SharedVolumeGetOrCreate, request)
|
222
222
|
return resp.shared_volume_id
|
223
223
|
|
224
|
+
@staticmethod
|
225
|
+
async def delete(label: str, client: Optional[_Client] = None, environment_name: Optional[str] = None):
|
226
|
+
obj = await _NetworkFileSystem.lookup(label, client=client, environment_name=environment_name)
|
227
|
+
req = api_pb2.SharedVolumeDeleteRequest(shared_volume_id=obj.object_id)
|
228
|
+
await retry_transient_errors(obj._client.stub.SharedVolumeDelete, req)
|
229
|
+
|
224
230
|
@live_method
|
225
231
|
async def write_file(self, remote_path: str, fp: BinaryIO, progress_cb: Optional[Callable[..., Any]] = None) -> int:
|
226
232
|
"""Write from a file object to a path on the network file system, atomically.
|
modal/network_file_system.pyi
CHANGED
@@ -41,6 +41,10 @@ class _NetworkFileSystem(modal.object._Object):
|
|
41
41
|
client: typing.Optional[modal.client._Client] = None,
|
42
42
|
environment_name: typing.Optional[str] = None,
|
43
43
|
) -> str: ...
|
44
|
+
@staticmethod
|
45
|
+
async def delete(
|
46
|
+
label: str, client: typing.Optional[modal.client._Client] = None, environment_name: typing.Optional[str] = None
|
47
|
+
): ...
|
44
48
|
async def write_file(
|
45
49
|
self,
|
46
50
|
remote_path: str,
|
@@ -118,6 +122,22 @@ class NetworkFileSystem(modal.object.Object):
|
|
118
122
|
|
119
123
|
create_deployed: __create_deployed_spec
|
120
124
|
|
125
|
+
class __delete_spec(typing_extensions.Protocol):
|
126
|
+
def __call__(
|
127
|
+
self,
|
128
|
+
label: str,
|
129
|
+
client: typing.Optional[modal.client.Client] = None,
|
130
|
+
environment_name: typing.Optional[str] = None,
|
131
|
+
): ...
|
132
|
+
async def aio(
|
133
|
+
self,
|
134
|
+
label: str,
|
135
|
+
client: typing.Optional[modal.client.Client] = None,
|
136
|
+
environment_name: typing.Optional[str] = None,
|
137
|
+
): ...
|
138
|
+
|
139
|
+
delete: __delete_spec
|
140
|
+
|
121
141
|
class __write_file_spec(typing_extensions.Protocol):
|
122
142
|
def __call__(
|
123
143
|
self,
|
modal/queue.py
CHANGED
@@ -27,38 +27,12 @@ class _Queue(_Object, type_prefix="qu"):
|
|
27
27
|
|
28
28
|
By default, the `Queue` object acts as a single FIFO queue which supports puts and gets (blocking and non-blocking).
|
29
29
|
|
30
|
-
**Queue partitions (beta)**
|
31
|
-
|
32
|
-
Specifying partition keys gives access to other independent FIFO partitions within the same `Queue` object.
|
33
|
-
Across any two partitions, puts and gets are completely independent.
|
34
|
-
For example, a put in one partition does not affect a get in any other partition.
|
35
|
-
|
36
|
-
When no partition key is specified (by default), puts and gets will operate on a default partition.
|
37
|
-
This default partition is also isolated from all other partitions.
|
38
|
-
Please see the Usage section below for an example using partitions.
|
39
|
-
|
40
|
-
**Lifetime of a queue and its partitions**
|
41
|
-
|
42
|
-
By default, each partition is cleared 24 hours after the last `put` operation.
|
43
|
-
A lower TTL can be specified by the `partition_ttl` argument in the `put` or `put_many` methods.
|
44
|
-
Each partition's expiry is handled independently.
|
45
|
-
|
46
|
-
As such, `Queue`s are best used for communication between active functions and not relied on for persistent storage.
|
47
|
-
|
48
|
-
On app completion or after stopping an app any associated `Queue` objects are cleaned up.
|
49
|
-
All its partitions will be cleared.
|
50
|
-
|
51
|
-
**Limits**
|
52
|
-
|
53
|
-
A single `Queue` can contain up to 100,000 partitions, each with up to 5,000 items. Each item can be up to 256 KiB.
|
54
|
-
|
55
|
-
Partition keys must be non-empty and must not exceed 64 bytes.
|
56
|
-
|
57
30
|
**Usage**
|
58
31
|
|
59
32
|
```python
|
60
33
|
from modal import Queue
|
61
34
|
|
35
|
+
# Create an ephemeral queue which is anonymous and garbage collected
|
62
36
|
with Queue.ephemeral() as my_queue:
|
63
37
|
# Putting values
|
64
38
|
my_queue.put("some value")
|
@@ -82,9 +56,41 @@ class _Queue(_Object, type_prefix="qu"):
|
|
82
56
|
# (beta feature) Iterate through items in place (read immutably)
|
83
57
|
my_queue.put(1)
|
84
58
|
assert [v for v in my_queue.iterate()] == [0, 1]
|
59
|
+
|
60
|
+
# You can also create persistent queues that can be used across apps
|
61
|
+
queue = Queue.from_name("my-persisted-queue", create_if_missing=True)
|
62
|
+
queue.put(42)
|
63
|
+
assert queue.get() == 42
|
85
64
|
```
|
86
65
|
|
87
66
|
For more examples, see the [guide](/docs/guide/dicts-and-queues#modal-queues).
|
67
|
+
|
68
|
+
**Queue partitions (beta)**
|
69
|
+
|
70
|
+
Specifying partition keys gives access to other independent FIFO partitions within the same `Queue` object.
|
71
|
+
Across any two partitions, puts and gets are completely independent.
|
72
|
+
For example, a put in one partition does not affect a get in any other partition.
|
73
|
+
|
74
|
+
When no partition key is specified (by default), puts and gets will operate on a default partition.
|
75
|
+
This default partition is also isolated from all other partitions.
|
76
|
+
Please see the Usage section below for an example using partitions.
|
77
|
+
|
78
|
+
**Lifetime of a queue and its partitions**
|
79
|
+
|
80
|
+
By default, each partition is cleared 24 hours after the last `put` operation.
|
81
|
+
A lower TTL can be specified by the `partition_ttl` argument in the `put` or `put_many` methods.
|
82
|
+
Each partition's expiry is handled independently.
|
83
|
+
|
84
|
+
As such, `Queue`s are best used for communication between active functions and not relied on for persistent storage.
|
85
|
+
|
86
|
+
On app completion or after stopping an app any associated `Queue` objects are cleaned up.
|
87
|
+
All its partitions will be cleared.
|
88
|
+
|
89
|
+
**Limits**
|
90
|
+
|
91
|
+
A single `Queue` can contain up to 100,000 partitions, each with up to 5,000 items. Each item can be up to 256 KiB.
|
92
|
+
|
93
|
+
Partition keys must be non-empty and must not exceed 64 bytes.
|
88
94
|
"""
|
89
95
|
|
90
96
|
@staticmethod
|
modal/runner.py
CHANGED
@@ -38,6 +38,7 @@ from .output import _get_output_manager, enable_output
|
|
38
38
|
from .running_app import RunningApp
|
39
39
|
from .sandbox import _Sandbox
|
40
40
|
from .secret import _Secret
|
41
|
+
from .stream_type import StreamType
|
41
42
|
|
42
43
|
if TYPE_CHECKING:
|
43
44
|
from .app import _App
|
@@ -557,7 +558,9 @@ async def _deploy_app(
|
|
557
558
|
)
|
558
559
|
|
559
560
|
|
560
|
-
async def _interactive_shell(
|
561
|
+
async def _interactive_shell(
|
562
|
+
_app: _App, cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: Any
|
563
|
+
) -> None:
|
561
564
|
"""Run an interactive shell (like `bash`) within the image for this app.
|
562
565
|
|
563
566
|
This is useful for online debugging and interactive exploration of the
|
@@ -599,9 +602,17 @@ async def _interactive_shell(_app: _App, cmds: list[str], environment_name: str
|
|
599
602
|
**kwargs,
|
600
603
|
)
|
601
604
|
|
602
|
-
container_process = await sandbox.exec(*sandbox_cmds, pty_info=get_pty_info(shell=True))
|
603
605
|
try:
|
604
|
-
|
606
|
+
if pty:
|
607
|
+
container_process = await sandbox.exec(
|
608
|
+
*sandbox_cmds, pty_info=get_pty_info(shell=True) if pty else None
|
609
|
+
)
|
610
|
+
await container_process.attach()
|
611
|
+
else:
|
612
|
+
container_process = await sandbox.exec(
|
613
|
+
*sandbox_cmds, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT
|
614
|
+
)
|
615
|
+
await container_process.wait()
|
605
616
|
except InteractiveTimeoutError:
|
606
617
|
# Check on status of Sandbox. It may have crashed, causing connection failure.
|
607
618
|
req = api_pb2.SandboxWaitRequest(sandbox_id=sandbox._object_id, timeout=0)
|
modal/runner.pyi
CHANGED
@@ -75,7 +75,9 @@ async def _deploy_app(
|
|
75
75
|
environment_name: typing.Optional[str] = None,
|
76
76
|
tag: str = "",
|
77
77
|
) -> DeployResult: ...
|
78
|
-
async def _interactive_shell(
|
78
|
+
async def _interactive_shell(
|
79
|
+
_app: _App, cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: typing.Any
|
80
|
+
) -> None: ...
|
79
81
|
def _run_stub(*args: typing.Any, **kwargs: typing.Any): ...
|
80
82
|
def _deploy_stub(*args: typing.Any, **kwargs: typing.Any): ...
|
81
83
|
|
@@ -134,8 +136,12 @@ class __deploy_app_spec(typing_extensions.Protocol):
|
|
134
136
|
deploy_app: __deploy_app_spec
|
135
137
|
|
136
138
|
class __interactive_shell_spec(typing_extensions.Protocol):
|
137
|
-
def __call__(
|
138
|
-
|
139
|
+
def __call__(
|
140
|
+
self, _app: _App, cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: typing.Any
|
141
|
+
) -> None: ...
|
142
|
+
async def aio(
|
143
|
+
self, _app: _App, cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: typing.Any
|
144
|
+
) -> None: ...
|
139
145
|
|
140
146
|
interactive_shell: __interactive_shell_spec
|
141
147
|
|