modal 1.0.3.dev10__py3-none-any.whl → 1.2.3.dev7__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.

Potentially problematic release.


This version of modal might be problematic. Click here for more details.

Files changed (160) hide show
  1. modal/__init__.py +0 -2
  2. modal/__main__.py +3 -4
  3. modal/_billing.py +80 -0
  4. modal/_clustered_functions.py +7 -3
  5. modal/_clustered_functions.pyi +15 -3
  6. modal/_container_entrypoint.py +51 -69
  7. modal/_functions.py +508 -240
  8. modal/_grpc_client.py +171 -0
  9. modal/_load_context.py +105 -0
  10. modal/_object.py +81 -21
  11. modal/_output.py +58 -45
  12. modal/_partial_function.py +48 -73
  13. modal/_pty.py +7 -3
  14. modal/_resolver.py +26 -46
  15. modal/_runtime/asgi.py +4 -3
  16. modal/_runtime/container_io_manager.py +358 -220
  17. modal/_runtime/container_io_manager.pyi +296 -101
  18. modal/_runtime/execution_context.py +18 -2
  19. modal/_runtime/execution_context.pyi +64 -7
  20. modal/_runtime/gpu_memory_snapshot.py +262 -57
  21. modal/_runtime/user_code_imports.py +28 -58
  22. modal/_serialization.py +90 -6
  23. modal/_traceback.py +42 -1
  24. modal/_tunnel.pyi +380 -12
  25. modal/_utils/async_utils.py +84 -29
  26. modal/_utils/auth_token_manager.py +111 -0
  27. modal/_utils/blob_utils.py +181 -58
  28. modal/_utils/deprecation.py +19 -0
  29. modal/_utils/function_utils.py +91 -47
  30. modal/_utils/grpc_utils.py +89 -66
  31. modal/_utils/mount_utils.py +26 -1
  32. modal/_utils/name_utils.py +17 -3
  33. modal/_utils/task_command_router_client.py +536 -0
  34. modal/_utils/time_utils.py +34 -6
  35. modal/app.py +256 -88
  36. modal/app.pyi +909 -92
  37. modal/billing.py +5 -0
  38. modal/builder/2025.06.txt +18 -0
  39. modal/builder/PREVIEW.txt +18 -0
  40. modal/builder/base-images.json +58 -0
  41. modal/cli/_download.py +19 -3
  42. modal/cli/_traceback.py +3 -2
  43. modal/cli/app.py +4 -4
  44. modal/cli/cluster.py +15 -7
  45. modal/cli/config.py +5 -3
  46. modal/cli/container.py +7 -6
  47. modal/cli/dict.py +22 -16
  48. modal/cli/entry_point.py +12 -5
  49. modal/cli/environment.py +5 -4
  50. modal/cli/import_refs.py +3 -3
  51. modal/cli/launch.py +102 -5
  52. modal/cli/network_file_system.py +11 -12
  53. modal/cli/profile.py +3 -2
  54. modal/cli/programs/launch_instance_ssh.py +94 -0
  55. modal/cli/programs/run_jupyter.py +1 -1
  56. modal/cli/programs/run_marimo.py +95 -0
  57. modal/cli/programs/vscode.py +1 -1
  58. modal/cli/queues.py +57 -26
  59. modal/cli/run.py +91 -23
  60. modal/cli/secret.py +48 -22
  61. modal/cli/token.py +7 -8
  62. modal/cli/utils.py +4 -7
  63. modal/cli/volume.py +31 -25
  64. modal/client.py +15 -85
  65. modal/client.pyi +183 -62
  66. modal/cloud_bucket_mount.py +5 -3
  67. modal/cloud_bucket_mount.pyi +197 -5
  68. modal/cls.py +200 -126
  69. modal/cls.pyi +446 -68
  70. modal/config.py +29 -11
  71. modal/container_process.py +319 -19
  72. modal/container_process.pyi +190 -20
  73. modal/dict.py +290 -71
  74. modal/dict.pyi +835 -83
  75. modal/environments.py +15 -27
  76. modal/environments.pyi +46 -24
  77. modal/exception.py +14 -2
  78. modal/experimental/__init__.py +194 -40
  79. modal/experimental/flash.py +618 -0
  80. modal/experimental/flash.pyi +380 -0
  81. modal/experimental/ipython.py +11 -7
  82. modal/file_io.py +29 -36
  83. modal/file_io.pyi +251 -53
  84. modal/file_pattern_matcher.py +56 -16
  85. modal/functions.pyi +673 -92
  86. modal/gpu.py +1 -1
  87. modal/image.py +528 -176
  88. modal/image.pyi +1572 -145
  89. modal/io_streams.py +458 -128
  90. modal/io_streams.pyi +433 -52
  91. modal/mount.py +216 -151
  92. modal/mount.pyi +225 -78
  93. modal/network_file_system.py +45 -62
  94. modal/network_file_system.pyi +277 -56
  95. modal/object.pyi +93 -17
  96. modal/parallel_map.py +942 -129
  97. modal/parallel_map.pyi +294 -15
  98. modal/partial_function.py +0 -2
  99. modal/partial_function.pyi +234 -19
  100. modal/proxy.py +17 -8
  101. modal/proxy.pyi +36 -3
  102. modal/queue.py +270 -65
  103. modal/queue.pyi +817 -57
  104. modal/runner.py +115 -101
  105. modal/runner.pyi +205 -49
  106. modal/sandbox.py +512 -136
  107. modal/sandbox.pyi +845 -111
  108. modal/schedule.py +1 -1
  109. modal/secret.py +300 -70
  110. modal/secret.pyi +589 -34
  111. modal/serving.py +7 -11
  112. modal/serving.pyi +7 -8
  113. modal/snapshot.py +11 -8
  114. modal/snapshot.pyi +25 -4
  115. modal/token_flow.py +4 -4
  116. modal/token_flow.pyi +28 -8
  117. modal/volume.py +416 -158
  118. modal/volume.pyi +1117 -121
  119. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +10 -9
  120. modal-1.2.3.dev7.dist-info/RECORD +195 -0
  121. modal_docs/mdmd/mdmd.py +17 -4
  122. modal_proto/api.proto +534 -79
  123. modal_proto/api_grpc.py +337 -1
  124. modal_proto/api_pb2.py +1522 -968
  125. modal_proto/api_pb2.pyi +1619 -134
  126. modal_proto/api_pb2_grpc.py +699 -4
  127. modal_proto/api_pb2_grpc.pyi +226 -14
  128. modal_proto/modal_api_grpc.py +175 -154
  129. modal_proto/sandbox_router.proto +145 -0
  130. modal_proto/sandbox_router_grpc.py +105 -0
  131. modal_proto/sandbox_router_pb2.py +149 -0
  132. modal_proto/sandbox_router_pb2.pyi +333 -0
  133. modal_proto/sandbox_router_pb2_grpc.py +203 -0
  134. modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
  135. modal_proto/task_command_router.proto +144 -0
  136. modal_proto/task_command_router_grpc.py +105 -0
  137. modal_proto/task_command_router_pb2.py +149 -0
  138. modal_proto/task_command_router_pb2.pyi +333 -0
  139. modal_proto/task_command_router_pb2_grpc.py +203 -0
  140. modal_proto/task_command_router_pb2_grpc.pyi +75 -0
  141. modal_version/__init__.py +1 -1
  142. modal/requirements/PREVIEW.txt +0 -16
  143. modal/requirements/base-images.json +0 -26
  144. modal-1.0.3.dev10.dist-info/RECORD +0 -179
  145. modal_proto/modal_options_grpc.py +0 -3
  146. modal_proto/options.proto +0 -19
  147. modal_proto/options_grpc.py +0 -3
  148. modal_proto/options_pb2.py +0 -35
  149. modal_proto/options_pb2.pyi +0 -20
  150. modal_proto/options_pb2_grpc.py +0 -4
  151. modal_proto/options_pb2_grpc.pyi +0 -7
  152. /modal/{requirements → builder}/2023.12.312.txt +0 -0
  153. /modal/{requirements → builder}/2023.12.txt +0 -0
  154. /modal/{requirements → builder}/2024.04.txt +0 -0
  155. /modal/{requirements → builder}/2024.10.txt +0 -0
  156. /modal/{requirements → builder}/README.md +0 -0
  157. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
  158. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
  159. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
  160. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
modal/billing.py ADDED
@@ -0,0 +1,5 @@
1
+ # Copyright Modal Labs 2025
2
+ from ._billing import _workspace_billing_report
3
+ from ._utils.async_utils import synchronize_api
4
+
5
+ workspace_billing_report = synchronize_api(_workspace_billing_report)
@@ -0,0 +1,18 @@
1
+ aiohappyeyeballs==2.6.1
2
+ aiohttp==3.12.7
3
+ aiosignal==1.3.2
4
+ async-timeout==5.0.1 ; python_version < "3.11"
5
+ attrs==25.3.0
6
+ cbor2==5.7.0
7
+ certifi==2025.4.26
8
+ frozenlist==1.6.0
9
+ grpclib==0.4.8
10
+ h2==4.2.0
11
+ hpack==4.1.0
12
+ hyperframe==6.1.0
13
+ idna==3.10
14
+ multidict==6.4.4
15
+ propcache==0.3.1
16
+ protobuf==6.31.1
17
+ typing_extensions==4.13.2
18
+ yarl==1.20.0
@@ -0,0 +1,18 @@
1
+ aiohappyeyeballs==2.6.1
2
+ aiohttp==3.12.7
3
+ aiosignal==1.3.2
4
+ async-timeout==5.0.1 ; python_version < "3.11"
5
+ attrs==25.3.0
6
+ cbor2==5.7.0
7
+ certifi==2025.4.26
8
+ frozenlist==1.6.0
9
+ grpclib==0.4.8
10
+ h2==4.2.0
11
+ hpack==4.1.0
12
+ hyperframe==6.1.0
13
+ idna==3.10
14
+ multidict==6.4.4
15
+ propcache==0.3.1
16
+ protobuf==6.31.1
17
+ typing_extensions==4.13.2
18
+ yarl==1.20.0
@@ -0,0 +1,58 @@
1
+ {
2
+ "debian": {
3
+ "PREVIEW": "bookworm",
4
+ "2025.06": "bookworm",
5
+ "2024.10": "bookworm",
6
+ "2024.04": "bookworm",
7
+ "2023.12": "bullseye"
8
+ },
9
+ "python": {
10
+ "PREVIEW": [
11
+ "3.9.22",
12
+ "3.10.17",
13
+ "3.11.12",
14
+ "3.12.10",
15
+ "3.13.3"
16
+ ],
17
+ "2025.06": [
18
+ "3.9.22",
19
+ "3.10.17",
20
+ "3.11.12",
21
+ "3.12.10",
22
+ "3.13.3"
23
+ ],
24
+ "2024.10": [
25
+ "3.9.20",
26
+ "3.10.15",
27
+ "3.11.10",
28
+ "3.12.6",
29
+ "3.13.0"
30
+ ],
31
+ "2024.04": [
32
+ "3.9.19",
33
+ "3.10.14",
34
+ "3.11.8",
35
+ "3.12.2"
36
+ ],
37
+ "2023.12": [
38
+ "3.9.15",
39
+ "3.10.8",
40
+ "3.11.0",
41
+ "3.12.1"
42
+ ]
43
+ },
44
+ "micromamba": {
45
+ "PREVIEW": "2.1.1-debian12-slim",
46
+ "2025.06": "2.1.1-debian12-slim",
47
+ "2024.10": "1.5.10-bookworm-slim",
48
+ "2024.04": "1.5.8-bookworm-slim",
49
+ "2023.12": "1.3.1-bullseye-slim"
50
+ },
51
+ "package_tools": {
52
+ "PREVIEW": "pip wheel uv",
53
+ "2025.06": "pip wheel uv",
54
+ "2024.10": "pip wheel uv",
55
+ "2024.04": "pip wheel uv",
56
+ "2023.12": "pip"
57
+ }
58
+ }
modal/cli/_download.py CHANGED
@@ -1,6 +1,7 @@
1
1
  # Copyright Modal Labs 2023
2
2
  import asyncio
3
3
  import functools
4
+ import multiprocessing
4
5
  import os
5
6
  import shutil
6
7
  import sys
@@ -23,12 +24,22 @@ async def _volume_download(
23
24
  remote_path: str,
24
25
  local_destination: Path,
25
26
  overwrite: bool,
26
- progress_cb: Callable,
27
+ concurrency: Optional[int] = None,
28
+ progress_cb: Optional[Callable] = None,
27
29
  ):
30
+ if progress_cb is None:
31
+
32
+ def progress_cb(*_, **__):
33
+ pass
34
+
35
+ if concurrency is None:
36
+ concurrency = max(128, 2 * multiprocessing.cpu_count())
37
+
28
38
  is_pipe = local_destination == PIPE_PATH
29
39
 
30
40
  q: asyncio.Queue[tuple[Optional[Path], Optional[FileEntry]]] = asyncio.Queue()
31
- num_consumers = 1 if is_pipe else 10 # concurrency limit for downloading files
41
+ num_consumers = 1 if is_pipe else concurrency # concurrency limit for downloading files
42
+ download_semaphore = asyncio.Semaphore(concurrency)
32
43
 
33
44
  async def producer():
34
45
  iterator: AsyncIterator[FileEntry]
@@ -86,7 +97,12 @@ async def _volume_download(
86
97
 
87
98
  with output_path.open("wb") as fp:
88
99
  if isinstance(volume, _Volume):
89
- b = await volume.read_file_into_fileobj(entry.path, fp, file_progress_cb)
100
+ b = await volume._read_file_into_fileobj(
101
+ path=entry.path,
102
+ fileobj=fp,
103
+ download_semaphore=download_semaphore,
104
+ progress_cb=file_progress_cb,
105
+ )
90
106
  else:
91
107
  b = 0
92
108
  async for chunk in volume.read_file(entry.path):
modal/cli/_traceback.py CHANGED
@@ -6,12 +6,13 @@ import re
6
6
  import warnings
7
7
  from typing import Optional
8
8
 
9
- from rich.console import Console, RenderResult, group
9
+ from rich.console import RenderResult, group
10
10
  from rich.panel import Panel
11
11
  from rich.syntax import Syntax
12
12
  from rich.text import Text
13
13
  from rich.traceback import PathHighlighter, Stack, Traceback, install
14
14
 
15
+ from .._output import make_console
15
16
  from ..exception import DeprecationError, PendingDeprecationError, ServerWarning
16
17
 
17
18
 
@@ -193,7 +194,7 @@ def highlight_modal_warnings() -> None:
193
194
  title=title,
194
195
  title_align="left",
195
196
  )
196
- Console().print(panel)
197
+ make_console().print(panel)
197
198
  else:
198
199
  base_showwarning(warning, category, filename, lineno, file=None, line=None)
199
200
 
modal/cli/app.py CHANGED
@@ -15,7 +15,7 @@ from modal.client import _Client
15
15
  from modal.environments import ensure_env
16
16
  from modal_proto import api_pb2
17
17
 
18
- from .._utils.time_utils import timestamp_to_local
18
+ from .._utils.time_utils import timestamp_to_localized_str
19
19
  from .utils import ENV_OPTION, display_table, get_app_id_from_name, stream_app_logs
20
20
 
21
21
  APP_IDENTIFIER = Argument("", help="App name or ID")
@@ -71,8 +71,8 @@ async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
71
71
  app_stats.description,
72
72
  state,
73
73
  str(app_stats.n_running_tasks),
74
- timestamp_to_local(app_stats.created_at, json),
75
- timestamp_to_local(app_stats.stopped_at, json),
74
+ timestamp_to_localized_str(app_stats.created_at, json),
75
+ timestamp_to_localized_str(app_stats.stopped_at, json),
76
76
  ]
77
77
  )
78
78
 
@@ -217,7 +217,7 @@ async def history(
217
217
 
218
218
  row = [
219
219
  Text(f"v{app_stats.version}", style=style),
220
- Text(timestamp_to_local(app_stats.deployed_at, json), style=style),
220
+ Text(timestamp_to_localized_str(app_stats.deployed_at, json), style=style),
221
221
  Text(app_stats.client_version, style=style),
222
222
  Text(app_stats.deployed_by, style=style),
223
223
  ]
modal/cli/cluster.py CHANGED
@@ -2,13 +2,14 @@
2
2
  from typing import Optional, Union
3
3
 
4
4
  import typer
5
- from rich.console import Console
5
+ from rich.table import Column
6
6
  from rich.text import Text
7
7
 
8
8
  from modal._object import _get_environment_name
9
+ from modal._output import make_console
9
10
  from modal._pty import get_pty_info
10
11
  from modal._utils.async_utils import synchronizer
11
- from modal._utils.time_utils import timestamp_to_local
12
+ from modal._utils.time_utils import timestamp_to_localized_str
12
13
  from modal.cli.utils import ENV_OPTION, display_table, is_tty
13
14
  from modal.client import _Client
14
15
  from modal.config import config
@@ -33,7 +34,12 @@ async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
33
34
  api_pb2.ClusterListRequest(environment_name=environment_name)
34
35
  )
35
36
 
36
- column_names = ["Cluster ID", "App ID", "Start Time", "Nodes"]
37
+ column_names: list[Union[Column, str]] = [
38
+ Column("Cluster ID", min_width=25),
39
+ Column("App ID", min_width=25),
40
+ "Start Time",
41
+ "Nodes",
42
+ ]
37
43
  rows: list[list[Union[Text, str]]] = []
38
44
  res.clusters.sort(key=lambda c: c.started_at, reverse=True)
39
45
 
@@ -42,7 +48,7 @@ async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
42
48
  [
43
49
  c.cluster_id,
44
50
  c.app_id,
45
- timestamp_to_local(c.started_at, json) if c.started_at else "Pending",
51
+ timestamp_to_localized_str(c.started_at, json) if c.started_at else "Pending",
46
52
  str(len(c.task_ids)),
47
53
  ]
48
54
  )
@@ -62,7 +68,7 @@ async def shell(
62
68
  if len(res.cluster.task_ids) <= rank:
63
69
  raise typer.Abort(f"No node with rank {rank} in cluster {cluster_id}")
64
70
  task_id = res.cluster.task_ids[rank]
65
- console = Console()
71
+ console = make_console()
66
72
  is_main = "(main)" if rank == 0 else ""
67
73
  console.print(
68
74
  f"Opening shell to node {rank} {is_main} of cluster {cluster_id} (container {task_id})", style="green"
@@ -77,7 +83,9 @@ async def shell(
77
83
  )
78
84
  exec_res: api_pb2.ContainerExecResponse = await client.stub.ContainerExec(req)
79
85
  if pty:
80
- await _ContainerProcess(exec_res.exec_id, client).attach()
86
+ await _ContainerProcess(exec_res.exec_id, task_id, client).attach()
81
87
  else:
82
88
  # TODO: redirect stderr to its own stream?
83
- await _ContainerProcess(exec_res.exec_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT).wait()
89
+ await _ContainerProcess(
90
+ exec_res.exec_id, task_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT
91
+ ).wait()
modal/cli/config.py CHANGED
@@ -1,7 +1,9 @@
1
1
  # Copyright Modal Labs 2022
2
+ import json
3
+
2
4
  import typer
3
- from rich.console import Console
4
5
 
6
+ from modal._output import make_console
5
7
  from modal.config import _profile, _store_user_config, config
6
8
  from modal.environments import Environment
7
9
 
@@ -24,8 +26,8 @@ def show(redact: bool = typer.Option(True, help="Redact the `token_secret` value
24
26
  if redact and config_dict.get("token_secret"):
25
27
  config_dict["token_secret"] = "***"
26
28
 
27
- console = Console()
28
- console.print(config_dict)
29
+ console = make_console()
30
+ console.print_json(json.dumps(config_dict))
29
31
 
30
32
 
31
33
  SET_DEFAULT_ENV_HELP = """Set the default Modal environment for the active profile
modal/cli/container.py CHANGED
@@ -7,8 +7,7 @@ from rich.text import Text
7
7
  from modal._object import _get_environment_name
8
8
  from modal._pty import get_pty_info
9
9
  from modal._utils.async_utils import synchronizer
10
- from modal._utils.grpc_utils import retry_transient_errors
11
- from modal._utils.time_utils import timestamp_to_local
10
+ from modal._utils.time_utils import timestamp_to_localized_str
12
11
  from modal.cli.utils import ENV_OPTION, display_table, is_tty, stream_app_logs
13
12
  from modal.client import _Client
14
13
  from modal.config import config
@@ -40,7 +39,7 @@ async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
40
39
  task_stats.task_id,
41
40
  task_stats.app_id,
42
41
  task_stats.app_description,
43
- timestamp_to_local(task_stats.started_at, json) if task_stats.started_at else "Pending",
42
+ timestamp_to_localized_str(task_stats.started_at, json) if task_stats.started_at else "Pending",
44
43
  ]
45
44
  )
46
45
 
@@ -80,10 +79,12 @@ async def exec(
80
79
  res: api_pb2.ContainerExecResponse = await client.stub.ContainerExec(req)
81
80
 
82
81
  if pty:
83
- await _ContainerProcess(res.exec_id, client).attach()
82
+ await _ContainerProcess(res.exec_id, container_id, client).attach()
84
83
  else:
85
84
  # TODO: redirect stderr to its own stream?
86
- await _ContainerProcess(res.exec_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT).wait()
85
+ await _ContainerProcess(
86
+ res.exec_id, container_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT
87
+ ).wait()
87
88
 
88
89
 
89
90
  @container_cli.command("stop")
@@ -95,4 +96,4 @@ async def stop(container_id: str = typer.Argument(help="Container ID")):
95
96
  """
96
97
  client = await _Client.from_env()
97
98
  request = api_pb2.ContainerStopRequest(task_id=container_id)
98
- await retry_transient_errors(client.stub.ContainerStop, request)
99
+ await client.stub.ContainerStop(request)
modal/cli/dict.py CHANGED
@@ -2,18 +2,17 @@
2
2
  from typing import Optional
3
3
 
4
4
  import typer
5
- from rich.console import Console
6
5
  from typer import Argument, Option, Typer
7
6
 
7
+ from modal._load_context import LoadContext
8
+ from modal._output import make_console
8
9
  from modal._resolver import Resolver
9
10
  from modal._utils.async_utils import synchronizer
10
- from modal._utils.grpc_utils import retry_transient_errors
11
- from modal._utils.time_utils import timestamp_to_local
11
+ from modal._utils.time_utils import timestamp_to_localized_str
12
12
  from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
13
13
  from modal.client import _Client
14
14
  from modal.dict import _Dict
15
15
  from modal.environments import ensure_env
16
- from modal_proto import api_pb2
17
16
 
18
17
  dict_cli = Typer(
19
18
  name="dict",
@@ -31,8 +30,10 @@ async def create(name: str, *, env: Optional[str] = ENV_OPTION):
31
30
  """
32
31
  d = _Dict.from_name(name, environment_name=env, create_if_missing=True)
33
32
  client = await _Client.from_env()
34
- resolver = Resolver(client=client)
35
- await resolver.load(d)
33
+ resolver = Resolver()
34
+
35
+ load_context = LoadContext(client=client, environment_name=env)
36
+ await resolver.load(d, load_context)
36
37
 
37
38
 
38
39
  @dict_cli.command(name="list", rich_help_panel="Management")
@@ -40,12 +41,13 @@ async def create(name: str, *, env: Optional[str] = ENV_OPTION):
40
41
  async def list_(*, json: bool = False, env: Optional[str] = ENV_OPTION):
41
42
  """List all named Dicts."""
42
43
  env = ensure_env(env)
43
- client = await _Client.from_env()
44
- request = api_pb2.DictListRequest(environment_name=env)
45
- response = await retry_transient_errors(client.stub.DictList, request)
44
+ dicts = await _Dict.objects.list(environment_name=env)
45
+ rows = []
46
+ for obj in dicts:
47
+ info = await obj.info()
48
+ rows.append((info.name, timestamp_to_localized_str(info.created_at.timestamp(), json), info.created_by))
46
49
 
47
- rows = [(d.name, timestamp_to_local(d.created_at, json)) for d in response.dicts]
48
- display_table(["Name", "Created at"], rows, json)
50
+ display_table(["Name", "Created at", "Created by"], rows, json)
49
51
 
50
52
 
51
53
  @dict_cli.command("clear", rich_help_panel="Management")
@@ -64,17 +66,21 @@ async def clear(name: str, *, yes: bool = YES_OPTION, env: Optional[str] = ENV_O
64
66
 
65
67
  @dict_cli.command(name="delete", rich_help_panel="Management")
66
68
  @synchronizer.create_blocking
67
- async def delete(name: str, *, yes: bool = YES_OPTION, env: Optional[str] = ENV_OPTION):
69
+ async def delete(
70
+ name: str,
71
+ *,
72
+ allow_missing: bool = Option(False, "--allow-missing", help="Don't error if the Dict doesn't exist."),
73
+ yes: bool = YES_OPTION,
74
+ env: Optional[str] = ENV_OPTION,
75
+ ):
68
76
  """Delete a named Dict and all of its data."""
69
- # Lookup first to validate the name, even though delete is a staticmethod
70
- await _Dict.from_name(name, environment_name=env).hydrate()
71
77
  if not yes:
72
78
  typer.confirm(
73
79
  f"Are you sure you want to irrevocably delete the modal.Dict '{name}'?",
74
80
  default=False,
75
81
  abort=True,
76
82
  )
77
- await _Dict.delete(name, environment_name=env)
83
+ await _Dict.objects.delete(name, environment_name=env, allow_missing=allow_missing)
78
84
 
79
85
 
80
86
  @dict_cli.command(name="get", rich_help_panel="Inspection")
@@ -85,7 +91,7 @@ async def get(name: str, key: str, *, env: Optional[str] = ENV_OPTION):
85
91
  Note: When using the CLI, keys are always interpreted as having a string type.
86
92
  """
87
93
  d = _Dict.from_name(name, environment_name=env)
88
- console = Console()
94
+ console = make_console()
89
95
  val = await d.get(key)
90
96
  console.print(val)
91
97
 
modal/cli/entry_point.py CHANGED
@@ -3,9 +3,9 @@ import subprocess
3
3
  from typing import Optional
4
4
 
5
5
  import typer
6
- from rich.console import Console
7
6
  from rich.rule import Rule
8
7
 
8
+ from modal._output import make_console
9
9
  from modal._utils.async_utils import synchronizer
10
10
 
11
11
  from . import run
@@ -33,9 +33,10 @@ def version_callback(value: bool):
33
33
 
34
34
 
35
35
  entrypoint_cli_typer = typer.Typer(
36
- no_args_is_help=True,
36
+ no_args_is_help=False,
37
37
  add_completion=False,
38
38
  rich_markup_mode="markdown",
39
+ context_settings={"help_option_names": ["-h", "--help"]},
39
40
  help="""
40
41
  Modal is the fastest way to run code in the cloud.
41
42
 
@@ -45,12 +46,18 @@ entrypoint_cli_typer = typer.Typer(
45
46
  )
46
47
 
47
48
 
48
- @entrypoint_cli_typer.callback()
49
+ @entrypoint_cli_typer.callback(invoke_without_command=True)
49
50
  def modal(
50
51
  ctx: typer.Context,
51
52
  version: bool = typer.Option(None, "--version", callback=version_callback),
52
53
  ):
53
- pass
54
+ # TODO: When https://github.com/fastapi/typer/pull/1240 gets shipped, then
55
+ # - set invoke_without_command=False in the callback decorator
56
+ # - set no_args_is_help=True in entrypoint_cli_typer
57
+ if ctx.invoked_subcommand is None:
58
+ console = make_console()
59
+ console.print(ctx.get_help())
60
+ raise typer.Exit()
54
61
 
55
62
 
56
63
  def check_path():
@@ -71,7 +78,7 @@ def check_path():
71
78
  "You may need to give it permissions or use `[white]python -m modal[/white]` as a workaround.[/red]\n"
72
79
  )
73
80
  text += f"See more information here:\n\n[link={url}]{url}[/link]\n"
74
- console = Console()
81
+ console = make_console()
75
82
  console.print(text)
76
83
  console.print(Rule(style="white"))
77
84
 
modal/cli/environment.py CHANGED
@@ -8,7 +8,7 @@ from rich.text import Text
8
8
 
9
9
  from modal import environments
10
10
  from modal._utils.name_utils import check_environment_name
11
- from modal.cli.utils import display_table
11
+ from modal.cli.utils import YES_OPTION, display_table
12
12
  from modal.config import config
13
13
  from modal.exception import InvalidError
14
14
 
@@ -80,13 +80,14 @@ Deletes all apps in the selected environment and deletes the environment irrevoc
80
80
  @environment_cli.command(name="delete", help=ENVIRONMENT_DELETE_HELP)
81
81
  def delete(
82
82
  name: str = typer.Argument(help="Name of the environment to be deleted. Case sensitive"),
83
- confirm: bool = typer.Option(default=False, help="Set this flag to delete without prompting for confirmation"),
83
+ *,
84
+ yes: bool = YES_OPTION,
84
85
  ):
85
- if not confirm:
86
+ if not yes:
86
87
  typer.confirm(
87
88
  (
88
89
  f"Are you sure you want to irrevocably delete the environment '{name}' and"
89
- " all its associated apps and secrets?"
90
+ " all its associated Apps, Secrets, Volumes, Dicts and Queues?"
90
91
  ),
91
92
  default=False,
92
93
  abort=True,
modal/cli/import_refs.py CHANGED
@@ -19,9 +19,9 @@ from pathlib import Path
19
19
  from typing import Optional, Union, cast
20
20
 
21
21
  import click
22
- from rich.console import Console
23
22
  from rich.markdown import Markdown
24
23
 
24
+ from modal._output import make_console
25
25
  from modal._utils.deprecation import deprecation_warning
26
26
  from modal.app import App, LocalEntrypoint
27
27
  from modal.cls import Cls
@@ -258,7 +258,7 @@ def import_app_from_ref(import_ref: ImportRef, base_cmd: str = "") -> App:
258
258
  app = getattr(module, object_path)
259
259
 
260
260
  if app is None:
261
- error_console = Console(stderr=True)
261
+ error_console = make_console(stderr=True)
262
262
  error_console.print(f"[bold red]Could not find Modal app '{object_path}' in {import_path}.[/bold red]")
263
263
 
264
264
  if not object_path:
@@ -282,7 +282,7 @@ def import_app_from_ref(import_ref: ImportRef, base_cmd: str = "") -> App:
282
282
  def _show_function_ref_help(app_ref: ImportRef, base_cmd: str) -> None:
283
283
  object_path = app_ref.object_path
284
284
  import_path = app_ref.file_or_module
285
- error_console = Console(stderr=True)
285
+ error_console = make_console(stderr=True)
286
286
  if object_path:
287
287
  error_console.print(
288
288
  f"[bold red]Could not find Modal function or local entrypoint"
modal/cli/launch.py CHANGED
@@ -3,11 +3,16 @@ import asyncio
3
3
  import inspect
4
4
  import json
5
5
  import os
6
+ import subprocess
7
+ import tempfile
6
8
  from pathlib import Path
7
9
  from typing import Any, Optional
8
10
 
11
+ import rich.panel
12
+ from rich.markdown import Markdown
9
13
  from typer import Typer
10
14
 
15
+ from .._output import make_console
11
16
  from ..exception import _CliUserExecutionError
12
17
  from ..output import enable_output
13
18
  from ..runner import run_app
@@ -16,15 +21,24 @@ from .import_refs import ImportRef, _get_runnable_app, import_file_or_module
16
21
  launch_cli = Typer(
17
22
  name="launch",
18
23
  no_args_is_help=True,
24
+ rich_markup_mode="markdown",
19
25
  help="""
20
- Open a serverless app instance on Modal.
21
-
22
- This command is in preview and may change in the future.
26
+ [Experimental] Open a serverless app instance on Modal.
23
27
  """,
24
28
  )
25
29
 
26
30
 
27
- def _launch_program(name: str, filename: str, detach: bool, args: dict[str, Any]) -> None:
31
+ def _launch_program(
32
+ name: str, filename: str, detach: bool, args: dict[str, Any], *, description: Optional[str] = None
33
+ ) -> None:
34
+ console = make_console()
35
+ console.print(
36
+ rich.panel.Panel(
37
+ Markdown(f"⚠️ `modal launch {name}` is **experimental** and may change in the future."),
38
+ border_style="yellow",
39
+ ),
40
+ )
41
+
28
42
  os.environ["MODAL_LAUNCH_ARGS"] = json.dumps(args)
29
43
 
30
44
  program_path = str(Path(__file__).parent / "programs" / filename)
@@ -33,7 +47,7 @@ def _launch_program(name: str, filename: str, detach: bool, args: dict[str, Any]
33
47
  entrypoint = module.main
34
48
 
35
49
  app = _get_runnable_app(entrypoint)
36
- app.set_description(base_cmd)
50
+ app.set_description(description if description else base_cmd)
37
51
 
38
52
  # `launch/` scripts must have a `local_entrypoint()` with no args, for simplicity here.
39
53
  func = entrypoint.info.raw_f
@@ -61,6 +75,17 @@ def jupyter(
61
75
  volume: Optional[str] = None, # Attach a persisted `modal.Volume` by name (creating if missing).
62
76
  detach: bool = False, # Run the app in "detached" mode to persist after local client disconnects
63
77
  ):
78
+ console = make_console()
79
+ console.print(
80
+ rich.panel.Panel(
81
+ (
82
+ "[link=https://modal.com/notebooks]Try Modal Notebooks! "
83
+ "modal.com/notebooks[/link]\n"
84
+ "Notebooks have a new UI, saved content, real-time collaboration and more."
85
+ ),
86
+ ),
87
+ style="bold cyan",
88
+ )
64
89
  args = {
65
90
  "cpu": cpu,
66
91
  "memory": memory,
@@ -95,3 +120,75 @@ def vscode(
95
120
  "volume": volume,
96
121
  }
97
122
  _launch_program("vscode", "vscode.py", detach, args)
123
+
124
+
125
+ @launch_cli.command(name="machine", help="Start an instance on Modal, with direct SSH access.", hidden=True)
126
+ def machine(
127
+ name: str, # Name of the machine App.
128
+ cpu: int = 8, # Reservation of CPU cores (can burst above this value).
129
+ memory: int = 32768, # Reservation of memory in MiB (can burst above this value).
130
+ gpu: Optional[str] = None, # GPU type and count, e.g. "t4" or "h100:2".
131
+ image: Optional[str] = None, # Image tag to use from registry. Defaults to the notebook base image.
132
+ timeout: int = 3600 * 24, # Timeout in seconds for the instance.
133
+ volume: str = "machine-vol", # Attach a persisted `modal.Volume` at /workspace (created if missing).
134
+ ):
135
+ tempdir = Path(tempfile.gettempdir())
136
+ key_path = tempdir / "modal-machine-keyfile.pem"
137
+ # Generate a new SSH key pair for this machine instance.
138
+ if not key_path.exists():
139
+ subprocess.run(
140
+ ["ssh-keygen", "-t", "ed25519", "-f", str(key_path), "-N", ""],
141
+ check=True,
142
+ stdout=subprocess.DEVNULL,
143
+ )
144
+ # Add the key with expiry 1d to ssh agent.
145
+ subprocess.run(
146
+ ["ssh-add", "-t", "1d", str(key_path)],
147
+ check=True,
148
+ stdout=subprocess.DEVNULL,
149
+ stderr=subprocess.DEVNULL,
150
+ )
151
+
152
+ os.environ["SSH_PUBLIC_KEY"] = Path(str(key_path) + ".pub").read_text()
153
+ os.environ["MODAL_LOGS_TIMEOUT"] = "0" # hack to work with --detach
154
+
155
+ args = {
156
+ "cpu": cpu,
157
+ "memory": memory,
158
+ "gpu": gpu,
159
+ "image": image,
160
+ "timeout": timeout,
161
+ "volume": volume,
162
+ }
163
+ _launch_program(
164
+ "machine",
165
+ "launch_instance_ssh.py",
166
+ True,
167
+ args,
168
+ description=name,
169
+ )
170
+
171
+
172
+ @launch_cli.command(name="marimo", help="Start a remote Marimo notebook on Modal.", hidden=True)
173
+ def marimo(
174
+ cpu: int = 8,
175
+ memory: int = 32768,
176
+ gpu: Optional[str] = None,
177
+ image: str = "debian:12",
178
+ timeout: int = 3600,
179
+ add_python: Optional[str] = "3.12",
180
+ mount: Optional[str] = None, # Create a `modal.Mount` from a local directory.
181
+ volume: Optional[str] = None, # Attach a persisted `modal.Volume` by name (creating if missing).
182
+ detach: bool = False, # Run the app in "detached" mode to persist after local client disconnects
183
+ ):
184
+ args = {
185
+ "cpu": cpu,
186
+ "memory": memory,
187
+ "gpu": gpu,
188
+ "timeout": timeout,
189
+ "image": image,
190
+ "add_python": add_python,
191
+ "mount": mount,
192
+ "volume": volume,
193
+ }
194
+ _launch_program("marimo", "run_marimo.py", detach, args)