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.
Files changed (286) hide show
  1. flwr/__init__.py +1 -1
  2. flwr/app/__init__.py +15 -0
  3. flwr/app/error.py +68 -0
  4. flwr/app/metadata.py +223 -0
  5. flwr/cli/__init__.py +1 -1
  6. flwr/cli/app.py +21 -2
  7. flwr/cli/build.py +83 -58
  8. flwr/cli/cli_user_auth_interceptor.py +1 -1
  9. flwr/cli/config_utils.py +53 -17
  10. flwr/cli/example.py +1 -1
  11. flwr/cli/install.py +1 -1
  12. flwr/cli/log.py +4 -4
  13. flwr/cli/login/__init__.py +1 -1
  14. flwr/cli/login/login.py +15 -8
  15. flwr/cli/ls.py +16 -37
  16. flwr/cli/new/__init__.py +1 -1
  17. flwr/cli/new/new.py +4 -4
  18. flwr/cli/new/templates/__init__.py +1 -1
  19. flwr/cli/new/templates/app/__init__.py +1 -1
  20. flwr/cli/new/templates/app/code/__init__.py +1 -1
  21. flwr/cli/new/templates/app/code/client.baseline.py.tpl +1 -1
  22. flwr/cli/new/templates/app/code/flwr_tune/__init__.py +1 -1
  23. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +4 -4
  24. flwr/cli/new/templates/app/code/model.baseline.py.tpl +1 -1
  25. flwr/cli/new/templates/app/code/server.baseline.py.tpl +2 -3
  26. flwr/cli/new/templates/app/code/task.sklearn.py.tpl +1 -1
  27. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +14 -17
  28. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +4 -4
  29. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  30. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  31. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  32. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  33. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
  34. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  35. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
  36. flwr/cli/run/__init__.py +1 -1
  37. flwr/cli/run/run.py +11 -19
  38. flwr/cli/stop.py +3 -3
  39. flwr/cli/utils.py +42 -17
  40. flwr/client/__init__.py +3 -3
  41. flwr/client/client.py +1 -1
  42. flwr/client/client_app.py +140 -138
  43. flwr/client/clientapp/__init__.py +1 -8
  44. flwr/client/clientapp/utils.py +1 -1
  45. flwr/client/dpfedavg_numpy_client.py +1 -1
  46. flwr/client/grpc_adapter_client/__init__.py +1 -1
  47. flwr/client/grpc_adapter_client/connection.py +5 -5
  48. flwr/client/grpc_rere_client/__init__.py +1 -1
  49. flwr/client/grpc_rere_client/client_interceptor.py +1 -1
  50. flwr/client/grpc_rere_client/connection.py +131 -61
  51. flwr/client/grpc_rere_client/grpc_adapter.py +35 -7
  52. flwr/client/message_handler/__init__.py +1 -1
  53. flwr/client/message_handler/message_handler.py +2 -2
  54. flwr/client/mod/__init__.py +1 -1
  55. flwr/client/mod/centraldp_mods.py +1 -1
  56. flwr/client/mod/comms_mods.py +39 -20
  57. flwr/client/mod/localdp_mod.py +6 -6
  58. flwr/client/mod/secure_aggregation/__init__.py +1 -1
  59. flwr/client/mod/secure_aggregation/secagg_mod.py +1 -1
  60. flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
  61. flwr/client/mod/utils.py +1 -1
  62. flwr/client/numpy_client.py +1 -1
  63. flwr/client/rest_client/__init__.py +1 -1
  64. flwr/client/rest_client/connection.py +174 -68
  65. flwr/client/run_info_store.py +1 -1
  66. flwr/client/typing.py +1 -1
  67. flwr/clientapp/__init__.py +15 -0
  68. flwr/common/__init__.py +3 -3
  69. flwr/common/address.py +1 -1
  70. flwr/common/args.py +1 -1
  71. flwr/common/auth_plugin/__init__.py +3 -1
  72. flwr/common/auth_plugin/auth_plugin.py +30 -4
  73. flwr/common/config.py +1 -1
  74. flwr/common/constant.py +37 -8
  75. flwr/common/context.py +1 -1
  76. flwr/common/date.py +1 -1
  77. flwr/common/differential_privacy.py +1 -1
  78. flwr/common/differential_privacy_constants.py +1 -1
  79. flwr/common/dp.py +1 -1
  80. flwr/common/event_log_plugin/event_log_plugin.py +3 -3
  81. flwr/common/exit/exit.py +6 -6
  82. flwr/common/exit_handlers.py +31 -1
  83. flwr/common/grpc.py +1 -1
  84. flwr/common/heartbeat.py +165 -0
  85. flwr/common/inflatable.py +290 -0
  86. flwr/common/inflatable_grpc_utils.py +99 -0
  87. flwr/common/inflatable_rest_utils.py +99 -0
  88. flwr/common/inflatable_utils.py +341 -0
  89. flwr/common/logger.py +1 -1
  90. flwr/common/message.py +137 -252
  91. flwr/common/object_ref.py +1 -1
  92. flwr/common/parameter.py +1 -1
  93. flwr/common/pyproject.py +1 -1
  94. flwr/common/record/__init__.py +3 -2
  95. flwr/common/record/array.py +323 -0
  96. flwr/common/record/arrayrecord.py +121 -243
  97. flwr/common/record/configrecord.py +71 -16
  98. flwr/common/record/conversion_utils.py +2 -2
  99. flwr/common/record/metricrecord.py +71 -20
  100. flwr/common/record/recorddict.py +207 -90
  101. flwr/common/record/typeddict.py +1 -1
  102. flwr/common/recorddict_compat.py +2 -2
  103. flwr/common/retry_invoker.py +15 -11
  104. flwr/common/secure_aggregation/__init__.py +1 -1
  105. flwr/common/secure_aggregation/crypto/__init__.py +1 -1
  106. flwr/common/secure_aggregation/crypto/shamir.py +52 -30
  107. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +1 -1
  108. flwr/common/secure_aggregation/ndarrays_arithmetic.py +1 -1
  109. flwr/common/secure_aggregation/quantization.py +1 -1
  110. flwr/common/secure_aggregation/secaggplus_constants.py +1 -1
  111. flwr/common/secure_aggregation/secaggplus_utils.py +1 -1
  112. flwr/common/serde.py +60 -184
  113. flwr/common/serde_utils.py +175 -0
  114. flwr/common/telemetry.py +2 -2
  115. flwr/common/typing.py +6 -4
  116. flwr/common/version.py +1 -1
  117. flwr/compat/__init__.py +15 -0
  118. flwr/compat/client/__init__.py +15 -0
  119. flwr/{client → compat/client}/app.py +71 -211
  120. flwr/{client → compat/client}/grpc_client/__init__.py +1 -1
  121. flwr/{client → compat/client}/grpc_client/connection.py +13 -13
  122. flwr/compat/common/__init__.py +15 -0
  123. flwr/compat/server/__init__.py +15 -0
  124. flwr/compat/server/app.py +174 -0
  125. flwr/compat/simulation/__init__.py +15 -0
  126. flwr/proto/__init__.py +1 -1
  127. flwr/proto/fleet_pb2.py +32 -27
  128. flwr/proto/fleet_pb2.pyi +49 -35
  129. flwr/proto/fleet_pb2_grpc.py +117 -13
  130. flwr/proto/fleet_pb2_grpc.pyi +47 -6
  131. flwr/proto/heartbeat_pb2.py +33 -0
  132. flwr/proto/heartbeat_pb2.pyi +66 -0
  133. flwr/proto/heartbeat_pb2_grpc.py +4 -0
  134. flwr/proto/heartbeat_pb2_grpc.pyi +4 -0
  135. flwr/proto/message_pb2.py +28 -11
  136. flwr/proto/message_pb2.pyi +125 -0
  137. flwr/proto/recorddict_pb2.py +16 -28
  138. flwr/proto/recorddict_pb2.pyi +46 -64
  139. flwr/proto/run_pb2.py +24 -32
  140. flwr/proto/run_pb2.pyi +4 -52
  141. flwr/proto/serverappio_pb2.py +32 -23
  142. flwr/proto/serverappio_pb2.pyi +45 -3
  143. flwr/proto/serverappio_pb2_grpc.py +138 -34
  144. flwr/proto/serverappio_pb2_grpc.pyi +54 -13
  145. flwr/proto/simulationio_pb2.py +12 -11
  146. flwr/proto/simulationio_pb2_grpc.py +35 -0
  147. flwr/proto/simulationio_pb2_grpc.pyi +14 -0
  148. flwr/server/__init__.py +2 -2
  149. flwr/server/app.py +69 -187
  150. flwr/server/client_manager.py +1 -1
  151. flwr/server/client_proxy.py +1 -1
  152. flwr/server/compat/__init__.py +1 -1
  153. flwr/server/compat/app.py +1 -1
  154. flwr/server/compat/app_utils.py +51 -29
  155. flwr/server/compat/legacy_context.py +1 -1
  156. flwr/server/criterion.py +1 -1
  157. flwr/server/fleet_event_log_interceptor.py +2 -2
  158. flwr/server/grid/grid.py +3 -3
  159. flwr/server/grid/grpc_grid.py +104 -34
  160. flwr/server/grid/inmemory_grid.py +5 -4
  161. flwr/server/history.py +1 -1
  162. flwr/server/run_serverapp.py +1 -1
  163. flwr/server/server.py +1 -1
  164. flwr/server/server_app.py +65 -58
  165. flwr/server/server_config.py +1 -1
  166. flwr/server/serverapp/__init__.py +1 -1
  167. flwr/server/serverapp/app.py +19 -1
  168. flwr/server/serverapp_components.py +1 -1
  169. flwr/server/strategy/__init__.py +1 -1
  170. flwr/server/strategy/aggregate.py +1 -1
  171. flwr/server/strategy/bulyan.py +2 -2
  172. flwr/server/strategy/dp_adaptive_clipping.py +17 -17
  173. flwr/server/strategy/dp_fixed_clipping.py +17 -17
  174. flwr/server/strategy/dpfedavg_adaptive.py +1 -1
  175. flwr/server/strategy/dpfedavg_fixed.py +1 -1
  176. flwr/server/strategy/fault_tolerant_fedavg.py +1 -1
  177. flwr/server/strategy/fedadagrad.py +1 -1
  178. flwr/server/strategy/fedadam.py +1 -1
  179. flwr/server/strategy/fedavg.py +1 -1
  180. flwr/server/strategy/fedavg_android.py +1 -1
  181. flwr/server/strategy/fedavgm.py +1 -1
  182. flwr/server/strategy/fedmedian.py +1 -1
  183. flwr/server/strategy/fedopt.py +1 -1
  184. flwr/server/strategy/fedprox.py +1 -1
  185. flwr/server/strategy/fedtrimmedavg.py +1 -1
  186. flwr/server/strategy/fedxgb_bagging.py +1 -1
  187. flwr/server/strategy/fedxgb_cyclic.py +1 -1
  188. flwr/server/strategy/fedxgb_nn_avg.py +3 -2
  189. flwr/server/strategy/fedyogi.py +1 -1
  190. flwr/server/strategy/krum.py +1 -1
  191. flwr/server/strategy/qfedavg.py +1 -1
  192. flwr/server/strategy/strategy.py +1 -1
  193. flwr/server/superlink/__init__.py +1 -1
  194. flwr/server/superlink/ffs/__init__.py +3 -1
  195. flwr/server/superlink/ffs/disk_ffs.py +1 -1
  196. flwr/server/superlink/ffs/ffs.py +1 -1
  197. flwr/server/superlink/ffs/ffs_factory.py +1 -1
  198. flwr/server/superlink/fleet/__init__.py +1 -1
  199. flwr/server/superlink/fleet/grpc_adapter/__init__.py +1 -1
  200. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +14 -4
  201. flwr/server/superlink/fleet/grpc_bidi/__init__.py +1 -1
  202. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -1
  203. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +1 -1
  204. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +1 -1
  205. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +13 -13
  206. flwr/server/superlink/fleet/grpc_rere/__init__.py +1 -1
  207. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +102 -8
  208. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +1 -1
  209. flwr/server/superlink/fleet/message_handler/__init__.py +1 -1
  210. flwr/server/superlink/fleet/message_handler/message_handler.py +136 -19
  211. flwr/server/superlink/fleet/rest_rere/__init__.py +1 -1
  212. flwr/server/superlink/fleet/rest_rere/rest_api.py +73 -12
  213. flwr/server/superlink/fleet/vce/__init__.py +1 -1
  214. flwr/server/superlink/fleet/vce/backend/__init__.py +1 -1
  215. flwr/server/superlink/fleet/vce/backend/backend.py +1 -1
  216. flwr/server/superlink/fleet/vce/backend/raybackend.py +1 -1
  217. flwr/server/superlink/fleet/vce/vce_api.py +7 -4
  218. flwr/server/superlink/linkstate/__init__.py +1 -1
  219. flwr/server/superlink/linkstate/in_memory_linkstate.py +139 -44
  220. flwr/server/superlink/linkstate/linkstate.py +54 -21
  221. flwr/server/superlink/linkstate/linkstate_factory.py +1 -1
  222. flwr/server/superlink/linkstate/sqlite_linkstate.py +150 -56
  223. flwr/server/superlink/linkstate/utils.py +34 -30
  224. flwr/server/superlink/serverappio/serverappio_grpc.py +3 -0
  225. flwr/server/superlink/serverappio/serverappio_servicer.py +211 -57
  226. flwr/server/superlink/simulation/__init__.py +1 -1
  227. flwr/server/superlink/simulation/simulationio_grpc.py +1 -1
  228. flwr/server/superlink/simulation/simulationio_servicer.py +26 -2
  229. flwr/server/superlink/utils.py +45 -3
  230. flwr/server/typing.py +1 -1
  231. flwr/server/utils/__init__.py +1 -1
  232. flwr/server/utils/tensorboard.py +1 -1
  233. flwr/server/utils/validator.py +3 -3
  234. flwr/server/workflow/__init__.py +1 -1
  235. flwr/server/workflow/constant.py +1 -1
  236. flwr/server/workflow/default_workflows.py +1 -1
  237. flwr/server/workflow/secure_aggregation/__init__.py +1 -1
  238. flwr/server/workflow/secure_aggregation/secagg_workflow.py +1 -1
  239. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
  240. flwr/serverapp/__init__.py +15 -0
  241. flwr/simulation/__init__.py +1 -1
  242. flwr/simulation/app.py +18 -1
  243. flwr/simulation/legacy_app.py +1 -1
  244. flwr/simulation/ray_transport/__init__.py +1 -1
  245. flwr/simulation/ray_transport/ray_actor.py +1 -1
  246. flwr/simulation/ray_transport/ray_client_proxy.py +1 -1
  247. flwr/simulation/ray_transport/utils.py +1 -1
  248. flwr/simulation/run_simulation.py +2 -2
  249. flwr/simulation/simulationio_connection.py +1 -1
  250. flwr/supercore/__init__.py +15 -0
  251. flwr/supercore/object_store/__init__.py +24 -0
  252. flwr/supercore/object_store/in_memory_object_store.py +229 -0
  253. flwr/supercore/object_store/object_store.py +192 -0
  254. flwr/supercore/object_store/object_store_factory.py +44 -0
  255. flwr/superexec/__init__.py +1 -1
  256. flwr/superexec/app.py +1 -1
  257. flwr/superexec/deployment.py +7 -3
  258. flwr/superexec/exec_event_log_interceptor.py +4 -4
  259. flwr/superexec/exec_grpc.py +8 -4
  260. flwr/superexec/exec_servicer.py +126 -24
  261. flwr/superexec/exec_user_auth_interceptor.py +38 -9
  262. flwr/superexec/executor.py +5 -1
  263. flwr/superexec/simulation.py +8 -2
  264. flwr/superlink/__init__.py +15 -0
  265. flwr/{client/supernode → supernode}/__init__.py +1 -8
  266. flwr/{client/nodestate/nodestate.py → supernode/cli/__init__.py} +8 -15
  267. flwr/{client/supernode/app.py → supernode/cli/flower_supernode.py} +4 -13
  268. flwr/supernode/cli/flwr_clientapp.py +81 -0
  269. flwr/{client → supernode}/nodestate/__init__.py +1 -1
  270. flwr/supernode/nodestate/in_memory_nodestate.py +190 -0
  271. flwr/supernode/nodestate/nodestate.py +212 -0
  272. flwr/{client → supernode}/nodestate/nodestate_factory.py +1 -1
  273. flwr/supernode/runtime/__init__.py +15 -0
  274. flwr/{client/clientapp/app.py → supernode/runtime/run_clientapp.py} +26 -57
  275. flwr/supernode/servicer/__init__.py +15 -0
  276. flwr/supernode/servicer/clientappio/__init__.py +24 -0
  277. flwr/{client/clientapp → supernode/servicer/clientappio}/clientappio_servicer.py +1 -1
  278. flwr/supernode/start_client_internal.py +491 -0
  279. {flwr-1.17.0.dist-info → flwr-1.19.0.dist-info}/METADATA +6 -5
  280. flwr-1.19.0.dist-info/RECORD +365 -0
  281. {flwr-1.17.0.dist-info → flwr-1.19.0.dist-info}/WHEEL +1 -1
  282. {flwr-1.17.0.dist-info → flwr-1.19.0.dist-info}/entry_points.txt +2 -2
  283. flwr/client/heartbeat.py +0 -74
  284. flwr/client/nodestate/in_memory_nodestate.py +0 -38
  285. flwr-1.17.0.dist-info/LICENSE +0 -202
  286. flwr-1.17.0.dist-info/RECORD +0 -333
@@ -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
+ """InflatableObject REST utils."""
16
+
17
+
18
+ from typing import Callable
19
+
20
+ from flwr.proto.message_pb2 import ( # pylint: disable=E0611
21
+ PullObjectRequest,
22
+ PullObjectResponse,
23
+ PushObjectRequest,
24
+ PushObjectResponse,
25
+ )
26
+ from flwr.proto.node_pb2 import Node # pylint: disable=E0611
27
+
28
+ from .inflatable_utils import ObjectIdNotPreregisteredError, ObjectUnavailableError
29
+
30
+
31
+ def make_pull_object_fn_rest(
32
+ pull_object_rest: Callable[[PullObjectRequest], PullObjectResponse],
33
+ node: Node,
34
+ run_id: int,
35
+ ) -> Callable[[str], bytes]:
36
+ """Create a pull object function that uses REST to pull objects.
37
+
38
+ Parameters
39
+ ----------
40
+ pull_object_rest : Callable[[PullObjectRequest], PullObjectResponse]
41
+ A function that makes a POST request against the `/push-object` REST endpoint
42
+ node : Node
43
+ The node making the request.
44
+ run_id : int
45
+ The run ID for the current operation.
46
+
47
+ Returns
48
+ -------
49
+ Callable[[str], bytes]
50
+ A function that takes an object ID and returns the object content as bytes.
51
+ The function raises `ObjectIdNotPreregisteredError` if the object ID is not
52
+ pre-registered, or `ObjectUnavailableError` if the object is not yet available.
53
+ """
54
+
55
+ def pull_object_fn(object_id: str) -> bytes:
56
+ request = PullObjectRequest(node=node, run_id=run_id, object_id=object_id)
57
+ response: PullObjectResponse = pull_object_rest(request)
58
+ if not response.object_found:
59
+ raise ObjectIdNotPreregisteredError(object_id)
60
+ if not response.object_available:
61
+ raise ObjectUnavailableError(object_id)
62
+ return response.object_content
63
+
64
+ return pull_object_fn
65
+
66
+
67
+ def make_push_object_fn_rest(
68
+ push_object_rest: Callable[[PushObjectRequest], PushObjectResponse],
69
+ node: Node,
70
+ run_id: int,
71
+ ) -> Callable[[str, bytes], None]:
72
+ """Create a push object function that uses REST to push objects.
73
+
74
+ Parameters
75
+ ----------
76
+ push_object_rest : Callable[[PushObjectRequest], PushObjectResponse]
77
+ A function that makes a POST request against the `/pull-object` REST endpoint
78
+ node : Node
79
+ The node making the request.
80
+ run_id : int
81
+ The run ID for the current operation.
82
+
83
+ Returns
84
+ -------
85
+ Callable[[str, bytes], None]
86
+ A function that takes an object ID and its content as bytes, and pushes it
87
+ to the servicer. The function raises `ObjectIdNotPreregisteredError` if
88
+ the object ID is not pre-registered.
89
+ """
90
+
91
+ def push_object_fn(object_id: str, object_content: bytes) -> None:
92
+ request = PushObjectRequest(
93
+ node=node, run_id=run_id, object_id=object_id, object_content=object_content
94
+ )
95
+ response: PushObjectResponse = push_object_rest(request)
96
+ if not response.stored:
97
+ raise ObjectIdNotPreregisteredError(object_id)
98
+
99
+ return push_object_fn
@@ -0,0 +1,341 @@
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
+ """InflatableObject utilities."""
16
+
17
+ import concurrent.futures
18
+ import random
19
+ import threading
20
+ import time
21
+ from typing import Callable, Optional
22
+
23
+ from .constant import (
24
+ HEAD_BODY_DIVIDER,
25
+ HEAD_VALUE_DIVIDER,
26
+ MAX_CONCURRENT_PULLS,
27
+ MAX_CONCURRENT_PUSHES,
28
+ PULL_BACKOFF_CAP,
29
+ PULL_INITIAL_BACKOFF,
30
+ PULL_MAX_TIME,
31
+ PULL_MAX_TRIES_PER_OBJECT,
32
+ )
33
+ from .inflatable import (
34
+ InflatableObject,
35
+ UnexpectedObjectContentError,
36
+ _get_object_head,
37
+ get_object_head_values_from_object_content,
38
+ get_object_id,
39
+ is_valid_sha256_hash,
40
+ )
41
+ from .message import Message
42
+ from .record import Array, ArrayRecord, ConfigRecord, MetricRecord, RecordDict
43
+
44
+ # Helper registry that maps names of classes to their type
45
+ inflatable_class_registry: dict[str, type[InflatableObject]] = {
46
+ Array.__qualname__: Array,
47
+ ArrayRecord.__qualname__: ArrayRecord,
48
+ ConfigRecord.__qualname__: ConfigRecord,
49
+ Message.__qualname__: Message,
50
+ MetricRecord.__qualname__: MetricRecord,
51
+ RecordDict.__qualname__: RecordDict,
52
+ }
53
+
54
+
55
+ class ObjectUnavailableError(Exception):
56
+ """Exception raised when an object has been pre-registered but is not yet
57
+ available."""
58
+
59
+ def __init__(self, object_id: str):
60
+ super().__init__(f"Object with ID '{object_id}' is not yet available.")
61
+
62
+
63
+ class ObjectIdNotPreregisteredError(Exception):
64
+ """Exception raised when an object ID is not pre-registered."""
65
+
66
+ def __init__(self, object_id: str):
67
+ super().__init__(f"Object with ID '{object_id}' could not be found.")
68
+
69
+
70
+ def push_objects(
71
+ objects: dict[str, InflatableObject],
72
+ push_object_fn: Callable[[str, bytes], None],
73
+ *,
74
+ object_ids_to_push: Optional[set[str]] = None,
75
+ keep_objects: bool = False,
76
+ max_concurrent_pushes: int = MAX_CONCURRENT_PUSHES,
77
+ ) -> None:
78
+ """Push multiple objects to the servicer.
79
+
80
+ Parameters
81
+ ----------
82
+ objects : dict[str, InflatableObject]
83
+ A dictionary of objects to push, where keys are object IDs and values are
84
+ `InflatableObject` instances.
85
+ push_object_fn : Callable[[str, bytes], None]
86
+ A function that takes an object ID and its content as bytes, and pushes
87
+ it to the servicer. This function should raise `ObjectIdNotPreregisteredError`
88
+ if the object ID is not pre-registered.
89
+ object_ids_to_push : Optional[set[str]] (default: None)
90
+ A set of object IDs to push. If not provided, all objects will be pushed.
91
+ keep_objects : bool (default: False)
92
+ If `True`, the original objects will be kept in the `objects` dictionary
93
+ after pushing. If `False`, they will be removed from the dictionary to avoid
94
+ high memory usage.
95
+ max_concurrent_pushes : int (default: MAX_CONCURRENT_PUSHES)
96
+ The maximum number of concurrent pushes to perform.
97
+ """
98
+ if object_ids_to_push is not None:
99
+ # Filter objects to push only those with IDs in the set
100
+ objects = {k: v for k, v in objects.items() if k in object_ids_to_push}
101
+
102
+ lock = threading.Lock()
103
+
104
+ def push(obj_id: str) -> None:
105
+ """Push a single object."""
106
+ object_content = objects[obj_id].deflate()
107
+ if not keep_objects:
108
+ with lock:
109
+ del objects[obj_id]
110
+ push_object_fn(obj_id, object_content)
111
+
112
+ with concurrent.futures.ThreadPoolExecutor(
113
+ max_workers=max_concurrent_pushes
114
+ ) as executor:
115
+ list(executor.map(push, list(objects.keys())))
116
+
117
+
118
+ def pull_objects( # pylint: disable=too-many-arguments
119
+ object_ids: list[str],
120
+ pull_object_fn: Callable[[str], bytes],
121
+ *,
122
+ max_concurrent_pulls: int = MAX_CONCURRENT_PULLS,
123
+ max_time: Optional[float] = PULL_MAX_TIME,
124
+ max_tries_per_object: Optional[int] = PULL_MAX_TRIES_PER_OBJECT,
125
+ initial_backoff: float = PULL_INITIAL_BACKOFF,
126
+ backoff_cap: float = PULL_BACKOFF_CAP,
127
+ ) -> dict[str, bytes]:
128
+ """Pull multiple objects from the servicer.
129
+
130
+ Parameters
131
+ ----------
132
+ object_ids : list[str]
133
+ A list of object IDs to pull.
134
+ pull_object_fn : Callable[[str], bytes]
135
+ A function that takes an object ID and returns the object content as bytes.
136
+ The function should raise `ObjectUnavailableError` if the object is not yet
137
+ available, or `ObjectIdNotPreregisteredError` if the object ID is not
138
+ pre-registered.
139
+ max_concurrent_pulls : int (default: MAX_CONCURRENT_PULLS)
140
+ The maximum number of concurrent pulls to perform.
141
+ max_time : Optional[float] (default: PULL_MAX_TIME)
142
+ The maximum time to wait for all pulls to complete. If `None`, waits
143
+ indefinitely.
144
+ max_tries_per_object : Optional[int] (default: PULL_MAX_TRIES_PER_OBJECT)
145
+ The maximum number of attempts to pull each object. If `None`, pulls
146
+ indefinitely until the object is available.
147
+ initial_backoff : float (default: PULL_INITIAL_BACKOFF)
148
+ The initial backoff time in seconds for retrying pulls after an
149
+ `ObjectUnavailableError`.
150
+ backoff_cap : float (default: PULL_BACKOFF_CAP)
151
+ The maximum backoff time in seconds. Backoff times will not exceed this value.
152
+
153
+ Returns
154
+ -------
155
+ dict[str, bytes]
156
+ A dictionary where keys are object IDs and values are the pulled
157
+ object contents.
158
+ """
159
+ if max_tries_per_object is None:
160
+ max_tries_per_object = int(1e9)
161
+ if max_time is None:
162
+ max_time = float("inf")
163
+
164
+ results: dict[str, bytes] = {}
165
+ results_lock = threading.Lock()
166
+ err_to_raise: Optional[Exception] = None
167
+ early_stop = threading.Event()
168
+ start = time.monotonic()
169
+
170
+ def pull_with_retries(object_id: str) -> None:
171
+ """Attempt to pull a single object with retry and backoff."""
172
+ nonlocal err_to_raise
173
+ tries = 0
174
+ delay = initial_backoff
175
+
176
+ while not early_stop.is_set():
177
+ try:
178
+ object_content = pull_object_fn(object_id)
179
+ with results_lock:
180
+ results[object_id] = object_content
181
+ return
182
+
183
+ except ObjectUnavailableError as err:
184
+ tries += 1
185
+ if (
186
+ tries >= max_tries_per_object
187
+ or time.monotonic() - start >= max_time
188
+ ):
189
+ # Stop all work if one object exhausts retries
190
+ early_stop.set()
191
+ with results_lock:
192
+ if err_to_raise is None:
193
+ err_to_raise = err
194
+ return
195
+
196
+ # Apply exponential backoff with ±20% jitter
197
+ sleep_time = delay * (1 + random.uniform(-0.2, 0.2))
198
+ early_stop.wait(sleep_time)
199
+ delay = min(delay * 2, backoff_cap)
200
+
201
+ except ObjectIdNotPreregisteredError as err:
202
+ # Permanent failure: object ID is invalid
203
+ early_stop.set()
204
+ with results_lock:
205
+ if err_to_raise is None:
206
+ err_to_raise = err
207
+ return
208
+
209
+ # Submit all pull tasks concurrently
210
+ with concurrent.futures.ThreadPoolExecutor(
211
+ max_workers=max_concurrent_pulls
212
+ ) as executor:
213
+ futures = {
214
+ executor.submit(pull_with_retries, obj_id): obj_id for obj_id in object_ids
215
+ }
216
+
217
+ # Wait for completion
218
+ concurrent.futures.wait(futures)
219
+
220
+ if err_to_raise is not None:
221
+ raise err_to_raise
222
+
223
+ return results
224
+
225
+
226
+ def inflate_object_from_contents(
227
+ object_id: str,
228
+ object_contents: dict[str, bytes],
229
+ *,
230
+ keep_object_contents: bool = False,
231
+ objects: Optional[dict[str, InflatableObject]] = None,
232
+ ) -> InflatableObject:
233
+ """Inflate an object from object contents.
234
+
235
+ Parameters
236
+ ----------
237
+ object_id : str
238
+ The ID of the object to inflate.
239
+ object_contents : dict[str, bytes]
240
+ A dictionary mapping object IDs to their contents as bytes.
241
+ All descendant objects must be present in this dictionary.
242
+ keep_object_contents : bool (default: False)
243
+ If `True`, the object content will be kept in the `object_contents`
244
+ dictionary after inflation. If `False`, the object content will be
245
+ removed from the dictionary to save memory.
246
+ objects : Optional[dict[str, InflatableObject]] (default: None)
247
+ No need to provide this parameter. A dictionary to store already
248
+ inflated objects, mapping object IDs to their corresponding
249
+ `InflatableObject` instances.
250
+
251
+ Returns
252
+ -------
253
+ InflatableObject
254
+ The inflated object.
255
+ """
256
+ if objects is None:
257
+ # Initialize objects dictionary
258
+ objects = {}
259
+
260
+ if object_id in objects:
261
+ # If the object is already in the objects dictionary, return it
262
+ return objects[object_id]
263
+
264
+ # Extract object class and object_ids of children
265
+ object_content = object_contents[object_id]
266
+ obj_type, children_obj_ids, _ = get_object_head_values_from_object_content(
267
+ object_content=object_contents[object_id]
268
+ )
269
+
270
+ # Remove the object content from the dictionary to save memory
271
+ if not keep_object_contents:
272
+ del object_contents[object_id]
273
+
274
+ # Resolve object class
275
+ cls_type = inflatable_class_registry[obj_type]
276
+
277
+ # Inflate all children objects
278
+ children: dict[str, InflatableObject] = {}
279
+ for child_obj_id in children_obj_ids:
280
+ children[child_obj_id] = inflate_object_from_contents(
281
+ child_obj_id,
282
+ object_contents,
283
+ keep_object_contents=keep_object_contents,
284
+ objects=objects,
285
+ )
286
+
287
+ # Inflate object passing its children
288
+ obj = cls_type.inflate(object_content, children=children)
289
+ del object_content # Free memory after inflation
290
+ objects[object_id] = obj
291
+ return obj
292
+
293
+
294
+ def validate_object_content(content: bytes) -> None:
295
+ """Validate the deflated content of an InflatableObject."""
296
+ try:
297
+ # Check if there is a head-body divider
298
+ index = content.find(HEAD_BODY_DIVIDER)
299
+ if index == -1:
300
+ raise ValueError(
301
+ "Unexpected format for object content. Head and body "
302
+ "could not be split."
303
+ )
304
+
305
+ head = _get_object_head(content)
306
+
307
+ # check if the head has three parts:
308
+ # <object_type> <children_ids> <object_body_len>
309
+ head_decoded = head.decode(encoding="utf-8")
310
+ head_parts = head_decoded.split(HEAD_VALUE_DIVIDER)
311
+
312
+ if len(head_parts) != 3:
313
+ raise ValueError("Unexpected format for object head.")
314
+
315
+ obj_type, children_str, body_len = head_parts
316
+
317
+ # Check that children IDs are valid IDs
318
+ children = children_str.split(",")
319
+ for children_id in children:
320
+ if children_id and not is_valid_sha256_hash(children_id):
321
+ raise ValueError(
322
+ f"Detected invalid object ID ({children_id}) in children."
323
+ )
324
+
325
+ # Check that object type is recognized
326
+ if obj_type not in inflatable_class_registry:
327
+ if obj_type != "CustomDataClass": # to allow for the class in tests
328
+ raise ValueError(f"Object of type {obj_type} is not supported.")
329
+
330
+ # Check if the body length in the head matches that of the body
331
+ actual_body_len = len(content) - len(head) - len(HEAD_BODY_DIVIDER)
332
+ if actual_body_len != int(body_len):
333
+ raise ValueError(
334
+ f"Object content length expected {body_len} bytes but got "
335
+ f"{actual_body_len} bytes."
336
+ )
337
+
338
+ except ValueError as err:
339
+ raise UnexpectedObjectContentError(
340
+ object_id=get_object_id(content), reason=str(err)
341
+ ) from err
flwr/common/logger.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2020 Flower Labs GmbH. All Rights Reserved.
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.