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.
- flwr/__init__.py +16 -5
- flwr/app/error.py +2 -2
- flwr/app/exception.py +3 -3
- flwr/cli/app.py +34 -1
- flwr/cli/app_cmd/__init__.py +23 -0
- flwr/cli/app_cmd/publish.py +285 -0
- flwr/cli/app_cmd/review.py +252 -0
- flwr/cli/auth_plugin/__init__.py +15 -6
- flwr/cli/auth_plugin/auth_plugin.py +94 -0
- flwr/cli/auth_plugin/noop_auth_plugin.py +101 -0
- flwr/cli/auth_plugin/oidc_cli_plugin.py +46 -32
- flwr/cli/build.py +166 -53
- flwr/cli/{cli_user_auth_interceptor.py → cli_account_auth_interceptor.py} +29 -11
- flwr/cli/config_utils.py +101 -13
- flwr/cli/federation/__init__.py +24 -0
- flwr/cli/federation/ls.py +140 -0
- flwr/cli/federation/show.py +317 -0
- flwr/cli/install.py +91 -13
- flwr/cli/log.py +54 -11
- flwr/cli/login/login.py +41 -27
- flwr/cli/ls.py +177 -133
- flwr/cli/new/new.py +175 -40
- flwr/cli/new/templates/app/code/task.pytorch.py.tpl +1 -0
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
- flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +2 -2
- flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +1 -1
- flwr/cli/pull.py +12 -7
- flwr/cli/run/run.py +82 -31
- flwr/cli/run_utils.py +130 -0
- flwr/cli/stop.py +27 -9
- flwr/cli/supernode/__init__.py +25 -0
- flwr/cli/supernode/ls.py +268 -0
- flwr/cli/supernode/register.py +190 -0
- flwr/cli/supernode/unregister.py +140 -0
- flwr/cli/utils.py +464 -81
- flwr/client/__init__.py +2 -1
- flwr/client/dpfedavg_numpy_client.py +4 -1
- flwr/client/grpc_adapter_client/connection.py +12 -15
- flwr/client/grpc_rere_client/connection.py +68 -41
- flwr/client/grpc_rere_client/grpc_adapter.py +34 -14
- flwr/client/grpc_rere_client/{client_interceptor.py → node_auth_client_interceptor.py} +5 -7
- flwr/client/message_handler/message_handler.py +2 -2
- flwr/client/mod/secure_aggregation/secaggplus_mod.py +10 -8
- flwr/client/numpy_client.py +1 -1
- flwr/client/rest_client/connection.py +94 -51
- flwr/client/run_info_store.py +4 -5
- flwr/client/typing.py +1 -1
- flwr/clientapp/__init__.py +1 -2
- flwr/{client → clientapp}/client_app.py +9 -10
- flwr/clientapp/mod/centraldp_mods.py +16 -17
- flwr/clientapp/mod/localdp_mod.py +8 -9
- flwr/clientapp/typing.py +1 -1
- flwr/{client/clientapp → clientapp}/utils.py +4 -4
- flwr/common/address.py +1 -2
- flwr/common/args.py +3 -4
- flwr/common/config.py +13 -16
- flwr/common/constant.py +56 -13
- flwr/common/differential_privacy.py +3 -4
- flwr/common/event_log_plugin/event_log_plugin.py +3 -4
- flwr/common/exit/exit.py +15 -2
- flwr/common/exit/exit_code.py +39 -10
- flwr/common/exit/exit_handler.py +6 -2
- flwr/common/exit/signal_handler.py +5 -5
- flwr/common/grpc.py +6 -6
- flwr/common/inflatable_protobuf_utils.py +1 -1
- flwr/common/inflatable_utils.py +48 -31
- flwr/common/logger.py +19 -19
- flwr/common/message.py +4 -4
- flwr/common/object_ref.py +7 -7
- flwr/common/record/array.py +6 -6
- flwr/common/record/arrayrecord.py +18 -21
- flwr/common/record/configrecord.py +3 -3
- flwr/common/record/recorddict.py +5 -5
- flwr/common/record/typeddict.py +9 -2
- flwr/common/recorddict_compat.py +7 -10
- flwr/common/retry_invoker.py +20 -20
- flwr/common/secure_aggregation/crypto/symmetric_encryption.py +1 -89
- flwr/common/secure_aggregation/ndarrays_arithmetic.py +3 -3
- flwr/common/serde.py +9 -6
- flwr/common/serde_utils.py +2 -2
- flwr/common/telemetry.py +9 -5
- flwr/common/typing.py +59 -43
- flwr/compat/client/app.py +39 -38
- flwr/compat/client/grpc_client/connection.py +13 -13
- flwr/compat/server/app.py +5 -6
- flwr/proto/appio_pb2.py +13 -3
- flwr/proto/appio_pb2.pyi +134 -65
- flwr/proto/appio_pb2_grpc.py +20 -0
- flwr/proto/appio_pb2_grpc.pyi +27 -0
- flwr/proto/clientappio_pb2.py +17 -7
- flwr/proto/clientappio_pb2.pyi +15 -0
- flwr/proto/clientappio_pb2_grpc.py +206 -40
- flwr/proto/clientappio_pb2_grpc.pyi +168 -53
- flwr/proto/control_pb2.py +72 -40
- flwr/proto/control_pb2.pyi +319 -87
- flwr/proto/control_pb2_grpc.py +339 -28
- flwr/proto/control_pb2_grpc.pyi +209 -37
- flwr/proto/error_pb2.py +13 -3
- flwr/proto/error_pb2.pyi +24 -6
- flwr/proto/error_pb2_grpc.py +20 -0
- flwr/proto/error_pb2_grpc.pyi +27 -0
- flwr/proto/fab_pb2.py +24 -10
- flwr/proto/fab_pb2.pyi +68 -20
- flwr/proto/fab_pb2_grpc.py +20 -0
- flwr/proto/fab_pb2_grpc.pyi +27 -0
- flwr/proto/federation_pb2.py +38 -0
- flwr/proto/federation_pb2.pyi +56 -0
- flwr/proto/federation_pb2_grpc.py +24 -0
- flwr/proto/federation_pb2_grpc.pyi +31 -0
- flwr/proto/fleet_pb2.py +45 -27
- flwr/proto/fleet_pb2.pyi +186 -70
- flwr/proto/fleet_pb2_grpc.py +277 -66
- flwr/proto/fleet_pb2_grpc.pyi +201 -55
- flwr/proto/grpcadapter_pb2.py +14 -4
- flwr/proto/grpcadapter_pb2.pyi +38 -16
- flwr/proto/grpcadapter_pb2_grpc.py +35 -4
- flwr/proto/grpcadapter_pb2_grpc.pyi +38 -7
- flwr/proto/heartbeat_pb2.py +17 -7
- flwr/proto/heartbeat_pb2.pyi +51 -22
- flwr/proto/heartbeat_pb2_grpc.py +20 -0
- flwr/proto/heartbeat_pb2_grpc.pyi +27 -0
- flwr/proto/log_pb2.py +13 -3
- flwr/proto/log_pb2.pyi +34 -11
- flwr/proto/log_pb2_grpc.py +20 -0
- flwr/proto/log_pb2_grpc.pyi +27 -0
- flwr/proto/message_pb2.py +15 -5
- flwr/proto/message_pb2.pyi +154 -86
- flwr/proto/message_pb2_grpc.py +20 -0
- flwr/proto/message_pb2_grpc.pyi +27 -0
- flwr/proto/node_pb2.py +16 -4
- flwr/proto/node_pb2.pyi +77 -4
- flwr/proto/node_pb2_grpc.py +20 -0
- flwr/proto/node_pb2_grpc.pyi +27 -0
- flwr/proto/recorddict_pb2.py +13 -3
- flwr/proto/recorddict_pb2.pyi +184 -107
- flwr/proto/recorddict_pb2_grpc.py +20 -0
- flwr/proto/recorddict_pb2_grpc.pyi +27 -0
- flwr/proto/run_pb2.py +40 -31
- flwr/proto/run_pb2.pyi +149 -84
- flwr/proto/run_pb2_grpc.py +20 -0
- flwr/proto/run_pb2_grpc.pyi +27 -0
- flwr/proto/serverappio_pb2.py +13 -3
- flwr/proto/serverappio_pb2.pyi +32 -8
- flwr/proto/serverappio_pb2_grpc.py +246 -65
- flwr/proto/serverappio_pb2_grpc.pyi +221 -85
- flwr/proto/simulationio_pb2.py +16 -8
- flwr/proto/simulationio_pb2.pyi +15 -0
- flwr/proto/simulationio_pb2_grpc.py +162 -41
- flwr/proto/simulationio_pb2_grpc.pyi +149 -55
- flwr/proto/transport_pb2.py +20 -10
- flwr/proto/transport_pb2.pyi +249 -160
- flwr/proto/transport_pb2_grpc.py +35 -4
- flwr/proto/transport_pb2_grpc.pyi +38 -8
- flwr/server/app.py +173 -127
- flwr/server/client_manager.py +4 -5
- flwr/server/client_proxy.py +10 -11
- flwr/server/compat/app.py +4 -5
- flwr/server/compat/app_utils.py +2 -1
- flwr/server/compat/grid_client_proxy.py +10 -12
- flwr/server/compat/legacy_context.py +3 -4
- flwr/server/fleet_event_log_interceptor.py +2 -1
- flwr/server/grid/grid.py +2 -3
- flwr/server/grid/grpc_grid.py +10 -8
- flwr/server/grid/inmemory_grid.py +4 -4
- flwr/server/run_serverapp.py +2 -3
- flwr/server/server.py +34 -39
- flwr/server/server_app.py +7 -8
- flwr/server/server_config.py +1 -2
- flwr/server/serverapp/app.py +34 -28
- flwr/server/serverapp_components.py +4 -5
- flwr/server/strategy/aggregate.py +9 -8
- flwr/server/strategy/bulyan.py +13 -11
- flwr/server/strategy/dp_adaptive_clipping.py +16 -20
- flwr/server/strategy/dp_fixed_clipping.py +12 -17
- flwr/server/strategy/dpfedavg_adaptive.py +3 -4
- flwr/server/strategy/dpfedavg_fixed.py +6 -10
- flwr/server/strategy/fault_tolerant_fedavg.py +14 -13
- flwr/server/strategy/fedadagrad.py +18 -14
- flwr/server/strategy/fedadam.py +16 -14
- flwr/server/strategy/fedavg.py +16 -17
- flwr/server/strategy/fedavg_android.py +15 -15
- flwr/server/strategy/fedavgm.py +21 -18
- flwr/server/strategy/fedmedian.py +2 -3
- flwr/server/strategy/fedopt.py +11 -10
- flwr/server/strategy/fedprox.py +10 -9
- flwr/server/strategy/fedtrimmedavg.py +12 -11
- flwr/server/strategy/fedxgb_bagging.py +13 -11
- flwr/server/strategy/fedxgb_cyclic.py +6 -6
- flwr/server/strategy/fedxgb_nn_avg.py +4 -4
- flwr/server/strategy/fedyogi.py +16 -14
- flwr/server/strategy/krum.py +12 -11
- flwr/server/strategy/qfedavg.py +16 -15
- flwr/server/strategy/strategy.py +6 -9
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +19 -8
- flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -2
- flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +3 -4
- flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +10 -12
- flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +1 -3
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +136 -42
- flwr/server/superlink/fleet/grpc_rere/{server_interceptor.py → node_auth_server_interceptor.py} +28 -51
- flwr/server/superlink/fleet/message_handler/message_handler.py +100 -49
- flwr/server/superlink/fleet/rest_rere/rest_api.py +54 -33
- flwr/server/superlink/fleet/vce/backend/backend.py +2 -2
- flwr/server/superlink/fleet/vce/backend/raybackend.py +6 -6
- flwr/server/superlink/fleet/vce/vce_api.py +32 -13
- flwr/server/superlink/linkstate/in_memory_linkstate.py +266 -207
- flwr/server/superlink/linkstate/linkstate.py +161 -62
- flwr/server/superlink/linkstate/linkstate_factory.py +24 -6
- flwr/server/superlink/linkstate/sqlite_linkstate.py +698 -638
- flwr/server/superlink/linkstate/utils.py +9 -60
- flwr/server/superlink/serverappio/serverappio_grpc.py +1 -2
- flwr/server/superlink/serverappio/serverappio_servicer.py +28 -23
- flwr/server/superlink/simulation/simulationio_grpc.py +1 -2
- flwr/server/superlink/simulation/simulationio_servicer.py +19 -14
- flwr/server/superlink/utils.py +4 -6
- flwr/server/typing.py +1 -1
- flwr/server/utils/tensorboard.py +15 -8
- flwr/server/utils/validator.py +2 -3
- flwr/server/workflow/default_workflows.py +5 -5
- flwr/server/workflow/secure_aggregation/secagg_workflow.py +2 -4
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +12 -10
- flwr/serverapp/strategy/bulyan.py +16 -15
- flwr/serverapp/strategy/dp_adaptive_clipping.py +12 -11
- flwr/serverapp/strategy/dp_fixed_clipping.py +11 -14
- flwr/serverapp/strategy/fedadagrad.py +10 -11
- flwr/serverapp/strategy/fedadam.py +10 -11
- flwr/serverapp/strategy/fedavg.py +9 -10
- flwr/serverapp/strategy/fedavgm.py +17 -16
- flwr/serverapp/strategy/fedmedian.py +2 -2
- flwr/serverapp/strategy/fedopt.py +10 -11
- flwr/serverapp/strategy/fedprox.py +7 -8
- flwr/serverapp/strategy/fedtrimmedavg.py +9 -9
- flwr/serverapp/strategy/fedxgb_bagging.py +3 -3
- flwr/serverapp/strategy/fedxgb_cyclic.py +9 -9
- flwr/serverapp/strategy/fedyogi.py +9 -11
- flwr/serverapp/strategy/krum.py +7 -7
- flwr/serverapp/strategy/multikrum.py +9 -9
- flwr/serverapp/strategy/qfedavg.py +17 -16
- flwr/serverapp/strategy/strategy.py +6 -9
- flwr/serverapp/strategy/strategy_utils.py +7 -8
- flwr/simulation/app.py +46 -42
- flwr/simulation/legacy_app.py +12 -12
- flwr/simulation/ray_transport/ray_actor.py +11 -12
- flwr/simulation/ray_transport/ray_client_proxy.py +12 -13
- flwr/simulation/run_simulation.py +44 -43
- flwr/simulation/simulationio_connection.py +4 -4
- flwr/supercore/cli/flower_superexec.py +3 -4
- flwr/supercore/constant.py +52 -0
- flwr/supercore/corestate/corestate.py +24 -3
- flwr/supercore/corestate/in_memory_corestate.py +138 -0
- flwr/supercore/corestate/sqlite_corestate.py +157 -0
- flwr/supercore/ffs/disk_ffs.py +1 -2
- flwr/supercore/ffs/ffs.py +1 -2
- flwr/supercore/ffs/ffs_factory.py +1 -2
- flwr/{common → supercore}/heartbeat.py +20 -25
- flwr/supercore/object_store/in_memory_object_store.py +1 -6
- flwr/supercore/object_store/object_store.py +1 -2
- flwr/supercore/object_store/object_store_factory.py +27 -8
- flwr/supercore/object_store/sqlite_object_store.py +253 -0
- flwr/{client/clientapp → supercore/primitives}/__init__.py +1 -1
- flwr/supercore/primitives/asymmetric.py +117 -0
- flwr/supercore/primitives/asymmetric_ed25519.py +175 -0
- flwr/supercore/sqlite_mixin.py +159 -0
- flwr/supercore/superexec/plugin/base_exec_plugin.py +1 -2
- flwr/supercore/superexec/plugin/exec_plugin.py +3 -3
- flwr/supercore/superexec/run_superexec.py +9 -13
- flwr/supercore/utils.py +20 -0
- flwr/superlink/artifact_provider/artifact_provider.py +1 -2
- flwr/{common → superlink}/auth_plugin/__init__.py +6 -6
- flwr/superlink/auth_plugin/auth_plugin.py +88 -0
- flwr/superlink/auth_plugin/noop_auth_plugin.py +84 -0
- flwr/superlink/federation/__init__.py +24 -0
- flwr/superlink/federation/federation_manager.py +64 -0
- flwr/superlink/federation/noop_federation_manager.py +71 -0
- flwr/superlink/servicer/control/{control_user_auth_interceptor.py → control_account_auth_interceptor.py} +41 -32
- flwr/superlink/servicer/control/control_event_log_interceptor.py +7 -7
- flwr/superlink/servicer/control/control_grpc.py +18 -17
- flwr/superlink/servicer/control/control_license_interceptor.py +3 -3
- flwr/superlink/servicer/control/control_servicer.py +239 -63
- flwr/supernode/cli/flower_supernode.py +74 -26
- flwr/supernode/nodestate/in_memory_nodestate.py +60 -49
- flwr/supernode/nodestate/nodestate.py +7 -8
- flwr/supernode/nodestate/nodestate_factory.py +7 -4
- flwr/supernode/runtime/run_clientapp.py +43 -24
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +40 -10
- flwr/supernode/start_client_internal.py +175 -51
- {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/METADATA +8 -8
- flwr-1.24.0.dist-info/RECORD +454 -0
- flwr/common/auth_plugin/auth_plugin.py +0 -149
- flwr/supercore/object_store/utils.py +0 -43
- flwr-1.22.0.dist-info/RECORD +0 -428
- {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/WHEEL +0 -0
- {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
|
+
]
|
flwr/cli/supernode/ls.py
ADDED
|
@@ -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)
|