modAL 1.4.3.dev11__py3-none-any.whl → 1.4.3.dev13__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.
@@ -10,10 +10,13 @@ from .container_io_manager import _ContainerIOManager
10
10
 
11
11
 
12
12
  def is_local() -> bool:
13
- """Returns if we are currently on the machine launching/deploying a Modal app
13
+ """Indicate the execution context of the current process.
14
+
15
+ Note: this function specifically returns False when the current process is
16
+ running a Modal Function and True in all other cases. It will return True
17
+ when called from a child process of a Function or inside a Modal Sandbox,
18
+ even though those processes are running on Modal hardware.
14
19
 
15
- Returns `True` when executed locally on the user's machine.
16
- Returns `False` when executed from a Modal container in the cloud.
17
20
  """
18
21
  return not _ContainerIOManager._singleton
19
22
 
@@ -4,10 +4,12 @@ import typing
4
4
  import typing_extensions
5
5
 
6
6
  def is_local() -> bool:
7
- """Returns if we are currently on the machine launching/deploying a Modal app
7
+ """Indicate the execution context of the current process.
8
8
 
9
- Returns `True` when executed locally on the user's machine.
10
- Returns `False` when executed from a Modal container in the cloud.
9
+ Note: this function specifically returns False when the current process is
10
+ running a Modal Function and True in all other cases. It will return True
11
+ when called from a child process of a Function or inside a Modal Sandbox,
12
+ even though those processes are running on Modal hardware.
11
13
  """
12
14
  ...
13
15
 
modal/_serialization.py CHANGED
@@ -12,6 +12,8 @@ from modal.config import config
12
12
  try:
13
13
  import cbor2 # type: ignore
14
14
  except ImportError: # pragma: no cover - optional dependency
15
+ # cbor2 is a required dependency of modal as a library, but it won't
16
+ # be available in containers using a legacy (pre 2025.06) image builder version
15
17
  cbor2 = None
16
18
 
17
19
  import google.protobuf.message
@@ -374,7 +376,7 @@ def serialize_data_format(obj: Any, data_format: int) -> bytes:
374
376
  raise InvalidError("CBOR support requires the 'cbor2' package to be installed.")
375
377
  try:
376
378
  return cbor2.dumps(obj)
377
- except cbor2.CBOREncodeTypeError:
379
+ except cbor2.CBOREncodeError:
378
380
  try:
379
381
  typename = f"{type(obj).__module__}.{type(obj).__name__}"
380
382
  except Exception:
@@ -255,6 +255,36 @@ def make_make_directory_command(remote_path: str, create_parents: bool) -> str:
255
255
  return json.dumps({"MakeDirectory": {"path": remote_path, "parents": create_parents}})
256
256
 
257
257
 
258
+ def raise_stat_error(returncode: int, stderr: Union[str, bytes], remote_path: str) -> NoReturn:
259
+ if payload := try_parse_error_payload(stderr):
260
+ logger.debug(
261
+ f"sandbox-fs-tools stat error: path={remote_path}, "
262
+ f"error_kind={payload.error_kind}, message={payload.message}, detail={payload.detail}"
263
+ )
264
+ if payload.error_kind == "NotFound":
265
+ raise SandboxFilesystemNotFoundError(f"{payload.message}: {remote_path}")
266
+ if payload.error_kind == "NotDirectory":
267
+ raise SandboxFilesystemNotADirectoryError(f"{payload.message}: {remote_path}")
268
+ if payload.error_kind == "PermissionDenied":
269
+ raise SandboxFilesystemPermissionError(f"{payload.message}: {remote_path}")
270
+ raise SandboxFilesystemError(payload.message)
271
+
272
+ if stderr_text := _stderr_to_text(stderr):
273
+ logger.debug(f"Unstructured modal-sandbox-fs-tools stderr: {stderr_text}")
274
+ raise SandboxFilesystemError(f"Operation on '{remote_path}' failed with exit code {returncode}")
275
+
276
+
277
+ def make_stat_command(remote_path: str) -> str:
278
+ """Build the JSON command string for a Stat operation.
279
+
280
+ The returned JSON must match the `Command` enum in the modal-sandbox-fs-tools
281
+ Rust crate (crates/modal-sandbox-fs-tools/src/lib.rs). Treat changes to
282
+ this schema like protobuf changes: fields must not be removed or renamed,
283
+ only added with backwards-compatible defaults.
284
+ """
285
+ return json.dumps({"Stat": {"path": remote_path}})
286
+
287
+
258
288
  def validate_absolute_remote_path(remote_path: str, operation: str) -> None:
259
289
  if not PurePosixPath(remote_path).is_absolute():
260
290
  raise InvalidError(f"Sandbox.filesystem.{operation}() currently only supports absolute remote_path values")
modal/app.py CHANGED
@@ -379,6 +379,7 @@ class _App:
379
379
  async def run(
380
380
  self,
381
381
  *,
382
+ name: Optional[str] = None,
382
383
  client: Optional[_Client] = None,
383
384
  detach: bool = False,
384
385
  interactive: bool = False,
@@ -426,7 +427,7 @@ class _App:
426
427
  from .runner import _run_app # Defer import of runner.py, which imports a lot from Rich
427
428
 
428
429
  async with _run_app(
429
- self, client=client, detach=detach, interactive=interactive, environment_name=environment_name
430
+ self, name=name, client=client, detach=detach, interactive=interactive, environment_name=environment_name
430
431
  ):
431
432
  yield self
432
433
 
modal/app.pyi CHANGED
@@ -256,6 +256,7 @@ class _App:
256
256
  def run(
257
257
  self,
258
258
  *,
259
+ name: typing.Optional[str] = None,
259
260
  client: typing.Optional[modal.client._Client] = None,
260
261
  detach: bool = False,
261
262
  interactive: bool = False,
@@ -864,6 +865,7 @@ class App:
864
865
  self,
865
866
  /,
866
867
  *,
868
+ name: typing.Optional[str] = None,
867
869
  client: typing.Optional[modal.client.Client] = None,
868
870
  detach: bool = False,
869
871
  interactive: bool = False,
@@ -913,6 +915,7 @@ class App:
913
915
  self,
914
916
  /,
915
917
  *,
918
+ name: typing.Optional[str] = None,
916
919
  client: typing.Optional[modal.client.Client] = None,
917
920
  detach: bool = False,
918
921
  interactive: bool = False,
modal/cli/_help.py CHANGED
@@ -5,11 +5,12 @@ from __future__ import annotations
5
5
 
6
6
  import inspect
7
7
  import os
8
+ import shutil
8
9
  import sys
9
10
  from typing import Any, Optional
10
11
 
11
12
  import click
12
- from rich.console import Console
13
+ from rich.console import Console, Group, RenderableType
13
14
  from rich.markdown import Markdown
14
15
  from rich.padding import Padding
15
16
  from rich.panel import Panel
@@ -24,6 +25,7 @@ _OPTION_METAVAR_STYLE = "dim"
24
25
  _ERROR_STYLE = "red"
25
26
 
26
27
  _MAX_HELP_WIDTH = 80
28
+ _HELP_PADDING = 1
27
29
 
28
30
 
29
31
  def use_rich_style() -> bool:
@@ -34,25 +36,24 @@ def use_rich_style() -> bool:
34
36
  return sys.stdout.isatty()
35
37
 
36
38
 
37
- def _make_console(file: Optional[Any] = None) -> Console:
38
- return Console(file=file, highlight=False)
39
+ def _make_help_console() -> Console:
40
+ columns, _ = shutil.get_terminal_size()
41
+ return Console(highlight=False, width=min(_MAX_HELP_WIDTH, columns))
39
42
 
40
43
 
41
- def _render_usage(cmd: click.Command, ctx: click.Context, console: Console) -> None:
44
+ def _build_usage(cmd: click.Command, ctx: click.Context) -> RenderableType:
42
45
  pieces = " ".join(cmd.collect_usage_pieces(ctx))
43
46
  usage = Text()
44
47
  usage.append("Usage: ", style=_HEADING_STYLE)
45
48
  usage.append(f"{ctx.command_path} {pieces}".rstrip())
46
- console.print(usage)
47
- console.print()
49
+ return usage
48
50
 
49
51
 
50
- def _render_help_text(cmd: click.Command, console: Console) -> None:
52
+ def _build_help_text(cmd: click.Command) -> Optional[RenderableType]:
51
53
  text = cmd.help or cmd.short_help or ""
52
54
  if not text:
53
- return
54
- console.print(Padding(Markdown(inspect.cleandoc(text)), (0, 2)))
55
- console.print()
55
+ return None
56
+ return Markdown(inspect.cleandoc(text))
56
57
 
57
58
 
58
59
  def _option_label(param: click.Parameter, ctx: click.Context) -> Text:
@@ -73,7 +74,7 @@ def _option_label(param: click.Parameter, ctx: click.Context) -> Text:
73
74
  return text
74
75
 
75
76
 
76
- def _render_options(cmd: click.Command, ctx: click.Context, console: Console) -> None:
77
+ def _build_options(cmd: click.Command, ctx: click.Context) -> Optional[RenderableType]:
77
78
  rows: list[tuple[Text, str]] = []
78
79
  for param in cmd.get_params(ctx):
79
80
  rec = param.get_help_record(ctx)
@@ -81,22 +82,20 @@ def _render_options(cmd: click.Command, ctx: click.Context, console: Console) ->
81
82
  continue
82
83
  rows.append((_option_label(param, ctx), rec[1] or ""))
83
84
  if not rows:
84
- return
85
+ return None
85
86
 
86
- console.print(Text("Options", style=_HEADING_STYLE))
87
87
  table = Table(box=None, show_header=False, pad_edge=False, padding=(0, 2))
88
88
  table.add_column(no_wrap=True) # styling lives in the Text cells
89
89
  table.add_column(overflow="fold")
90
90
  for label, help_str in rows:
91
91
  table.add_row(label, help_str)
92
- console.print(table)
93
- console.print()
92
+ return Group(Text("Options", style=_HEADING_STYLE), table)
94
93
 
95
94
 
96
- def _render_epilog(cmd: click.Command, console: Console) -> None:
97
- if cmd.epilog:
98
- console.print(cmd.epilog)
99
- console.print()
95
+ def _build_epilog(cmd: click.Command) -> Optional[RenderableType]:
96
+ if not cmd.epilog:
97
+ return None
98
+ return Text(cmd.epilog)
100
99
 
101
100
 
102
101
  def _group_commands_by_panel(group: click.Group) -> dict[str, list[tuple[str, click.Command]]]:
@@ -109,23 +108,25 @@ def _group_commands_by_panel(group: click.Group) -> dict[str, list[tuple[str, cl
109
108
  return panels
110
109
 
111
110
 
112
- def _render_commands(group: click.Group, console: Console) -> None:
111
+ def _build_commands(group: click.Group, available_width: int) -> Optional[RenderableType]:
113
112
  panels = _group_commands_by_panel(group)
114
113
  if not panels:
115
- return
114
+ return None
116
115
 
117
116
  # We want the name / description columns to be the same widths across groups
118
117
  name_width = max(len(name) for items in panels.values() for name, _ in items)
119
- table_width = min(_MAX_HELP_WIDTH, console.width)
120
118
 
119
+ parts: list[RenderableType] = []
121
120
  for panel_name, items in panels.items():
122
- console.print(Text(panel_name.ljust(table_width), style=f"{_HEADING_STYLE} underline"))
123
- _render_command_table(console, items, name_width, table_width)
124
- console.print()
121
+ if parts:
122
+ parts.append(Text(""))
123
+ parts.append(Text(panel_name.ljust(available_width), style=f"{_HEADING_STYLE} underline"))
124
+ parts.append(_build_command_table(items, name_width, available_width))
125
+ return Group(*parts)
125
126
 
126
127
 
127
128
  def build_command_table(name_width: int, table_width: Optional[int] = None) -> Table:
128
- kwargs: dict[str, Any] = {"box": None, "show_header": False, "pad_edge": False, "padding": (0, 2)}
129
+ kwargs: dict[str, Any] = {"box": None, "show_header": False, "pad_edge": True, "padding": (0, 1)}
129
130
  if table_width is not None:
130
131
  kwargs["width"] = table_width
131
132
  table = Table(**kwargs)
@@ -137,16 +138,39 @@ def build_command_table(name_width: int, table_width: Optional[int] = None) -> T
137
138
  return table
138
139
 
139
140
 
140
- def _render_command_table(
141
- console: Console,
141
+ def _build_command_table(
142
142
  items: list[tuple[str, click.Command]],
143
143
  name_width: int,
144
144
  table_width: int,
145
- ) -> None:
145
+ ) -> Table:
146
146
  table = build_command_table(name_width, table_width)
147
147
  for name, sub in items:
148
148
  table.add_row(name, sub.get_short_help_str(limit=80))
149
- console.print(table)
149
+ return table
150
+
151
+
152
+ def _available_width(console: Console) -> int:
153
+ return min(_MAX_HELP_WIDTH, console.width) - _HELP_PADDING * 2
154
+
155
+
156
+ def _emit(
157
+ console: Console,
158
+ sections: list[Optional[RenderableType]],
159
+ formatter: click.HelpFormatter,
160
+ ) -> None:
161
+ parts: list[RenderableType] = []
162
+ for section in sections:
163
+ if section is None:
164
+ continue
165
+ if parts:
166
+ parts.append(Text(""))
167
+ parts.append(section)
168
+ if parts:
169
+ # Always pad with a blank line before the next prompt
170
+ parts.append(Text(""))
171
+ with console.capture() as capture:
172
+ console.print(Padding(Group(*parts), (0, _HELP_PADDING)))
173
+ formatter.write(capture.get())
150
174
 
151
175
 
152
176
  # -- Public Command / Group subclasses ---------------------------------------
@@ -162,13 +186,17 @@ class ModalCommand(click.Command):
162
186
  def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
163
187
  if not use_rich_style():
164
188
  return super().format_help(ctx, formatter)
165
- console = _make_console()
166
- with console.capture() as capture:
167
- _render_usage(self, ctx, console)
168
- _render_help_text(self, console)
169
- _render_options(self, ctx, console)
170
- _render_epilog(self, console)
171
- formatter.write(capture.get())
189
+ console = _make_help_console()
190
+ _emit(
191
+ console,
192
+ [
193
+ _build_usage(self, ctx),
194
+ _build_help_text(self),
195
+ _build_options(self, ctx),
196
+ _build_epilog(self),
197
+ ],
198
+ formatter,
199
+ )
172
200
 
173
201
 
174
202
  class ModalGroup(click.Group):
@@ -209,22 +237,25 @@ class ModalGroup(click.Group):
209
237
  def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
210
238
  if not use_rich_style():
211
239
  return super().format_help(ctx, formatter)
212
- console = _make_console()
213
- with console.capture() as capture:
214
- _render_usage(self, ctx, console)
215
- _render_help_text(self, console)
216
- _render_options(self, ctx, console)
217
- _render_commands(self, console)
218
- _render_epilog(self, console)
219
- formatter.write(capture.get())
240
+ console = _make_help_console()
241
+ _emit(
242
+ console,
243
+ [
244
+ _build_usage(self, ctx),
245
+ _build_help_text(self),
246
+ _build_options(self, ctx),
247
+ _build_commands(self, _available_width(console)),
248
+ _build_epilog(self),
249
+ ],
250
+ formatter,
251
+ )
220
252
 
221
253
 
222
254
  # -- Error rendering ---------------------------------------------------------
223
255
 
224
256
 
225
257
  def _render_click_exception(exc: click.ClickException, file: Any) -> None:
226
- console = _make_console(file if file is not None else sys.stderr)
227
- title = "Error"
258
+ console = Console(file=file if file is not None else sys.stderr, highlight=False)
228
259
 
229
260
  if isinstance(exc, click.UsageError) and exc.ctx is not None:
230
261
  ctx = exc.ctx
@@ -236,7 +267,7 @@ def _render_click_exception(exc: click.ClickException, file: Any) -> None:
236
267
  console.print(
237
268
  Panel(
238
269
  Text(exc.format_message()),
239
- title=title,
270
+ title="Error",
240
271
  title_align="left",
241
272
  border_style=_ERROR_STYLE,
242
273
  expand=True,
modal/cli/run.py CHANGED
@@ -235,6 +235,7 @@ def _make_click_function(app, signature: CliRunnableSignature, inner: Callable[[
235
235
  output_mgr.set_timestamps(ctx.obj["show_timestamps"])
236
236
  with run_app(
237
237
  app,
238
+ name=ctx.obj["name"],
238
239
  detach=ctx.obj["detach"],
239
240
  environment_name=ctx.obj["env"],
240
241
  interactive=ctx.obj["interactive"],
@@ -365,6 +366,7 @@ def _get_click_command_for_local_entrypoint(app: App, entrypoint: LocalEntrypoin
365
366
  output_mgr.set_timestamps(ctx.obj["show_timestamps"])
366
367
  with run_app(
367
368
  app,
369
+ name=ctx.obj["name"],
368
370
  detach=ctx.obj["detach"],
369
371
  environment_name=ctx.obj["env"],
370
372
  interactive=ctx.obj["interactive"],
@@ -448,6 +450,7 @@ class RunGroup(ModalGroup):
448
450
  cls=RunGroup,
449
451
  subcommand_metavar="FUNC_REF",
450
452
  )
453
+ @click.option("-n", "--name", help="Name for this run of the App.")
451
454
  @click.option("-w", "--write-result", help="Write return value (which must be str or bytes) to this local path.")
452
455
  @click.option("-q", "--quiet", is_flag=True, help="Don't show Modal progress indicators.")
453
456
  @click.option("-d", "--detach", is_flag=True, help="Don't stop the app if the local process dies or disconnects.")
@@ -458,6 +461,7 @@ class RunGroup(ModalGroup):
458
461
  @click.pass_context
459
462
  def run(
460
463
  ctx: click.Context,
464
+ name: Optional[str],
461
465
  write_result: Optional[str],
462
466
  detach: bool,
463
467
  quiet: bool,
@@ -498,6 +502,7 @@ def run(
498
502
  ```
499
503
  """
500
504
  ctx.ensure_object(dict)
505
+ ctx.obj["name"] = name
501
506
  ctx.obj["result_path"] = write_result
502
507
  ctx.obj["detach"] = detach # if subcommand would be a click command...
503
508
  ctx.obj["show_progress"] = False if quiet else True
@@ -573,6 +578,7 @@ def deploy(
573
578
 
574
579
  @click.command("serve", cls=ModalCommand, no_args_is_help=True)
575
580
  @click.argument("app_ref")
581
+ @click.option("-n", "--name", help="Name for this run of the App.")
576
582
  @click.option("--timeout", default=None, type=float)
577
583
  @click.option("-e", "--env", default=None, help=ENV_OPTION_HELP)
578
584
  @click.option(
@@ -585,6 +591,7 @@ def deploy(
585
591
  @click.option("--timestamps", is_flag=True, default=False, help="Show timestamps for each log line.")
586
592
  def serve(
587
593
  app_ref: str,
594
+ name: Optional[str] = None,
588
595
  timeout: Optional[float] = None,
589
596
  env: Optional[str] = None,
590
597
  use_module_mode: bool = False,
@@ -611,7 +618,7 @@ def serve(
611
618
  app = import_app_from_ref(import_ref, base_cmd="modal serve")
612
619
  if app.description is None:
613
620
  app.set_description(_get_clean_app_description(app_ref))
614
- with serve_app(app, import_ref, environment_name=env):
621
+ with serve_app(app, import_ref, name=name, environment_name=env):
615
622
  if timeout is None:
616
623
  timeout = config["serve_timeout"]
617
624
  if timeout is None:
modal/cli/utils.py CHANGED
@@ -162,11 +162,10 @@ def display_table(
162
162
  output.print(table)
163
163
 
164
164
 
165
- ENV_OPTION_HELP = """Environment to interact with.
166
-
167
- If not specified, Modal will use the default environment of your current profile, or the `MODAL_ENVIRONMENT` variable.
168
- Otherwise, raises an error if the workspace has multiple environments.
169
- """
165
+ ENV_OPTION_HELP = (
166
+ "Environment to interact with. If unspecified, defers to `MODAL_ENVIRONMENT`, "
167
+ "your active local profile, or your workspace default, in that order."
168
+ )
170
169
 
171
170
 
172
171
  def env_option(func):
modal/client.pyi CHANGED
@@ -35,7 +35,7 @@ class _Client:
35
35
  server_url: str,
36
36
  client_type: int,
37
37
  credentials: typing.Optional[tuple[str, str]],
38
- version: str = "1.4.3.dev11",
38
+ version: str = "1.4.3.dev13",
39
39
  ):
40
40
  """mdmd:hidden
41
41
  The Modal client object is not intended to be instantiated directly by users.
@@ -175,7 +175,7 @@ class Client:
175
175
  server_url: str,
176
176
  client_type: int,
177
177
  credentials: typing.Optional[tuple[str, str]],
178
- version: str = "1.4.3.dev11",
178
+ version: str = "1.4.3.dev13",
179
179
  ):
180
180
  """mdmd:hidden
181
181
  The Modal client object is not intended to be instantiated directly by users.
modal/functions.pyi CHANGED
@@ -347,7 +347,7 @@ class Function(
347
347
 
348
348
  _call_generator: ___call_generator_spec
349
349
 
350
- class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
350
+ class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
351
351
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
352
352
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
353
353
  ...
@@ -356,7 +356,7 @@ class Function(
356
356
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
357
357
  ...
358
358
 
359
- remote: __remote_spec[modal._functions.ReturnType, modal._functions.P]
359
+ remote: __remote_spec[modal._functions.P, modal._functions.ReturnType]
360
360
 
361
361
  class __remote_gen_spec(typing_extensions.Protocol):
362
362
  def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
@@ -383,7 +383,7 @@ class Function(
383
383
  """
384
384
  ...
385
385
 
386
- class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
386
+ class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
387
387
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
388
388
  """[Experimental] Calls the function with the given arguments, without waiting for the results.
389
389
 
@@ -406,7 +406,7 @@ class Function(
406
406
  """
407
407
  ...
408
408
 
409
- _experimental_spawn: ___experimental_spawn_spec[modal._functions.ReturnType, modal._functions.P]
409
+ _experimental_spawn: ___experimental_spawn_spec[modal._functions.P, modal._functions.ReturnType]
410
410
 
411
411
  class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER]):
412
412
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> None: ...
@@ -414,7 +414,7 @@ class Function(
414
414
 
415
415
  _spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P]
416
416
 
417
- class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
417
+ class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
418
418
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
419
419
  """Calls the function with the given arguments, without waiting for the results.
420
420
 
@@ -435,7 +435,7 @@ class Function(
435
435
  """
436
436
  ...
437
437
 
438
- spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P]
438
+ spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType]
439
439
 
440
440
  def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
441
441
  """Return the inner Python object wrapped by this Modal Function."""
modal/runner.py CHANGED
@@ -360,6 +360,7 @@ async def _status_based_disconnect(client: _Client, app_id: str, exc_info: Optio
360
360
  async def _run_app(
361
361
  app: "modal.app._App",
362
362
  *,
363
+ name: Optional[str] = None,
363
364
  client: Optional[_Client] = None,
364
365
  detach: bool = False,
365
366
  environment_name: Optional[str] = None,
@@ -380,7 +381,9 @@ async def _run_app(
380
381
  "You should not use `app.run` or `run_app` within a Modal `local_entrypoint`"
381
382
  )
382
383
 
383
- if app.description is None:
384
+ if name:
385
+ app.set_description(name)
386
+ elif app.description is None:
384
387
  import __main__
385
388
 
386
389
  if "__file__" in dir(__main__):
modal/runner.pyi CHANGED
@@ -75,6 +75,7 @@ async def _status_based_disconnect(
75
75
  def _run_app(
76
76
  app: modal.app._App,
77
77
  *,
78
+ name: typing.Optional[str] = None,
78
79
  client: typing.Optional[modal.client._Client] = None,
79
80
  detach: bool = False,
80
81
  environment_name: typing.Optional[str] = None,
@@ -170,6 +171,7 @@ class __run_app_spec(typing_extensions.Protocol):
170
171
  /,
171
172
  app: modal.app.App,
172
173
  *,
174
+ name: typing.Optional[str] = None,
173
175
  client: typing.Optional[modal.client.Client] = None,
174
176
  detach: bool = False,
175
177
  environment_name: typing.Optional[str] = None,
@@ -184,6 +186,7 @@ class __run_app_spec(typing_extensions.Protocol):
184
186
  /,
185
187
  app: modal.app.App,
186
188
  *,
189
+ name: typing.Optional[str] = None,
187
190
  client: typing.Optional[modal.client.Client] = None,
188
191
  detach: bool = False,
189
192
  environment_name: typing.Optional[str] = None,
modal/sandbox_fs.py CHANGED
@@ -18,11 +18,13 @@ from ._utils.sandbox_fs_utils import (
18
18
  make_make_directory_command,
19
19
  make_read_file_command,
20
20
  make_remove_command,
21
+ make_stat_command,
21
22
  make_write_file_command,
22
23
  raise_list_files_error,
23
24
  raise_make_directory_error,
24
25
  raise_read_file_error,
25
26
  raise_remove_error,
27
+ raise_stat_error,
26
28
  raise_write_file_error,
27
29
  translate_exec_errors,
28
30
  validate_absolute_remote_path,
@@ -419,6 +421,57 @@ class _SandboxFilesystem:
419
421
  if returncode != 0:
420
422
  raise_remove_error(returncode, stderr, remote_path)
421
423
 
424
+ async def stat(self, remote_path: str) -> FileInfo:
425
+ """Return metadata for a single file, directory, or symlink in the Sandbox.
426
+
427
+ `remote_path` must be an absolute path in the Sandbox. If `remote_path` is a symlink, the returned
428
+ `FileInfo` object describes the symlink, not the target it points to.
429
+
430
+ **Raises**
431
+
432
+ - `SandboxFilesystemNotFoundError`: the path does not exist.
433
+ - `SandboxFilesystemNotADirectoryError`: a non-leaf component of the path is not a directory.
434
+ - `SandboxFilesystemPermissionError`: a component of the path is not searchable.
435
+ - `SandboxFilesystemError`: the command fails for any other reason.
436
+
437
+ **Usage**
438
+
439
+ ```python fixture:sandbox
440
+ sandbox.filesystem.write_text("Hello, world!\\n", "/tmp/hello.txt")
441
+ info = sandbox.filesystem.stat("/tmp/hello.txt")
442
+ print(info.size, info.permissions, info.modified_time)
443
+ ```
444
+ """
445
+ validate_absolute_remote_path(remote_path, "stat")
446
+
447
+ t0 = time.monotonic()
448
+ with translate_exec_errors("stat", remote_path):
449
+ process = await self._sandbox.exec(_SANDBOX_FS_TOOLS_PATH, make_stat_command(remote_path))
450
+ stdout, stderr, returncode = await asyncio.gather(
451
+ process.stdout.read(), process.stderr.read(), process.wait()
452
+ )
453
+
454
+ if returncode != 0:
455
+ raise_stat_error(returncode, stderr, remote_path)
456
+
457
+ entry = json.loads(stdout)
458
+ result = FileInfo(
459
+ name=entry["name"],
460
+ path=entry["path"],
461
+ type=FileType(entry["type"]),
462
+ size=entry["size"],
463
+ mode=entry["mode"],
464
+ permissions=entry["permissions"],
465
+ owner=entry["owner"],
466
+ group=entry["group"],
467
+ modified_time=entry["modified_time"],
468
+ symlink_target=entry.get("symlink_target"),
469
+ )
470
+
471
+ dur_s = max(time.monotonic() - t0, 0.001)
472
+ logger.debug(f"sandbox stat {remote_path}: ({dur_s:.2f}s)")
473
+ return result
474
+
422
475
  async def write_bytes(self, data: Union[bytes, bytearray, memoryview], remote_path: str) -> None:
423
476
  """Write binary content to a file in the Sandbox.
424
477
 
modal/sandbox_fs.pyi CHANGED
@@ -270,6 +270,29 @@ class _SandboxFilesystem:
270
270
  """
271
271
  ...
272
272
 
273
+ async def stat(self, remote_path: str) -> FileInfo:
274
+ """Return metadata for a single file, directory, or symlink in the Sandbox.
275
+
276
+ `remote_path` must be an absolute path in the Sandbox. If `remote_path` is a symlink, the returned
277
+ `FileInfo` object describes the symlink, not the target it points to.
278
+
279
+ **Raises**
280
+
281
+ - `SandboxFilesystemNotFoundError`: the path does not exist.
282
+ - `SandboxFilesystemNotADirectoryError`: a non-leaf component of the path is not a directory.
283
+ - `SandboxFilesystemPermissionError`: a component of the path is not searchable.
284
+ - `SandboxFilesystemError`: the command fails for any other reason.
285
+
286
+ **Usage**
287
+
288
+ ```python fixture:sandbox
289
+ sandbox.filesystem.write_text("Hello, world!\n", "/tmp/hello.txt")
290
+ info = sandbox.filesystem.stat("/tmp/hello.txt")
291
+ print(info.size, info.permissions, info.modified_time)
292
+ ```
293
+ """
294
+ ...
295
+
273
296
  async def write_bytes(self, data: typing.Union[bytes, bytearray, memoryview], remote_path: str) -> None:
274
297
  """Write binary content to a file in the Sandbox.
275
298
 
@@ -719,6 +742,55 @@ class SandboxFilesystem:
719
742
 
720
743
  remove: __remove_spec
721
744
 
745
+ class __stat_spec(typing_extensions.Protocol):
746
+ def __call__(self, /, remote_path: str) -> FileInfo:
747
+ """Return metadata for a single file, directory, or symlink in the Sandbox.
748
+
749
+ `remote_path` must be an absolute path in the Sandbox. If `remote_path` is a symlink, the returned
750
+ `FileInfo` object describes the symlink, not the target it points to.
751
+
752
+ **Raises**
753
+
754
+ - `SandboxFilesystemNotFoundError`: the path does not exist.
755
+ - `SandboxFilesystemNotADirectoryError`: a non-leaf component of the path is not a directory.
756
+ - `SandboxFilesystemPermissionError`: a component of the path is not searchable.
757
+ - `SandboxFilesystemError`: the command fails for any other reason.
758
+
759
+ **Usage**
760
+
761
+ ```python fixture:sandbox
762
+ sandbox.filesystem.write_text("Hello, world!\n", "/tmp/hello.txt")
763
+ info = sandbox.filesystem.stat("/tmp/hello.txt")
764
+ print(info.size, info.permissions, info.modified_time)
765
+ ```
766
+ """
767
+ ...
768
+
769
+ async def aio(self, /, remote_path: str) -> FileInfo:
770
+ """Return metadata for a single file, directory, or symlink in the Sandbox.
771
+
772
+ `remote_path` must be an absolute path in the Sandbox. If `remote_path` is a symlink, the returned
773
+ `FileInfo` object describes the symlink, not the target it points to.
774
+
775
+ **Raises**
776
+
777
+ - `SandboxFilesystemNotFoundError`: the path does not exist.
778
+ - `SandboxFilesystemNotADirectoryError`: a non-leaf component of the path is not a directory.
779
+ - `SandboxFilesystemPermissionError`: a component of the path is not searchable.
780
+ - `SandboxFilesystemError`: the command fails for any other reason.
781
+
782
+ **Usage**
783
+
784
+ ```python fixture:sandbox
785
+ sandbox.filesystem.write_text("Hello, world!\n", "/tmp/hello.txt")
786
+ info = sandbox.filesystem.stat("/tmp/hello.txt")
787
+ print(info.size, info.permissions, info.modified_time)
788
+ ```
789
+ """
790
+ ...
791
+
792
+ stat: __stat_spec
793
+
722
794
  class __write_bytes_spec(typing_extensions.Protocol):
723
795
  def __call__(self, /, data: typing.Union[bytes, bytearray, memoryview], remote_path: str) -> None:
724
796
  """Write binary content to a file in the Sandbox.
modal/serving.py CHANGED
@@ -93,6 +93,7 @@ async def _serve_app(
93
93
  app: "modal.app._App",
94
94
  import_ref: ImportRef,
95
95
  *,
96
+ name: Optional[str] = None,
96
97
  _watcher: Optional[AsyncGenerator[set[str], None]] = None, # for testing
97
98
  environment_name: Optional[str] = None,
98
99
  ) -> AsyncGenerator["modal.app._App", None]:
@@ -101,7 +102,9 @@ async def _serve_app(
101
102
 
102
103
  client = await _Client.from_env()
103
104
 
104
- async with _run_app(app, client=client, environment_name=environment_name, deployment_strategy="recreate"):
105
+ async with _run_app(
106
+ app, name=name, client=client, environment_name=environment_name, deployment_strategy="recreate"
107
+ ):
105
108
  if _watcher is not None:
106
109
  watcher = _watcher # Only used by tests
107
110
  else:
modal/serving.pyi CHANGED
@@ -29,6 +29,7 @@ def _serve_app(
29
29
  app: modal.app._App,
30
30
  import_ref: modal.cli.import_refs.ImportRef,
31
31
  *,
32
+ name: typing.Optional[str] = None,
32
33
  _watcher: typing.Optional[collections.abc.AsyncGenerator[set[str], None]] = None,
33
34
  environment_name: typing.Optional[str] = None,
34
35
  ) -> typing.AsyncContextManager[modal.app._App]: ...
@@ -40,6 +41,7 @@ class __serve_app_spec(typing_extensions.Protocol):
40
41
  app: modal.app.App,
41
42
  import_ref: modal.cli.import_refs.ImportRef,
42
43
  *,
44
+ name: typing.Optional[str] = None,
43
45
  _watcher: typing.Optional[typing.Generator[set[str], None, None]] = None,
44
46
  environment_name: typing.Optional[str] = None,
45
47
  ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[modal.app.App]: ...
@@ -49,6 +51,7 @@ class __serve_app_spec(typing_extensions.Protocol):
49
51
  app: modal.app.App,
50
52
  import_ref: modal.cli.import_refs.ImportRef,
51
53
  *,
54
+ name: typing.Optional[str] = None,
52
55
  _watcher: typing.Optional[collections.abc.AsyncGenerator[set[str], None]] = None,
53
56
  environment_name: typing.Optional[str] = None,
54
57
  ) -> typing.AsyncContextManager[modal.app.App]: ...
modal/volume.py CHANGED
@@ -31,7 +31,15 @@ from synchronicity.async_wrap import asynccontextmanager
31
31
 
32
32
  import modal.exception
33
33
  import modal_proto.api_pb2
34
- from modal.exception import AlreadyExistsError, ConflictError, InvalidError, NotFoundError, VolumeUploadTimeoutError
34
+ from modal.exception import (
35
+ AlreadyExistsError,
36
+ ConflictError,
37
+ ExecutionError,
38
+ InvalidError,
39
+ NotFoundError,
40
+ ServiceError,
41
+ VolumeUploadTimeoutError,
42
+ )
35
43
  from modal_proto import api_pb2
36
44
 
37
45
  from ._load_context import LoadContext
@@ -75,6 +83,26 @@ from .config import logger
75
83
  VOLUME_PUT_FILE_CLIENT_TIMEOUT = 60 * 60
76
84
 
77
85
 
86
+ async def _raise_on_block_response_error(response) -> None:
87
+ """Raise a picklable Modal exception on error.
88
+
89
+ We avoid `aiohttp.ClientResponse.raise_for_status()` because the resulting
90
+ `ClientResponseError` is not picklable, which breaks result serialization
91
+ when raised from within a Modal function.
92
+ """
93
+ if response.status < 400:
94
+ return
95
+
96
+ try:
97
+ body = await response.text()
98
+ except Exception:
99
+ body = "<unavailable>"
100
+
101
+ if response.status == 503:
102
+ raise ServiceError(f"Service temporarily unavailable: {body}")
103
+ raise ExecutionError(f"Request failed with status {response.status} {response.reason}: {body}")
104
+
105
+
78
106
  def _validate_volume_version(
79
107
  requested_version: int,
80
108
  actual_version: int,
@@ -742,7 +770,7 @@ class _Volume(_Object, type_prefix="vo"):
742
770
  @retry(n_attempts=5, base_delay=0.1, attempt_timeout=None)
743
771
  async def read_block(block_url: str) -> bytes:
744
772
  async with ClientSessionRegistry.get_session().get(block_url) as get_response:
745
- get_response.raise_for_status()
773
+ await _raise_on_block_response_error(get_response)
746
774
  return await get_response.content.read()
747
775
 
748
776
  async def iter_urls() -> AsyncGenerator[str]:
@@ -811,7 +839,7 @@ class _Volume(_Object, type_prefix="vo"):
811
839
  num_bytes_written = 0
812
840
 
813
841
  async with download_semaphore, ClientSessionRegistry.get_session().get(url) as get_response:
814
- get_response.raise_for_status()
842
+ await _raise_on_block_response_error(get_response)
815
843
  async for chunk in get_response.content.iter_any():
816
844
  num_chunk_bytes_written = 0
817
845
 
@@ -1377,7 +1405,7 @@ async def _put_missing_blocks(
1377
1405
  missing_block.put_url,
1378
1406
  data=payload,
1379
1407
  ) as response:
1380
- response.raise_for_status()
1408
+ await _raise_on_block_response_error(response)
1381
1409
  return await response.content.read()
1382
1410
 
1383
1411
  async with put_semaphore:
modal/volume.pyi CHANGED
@@ -15,6 +15,15 @@ import synchronicity.combined_types
15
15
  import typing
16
16
  import typing_extensions
17
17
 
18
+ async def _raise_on_block_response_error(response) -> None:
19
+ """Raise a picklable Modal exception on error.
20
+
21
+ We avoid `aiohttp.ClientResponse.raise_for_status()` because the resulting
22
+ `ClientResponseError` is not picklable, which breaks result serialization
23
+ when raised from within a Modal function.
24
+ """
25
+ ...
26
+
18
27
  def _validate_volume_version(requested_version: int, actual_version: int, volume_name: str) -> None:
19
28
  """Validate that the returned volume version matches the requested version."""
20
29
  ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.4.3.dev11
3
+ Version: 1.4.3.dev13
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -14,19 +14,19 @@ modal/_object.py,sha256=144YrNnTG77vK_PAHRU9GFxHBOlt2PWFgTUXpgX4BQk,14315
14
14
  modal/_partial_function.py,sha256=gJbvXIbxkXgVE6eXOJDmvufTPdrOV5DFNBumgNEI1aU,34756
15
15
  modal/_resolver.py,sha256=u9GdlHo3Ul_40B97LH0vSrjtEJB__WeRf-Oo79vSmPE,5516
16
16
  modal/_resources.py,sha256=Lan7aiWN2IdbIeJIIrmQZqt-BLnW2K5rMk5mB0kjc4k,1785
17
- modal/_serialization.py,sha256=Yc4czrt8x3xkX43oJngb7BSI4zPgtBVuifYrxTCdsAQ,26451
17
+ modal/_serialization.py,sha256=KQHv6rHoJHwXSNnsB6KVZVQH5EeS80Sa4bu7ykVmHF4,26604
18
18
  modal/_server.py,sha256=RLuY_VK5Kor_8VzhcIYKMqNc0_ITe2gWl26OusdFkyc,10049
19
19
  modal/_traceback.py,sha256=N0EumYnHHqPHkmP2L6cJKjTpoxc8QYmiofROmdgSthQ,6238
20
20
  modal/_tunnel.py,sha256=ixQGklCn6eV2TekgET8o0ZD9dEo36Iw_grUsg-S5Bhg,7084
21
21
  modal/_tunnel.pyi,sha256=FoedJzHymbPk4Bmnbszm6uhay2rMq1JS9mIiHELYf3k,13804
22
22
  modal/_type_manager.py,sha256=DWjgmjYJuOagw2erin506UUbG2H5UzZCFEekS-7hmfA,9087
23
23
  modal/_watcher.py,sha256=LerzeKqyGKOlPw9caRGnIyjcZTFXe5YvmeZ2grT2_q8,3539
24
- modal/app.py,sha256=I6wpFPPonC61nJ7lACwM5uqStf-IqNEq81L983QOvcc,62584
25
- modal/app.pyi,sha256=mysbOCD6OjZi938NeMY40RpdpQbnI43Bb4s5P79x5OQ,53773
24
+ modal/app.py,sha256=wnguq9LvxEKSYvy2EMs5wWWKV8WJSnszCjaL8t_rb2A,62631
25
+ modal/app.pyi,sha256=-9667eLtUgoinLTRrJ-Z3SOgMR0S89Qw_vpwEZfp2Pk,53910
26
26
  modal/billing.py,sha256=B2zXa7KeMnpqi2AQINycEk-KSXVYaLTTTAQTtuf2Gfs,257
27
27
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
28
28
  modal/client.py,sha256=jjT4eLFHGcvIF6Xw4kPptIx-VmExtphRN8XJ1JA7fWI,15502
29
- modal/client.pyi,sha256=TESKhuxg_HRtpCQCyKVfNaY3aoAEnHhn6KRAAtASauw,15703
29
+ modal/client.pyi,sha256=oOJ_0UgersqG_9eUWIT8rAsyMXy6rK6wrZp-Rec7Gy8,15703
30
30
  modal/cloud_bucket_mount.py,sha256=KkaJVy5vpO_u2NtDYtv1JM-M3y4J0UtWBapTDKpQRcA,6034
31
31
  modal/cloud_bucket_mount.pyi,sha256=mGDeEd1XvxGpCE6O5jRBSfIvMXvZ5AtIybea5k-0w9w,7560
32
32
  modal/cls.py,sha256=Yrl5b_tQhZMhc5avInM-i5UlGuqeog6Gs4rX2d9lLjI,40390
@@ -43,7 +43,7 @@ modal/file_io.py,sha256=45gO6ScEPKrE-KTQ619RwejaqoHUregwMgKcVktfoAk,22275
43
43
  modal/file_io.pyi,sha256=DPaq0BhySaiCX3g56QHhyIMdU1izbfWsjpSp30JoJUU,15745
44
44
  modal/file_pattern_matcher.py,sha256=A_Kdkej6q7YQyhM_2-BvpFmPqJ0oHb54B6yf9VqvPVE,8116
45
45
  modal/functions.py,sha256=EZRcHFfnoYbvawUJnAHF5nIvoo9GX2Djy7mUmOCXmXc,258
46
- modal/functions.pyi,sha256=_sZ27MFClkElRg0iyVXmn9pGXwm9F-r49sHjrYhH4Sg,34255
46
+ modal/functions.pyi,sha256=m2uNU9DMi9LWprk9PJykCM1Zs_oEG2-Vl2FaJMGSNpY,34255
47
47
  modal/image.py,sha256=EHdzQMPBXtbwHBajZJAC-uLCypF-DoNbB0vMWVfaZ5c,112579
48
48
  modal/image.pyi,sha256=zYiJsc5tEhGev_rT9R3cGpyQJf-Hm08OJl37sV4E_fk,79377
49
49
  modal/io_streams.py,sha256=JcYbvvjMZFugv53yHc9Id0N4nIvDMwV63ZtnN1bPmZc,30940
@@ -65,28 +65,28 @@ modal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
65
  modal/queue.py,sha256=yd-V0Fds5RrmQqg-Pe9kF7kY-tcvrktxwmwzJT-XTxA,27247
66
66
  modal/queue.pyi,sha256=XFkassplPM8wsu-ef_dvls1-0vPfT3H4VdOchVFcsvU,40199
67
67
  modal/retries.py,sha256=vPWWKP-FRolJwTBuLPXdWqHV7aJfSQFgEW7vhlBBXRQ,5067
68
- modal/runner.py,sha256=yIB1PE5EGdvLWPPymyT4P0KYliREnFf-MAtULl3tVN0,30062
69
- modal/runner.pyi,sha256=ySefjmlMQ5FsvJgJkCWiUZKvcPNfEqdB5hwxI1fGtDo,9445
68
+ modal/runner.py,sha256=P1R9n1O2Mrv_J0RIxqAz_oyioE9W2l5pmEykLaHBCrY,30143
69
+ modal/runner.pyi,sha256=7QHbZawq1P5N9DiSrrEYtXtBCDAWlRibNonBO0q6MdM,9570
70
70
  modal/running_app.py,sha256=v61mapYNV1-O-Uaho5EfJlryMLvIT9We0amUOSvSGx8,1188
71
71
  modal/sandbox.py,sha256=byesQYe1QWRNB6yA3eTpr8TUIlb398U9kSoSm77X9P0,83622
72
72
  modal/sandbox.pyi,sha256=fDUIw3NsJPBRZ198lEo_Bw9WfESsqmUWcFLRnow8F-0,86546
73
- modal/sandbox_fs.py,sha256=IfsbUfYO1whNENRLP1UukH5TdYsdctveE1hvIPoKoGA,19789
74
- modal/sandbox_fs.pyi,sha256=0PlbdZmBERFbQ2ZgwHnqBZybD6SVOmWecBDuHQa3ZY8,31826
73
+ modal/sandbox_fs.py,sha256=R-uYmsPkUWua5dWzh8DhCdCIa6FvHNJFYtXbffnONa0,21891
74
+ modal/sandbox_fs.pyi,sha256=zsSDGNSTkjo_gJYQH3J7if8Y6J8_Uy4f9u0CypCo5uc,34946
75
75
  modal/schedule.py,sha256=ng0g0AqNY5GQI9KhkXZQ5Wam5G42glbkqVQsNpBtbDE,3078
76
76
  modal/scheduler_placement.py,sha256=BAREdOY5HzHpzSBqt6jDVR6YC_jYfHMVqOzkyqQfngU,1235
77
77
  modal/secret.py,sha256=N44g75jJ8r8kcit4U_m5Vu9AdQwi9pXFehpKviP8amQ,18655
78
78
  modal/secret.pyi,sha256=DqG0o5ndliJNO71vNRe9Ee_6ioTgsLKzHkr7S5ru8eI,21488
79
79
  modal/server.py,sha256=zWoDdtwCEKsKbFfWC4l7-NkpnJJybweC284VIoCavrI,164
80
80
  modal/server.pyi,sha256=d_boB2FhK2b3C_G7YPM9oeWlCWEZhBCxbeebAKcrY-Q,5242
81
- modal/serving.py,sha256=akauomk_rQNabYKXhgp6SGxBU4cFontanDqMfAkk7Ng,4188
82
- modal/serving.pyi,sha256=eNqF_bhO_JQ0EDpqvSjSUdNs48vv7AcPXinp7GEs5wI,1966
81
+ modal/serving.py,sha256=zhAVUyztbB-mVz0ddYs349t1BYTmoWHaE2G8IdTNAMM,4245
82
+ modal/serving.pyi,sha256=imGbIuEVwqJJgJURC-k3p9kQT6hYA4try7guI-AWZOs,2091
83
83
  modal/snapshot.py,sha256=vIKxA1XRRsH8cFcpSUqC9M8NccKIaYaDpmiEC2clNks,2297
84
84
  modal/snapshot.pyi,sha256=vMYDx1s9Ds4qtH-PXgB_9RzVuS5wsTchVcclR5Q-mmM,2014
85
85
  modal/stream_type.py,sha256=A6320qoAAWhEfwOCZfGtymQTu5AfLfJXXgARqooTPvY,417
86
86
  modal/token_flow.py,sha256=muvMsYAcLB2X1s61K-ChGNnu-vPpQUW1N8IYOrA8eho,7036
87
87
  modal/token_flow.pyi,sha256=5zdXJ5SLLHM4j6xSPuEvnpSeEJBbzE6NkSX-MSyE5wM,2282
88
- modal/volume.py,sha256=AmpakUZKJueLAVzAEkx46rMBAlr8XEWns7FWEGdeznU,55633
89
- modal/volume.pyi,sha256=AkEAVascR5GO30s1cRIZYcF8ehT6jPs-G1miOBUcIRA,55857
88
+ modal/volume.py,sha256=N8QBz-KJ59O79eSOaetatoVO_fev_ywOQZjsnihKu6I,56426
89
+ modal/volume.pyi,sha256=nOTgO1HThjldwxcUSw8S534BMNYooDGK1sI4ySnHQ6o,56189
90
90
  modal/_output/__init__.py,sha256=xtGN6SPv2zvByrJ32JPao0yJKpxyCIZ-2JMTScrisuQ,28
91
91
  modal/_output/manager.py,sha256=8JGUm8S1PfiibvyI1t_pcniktZbe8KAV1TvLtoZ80rk,18161
92
92
  modal/_output/pty.py,sha256=wniYe_MW9_pEhCjDgRyrruYJNNtZoAJ0MuOOl3UrGI0,10174
@@ -96,8 +96,8 @@ modal/_runtime/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
96
96
  modal/_runtime/asgi.py,sha256=1d6PG6pxQAOg1FZTeB6YW_86zAw4OxnY-uOjaaGzyxA,22808
97
97
  modal/_runtime/container_io_manager.py,sha256=EQF1aAtzb0A2chhr0B8es_G0rawbBUKZaFu0-p6dsQU,52102
98
98
  modal/_runtime/container_io_manager.pyi,sha256=qwnmErhulV_2OYRWbw8wM3vw6r2Pub1W98NccrKrXaY,22701
99
- modal/_runtime/execution_context.py,sha256=AYrNQRHHXEqX2MwMf8zxelKZnYf25RE_B-NRLWf93n8,3521
100
- modal/_runtime/execution_context.pyi,sha256=FVzakehz72ndL-ufe8-EC7TM4IHO_MEBcAdgWuU4W9k,2426
99
+ modal/_runtime/execution_context.py,sha256=vanzJeaTpknTB2-DJQIpPjCJGTfn8gqiQlqGOfkxn2g,3665
100
+ modal/_runtime/execution_context.pyi,sha256=i30NeuFxr7wcaqIUnLN9-fh_LqpcMe-KZ6Cn0VXZvcU,2569
101
101
  modal/_runtime/gpu_memory_snapshot.py,sha256=vRLzSVZckD7xYjahrXlRPExuoOdDwTTKIaEMsWDTrzM,12113
102
102
  modal/_runtime/telemetry.py,sha256=T1RoAGyjBDr1swiM6pPsGRSITm7LI5FDK18oNXxY08U,5163
103
103
  modal/_runtime/user_code_event_loop.py,sha256=HwOuDyCZLOakr_DG7p2MrcscjSQNheIhBBuLgB5I3Cc,3014
@@ -124,7 +124,7 @@ modal/_utils/name_utils.py,sha256=CIQL0Y4z47tq0rfNL2Kdo5dxsGrxoN2hR9Ko0HapeE4,25
124
124
  modal/_utils/package_utils.py,sha256=jYE7WFVzYWIfO0ZJhno_BYHb6Skp48uupOiw1a3yWzw,2056
125
125
  modal/_utils/pattern_utils.py,sha256=ZUffaECfe2iYBhH6cvCB-0-UWhmEBTZEl_TwG_So3ag,6714
126
126
  modal/_utils/rand_pb_testing.py,sha256=OYfbSeEXNMgwy-nAi4hHbgZdb08Ss3BV6TyJp6-gwNs,4360
127
- modal/_utils/sandbox_fs_utils.py,sha256=ldHfvdfsTXnMmZHgGlJS-wThCBykLr1oDKJkZ6qo8Xg,12411
127
+ modal/_utils/sandbox_fs_utils.py,sha256=lfYcYA8wQgnUWtSC0UxRzrEThFWOY5ppuA0GJeYzhrg,13914
128
128
  modal/_utils/shell_utils.py,sha256=GjMrO1YxnBr4_UlpA2ZYQKCHxV78QTg0mqxAMHWGTE4,3427
129
129
  modal/_utils/task_command_router_client.py,sha256=t2DCt2zPcULqXZJH6JWnWT_wx-g8DRQpPjnSb9bc5dw,28288
130
130
  modal/_utils/time_utils.py,sha256=PBMHCGzysBVaEnsjc0BSf0bPdSOSXeV11rtTAu5vvus,8451
@@ -143,7 +143,7 @@ modal/builder/README.md,sha256=9tK76KP0Uph7O0M5oUgsSwEZDj5y-dcUPsnpR0Sc-Ik,854
143
143
  modal/builder/base-images.json,sha256=9zgY2-a2U_iEBBe60VNLQvErJQosRhQuXXRqcG6mHGg,1348
144
144
  modal/cli/__init__.py,sha256=6FRleWQxBDT19y7OayO4lBOzuL6Bs9r0rLINYYYbHwQ,769
145
145
  modal/cli/_download.py,sha256=5PQL3tkTJkG-Wb6I0vxfHY4L8kUzd4Fy83jlT6vh9Og,5062
146
- modal/cli/_help.py,sha256=jvho74ebwwXO41bNmcDS6QRGIPznoFB_mT0AI_6K2Xg,9288
146
+ modal/cli/_help.py,sha256=5-qhkf9vdF1ziaupWlfthuWu-roNFJFRLkQzbVdecDg,10034
147
147
  modal/cli/_traceback.py,sha256=QxR4xmA-cmT6Uyh6VLp5fgxwSI13QaF6MM63v2WSZ2U,5982
148
148
  modal/cli/app.py,sha256=9mTkT2JZaGs2_GomWMk_WIXAuP4HelmgyqLUWhc-gwk,21990
149
149
  modal/cli/billing.py,sha256=afXmnVmak5gzPF9n-3v_WQ7jTr2idyZCKk7gKRKuYmo,5727
@@ -162,12 +162,12 @@ modal/cli/logo.py,sha256=plv82syvPb8j3qrR1A_nojZwp823t3VEXaDlrx1jkt0,3221
162
162
  modal/cli/network_file_system.py,sha256=GbbgzZ86iprMfV3e7mmLpwp_JWoBxEp1tB_XeBEt_GE,7588
163
163
  modal/cli/profile.py,sha256=S0DZL42eeXbNgGFxHcQG_KMRH946U7V1wW4EIeRjX04,3308
164
164
  modal/cli/queues.py,sha256=jv3sAK1YGMaXo2G011mvwYIZVQtKJH2HLPI5m4CTniI,6567
165
- modal/cli/run.py,sha256=_8rJAqBhxWNAY8VWUq2ZPoRvigYjTceoGjkjnEWMkw0,22351
165
+ modal/cli/run.py,sha256=9f5vxIrc-0USOavm6yPMpTf0SJFDaKJHrHyRhb1n25A,22650
166
166
  modal/cli/secret.py,sha256=4VLo0Sgnpoxm_qZxNOFZkip92faynowtwGYan9qVckg,8254
167
167
  modal/cli/selector.py,sha256=OJr9Ua3K_l-qu3N3-I8z8oVq8lBakws2-XgfheR5p-c,4741
168
168
  modal/cli/shell.py,sha256=bzFy3G2wW7kSJSD5I-xnR41CghmKeBbZeEVZgWr_oZ0,14724
169
169
  modal/cli/token.py,sha256=Km7V7pHXsNKyjOCEdioQT8-o04Rh2-PzmNpiL1_i8tw,4352
170
- modal/cli/utils.py,sha256=NMlMfTxXRBnaRjRkX-GXAEA2jX90nEIZ4GUzQevwuFc,6370
170
+ modal/cli/utils.py,sha256=CUNFXLcV8EJymZCTXettHDJTDBJi73uHMywayr0ytow,6305
171
171
  modal/cli/volume.py,sha256=tdewchdKd3sUpFof0jfSKDX3tPk-TziSUzxpRbwTVb4,11513
172
172
  modal/cli/programs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
173
173
  modal/cli/programs/run_jupyter.py,sha256=IJw8nds8Cjl9j4dxBqMGxhz-bIyVX0kle7jbt8HFOhM,2688
@@ -176,7 +176,7 @@ modal/experimental/__init__.py,sha256=Wb1YoAKAnNxH_0c2wA0T12I7bSu9PSO2nS_LuLska7
176
176
  modal/experimental/flash.py,sha256=ATGhVO8AleJ-TOe2PGolI5T7zfCEtj0kl_ivMPp7Rqg,33362
177
177
  modal/experimental/flash.pyi,sha256=R4h2nQl7-oiwqq_twCp0YqID_LnPjPGqxywXvxbiLIk,17011
178
178
  modal/experimental/ipython.py,sha256=TrCfmol9LGsRZMeDoeMPx3Hv3BFqQhYnmD_iH0pqdhk,2904
179
- modal-1.4.3.dev11.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
179
+ modal-1.4.3.dev13.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
180
180
  modal_docs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
181
181
  modal_docs/gen_cli_docs.py,sha256=bfUSLscHfoQoKkKjIqJvQMe3fURzqcQK7gF1h9WQLtM,3742
182
182
  modal_docs/gen_cli_docs_main.py,sha256=UPGr7O1DYTIRLwfINe2rbdh89J3cPVTU1OWL-SjxnOQ,198
@@ -194,14 +194,14 @@ modal_proto/api_pb2_grpc.pyi,sha256=qDqqKIs18olWaoBjLOJ8Z6Gn8_GjpNn3Z1Pw1yG_M4I,
194
194
  modal_proto/modal_api_grpc.py,sha256=bBPhTehvCriGANMHxXNxKa58ew_2QbSvO88zhKCr4CE,24177
195
195
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
196
196
  modal_proto/task_command_router_grpc.py,sha256=i5ho152oU3hgfY3DZLXdT44ogcMy7rQUfGcEbmi0vXc,14477
197
- modal_proto/task_command_router_pb2.py,sha256=cFzU-s7Y1cnqUHIVrSFauBQ37Xv_jEQRDTHSNb-70t0,28015
198
- modal_proto/task_command_router_pb2.pyi,sha256=0fjfaucVSziP8VCLMalnqTaSfPCDjwMsWlD3EDQA_sk,30466
197
+ modal_proto/task_command_router_pb2.py,sha256=6TY5umHeqYdFNuRaOoZP2YfUbtCxRHqyi27CBZKLV_M,28101
198
+ modal_proto/task_command_router_pb2.pyi,sha256=PYljoZVw16_d4Hn-9cD6SDssmYsvVqx_HOgEEUGYBLk,31452
199
199
  modal_proto/task_command_router_pb2_grpc.py,sha256=XVmcdxLnK_IY3trlF6nLmzFhqSG-bbYNABC7xIs6fT8,28732
200
200
  modal_proto/task_command_router_pb2_grpc.pyi,sha256=og_J8CxYeAV0ikpHXZMvr_uC15UD3I3fgyyAO2pGm1E,8981
201
- modal_version/__init__.py,sha256=yRwV3aDQyH5zSEI0qg__V1CgMd-dxzN2Y2TPsMMH5vQ,121
201
+ modal_version/__init__.py,sha256=RN59WVKtaDa4sk1gZLix2zOhp6cMeZyiQXIL_DExp_U,121
202
202
  modal_version/__main__.py,sha256=nLUbubxKx7GXNzJIkTB7M7_u8080LyLM0IS0rzFkC_c,119
203
- modal-1.4.3.dev11.dist-info/METADATA,sha256=yBw9sX-TvJ8clh7hc46K5NHNKkAjx90i3qv3OEZcNc0,2339
204
- modal-1.4.3.dev11.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
205
- modal-1.4.3.dev11.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
206
- modal-1.4.3.dev11.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
207
- modal-1.4.3.dev11.dist-info/RECORD,,
203
+ modal-1.4.3.dev13.dist-info/METADATA,sha256=Ei1LsDqnltoMGnOnGkAcSm8M8b5gQMUsbueBXX9whO4,2339
204
+ modal-1.4.3.dev13.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
205
+ modal-1.4.3.dev13.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
206
+ modal-1.4.3.dev13.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
207
+ modal-1.4.3.dev13.dist-info/RECORD,,
@@ -17,7 +17,7 @@ from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
17
17
  from modal_proto import api_pb2 as modal__proto_dot_api__pb2
18
18
 
19
19
 
20
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%modal_proto/task_command_router.proto\x12\x19modal.task_command_router\x1a\x1bgoogle/protobuf/empty.proto\x1a\x15modal_proto/api.proto\"X\n\x1aSandboxStdinWriteV2Request\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0e\n\x06offset\x18\x02 \x01(\x04\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x0b\n\x03\x65of\x18\x04 \x01(\x08\"\x1d\n\x1bSandboxStdinWriteV2Response\"\x8c\x01\n\x19SandboxStdioReadV2Request\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0e\n\x06offset\x18\x02 \x01(\x04\x12N\n\x0f\x66ile_descriptor\x18\x03 \x01(\x0e\x32\x35.modal.task_command_router.SandboxStdioFileDescriptor\"*\n\x1aSandboxStdioReadV2Response\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x83\x02\n\x1aTaskContainerCreateRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x16\n\x0e\x63ontainer_name\x18\x02 \x01(\t\x12\x10\n\x08image_id\x18\x03 \x01(\t\x12\x0c\n\x04\x61rgs\x18\x05 \x03(\t\x12K\n\x03\x65nv\x18\x06 \x03(\x0b\x32>.modal.task_command_router.TaskContainerCreateRequest.EnvEntry\x12\x0f\n\x07workdir\x18\x07 \x01(\t\x12\x12\n\nsecret_ids\x18\x08 \x03(\t\x1a*\n\x08\x45nvEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"K\n\x1bTaskContainerCreateResponse\x12\x14\n\x0c\x63ontainer_id\x18\x01 \x01(\t\x12\x16\n\x0e\x63ontainer_name\x18\x02 \x01(\t\"^\n\x17TaskContainerGetRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x16\n\x0e\x63ontainer_name\x18\x02 \x01(\t\x12\x1a\n\x12include_terminated\x18\x03 \x01(\x08\"[\n\x18TaskContainerGetResponse\x12?\n\tcontainer\x18\x01 \x01(\x0b\x32,.modal.task_command_router.TaskContainerInfo\"~\n\x11TaskContainerInfo\x12\x14\n\x0c\x63ontainer_id\x18\x01 \x01(\t\x12\x16\n\x0e\x63ontainer_name\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\t\x12+\n\x06result\x18\x04 \x01(\x0b\x32\x1b.modal.client.GenericResult\"G\n\x18TaskContainerListRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x1a\n\x12include_terminated\x18\x02 \x01(\x08\"]\n\x19TaskContainerListResponse\x12@\n\ncontainers\x18\x01 \x03(\x0b\x32,.modal.task_command_router.TaskContainerInfo\"F\n\x1dTaskContainerTerminateRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x14\n\x0c\x63ontainer_id\x18\x02 \x01(\t\" \n\x1eTaskContainerTerminateResponse\"R\n\x18TaskContainerWaitRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x14\n\x0c\x63ontainer_id\x18\x02 \x01(\t\x12\x0f\n\x07timeout\x18\x03 \x01(\x02\"H\n\x19TaskContainerWaitResponse\x12+\n\x06result\x18\x01 \x01(\x0b\x32\x1b.modal.client.GenericResult\"7\n\x13TaskExecPollRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07\x65xec_id\x18\x02 \x01(\t\"G\n\x14TaskExecPollResponse\x12\x0e\n\x04\x63ode\x18\x01 \x01(\x05H\x00\x12\x10\n\x06signal\x18\x02 \x01(\x05H\x00\x42\r\n\x0b\x65xit_status\"\x9b\x04\n\x14TaskExecStartRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07\x65xec_id\x18\x02 \x01(\t\x12\x14\n\x0c\x63ommand_args\x18\x03 \x03(\t\x12\x46\n\rstdout_config\x18\x04 \x01(\x0e\x32/.modal.task_command_router.TaskExecStdoutConfig\x12\x46\n\rstderr_config\x18\x05 \x01(\x0e\x32/.modal.task_command_router.TaskExecStderrConfig\x12\x19\n\x0ctimeout_secs\x18\x06 \x01(\rH\x00\x88\x01\x01\x12\x14\n\x07workdir\x18\x07 \x01(\tH\x01\x88\x01\x01\x12\x12\n\nsecret_ids\x18\x08 \x03(\t\x12,\n\x08pty_info\x18\t \x01(\x0b\x32\x15.modal.client.PTYInfoH\x02\x88\x01\x01\x12\x15\n\rruntime_debug\x18\n \x01(\x08\x12\x14\n\x0c\x63ontainer_id\x18\x0b \x01(\t\x12\x45\n\x03\x65nv\x18\x0c \x03(\x0b\x32\x38.modal.task_command_router.TaskExecStartRequest.EnvEntry\x1a*\n\x08\x45nvEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x0f\n\r_timeout_secsB\n\n\x08_workdirB\x0b\n\t_pty_info\"\x17\n\x15TaskExecStartResponse\"h\n\x19TaskExecStdinWriteRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07\x65xec_id\x18\x02 \x01(\t\x12\x0e\n\x06offset\x18\x03 \x01(\x04\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12\x0b\n\x03\x65of\x18\x05 \x01(\x08\"\x1c\n\x1aTaskExecStdinWriteResponse\"\x9d\x01\n\x18TaskExecStdioReadRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07\x65xec_id\x18\x02 \x01(\t\x12\x0e\n\x06offset\x18\x03 \x01(\x04\x12O\n\x0f\x66ile_descriptor\x18\x04 \x01(\x0e\x32\x36.modal.task_command_router.TaskExecStdioFileDescriptor\")\n\x19TaskExecStdioReadResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"7\n\x13TaskExecWaitRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07\x65xec_id\x18\x02 \x01(\t\"G\n\x14TaskExecWaitResponse\x12\x0e\n\x04\x63ode\x18\x01 \x01(\x05H\x00\x12\x10\n\x06signal\x18\x02 \x01(\x05H\x00\x42\r\n\x0b\x65xit_status\"L\n\x19TaskMountDirectoryRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\x0c\x12\x10\n\x08image_id\x18\x03 \x01(\t\"R\n\x1cTaskSnapshotDirectoryRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\x0c\x12\x13\n\x0bsnapshot_id\x18\x03 \x01(\t\"1\n\x1dTaskSnapshotDirectoryResponse\x12\x10\n\x08image_id\x18\x01 \x01(\t\"<\n\x1bTaskUnmountDirectoryRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\x0c*p\n\x1aSandboxStdioFileDescriptor\x12(\n$SANDBOX_STDIO_FILE_DESCRIPTOR_STDOUT\x10\x00\x12(\n$SANDBOX_STDIO_FILE_DESCRIPTOR_STDERR\x10\x01*\x81\x01\n\x14TaskExecStderrConfig\x12#\n\x1fTASK_EXEC_STDERR_CONFIG_DEVNULL\x10\x00\x12 \n\x1cTASK_EXEC_STDERR_CONFIG_PIPE\x10\x01\x12\"\n\x1eTASK_EXEC_STDERR_CONFIG_STDOUT\x10\x02*u\n\x1bTaskExecStdioFileDescriptor\x12*\n&TASK_EXEC_STDIO_FILE_DESCRIPTOR_STDOUT\x10\x00\x12*\n&TASK_EXEC_STDIO_FILE_DESCRIPTOR_STDERR\x10\x01*]\n\x14TaskExecStdoutConfig\x12#\n\x1fTASK_EXEC_STDOUT_CONFIG_DEVNULL\x10\x00\x12 \n\x1cTASK_EXEC_STDOUT_CONFIG_PIPE\x10\x01\x32\xea\x0e\n\x11TaskCommandRouter\x12\x84\x01\n\x13SandboxStdinWriteV2\x12\x35.modal.task_command_router.SandboxStdinWriteV2Request\x1a\x36.modal.task_command_router.SandboxStdinWriteV2Response\x12\x83\x01\n\x12SandboxStdioReadV2\x12\x34.modal.task_command_router.SandboxStdioReadV2Request\x1a\x35.modal.task_command_router.SandboxStdioReadV2Response0\x01\x12\x84\x01\n\x13TaskContainerCreate\x12\x35.modal.task_command_router.TaskContainerCreateRequest\x1a\x36.modal.task_command_router.TaskContainerCreateResponse\x12{\n\x10TaskContainerGet\x12\x32.modal.task_command_router.TaskContainerGetRequest\x1a\x33.modal.task_command_router.TaskContainerGetResponse\x12~\n\x11TaskContainerList\x12\x33.modal.task_command_router.TaskContainerListRequest\x1a\x34.modal.task_command_router.TaskContainerListResponse\x12\x8d\x01\n\x16TaskContainerTerminate\x12\x38.modal.task_command_router.TaskContainerTerminateRequest\x1a\x39.modal.task_command_router.TaskContainerTerminateResponse\x12~\n\x11TaskContainerWait\x12\x33.modal.task_command_router.TaskContainerWaitRequest\x1a\x34.modal.task_command_router.TaskContainerWaitResponse\x12o\n\x0cTaskExecPoll\x12..modal.task_command_router.TaskExecPollRequest\x1a/.modal.task_command_router.TaskExecPollResponse\x12r\n\rTaskExecStart\x12/.modal.task_command_router.TaskExecStartRequest\x1a\x30.modal.task_command_router.TaskExecStartResponse\x12\x81\x01\n\x12TaskExecStdinWrite\x12\x34.modal.task_command_router.TaskExecStdinWriteRequest\x1a\x35.modal.task_command_router.TaskExecStdinWriteResponse\x12\x80\x01\n\x11TaskExecStdioRead\x12\x33.modal.task_command_router.TaskExecStdioReadRequest\x1a\x34.modal.task_command_router.TaskExecStdioReadResponse0\x01\x12o\n\x0cTaskExecWait\x12..modal.task_command_router.TaskExecWaitRequest\x1a/.modal.task_command_router.TaskExecWaitResponse\x12\x62\n\x12TaskMountDirectory\x12\x34.modal.task_command_router.TaskMountDirectoryRequest\x1a\x16.google.protobuf.Empty\x12\x8a\x01\n\x15TaskSnapshotDirectory\x12\x37.modal.task_command_router.TaskSnapshotDirectoryRequest\x1a\x38.modal.task_command_router.TaskSnapshotDirectoryResponse\x12\x66\n\x14TaskUnmountDirectory\x12\x36.modal.task_command_router.TaskUnmountDirectoryRequest\x1a\x16.google.protobuf.EmptyB&Z$github.com/modal-labs/modal/go/protob\x06proto3')
20
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%modal_proto/task_command_router.proto\x12\x19modal.task_command_router\x1a\x1bgoogle/protobuf/empty.proto\x1a\x15modal_proto/api.proto\"X\n\x1aSandboxStdinWriteV2Request\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0e\n\x06offset\x18\x02 \x01(\x04\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x0b\n\x03\x65of\x18\x04 \x01(\x08\"\x1d\n\x1bSandboxStdinWriteV2Response\"\x8c\x01\n\x19SandboxStdioReadV2Request\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0e\n\x06offset\x18\x02 \x01(\x04\x12N\n\x0f\x66ile_descriptor\x18\x03 \x01(\x0e\x32\x35.modal.task_command_router.SandboxStdioFileDescriptor\"*\n\x1aSandboxStdioReadV2Response\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x83\x02\n\x1aTaskContainerCreateRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x16\n\x0e\x63ontainer_name\x18\x02 \x01(\t\x12\x10\n\x08image_id\x18\x03 \x01(\t\x12\x0c\n\x04\x61rgs\x18\x05 \x03(\t\x12K\n\x03\x65nv\x18\x06 \x03(\x0b\x32>.modal.task_command_router.TaskContainerCreateRequest.EnvEntry\x12\x0f\n\x07workdir\x18\x07 \x01(\t\x12\x12\n\nsecret_ids\x18\x08 \x03(\t\x1a*\n\x08\x45nvEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"K\n\x1bTaskContainerCreateResponse\x12\x14\n\x0c\x63ontainer_id\x18\x01 \x01(\t\x12\x16\n\x0e\x63ontainer_name\x18\x02 \x01(\t\"^\n\x17TaskContainerGetRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x16\n\x0e\x63ontainer_name\x18\x02 \x01(\t\x12\x1a\n\x12include_terminated\x18\x03 \x01(\x08\"[\n\x18TaskContainerGetResponse\x12?\n\tcontainer\x18\x01 \x01(\x0b\x32,.modal.task_command_router.TaskContainerInfo\"~\n\x11TaskContainerInfo\x12\x14\n\x0c\x63ontainer_id\x18\x01 \x01(\t\x12\x16\n\x0e\x63ontainer_name\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\t\x12+\n\x06result\x18\x04 \x01(\x0b\x32\x1b.modal.client.GenericResult\"G\n\x18TaskContainerListRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x1a\n\x12include_terminated\x18\x02 \x01(\x08\"]\n\x19TaskContainerListResponse\x12@\n\ncontainers\x18\x01 \x03(\x0b\x32,.modal.task_command_router.TaskContainerInfo\"F\n\x1dTaskContainerTerminateRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x14\n\x0c\x63ontainer_id\x18\x02 \x01(\t\" \n\x1eTaskContainerTerminateResponse\"R\n\x18TaskContainerWaitRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x14\n\x0c\x63ontainer_id\x18\x02 \x01(\t\x12\x0f\n\x07timeout\x18\x03 \x01(\x02\"H\n\x19TaskContainerWaitResponse\x12+\n\x06result\x18\x01 \x01(\x0b\x32\x1b.modal.client.GenericResult\"7\n\x13TaskExecPollRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07\x65xec_id\x18\x02 \x01(\t\"G\n\x14TaskExecPollResponse\x12\x0e\n\x04\x63ode\x18\x01 \x01(\x05H\x00\x12\x10\n\x06signal\x18\x02 \x01(\x05H\x00\x42\r\n\x0b\x65xit_status\"\x9b\x04\n\x14TaskExecStartRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07\x65xec_id\x18\x02 \x01(\t\x12\x14\n\x0c\x63ommand_args\x18\x03 \x03(\t\x12\x46\n\rstdout_config\x18\x04 \x01(\x0e\x32/.modal.task_command_router.TaskExecStdoutConfig\x12\x46\n\rstderr_config\x18\x05 \x01(\x0e\x32/.modal.task_command_router.TaskExecStderrConfig\x12\x19\n\x0ctimeout_secs\x18\x06 \x01(\rH\x00\x88\x01\x01\x12\x14\n\x07workdir\x18\x07 \x01(\tH\x01\x88\x01\x01\x12\x12\n\nsecret_ids\x18\x08 \x03(\t\x12,\n\x08pty_info\x18\t \x01(\x0b\x32\x15.modal.client.PTYInfoH\x02\x88\x01\x01\x12\x15\n\rruntime_debug\x18\n \x01(\x08\x12\x14\n\x0c\x63ontainer_id\x18\x0b \x01(\t\x12\x45\n\x03\x65nv\x18\x0c \x03(\x0b\x32\x38.modal.task_command_router.TaskExecStartRequest.EnvEntry\x1a*\n\x08\x45nvEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x0f\n\r_timeout_secsB\n\n\x08_workdirB\x0b\n\t_pty_info\"\x17\n\x15TaskExecStartResponse\"h\n\x19TaskExecStdinWriteRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07\x65xec_id\x18\x02 \x01(\t\x12\x0e\n\x06offset\x18\x03 \x01(\x04\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12\x0b\n\x03\x65of\x18\x05 \x01(\x08\"\x1c\n\x1aTaskExecStdinWriteResponse\"\x9d\x01\n\x18TaskExecStdioReadRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07\x65xec_id\x18\x02 \x01(\t\x12\x0e\n\x06offset\x18\x03 \x01(\x04\x12O\n\x0f\x66ile_descriptor\x18\x04 \x01(\x0e\x32\x36.modal.task_command_router.TaskExecStdioFileDescriptor\")\n\x19TaskExecStdioReadResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"7\n\x13TaskExecWaitRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07\x65xec_id\x18\x02 \x01(\t\"G\n\x14TaskExecWaitResponse\x12\x0e\n\x04\x63ode\x18\x01 \x01(\x05H\x00\x12\x10\n\x06signal\x18\x02 \x01(\x05H\x00\x42\r\n\x0b\x65xit_status\"L\n\x19TaskMountDirectoryRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\x0c\x12\x10\n\x08image_id\x18\x03 \x01(\t\"|\n\x1cTaskSnapshotDirectoryRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\x0c\x12\x13\n\x0bsnapshot_id\x18\x03 \x01(\t\x12\x18\n\x0bttl_seconds\x18\x04 \x01(\x03H\x00\x88\x01\x01\x42\x0e\n\x0c_ttl_seconds\"1\n\x1dTaskSnapshotDirectoryResponse\x12\x10\n\x08image_id\x18\x01 \x01(\t\"<\n\x1bTaskUnmountDirectoryRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\x0c*p\n\x1aSandboxStdioFileDescriptor\x12(\n$SANDBOX_STDIO_FILE_DESCRIPTOR_STDOUT\x10\x00\x12(\n$SANDBOX_STDIO_FILE_DESCRIPTOR_STDERR\x10\x01*\x81\x01\n\x14TaskExecStderrConfig\x12#\n\x1fTASK_EXEC_STDERR_CONFIG_DEVNULL\x10\x00\x12 \n\x1cTASK_EXEC_STDERR_CONFIG_PIPE\x10\x01\x12\"\n\x1eTASK_EXEC_STDERR_CONFIG_STDOUT\x10\x02*u\n\x1bTaskExecStdioFileDescriptor\x12*\n&TASK_EXEC_STDIO_FILE_DESCRIPTOR_STDOUT\x10\x00\x12*\n&TASK_EXEC_STDIO_FILE_DESCRIPTOR_STDERR\x10\x01*]\n\x14TaskExecStdoutConfig\x12#\n\x1fTASK_EXEC_STDOUT_CONFIG_DEVNULL\x10\x00\x12 \n\x1cTASK_EXEC_STDOUT_CONFIG_PIPE\x10\x01\x32\xea\x0e\n\x11TaskCommandRouter\x12\x84\x01\n\x13SandboxStdinWriteV2\x12\x35.modal.task_command_router.SandboxStdinWriteV2Request\x1a\x36.modal.task_command_router.SandboxStdinWriteV2Response\x12\x83\x01\n\x12SandboxStdioReadV2\x12\x34.modal.task_command_router.SandboxStdioReadV2Request\x1a\x35.modal.task_command_router.SandboxStdioReadV2Response0\x01\x12\x84\x01\n\x13TaskContainerCreate\x12\x35.modal.task_command_router.TaskContainerCreateRequest\x1a\x36.modal.task_command_router.TaskContainerCreateResponse\x12{\n\x10TaskContainerGet\x12\x32.modal.task_command_router.TaskContainerGetRequest\x1a\x33.modal.task_command_router.TaskContainerGetResponse\x12~\n\x11TaskContainerList\x12\x33.modal.task_command_router.TaskContainerListRequest\x1a\x34.modal.task_command_router.TaskContainerListResponse\x12\x8d\x01\n\x16TaskContainerTerminate\x12\x38.modal.task_command_router.TaskContainerTerminateRequest\x1a\x39.modal.task_command_router.TaskContainerTerminateResponse\x12~\n\x11TaskContainerWait\x12\x33.modal.task_command_router.TaskContainerWaitRequest\x1a\x34.modal.task_command_router.TaskContainerWaitResponse\x12o\n\x0cTaskExecPoll\x12..modal.task_command_router.TaskExecPollRequest\x1a/.modal.task_command_router.TaskExecPollResponse\x12r\n\rTaskExecStart\x12/.modal.task_command_router.TaskExecStartRequest\x1a\x30.modal.task_command_router.TaskExecStartResponse\x12\x81\x01\n\x12TaskExecStdinWrite\x12\x34.modal.task_command_router.TaskExecStdinWriteRequest\x1a\x35.modal.task_command_router.TaskExecStdinWriteResponse\x12\x80\x01\n\x11TaskExecStdioRead\x12\x33.modal.task_command_router.TaskExecStdioReadRequest\x1a\x34.modal.task_command_router.TaskExecStdioReadResponse0\x01\x12o\n\x0cTaskExecWait\x12..modal.task_command_router.TaskExecWaitRequest\x1a/.modal.task_command_router.TaskExecWaitResponse\x12\x62\n\x12TaskMountDirectory\x12\x34.modal.task_command_router.TaskMountDirectoryRequest\x1a\x16.google.protobuf.Empty\x12\x8a\x01\n\x15TaskSnapshotDirectory\x12\x37.modal.task_command_router.TaskSnapshotDirectoryRequest\x1a\x38.modal.task_command_router.TaskSnapshotDirectoryResponse\x12\x66\n\x14TaskUnmountDirectory\x12\x36.modal.task_command_router.TaskUnmountDirectoryRequest\x1a\x16.google.protobuf.EmptyB&Z$github.com/modal-labs/modal/go/protob\x06proto3')
21
21
 
22
22
  _SANDBOXSTDIOFILEDESCRIPTOR = DESCRIPTOR.enum_types_by_name['SandboxStdioFileDescriptor']
23
23
  SandboxStdioFileDescriptor = enum_type_wrapper.EnumTypeWrapper(_SANDBOXSTDIOFILEDESCRIPTOR)
@@ -297,14 +297,14 @@ if _descriptor._USE_C_DESCRIPTORS == False:
297
297
  _TASKCONTAINERCREATEREQUEST_ENVENTRY._serialized_options = b'8\001'
298
298
  _TASKEXECSTARTREQUEST_ENVENTRY._options = None
299
299
  _TASKEXECSTARTREQUEST_ENVENTRY._serialized_options = b'8\001'
300
- _SANDBOXSTDIOFILEDESCRIPTOR._serialized_start=2957
301
- _SANDBOXSTDIOFILEDESCRIPTOR._serialized_end=3069
302
- _TASKEXECSTDERRCONFIG._serialized_start=3072
303
- _TASKEXECSTDERRCONFIG._serialized_end=3201
304
- _TASKEXECSTDIOFILEDESCRIPTOR._serialized_start=3203
305
- _TASKEXECSTDIOFILEDESCRIPTOR._serialized_end=3320
306
- _TASKEXECSTDOUTCONFIG._serialized_start=3322
307
- _TASKEXECSTDOUTCONFIG._serialized_end=3415
300
+ _SANDBOXSTDIOFILEDESCRIPTOR._serialized_start=2999
301
+ _SANDBOXSTDIOFILEDESCRIPTOR._serialized_end=3111
302
+ _TASKEXECSTDERRCONFIG._serialized_start=3114
303
+ _TASKEXECSTDERRCONFIG._serialized_end=3243
304
+ _TASKEXECSTDIOFILEDESCRIPTOR._serialized_start=3245
305
+ _TASKEXECSTDIOFILEDESCRIPTOR._serialized_end=3362
306
+ _TASKEXECSTDOUTCONFIG._serialized_start=3364
307
+ _TASKEXECSTDOUTCONFIG._serialized_end=3457
308
308
  _SANDBOXSTDINWRITEV2REQUEST._serialized_start=120
309
309
  _SANDBOXSTDINWRITEV2REQUEST._serialized_end=208
310
310
  _SANDBOXSTDINWRITEV2RESPONSE._serialized_start=210
@@ -362,11 +362,11 @@ if _descriptor._USE_C_DESCRIPTORS == False:
362
362
  _TASKMOUNTDIRECTORYREQUEST._serialized_start=2682
363
363
  _TASKMOUNTDIRECTORYREQUEST._serialized_end=2758
364
364
  _TASKSNAPSHOTDIRECTORYREQUEST._serialized_start=2760
365
- _TASKSNAPSHOTDIRECTORYREQUEST._serialized_end=2842
366
- _TASKSNAPSHOTDIRECTORYRESPONSE._serialized_start=2844
367
- _TASKSNAPSHOTDIRECTORYRESPONSE._serialized_end=2893
368
- _TASKUNMOUNTDIRECTORYREQUEST._serialized_start=2895
369
- _TASKUNMOUNTDIRECTORYREQUEST._serialized_end=2955
370
- _TASKCOMMANDROUTER._serialized_start=3418
371
- _TASKCOMMANDROUTER._serialized_end=5316
365
+ _TASKSNAPSHOTDIRECTORYREQUEST._serialized_end=2884
366
+ _TASKSNAPSHOTDIRECTORYRESPONSE._serialized_start=2886
367
+ _TASKSNAPSHOTDIRECTORYRESPONSE._serialized_end=2935
368
+ _TASKUNMOUNTDIRECTORYREQUEST._serialized_start=2937
369
+ _TASKUNMOUNTDIRECTORYREQUEST._serialized_end=2997
370
+ _TASKCOMMANDROUTER._serialized_start=3460
371
+ _TASKCOMMANDROUTER._serialized_end=5358
372
372
  # @@protoc_insertion_point(module_scope)
@@ -691,18 +691,35 @@ class TaskSnapshotDirectoryRequest(google.protobuf.message.Message):
691
691
  TASK_ID_FIELD_NUMBER: builtins.int
692
692
  PATH_FIELD_NUMBER: builtins.int
693
693
  SNAPSHOT_ID_FIELD_NUMBER: builtins.int
694
+ TTL_SECONDS_FIELD_NUMBER: builtins.int
694
695
  task_id: builtins.str
695
696
  path: builtins.bytes
696
697
  snapshot_id: builtins.str
697
698
  """A unique ID to ensure idempotency of directory snapshot requests."""
699
+ ttl_seconds: builtins.int
700
+ """Lifetime for the resulting image, in seconds. When set, the
701
+ image's expires_at is a hard cutoff: it is fixed at creation time
702
+ and is not extended on use.
703
+
704
+ When unset, the resulting image gets the legacy retention policy:
705
+ a 30-day TTL that is bumped forward by 30 days on every use, so
706
+ active images effectively never expire.
707
+
708
+ Sentinel: -1 means "no expiry" (the image never expires).
709
+ 0 and other negative values are invalid and rejected by the
710
+ server.
711
+ """
698
712
  def __init__(
699
713
  self,
700
714
  *,
701
715
  task_id: builtins.str = ...,
702
716
  path: builtins.bytes = ...,
703
717
  snapshot_id: builtins.str = ...,
718
+ ttl_seconds: builtins.int | None = ...,
704
719
  ) -> None: ...
705
- def ClearField(self, field_name: typing_extensions.Literal["path", b"path", "snapshot_id", b"snapshot_id", "task_id", b"task_id"]) -> None: ...
720
+ def HasField(self, field_name: typing_extensions.Literal["_ttl_seconds", b"_ttl_seconds", "ttl_seconds", b"ttl_seconds"]) -> builtins.bool: ...
721
+ def ClearField(self, field_name: typing_extensions.Literal["_ttl_seconds", b"_ttl_seconds", "path", b"path", "snapshot_id", b"snapshot_id", "task_id", b"task_id", "ttl_seconds", b"ttl_seconds"]) -> None: ...
722
+ def WhichOneof(self, oneof_group: typing_extensions.Literal["_ttl_seconds", b"_ttl_seconds"]) -> typing_extensions.Literal["ttl_seconds"] | None: ...
706
723
 
707
724
  global___TaskSnapshotDirectoryRequest = TaskSnapshotDirectoryRequest
708
725
 
modal_version/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2026
2
2
  """Supplies the current version of the modal client library."""
3
3
 
4
- __version__ = "1.4.3.dev11"
4
+ __version__ = "1.4.3.dev13"