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,25 @@
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 `supernode` command."""
16
+
17
+ from .ls import ls as ls
18
+ from .register import register as register
19
+ from .unregister import unregister as unregister
20
+
21
+ __all__ = [
22
+ "ls",
23
+ "register",
24
+ "unregister",
25
+ ]
@@ -0,0 +1,268 @@
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 `supernode list` command."""
16
+
17
+
18
+ import io
19
+ import json
20
+ from datetime import datetime, timedelta
21
+ from pathlib import Path
22
+ from typing import Annotated, cast
23
+
24
+ import typer
25
+ from rich.console import Console
26
+ from rich.table import Table
27
+ from rich.text import Text
28
+
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 format_timedelta, isoformat8601_utc
37
+ from flwr.common.logger import print_json_error, redirect_output, restore_output
38
+ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
39
+ ListNodesRequest,
40
+ ListNodesResponse,
41
+ )
42
+ from flwr.proto.control_pb2_grpc import ControlStub
43
+ from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
44
+
45
+ from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
46
+
47
+ _NodeListType = tuple[int, str, str, str, str, str, str, str, str]
48
+
49
+
50
+ def ls( # pylint: disable=R0914, R0913, R0917
51
+ ctx: typer.Context,
52
+ app: Annotated[
53
+ Path,
54
+ typer.Argument(help="Path of the Flower project"),
55
+ ] = Path("."),
56
+ federation: Annotated[
57
+ str | None,
58
+ typer.Argument(help="Name of the federation"),
59
+ ] = None,
60
+ output_format: Annotated[
61
+ str,
62
+ typer.Option(
63
+ "--format",
64
+ case_sensitive=False,
65
+ help="Format output using 'default' view or 'json'",
66
+ ),
67
+ ] = CliOutputFormat.DEFAULT,
68
+ verbose: Annotated[
69
+ bool,
70
+ typer.Option(
71
+ "--verbose",
72
+ "-v",
73
+ help="Enable verbose output",
74
+ ),
75
+ ] = False,
76
+ ) -> None:
77
+ """List SuperNodes in the federation."""
78
+ # Resolve command used (list or ls)
79
+ command_name = cast(str, ctx.command.name) if ctx.command else "ls"
80
+
81
+ suppress_output = output_format == CliOutputFormat.JSON
82
+ captured_output = io.StringIO()
83
+ try:
84
+ if suppress_output:
85
+ redirect_output(captured_output)
86
+ typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
87
+
88
+ pyproject_path = app / FAB_CONFIG_FILE if app else None
89
+ config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
90
+ config = process_loaded_project_config(config, errors, warnings)
91
+ federation, federation_config = validate_federation_in_project_config(
92
+ federation, config
93
+ )
94
+ exit_if_no_address(federation_config, f"supernode {command_name}")
95
+ channel = None
96
+ try:
97
+ auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
98
+ channel = init_channel(app, federation_config, auth_plugin)
99
+ stub = ControlStub(channel)
100
+ typer.echo("📄 Listing all nodes...")
101
+ formatted_nodes = _list_nodes(stub)
102
+ restore_output()
103
+ if output_format == CliOutputFormat.JSON:
104
+ Console().print_json(_to_json(formatted_nodes, verbose=verbose))
105
+ else:
106
+ Console().print(_to_table(formatted_nodes, verbose=verbose))
107
+
108
+ finally:
109
+ if channel:
110
+ channel.close()
111
+ except (typer.Exit, Exception) as err: # pylint: disable=broad-except
112
+ if suppress_output:
113
+ restore_output()
114
+ e_message = captured_output.getvalue()
115
+ print_json_error(e_message, err)
116
+ else:
117
+ typer.secho(
118
+ f"{err}",
119
+ fg=typer.colors.RED,
120
+ bold=True,
121
+ )
122
+ finally:
123
+ if suppress_output:
124
+ restore_output()
125
+ captured_output.close()
126
+
127
+
128
+ def _list_nodes(stub: ControlStub) -> list[_NodeListType]:
129
+ """List all nodes."""
130
+ with flwr_cli_grpc_exc_handler():
131
+ res: ListNodesResponse = stub.ListNodes(ListNodesRequest())
132
+
133
+ return _format_nodes(list(res.nodes_info), res.now)
134
+
135
+
136
+ def _format_nodes(
137
+ nodes_info: list[NodeInfo], now_isoformat: str
138
+ ) -> list[_NodeListType]:
139
+ """Format node information for display."""
140
+
141
+ def _format_datetime(dt_str: str | None) -> str:
142
+ dt = datetime.fromisoformat(dt_str) if dt_str else None
143
+ return isoformat8601_utc(dt).replace("T", " ") if dt else "N/A"
144
+
145
+ formatted_nodes: list[_NodeListType] = []
146
+ # Add rows
147
+ for node in sorted(
148
+ nodes_info, key=lambda x: datetime.fromisoformat(x.registered_at)
149
+ ):
150
+
151
+ # Calculate elapsed times
152
+ elapsed_time_activated = timedelta()
153
+ if node.last_activated_at:
154
+ end_time = datetime.fromisoformat(now_isoformat)
155
+ elapsed_time_activated = end_time - datetime.fromisoformat(
156
+ node.last_activated_at
157
+ )
158
+
159
+ formatted_nodes.append(
160
+ (
161
+ node.node_id,
162
+ node.owner_aid,
163
+ node.owner_name,
164
+ node.status,
165
+ _format_datetime(node.registered_at),
166
+ _format_datetime(node.last_activated_at),
167
+ _format_datetime(node.last_deactivated_at),
168
+ _format_datetime(node.unregistered_at),
169
+ format_timedelta(elapsed_time_activated),
170
+ )
171
+ )
172
+
173
+ return formatted_nodes
174
+
175
+
176
+ def _to_table(nodes_info: list[_NodeListType], verbose: bool) -> Table:
177
+ """Format the provided node list to a rich Table."""
178
+ table = Table(header_style="bold cyan", show_lines=True)
179
+
180
+ # Add columns
181
+ table.add_column(
182
+ Text("Node ID", justify="center"), style="bright_black", no_wrap=True
183
+ )
184
+ table.add_column(Text("Owner", justify="center"))
185
+ table.add_column(Text("Status", justify="center"))
186
+ table.add_column(Text("Elapsed", justify="center"))
187
+ table.add_column(Text("Status Changed @", justify="center"), style="bright_black")
188
+
189
+ for row in nodes_info:
190
+ (
191
+ node_id,
192
+ _,
193
+ owner_name,
194
+ status,
195
+ _,
196
+ last_activated_at,
197
+ last_deactivated_at,
198
+ unregistered_at,
199
+ elapse_activated,
200
+ ) = row
201
+
202
+ if status == "online":
203
+ status_style = "green"
204
+ time_at = last_activated_at
205
+ elif status == "offline":
206
+ status_style = "bright_yellow"
207
+ time_at = last_deactivated_at
208
+ elif status == "unregistered":
209
+ if not verbose:
210
+ continue
211
+ status_style = "red"
212
+ time_at = unregistered_at
213
+ elif status == "registered":
214
+ status_style = "blue"
215
+ time_at = "N/A"
216
+ else:
217
+ raise ValueError(f"Unexpected node status '{status}'")
218
+
219
+ formatted_row = (
220
+ f"[bold]{node_id}[/bold]",
221
+ (
222
+ f"{owner_name}"
223
+ if owner_name != NOOP_ACCOUNT_NAME
224
+ else f"[dim]{owner_name}[/dim]"
225
+ ),
226
+ f"[{status_style}]{status}",
227
+ f"[cyan]{elapse_activated}[/cyan]" if status == "online" else "",
228
+ time_at,
229
+ )
230
+ table.add_row(*formatted_row)
231
+
232
+ return table
233
+
234
+
235
+ def _to_json(nodes_info: list[_NodeListType], verbose: bool) -> str:
236
+ """Format node list to a JSON formatted string."""
237
+ nodes_list = []
238
+ for row in nodes_info:
239
+ (
240
+ node_id,
241
+ owner_aid,
242
+ owner_name,
243
+ status,
244
+ created_at,
245
+ activated_at,
246
+ deactivated_at,
247
+ deleted_at,
248
+ elapse_activated,
249
+ ) = row
250
+
251
+ if status == "deleted" and not verbose:
252
+ continue
253
+
254
+ nodes_list.append(
255
+ {
256
+ "node-id": node_id,
257
+ "owner-aid": owner_aid,
258
+ "owner-name": owner_name,
259
+ "status": status,
260
+ "created-at": created_at,
261
+ "online-at": activated_at,
262
+ "online-elapsed": elapse_activated,
263
+ "offline-at": deactivated_at,
264
+ "deleted-at": deleted_at,
265
+ }
266
+ )
267
+
268
+ return json.dumps({"success": True, "nodes": nodes_list})
@@ -0,0 +1,190 @@
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 `supernode register` command."""
16
+
17
+
18
+ import io
19
+ import json
20
+ from pathlib import Path
21
+ from typing import Annotated
22
+
23
+ import typer
24
+ from cryptography.exceptions import UnsupportedAlgorithm
25
+ from cryptography.hazmat.primitives import serialization
26
+ from cryptography.hazmat.primitives.asymmetric import ec
27
+ from rich.console import Console
28
+
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, CliOutputFormat
36
+ from flwr.common.exit import ExitCode, flwr_exit
37
+ from flwr.common.logger import print_json_error, redirect_output, restore_output
38
+ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
39
+ RegisterNodeRequest,
40
+ RegisterNodeResponse,
41
+ )
42
+ from flwr.proto.control_pb2_grpc import ControlStub
43
+ from flwr.supercore.primitives.asymmetric import public_key_to_bytes, uses_nist_ec_curve
44
+
45
+ from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
46
+
47
+
48
+ def register( # pylint: disable=R0914
49
+ public_key: Annotated[
50
+ Path,
51
+ typer.Argument(
52
+ help="Path to a P-384 (or any other NIST EC curve) public key file.",
53
+ ),
54
+ ],
55
+ app: Annotated[
56
+ Path,
57
+ typer.Argument(help="Path of the Flower project"),
58
+ ] = Path("."),
59
+ federation: Annotated[
60
+ str | None,
61
+ typer.Argument(help="Name of the federation"),
62
+ ] = None,
63
+ output_format: Annotated[
64
+ str,
65
+ typer.Option(
66
+ "--format",
67
+ case_sensitive=False,
68
+ help="Format output using 'default' view or 'json'",
69
+ ),
70
+ ] = CliOutputFormat.DEFAULT,
71
+ ) -> None:
72
+ """Add a SuperNode to the federation."""
73
+ suppress_output = output_format == CliOutputFormat.JSON
74
+ captured_output = io.StringIO()
75
+
76
+ # Load public key
77
+ public_key_path = Path(public_key)
78
+ public_key_bytes = try_load_public_key(public_key_path)
79
+
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
92
+ )
93
+ exit_if_no_address(federation_config, "supernode register")
94
+
95
+ channel = None
96
+ try:
97
+ auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
98
+ channel = init_channel(app, federation_config, auth_plugin)
99
+ stub = ControlStub(channel) # pylint: disable=unused-variable # noqa: F841
100
+
101
+ _register_node(
102
+ stub=stub, public_key=public_key_bytes, output_format=output_format
103
+ )
104
+
105
+ except ValueError as err:
106
+ typer.secho(
107
+ f"❌ {err}",
108
+ fg=typer.colors.RED,
109
+ bold=True,
110
+ err=True,
111
+ )
112
+ raise typer.Exit(code=1) from err
113
+ finally:
114
+ if channel:
115
+ channel.close()
116
+
117
+ except (typer.Exit, Exception) as err: # pylint: disable=broad-except
118
+ if suppress_output:
119
+ restore_output()
120
+ e_message = captured_output.getvalue()
121
+ print_json_error(e_message, err)
122
+ else:
123
+ typer.secho(
124
+ f"{err}",
125
+ fg=typer.colors.RED,
126
+ bold=True,
127
+ err=True,
128
+ )
129
+ finally:
130
+ if suppress_output:
131
+ restore_output()
132
+ captured_output.close()
133
+
134
+
135
+ def _register_node(stub: ControlStub, public_key: bytes, output_format: str) -> None:
136
+ """Register a node."""
137
+ with flwr_cli_grpc_exc_handler():
138
+ response: RegisterNodeResponse = stub.RegisterNode(
139
+ request=RegisterNodeRequest(public_key=public_key)
140
+ )
141
+ if response.node_id:
142
+ typer.secho(
143
+ f"✅ SuperNode {response.node_id} registered successfully.",
144
+ fg=typer.colors.GREEN,
145
+ )
146
+ if output_format == CliOutputFormat.JSON:
147
+ run_output = json.dumps(
148
+ {
149
+ "success": True,
150
+ "node-id": response.node_id,
151
+ }
152
+ )
153
+ restore_output()
154
+ Console().print_json(run_output)
155
+ else:
156
+ typer.secho(
157
+ "❌ SuperNode couldn't be registered.", fg=typer.colors.RED, err=True
158
+ )
159
+
160
+
161
+ def try_load_public_key(public_key_path: Path) -> bytes:
162
+ """Try to load a public key from a file."""
163
+ if not public_key_path.exists():
164
+ typer.secho(
165
+ f"❌ Public key file '{public_key_path}' does not exist.",
166
+ fg=typer.colors.RED,
167
+ bold=True,
168
+ err=True,
169
+ )
170
+ raise typer.Exit(code=1)
171
+
172
+ with open(public_key_path, "rb") as key_file:
173
+ try:
174
+ public_key = serialization.load_ssh_public_key(key_file.read())
175
+
176
+ if not isinstance(public_key, ec.EllipticCurvePublicKey):
177
+ raise ValueError(f"Not an EC public key, got {type(public_key)}")
178
+
179
+ # Verify it's one of the approved NIST curves
180
+ if not uses_nist_ec_curve(public_key):
181
+ raise ValueError(
182
+ f"EC curve {public_key.curve.name} is not an approved NIST curve"
183
+ )
184
+
185
+ except (ValueError, UnsupportedAlgorithm) as err:
186
+ flwr_exit(
187
+ ExitCode.FLWRCLI_NODE_AUTH_PUBLIC_KEY_INVALID,
188
+ str(err),
189
+ )
190
+ return public_key_to_bytes(public_key)
@@ -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 `supernode unregister` command."""
16
+
17
+
18
+ import io
19
+ import json
20
+ from pathlib import Path
21
+ from typing import Annotated
22
+
23
+ import typer
24
+ from rich.console import Console
25
+
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
+ )
32
+ from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
33
+ from flwr.common.logger import print_json_error, redirect_output, restore_output
34
+ from flwr.proto.control_pb2 import UnregisterNodeRequest # pylint: disable=E0611
35
+ from flwr.proto.control_pb2_grpc import ControlStub
36
+
37
+ from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
38
+
39
+
40
+ def unregister( # pylint: disable=R0914
41
+ node_id: Annotated[
42
+ int,
43
+ typer.Argument(
44
+ help="ID of the SuperNode to remove.",
45
+ ),
46
+ ],
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
+ """Unregister a SuperNode from the federation."""
65
+ suppress_output = output_format == CliOutputFormat.JSON
66
+ captured_output = io.StringIO()
67
+
68
+ try:
69
+ if suppress_output:
70
+ redirect_output(captured_output)
71
+
72
+ # Load and validate federation config
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, "supernode unregister")
82
+
83
+ channel = None
84
+ try:
85
+ auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
86
+ channel = init_channel(app, federation_config, auth_plugin)
87
+ stub = ControlStub(channel) # pylint: disable=unused-variable # noqa: F841
88
+
89
+ _unregister_node(stub=stub, node_id=node_id, output_format=output_format)
90
+
91
+ except ValueError as err:
92
+ typer.secho(
93
+ f"❌ {err}",
94
+ fg=typer.colors.RED,
95
+ bold=True,
96
+ err=True,
97
+ )
98
+ raise typer.Exit(code=1) from err
99
+ finally:
100
+ if channel:
101
+ channel.close()
102
+
103
+ except (typer.Exit, Exception) as err: # pylint: disable=broad-except
104
+ if suppress_output:
105
+ restore_output()
106
+ e_message = captured_output.getvalue()
107
+ print_json_error(e_message, err)
108
+ else:
109
+ typer.secho(
110
+ f"{err}",
111
+ fg=typer.colors.RED,
112
+ bold=True,
113
+ err=True,
114
+ )
115
+ finally:
116
+ if suppress_output:
117
+ restore_output()
118
+ captured_output.close()
119
+
120
+
121
+ def _unregister_node(
122
+ stub: ControlStub,
123
+ node_id: int,
124
+ output_format: str,
125
+ ) -> None:
126
+ """Unregister a SuperNode from the federation."""
127
+ with flwr_cli_grpc_exc_handler():
128
+ stub.UnregisterNode(request=UnregisterNodeRequest(node_id=node_id))
129
+ typer.secho(
130
+ f"✅ SuperNode {node_id} unregistered successfully.", fg=typer.colors.GREEN
131
+ )
132
+ if output_format == CliOutputFormat.JSON:
133
+ run_output = json.dumps(
134
+ {
135
+ "success": True,
136
+ "node-id": node_id,
137
+ }
138
+ )
139
+ restore_output()
140
+ Console().print_json(run_output)