flwr 1.21.0__py3-none-any.whl → 1.23.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 (175) hide show
  1. flwr/cli/app.py +17 -1
  2. flwr/cli/auth_plugin/__init__.py +15 -6
  3. flwr/cli/auth_plugin/auth_plugin.py +95 -0
  4. flwr/cli/auth_plugin/noop_auth_plugin.py +58 -0
  5. flwr/cli/auth_plugin/oidc_cli_plugin.py +16 -25
  6. flwr/cli/build.py +118 -47
  7. flwr/cli/{cli_user_auth_interceptor.py → cli_account_auth_interceptor.py} +6 -5
  8. flwr/cli/log.py +2 -2
  9. flwr/cli/login/login.py +34 -23
  10. flwr/cli/ls.py +13 -9
  11. flwr/cli/new/new.py +196 -42
  12. flwr/cli/new/templates/app/README.flowertune.md.tpl +1 -1
  13. flwr/cli/new/templates/app/code/client.baseline.py.tpl +64 -47
  14. flwr/cli/new/templates/app/code/client.huggingface.py.tpl +68 -30
  15. flwr/cli/new/templates/app/code/client.jax.py.tpl +63 -42
  16. flwr/cli/new/templates/app/code/client.mlx.py.tpl +80 -51
  17. flwr/cli/new/templates/app/code/client.numpy.py.tpl +36 -13
  18. flwr/cli/new/templates/app/code/client.pytorch.py.tpl +71 -46
  19. flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +55 -0
  20. flwr/cli/new/templates/app/code/client.sklearn.py.tpl +75 -30
  21. flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +69 -44
  22. flwr/cli/new/templates/app/code/client.xgboost.py.tpl +110 -0
  23. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +56 -90
  24. flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +1 -23
  25. flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +37 -58
  26. flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +39 -44
  27. flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -14
  28. flwr/cli/new/templates/app/code/server.baseline.py.tpl +27 -29
  29. flwr/cli/new/templates/app/code/server.huggingface.py.tpl +23 -19
  30. flwr/cli/new/templates/app/code/server.jax.py.tpl +27 -14
  31. flwr/cli/new/templates/app/code/server.mlx.py.tpl +29 -19
  32. flwr/cli/new/templates/app/code/server.numpy.py.tpl +30 -17
  33. flwr/cli/new/templates/app/code/server.pytorch.py.tpl +36 -26
  34. flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +31 -0
  35. flwr/cli/new/templates/app/code/server.sklearn.py.tpl +29 -21
  36. flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +28 -19
  37. flwr/cli/new/templates/app/code/server.xgboost.py.tpl +56 -0
  38. flwr/cli/new/templates/app/code/task.huggingface.py.tpl +16 -20
  39. flwr/cli/new/templates/app/code/task.jax.py.tpl +1 -1
  40. flwr/cli/new/templates/app/code/task.numpy.py.tpl +1 -1
  41. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +14 -27
  42. flwr/cli/new/templates/app/code/{task.pytorch_msg_api.py.tpl → task.pytorch_legacy_api.py.tpl} +27 -14
  43. flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +1 -2
  44. flwr/cli/new/templates/app/code/task.xgboost.py.tpl +67 -0
  45. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +4 -4
  46. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +2 -2
  47. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +4 -4
  48. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  49. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +2 -2
  50. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  51. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
  52. flwr/cli/new/templates/app/{pyproject.pytorch_msg_api.toml.tpl → pyproject.pytorch_legacy_api.toml.tpl} +3 -3
  53. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  54. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
  55. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +61 -0
  56. flwr/cli/pull.py +100 -0
  57. flwr/cli/run/run.py +11 -7
  58. flwr/cli/stop.py +2 -2
  59. flwr/cli/supernode/__init__.py +25 -0
  60. flwr/cli/supernode/ls.py +260 -0
  61. flwr/cli/supernode/register.py +185 -0
  62. flwr/cli/supernode/unregister.py +138 -0
  63. flwr/cli/utils.py +109 -69
  64. flwr/client/__init__.py +2 -1
  65. flwr/client/grpc_adapter_client/connection.py +6 -8
  66. flwr/client/grpc_rere_client/connection.py +59 -31
  67. flwr/client/grpc_rere_client/grpc_adapter.py +28 -12
  68. flwr/client/grpc_rere_client/{client_interceptor.py → node_auth_client_interceptor.py} +3 -6
  69. flwr/client/mod/secure_aggregation/secaggplus_mod.py +7 -5
  70. flwr/client/rest_client/connection.py +82 -37
  71. flwr/clientapp/__init__.py +1 -2
  72. flwr/clientapp/mod/__init__.py +4 -1
  73. flwr/clientapp/mod/centraldp_mods.py +156 -40
  74. flwr/clientapp/mod/localdp_mod.py +169 -0
  75. flwr/clientapp/typing.py +22 -0
  76. flwr/{client/clientapp → clientapp}/utils.py +1 -1
  77. flwr/common/constant.py +56 -13
  78. flwr/common/exit/exit_code.py +24 -10
  79. flwr/common/inflatable_utils.py +10 -10
  80. flwr/common/record/array.py +3 -3
  81. flwr/common/record/arrayrecord.py +10 -1
  82. flwr/common/record/typeddict.py +12 -0
  83. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +1 -89
  84. flwr/common/serde.py +4 -2
  85. flwr/common/typing.py +7 -6
  86. flwr/compat/client/app.py +1 -1
  87. flwr/compat/client/grpc_client/connection.py +2 -2
  88. flwr/proto/control_pb2.py +48 -31
  89. flwr/proto/control_pb2.pyi +95 -5
  90. flwr/proto/control_pb2_grpc.py +136 -0
  91. flwr/proto/control_pb2_grpc.pyi +52 -0
  92. flwr/proto/fab_pb2.py +11 -7
  93. flwr/proto/fab_pb2.pyi +21 -1
  94. flwr/proto/fleet_pb2.py +31 -23
  95. flwr/proto/fleet_pb2.pyi +63 -23
  96. flwr/proto/fleet_pb2_grpc.py +98 -28
  97. flwr/proto/fleet_pb2_grpc.pyi +45 -13
  98. flwr/proto/node_pb2.py +3 -1
  99. flwr/proto/node_pb2.pyi +48 -0
  100. flwr/server/app.py +152 -114
  101. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +17 -7
  102. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +132 -38
  103. flwr/server/superlink/fleet/grpc_rere/{server_interceptor.py → node_auth_server_interceptor.py} +27 -51
  104. flwr/server/superlink/fleet/message_handler/message_handler.py +67 -22
  105. flwr/server/superlink/fleet/rest_rere/rest_api.py +52 -31
  106. flwr/server/superlink/fleet/vce/backend/backend.py +1 -1
  107. flwr/server/superlink/fleet/vce/backend/raybackend.py +1 -1
  108. flwr/server/superlink/fleet/vce/vce_api.py +18 -5
  109. flwr/server/superlink/linkstate/in_memory_linkstate.py +167 -73
  110. flwr/server/superlink/linkstate/linkstate.py +107 -24
  111. flwr/server/superlink/linkstate/linkstate_factory.py +2 -1
  112. flwr/server/superlink/linkstate/sqlite_linkstate.py +306 -255
  113. flwr/server/superlink/linkstate/utils.py +3 -54
  114. flwr/server/superlink/serverappio/serverappio_servicer.py +2 -2
  115. flwr/server/superlink/simulation/simulationio_servicer.py +1 -1
  116. flwr/server/utils/validator.py +2 -3
  117. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +4 -2
  118. flwr/serverapp/strategy/__init__.py +26 -0
  119. flwr/serverapp/strategy/bulyan.py +238 -0
  120. flwr/serverapp/strategy/dp_adaptive_clipping.py +335 -0
  121. flwr/serverapp/strategy/dp_fixed_clipping.py +71 -49
  122. flwr/serverapp/strategy/fedadagrad.py +0 -3
  123. flwr/serverapp/strategy/fedadam.py +0 -3
  124. flwr/serverapp/strategy/fedavg.py +89 -64
  125. flwr/serverapp/strategy/fedavgm.py +198 -0
  126. flwr/serverapp/strategy/fedmedian.py +105 -0
  127. flwr/serverapp/strategy/fedprox.py +174 -0
  128. flwr/serverapp/strategy/fedtrimmedavg.py +176 -0
  129. flwr/serverapp/strategy/fedxgb_bagging.py +117 -0
  130. flwr/serverapp/strategy/fedxgb_cyclic.py +220 -0
  131. flwr/serverapp/strategy/fedyogi.py +0 -3
  132. flwr/serverapp/strategy/krum.py +112 -0
  133. flwr/serverapp/strategy/multikrum.py +247 -0
  134. flwr/serverapp/strategy/qfedavg.py +252 -0
  135. flwr/serverapp/strategy/strategy_utils.py +48 -0
  136. flwr/simulation/app.py +1 -1
  137. flwr/simulation/ray_transport/ray_actor.py +1 -1
  138. flwr/simulation/ray_transport/ray_client_proxy.py +1 -1
  139. flwr/simulation/run_simulation.py +28 -32
  140. flwr/supercore/cli/flower_superexec.py +26 -1
  141. flwr/supercore/constant.py +41 -0
  142. flwr/supercore/object_store/in_memory_object_store.py +0 -4
  143. flwr/supercore/object_store/object_store_factory.py +26 -6
  144. flwr/supercore/object_store/sqlite_object_store.py +252 -0
  145. flwr/{client/clientapp → supercore/primitives}/__init__.py +1 -1
  146. flwr/supercore/primitives/asymmetric.py +117 -0
  147. flwr/supercore/primitives/asymmetric_ed25519.py +165 -0
  148. flwr/supercore/sqlite_mixin.py +156 -0
  149. flwr/supercore/superexec/plugin/exec_plugin.py +11 -1
  150. flwr/supercore/superexec/run_superexec.py +16 -2
  151. flwr/supercore/utils.py +20 -0
  152. flwr/superlink/artifact_provider/__init__.py +22 -0
  153. flwr/superlink/artifact_provider/artifact_provider.py +37 -0
  154. flwr/{common → superlink}/auth_plugin/__init__.py +6 -6
  155. flwr/superlink/auth_plugin/auth_plugin.py +91 -0
  156. flwr/superlink/auth_plugin/noop_auth_plugin.py +87 -0
  157. flwr/superlink/servicer/control/{control_user_auth_interceptor.py → control_account_auth_interceptor.py} +19 -19
  158. flwr/superlink/servicer/control/control_event_log_interceptor.py +1 -1
  159. flwr/superlink/servicer/control/control_grpc.py +16 -11
  160. flwr/superlink/servicer/control/control_servicer.py +207 -58
  161. flwr/supernode/cli/flower_supernode.py +19 -26
  162. flwr/supernode/runtime/run_clientapp.py +2 -2
  163. flwr/supernode/servicer/clientappio/clientappio_servicer.py +1 -1
  164. flwr/supernode/start_client_internal.py +17 -9
  165. {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/METADATA +6 -16
  166. {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/RECORD +170 -140
  167. flwr/cli/new/templates/app/code/client.pytorch_msg_api.py.tpl +0 -80
  168. flwr/cli/new/templates/app/code/server.pytorch_msg_api.py.tpl +0 -41
  169. flwr/common/auth_plugin/auth_plugin.py +0 -149
  170. flwr/serverapp/dp_fixed_clipping.py +0 -352
  171. flwr/serverapp/strategy/strategy_utils_tests.py +0 -304
  172. /flwr/cli/new/templates/app/code/{__init__.pytorch_msg_api.py.tpl → __init__.pytorch_legacy_api.py.tpl} +0 -0
  173. /flwr/{client → clientapp}/client_app.py +0 -0
  174. {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/WHEEL +0 -0
  175. {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,61 @@
1
+ # =====================================================================
2
+ # For a full TOML configuration guide, check the Flower docs:
3
+ # https://flower.ai/docs/framework/how-to-configure-pyproject-toml.html
4
+ # =====================================================================
5
+
6
+ [build-system]
7
+ requires = ["hatchling"]
8
+ build-backend = "hatchling.build"
9
+
10
+ [project]
11
+ name = "$package_name"
12
+ version = "1.0.0"
13
+ description = ""
14
+ license = "Apache-2.0"
15
+ # Dependencies for your Flower App
16
+ dependencies = [
17
+ "flwr[simulation]>=1.23.0",
18
+ "flwr-datasets>=0.5.0",
19
+ "xgboost>=2.0.0",
20
+ ]
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["."]
24
+
25
+ [tool.flwr.app]
26
+ publisher = "$username"
27
+
28
+ [tool.flwr.app.components]
29
+ serverapp = "$import_name.server_app:app"
30
+ clientapp = "$import_name.client_app:app"
31
+
32
+ # Custom config values accessible via `context.run_config`
33
+ [tool.flwr.app.config]
34
+ num-server-rounds = 3
35
+ fraction-train = 0.1
36
+ fraction-evaluate = 0.1
37
+ local-epochs = 1
38
+
39
+ # XGBoost parameters
40
+ params.objective = "binary:logistic"
41
+ params.eta = 0.1 # Learning rate
42
+ params.max-depth = 8
43
+ params.eval-metric = "auc"
44
+ params.nthread = 16
45
+ params.num-parallel-tree = 1
46
+ params.subsample = 1
47
+ params.tree-method = "hist"
48
+
49
+ # Default federation to use when running the app
50
+ [tool.flwr.federations]
51
+ default = "local-simulation"
52
+
53
+ # Local simulation federation with 10 virtual SuperNodes
54
+ [tool.flwr.federations.local-simulation]
55
+ options.num-supernodes = 10
56
+
57
+ # Remote federation example for use with SuperLink
58
+ [tool.flwr.federations.remote-federation]
59
+ address = "<SUPERLINK-ADDRESS>:<PORT>"
60
+ insecure = true # Remove this line to enable TLS
61
+ # root-certificates = "<PATH/TO/ca.crt>" # For TLS setup
flwr/cli/pull.py ADDED
@@ -0,0 +1,100 @@
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 `pull` command."""
16
+
17
+
18
+ from pathlib import Path
19
+ from typing import Annotated, Optional
20
+
21
+ import typer
22
+
23
+ from flwr.cli.config_utils import (
24
+ exit_if_no_address,
25
+ load_and_validate,
26
+ process_loaded_project_config,
27
+ validate_federation_in_project_config,
28
+ )
29
+ from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
30
+ from flwr.common.constant import FAB_CONFIG_FILE
31
+ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
32
+ PullArtifactsRequest,
33
+ PullArtifactsResponse,
34
+ )
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 pull( # pylint: disable=R0914
41
+ run_id: Annotated[
42
+ int,
43
+ typer.Option(
44
+ "--run-id",
45
+ help="Run ID to pull artifacts from.",
46
+ ),
47
+ ],
48
+ app: Annotated[
49
+ Path,
50
+ typer.Argument(help="Path of the Flower App to run."),
51
+ ] = Path("."),
52
+ federation: Annotated[
53
+ Optional[str],
54
+ typer.Argument(help="Name of the federation."),
55
+ ] = None,
56
+ federation_config_overrides: Annotated[
57
+ Optional[list[str]],
58
+ typer.Option(
59
+ "--federation-config",
60
+ help=FEDERATION_CONFIG_HELP_MESSAGE,
61
+ ),
62
+ ] = None,
63
+ ) -> None:
64
+ """Pull artifacts from a Flower run."""
65
+ typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
66
+
67
+ pyproject_path = app / FAB_CONFIG_FILE if app else None
68
+ config, errors, warnings = load_and_validate(path=pyproject_path)
69
+ config = process_loaded_project_config(config, errors, warnings)
70
+ federation, federation_config = validate_federation_in_project_config(
71
+ federation, config, federation_config_overrides
72
+ )
73
+ exit_if_no_address(federation_config, "pull")
74
+ channel = None
75
+ try:
76
+
77
+ auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
78
+ channel = init_channel(app, federation_config, auth_plugin)
79
+ stub = ControlStub(channel)
80
+ with flwr_cli_grpc_exc_handler():
81
+ res: PullArtifactsResponse = stub.PullArtifacts(
82
+ PullArtifactsRequest(run_id=run_id)
83
+ )
84
+
85
+ if not res.url:
86
+ typer.secho(
87
+ f"❌ A download URL for artifacts from run {run_id} couldn't be "
88
+ "obtained.",
89
+ fg=typer.colors.RED,
90
+ bold=True,
91
+ )
92
+ raise typer.Exit(code=1)
93
+
94
+ typer.secho(
95
+ f"✅ Artifacts for run {run_id} can be downloaded from: {res.url}",
96
+ fg=typer.colors.GREEN,
97
+ )
98
+ finally:
99
+ if channel:
100
+ channel.close()
flwr/cli/run/run.py CHANGED
@@ -15,16 +15,18 @@
15
15
  """Flower command line interface `run` command."""
16
16
 
17
17
 
18
+ import hashlib
18
19
  import io
19
20
  import json
20
21
  import subprocess
21
22
  from pathlib import Path
22
- from typing import Annotated, Any, Optional
23
+ from typing import Annotated, Any, Optional, cast
23
24
 
24
25
  import typer
25
26
  from rich.console import Console
26
27
 
27
- from flwr.cli.build import build_fab, get_fab_filename
28
+ from flwr.cli.build import build_fab_from_disk, get_fab_filename
29
+ from flwr.cli.config_utils import load as load_toml
28
30
  from flwr.cli.config_utils import (
29
31
  load_and_validate,
30
32
  process_loaded_project_config,
@@ -37,7 +39,7 @@ from flwr.common.config import (
37
39
  parse_config_args,
38
40
  user_config_to_configrecord,
39
41
  )
40
- from flwr.common.constant import CliOutputFormat
42
+ from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
41
43
  from flwr.common.logger import print_json_error, redirect_output, restore_output
42
44
  from flwr.common.serde import config_record_to_proto, fab_to_proto, user_config_to_proto
43
45
  from flwr.common.typing import Fab
@@ -45,7 +47,7 @@ from flwr.proto.control_pb2 import StartRunRequest # pylint: disable=E0611
45
47
  from flwr.proto.control_pb2_grpc import ControlStub
46
48
 
47
49
  from ..log import start_stream
48
- from ..utils import flwr_cli_grpc_exc_handler, init_channel, try_obtain_cli_auth_plugin
50
+ from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
49
51
 
50
52
  CONN_REFRESH_PERIOD = 60 # Connection refresh period for log streaming (seconds)
51
53
 
@@ -148,14 +150,16 @@ def _run_with_control_api(
148
150
  ) -> None:
149
151
  channel = None
150
152
  try:
151
- auth_plugin = try_obtain_cli_auth_plugin(app, federation, federation_config)
153
+ auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
152
154
  channel = init_channel(app, federation_config, auth_plugin)
153
155
  stub = ControlStub(channel)
154
156
 
155
- fab_bytes, fab_hash, config = build_fab(app)
157
+ fab_bytes = build_fab_from_disk(app)
158
+ fab_hash = hashlib.sha256(fab_bytes).hexdigest()
159
+ config = cast(dict[str, Any], load_toml(app / FAB_CONFIG_FILE))
156
160
  fab_id, fab_version = get_metadata_from_config(config)
157
161
 
158
- fab = Fab(fab_hash, fab_bytes)
162
+ fab = Fab(fab_hash, fab_bytes, {})
159
163
 
160
164
  # Construct a `ConfigRecord` out of a flattened `UserConfig`
161
165
  fed_config = flatten_dict(federation_config.get("options", {}))
flwr/cli/stop.py CHANGED
@@ -38,7 +38,7 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
38
38
  )
39
39
  from flwr.proto.control_pb2_grpc import ControlStub
40
40
 
41
- from .utils import flwr_cli_grpc_exc_handler, init_channel, try_obtain_cli_auth_plugin
41
+ from .utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
42
42
 
43
43
 
44
44
  def stop( # pylint: disable=R0914
@@ -89,7 +89,7 @@ def stop( # pylint: disable=R0914
89
89
  exit_if_no_address(federation_config, "stop")
90
90
  channel = None
91
91
  try:
92
- auth_plugin = try_obtain_cli_auth_plugin(app, federation, federation_config)
92
+ auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
93
93
  channel = init_channel(app, federation_config, auth_plugin)
94
94
  stub = ControlStub(channel) # pylint: disable=unused-variable # noqa: F841
95
95
 
@@ -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,260 @@
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, Optional, 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_FLWR_AID, 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]
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
+ Optional[str],
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(path=pyproject_path)
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: Optional[str]) -> 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.status,
164
+ _format_datetime(node.registered_at),
165
+ _format_datetime(node.last_activated_at),
166
+ _format_datetime(node.last_deactivated_at),
167
+ _format_datetime(node.unregistered_at),
168
+ format_timedelta(elapsed_time_activated),
169
+ )
170
+ )
171
+
172
+ return formatted_nodes
173
+
174
+
175
+ def _to_table(nodes_info: list[_NodeListType], verbose: bool) -> Table:
176
+ """Format the provided node list to a rich Table."""
177
+ table = Table(header_style="bold cyan", show_lines=True)
178
+
179
+ # Add columns
180
+ table.add_column(
181
+ Text("Node ID", justify="center"), style="bright_black", no_wrap=True
182
+ )
183
+ table.add_column(Text("Owner", justify="center"))
184
+ table.add_column(Text("Status", justify="center"))
185
+ table.add_column(Text("Elapsed", justify="center"))
186
+ table.add_column(Text("Status Changed @", justify="center"), style="bright_black")
187
+
188
+ for row in nodes_info:
189
+ (
190
+ node_id,
191
+ owner_aid,
192
+ status,
193
+ _,
194
+ last_activated_at,
195
+ last_deactivated_at,
196
+ unregistered_at,
197
+ elapse_activated,
198
+ ) = row
199
+
200
+ if status == "online":
201
+ status_style = "green"
202
+ time_at = last_activated_at
203
+ elif status == "offline":
204
+ status_style = "bright_yellow"
205
+ time_at = last_deactivated_at
206
+ elif status == "unregistered":
207
+ if not verbose:
208
+ continue
209
+ status_style = "red"
210
+ time_at = unregistered_at
211
+ elif status == "registered":
212
+ status_style = "blue"
213
+ time_at = "N/A"
214
+ else:
215
+ raise ValueError(f"Unexpected node status '{status}'")
216
+
217
+ formatted_row = (
218
+ f"[bold]{node_id}[/bold]",
219
+ f"{owner_aid}" if owner_aid != NOOP_FLWR_AID else f"[dim]{owner_aid}[/dim]",
220
+ f"[{status_style}]{status}",
221
+ f"[cyan]{elapse_activated}[/cyan]" if status == "online" else "",
222
+ time_at,
223
+ )
224
+ table.add_row(*formatted_row)
225
+
226
+ return table
227
+
228
+
229
+ def _to_json(nodes_info: list[_NodeListType], verbose: bool) -> str:
230
+ """Format node list to a JSON formatted string."""
231
+ nodes_list = []
232
+ for row in nodes_info:
233
+ (
234
+ node_id,
235
+ owner_aid,
236
+ status,
237
+ created_at,
238
+ activated_at,
239
+ deactivated_at,
240
+ deleted_at,
241
+ elapse_activated,
242
+ ) = row
243
+
244
+ if status == "deleted" and not verbose:
245
+ continue
246
+
247
+ nodes_list.append(
248
+ {
249
+ "node-id": node_id,
250
+ "owner-aid": owner_aid,
251
+ "status": status,
252
+ "created-at": created_at,
253
+ "online-at": activated_at,
254
+ "online-elapsed": elapse_activated,
255
+ "offline-at": deactivated_at,
256
+ "deleted-at": deleted_at,
257
+ }
258
+ )
259
+
260
+ return json.dumps({"success": True, "nodes": nodes_list})