flwr-nightly 1.26.0.dev20260127__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 (49) hide show
  1. flwr/cli/app_cmd/review.py +7 -7
  2. flwr/cli/build.py +9 -11
  3. flwr/cli/config/ls.py +6 -34
  4. flwr/cli/config_migration.py +7 -10
  5. flwr/cli/config_utils.py +18 -12
  6. flwr/cli/constant.py +1 -1
  7. flwr/cli/federation/ls.py +31 -43
  8. flwr/cli/flower_config.py +5 -0
  9. flwr/cli/install.py +5 -5
  10. flwr/cli/ls.py +18 -33
  11. flwr/cli/run/run.py +39 -41
  12. flwr/cli/stop.py +21 -41
  13. flwr/cli/supernode/ls.py +16 -34
  14. flwr/cli/supernode/register.py +18 -37
  15. flwr/cli/supernode/unregister.py +16 -38
  16. flwr/cli/typing.py +13 -5
  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/exit/exit_code.py +6 -0
  23. flwr/common/retry_invoker.py +13 -5
  24. flwr/common/typing.py +2 -1
  25. flwr/proto/federation_pb2.py +5 -3
  26. flwr/proto/federation_pb2.pyi +22 -4
  27. flwr/server/app.py +4 -11
  28. flwr/server/grid/grpc_grid.py +3 -3
  29. flwr/simulation/run_simulation.py +1 -11
  30. flwr/simulation/simulationio_connection.py +3 -3
  31. flwr/supercore/address.py +9 -0
  32. flwr/supercore/constant.py +5 -2
  33. flwr/supercore/sql_mixin.py +2 -2
  34. flwr/supercore/state/alembic/__init__.py +15 -0
  35. flwr/supercore/state/alembic/env.py +103 -0
  36. flwr/supercore/state/alembic/script.py.mako +43 -0
  37. flwr/supercore/state/alembic/utils.py +239 -0
  38. flwr/supercore/state/alembic/versions/__init__.py +15 -0
  39. flwr/supercore/state/alembic/versions/rev_2026_01_28_initialize_migration_of_state_tables.py +200 -0
  40. flwr/supercore/superexec/run_superexec.py +2 -2
  41. flwr/supercore/utils.py +21 -0
  42. flwr/superlink/federation/noop_federation_manager.py +3 -2
  43. flwr/superlink/servicer/control/control_servicer.py +1 -1
  44. flwr/supernode/runtime/run_clientapp.py +2 -2
  45. flwr/supernode/start_client_internal.py +7 -4
  46. {flwr_nightly-1.26.0.dev20260127.dist-info → flwr_nightly-1.26.0.dev20260129.dist-info}/METADATA +3 -2
  47. {flwr_nightly-1.26.0.dev20260127.dist-info → flwr_nightly-1.26.0.dev20260129.dist-info}/RECORD +49 -43
  48. {flwr_nightly-1.26.0.dev20260127.dist-info → flwr_nightly-1.26.0.dev20260129.dist-info}/WHEEL +0 -0
  49. {flwr_nightly-1.26.0.dev20260127.dist-info → flwr_nightly-1.26.0.dev20260129.dist-info}/entry_points.txt +0 -0
flwr/cli/run/run.py CHANGED
@@ -16,19 +16,17 @@
16
16
 
17
17
 
18
18
  import hashlib
19
- import io
20
19
  import json
21
20
  import subprocess
22
21
  from pathlib import Path
23
- from typing import Annotated, Any, cast
22
+ from typing import Annotated, Any
24
23
 
25
24
  import click
26
25
  import typer
27
- from rich.console import Console
28
26
 
29
27
  from flwr.cli.build import build_fab_from_disk, get_fab_filename
30
28
  from flwr.cli.config_migration import migrate, warn_if_federation_config_overrides
31
- from flwr.cli.config_utils import load as load_toml
29
+ from flwr.cli.config_utils import load_and_validate
32
30
  from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE, RUN_CONFIG_HELP_MESSAGE
33
31
  from flwr.cli.flower_config import (
34
32
  _serialize_simulation_options,
@@ -36,12 +34,12 @@ from flwr.cli.flower_config import (
36
34
  )
37
35
  from flwr.cli.typing import SuperLinkConnection, SuperLinkSimulationOptions
38
36
  from flwr.common.config import (
37
+ flatten_dict,
39
38
  get_metadata_from_config,
40
39
  parse_config_args,
41
40
  user_config_to_configrecord,
42
41
  )
43
42
  from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
44
- from flwr.common.logger import print_json_error, redirect_output, restore_output
45
43
  from flwr.common.serde import config_record_to_proto, fab_to_proto, user_config_to_proto
46
44
  from flwr.common.typing import Fab
47
45
  from flwr.proto.control_pb2 import StartRunRequest # pylint: disable=E0611
@@ -49,7 +47,12 @@ from flwr.proto.control_pb2_grpc import ControlStub
49
47
  from flwr.supercore.utils import parse_app_spec
50
48
 
51
49
  from ..log import start_stream
52
- from ..utils import flwr_cli_grpc_exc_handler, init_channel_from_connection
50
+ from ..utils import (
51
+ cli_output_handler,
52
+ flwr_cli_grpc_exc_handler,
53
+ init_channel_from_connection,
54
+ print_json_to_stdout,
55
+ )
53
56
 
54
57
  CONN_REFRESH_PERIOD = 60 # Connection refresh period for log streaming (seconds)
55
58
 
@@ -98,25 +101,19 @@ def run(
98
101
  ] = CliOutputFormat.DEFAULT,
99
102
  ) -> None:
100
103
  """Run Flower App."""
101
- suppress_output = output_format == CliOutputFormat.JSON
102
- captured_output = io.StringIO()
103
-
104
- if suppress_output:
105
- redirect_output(captured_output)
106
-
107
- # Warn `--federation-config` is ignored
108
- warn_if_federation_config_overrides(federation_config_overrides)
104
+ with cli_output_handler(output_format=output_format) as is_json:
105
+ # Warn `--federation-config` is ignored
106
+ warn_if_federation_config_overrides(federation_config_overrides)
109
107
 
110
- # Migrate legacy usage if any
111
- migrate(str(app), [], ignore_legacy_usage=True)
108
+ # Migrate legacy usage if any
109
+ migrate(str(app), [], ignore_legacy_usage=True)
112
110
 
113
- # Read superlink connection configuration
114
- superlink_connection = read_superlink_connection(superlink)
115
-
116
- try:
111
+ # Read superlink connection configuration
112
+ superlink_connection = read_superlink_connection(superlink)
117
113
 
118
114
  # Determine if app is remote
119
115
  app_spec = None
116
+ config: dict[str, Any] = {}
120
117
  if (app_str := str(app)).startswith("@"):
121
118
  # Validate app version and ID format
122
119
  try:
@@ -125,16 +122,27 @@ def run(
125
122
  raise click.ClickException(str(e)) from e
126
123
 
127
124
  app_spec = app_str
128
- # Set `app` to current directory for credential storage
129
- app = Path(".")
125
+
126
+ # Validate TOML configuration for local app
127
+ else:
128
+ config, warnings = load_and_validate(app / FAB_CONFIG_FILE)
129
+ if warnings:
130
+ typer.secho(
131
+ "Missing recommended fields in Flower App configuration "
132
+ "(pyproject.toml):\n"
133
+ + "\n".join([f"- {line}" for line in warnings]),
134
+ fg=typer.colors.YELLOW,
135
+ bold=True,
136
+ )
130
137
 
131
138
  if superlink_connection.address:
132
139
  _run_with_control_api(
133
140
  app,
141
+ config,
134
142
  superlink_connection,
135
143
  run_config_overrides,
136
144
  stream,
137
- output_format,
145
+ is_json,
138
146
  app_spec,
139
147
  )
140
148
  else:
@@ -143,26 +151,16 @@ def run(
143
151
  simulation_options=superlink_connection.options, # type: ignore
144
152
  config_overrides=run_config_overrides,
145
153
  )
146
- except Exception as err: # pylint: disable=broad-except
147
- if suppress_output:
148
- restore_output()
149
- e_message = captured_output.getvalue()
150
- print_json_error(e_message, err)
151
- else:
152
- raise click.ClickException(str(err)) from None
153
- finally:
154
- if suppress_output:
155
- restore_output()
156
- captured_output.close()
157
154
 
158
155
 
159
156
  # pylint: disable-next=R0913, R0914, R0917
160
157
  def _run_with_control_api(
161
158
  app: Path,
159
+ config: dict[str, Any],
162
160
  superlink_connection: SuperLinkConnection,
163
161
  config_overrides: list[str] | None,
164
162
  stream: bool,
165
- output_format: str,
163
+ is_json: bool,
166
164
  app_spec: str | None,
167
165
  ) -> None:
168
166
  channel = None
@@ -175,7 +173,6 @@ def _run_with_control_api(
175
173
  if not is_remote_app:
176
174
  fab_bytes = build_fab_from_disk(app)
177
175
  fab_hash = hashlib.sha256(fab_bytes).hexdigest()
178
- config = cast(dict[str, Any], load_toml(app / FAB_CONFIG_FILE))
179
176
  fab_id, fab_version = get_metadata_from_config(config)
180
177
  fab = Fab(fab_hash, fab_bytes, {})
181
178
  # Skip FAB build if remote app
@@ -187,7 +184,9 @@ def _run_with_control_api(
187
184
  # Construct a `ConfigRecord` out of a flattened `UserConfig`
188
185
  options = {}
189
186
  if superlink_connection.options:
190
- options = _serialize_simulation_options(superlink_connection.options)
187
+ options = flatten_dict(
188
+ _serialize_simulation_options(superlink_connection.options)
189
+ )
191
190
 
192
191
  c_record = user_config_to_configrecord(options)
193
192
 
@@ -208,7 +207,7 @@ def _run_with_control_api(
208
207
  else:
209
208
  raise click.ClickException("Failed to start run")
210
209
 
211
- if output_format == CliOutputFormat.JSON:
210
+ if is_json:
212
211
  # Only include FAB metadata if we actually built a local FAB
213
212
  payload: dict[str, Any] = {
214
213
  "success": res.HasField("run_id"),
@@ -224,8 +223,7 @@ def _run_with_control_api(
224
223
  "fab-filename": get_fab_filename(config, fab_hash),
225
224
  }
226
225
  )
227
- restore_output()
228
- Console().print_json(json.dumps(payload))
226
+ print_json_to_stdout(payload)
229
227
 
230
228
  if stream:
231
229
  start_stream(res.run_id, channel, CONN_REFRESH_PERIOD)
@@ -241,7 +239,7 @@ def _run_without_control_api(
241
239
  ) -> None:
242
240
 
243
241
  num_supernodes = simulation_options.num_supernodes
244
- verbose = False # bool | None = superlink_connection.options.verbose
242
+ verbose = simulation_options.verbose or False
245
243
 
246
244
  command = [
247
245
  "flower-simulation",
flwr/cli/stop.py CHANGED
@@ -15,26 +15,27 @@
15
15
  """Flower command line interface `stop` command."""
16
16
 
17
17
 
18
- import io
19
- import json
20
18
  from typing import Annotated
21
19
 
22
20
  import click
23
21
  import typer
24
- from rich.console import Console
25
22
 
26
23
  from flwr.cli.config_migration import migrate, warn_if_federation_config_overrides
27
24
  from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
28
25
  from flwr.cli.flower_config import read_superlink_connection
29
26
  from flwr.common.constant import CliOutputFormat
30
- from flwr.common.logger import print_json_error, redirect_output, restore_output
31
27
  from flwr.proto.control_pb2 import ( # pylint: disable=E0611
32
28
  StopRunRequest,
33
29
  StopRunResponse,
34
30
  )
35
31
  from flwr.proto.control_pb2_grpc import ControlStub
36
32
 
37
- from .utils import flwr_cli_grpc_exc_handler, init_channel_from_connection
33
+ from .utils import (
34
+ cli_output_handler,
35
+ flwr_cli_grpc_exc_handler,
36
+ init_channel_from_connection,
37
+ print_json_to_stdout,
38
+ )
38
39
 
39
40
 
40
41
  def stop( # pylint: disable=R0914
@@ -69,48 +70,29 @@ def stop( # pylint: disable=R0914
69
70
  This command stops a running Flower App execution by sending a stop request to the
70
71
  SuperLink via the Control API.
71
72
  """
72
- suppress_output = output_format == CliOutputFormat.JSON
73
- captured_output = io.StringIO()
74
-
75
- if suppress_output:
76
- redirect_output(captured_output)
77
-
78
- # Warn `--federation-config` is ignored
79
- warn_if_federation_config_overrides(federation_config_overrides)
73
+ with cli_output_handler(output_format=output_format) as is_json:
74
+ # Warn `--federation-config` is ignored
75
+ warn_if_federation_config_overrides(federation_config_overrides)
80
76
 
81
- migrate(superlink, args=ctx.args)
77
+ migrate(superlink, args=ctx.args)
82
78
 
83
- # Read superlink connection configuration
84
- superlink_connection = read_superlink_connection(superlink)
85
- channel = None
79
+ # Read superlink connection configuration
80
+ superlink_connection = read_superlink_connection(superlink)
81
+ channel = None
86
82
 
87
- try:
88
83
  try:
89
84
  channel = init_channel_from_connection(superlink_connection)
90
85
  stub = ControlStub(channel) # pylint: disable=unused-variable # noqa: F841
91
86
 
92
87
  typer.secho(f"✋ Stopping run ID {run_id}...", fg=typer.colors.GREEN)
93
- _stop_run(stub=stub, run_id=run_id, output_format=output_format)
88
+ _stop_run(stub=stub, run_id=run_id, is_json=is_json)
94
89
 
95
- except ValueError as err:
96
- raise click.ClickException(str(err)) from err
97
90
  finally:
98
91
  if channel:
99
92
  channel.close()
100
- except Exception as err: # pylint: disable=broad-except
101
- if suppress_output:
102
- restore_output()
103
- e_message = captured_output.getvalue()
104
- print_json_error(e_message, err)
105
- else:
106
- raise click.ClickException(str(err)) from None
107
- finally:
108
- if suppress_output:
109
- restore_output()
110
- captured_output.close()
111
-
112
-
113
- def _stop_run(stub: ControlStub, run_id: int, output_format: str) -> None:
93
+
94
+
95
+ def _stop_run(stub: ControlStub, run_id: int, is_json: bool) -> None:
114
96
  """Stop a run and display the result.
115
97
 
116
98
  Parameters
@@ -119,21 +101,19 @@ def _stop_run(stub: ControlStub, run_id: int, output_format: str) -> None:
119
101
  The gRPC stub for Control API communication.
120
102
  run_id : int
121
103
  The unique identifier of the run to stop.
122
- output_format : str
123
- Output format ('default' or 'json').
104
+ is_json : bool
105
+ Whether JSON output format is requested.
124
106
  """
125
107
  with flwr_cli_grpc_exc_handler():
126
108
  response: StopRunResponse = stub.StopRun(request=StopRunRequest(run_id=run_id))
127
109
  if response.success:
128
110
  typer.secho(f"✅ Run {run_id} successfully stopped.", fg=typer.colors.GREEN)
129
- if output_format == CliOutputFormat.JSON:
130
- run_output = json.dumps(
111
+ if is_json:
112
+ print_json_to_stdout(
131
113
  {
132
114
  "success": True,
133
115
  "run-id": f"{run_id}",
134
116
  }
135
117
  )
136
- restore_output()
137
- Console().print_json(run_output)
138
118
  else:
139
119
  raise click.ClickException(f"Run {run_id} couldn't be stopped.")
flwr/cli/supernode/ls.py CHANGED
@@ -15,7 +15,6 @@
15
15
  """Flower command line interface `supernode list` command."""
16
16
 
17
17
 
18
- import io
19
18
  import json
20
19
  from datetime import datetime, timedelta
21
20
  from typing import Annotated
@@ -28,7 +27,6 @@ from rich.text import Text
28
27
  from flwr.cli.config_migration import migrate
29
28
  from flwr.cli.flower_config import read_superlink_connection
30
29
  from flwr.common.constant import NOOP_ACCOUNT_NAME, CliOutputFormat
31
- from flwr.common.logger import print_json_error, redirect_output, restore_output
32
30
  from flwr.proto.control_pb2 import ( # pylint: disable=E0611
33
31
  ListNodesRequest,
34
32
  ListNodesResponse,
@@ -38,7 +36,12 @@ from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
38
36
  from flwr.supercore.date import isoformat8601_utc
39
37
  from flwr.supercore.utils import humanize_duration
40
38
 
41
- 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
+ )
42
45
 
43
46
  _NodeListType = tuple[int, str, str, str, str, str, str, str, float]
44
47
 
@@ -66,50 +69,29 @@ def ls( # pylint: disable=R0914, R0913, R0917
66
69
  ),
67
70
  ] = False,
68
71
  ) -> None:
69
- """List SuperNodes in the federation."""
70
- suppress_output = output_format == CliOutputFormat.JSON
71
- captured_output = io.StringIO()
72
-
73
- if suppress_output:
74
- redirect_output(captured_output)
75
-
76
- # Migrate legacy usage if any
77
- migrate(superlink, args=ctx.args)
72
+ """List SuperNodes in the federation (alias: ls)."""
73
+ with cli_output_handler(output_format=output_format) as is_json:
74
+ # Migrate legacy usage if any
75
+ migrate(superlink, args=ctx.args)
78
76
 
79
- # Read superlink connection configuration
80
- superlink_connection = read_superlink_connection(superlink)
81
- channel = None
77
+ # Read superlink connection configuration
78
+ superlink_connection = read_superlink_connection(superlink)
79
+ channel = None
82
80
 
83
- try:
84
81
  try:
85
82
  channel = init_channel_from_connection(superlink_connection)
86
83
  stub = ControlStub(channel)
87
84
  typer.echo("📄 Listing all nodes...")
88
85
  formatted_nodes = _list_nodes(stub)
89
- restore_output()
90
- if output_format == CliOutputFormat.JSON:
91
- Console().print_json(_to_json(formatted_nodes, verbose=verbose))
86
+
87
+ if is_json:
88
+ print_json_to_stdout(_to_json(formatted_nodes, verbose=verbose))
92
89
  else:
93
90
  Console().print(_to_table(formatted_nodes, verbose=verbose))
94
91
 
95
92
  finally:
96
93
  if channel:
97
94
  channel.close()
98
- except (typer.Exit, Exception) as err: # pylint: disable=broad-except
99
- if suppress_output:
100
- restore_output()
101
- e_message = captured_output.getvalue()
102
- print_json_error(e_message, err)
103
- else:
104
- typer.secho(
105
- f"{err}",
106
- fg=typer.colors.RED,
107
- bold=True,
108
- )
109
- finally:
110
- if suppress_output:
111
- restore_output()
112
- captured_output.close()
113
95
 
114
96
 
115
97
  def _list_nodes(stub: ControlStub) -> list[_NodeListType]:
@@ -15,8 +15,6 @@
15
15
  """Flower command line interface `supernode register` command."""
16
16
 
17
17
 
18
- import io
19
- import json
20
18
  from pathlib import Path
21
19
  from typing import Annotated
22
20
 
@@ -25,13 +23,11 @@ import typer
25
23
  from cryptography.exceptions import UnsupportedAlgorithm
26
24
  from cryptography.hazmat.primitives import serialization
27
25
  from cryptography.hazmat.primitives.asymmetric import ec
28
- from rich.console import Console
29
26
 
30
27
  from flwr.cli.config_migration import migrate
31
28
  from flwr.cli.flower_config import read_superlink_connection
32
29
  from flwr.common.constant import CliOutputFormat
33
30
  from flwr.common.exit import ExitCode, flwr_exit
34
- from flwr.common.logger import print_json_error, redirect_output, restore_output
35
31
  from flwr.proto.control_pb2 import ( # pylint: disable=E0611
36
32
  RegisterNodeRequest,
37
33
  RegisterNodeResponse,
@@ -39,7 +35,12 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
39
35
  from flwr.proto.control_pb2_grpc import ControlStub
40
36
  from flwr.supercore.primitives.asymmetric import public_key_to_bytes, uses_nist_ec_curve
41
37
 
42
- from ..utils import flwr_cli_grpc_exc_handler, init_channel_from_connection
38
+ from ..utils import (
39
+ cli_output_handler,
40
+ flwr_cli_grpc_exc_handler,
41
+ init_channel_from_connection,
42
+ print_json_to_stdout,
43
+ )
43
44
 
44
45
 
45
46
  def register( # pylint: disable=R0914
@@ -64,52 +65,34 @@ def register( # pylint: disable=R0914
64
65
  ] = CliOutputFormat.DEFAULT,
65
66
  ) -> None:
66
67
  """Add a SuperNode to the federation."""
67
- suppress_output = output_format == CliOutputFormat.JSON
68
- captured_output = io.StringIO()
69
-
70
68
  # Load public key
71
69
  public_key_path = Path(public_key)
72
70
  public_key_bytes = try_load_public_key(public_key_path)
73
71
 
74
- if suppress_output:
75
- redirect_output(captured_output)
72
+ with cli_output_handler(output_format=output_format) as is_json:
73
+ # Migrate legacy usage if any
74
+ migrate(superlink, args=ctx.args)
76
75
 
77
- # Migrate legacy usage if any
78
- migrate(superlink, args=ctx.args)
76
+ # Read superlink connection configuration
77
+ superlink_connection = read_superlink_connection(superlink)
78
+ channel = None
79
79
 
80
- # Read superlink connection configuration
81
- superlink_connection = read_superlink_connection(superlink)
82
- channel = None
83
-
84
- try:
85
80
  try:
86
81
  channel = init_channel_from_connection(superlink_connection)
87
82
  stub = ControlStub(channel)
88
83
 
89
84
  _register_node(
90
- stub=stub, public_key=public_key_bytes, output_format=output_format
85
+ stub=stub,
86
+ public_key=public_key_bytes,
87
+ is_json=is_json,
91
88
  )
92
89
 
93
- except ValueError as err:
94
- raise click.ClickException(str(err)) from err
95
90
  finally:
96
91
  if channel:
97
92
  channel.close()
98
93
 
99
- except (typer.Exit, Exception) as err: # pylint: disable=broad-except
100
- if suppress_output:
101
- restore_output()
102
- e_message = captured_output.getvalue()
103
- print_json_error(e_message, err)
104
- else:
105
- raise click.ClickException(str(err)) from None
106
- finally:
107
- if suppress_output:
108
- restore_output()
109
- captured_output.close()
110
-
111
94
 
112
- def _register_node(stub: ControlStub, public_key: bytes, output_format: str) -> None:
95
+ def _register_node(stub: ControlStub, public_key: bytes, is_json: bool) -> None:
113
96
  """Register a node."""
114
97
  with flwr_cli_grpc_exc_handler():
115
98
  response: RegisterNodeResponse = stub.RegisterNode(
@@ -120,15 +103,13 @@ def _register_node(stub: ControlStub, public_key: bytes, output_format: str) ->
120
103
  f"✅ SuperNode {response.node_id} registered successfully.",
121
104
  fg=typer.colors.GREEN,
122
105
  )
123
- if output_format == CliOutputFormat.JSON:
124
- run_output = json.dumps(
106
+ if is_json:
107
+ print_json_to_stdout(
125
108
  {
126
109
  "success": True,
127
110
  "node-id": response.node_id,
128
111
  }
129
112
  )
130
- restore_output()
131
- Console().print_json(run_output)
132
113
  else:
133
114
  raise click.ClickException("SuperNode couldn't be registered.")
134
115
 
@@ -15,22 +15,22 @@
15
15
  """Flower command line interface `supernode unregister` 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.cli.config_migration import migrate
27
23
  from flwr.cli.flower_config import read_superlink_connection
28
24
  from flwr.common.constant import CliOutputFormat
29
- from flwr.common.logger import print_json_error, redirect_output, restore_output
30
25
  from flwr.proto.control_pb2 import UnregisterNodeRequest # pylint: disable=E0611
31
26
  from flwr.proto.control_pb2_grpc import ControlStub
32
27
 
33
- from ..utils import flwr_cli_grpc_exc_handler, init_channel_from_connection
28
+ from ..utils import (
29
+ cli_output_handler,
30
+ flwr_cli_grpc_exc_handler,
31
+ init_channel_from_connection,
32
+ print_json_to_stdout,
33
+ )
34
34
 
35
35
 
36
36
  def unregister( # pylint: disable=R0914
@@ -55,49 +55,29 @@ def unregister( # pylint: disable=R0914
55
55
  ] = CliOutputFormat.DEFAULT,
56
56
  ) -> None:
57
57
  """Unregister a SuperNode from the federation."""
58
- suppress_output = output_format == CliOutputFormat.JSON
59
- captured_output = io.StringIO()
58
+ with cli_output_handler(output_format=output_format) as is_json:
59
+ # Migrate legacy usage if any
60
+ migrate(superlink, args=ctx.args)
60
61
 
61
- if suppress_output:
62
- redirect_output(captured_output)
62
+ # Read superlink connection configuration
63
+ superlink_connection = read_superlink_connection(superlink)
64
+ channel = None
63
65
 
64
- # Migrate legacy usage if any
65
- migrate(superlink, args=ctx.args)
66
-
67
- # Read superlink connection configuration
68
- superlink_connection = read_superlink_connection(superlink)
69
- channel = None
70
-
71
- try:
72
66
  try:
73
67
  channel = init_channel_from_connection(superlink_connection)
74
68
  stub = ControlStub(channel)
75
69
 
76
- _unregister_node(stub=stub, node_id=node_id, output_format=output_format)
70
+ _unregister_node(stub=stub, node_id=node_id, is_json=is_json)
77
71
 
78
- except ValueError as err:
79
- raise click.ClickException(str(err)) from err
80
72
  finally:
81
73
  if channel:
82
74
  channel.close()
83
75
 
84
- except (typer.Exit, Exception) as err: # pylint: disable=broad-except
85
- if suppress_output:
86
- restore_output()
87
- e_message = captured_output.getvalue()
88
- print_json_error(e_message, err)
89
- else:
90
- raise click.ClickException(str(err)) from None
91
- finally:
92
- if suppress_output:
93
- restore_output()
94
- captured_output.close()
95
-
96
76
 
97
77
  def _unregister_node(
98
78
  stub: ControlStub,
99
79
  node_id: int,
100
- output_format: str,
80
+ is_json: bool,
101
81
  ) -> None:
102
82
  """Unregister a SuperNode from the federation."""
103
83
  with flwr_cli_grpc_exc_handler():
@@ -105,12 +85,10 @@ def _unregister_node(
105
85
  typer.secho(
106
86
  f"✅ SuperNode {node_id} unregistered successfully.", fg=typer.colors.GREEN
107
87
  )
108
- if output_format == CliOutputFormat.JSON:
109
- run_output = json.dumps(
88
+ if is_json:
89
+ print_json_to_stdout(
110
90
  {
111
91
  "success": True,
112
92
  "node-id": node_id,
113
93
  }
114
94
  )
115
- restore_output()
116
- Console().print_json(run_output)
flwr/cli/typing.py CHANGED
@@ -22,6 +22,7 @@ from flwr.cli.constant import (
22
22
  DEFAULT_SIMULATION_BACKEND_NAME,
23
23
  SuperLinkConnectionTomlKey,
24
24
  )
25
+ from flwr.supercore.utils import check_federation_format
25
26
 
26
27
  _ERROR_MSG_FMT = "SuperLinkConnection.%s is None"
27
28
 
@@ -82,6 +83,7 @@ class SuperLinkSimulationOptions:
82
83
 
83
84
  num_supernodes: int
84
85
  backend: SimulationBackendConfig | None = None
86
+ verbose: bool | None = None
85
87
 
86
88
  def __post_init__(self) -> None:
87
89
  """Validate simulation options."""
@@ -89,6 +91,8 @@ class SuperLinkSimulationOptions:
89
91
  raise ValueError(
90
92
  "Invalid simulation options: num-supernodes must be an integer."
91
93
  )
94
+ if self.verbose is not None and not isinstance(self.verbose, bool):
95
+ raise ValueError("Invalid simulation options: verbose must be a boolean.")
92
96
 
93
97
 
94
98
  @dataclass
@@ -176,11 +180,15 @@ class SuperLinkConnection:
176
180
  + f"expected bool, but got {type(self._insecure).__name__}."
177
181
  )
178
182
 
179
- if self.federation is not None and not isinstance(self.federation, str):
180
- raise ValueError(
181
- err_prefix % SuperLinkConnectionTomlKey.FEDERATION
182
- + f"expected str, but got {type(self.federation).__name__}."
183
- )
183
+ if self.federation is not None:
184
+ if not isinstance(self.federation, str):
185
+ raise ValueError(
186
+ err_prefix % SuperLinkConnectionTomlKey.FEDERATION
187
+ + f"expected str, but got {type(self.federation).__name__}."
188
+ )
189
+
190
+ # Check if the federation string is valid
191
+ check_federation_format(self.federation)
184
192
 
185
193
  # The connection needs to have either an address or options (or both).
186
194
  if self.address is None and self.options is None: