flwr-nightly 1.26.0.dev20260122__py3-none-any.whl → 1.26.0.dev20260126__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 (35) hide show
  1. flwr/cli/app_cmd/publish.py +18 -44
  2. flwr/cli/app_cmd/review.py +8 -25
  3. flwr/cli/auth_plugin/oidc_cli_plugin.py +3 -6
  4. flwr/cli/build.py +8 -19
  5. flwr/cli/config/ls.py +8 -13
  6. flwr/cli/config_utils.py +19 -171
  7. flwr/cli/federation/ls.py +3 -7
  8. flwr/cli/flower_config.py +28 -47
  9. flwr/cli/install.py +18 -57
  10. flwr/cli/log.py +2 -2
  11. flwr/cli/login/login.py +8 -21
  12. flwr/cli/ls.py +3 -7
  13. flwr/cli/new/new.py +9 -29
  14. flwr/cli/pull.py +3 -7
  15. flwr/cli/run/run.py +6 -15
  16. flwr/cli/stop.py +5 -17
  17. flwr/cli/supernode/register.py +6 -22
  18. flwr/cli/supernode/unregister.py +3 -13
  19. flwr/cli/utils.py +66 -169
  20. flwr/common/config.py +5 -9
  21. flwr/common/constant.py +2 -0
  22. flwr/server/superlink/fleet/message_handler/message_handler.py +4 -4
  23. flwr/server/superlink/linkstate/__init__.py +0 -2
  24. flwr/server/superlink/linkstate/sql_linkstate.py +38 -10
  25. flwr/supercore/object_store/object_store_factory.py +4 -4
  26. flwr/supercore/object_store/sql_object_store.py +171 -6
  27. flwr/superlink/servicer/control/control_servicer.py +11 -12
  28. {flwr_nightly-1.26.0.dev20260122.dist-info → flwr_nightly-1.26.0.dev20260126.dist-info}/METADATA +2 -2
  29. {flwr_nightly-1.26.0.dev20260122.dist-info → flwr_nightly-1.26.0.dev20260126.dist-info}/RECORD +31 -35
  30. flwr/server/superlink/linkstate/sqlite_linkstate.py +0 -1302
  31. flwr/supercore/corestate/sqlite_corestate.py +0 -157
  32. flwr/supercore/object_store/sqlite_object_store.py +0 -253
  33. flwr/supercore/sqlite_mixin.py +0 -156
  34. {flwr_nightly-1.26.0.dev20260122.dist-info → flwr_nightly-1.26.0.dev20260126.dist-info}/WHEEL +0 -0
  35. {flwr_nightly-1.26.0.dev20260122.dist-info → flwr_nightly-1.26.0.dev20260126.dist-info}/entry_points.txt +0 -0
@@ -19,6 +19,7 @@ from contextlib import ExitStack
19
19
  from pathlib import Path
20
20
  from typing import IO, Annotated
21
21
 
22
+ import click
22
23
  import requests
23
24
  import typer
24
25
  from requests import Response
@@ -62,12 +63,7 @@ def publish(
62
63
  auth_plugin = load_cli_auth_plugin_from_connection(SUPERGRID_ADDRESS)
63
64
  auth_plugin.load_tokens()
64
65
  if not isinstance(auth_plugin, OidcCliPlugin) or not auth_plugin.access_token:
65
- typer.secho(
66
- "❌ Please log in before publishing app.",
67
- fg=typer.colors.RED,
68
- err=True,
69
- )
70
- raise typer.Exit(code=1)
66
+ raise click.ClickException("Please log in before publishing app.")
71
67
 
72
68
  # Load token from the plugin
73
69
  token = auth_plugin.access_token
@@ -82,19 +78,17 @@ def publish(
82
78
  try:
83
79
  resp = _post_files(files_param, token)
84
80
  except requests.RequestException as err:
85
- typer.secho(f"Network error: {err}", fg=typer.colors.RED, err=True)
86
- raise typer.Exit(code=1) from err
81
+ raise click.ClickException(f"Network error: {err}") from err
87
82
 
88
83
  if resp.ok:
89
84
  typer.secho("🎊 Upload successful", fg=typer.colors.GREEN, bold=True)
90
85
  return # success
91
86
 
92
87
  # Error path:
93
- msg = f"Upload failed with status {resp.status_code}"
88
+ msg = f"Upload failed with status {resp.status_code}"
94
89
  if resp.text:
95
90
  msg += f": {resp.text}"
96
- typer.secho(msg, fg=typer.colors.RED, err=True)
97
- raise typer.Exit(code=1)
91
+ raise click.ClickException(msg)
98
92
 
99
93
 
100
94
  def _depth_of(relative_path_to_root: Path) -> int:
@@ -138,14 +132,9 @@ def _collect_file_paths(root: Path) -> list[Path]:
138
132
 
139
133
  # Check max depth
140
134
  if _depth_of(relative_path) > MAX_DIR_DEPTH:
141
- typer.secho(
142
- f"Error: '{path}' "
143
- f"exceeds the maximum directory depth "
144
- f"of {MAX_DIR_DEPTH}.",
145
- fg=typer.colors.RED,
146
- err=True,
135
+ raise click.ClickException(
136
+ f"'{path}' exceeds the maximum directory depth of {MAX_DIR_DEPTH}."
147
137
  )
148
- raise typer.Exit(code=2)
149
138
 
150
139
  file_paths.append(path)
151
140
 
@@ -160,21 +149,15 @@ def _validate_files(file_paths: list[Path]) -> None:
160
149
  Checks file count, individual file size, total size, and UTF-8 encoding.
161
150
  """
162
151
  if len(file_paths) == 0:
163
- typer.secho(
152
+ raise click.ClickException(
164
153
  "Nothing to upload: no files matched after applying .gitignore and "
165
- "allowed extensions.",
166
- fg=typer.colors.RED,
167
- err=True,
154
+ "allowed extensions."
168
155
  )
169
- raise typer.Exit(code=2)
170
156
 
171
157
  if len(file_paths) > MAX_FILE_COUNT:
172
- typer.secho(
173
- f"Too many files: {len(file_paths)} > allowed maximum of {MAX_FILE_COUNT}.",
174
- fg=typer.colors.RED,
175
- err=True,
158
+ raise click.ClickException(
159
+ f"Too many files: {len(file_paths)} > allowed maximum of {MAX_FILE_COUNT}."
176
160
  )
177
- raise typer.Exit(code=2)
178
161
 
179
162
  # Calculate files size
180
163
  total_size = 0
@@ -184,34 +167,25 @@ def _validate_files(file_paths: list[Path]) -> None:
184
167
 
185
168
  # Check single file size
186
169
  if file_size > MAX_FILE_BYTES:
187
- typer.secho(
170
+ raise click.ClickException(
188
171
  f"File too large: '{path.as_posix()}' is {file_size:,} bytes, "
189
- f"exceeding the per-file limit of {MAX_FILE_BYTES:,} bytes.",
190
- fg=typer.colors.RED,
191
- err=True,
172
+ f"exceeding the per-file limit of {MAX_FILE_BYTES:,} bytes."
192
173
  )
193
- raise typer.Exit(code=2)
194
174
 
195
175
  # Ensure we can decode as UTF-8.
196
176
  try:
197
177
  path.read_text(encoding=UTF8)
198
178
  except UnicodeDecodeError as err:
199
- typer.secho(
200
- f"Encoding error: '{path}' is not UTF-8 encoded.",
201
- fg=typer.colors.RED,
202
- err=True,
203
- )
204
- raise typer.Exit(code=2) from err
179
+ raise click.ClickException(
180
+ f"Encoding error: '{path}' is not UTF-8 encoded."
181
+ ) from err
205
182
 
206
183
  # Check total files size
207
184
  if total_size > MAX_TOTAL_BYTES:
208
- typer.secho(
185
+ raise click.ClickException(
209
186
  "Total size of all files is too large: "
210
- f"{total_size:,} bytes > {MAX_TOTAL_BYTES:,} bytes.",
211
- fg=typer.colors.RED,
212
- err=True,
187
+ f"{total_size:,} bytes > {MAX_TOTAL_BYTES:,} bytes."
213
188
  )
214
- raise typer.Exit(code=2)
215
189
 
216
190
  # Print validation passed prompt
217
191
  typer.echo(typer.style("✅ Validation passed", fg=typer.colors.GREEN, bold=True))
@@ -21,6 +21,7 @@ import re
21
21
  from pathlib import Path
22
22
  from typing import Annotated
23
23
 
24
+ import click
24
25
  import requests
25
26
  import typer
26
27
  from cryptography.exceptions import UnsupportedAlgorithm
@@ -60,12 +61,7 @@ def review(
60
61
 
61
62
  auth_plugin.load_tokens()
62
63
  if not isinstance(auth_plugin, OidcCliPlugin) or not auth_plugin.access_token:
63
- typer.secho(
64
- "❌ Please log in before reviewing app.",
65
- fg=typer.colors.RED,
66
- err=True,
67
- )
68
- raise typer.Exit(code=1)
64
+ raise click.ClickException("Please log in before reviewing app.")
69
65
 
70
66
  # Load token from the plugin
71
67
  token = auth_plugin.access_token
@@ -74,8 +70,7 @@ def review(
74
70
  try:
75
71
  app_id, app_version = parse_app_spec(app_spec)
76
72
  except ValueError as e:
77
- typer.secho(f"❌ {e}", fg=typer.colors.RED, err=True)
78
- raise typer.Exit(code=1) from e
73
+ raise click.ClickException(str(e)) from e
79
74
 
80
75
  # Download FAB
81
76
  typer.secho("Downloading FAB... ", fg=typer.colors.BLUE)
@@ -83,8 +78,7 @@ def review(
83
78
  try:
84
79
  presigned_url, _ = request_download_link(app_id, app_version, url, "fab_url")
85
80
  except ValueError as e:
86
- typer.secho(f"❌ {e}", fg=typer.colors.RED, err=True)
87
- raise typer.Exit(code=1) from e
81
+ raise click.ClickException(str(e)) from e
88
82
 
89
83
  fab_bytes = _download_fab(presigned_url)
90
84
 
@@ -170,12 +164,7 @@ def _download_fab(url: str) -> bytes:
170
164
  r = requests.get(url, timeout=60)
171
165
  r.raise_for_status()
172
166
  except requests.RequestException as e:
173
- typer.secho(
174
- f"❌ FAB download failed: {e}",
175
- fg=typer.colors.RED,
176
- err=True,
177
- )
178
- raise typer.Exit(code=1) from e
167
+ raise click.ClickException(f"FAB download failed: {e}") from e
179
168
  return r.content
180
169
 
181
170
 
@@ -209,20 +198,14 @@ def _submit_review(
209
198
  try:
210
199
  resp = requests.post(url, headers=headers, json=payload, timeout=120)
211
200
  except requests.RequestException as e:
212
- typer.secho(
213
- f"❌ Network error while submitting review: {e}",
214
- fg=typer.colors.RED,
215
- err=True,
216
- )
217
- raise typer.Exit(code=1) from e
201
+ raise click.ClickException(f"Network error while submitting review: {e}") from e
218
202
 
219
203
  if resp.ok:
220
204
  typer.secho("🎊 Review submitted", fg=typer.colors.GREEN, bold=True)
221
205
  return
222
206
 
223
207
  # Error path:
224
- msg = f"Review submission failed (HTTP {resp.status_code})"
208
+ msg = f"Review submission failed (HTTP {resp.status_code})"
225
209
  if resp.text:
226
210
  msg += f": {resp.text}"
227
- typer.secho(msg, fg=typer.colors.RED, err=True)
228
- raise typer.Exit(code=1)
211
+ raise click.ClickException(msg)
@@ -19,6 +19,7 @@ import time
19
19
  import webbrowser
20
20
  from collections.abc import Sequence
21
21
 
22
+ import click
22
23
  import typer
23
24
 
24
25
  from flwr.cli.constant import (
@@ -137,13 +138,9 @@ class OidcCliPlugin(CliAuthPlugin):
137
138
  ) -> Sequence[tuple[str, str | bytes]]:
138
139
  """Write authentication tokens to the provided metadata."""
139
140
  if self.access_token is None or self.refresh_token is None:
140
- typer.secho(
141
- "Missing authentication tokens. Please login first.",
142
- fg=typer.colors.RED,
143
- bold=True,
144
- err=True,
141
+ raise click.ClickException(
142
+ "Missing authentication tokens. Please login first."
145
143
  )
146
- raise typer.Exit(code=1)
147
144
 
148
145
  return list(metadata) + [
149
146
  (ACCESS_TOKEN_KEY, self.access_token),
flwr/cli/build.py CHANGED
@@ -21,6 +21,7 @@ from io import BytesIO
21
21
  from pathlib import Path
22
22
  from typing import Annotated, Any
23
23
 
24
+ import click
24
25
  import pathspec
25
26
  import tomli
26
27
  import tomli_w
@@ -107,35 +108,23 @@ def build(
107
108
 
108
109
  app = app.resolve()
109
110
  if not app.is_dir():
110
- typer.secho(
111
- f"The path {app} is not a valid path to a Flower app.",
112
- fg=typer.colors.RED,
113
- bold=True,
114
- err=True,
111
+ raise click.ClickException(
112
+ f"The path {app} is not a valid path to a Flower app."
115
113
  )
116
- raise typer.Exit(code=1)
117
114
 
118
115
  if not is_valid_project_name(app.name):
119
- typer.secho(
120
- f"The project name {app.name} is invalid, "
116
+ raise click.ClickException(
117
+ f"The project name {app.name} is invalid, "
121
118
  "a valid project name must start with a letter, "
122
- "and can only contain letters, digits, and hyphens.",
123
- fg=typer.colors.RED,
124
- bold=True,
125
- err=True,
119
+ "and can only contain letters, digits, and hyphens."
126
120
  )
127
- raise typer.Exit(code=1)
128
121
 
129
122
  config, errors, warnings = load_and_validate(app / "pyproject.toml")
130
123
  if config is None:
131
- typer.secho(
124
+ raise click.ClickException(
132
125
  "Project configuration could not be loaded.\npyproject.toml is invalid:\n"
133
- + "\n".join([f"- {line}" for line in errors]),
134
- fg=typer.colors.RED,
135
- bold=True,
136
- err=True,
126
+ + "\n".join([f"- {line}" for line in errors])
137
127
  )
138
- raise typer.Exit(code=1)
139
128
 
140
129
  if warnings:
141
130
  typer.secho(
flwr/cli/config/ls.py CHANGED
@@ -19,6 +19,7 @@ import io
19
19
  import json
20
20
  from typing import Annotated
21
21
 
22
+ import click
22
23
  import typer
23
24
  from rich.console import Console
24
25
 
@@ -66,6 +67,8 @@ def ls(
66
67
  }
67
68
  Console().print_json(json.dumps(conn))
68
69
  else:
70
+ typer.secho("Flower Config file: ", fg=typer.colors.BLUE, nl=False)
71
+ typer.secho(f"{config_path}", fg=typer.colors.GREEN)
69
72
  typer.secho("SuperLink connections:", fg=typer.colors.BLUE)
70
73
  # List SuperLink connections and highlight default
71
74
  for k in connection_names:
@@ -77,26 +80,18 @@ def ls(
77
80
  nl=False,
78
81
  )
79
82
  typer.echo()
80
- except typer.Exit as err:
81
- # log the error if json format requested
82
- # else do nothing since it will be logged by typer
83
- if suppress_output:
84
- restore_output()
85
- e_message = captured_output.getvalue()
86
- print_json_error(e_message, err)
87
83
 
88
84
  except Exception as err: # pylint: disable=broad-except
85
+ # log the error if json format requested
89
86
  if suppress_output:
90
87
  restore_output()
91
88
  e_message = captured_output.getvalue()
92
89
  print_json_error(e_message, err)
93
90
  else:
94
- typer.secho(
95
- f"An unexpected error occurred while listing the SuperLink "
96
- f"connections in the Flower configuration file ({config_path}): {err}",
97
- fg=typer.colors.RED,
98
- err=True,
99
- )
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
100
95
 
101
96
  finally:
102
97
  if suppress_output:
flwr/cli/config_utils.py CHANGED
@@ -18,8 +18,8 @@
18
18
  from pathlib import Path
19
19
  from typing import Any
20
20
 
21
+ import click
21
22
  import tomli
22
- import typer
23
23
 
24
24
  from flwr.cli.typing import SuperLinkConnection
25
25
  from flwr.common.config import (
@@ -114,57 +114,6 @@ def load(toml_path: Path) -> dict[str, Any] | None:
114
114
  return None
115
115
 
116
116
 
117
- def process_loaded_project_config(
118
- config: dict[str, Any] | None, errors: list[str], warnings: list[str]
119
- ) -> dict[str, Any]:
120
- """Process and return the loaded project configuration.
121
-
122
- This function handles errors and warnings from the `load_and_validate` function,
123
- exits on critical issues, and returns the validated configuration.
124
-
125
- Parameters
126
- ----------
127
- config : dict[str, Any] | None
128
- The loaded configuration dictionary, or None if loading failed.
129
- errors : list[str]
130
- List of error messages from validation.
131
- warnings : list[str]
132
- List of warning messages from validation.
133
-
134
- Returns
135
- -------
136
- dict[str, Any]
137
- The validated configuration dictionary.
138
-
139
- Raises
140
- ------
141
- typer.Exit
142
- If config is None or contains critical errors.
143
- """
144
- if config is None:
145
- typer.secho(
146
- "Project configuration could not be loaded.\n"
147
- "pyproject.toml is invalid:\n"
148
- + "\n".join([f"- {line}" for line in errors]),
149
- fg=typer.colors.RED,
150
- bold=True,
151
- err=True,
152
- )
153
- raise typer.Exit(code=1)
154
-
155
- if warnings:
156
- typer.secho(
157
- "Project configuration is missing the following "
158
- "recommended properties:\n" + "\n".join([f"- {line}" for line in warnings]),
159
- fg=typer.colors.RED,
160
- bold=True,
161
- )
162
-
163
- typer.secho("Success", fg=typer.colors.GREEN)
164
-
165
- return config
166
-
167
-
168
117
  def validate_federation_in_project_config(
169
118
  federation: str | None,
170
119
  config: dict[str, Any],
@@ -188,22 +137,18 @@ def validate_federation_in_project_config(
188
137
 
189
138
  Raises
190
139
  ------
191
- typer.Exit
140
+ click.ClickException
192
141
  If no federation name provided and no default found, or if federation
193
142
  doesn't exist in config.
194
143
  """
195
144
  federation = federation or config["tool"]["flwr"]["federations"].get("default")
196
145
 
197
146
  if federation is None:
198
- typer.secho(
199
- "No federation name was provided and the project's `pyproject.toml` "
147
+ raise click.ClickException(
148
+ "No federation name was provided and the project's `pyproject.toml` "
200
149
  "doesn't declare a default federation (with an Control API address or an "
201
- "`options.num-supernodes` value).",
202
- fg=typer.colors.RED,
203
- bold=True,
204
- err=True,
150
+ "`options.num-supernodes` value)."
205
151
  )
206
- raise typer.Exit(code=1)
207
152
 
208
153
  # Validate the federation exists in the configuration
209
154
  federation_config = config["tool"]["flwr"]["federations"].get(federation)
@@ -211,15 +156,11 @@ def validate_federation_in_project_config(
211
156
  available_feds = {
212
157
  fed for fed in config["tool"]["flwr"]["federations"] if fed != "default"
213
158
  }
214
- typer.secho(
215
- f"There is no `{federation}` federation declared in the "
159
+ raise click.ClickException(
160
+ f"There is no `{federation}` federation declared in the "
216
161
  "`pyproject.toml`.\n The following federations were found:\n\n"
217
- + "\n".join(available_feds),
218
- fg=typer.colors.RED,
219
- bold=True,
220
- err=True,
162
+ + "\n".join(available_feds)
221
163
  )
222
- raise typer.Exit(code=1)
223
164
 
224
165
  # Override the federation configuration if provided
225
166
  if overrides:
@@ -229,61 +170,6 @@ def validate_federation_in_project_config(
229
170
  return federation, federation_config
230
171
 
231
172
 
232
- def validate_certificate_in_federation_config(
233
- app: Path, federation_config: dict[str, Any]
234
- ) -> tuple[bool, bytes | None]:
235
- """Validate the certificates in the Flower project configuration.
236
-
237
- Accepted configurations:
238
- 1. TLS enabled and gRPC will load(*) the trusted certificate bundle:
239
- - Only `address` is provided. `root-certificates` and `insecure` not set.
240
- - `address` is provided and `insecure` set to `false`. `root-certificates` not
241
- set.
242
- (*)gRPC uses a multi-step fallback mechanism to load the trusted certificate
243
- bundle in the following sequence:
244
- a. A configured file path (if set via configuration or environment),
245
- b. An override callback (if registered via
246
- `grpc_set_ssl_roots_override_callback`),
247
- c. The OS trust store (if available),
248
- d. A bundled default certificate file.
249
- 2. TLS enabled with self-signed certificates:
250
- - `address` and `root-certificates` are provided. `insecure` not set.
251
- - `address` and `root-certificates` are provided. `insecure` set to `false`.
252
- 3. TLS disabled. This is not recommended and should only be used for prototyping:
253
- - `address` is provided and `insecure = true`. If `root-certificates` is
254
- set, exit with an error.
255
- """
256
- insecure = get_insecure_flag(federation_config)
257
-
258
- # Process root certificates
259
- if root_certificates := federation_config.get("root-certificates"):
260
- if insecure:
261
- typer.secho(
262
- "❌ `root-certificates` were provided but the `insecure` parameter "
263
- "is set to `True`.",
264
- fg=typer.colors.RED,
265
- bold=True,
266
- err=True,
267
- )
268
- raise typer.Exit(code=1)
269
-
270
- # TLS is enabled with self-signed certificates: attempt to read the file
271
- try:
272
- root_certificates_bytes = (app / root_certificates).read_bytes()
273
- except Exception as e:
274
- typer.secho(
275
- f"❌ Failed to read certificate file `{root_certificates}`: {e}",
276
- fg=typer.colors.RED,
277
- bold=True,
278
- err=True,
279
- )
280
- raise typer.Exit(code=1) from e
281
- else:
282
- root_certificates_bytes = None
283
-
284
- return insecure, root_certificates_bytes
285
-
286
-
287
173
  def load_certificate_in_connection(
288
174
  connection: SuperLinkConnection,
289
175
  ) -> bytes | None:
@@ -304,64 +190,30 @@ def load_certificate_in_connection(
304
190
  ------
305
191
  ValueError
306
192
  If required TLS settings are missing.
307
- typer.Exit
193
+ click.ClickException
308
194
  If the configuration is invalid or the certificate file cannot be read.
309
195
  """
310
196
  # Process root certificates
311
197
  if root_certificates := connection.root_certificates:
312
198
  if connection.insecure:
313
- typer.secho(
314
- "`root-certificates` were provided but the `insecure` parameter "
315
- "is set to `True`.",
316
- fg=typer.colors.RED,
317
- bold=True,
318
- err=True,
199
+ raise click.ClickException(
200
+ "`root-certificates` were provided but the `insecure` parameter "
201
+ "is set to `True`."
319
202
  )
320
- raise typer.Exit(code=1)
321
203
 
322
204
  # TLS is enabled with self-signed certificates: attempt to read the file
323
205
  try:
324
206
  root_certificates_bytes = Path(root_certificates).read_bytes()
325
207
  except Exception as e:
326
- typer.secho(
327
- f"Failed to read certificate file `{root_certificates}`: {e}",
328
- fg=typer.colors.RED,
329
- bold=True,
330
- err=True,
331
- )
332
- raise typer.Exit(code=1) from e
208
+ raise click.ClickException(
209
+ f"Failed to read certificate file `{root_certificates}`: {e}"
210
+ ) from e
333
211
  else:
334
212
  root_certificates_bytes = None
335
213
 
336
214
  return root_certificates_bytes
337
215
 
338
216
 
339
- def exit_if_no_address(federation_config: dict[str, Any], cmd: str) -> None:
340
- """Exit if the provided federation_config has no "address" key.
341
-
342
- Parameters
343
- ----------
344
- federation_config : dict[str, Any]
345
- The federation configuration dictionary to check.
346
- cmd : str
347
- The command name to display in the error message.
348
-
349
- Raises
350
- ------
351
- typer.Exit
352
- If 'address' key is not present in federation_config.
353
- """
354
- if "address" not in federation_config:
355
- typer.secho(
356
- f"❌ `flwr {cmd}` currently works with a SuperLink. Ensure that the "
357
- "correct SuperLink (Control API) address is provided in `pyproject.toml`.",
358
- fg=typer.colors.RED,
359
- bold=True,
360
- err=True,
361
- )
362
- raise typer.Exit(code=1)
363
-
364
-
365
217
  def get_insecure_flag(federation_config: dict[str, Any]) -> bool:
366
218
  """Extract and validate the `insecure` flag from the federation configuration.
367
219
 
@@ -377,7 +229,7 @@ def get_insecure_flag(federation_config: dict[str, Any]) -> bool:
377
229
 
378
230
  Raises
379
231
  ------
380
- typer.Exit
232
+ click.ClickException
381
233
  If insecure value is not a boolean type.
382
234
  """
383
235
  insecure_value = federation_config.get("insecure")
@@ -387,11 +239,7 @@ def get_insecure_flag(federation_config: dict[str, Any]) -> bool:
387
239
  return False
388
240
  if isinstance(insecure_value, bool):
389
241
  return insecure_value
390
- typer.secho(
391
- "Invalid type for `insecure`: expected a boolean if provided. "
392
- "(`insecure = true` or `insecure = false`)",
393
- fg=typer.colors.RED,
394
- bold=True,
395
- err=True,
242
+ raise click.ClickException(
243
+ "Invalid type for `insecure`: expected a boolean if provided. "
244
+ "(`insecure = true` or `insecure = false`)"
396
245
  )
397
- raise typer.Exit(code=1)
flwr/cli/federation/ls.py CHANGED
@@ -18,6 +18,7 @@
18
18
  import io
19
19
  from typing import Annotated, Any
20
20
 
21
+ import click
21
22
  import typer
22
23
  from rich.console import Console
23
24
  from rich.table import Table
@@ -112,18 +113,13 @@ def ls( # pylint: disable=R0914, R0913, R0917, R0912
112
113
  finally:
113
114
  if channel:
114
115
  channel.close()
115
- except (typer.Exit, Exception) as err: # pylint: disable=broad-except
116
+ except Exception as err: # pylint: disable=broad-except
116
117
  if suppress_output:
117
118
  restore_output()
118
119
  e_message = captured_output.getvalue()
119
120
  print_json_error(e_message, err)
120
121
  else:
121
- typer.secho(
122
- f"{err}",
123
- fg=typer.colors.RED,
124
- bold=True,
125
- err=True,
126
- )
122
+ raise click.ClickException(str(err)) from None
127
123
  finally:
128
124
  if suppress_output:
129
125
  restore_output()