flwr 1.23.0__py3-none-any.whl → 1.24.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 (292) hide show
  1. flwr/__init__.py +16 -5
  2. flwr/app/error.py +2 -2
  3. flwr/app/exception.py +3 -3
  4. flwr/cli/app.py +19 -0
  5. flwr/cli/app_cmd/__init__.py +23 -0
  6. flwr/cli/app_cmd/publish.py +285 -0
  7. flwr/cli/app_cmd/review.py +252 -0
  8. flwr/cli/auth_plugin/auth_plugin.py +4 -5
  9. flwr/cli/auth_plugin/noop_auth_plugin.py +54 -11
  10. flwr/cli/auth_plugin/oidc_cli_plugin.py +32 -9
  11. flwr/cli/build.py +60 -18
  12. flwr/cli/cli_account_auth_interceptor.py +24 -7
  13. flwr/cli/config_utils.py +101 -13
  14. flwr/cli/federation/__init__.py +24 -0
  15. flwr/cli/federation/ls.py +140 -0
  16. flwr/cli/federation/show.py +317 -0
  17. flwr/cli/install.py +91 -13
  18. flwr/cli/log.py +52 -9
  19. flwr/cli/login/login.py +7 -4
  20. flwr/cli/ls.py +170 -130
  21. flwr/cli/new/new.py +33 -50
  22. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +1 -0
  23. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
  24. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
  25. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  26. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  27. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  28. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  29. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
  30. flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +1 -1
  31. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  32. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +2 -2
  33. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +1 -1
  34. flwr/cli/pull.py +10 -5
  35. flwr/cli/run/run.py +77 -30
  36. flwr/cli/run_utils.py +130 -0
  37. flwr/cli/stop.py +25 -7
  38. flwr/cli/supernode/ls.py +16 -8
  39. flwr/cli/supernode/register.py +9 -4
  40. flwr/cli/supernode/unregister.py +5 -3
  41. flwr/cli/utils.py +376 -16
  42. flwr/client/__init__.py +1 -1
  43. flwr/client/dpfedavg_numpy_client.py +4 -1
  44. flwr/client/grpc_adapter_client/connection.py +6 -7
  45. flwr/client/grpc_rere_client/connection.py +10 -11
  46. flwr/client/grpc_rere_client/grpc_adapter.py +6 -2
  47. flwr/client/grpc_rere_client/node_auth_client_interceptor.py +2 -1
  48. flwr/client/message_handler/message_handler.py +2 -2
  49. flwr/client/mod/secure_aggregation/secaggplus_mod.py +3 -3
  50. flwr/client/numpy_client.py +1 -1
  51. flwr/client/rest_client/connection.py +12 -14
  52. flwr/client/run_info_store.py +4 -5
  53. flwr/client/typing.py +1 -1
  54. flwr/clientapp/client_app.py +9 -10
  55. flwr/clientapp/mod/centraldp_mods.py +16 -17
  56. flwr/clientapp/mod/localdp_mod.py +8 -9
  57. flwr/clientapp/typing.py +1 -1
  58. flwr/clientapp/utils.py +3 -3
  59. flwr/common/address.py +1 -2
  60. flwr/common/args.py +3 -4
  61. flwr/common/config.py +13 -16
  62. flwr/common/constant.py +5 -2
  63. flwr/common/differential_privacy.py +3 -4
  64. flwr/common/event_log_plugin/event_log_plugin.py +3 -4
  65. flwr/common/exit/exit.py +15 -2
  66. flwr/common/exit/exit_code.py +19 -0
  67. flwr/common/exit/exit_handler.py +6 -2
  68. flwr/common/exit/signal_handler.py +5 -5
  69. flwr/common/grpc.py +6 -6
  70. flwr/common/inflatable_protobuf_utils.py +1 -1
  71. flwr/common/inflatable_utils.py +38 -21
  72. flwr/common/logger.py +19 -19
  73. flwr/common/message.py +4 -4
  74. flwr/common/object_ref.py +7 -7
  75. flwr/common/record/array.py +3 -3
  76. flwr/common/record/arrayrecord.py +18 -30
  77. flwr/common/record/configrecord.py +3 -3
  78. flwr/common/record/recorddict.py +5 -5
  79. flwr/common/record/typeddict.py +9 -2
  80. flwr/common/recorddict_compat.py +7 -10
  81. flwr/common/retry_invoker.py +20 -20
  82. flwr/common/secure_aggregation/ndarrays_arithmetic.py +3 -3
  83. flwr/common/serde.py +5 -4
  84. flwr/common/serde_utils.py +2 -2
  85. flwr/common/telemetry.py +9 -5
  86. flwr/common/typing.py +52 -37
  87. flwr/compat/client/app.py +38 -37
  88. flwr/compat/client/grpc_client/connection.py +11 -11
  89. flwr/compat/server/app.py +5 -6
  90. flwr/proto/appio_pb2.py +13 -3
  91. flwr/proto/appio_pb2.pyi +134 -65
  92. flwr/proto/appio_pb2_grpc.py +20 -0
  93. flwr/proto/appio_pb2_grpc.pyi +27 -0
  94. flwr/proto/clientappio_pb2.py +17 -7
  95. flwr/proto/clientappio_pb2.pyi +15 -0
  96. flwr/proto/clientappio_pb2_grpc.py +206 -40
  97. flwr/proto/clientappio_pb2_grpc.pyi +168 -53
  98. flwr/proto/control_pb2.py +71 -52
  99. flwr/proto/control_pb2.pyi +277 -111
  100. flwr/proto/control_pb2_grpc.py +249 -40
  101. flwr/proto/control_pb2_grpc.pyi +185 -52
  102. flwr/proto/error_pb2.py +13 -3
  103. flwr/proto/error_pb2.pyi +24 -6
  104. flwr/proto/error_pb2_grpc.py +20 -0
  105. flwr/proto/error_pb2_grpc.pyi +27 -0
  106. flwr/proto/fab_pb2.py +14 -4
  107. flwr/proto/fab_pb2.pyi +59 -31
  108. flwr/proto/fab_pb2_grpc.py +20 -0
  109. flwr/proto/fab_pb2_grpc.pyi +27 -0
  110. flwr/proto/federation_pb2.py +38 -0
  111. flwr/proto/federation_pb2.pyi +56 -0
  112. flwr/proto/federation_pb2_grpc.py +24 -0
  113. flwr/proto/federation_pb2_grpc.pyi +31 -0
  114. flwr/proto/fleet_pb2.py +14 -4
  115. flwr/proto/fleet_pb2.pyi +137 -61
  116. flwr/proto/fleet_pb2_grpc.py +189 -48
  117. flwr/proto/fleet_pb2_grpc.pyi +175 -61
  118. flwr/proto/grpcadapter_pb2.py +14 -4
  119. flwr/proto/grpcadapter_pb2.pyi +38 -16
  120. flwr/proto/grpcadapter_pb2_grpc.py +35 -4
  121. flwr/proto/grpcadapter_pb2_grpc.pyi +38 -7
  122. flwr/proto/heartbeat_pb2.py +17 -7
  123. flwr/proto/heartbeat_pb2.pyi +51 -22
  124. flwr/proto/heartbeat_pb2_grpc.py +20 -0
  125. flwr/proto/heartbeat_pb2_grpc.pyi +27 -0
  126. flwr/proto/log_pb2.py +13 -3
  127. flwr/proto/log_pb2.pyi +34 -11
  128. flwr/proto/log_pb2_grpc.py +20 -0
  129. flwr/proto/log_pb2_grpc.pyi +27 -0
  130. flwr/proto/message_pb2.py +15 -5
  131. flwr/proto/message_pb2.pyi +154 -86
  132. flwr/proto/message_pb2_grpc.py +20 -0
  133. flwr/proto/message_pb2_grpc.pyi +27 -0
  134. flwr/proto/node_pb2.py +15 -5
  135. flwr/proto/node_pb2.pyi +50 -25
  136. flwr/proto/node_pb2_grpc.py +20 -0
  137. flwr/proto/node_pb2_grpc.pyi +27 -0
  138. flwr/proto/recorddict_pb2.py +13 -3
  139. flwr/proto/recorddict_pb2.pyi +184 -107
  140. flwr/proto/recorddict_pb2_grpc.py +20 -0
  141. flwr/proto/recorddict_pb2_grpc.pyi +27 -0
  142. flwr/proto/run_pb2.py +40 -31
  143. flwr/proto/run_pb2.pyi +149 -84
  144. flwr/proto/run_pb2_grpc.py +20 -0
  145. flwr/proto/run_pb2_grpc.pyi +27 -0
  146. flwr/proto/serverappio_pb2.py +13 -3
  147. flwr/proto/serverappio_pb2.pyi +32 -8
  148. flwr/proto/serverappio_pb2_grpc.py +246 -65
  149. flwr/proto/serverappio_pb2_grpc.pyi +221 -85
  150. flwr/proto/simulationio_pb2.py +16 -8
  151. flwr/proto/simulationio_pb2.pyi +15 -0
  152. flwr/proto/simulationio_pb2_grpc.py +162 -41
  153. flwr/proto/simulationio_pb2_grpc.pyi +149 -55
  154. flwr/proto/transport_pb2.py +20 -10
  155. flwr/proto/transport_pb2.pyi +249 -160
  156. flwr/proto/transport_pb2_grpc.py +35 -4
  157. flwr/proto/transport_pb2_grpc.pyi +38 -8
  158. flwr/server/app.py +38 -17
  159. flwr/server/client_manager.py +4 -5
  160. flwr/server/client_proxy.py +10 -11
  161. flwr/server/compat/app.py +4 -5
  162. flwr/server/compat/app_utils.py +2 -1
  163. flwr/server/compat/grid_client_proxy.py +10 -12
  164. flwr/server/compat/legacy_context.py +3 -4
  165. flwr/server/fleet_event_log_interceptor.py +2 -1
  166. flwr/server/grid/grid.py +2 -3
  167. flwr/server/grid/grpc_grid.py +10 -8
  168. flwr/server/grid/inmemory_grid.py +4 -4
  169. flwr/server/run_serverapp.py +2 -3
  170. flwr/server/server.py +34 -39
  171. flwr/server/server_app.py +7 -8
  172. flwr/server/server_config.py +1 -2
  173. flwr/server/serverapp/app.py +34 -28
  174. flwr/server/serverapp_components.py +4 -5
  175. flwr/server/strategy/aggregate.py +9 -8
  176. flwr/server/strategy/bulyan.py +13 -11
  177. flwr/server/strategy/dp_adaptive_clipping.py +16 -20
  178. flwr/server/strategy/dp_fixed_clipping.py +12 -17
  179. flwr/server/strategy/dpfedavg_adaptive.py +3 -4
  180. flwr/server/strategy/dpfedavg_fixed.py +6 -10
  181. flwr/server/strategy/fault_tolerant_fedavg.py +14 -13
  182. flwr/server/strategy/fedadagrad.py +18 -14
  183. flwr/server/strategy/fedadam.py +16 -14
  184. flwr/server/strategy/fedavg.py +16 -17
  185. flwr/server/strategy/fedavg_android.py +15 -15
  186. flwr/server/strategy/fedavgm.py +21 -18
  187. flwr/server/strategy/fedmedian.py +2 -3
  188. flwr/server/strategy/fedopt.py +11 -10
  189. flwr/server/strategy/fedprox.py +10 -9
  190. flwr/server/strategy/fedtrimmedavg.py +12 -11
  191. flwr/server/strategy/fedxgb_bagging.py +13 -11
  192. flwr/server/strategy/fedxgb_cyclic.py +6 -6
  193. flwr/server/strategy/fedxgb_nn_avg.py +4 -4
  194. flwr/server/strategy/fedyogi.py +16 -14
  195. flwr/server/strategy/krum.py +12 -11
  196. flwr/server/strategy/qfedavg.py +16 -15
  197. flwr/server/strategy/strategy.py +6 -9
  198. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +2 -1
  199. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -2
  200. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +3 -4
  201. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +10 -12
  202. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +1 -3
  203. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +4 -4
  204. flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +3 -2
  205. flwr/server/superlink/fleet/message_handler/message_handler.py +34 -28
  206. flwr/server/superlink/fleet/rest_rere/rest_api.py +2 -2
  207. flwr/server/superlink/fleet/vce/backend/backend.py +1 -1
  208. flwr/server/superlink/fleet/vce/backend/raybackend.py +5 -5
  209. flwr/server/superlink/fleet/vce/vce_api.py +15 -9
  210. flwr/server/superlink/linkstate/in_memory_linkstate.py +115 -150
  211. flwr/server/superlink/linkstate/linkstate.py +59 -43
  212. flwr/server/superlink/linkstate/linkstate_factory.py +22 -5
  213. flwr/server/superlink/linkstate/sqlite_linkstate.py +447 -438
  214. flwr/server/superlink/linkstate/utils.py +6 -6
  215. flwr/server/superlink/serverappio/serverappio_grpc.py +1 -2
  216. flwr/server/superlink/serverappio/serverappio_servicer.py +26 -21
  217. flwr/server/superlink/simulation/simulationio_grpc.py +1 -2
  218. flwr/server/superlink/simulation/simulationio_servicer.py +18 -13
  219. flwr/server/superlink/utils.py +4 -6
  220. flwr/server/typing.py +1 -1
  221. flwr/server/utils/tensorboard.py +15 -8
  222. flwr/server/workflow/default_workflows.py +5 -5
  223. flwr/server/workflow/secure_aggregation/secagg_workflow.py +2 -4
  224. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +8 -8
  225. flwr/serverapp/strategy/bulyan.py +16 -15
  226. flwr/serverapp/strategy/dp_adaptive_clipping.py +12 -11
  227. flwr/serverapp/strategy/dp_fixed_clipping.py +11 -14
  228. flwr/serverapp/strategy/fedadagrad.py +10 -11
  229. flwr/serverapp/strategy/fedadam.py +10 -11
  230. flwr/serverapp/strategy/fedavg.py +9 -10
  231. flwr/serverapp/strategy/fedavgm.py +17 -16
  232. flwr/serverapp/strategy/fedmedian.py +2 -2
  233. flwr/serverapp/strategy/fedopt.py +10 -11
  234. flwr/serverapp/strategy/fedprox.py +7 -8
  235. flwr/serverapp/strategy/fedtrimmedavg.py +9 -9
  236. flwr/serverapp/strategy/fedxgb_bagging.py +3 -3
  237. flwr/serverapp/strategy/fedxgb_cyclic.py +9 -9
  238. flwr/serverapp/strategy/fedyogi.py +9 -11
  239. flwr/serverapp/strategy/krum.py +7 -7
  240. flwr/serverapp/strategy/multikrum.py +9 -9
  241. flwr/serverapp/strategy/qfedavg.py +17 -16
  242. flwr/serverapp/strategy/strategy.py +6 -9
  243. flwr/serverapp/strategy/strategy_utils.py +7 -8
  244. flwr/simulation/app.py +46 -42
  245. flwr/simulation/legacy_app.py +12 -12
  246. flwr/simulation/ray_transport/ray_actor.py +10 -11
  247. flwr/simulation/ray_transport/ray_client_proxy.py +11 -12
  248. flwr/simulation/run_simulation.py +43 -43
  249. flwr/simulation/simulationio_connection.py +4 -4
  250. flwr/supercore/cli/flower_superexec.py +3 -4
  251. flwr/supercore/constant.py +31 -1
  252. flwr/supercore/corestate/corestate.py +24 -3
  253. flwr/supercore/corestate/in_memory_corestate.py +138 -0
  254. flwr/supercore/corestate/sqlite_corestate.py +157 -0
  255. flwr/supercore/ffs/disk_ffs.py +1 -2
  256. flwr/supercore/ffs/ffs.py +1 -2
  257. flwr/supercore/ffs/ffs_factory.py +1 -2
  258. flwr/{common → supercore}/heartbeat.py +20 -25
  259. flwr/supercore/object_store/in_memory_object_store.py +1 -2
  260. flwr/supercore/object_store/object_store.py +1 -2
  261. flwr/supercore/object_store/object_store_factory.py +1 -2
  262. flwr/supercore/object_store/sqlite_object_store.py +8 -7
  263. flwr/supercore/primitives/asymmetric.py +1 -1
  264. flwr/supercore/primitives/asymmetric_ed25519.py +11 -1
  265. flwr/supercore/sqlite_mixin.py +37 -34
  266. flwr/supercore/superexec/plugin/base_exec_plugin.py +1 -2
  267. flwr/supercore/superexec/plugin/exec_plugin.py +3 -3
  268. flwr/supercore/superexec/run_superexec.py +9 -13
  269. flwr/superlink/artifact_provider/artifact_provider.py +1 -2
  270. flwr/superlink/auth_plugin/auth_plugin.py +6 -9
  271. flwr/superlink/auth_plugin/noop_auth_plugin.py +6 -9
  272. flwr/superlink/federation/__init__.py +24 -0
  273. flwr/superlink/federation/federation_manager.py +64 -0
  274. flwr/superlink/federation/noop_federation_manager.py +71 -0
  275. flwr/superlink/servicer/control/control_account_auth_interceptor.py +22 -13
  276. flwr/superlink/servicer/control/control_event_log_interceptor.py +7 -7
  277. flwr/superlink/servicer/control/control_grpc.py +5 -6
  278. flwr/superlink/servicer/control/control_license_interceptor.py +3 -3
  279. flwr/superlink/servicer/control/control_servicer.py +102 -18
  280. flwr/supernode/cli/flower_supernode.py +58 -3
  281. flwr/supernode/nodestate/in_memory_nodestate.py +60 -49
  282. flwr/supernode/nodestate/nodestate.py +7 -8
  283. flwr/supernode/nodestate/nodestate_factory.py +7 -4
  284. flwr/supernode/runtime/run_clientapp.py +41 -22
  285. flwr/supernode/servicer/clientappio/clientappio_servicer.py +40 -10
  286. flwr/supernode/start_client_internal.py +158 -42
  287. {flwr-1.23.0.dist-info → flwr-1.24.0.dist-info}/METADATA +8 -8
  288. flwr-1.24.0.dist-info/RECORD +454 -0
  289. flwr/supercore/object_store/utils.py +0 -43
  290. flwr-1.23.0.dist-info/RECORD +0 -439
  291. {flwr-1.23.0.dist-info → flwr-1.24.0.dist-info}/WHEEL +0 -0
  292. {flwr-1.23.0.dist-info → flwr-1.24.0.dist-info}/entry_points.txt +0 -0
@@ -16,24 +16,27 @@
16
16
 
17
17
 
18
18
  import random
19
+ import signal
19
20
  import threading
20
- from typing import Callable, Union
21
+ from collections.abc import Callable
21
22
 
22
23
  import grpc
23
24
 
25
+ from flwr.common.constant import (
26
+ HEARTBEAT_BASE_MULTIPLIER,
27
+ HEARTBEAT_CALL_TIMEOUT,
28
+ HEARTBEAT_DEFAULT_INTERVAL,
29
+ HEARTBEAT_RANDOM_RANGE,
30
+ )
31
+ from flwr.common.retry_invoker import RetryInvoker, exponential
32
+ from flwr.proto.clientappio_pb2_grpc import ClientAppIoStub
33
+
24
34
  # pylint: disable=E0611
25
35
  from flwr.proto.heartbeat_pb2 import SendAppHeartbeatRequest
26
36
  from flwr.proto.serverappio_pb2_grpc import ServerAppIoStub
27
37
  from flwr.proto.simulationio_pb2_grpc import SimulationIoStub
28
38
 
29
39
  # pylint: enable=E0611
30
- from .constant import (
31
- HEARTBEAT_BASE_MULTIPLIER,
32
- HEARTBEAT_CALL_TIMEOUT,
33
- HEARTBEAT_DEFAULT_INTERVAL,
34
- HEARTBEAT_RANDOM_RANGE,
35
- )
36
- from .retry_invoker import RetryInvoker, exponential
37
40
 
38
41
 
39
42
  class HeartbeatFailure(Exception):
@@ -116,24 +119,18 @@ class HeartbeatSender:
116
119
  raise HeartbeatFailure
117
120
 
118
121
 
119
- def get_grpc_app_heartbeat_fn(
120
- stub: Union[ServerAppIoStub, SimulationIoStub],
121
- run_id: int,
122
- *,
123
- failure_message: str,
122
+ def make_app_heartbeat_fn_grpc(
123
+ stub: ServerAppIoStub | SimulationIoStub | ClientAppIoStub,
124
+ token: str,
124
125
  ) -> Callable[[], bool]:
125
- """Get the function to send a heartbeat to gRPC endpoint.
126
-
127
- This function is for app heartbeats only. It is not used for node heartbeats.
126
+ """Get the function to send a heartbeat to gRPC endpoint from an app process.
128
127
 
129
128
  Parameters
130
129
  ----------
131
130
  stub : Union[ServerAppIoStub, SimulationIoStub]
132
131
  gRPC stub to send the heartbeat.
133
- run_id : int
134
- The run ID to use in the heartbeat request.
135
- failure_message : str
136
- Error message to raise if the heartbeat fails.
132
+ token : str
133
+ The token to use in the heartbeat request.
137
134
 
138
135
  Returns
139
136
  -------
@@ -141,9 +138,7 @@ def get_grpc_app_heartbeat_fn(
141
138
  Function that sends a heartbeat to the gRPC endpoint.
142
139
  """
143
140
  # Construct the heartbeat request
144
- req = SendAppHeartbeatRequest(
145
- run_id=run_id, heartbeat_interval=HEARTBEAT_DEFAULT_INTERVAL
146
- )
141
+ req = SendAppHeartbeatRequest(token=token)
147
142
 
148
143
  def fn() -> bool:
149
144
  # Call ServerAppIo API
@@ -157,9 +152,9 @@ def get_grpc_app_heartbeat_fn(
157
152
  return False
158
153
  raise
159
154
 
160
- # Check if not successful
155
+ # Raise SIGINT to trigger graceful shutdown if heartbeat failed
161
156
  if not res.success:
162
- raise RuntimeError(failure_message)
157
+ signal.raise_signal(signal.SIGINT)
163
158
  return True
164
159
 
165
160
  return fn
@@ -17,7 +17,6 @@
17
17
 
18
18
  import threading
19
19
  from dataclasses import dataclass
20
- from typing import Optional
21
20
 
22
21
  from flwr.common.inflatable import (
23
22
  get_object_id,
@@ -154,7 +153,7 @@ class InMemoryObjectStore(ObjectStore):
154
153
  self.store[object_id].content = object_content
155
154
  self.store[object_id].is_available = True
156
155
 
157
- def get(self, object_id: str) -> Optional[bytes]:
156
+ def get(self, object_id: str) -> bytes | None:
158
157
  """Get an object from the store."""
159
158
  with self.lock_store:
160
159
  # Check if the object ID is pre-registered
@@ -16,7 +16,6 @@
16
16
 
17
17
 
18
18
  import abc
19
- from typing import Optional
20
19
 
21
20
  from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
22
21
 
@@ -89,7 +88,7 @@ class ObjectStore(abc.ABC):
89
88
  """
90
89
 
91
90
  @abc.abstractmethod
92
- def get(self, object_id: str) -> Optional[bytes]:
91
+ def get(self, object_id: str) -> bytes | None:
93
92
  """Get an object from the store.
94
93
 
95
94
  Parameters
@@ -16,7 +16,6 @@
16
16
 
17
17
 
18
18
  from logging import DEBUG
19
- from typing import Optional
20
19
 
21
20
  from flwr.common.logger import log
22
21
  from flwr.supercore.constant import FLWR_IN_MEMORY_DB_NAME
@@ -40,7 +39,7 @@ class ObjectStoreFactory:
40
39
 
41
40
  def __init__(self, database: str = FLWR_IN_MEMORY_DB_NAME) -> None:
42
41
  self.database = database
43
- self.store_instance: Optional[ObjectStore] = None
42
+ self.store_instance: ObjectStore | None = None
44
43
 
45
44
  def store(self) -> ObjectStore:
46
45
  """Return an ObjectStore instance and create it, if necessary.
@@ -15,7 +15,7 @@
15
15
  """Flower SQLite ObjectStore implementation."""
16
16
 
17
17
 
18
- from typing import Optional, cast
18
+ from typing import cast
19
19
 
20
20
  from flwr.common.inflatable import (
21
21
  get_object_id,
@@ -63,13 +63,12 @@ class SqliteObjectStore(ObjectStore, SqliteMixin):
63
63
  super().__init__(database_path)
64
64
  self.verify = verify
65
65
 
66
- def initialize(self, log_queries: bool = False) -> list[tuple[str]]:
67
- """Connect to the DB, enable FK support, and create tables if needed."""
68
- return self._ensure_initialized(
66
+ def get_sql_statements(self) -> tuple[str, ...]:
67
+ """Return SQL statements for ObjectStore tables."""
68
+ return (
69
69
  SQL_CREATE_OBJECTS,
70
70
  SQL_CREATE_OBJECT_CHILDREN,
71
71
  SQL_CREATE_RUN_OBJECTS,
72
- log_queries=log_queries,
73
72
  )
74
73
 
75
74
  def preregister(self, run_id: int, object_tree: ObjectTree) -> list[str]:
@@ -126,7 +125,9 @@ class SqliteObjectStore(ObjectStore, SqliteMixin):
126
125
  "SELECT object_id FROM objects WHERE object_id=?", (object_id,)
127
126
  ).fetchone()
128
127
  if not row:
129
- raise NoObjectInStoreError(f"Object {object_id} not found.")
128
+ raise NoObjectInStoreError(
129
+ f"Object {object_id} was not pre-registered."
130
+ )
130
131
  children = self.query(
131
132
  "SELECT child_id FROM object_children WHERE parent_id=?", (object_id,)
132
133
  )
@@ -176,7 +177,7 @@ class SqliteObjectStore(ObjectStore, SqliteMixin):
176
177
  (object_content, object_id),
177
178
  )
178
179
 
179
- def get(self, object_id: str) -> Optional[bytes]:
180
+ def get(self, object_id: str) -> bytes | None:
180
181
  """Get an object from the store."""
181
182
  rows = self.query("SELECT content FROM objects WHERE object_id=?", (object_id,))
182
183
  return rows[0]["content"] if rows else None
@@ -113,5 +113,5 @@ def uses_nist_ec_curve(public_key: ec.EllipticCurvePublicKey) -> bool:
113
113
  """Return True if the provided key uses a NIST EC curve."""
114
114
  return isinstance(
115
115
  public_key.curve,
116
- (ec.SECP192R1, ec.SECP224R1, ec.SECP256R1, ec.SECP384R1, ec.SECP521R1),
116
+ (ec.SECP192R1 | ec.SECP224R1 | ec.SECP256R1 | ec.SECP384R1 | ec.SECP521R1),
117
117
  )
@@ -15,6 +15,7 @@
15
15
  """Ed25519-only asymmetric cryptography utilities."""
16
16
 
17
17
  import base64
18
+ from pathlib import Path
18
19
 
19
20
  from cryptography.exceptions import InvalidSignature
20
21
  from cryptography.hazmat.primitives import serialization
@@ -150,7 +151,7 @@ def verify_signature(
150
151
  return False
151
152
 
152
153
 
153
- def create_signed_message(fab_digest: bytes, timestamp: int) -> bytes:
154
+ def create_message_to_sign(fab_digest: bytes, timestamp: int) -> bytes:
154
155
  """Create a canonical message:
155
156
  timestamp (8 bytes big-endian) + fab_digest.
156
157
  """
@@ -163,3 +164,12 @@ def decode_base64url(sig: str) -> bytes:
163
164
  # add missing padding (=) to a multiple of 4
164
165
  pad = (-len(sig)) % 4
165
166
  return base64.urlsafe_b64decode(sig + ("=" * pad))
167
+
168
+
169
+ def load_private_key(path: Path) -> ed25519.Ed25519PrivateKey:
170
+ """Load an SSH-format private key (Ed25519) using cryptography."""
171
+ key_bytes = path.read_bytes()
172
+ private_key = serialization.load_ssh_private_key(key_bytes, password=None)
173
+ if not isinstance(private_key, ed25519.Ed25519PrivateKey):
174
+ raise ValueError("Private key is not Ed25519")
175
+ return private_key
@@ -17,14 +17,14 @@
17
17
 
18
18
  import re
19
19
  import sqlite3
20
- from abc import ABC, abstractmethod
20
+ from abc import ABC
21
21
  from collections.abc import Sequence
22
22
  from logging import DEBUG, ERROR
23
- from typing import Any, Optional, Union
23
+ from typing import Any
24
24
 
25
25
  from flwr.common.logger import log
26
26
 
27
- DictOrTuple = Union[tuple[Any, ...], dict[str, Any]]
27
+ DictOrTuple = tuple[Any, ...] | dict[str, Any]
28
28
 
29
29
 
30
30
  class SqliteMixin(ABC):
@@ -32,7 +32,7 @@ class SqliteMixin(ABC):
32
32
 
33
33
  def __init__(self, database_path: str) -> None:
34
34
  self.database_path = database_path
35
- self._conn: Optional[sqlite3.Connection] = None
35
+ self._conn: sqlite3.Connection | None = None
36
36
 
37
37
  @property
38
38
  def conn(self) -> sqlite3.Connection:
@@ -41,10 +41,24 @@ class SqliteMixin(ABC):
41
41
  raise AttributeError("Database not initialized. Call initialize() first.")
42
42
  return self._conn
43
43
 
44
- @abstractmethod
44
+ def get_sql_statements(self) -> tuple[str, ...]:
45
+ """Return SQL statements for this class.
46
+
47
+ Subclasses can override this to provide their SQL CREATE statements.
48
+ The base implementation returns an empty tuple.
49
+
50
+ Returns
51
+ -------
52
+ tuple[str, ...]
53
+ SQL CREATE TABLE/INDEX statements for this class.
54
+ """
55
+ return ()
56
+
45
57
  def initialize(self, log_queries: bool = False) -> list[tuple[str]]:
46
58
  """Connect to the DB, enable FK support, and create tables if needed.
47
59
 
60
+ This method executes SQL statements returned by `get_sql_statements()`.
61
+
48
62
  Parameters
49
63
  ----------
50
64
  log_queries : bool
@@ -57,45 +71,34 @@ class SqliteMixin(ABC):
57
71
 
58
72
  Examples
59
73
  --------
60
- Implement in subclass:
74
+ Override `get_sql_statements()` in your subclass:
61
75
 
62
76
  .. code:: python
63
77
 
64
- def initialize(self, log_queries: bool = False) -> list[tuple[str]]:
65
- return self._ensure_initialized(
78
+ def get_sql_statements(self) -> tuple[str, ...]:
79
+ return (
66
80
  SQL_CREATE_TABLE_FOO,
67
81
  SQL_CREATE_TABLE_BAR,
68
- log_queries=log_queries
69
82
  )
70
- """
71
-
72
- def _ensure_initialized(
73
- self,
74
- *create_statements: str,
75
- log_queries: bool = False,
76
- ) -> list[tuple[str]]:
77
- """Connect to the DB, enable FK support, and create tables if needed.
78
83
 
79
- Subclasses should call this with their own CREATE TABLE/INDEX statements in
80
- their `.initialize()` methods.
84
+ To include parent SQL statements, call super():
81
85
 
82
- Parameters
83
- ----------
84
- create_statements : str
85
- SQL statements to create tables and indexes.
86
- log_queries : bool
87
- Log each query which is executed.
86
+ .. code:: python
88
87
 
89
- Returns
90
- -------
91
- list[tuple[str]]
92
- The list of all tables in the DB.
88
+ def get_sql_statements(self) -> tuple[str, ...]:
89
+ return super().get_sql_statements() + (
90
+ SQL_CREATE_TABLE_FOO,
91
+ SQL_CREATE_TABLE_BAR,
92
+ )
93
93
  """
94
94
  self._conn = sqlite3.connect(self.database_path)
95
95
  # Enable Write-Ahead Logging (WAL) for better concurrency
96
96
  self._conn.execute("PRAGMA journal_mode = WAL;")
97
97
  self._conn.execute("PRAGMA synchronous = NORMAL;")
98
98
  self._conn.execute("PRAGMA foreign_keys = ON;")
99
+ self._conn.execute("PRAGMA cache_size = -64000;") # 64MB cache
100
+ self._conn.execute("PRAGMA temp_store = MEMORY;") # In-memory temp tables
101
+ self._conn.execute("PRAGMA mmap_size = 268435456;") # 256MB memory-mapped I/O
99
102
  self._conn.row_factory = dict_factory
100
103
 
101
104
  if log_queries:
@@ -103,7 +106,7 @@ class SqliteMixin(ABC):
103
106
 
104
107
  # Create tables and indexes
105
108
  cur = self._conn.cursor()
106
- for sql in create_statements:
109
+ for sql in self.get_sql_statements():
107
110
  cur.execute(sql)
108
111
  res = cur.execute("SELECT name FROM sqlite_schema;")
109
112
  return res.fetchall()
@@ -111,7 +114,7 @@ class SqliteMixin(ABC):
111
114
  def query(
112
115
  self,
113
116
  query: str,
114
- data: Optional[Union[Sequence[DictOrTuple], DictOrTuple]] = None,
117
+ data: Sequence[DictOrTuple] | DictOrTuple | None = None,
115
118
  ) -> list[dict[str, Any]]:
116
119
  """Execute a SQL query and return the results as list of dicts."""
117
120
  if self._conn is None:
@@ -127,8 +130,8 @@ class SqliteMixin(ABC):
127
130
  with self._conn:
128
131
  if (
129
132
  len(data) > 0
130
- and isinstance(data, (tuple, list))
131
- and isinstance(data[0], (tuple, dict))
133
+ and isinstance(data, (tuple | list))
134
+ and isinstance(data[0], (tuple | dict))
132
135
  ):
133
136
  rows = self._conn.executemany(query, data)
134
137
  else:
@@ -153,4 +156,4 @@ def dict_factory(
153
156
  Less efficent for retrival of large amounts of data but easier to use.
154
157
  """
155
158
  fields = [column[0] for column in cursor.description]
156
- return dict(zip(fields, row))
159
+ return dict(zip(fields, row, strict=True))
@@ -18,7 +18,6 @@
18
18
  import os
19
19
  import subprocess
20
20
  from collections.abc import Sequence
21
- from typing import Optional
22
21
 
23
22
  from .exec_plugin import ExecPlugin
24
23
 
@@ -33,7 +32,7 @@ class BaseExecPlugin(ExecPlugin):
33
32
  command = ""
34
33
  appio_api_address_arg = ""
35
34
 
36
- def select_run_id(self, candidate_run_ids: Sequence[int]) -> Optional[int]:
35
+ def select_run_id(self, candidate_run_ids: Sequence[int]) -> int | None:
37
36
  """Select a run ID to execute from a sequence of candidates."""
38
37
  if not candidate_run_ids:
39
38
  return None
@@ -16,8 +16,8 @@
16
16
 
17
17
 
18
18
  from abc import ABC, abstractmethod
19
- from collections.abc import Sequence
20
- from typing import Any, Callable, Optional
19
+ from collections.abc import Callable, Sequence
20
+ from typing import Any
21
21
 
22
22
  from flwr.common.typing import Run
23
23
 
@@ -36,7 +36,7 @@ class ExecPlugin(ABC):
36
36
  self.get_run = get_run
37
37
 
38
38
  @abstractmethod
39
- def select_run_id(self, candidate_run_ids: Sequence[int]) -> Optional[int]:
39
+ def select_run_id(self, candidate_run_ids: Sequence[int]) -> int | None:
40
40
  """Select a run ID to execute from a sequence of candidates.
41
41
 
42
42
  A candidate run ID is one that has at least one pending message and is
@@ -17,7 +17,7 @@
17
17
 
18
18
  import time
19
19
  from logging import WARN
20
- from typing import Any, Optional, Union
20
+ from typing import Any
21
21
 
22
22
  from flwr.common.config import get_flwr_dir
23
23
  from flwr.common.exit import ExitCode, flwr_exit, register_signal_handlers
@@ -43,14 +43,12 @@ from .plugin import ExecPlugin
43
43
 
44
44
  def run_superexec( # pylint: disable=R0913,R0914,R0917
45
45
  plugin_class: type[ExecPlugin],
46
- stub_class: Union[
47
- type[ClientAppIoStub], type[ServerAppIoStub], type[SimulationIoStub]
48
- ],
46
+ stub_class: type[ClientAppIoStub] | type[ServerAppIoStub] | type[SimulationIoStub],
49
47
  appio_api_address: str,
50
- plugin_config: Optional[dict[str, Any]] = None,
51
- flwr_dir: Optional[str] = None,
52
- parent_pid: Optional[int] = None,
53
- health_server_address: Optional[str] = None,
48
+ plugin_config: dict[str, Any] | None = None,
49
+ flwr_dir: str | None = None,
50
+ parent_pid: int | None = None,
51
+ health_server_address: str | None = None,
54
52
  ) -> None:
55
53
  """Run Flower SuperExec.
56
54
 
@@ -158,12 +156,10 @@ def run_with_deprecation_warning( # pylint: disable=R0913, R0917
158
156
  cmd: str,
159
157
  plugin_type: str,
160
158
  plugin_class: type[ExecPlugin],
161
- stub_class: Union[
162
- type[ClientAppIoStub], type[ServerAppIoStub], type[SimulationIoStub]
163
- ],
159
+ stub_class: type[ClientAppIoStub] | type[ServerAppIoStub] | type[SimulationIoStub],
164
160
  appio_api_address: str,
165
- flwr_dir: Optional[str],
166
- parent_pid: Optional[int],
161
+ flwr_dir: str | None,
162
+ parent_pid: int | None,
167
163
  warn_run_once: bool,
168
164
  ) -> None:
169
165
  """Log a deprecation warning and run the equivalent `flower-superexec` command.
@@ -16,14 +16,13 @@
16
16
 
17
17
 
18
18
  from abc import ABC, abstractmethod
19
- from typing import Optional
20
19
 
21
20
 
22
21
  class ArtifactProvider(ABC):
23
22
  """ArtifactProvider interface for providing artifact download links."""
24
23
 
25
24
  @abstractmethod
26
- def get_url(self, run_id: int) -> Optional[str]:
25
+ def get_url(self, run_id: int) -> str | None:
27
26
  """Return the artifact download link for the given run ID."""
28
27
 
29
28
  @property
@@ -18,7 +18,6 @@
18
18
  from abc import ABC, abstractmethod
19
19
  from collections.abc import Sequence
20
20
  from pathlib import Path
21
- from typing import Optional, Union
22
21
 
23
22
  from flwr.common.typing import (
24
23
  AccountAuthCredentials,
@@ -48,25 +47,23 @@ class ControlAuthnPlugin(ABC):
48
47
  """Abstract constructor."""
49
48
 
50
49
  @abstractmethod
51
- def get_login_details(self) -> Optional[AccountAuthLoginDetails]:
50
+ def get_login_details(self) -> AccountAuthLoginDetails | None:
52
51
  """Get the login details."""
53
52
 
54
53
  @abstractmethod
55
54
  def validate_tokens_in_metadata(
56
- self, metadata: Sequence[tuple[str, Union[str, bytes]]]
57
- ) -> tuple[bool, Optional[AccountInfo]]:
55
+ self, metadata: Sequence[tuple[str, str | bytes]]
56
+ ) -> tuple[bool, AccountInfo | None]:
58
57
  """Validate authentication tokens in the provided metadata."""
59
58
 
60
59
  @abstractmethod
61
- def get_auth_tokens(self, device_code: str) -> Optional[AccountAuthCredentials]:
60
+ def get_auth_tokens(self, device_code: str) -> AccountAuthCredentials | None:
62
61
  """Get authentication tokens."""
63
62
 
64
63
  @abstractmethod
65
64
  def refresh_tokens(
66
- self, metadata: Sequence[tuple[str, Union[str, bytes]]]
67
- ) -> tuple[
68
- Optional[Sequence[tuple[str, Union[str, bytes]]]], Optional[AccountInfo]
69
- ]:
65
+ self, metadata: Sequence[tuple[str, str | bytes]]
66
+ ) -> tuple[Sequence[tuple[str, str | bytes]] | None, AccountInfo | None]:
70
67
  """Refresh authentication tokens in the provided metadata."""
71
68
 
72
69
 
@@ -18,7 +18,6 @@ authorization plugins."""
18
18
 
19
19
  from collections.abc import Sequence
20
20
  from pathlib import Path
21
- from typing import Optional, Union
22
21
 
23
22
  from flwr.common.constant import NOOP_ACCOUNT_NAME, NOOP_FLWR_AID, AuthnType
24
23
  from flwr.common.typing import (
@@ -45,7 +44,7 @@ class NoOpControlAuthnPlugin(ControlAuthnPlugin):
45
44
  ):
46
45
  pass
47
46
 
48
- def get_login_details(self) -> Optional[AccountAuthLoginDetails]:
47
+ def get_login_details(self) -> AccountAuthLoginDetails | None:
49
48
  """Get the login details."""
50
49
  # This allows the `flwr login` command to load the NoOp plugin accordingly,
51
50
  # which then raises a LoginError when attempting to login.
@@ -58,20 +57,18 @@ class NoOpControlAuthnPlugin(ControlAuthnPlugin):
58
57
  )
59
58
 
60
59
  def validate_tokens_in_metadata(
61
- self, metadata: Sequence[tuple[str, Union[str, bytes]]]
62
- ) -> tuple[bool, Optional[AccountInfo]]:
60
+ self, metadata: Sequence[tuple[str, str | bytes]]
61
+ ) -> tuple[bool, AccountInfo | None]:
63
62
  """Return valid for no-op plugin."""
64
63
  return True, NOOP_ACCOUNT_INFO
65
64
 
66
- def get_auth_tokens(self, device_code: str) -> Optional[AccountAuthCredentials]:
65
+ def get_auth_tokens(self, device_code: str) -> AccountAuthCredentials | None:
67
66
  """Get authentication tokens."""
68
67
  raise RuntimeError("NoOp plugin does not support getting auth tokens.")
69
68
 
70
69
  def refresh_tokens(
71
- self, metadata: Sequence[tuple[str, Union[str, bytes]]]
72
- ) -> tuple[
73
- Optional[Sequence[tuple[str, Union[str, bytes]]]], Optional[AccountInfo]
74
- ]:
70
+ self, metadata: Sequence[tuple[str, str | bytes]]
71
+ ) -> tuple[Sequence[tuple[str, str | bytes]] | None, AccountInfo | None]:
75
72
  """Refresh authentication tokens in the provided metadata."""
76
73
  return metadata, NOOP_ACCOUNT_INFO
77
74
 
@@ -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
+ """Federation Managers."""
16
+
17
+
18
+ from .federation_manager import FederationManager
19
+ from .noop_federation_manager import NoOpFederationManager
20
+
21
+ __all__ = [
22
+ "FederationManager",
23
+ "NoOpFederationManager",
24
+ ]
@@ -0,0 +1,64 @@
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
+ """Abstract base class FederationManager."""
16
+
17
+
18
+ from abc import ABC, abstractmethod
19
+ from typing import TYPE_CHECKING
20
+
21
+ from flwr.common.typing import Federation
22
+
23
+ if TYPE_CHECKING:
24
+ from flwr.server.superlink.linkstate.linkstate import LinkState
25
+
26
+
27
+ class FederationManager(ABC):
28
+ """Abstract base class for FederationManager."""
29
+
30
+ @property
31
+ def linkstate(self) -> "LinkState":
32
+ """Return the LinkState instance."""
33
+ if not (ret := getattr(self, "_linkstate", None)):
34
+ raise RuntimeError("linkstate not set. Assign to linkstate property first.")
35
+ return ret # type: ignore
36
+
37
+ @linkstate.setter
38
+ def linkstate(self, linkstate: "LinkState") -> None:
39
+ """Set the LinkState instance."""
40
+ self._linkstate = linkstate
41
+
42
+ @abstractmethod
43
+ def exists(self, federation: str) -> bool:
44
+ """Check if a federation exists."""
45
+
46
+ @abstractmethod
47
+ def has_member(self, flwr_aid: str, federation: str) -> bool:
48
+ """Check if the given account is a member of the federation."""
49
+
50
+ @abstractmethod
51
+ def filter_nodes(self, node_ids: set[int], federation: str) -> set[int]:
52
+ """Given a list of node IDs, return sublist with nodes in federation."""
53
+
54
+ @abstractmethod
55
+ def has_node(self, node_id: int, federation: str) -> bool:
56
+ """Given a node ID, check if it is in the federation."""
57
+
58
+ @abstractmethod
59
+ def get_federations(self, flwr_aid: str) -> list[str]:
60
+ """Get federations of which the account is a member."""
61
+
62
+ @abstractmethod
63
+ def get_details(self, federation: str) -> Federation:
64
+ """Get details of the federation."""