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.
@@ -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
- logger.debug("No checkpoint ID provided (Memory Snapshot ID)")
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 or "")
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
- date = content[:10]
170
- message = content[11:].strip()
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
@@ -115,7 +115,7 @@ def logs(
115
115
  ```
116
116
 
117
117
  """
118
- app_identifier = warn_on_name_option("stop", app_identifier, name)
118
+ app_identifier = warn_on_name_option("logs", app_identifier, name)
119
119
  app_id = get_app_id(app_identifier, env)
120
120
  stream_app_logs(app_id)
121
121
 
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(help="A command to run inside the container."),
60
- pty: bool = typer.Option(default=True, help="Run the command using a PTY."),
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
- await _ContainerProcess(res.exec_id, client).attach(pty=pty)
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")
@@ -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 an interactive shell inside a Modal container.
397
+ """Run a command or interactive shell inside a Modal container.
397
398
 
398
- **Examples:**
399
+ \b**Examples:**
399
400
 
400
- Start a shell inside the default Debian-based image:
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
- Start a bash shell using the spec for `my_function` in your App:
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
- Or, if you're using a [modal.Cls](/docs/reference/modal.Cls), you can refer to a `@modal.method` directly:
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
- console = Console()
427
- if not console.is_terminal:
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), pty=True)
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.warn(f"{ALARM_EMOJI} {resp.warning} {ALARM_EMOJI}", DeprecationError)
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.33"
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.33"
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
@@ -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=pty):
161
+ async with stream_from_stdin(_handle_input, use_raw_terminal=True):
155
162
  await stdout_task
156
163
  await stderr_task
157
164
 
@@ -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.Optional[float]
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.Optional[float],
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.Optional[float] = None,
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.Optional[float] = None,
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:
@@ -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.
@@ -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(_app: _App, cmds: list[str], environment_name: str = "", **kwargs: Any) -> None:
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
- await container_process.attach(pty=True)
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(_app: _App, cmds: list[str], environment_name: str = "", **kwargs: typing.Any) -> None: ...
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__(self, _app: _App, cmds: list[str], environment_name: str = "", **kwargs: typing.Any) -> None: ...
138
- async def aio(self, _app: _App, cmds: list[str], environment_name: str = "", **kwargs: typing.Any) -> None: ...
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