flwr 1.24.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 (204) 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 +25 -66
  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 +72 -61
  25. flwr/cli/new/new.py +98 -309
  26. flwr/cli/pull.py +19 -37
  27. flwr/cli/run/run.py +87 -100
  28. flwr/cli/run_utils.py +23 -5
  29. flwr/cli/stop.py +33 -74
  30. flwr/cli/supernode/ls.py +35 -62
  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 -412
  35. flwr/client/grpc_adapter_client/connection.py +2 -2
  36. flwr/client/grpc_rere_client/connection.py +9 -6
  37. flwr/client/grpc_rere_client/grpc_adapter.py +1 -1
  38. flwr/client/message_handler/message_handler.py +2 -1
  39. flwr/client/mod/centraldp_mods.py +1 -1
  40. flwr/client/mod/localdp_mod.py +1 -1
  41. flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
  42. flwr/client/rest_client/connection.py +6 -4
  43. flwr/client/run_info_store.py +2 -1
  44. flwr/clientapp/client_app.py +2 -1
  45. flwr/common/__init__.py +3 -2
  46. flwr/common/args.py +5 -5
  47. flwr/common/config.py +12 -17
  48. flwr/common/constant.py +3 -16
  49. flwr/common/context.py +2 -1
  50. flwr/common/exit/exit.py +4 -4
  51. flwr/common/exit/exit_code.py +6 -0
  52. flwr/common/grpc.py +2 -1
  53. flwr/common/logger.py +1 -1
  54. flwr/common/message.py +1 -1
  55. flwr/common/retry_invoker.py +13 -5
  56. flwr/common/secure_aggregation/ndarrays_arithmetic.py +5 -2
  57. flwr/common/serde.py +13 -5
  58. flwr/common/telemetry.py +1 -1
  59. flwr/common/typing.py +10 -3
  60. flwr/compat/client/app.py +6 -9
  61. flwr/compat/client/grpc_client/connection.py +2 -1
  62. flwr/compat/common/constant.py +29 -0
  63. flwr/compat/server/app.py +1 -1
  64. flwr/proto/clientappio_pb2.py +2 -2
  65. flwr/proto/clientappio_pb2_grpc.py +104 -88
  66. flwr/proto/clientappio_pb2_grpc.pyi +140 -80
  67. flwr/proto/federation_pb2.py +5 -3
  68. flwr/proto/federation_pb2.pyi +32 -2
  69. flwr/proto/fleet_pb2.py +10 -10
  70. flwr/proto/fleet_pb2.pyi +5 -1
  71. flwr/proto/run_pb2.py +18 -26
  72. flwr/proto/run_pb2.pyi +10 -58
  73. flwr/proto/serverappio_pb2.py +2 -2
  74. flwr/proto/serverappio_pb2_grpc.py +138 -207
  75. flwr/proto/serverappio_pb2_grpc.pyi +189 -155
  76. flwr/proto/simulationio_pb2.py +2 -2
  77. flwr/proto/simulationio_pb2_grpc.py +62 -90
  78. flwr/proto/simulationio_pb2_grpc.pyi +95 -55
  79. flwr/server/app.py +7 -13
  80. flwr/server/compat/grid_client_proxy.py +2 -1
  81. flwr/server/grid/grpc_grid.py +5 -5
  82. flwr/server/serverapp/app.py +11 -4
  83. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -1
  84. flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +13 -12
  85. flwr/server/superlink/fleet/message_handler/message_handler.py +42 -2
  86. flwr/server/superlink/linkstate/__init__.py +2 -2
  87. flwr/server/superlink/linkstate/in_memory_linkstate.py +36 -10
  88. flwr/server/superlink/linkstate/linkstate.py +34 -21
  89. flwr/server/superlink/linkstate/linkstate_factory.py +16 -8
  90. flwr/server/superlink/linkstate/{sqlite_linkstate.py → sql_linkstate.py} +471 -516
  91. flwr/server/superlink/linkstate/utils.py +49 -2
  92. flwr/server/superlink/serverappio/serverappio_servicer.py +1 -33
  93. flwr/server/superlink/simulation/simulationio_servicer.py +0 -19
  94. flwr/server/utils/validator.py +1 -1
  95. flwr/server/workflow/default_workflows.py +2 -1
  96. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
  97. flwr/serverapp/strategy/bulyan.py +7 -1
  98. flwr/serverapp/strategy/dp_fixed_clipping.py +9 -1
  99. flwr/serverapp/strategy/fedavg.py +1 -1
  100. flwr/serverapp/strategy/fedxgb_cyclic.py +1 -1
  101. flwr/simulation/ray_transport/ray_client_proxy.py +2 -6
  102. flwr/simulation/run_simulation.py +3 -12
  103. flwr/simulation/simulationio_connection.py +3 -3
  104. flwr/{common → supercore}/address.py +7 -33
  105. flwr/supercore/app_utils.py +2 -1
  106. flwr/supercore/constant.py +27 -2
  107. flwr/supercore/corestate/{sqlite_corestate.py → sql_corestate.py} +19 -23
  108. flwr/supercore/credential_store/__init__.py +33 -0
  109. flwr/supercore/credential_store/credential_store.py +34 -0
  110. flwr/supercore/credential_store/file_credential_store.py +76 -0
  111. flwr/{common → supercore}/date.py +0 -11
  112. flwr/supercore/ffs/disk_ffs.py +1 -1
  113. flwr/supercore/object_store/object_store_factory.py +14 -6
  114. flwr/supercore/object_store/{sqlite_object_store.py → sql_object_store.py} +115 -117
  115. flwr/supercore/sql_mixin.py +315 -0
  116. flwr/{cli/new/templates → supercore/state}/__init__.py +2 -2
  117. flwr/{cli/new/templates/app/code/flwr_tune → supercore/state/alembic}/__init__.py +2 -2
  118. flwr/supercore/state/alembic/env.py +103 -0
  119. flwr/supercore/state/alembic/script.py.mako +43 -0
  120. flwr/supercore/state/alembic/utils.py +239 -0
  121. flwr/{cli/new/templates/app → supercore/state/alembic/versions}/__init__.py +2 -2
  122. flwr/supercore/state/alembic/versions/rev_2026_01_28_initialize_migration_of_state_tables.py +200 -0
  123. flwr/supercore/state/schema/README.md +121 -0
  124. flwr/{cli/new/templates/app/code → supercore/state/schema}/__init__.py +2 -2
  125. flwr/supercore/state/schema/corestate_tables.py +36 -0
  126. flwr/supercore/state/schema/linkstate_tables.py +152 -0
  127. flwr/supercore/state/schema/objectstore_tables.py +90 -0
  128. flwr/supercore/superexec/run_superexec.py +2 -2
  129. flwr/supercore/utils.py +225 -0
  130. flwr/superlink/federation/federation_manager.py +2 -2
  131. flwr/superlink/federation/noop_federation_manager.py +8 -6
  132. flwr/superlink/servicer/control/control_grpc.py +2 -0
  133. flwr/superlink/servicer/control/control_servicer.py +106 -21
  134. flwr/supernode/cli/flower_supernode.py +2 -1
  135. flwr/supernode/nodestate/in_memory_nodestate.py +62 -1
  136. flwr/supernode/nodestate/nodestate.py +45 -0
  137. flwr/supernode/runtime/run_clientapp.py +14 -14
  138. flwr/supernode/servicer/clientappio/clientappio_servicer.py +13 -5
  139. flwr/supernode/start_client_internal.py +17 -10
  140. {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/METADATA +8 -8
  141. {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/RECORD +144 -184
  142. flwr/cli/federation/show.py +0 -317
  143. flwr/cli/new/templates/app/.gitignore.tpl +0 -163
  144. flwr/cli/new/templates/app/LICENSE.tpl +0 -202
  145. flwr/cli/new/templates/app/README.baseline.md.tpl +0 -127
  146. flwr/cli/new/templates/app/README.flowertune.md.tpl +0 -68
  147. flwr/cli/new/templates/app/README.md.tpl +0 -37
  148. flwr/cli/new/templates/app/code/__init__.baseline.py.tpl +0 -1
  149. flwr/cli/new/templates/app/code/__init__.py.tpl +0 -1
  150. flwr/cli/new/templates/app/code/__init__.pytorch_legacy_api.py.tpl +0 -1
  151. flwr/cli/new/templates/app/code/client.baseline.py.tpl +0 -75
  152. flwr/cli/new/templates/app/code/client.huggingface.py.tpl +0 -93
  153. flwr/cli/new/templates/app/code/client.jax.py.tpl +0 -71
  154. flwr/cli/new/templates/app/code/client.mlx.py.tpl +0 -102
  155. flwr/cli/new/templates/app/code/client.numpy.py.tpl +0 -46
  156. flwr/cli/new/templates/app/code/client.pytorch.py.tpl +0 -80
  157. flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +0 -55
  158. flwr/cli/new/templates/app/code/client.sklearn.py.tpl +0 -108
  159. flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +0 -82
  160. flwr/cli/new/templates/app/code/client.xgboost.py.tpl +0 -110
  161. flwr/cli/new/templates/app/code/dataset.baseline.py.tpl +0 -36
  162. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +0 -92
  163. flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +0 -87
  164. flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +0 -56
  165. flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +0 -73
  166. flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +0 -78
  167. flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -66
  168. flwr/cli/new/templates/app/code/server.baseline.py.tpl +0 -43
  169. flwr/cli/new/templates/app/code/server.huggingface.py.tpl +0 -42
  170. flwr/cli/new/templates/app/code/server.jax.py.tpl +0 -39
  171. flwr/cli/new/templates/app/code/server.mlx.py.tpl +0 -41
  172. flwr/cli/new/templates/app/code/server.numpy.py.tpl +0 -38
  173. flwr/cli/new/templates/app/code/server.pytorch.py.tpl +0 -41
  174. flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +0 -31
  175. flwr/cli/new/templates/app/code/server.sklearn.py.tpl +0 -44
  176. flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +0 -38
  177. flwr/cli/new/templates/app/code/server.xgboost.py.tpl +0 -56
  178. flwr/cli/new/templates/app/code/strategy.baseline.py.tpl +0 -1
  179. flwr/cli/new/templates/app/code/task.huggingface.py.tpl +0 -98
  180. flwr/cli/new/templates/app/code/task.jax.py.tpl +0 -57
  181. flwr/cli/new/templates/app/code/task.mlx.py.tpl +0 -102
  182. flwr/cli/new/templates/app/code/task.numpy.py.tpl +0 -7
  183. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +0 -99
  184. flwr/cli/new/templates/app/code/task.pytorch_legacy_api.py.tpl +0 -111
  185. flwr/cli/new/templates/app/code/task.sklearn.py.tpl +0 -67
  186. flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +0 -52
  187. flwr/cli/new/templates/app/code/task.xgboost.py.tpl +0 -67
  188. flwr/cli/new/templates/app/code/utils.baseline.py.tpl +0 -1
  189. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +0 -146
  190. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +0 -80
  191. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +0 -65
  192. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +0 -52
  193. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +0 -56
  194. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +0 -49
  195. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +0 -53
  196. flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +0 -53
  197. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +0 -52
  198. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +0 -53
  199. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +0 -61
  200. flwr/common/pyproject.py +0 -42
  201. flwr/supercore/sqlite_mixin.py +0 -159
  202. /flwr/{common → supercore}/version.py +0 -0
  203. {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/WHEEL +0 -0
  204. {flwr-1.24.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,19 +40,18 @@ 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
47
+ from flwr.supercore.utils import check_federation_format, parse_app_spec
49
48
 
50
49
  from ..log import start_stream
51
50
  from ..utils import (
51
+ cli_output_handler,
52
52
  flwr_cli_grpc_exc_handler,
53
- init_channel,
54
- load_cli_auth_plugin,
55
- parse_app_spec,
53
+ init_channel_from_connection,
54
+ print_json_to_stdout,
56
55
  )
57
56
 
58
57
  CONN_REFRESH_PERIOD = 60 # Connection refresh period for log streaming (seconds)
@@ -64,9 +63,17 @@ def run(
64
63
  Path,
65
64
  typer.Argument(help="Path of the Flower App to run."),
66
65
  ] = Path("."),
66
+ superlink: Annotated[
67
+ str | None,
68
+ typer.Argument(help="Name of the SuperLink connection."),
69
+ ] = None,
67
70
  federation: Annotated[
68
71
  str | None,
69
- 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
+ ),
70
77
  ] = None,
71
78
  run_config_overrides: Annotated[
72
79
  list[str] | None,
@@ -81,6 +88,7 @@ def run(
81
88
  typer.Option(
82
89
  "--federation-config",
83
90
  help=FEDERATION_CONFIG_HELP_MESSAGE,
91
+ hidden=True,
84
92
  ),
85
93
  ] = None,
86
94
  stream: Annotated[
@@ -101,88 +109,87 @@ def run(
101
109
  ] = CliOutputFormat.DEFAULT,
102
110
  ) -> None:
103
111
  """Run Flower App."""
104
- suppress_output = output_format == CliOutputFormat.JSON
105
- captured_output = io.StringIO()
106
- try:
107
- if suppress_output:
108
- 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)
109
121
 
110
122
  # Determine if app is remote
111
123
  app_spec = None
124
+ config: dict[str, Any] = {}
112
125
  if (app_str := str(app)).startswith("@"):
113
126
  # Validate app version and ID format
114
- _ = parse_app_spec(app_str)
115
- app_spec = app_str
116
- is_remote_app = app_spec is not None
127
+ try:
128
+ _ = parse_app_spec(app_str)
129
+ except ValueError as e:
130
+ raise click.ClickException(str(e)) from e
117
131
 
118
- typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
119
-
120
- # Disable the validation for remote apps
121
- pyproject_path = app / "pyproject.toml" if not is_remote_app else None
122
- # `./pyproject.toml` will be loaded when `pyproject_path` is None
123
- config, errors, warnings = load_and_validate(
124
- pyproject_path, check_module=not is_remote_app
125
- )
126
- config = process_loaded_project_config(config, errors, warnings)
132
+ app_spec = app_str
127
133
 
128
- federation, federation_config = validate_federation_in_project_config(
129
- federation, config, federation_config_overrides
130
- )
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
+ )
131
145
 
132
- if "address" in federation_config:
146
+ if superlink_connection.address:
133
147
  _run_with_control_api(
134
148
  app,
149
+ config,
135
150
  federation,
136
- federation_config,
151
+ superlink_connection,
137
152
  run_config_overrides,
138
153
  stream,
139
- output_format,
154
+ is_json,
140
155
  app_spec,
141
156
  )
142
157
  else:
143
158
  _run_without_control_api(
144
- app, federation_config, run_config_overrides, federation
145
- )
146
- except (typer.Exit, 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
- typer.secho(
153
- f"{err}",
154
- fg=typer.colors.RED,
155
- bold=True,
156
- err=True,
159
+ app=app,
160
+ simulation_options=superlink_connection.options, # type: ignore
161
+ config_overrides=run_config_overrides,
157
162
  )
158
- finally:
159
- if suppress_output:
160
- restore_output()
161
- captured_output.close()
162
163
 
163
164
 
164
165
  # pylint: disable-next=R0913, R0914, R0917
165
166
  def _run_with_control_api(
166
167
  app: Path,
167
- federation: str,
168
- federation_config: dict[str, Any],
168
+ config: dict[str, Any],
169
+ federation: str | None,
170
+ superlink_connection: SuperLinkConnection,
169
171
  config_overrides: list[str] | None,
170
172
  stream: bool,
171
- output_format: str,
173
+ is_json: bool,
172
174
  app_spec: str | None,
173
175
  ) -> None:
174
176
  channel = None
175
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
+
176
185
  try:
177
- auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
178
- channel = init_channel(app, federation_config, auth_plugin)
186
+ channel = init_channel_from_connection(superlink_connection)
179
187
  stub = ControlStub(channel)
180
188
 
181
189
  # Build FAB if local app
182
190
  if not is_remote_app:
183
191
  fab_bytes = build_fab_from_disk(app)
184
192
  fab_hash = hashlib.sha256(fab_bytes).hexdigest()
185
- config = cast(dict[str, Any], load_toml(app / FAB_CONFIG_FILE))
186
193
  fab_id, fab_version = get_metadata_from_config(config)
187
194
  fab = Fab(fab_hash, fab_bytes, {})
188
195
  # Skip FAB build if remote app
@@ -191,16 +198,19 @@ def _run_with_control_api(
191
198
  fab_id = fab_version = fab_hash = ""
192
199
  fab = Fab(fab_hash, b"", {})
193
200
 
194
- real_federation: str = federation_config.get("federation", NOOP_FEDERATION)
195
-
196
201
  # Construct a `ConfigRecord` out of a flattened `UserConfig`
197
- fed_config = flatten_dict(federation_config.get("options", {}))
198
- 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)
199
209
 
200
210
  req = StartRunRequest(
201
211
  fab=fab_to_proto(fab),
202
212
  override_config=user_config_to_proto(parse_config_args(config_overrides)),
203
- federation=real_federation,
213
+ federation=federation,
204
214
  federation_options=config_record_to_proto(c_record),
205
215
  app_spec=app_spec or "",
206
216
  )
@@ -212,22 +222,13 @@ def _run_with_control_api(
212
222
  f"🎊 Successfully started run {res.run_id}", fg=typer.colors.GREEN
213
223
  )
214
224
  else:
215
- if is_remote_app:
216
- typer.secho(
217
- "❌ Failed to start run. Please check that the provided "
218
- "app identifier (@account_name/app_name) is correct.",
219
- fg=typer.colors.RED,
220
- err=True,
221
- )
222
- else:
223
- typer.secho("❌ Failed to start run", fg=typer.colors.RED, err=True)
224
- raise typer.Exit(code=1)
225
+ raise click.ClickException("Failed to start run")
225
226
 
226
- if output_format == CliOutputFormat.JSON:
227
+ if is_json:
227
228
  # Only include FAB metadata if we actually built a local FAB
228
229
  payload: dict[str, Any] = {
229
230
  "success": res.HasField("run_id"),
230
- "run-id": res.run_id if res.HasField("run_id") else None,
231
+ "run-id": f"{res.run_id}" if res.HasField("run_id") else None,
231
232
  }
232
233
  if not is_remote_app:
233
234
  payload.update(
@@ -239,8 +240,7 @@ def _run_with_control_api(
239
240
  "fab-filename": get_fab_filename(config, fab_hash),
240
241
  }
241
242
  )
242
- restore_output()
243
- Console().print_json(json.dumps(payload))
243
+ print_json_to_stdout(payload)
244
244
 
245
245
  if stream:
246
246
  start_stream(res.run_id, channel, CONN_REFRESH_PERIOD)
@@ -251,26 +251,12 @@ def _run_with_control_api(
251
251
 
252
252
  def _run_without_control_api(
253
253
  app: Path | None,
254
- federation_config: dict[str, Any],
254
+ simulation_options: SuperLinkSimulationOptions,
255
255
  config_overrides: list[str] | None,
256
- federation: str,
257
256
  ) -> None:
258
- try:
259
- num_supernodes = federation_config["options"]["num-supernodes"]
260
- verbose: bool | None = federation_config["options"].get("verbose")
261
- backend_cfg = federation_config["options"].get("backend", {})
262
- except KeyError as err:
263
- typer.secho(
264
- "❌ The project's `pyproject.toml` needs to declare the number of"
265
- " SuperNodes in the simulation. To simulate 10 SuperNodes,"
266
- " use the following notation:\n\n"
267
- f"[tool.flwr.federations.{federation}]\n"
268
- "options.num-supernodes = 10\n",
269
- fg=typer.colors.RED,
270
- bold=True,
271
- err=True,
272
- )
273
- raise typer.Exit(code=1) from err
257
+
258
+ num_supernodes = simulation_options.num_supernodes
259
+ verbose = simulation_options.verbose or False
274
260
 
275
261
  command = [
276
262
  "flower-simulation",
@@ -280,9 +266,10 @@ def _run_without_control_api(
280
266
  f"{num_supernodes}",
281
267
  ]
282
268
 
283
- if backend_cfg:
269
+ if simulation_options.backend:
284
270
  # Stringify as JSON
285
- 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)])
286
273
 
287
274
  if verbose:
288
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 format_timedelta, 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
@@ -40,8 +40,8 @@ class RunRow: # pylint: disable=too-many-instance-attributes
40
40
  The SHA-256 hash of the FAB.
41
41
  status_text : str
42
42
  The formatted status text.
43
- elapsed : str
44
- The formatted elapsed time.
43
+ elapsed : float
44
+ The elapsed time in seconds.
45
45
  pending_at : str
46
46
  Timestamp when run entered pending state.
47
47
  starting_at : str
@@ -50,6 +50,16 @@ class RunRow: # pylint: disable=too-many-instance-attributes
50
50
  Timestamp when run entered running state.
51
51
  finished_at : str
52
52
  Timestamp when run finished.
53
+ network_traffic_inbound : int
54
+ The total inbound network traffic (in bytes) used during the run.
55
+ It includes the traffic from SuperNodes to SuperLink.
56
+ network_traffic_outbound : int
57
+ The total outbound network traffic (in bytes) used during the run.
58
+ It includes the traffic from SuperLink to SuperNodes.
59
+ compute_time_serverapp : float
60
+ The total compute time (in seconds) of the ServerApp during the run.
61
+ compute_time_clientapp : float
62
+ The total compute time (in seconds) of all ClientApps during the run.
53
63
  """
54
64
 
55
65
  run_id: int
@@ -58,11 +68,15 @@ class RunRow: # pylint: disable=too-many-instance-attributes
58
68
  fab_version: str
59
69
  fab_hash: str
60
70
  status_text: str
61
- elapsed: str
71
+ elapsed: float
62
72
  pending_at: str
63
73
  starting_at: str
64
74
  running_at: str
65
75
  finished_at: str
76
+ network_traffic_inbound: int
77
+ network_traffic_outbound: int
78
+ compute_time_serverapp: float
79
+ compute_time_clientapp: float
66
80
 
67
81
 
68
82
  def format_runs(runs: list[Run], now_isoformat: str) -> list[RunRow]:
@@ -120,11 +134,15 @@ def format_runs(runs: list[Run], now_isoformat: str) -> list[RunRow]:
120
134
  fab_version=run.fab_version,
121
135
  fab_hash=run.fab_hash,
122
136
  status_text=status_text,
123
- elapsed=format_timedelta(elapsed_time),
137
+ elapsed=elapsed_time.total_seconds(),
124
138
  pending_at=_format_datetime(pending_at),
125
139
  starting_at=_format_datetime(starting_at),
126
140
  running_at=_format_datetime(running_at),
127
141
  finished_at=_format_datetime(finished_at),
142
+ network_traffic_inbound=run.bytes_recv,
143
+ network_traffic_outbound=run.bytes_sent,
144
+ compute_time_serverapp=elapsed_time.total_seconds(),
145
+ compute_time_clientapp=run.clientapp_runtime,
128
146
  )
129
147
  run_list.append(row)
130
148
  return run_list
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
- "run-id": run_id,
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.")