flwr 1.13.1__py3-none-any.whl → 1.15.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 +5 -0
- flwr/cli/auth_plugin/__init__.py +31 -0
- flwr/cli/auth_plugin/oidc_cli_plugin.py +150 -0
- flwr/cli/build.py +1 -0
- flwr/cli/cli_user_auth_interceptor.py +90 -0
- flwr/cli/config_utils.py +43 -149
- flwr/cli/constant.py +27 -0
- flwr/cli/example.py +1 -0
- flwr/cli/install.py +2 -1
- flwr/cli/log.py +34 -37
- flwr/cli/login/__init__.py +22 -0
- flwr/cli/login/login.py +116 -0
- flwr/cli/ls.py +214 -106
- flwr/cli/new/__init__.py +1 -0
- flwr/cli/new/new.py +2 -1
- flwr/cli/new/templates/app/.gitignore.tpl +3 -0
- flwr/cli/new/templates/app/README.md.tpl +3 -2
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +4 -4
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +4 -4
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +4 -4
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +2 -2
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +3 -4
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +2 -2
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +4 -4
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +3 -3
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +2 -2
- flwr/cli/run/__init__.py +1 -0
- flwr/cli/run/run.py +103 -43
- flwr/cli/stop.py +139 -0
- flwr/cli/utils.py +186 -8
- flwr/client/app.py +49 -50
- flwr/client/client.py +1 -32
- flwr/client/clientapp/app.py +23 -26
- flwr/client/clientapp/utils.py +2 -1
- flwr/client/grpc_adapter_client/connection.py +1 -1
- flwr/client/grpc_client/connection.py +2 -13
- flwr/client/grpc_rere_client/client_interceptor.py +19 -119
- flwr/client/grpc_rere_client/connection.py +59 -43
- flwr/client/grpc_rere_client/grpc_adapter.py +12 -12
- flwr/client/message_handler/message_handler.py +1 -2
- flwr/client/message_handler/task_handler.py +0 -17
- flwr/client/mod/comms_mods.py +1 -0
- flwr/client/mod/localdp_mod.py +1 -1
- flwr/client/nodestate/__init__.py +1 -0
- flwr/client/nodestate/nodestate.py +1 -0
- flwr/client/nodestate/nodestate_factory.py +1 -0
- flwr/client/numpy_client.py +0 -44
- flwr/client/rest_client/connection.py +37 -29
- flwr/client/supernode/app.py +20 -74
- flwr/common/address.py +1 -0
- flwr/common/args.py +26 -47
- flwr/common/auth_plugin/__init__.py +24 -0
- flwr/common/auth_plugin/auth_plugin.py +122 -0
- flwr/common/config.py +169 -17
- flwr/common/constant.py +38 -9
- flwr/common/differential_privacy.py +2 -1
- flwr/common/exit/__init__.py +24 -0
- flwr/common/exit/exit.py +99 -0
- flwr/common/exit/exit_code.py +93 -0
- flwr/common/exit_handlers.py +24 -10
- flwr/common/grpc.py +167 -4
- flwr/common/logger.py +66 -7
- flwr/common/message.py +1 -0
- flwr/common/object_ref.py +57 -54
- flwr/common/pyproject.py +1 -0
- flwr/common/record/__init__.py +1 -0
- flwr/common/record/parametersrecord.py +1 -0
- flwr/common/record/recordset.py +1 -1
- flwr/common/retry_invoker.py +77 -0
- flwr/common/secure_aggregation/crypto/symmetric_encryption.py +45 -0
- flwr/common/secure_aggregation/secaggplus_utils.py +2 -2
- flwr/common/serde.py +6 -4
- flwr/common/telemetry.py +15 -4
- flwr/common/typing.py +32 -0
- flwr/common/version.py +1 -0
- flwr/proto/clientappio_pb2.py +1 -1
- flwr/proto/error_pb2.py +1 -1
- flwr/proto/exec_pb2.py +27 -15
- flwr/proto/exec_pb2.pyi +80 -2
- flwr/proto/exec_pb2_grpc.py +102 -0
- flwr/proto/exec_pb2_grpc.pyi +39 -0
- flwr/proto/fab_pb2.py +5 -5
- flwr/proto/fab_pb2.pyi +4 -1
- flwr/proto/fleet_pb2.py +31 -31
- flwr/proto/fleet_pb2.pyi +23 -23
- flwr/proto/fleet_pb2_grpc.py +30 -30
- flwr/proto/fleet_pb2_grpc.pyi +20 -20
- flwr/proto/grpcadapter_pb2.py +1 -1
- flwr/proto/log_pb2.py +1 -1
- flwr/proto/message_pb2.py +1 -1
- flwr/proto/node_pb2.py +3 -3
- flwr/proto/node_pb2.pyi +1 -4
- flwr/proto/recordset_pb2.py +1 -1
- flwr/proto/run_pb2.py +1 -1
- flwr/proto/serverappio_pb2.py +24 -25
- flwr/proto/serverappio_pb2.pyi +32 -32
- flwr/proto/serverappio_pb2_grpc.py +62 -28
- flwr/proto/serverappio_pb2_grpc.pyi +29 -16
- flwr/proto/simulationio_pb2.py +3 -3
- flwr/proto/simulationio_pb2_grpc.py +34 -0
- flwr/proto/simulationio_pb2_grpc.pyi +13 -0
- flwr/proto/task_pb2.py +1 -1
- flwr/proto/transport_pb2.py +1 -1
- flwr/server/app.py +152 -112
- flwr/server/compat/app_utils.py +7 -2
- flwr/server/compat/driver_client_proxy.py +1 -2
- flwr/server/driver/grpc_driver.py +38 -85
- flwr/server/driver/inmemory_driver.py +7 -2
- flwr/server/run_serverapp.py +8 -9
- flwr/server/serverapp/app.py +37 -13
- flwr/server/strategy/dpfedavg_fixed.py +1 -0
- flwr/server/superlink/driver/serverappio_grpc.py +2 -1
- flwr/server/superlink/driver/serverappio_servicer.py +148 -63
- flwr/server/superlink/ffs/disk_ffs.py +1 -0
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +20 -87
- flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -0
- flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +2 -165
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +56 -35
- flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +99 -169
- flwr/server/superlink/fleet/message_handler/message_handler.py +69 -29
- flwr/server/superlink/fleet/rest_rere/rest_api.py +20 -19
- flwr/server/superlink/fleet/vce/__init__.py +1 -0
- flwr/server/superlink/fleet/vce/backend/__init__.py +1 -0
- flwr/server/superlink/fleet/vce/backend/raybackend.py +1 -0
- flwr/server/superlink/fleet/vce/vce_api.py +2 -2
- flwr/server/superlink/linkstate/in_memory_linkstate.py +60 -99
- flwr/server/superlink/linkstate/linkstate.py +30 -36
- flwr/server/superlink/linkstate/sqlite_linkstate.py +105 -188
- flwr/server/superlink/linkstate/utils.py +18 -8
- flwr/server/superlink/simulation/simulationio_grpc.py +1 -1
- flwr/server/superlink/simulation/simulationio_servicer.py +33 -0
- flwr/server/superlink/utils.py +65 -0
- flwr/server/utils/validator.py +9 -34
- flwr/simulation/app.py +20 -10
- flwr/simulation/legacy_app.py +4 -2
- flwr/simulation/ray_transport/ray_actor.py +1 -0
- flwr/simulation/ray_transport/utils.py +1 -0
- flwr/simulation/run_simulation.py +36 -22
- flwr/simulation/simulationio_connection.py +5 -1
- flwr/superexec/app.py +1 -0
- flwr/superexec/deployment.py +1 -0
- flwr/superexec/exec_grpc.py +20 -2
- flwr/superexec/exec_servicer.py +97 -2
- flwr/superexec/exec_user_auth_interceptor.py +101 -0
- flwr/superexec/executor.py +1 -0
- {flwr-1.13.1.dist-info → flwr-1.15.0.dist-info}/METADATA +14 -13
- {flwr-1.13.1.dist-info → flwr-1.15.0.dist-info}/RECORD +150 -144
- flwr/proto/common_pb2.py +0 -36
- flwr/proto/common_pb2.pyi +0 -121
- flwr/proto/common_pb2_grpc.py +0 -4
- flwr/proto/common_pb2_grpc.pyi +0 -4
- flwr/proto/control_pb2.py +0 -27
- flwr/proto/control_pb2.pyi +0 -7
- flwr/proto/control_pb2_grpc.py +0 -135
- flwr/proto/control_pb2_grpc.pyi +0 -53
- {flwr-1.13.1.dist-info → flwr-1.15.0.dist-info}/LICENSE +0 -0
- {flwr-1.13.1.dist-info → flwr-1.15.0.dist-info}/WHEEL +0 -0
- {flwr-1.13.1.dist-info → flwr-1.15.0.dist-info}/entry_points.txt +0 -0
flwr/common/config.py
CHANGED
|
@@ -14,23 +14,30 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Provide functions for managing global Flower config."""
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
import os
|
|
18
19
|
import re
|
|
20
|
+
import zipfile
|
|
21
|
+
from io import BytesIO
|
|
19
22
|
from pathlib import Path
|
|
20
|
-
from typing import Any, Optional, Union, cast, get_args
|
|
23
|
+
from typing import IO, Any, Optional, TypeVar, Union, cast, get_args
|
|
21
24
|
|
|
22
25
|
import tomli
|
|
26
|
+
import typer
|
|
23
27
|
|
|
24
|
-
from flwr.cli.config_utils import get_fab_config, validate_fields
|
|
25
|
-
from flwr.common import ConfigsRecord
|
|
26
28
|
from flwr.common.constant import (
|
|
27
29
|
APP_DIR,
|
|
28
30
|
FAB_CONFIG_FILE,
|
|
29
31
|
FAB_HASH_TRUNCATION,
|
|
32
|
+
FLWR_DIR,
|
|
30
33
|
FLWR_HOME,
|
|
31
34
|
)
|
|
32
35
|
from flwr.common.typing import Run, UserConfig, UserConfigValue
|
|
33
36
|
|
|
37
|
+
from . import ConfigsRecord, object_ref
|
|
38
|
+
|
|
39
|
+
T_dict = TypeVar("T_dict", bound=dict[str, Any]) # pylint: disable=invalid-name
|
|
40
|
+
|
|
34
41
|
|
|
35
42
|
def get_flwr_dir(provided_path: Optional[str] = None) -> Path:
|
|
36
43
|
"""Return the Flower home directory based on env variables."""
|
|
@@ -38,7 +45,7 @@ def get_flwr_dir(provided_path: Optional[str] = None) -> Path:
|
|
|
38
45
|
return Path(
|
|
39
46
|
os.getenv(
|
|
40
47
|
FLWR_HOME,
|
|
41
|
-
Path(f"{os.getenv('XDG_DATA_HOME', os.getenv('HOME'))}") /
|
|
48
|
+
Path(f"{os.getenv('XDG_DATA_HOME', os.getenv('HOME'))}") / FLWR_DIR,
|
|
42
49
|
)
|
|
43
50
|
)
|
|
44
51
|
return Path(provided_path).absolute()
|
|
@@ -78,7 +85,7 @@ def get_project_config(project_dir: Union[str, Path]) -> dict[str, Any]:
|
|
|
78
85
|
config = tomli.loads(toml_file.read())
|
|
79
86
|
|
|
80
87
|
# Validate pyproject.toml fields
|
|
81
|
-
is_valid, errors, _ =
|
|
88
|
+
is_valid, errors, _ = validate_fields_in_config(config)
|
|
82
89
|
if not is_valid:
|
|
83
90
|
error_msg = "\n".join([f" - {error}" for error in errors])
|
|
84
91
|
raise ValueError(
|
|
@@ -89,19 +96,28 @@ def get_project_config(project_dir: Union[str, Path]) -> dict[str, Any]:
|
|
|
89
96
|
|
|
90
97
|
|
|
91
98
|
def fuse_dicts(
|
|
92
|
-
main_dict:
|
|
93
|
-
override_dict:
|
|
94
|
-
|
|
99
|
+
main_dict: T_dict,
|
|
100
|
+
override_dict: T_dict,
|
|
101
|
+
check_keys: bool = True,
|
|
102
|
+
) -> T_dict:
|
|
95
103
|
"""Merge a config with the overrides.
|
|
96
104
|
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
If `check_keys` is set to True, an error will be raised if the override
|
|
106
|
+
dictionary contains keys that are not present in the main dictionary.
|
|
107
|
+
Otherwise, only the keys present in the main dictionary will be updated.
|
|
99
108
|
"""
|
|
100
|
-
|
|
109
|
+
if not isinstance(main_dict, dict) or not isinstance(override_dict, dict):
|
|
110
|
+
raise ValueError("Both dictionaries must be of type dict")
|
|
111
|
+
|
|
112
|
+
fused_dict = cast(T_dict, main_dict.copy())
|
|
101
113
|
|
|
102
114
|
for key, value in override_dict.items():
|
|
103
115
|
if key in main_dict:
|
|
116
|
+
if isinstance(value, dict):
|
|
117
|
+
fused_dict[key] = fuse_dicts(main_dict[key], value)
|
|
104
118
|
fused_dict[key] = value
|
|
119
|
+
elif check_keys:
|
|
120
|
+
raise ValueError(f"Key '{key}' is not present in the main dictionary")
|
|
105
121
|
|
|
106
122
|
return fused_dict
|
|
107
123
|
|
|
@@ -190,8 +206,8 @@ def unflatten_dict(flat_dict: dict[str, Any]) -> dict[str, Any]:
|
|
|
190
206
|
|
|
191
207
|
|
|
192
208
|
def parse_config_args(
|
|
193
|
-
config: Optional[list[str]],
|
|
194
|
-
) ->
|
|
209
|
+
config: Optional[list[str]], flatten: bool = True
|
|
210
|
+
) -> dict[str, Any]:
|
|
195
211
|
"""Parse separator separated list of key-value pairs separated by '='."""
|
|
196
212
|
overrides: UserConfig = {}
|
|
197
213
|
|
|
@@ -218,17 +234,29 @@ def parse_config_args(
|
|
|
218
234
|
|
|
219
235
|
matches = pattern.findall(config_line)
|
|
220
236
|
toml_str = "\n".join(f"{k} = {v}" for k, v in matches)
|
|
221
|
-
|
|
222
|
-
|
|
237
|
+
try:
|
|
238
|
+
overrides.update(tomli.loads(toml_str))
|
|
239
|
+
flat_overrides = flatten_dict(overrides) if flatten else overrides
|
|
240
|
+
except tomli.TOMLDecodeError as err:
|
|
241
|
+
typer.secho(
|
|
242
|
+
"❌ The provided configuration string is in an invalid format. "
|
|
243
|
+
"The correct format should be, e.g., 'key1=123 key2=false "
|
|
244
|
+
'key3="string"\', where values must be of type bool, int, '
|
|
245
|
+
"string, or float. Ensure proper formatting with "
|
|
246
|
+
"space-separated key-value pairs.",
|
|
247
|
+
fg=typer.colors.RED,
|
|
248
|
+
bold=True,
|
|
249
|
+
)
|
|
250
|
+
raise typer.Exit(code=1) from err
|
|
223
251
|
|
|
224
252
|
return flat_overrides
|
|
225
253
|
|
|
226
254
|
|
|
227
255
|
def get_metadata_from_config(config: dict[str, Any]) -> tuple[str, str]:
|
|
228
|
-
"""Extract `
|
|
256
|
+
"""Extract `fab_id` and `fab_version` from a project config."""
|
|
229
257
|
return (
|
|
230
|
-
config["project"]["version"],
|
|
231
258
|
f"{config['tool']['flwr']['app']['publisher']}/{config['project']['name']}",
|
|
259
|
+
config["project"]["version"],
|
|
232
260
|
)
|
|
233
261
|
|
|
234
262
|
|
|
@@ -239,3 +267,127 @@ def user_config_to_configsrecord(config: UserConfig) -> ConfigsRecord:
|
|
|
239
267
|
c_record[k] = v
|
|
240
268
|
|
|
241
269
|
return c_record
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def get_fab_config(fab_file: Union[Path, bytes]) -> dict[str, Any]:
|
|
273
|
+
"""Extract the config from a FAB file or path.
|
|
274
|
+
|
|
275
|
+
Parameters
|
|
276
|
+
----------
|
|
277
|
+
fab_file : Union[Path, bytes]
|
|
278
|
+
The Flower App Bundle file to validate and extract the metadata from.
|
|
279
|
+
It can either be a path to the file or the file itself as bytes.
|
|
280
|
+
|
|
281
|
+
Returns
|
|
282
|
+
-------
|
|
283
|
+
Dict[str, Any]
|
|
284
|
+
The `config` of the given Flower App Bundle.
|
|
285
|
+
"""
|
|
286
|
+
fab_file_archive: Union[Path, IO[bytes]]
|
|
287
|
+
if isinstance(fab_file, bytes):
|
|
288
|
+
fab_file_archive = BytesIO(fab_file)
|
|
289
|
+
elif isinstance(fab_file, Path):
|
|
290
|
+
fab_file_archive = fab_file
|
|
291
|
+
else:
|
|
292
|
+
raise ValueError("fab_file must be either a Path or bytes")
|
|
293
|
+
|
|
294
|
+
with zipfile.ZipFile(fab_file_archive, "r") as zipf:
|
|
295
|
+
with zipf.open("pyproject.toml") as file:
|
|
296
|
+
toml_content = file.read().decode("utf-8")
|
|
297
|
+
try:
|
|
298
|
+
conf = tomli.loads(toml_content)
|
|
299
|
+
except tomli.TOMLDecodeError:
|
|
300
|
+
raise ValueError("Invalid TOML content in pyproject.toml") from None
|
|
301
|
+
|
|
302
|
+
is_valid, errors, _ = validate_config(conf, check_module=False)
|
|
303
|
+
if not is_valid:
|
|
304
|
+
raise ValueError(errors)
|
|
305
|
+
|
|
306
|
+
return conf
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _validate_run_config(config_dict: dict[str, Any], errors: list[str]) -> None:
|
|
310
|
+
for key, value in config_dict.items():
|
|
311
|
+
if isinstance(value, dict):
|
|
312
|
+
_validate_run_config(config_dict[key], errors)
|
|
313
|
+
elif not isinstance(value, get_args(UserConfigValue)):
|
|
314
|
+
raise ValueError(
|
|
315
|
+
f"The value for key {key} needs to be of type `int`, `float`, "
|
|
316
|
+
"`bool, `str`, or a `dict` of those.",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# pylint: disable=too-many-branches
|
|
321
|
+
def validate_fields_in_config(
|
|
322
|
+
config: dict[str, Any]
|
|
323
|
+
) -> tuple[bool, list[str], list[str]]:
|
|
324
|
+
"""Validate pyproject.toml fields."""
|
|
325
|
+
errors = []
|
|
326
|
+
warnings = []
|
|
327
|
+
|
|
328
|
+
if "project" not in config:
|
|
329
|
+
errors.append("Missing [project] section")
|
|
330
|
+
else:
|
|
331
|
+
if "name" not in config["project"]:
|
|
332
|
+
errors.append('Property "name" missing in [project]')
|
|
333
|
+
if "version" not in config["project"]:
|
|
334
|
+
errors.append('Property "version" missing in [project]')
|
|
335
|
+
if "description" not in config["project"]:
|
|
336
|
+
warnings.append('Recommended property "description" missing in [project]')
|
|
337
|
+
if "license" not in config["project"]:
|
|
338
|
+
warnings.append('Recommended property "license" missing in [project]')
|
|
339
|
+
if "authors" not in config["project"]:
|
|
340
|
+
warnings.append('Recommended property "authors" missing in [project]')
|
|
341
|
+
|
|
342
|
+
if (
|
|
343
|
+
"tool" not in config
|
|
344
|
+
or "flwr" not in config["tool"]
|
|
345
|
+
or "app" not in config["tool"]["flwr"]
|
|
346
|
+
):
|
|
347
|
+
errors.append("Missing [tool.flwr.app] section")
|
|
348
|
+
else:
|
|
349
|
+
if "publisher" not in config["tool"]["flwr"]["app"]:
|
|
350
|
+
errors.append('Property "publisher" missing in [tool.flwr.app]')
|
|
351
|
+
if "config" in config["tool"]["flwr"]["app"]:
|
|
352
|
+
_validate_run_config(config["tool"]["flwr"]["app"]["config"], errors)
|
|
353
|
+
if "components" not in config["tool"]["flwr"]["app"]:
|
|
354
|
+
errors.append("Missing [tool.flwr.app.components] section")
|
|
355
|
+
else:
|
|
356
|
+
if "serverapp" not in config["tool"]["flwr"]["app"]["components"]:
|
|
357
|
+
errors.append(
|
|
358
|
+
'Property "serverapp" missing in [tool.flwr.app.components]'
|
|
359
|
+
)
|
|
360
|
+
if "clientapp" not in config["tool"]["flwr"]["app"]["components"]:
|
|
361
|
+
errors.append(
|
|
362
|
+
'Property "clientapp" missing in [tool.flwr.app.components]'
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
return len(errors) == 0, errors, warnings
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def validate_config(
|
|
369
|
+
config: dict[str, Any],
|
|
370
|
+
check_module: bool = True,
|
|
371
|
+
project_dir: Optional[Union[str, Path]] = None,
|
|
372
|
+
) -> tuple[bool, list[str], list[str]]:
|
|
373
|
+
"""Validate pyproject.toml."""
|
|
374
|
+
is_valid, errors, warnings = validate_fields_in_config(config)
|
|
375
|
+
|
|
376
|
+
if not is_valid:
|
|
377
|
+
return False, errors, warnings
|
|
378
|
+
|
|
379
|
+
# Validate serverapp
|
|
380
|
+
serverapp_ref = config["tool"]["flwr"]["app"]["components"]["serverapp"]
|
|
381
|
+
is_valid, reason = object_ref.validate(serverapp_ref, check_module, project_dir)
|
|
382
|
+
|
|
383
|
+
if not is_valid and isinstance(reason, str):
|
|
384
|
+
return False, [reason], []
|
|
385
|
+
|
|
386
|
+
# Validate clientapp
|
|
387
|
+
clientapp_ref = config["tool"]["flwr"]["app"]["components"]["clientapp"]
|
|
388
|
+
is_valid, reason = object_ref.validate(clientapp_ref, check_module, project_dir)
|
|
389
|
+
|
|
390
|
+
if not is_valid and isinstance(reason, str):
|
|
391
|
+
return False, [reason], []
|
|
392
|
+
|
|
393
|
+
return True, [], []
|
flwr/common/constant.py
CHANGED
|
@@ -17,14 +17,6 @@
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
MISSING_EXTRA_REST = """
|
|
21
|
-
Extra dependencies required for using the REST-based Fleet API are missing.
|
|
22
|
-
|
|
23
|
-
To use the REST API, install `flwr` with the `rest` extra:
|
|
24
|
-
|
|
25
|
-
`pip install flwr[rest]`.
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
20
|
TRANSPORT_TYPE_GRPC_BIDI = "grpc-bidi"
|
|
29
21
|
TRANSPORT_TYPE_GRPC_RERE = "grpc-rere"
|
|
30
22
|
TRANSPORT_TYPE_GRPC_ADAPTER = "grpc-adapter"
|
|
@@ -80,7 +72,11 @@ FAB_ALLOWED_EXTENSIONS = {".py", ".toml", ".md"}
|
|
|
80
72
|
FAB_CONFIG_FILE = "pyproject.toml"
|
|
81
73
|
FAB_DATE = (2024, 10, 1, 0, 0, 0)
|
|
82
74
|
FAB_HASH_TRUNCATION = 8
|
|
83
|
-
|
|
75
|
+
FLWR_DIR = ".flwr" # The default Flower directory: ~/.flwr/
|
|
76
|
+
FLWR_HOME = "FLWR_HOME" # If set, override the default Flower directory
|
|
77
|
+
|
|
78
|
+
# Constant for SuperLink
|
|
79
|
+
SUPERLINK_NODE_ID = 1
|
|
84
80
|
|
|
85
81
|
# Constants entries in Node config for Simulation
|
|
86
82
|
PARTITION_ID_KEY = "partition-id"
|
|
@@ -110,6 +106,18 @@ LOG_UPLOAD_INTERVAL = 0.2 # Minimum interval between two log uploads
|
|
|
110
106
|
# Retry configurations
|
|
111
107
|
MAX_RETRY_DELAY = 20 # Maximum delay duration between two consecutive retries.
|
|
112
108
|
|
|
109
|
+
# Constants for user authentication
|
|
110
|
+
CREDENTIALS_DIR = ".credentials"
|
|
111
|
+
AUTH_TYPE_KEY = "auth_type"
|
|
112
|
+
ACCESS_TOKEN_KEY = "access_token"
|
|
113
|
+
REFRESH_TOKEN_KEY = "refresh_token"
|
|
114
|
+
|
|
115
|
+
# Constants for node authentication
|
|
116
|
+
PUBLIC_KEY_HEADER = "public-key-bin" # Must end with "-bin" for binary data
|
|
117
|
+
SIGNATURE_HEADER = "signature-bin" # Must end with "-bin" for binary data
|
|
118
|
+
TIMESTAMP_HEADER = "timestamp"
|
|
119
|
+
TIMESTAMP_TOLERANCE = 10 # Tolerance for timestamp verification
|
|
120
|
+
|
|
113
121
|
|
|
114
122
|
class MessageType:
|
|
115
123
|
"""Message type."""
|
|
@@ -181,3 +189,24 @@ class SubStatus:
|
|
|
181
189
|
def __new__(cls) -> SubStatus:
|
|
182
190
|
"""Prevent instantiation."""
|
|
183
191
|
raise TypeError(f"{cls.__name__} cannot be instantiated.")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class CliOutputFormat:
|
|
195
|
+
"""Define output format for `flwr` CLI commands."""
|
|
196
|
+
|
|
197
|
+
DEFAULT = "default"
|
|
198
|
+
JSON = "json"
|
|
199
|
+
|
|
200
|
+
def __new__(cls) -> CliOutputFormat:
|
|
201
|
+
"""Prevent instantiation."""
|
|
202
|
+
raise TypeError(f"{cls.__name__} cannot be instantiated.")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class AuthType:
|
|
206
|
+
"""User authentication types."""
|
|
207
|
+
|
|
208
|
+
OIDC = "oidc"
|
|
209
|
+
|
|
210
|
+
def __new__(cls) -> AuthType:
|
|
211
|
+
"""Prevent instantiation."""
|
|
212
|
+
raise TypeError(f"{cls.__name__} cannot be instantiated.")
|
|
@@ -39,7 +39,8 @@ def get_norm(input_arrays: NDArrays) -> float:
|
|
|
39
39
|
def add_gaussian_noise_inplace(input_arrays: NDArrays, std_dev: float) -> None:
|
|
40
40
|
"""Add Gaussian noise to each element of the input arrays."""
|
|
41
41
|
for array in input_arrays:
|
|
42
|
-
|
|
42
|
+
noise = np.random.normal(0, std_dev, array.shape).astype(array.dtype)
|
|
43
|
+
array += noise
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
def clip_inputs_inplace(input_arrays: NDArrays, clipping_norm: float) -> None:
|
|
@@ -0,0 +1,24 @@
|
|
|
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 exit functionality."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from .exit import flwr_exit
|
|
19
|
+
from .exit_code import ExitCode
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"ExitCode",
|
|
23
|
+
"flwr_exit",
|
|
24
|
+
]
|
flwr/common/exit/exit.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
"""Unified exit function."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import sys
|
|
21
|
+
from logging import ERROR, INFO
|
|
22
|
+
from typing import Any, NoReturn
|
|
23
|
+
|
|
24
|
+
from flwr.common import EventType, event
|
|
25
|
+
|
|
26
|
+
from ..logger import log
|
|
27
|
+
from .exit_code import EXIT_CODE_HELP
|
|
28
|
+
|
|
29
|
+
HELP_PAGE_URL = "https://flower.ai/docs/framework/ref-exit-codes/"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def flwr_exit(
|
|
33
|
+
code: int,
|
|
34
|
+
message: str | None = None,
|
|
35
|
+
event_type: EventType | None = None,
|
|
36
|
+
event_details: dict[str, Any] | None = None,
|
|
37
|
+
) -> NoReturn:
|
|
38
|
+
"""Handle application exit with an optional message.
|
|
39
|
+
|
|
40
|
+
The exit message logged and displayed will follow this structure:
|
|
41
|
+
|
|
42
|
+
>>> Exit Code: <code>
|
|
43
|
+
>>> <message>
|
|
44
|
+
>>> <short-help-message>
|
|
45
|
+
>>>
|
|
46
|
+
>>> For more information, visit: <help-page-url>
|
|
47
|
+
|
|
48
|
+
- `<code>`: The unique exit code representing the termination reason.
|
|
49
|
+
- `<message>`: Optional context or additional information about the exit.
|
|
50
|
+
- `<short-help-message>`: A brief explanation for the given exit code.
|
|
51
|
+
- `<help-page-url>`: A URL providing detailed documentation and resolution steps.
|
|
52
|
+
"""
|
|
53
|
+
is_error = not 0 <= code < 100 # 0-99 are success exit codes
|
|
54
|
+
|
|
55
|
+
# Construct exit message
|
|
56
|
+
exit_message = f"Exit Code: {code}\n" if is_error else ""
|
|
57
|
+
exit_message += message or ""
|
|
58
|
+
if short_help_message := EXIT_CODE_HELP.get(code, ""):
|
|
59
|
+
exit_message += f"\n{short_help_message}"
|
|
60
|
+
|
|
61
|
+
# Set log level and system exit code
|
|
62
|
+
log_level = ERROR if is_error else INFO
|
|
63
|
+
sys_exit_code = 1 if is_error else 0
|
|
64
|
+
|
|
65
|
+
# Add help URL for non-successful/graceful exits
|
|
66
|
+
if is_error:
|
|
67
|
+
help_url = f"{HELP_PAGE_URL}{code}.html"
|
|
68
|
+
exit_message += f"\n\nFor more information, visit: <{help_url}>"
|
|
69
|
+
|
|
70
|
+
# Telemetry event
|
|
71
|
+
event_type = event_type or _try_obtain_telemetry_event()
|
|
72
|
+
if event_type:
|
|
73
|
+
event_details = event_details or {}
|
|
74
|
+
event_details["exit_code"] = code
|
|
75
|
+
event(event_type, event_details).result()
|
|
76
|
+
|
|
77
|
+
# Log the exit message
|
|
78
|
+
log(log_level, exit_message)
|
|
79
|
+
|
|
80
|
+
# Exit
|
|
81
|
+
sys.exit(sys_exit_code)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# pylint: disable-next=too-many-return-statements
|
|
85
|
+
def _try_obtain_telemetry_event() -> EventType | None:
|
|
86
|
+
"""Try to obtain a telemetry event."""
|
|
87
|
+
if sys.argv[0].endswith("flower-superlink"):
|
|
88
|
+
return EventType.RUN_SUPERLINK_LEAVE
|
|
89
|
+
if sys.argv[0].endswith("flower-supernode"):
|
|
90
|
+
return EventType.RUN_SUPERNODE_LEAVE
|
|
91
|
+
if sys.argv[0].endswith("flwr-serverapp"):
|
|
92
|
+
return EventType.FLWR_SERVERAPP_RUN_LEAVE
|
|
93
|
+
if sys.argv[0].endswith("flwr-clientapp"):
|
|
94
|
+
return None # Not yet implemented
|
|
95
|
+
if sys.argv[0].endswith("flwr-simulation"):
|
|
96
|
+
return EventType.FLWR_SIMULATION_RUN_LEAVE
|
|
97
|
+
if sys.argv[0].endswith("flower-simulation"):
|
|
98
|
+
return EventType.CLI_FLOWER_SIMULATION_LEAVE
|
|
99
|
+
return None
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
"""Exit codes."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ExitCode:
|
|
22
|
+
"""Exit codes for Flower components."""
|
|
23
|
+
|
|
24
|
+
# Success exit codes (0-99)
|
|
25
|
+
SUCCESS = 0 # Successful exit without any errors or signals
|
|
26
|
+
GRACEFUL_EXIT_SIGINT = 1 # Graceful exit triggered by SIGINT
|
|
27
|
+
GRACEFUL_EXIT_SIGQUIT = 2 # Graceful exit triggered by SIGQUIT
|
|
28
|
+
GRACEFUL_EXIT_SIGTERM = 3 # Graceful exit triggered by SIGTERM
|
|
29
|
+
|
|
30
|
+
# SuperLink-specific exit codes (100-199)
|
|
31
|
+
SUPERLINK_THREAD_CRASH = 100
|
|
32
|
+
|
|
33
|
+
# ServerApp-specific exit codes (200-299)
|
|
34
|
+
|
|
35
|
+
# SuperNode-specific exit codes (300-399)
|
|
36
|
+
SUPERNODE_REST_ADDRESS_INVALID = 300
|
|
37
|
+
SUPERNODE_NODE_AUTH_KEYS_REQUIRED = 301
|
|
38
|
+
SUPERNODE_NODE_AUTH_KEYS_INVALID = 302
|
|
39
|
+
|
|
40
|
+
# ClientApp-specific exit codes (400-499)
|
|
41
|
+
|
|
42
|
+
# Simulation-specific exit codes (500-599)
|
|
43
|
+
|
|
44
|
+
# Common exit codes (600-)
|
|
45
|
+
COMMON_ADDRESS_INVALID = 600
|
|
46
|
+
COMMON_MISSING_EXTRA_REST = 601
|
|
47
|
+
COMMON_TLS_NOT_SUPPORTED = 602
|
|
48
|
+
|
|
49
|
+
def __new__(cls) -> ExitCode:
|
|
50
|
+
"""Prevent instantiation."""
|
|
51
|
+
raise TypeError(f"{cls.__name__} cannot be instantiated.")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# All short help messages for exit codes
|
|
55
|
+
EXIT_CODE_HELP = {
|
|
56
|
+
# Success exit codes (0-99)
|
|
57
|
+
ExitCode.SUCCESS: "",
|
|
58
|
+
ExitCode.GRACEFUL_EXIT_SIGINT: "",
|
|
59
|
+
ExitCode.GRACEFUL_EXIT_SIGQUIT: "",
|
|
60
|
+
ExitCode.GRACEFUL_EXIT_SIGTERM: "",
|
|
61
|
+
# SuperLink-specific exit codes (100-199)
|
|
62
|
+
ExitCode.SUPERLINK_THREAD_CRASH: "An important background thread has crashed.",
|
|
63
|
+
# ServerApp-specific exit codes (200-299)
|
|
64
|
+
# SuperNode-specific exit codes (300-399)
|
|
65
|
+
ExitCode.SUPERNODE_REST_ADDRESS_INVALID: (
|
|
66
|
+
"When using the REST API, please provide `https://` or "
|
|
67
|
+
"`http://` before the server address (e.g. `http://127.0.0.1:8080`)"
|
|
68
|
+
),
|
|
69
|
+
ExitCode.SUPERNODE_NODE_AUTH_KEYS_REQUIRED: (
|
|
70
|
+
"Node authentication requires file paths to both "
|
|
71
|
+
"'--auth-supernode-private-key' and '--auth-supernode-public-key' "
|
|
72
|
+
"to be provided (providing only one of them is not sufficient)."
|
|
73
|
+
),
|
|
74
|
+
ExitCode.SUPERNODE_NODE_AUTH_KEYS_INVALID: (
|
|
75
|
+
"Node uthentication requires elliptic curve private and public key pair. "
|
|
76
|
+
"Please ensure that the file path points to a valid private/public key "
|
|
77
|
+
"file and try again."
|
|
78
|
+
),
|
|
79
|
+
# ClientApp-specific exit codes (400-499)
|
|
80
|
+
# Simulation-specific exit codes (500-599)
|
|
81
|
+
# Common exit codes (600-)
|
|
82
|
+
ExitCode.COMMON_ADDRESS_INVALID: (
|
|
83
|
+
"Please provide a valid URL, IPv4 or IPv6 address."
|
|
84
|
+
),
|
|
85
|
+
ExitCode.COMMON_MISSING_EXTRA_REST: """
|
|
86
|
+
Extra dependencies required for using the REST-based Fleet API are missing.
|
|
87
|
+
|
|
88
|
+
To use the REST API, install `flwr` with the `rest` extra:
|
|
89
|
+
|
|
90
|
+
`pip install "flwr[rest]"`.
|
|
91
|
+
""",
|
|
92
|
+
ExitCode.COMMON_TLS_NOT_SUPPORTED: "Please use the '--insecure' flag.",
|
|
93
|
+
}
|
flwr/common/exit_handlers.py
CHANGED
|
@@ -15,28 +15,38 @@
|
|
|
15
15
|
"""Common function to register exit handlers for server and client."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
import
|
|
19
|
-
from signal import SIGINT, SIGTERM, signal
|
|
18
|
+
from signal import SIGINT, SIGQUIT, SIGTERM, signal
|
|
20
19
|
from threading import Thread
|
|
21
20
|
from types import FrameType
|
|
22
21
|
from typing import Optional
|
|
23
22
|
|
|
24
23
|
from grpc import Server
|
|
25
24
|
|
|
26
|
-
from flwr.common.telemetry import EventType
|
|
25
|
+
from flwr.common.telemetry import EventType
|
|
26
|
+
|
|
27
|
+
from .exit import ExitCode, flwr_exit
|
|
28
|
+
|
|
29
|
+
SIGNAL_TO_EXIT_CODE = {
|
|
30
|
+
SIGINT: ExitCode.GRACEFUL_EXIT_SIGINT,
|
|
31
|
+
SIGQUIT: ExitCode.GRACEFUL_EXIT_SIGQUIT,
|
|
32
|
+
SIGTERM: ExitCode.GRACEFUL_EXIT_SIGTERM,
|
|
33
|
+
}
|
|
27
34
|
|
|
28
35
|
|
|
29
36
|
def register_exit_handlers(
|
|
30
37
|
event_type: EventType,
|
|
38
|
+
exit_message: Optional[str] = None,
|
|
31
39
|
grpc_servers: Optional[list[Server]] = None,
|
|
32
40
|
bckg_threads: Optional[list[Thread]] = None,
|
|
33
41
|
) -> None:
|
|
34
|
-
"""Register exit handlers for `SIGINT` and `
|
|
42
|
+
"""Register exit handlers for `SIGINT`, `SIGTERM` and `SIGQUIT` signals.
|
|
35
43
|
|
|
36
44
|
Parameters
|
|
37
45
|
----------
|
|
38
46
|
event_type : EventType
|
|
39
47
|
The telemetry event that should be logged before exit.
|
|
48
|
+
exit_message : Optional[str] (default: None)
|
|
49
|
+
The message to be logged before exiting.
|
|
40
50
|
grpc_servers: Optional[List[Server]] (default: None)
|
|
41
51
|
An otpional list of gRPC servers that need to be gracefully
|
|
42
52
|
terminated before exiting.
|
|
@@ -46,6 +56,7 @@ def register_exit_handlers(
|
|
|
46
56
|
"""
|
|
47
57
|
default_handlers = {
|
|
48
58
|
SIGINT: None,
|
|
59
|
+
SIGQUIT: None,
|
|
49
60
|
SIGTERM: None,
|
|
50
61
|
}
|
|
51
62
|
|
|
@@ -61,8 +72,6 @@ def register_exit_handlers(
|
|
|
61
72
|
# Reset to default handler
|
|
62
73
|
signal(signalnum, default_handlers[signalnum])
|
|
63
74
|
|
|
64
|
-
event_res = event(event_type=event_type)
|
|
65
|
-
|
|
66
75
|
if grpc_servers is not None:
|
|
67
76
|
for grpc_server in grpc_servers:
|
|
68
77
|
grpc_server.stop(grace=1)
|
|
@@ -71,16 +80,21 @@ def register_exit_handlers(
|
|
|
71
80
|
for bckg_thread in bckg_threads:
|
|
72
81
|
bckg_thread.join()
|
|
73
82
|
|
|
74
|
-
# Ensure event has happend
|
|
75
|
-
event_res.result()
|
|
76
|
-
|
|
77
83
|
# Setup things for graceful exit
|
|
78
|
-
|
|
84
|
+
flwr_exit(
|
|
85
|
+
code=SIGNAL_TO_EXIT_CODE[signalnum],
|
|
86
|
+
message=exit_message,
|
|
87
|
+
event_type=event_type,
|
|
88
|
+
)
|
|
79
89
|
|
|
80
90
|
default_handlers[SIGINT] = signal( # type: ignore
|
|
81
91
|
SIGINT,
|
|
82
92
|
graceful_exit_handler, # type: ignore
|
|
83
93
|
)
|
|
94
|
+
default_handlers[SIGQUIT] = signal( # type: ignore
|
|
95
|
+
SIGQUIT,
|
|
96
|
+
graceful_exit_handler, # type: ignore
|
|
97
|
+
)
|
|
84
98
|
default_handlers[SIGTERM] = signal( # type: ignore
|
|
85
99
|
SIGTERM,
|
|
86
100
|
graceful_exit_handler, # type: ignore
|