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.
- modal/__init__.py +17 -13
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +420 -937
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -59
- modal/_resources.py +51 -0
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1036 -0
- modal/_runtime/execution_context.py +89 -0
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +134 -9
- modal/_traceback.py +47 -187
- modal/_tunnel.py +52 -16
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +479 -100
- modal/_utils/blob_utils.py +157 -186
- modal/_utils/bytes_io_segment_payload.py +97 -0
- modal/_utils/deprecation.py +89 -0
- modal/_utils/docker_utils.py +98 -0
- modal/_utils/function_utils.py +460 -171
- modal/_utils/grpc_testing.py +47 -31
- modal/_utils/grpc_utils.py +62 -109
- modal/_utils/hash_utils.py +61 -19
- modal/_utils/http_utils.py +39 -9
- modal/_utils/logger.py +2 -1
- modal/_utils/mount_utils.py +34 -16
- modal/_utils/name_utils.py +58 -0
- modal/_utils/package_utils.py +14 -1
- modal/_utils/pattern_utils.py +205 -0
- modal/_utils/rand_pb_testing.py +5 -7
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +14 -12
- modal/app.py +1003 -314
- modal/app.pyi +540 -264
- modal/call_graph.py +7 -6
- modal/cli/_download.py +63 -53
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +205 -45
- modal/cli/config.py +12 -5
- modal/cli/container.py +62 -14
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +64 -58
- modal/cli/launch.py +32 -18
- modal/cli/network_file_system.py +64 -83
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +35 -10
- modal/cli/programs/vscode.py +60 -10
- modal/cli/queues.py +131 -0
- modal/cli/run.py +234 -131
- modal/cli/secret.py +8 -7
- modal/cli/token.py +7 -2
- modal/cli/utils.py +79 -10
- modal/cli/volume.py +110 -109
- modal/client.py +250 -144
- modal/client.pyi +157 -118
- modal/cloud_bucket_mount.py +108 -34
- modal/cloud_bucket_mount.pyi +32 -38
- modal/cls.py +535 -148
- modal/cls.pyi +190 -146
- modal/config.py +41 -19
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +111 -65
- modal/dict.pyi +136 -131
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +34 -43
- modal/experimental.py +61 -2
- modal/extensions/ipython.py +5 -5
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +197 -0
- modal/functions.py +906 -911
- modal/functions.pyi +466 -430
- modal/gpu.py +57 -44
- modal/image.py +1089 -479
- modal/image.pyi +584 -228
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +314 -101
- modal/mount.pyi +241 -235
- modal/network_file_system.py +92 -92
- modal/network_file_system.pyi +152 -110
- modal/object.py +67 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +434 -0
- modal/parallel_map.pyi +75 -0
- modal/partial_function.py +282 -117
- modal/partial_function.pyi +222 -129
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +182 -65
- modal/queue.pyi +218 -118
- modal/requirements/2024.04.txt +29 -0
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +48 -7
- modal/runner.py +459 -156
- modal/runner.pyi +135 -71
- modal/running_app.py +38 -0
- modal/sandbox.py +514 -236
- modal/sandbox.pyi +397 -169
- modal/schedule.py +4 -4
- modal/scheduler_placement.py +20 -3
- modal/secret.py +56 -31
- modal/secret.pyi +62 -42
- modal/serving.py +51 -56
- modal/serving.pyi +44 -36
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +285 -157
- modal/volume.pyi +249 -184
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
- modal-0.72.11.dist-info/RECORD +174 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
- modal_docs/gen_reference_docs.py +3 -1
- modal_docs/mdmd/mdmd.py +0 -1
- modal_docs/mdmd/signatures.py +5 -2
- modal_global_objects/images/base_images.py +28 -0
- modal_global_objects/mounts/python_standalone.py +2 -2
- modal_proto/__init__.py +1 -1
- modal_proto/api.proto +1288 -533
- modal_proto/api_grpc.py +856 -456
- modal_proto/api_pb2.py +2165 -1157
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1674 -855
- modal_proto/api_pb2_grpc.pyi +1416 -0
- modal_proto/modal_api_grpc.py +149 -0
- modal_proto/modal_options_grpc.py +3 -0
- modal_proto/options_pb2.pyi +20 -0
- modal_proto/options_pb2_grpc.pyi +7 -0
- modal_proto/py.typed +0 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +2 -2
- modal/_asgi.py +0 -370
- modal/_container_entrypoint.pyi +0 -378
- modal/_container_exec.py +0 -128
- modal/_sandbox_shell.py +0 -49
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal/stub.py +0 -783
- modal/stub.pyi +0 -332
- modal-0.62.16.dist-info/RECORD +0 -198
- modal_global_objects/images/conda.py +0 -15
- modal_global_objects/images/debian_slim.py +0 -15
- modal_global_objects/images/micromamba.py +0 -15
- test/__init__.py +0 -1
- test/aio_test.py +0 -12
- test/async_utils_test.py +0 -262
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -659
- test/client_test.py +0 -194
- test/cls_test.py +0 -630
- test/config_test.py +0 -137
- test/conftest.py +0 -1420
- test/container_app_test.py +0 -32
- test/container_test.py +0 -1389
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -33
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -653
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -141
- test/helpers.py +0 -42
- test/image_test.py +0 -669
- test/live_reload_test.py +0 -80
- test/lookup_test.py +0 -70
- test/mdmd_test.py +0 -329
- test/mount_test.py +0 -162
- test/mounted_files_test.py +0 -329
- test/network_file_system_test.py +0 -181
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -97
- test/resolver_test.py +0 -58
- test/retries_test.py +0 -67
- test/runner_test.py +0 -85
- test/sandbox_test.py +0 -191
- test/schedule_test.py +0 -15
- test/scheduler_placement_test.py +0 -29
- test/secret_test.py +0 -78
- test/serialization_test.py +0 -42
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -360
- test/test_asgi_wrapper.py +0 -234
- test/token_flow_test.py +0 -18
- test/traceback_test.py +0 -135
- test/tunnel_test.py +0 -29
- test/utils_test.py +0 -88
- test/version_test.py +0 -14
- test/volume_test.py +0 -341
- test/watcher_test.py +0 -30
- test/webhook_test.py +0 -146
- /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
- /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
- {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
|
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
|
-
|
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.
|
7
|
+
from modal._pty import get_pty_info
|
9
8
|
from modal._utils.async_utils import synchronizer
|
10
|
-
from modal.
|
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
|
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
|
-
|
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:
|
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=
|
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
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
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
|
-
|
85
|
-
entrypoint_cli_typer.
|
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
|
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
|
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 ==
|
41
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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")
|