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
|
@@ -0,0 +1,429 @@
|
|
|
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 configuration utils."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, cast
|
|
21
|
+
|
|
22
|
+
import click
|
|
23
|
+
import tomli
|
|
24
|
+
import tomli_w
|
|
25
|
+
import typer
|
|
26
|
+
|
|
27
|
+
from flwr.cli.constant import (
|
|
28
|
+
DEFAULT_FLOWER_CONFIG_TOML,
|
|
29
|
+
DEFAULT_SIMULATION_BACKEND_NAME,
|
|
30
|
+
FLOWER_CONFIG_FILE,
|
|
31
|
+
SimulationBackendConfigTomlKey,
|
|
32
|
+
SimulationClientResourcesTomlKey,
|
|
33
|
+
SimulationInitArgsTomlKey,
|
|
34
|
+
SuperLinkConnectionTomlKey,
|
|
35
|
+
SuperLinkSimulationOptionsTomlKey,
|
|
36
|
+
)
|
|
37
|
+
from flwr.cli.typing import (
|
|
38
|
+
SimulationBackendConfig,
|
|
39
|
+
SimulationClientResources,
|
|
40
|
+
SimulationInitArgs,
|
|
41
|
+
SuperLinkConnection,
|
|
42
|
+
SuperLinkSimulationOptions,
|
|
43
|
+
)
|
|
44
|
+
from flwr.common.config import flatten_dict
|
|
45
|
+
from flwr.supercore.utils import get_flwr_home
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _parse_simulation_options(options: dict[str, Any]) -> SuperLinkSimulationOptions:
|
|
49
|
+
"""Parse simulation options from a dictionary in a SuperLink connection."""
|
|
50
|
+
num_supernodes = options.get(SuperLinkSimulationOptionsTomlKey.NUM_SUPERNODES)
|
|
51
|
+
verbose = options.get(SuperLinkSimulationOptionsTomlKey.VERBOSE)
|
|
52
|
+
# Validation handled in SuperLinkSimulationOptions.__post_init__
|
|
53
|
+
|
|
54
|
+
backend_dict = options.get(SuperLinkSimulationOptionsTomlKey.BACKEND)
|
|
55
|
+
simulation_backend: SimulationBackendConfig | None = None
|
|
56
|
+
|
|
57
|
+
if isinstance(backend_dict, dict):
|
|
58
|
+
# Parse client resources
|
|
59
|
+
client_resources_dict = backend_dict.get(
|
|
60
|
+
SimulationBackendConfigTomlKey.CLIENT_RESOURCES
|
|
61
|
+
)
|
|
62
|
+
client_resources: SimulationClientResources | None = None
|
|
63
|
+
if isinstance(client_resources_dict, dict):
|
|
64
|
+
client_resources = SimulationClientResources(
|
|
65
|
+
num_cpus=client_resources_dict.get(
|
|
66
|
+
SimulationClientResourcesTomlKey.NUM_CPUS
|
|
67
|
+
),
|
|
68
|
+
num_gpus=client_resources_dict.get(
|
|
69
|
+
SimulationClientResourcesTomlKey.NUM_GPUS
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Parse init args
|
|
74
|
+
init_args_dict = backend_dict.get(SimulationBackendConfigTomlKey.INIT_ARGS)
|
|
75
|
+
init_args: SimulationInitArgs | None = None
|
|
76
|
+
if isinstance(init_args_dict, dict):
|
|
77
|
+
init_args = SimulationInitArgs(
|
|
78
|
+
num_cpus=init_args_dict.get(SimulationInitArgsTomlKey.NUM_CPUS),
|
|
79
|
+
num_gpus=init_args_dict.get(SimulationInitArgsTomlKey.NUM_GPUS),
|
|
80
|
+
logging_level=init_args_dict.get(
|
|
81
|
+
SimulationInitArgsTomlKey.LOGGING_LEVEL
|
|
82
|
+
),
|
|
83
|
+
log_to_drive=init_args_dict.get(SimulationInitArgsTomlKey.LOG_TO_DRIVE),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
simulation_backend = SimulationBackendConfig(
|
|
87
|
+
name=backend_dict.get(
|
|
88
|
+
SimulationBackendConfigTomlKey.NAME, DEFAULT_SIMULATION_BACKEND_NAME
|
|
89
|
+
),
|
|
90
|
+
client_resources=client_resources,
|
|
91
|
+
init_args=init_args,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Note: validation happens in SuperLinkSimulationOptions.__post_init__
|
|
95
|
+
return SuperLinkSimulationOptions(
|
|
96
|
+
num_supernodes=cast(int, num_supernodes),
|
|
97
|
+
backend=simulation_backend,
|
|
98
|
+
verbose=verbose,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _serialize_simulation_options(
|
|
103
|
+
options: SuperLinkSimulationOptions,
|
|
104
|
+
) -> dict[str, Any]:
|
|
105
|
+
"""Convert SuperLinkSimulationOptions to a dictionary for TOML serialization."""
|
|
106
|
+
options_dict: dict[str, Any] = {
|
|
107
|
+
SuperLinkSimulationOptionsTomlKey.NUM_SUPERNODES: options.num_supernodes
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if options.verbose is not None:
|
|
111
|
+
options_dict[SuperLinkSimulationOptionsTomlKey.VERBOSE] = options.verbose
|
|
112
|
+
|
|
113
|
+
if options.backend is not None:
|
|
114
|
+
backend = options.backend
|
|
115
|
+
|
|
116
|
+
# Serialize client resources
|
|
117
|
+
c_res_dict: dict[str, Any] = {}
|
|
118
|
+
if backend.client_resources is not None:
|
|
119
|
+
client_res = backend.client_resources
|
|
120
|
+
c_res_dict = {
|
|
121
|
+
SimulationClientResourcesTomlKey.NUM_CPUS: client_res.num_cpus,
|
|
122
|
+
SimulationClientResourcesTomlKey.NUM_GPUS: client_res.num_gpus,
|
|
123
|
+
}
|
|
124
|
+
# Remove None values
|
|
125
|
+
c_res_dict = {k: v for k, v in c_res_dict.items() if v is not None}
|
|
126
|
+
|
|
127
|
+
# Serialize init args
|
|
128
|
+
init_args_dict: dict[str, Any] = {}
|
|
129
|
+
if backend.init_args is not None:
|
|
130
|
+
init_args = backend.init_args
|
|
131
|
+
init_args_dict = {
|
|
132
|
+
SimulationInitArgsTomlKey.NUM_CPUS: init_args.num_cpus,
|
|
133
|
+
SimulationInitArgsTomlKey.NUM_GPUS: init_args.num_gpus,
|
|
134
|
+
SimulationInitArgsTomlKey.LOGGING_LEVEL: init_args.logging_level,
|
|
135
|
+
SimulationInitArgsTomlKey.LOG_TO_DRIVE: init_args.log_to_drive,
|
|
136
|
+
}
|
|
137
|
+
# Remove None values
|
|
138
|
+
init_args_dict = {k: v for k, v in init_args_dict.items() if v is not None}
|
|
139
|
+
|
|
140
|
+
backend_dict = {
|
|
141
|
+
SimulationBackendConfigTomlKey.NAME: backend.name,
|
|
142
|
+
SimulationBackendConfigTomlKey.CLIENT_RESOURCES: c_res_dict,
|
|
143
|
+
SimulationBackendConfigTomlKey.INIT_ARGS: init_args_dict,
|
|
144
|
+
}
|
|
145
|
+
# Remove empty dicts
|
|
146
|
+
backend_dict = {k: v for k, v in backend_dict.items() if v}
|
|
147
|
+
|
|
148
|
+
options_dict[SuperLinkSimulationOptionsTomlKey.BACKEND] = backend_dict
|
|
149
|
+
|
|
150
|
+
return options_dict
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def init_flwr_config() -> None:
|
|
154
|
+
"""Initialize the Flower configuration file."""
|
|
155
|
+
config_path = get_flwr_home() / FLOWER_CONFIG_FILE
|
|
156
|
+
|
|
157
|
+
if not config_path.exists():
|
|
158
|
+
# Create parent directory if it doesn't exist
|
|
159
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
# Write Flower config file
|
|
161
|
+
config_path.write_text(DEFAULT_FLOWER_CONFIG_TOML, encoding="utf-8")
|
|
162
|
+
|
|
163
|
+
typer.secho(
|
|
164
|
+
f"\nFlower configuration not found. Created default configuration"
|
|
165
|
+
f" at {config_path}\n",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def parse_superlink_connection(
|
|
170
|
+
conn_dict: dict[str, Any], name: str
|
|
171
|
+
) -> SuperLinkConnection:
|
|
172
|
+
"""Parse SuperLink connection configuration from a TOML dictionary.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
conn_dict : dict[str, Any]
|
|
177
|
+
The TOML configuration dictionary for the connection.
|
|
178
|
+
name : str
|
|
179
|
+
The name of the connection.
|
|
180
|
+
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
SuperLinkConnection
|
|
184
|
+
The parsed SuperLink connection configuration.
|
|
185
|
+
"""
|
|
186
|
+
simulation_options: SuperLinkSimulationOptions | None = None
|
|
187
|
+
if SuperLinkConnectionTomlKey.OPTIONS in conn_dict:
|
|
188
|
+
options = conn_dict[SuperLinkConnectionTomlKey.OPTIONS]
|
|
189
|
+
if isinstance(options, dict):
|
|
190
|
+
simulation_options = _parse_simulation_options(options)
|
|
191
|
+
else:
|
|
192
|
+
raise ValueError(
|
|
193
|
+
f"Invalid value for key '{SuperLinkConnectionTomlKey.OPTIONS}': "
|
|
194
|
+
f"expected dict, but got {type(options).__name__}."
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Build and return SuperLinkConnection
|
|
198
|
+
return SuperLinkConnection(
|
|
199
|
+
name=name,
|
|
200
|
+
address=conn_dict.get(SuperLinkConnectionTomlKey.ADDRESS),
|
|
201
|
+
root_certificates=conn_dict.get(SuperLinkConnectionTomlKey.ROOT_CERTIFICATES),
|
|
202
|
+
insecure=conn_dict.get(SuperLinkConnectionTomlKey.INSECURE),
|
|
203
|
+
federation=conn_dict.get(SuperLinkConnectionTomlKey.FEDERATION),
|
|
204
|
+
options=simulation_options,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def serialize_superlink_connection(connection: SuperLinkConnection) -> dict[str, Any]:
|
|
209
|
+
"""Convert SuperLinkConnection to a dictionary for TOML serialization.
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
connection : SuperLinkConnection
|
|
214
|
+
The SuperLink connection to serialize.
|
|
215
|
+
|
|
216
|
+
Returns
|
|
217
|
+
-------
|
|
218
|
+
dict[str, Any]
|
|
219
|
+
Dictionary representation suitable for TOML serialization.
|
|
220
|
+
"""
|
|
221
|
+
# pylint: disable=protected-access
|
|
222
|
+
conn_dict: dict[str, Any] = {
|
|
223
|
+
SuperLinkConnectionTomlKey.ADDRESS: connection.address,
|
|
224
|
+
SuperLinkConnectionTomlKey.ROOT_CERTIFICATES: connection.root_certificates,
|
|
225
|
+
SuperLinkConnectionTomlKey.INSECURE: connection._insecure,
|
|
226
|
+
SuperLinkConnectionTomlKey.FEDERATION: connection.federation,
|
|
227
|
+
}
|
|
228
|
+
# Remove None values
|
|
229
|
+
conn_dict = {k: v for k, v in conn_dict.items() if v is not None}
|
|
230
|
+
|
|
231
|
+
if connection.options is not None:
|
|
232
|
+
options_dict = _serialize_simulation_options(connection.options)
|
|
233
|
+
conn_dict[SuperLinkConnectionTomlKey.OPTIONS] = options_dict
|
|
234
|
+
|
|
235
|
+
return conn_dict
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def read_superlink_connection(
|
|
239
|
+
connection_name: str | None = None,
|
|
240
|
+
) -> SuperLinkConnection:
|
|
241
|
+
"""Read a SuperLink connection from the Flower configuration file.
|
|
242
|
+
|
|
243
|
+
Parameters
|
|
244
|
+
----------
|
|
245
|
+
connection_name : str | None
|
|
246
|
+
The name of the SuperLink connection to load. If None, the default connection
|
|
247
|
+
will be loaded.
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
SuperLinkConnection
|
|
252
|
+
The SuperLink connection.
|
|
253
|
+
|
|
254
|
+
Raises
|
|
255
|
+
------
|
|
256
|
+
click.ClickException
|
|
257
|
+
Raised if the configuration file is corrupted, or if the requested
|
|
258
|
+
connection (or default) cannot be found.
|
|
259
|
+
"""
|
|
260
|
+
toml_dict, config_path = read_flower_config()
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
superlink_config = toml_dict.get(SuperLinkConnectionTomlKey.SUPERLINK, {})
|
|
264
|
+
|
|
265
|
+
# Load the default SuperLink connection when not provided
|
|
266
|
+
if connection_name is None:
|
|
267
|
+
connection_name = superlink_config.get(SuperLinkConnectionTomlKey.DEFAULT)
|
|
268
|
+
|
|
269
|
+
# Exit when no connection name is available
|
|
270
|
+
if connection_name is None:
|
|
271
|
+
raise click.ClickException(
|
|
272
|
+
"No SuperLink connection set. A SuperLink connection needs to be "
|
|
273
|
+
"provided or one must be set as default in the Flower "
|
|
274
|
+
f"configuration file ({config_path}). Specify a default SuperLink "
|
|
275
|
+
"connection by adding: \n\n[superlink]\ndefault = 'connection_name'\n\n"
|
|
276
|
+
f"to the Flower configuration file ({config_path})."
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Try to find the connection with the given name
|
|
280
|
+
if connection_name not in superlink_config:
|
|
281
|
+
msg = (
|
|
282
|
+
f"SuperLink connection '{connection_name}' not found in the "
|
|
283
|
+
f"Flower configuration file ({config_path})."
|
|
284
|
+
)
|
|
285
|
+
# If default was used, show a specific error message
|
|
286
|
+
if connection_name == superlink_config.get(
|
|
287
|
+
SuperLinkConnectionTomlKey.DEFAULT
|
|
288
|
+
):
|
|
289
|
+
msg += (
|
|
290
|
+
f"\nPlease check that the default connection '{connection_name}' "
|
|
291
|
+
"is defined in the [superlink] section."
|
|
292
|
+
)
|
|
293
|
+
raise click.ClickException(msg)
|
|
294
|
+
|
|
295
|
+
conn_dict = superlink_config[connection_name]
|
|
296
|
+
return parse_superlink_connection(conn_dict, connection_name)
|
|
297
|
+
|
|
298
|
+
except ValueError as err:
|
|
299
|
+
raise click.ClickException(
|
|
300
|
+
f"Failed to parse the Flower configuration file ({config_path}). {err}"
|
|
301
|
+
) from err
|
|
302
|
+
except Exception as err:
|
|
303
|
+
raise click.ClickException(
|
|
304
|
+
f"An unexpected error occurred while reading the Flower configuration "
|
|
305
|
+
f"file ({config_path}). {err}"
|
|
306
|
+
) from err
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def write_superlink_connection(connection: SuperLinkConnection) -> None:
|
|
310
|
+
"""Write a SuperLink connection to the Flower configuration file.
|
|
311
|
+
|
|
312
|
+
Parameters
|
|
313
|
+
----------
|
|
314
|
+
connection : SuperLinkConnection
|
|
315
|
+
The SuperLink connection to write to the configuration file.
|
|
316
|
+
|
|
317
|
+
Raises
|
|
318
|
+
------
|
|
319
|
+
click.ClickException
|
|
320
|
+
Raised if the configuration file cannot be read or written.
|
|
321
|
+
"""
|
|
322
|
+
toml_dict, _ = read_flower_config()
|
|
323
|
+
|
|
324
|
+
# Ensure superlink section exists
|
|
325
|
+
if SuperLinkConnectionTomlKey.SUPERLINK not in toml_dict:
|
|
326
|
+
toml_dict[SuperLinkConnectionTomlKey.SUPERLINK] = {}
|
|
327
|
+
|
|
328
|
+
superlink_config = toml_dict[SuperLinkConnectionTomlKey.SUPERLINK]
|
|
329
|
+
|
|
330
|
+
# Serialize connection and flatten nested dicts using dotted keys
|
|
331
|
+
conn_dict = serialize_superlink_connection(connection)
|
|
332
|
+
|
|
333
|
+
# Add/update the connection
|
|
334
|
+
superlink_config[connection.name] = conn_dict
|
|
335
|
+
|
|
336
|
+
# Write back to file
|
|
337
|
+
write_flower_config(toml_dict)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def set_default_superlink_connection(connection_name: str) -> None:
|
|
341
|
+
"""Set the default SuperLink connection."""
|
|
342
|
+
toml_dict, _ = read_flower_config()
|
|
343
|
+
|
|
344
|
+
# Get superlink section
|
|
345
|
+
superlink_config = toml_dict[SuperLinkConnectionTomlKey.SUPERLINK]
|
|
346
|
+
|
|
347
|
+
# Check if the connection exists
|
|
348
|
+
if connection_name not in superlink_config:
|
|
349
|
+
raise click.ClickException(
|
|
350
|
+
f"SuperLink connection '{connection_name}' not found in the Flower "
|
|
351
|
+
"configuration file. Cannot set as default."
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Set default connection
|
|
355
|
+
superlink_config[SuperLinkConnectionTomlKey.DEFAULT] = connection_name
|
|
356
|
+
|
|
357
|
+
# Write back to file
|
|
358
|
+
write_flower_config(toml_dict)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def read_flower_config() -> tuple[dict[str, Any], Path]:
|
|
362
|
+
"""Read the Flower configuration file.
|
|
363
|
+
|
|
364
|
+
Returns
|
|
365
|
+
-------
|
|
366
|
+
tuple[dict[str, Any], Path]
|
|
367
|
+
A tuple containing the TOML configuration dictionary and the path to the
|
|
368
|
+
configuration file.
|
|
369
|
+
|
|
370
|
+
Raises
|
|
371
|
+
------
|
|
372
|
+
click.ClickException
|
|
373
|
+
Raised if the configuration file is corrupted.
|
|
374
|
+
"""
|
|
375
|
+
init_flwr_config()
|
|
376
|
+
|
|
377
|
+
config_path = get_flwr_home() / FLOWER_CONFIG_FILE
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
with config_path.open("rb") as file:
|
|
381
|
+
return tomli.load(file), config_path
|
|
382
|
+
|
|
383
|
+
except tomli.TOMLDecodeError as err:
|
|
384
|
+
raise click.ClickException(
|
|
385
|
+
f"Failed to read the Flower configuration file ({config_path}). "
|
|
386
|
+
"Please ensure it is valid TOML."
|
|
387
|
+
) from err
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# This function may be subject to change once we introduce more configuration
|
|
391
|
+
def write_flower_config(toml_dict: dict[str, Any]) -> Path:
|
|
392
|
+
"""Write the Flower configuration file.
|
|
393
|
+
|
|
394
|
+
Parameters
|
|
395
|
+
----------
|
|
396
|
+
toml_dict : dict[str, Any]
|
|
397
|
+
The TOML configuration dictionary to write to the file.
|
|
398
|
+
|
|
399
|
+
Returns
|
|
400
|
+
-------
|
|
401
|
+
Path
|
|
402
|
+
The path to the configuration file.
|
|
403
|
+
"""
|
|
404
|
+
config_path = get_flwr_home() / FLOWER_CONFIG_FILE
|
|
405
|
+
|
|
406
|
+
# Flatten SuperLink connections
|
|
407
|
+
superlink_config: dict[str, Any] = toml_dict[SuperLinkConnectionTomlKey.SUPERLINK]
|
|
408
|
+
for name in list(superlink_config.keys()):
|
|
409
|
+
if isinstance(superlink_config[name], dict):
|
|
410
|
+
superlink_config[name] = flatten_dict(superlink_config[name])
|
|
411
|
+
|
|
412
|
+
# Get the standard TOML text
|
|
413
|
+
toml_content = tomli_w.dumps(toml_dict)
|
|
414
|
+
|
|
415
|
+
# Remove double quotes around multi-dot keys
|
|
416
|
+
# All keys must be [A-Za-z0-9_-]+ except dots
|
|
417
|
+
lines = toml_content.splitlines(keepends=True)
|
|
418
|
+
pattern = re.compile(r'^"([^"]+\.[^"]+)"\s*=')
|
|
419
|
+
for i, line in enumerate(lines):
|
|
420
|
+
if match := pattern.match(line):
|
|
421
|
+
key = match.group(1)
|
|
422
|
+
lines[i] = line.replace(f'"{key}"', key)
|
|
423
|
+
|
|
424
|
+
toml_content = "".join(lines)
|
|
425
|
+
|
|
426
|
+
with config_path.open("w") as file:
|
|
427
|
+
file.write(toml_content)
|
|
428
|
+
|
|
429
|
+
return config_path
|
flwr/cli/install.py
CHANGED
|
@@ -23,6 +23,7 @@ from io import BytesIO
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from typing import IO, Annotated
|
|
25
25
|
|
|
26
|
+
import click
|
|
26
27
|
import typer
|
|
27
28
|
|
|
28
29
|
from flwr.common.config import get_flwr_dir, get_metadata_from_config
|
|
@@ -62,26 +63,19 @@ def install(
|
|
|
62
63
|
if source is None:
|
|
63
64
|
source = Path(typer.prompt("Enter the source FAB file"))
|
|
64
65
|
|
|
65
|
-
source = source.resolve()
|
|
66
|
+
source = source.expanduser().resolve()
|
|
66
67
|
if not source.exists() or not source.is_file():
|
|
67
|
-
|
|
68
|
-
f"
|
|
69
|
-
fg=typer.colors.RED,
|
|
70
|
-
bold=True,
|
|
71
|
-
err=True,
|
|
68
|
+
raise click.ClickException(
|
|
69
|
+
f"The source {source} does not exist or is not a file."
|
|
72
70
|
)
|
|
73
|
-
raise typer.Exit(code=1)
|
|
74
71
|
|
|
75
72
|
if source.suffix != ".fab":
|
|
76
|
-
|
|
77
|
-
f"❌ The source {source} is not a `.fab` file.",
|
|
78
|
-
fg=typer.colors.RED,
|
|
79
|
-
bold=True,
|
|
80
|
-
err=True,
|
|
81
|
-
)
|
|
82
|
-
raise typer.Exit(code=1)
|
|
73
|
+
raise click.ClickException(f"The source {source} is not a `.fab` file.")
|
|
83
74
|
|
|
84
|
-
|
|
75
|
+
try:
|
|
76
|
+
install_from_fab(source, flwr_dir)
|
|
77
|
+
except ValueError as e:
|
|
78
|
+
raise click.ClickException(str(e)) from None
|
|
85
79
|
|
|
86
80
|
|
|
87
81
|
def install_from_fab(
|
|
@@ -107,7 +101,7 @@ def install_from_fab(
|
|
|
107
101
|
|
|
108
102
|
Raises
|
|
109
103
|
------
|
|
110
|
-
|
|
104
|
+
click.ClickException
|
|
111
105
|
If FAB format is invalid or hash verification fails.
|
|
112
106
|
"""
|
|
113
107
|
fab_file_archive: Path | IO[bytes]
|
|
@@ -129,26 +123,14 @@ def install_from_fab(
|
|
|
129
123
|
tmpdir_path = Path(tmpdir)
|
|
130
124
|
info_dir = tmpdir_path / ".info"
|
|
131
125
|
if not info_dir.exists():
|
|
132
|
-
|
|
133
|
-
"❌ FAB file has incorrect format.",
|
|
134
|
-
fg=typer.colors.RED,
|
|
135
|
-
bold=True,
|
|
136
|
-
err=True,
|
|
137
|
-
)
|
|
138
|
-
raise typer.Exit(code=1)
|
|
126
|
+
raise click.ClickException("FAB file has incorrect format.")
|
|
139
127
|
|
|
140
128
|
content_file = info_dir / "CONTENT"
|
|
141
129
|
|
|
142
130
|
if not content_file.exists() or not _verify_hashes(
|
|
143
131
|
content_file.read_text(), tmpdir_path
|
|
144
132
|
):
|
|
145
|
-
|
|
146
|
-
"❌ File hashes couldn't be verified.",
|
|
147
|
-
fg=typer.colors.RED,
|
|
148
|
-
bold=True,
|
|
149
|
-
err=True,
|
|
150
|
-
)
|
|
151
|
-
raise typer.Exit(code=1)
|
|
133
|
+
raise click.ClickException("File hashes couldn't be verified.")
|
|
152
134
|
|
|
153
135
|
shutil.rmtree(info_dir)
|
|
154
136
|
|
|
@@ -189,19 +171,10 @@ def validate_and_install(
|
|
|
189
171
|
|
|
190
172
|
Raises
|
|
191
173
|
------
|
|
192
|
-
|
|
174
|
+
click.ClickException
|
|
193
175
|
If configuration is invalid or metadata doesn't match.
|
|
194
176
|
"""
|
|
195
|
-
config, _
|
|
196
|
-
|
|
197
|
-
if config is None:
|
|
198
|
-
typer.secho(
|
|
199
|
-
"❌ Invalid config inside FAB file.",
|
|
200
|
-
fg=typer.colors.RED,
|
|
201
|
-
bold=True,
|
|
202
|
-
err=True,
|
|
203
|
-
)
|
|
204
|
-
raise typer.Exit(code=1)
|
|
177
|
+
config, _ = load_and_validate(project_dir / "pyproject.toml", check_module=False)
|
|
205
178
|
|
|
206
179
|
fab_id, version = get_metadata_from_config(config)
|
|
207
180
|
publisher, project_name = fab_id.split("/")
|
|
@@ -283,7 +256,7 @@ def _validate_fab_and_config_metadata(
|
|
|
283
256
|
|
|
284
257
|
Raises
|
|
285
258
|
------
|
|
286
|
-
|
|
259
|
+
click.ClickException
|
|
287
260
|
If filename format is incorrect or hash doesn't match.
|
|
288
261
|
"""
|
|
289
262
|
publisher, project_name, version, fab_hash = config_metadata
|
|
@@ -299,33 +272,21 @@ def _validate_fab_and_config_metadata(
|
|
|
299
272
|
!= f"{publisher}.{project_name}.{version}"
|
|
300
273
|
or len(fab_shorthash) != FAB_HASH_TRUNCATION # Verify hash length
|
|
301
274
|
):
|
|
302
|
-
|
|
303
|
-
"
|
|
304
|
-
"`<publisher>.<project_name>.<version>.<8hexchars>.fab`."
|
|
305
|
-
fg=typer.colors.RED,
|
|
306
|
-
bold=True,
|
|
307
|
-
err=True,
|
|
275
|
+
raise click.ClickException(
|
|
276
|
+
"FAB file has incorrect name. The file name must follow the format "
|
|
277
|
+
"`<publisher>.<project_name>.<version>.<8hexchars>.fab`."
|
|
308
278
|
)
|
|
309
|
-
raise typer.Exit(code=1)
|
|
310
279
|
|
|
311
280
|
# Verify hash is a valid hexadecimal
|
|
312
281
|
try:
|
|
313
282
|
_ = int(fab_shorthash, 16)
|
|
314
283
|
except Exception as e:
|
|
315
|
-
|
|
316
|
-
f"
|
|
317
|
-
|
|
318
|
-
bold=True,
|
|
319
|
-
err=True,
|
|
320
|
-
)
|
|
321
|
-
raise typer.Exit(code=1) from e
|
|
284
|
+
raise click.ClickException(
|
|
285
|
+
f"FAB file has an invalid hexadecimal string `{fab_shorthash}`."
|
|
286
|
+
) from e
|
|
322
287
|
|
|
323
288
|
# Verify shorthash matches
|
|
324
289
|
if fab_shorthash != fab_hash[:FAB_HASH_TRUNCATION]:
|
|
325
|
-
|
|
326
|
-
"
|
|
327
|
-
fg=typer.colors.RED,
|
|
328
|
-
bold=True,
|
|
329
|
-
err=True,
|
|
290
|
+
raise click.ClickException(
|
|
291
|
+
"The hash in the FAB file name does not match the hash of the FAB."
|
|
330
292
|
)
|
|
331
|
-
raise typer.Exit(code=1)
|
flwr/cli/log.py
CHANGED
|
@@ -17,25 +17,22 @@
|
|
|
17
17
|
|
|
18
18
|
import time
|
|
19
19
|
from logging import DEBUG, ERROR, INFO
|
|
20
|
-
from
|
|
21
|
-
from typing import Annotated, Any, cast
|
|
20
|
+
from typing import Annotated, cast
|
|
22
21
|
|
|
22
|
+
import click
|
|
23
23
|
import grpc
|
|
24
24
|
import typer
|
|
25
25
|
|
|
26
|
-
from flwr.cli.
|
|
27
|
-
exit_if_no_address,
|
|
28
|
-
load_and_validate,
|
|
29
|
-
process_loaded_project_config,
|
|
30
|
-
validate_federation_in_project_config,
|
|
31
|
-
)
|
|
26
|
+
from flwr.cli.config_migration import migrate, warn_if_federation_config_overrides
|
|
32
27
|
from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
|
|
28
|
+
from flwr.cli.flower_config import read_superlink_connection
|
|
29
|
+
from flwr.cli.typing import SuperLinkConnection
|
|
33
30
|
from flwr.common.constant import CONN_RECONNECT_INTERVAL, CONN_REFRESH_PERIOD
|
|
34
31
|
from flwr.common.logger import log as logger
|
|
35
32
|
from flwr.proto.control_pb2 import StreamLogsRequest # pylint: disable=E0611
|
|
36
33
|
from flwr.proto.control_pb2_grpc import ControlStub
|
|
37
34
|
|
|
38
|
-
from .utils import flwr_cli_grpc_exc_handler,
|
|
35
|
+
from .utils import flwr_cli_grpc_exc_handler, init_channel_from_connection
|
|
39
36
|
|
|
40
37
|
|
|
41
38
|
class AllLogsRetrieved(BaseException):
|
|
@@ -158,23 +155,21 @@ def print_logs(run_id: int, channel: grpc.Channel, timeout: int) -> None:
|
|
|
158
155
|
|
|
159
156
|
|
|
160
157
|
def log(
|
|
158
|
+
ctx: typer.Context,
|
|
161
159
|
run_id: Annotated[
|
|
162
160
|
int,
|
|
163
161
|
typer.Argument(help="The Flower run ID to query"),
|
|
164
162
|
],
|
|
165
|
-
|
|
166
|
-
Path,
|
|
167
|
-
typer.Argument(help="Path of the Flower project to run"),
|
|
168
|
-
] = Path("."),
|
|
169
|
-
federation: Annotated[
|
|
163
|
+
superlink: Annotated[
|
|
170
164
|
str | None,
|
|
171
|
-
typer.Argument(help="Name of the
|
|
165
|
+
typer.Argument(help="Name of the SuperLink connection."),
|
|
172
166
|
] = None,
|
|
173
167
|
federation_config_overrides: Annotated[
|
|
174
168
|
list[str] | None,
|
|
175
169
|
typer.Option(
|
|
176
170
|
"--federation-config",
|
|
177
171
|
help=FEDERATION_CONFIG_HELP_MESSAGE,
|
|
172
|
+
hidden=True,
|
|
178
173
|
),
|
|
179
174
|
] = None,
|
|
180
175
|
stream: Annotated[
|
|
@@ -190,27 +185,23 @@ def log(
|
|
|
190
185
|
Retrieve and display logs from a Flower run. Logs can be streamed in real-time (with
|
|
191
186
|
--stream) or printed once (with --show).
|
|
192
187
|
"""
|
|
193
|
-
|
|
188
|
+
# Warn `--federation-config` is ignored
|
|
189
|
+
warn_if_federation_config_overrides(federation_config_overrides)
|
|
194
190
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
)
|
|
201
|
-
exit_if_no_address(federation_config, "log")
|
|
191
|
+
# Migrate legacy usage if any
|
|
192
|
+
migrate(superlink, args=ctx.args)
|
|
193
|
+
|
|
194
|
+
# Read superlink connection configuration
|
|
195
|
+
superlink_connection = read_superlink_connection(superlink)
|
|
202
196
|
|
|
203
197
|
try:
|
|
204
|
-
_log_with_control_api(
|
|
198
|
+
_log_with_control_api(superlink_connection, run_id, stream)
|
|
205
199
|
except Exception as err: # pylint: disable=broad-except
|
|
206
|
-
|
|
207
|
-
raise typer.Exit(code=1) from None
|
|
200
|
+
raise click.ClickException(str(err)) from None
|
|
208
201
|
|
|
209
202
|
|
|
210
203
|
def _log_with_control_api(
|
|
211
|
-
|
|
212
|
-
federation: str,
|
|
213
|
-
federation_config: dict[str, Any],
|
|
204
|
+
superlink_connection: SuperLinkConnection,
|
|
214
205
|
run_id: int,
|
|
215
206
|
stream: bool,
|
|
216
207
|
) -> None:
|
|
@@ -218,19 +209,14 @@ def _log_with_control_api(
|
|
|
218
209
|
|
|
219
210
|
Parameters
|
|
220
211
|
----------
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
federation : str
|
|
224
|
-
Name of the federation.
|
|
225
|
-
federation_config : dict[str, Any]
|
|
226
|
-
Federation configuration dictionary.
|
|
212
|
+
superlink_connection : SuperLinkConnection
|
|
213
|
+
Superlink connection configuration.
|
|
227
214
|
run_id : int
|
|
228
215
|
The unique identifier of the run to retrieve logs from.
|
|
229
216
|
stream : bool
|
|
230
217
|
If True, stream logs continuously; if False, print once.
|
|
231
218
|
"""
|
|
232
|
-
|
|
233
|
-
channel = init_channel(app, federation_config, auth_plugin)
|
|
219
|
+
channel = init_channel_from_connection(superlink_connection)
|
|
234
220
|
|
|
235
221
|
if stream:
|
|
236
222
|
start_stream(run_id, channel, CONN_REFRESH_PERIOD)
|