modal 0.62.16__py3-none-any.whl → 0.72.11__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.
Files changed (220) hide show
  1. modal/__init__.py +17 -13
  2. modal/__main__.py +41 -3
  3. modal/_clustered_functions.py +80 -0
  4. modal/_clustered_functions.pyi +22 -0
  5. modal/_container_entrypoint.py +420 -937
  6. modal/_ipython.py +3 -13
  7. modal/_location.py +17 -10
  8. modal/_output.py +243 -99
  9. modal/_pty.py +2 -2
  10. modal/_resolver.py +55 -59
  11. modal/_resources.py +51 -0
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1036 -0
  15. modal/_runtime/execution_context.py +89 -0
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +134 -9
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +52 -16
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +479 -100
  24. modal/_utils/blob_utils.py +157 -186
  25. modal/_utils/bytes_io_segment_payload.py +97 -0
  26. modal/_utils/deprecation.py +89 -0
  27. modal/_utils/docker_utils.py +98 -0
  28. modal/_utils/function_utils.py +460 -171
  29. modal/_utils/grpc_testing.py +47 -31
  30. modal/_utils/grpc_utils.py +62 -109
  31. modal/_utils/hash_utils.py +61 -19
  32. modal/_utils/http_utils.py +39 -9
  33. modal/_utils/logger.py +2 -1
  34. modal/_utils/mount_utils.py +34 -16
  35. modal/_utils/name_utils.py +58 -0
  36. modal/_utils/package_utils.py +14 -1
  37. modal/_utils/pattern_utils.py +205 -0
  38. modal/_utils/rand_pb_testing.py +5 -7
  39. modal/_utils/shell_utils.py +15 -49
  40. modal/_vendor/a2wsgi_wsgi.py +62 -72
  41. modal/_vendor/cloudpickle.py +1 -1
  42. modal/_watcher.py +14 -12
  43. modal/app.py +1003 -314
  44. modal/app.pyi +540 -264
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +63 -53
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +205 -45
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +62 -14
  51. modal/cli/dict.py +128 -0
  52. modal/cli/entry_point.py +26 -13
  53. modal/cli/environment.py +40 -9
  54. modal/cli/import_refs.py +64 -58
  55. modal/cli/launch.py +32 -18
  56. modal/cli/network_file_system.py +64 -83
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +35 -10
  59. modal/cli/programs/vscode.py +60 -10
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +234 -131
  62. modal/cli/secret.py +8 -7
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +79 -10
  65. modal/cli/volume.py +110 -109
  66. modal/client.py +250 -144
  67. modal/client.pyi +157 -118
  68. modal/cloud_bucket_mount.py +108 -34
  69. modal/cloud_bucket_mount.pyi +32 -38
  70. modal/cls.py +535 -148
  71. modal/cls.pyi +190 -146
  72. modal/config.py +41 -19
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +111 -65
  76. modal/dict.pyi +136 -131
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +34 -43
  80. modal/experimental.py +61 -2
  81. modal/extensions/ipython.py +5 -5
  82. modal/file_io.py +537 -0
  83. modal/file_io.pyi +235 -0
  84. modal/file_pattern_matcher.py +197 -0
  85. modal/functions.py +906 -911
  86. modal/functions.pyi +466 -430
  87. modal/gpu.py +57 -44
  88. modal/image.py +1089 -479
  89. modal/image.pyi +584 -228
  90. modal/io_streams.py +434 -0
  91. modal/io_streams.pyi +122 -0
  92. modal/mount.py +314 -101
  93. modal/mount.pyi +241 -235
  94. modal/network_file_system.py +92 -92
  95. modal/network_file_system.pyi +152 -110
  96. modal/object.py +67 -36
  97. modal/object.pyi +166 -143
  98. modal/output.py +63 -0
  99. modal/parallel_map.py +434 -0
  100. modal/parallel_map.pyi +75 -0
  101. modal/partial_function.py +282 -117
  102. modal/partial_function.pyi +222 -129
  103. modal/proxy.py +15 -12
  104. modal/proxy.pyi +3 -8
  105. modal/queue.py +182 -65
  106. modal/queue.pyi +218 -118
  107. modal/requirements/2024.04.txt +29 -0
  108. modal/requirements/2024.10.txt +16 -0
  109. modal/requirements/README.md +21 -0
  110. modal/requirements/base-images.json +22 -0
  111. modal/retries.py +48 -7
  112. modal/runner.py +459 -156
  113. modal/runner.pyi +135 -71
  114. modal/running_app.py +38 -0
  115. modal/sandbox.py +514 -236
  116. modal/sandbox.pyi +397 -169
  117. modal/schedule.py +4 -4
  118. modal/scheduler_placement.py +20 -3
  119. modal/secret.py +56 -31
  120. modal/secret.pyi +62 -42
  121. modal/serving.py +51 -56
  122. modal/serving.pyi +44 -36
  123. modal/stream_type.py +15 -0
  124. modal/token_flow.py +5 -3
  125. modal/token_flow.pyi +37 -32
  126. modal/volume.py +285 -157
  127. modal/volume.pyi +249 -184
  128. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
  129. modal-0.72.11.dist-info/RECORD +174 -0
  130. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
  131. modal_docs/gen_reference_docs.py +3 -1
  132. modal_docs/mdmd/mdmd.py +0 -1
  133. modal_docs/mdmd/signatures.py +5 -2
  134. modal_global_objects/images/base_images.py +28 -0
  135. modal_global_objects/mounts/python_standalone.py +2 -2
  136. modal_proto/__init__.py +1 -1
  137. modal_proto/api.proto +1288 -533
  138. modal_proto/api_grpc.py +856 -456
  139. modal_proto/api_pb2.py +2165 -1157
  140. modal_proto/api_pb2.pyi +8859 -0
  141. modal_proto/api_pb2_grpc.py +1674 -855
  142. modal_proto/api_pb2_grpc.pyi +1416 -0
  143. modal_proto/modal_api_grpc.py +149 -0
  144. modal_proto/modal_options_grpc.py +3 -0
  145. modal_proto/options_pb2.pyi +20 -0
  146. modal_proto/options_pb2_grpc.pyi +7 -0
  147. modal_proto/py.typed +0 -0
  148. modal_version/__init__.py +1 -1
  149. modal_version/_version_generated.py +2 -2
  150. modal/_asgi.py +0 -370
  151. modal/_container_entrypoint.pyi +0 -378
  152. modal/_container_exec.py +0 -128
  153. modal/_sandbox_shell.py +0 -49
  154. modal/shared_volume.py +0 -23
  155. modal/shared_volume.pyi +0 -24
  156. modal/stub.py +0 -783
  157. modal/stub.pyi +0 -332
  158. modal-0.62.16.dist-info/RECORD +0 -198
  159. modal_global_objects/images/conda.py +0 -15
  160. modal_global_objects/images/debian_slim.py +0 -15
  161. modal_global_objects/images/micromamba.py +0 -15
  162. test/__init__.py +0 -1
  163. test/aio_test.py +0 -12
  164. test/async_utils_test.py +0 -262
  165. test/blob_test.py +0 -67
  166. test/cli_imports_test.py +0 -149
  167. test/cli_test.py +0 -659
  168. test/client_test.py +0 -194
  169. test/cls_test.py +0 -630
  170. test/config_test.py +0 -137
  171. test/conftest.py +0 -1420
  172. test/container_app_test.py +0 -32
  173. test/container_test.py +0 -1389
  174. test/cpu_test.py +0 -23
  175. test/decorator_test.py +0 -85
  176. test/deprecation_test.py +0 -34
  177. test/dict_test.py +0 -33
  178. test/e2e_test.py +0 -68
  179. test/error_test.py +0 -7
  180. test/function_serialization_test.py +0 -32
  181. test/function_test.py +0 -653
  182. test/function_utils_test.py +0 -101
  183. test/gpu_test.py +0 -159
  184. test/grpc_utils_test.py +0 -141
  185. test/helpers.py +0 -42
  186. test/image_test.py +0 -669
  187. test/live_reload_test.py +0 -80
  188. test/lookup_test.py +0 -70
  189. test/mdmd_test.py +0 -329
  190. test/mount_test.py +0 -162
  191. test/mounted_files_test.py +0 -329
  192. test/network_file_system_test.py +0 -181
  193. test/notebook_test.py +0 -66
  194. test/object_test.py +0 -41
  195. test/package_utils_test.py +0 -25
  196. test/queue_test.py +0 -97
  197. test/resolver_test.py +0 -58
  198. test/retries_test.py +0 -67
  199. test/runner_test.py +0 -85
  200. test/sandbox_test.py +0 -191
  201. test/schedule_test.py +0 -15
  202. test/scheduler_placement_test.py +0 -29
  203. test/secret_test.py +0 -78
  204. test/serialization_test.py +0 -42
  205. test/stub_composition_test.py +0 -10
  206. test/stub_test.py +0 -360
  207. test/test_asgi_wrapper.py +0 -234
  208. test/token_flow_test.py +0 -18
  209. test/traceback_test.py +0 -135
  210. test/tunnel_test.py +0 -29
  211. test/utils_test.py +0 -88
  212. test/version_test.py +0 -14
  213. test/volume_test.py +0 -341
  214. test/watcher_test.py +0 -30
  215. test/webhook_test.py +0 -146
  216. /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
  217. /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
  218. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
  219. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
  220. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/entry_points.txt +0 -0
modal/cli/config.py CHANGED
@@ -1,9 +1,9 @@
1
1
  # Copyright Modal Labs 2022
2
- import pprint
3
-
4
2
  import typer
3
+ from rich.console import Console
5
4
 
6
5
  from modal.config import _profile, _store_user_config, config
6
+ from modal.environments import Environment
7
7
 
8
8
  config_cli = typer.Typer(
9
9
  name="config",
@@ -17,10 +17,15 @@ config_cli = typer.Typer(
17
17
  )
18
18
 
19
19
 
20
- @config_cli.command(help="Show configuration values for the current profile (debug command).")
21
- def show():
20
+ @config_cli.command(help="Show current configuration values (debugging command).")
21
+ def show(redact: bool = typer.Option(True, help="Redact the `token_secret` value.")):
22
22
  # This is just a test command
23
- pprint.pprint(config.to_dict())
23
+ config_dict = config.to_dict()
24
+ if redact and config_dict.get("token_secret"):
25
+ config_dict["token_secret"] = "***"
26
+
27
+ console = Console()
28
+ console.print(config_dict)
24
29
 
25
30
 
26
31
  SET_DEFAULT_ENV_HELP = """Set the default Modal environment for the active profile
@@ -34,6 +39,8 @@ when running a command that requires an environment.
34
39
 
35
40
  @config_cli.command(help=SET_DEFAULT_ENV_HELP)
36
41
  def set_environment(environment_name: str):
42
+ # Confirm that the environment exists by looking it up
43
+ Environment.lookup(environment_name)
37
44
  _store_user_config({"environment": environment_name})
38
45
  typer.echo(f"New default environment for profile {_profile}: {environment_name}")
39
46
 
modal/cli/container.py CHANGED
@@ -1,28 +1,37 @@
1
1
  # Copyright Modal Labs 2022
2
-
3
- from typing import List, Union
2
+ from typing import Optional, Union
4
3
 
5
4
  import typer
6
5
  from rich.text import Text
7
6
 
8
- from modal._container_exec import container_exec
7
+ from modal._pty import get_pty_info
9
8
  from modal._utils.async_utils import synchronizer
10
- from modal.cli.utils import display_table, timestamp_to_local
9
+ from modal._utils.grpc_utils import retry_transient_errors
10
+ from modal.cli.utils import ENV_OPTION, display_table, is_tty, stream_app_logs, timestamp_to_local
11
11
  from modal.client import _Client
12
+ from modal.config import config
13
+ from modal.container_process import _ContainerProcess
14
+ from modal.environments import ensure_env
15
+ from modal.object import _get_environment_name
16
+ from modal.stream_type import StreamType
12
17
  from modal_proto import api_pb2
13
18
 
14
- container_cli = typer.Typer(name="container", help="Manage running containers.", no_args_is_help=True)
19
+ container_cli = typer.Typer(name="container", help="Manage and connect to running containers.", no_args_is_help=True)
15
20
 
16
21
 
17
22
  @container_cli.command("list")
18
23
  @synchronizer.create_blocking
19
- async def list():
24
+ async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
20
25
  """List all containers that are currently running."""
26
+ env = ensure_env(env)
21
27
  client = await _Client.from_env()
22
- res: api_pb2.TaskListResponse = await client.stub.TaskList(api_pb2.TaskListRequest())
28
+ environment_name = _get_environment_name(env)
29
+ res: api_pb2.TaskListResponse = await client.stub.TaskList(
30
+ api_pb2.TaskListRequest(environment_name=environment_name)
31
+ )
23
32
 
24
33
  column_names = ["Container ID", "App ID", "App Name", "Start Time"]
25
- rows: List[List[Union[Text, str]]] = []
34
+ rows: list[list[Union[Text, str]]] = []
26
35
  res.tasks.sort(key=lambda task: task.started_at, reverse=True)
27
36
  for task_stats in res.tasks:
28
37
  rows.append(
@@ -30,20 +39,59 @@ async def list():
30
39
  task_stats.task_id,
31
40
  task_stats.app_id,
32
41
  task_stats.app_description,
33
- timestamp_to_local(task_stats.started_at) if task_stats.started_at else "Pending",
42
+ timestamp_to_local(task_stats.started_at, json) if task_stats.started_at else "Pending",
34
43
  ]
35
44
  )
36
45
 
37
- display_table(column_names, rows, json=False, title="Active Containers")
46
+ display_table(column_names, rows, json=json, title=f"Active Containers in environment: {environment_name}")
47
+
48
+
49
+ @container_cli.command("logs")
50
+ def logs(container_id: str = typer.Argument(help="Container ID")):
51
+ """Show logs for a specific container, streaming while active."""
52
+ stream_app_logs(task_id=container_id)
38
53
 
39
54
 
40
55
  @container_cli.command("exec")
41
56
  @synchronizer.create_blocking
42
57
  async def exec(
43
- container_id: str = typer.Argument(help="Container ID."),
44
- command: List[str] = typer.Argument(help="A command to run inside the container."),
45
- pty: bool = typer.Option(is_flag=True, default=True, help="Run the command using a PTY."),
58
+ pty: Optional[bool] = typer.Option(default=None, help="Run the command using a PTY."),
59
+ container_id: str = typer.Argument(help="Container ID"),
60
+ command: list[str] = typer.Argument(
61
+ help="A command to run inside the container.\n\n"
62
+ "To pass command-line flags or options, add `--` before the start of your commands. "
63
+ "For example: `modal container exec <id> -- /bin/bash -c 'echo hi'`"
64
+ ),
46
65
  ):
47
66
  """Execute a command in a container."""
67
+
68
+ if pty is None:
69
+ pty = is_tty()
70
+
71
+ client = await _Client.from_env()
72
+
73
+ req = api_pb2.ContainerExecRequest(
74
+ task_id=container_id,
75
+ command=command,
76
+ pty_info=get_pty_info(shell=True) if pty else None,
77
+ runtime_debug=config.get("function_runtime_debug"),
78
+ )
79
+ res: api_pb2.ContainerExecResponse = await client.stub.ContainerExec(req)
80
+
81
+ if pty:
82
+ await _ContainerProcess(res.exec_id, client).attach()
83
+ else:
84
+ # TODO: redirect stderr to its own stream?
85
+ await _ContainerProcess(res.exec_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT).wait()
86
+
87
+
88
+ @container_cli.command("stop")
89
+ @synchronizer.create_blocking
90
+ async def stop(container_id: str = typer.Argument(help="Container ID")):
91
+ """Stop a currently-running container and reassign its in-progress inputs.
92
+
93
+ This will send the container a SIGINT signal that Modal will handle.
94
+ """
48
95
  client = await _Client.from_env()
49
- await container_exec(container_id, command, pty=pty, client=client)
96
+ request = api_pb2.ContainerStopRequest(task_id=container_id)
97
+ await retry_transient_errors(client.stub.ContainerStop, request)
modal/cli/dict.py ADDED
@@ -0,0 +1,128 @@
1
+ # Copyright Modal Labs 2024
2
+ from typing import Optional
3
+
4
+ import typer
5
+ from rich.console import Console
6
+ from typer import Argument, Option, Typer
7
+
8
+ from modal._resolver import Resolver
9
+ from modal._utils.async_utils import synchronizer
10
+ from modal._utils.grpc_utils import retry_transient_errors
11
+ from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table, timestamp_to_local
12
+ from modal.client import _Client
13
+ from modal.dict import _Dict
14
+ from modal.environments import ensure_env
15
+ from modal_proto import api_pb2
16
+
17
+ dict_cli = Typer(
18
+ name="dict",
19
+ no_args_is_help=True,
20
+ help="Manage `modal.Dict` objects and inspect their contents.",
21
+ )
22
+
23
+
24
+ @dict_cli.command(name="create", rich_help_panel="Management")
25
+ @synchronizer.create_blocking
26
+ async def create(name: str, *, env: Optional[str] = ENV_OPTION):
27
+ """Create a named Dict object.
28
+
29
+ Note: This is a no-op when the Dict already exists.
30
+ """
31
+ d = _Dict.from_name(name, environment_name=env, create_if_missing=True)
32
+ client = await _Client.from_env()
33
+ resolver = Resolver(client=client)
34
+ await resolver.load(d)
35
+
36
+
37
+ @dict_cli.command(name="list", rich_help_panel="Management")
38
+ @synchronizer.create_blocking
39
+ async def list_(*, json: bool = False, env: Optional[str] = ENV_OPTION):
40
+ """List all named Dicts."""
41
+ env = ensure_env(env)
42
+ client = await _Client.from_env()
43
+ request = api_pb2.DictListRequest(environment_name=env)
44
+ response = await retry_transient_errors(client.stub.DictList, request)
45
+
46
+ rows = [(d.name, timestamp_to_local(d.created_at, json)) for d in response.dicts]
47
+ display_table(["Name", "Created at"], rows, json)
48
+
49
+
50
+ @dict_cli.command("clear", rich_help_panel="Management")
51
+ @synchronizer.create_blocking
52
+ async def clear(name: str, *, yes: bool = YES_OPTION, env: Optional[str] = ENV_OPTION):
53
+ """Clear the contents of a named Dict by deleting all of its data."""
54
+ d = await _Dict.lookup(name, environment_name=env)
55
+ if not yes:
56
+ typer.confirm(
57
+ f"Are you sure you want to irrevocably delete the contents of modal.Dict '{name}'?",
58
+ default=False,
59
+ abort=True,
60
+ )
61
+ await d.clear()
62
+
63
+
64
+ @dict_cli.command(name="delete", rich_help_panel="Management")
65
+ @synchronizer.create_blocking
66
+ async def delete(name: str, *, yes: bool = YES_OPTION, env: Optional[str] = ENV_OPTION):
67
+ """Delete a named Dict and all of its data."""
68
+ # Lookup first to validate the name, even though delete is a staticmethod
69
+ await _Dict.lookup(name, environment_name=env)
70
+ if not yes:
71
+ typer.confirm(
72
+ f"Are you sure you want to irrevocably delete the modal.Dict '{name}'?",
73
+ default=False,
74
+ abort=True,
75
+ )
76
+ await _Dict.delete(name, environment_name=env)
77
+
78
+
79
+ @dict_cli.command(name="get", rich_help_panel="Inspection")
80
+ @synchronizer.create_blocking
81
+ async def get(name: str, key: str, *, env: Optional[str] = ENV_OPTION):
82
+ """Print the value for a specific key.
83
+
84
+ Note: When using the CLI, keys are always interpreted as having a string type.
85
+ """
86
+ d = await _Dict.lookup(name, environment_name=env)
87
+ console = Console()
88
+ val = await d.get(key)
89
+ console.print(val)
90
+
91
+
92
+ def _display(input: str, use_repr: bool) -> str:
93
+ val = repr(input) if use_repr else str(input)
94
+ return val[:80] + "..." if len(val) > 80 else val
95
+
96
+
97
+ @dict_cli.command(name="items", rich_help_panel="Inspection")
98
+ @synchronizer.create_blocking
99
+ async def items(
100
+ name: str,
101
+ n: int = Argument(default=20, help="Limit the number of entries shown"),
102
+ *,
103
+ all: bool = Option(False, "-a", "--all", help="Ignore N and print all entries in the Dict (may be slow)"),
104
+ use_repr: bool = Option(False, "-r", "--repr", help="Display items using `repr()` to see more details"),
105
+ json: bool = False,
106
+ env: Optional[str] = ENV_OPTION,
107
+ ):
108
+ """Print the contents of a Dict.
109
+
110
+ Note: By default, this command truncates the contents. Use the `N` argument to control the
111
+ amount of data shown or the `--all` option to retrieve the entire Dict, which may be slow.
112
+ """
113
+ d = await _Dict.lookup(name, environment_name=env)
114
+
115
+ i, items = 0, []
116
+ async for key, val in d.items():
117
+ i += 1
118
+ if not json and not all and i > n:
119
+ items.append(("...", "..."))
120
+ break
121
+ else:
122
+ if json:
123
+ display_item = key, val
124
+ else:
125
+ display_item = _display(key, use_repr), _display(val, use_repr) # type: ignore # mypy/issue/12056
126
+ items.append(display_item)
127
+
128
+ display_table(["Key", "Value"], items, json)
modal/cli/entry_point.py CHANGED
@@ -12,10 +12,12 @@ from . import run
12
12
  from .app import app_cli
13
13
  from .config import config_cli
14
14
  from .container import container_cli
15
+ from .dict import dict_cli
15
16
  from .environment import environment_cli
16
17
  from .launch import launch_cli
17
18
  from .network_file_system import nfs_cli
18
19
  from .profile import profile_cli
20
+ from .queues import queue_cli
19
21
  from .secret import secret_cli
20
22
  from .token import _new_token, token_cli
21
23
  from .volume import volume_cli
@@ -81,22 +83,33 @@ async def setup(profile: Optional[str] = None):
81
83
  await _new_token(profile=profile, next_url="/home")
82
84
 
83
85
 
84
- entrypoint_cli_typer.add_typer(app_cli)
85
- entrypoint_cli_typer.add_typer(config_cli)
86
- entrypoint_cli_typer.add_typer(container_cli)
87
- entrypoint_cli_typer.add_typer(environment_cli)
88
- entrypoint_cli_typer.add_typer(launch_cli)
89
- entrypoint_cli_typer.add_typer(nfs_cli)
90
- entrypoint_cli_typer.add_typer(profile_cli)
91
- entrypoint_cli_typer.add_typer(secret_cli)
92
- entrypoint_cli_typer.add_typer(token_cli)
93
- entrypoint_cli_typer.add_typer(volume_cli)
94
-
95
- entrypoint_cli_typer.command("deploy", help="Deploy a Modal stub as an application.", no_args_is_help=True)(run.deploy)
86
+ # Commands
87
+ entrypoint_cli_typer.command("deploy", help="Deploy a Modal application.", no_args_is_help=True)(run.deploy)
96
88
  entrypoint_cli_typer.command("serve", no_args_is_help=True)(run.serve)
97
- entrypoint_cli_typer.command("setup", help="Bootstrap Modal's configuration.")(setup)
98
89
  entrypoint_cli_typer.command("shell")(run.shell)
90
+ entrypoint_cli_typer.add_typer(launch_cli)
91
+
92
+ # Deployments
93
+ entrypoint_cli_typer.add_typer(app_cli, rich_help_panel="Deployments")
94
+ entrypoint_cli_typer.add_typer(container_cli, rich_help_panel="Deployments")
95
+
96
+ # Storage
97
+ entrypoint_cli_typer.add_typer(dict_cli, rich_help_panel="Storage")
98
+ entrypoint_cli_typer.add_typer(nfs_cli, rich_help_panel="Storage")
99
+ entrypoint_cli_typer.add_typer(secret_cli, rich_help_panel="Storage")
100
+ entrypoint_cli_typer.add_typer(queue_cli, rich_help_panel="Storage")
101
+ entrypoint_cli_typer.add_typer(volume_cli, rich_help_panel="Storage")
102
+
103
+ # Configuration
104
+ entrypoint_cli_typer.add_typer(config_cli, rich_help_panel="Configuration")
105
+ entrypoint_cli_typer.add_typer(environment_cli, rich_help_panel="Configuration")
106
+ entrypoint_cli_typer.add_typer(profile_cli, rich_help_panel="Configuration")
107
+ entrypoint_cli_typer.add_typer(token_cli, rich_help_panel="Configuration")
108
+
109
+ # Hide setup from help as it's redundant with modal token new, but nicer for onboarding
110
+ entrypoint_cli_typer.command("setup", help="Bootstrap Modal's configuration.", rich_help_panel="Onboarding")(setup)
99
111
 
112
+ # Special handling for modal run, which is more complicated
100
113
  entrypoint_cli = typer.main.get_command(entrypoint_cli_typer)
101
114
  entrypoint_cli.add_command(run.run, name="run") # type: ignore
102
115
  entrypoint_cli.list_commands(None) # type: ignore
modal/cli/environment.py CHANGED
@@ -1,13 +1,16 @@
1
1
  # Copyright Modal Labs 2023
2
- from typing import Optional
2
+ from typing import Annotated, Optional, Union
3
3
 
4
4
  import typer
5
5
  from click import UsageError
6
- from typing_extensions import Annotated
6
+ from grpclib import GRPCError, Status
7
+ from rich.text import Text
7
8
 
8
9
  from modal import environments
10
+ from modal._utils.name_utils import check_environment_name
9
11
  from modal.cli.utils import display_table
10
12
  from modal.config import config
13
+ from modal.exception import InvalidError
11
14
 
12
15
  ENVIRONMENT_HELP_TEXT = """Create and interact with Environments
13
16
 
@@ -24,7 +27,7 @@ while still being able to deploy changes to a live environment.
24
27
  environment_cli = typer.Typer(name="environment", help=ENVIRONMENT_HELP_TEXT, no_args_is_help=True)
25
28
 
26
29
 
27
- class RenderableBool:
30
+ class RenderableBool(Text):
28
31
  def __init__(self, value: bool):
29
32
  self.value = value
30
33
 
@@ -33,12 +36,21 @@ class RenderableBool:
33
36
 
34
37
 
35
38
  @environment_cli.command(name="list", help="List all environments in the current workspace")
36
- def list(json: Optional[bool] = False):
39
+ def list_(json: Optional[bool] = False):
37
40
  envs = environments.list_environments()
41
+
42
+ # determine which environment is currently active, prioritizing the local default
43
+ # over the server default
44
+ active_env = config.get("environment")
45
+ for env in envs:
46
+ if env.default is True and active_env is None:
47
+ active_env = env.name
48
+
38
49
  table_data = []
39
50
  for item in envs:
40
- is_active = item.name == config.get("environment")
41
- row = [item.name, item.webhook_suffix, is_active if json else RenderableBool(is_active)]
51
+ is_active = item.name == active_env
52
+ is_active_display: Union[Text, str] = str(is_active) if json else RenderableBool(is_active)
53
+ row = [item.name, item.webhook_suffix, is_active_display]
42
54
  table_data.append(row)
43
55
  display_table(["name", "web suffix", "active"], table_data, json=json)
44
56
 
@@ -48,7 +60,14 @@ ENVIRONMENT_CREATE_HELP = """Create a new environment in the current workspace""
48
60
 
49
61
  @environment_cli.command(name="create", help=ENVIRONMENT_CREATE_HELP)
50
62
  def create(name: Annotated[str, typer.Argument(help="Name of the new environment. Must be unique. Case sensitive")]):
51
- environments.create_environment(name)
63
+ check_environment_name(name)
64
+
65
+ try:
66
+ environments.create_environment(name)
67
+ except GRPCError as exc:
68
+ if exc.status == Status.INVALID_ARGUMENT:
69
+ raise InvalidError(exc.message)
70
+ raise
52
71
  typer.echo(f"Environment created: {name}")
53
72
 
54
73
 
@@ -65,7 +84,10 @@ def delete(
65
84
  ):
66
85
  if not confirm:
67
86
  typer.confirm(
68
- f"Are you sure you want to irrevocably delete the environment '{name}' and all its associated apps and secrets?",
87
+ (
88
+ f"Are you sure you want to irrevocably delete the environment '{name}' and"
89
+ " all its associated apps and secrets?"
90
+ ),
69
91
  default=False,
70
92
  abort=True,
71
93
  )
@@ -88,5 +110,14 @@ def update(
88
110
  if set_name is None and set_web_suffix is None:
89
111
  raise UsageError("You need to at least one new property (using --set-name or --set-web-suffix)")
90
112
 
91
- environments.update_environment(current_name, new_name=set_name, new_web_suffix=set_web_suffix)
113
+ if set_name:
114
+ check_environment_name(set_name)
115
+
116
+ try:
117
+ environments.update_environment(current_name, new_name=set_name, new_web_suffix=set_web_suffix)
118
+ except GRPCError as exc:
119
+ if exc.status == Status.INVALID_ARGUMENT:
120
+ raise InvalidError(exc.message)
121
+ raise
122
+
92
123
  typer.echo("Environment updated")