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
|
@@ -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 ServerApp APIs."""
|
flwr/simulation/__init__.py
CHANGED
flwr/simulation/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.
|
|
@@ -39,6 +39,7 @@ from flwr.common.constant import (
|
|
|
39
39
|
SubStatus,
|
|
40
40
|
)
|
|
41
41
|
from flwr.common.exit import ExitCode, flwr_exit
|
|
42
|
+
from flwr.common.heartbeat import HeartbeatSender, get_grpc_app_heartbeat_fn
|
|
42
43
|
from flwr.common.logger import (
|
|
43
44
|
log,
|
|
44
45
|
mirror_output_to_queue,
|
|
@@ -120,6 +121,7 @@ def run_simulation_process( # pylint: disable=R0914, disable=W0212, disable=R09
|
|
|
120
121
|
# Resolve directory where FABs are installed
|
|
121
122
|
flwr_dir = get_flwr_dir(flwr_dir_)
|
|
122
123
|
log_uploader = None
|
|
124
|
+
heartbeat_sender = None
|
|
123
125
|
|
|
124
126
|
while True:
|
|
125
127
|
|
|
@@ -210,6 +212,16 @@ def run_simulation_process( # pylint: disable=R0914, disable=W0212, disable=R09
|
|
|
210
212
|
},
|
|
211
213
|
)
|
|
212
214
|
|
|
215
|
+
# Set up heartbeat sender
|
|
216
|
+
heartbeat_fn = get_grpc_app_heartbeat_fn(
|
|
217
|
+
conn._stub,
|
|
218
|
+
run.run_id,
|
|
219
|
+
failure_message="Heartbeat failed unexpectedly. The SuperLink could "
|
|
220
|
+
"not find the provided run ID, or the run status is invalid.",
|
|
221
|
+
)
|
|
222
|
+
heartbeat_sender = HeartbeatSender(heartbeat_fn)
|
|
223
|
+
heartbeat_sender.start()
|
|
224
|
+
|
|
213
225
|
# Launch the simulation
|
|
214
226
|
updated_context = _run_simulation(
|
|
215
227
|
server_app_attr=server_app_attr,
|
|
@@ -240,6 +252,11 @@ def run_simulation_process( # pylint: disable=R0914, disable=W0212, disable=R09
|
|
|
240
252
|
run_status = RunStatus(Status.FINISHED, SubStatus.FAILED, str(ex))
|
|
241
253
|
|
|
242
254
|
finally:
|
|
255
|
+
# Stop heartbeat sender
|
|
256
|
+
if heartbeat_sender:
|
|
257
|
+
heartbeat_sender.stop()
|
|
258
|
+
heartbeat_sender = None
|
|
259
|
+
|
|
243
260
|
# Stop log uploader for this run and upload final logs
|
|
244
261
|
if log_uploader:
|
|
245
262
|
stop_log_uploader(log_queue, log_uploader)
|
flwr/simulation/legacy_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.
|
|
@@ -172,7 +172,7 @@ def run_simulation(
|
|
|
172
172
|
ServerApp and receive a Message describing what the ClientApp should perform.
|
|
173
173
|
|
|
174
174
|
backend_name : str (default: ray)
|
|
175
|
-
A simulation backend that runs `ClientApp`
|
|
175
|
+
A simulation backend that runs `ClientApp` objects.
|
|
176
176
|
|
|
177
177
|
backend_config : Optional[BackendConfig]
|
|
178
178
|
'A dictionary to configure a backend. Separate dictionaries to configure
|
|
@@ -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
|
+
"""Infrastructure components shared between SuperLink and SuperNode."""
|
|
@@ -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 ObjectStore."""
|
|
16
|
+
|
|
17
|
+
from .object_store import NoObjectInStoreError, ObjectStore
|
|
18
|
+
from .object_store_factory import ObjectStoreFactory
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"NoObjectInStoreError",
|
|
22
|
+
"ObjectStore",
|
|
23
|
+
"ObjectStoreFactory",
|
|
24
|
+
]
|
|
@@ -0,0 +1,229 @@
|
|
|
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 in-memory ObjectStore implementation."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
import threading
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
from flwr.common.inflatable import (
|
|
23
|
+
get_object_children_ids_from_object_content,
|
|
24
|
+
get_object_id,
|
|
25
|
+
is_valid_sha256_hash,
|
|
26
|
+
iterate_object_tree,
|
|
27
|
+
)
|
|
28
|
+
from flwr.common.inflatable_utils import validate_object_content
|
|
29
|
+
from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
|
|
30
|
+
|
|
31
|
+
from .object_store import NoObjectInStoreError, ObjectStore
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ObjectEntry:
|
|
36
|
+
"""Data class representing an object entry in the store."""
|
|
37
|
+
|
|
38
|
+
content: bytes
|
|
39
|
+
is_available: bool
|
|
40
|
+
ref_count: int # Number of references (direct parents) to this object
|
|
41
|
+
runs: set[int] # Set of run IDs that used this object
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class InMemoryObjectStore(ObjectStore):
|
|
45
|
+
"""In-memory implementation of the ObjectStore interface."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, verify: bool = True) -> None:
|
|
48
|
+
self.verify = verify
|
|
49
|
+
self.store: dict[str, ObjectEntry] = {}
|
|
50
|
+
self.lock_store = threading.RLock()
|
|
51
|
+
# Mapping the Object ID of a message to the list of descendant object IDs
|
|
52
|
+
self.msg_descendant_objects_mapping: dict[str, list[str]] = {}
|
|
53
|
+
self.lock_msg_mapping = threading.RLock()
|
|
54
|
+
# Mapping each run ID to a set of object IDs that are used in that run
|
|
55
|
+
self.run_objects_mapping: dict[int, set[str]] = {}
|
|
56
|
+
|
|
57
|
+
def preregister(self, run_id: int, object_tree: ObjectTree) -> list[str]:
|
|
58
|
+
"""Identify and preregister missing objects."""
|
|
59
|
+
new_objects = []
|
|
60
|
+
if run_id not in self.run_objects_mapping:
|
|
61
|
+
self.run_objects_mapping[run_id] = set()
|
|
62
|
+
|
|
63
|
+
for tree_node in iterate_object_tree(object_tree):
|
|
64
|
+
obj_id = tree_node.object_id
|
|
65
|
+
# Verify object ID format (must be a valid sha256 hash)
|
|
66
|
+
if not is_valid_sha256_hash(obj_id):
|
|
67
|
+
raise ValueError(f"Invalid object ID format: {obj_id}")
|
|
68
|
+
with self.lock_store:
|
|
69
|
+
if obj_id not in self.store:
|
|
70
|
+
self.store[obj_id] = ObjectEntry(
|
|
71
|
+
content=b"", # Initially empty content
|
|
72
|
+
is_available=False, # Initially not available
|
|
73
|
+
ref_count=0, # Reference count starts at 0
|
|
74
|
+
runs={run_id}, # Start with the current run ID
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Increment the reference count for all its children
|
|
78
|
+
# Post-order traversal ensures that children are registered
|
|
79
|
+
# before parents
|
|
80
|
+
for child_node in tree_node.children:
|
|
81
|
+
child_id = child_node.object_id
|
|
82
|
+
self.store[child_id].ref_count += 1
|
|
83
|
+
|
|
84
|
+
# Add the object ID to the run's mapping
|
|
85
|
+
self.run_objects_mapping[run_id].add(obj_id)
|
|
86
|
+
|
|
87
|
+
# Add to the list of new objects
|
|
88
|
+
new_objects.append(obj_id)
|
|
89
|
+
else:
|
|
90
|
+
# Object is in store, retrieve it
|
|
91
|
+
obj_entry = self.store[obj_id]
|
|
92
|
+
|
|
93
|
+
# Add to the list of new objects if not available
|
|
94
|
+
if not obj_entry.is_available:
|
|
95
|
+
new_objects.append(obj_id)
|
|
96
|
+
|
|
97
|
+
# If the object is already registered but not in this run,
|
|
98
|
+
# add the run ID to its runs
|
|
99
|
+
if obj_id not in self.run_objects_mapping[run_id]:
|
|
100
|
+
obj_entry.runs.add(run_id)
|
|
101
|
+
self.run_objects_mapping[run_id].add(obj_id)
|
|
102
|
+
|
|
103
|
+
return new_objects
|
|
104
|
+
|
|
105
|
+
def put(self, object_id: str, object_content: bytes) -> None:
|
|
106
|
+
"""Put an object into the store."""
|
|
107
|
+
if self.verify:
|
|
108
|
+
# Verify object_id and object_content match
|
|
109
|
+
object_id_from_content = get_object_id(object_content)
|
|
110
|
+
if object_id != object_id_from_content:
|
|
111
|
+
raise ValueError(f"Object ID {object_id} does not match content hash")
|
|
112
|
+
|
|
113
|
+
# Validate object content
|
|
114
|
+
validate_object_content(content=object_content)
|
|
115
|
+
|
|
116
|
+
with self.lock_store:
|
|
117
|
+
# Only allow adding the object if it has been preregistered
|
|
118
|
+
if object_id not in self.store:
|
|
119
|
+
raise NoObjectInStoreError(
|
|
120
|
+
f"Object with ID '{object_id}' was not pre-registered."
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Return if object is already present in the store
|
|
124
|
+
if self.store[object_id].is_available:
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
# Update the object entry in the store
|
|
128
|
+
self.store[object_id].content = object_content
|
|
129
|
+
self.store[object_id].is_available = True
|
|
130
|
+
|
|
131
|
+
def set_message_descendant_ids(
|
|
132
|
+
self, msg_object_id: str, descendant_ids: list[str]
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Store the mapping from a ``Message`` object ID to the object IDs of its
|
|
135
|
+
descendants."""
|
|
136
|
+
with self.lock_msg_mapping:
|
|
137
|
+
self.msg_descendant_objects_mapping[msg_object_id] = descendant_ids
|
|
138
|
+
|
|
139
|
+
def get_message_descendant_ids(self, msg_object_id: str) -> list[str]:
|
|
140
|
+
"""Retrieve the object IDs of all descendants of a given Message."""
|
|
141
|
+
with self.lock_msg_mapping:
|
|
142
|
+
if msg_object_id not in self.msg_descendant_objects_mapping:
|
|
143
|
+
raise NoObjectInStoreError(
|
|
144
|
+
f"No message registered in Object Store with ID '{msg_object_id}'. "
|
|
145
|
+
"Mapping to descendants could not be found."
|
|
146
|
+
)
|
|
147
|
+
return self.msg_descendant_objects_mapping[msg_object_id]
|
|
148
|
+
|
|
149
|
+
def delete_message_descendant_ids(self, msg_object_id: str) -> None:
|
|
150
|
+
"""Delete the mapping from a ``Message`` object ID to its descendants."""
|
|
151
|
+
with self.lock_msg_mapping:
|
|
152
|
+
self.msg_descendant_objects_mapping.pop(msg_object_id, None)
|
|
153
|
+
|
|
154
|
+
def get(self, object_id: str) -> Optional[bytes]:
|
|
155
|
+
"""Get an object from the store."""
|
|
156
|
+
with self.lock_store:
|
|
157
|
+
# Check if the object ID is pre-registered
|
|
158
|
+
if object_id not in self.store:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
# Return content (if not yet available, it will b"")
|
|
162
|
+
return self.store[object_id].content
|
|
163
|
+
|
|
164
|
+
def delete(self, object_id: str) -> None:
|
|
165
|
+
"""Delete an object and its unreferenced descendants from the store."""
|
|
166
|
+
with self.lock_store:
|
|
167
|
+
# If the object is not in the store, nothing to delete
|
|
168
|
+
if (object_entry := self.store.get(object_id)) is None:
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
# Delete the object if it has no references left
|
|
172
|
+
if object_entry.ref_count == 0:
|
|
173
|
+
del self.store[object_id]
|
|
174
|
+
|
|
175
|
+
# Remove the object from the run's mapping
|
|
176
|
+
for run_id in object_entry.runs:
|
|
177
|
+
self.run_objects_mapping[run_id].discard(object_id)
|
|
178
|
+
|
|
179
|
+
# Decrease the reference count of its children
|
|
180
|
+
children_ids = get_object_children_ids_from_object_content(
|
|
181
|
+
object_entry.content
|
|
182
|
+
)
|
|
183
|
+
for child_id in children_ids:
|
|
184
|
+
self.store[child_id].ref_count -= 1
|
|
185
|
+
|
|
186
|
+
# Recursively try to delete the child object
|
|
187
|
+
self.delete(child_id)
|
|
188
|
+
|
|
189
|
+
def delete_objects_in_run(self, run_id: int) -> None:
|
|
190
|
+
"""Delete all objects that were registered in a specific run."""
|
|
191
|
+
with self.lock_store:
|
|
192
|
+
if run_id not in self.run_objects_mapping:
|
|
193
|
+
return
|
|
194
|
+
for object_id in list(self.run_objects_mapping[run_id]):
|
|
195
|
+
# Check if the object is still in the store
|
|
196
|
+
if (object_entry := self.store.get(object_id)) is None:
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
# Remove the run ID from the object's runs
|
|
200
|
+
object_entry.runs.discard(run_id)
|
|
201
|
+
|
|
202
|
+
# Only message objects are allowed to have a `ref_count` of 0,
|
|
203
|
+
# and every message object must have a `ref_count` of 0
|
|
204
|
+
if object_entry.ref_count == 0:
|
|
205
|
+
# Delete the message object and its unreferenced descendants
|
|
206
|
+
self.delete(object_id)
|
|
207
|
+
|
|
208
|
+
# Delete the message's descendants mapping
|
|
209
|
+
self.delete_message_descendant_ids(object_id)
|
|
210
|
+
|
|
211
|
+
# Remove the run from the mapping
|
|
212
|
+
del self.run_objects_mapping[run_id]
|
|
213
|
+
|
|
214
|
+
def clear(self) -> None:
|
|
215
|
+
"""Clear the store."""
|
|
216
|
+
with self.lock_store:
|
|
217
|
+
self.store.clear()
|
|
218
|
+
self.msg_descendant_objects_mapping.clear()
|
|
219
|
+
self.run_objects_mapping.clear()
|
|
220
|
+
|
|
221
|
+
def __contains__(self, object_id: str) -> bool:
|
|
222
|
+
"""Check if an object_id is in the store."""
|
|
223
|
+
with self.lock_store:
|
|
224
|
+
return object_id in self.store
|
|
225
|
+
|
|
226
|
+
def __len__(self) -> int:
|
|
227
|
+
"""Get the number of objects in the store."""
|
|
228
|
+
with self.lock_store:
|
|
229
|
+
return len(self.store)
|
|
@@ -0,0 +1,192 @@
|
|
|
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 abstract ObjectStore definition."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
import abc
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NoObjectInStoreError(Exception):
|
|
25
|
+
"""Error when trying to access an element in the ObjectStore that does not exist."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, message: str):
|
|
28
|
+
super().__init__(message)
|
|
29
|
+
self.message = message
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
"""Return formatted exception message string."""
|
|
33
|
+
return f"NoObjectInStoreError: {self.message}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ObjectStore(abc.ABC):
|
|
37
|
+
"""Abstract base class for `ObjectStore` implementations.
|
|
38
|
+
|
|
39
|
+
This class defines the interface for an object store that can store, retrieve, and
|
|
40
|
+
delete objects identified by object IDs.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
@abc.abstractmethod
|
|
44
|
+
def preregister(self, run_id: int, object_tree: ObjectTree) -> list[str]:
|
|
45
|
+
"""Identify and preregister missing objects in the `ObjectStore`.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
run_id : int
|
|
50
|
+
The ID of the run for which to preregister objects.
|
|
51
|
+
object_tree : ObjectTree
|
|
52
|
+
The object tree containing the IDs of objects to preregister.
|
|
53
|
+
This tree should contain all objects that are expected to be
|
|
54
|
+
stored in the `ObjectStore`.
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
list[str]
|
|
59
|
+
A list of object IDs that were either not previously preregistered
|
|
60
|
+
in the `ObjectStore`, or were preregistered but are not yet available.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
@abc.abstractmethod
|
|
64
|
+
def put(self, object_id: str, object_content: bytes) -> None:
|
|
65
|
+
"""Put an object into the store.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
object_id : str
|
|
70
|
+
The object_id under which to store the object. Must be preregistered.
|
|
71
|
+
object_content : bytes
|
|
72
|
+
The deflated object to store.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
@abc.abstractmethod
|
|
76
|
+
def get(self, object_id: str) -> Optional[bytes]:
|
|
77
|
+
"""Get an object from the store.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
object_id : str
|
|
82
|
+
The object_id under which the object is stored.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
bytes
|
|
87
|
+
The object stored under the given object_id.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
@abc.abstractmethod
|
|
91
|
+
def delete(self, object_id: str) -> None:
|
|
92
|
+
"""Delete an object and its unreferenced descendants from the store.
|
|
93
|
+
|
|
94
|
+
This method attempts to recursively delete the specified object and its
|
|
95
|
+
descendants, if they are not referenced by any other object.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
object_id : str
|
|
100
|
+
The object_id under which the object is stored.
|
|
101
|
+
|
|
102
|
+
Notes
|
|
103
|
+
-----
|
|
104
|
+
The object of the given object_id will NOT be deleted if it is still referenced
|
|
105
|
+
by any other object in the store.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
@abc.abstractmethod
|
|
109
|
+
def delete_objects_in_run(self, run_id: int) -> None:
|
|
110
|
+
"""Delete all objects that were registered in a specific run.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
run_id : int
|
|
115
|
+
The ID of the run for which to delete objects.
|
|
116
|
+
|
|
117
|
+
Notes
|
|
118
|
+
-----
|
|
119
|
+
Objects that are still registered in other runs will NOT be deleted.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
@abc.abstractmethod
|
|
123
|
+
def clear(self) -> None:
|
|
124
|
+
"""Clear the store.
|
|
125
|
+
|
|
126
|
+
This method should remove all objects from the store.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
@abc.abstractmethod
|
|
130
|
+
def set_message_descendant_ids(
|
|
131
|
+
self, msg_object_id: str, descendant_ids: list[str]
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Store the mapping from a ``Message`` object ID to the object IDs of its
|
|
134
|
+
descendants.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
msg_object_id : str
|
|
139
|
+
The object ID of the ``Message``.
|
|
140
|
+
descendant_ids : list[str]
|
|
141
|
+
A list of object IDs representing all descendant objects of the ``Message``.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
@abc.abstractmethod
|
|
145
|
+
def get_message_descendant_ids(self, msg_object_id: str) -> list[str]:
|
|
146
|
+
"""Retrieve the object IDs of all descendants of a given ``Message``.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
msg_object_id : str
|
|
151
|
+
The object ID of the ``Message``.
|
|
152
|
+
|
|
153
|
+
Returns
|
|
154
|
+
-------
|
|
155
|
+
list[str]
|
|
156
|
+
A list of object IDs of all descendant objects of the ``Message``.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
@abc.abstractmethod
|
|
160
|
+
def delete_message_descendant_ids(self, msg_object_id: str) -> None:
|
|
161
|
+
"""Delete the mapping from a ``Message`` object ID to its descendants.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
msg_object_id : str
|
|
166
|
+
The object ID of the ``Message``.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
@abc.abstractmethod
|
|
170
|
+
def __contains__(self, object_id: str) -> bool:
|
|
171
|
+
"""Check if an object_id is in the store.
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
object_id : str
|
|
176
|
+
The object_id to check.
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
bool
|
|
181
|
+
True if the object_id is in the store, False otherwise.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
@abc.abstractmethod
|
|
185
|
+
def __len__(self) -> int:
|
|
186
|
+
"""Return the number of objects in the store.
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
int
|
|
191
|
+
The number of objects currently stored.
|
|
192
|
+
"""
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
"""Factory class that creates ObjectStore instances."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from logging import DEBUG
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from flwr.common.logger import log
|
|
22
|
+
|
|
23
|
+
from .in_memory_object_store import InMemoryObjectStore
|
|
24
|
+
from .object_store import ObjectStore
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ObjectStoreFactory:
|
|
28
|
+
"""Factory class that creates ObjectStore instances."""
|
|
29
|
+
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
self.store_instance: Optional[ObjectStore] = None
|
|
32
|
+
|
|
33
|
+
def store(self) -> ObjectStore:
|
|
34
|
+
"""Return an ObjectStore instance and create it, if necessary.
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
ObjectStore
|
|
39
|
+
An ObjectStore instance for storing objects by object_id.
|
|
40
|
+
"""
|
|
41
|
+
if self.store_instance is None:
|
|
42
|
+
self.store_instance = InMemoryObjectStore()
|
|
43
|
+
log(DEBUG, "Using InMemoryObjectStore")
|
|
44
|
+
return self.store_instance
|
flwr/superexec/__init__.py
CHANGED
flwr/superexec/app.py
CHANGED