flwr 1.22.0__py3-none-any.whl → 1.24.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 (301) hide show
  1. flwr/__init__.py +16 -5
  2. flwr/app/error.py +2 -2
  3. flwr/app/exception.py +3 -3
  4. flwr/cli/app.py +34 -1
  5. flwr/cli/app_cmd/__init__.py +23 -0
  6. flwr/cli/app_cmd/publish.py +285 -0
  7. flwr/cli/app_cmd/review.py +252 -0
  8. flwr/cli/auth_plugin/__init__.py +15 -6
  9. flwr/cli/auth_plugin/auth_plugin.py +94 -0
  10. flwr/cli/auth_plugin/noop_auth_plugin.py +101 -0
  11. flwr/cli/auth_plugin/oidc_cli_plugin.py +46 -32
  12. flwr/cli/build.py +166 -53
  13. flwr/cli/{cli_user_auth_interceptor.py → cli_account_auth_interceptor.py} +29 -11
  14. flwr/cli/config_utils.py +101 -13
  15. flwr/cli/federation/__init__.py +24 -0
  16. flwr/cli/federation/ls.py +140 -0
  17. flwr/cli/federation/show.py +317 -0
  18. flwr/cli/install.py +91 -13
  19. flwr/cli/log.py +54 -11
  20. flwr/cli/login/login.py +41 -27
  21. flwr/cli/ls.py +177 -133
  22. flwr/cli/new/new.py +175 -40
  23. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +1 -0
  24. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
  25. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
  26. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  27. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  28. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  29. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  30. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
  31. flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +1 -1
  32. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  33. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +2 -2
  34. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +1 -1
  35. flwr/cli/pull.py +12 -7
  36. flwr/cli/run/run.py +82 -31
  37. flwr/cli/run_utils.py +130 -0
  38. flwr/cli/stop.py +27 -9
  39. flwr/cli/supernode/__init__.py +25 -0
  40. flwr/cli/supernode/ls.py +268 -0
  41. flwr/cli/supernode/register.py +190 -0
  42. flwr/cli/supernode/unregister.py +140 -0
  43. flwr/cli/utils.py +464 -81
  44. flwr/client/__init__.py +2 -1
  45. flwr/client/dpfedavg_numpy_client.py +4 -1
  46. flwr/client/grpc_adapter_client/connection.py +12 -15
  47. flwr/client/grpc_rere_client/connection.py +68 -41
  48. flwr/client/grpc_rere_client/grpc_adapter.py +34 -14
  49. flwr/client/grpc_rere_client/{client_interceptor.py → node_auth_client_interceptor.py} +5 -7
  50. flwr/client/message_handler/message_handler.py +2 -2
  51. flwr/client/mod/secure_aggregation/secaggplus_mod.py +10 -8
  52. flwr/client/numpy_client.py +1 -1
  53. flwr/client/rest_client/connection.py +94 -51
  54. flwr/client/run_info_store.py +4 -5
  55. flwr/client/typing.py +1 -1
  56. flwr/clientapp/__init__.py +1 -2
  57. flwr/{client → clientapp}/client_app.py +9 -10
  58. flwr/clientapp/mod/centraldp_mods.py +16 -17
  59. flwr/clientapp/mod/localdp_mod.py +8 -9
  60. flwr/clientapp/typing.py +1 -1
  61. flwr/{client/clientapp → clientapp}/utils.py +4 -4
  62. flwr/common/address.py +1 -2
  63. flwr/common/args.py +3 -4
  64. flwr/common/config.py +13 -16
  65. flwr/common/constant.py +56 -13
  66. flwr/common/differential_privacy.py +3 -4
  67. flwr/common/event_log_plugin/event_log_plugin.py +3 -4
  68. flwr/common/exit/exit.py +15 -2
  69. flwr/common/exit/exit_code.py +39 -10
  70. flwr/common/exit/exit_handler.py +6 -2
  71. flwr/common/exit/signal_handler.py +5 -5
  72. flwr/common/grpc.py +6 -6
  73. flwr/common/inflatable_protobuf_utils.py +1 -1
  74. flwr/common/inflatable_utils.py +48 -31
  75. flwr/common/logger.py +19 -19
  76. flwr/common/message.py +4 -4
  77. flwr/common/object_ref.py +7 -7
  78. flwr/common/record/array.py +6 -6
  79. flwr/common/record/arrayrecord.py +18 -21
  80. flwr/common/record/configrecord.py +3 -3
  81. flwr/common/record/recorddict.py +5 -5
  82. flwr/common/record/typeddict.py +9 -2
  83. flwr/common/recorddict_compat.py +7 -10
  84. flwr/common/retry_invoker.py +20 -20
  85. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +1 -89
  86. flwr/common/secure_aggregation/ndarrays_arithmetic.py +3 -3
  87. flwr/common/serde.py +9 -6
  88. flwr/common/serde_utils.py +2 -2
  89. flwr/common/telemetry.py +9 -5
  90. flwr/common/typing.py +59 -43
  91. flwr/compat/client/app.py +39 -38
  92. flwr/compat/client/grpc_client/connection.py +13 -13
  93. flwr/compat/server/app.py +5 -6
  94. flwr/proto/appio_pb2.py +13 -3
  95. flwr/proto/appio_pb2.pyi +134 -65
  96. flwr/proto/appio_pb2_grpc.py +20 -0
  97. flwr/proto/appio_pb2_grpc.pyi +27 -0
  98. flwr/proto/clientappio_pb2.py +17 -7
  99. flwr/proto/clientappio_pb2.pyi +15 -0
  100. flwr/proto/clientappio_pb2_grpc.py +206 -40
  101. flwr/proto/clientappio_pb2_grpc.pyi +168 -53
  102. flwr/proto/control_pb2.py +72 -40
  103. flwr/proto/control_pb2.pyi +319 -87
  104. flwr/proto/control_pb2_grpc.py +339 -28
  105. flwr/proto/control_pb2_grpc.pyi +209 -37
  106. flwr/proto/error_pb2.py +13 -3
  107. flwr/proto/error_pb2.pyi +24 -6
  108. flwr/proto/error_pb2_grpc.py +20 -0
  109. flwr/proto/error_pb2_grpc.pyi +27 -0
  110. flwr/proto/fab_pb2.py +24 -10
  111. flwr/proto/fab_pb2.pyi +68 -20
  112. flwr/proto/fab_pb2_grpc.py +20 -0
  113. flwr/proto/fab_pb2_grpc.pyi +27 -0
  114. flwr/proto/federation_pb2.py +38 -0
  115. flwr/proto/federation_pb2.pyi +56 -0
  116. flwr/proto/federation_pb2_grpc.py +24 -0
  117. flwr/proto/federation_pb2_grpc.pyi +31 -0
  118. flwr/proto/fleet_pb2.py +45 -27
  119. flwr/proto/fleet_pb2.pyi +186 -70
  120. flwr/proto/fleet_pb2_grpc.py +277 -66
  121. flwr/proto/fleet_pb2_grpc.pyi +201 -55
  122. flwr/proto/grpcadapter_pb2.py +14 -4
  123. flwr/proto/grpcadapter_pb2.pyi +38 -16
  124. flwr/proto/grpcadapter_pb2_grpc.py +35 -4
  125. flwr/proto/grpcadapter_pb2_grpc.pyi +38 -7
  126. flwr/proto/heartbeat_pb2.py +17 -7
  127. flwr/proto/heartbeat_pb2.pyi +51 -22
  128. flwr/proto/heartbeat_pb2_grpc.py +20 -0
  129. flwr/proto/heartbeat_pb2_grpc.pyi +27 -0
  130. flwr/proto/log_pb2.py +13 -3
  131. flwr/proto/log_pb2.pyi +34 -11
  132. flwr/proto/log_pb2_grpc.py +20 -0
  133. flwr/proto/log_pb2_grpc.pyi +27 -0
  134. flwr/proto/message_pb2.py +15 -5
  135. flwr/proto/message_pb2.pyi +154 -86
  136. flwr/proto/message_pb2_grpc.py +20 -0
  137. flwr/proto/message_pb2_grpc.pyi +27 -0
  138. flwr/proto/node_pb2.py +16 -4
  139. flwr/proto/node_pb2.pyi +77 -4
  140. flwr/proto/node_pb2_grpc.py +20 -0
  141. flwr/proto/node_pb2_grpc.pyi +27 -0
  142. flwr/proto/recorddict_pb2.py +13 -3
  143. flwr/proto/recorddict_pb2.pyi +184 -107
  144. flwr/proto/recorddict_pb2_grpc.py +20 -0
  145. flwr/proto/recorddict_pb2_grpc.pyi +27 -0
  146. flwr/proto/run_pb2.py +40 -31
  147. flwr/proto/run_pb2.pyi +149 -84
  148. flwr/proto/run_pb2_grpc.py +20 -0
  149. flwr/proto/run_pb2_grpc.pyi +27 -0
  150. flwr/proto/serverappio_pb2.py +13 -3
  151. flwr/proto/serverappio_pb2.pyi +32 -8
  152. flwr/proto/serverappio_pb2_grpc.py +246 -65
  153. flwr/proto/serverappio_pb2_grpc.pyi +221 -85
  154. flwr/proto/simulationio_pb2.py +16 -8
  155. flwr/proto/simulationio_pb2.pyi +15 -0
  156. flwr/proto/simulationio_pb2_grpc.py +162 -41
  157. flwr/proto/simulationio_pb2_grpc.pyi +149 -55
  158. flwr/proto/transport_pb2.py +20 -10
  159. flwr/proto/transport_pb2.pyi +249 -160
  160. flwr/proto/transport_pb2_grpc.py +35 -4
  161. flwr/proto/transport_pb2_grpc.pyi +38 -8
  162. flwr/server/app.py +173 -127
  163. flwr/server/client_manager.py +4 -5
  164. flwr/server/client_proxy.py +10 -11
  165. flwr/server/compat/app.py +4 -5
  166. flwr/server/compat/app_utils.py +2 -1
  167. flwr/server/compat/grid_client_proxy.py +10 -12
  168. flwr/server/compat/legacy_context.py +3 -4
  169. flwr/server/fleet_event_log_interceptor.py +2 -1
  170. flwr/server/grid/grid.py +2 -3
  171. flwr/server/grid/grpc_grid.py +10 -8
  172. flwr/server/grid/inmemory_grid.py +4 -4
  173. flwr/server/run_serverapp.py +2 -3
  174. flwr/server/server.py +34 -39
  175. flwr/server/server_app.py +7 -8
  176. flwr/server/server_config.py +1 -2
  177. flwr/server/serverapp/app.py +34 -28
  178. flwr/server/serverapp_components.py +4 -5
  179. flwr/server/strategy/aggregate.py +9 -8
  180. flwr/server/strategy/bulyan.py +13 -11
  181. flwr/server/strategy/dp_adaptive_clipping.py +16 -20
  182. flwr/server/strategy/dp_fixed_clipping.py +12 -17
  183. flwr/server/strategy/dpfedavg_adaptive.py +3 -4
  184. flwr/server/strategy/dpfedavg_fixed.py +6 -10
  185. flwr/server/strategy/fault_tolerant_fedavg.py +14 -13
  186. flwr/server/strategy/fedadagrad.py +18 -14
  187. flwr/server/strategy/fedadam.py +16 -14
  188. flwr/server/strategy/fedavg.py +16 -17
  189. flwr/server/strategy/fedavg_android.py +15 -15
  190. flwr/server/strategy/fedavgm.py +21 -18
  191. flwr/server/strategy/fedmedian.py +2 -3
  192. flwr/server/strategy/fedopt.py +11 -10
  193. flwr/server/strategy/fedprox.py +10 -9
  194. flwr/server/strategy/fedtrimmedavg.py +12 -11
  195. flwr/server/strategy/fedxgb_bagging.py +13 -11
  196. flwr/server/strategy/fedxgb_cyclic.py +6 -6
  197. flwr/server/strategy/fedxgb_nn_avg.py +4 -4
  198. flwr/server/strategy/fedyogi.py +16 -14
  199. flwr/server/strategy/krum.py +12 -11
  200. flwr/server/strategy/qfedavg.py +16 -15
  201. flwr/server/strategy/strategy.py +6 -9
  202. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +19 -8
  203. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -2
  204. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +3 -4
  205. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +10 -12
  206. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +1 -3
  207. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +136 -42
  208. flwr/server/superlink/fleet/grpc_rere/{server_interceptor.py → node_auth_server_interceptor.py} +28 -51
  209. flwr/server/superlink/fleet/message_handler/message_handler.py +100 -49
  210. flwr/server/superlink/fleet/rest_rere/rest_api.py +54 -33
  211. flwr/server/superlink/fleet/vce/backend/backend.py +2 -2
  212. flwr/server/superlink/fleet/vce/backend/raybackend.py +6 -6
  213. flwr/server/superlink/fleet/vce/vce_api.py +32 -13
  214. flwr/server/superlink/linkstate/in_memory_linkstate.py +266 -207
  215. flwr/server/superlink/linkstate/linkstate.py +161 -62
  216. flwr/server/superlink/linkstate/linkstate_factory.py +24 -6
  217. flwr/server/superlink/linkstate/sqlite_linkstate.py +698 -638
  218. flwr/server/superlink/linkstate/utils.py +9 -60
  219. flwr/server/superlink/serverappio/serverappio_grpc.py +1 -2
  220. flwr/server/superlink/serverappio/serverappio_servicer.py +28 -23
  221. flwr/server/superlink/simulation/simulationio_grpc.py +1 -2
  222. flwr/server/superlink/simulation/simulationio_servicer.py +19 -14
  223. flwr/server/superlink/utils.py +4 -6
  224. flwr/server/typing.py +1 -1
  225. flwr/server/utils/tensorboard.py +15 -8
  226. flwr/server/utils/validator.py +2 -3
  227. flwr/server/workflow/default_workflows.py +5 -5
  228. flwr/server/workflow/secure_aggregation/secagg_workflow.py +2 -4
  229. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +12 -10
  230. flwr/serverapp/strategy/bulyan.py +16 -15
  231. flwr/serverapp/strategy/dp_adaptive_clipping.py +12 -11
  232. flwr/serverapp/strategy/dp_fixed_clipping.py +11 -14
  233. flwr/serverapp/strategy/fedadagrad.py +10 -11
  234. flwr/serverapp/strategy/fedadam.py +10 -11
  235. flwr/serverapp/strategy/fedavg.py +9 -10
  236. flwr/serverapp/strategy/fedavgm.py +17 -16
  237. flwr/serverapp/strategy/fedmedian.py +2 -2
  238. flwr/serverapp/strategy/fedopt.py +10 -11
  239. flwr/serverapp/strategy/fedprox.py +7 -8
  240. flwr/serverapp/strategy/fedtrimmedavg.py +9 -9
  241. flwr/serverapp/strategy/fedxgb_bagging.py +3 -3
  242. flwr/serverapp/strategy/fedxgb_cyclic.py +9 -9
  243. flwr/serverapp/strategy/fedyogi.py +9 -11
  244. flwr/serverapp/strategy/krum.py +7 -7
  245. flwr/serverapp/strategy/multikrum.py +9 -9
  246. flwr/serverapp/strategy/qfedavg.py +17 -16
  247. flwr/serverapp/strategy/strategy.py +6 -9
  248. flwr/serverapp/strategy/strategy_utils.py +7 -8
  249. flwr/simulation/app.py +46 -42
  250. flwr/simulation/legacy_app.py +12 -12
  251. flwr/simulation/ray_transport/ray_actor.py +11 -12
  252. flwr/simulation/ray_transport/ray_client_proxy.py +12 -13
  253. flwr/simulation/run_simulation.py +44 -43
  254. flwr/simulation/simulationio_connection.py +4 -4
  255. flwr/supercore/cli/flower_superexec.py +3 -4
  256. flwr/supercore/constant.py +52 -0
  257. flwr/supercore/corestate/corestate.py +24 -3
  258. flwr/supercore/corestate/in_memory_corestate.py +138 -0
  259. flwr/supercore/corestate/sqlite_corestate.py +157 -0
  260. flwr/supercore/ffs/disk_ffs.py +1 -2
  261. flwr/supercore/ffs/ffs.py +1 -2
  262. flwr/supercore/ffs/ffs_factory.py +1 -2
  263. flwr/{common → supercore}/heartbeat.py +20 -25
  264. flwr/supercore/object_store/in_memory_object_store.py +1 -6
  265. flwr/supercore/object_store/object_store.py +1 -2
  266. flwr/supercore/object_store/object_store_factory.py +27 -8
  267. flwr/supercore/object_store/sqlite_object_store.py +253 -0
  268. flwr/{client/clientapp → supercore/primitives}/__init__.py +1 -1
  269. flwr/supercore/primitives/asymmetric.py +117 -0
  270. flwr/supercore/primitives/asymmetric_ed25519.py +175 -0
  271. flwr/supercore/sqlite_mixin.py +159 -0
  272. flwr/supercore/superexec/plugin/base_exec_plugin.py +1 -2
  273. flwr/supercore/superexec/plugin/exec_plugin.py +3 -3
  274. flwr/supercore/superexec/run_superexec.py +9 -13
  275. flwr/supercore/utils.py +20 -0
  276. flwr/superlink/artifact_provider/artifact_provider.py +1 -2
  277. flwr/{common → superlink}/auth_plugin/__init__.py +6 -6
  278. flwr/superlink/auth_plugin/auth_plugin.py +88 -0
  279. flwr/superlink/auth_plugin/noop_auth_plugin.py +84 -0
  280. flwr/superlink/federation/__init__.py +24 -0
  281. flwr/superlink/federation/federation_manager.py +64 -0
  282. flwr/superlink/federation/noop_federation_manager.py +71 -0
  283. flwr/superlink/servicer/control/{control_user_auth_interceptor.py → control_account_auth_interceptor.py} +41 -32
  284. flwr/superlink/servicer/control/control_event_log_interceptor.py +7 -7
  285. flwr/superlink/servicer/control/control_grpc.py +18 -17
  286. flwr/superlink/servicer/control/control_license_interceptor.py +3 -3
  287. flwr/superlink/servicer/control/control_servicer.py +239 -63
  288. flwr/supernode/cli/flower_supernode.py +74 -26
  289. flwr/supernode/nodestate/in_memory_nodestate.py +60 -49
  290. flwr/supernode/nodestate/nodestate.py +7 -8
  291. flwr/supernode/nodestate/nodestate_factory.py +7 -4
  292. flwr/supernode/runtime/run_clientapp.py +43 -24
  293. flwr/supernode/servicer/clientappio/clientappio_servicer.py +40 -10
  294. flwr/supernode/start_client_internal.py +175 -51
  295. {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/METADATA +8 -8
  296. flwr-1.24.0.dist-info/RECORD +454 -0
  297. flwr/common/auth_plugin/auth_plugin.py +0 -149
  298. flwr/supercore/object_store/utils.py +0 -43
  299. flwr-1.22.0.dist-info/RECORD +0 -428
  300. {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/WHEEL +0 -0
  301. {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,140 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Flower command line interface `federation list` command."""
16
+
17
+
18
+ import io
19
+ from pathlib import Path
20
+ from typing import Annotated, cast
21
+
22
+ import typer
23
+ from rich.console import Console
24
+ from rich.table import Table
25
+ from rich.text import Text
26
+
27
+ from flwr.cli.config_utils import (
28
+ exit_if_no_address,
29
+ load_and_validate,
30
+ process_loaded_project_config,
31
+ validate_federation_in_project_config,
32
+ )
33
+ from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
34
+ from flwr.common.logger import print_json_error, redirect_output, restore_output
35
+ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
36
+ ListFederationsRequest,
37
+ ListFederationsResponse,
38
+ )
39
+ from flwr.proto.control_pb2_grpc import ControlStub
40
+ from flwr.proto.federation_pb2 import Federation # pylint: disable=E0611
41
+
42
+ from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
43
+
44
+
45
+ def ls( # pylint: disable=R0914, R0913, R0917
46
+ ctx: typer.Context,
47
+ app: Annotated[
48
+ Path,
49
+ typer.Argument(help="Path of the Flower project"),
50
+ ] = Path("."),
51
+ federation: Annotated[
52
+ str | None,
53
+ typer.Argument(help="Name of the federation"),
54
+ ] = None,
55
+ output_format: Annotated[
56
+ str,
57
+ typer.Option(
58
+ "--format",
59
+ case_sensitive=False,
60
+ help="Format output using 'default' view or 'json'",
61
+ ),
62
+ ] = CliOutputFormat.DEFAULT,
63
+ ) -> None:
64
+ """List available federations."""
65
+ # Resolve command used (list or ls)
66
+ command_name = cast(str, ctx.command.name) if ctx.command else "ls"
67
+
68
+ suppress_output = output_format == CliOutputFormat.JSON
69
+ captured_output = io.StringIO()
70
+ try:
71
+ if suppress_output:
72
+ redirect_output(captured_output)
73
+ typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
74
+
75
+ pyproject_path = app / FAB_CONFIG_FILE if app else None
76
+ config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
77
+ config = process_loaded_project_config(config, errors, warnings)
78
+ federation, federation_config = validate_federation_in_project_config(
79
+ federation, config
80
+ )
81
+ exit_if_no_address(federation_config, f"federation {command_name}")
82
+ channel = None
83
+ try:
84
+ auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
85
+ channel = init_channel(app, federation_config, auth_plugin)
86
+ stub = ControlStub(channel)
87
+ typer.echo("📄 Listing federations...")
88
+ federations = _list_federations(stub)
89
+ restore_output()
90
+ if output_format == CliOutputFormat.JSON:
91
+ Console().print_json(data=_to_json(federations))
92
+ else:
93
+ Console().print(_to_table(federations))
94
+ finally:
95
+ if channel:
96
+ channel.close()
97
+ except (typer.Exit, Exception) as err: # pylint: disable=broad-except
98
+ if suppress_output:
99
+ restore_output()
100
+ e_message = captured_output.getvalue()
101
+ print_json_error(e_message, err)
102
+ else:
103
+ typer.secho(
104
+ f"{err}",
105
+ fg=typer.colors.RED,
106
+ bold=True,
107
+ err=True,
108
+ )
109
+ finally:
110
+ if suppress_output:
111
+ restore_output()
112
+ captured_output.close()
113
+
114
+
115
+ def _list_federations(stub: ControlStub) -> list[Federation]:
116
+ """List all federations."""
117
+ with flwr_cli_grpc_exc_handler():
118
+ res: ListFederationsResponse = stub.ListFederations(ListFederationsRequest())
119
+
120
+ return list(res.federations)
121
+
122
+
123
+ def _to_table(federations: list[Federation]) -> Table:
124
+ """Format the provided federations list to a rich Table."""
125
+ table = Table(header_style="bold cyan", show_lines=True)
126
+
127
+ # Add columns
128
+ table.add_column(
129
+ Text("Federation", justify="center"), style="bright_black", no_wrap=True
130
+ )
131
+
132
+ for federation in federations:
133
+ table.add_row(federation.name)
134
+
135
+ return table
136
+
137
+
138
+ def _to_json(federations: list[Federation]) -> list[dict[str, str]]:
139
+ """Format the provided federations list to JSON serializable format."""
140
+ return [{"name": federation.name} for federation in federations]
@@ -0,0 +1,317 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Flower command line interface `federation show` command."""
16
+
17
+
18
+ import io
19
+ from pathlib import Path
20
+ from typing import Annotated, Any
21
+
22
+ import typer
23
+ from rich.console import Console
24
+ from rich.table import Table
25
+ from rich.text import Text
26
+
27
+ from flwr.cli.config_utils import (
28
+ exit_if_no_address,
29
+ load_and_validate,
30
+ process_loaded_project_config,
31
+ validate_federation_in_project_config,
32
+ )
33
+ from flwr.cli.ls import _get_status_style
34
+ from flwr.common.constant import FAB_CONFIG_FILE, NOOP_ACCOUNT_NAME, CliOutputFormat
35
+ from flwr.common.logger import print_json_error, redirect_output, restore_output
36
+ from flwr.common.serde import run_from_proto
37
+ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
38
+ ShowFederationRequest,
39
+ ShowFederationResponse,
40
+ )
41
+ from flwr.proto.control_pb2_grpc import ControlStub
42
+ from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
43
+ from flwr.supercore.constant import NOOP_FEDERATION
44
+
45
+ from ..run_utils import RunRow, format_runs
46
+ from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
47
+
48
+
49
+ def show( # pylint: disable=R0914, R0913, R0917
50
+ app: Annotated[
51
+ Path,
52
+ typer.Argument(help="Path of the Flower project"),
53
+ ] = Path("."),
54
+ federation: Annotated[
55
+ str | None,
56
+ typer.Argument(help="Name of the federation"),
57
+ ] = None,
58
+ output_format: Annotated[
59
+ str,
60
+ typer.Option(
61
+ "--format",
62
+ case_sensitive=False,
63
+ help="Format output using 'default' view or 'json'",
64
+ ),
65
+ ] = CliOutputFormat.DEFAULT,
66
+ ) -> None:
67
+ """Show details of a federation.
68
+
69
+ Display comprehensive information about a federation including its members,
70
+ registered SuperNodes, and runs.
71
+ """
72
+ suppress_output = output_format == CliOutputFormat.JSON
73
+ captured_output = io.StringIO()
74
+ try:
75
+ if suppress_output:
76
+ redirect_output(captured_output)
77
+ typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
78
+
79
+ pyproject_path = app / FAB_CONFIG_FILE if app else None
80
+ config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
81
+ config = process_loaded_project_config(config, errors, warnings)
82
+ federation, federation_config = validate_federation_in_project_config(
83
+ federation, config
84
+ )
85
+ exit_if_no_address(federation_config, "federation show")
86
+ real_federation: str = federation_config.get("federation", NOOP_FEDERATION)
87
+ channel = None
88
+ try:
89
+ auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
90
+ channel = init_channel(app, federation_config, auth_plugin)
91
+ stub = ControlStub(channel)
92
+ typer.echo(f"📄 Showing '{real_federation}' federation ...")
93
+ members, nodes, runs = _show_federation(stub, real_federation)
94
+ restore_output()
95
+ if output_format == CliOutputFormat.JSON:
96
+ Console().print_json(data=_to_json(members, nodes, runs))
97
+ else:
98
+ Console().print(_to_members_table(members))
99
+ Console().print(_to_nodes_table(nodes))
100
+ Console().print(_to_runs_table(runs))
101
+ finally:
102
+ if channel:
103
+ channel.close()
104
+ except (typer.Exit, Exception) as err: # pylint: disable=broad-except
105
+ if suppress_output:
106
+ restore_output()
107
+ e_message = captured_output.getvalue()
108
+ print_json_error(e_message, err)
109
+ else:
110
+ typer.secho(
111
+ f"{err}",
112
+ fg=typer.colors.RED,
113
+ bold=True,
114
+ err=True,
115
+ )
116
+ finally:
117
+ if suppress_output:
118
+ restore_output()
119
+ captured_output.close()
120
+
121
+
122
+ def _show_federation(
123
+ stub: ControlStub, federation: str
124
+ ) -> tuple[list[str], list[NodeInfo], list[RunRow]]:
125
+ """Show federation details.
126
+
127
+ Parameters
128
+ ----------
129
+ stub : ControlStub
130
+ The gRPC stub for Control API communication.
131
+ federation : str
132
+ Name of the federation to show.
133
+
134
+ Returns
135
+ -------
136
+ tuple[list[str], list[NodeInfo], list[RunRow]]
137
+ A tuple containing (member_account_ids, nodes, runs).
138
+ """
139
+ with flwr_cli_grpc_exc_handler():
140
+ res: ShowFederationResponse = stub.ShowFederation(
141
+ ShowFederationRequest(federation_name=federation)
142
+ )
143
+
144
+ fed_proto = res.federation
145
+ runs = [run_from_proto(run_proto) for run_proto in fed_proto.runs]
146
+ formatted_runs = format_runs(runs, res.now)
147
+
148
+ return list(fed_proto.member_aids), list(fed_proto.nodes), formatted_runs
149
+
150
+
151
+ def _to_members_table(member_aids: list[str]) -> Table:
152
+ """Format the provided list of federation members as a rich Table.
153
+
154
+ Parameters
155
+ ----------
156
+ member_aids : list[str]
157
+ List of member account identifiers.
158
+
159
+ Returns
160
+ -------
161
+ Table
162
+ Rich Table object with formatted member information.
163
+ """
164
+ table = Table(title="Federation Members", header_style="bold cyan", show_lines=True)
165
+
166
+ table.add_column(
167
+ Text("Account ID", justify="center"), style="bright_black", no_wrap=True
168
+ )
169
+ table.add_column(Text("Role", justify="center"), style="bright_black", no_wrap=True)
170
+
171
+ for member_aid in member_aids:
172
+ table.add_row(member_aid, "Member")
173
+
174
+ return table
175
+
176
+
177
+ def _to_nodes_table(nodes: list[NodeInfo]) -> Table:
178
+ """Format the provided list of federation nodes as a rich Table.
179
+
180
+ Parameters
181
+ ----------
182
+ nodes : list[NodeInfo]
183
+ List of NodeInfo objects containing node details.
184
+
185
+ Returns
186
+ -------
187
+ Table
188
+ Rich Table object with formatted node information.
189
+
190
+ Raises
191
+ ------
192
+ ValueError
193
+ If an unexpected node status is encountered.
194
+ """
195
+ table = Table(
196
+ title="SuperNodes in the Federation", header_style="bold cyan", show_lines=True
197
+ )
198
+
199
+ # Add columns
200
+ table.add_column(
201
+ Text("Node ID", justify="center"), style="bright_black", no_wrap=True
202
+ )
203
+ table.add_column(Text("Owner", justify="center"))
204
+ table.add_column(Text("Status", justify="center"))
205
+
206
+ for row in nodes:
207
+ owner_name = row.owner_name
208
+ status = row.status
209
+
210
+ if status == "online":
211
+ status_style = "green"
212
+ elif status == "offline":
213
+ status_style = "bright_yellow"
214
+ elif status == "unregistered":
215
+ continue
216
+ elif status == "registered":
217
+ status_style = "blue"
218
+ else:
219
+ raise ValueError(f"Unexpected node status '{status}'")
220
+
221
+ formatted_row = (
222
+ f"[bold]{row.node_id}[/bold]",
223
+ (
224
+ f"{owner_name}"
225
+ if owner_name != NOOP_ACCOUNT_NAME
226
+ else f"[dim]{owner_name}[/dim]"
227
+ ),
228
+ f"[{status_style}]{status}",
229
+ )
230
+ table.add_row(*formatted_row)
231
+
232
+ return table
233
+
234
+
235
+ def _to_runs_table(run_list: list[RunRow]) -> Table:
236
+ """Format the provided list of federation runs as a rich Table.
237
+
238
+ Parameters
239
+ ----------
240
+ run_list : list[RunRow]
241
+ List of RunRow objects containing run details.
242
+
243
+ Returns
244
+ -------
245
+ Table
246
+ Rich Table object with formatted run information.
247
+ """
248
+ table = Table(
249
+ title="Runs in the Federation", header_style="bold cyan", show_lines=True
250
+ )
251
+
252
+ # Add columns
253
+ table.add_column(Text("Run ID", justify="center"), no_wrap=True)
254
+ table.add_column(Text("App", justify="center"))
255
+ table.add_column(Text("Status", justify="center"))
256
+ table.add_column(Text("Elapsed", justify="center"), style="blue")
257
+
258
+ for row in run_list:
259
+ status_style = _get_status_style(row.status_text)
260
+
261
+ formatted_row = (
262
+ f"[bold]{row.run_id}[/bold]",
263
+ f"@{row.fab_id}=={row.fab_version}",
264
+ f"[{status_style}]{row.status_text}[/{status_style}]",
265
+ row.elapsed,
266
+ )
267
+ table.add_row(*formatted_row)
268
+
269
+ return table
270
+
271
+
272
+ def _to_json(
273
+ members: list[str], nodes: list[NodeInfo], runs: list[RunRow]
274
+ ) -> list[list[dict[str, str]]]:
275
+ """Format the provided federation information to JSON serializable format.
276
+
277
+ Parameters
278
+ ----------
279
+ members : list[str]
280
+ List of member account identifiers.
281
+ nodes : list[NodeInfo]
282
+ List of NodeInfo objects.
283
+ runs : list[RunRow]
284
+ List of RunRow objects.
285
+
286
+ Returns
287
+ -------
288
+ list[list[dict[str, str]]]
289
+ Nested list containing dictionaries for members, nodes, and runs.
290
+ """
291
+ members_list: list[dict[str, Any]] = []
292
+ nodes_list: list[dict[str, Any]] = []
293
+ runs_list: list[dict[str, Any]] = []
294
+
295
+ for member in members:
296
+ members_list.append({"member_id": member, "role": "Member"})
297
+
298
+ for node in nodes:
299
+ nodes_list.append(
300
+ {
301
+ "node_id": node.node_id,
302
+ "owner": node.owner_name,
303
+ "status": node.status,
304
+ }
305
+ )
306
+
307
+ for run in runs:
308
+ runs_list.append(
309
+ {
310
+ "run_id": run.run_id,
311
+ "app": f"@{run.fab_id}=={run.fab_version}",
312
+ "status": run.status_text,
313
+ "elapsed": run.elapsed,
314
+ }
315
+ )
316
+
317
+ return [members_list, nodes_list, runs_list]
flwr/cli/install.py CHANGED
@@ -21,7 +21,7 @@ import tempfile
21
21
  import zipfile
22
22
  from io import BytesIO
23
23
  from pathlib import Path
24
- from typing import IO, Annotated, Optional, Union
24
+ from typing import IO, Annotated
25
25
 
26
26
  import typer
27
27
 
@@ -34,11 +34,11 @@ from .utils import get_sha256_hash
34
34
 
35
35
  def install(
36
36
  source: Annotated[
37
- Optional[Path],
37
+ Path | None,
38
38
  typer.Argument(metavar="source", help="The source FAB file to install."),
39
39
  ] = None,
40
40
  flwr_dir: Annotated[
41
- Optional[Path],
41
+ Path | None,
42
42
  typer.Option(help="The desired install path."),
43
43
  ] = None,
44
44
  ) -> None:
@@ -68,6 +68,7 @@ def install(
68
68
  f"❌ The source {source} does not exist or is not a file.",
69
69
  fg=typer.colors.RED,
70
70
  bold=True,
71
+ err=True,
71
72
  )
72
73
  raise typer.Exit(code=1)
73
74
 
@@ -76,6 +77,7 @@ def install(
76
77
  f"❌ The source {source} is not a `.fab` file.",
77
78
  fg=typer.colors.RED,
78
79
  bold=True,
80
+ err=True,
79
81
  )
80
82
  raise typer.Exit(code=1)
81
83
 
@@ -83,13 +85,33 @@ def install(
83
85
 
84
86
 
85
87
  def install_from_fab(
86
- fab_file: Union[Path, bytes],
87
- flwr_dir: Optional[Path],
88
+ fab_file: Path | bytes,
89
+ flwr_dir: Path | None,
88
90
  skip_prompt: bool = False,
89
91
  ) -> Path:
90
- """Install from a FAB file after extracting and validating."""
91
- fab_file_archive: Union[Path, IO[bytes]]
92
- fab_name: Optional[str]
92
+ """Install from a FAB file after extracting and validating.
93
+
94
+ Parameters
95
+ ----------
96
+ fab_file : Path | bytes
97
+ Either a path to the FAB file or the FAB file content as bytes.
98
+ flwr_dir : Path | None
99
+ Target installation directory, or None to use default.
100
+ skip_prompt : bool
101
+ If True, skip confirmation prompts. Default is False.
102
+
103
+ Returns
104
+ -------
105
+ Path
106
+ Path to the installed application directory.
107
+
108
+ Raises
109
+ ------
110
+ typer.Exit
111
+ If FAB format is invalid or hash verification fails.
112
+ """
113
+ fab_file_archive: Path | IO[bytes]
114
+ fab_name: str | None
93
115
  if isinstance(fab_file, bytes):
94
116
  fab_file_archive = BytesIO(fab_file)
95
117
  fab_hash = hashlib.sha256(fab_file).hexdigest()
@@ -111,6 +133,7 @@ def install_from_fab(
111
133
  "❌ FAB file has incorrect format.",
112
134
  fg=typer.colors.RED,
113
135
  bold=True,
136
+ err=True,
114
137
  )
115
138
  raise typer.Exit(code=1)
116
139
 
@@ -123,6 +146,7 @@ def install_from_fab(
123
146
  "❌ File hashes couldn't be verified.",
124
147
  fg=typer.colors.RED,
125
148
  bold=True,
149
+ err=True,
126
150
  )
127
151
  raise typer.Exit(code=1)
128
152
 
@@ -139,11 +163,35 @@ def install_from_fab(
139
163
  def validate_and_install(
140
164
  project_dir: Path,
141
165
  fab_hash: str,
142
- fab_name: Optional[str],
143
- flwr_dir: Optional[Path],
166
+ fab_name: str | None,
167
+ flwr_dir: Path | None,
144
168
  skip_prompt: bool = False,
145
169
  ) -> Path:
146
- """Validate TOML files and install the project to the desired directory."""
170
+ """Validate the TOML file and install the project to the desired directory.
171
+
172
+ Parameters
173
+ ----------
174
+ project_dir : Path
175
+ Path to the extracted project directory.
176
+ fab_hash : str
177
+ SHA-256 hash of the FAB file.
178
+ fab_name : str | None
179
+ Name of the FAB file, or None if installing from bytes.
180
+ flwr_dir : Path | None
181
+ Target installation directory, or None to use default.
182
+ skip_prompt : bool (default: False)
183
+ If True, skip confirmation prompts.
184
+
185
+ Returns
186
+ -------
187
+ Path
188
+ Path to the installed application directory.
189
+
190
+ Raises
191
+ ------
192
+ typer.Exit
193
+ If configuration is invalid or metadata doesn't match.
194
+ """
147
195
  config, _, _ = load_and_validate(project_dir / "pyproject.toml", check_module=False)
148
196
 
149
197
  if config is None:
@@ -151,6 +199,7 @@ def validate_and_install(
151
199
  "❌ Invalid config inside FAB file.",
152
200
  fg=typer.colors.RED,
153
201
  bold=True,
202
+ err=True,
154
203
  )
155
204
  raise typer.Exit(code=1)
156
205
 
@@ -198,7 +247,20 @@ def validate_and_install(
198
247
 
199
248
 
200
249
  def _verify_hashes(list_content: str, tmpdir: Path) -> bool:
201
- """Verify file hashes based on the LIST content."""
250
+ """Verify file hashes based on the CONTENT manifest.
251
+
252
+ Parameters
253
+ ----------
254
+ list_content : str
255
+ Content of the CONTENT manifest file with hash information.
256
+ tmpdir : Path
257
+ Temporary directory containing extracted files.
258
+
259
+ Returns
260
+ -------
261
+ bool
262
+ True if all file hashes match, False otherwise.
263
+ """
202
264
  for line in list_content.strip().split("\n"):
203
265
  rel_path, hash_expected, _ = line.split(",")
204
266
  file_path = tmpdir / rel_path
@@ -210,7 +272,20 @@ def _verify_hashes(list_content: str, tmpdir: Path) -> bool:
210
272
  def _validate_fab_and_config_metadata(
211
273
  fab_name: str, config_metadata: tuple[str, str, str, str]
212
274
  ) -> None:
213
- """Validate metadata from the FAB filename and config."""
275
+ """Validate metadata from the FAB filename and config.
276
+
277
+ Parameters
278
+ ----------
279
+ fab_name : str
280
+ The FAB filename (with or without .fab extension).
281
+ config_metadata : tuple[str, str, str, str]
282
+ Tuple of (publisher, project_name, version, fab_hash).
283
+
284
+ Raises
285
+ ------
286
+ typer.Exit
287
+ If filename format is incorrect or hash doesn't match.
288
+ """
214
289
  publisher, project_name, version, fab_hash = config_metadata
215
290
 
216
291
  fab_name = fab_name.removesuffix(".fab")
@@ -229,6 +304,7 @@ def _validate_fab_and_config_metadata(
229
304
  "`<publisher>.<project_name>.<version>.<8hexchars>.fab`.",
230
305
  fg=typer.colors.RED,
231
306
  bold=True,
307
+ err=True,
232
308
  )
233
309
  raise typer.Exit(code=1)
234
310
 
@@ -240,6 +316,7 @@ def _validate_fab_and_config_metadata(
240
316
  f"❌ FAB file has an invalid hexadecimal string `{fab_shorthash}`.",
241
317
  fg=typer.colors.RED,
242
318
  bold=True,
319
+ err=True,
243
320
  )
244
321
  raise typer.Exit(code=1) from e
245
322
 
@@ -249,5 +326,6 @@ def _validate_fab_and_config_metadata(
249
326
  "❌ The hash in the FAB file name does not match the hash of the FAB.",
250
327
  fg=typer.colors.RED,
251
328
  bold=True,
329
+ err=True,
252
330
  )
253
331
  raise typer.Exit(code=1)