flwr 1.17.0__py3-none-any.whl → 1.19.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 +15 -0
- flwr/app/error.py +68 -0
- flwr/app/metadata.py +223 -0
- flwr/cli/__init__.py +1 -1
- flwr/cli/app.py +21 -2
- flwr/cli/build.py +83 -58
- flwr/cli/cli_user_auth_interceptor.py +1 -1
- flwr/cli/config_utils.py +53 -17
- flwr/cli/example.py +1 -1
- flwr/cli/install.py +1 -1
- flwr/cli/log.py +4 -4
- flwr/cli/login/__init__.py +1 -1
- flwr/cli/login/login.py +15 -8
- flwr/cli/ls.py +16 -37
- flwr/cli/new/__init__.py +1 -1
- flwr/cli/new/new.py +4 -4
- flwr/cli/new/templates/__init__.py +1 -1
- flwr/cli/new/templates/app/__init__.py +1 -1
- flwr/cli/new/templates/app/code/__init__.py +1 -1
- flwr/cli/new/templates/app/code/client.baseline.py.tpl +1 -1
- flwr/cli/new/templates/app/code/flwr_tune/__init__.py +1 -1
- flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +4 -4
- flwr/cli/new/templates/app/code/model.baseline.py.tpl +1 -1
- flwr/cli/new/templates/app/code/server.baseline.py.tpl +2 -3
- flwr/cli/new/templates/app/code/task.sklearn.py.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +14 -17
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +4 -4
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
- flwr/cli/run/__init__.py +1 -1
- flwr/cli/run/run.py +11 -19
- flwr/cli/stop.py +3 -3
- flwr/cli/utils.py +42 -17
- flwr/client/__init__.py +3 -3
- flwr/client/client.py +1 -1
- flwr/client/client_app.py +140 -138
- flwr/client/clientapp/__init__.py +1 -8
- flwr/client/clientapp/utils.py +1 -1
- flwr/client/dpfedavg_numpy_client.py +1 -1
- flwr/client/grpc_adapter_client/__init__.py +1 -1
- flwr/client/grpc_adapter_client/connection.py +5 -5
- flwr/client/grpc_rere_client/__init__.py +1 -1
- flwr/client/grpc_rere_client/client_interceptor.py +1 -1
- flwr/client/grpc_rere_client/connection.py +131 -61
- flwr/client/grpc_rere_client/grpc_adapter.py +35 -7
- flwr/client/message_handler/__init__.py +1 -1
- flwr/client/message_handler/message_handler.py +2 -2
- flwr/client/mod/__init__.py +1 -1
- flwr/client/mod/centraldp_mods.py +1 -1
- flwr/client/mod/comms_mods.py +39 -20
- flwr/client/mod/localdp_mod.py +6 -6
- flwr/client/mod/secure_aggregation/__init__.py +1 -1
- flwr/client/mod/secure_aggregation/secagg_mod.py +1 -1
- flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
- flwr/client/mod/utils.py +1 -1
- flwr/client/numpy_client.py +1 -1
- flwr/client/rest_client/__init__.py +1 -1
- flwr/client/rest_client/connection.py +174 -68
- flwr/client/run_info_store.py +1 -1
- flwr/client/typing.py +1 -1
- flwr/clientapp/__init__.py +15 -0
- flwr/common/__init__.py +3 -3
- flwr/common/address.py +1 -1
- flwr/common/args.py +1 -1
- flwr/common/auth_plugin/__init__.py +3 -1
- flwr/common/auth_plugin/auth_plugin.py +30 -4
- flwr/common/config.py +1 -1
- flwr/common/constant.py +37 -8
- flwr/common/context.py +1 -1
- flwr/common/date.py +1 -1
- flwr/common/differential_privacy.py +1 -1
- flwr/common/differential_privacy_constants.py +1 -1
- flwr/common/dp.py +1 -1
- flwr/common/event_log_plugin/event_log_plugin.py +3 -3
- flwr/common/exit/exit.py +6 -6
- flwr/common/exit_handlers.py +31 -1
- flwr/common/grpc.py +1 -1
- flwr/common/heartbeat.py +165 -0
- flwr/common/inflatable.py +290 -0
- flwr/common/inflatable_grpc_utils.py +99 -0
- flwr/common/inflatable_rest_utils.py +99 -0
- flwr/common/inflatable_utils.py +341 -0
- flwr/common/logger.py +1 -1
- flwr/common/message.py +137 -252
- flwr/common/object_ref.py +1 -1
- flwr/common/parameter.py +1 -1
- flwr/common/pyproject.py +1 -1
- flwr/common/record/__init__.py +3 -2
- flwr/common/record/array.py +323 -0
- flwr/common/record/arrayrecord.py +121 -243
- flwr/common/record/configrecord.py +71 -16
- flwr/common/record/conversion_utils.py +2 -2
- flwr/common/record/metricrecord.py +71 -20
- flwr/common/record/recorddict.py +207 -90
- flwr/common/record/typeddict.py +1 -1
- flwr/common/recorddict_compat.py +2 -2
- flwr/common/retry_invoker.py +15 -11
- flwr/common/secure_aggregation/__init__.py +1 -1
- flwr/common/secure_aggregation/crypto/__init__.py +1 -1
- flwr/common/secure_aggregation/crypto/shamir.py +52 -30
- flwr/common/secure_aggregation/crypto/symmetric_encryption.py +1 -1
- flwr/common/secure_aggregation/ndarrays_arithmetic.py +1 -1
- flwr/common/secure_aggregation/quantization.py +1 -1
- flwr/common/secure_aggregation/secaggplus_constants.py +1 -1
- flwr/common/secure_aggregation/secaggplus_utils.py +1 -1
- flwr/common/serde.py +60 -184
- flwr/common/serde_utils.py +175 -0
- flwr/common/telemetry.py +2 -2
- flwr/common/typing.py +6 -4
- flwr/common/version.py +1 -1
- flwr/compat/__init__.py +15 -0
- flwr/compat/client/__init__.py +15 -0
- flwr/{client → compat/client}/app.py +71 -211
- flwr/{client → compat/client}/grpc_client/__init__.py +1 -1
- flwr/{client → compat/client}/grpc_client/connection.py +13 -13
- flwr/compat/common/__init__.py +15 -0
- flwr/compat/server/__init__.py +15 -0
- flwr/compat/server/app.py +174 -0
- flwr/compat/simulation/__init__.py +15 -0
- flwr/proto/__init__.py +1 -1
- flwr/proto/fleet_pb2.py +32 -27
- flwr/proto/fleet_pb2.pyi +49 -35
- flwr/proto/fleet_pb2_grpc.py +117 -13
- flwr/proto/fleet_pb2_grpc.pyi +47 -6
- flwr/proto/heartbeat_pb2.py +33 -0
- flwr/proto/heartbeat_pb2.pyi +66 -0
- flwr/proto/heartbeat_pb2_grpc.py +4 -0
- flwr/proto/heartbeat_pb2_grpc.pyi +4 -0
- flwr/proto/message_pb2.py +28 -11
- flwr/proto/message_pb2.pyi +125 -0
- flwr/proto/recorddict_pb2.py +16 -28
- flwr/proto/recorddict_pb2.pyi +46 -64
- flwr/proto/run_pb2.py +24 -32
- flwr/proto/run_pb2.pyi +4 -52
- flwr/proto/serverappio_pb2.py +32 -23
- flwr/proto/serverappio_pb2.pyi +45 -3
- flwr/proto/serverappio_pb2_grpc.py +138 -34
- flwr/proto/serverappio_pb2_grpc.pyi +54 -13
- flwr/proto/simulationio_pb2.py +12 -11
- flwr/proto/simulationio_pb2_grpc.py +35 -0
- flwr/proto/simulationio_pb2_grpc.pyi +14 -0
- flwr/server/__init__.py +2 -2
- flwr/server/app.py +69 -187
- flwr/server/client_manager.py +1 -1
- flwr/server/client_proxy.py +1 -1
- flwr/server/compat/__init__.py +1 -1
- flwr/server/compat/app.py +1 -1
- flwr/server/compat/app_utils.py +51 -29
- flwr/server/compat/legacy_context.py +1 -1
- flwr/server/criterion.py +1 -1
- flwr/server/fleet_event_log_interceptor.py +2 -2
- flwr/server/grid/grid.py +3 -3
- flwr/server/grid/grpc_grid.py +104 -34
- flwr/server/grid/inmemory_grid.py +5 -4
- flwr/server/history.py +1 -1
- flwr/server/run_serverapp.py +1 -1
- flwr/server/server.py +1 -1
- flwr/server/server_app.py +65 -58
- flwr/server/server_config.py +1 -1
- flwr/server/serverapp/__init__.py +1 -1
- flwr/server/serverapp/app.py +19 -1
- flwr/server/serverapp_components.py +1 -1
- flwr/server/strategy/__init__.py +1 -1
- flwr/server/strategy/aggregate.py +1 -1
- flwr/server/strategy/bulyan.py +2 -2
- flwr/server/strategy/dp_adaptive_clipping.py +17 -17
- flwr/server/strategy/dp_fixed_clipping.py +17 -17
- flwr/server/strategy/dpfedavg_adaptive.py +1 -1
- flwr/server/strategy/dpfedavg_fixed.py +1 -1
- flwr/server/strategy/fault_tolerant_fedavg.py +1 -1
- flwr/server/strategy/fedadagrad.py +1 -1
- flwr/server/strategy/fedadam.py +1 -1
- flwr/server/strategy/fedavg.py +1 -1
- flwr/server/strategy/fedavg_android.py +1 -1
- flwr/server/strategy/fedavgm.py +1 -1
- flwr/server/strategy/fedmedian.py +1 -1
- flwr/server/strategy/fedopt.py +1 -1
- flwr/server/strategy/fedprox.py +1 -1
- flwr/server/strategy/fedtrimmedavg.py +1 -1
- flwr/server/strategy/fedxgb_bagging.py +1 -1
- flwr/server/strategy/fedxgb_cyclic.py +1 -1
- flwr/server/strategy/fedxgb_nn_avg.py +3 -2
- flwr/server/strategy/fedyogi.py +1 -1
- flwr/server/strategy/krum.py +1 -1
- flwr/server/strategy/qfedavg.py +1 -1
- flwr/server/strategy/strategy.py +1 -1
- flwr/server/superlink/__init__.py +1 -1
- flwr/server/superlink/ffs/__init__.py +3 -1
- flwr/server/superlink/ffs/disk_ffs.py +1 -1
- flwr/server/superlink/ffs/ffs.py +1 -1
- flwr/server/superlink/ffs/ffs_factory.py +1 -1
- flwr/server/superlink/fleet/__init__.py +1 -1
- flwr/server/superlink/fleet/grpc_adapter/__init__.py +1 -1
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +14 -4
- flwr/server/superlink/fleet/grpc_bidi/__init__.py +1 -1
- flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -1
- flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +1 -1
- flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +1 -1
- flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +13 -13
- flwr/server/superlink/fleet/grpc_rere/__init__.py +1 -1
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +102 -8
- flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +1 -1
- flwr/server/superlink/fleet/message_handler/__init__.py +1 -1
- flwr/server/superlink/fleet/message_handler/message_handler.py +136 -19
- flwr/server/superlink/fleet/rest_rere/__init__.py +1 -1
- flwr/server/superlink/fleet/rest_rere/rest_api.py +73 -12
- flwr/server/superlink/fleet/vce/__init__.py +1 -1
- flwr/server/superlink/fleet/vce/backend/__init__.py +1 -1
- flwr/server/superlink/fleet/vce/backend/backend.py +1 -1
- flwr/server/superlink/fleet/vce/backend/raybackend.py +1 -1
- flwr/server/superlink/fleet/vce/vce_api.py +7 -4
- flwr/server/superlink/linkstate/__init__.py +1 -1
- flwr/server/superlink/linkstate/in_memory_linkstate.py +139 -44
- flwr/server/superlink/linkstate/linkstate.py +54 -21
- flwr/server/superlink/linkstate/linkstate_factory.py +1 -1
- flwr/server/superlink/linkstate/sqlite_linkstate.py +150 -56
- flwr/server/superlink/linkstate/utils.py +34 -30
- flwr/server/superlink/serverappio/serverappio_grpc.py +3 -0
- flwr/server/superlink/serverappio/serverappio_servicer.py +211 -57
- flwr/server/superlink/simulation/__init__.py +1 -1
- flwr/server/superlink/simulation/simulationio_grpc.py +1 -1
- flwr/server/superlink/simulation/simulationio_servicer.py +26 -2
- flwr/server/superlink/utils.py +45 -3
- flwr/server/typing.py +1 -1
- flwr/server/utils/__init__.py +1 -1
- flwr/server/utils/tensorboard.py +1 -1
- flwr/server/utils/validator.py +3 -3
- flwr/server/workflow/__init__.py +1 -1
- flwr/server/workflow/constant.py +1 -1
- flwr/server/workflow/default_workflows.py +1 -1
- flwr/server/workflow/secure_aggregation/__init__.py +1 -1
- flwr/server/workflow/secure_aggregation/secagg_workflow.py +1 -1
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
- flwr/serverapp/__init__.py +15 -0
- flwr/simulation/__init__.py +1 -1
- flwr/simulation/app.py +18 -1
- flwr/simulation/legacy_app.py +1 -1
- flwr/simulation/ray_transport/__init__.py +1 -1
- flwr/simulation/ray_transport/ray_actor.py +1 -1
- flwr/simulation/ray_transport/ray_client_proxy.py +1 -1
- flwr/simulation/ray_transport/utils.py +1 -1
- flwr/simulation/run_simulation.py +2 -2
- flwr/simulation/simulationio_connection.py +1 -1
- flwr/supercore/__init__.py +15 -0
- flwr/supercore/object_store/__init__.py +24 -0
- flwr/supercore/object_store/in_memory_object_store.py +229 -0
- flwr/supercore/object_store/object_store.py +192 -0
- flwr/supercore/object_store/object_store_factory.py +44 -0
- flwr/superexec/__init__.py +1 -1
- flwr/superexec/app.py +1 -1
- flwr/superexec/deployment.py +7 -3
- flwr/superexec/exec_event_log_interceptor.py +4 -4
- flwr/superexec/exec_grpc.py +8 -4
- flwr/superexec/exec_servicer.py +126 -24
- flwr/superexec/exec_user_auth_interceptor.py +38 -9
- flwr/superexec/executor.py +5 -1
- flwr/superexec/simulation.py +8 -2
- flwr/superlink/__init__.py +15 -0
- flwr/{client/supernode → supernode}/__init__.py +1 -8
- flwr/{client/nodestate/nodestate.py → supernode/cli/__init__.py} +8 -15
- flwr/{client/supernode/app.py → supernode/cli/flower_supernode.py} +4 -13
- flwr/supernode/cli/flwr_clientapp.py +81 -0
- flwr/{client → supernode}/nodestate/__init__.py +1 -1
- flwr/supernode/nodestate/in_memory_nodestate.py +190 -0
- flwr/supernode/nodestate/nodestate.py +212 -0
- flwr/{client → supernode}/nodestate/nodestate_factory.py +1 -1
- flwr/supernode/runtime/__init__.py +15 -0
- flwr/{client/clientapp/app.py → supernode/runtime/run_clientapp.py} +26 -57
- flwr/supernode/servicer/__init__.py +15 -0
- flwr/supernode/servicer/clientappio/__init__.py +24 -0
- flwr/{client/clientapp → supernode/servicer/clientappio}/clientappio_servicer.py +1 -1
- flwr/supernode/start_client_internal.py +491 -0
- {flwr-1.17.0.dist-info → flwr-1.19.0.dist-info}/METADATA +6 -5
- flwr-1.19.0.dist-info/RECORD +365 -0
- {flwr-1.17.0.dist-info → flwr-1.19.0.dist-info}/WHEEL +1 -1
- {flwr-1.17.0.dist-info → flwr-1.19.0.dist-info}/entry_points.txt +2 -2
- flwr/client/heartbeat.py +0 -74
- flwr/client/nodestate/in_memory_nodestate.py +0 -38
- flwr-1.17.0.dist-info/LICENSE +0 -202
- flwr-1.17.0.dist-info/RECORD +0 -333
flwr/__init__.py
CHANGED
flwr/app/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
"""Public Flower App APIs."""
|
flwr/app/error.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
"""Error."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Optional, cast
|
|
21
|
+
|
|
22
|
+
DEFAULT_TTL = 43200 # This is 12 hours
|
|
23
|
+
MESSAGE_INIT_ERROR_MESSAGE = (
|
|
24
|
+
"Invalid arguments for Message. Expected one of the documented "
|
|
25
|
+
"signatures: Message(content: RecordDict, dst_node_id: int, message_type: str,"
|
|
26
|
+
" *, [ttl: float, group_id: str]) or Message(content: RecordDict | error: Error,"
|
|
27
|
+
" *, reply_to: Message, [ttl: float])."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Error:
|
|
32
|
+
"""The class storing information about an error that occurred.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
code : int
|
|
37
|
+
An identifier for the error.
|
|
38
|
+
reason : Optional[str]
|
|
39
|
+
A reason for why the error arose (e.g. an exception stack-trace)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, code: int, reason: str | None = None) -> None:
|
|
43
|
+
var_dict = {
|
|
44
|
+
"_code": code,
|
|
45
|
+
"_reason": reason,
|
|
46
|
+
}
|
|
47
|
+
self.__dict__.update(var_dict)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def code(self) -> int:
|
|
51
|
+
"""Error code."""
|
|
52
|
+
return cast(int, self.__dict__["_code"])
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def reason(self) -> str | None:
|
|
56
|
+
"""Reason reported about the error."""
|
|
57
|
+
return cast(Optional[str], self.__dict__["_reason"])
|
|
58
|
+
|
|
59
|
+
def __repr__(self) -> str:
|
|
60
|
+
"""Return a string representation of this instance."""
|
|
61
|
+
view = ", ".join([f"{k.lstrip('_')}={v!r}" for k, v in self.__dict__.items()])
|
|
62
|
+
return f"{self.__class__.__qualname__}({view})"
|
|
63
|
+
|
|
64
|
+
def __eq__(self, other: object) -> bool:
|
|
65
|
+
"""Compare two instances of the class."""
|
|
66
|
+
if not isinstance(other, self.__class__):
|
|
67
|
+
raise NotImplementedError
|
|
68
|
+
return self.__dict__ == other.__dict__
|
flwr/app/metadata.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
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
|
+
"""Metadata."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import cast
|
|
21
|
+
|
|
22
|
+
from ..common.constant import MessageType, MessageTypeLegacy
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Metadata: # pylint: disable=too-many-instance-attributes
|
|
26
|
+
"""The class representing metadata associated with the current message.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
run_id : int
|
|
31
|
+
An identifier for the current run.
|
|
32
|
+
message_id : str
|
|
33
|
+
An identifier for the current message.
|
|
34
|
+
src_node_id : int
|
|
35
|
+
An identifier for the node sending this message.
|
|
36
|
+
dst_node_id : int
|
|
37
|
+
An identifier for the node receiving this message.
|
|
38
|
+
reply_to_message_id : str
|
|
39
|
+
An identifier for the message to which this message is a reply.
|
|
40
|
+
group_id : str
|
|
41
|
+
An identifier for grouping messages. In some settings,
|
|
42
|
+
this is used as the FL round.
|
|
43
|
+
created_at : float
|
|
44
|
+
Unix timestamp when the message was created.
|
|
45
|
+
ttl : float
|
|
46
|
+
Time-to-live for this message in seconds.
|
|
47
|
+
message_type : str
|
|
48
|
+
A string that encodes the action to be executed on
|
|
49
|
+
the receiving end.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
53
|
+
self,
|
|
54
|
+
run_id: int,
|
|
55
|
+
message_id: str,
|
|
56
|
+
src_node_id: int,
|
|
57
|
+
dst_node_id: int,
|
|
58
|
+
reply_to_message_id: str,
|
|
59
|
+
group_id: str,
|
|
60
|
+
created_at: float,
|
|
61
|
+
ttl: float,
|
|
62
|
+
message_type: str,
|
|
63
|
+
) -> None:
|
|
64
|
+
var_dict = {
|
|
65
|
+
"_run_id": run_id,
|
|
66
|
+
"_message_id": message_id,
|
|
67
|
+
"_src_node_id": src_node_id,
|
|
68
|
+
"_dst_node_id": dst_node_id,
|
|
69
|
+
"_reply_to_message_id": reply_to_message_id,
|
|
70
|
+
"_group_id": group_id,
|
|
71
|
+
"_created_at": created_at,
|
|
72
|
+
"_ttl": ttl,
|
|
73
|
+
"_message_type": message_type,
|
|
74
|
+
}
|
|
75
|
+
self.__dict__.update(var_dict)
|
|
76
|
+
self.message_type = message_type # Trigger validation
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def run_id(self) -> int:
|
|
80
|
+
"""An identifier for the current run."""
|
|
81
|
+
return cast(int, self.__dict__["_run_id"])
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def message_id(self) -> str:
|
|
85
|
+
"""An identifier for the current message."""
|
|
86
|
+
return cast(str, self.__dict__["_message_id"])
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def src_node_id(self) -> int:
|
|
90
|
+
"""An identifier for the node sending this message."""
|
|
91
|
+
return cast(int, self.__dict__["_src_node_id"])
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def reply_to_message_id(self) -> str:
|
|
95
|
+
"""An identifier for the message to which this message is a reply."""
|
|
96
|
+
return cast(str, self.__dict__["_reply_to_message_id"])
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def dst_node_id(self) -> int:
|
|
100
|
+
"""An identifier for the node receiving this message."""
|
|
101
|
+
return cast(int, self.__dict__["_dst_node_id"])
|
|
102
|
+
|
|
103
|
+
@dst_node_id.setter
|
|
104
|
+
def dst_node_id(self, value: int) -> None:
|
|
105
|
+
"""Set dst_node_id."""
|
|
106
|
+
self.__dict__["_dst_node_id"] = value
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def group_id(self) -> str:
|
|
110
|
+
"""An identifier for grouping messages."""
|
|
111
|
+
return cast(str, self.__dict__["_group_id"])
|
|
112
|
+
|
|
113
|
+
@group_id.setter
|
|
114
|
+
def group_id(self, value: str) -> None:
|
|
115
|
+
"""Set group_id."""
|
|
116
|
+
self.__dict__["_group_id"] = value
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def created_at(self) -> float:
|
|
120
|
+
"""Unix timestamp when the message was created."""
|
|
121
|
+
return cast(float, self.__dict__["_created_at"])
|
|
122
|
+
|
|
123
|
+
@created_at.setter
|
|
124
|
+
def created_at(self, value: float) -> None:
|
|
125
|
+
"""Set creation timestamp of this message."""
|
|
126
|
+
self.__dict__["_created_at"] = value
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def delivered_at(self) -> str:
|
|
130
|
+
"""Unix timestamp when the message was delivered."""
|
|
131
|
+
return cast(str, self.__dict__["_delivered_at"])
|
|
132
|
+
|
|
133
|
+
@delivered_at.setter
|
|
134
|
+
def delivered_at(self, value: str) -> None:
|
|
135
|
+
"""Set delivery timestamp of this message."""
|
|
136
|
+
self.__dict__["_delivered_at"] = value
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def ttl(self) -> float:
|
|
140
|
+
"""Time-to-live for this message."""
|
|
141
|
+
return cast(float, self.__dict__["_ttl"])
|
|
142
|
+
|
|
143
|
+
@ttl.setter
|
|
144
|
+
def ttl(self, value: float) -> None:
|
|
145
|
+
"""Set ttl."""
|
|
146
|
+
self.__dict__["_ttl"] = value
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def message_type(self) -> str:
|
|
150
|
+
"""A string that encodes the action to be executed on the receiving end."""
|
|
151
|
+
return cast(str, self.__dict__["_message_type"])
|
|
152
|
+
|
|
153
|
+
@message_type.setter
|
|
154
|
+
def message_type(self, value: str) -> None:
|
|
155
|
+
"""Set message_type."""
|
|
156
|
+
# Validate message type
|
|
157
|
+
if validate_legacy_message_type(value):
|
|
158
|
+
pass # Backward compatibility for legacy message types
|
|
159
|
+
elif not validate_message_type(value):
|
|
160
|
+
raise ValueError(
|
|
161
|
+
f"Invalid message type: '{value}'. "
|
|
162
|
+
"Expected format: '<category>' or '<category>.<action>', "
|
|
163
|
+
"where <category> must be 'train', 'evaluate', or 'query', "
|
|
164
|
+
"and <action> must be a valid Python identifier."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
self.__dict__["_message_type"] = value
|
|
168
|
+
|
|
169
|
+
def __repr__(self) -> str:
|
|
170
|
+
"""Return a string representation of this instance."""
|
|
171
|
+
view = ", ".join([f"{k.lstrip('_')}={v!r}" for k, v in self.__dict__.items()])
|
|
172
|
+
return f"{self.__class__.__qualname__}({view})"
|
|
173
|
+
|
|
174
|
+
def __eq__(self, other: object) -> bool:
|
|
175
|
+
"""Compare two instances of the class."""
|
|
176
|
+
if not isinstance(other, self.__class__):
|
|
177
|
+
raise NotImplementedError
|
|
178
|
+
return self.__dict__ == other.__dict__
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def validate_message_type(message_type: str) -> bool:
|
|
182
|
+
"""Validate if the message type is valid.
|
|
183
|
+
|
|
184
|
+
A valid message type format must be one of the following:
|
|
185
|
+
|
|
186
|
+
- "<category>"
|
|
187
|
+
- "<category>.<action>"
|
|
188
|
+
|
|
189
|
+
where `category` must be one of "train", "evaluate", or "query",
|
|
190
|
+
and `action` must be a valid Python identifier.
|
|
191
|
+
"""
|
|
192
|
+
# Check if conforming to the format "<category>"
|
|
193
|
+
valid_types = {
|
|
194
|
+
MessageType.TRAIN,
|
|
195
|
+
MessageType.EVALUATE,
|
|
196
|
+
MessageType.QUERY,
|
|
197
|
+
MessageType.SYSTEM,
|
|
198
|
+
}
|
|
199
|
+
if message_type in valid_types:
|
|
200
|
+
return True
|
|
201
|
+
|
|
202
|
+
# Check if conforming to the format "<category>.<action>"
|
|
203
|
+
if message_type.count(".") != 1:
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
category, action = message_type.split(".")
|
|
207
|
+
if category in valid_types and action.isidentifier():
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def validate_legacy_message_type(message_type: str) -> bool:
|
|
214
|
+
"""Validate if the legacy message type is valid."""
|
|
215
|
+
# Backward compatibility for legacy message types
|
|
216
|
+
if message_type in (
|
|
217
|
+
MessageTypeLegacy.GET_PARAMETERS,
|
|
218
|
+
MessageTypeLegacy.GET_PROPERTIES,
|
|
219
|
+
"reconnect",
|
|
220
|
+
):
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
return False
|
flwr/cli/__init__.py
CHANGED
flwr/cli/app.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Flower command line interface."""
|
|
16
16
|
|
|
17
|
-
|
|
18
17
|
import typer
|
|
19
18
|
from typer.main import get_command
|
|
20
19
|
|
|
20
|
+
from flwr.common.version import package_version
|
|
21
|
+
|
|
21
22
|
from .build import build
|
|
22
23
|
from .install import install
|
|
23
24
|
from .log import log
|
|
@@ -34,6 +35,7 @@ app = typer.Typer(
|
|
|
34
35
|
bold=True,
|
|
35
36
|
),
|
|
36
37
|
no_args_is_help=True,
|
|
38
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
37
39
|
)
|
|
38
40
|
|
|
39
41
|
app.command()(new)
|
|
@@ -47,5 +49,22 @@ app.command()(login)
|
|
|
47
49
|
|
|
48
50
|
typer_click_object = get_command(app)
|
|
49
51
|
|
|
52
|
+
|
|
53
|
+
@app.callback(invoke_without_command=True)
|
|
54
|
+
def version_callback(
|
|
55
|
+
ver: bool = typer.Option(
|
|
56
|
+
None,
|
|
57
|
+
"-V",
|
|
58
|
+
"--version",
|
|
59
|
+
is_eager=True,
|
|
60
|
+
help="Show the version and exit.",
|
|
61
|
+
),
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Print version."""
|
|
64
|
+
if ver:
|
|
65
|
+
typer.secho(f"Flower version: {package_version}", fg="blue")
|
|
66
|
+
raise typer.Exit()
|
|
67
|
+
|
|
68
|
+
|
|
50
69
|
if __name__ == "__main__":
|
|
51
70
|
app()
|
flwr/cli/build.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -16,10 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
import hashlib
|
|
19
|
-
import os
|
|
20
|
-
import shutil
|
|
21
|
-
import tempfile
|
|
22
19
|
import zipfile
|
|
20
|
+
from io import BytesIO
|
|
23
21
|
from pathlib import Path
|
|
24
22
|
from typing import Annotated, Any, Optional, Union
|
|
25
23
|
|
|
@@ -29,6 +27,7 @@ import typer
|
|
|
29
27
|
|
|
30
28
|
from flwr.common.constant import FAB_ALLOWED_EXTENSIONS, FAB_DATE, FAB_HASH_TRUNCATION
|
|
31
29
|
|
|
30
|
+
from .config_utils import load as load_toml
|
|
32
31
|
from .config_utils import load_and_validate
|
|
33
32
|
from .utils import is_valid_project_name
|
|
34
33
|
|
|
@@ -43,11 +42,11 @@ def write_to_zip(
|
|
|
43
42
|
return zipfile_obj
|
|
44
43
|
|
|
45
44
|
|
|
46
|
-
def get_fab_filename(
|
|
45
|
+
def get_fab_filename(config: dict[str, Any], fab_hash: str) -> str:
|
|
47
46
|
"""Get the FAB filename based on the given config and FAB hash."""
|
|
48
|
-
publisher =
|
|
49
|
-
name =
|
|
50
|
-
version =
|
|
47
|
+
publisher = config["tool"]["flwr"]["app"]["publisher"]
|
|
48
|
+
name = config["project"]["name"]
|
|
49
|
+
version = config["project"]["version"].replace(".", "-")
|
|
51
50
|
fab_hash_truncated = fab_hash[:FAB_HASH_TRUNCATION]
|
|
52
51
|
return f"{publisher}.{name}.{version}.{fab_hash_truncated}.fab"
|
|
53
52
|
|
|
@@ -89,8 +88,8 @@ def build(
|
|
|
89
88
|
)
|
|
90
89
|
raise typer.Exit(code=1)
|
|
91
90
|
|
|
92
|
-
|
|
93
|
-
if
|
|
91
|
+
config, errors, warnings = load_and_validate(app / "pyproject.toml")
|
|
92
|
+
if config is None:
|
|
94
93
|
typer.secho(
|
|
95
94
|
"Project configuration could not be loaded.\npyproject.toml is invalid:\n"
|
|
96
95
|
+ "\n".join([f"- {line}" for line in errors]),
|
|
@@ -107,70 +106,96 @@ def build(
|
|
|
107
106
|
bold=True,
|
|
108
107
|
)
|
|
109
108
|
|
|
110
|
-
#
|
|
111
|
-
|
|
109
|
+
# Build FAB
|
|
110
|
+
fab_bytes, fab_hash, _ = build_fab(app)
|
|
112
111
|
|
|
113
|
-
|
|
112
|
+
# Get the name of the zip file
|
|
113
|
+
fab_filename = get_fab_filename(config, fab_hash)
|
|
114
|
+
|
|
115
|
+
# Write the FAB
|
|
116
|
+
Path(fab_filename).write_bytes(fab_bytes)
|
|
117
|
+
|
|
118
|
+
typer.secho(
|
|
119
|
+
f"🎊 Successfully built {fab_filename}", fg=typer.colors.GREEN, bold=True
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return fab_filename, fab_hash
|
|
114
123
|
|
|
115
|
-
# Remove the 'federations' field from 'tool.flwr' if it exists
|
|
116
|
-
if (
|
|
117
|
-
"tool" in conf
|
|
118
|
-
and "flwr" in conf["tool"]
|
|
119
|
-
and "federations" in conf["tool"]["flwr"]
|
|
120
|
-
):
|
|
121
|
-
del conf["tool"]["flwr"]["federations"]
|
|
122
124
|
|
|
123
|
-
|
|
125
|
+
def build_fab(app: Path) -> tuple[bytes, str, dict[str, Any]]:
|
|
126
|
+
"""Build a FAB in memory and return the bytes, hash, and config.
|
|
124
127
|
|
|
125
|
-
|
|
126
|
-
|
|
128
|
+
This function assumes that the provided path points to a valid Flower app and
|
|
129
|
+
bundles it into a FAB without performing additional validation.
|
|
127
130
|
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
app : Path
|
|
134
|
+
Path to the Flower app to bundle into a FAB.
|
|
130
135
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
tuple[bytes, str, dict[str, Any]]
|
|
139
|
+
A tuple containing:
|
|
140
|
+
- the FAB as bytes
|
|
141
|
+
- the SHA256 hash of the FAB
|
|
142
|
+
- the project configuration (with the 'federations' field removed)
|
|
143
|
+
"""
|
|
144
|
+
app = app.resolve()
|
|
145
|
+
|
|
146
|
+
# Load the pyproject.toml file
|
|
147
|
+
config = load_toml(app / "pyproject.toml")
|
|
148
|
+
if config is None:
|
|
149
|
+
raise ValueError("Project configuration could not be loaded.")
|
|
140
150
|
|
|
141
|
-
|
|
151
|
+
# Remove the 'federations' field if it exists
|
|
152
|
+
if (
|
|
153
|
+
"tool" in config
|
|
154
|
+
and "flwr" in config["tool"]
|
|
155
|
+
and "federations" in config["tool"]["flwr"]
|
|
156
|
+
):
|
|
157
|
+
del config["tool"]["flwr"]["federations"]
|
|
142
158
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
with open(file_path, "rb") as f:
|
|
146
|
-
file_contents = f.read()
|
|
159
|
+
# Load .gitignore rules if present
|
|
160
|
+
ignore_spec = _load_gitignore(app)
|
|
147
161
|
|
|
148
|
-
|
|
149
|
-
|
|
162
|
+
# Search for all files in the app directory
|
|
163
|
+
all_files = [
|
|
164
|
+
f
|
|
165
|
+
for f in app.rglob("*")
|
|
166
|
+
if not ignore_spec.match_file(f)
|
|
167
|
+
and f.suffix in FAB_ALLOWED_EXTENSIONS
|
|
168
|
+
and f.name != "pyproject.toml" # Exclude the original pyproject.toml
|
|
169
|
+
]
|
|
170
|
+
all_files.sort()
|
|
171
|
+
|
|
172
|
+
# Create a zip file in memory
|
|
173
|
+
list_file_content = ""
|
|
150
174
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
175
|
+
fab_buffer = BytesIO()
|
|
176
|
+
with zipfile.ZipFile(fab_buffer, "w", zipfile.ZIP_DEFLATED) as fab_file:
|
|
177
|
+
# Add pyproject.toml
|
|
178
|
+
write_to_zip(fab_file, "pyproject.toml", tomli_w.dumps(config))
|
|
155
179
|
|
|
156
|
-
|
|
157
|
-
|
|
180
|
+
for file_path in all_files:
|
|
181
|
+
# Read the file content manually
|
|
182
|
+
file_contents = file_path.read_bytes()
|
|
158
183
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
fab_hash = hashlib.sha256(content).hexdigest()
|
|
184
|
+
archive_path = str(file_path.relative_to(app))
|
|
185
|
+
write_to_zip(fab_file, archive_path, file_contents)
|
|
162
186
|
|
|
163
|
-
|
|
164
|
-
|
|
187
|
+
# Calculate file info
|
|
188
|
+
sha256_hash = hashlib.sha256(file_contents).hexdigest()
|
|
189
|
+
file_size_bits = len(file_contents) * 8 # size in bits
|
|
190
|
+
list_file_content += f"{archive_path},{sha256_hash},{file_size_bits}\n"
|
|
165
191
|
|
|
166
|
-
|
|
167
|
-
|
|
192
|
+
# Add CONTENT and CONTENT.jwt to the zip file
|
|
193
|
+
write_to_zip(fab_file, ".info/CONTENT", list_file_content)
|
|
168
194
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
)
|
|
195
|
+
fab_bytes = fab_buffer.getvalue()
|
|
196
|
+
fab_hash = hashlib.sha256(fab_bytes).hexdigest()
|
|
172
197
|
|
|
173
|
-
return
|
|
198
|
+
return fab_bytes, fab_hash, config
|
|
174
199
|
|
|
175
200
|
|
|
176
201
|
def _load_gitignore(app: Path) -> pathspec.PathSpec:
|
flwr/cli/config_utils.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -176,11 +176,32 @@ def validate_federation_in_project_config(
|
|
|
176
176
|
def validate_certificate_in_federation_config(
|
|
177
177
|
app: Path, federation_config: dict[str, Any]
|
|
178
178
|
) -> tuple[bool, Optional[bytes]]:
|
|
179
|
-
"""Validate the certificates in the Flower project configuration.
|
|
180
|
-
|
|
179
|
+
"""Validate the certificates in the Flower project configuration.
|
|
180
|
+
|
|
181
|
+
Accepted configurations:
|
|
182
|
+
1. TLS enabled and gRPC will load(*) the trusted certificate bundle:
|
|
183
|
+
- Only `address` is provided. `root-certificates` and `insecure` not set.
|
|
184
|
+
- `address` is provided and `insecure` set to `false`. `root-certificates` not
|
|
185
|
+
set.
|
|
186
|
+
(*)gRPC uses a multi-step fallback mechanism to load the trusted certificate
|
|
187
|
+
bundle in the following sequence:
|
|
188
|
+
a. A configured file path (if set via configuration or environment),
|
|
189
|
+
b. An override callback (if registered via
|
|
190
|
+
`grpc_set_ssl_roots_override_callback`),
|
|
191
|
+
c. The OS trust store (if available),
|
|
192
|
+
d. A bundled default certificate file.
|
|
193
|
+
2. TLS enabled with self-signed certificates:
|
|
194
|
+
- `address` and `root-certificates` are provided. `insecure` not set.
|
|
195
|
+
- `address` and `root-certificates` are provided. `insecure` set to `false`.
|
|
196
|
+
3. TLS disabled. This is not recommended and should only be used for prototyping:
|
|
197
|
+
- `address` is provided and `insecure = true`. If `root-certificates` is
|
|
198
|
+
set, exit with an error.
|
|
199
|
+
"""
|
|
200
|
+
insecure = get_insecure_flag(federation_config)
|
|
201
|
+
|
|
202
|
+
# Process root certificates
|
|
181
203
|
if root_certificates := federation_config.get("root-certificates"):
|
|
182
|
-
|
|
183
|
-
if insecure := bool(insecure_str):
|
|
204
|
+
if insecure:
|
|
184
205
|
typer.secho(
|
|
185
206
|
"❌ `root-certificates` were provided but the `insecure` parameter "
|
|
186
207
|
"is set to `True`.",
|
|
@@ -188,22 +209,19 @@ def validate_certificate_in_federation_config(
|
|
|
188
209
|
bold=True,
|
|
189
210
|
)
|
|
190
211
|
raise typer.Exit(code=1)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
fg=typer.colors.RED,
|
|
197
|
-
bold=True,
|
|
198
|
-
)
|
|
199
|
-
raise typer.Exit(code=1)
|
|
200
|
-
if not (insecure := bool(insecure_str)):
|
|
212
|
+
|
|
213
|
+
# TLS is enabled with self-signed certificates: attempt to read the file
|
|
214
|
+
try:
|
|
215
|
+
root_certificates_bytes = (app / root_certificates).read_bytes()
|
|
216
|
+
except Exception as e:
|
|
201
217
|
typer.secho(
|
|
202
|
-
"❌
|
|
218
|
+
f"❌ Failed to read certificate file `{root_certificates}`: {e}",
|
|
203
219
|
fg=typer.colors.RED,
|
|
204
220
|
bold=True,
|
|
205
221
|
)
|
|
206
|
-
raise typer.Exit(code=1)
|
|
222
|
+
raise typer.Exit(code=1) from e
|
|
223
|
+
else:
|
|
224
|
+
root_certificates_bytes = None
|
|
207
225
|
|
|
208
226
|
return insecure, root_certificates_bytes
|
|
209
227
|
|
|
@@ -218,3 +236,21 @@ def exit_if_no_address(federation_config: dict[str, Any], cmd: str) -> None:
|
|
|
218
236
|
bold=True,
|
|
219
237
|
)
|
|
220
238
|
raise typer.Exit(code=1)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def get_insecure_flag(federation_config: dict[str, Any]) -> bool:
|
|
242
|
+
"""Extract and validate the `insecure` flag from the federation configuration."""
|
|
243
|
+
insecure_value = federation_config.get("insecure")
|
|
244
|
+
|
|
245
|
+
if insecure_value is None:
|
|
246
|
+
# Not provided, default to False (TLS enabled)
|
|
247
|
+
return False
|
|
248
|
+
if isinstance(insecure_value, bool):
|
|
249
|
+
return insecure_value
|
|
250
|
+
typer.secho(
|
|
251
|
+
"❌ Invalid type for `insecure`: expected a boolean if provided. "
|
|
252
|
+
"(`insecure = true` or `insecure = false`)",
|
|
253
|
+
fg=typer.colors.RED,
|
|
254
|
+
bold=True,
|
|
255
|
+
)
|
|
256
|
+
raise typer.Exit(code=1)
|
flwr/cli/example.py
CHANGED