flwr 1.25.0__py3-none-any.whl → 1.26.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 +1 -1
- flwr/app/__init__.py +4 -1
- flwr/app/message_type.py +29 -0
- flwr/app/metadata.py +5 -2
- flwr/app/user_config.py +19 -0
- flwr/cli/app.py +37 -19
- flwr/cli/app_cmd/publish.py +25 -75
- flwr/cli/app_cmd/review.py +18 -69
- flwr/cli/auth_plugin/auth_plugin.py +5 -10
- flwr/cli/auth_plugin/noop_auth_plugin.py +1 -2
- flwr/cli/auth_plugin/oidc_cli_plugin.py +38 -38
- flwr/cli/build.py +15 -28
- flwr/cli/config/__init__.py +21 -0
- flwr/cli/config/ls.py +71 -0
- flwr/cli/config_migration.py +297 -0
- flwr/cli/config_utils.py +63 -156
- flwr/cli/constant.py +71 -0
- flwr/cli/federation/__init__.py +0 -2
- flwr/cli/federation/ls.py +256 -64
- flwr/cli/flower_config.py +429 -0
- flwr/cli/install.py +23 -62
- flwr/cli/log.py +23 -37
- flwr/cli/login/login.py +29 -63
- flwr/cli/ls.py +28 -58
- flwr/cli/new/new.py +9 -29
- flwr/cli/pull.py +19 -37
- flwr/cli/run/run.py +85 -93
- flwr/cli/run_utils.py +1 -1
- flwr/cli/stop.py +32 -73
- flwr/cli/supernode/ls.py +25 -57
- flwr/cli/supernode/register.py +31 -80
- flwr/cli/supernode/unregister.py +24 -70
- flwr/cli/typing.py +200 -0
- flwr/cli/utils.py +160 -275
- flwr/client/grpc_rere_client/connection.py +3 -3
- flwr/client/grpc_rere_client/grpc_adapter.py +1 -1
- flwr/client/message_handler/message_handler.py +2 -1
- flwr/client/mod/centraldp_mods.py +1 -1
- flwr/client/mod/localdp_mod.py +1 -1
- flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
- flwr/client/run_info_store.py +2 -1
- flwr/clientapp/client_app.py +2 -1
- flwr/common/__init__.py +3 -2
- flwr/common/args.py +5 -5
- flwr/common/config.py +12 -17
- flwr/common/constant.py +3 -16
- flwr/common/context.py +2 -1
- flwr/common/exit/exit.py +4 -4
- flwr/common/exit/exit_code.py +6 -0
- flwr/common/grpc.py +2 -1
- flwr/common/logger.py +1 -1
- flwr/common/message.py +1 -1
- flwr/common/retry_invoker.py +13 -5
- flwr/common/secure_aggregation/ndarrays_arithmetic.py +5 -2
- flwr/common/serde.py +7 -5
- flwr/common/telemetry.py +1 -1
- flwr/common/typing.py +4 -3
- flwr/compat/client/app.py +6 -9
- flwr/compat/client/grpc_client/connection.py +2 -1
- flwr/compat/common/constant.py +29 -0
- flwr/compat/server/app.py +1 -1
- flwr/proto/clientappio_pb2.py +2 -2
- flwr/proto/clientappio_pb2_grpc.py +104 -88
- flwr/proto/clientappio_pb2_grpc.pyi +140 -80
- flwr/proto/federation_pb2.py +5 -3
- flwr/proto/federation_pb2.pyi +32 -2
- flwr/proto/run_pb2.py +5 -13
- flwr/proto/run_pb2.pyi +0 -57
- flwr/proto/serverappio_pb2.py +2 -2
- flwr/proto/serverappio_pb2_grpc.py +138 -207
- flwr/proto/serverappio_pb2_grpc.pyi +189 -155
- flwr/proto/simulationio_pb2.py +2 -2
- flwr/proto/simulationio_pb2_grpc.py +62 -90
- flwr/proto/simulationio_pb2_grpc.pyi +95 -55
- flwr/server/app.py +6 -13
- flwr/server/compat/grid_client_proxy.py +2 -1
- flwr/server/grid/grpc_grid.py +5 -5
- flwr/server/serverapp/app.py +11 -4
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -1
- flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +13 -12
- flwr/server/superlink/fleet/message_handler/message_handler.py +6 -5
- flwr/server/superlink/linkstate/__init__.py +2 -2
- flwr/server/superlink/linkstate/in_memory_linkstate.py +2 -10
- flwr/server/superlink/linkstate/linkstate.py +2 -21
- flwr/server/superlink/linkstate/linkstate_factory.py +16 -8
- flwr/server/superlink/linkstate/{sqlite_linkstate.py → sql_linkstate.py} +432 -534
- flwr/server/superlink/linkstate/utils.py +49 -2
- flwr/server/superlink/serverappio/serverappio_servicer.py +1 -33
- flwr/server/superlink/simulation/simulationio_servicer.py +0 -19
- flwr/server/utils/validator.py +1 -1
- flwr/server/workflow/default_workflows.py +2 -1
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
- flwr/serverapp/strategy/bulyan.py +7 -1
- flwr/serverapp/strategy/dp_fixed_clipping.py +9 -1
- flwr/serverapp/strategy/fedavg.py +1 -1
- flwr/serverapp/strategy/fedxgb_cyclic.py +1 -1
- flwr/simulation/ray_transport/ray_client_proxy.py +2 -6
- flwr/simulation/run_simulation.py +3 -12
- flwr/simulation/simulationio_connection.py +3 -3
- flwr/{common → supercore}/address.py +7 -33
- flwr/supercore/app_utils.py +2 -1
- flwr/supercore/constant.py +24 -2
- flwr/supercore/corestate/{sqlite_corestate.py → sql_corestate.py} +19 -23
- flwr/supercore/credential_store/__init__.py +33 -0
- flwr/supercore/credential_store/credential_store.py +34 -0
- flwr/supercore/credential_store/file_credential_store.py +76 -0
- flwr/{common → supercore}/date.py +0 -11
- flwr/supercore/ffs/disk_ffs.py +1 -1
- flwr/supercore/object_store/object_store_factory.py +14 -6
- flwr/supercore/object_store/{sqlite_object_store.py → sql_object_store.py} +115 -117
- flwr/supercore/sql_mixin.py +315 -0
- flwr/supercore/state/__init__.py +15 -0
- flwr/supercore/state/alembic/__init__.py +15 -0
- flwr/supercore/state/alembic/env.py +103 -0
- flwr/supercore/state/alembic/script.py.mako +43 -0
- flwr/supercore/state/alembic/utils.py +239 -0
- flwr/supercore/state/alembic/versions/__init__.py +15 -0
- flwr/supercore/state/alembic/versions/rev_2026_01_28_initialize_migration_of_state_tables.py +200 -0
- flwr/supercore/state/schema/README.md +121 -0
- flwr/supercore/state/schema/__init__.py +15 -0
- flwr/supercore/state/schema/corestate_tables.py +36 -0
- flwr/supercore/state/schema/linkstate_tables.py +152 -0
- flwr/supercore/state/schema/objectstore_tables.py +90 -0
- flwr/supercore/superexec/run_superexec.py +2 -2
- flwr/supercore/utils.py +36 -1
- flwr/superlink/federation/federation_manager.py +2 -2
- flwr/superlink/federation/noop_federation_manager.py +8 -6
- flwr/superlink/servicer/control/control_servicer.py +19 -17
- flwr/supernode/cli/flower_supernode.py +2 -1
- flwr/supernode/runtime/run_clientapp.py +14 -14
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +10 -8
- flwr/supernode/start_client_internal.py +10 -6
- {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/METADATA +7 -5
- {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/RECORD +137 -116
- flwr/cli/federation/show.py +0 -318
- flwr/common/pyproject.py +0 -42
- flwr/supercore/sqlite_mixin.py +0 -159
- /flwr/{common → supercore}/version.py +0 -0
- {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/WHEEL +0 -0
- {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/entry_points.txt +0 -0
flwr/cli/supernode/register.py
CHANGED
|
@@ -15,26 +15,19 @@
|
|
|
15
15
|
"""Flower command line interface `supernode register` command."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
import io
|
|
19
|
-
import json
|
|
20
18
|
from pathlib import Path
|
|
21
19
|
from typing import Annotated
|
|
22
20
|
|
|
21
|
+
import click
|
|
23
22
|
import typer
|
|
24
23
|
from cryptography.exceptions import UnsupportedAlgorithm
|
|
25
24
|
from cryptography.hazmat.primitives import serialization
|
|
26
25
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
27
|
-
from rich.console import Console
|
|
28
26
|
|
|
29
|
-
from flwr.cli.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
process_loaded_project_config,
|
|
33
|
-
validate_federation_in_project_config,
|
|
34
|
-
)
|
|
35
|
-
from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
|
|
27
|
+
from flwr.cli.config_migration import migrate
|
|
28
|
+
from flwr.cli.flower_config import read_superlink_connection
|
|
29
|
+
from flwr.common.constant import CliOutputFormat
|
|
36
30
|
from flwr.common.exit import ExitCode, flwr_exit
|
|
37
|
-
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
|
38
31
|
from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
39
32
|
RegisterNodeRequest,
|
|
40
33
|
RegisterNodeResponse,
|
|
@@ -42,23 +35,25 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
|
42
35
|
from flwr.proto.control_pb2_grpc import ControlStub
|
|
43
36
|
from flwr.supercore.primitives.asymmetric import public_key_to_bytes, uses_nist_ec_curve
|
|
44
37
|
|
|
45
|
-
from ..utils import
|
|
38
|
+
from ..utils import (
|
|
39
|
+
cli_output_handler,
|
|
40
|
+
flwr_cli_grpc_exc_handler,
|
|
41
|
+
init_channel_from_connection,
|
|
42
|
+
print_json_to_stdout,
|
|
43
|
+
)
|
|
46
44
|
|
|
47
45
|
|
|
48
46
|
def register( # pylint: disable=R0914
|
|
47
|
+
ctx: typer.Context,
|
|
49
48
|
public_key: Annotated[
|
|
50
49
|
Path,
|
|
51
50
|
typer.Argument(
|
|
52
51
|
help="Path to a P-384 (or any other NIST EC curve) public key file.",
|
|
53
52
|
),
|
|
54
53
|
],
|
|
55
|
-
|
|
56
|
-
Path,
|
|
57
|
-
typer.Argument(help="Path of the Flower project"),
|
|
58
|
-
] = Path("."),
|
|
59
|
-
federation: Annotated[
|
|
54
|
+
superlink: Annotated[
|
|
60
55
|
str | None,
|
|
61
|
-
typer.Argument(help="Name of the
|
|
56
|
+
typer.Argument(help="Name of the SuperLink connection."),
|
|
62
57
|
] = None,
|
|
63
58
|
output_format: Annotated[
|
|
64
59
|
str,
|
|
@@ -70,69 +65,33 @@ def register( # pylint: disable=R0914
|
|
|
70
65
|
] = CliOutputFormat.DEFAULT,
|
|
71
66
|
) -> None:
|
|
72
67
|
"""Add a SuperNode to the federation."""
|
|
73
|
-
suppress_output = output_format == CliOutputFormat.JSON
|
|
74
|
-
captured_output = io.StringIO()
|
|
75
|
-
|
|
76
68
|
# Load public key
|
|
77
|
-
|
|
78
|
-
public_key_bytes = try_load_public_key(public_key_path)
|
|
69
|
+
public_key_bytes = try_load_public_key(public_key.expanduser())
|
|
79
70
|
|
|
80
|
-
|
|
81
|
-
if
|
|
82
|
-
|
|
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")
|
|
71
|
+
with cli_output_handler(output_format=output_format) as is_json:
|
|
72
|
+
# Migrate legacy usage if any
|
|
73
|
+
migrate(superlink, args=ctx.args)
|
|
94
74
|
|
|
75
|
+
# Read superlink connection configuration
|
|
76
|
+
superlink_connection = read_superlink_connection(superlink)
|
|
95
77
|
channel = None
|
|
78
|
+
|
|
96
79
|
try:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
stub = ControlStub(channel) # pylint: disable=unused-variable # noqa: F841
|
|
80
|
+
channel = init_channel_from_connection(superlink_connection)
|
|
81
|
+
stub = ControlStub(channel)
|
|
100
82
|
|
|
101
83
|
_register_node(
|
|
102
|
-
stub=stub,
|
|
84
|
+
stub=stub,
|
|
85
|
+
public_key=public_key_bytes,
|
|
86
|
+
is_json=is_json,
|
|
103
87
|
)
|
|
104
88
|
|
|
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
89
|
finally:
|
|
114
90
|
if channel:
|
|
115
91
|
channel.close()
|
|
116
92
|
|
|
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
93
|
|
|
135
|
-
def _register_node(stub: ControlStub, public_key: bytes,
|
|
94
|
+
def _register_node(stub: ControlStub, public_key: bytes, is_json: bool) -> None:
|
|
136
95
|
"""Register a node."""
|
|
137
96
|
with flwr_cli_grpc_exc_handler():
|
|
138
97
|
response: RegisterNodeResponse = stub.RegisterNode(
|
|
@@ -143,31 +102,23 @@ def _register_node(stub: ControlStub, public_key: bytes, output_format: str) ->
|
|
|
143
102
|
f"✅ SuperNode {response.node_id} registered successfully.",
|
|
144
103
|
fg=typer.colors.GREEN,
|
|
145
104
|
)
|
|
146
|
-
if
|
|
147
|
-
|
|
105
|
+
if is_json:
|
|
106
|
+
print_json_to_stdout(
|
|
148
107
|
{
|
|
149
108
|
"success": True,
|
|
150
109
|
"node-id": response.node_id,
|
|
151
110
|
}
|
|
152
111
|
)
|
|
153
|
-
restore_output()
|
|
154
|
-
Console().print_json(run_output)
|
|
155
112
|
else:
|
|
156
|
-
|
|
157
|
-
"❌ SuperNode couldn't be registered.", fg=typer.colors.RED, err=True
|
|
158
|
-
)
|
|
113
|
+
raise click.ClickException("SuperNode couldn't be registered.")
|
|
159
114
|
|
|
160
115
|
|
|
161
116
|
def try_load_public_key(public_key_path: Path) -> bytes:
|
|
162
117
|
"""Try to load a public key from a file."""
|
|
163
118
|
if not public_key_path.exists():
|
|
164
|
-
|
|
165
|
-
f"
|
|
166
|
-
fg=typer.colors.RED,
|
|
167
|
-
bold=True,
|
|
168
|
-
err=True,
|
|
119
|
+
raise click.ClickException(
|
|
120
|
+
f"Public key file '{public_key_path}' does not exist."
|
|
169
121
|
)
|
|
170
|
-
raise typer.Exit(code=1)
|
|
171
122
|
|
|
172
123
|
with open(public_key_path, "rb") as key_file:
|
|
173
124
|
try:
|
flwr/cli/supernode/unregister.py
CHANGED
|
@@ -15,42 +15,35 @@
|
|
|
15
15
|
"""Flower command line interface `supernode unregister` command."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
import io
|
|
19
|
-
import json
|
|
20
|
-
from pathlib import Path
|
|
21
18
|
from typing import Annotated
|
|
22
19
|
|
|
23
20
|
import typer
|
|
24
|
-
from rich.console import Console
|
|
25
21
|
|
|
26
|
-
from flwr.cli.
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
22
|
+
from flwr.cli.config_migration import migrate
|
|
23
|
+
from flwr.cli.flower_config import read_superlink_connection
|
|
24
|
+
from flwr.common.constant import CliOutputFormat
|
|
34
25
|
from flwr.proto.control_pb2 import UnregisterNodeRequest # pylint: disable=E0611
|
|
35
26
|
from flwr.proto.control_pb2_grpc import ControlStub
|
|
36
27
|
|
|
37
|
-
from ..utils import
|
|
28
|
+
from ..utils import (
|
|
29
|
+
cli_output_handler,
|
|
30
|
+
flwr_cli_grpc_exc_handler,
|
|
31
|
+
init_channel_from_connection,
|
|
32
|
+
print_json_to_stdout,
|
|
33
|
+
)
|
|
38
34
|
|
|
39
35
|
|
|
40
36
|
def unregister( # pylint: disable=R0914
|
|
37
|
+
ctx: typer.Context,
|
|
41
38
|
node_id: Annotated[
|
|
42
39
|
int,
|
|
43
40
|
typer.Argument(
|
|
44
41
|
help="ID of the SuperNode to remove.",
|
|
45
42
|
),
|
|
46
43
|
],
|
|
47
|
-
|
|
48
|
-
Path,
|
|
49
|
-
typer.Argument(help="Path of the Flower project"),
|
|
50
|
-
] = Path("."),
|
|
51
|
-
federation: Annotated[
|
|
44
|
+
superlink: Annotated[
|
|
52
45
|
str | None,
|
|
53
|
-
typer.Argument(help="Name of the
|
|
46
|
+
typer.Argument(help="Name of the SuperLink connection."),
|
|
54
47
|
] = None,
|
|
55
48
|
output_format: Annotated[
|
|
56
49
|
str,
|
|
@@ -62,66 +55,29 @@ def unregister( # pylint: disable=R0914
|
|
|
62
55
|
] = CliOutputFormat.DEFAULT,
|
|
63
56
|
) -> None:
|
|
64
57
|
"""Unregister a SuperNode from the federation."""
|
|
65
|
-
|
|
66
|
-
|
|
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")
|
|
58
|
+
with cli_output_handler(output_format=output_format) as is_json:
|
|
59
|
+
# Migrate legacy usage if any
|
|
60
|
+
migrate(superlink, args=ctx.args)
|
|
82
61
|
|
|
62
|
+
# Read superlink connection configuration
|
|
63
|
+
superlink_connection = read_superlink_connection(superlink)
|
|
83
64
|
channel = None
|
|
65
|
+
|
|
84
66
|
try:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
stub = ControlStub(channel) # pylint: disable=unused-variable # noqa: F841
|
|
67
|
+
channel = init_channel_from_connection(superlink_connection)
|
|
68
|
+
stub = ControlStub(channel)
|
|
88
69
|
|
|
89
|
-
_unregister_node(stub=stub, node_id=node_id,
|
|
70
|
+
_unregister_node(stub=stub, node_id=node_id, is_json=is_json)
|
|
90
71
|
|
|
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
72
|
finally:
|
|
100
73
|
if channel:
|
|
101
74
|
channel.close()
|
|
102
75
|
|
|
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
76
|
|
|
121
77
|
def _unregister_node(
|
|
122
78
|
stub: ControlStub,
|
|
123
79
|
node_id: int,
|
|
124
|
-
|
|
80
|
+
is_json: bool,
|
|
125
81
|
) -> None:
|
|
126
82
|
"""Unregister a SuperNode from the federation."""
|
|
127
83
|
with flwr_cli_grpc_exc_handler():
|
|
@@ -129,12 +85,10 @@ def _unregister_node(
|
|
|
129
85
|
typer.secho(
|
|
130
86
|
f"✅ SuperNode {node_id} unregistered successfully.", fg=typer.colors.GREEN
|
|
131
87
|
)
|
|
132
|
-
if
|
|
133
|
-
|
|
88
|
+
if is_json:
|
|
89
|
+
print_json_to_stdout(
|
|
134
90
|
{
|
|
135
91
|
"success": True,
|
|
136
92
|
"node-id": node_id,
|
|
137
93
|
}
|
|
138
94
|
)
|
|
139
|
-
restore_output()
|
|
140
|
-
Console().print_json(run_output)
|
flwr/cli/typing.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Copyright 2026 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 type definitions."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from flwr.cli.constant import (
|
|
22
|
+
DEFAULT_SIMULATION_BACKEND_NAME,
|
|
23
|
+
SuperLinkConnectionTomlKey,
|
|
24
|
+
)
|
|
25
|
+
from flwr.supercore.utils import check_federation_format
|
|
26
|
+
|
|
27
|
+
_ERROR_MSG_FMT = "SuperLinkConnection.%s is None"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class SimulationClientResources:
|
|
32
|
+
"""Resource configuration for the ClientApp."""
|
|
33
|
+
|
|
34
|
+
num_cpus: float | None = None
|
|
35
|
+
num_gpus: float | None = None
|
|
36
|
+
|
|
37
|
+
def __post_init__(self) -> None:
|
|
38
|
+
"""Validate client resources configuration."""
|
|
39
|
+
if self.num_cpus is not None and not isinstance(self.num_cpus, (int, float)):
|
|
40
|
+
raise ValueError("client-resources.num-cpus must be a number (int/float).")
|
|
41
|
+
if self.num_gpus is not None and not isinstance(self.num_gpus, (int, float)):
|
|
42
|
+
raise ValueError("client-resources.num-gpus must be a number (int/float).")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class SimulationInitArgs:
|
|
47
|
+
"""Initialization arguments for the simulation."""
|
|
48
|
+
|
|
49
|
+
num_cpus: int | None = None
|
|
50
|
+
num_gpus: int | None = None
|
|
51
|
+
logging_level: str | None = None
|
|
52
|
+
log_to_drive: bool | None = None
|
|
53
|
+
|
|
54
|
+
def __post_init__(self) -> None:
|
|
55
|
+
"""Validate initialization arguments."""
|
|
56
|
+
if self.num_cpus is not None and not isinstance(self.num_cpus, int):
|
|
57
|
+
raise ValueError("init-args.num-cpus must be an integer.")
|
|
58
|
+
if self.num_gpus is not None and not isinstance(self.num_gpus, int):
|
|
59
|
+
raise ValueError("init-args.num-gpus must be an integer.")
|
|
60
|
+
if self.logging_level is not None and not isinstance(self.logging_level, str):
|
|
61
|
+
raise ValueError("init-args.logging-level must be a string.")
|
|
62
|
+
if self.log_to_drive is not None and not isinstance(self.log_to_drive, bool):
|
|
63
|
+
raise ValueError("init-args.log-to-drive must be a boolean.")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class SimulationBackendConfig:
|
|
68
|
+
"""Backend configuration for the simulation."""
|
|
69
|
+
|
|
70
|
+
client_resources: SimulationClientResources | None = None
|
|
71
|
+
init_args: SimulationInitArgs | None = None
|
|
72
|
+
name: str = DEFAULT_SIMULATION_BACKEND_NAME
|
|
73
|
+
|
|
74
|
+
def __post_init__(self) -> None:
|
|
75
|
+
"""Validate backend configuration."""
|
|
76
|
+
if not isinstance(self.name, str):
|
|
77
|
+
raise ValueError("backend.name must be a string.")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class SuperLinkSimulationOptions:
|
|
82
|
+
"""Options for local simulation."""
|
|
83
|
+
|
|
84
|
+
num_supernodes: int
|
|
85
|
+
backend: SimulationBackendConfig | None = None
|
|
86
|
+
verbose: bool | None = None
|
|
87
|
+
|
|
88
|
+
def __post_init__(self) -> None:
|
|
89
|
+
"""Validate simulation options."""
|
|
90
|
+
if not isinstance(self.num_supernodes, int):
|
|
91
|
+
raise ValueError(
|
|
92
|
+
"Invalid simulation options: num-supernodes must be an integer."
|
|
93
|
+
)
|
|
94
|
+
if self.verbose is not None and not isinstance(self.verbose, bool):
|
|
95
|
+
raise ValueError("Invalid simulation options: verbose must be a boolean.")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class SuperLinkConnection:
|
|
100
|
+
"""SuperLink connection configuration for CLI commands.
|
|
101
|
+
|
|
102
|
+
Attributes
|
|
103
|
+
----------
|
|
104
|
+
name : str
|
|
105
|
+
The name of the connection configuration.
|
|
106
|
+
address : str
|
|
107
|
+
The address of the SuperLink (Control API).
|
|
108
|
+
root_certificates : str
|
|
109
|
+
The absolute path to the root CA certificate file.
|
|
110
|
+
insecure : bool (default: False)
|
|
111
|
+
Whether to use an insecure channel. If True, the
|
|
112
|
+
connection will not use TLS encryption.
|
|
113
|
+
federation : str
|
|
114
|
+
The name of the federation to interface with.
|
|
115
|
+
options : SuperLinkSimulationOptions
|
|
116
|
+
Configuration options for the simulation runtime.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
name: str
|
|
120
|
+
address: str | None = None
|
|
121
|
+
root_certificates: str | None = None
|
|
122
|
+
_insecure: bool | None = None
|
|
123
|
+
federation: str | None = None
|
|
124
|
+
options: SuperLinkSimulationOptions | None = None
|
|
125
|
+
|
|
126
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
name: str,
|
|
130
|
+
address: str | None = None,
|
|
131
|
+
root_certificates: str | None = None,
|
|
132
|
+
insecure: bool | None = None,
|
|
133
|
+
federation: str | None = None,
|
|
134
|
+
options: SuperLinkSimulationOptions | None = None,
|
|
135
|
+
) -> None:
|
|
136
|
+
self.name = name
|
|
137
|
+
self.address = address
|
|
138
|
+
self.root_certificates = root_certificates
|
|
139
|
+
self._insecure = insecure
|
|
140
|
+
self.federation = federation
|
|
141
|
+
self.options = options
|
|
142
|
+
|
|
143
|
+
self.__post_init__()
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def insecure(self) -> bool:
|
|
147
|
+
"""Return the insecure flag or its default (False) if unset."""
|
|
148
|
+
if self._insecure is None:
|
|
149
|
+
return False
|
|
150
|
+
return self._insecure
|
|
151
|
+
|
|
152
|
+
def __post_init__(self) -> None:
|
|
153
|
+
"""Validate SuperLink connection configuration."""
|
|
154
|
+
err_prefix = f"Invalid value for key '%s' in connection '{self.name}': "
|
|
155
|
+
if self.address is not None and not isinstance(self.address, str):
|
|
156
|
+
raise ValueError(
|
|
157
|
+
err_prefix % SuperLinkConnectionTomlKey.ADDRESS
|
|
158
|
+
+ f"expected str, but got {type(self.address).__name__}."
|
|
159
|
+
)
|
|
160
|
+
if self.root_certificates is not None and not isinstance(
|
|
161
|
+
self.root_certificates, str
|
|
162
|
+
):
|
|
163
|
+
raise ValueError(
|
|
164
|
+
err_prefix % SuperLinkConnectionTomlKey.ROOT_CERTIFICATES
|
|
165
|
+
+ f"expected str, but got {type(self.root_certificates).__name__}."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Ensure root certificates path is absolute
|
|
169
|
+
if self.root_certificates is not None:
|
|
170
|
+
if not Path(self.root_certificates).is_absolute():
|
|
171
|
+
raise ValueError(
|
|
172
|
+
err_prefix % SuperLinkConnectionTomlKey.ROOT_CERTIFICATES
|
|
173
|
+
+ "expected absolute path, but got relative path "
|
|
174
|
+
f"'{self.root_certificates}'."
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if self._insecure is not None and not isinstance(self._insecure, bool):
|
|
178
|
+
raise ValueError(
|
|
179
|
+
err_prefix % SuperLinkConnectionTomlKey.INSECURE
|
|
180
|
+
+ f"expected bool, but got {type(self._insecure).__name__}."
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if self.federation is not None:
|
|
184
|
+
if not isinstance(self.federation, str):
|
|
185
|
+
raise ValueError(
|
|
186
|
+
err_prefix % SuperLinkConnectionTomlKey.FEDERATION
|
|
187
|
+
+ f"expected str, but got {type(self.federation).__name__}."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Check if the federation string is valid
|
|
191
|
+
check_federation_format(self.federation)
|
|
192
|
+
|
|
193
|
+
# The connection needs to have either an address or options (or both).
|
|
194
|
+
if self.address is None and self.options is None:
|
|
195
|
+
raise ValueError(
|
|
196
|
+
"Invalid SuperLink connection format: "
|
|
197
|
+
f"'{SuperLinkConnectionTomlKey.ADDRESS}' and/or "
|
|
198
|
+
f"'{SuperLinkConnectionTomlKey.OPTIONS}' key "
|
|
199
|
+
"need to be specified."
|
|
200
|
+
)
|