flwr 1.25.0__py3-none-any.whl → 1.26.0__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 (140) hide show
  1. flwr/__init__.py +1 -1
  2. flwr/app/__init__.py +4 -1
  3. flwr/app/message_type.py +29 -0
  4. flwr/app/metadata.py +5 -2
  5. flwr/app/user_config.py +19 -0
  6. flwr/cli/app.py +37 -19
  7. flwr/cli/app_cmd/publish.py +25 -75
  8. flwr/cli/app_cmd/review.py +18 -69
  9. flwr/cli/auth_plugin/auth_plugin.py +5 -10
  10. flwr/cli/auth_plugin/noop_auth_plugin.py +1 -2
  11. flwr/cli/auth_plugin/oidc_cli_plugin.py +38 -38
  12. flwr/cli/build.py +15 -28
  13. flwr/cli/config/__init__.py +21 -0
  14. flwr/cli/config/ls.py +71 -0
  15. flwr/cli/config_migration.py +297 -0
  16. flwr/cli/config_utils.py +63 -156
  17. flwr/cli/constant.py +71 -0
  18. flwr/cli/federation/__init__.py +0 -2
  19. flwr/cli/federation/ls.py +256 -64
  20. flwr/cli/flower_config.py +429 -0
  21. flwr/cli/install.py +23 -62
  22. flwr/cli/log.py +23 -37
  23. flwr/cli/login/login.py +29 -63
  24. flwr/cli/ls.py +28 -58
  25. flwr/cli/new/new.py +9 -29
  26. flwr/cli/pull.py +19 -37
  27. flwr/cli/run/run.py +85 -93
  28. flwr/cli/run_utils.py +1 -1
  29. flwr/cli/stop.py +32 -73
  30. flwr/cli/supernode/ls.py +25 -57
  31. flwr/cli/supernode/register.py +31 -80
  32. flwr/cli/supernode/unregister.py +24 -70
  33. flwr/cli/typing.py +200 -0
  34. flwr/cli/utils.py +160 -275
  35. flwr/client/grpc_rere_client/connection.py +3 -3
  36. flwr/client/grpc_rere_client/grpc_adapter.py +1 -1
  37. flwr/client/message_handler/message_handler.py +2 -1
  38. flwr/client/mod/centraldp_mods.py +1 -1
  39. flwr/client/mod/localdp_mod.py +1 -1
  40. flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
  41. flwr/client/run_info_store.py +2 -1
  42. flwr/clientapp/client_app.py +2 -1
  43. flwr/common/__init__.py +3 -2
  44. flwr/common/args.py +5 -5
  45. flwr/common/config.py +12 -17
  46. flwr/common/constant.py +3 -16
  47. flwr/common/context.py +2 -1
  48. flwr/common/exit/exit.py +4 -4
  49. flwr/common/exit/exit_code.py +6 -0
  50. flwr/common/grpc.py +2 -1
  51. flwr/common/logger.py +1 -1
  52. flwr/common/message.py +1 -1
  53. flwr/common/retry_invoker.py +13 -5
  54. flwr/common/secure_aggregation/ndarrays_arithmetic.py +5 -2
  55. flwr/common/serde.py +7 -5
  56. flwr/common/telemetry.py +1 -1
  57. flwr/common/typing.py +4 -3
  58. flwr/compat/client/app.py +6 -9
  59. flwr/compat/client/grpc_client/connection.py +2 -1
  60. flwr/compat/common/constant.py +29 -0
  61. flwr/compat/server/app.py +1 -1
  62. flwr/proto/clientappio_pb2.py +2 -2
  63. flwr/proto/clientappio_pb2_grpc.py +104 -88
  64. flwr/proto/clientappio_pb2_grpc.pyi +140 -80
  65. flwr/proto/federation_pb2.py +5 -3
  66. flwr/proto/federation_pb2.pyi +32 -2
  67. flwr/proto/run_pb2.py +5 -13
  68. flwr/proto/run_pb2.pyi +0 -57
  69. flwr/proto/serverappio_pb2.py +2 -2
  70. flwr/proto/serverappio_pb2_grpc.py +138 -207
  71. flwr/proto/serverappio_pb2_grpc.pyi +189 -155
  72. flwr/proto/simulationio_pb2.py +2 -2
  73. flwr/proto/simulationio_pb2_grpc.py +62 -90
  74. flwr/proto/simulationio_pb2_grpc.pyi +95 -55
  75. flwr/server/app.py +6 -13
  76. flwr/server/compat/grid_client_proxy.py +2 -1
  77. flwr/server/grid/grpc_grid.py +5 -5
  78. flwr/server/serverapp/app.py +11 -4
  79. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -1
  80. flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +13 -12
  81. flwr/server/superlink/fleet/message_handler/message_handler.py +6 -5
  82. flwr/server/superlink/linkstate/__init__.py +2 -2
  83. flwr/server/superlink/linkstate/in_memory_linkstate.py +2 -10
  84. flwr/server/superlink/linkstate/linkstate.py +2 -21
  85. flwr/server/superlink/linkstate/linkstate_factory.py +16 -8
  86. flwr/server/superlink/linkstate/{sqlite_linkstate.py → sql_linkstate.py} +432 -534
  87. flwr/server/superlink/linkstate/utils.py +49 -2
  88. flwr/server/superlink/serverappio/serverappio_servicer.py +1 -33
  89. flwr/server/superlink/simulation/simulationio_servicer.py +0 -19
  90. flwr/server/utils/validator.py +1 -1
  91. flwr/server/workflow/default_workflows.py +2 -1
  92. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
  93. flwr/serverapp/strategy/bulyan.py +7 -1
  94. flwr/serverapp/strategy/dp_fixed_clipping.py +9 -1
  95. flwr/serverapp/strategy/fedavg.py +1 -1
  96. flwr/serverapp/strategy/fedxgb_cyclic.py +1 -1
  97. flwr/simulation/ray_transport/ray_client_proxy.py +2 -6
  98. flwr/simulation/run_simulation.py +3 -12
  99. flwr/simulation/simulationio_connection.py +3 -3
  100. flwr/{common → supercore}/address.py +7 -33
  101. flwr/supercore/app_utils.py +2 -1
  102. flwr/supercore/constant.py +24 -2
  103. flwr/supercore/corestate/{sqlite_corestate.py → sql_corestate.py} +19 -23
  104. flwr/supercore/credential_store/__init__.py +33 -0
  105. flwr/supercore/credential_store/credential_store.py +34 -0
  106. flwr/supercore/credential_store/file_credential_store.py +76 -0
  107. flwr/{common → supercore}/date.py +0 -11
  108. flwr/supercore/ffs/disk_ffs.py +1 -1
  109. flwr/supercore/object_store/object_store_factory.py +14 -6
  110. flwr/supercore/object_store/{sqlite_object_store.py → sql_object_store.py} +115 -117
  111. flwr/supercore/sql_mixin.py +315 -0
  112. flwr/supercore/state/__init__.py +15 -0
  113. flwr/supercore/state/alembic/__init__.py +15 -0
  114. flwr/supercore/state/alembic/env.py +103 -0
  115. flwr/supercore/state/alembic/script.py.mako +43 -0
  116. flwr/supercore/state/alembic/utils.py +239 -0
  117. flwr/supercore/state/alembic/versions/__init__.py +15 -0
  118. flwr/supercore/state/alembic/versions/rev_2026_01_28_initialize_migration_of_state_tables.py +200 -0
  119. flwr/supercore/state/schema/README.md +121 -0
  120. flwr/supercore/state/schema/__init__.py +15 -0
  121. flwr/supercore/state/schema/corestate_tables.py +36 -0
  122. flwr/supercore/state/schema/linkstate_tables.py +152 -0
  123. flwr/supercore/state/schema/objectstore_tables.py +90 -0
  124. flwr/supercore/superexec/run_superexec.py +2 -2
  125. flwr/supercore/utils.py +36 -1
  126. flwr/superlink/federation/federation_manager.py +2 -2
  127. flwr/superlink/federation/noop_federation_manager.py +8 -6
  128. flwr/superlink/servicer/control/control_servicer.py +19 -17
  129. flwr/supernode/cli/flower_supernode.py +2 -1
  130. flwr/supernode/runtime/run_clientapp.py +14 -14
  131. flwr/supernode/servicer/clientappio/clientappio_servicer.py +10 -8
  132. flwr/supernode/start_client_internal.py +10 -6
  133. {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/METADATA +7 -5
  134. {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/RECORD +137 -116
  135. flwr/cli/federation/show.py +0 -318
  136. flwr/common/pyproject.py +0 -42
  137. flwr/supercore/sqlite_mixin.py +0 -159
  138. /flwr/{common → supercore}/version.py +0 -0
  139. {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/WHEEL +0 -0
  140. {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/entry_points.txt +0 -0
flwr/cli/run/run.py CHANGED
@@ -16,23 +16,23 @@
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
 
24
+ import click
25
25
  import typer
26
- from rich.console import Console
27
26
 
28
27
  from flwr.cli.build import build_fab_from_disk, get_fab_filename
29
- from flwr.cli.config_utils import load as load_toml
30
- from flwr.cli.config_utils import (
31
- load_and_validate,
32
- process_loaded_project_config,
33
- validate_federation_in_project_config,
34
- )
28
+ from flwr.cli.config_migration import migrate, warn_if_federation_config_overrides
29
+ from flwr.cli.config_utils import load_and_validate
35
30
  from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE, RUN_CONFIG_HELP_MESSAGE
31
+ from flwr.cli.flower_config import (
32
+ _serialize_simulation_options,
33
+ read_superlink_connection,
34
+ )
35
+ from flwr.cli.typing import SuperLinkConnection, SuperLinkSimulationOptions
36
36
  from flwr.common.config import (
37
37
  flatten_dict,
38
38
  get_metadata_from_config,
@@ -40,16 +40,19 @@ from flwr.common.config import (
40
40
  user_config_to_configrecord,
41
41
  )
42
42
  from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
43
- from flwr.common.logger import print_json_error, redirect_output, restore_output
44
43
  from flwr.common.serde import config_record_to_proto, fab_to_proto, user_config_to_proto
45
44
  from flwr.common.typing import Fab
46
45
  from flwr.proto.control_pb2 import StartRunRequest # pylint: disable=E0611
47
46
  from flwr.proto.control_pb2_grpc import ControlStub
48
- from flwr.supercore.constant import NOOP_FEDERATION
49
- from flwr.supercore.utils import parse_app_spec
47
+ from flwr.supercore.utils import check_federation_format, parse_app_spec
50
48
 
51
49
  from ..log import start_stream
52
- from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
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
 
@@ -60,9 +63,17 @@ def run(
60
63
  Path,
61
64
  typer.Argument(help="Path of the Flower App to run."),
62
65
  ] = Path("."),
66
+ superlink: Annotated[
67
+ str | None,
68
+ typer.Argument(help="Name of the SuperLink connection."),
69
+ ] = None,
63
70
  federation: Annotated[
64
71
  str | None,
65
- typer.Argument(help="Name of the federation to run the app on."),
72
+ typer.Option(
73
+ "--federation",
74
+ help="The federation to submit the run to; must be in the "
75
+ "format `@<account>/<federation>`.",
76
+ ),
66
77
  ] = None,
67
78
  run_config_overrides: Annotated[
68
79
  list[str] | None,
@@ -77,6 +88,7 @@ def run(
77
88
  typer.Option(
78
89
  "--federation-config",
79
90
  help=FEDERATION_CONFIG_HELP_MESSAGE,
91
+ hidden=True,
80
92
  ),
81
93
  ] = None,
82
94
  stream: Annotated[
@@ -97,95 +109,87 @@ def run(
97
109
  ] = CliOutputFormat.DEFAULT,
98
110
  ) -> None:
99
111
  """Run Flower App."""
100
- suppress_output = output_format == CliOutputFormat.JSON
101
- captured_output = io.StringIO()
102
- try:
103
- if suppress_output:
104
- redirect_output(captured_output)
112
+ with cli_output_handler(output_format=output_format) as is_json:
113
+ # Warn `--federation-config` is ignored
114
+ warn_if_federation_config_overrides(federation_config_overrides)
115
+
116
+ # Migrate legacy usage if any
117
+ migrate(str(app), [], ignore_legacy_usage=True)
118
+
119
+ # Read superlink connection configuration
120
+ superlink_connection = read_superlink_connection(superlink)
105
121
 
106
122
  # Determine if app is remote
107
123
  app_spec = None
124
+ config: dict[str, Any] = {}
108
125
  if (app_str := str(app)).startswith("@"):
109
126
  # Validate app version and ID format
110
127
  try:
111
128
  _ = parse_app_spec(app_str)
112
129
  except ValueError as e:
113
- typer.secho(f"❌ {e}", fg=typer.colors.RED, err=True)
114
- raise typer.Exit(code=1) from e
130
+ raise click.ClickException(str(e)) from e
115
131
 
116
132
  app_spec = app_str
117
- # Set `app` to current directory for credential storage
118
- app = Path(".")
119
- is_remote_app = app_spec is not None
120
-
121
- typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
122
133
 
123
- # Disable the validation for remote apps
124
- pyproject_path = app / "pyproject.toml" if not is_remote_app else None
125
- # `./pyproject.toml` will be loaded when `pyproject_path` is None
126
- config, errors, warnings = load_and_validate(
127
- pyproject_path, check_module=not is_remote_app
128
- )
129
- config = process_loaded_project_config(config, errors, warnings)
130
-
131
- federation, federation_config = validate_federation_in_project_config(
132
- federation, config, federation_config_overrides
133
- )
134
+ # Validate TOML configuration for local app
135
+ else:
136
+ app = app.expanduser().resolve() # Resolve path to absolute
137
+ config, warnings = load_and_validate(app / FAB_CONFIG_FILE)
138
+ if warnings:
139
+ typer.secho(
140
+ f"Flower App configuration warnings in '{app / FAB_CONFIG_FILE}':\n"
141
+ + "\n".join([f"- {line}" for line in warnings]),
142
+ fg=typer.colors.YELLOW,
143
+ bold=True,
144
+ )
134
145
 
135
- if "address" in federation_config:
146
+ if superlink_connection.address:
136
147
  _run_with_control_api(
137
148
  app,
149
+ config,
138
150
  federation,
139
- federation_config,
151
+ superlink_connection,
140
152
  run_config_overrides,
141
153
  stream,
142
- output_format,
154
+ is_json,
143
155
  app_spec,
144
156
  )
145
157
  else:
146
158
  _run_without_control_api(
147
- app, federation_config, run_config_overrides, federation
159
+ app=app,
160
+ simulation_options=superlink_connection.options, # type: ignore
161
+ config_overrides=run_config_overrides,
148
162
  )
149
- except (typer.Exit, Exception) as err: # pylint: disable=broad-except
150
- if suppress_output:
151
- restore_output()
152
- e_message = captured_output.getvalue()
153
- print_json_error(e_message, err)
154
- else:
155
- typer.secho(
156
- f"{err}",
157
- fg=typer.colors.RED,
158
- bold=True,
159
- err=True,
160
- )
161
- finally:
162
- if suppress_output:
163
- restore_output()
164
- captured_output.close()
165
163
 
166
164
 
167
165
  # pylint: disable-next=R0913, R0914, R0917
168
166
  def _run_with_control_api(
169
167
  app: Path,
170
- federation: str,
171
- federation_config: dict[str, Any],
168
+ config: dict[str, Any],
169
+ federation: str | None,
170
+ superlink_connection: SuperLinkConnection,
172
171
  config_overrides: list[str] | None,
173
172
  stream: bool,
174
- output_format: str,
173
+ is_json: bool,
175
174
  app_spec: str | None,
176
175
  ) -> None:
177
176
  channel = None
178
177
  is_remote_app = app_spec is not None
178
+
179
+ # Determine federation to use
180
+ if federation: # Override federation from CLI
181
+ check_federation_format(federation)
182
+ else: # Use federation from SuperLink connection if set
183
+ federation = superlink_connection.federation or ""
184
+
179
185
  try:
180
- auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
181
- channel = init_channel(app, federation_config, auth_plugin)
186
+ channel = init_channel_from_connection(superlink_connection)
182
187
  stub = ControlStub(channel)
183
188
 
184
189
  # Build FAB if local app
185
190
  if not is_remote_app:
186
191
  fab_bytes = build_fab_from_disk(app)
187
192
  fab_hash = hashlib.sha256(fab_bytes).hexdigest()
188
- config = cast(dict[str, Any], load_toml(app / FAB_CONFIG_FILE))
189
193
  fab_id, fab_version = get_metadata_from_config(config)
190
194
  fab = Fab(fab_hash, fab_bytes, {})
191
195
  # Skip FAB build if remote app
@@ -194,16 +198,19 @@ def _run_with_control_api(
194
198
  fab_id = fab_version = fab_hash = ""
195
199
  fab = Fab(fab_hash, b"", {})
196
200
 
197
- real_federation: str = federation_config.get("federation", NOOP_FEDERATION)
198
-
199
201
  # Construct a `ConfigRecord` out of a flattened `UserConfig`
200
- fed_config = flatten_dict(federation_config.get("options", {}))
201
- c_record = user_config_to_configrecord(fed_config)
202
+ options = {}
203
+ if superlink_connection.options:
204
+ options = flatten_dict(
205
+ _serialize_simulation_options(superlink_connection.options)
206
+ )
207
+
208
+ c_record = user_config_to_configrecord(options)
202
209
 
203
210
  req = StartRunRequest(
204
211
  fab=fab_to_proto(fab),
205
212
  override_config=user_config_to_proto(parse_config_args(config_overrides)),
206
- federation=real_federation,
213
+ federation=federation,
207
214
  federation_options=config_record_to_proto(c_record),
208
215
  app_spec=app_spec or "",
209
216
  )
@@ -215,10 +222,9 @@ def _run_with_control_api(
215
222
  f"🎊 Successfully started run {res.run_id}", fg=typer.colors.GREEN
216
223
  )
217
224
  else:
218
- typer.secho("Failed to start run", fg=typer.colors.RED, err=True)
219
- raise typer.Exit(code=1)
225
+ raise click.ClickException("Failed to start run")
220
226
 
221
- if output_format == CliOutputFormat.JSON:
227
+ if is_json:
222
228
  # Only include FAB metadata if we actually built a local FAB
223
229
  payload: dict[str, Any] = {
224
230
  "success": res.HasField("run_id"),
@@ -234,8 +240,7 @@ def _run_with_control_api(
234
240
  "fab-filename": get_fab_filename(config, fab_hash),
235
241
  }
236
242
  )
237
- restore_output()
238
- Console().print_json(json.dumps(payload))
243
+ print_json_to_stdout(payload)
239
244
 
240
245
  if stream:
241
246
  start_stream(res.run_id, channel, CONN_REFRESH_PERIOD)
@@ -246,26 +251,12 @@ def _run_with_control_api(
246
251
 
247
252
  def _run_without_control_api(
248
253
  app: Path | None,
249
- federation_config: dict[str, Any],
254
+ simulation_options: SuperLinkSimulationOptions,
250
255
  config_overrides: list[str] | None,
251
- federation: str,
252
256
  ) -> None:
253
- try:
254
- num_supernodes = federation_config["options"]["num-supernodes"]
255
- verbose: bool | None = federation_config["options"].get("verbose")
256
- backend_cfg = federation_config["options"].get("backend", {})
257
- except KeyError as err:
258
- typer.secho(
259
- "❌ The project's `pyproject.toml` needs to declare the number of"
260
- " SuperNodes in the simulation. To simulate 10 SuperNodes,"
261
- " use the following notation:\n\n"
262
- f"[tool.flwr.federations.{federation}]\n"
263
- "options.num-supernodes = 10\n",
264
- fg=typer.colors.RED,
265
- bold=True,
266
- err=True,
267
- )
268
- raise typer.Exit(code=1) from err
257
+
258
+ num_supernodes = simulation_options.num_supernodes
259
+ verbose = simulation_options.verbose or False
269
260
 
270
261
  command = [
271
262
  "flower-simulation",
@@ -275,9 +266,10 @@ def _run_without_control_api(
275
266
  f"{num_supernodes}",
276
267
  ]
277
268
 
278
- if backend_cfg:
269
+ if simulation_options.backend:
279
270
  # Stringify as JSON
280
- command.extend(["--backend-config", json.dumps(backend_cfg)])
271
+ backend_serial = _serialize_simulation_options(simulation_options)
272
+ command.extend(["--backend-config", json.dumps(backend_serial)])
281
273
 
282
274
  if verbose:
283
275
  command.extend(["--verbose"])
flwr/cli/run_utils.py CHANGED
@@ -18,8 +18,8 @@
18
18
  from dataclasses import dataclass
19
19
  from datetime import datetime, timedelta
20
20
 
21
- from flwr.common.date import isoformat8601_utc
22
21
  from flwr.common.typing import Run
22
+ from flwr.supercore.date import isoformat8601_utc
23
23
 
24
24
 
25
25
  @dataclass
flwr/cli/stop.py CHANGED
@@ -15,50 +15,45 @@
15
15
  """Flower command line interface `stop` command."""
16
16
 
17
17
 
18
- import io
19
- import json
20
- from pathlib import Path
21
18
  from typing import Annotated
22
19
 
20
+ import click
23
21
  import typer
24
- from rich.console import Console
25
22
 
26
- from flwr.cli.config_utils import (
27
- exit_if_no_address,
28
- load_and_validate,
29
- process_loaded_project_config,
30
- validate_federation_in_project_config,
31
- )
23
+ from flwr.cli.config_migration import migrate, warn_if_federation_config_overrides
32
24
  from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
33
- from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
34
- from flwr.common.logger import print_json_error, redirect_output, restore_output
25
+ from flwr.cli.flower_config import read_superlink_connection
26
+ from flwr.common.constant import CliOutputFormat
35
27
  from flwr.proto.control_pb2 import ( # pylint: disable=E0611
36
28
  StopRunRequest,
37
29
  StopRunResponse,
38
30
  )
39
31
  from flwr.proto.control_pb2_grpc import ControlStub
40
32
 
41
- from .utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
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
+ )
42
39
 
43
40
 
44
41
  def stop( # pylint: disable=R0914
42
+ ctx: typer.Context,
45
43
  run_id: Annotated[ # pylint: disable=unused-argument
46
44
  int,
47
45
  typer.Argument(help="The Flower run ID to stop"),
48
46
  ],
49
- app: Annotated[
50
- Path,
51
- typer.Argument(help="Path of the Flower project"),
52
- ] = Path("."),
53
- federation: Annotated[
47
+ superlink: Annotated[
54
48
  str | None,
55
- typer.Argument(help="Name of the federation"),
49
+ typer.Argument(help="Name of the SuperLink connection."),
56
50
  ] = None,
57
51
  federation_config_overrides: Annotated[
58
52
  list[str] | None,
59
53
  typer.Option(
60
54
  "--federation-config",
61
55
  help=FEDERATION_CONFIG_HELP_MESSAGE,
56
+ hidden=True,
62
57
  ),
63
58
  ] = None,
64
59
  output_format: Annotated[
@@ -75,61 +70,29 @@ def stop( # pylint: disable=R0914
75
70
  This command stops a running Flower App execution by sending a stop request to the
76
71
  SuperLink via the Control API.
77
72
  """
78
- suppress_output = output_format == CliOutputFormat.JSON
79
- captured_output = io.StringIO()
80
- try:
81
- if suppress_output:
82
- redirect_output(captured_output)
83
-
84
- # Load and validate federation config
85
- typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
86
-
87
- pyproject_path = app / FAB_CONFIG_FILE if app else None
88
- config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
89
- config = process_loaded_project_config(config, errors, warnings)
90
- federation, federation_config = validate_federation_in_project_config(
91
- federation, config, federation_config_overrides
92
- )
93
- exit_if_no_address(federation_config, "stop")
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)
76
+
77
+ migrate(superlink, args=ctx.args)
78
+
79
+ # Read superlink connection configuration
80
+ superlink_connection = read_superlink_connection(superlink)
94
81
  channel = None
82
+
95
83
  try:
96
- auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
97
- channel = init_channel(app, federation_config, auth_plugin)
84
+ channel = init_channel_from_connection(superlink_connection)
98
85
  stub = ControlStub(channel) # pylint: disable=unused-variable # noqa: F841
99
86
 
100
87
  typer.secho(f"✋ Stopping run ID {run_id}...", fg=typer.colors.GREEN)
101
- _stop_run(stub=stub, run_id=run_id, output_format=output_format)
102
-
103
- except ValueError as err:
104
- typer.secho(
105
- f"❌ {err}",
106
- fg=typer.colors.RED,
107
- bold=True,
108
- err=True,
109
- )
110
- raise typer.Exit(code=1) from err
88
+ _stop_run(stub=stub, run_id=run_id, is_json=is_json)
89
+
111
90
  finally:
112
91
  if channel:
113
92
  channel.close()
114
- except (typer.Exit, Exception) as err: # pylint: disable=broad-except
115
- if suppress_output:
116
- restore_output()
117
- e_message = captured_output.getvalue()
118
- print_json_error(e_message, err)
119
- else:
120
- typer.secho(
121
- f"{err}",
122
- fg=typer.colors.RED,
123
- bold=True,
124
- err=True,
125
- )
126
- finally:
127
- if suppress_output:
128
- restore_output()
129
- captured_output.close()
130
93
 
131
94
 
132
- def _stop_run(stub: ControlStub, run_id: int, output_format: str) -> None:
95
+ def _stop_run(stub: ControlStub, run_id: int, is_json: bool) -> None:
133
96
  """Stop a run and display the result.
134
97
 
135
98
  Parameters
@@ -138,23 +101,19 @@ def _stop_run(stub: ControlStub, run_id: int, output_format: str) -> None:
138
101
  The gRPC stub for Control API communication.
139
102
  run_id : int
140
103
  The unique identifier of the run to stop.
141
- output_format : str
142
- Output format ('default' or 'json').
104
+ is_json : bool
105
+ Whether JSON output format is requested.
143
106
  """
144
107
  with flwr_cli_grpc_exc_handler():
145
108
  response: StopRunResponse = stub.StopRun(request=StopRunRequest(run_id=run_id))
146
109
  if response.success:
147
110
  typer.secho(f"✅ Run {run_id} successfully stopped.", fg=typer.colors.GREEN)
148
- if output_format == CliOutputFormat.JSON:
149
- run_output = json.dumps(
111
+ if is_json:
112
+ print_json_to_stdout(
150
113
  {
151
114
  "success": True,
152
115
  "run-id": f"{run_id}",
153
116
  }
154
117
  )
155
- restore_output()
156
- Console().print_json(run_output)
157
118
  else:
158
- typer.secho(
159
- f"❌ Run {run_id} couldn't be stopped.", fg=typer.colors.RED, err=True
160
- )
119
+ raise click.ClickException(f"Run {run_id} couldn't be stopped.")
flwr/cli/supernode/ls.py CHANGED
@@ -15,48 +15,42 @@
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
- from pathlib import Path
22
- from typing import Annotated, cast
20
+ from typing import Annotated
23
21
 
24
22
  import typer
25
23
  from rich.console import Console
26
24
  from rich.table import Table
27
25
  from rich.text import Text
28
26
 
29
- from flwr.cli.config_utils import (
30
- exit_if_no_address,
31
- load_and_validate,
32
- process_loaded_project_config,
33
- validate_federation_in_project_config,
34
- )
35
- from flwr.common.constant import FAB_CONFIG_FILE, NOOP_ACCOUNT_NAME, CliOutputFormat
36
- from flwr.common.date import isoformat8601_utc
37
- from flwr.common.logger import print_json_error, redirect_output, restore_output
27
+ from flwr.cli.config_migration import migrate
28
+ from flwr.cli.flower_config import read_superlink_connection
29
+ from flwr.common.constant import NOOP_ACCOUNT_NAME, CliOutputFormat
38
30
  from flwr.proto.control_pb2 import ( # pylint: disable=E0611
39
31
  ListNodesRequest,
40
32
  ListNodesResponse,
41
33
  )
42
34
  from flwr.proto.control_pb2_grpc import ControlStub
43
35
  from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
36
+ from flwr.supercore.date import isoformat8601_utc
44
37
  from flwr.supercore.utils import humanize_duration
45
38
 
46
- from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
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
+ )
47
45
 
48
46
  _NodeListType = tuple[int, str, str, str, str, str, str, str, float]
49
47
 
50
48
 
51
49
  def ls( # pylint: disable=R0914, R0913, R0917
52
50
  ctx: typer.Context,
53
- app: Annotated[
54
- Path,
55
- typer.Argument(help="Path of the Flower project"),
56
- ] = Path("."),
57
- federation: Annotated[
51
+ superlink: Annotated[
58
52
  str | None,
59
- typer.Argument(help="Name of the federation"),
53
+ typer.Argument(help="Name of the SuperLink connection."),
60
54
  ] = None,
61
55
  output_format: Annotated[
62
56
  str,
@@ -75,55 +69,29 @@ def ls( # pylint: disable=R0914, R0913, R0917
75
69
  ),
76
70
  ] = False,
77
71
  ) -> None:
78
- """List SuperNodes in the federation."""
79
- # Resolve command used (list or ls)
80
- command_name = cast(str, ctx.command.name) if ctx.command else "ls"
81
-
82
- suppress_output = output_format == CliOutputFormat.JSON
83
- captured_output = io.StringIO()
84
- try:
85
- if suppress_output:
86
- redirect_output(captured_output)
87
- typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
88
-
89
- pyproject_path = app / FAB_CONFIG_FILE if app else None
90
- config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
91
- config = process_loaded_project_config(config, errors, warnings)
92
- federation, federation_config = validate_federation_in_project_config(
93
- federation, config
94
- )
95
- exit_if_no_address(federation_config, f"supernode {command_name}")
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)
76
+
77
+ # Read superlink connection configuration
78
+ superlink_connection = read_superlink_connection(superlink)
96
79
  channel = None
80
+
97
81
  try:
98
- auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
99
- channel = init_channel(app, federation_config, auth_plugin)
82
+ channel = init_channel_from_connection(superlink_connection)
100
83
  stub = ControlStub(channel)
101
84
  typer.echo("📄 Listing all nodes...")
102
85
  formatted_nodes = _list_nodes(stub)
103
- restore_output()
104
- if output_format == CliOutputFormat.JSON:
105
- 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))
106
89
  else:
107
90
  Console().print(_to_table(formatted_nodes, verbose=verbose))
108
91
 
109
92
  finally:
110
93
  if channel:
111
94
  channel.close()
112
- except (typer.Exit, Exception) as err: # pylint: disable=broad-except
113
- if suppress_output:
114
- restore_output()
115
- e_message = captured_output.getvalue()
116
- print_json_error(e_message, err)
117
- else:
118
- typer.secho(
119
- f"{err}",
120
- fg=typer.colors.RED,
121
- bold=True,
122
- )
123
- finally:
124
- if suppress_output:
125
- restore_output()
126
- captured_output.close()
127
95
 
128
96
 
129
97
  def _list_nodes(stub: ControlStub) -> list[_NodeListType]: