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/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 = typer.Typer(name="billing", help="View workspace billing information.", no_args_is_help=True)
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] = typer.Option(None, "--start", help=f"Start date. {DATE_HELP}"),
25
- end: Optional[str] = typer.Option(None, "--end", help=f"End date. {DATE_HELP} Defaults to now."),
26
- for_: Optional[str] = typer.Option(
27
- None, "--for", help="Convenience range: today, yesterday, this week, last week, this month, last month."
28
- ),
29
- resolution: str = typer.Option("d", "--resolution", "-r", help="Time resolution: 'd' (daily) or 'h' (hourly)."),
30
- tz: Optional[str] = typer.Option(
31
- None,
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] = typer.Argument(None, help="The name of the template to load."),
25
- output: str = typer.Option(".", "-o", "--output", help="Location for storing the template."),
26
- force: bool = typer.Option(False, "--force", help="Overwrite the output directory if it already exists."),
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 typer.Exit(1)
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 typer.Exit(0)
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 typer.Exit(1)
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 typer.Exit(1)
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 typer.Exit(1)
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 typer.Exit(1)
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 typer.Exit(1)
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 typer
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 typer.BadParameter(f"Invalid version format: {for_version!r}. Use X.Y or X.Y.Z.")
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] = typer.Option(
98
- None, "--last", help="Show the N most recent entries before the installed version."
99
- ),
100
- since: Optional[str] = typer.Option(
101
- None, "--since", help="Show entries after a version (X.Y.Z) or date (YYYY-MM-DD), exclusive."
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 typer.BadParameter("Only one of --last, --since, --for, --newer, or --all may be used at a time.")
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
- typer.echo("No changelog entries found.")
156
- raise typer.Exit()
158
+ click.echo("No changelog entries found.")
159
+ raise SystemExit(0)
157
160
 
158
161
  output = _format_entries(filtered, as_json=json)
159
- typer.echo(output)
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 typer
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 ENV_OPTION, display_table, is_tty
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
- cluster_cli = typer.Typer(
22
- name="cluster", help="Manage and connect to running multi-node clusters.", no_args_is_help=True
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] = ENV_OPTION, json: bool = False):
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 typer.Abort(f"No node with rank {rank} in cluster {cluster_id}")
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 typer
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
- config_cli = typer.Typer(
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
- def show(redact: bool = typer.Option(True, help="Redact the `token_secret` value.")):
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
- typer.echo(f"New default environment for profile {_profile}: {environment_name}")
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 typer
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
- container_cli = typer.Typer(name="container", help="Manage and connect to running containers.", no_args_is_help=True)
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 = typer.Option("", "--app-id", help="List containers running for a specific App."),
42
- env: Optional[str] = ENV_OPTION,
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 = typer.Argument(help="Container ID"),
78
- follow: bool = typer.Option(False, "-f", "--follow", help="Stream log output until Container stops"),
79
- all_logs: bool = typer.Option(False, "--all", help="Show all logs for the container"),
80
- since: Optional[str] = typer.Option(
81
- None,
82
- "--since",
83
- help=(
84
- "Start of time range. Accepts ISO 8601 datetime or relative time, e.g. '1d' (1 day ago), '2h', '30m', etc."
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 exec(
263
- pty: Optional[bool] = typer.Option(default=None, help="Run the command using a PTY."),
264
- container_id: str = typer.Argument(help="Container ID"),
265
- command: list[str] = typer.Argument(
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
- container_id: str = typer.Argument(help="Container ID"),
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 typer
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] = typer.Argument(None, help="Open a view for a specific object."),
19
+ object_id: Optional[str] = None,
16
20
  ):
17
21
  """Open the Modal Dashboard in a web browser."""
18
22
  if object_id: