flwr-nightly 1.26.0.dev20260128__py3-none-any.whl → 1.26.0.dev20260129__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 (40) hide show
  1. flwr/cli/app_cmd/review.py +7 -7
  2. flwr/cli/build.py +9 -11
  3. flwr/cli/config/ls.py +5 -33
  4. flwr/cli/config_migration.py +7 -10
  5. flwr/cli/config_utils.py +18 -12
  6. flwr/cli/constant.py +1 -0
  7. flwr/cli/federation/ls.py +30 -42
  8. flwr/cli/flower_config.py +5 -0
  9. flwr/cli/install.py +5 -5
  10. flwr/cli/ls.py +17 -32
  11. flwr/cli/run/run.py +39 -41
  12. flwr/cli/stop.py +21 -41
  13. flwr/cli/supernode/ls.py +15 -33
  14. flwr/cli/supernode/register.py +18 -37
  15. flwr/cli/supernode/unregister.py +16 -38
  16. flwr/cli/typing.py +3 -0
  17. flwr/cli/utils.py +51 -1
  18. flwr/client/grpc_rere_client/connection.py +2 -2
  19. flwr/common/args.py +1 -1
  20. flwr/common/config.py +3 -5
  21. flwr/common/constant.py +1 -1
  22. flwr/common/retry_invoker.py +13 -5
  23. flwr/common/typing.py +0 -1
  24. flwr/proto/federation_pb2.py +2 -2
  25. flwr/proto/federation_pb2.pyi +1 -5
  26. flwr/server/grid/grpc_grid.py +3 -3
  27. flwr/simulation/run_simulation.py +1 -11
  28. flwr/simulation/simulationio_connection.py +3 -3
  29. flwr/supercore/constant.py +2 -2
  30. flwr/supercore/state/alembic/env.py +33 -11
  31. flwr/supercore/state/alembic/utils.py +191 -1
  32. flwr/supercore/superexec/run_superexec.py +2 -2
  33. flwr/superlink/federation/noop_federation_manager.py +0 -1
  34. flwr/superlink/servicer/control/control_servicer.py +1 -1
  35. flwr/supernode/runtime/run_clientapp.py +2 -2
  36. flwr/supernode/start_client_internal.py +2 -2
  37. {flwr_nightly-1.26.0.dev20260128.dist-info → flwr_nightly-1.26.0.dev20260129.dist-info}/METADATA +2 -2
  38. {flwr_nightly-1.26.0.dev20260128.dist-info → flwr_nightly-1.26.0.dev20260129.dist-info}/RECORD +40 -40
  39. {flwr_nightly-1.26.0.dev20260128.dist-info → flwr_nightly-1.26.0.dev20260129.dist-info}/WHEEL +0 -0
  40. {flwr_nightly-1.26.0.dev20260128.dist-info → flwr_nightly-1.26.0.dev20260129.dist-info}/entry_points.txt +0 -0
@@ -77,15 +77,15 @@ def review(
77
77
  url = f"{PLATFORM_API_URL}/hub/fetch-fab"
78
78
  try:
79
79
  presigned_url, _ = request_download_link(app_id, app_version, url, "fab_url")
80
- except ValueError as e:
81
- raise click.ClickException(str(e)) from e
82
80
 
83
- fab_bytes = _download_fab(presigned_url)
81
+ fab_bytes = _download_fab(presigned_url)
84
82
 
85
- # Unpack FAB
86
- typer.secho("Unpacking FAB... ", fg=typer.colors.BLUE)
87
- review_dir = _create_review_dir()
88
- review_app_path = install_from_fab(fab_bytes, review_dir)
83
+ # Unpack FAB
84
+ typer.secho("Unpacking FAB... ", fg=typer.colors.BLUE)
85
+ review_dir = _create_review_dir()
86
+ review_app_path = install_from_fab(fab_bytes, review_dir)
87
+ except ValueError as e:
88
+ raise click.ClickException(str(e)) from e
89
89
 
90
90
  # Extract app version
91
91
  version_pattern = re.compile(r"\b(\d+\.\d+\.\d+)\b")
flwr/cli/build.py CHANGED
@@ -71,7 +71,7 @@ def get_fab_filename(config: dict[str, Any], fab_hash: str) -> str:
71
71
  Parameters
72
72
  ----------
73
73
  config : dict[str, Any]
74
- The project configuration dictionary.
74
+ The Flower App configuration dictionary.
75
75
  fab_hash : str
76
76
  The SHA-256 hash of the FAB file.
77
77
 
@@ -114,23 +114,21 @@ def build(
114
114
 
115
115
  if not is_valid_project_name(app.name):
116
116
  raise click.ClickException(
117
- f"The project name {app.name} is invalid, "
118
- "a valid project name must start with a letter, "
117
+ f"The Flower App name {app.name} is invalid, "
118
+ "a valid app name must start with a letter, "
119
119
  "and can only contain letters, digits, and hyphens."
120
120
  )
121
121
 
122
- config, errors, warnings = load_and_validate(app / "pyproject.toml")
123
- if config is None:
124
- raise click.ClickException(
125
- "Project configuration could not be loaded.\npyproject.toml is invalid:\n"
126
- + "\n".join([f"- {line}" for line in errors])
127
- )
122
+ try:
123
+ config, warnings = load_and_validate(app / "pyproject.toml")
124
+ except ValueError as e:
125
+ raise click.ClickException(str(e)) from None
128
126
 
129
127
  if warnings:
130
128
  typer.secho(
131
- "Project configuration is missing the following "
129
+ "Flower App configuration (pyproject.toml) is missing the following "
132
130
  "recommended properties:\n" + "\n".join([f"- {line}" for line in warnings]),
133
- fg=typer.colors.RED,
131
+ fg=typer.colors.YELLOW,
134
132
  bold=True,
135
133
  )
136
134
 
flwr/cli/config/ls.py CHANGED
@@ -15,19 +15,15 @@
15
15
  """Flower command line interface `config list` command."""
16
16
 
17
17
 
18
- import io
19
- import json
20
18
  from typing import Annotated
21
19
 
22
- import click
23
20
  import typer
24
- from rich.console import Console
25
21
 
26
22
  from flwr.common.constant import CliOutputFormat
27
- from flwr.common.logger import print_json_error, redirect_output, restore_output
28
23
 
29
24
  from ..constant import SuperLinkConnectionTomlKey
30
25
  from ..flower_config import read_flower_config
26
+ from ..utils import cli_output_handler, print_json_to_stdout
31
27
 
32
28
 
33
29
  def ls(
@@ -41,14 +37,7 @@ def ls(
41
37
  ] = CliOutputFormat.DEFAULT,
42
38
  ) -> None:
43
39
  """List all SuperLink connections (alias: ls)."""
44
- suppress_output = output_format == CliOutputFormat.JSON
45
- captured_output = io.StringIO()
46
- config_path = None
47
-
48
- if suppress_output:
49
- redirect_output(captured_output)
50
-
51
- try:
40
+ with cli_output_handler(output_format=output_format) as is_json:
52
41
  # Load Flower Config
53
42
  config, config_path = read_flower_config()
54
43
 
@@ -59,13 +48,13 @@ def ls(
59
48
  default = superlink_connections.pop(SuperLinkConnectionTomlKey.DEFAULT, None)
60
49
 
61
50
  connection_names = list(superlink_connections.keys())
62
- restore_output()
63
- if output_format == CliOutputFormat.JSON:
51
+
52
+ if is_json:
64
53
  conn = {
65
54
  SuperLinkConnectionTomlKey.SUPERLINK: connection_names,
66
55
  SuperLinkConnectionTomlKey.DEFAULT: default,
67
56
  }
68
- Console().print_json(json.dumps(conn))
57
+ print_json_to_stdout(conn)
69
58
  else:
70
59
  typer.secho("Flower Config file: ", fg=typer.colors.BLUE, nl=False)
71
60
  typer.secho(f"{config_path}", fg=typer.colors.GREEN)
@@ -80,20 +69,3 @@ def ls(
80
69
  nl=False,
81
70
  )
82
71
  typer.echo()
83
-
84
- except Exception as err: # pylint: disable=broad-except
85
- # log the error if json format requested
86
- if suppress_output:
87
- restore_output()
88
- e_message = captured_output.getvalue()
89
- print_json_error(e_message, err)
90
- else:
91
- raise click.ClickException(
92
- f"An unexpected error occurred while listing the SuperLink "
93
- f"connections in the Flower configuration file ({config_path}): {err}"
94
- ) from err
95
-
96
- finally:
97
- if suppress_output:
98
- restore_output()
99
- captured_output.close()
@@ -94,13 +94,12 @@ def _is_migratable(app: Path) -> tuple[bool, str | None]:
94
94
  toml_path = app / "pyproject.toml"
95
95
  if not toml_path.exists():
96
96
  return False, f"No pyproject.toml found in '{app}'"
97
- config, errors, _ = load_and_validate(toml_path, check_module=False)
98
- if config is None:
99
- return False, f"Failed to load TOML configuration: {toml_path}"
100
- if errors:
101
- err_msg = f"Invalid TOML configuration found in '{toml_path}':\n"
102
- err_msg += "\n".join(f"- {err}" for err in errors)
103
- return False, err_msg
97
+
98
+ try:
99
+ config, _ = load_and_validate(toml_path, check_module=False)
100
+ except ValueError as e:
101
+ return False, str(e)
102
+
104
103
  try:
105
104
  _ = config["tool"]["flwr"]["federations"]
106
105
  return True, None
@@ -114,9 +113,7 @@ def _migrate_pyproject_toml_to_flower_config(
114
113
  """Migrate old TOML configuration to Flower config."""
115
114
  # Load and validate the old TOML configuration
116
115
  toml_path = app / "pyproject.toml"
117
- config, _, _ = load_and_validate(toml_path, check_module=False)
118
- if config is None:
119
- raise ValueError(f"Failed to load TOML configuration: {toml_path}")
116
+ config, _ = load_and_validate(toml_path, check_module=False)
120
117
  validate_federation_in_project_config(toml_federation, config)
121
118
 
122
119
  # Construct SuperLinkConnection
flwr/cli/config_utils.py CHANGED
@@ -51,7 +51,7 @@ def get_fab_metadata(fab_file: Path | bytes) -> tuple[str, str]:
51
51
  def load_and_validate(
52
52
  path: Path | None = None,
53
53
  check_module: bool = True,
54
- ) -> tuple[dict[str, Any] | None, list[str], list[str]]:
54
+ ) -> tuple[dict[str, Any], list[str]]:
55
55
  """Load and validate pyproject.toml as dict.
56
56
 
57
57
  Parameters
@@ -66,28 +66,34 @@ def load_and_validate(
66
66
 
67
67
  Returns
68
68
  -------
69
- Tuple[Optional[config], List[str], List[str]]
70
- A tuple with the optional config in case it exists and is valid
71
- and associated errors and warnings.
69
+ tuple[dict[str, Any], list[str]]
70
+ A tuple of the loaded configuration as dictionary and a list of warnings.
71
+
72
+ Raises
73
+ ------
74
+ ValueError
75
+ If the configuration is invalid or the file cannot be loaded.
72
76
  """
73
77
  if path is None:
74
78
  path = Path.cwd() / "pyproject.toml"
75
-
79
+ path = path.resolve()
76
80
  config = load(path)
77
81
 
78
82
  if config is None:
79
- errors = [
80
- "Project configuration could not be loaded. "
81
- "`pyproject.toml` does not exist."
82
- ]
83
- return (None, errors, [])
83
+ raise ValueError(
84
+ f"Failed to load Flower App configuration in '{path}'. "
85
+ "File may be missing or invalid."
86
+ )
84
87
 
85
88
  is_valid, errors, warnings = validate_config(config, check_module, path.parent)
86
89
 
87
90
  if not is_valid:
88
- return (None, errors, warnings)
91
+ raise ValueError(
92
+ f"Invalid Flower App configuration in '{path}':\n"
93
+ + "\n".join([f"- {line}" for line in errors])
94
+ )
89
95
 
90
- return (config, errors, warnings)
96
+ return config, warnings
91
97
 
92
98
 
93
99
  def load(toml_path: Path) -> dict[str, Any] | None:
flwr/cli/constant.py CHANGED
@@ -66,6 +66,7 @@ class SuperLinkSimulationOptionsTomlKey:
66
66
 
67
67
  NUM_SUPERNODES = "num-supernodes"
68
68
  BACKEND = "backend"
69
+ VERBOSE = "verbose"
69
70
 
70
71
 
71
72
  class SimulationClientResourcesTomlKey:
flwr/cli/federation/ls.py CHANGED
@@ -15,10 +15,8 @@
15
15
  """Flower command line interface `federation list` command."""
16
16
 
17
17
 
18
- import io
19
18
  from typing import Annotated, Any
20
19
 
21
- import click
22
20
  import typer
23
21
  from rich.console import Console
24
22
  from rich.table import Table
@@ -28,7 +26,6 @@ from flwr.cli.config_migration import migrate
28
26
  from flwr.cli.flower_config import read_superlink_connection
29
27
  from flwr.cli.ls import _get_status_style
30
28
  from flwr.common.constant import NOOP_ACCOUNT_NAME, CliOutputFormat
31
- from flwr.common.logger import print_json_error, redirect_output, restore_output
32
29
  from flwr.common.serde import run_from_proto
33
30
  from flwr.proto.control_pb2 import ( # pylint: disable=E0611
34
31
  ListFederationsRequest,
@@ -42,7 +39,12 @@ from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
42
39
  from flwr.supercore.utils import humanize_duration
43
40
 
44
41
  from ..run_utils import RunRow, format_runs
45
- from ..utils import flwr_cli_grpc_exc_handler, init_channel_from_connection
42
+ from ..utils import (
43
+ cli_output_handler,
44
+ flwr_cli_grpc_exc_handler,
45
+ init_channel_from_connection,
46
+ print_json_to_stdout,
47
+ )
46
48
 
47
49
 
48
50
  def ls( # pylint: disable=R0914, R0913, R0917, R0912
@@ -69,20 +71,14 @@ def ls( # pylint: disable=R0914, R0913, R0917, R0912
69
71
  ] = None,
70
72
  ) -> None:
71
73
  """List available federations or details of a specific federation (alias: ls)."""
72
- suppress_output = output_format == CliOutputFormat.JSON
73
- captured_output = io.StringIO()
74
-
75
- if suppress_output:
76
- redirect_output(captured_output)
77
-
78
- # Migrate legacy usage if any
79
- migrate(superlink, args=ctx.args)
74
+ with cli_output_handler(output_format=output_format) as is_json:
75
+ # Migrate legacy usage if any
76
+ migrate(superlink, args=ctx.args)
80
77
 
81
- # Read superlink connection configuration
82
- superlink_connection = read_superlink_connection(superlink)
83
- channel = None
78
+ # Read superlink connection configuration
79
+ superlink_connection = read_superlink_connection(superlink)
80
+ channel = None
84
81
 
85
- try:
86
82
  try:
87
83
  channel = init_channel_from_connection(superlink_connection)
88
84
  stub = ControlStub(channel)
@@ -92,10 +88,9 @@ def ls( # pylint: disable=R0914, R0913, R0917, R0912
92
88
  typer.echo(f"📄 Showing '{federation}' federation ...")
93
89
  members, nodes, runs = _show_federation(stub, federation)
94
90
 
95
- restore_output()
96
- if output_format == CliOutputFormat.JSON:
97
- Console().print_json(
98
- data=_to_json(members=members, nodes=nodes, runs=runs)
91
+ if is_json:
92
+ print_json_to_stdout(
93
+ _to_json(members=members, nodes=nodes, runs=runs)
99
94
  )
100
95
  else:
101
96
  Console().print(_to_members_table(members))
@@ -105,25 +100,14 @@ def ls( # pylint: disable=R0914, R0913, R0917, R0912
105
100
  # List federations
106
101
  typer.echo("📄 Listing federations...")
107
102
  federations = _list_federations(stub)
108
- restore_output()
109
- if output_format == CliOutputFormat.JSON:
110
- Console().print_json(data=_to_json(federations=federations))
103
+
104
+ if is_json:
105
+ print_json_to_stdout(_to_json(federations=federations))
111
106
  else:
112
107
  Console().print(_to_table(federations))
113
108
  finally:
114
109
  if channel:
115
110
  channel.close()
116
- except Exception as err: # pylint: disable=broad-except
117
- if suppress_output:
118
- restore_output()
119
- e_message = captured_output.getvalue()
120
- print_json_error(e_message, err)
121
- else:
122
- raise click.ClickException(str(err)) from None
123
- finally:
124
- if suppress_output:
125
- restore_output()
126
- captured_output.close()
127
111
 
128
112
 
129
113
  def _list_federations(stub: ControlStub) -> list[Federation]:
@@ -206,7 +190,7 @@ def _show_federation(
206
190
  Returns
207
191
  -------
208
192
  tuple[list[str], list[NodeInfo], list[RunRow]]
209
- A tuple containing (member_account_ids, nodes, runs).
193
+ A tuple containing (account_names, nodes, runs).
210
194
  """
211
195
  with flwr_cli_grpc_exc_handler():
212
196
  res: ShowFederationResponse = stub.ShowFederation(
@@ -217,16 +201,20 @@ def _show_federation(
217
201
  runs = [run_from_proto(run_proto) for run_proto in fed_proto.runs]
218
202
  formatted_runs = format_runs(runs, res.now)
219
203
 
220
- return list(fed_proto.member_aids), list(fed_proto.nodes), formatted_runs
204
+ return (
205
+ [account.name for account in fed_proto.accounts],
206
+ list(fed_proto.nodes),
207
+ formatted_runs,
208
+ )
221
209
 
222
210
 
223
- def _to_members_table(member_aids: list[str]) -> Table:
211
+ def _to_members_table(account_names: list[str]) -> Table:
224
212
  """Format the provided list of federation members as a rich Table.
225
213
 
226
214
  Parameters
227
215
  ----------
228
- member_aids : list[str]
229
- List of member account identifiers.
216
+ account_names : list[str]
217
+ List of member account names.
230
218
 
231
219
  Returns
232
220
  -------
@@ -236,12 +224,12 @@ def _to_members_table(member_aids: list[str]) -> Table:
236
224
  table = Table(title="Federation Members", header_style="bold cyan", show_lines=True)
237
225
 
238
226
  table.add_column(
239
- Text("Account ID", justify="center"), style="bright_black", no_wrap=True
227
+ Text("Account Name", justify="center"), style="bright_black", no_wrap=True
240
228
  )
241
229
  table.add_column(Text("Role", justify="center"), style="bright_black", no_wrap=True)
242
230
 
243
- for member_aid in member_aids:
244
- table.add_row(member_aid, "Member")
231
+ for account_name in account_names:
232
+ table.add_row(account_name, "Member")
245
233
 
246
234
  return table
247
235
 
flwr/cli/flower_config.py CHANGED
@@ -48,6 +48,7 @@ from flwr.supercore.utils import get_flwr_home
48
48
  def _parse_simulation_options(options: dict[str, Any]) -> SuperLinkSimulationOptions:
49
49
  """Parse simulation options from a dictionary in a SuperLink connection."""
50
50
  num_supernodes = options.get(SuperLinkSimulationOptionsTomlKey.NUM_SUPERNODES)
51
+ verbose = options.get(SuperLinkSimulationOptionsTomlKey.VERBOSE)
51
52
  # Validation handled in SuperLinkSimulationOptions.__post_init__
52
53
 
53
54
  backend_dict = options.get(SuperLinkSimulationOptionsTomlKey.BACKEND)
@@ -94,6 +95,7 @@ def _parse_simulation_options(options: dict[str, Any]) -> SuperLinkSimulationOpt
94
95
  return SuperLinkSimulationOptions(
95
96
  num_supernodes=cast(int, num_supernodes),
96
97
  backend=simulation_backend,
98
+ verbose=verbose,
97
99
  )
98
100
 
99
101
 
@@ -105,6 +107,9 @@ def _serialize_simulation_options(
105
107
  SuperLinkSimulationOptionsTomlKey.NUM_SUPERNODES: options.num_supernodes
106
108
  }
107
109
 
110
+ if options.verbose is not None:
111
+ options_dict[SuperLinkSimulationOptionsTomlKey.VERBOSE] = options.verbose
112
+
108
113
  if options.backend is not None:
109
114
  backend = options.backend
110
115
 
flwr/cli/install.py CHANGED
@@ -72,7 +72,10 @@ def install(
72
72
  if source.suffix != ".fab":
73
73
  raise click.ClickException(f"The source {source} is not a `.fab` file.")
74
74
 
75
- install_from_fab(source, flwr_dir)
75
+ try:
76
+ install_from_fab(source, flwr_dir)
77
+ except ValueError as e:
78
+ raise click.ClickException(str(e)) from None
76
79
 
77
80
 
78
81
  def install_from_fab(
@@ -171,10 +174,7 @@ def validate_and_install(
171
174
  click.ClickException
172
175
  If configuration is invalid or metadata doesn't match.
173
176
  """
174
- config, _, _ = load_and_validate(project_dir / "pyproject.toml", check_module=False)
175
-
176
- if config is None:
177
- raise click.ClickException("Invalid config inside FAB file.")
177
+ config, _ = load_and_validate(project_dir / "pyproject.toml", check_module=False)
178
178
 
179
179
  fab_id, version = get_metadata_from_config(config)
180
180
  publisher, project_name = fab_id.split("/")
flwr/cli/ls.py CHANGED
@@ -15,11 +15,9 @@
15
15
  """Flower command line interface `ls` command."""
16
16
 
17
17
 
18
- import io
19
18
  import json
20
19
  from typing import Annotated
21
20
 
22
- import click
23
21
  import typer
24
22
  from rich.console import Console
25
23
  from rich.table import Table
@@ -29,7 +27,6 @@ from flwr.cli.config_migration import migrate, warn_if_federation_config_overrid
29
27
  from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
30
28
  from flwr.cli.flower_config import read_superlink_connection
31
29
  from flwr.common.constant import CliOutputFormat, Status, SubStatus
32
- from flwr.common.logger import print_json_error, redirect_output, restore_output
33
30
  from flwr.common.serde import run_from_proto
34
31
  from flwr.proto.control_pb2 import ( # pylint: disable=E0611
35
32
  ListRunsRequest,
@@ -39,7 +36,12 @@ from flwr.proto.control_pb2_grpc import ControlStub
39
36
  from flwr.supercore.utils import humanize_bytes, humanize_duration
40
37
 
41
38
  from .run_utils import RunRow, format_runs
42
- from .utils import flwr_cli_grpc_exc_handler, init_channel_from_connection
39
+ from .utils import (
40
+ cli_output_handler,
41
+ flwr_cli_grpc_exc_handler,
42
+ init_channel_from_connection,
43
+ print_json_to_stdout,
44
+ )
43
45
 
44
46
 
45
47
  def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
@@ -92,23 +94,17 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
92
94
 
93
95
  All timestamps follow ISO 8601, UTC and are formatted as ``YYYY-MM-DD HH:MM:SSZ``.
94
96
  """
95
- suppress_output = output_format == CliOutputFormat.JSON
96
- captured_output = io.StringIO()
97
-
98
- if suppress_output:
99
- redirect_output(captured_output)
100
-
101
- # Warn `--federation-config` is ignored
102
- warn_if_federation_config_overrides(federation_config_overrides)
97
+ with cli_output_handler(output_format=output_format) as is_json:
98
+ # Warn `--federation-config` is ignored
99
+ warn_if_federation_config_overrides(federation_config_overrides)
103
100
 
104
- # Migrate legacy usage if any
105
- migrate(superlink, args=ctx.args)
101
+ # Migrate legacy usage if any
102
+ migrate(superlink, args=ctx.args)
106
103
 
107
- # Read superlink connection configuration
108
- superlink_connection = read_superlink_connection(superlink)
109
- channel = None
104
+ # Read superlink connection configuration
105
+ superlink_connection = read_superlink_connection(superlink)
106
+ channel = None
110
107
 
111
- try:
112
108
  try:
113
109
  if runs and run_id is not None:
114
110
  raise ValueError(
@@ -125,9 +121,9 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
125
121
  else:
126
122
  typer.echo("📄 Listing all runs...")
127
123
  formatted_runs = _list_runs(stub)
128
- restore_output()
129
- if output_format == CliOutputFormat.JSON:
130
- Console().print_json(_to_json(formatted_runs))
124
+
125
+ if is_json:
126
+ print_json_to_stdout(_to_json(formatted_runs))
131
127
  else:
132
128
  if run_id is not None:
133
129
  Console().print(_to_detail_table(formatted_runs[0]))
@@ -136,17 +132,6 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
136
132
  finally:
137
133
  if channel:
138
134
  channel.close()
139
- except Exception as err: # pylint: disable=broad-except
140
- if suppress_output:
141
- restore_output()
142
- e_message = captured_output.getvalue()
143
- print_json_error(e_message, err)
144
- else:
145
- raise click.ClickException(str(err)) from None
146
- finally:
147
- if suppress_output:
148
- restore_output()
149
- captured_output.close()
150
135
 
151
136
 
152
137
  def _get_status_style(status_text: str) -> str: