modAL 1.4.3.dev2__py3-none-any.whl → 1.4.3.dev4__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/_utils/blob_utils.py +73 -6
- modal/cli/_help.py +264 -0
- modal/cli/_traceback.py +1 -2
- modal/cli/app.py +74 -58
- modal/cli/billing.py +27 -21
- modal/cli/bootstrap.py +15 -11
- modal/cli/changelog.py +21 -18
- modal/cli/cluster.py +12 -11
- modal/cli/config.py +13 -8
- modal/cli/container.py +57 -43
- modal/cli/dashboard.py +6 -2
- modal/cli/dict.py +59 -24
- modal/cli/entry_point.py +50 -63
- modal/cli/environment.py +26 -19
- modal/cli/launch.py +39 -15
- modal/cli/network_file_system.py +56 -45
- modal/cli/profile.py +12 -8
- modal/cli/queues.py +62 -40
- modal/cli/run.py +72 -35
- modal/cli/secret.py +38 -16
- modal/cli/shell.py +98 -99
- modal/cli/token.py +27 -31
- modal/cli/utils.py +12 -5
- modal/cli/volume.py +89 -80
- modal/client.pyi +2 -2
- modal/functions.pyi +6 -6
- modal/mount.py +37 -27
- modal/volume.py +3 -0
- {modal-1.4.3.dev2.dist-info → modal-1.4.3.dev4.dist-info}/METADATA +1 -2
- {modal-1.4.3.dev2.dist-info → modal-1.4.3.dev4.dist-info}/RECORD +36 -35
- modal_docs/gen_cli_docs.py +1 -1
- modal_version/__init__.py +1 -1
- {modal-1.4.3.dev2.dist-info → modal-1.4.3.dev4.dist-info}/WHEEL +0 -0
- {modal-1.4.3.dev2.dist-info → modal-1.4.3.dev4.dist-info}/entry_points.txt +0 -0
- {modal-1.4.3.dev2.dist-info → modal-1.4.3.dev4.dist-info}/licenses/LICENSE +0 -0
- {modal-1.4.3.dev2.dist-info → modal-1.4.3.dev4.dist-info}/top_level.txt +0 -0
modal/cli/billing.py
CHANGED
|
@@ -4,42 +4,48 @@ from json import dumps
|
|
|
4
4
|
from typing import Optional
|
|
5
5
|
|
|
6
6
|
import click
|
|
7
|
-
import typer
|
|
8
7
|
|
|
9
8
|
from modal._billing import _workspace_billing_report
|
|
10
9
|
from modal._utils.async_utils import synchronizer
|
|
11
10
|
from modal._utils.time_utils import parse_date, parse_date_range, resolve_timezone
|
|
12
11
|
|
|
12
|
+
from ._help import ModalGroup
|
|
13
13
|
from .utils import display_table
|
|
14
14
|
|
|
15
|
-
billing_cli =
|
|
15
|
+
billing_cli = ModalGroup(name="billing", help="View workspace billing information.")
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
DATE_HELP = "Date (in UTC by default): ISO format (2025-01-01) or relative (yesterday, 3 days ago, etc.)."
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@billing_cli.command("report", no_args_is_help=True)
|
|
22
|
+
@click.option("--start", default=None, help=f"Start date. {DATE_HELP}")
|
|
23
|
+
@click.option("--end", default=None, help=f"End date. {DATE_HELP} Defaults to now.")
|
|
24
|
+
@click.option(
|
|
25
|
+
"--for",
|
|
26
|
+
"for_",
|
|
27
|
+
default=None,
|
|
28
|
+
help="Convenience range: today, yesterday, this week, last week, this month, last month.",
|
|
29
|
+
)
|
|
30
|
+
@click.option("-r", "--resolution", default="d", help="Time resolution: 'd' (daily) or 'h' (hourly).")
|
|
31
|
+
@click.option(
|
|
32
|
+
"--tz",
|
|
33
|
+
default=None,
|
|
34
|
+
help="Timezone for date interpretation: 'local', offset (5, -4, +05:30), or IANA name. Requires hourly resolution.",
|
|
35
|
+
)
|
|
36
|
+
@click.option("-t", "--tag-names", default=None, help="Comma-separated list of tag names to include.")
|
|
37
|
+
@click.option("--json", "json", is_flag=True, default=False, help="Output as JSON.")
|
|
38
|
+
@click.option("--csv", "csv", is_flag=True, default=False, help="Output as CSV.")
|
|
22
39
|
@synchronizer.create_blocking
|
|
23
40
|
async def report(
|
|
24
|
-
start: Optional[str]
|
|
25
|
-
end: Optional[str]
|
|
26
|
-
for_: Optional[str]
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"--tz",
|
|
33
|
-
help=(
|
|
34
|
-
"Timezone for date interpretation: 'local', offset (5, -4, +05:30), or IANA name."
|
|
35
|
-
" Requires hourly resolution."
|
|
36
|
-
),
|
|
37
|
-
),
|
|
38
|
-
tag_names: Optional[str] = typer.Option(
|
|
39
|
-
None, "--tag-names", "-t", help="Comma-separated list of tag names to include."
|
|
40
|
-
),
|
|
41
|
-
json: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
42
|
-
csv: bool = typer.Option(False, "--csv", help="Output as CSV."),
|
|
41
|
+
start: Optional[str],
|
|
42
|
+
end: Optional[str],
|
|
43
|
+
for_: Optional[str],
|
|
44
|
+
resolution: str,
|
|
45
|
+
tz: Optional[str],
|
|
46
|
+
tag_names: Optional[str],
|
|
47
|
+
json: bool,
|
|
48
|
+
csv: bool,
|
|
43
49
|
):
|
|
44
50
|
"""Generate a billing report for the workspace.
|
|
45
51
|
|
modal/cli/bootstrap.py
CHANGED
|
@@ -6,7 +6,6 @@ from pathlib import Path
|
|
|
6
6
|
from typing import Optional
|
|
7
7
|
|
|
8
8
|
import click
|
|
9
|
-
import typer
|
|
10
9
|
|
|
11
10
|
from modal._utils.async_utils import synchronizer
|
|
12
11
|
from modal._utils.http_utils import _http_client_with_tls
|
|
@@ -15,15 +14,20 @@ from modal.config import logger
|
|
|
15
14
|
from modal.output import OutputManager
|
|
16
15
|
from modal_proto import api_pb2
|
|
17
16
|
|
|
17
|
+
from ._help import ModalCommand
|
|
18
18
|
from .logo import GREEN, print_logo
|
|
19
19
|
from .selector import Selector
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
@click.command("bootstrap", cls=ModalCommand)
|
|
23
|
+
@click.argument("name", required=False, default=None)
|
|
24
|
+
@click.option("-o", "--output", default=".", help="Location for storing the template.")
|
|
25
|
+
@click.option("--force", is_flag=True, default=False, help="Overwrite the output directory if it already exists.")
|
|
22
26
|
@synchronizer.create_blocking
|
|
23
27
|
async def bootstrap(
|
|
24
|
-
name: Optional[str] =
|
|
25
|
-
output: str =
|
|
26
|
-
force: bool =
|
|
28
|
+
name: Optional[str] = None,
|
|
29
|
+
output: str = ".",
|
|
30
|
+
force: bool = False,
|
|
27
31
|
) -> None:
|
|
28
32
|
"""Initialize a sample Modal App."""
|
|
29
33
|
client = await _Client.from_env()
|
|
@@ -36,7 +40,7 @@ async def bootstrap(
|
|
|
36
40
|
if name is None:
|
|
37
41
|
if not names:
|
|
38
42
|
output_manager.print("No templates available. Try updating modal.")
|
|
39
|
-
raise
|
|
43
|
+
raise SystemExit(1)
|
|
40
44
|
try:
|
|
41
45
|
selector = Selector(names, title="Select a template", highlight_style=f"bold {GREEN}")
|
|
42
46
|
name = selector.run()
|
|
@@ -45,13 +49,13 @@ async def bootstrap(
|
|
|
45
49
|
for n in names:
|
|
46
50
|
output_manager.print(f" - {n}")
|
|
47
51
|
output_manager.print("\nRun `modal bootstrap <name>` to select a template.")
|
|
48
|
-
raise
|
|
52
|
+
raise SystemExit(0)
|
|
49
53
|
|
|
50
54
|
item = next((item for item in resp.items if item.name == name), None)
|
|
51
55
|
if item is None:
|
|
52
56
|
output_manager.print(f"Unknown template: {name}")
|
|
53
57
|
output_manager.print(f"Available templates: {', '.join(names)}")
|
|
54
|
-
raise
|
|
58
|
+
raise SystemExit(1)
|
|
55
59
|
|
|
56
60
|
template_root = Path(output)
|
|
57
61
|
template_root.mkdir(parents=True, exist_ok=True)
|
|
@@ -76,7 +80,7 @@ async def bootstrap(
|
|
|
76
80
|
await session.close()
|
|
77
81
|
except Exception as e:
|
|
78
82
|
output_manager.print(f"Failed to download template from {archive_url}: {e}")
|
|
79
|
-
raise
|
|
83
|
+
raise SystemExit(1)
|
|
80
84
|
|
|
81
85
|
try:
|
|
82
86
|
zf = zipfile.ZipFile(io.BytesIO(data))
|
|
@@ -85,7 +89,7 @@ async def bootstrap(
|
|
|
85
89
|
except Exception as e:
|
|
86
90
|
logger.debug(f"Failed to parse template archive: {e}")
|
|
87
91
|
output_manager.print(f"Unable to fetch template '{name}'.")
|
|
88
|
-
raise
|
|
92
|
+
raise SystemExit(1)
|
|
89
93
|
|
|
90
94
|
with zf:
|
|
91
95
|
template_prefix = root_prefix + name + "/"
|
|
@@ -98,7 +102,7 @@ async def bootstrap(
|
|
|
98
102
|
|
|
99
103
|
if not members:
|
|
100
104
|
output_manager.print(f"Template '{name}' not found in repository.")
|
|
101
|
-
raise
|
|
105
|
+
raise SystemExit(1)
|
|
102
106
|
|
|
103
107
|
if dest.exists() and force:
|
|
104
108
|
shutil.rmtree(dest)
|
|
@@ -112,7 +116,7 @@ async def bootstrap(
|
|
|
112
116
|
if not out_path.is_relative_to(resolved_dest):
|
|
113
117
|
logger.debug(f"Zip entry escapes destination: {member!r}")
|
|
114
118
|
output_manager.print(f"Unable to fetch template '{name}'.")
|
|
115
|
-
raise
|
|
119
|
+
raise SystemExit(1)
|
|
116
120
|
if member.endswith("/"):
|
|
117
121
|
out_path.mkdir(parents=True, exist_ok=True)
|
|
118
122
|
else:
|
modal/cli/changelog.py
CHANGED
|
@@ -5,10 +5,12 @@ import urllib.request
|
|
|
5
5
|
from dataclasses import asdict, dataclass
|
|
6
6
|
from typing import Optional
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import click
|
|
9
9
|
|
|
10
10
|
from modal._vendor.version import Version
|
|
11
11
|
|
|
12
|
+
from ._help import ModalCommand
|
|
13
|
+
|
|
12
14
|
CHANGELOG_URL = "https://modal.com/docs/reference/changelog.md"
|
|
13
15
|
|
|
14
16
|
|
|
@@ -73,7 +75,7 @@ def _filter_entries(
|
|
|
73
75
|
if for_version is not None:
|
|
74
76
|
n = len(for_version.split("."))
|
|
75
77
|
if n not in (2, 3):
|
|
76
|
-
raise
|
|
78
|
+
raise click.BadParameter(f"Invalid version format: {for_version!r}. Use X.Y or X.Y.Z.")
|
|
77
79
|
fv = Version(for_version)
|
|
78
80
|
return [e for e in entries if Version(e.version).release[:n] == fv.release[:n]]
|
|
79
81
|
if newer:
|
|
@@ -93,19 +95,20 @@ def _format_entries(entries: list[ChangelogEntry], *, as_json: bool) -> str:
|
|
|
93
95
|
return "\n\n".join(parts)
|
|
94
96
|
|
|
95
97
|
|
|
98
|
+
@click.command("changelog", cls=ModalCommand)
|
|
99
|
+
@click.option("--last", default=None, type=int, help="Show the N most recent entries before the installed version.")
|
|
100
|
+
@click.option("--since", default=None, help="Show entries after a version (X.Y.Z) or date (YYYY-MM-DD), exclusive.")
|
|
101
|
+
@click.option("--for", "for_version", default=None, help="Show entries for a version (X.Y.Z) or series (X.Y).")
|
|
102
|
+
@click.option("--newer", is_flag=True, default=False, help="Show entries newer than the installed version.")
|
|
103
|
+
@click.option("--all", "all", is_flag=True, default=False, help="Show all entries.")
|
|
104
|
+
@click.option("--json", "json", is_flag=True, default=False, help="Output as JSON.")
|
|
96
105
|
def changelog(
|
|
97
|
-
last: Optional[int]
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
for_version: Optional[str] = typer.Option(
|
|
104
|
-
None, "--for", help="Show entries for a version (X.Y.Z) or series (X.Y)."
|
|
105
|
-
),
|
|
106
|
-
newer: bool = typer.Option(False, "--newer", help="Show entries newer than the installed version."),
|
|
107
|
-
all: bool = typer.Option(False, "--all", help="Show all entries."),
|
|
108
|
-
json: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
106
|
+
last: Optional[int],
|
|
107
|
+
since: Optional[str],
|
|
108
|
+
for_version: Optional[str],
|
|
109
|
+
newer: bool,
|
|
110
|
+
all: bool,
|
|
111
|
+
json: bool,
|
|
109
112
|
):
|
|
110
113
|
"""Fetch release notes from the Modal changelog.
|
|
111
114
|
|
|
@@ -137,7 +140,7 @@ def changelog(
|
|
|
137
140
|
|
|
138
141
|
filter_flags = sum([last is not None, since is not None, for_version is not None, newer, all])
|
|
139
142
|
if filter_flags > 1:
|
|
140
|
-
raise
|
|
143
|
+
raise click.BadParameter("Only one of --last, --since, --for, --newer, or --all may be used at a time.")
|
|
141
144
|
|
|
142
145
|
markdown = _fetch_changelog_markdown()
|
|
143
146
|
entries = _parse_entries(markdown)
|
|
@@ -152,8 +155,8 @@ def changelog(
|
|
|
152
155
|
)
|
|
153
156
|
|
|
154
157
|
if not filtered:
|
|
155
|
-
|
|
156
|
-
raise
|
|
158
|
+
click.echo("No changelog entries found.")
|
|
159
|
+
raise SystemExit(0)
|
|
157
160
|
|
|
158
161
|
output = _format_entries(filtered, as_json=json)
|
|
159
|
-
|
|
162
|
+
click.echo(output)
|
modal/cli/cluster.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
2
|
from typing import Optional, Union
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import click
|
|
5
5
|
from rich.table import Column
|
|
6
6
|
from rich.text import Text
|
|
7
7
|
|
|
@@ -9,7 +9,7 @@ from modal._object import _get_environment_name
|
|
|
9
9
|
from modal._output.pty import get_pty_info
|
|
10
10
|
from modal._utils.async_utils import synchronizer
|
|
11
11
|
from modal._utils.time_utils import timestamp_to_localized_str
|
|
12
|
-
from modal.cli.utils import
|
|
12
|
+
from modal.cli.utils import display_table, env_option, is_tty
|
|
13
13
|
from modal.client import _Client
|
|
14
14
|
from modal.config import config
|
|
15
15
|
from modal.container_process import _ContainerProcess
|
|
@@ -18,14 +18,16 @@ from modal.output import OutputManager
|
|
|
18
18
|
from modal.stream_type import StreamType
|
|
19
19
|
from modal_proto import api_pb2
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
)
|
|
21
|
+
from ._help import ModalGroup
|
|
22
|
+
|
|
23
|
+
cluster_cli = ModalGroup(name="cluster", help="Manage and connect to running multi-node clusters.")
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
@cluster_cli.command("list")
|
|
27
|
+
@env_option
|
|
28
|
+
@click.option("--json", is_flag=True, default=False)
|
|
27
29
|
@synchronizer.create_blocking
|
|
28
|
-
async def list_(env: Optional[str] =
|
|
30
|
+
async def list_(env: Optional[str] = None, json: bool = False):
|
|
29
31
|
"""List all clusters that are currently running."""
|
|
30
32
|
env = ensure_env(env)
|
|
31
33
|
client = await _Client.from_env()
|
|
@@ -57,16 +59,15 @@ async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
|
|
|
57
59
|
|
|
58
60
|
|
|
59
61
|
@cluster_cli.command("shell")
|
|
62
|
+
@click.argument("cluster_id")
|
|
63
|
+
@click.option("--rank", default=0, help="Rank of the node to shell into")
|
|
60
64
|
@synchronizer.create_blocking
|
|
61
|
-
async def shell(
|
|
62
|
-
cluster_id: str = typer.Argument(help="Cluster ID"),
|
|
63
|
-
rank: int = typer.Option(default=0, help="Rank of the node to shell into"),
|
|
64
|
-
):
|
|
65
|
+
async def shell(cluster_id: str, rank: int = 0):
|
|
65
66
|
"""Open a shell to a multi-node cluster node."""
|
|
66
67
|
client = await _Client.from_env()
|
|
67
68
|
res: api_pb2.ClusterGetResponse = await client.stub.ClusterGet(api_pb2.ClusterGetRequest(cluster_id=cluster_id))
|
|
68
69
|
if len(res.cluster.task_ids) <= rank:
|
|
69
|
-
raise
|
|
70
|
+
raise click.ClickException(f"No node with rank {rank} in cluster {cluster_id}")
|
|
70
71
|
task_id = res.cluster.task_ids[rank]
|
|
71
72
|
is_main = "(main)" if rank == 0 else ""
|
|
72
73
|
OutputManager.get().print(
|
modal/cli/config.py
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
2
|
import json
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import click
|
|
5
5
|
|
|
6
6
|
from modal.config import _profile, _store_user_config, config
|
|
7
7
|
from modal.environments import Environment
|
|
8
8
|
from modal.output import OutputManager
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
from ._help import ModalGroup
|
|
11
|
+
|
|
12
|
+
config_cli = ModalGroup(
|
|
11
13
|
name="config",
|
|
12
14
|
help="""
|
|
13
15
|
Manage client configuration for the current profile.
|
|
@@ -15,12 +17,12 @@ config_cli = typer.Typer(
|
|
|
15
17
|
Refer to https://modal.com/docs/reference/modal.config for a full explanation
|
|
16
18
|
of what these options mean, and how to set them.
|
|
17
19
|
""",
|
|
18
|
-
no_args_is_help=True,
|
|
19
20
|
)
|
|
20
21
|
|
|
21
22
|
|
|
22
|
-
@config_cli.command(help="Show current configuration values (debugging command).")
|
|
23
|
-
|
|
23
|
+
@config_cli.command("show", help="Show current configuration values (debugging command).")
|
|
24
|
+
@click.option("--redact/--no-redact", default=True, help="Redact the `token_secret` value.")
|
|
25
|
+
def show(redact: bool):
|
|
24
26
|
# This is just a test command
|
|
25
27
|
config_dict = config.to_dict()
|
|
26
28
|
if redact and config_dict.get("token_secret"):
|
|
@@ -38,14 +40,17 @@ when running a command that requires an environment.
|
|
|
38
40
|
"""
|
|
39
41
|
|
|
40
42
|
|
|
41
|
-
@config_cli.command(help=SET_DEFAULT_ENV_HELP)
|
|
43
|
+
@config_cli.command("set-environment", help=SET_DEFAULT_ENV_HELP)
|
|
44
|
+
@click.argument("environment_name")
|
|
42
45
|
def set_environment(environment_name: str):
|
|
43
46
|
# Confirm that the environment exists by looking it up
|
|
44
47
|
Environment.from_name(environment_name).hydrate()
|
|
45
48
|
_store_user_config({"environment": environment_name})
|
|
46
|
-
|
|
49
|
+
click.echo(f"New default environment for profile {_profile}: {environment_name}")
|
|
47
50
|
|
|
48
51
|
|
|
49
|
-
@config_cli.command(hidden=True)
|
|
52
|
+
@config_cli.command("set", hidden=True)
|
|
53
|
+
@click.argument("key")
|
|
54
|
+
@click.argument("value")
|
|
50
55
|
def set(key: str, value: str):
|
|
51
56
|
_store_user_config({key: value})
|
modal/cli/container.py
CHANGED
|
@@ -3,7 +3,7 @@ import warnings
|
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
4
|
from typing import Optional, Union
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import click
|
|
7
7
|
from click import UsageError
|
|
8
8
|
from rich.table import Column
|
|
9
9
|
from rich.text import Text
|
|
@@ -15,14 +15,14 @@ from modal._utils.async_utils import synchronizer
|
|
|
15
15
|
from modal._utils.time_utils import timestamp_to_localized_str
|
|
16
16
|
from modal.cli.app import _DEFAULT_LOGS_TAIL, _SOURCE_OPTIONS, _parse_time_arg
|
|
17
17
|
from modal.cli.utils import (
|
|
18
|
-
ENV_OPTION,
|
|
19
|
-
YES_OPTION,
|
|
20
18
|
confirm_or_suggest_yes,
|
|
21
19
|
display_table,
|
|
20
|
+
env_option,
|
|
22
21
|
fetch_app_logs,
|
|
23
22
|
is_tty,
|
|
24
23
|
stream_app_logs,
|
|
25
24
|
tail_app_logs,
|
|
25
|
+
yes_option,
|
|
26
26
|
)
|
|
27
27
|
from modal.client import _Client
|
|
28
28
|
from modal.config import config
|
|
@@ -32,14 +32,19 @@ from modal.exception import InvalidError
|
|
|
32
32
|
from modal.stream_type import StreamType
|
|
33
33
|
from modal_proto import api_pb2
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
from ._help import ModalGroup
|
|
36
|
+
|
|
37
|
+
container_cli = ModalGroup(name="container", help="Manage and connect to running containers.")
|
|
36
38
|
|
|
37
39
|
|
|
38
40
|
@container_cli.command("list")
|
|
41
|
+
@click.option("--app-id", default="", help="List containers running for a specific App.")
|
|
42
|
+
@env_option
|
|
43
|
+
@click.option("--json", is_flag=True, default=False)
|
|
39
44
|
@synchronizer.create_blocking
|
|
40
45
|
async def list_(
|
|
41
|
-
app_id: str =
|
|
42
|
-
env: Optional[str] =
|
|
46
|
+
app_id: str = "",
|
|
47
|
+
env: Optional[str] = None,
|
|
43
48
|
json: bool = False,
|
|
44
49
|
):
|
|
45
50
|
"""List all containers that are currently running."""
|
|
@@ -72,29 +77,30 @@ async def list_(
|
|
|
72
77
|
|
|
73
78
|
|
|
74
79
|
@container_cli.command("logs", no_args_is_help=True)
|
|
80
|
+
@click.argument("container_id")
|
|
81
|
+
@click.option("-f", "--follow", is_flag=True, default=False, help="Stream log output until Container stops")
|
|
82
|
+
@click.option("--all", "all_logs", is_flag=True, default=False, help="Show all logs for the container")
|
|
83
|
+
@click.option(
|
|
84
|
+
"--since",
|
|
85
|
+
default=None,
|
|
86
|
+
help="Start of time range. Accepts ISO 8601 datetime or relative time, e.g. '1d' (1 day ago), '2h', '30m', etc.",
|
|
87
|
+
)
|
|
88
|
+
@click.option("--until", default=None, help="End of time range; accepts same argument types as --since")
|
|
89
|
+
@click.option("-n", "--tail", default=None, type=int, help="Show only the last N log entries")
|
|
90
|
+
@click.option("--search", default=None, help="Filter by search text")
|
|
91
|
+
@click.option("-s", "--source", default=None, help="Filter by source: 'stdout', 'stderr', or 'system'")
|
|
92
|
+
@click.option("--timestamps", is_flag=True, default=False, help="Prefix each line with its timestamp")
|
|
75
93
|
@synchronizer.create_blocking
|
|
76
94
|
async def logs(
|
|
77
|
-
container_id: str
|
|
78
|
-
follow: bool =
|
|
79
|
-
all_logs: bool =
|
|
80
|
-
since: Optional[str] =
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
),
|
|
87
|
-
until: Optional[str] = typer.Option(
|
|
88
|
-
None,
|
|
89
|
-
"--until",
|
|
90
|
-
help="End of time range; accepts same argument types as --since",
|
|
91
|
-
),
|
|
92
|
-
tail: Optional[int] = typer.Option(None, "--tail", "-n", help="Show only the last N log entries"),
|
|
93
|
-
search: Optional[str] = typer.Option(None, "--search", help="Filter by search text"),
|
|
94
|
-
source: Optional[str] = typer.Option(
|
|
95
|
-
None, "--source", "-s", help="Filter by source: 'stdout', 'stderr', or 'system'"
|
|
96
|
-
),
|
|
97
|
-
timestamps: bool = typer.Option(False, "--timestamps", help="Prefix each line with its timestamp"),
|
|
95
|
+
container_id: str,
|
|
96
|
+
follow: bool = False,
|
|
97
|
+
all_logs: bool = False,
|
|
98
|
+
since: Optional[str] = None,
|
|
99
|
+
until: Optional[str] = None,
|
|
100
|
+
tail: Optional[int] = None,
|
|
101
|
+
search: Optional[str] = None,
|
|
102
|
+
source: Optional[str] = None,
|
|
103
|
+
timestamps: bool = False,
|
|
98
104
|
):
|
|
99
105
|
"""Fetch or stream logs for a specific container.
|
|
100
106
|
|
|
@@ -257,18 +263,13 @@ async def logs(
|
|
|
257
263
|
)
|
|
258
264
|
|
|
259
265
|
|
|
260
|
-
@container_cli.command("exec")
|
|
261
266
|
@synchronizer.create_blocking
|
|
262
|
-
async def
|
|
263
|
-
pty: Optional[bool] =
|
|
264
|
-
container_id: str =
|
|
265
|
-
command:
|
|
266
|
-
help="A command to run inside the container.\n\n"
|
|
267
|
-
"To pass command-line flags or options, add `--` before the start of your commands. "
|
|
268
|
-
"For example: `modal container exec <id> -- /bin/bash -c 'echo hi'`"
|
|
269
|
-
),
|
|
267
|
+
async def _exec_impl(
|
|
268
|
+
pty: Optional[bool] = None,
|
|
269
|
+
container_id: str = "",
|
|
270
|
+
command: tuple[str, ...] = (),
|
|
270
271
|
):
|
|
271
|
-
"""Execute a command in a container."""
|
|
272
|
+
"""Execute a command in a container (implementation)."""
|
|
272
273
|
|
|
273
274
|
if pty is None:
|
|
274
275
|
pty = is_tty()
|
|
@@ -292,16 +293,29 @@ async def exec(
|
|
|
292
293
|
).wait()
|
|
293
294
|
|
|
294
295
|
|
|
296
|
+
@container_cli.command("exec")
|
|
297
|
+
@click.option("--pty/--no-pty", default=None, help="Run the command using a PTY.")
|
|
298
|
+
@click.argument("container_id")
|
|
299
|
+
@click.argument("command", nargs=-1, required=True)
|
|
300
|
+
def exec(
|
|
301
|
+
pty: Optional[bool] = None,
|
|
302
|
+
container_id: str = "",
|
|
303
|
+
command: tuple[str, ...] = (),
|
|
304
|
+
):
|
|
305
|
+
"""Execute a command in a container."""
|
|
306
|
+
_exec_impl(pty=pty, container_id=container_id, command=command)
|
|
307
|
+
|
|
308
|
+
|
|
295
309
|
@container_cli.command("stop")
|
|
310
|
+
@click.argument("container_id")
|
|
311
|
+
@yes_option
|
|
296
312
|
@synchronizer.create_blocking
|
|
297
|
-
async def stop(
|
|
298
|
-
|
|
299
|
-
*,
|
|
300
|
-
yes: bool = YES_OPTION,
|
|
301
|
-
):
|
|
302
|
-
"""Stop a currently-running container and reassign its in-progress inputs.
|
|
313
|
+
async def stop(container_id: str = "", *, yes: bool = False):
|
|
314
|
+
"""Terminate a running container.
|
|
303
315
|
|
|
304
316
|
This will send the container a SIGINT signal that Modal will handle.
|
|
317
|
+
Any inputs that are currently running on the container will be cancelled and rescheduled
|
|
318
|
+
on other containers.
|
|
305
319
|
"""
|
|
306
320
|
client = await _Client.from_env()
|
|
307
321
|
resp = await client.stub.TaskGetInfo(api_pb2.TaskGetInfoRequest(task_id=container_id))
|
modal/cli/dashboard.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Copyright Modal Labs 2026
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import click
|
|
5
5
|
|
|
6
6
|
from modal._utils.async_utils import synchronizer
|
|
7
7
|
from modal._utils.browser_utils import open_url_and_display
|
|
@@ -9,10 +9,14 @@ from modal.client import _Client
|
|
|
9
9
|
from modal.config import config
|
|
10
10
|
from modal_proto import api_pb2
|
|
11
11
|
|
|
12
|
+
from ._help import ModalCommand
|
|
12
13
|
|
|
14
|
+
|
|
15
|
+
@click.command("dashboard", cls=ModalCommand)
|
|
16
|
+
@click.argument("object_id", default=None, required=False)
|
|
13
17
|
@synchronizer.create_blocking
|
|
14
18
|
async def dashboard(
|
|
15
|
-
object_id: Optional[str] =
|
|
19
|
+
object_id: Optional[str] = None,
|
|
16
20
|
):
|
|
17
21
|
"""Open the Modal Dashboard in a web browser."""
|
|
18
22
|
if object_id:
|