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.
- flwr/cli/app.py +17 -1
- flwr/cli/auth_plugin/__init__.py +15 -6
- flwr/cli/auth_plugin/auth_plugin.py +95 -0
- flwr/cli/auth_plugin/noop_auth_plugin.py +58 -0
- flwr/cli/auth_plugin/oidc_cli_plugin.py +16 -25
- flwr/cli/build.py +118 -47
- flwr/cli/{cli_user_auth_interceptor.py → cli_account_auth_interceptor.py} +6 -5
- flwr/cli/log.py +2 -2
- flwr/cli/login/login.py +34 -23
- flwr/cli/ls.py +13 -9
- flwr/cli/new/new.py +196 -42
- flwr/cli/new/templates/app/README.flowertune.md.tpl +1 -1
- flwr/cli/new/templates/app/code/client.baseline.py.tpl +64 -47
- flwr/cli/new/templates/app/code/client.huggingface.py.tpl +68 -30
- flwr/cli/new/templates/app/code/client.jax.py.tpl +63 -42
- flwr/cli/new/templates/app/code/client.mlx.py.tpl +80 -51
- flwr/cli/new/templates/app/code/client.numpy.py.tpl +36 -13
- flwr/cli/new/templates/app/code/client.pytorch.py.tpl +71 -46
- flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +55 -0
- flwr/cli/new/templates/app/code/client.sklearn.py.tpl +75 -30
- flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +69 -44
- flwr/cli/new/templates/app/code/client.xgboost.py.tpl +110 -0
- flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +56 -90
- flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +1 -23
- flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +37 -58
- flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +39 -44
- flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -14
- flwr/cli/new/templates/app/code/server.baseline.py.tpl +27 -29
- flwr/cli/new/templates/app/code/server.huggingface.py.tpl +23 -19
- flwr/cli/new/templates/app/code/server.jax.py.tpl +27 -14
- flwr/cli/new/templates/app/code/server.mlx.py.tpl +29 -19
- flwr/cli/new/templates/app/code/server.numpy.py.tpl +30 -17
- flwr/cli/new/templates/app/code/server.pytorch.py.tpl +36 -26
- flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +31 -0
- flwr/cli/new/templates/app/code/server.sklearn.py.tpl +29 -21
- flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +28 -19
- flwr/cli/new/templates/app/code/server.xgboost.py.tpl +56 -0
- flwr/cli/new/templates/app/code/task.huggingface.py.tpl +16 -20
- flwr/cli/new/templates/app/code/task.jax.py.tpl +1 -1
- flwr/cli/new/templates/app/code/task.numpy.py.tpl +1 -1
- flwr/cli/new/templates/app/code/task.pytorch.py.tpl +14 -27
- flwr/cli/new/templates/app/code/{task.pytorch_msg_api.py.tpl → task.pytorch_legacy_api.py.tpl} +27 -14
- flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +1 -2
- flwr/cli/new/templates/app/code/task.xgboost.py.tpl +67 -0
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +4 -4
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +2 -2
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +4 -4
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +2 -2
- 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_msg_api.toml.tpl → pyproject.pytorch_legacy_api.toml.tpl} +3 -3
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +61 -0
- flwr/cli/pull.py +100 -0
- flwr/cli/run/run.py +11 -7
- flwr/cli/stop.py +2 -2
- flwr/cli/supernode/__init__.py +25 -0
- flwr/cli/supernode/ls.py +260 -0
- flwr/cli/supernode/register.py +185 -0
- flwr/cli/supernode/unregister.py +138 -0
- flwr/cli/utils.py +109 -69
- flwr/client/__init__.py +2 -1
- flwr/client/grpc_adapter_client/connection.py +6 -8
- flwr/client/grpc_rere_client/connection.py +59 -31
- flwr/client/grpc_rere_client/grpc_adapter.py +28 -12
- flwr/client/grpc_rere_client/{client_interceptor.py → node_auth_client_interceptor.py} +3 -6
- flwr/client/mod/secure_aggregation/secaggplus_mod.py +7 -5
- flwr/client/rest_client/connection.py +82 -37
- flwr/clientapp/__init__.py +1 -2
- flwr/clientapp/mod/__init__.py +4 -1
- flwr/clientapp/mod/centraldp_mods.py +156 -40
- flwr/clientapp/mod/localdp_mod.py +169 -0
- flwr/clientapp/typing.py +22 -0
- flwr/{client/clientapp → clientapp}/utils.py +1 -1
- flwr/common/constant.py +56 -13
- flwr/common/exit/exit_code.py +24 -10
- flwr/common/inflatable_utils.py +10 -10
- flwr/common/record/array.py +3 -3
- flwr/common/record/arrayrecord.py +10 -1
- flwr/common/record/typeddict.py +12 -0
- flwr/common/secure_aggregation/crypto/symmetric_encryption.py +1 -89
- flwr/common/serde.py +4 -2
- flwr/common/typing.py +7 -6
- flwr/compat/client/app.py +1 -1
- flwr/compat/client/grpc_client/connection.py +2 -2
- flwr/proto/control_pb2.py +48 -31
- flwr/proto/control_pb2.pyi +95 -5
- flwr/proto/control_pb2_grpc.py +136 -0
- flwr/proto/control_pb2_grpc.pyi +52 -0
- flwr/proto/fab_pb2.py +11 -7
- flwr/proto/fab_pb2.pyi +21 -1
- flwr/proto/fleet_pb2.py +31 -23
- flwr/proto/fleet_pb2.pyi +63 -23
- flwr/proto/fleet_pb2_grpc.py +98 -28
- flwr/proto/fleet_pb2_grpc.pyi +45 -13
- flwr/proto/node_pb2.py +3 -1
- flwr/proto/node_pb2.pyi +48 -0
- flwr/server/app.py +152 -114
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +17 -7
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +132 -38
- flwr/server/superlink/fleet/grpc_rere/{server_interceptor.py → node_auth_server_interceptor.py} +27 -51
- flwr/server/superlink/fleet/message_handler/message_handler.py +67 -22
- flwr/server/superlink/fleet/rest_rere/rest_api.py +52 -31
- flwr/server/superlink/fleet/vce/backend/backend.py +1 -1
- flwr/server/superlink/fleet/vce/backend/raybackend.py +1 -1
- flwr/server/superlink/fleet/vce/vce_api.py +18 -5
- flwr/server/superlink/linkstate/in_memory_linkstate.py +167 -73
- flwr/server/superlink/linkstate/linkstate.py +107 -24
- flwr/server/superlink/linkstate/linkstate_factory.py +2 -1
- flwr/server/superlink/linkstate/sqlite_linkstate.py +306 -255
- flwr/server/superlink/linkstate/utils.py +3 -54
- flwr/server/superlink/serverappio/serverappio_servicer.py +2 -2
- flwr/server/superlink/simulation/simulationio_servicer.py +1 -1
- flwr/server/utils/validator.py +2 -3
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +4 -2
- flwr/serverapp/strategy/__init__.py +26 -0
- flwr/serverapp/strategy/bulyan.py +238 -0
- flwr/serverapp/strategy/dp_adaptive_clipping.py +335 -0
- flwr/serverapp/strategy/dp_fixed_clipping.py +71 -49
- flwr/serverapp/strategy/fedadagrad.py +0 -3
- flwr/serverapp/strategy/fedadam.py +0 -3
- flwr/serverapp/strategy/fedavg.py +89 -64
- flwr/serverapp/strategy/fedavgm.py +198 -0
- flwr/serverapp/strategy/fedmedian.py +105 -0
- flwr/serverapp/strategy/fedprox.py +174 -0
- flwr/serverapp/strategy/fedtrimmedavg.py +176 -0
- flwr/serverapp/strategy/fedxgb_bagging.py +117 -0
- flwr/serverapp/strategy/fedxgb_cyclic.py +220 -0
- flwr/serverapp/strategy/fedyogi.py +0 -3
- flwr/serverapp/strategy/krum.py +112 -0
- flwr/serverapp/strategy/multikrum.py +247 -0
- flwr/serverapp/strategy/qfedavg.py +252 -0
- flwr/serverapp/strategy/strategy_utils.py +48 -0
- flwr/simulation/app.py +1 -1
- flwr/simulation/ray_transport/ray_actor.py +1 -1
- flwr/simulation/ray_transport/ray_client_proxy.py +1 -1
- flwr/simulation/run_simulation.py +28 -32
- flwr/supercore/cli/flower_superexec.py +26 -1
- flwr/supercore/constant.py +41 -0
- flwr/supercore/object_store/in_memory_object_store.py +0 -4
- flwr/supercore/object_store/object_store_factory.py +26 -6
- flwr/supercore/object_store/sqlite_object_store.py +252 -0
- flwr/{client/clientapp → supercore/primitives}/__init__.py +1 -1
- flwr/supercore/primitives/asymmetric.py +117 -0
- flwr/supercore/primitives/asymmetric_ed25519.py +165 -0
- flwr/supercore/sqlite_mixin.py +156 -0
- flwr/supercore/superexec/plugin/exec_plugin.py +11 -1
- flwr/supercore/superexec/run_superexec.py +16 -2
- flwr/supercore/utils.py +20 -0
- flwr/superlink/artifact_provider/__init__.py +22 -0
- flwr/superlink/artifact_provider/artifact_provider.py +37 -0
- flwr/{common → superlink}/auth_plugin/__init__.py +6 -6
- flwr/superlink/auth_plugin/auth_plugin.py +91 -0
- flwr/superlink/auth_plugin/noop_auth_plugin.py +87 -0
- flwr/superlink/servicer/control/{control_user_auth_interceptor.py → control_account_auth_interceptor.py} +19 -19
- flwr/superlink/servicer/control/control_event_log_interceptor.py +1 -1
- flwr/superlink/servicer/control/control_grpc.py +16 -11
- flwr/superlink/servicer/control/control_servicer.py +207 -58
- flwr/supernode/cli/flower_supernode.py +19 -26
- flwr/supernode/runtime/run_clientapp.py +2 -2
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +1 -1
- flwr/supernode/start_client_internal.py +17 -9
- {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/METADATA +6 -16
- {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/RECORD +170 -140
- flwr/cli/new/templates/app/code/client.pytorch_msg_api.py.tpl +0 -80
- flwr/cli/new/templates/app/code/server.pytorch_msg_api.py.tpl +0 -41
- flwr/common/auth_plugin/auth_plugin.py +0 -149
- flwr/serverapp/dp_fixed_clipping.py +0 -352
- flwr/serverapp/strategy/strategy_utils_tests.py +0 -304
- /flwr/cli/new/templates/app/code/{__init__.pytorch_msg_api.py.tpl → __init__.pytorch_legacy_api.py.tpl} +0 -0
- /flwr/{client → clientapp}/client_app.py +0 -0
- {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/WHEEL +0 -0
- {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
|
|
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,
|
|
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 =
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
+
]
|
flwr/cli/supernode/ls.py
ADDED
|
@@ -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})
|