flwr 1.22.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 (301) 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 +34 -1
  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/__init__.py +15 -6
  9. flwr/cli/auth_plugin/auth_plugin.py +94 -0
  10. flwr/cli/auth_plugin/noop_auth_plugin.py +101 -0
  11. flwr/cli/auth_plugin/oidc_cli_plugin.py +46 -32
  12. flwr/cli/build.py +166 -53
  13. flwr/cli/{cli_user_auth_interceptor.py → cli_account_auth_interceptor.py} +29 -11
  14. flwr/cli/config_utils.py +101 -13
  15. flwr/cli/federation/__init__.py +24 -0
  16. flwr/cli/federation/ls.py +140 -0
  17. flwr/cli/federation/show.py +317 -0
  18. flwr/cli/install.py +91 -13
  19. flwr/cli/log.py +54 -11
  20. flwr/cli/login/login.py +41 -27
  21. flwr/cli/ls.py +177 -133
  22. flwr/cli/new/new.py +175 -40
  23. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +1 -0
  24. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
  25. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
  26. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  27. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  28. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  29. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  30. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
  31. flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +1 -1
  32. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  33. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +2 -2
  34. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +1 -1
  35. flwr/cli/pull.py +12 -7
  36. flwr/cli/run/run.py +82 -31
  37. flwr/cli/run_utils.py +130 -0
  38. flwr/cli/stop.py +27 -9
  39. flwr/cli/supernode/__init__.py +25 -0
  40. flwr/cli/supernode/ls.py +268 -0
  41. flwr/cli/supernode/register.py +190 -0
  42. flwr/cli/supernode/unregister.py +140 -0
  43. flwr/cli/utils.py +464 -81
  44. flwr/client/__init__.py +2 -1
  45. flwr/client/dpfedavg_numpy_client.py +4 -1
  46. flwr/client/grpc_adapter_client/connection.py +12 -15
  47. flwr/client/grpc_rere_client/connection.py +68 -41
  48. flwr/client/grpc_rere_client/grpc_adapter.py +34 -14
  49. flwr/client/grpc_rere_client/{client_interceptor.py → node_auth_client_interceptor.py} +5 -7
  50. flwr/client/message_handler/message_handler.py +2 -2
  51. flwr/client/mod/secure_aggregation/secaggplus_mod.py +10 -8
  52. flwr/client/numpy_client.py +1 -1
  53. flwr/client/rest_client/connection.py +94 -51
  54. flwr/client/run_info_store.py +4 -5
  55. flwr/client/typing.py +1 -1
  56. flwr/clientapp/__init__.py +1 -2
  57. flwr/{client → clientapp}/client_app.py +9 -10
  58. flwr/clientapp/mod/centraldp_mods.py +16 -17
  59. flwr/clientapp/mod/localdp_mod.py +8 -9
  60. flwr/clientapp/typing.py +1 -1
  61. flwr/{client/clientapp → clientapp}/utils.py +4 -4
  62. flwr/common/address.py +1 -2
  63. flwr/common/args.py +3 -4
  64. flwr/common/config.py +13 -16
  65. flwr/common/constant.py +56 -13
  66. flwr/common/differential_privacy.py +3 -4
  67. flwr/common/event_log_plugin/event_log_plugin.py +3 -4
  68. flwr/common/exit/exit.py +15 -2
  69. flwr/common/exit/exit_code.py +39 -10
  70. flwr/common/exit/exit_handler.py +6 -2
  71. flwr/common/exit/signal_handler.py +5 -5
  72. flwr/common/grpc.py +6 -6
  73. flwr/common/inflatable_protobuf_utils.py +1 -1
  74. flwr/common/inflatable_utils.py +48 -31
  75. flwr/common/logger.py +19 -19
  76. flwr/common/message.py +4 -4
  77. flwr/common/object_ref.py +7 -7
  78. flwr/common/record/array.py +6 -6
  79. flwr/common/record/arrayrecord.py +18 -21
  80. flwr/common/record/configrecord.py +3 -3
  81. flwr/common/record/recorddict.py +5 -5
  82. flwr/common/record/typeddict.py +9 -2
  83. flwr/common/recorddict_compat.py +7 -10
  84. flwr/common/retry_invoker.py +20 -20
  85. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +1 -89
  86. flwr/common/secure_aggregation/ndarrays_arithmetic.py +3 -3
  87. flwr/common/serde.py +9 -6
  88. flwr/common/serde_utils.py +2 -2
  89. flwr/common/telemetry.py +9 -5
  90. flwr/common/typing.py +59 -43
  91. flwr/compat/client/app.py +39 -38
  92. flwr/compat/client/grpc_client/connection.py +13 -13
  93. flwr/compat/server/app.py +5 -6
  94. flwr/proto/appio_pb2.py +13 -3
  95. flwr/proto/appio_pb2.pyi +134 -65
  96. flwr/proto/appio_pb2_grpc.py +20 -0
  97. flwr/proto/appio_pb2_grpc.pyi +27 -0
  98. flwr/proto/clientappio_pb2.py +17 -7
  99. flwr/proto/clientappio_pb2.pyi +15 -0
  100. flwr/proto/clientappio_pb2_grpc.py +206 -40
  101. flwr/proto/clientappio_pb2_grpc.pyi +168 -53
  102. flwr/proto/control_pb2.py +72 -40
  103. flwr/proto/control_pb2.pyi +319 -87
  104. flwr/proto/control_pb2_grpc.py +339 -28
  105. flwr/proto/control_pb2_grpc.pyi +209 -37
  106. flwr/proto/error_pb2.py +13 -3
  107. flwr/proto/error_pb2.pyi +24 -6
  108. flwr/proto/error_pb2_grpc.py +20 -0
  109. flwr/proto/error_pb2_grpc.pyi +27 -0
  110. flwr/proto/fab_pb2.py +24 -10
  111. flwr/proto/fab_pb2.pyi +68 -20
  112. flwr/proto/fab_pb2_grpc.py +20 -0
  113. flwr/proto/fab_pb2_grpc.pyi +27 -0
  114. flwr/proto/federation_pb2.py +38 -0
  115. flwr/proto/federation_pb2.pyi +56 -0
  116. flwr/proto/federation_pb2_grpc.py +24 -0
  117. flwr/proto/federation_pb2_grpc.pyi +31 -0
  118. flwr/proto/fleet_pb2.py +45 -27
  119. flwr/proto/fleet_pb2.pyi +186 -70
  120. flwr/proto/fleet_pb2_grpc.py +277 -66
  121. flwr/proto/fleet_pb2_grpc.pyi +201 -55
  122. flwr/proto/grpcadapter_pb2.py +14 -4
  123. flwr/proto/grpcadapter_pb2.pyi +38 -16
  124. flwr/proto/grpcadapter_pb2_grpc.py +35 -4
  125. flwr/proto/grpcadapter_pb2_grpc.pyi +38 -7
  126. flwr/proto/heartbeat_pb2.py +17 -7
  127. flwr/proto/heartbeat_pb2.pyi +51 -22
  128. flwr/proto/heartbeat_pb2_grpc.py +20 -0
  129. flwr/proto/heartbeat_pb2_grpc.pyi +27 -0
  130. flwr/proto/log_pb2.py +13 -3
  131. flwr/proto/log_pb2.pyi +34 -11
  132. flwr/proto/log_pb2_grpc.py +20 -0
  133. flwr/proto/log_pb2_grpc.pyi +27 -0
  134. flwr/proto/message_pb2.py +15 -5
  135. flwr/proto/message_pb2.pyi +154 -86
  136. flwr/proto/message_pb2_grpc.py +20 -0
  137. flwr/proto/message_pb2_grpc.pyi +27 -0
  138. flwr/proto/node_pb2.py +16 -4
  139. flwr/proto/node_pb2.pyi +77 -4
  140. flwr/proto/node_pb2_grpc.py +20 -0
  141. flwr/proto/node_pb2_grpc.pyi +27 -0
  142. flwr/proto/recorddict_pb2.py +13 -3
  143. flwr/proto/recorddict_pb2.pyi +184 -107
  144. flwr/proto/recorddict_pb2_grpc.py +20 -0
  145. flwr/proto/recorddict_pb2_grpc.pyi +27 -0
  146. flwr/proto/run_pb2.py +40 -31
  147. flwr/proto/run_pb2.pyi +149 -84
  148. flwr/proto/run_pb2_grpc.py +20 -0
  149. flwr/proto/run_pb2_grpc.pyi +27 -0
  150. flwr/proto/serverappio_pb2.py +13 -3
  151. flwr/proto/serverappio_pb2.pyi +32 -8
  152. flwr/proto/serverappio_pb2_grpc.py +246 -65
  153. flwr/proto/serverappio_pb2_grpc.pyi +221 -85
  154. flwr/proto/simulationio_pb2.py +16 -8
  155. flwr/proto/simulationio_pb2.pyi +15 -0
  156. flwr/proto/simulationio_pb2_grpc.py +162 -41
  157. flwr/proto/simulationio_pb2_grpc.pyi +149 -55
  158. flwr/proto/transport_pb2.py +20 -10
  159. flwr/proto/transport_pb2.pyi +249 -160
  160. flwr/proto/transport_pb2_grpc.py +35 -4
  161. flwr/proto/transport_pb2_grpc.pyi +38 -8
  162. flwr/server/app.py +173 -127
  163. flwr/server/client_manager.py +4 -5
  164. flwr/server/client_proxy.py +10 -11
  165. flwr/server/compat/app.py +4 -5
  166. flwr/server/compat/app_utils.py +2 -1
  167. flwr/server/compat/grid_client_proxy.py +10 -12
  168. flwr/server/compat/legacy_context.py +3 -4
  169. flwr/server/fleet_event_log_interceptor.py +2 -1
  170. flwr/server/grid/grid.py +2 -3
  171. flwr/server/grid/grpc_grid.py +10 -8
  172. flwr/server/grid/inmemory_grid.py +4 -4
  173. flwr/server/run_serverapp.py +2 -3
  174. flwr/server/server.py +34 -39
  175. flwr/server/server_app.py +7 -8
  176. flwr/server/server_config.py +1 -2
  177. flwr/server/serverapp/app.py +34 -28
  178. flwr/server/serverapp_components.py +4 -5
  179. flwr/server/strategy/aggregate.py +9 -8
  180. flwr/server/strategy/bulyan.py +13 -11
  181. flwr/server/strategy/dp_adaptive_clipping.py +16 -20
  182. flwr/server/strategy/dp_fixed_clipping.py +12 -17
  183. flwr/server/strategy/dpfedavg_adaptive.py +3 -4
  184. flwr/server/strategy/dpfedavg_fixed.py +6 -10
  185. flwr/server/strategy/fault_tolerant_fedavg.py +14 -13
  186. flwr/server/strategy/fedadagrad.py +18 -14
  187. flwr/server/strategy/fedadam.py +16 -14
  188. flwr/server/strategy/fedavg.py +16 -17
  189. flwr/server/strategy/fedavg_android.py +15 -15
  190. flwr/server/strategy/fedavgm.py +21 -18
  191. flwr/server/strategy/fedmedian.py +2 -3
  192. flwr/server/strategy/fedopt.py +11 -10
  193. flwr/server/strategy/fedprox.py +10 -9
  194. flwr/server/strategy/fedtrimmedavg.py +12 -11
  195. flwr/server/strategy/fedxgb_bagging.py +13 -11
  196. flwr/server/strategy/fedxgb_cyclic.py +6 -6
  197. flwr/server/strategy/fedxgb_nn_avg.py +4 -4
  198. flwr/server/strategy/fedyogi.py +16 -14
  199. flwr/server/strategy/krum.py +12 -11
  200. flwr/server/strategy/qfedavg.py +16 -15
  201. flwr/server/strategy/strategy.py +6 -9
  202. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +19 -8
  203. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -2
  204. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +3 -4
  205. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +10 -12
  206. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +1 -3
  207. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +136 -42
  208. flwr/server/superlink/fleet/grpc_rere/{server_interceptor.py → node_auth_server_interceptor.py} +28 -51
  209. flwr/server/superlink/fleet/message_handler/message_handler.py +100 -49
  210. flwr/server/superlink/fleet/rest_rere/rest_api.py +54 -33
  211. flwr/server/superlink/fleet/vce/backend/backend.py +2 -2
  212. flwr/server/superlink/fleet/vce/backend/raybackend.py +6 -6
  213. flwr/server/superlink/fleet/vce/vce_api.py +32 -13
  214. flwr/server/superlink/linkstate/in_memory_linkstate.py +266 -207
  215. flwr/server/superlink/linkstate/linkstate.py +161 -62
  216. flwr/server/superlink/linkstate/linkstate_factory.py +24 -6
  217. flwr/server/superlink/linkstate/sqlite_linkstate.py +698 -638
  218. flwr/server/superlink/linkstate/utils.py +9 -60
  219. flwr/server/superlink/serverappio/serverappio_grpc.py +1 -2
  220. flwr/server/superlink/serverappio/serverappio_servicer.py +28 -23
  221. flwr/server/superlink/simulation/simulationio_grpc.py +1 -2
  222. flwr/server/superlink/simulation/simulationio_servicer.py +19 -14
  223. flwr/server/superlink/utils.py +4 -6
  224. flwr/server/typing.py +1 -1
  225. flwr/server/utils/tensorboard.py +15 -8
  226. flwr/server/utils/validator.py +2 -3
  227. flwr/server/workflow/default_workflows.py +5 -5
  228. flwr/server/workflow/secure_aggregation/secagg_workflow.py +2 -4
  229. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +12 -10
  230. flwr/serverapp/strategy/bulyan.py +16 -15
  231. flwr/serverapp/strategy/dp_adaptive_clipping.py +12 -11
  232. flwr/serverapp/strategy/dp_fixed_clipping.py +11 -14
  233. flwr/serverapp/strategy/fedadagrad.py +10 -11
  234. flwr/serverapp/strategy/fedadam.py +10 -11
  235. flwr/serverapp/strategy/fedavg.py +9 -10
  236. flwr/serverapp/strategy/fedavgm.py +17 -16
  237. flwr/serverapp/strategy/fedmedian.py +2 -2
  238. flwr/serverapp/strategy/fedopt.py +10 -11
  239. flwr/serverapp/strategy/fedprox.py +7 -8
  240. flwr/serverapp/strategy/fedtrimmedavg.py +9 -9
  241. flwr/serverapp/strategy/fedxgb_bagging.py +3 -3
  242. flwr/serverapp/strategy/fedxgb_cyclic.py +9 -9
  243. flwr/serverapp/strategy/fedyogi.py +9 -11
  244. flwr/serverapp/strategy/krum.py +7 -7
  245. flwr/serverapp/strategy/multikrum.py +9 -9
  246. flwr/serverapp/strategy/qfedavg.py +17 -16
  247. flwr/serverapp/strategy/strategy.py +6 -9
  248. flwr/serverapp/strategy/strategy_utils.py +7 -8
  249. flwr/simulation/app.py +46 -42
  250. flwr/simulation/legacy_app.py +12 -12
  251. flwr/simulation/ray_transport/ray_actor.py +11 -12
  252. flwr/simulation/ray_transport/ray_client_proxy.py +12 -13
  253. flwr/simulation/run_simulation.py +44 -43
  254. flwr/simulation/simulationio_connection.py +4 -4
  255. flwr/supercore/cli/flower_superexec.py +3 -4
  256. flwr/supercore/constant.py +52 -0
  257. flwr/supercore/corestate/corestate.py +24 -3
  258. flwr/supercore/corestate/in_memory_corestate.py +138 -0
  259. flwr/supercore/corestate/sqlite_corestate.py +157 -0
  260. flwr/supercore/ffs/disk_ffs.py +1 -2
  261. flwr/supercore/ffs/ffs.py +1 -2
  262. flwr/supercore/ffs/ffs_factory.py +1 -2
  263. flwr/{common → supercore}/heartbeat.py +20 -25
  264. flwr/supercore/object_store/in_memory_object_store.py +1 -6
  265. flwr/supercore/object_store/object_store.py +1 -2
  266. flwr/supercore/object_store/object_store_factory.py +27 -8
  267. flwr/supercore/object_store/sqlite_object_store.py +253 -0
  268. flwr/{client/clientapp → supercore/primitives}/__init__.py +1 -1
  269. flwr/supercore/primitives/asymmetric.py +117 -0
  270. flwr/supercore/primitives/asymmetric_ed25519.py +175 -0
  271. flwr/supercore/sqlite_mixin.py +159 -0
  272. flwr/supercore/superexec/plugin/base_exec_plugin.py +1 -2
  273. flwr/supercore/superexec/plugin/exec_plugin.py +3 -3
  274. flwr/supercore/superexec/run_superexec.py +9 -13
  275. flwr/supercore/utils.py +20 -0
  276. flwr/superlink/artifact_provider/artifact_provider.py +1 -2
  277. flwr/{common → superlink}/auth_plugin/__init__.py +6 -6
  278. flwr/superlink/auth_plugin/auth_plugin.py +88 -0
  279. flwr/superlink/auth_plugin/noop_auth_plugin.py +84 -0
  280. flwr/superlink/federation/__init__.py +24 -0
  281. flwr/superlink/federation/federation_manager.py +64 -0
  282. flwr/superlink/federation/noop_federation_manager.py +71 -0
  283. flwr/superlink/servicer/control/{control_user_auth_interceptor.py → control_account_auth_interceptor.py} +41 -32
  284. flwr/superlink/servicer/control/control_event_log_interceptor.py +7 -7
  285. flwr/superlink/servicer/control/control_grpc.py +18 -17
  286. flwr/superlink/servicer/control/control_license_interceptor.py +3 -3
  287. flwr/superlink/servicer/control/control_servicer.py +239 -63
  288. flwr/supernode/cli/flower_supernode.py +74 -26
  289. flwr/supernode/nodestate/in_memory_nodestate.py +60 -49
  290. flwr/supernode/nodestate/nodestate.py +7 -8
  291. flwr/supernode/nodestate/nodestate_factory.py +7 -4
  292. flwr/supernode/runtime/run_clientapp.py +43 -24
  293. flwr/supernode/servicer/clientappio/clientappio_servicer.py +40 -10
  294. flwr/supernode/start_client_internal.py +175 -51
  295. {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/METADATA +8 -8
  296. flwr-1.24.0.dist-info/RECORD +454 -0
  297. flwr/common/auth_plugin/auth_plugin.py +0 -149
  298. flwr/supercore/object_store/utils.py +0 -43
  299. flwr-1.22.0.dist-info/RECORD +0 -428
  300. {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/WHEEL +0 -0
  301. {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,157 @@
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
+ """SQLite-based CoreState implementation."""
16
+
17
+
18
+ import secrets
19
+ import sqlite3
20
+ from typing import cast
21
+
22
+ from flwr.common import now
23
+ from flwr.common.constant import (
24
+ FLWR_APP_TOKEN_LENGTH,
25
+ HEARTBEAT_DEFAULT_INTERVAL,
26
+ HEARTBEAT_PATIENCE,
27
+ )
28
+ from flwr.supercore.sqlite_mixin import SqliteMixin
29
+ from flwr.supercore.utils import int64_to_uint64, uint64_to_int64
30
+
31
+ from ..object_store import ObjectStore
32
+ from .corestate import CoreState
33
+
34
+ SQL_CREATE_TABLE_TOKEN_STORE = """
35
+ CREATE TABLE IF NOT EXISTS token_store (
36
+ run_id INTEGER PRIMARY KEY,
37
+ token TEXT UNIQUE NOT NULL,
38
+ active_until REAL
39
+ );
40
+ """
41
+
42
+
43
+ class SqliteCoreState(CoreState, SqliteMixin):
44
+ """SQLite-based CoreState implementation."""
45
+
46
+ def __init__(self, database_path: str, object_store: ObjectStore) -> None:
47
+ super().__init__(database_path)
48
+ self._object_store = object_store
49
+
50
+ @property
51
+ def object_store(self) -> ObjectStore:
52
+ """Return the ObjectStore instance used by this CoreState."""
53
+ return self._object_store
54
+
55
+ def get_sql_statements(self) -> tuple[str, ...]:
56
+ """Return SQL statements needed for CoreState tables."""
57
+ return (SQL_CREATE_TABLE_TOKEN_STORE,)
58
+
59
+ def create_token(self, run_id: int) -> str | None:
60
+ """Create a token for the given run ID."""
61
+ token = secrets.token_hex(FLWR_APP_TOKEN_LENGTH) # Generate a random token
62
+ current = now().timestamp()
63
+ active_until = current + HEARTBEAT_DEFAULT_INTERVAL
64
+ query = """
65
+ INSERT INTO token_store (run_id, token, active_until)
66
+ VALUES (:run_id, :token, :active_until);
67
+ """
68
+ data = {
69
+ "run_id": uint64_to_int64(run_id),
70
+ "token": token,
71
+ "active_until": active_until,
72
+ }
73
+ try:
74
+ self.query(query, data)
75
+ except sqlite3.IntegrityError:
76
+ return None # Token already created for this run ID
77
+ return token
78
+
79
+ def verify_token(self, run_id: int, token: str) -> bool:
80
+ """Verify a token for the given run ID."""
81
+ self._cleanup_expired_tokens()
82
+ query = "SELECT token FROM token_store WHERE run_id = :run_id;"
83
+ data = {"run_id": uint64_to_int64(run_id)}
84
+ rows = self.query(query, data)
85
+ if not rows:
86
+ return False
87
+ return cast(str, rows[0]["token"]) == token
88
+
89
+ def delete_token(self, run_id: int) -> None:
90
+ """Delete the token for the given run ID."""
91
+ query = "DELETE FROM token_store WHERE run_id = :run_id;"
92
+ data = {"run_id": uint64_to_int64(run_id)}
93
+ self.query(query, data)
94
+
95
+ def get_run_id_by_token(self, token: str) -> int | None:
96
+ """Get the run ID associated with a given token."""
97
+ self._cleanup_expired_tokens()
98
+ query = "SELECT run_id FROM token_store WHERE token = :token;"
99
+ data = {"token": token}
100
+ rows = self.query(query, data)
101
+ if not rows:
102
+ return None
103
+ return int64_to_uint64(rows[0]["run_id"])
104
+
105
+ def acknowledge_app_heartbeat(self, token: str) -> bool:
106
+ """Acknowledge an app heartbeat with the provided token."""
107
+ # Clean up expired tokens
108
+ self._cleanup_expired_tokens()
109
+
110
+ # Update the active_until field
111
+ current = now().timestamp()
112
+ active_until = current + HEARTBEAT_PATIENCE * HEARTBEAT_DEFAULT_INTERVAL
113
+ query = """
114
+ UPDATE token_store
115
+ SET active_until = :active_until
116
+ WHERE token = :token
117
+ RETURNING run_id;
118
+ """
119
+ data = {"active_until": active_until, "token": token}
120
+ rows = self.query(query, data)
121
+ return len(rows) > 0
122
+
123
+ def _cleanup_expired_tokens(self) -> None:
124
+ """Remove expired tokens and perform additional cleanup.
125
+
126
+ This method is called before token operations to ensure integrity.
127
+ Subclasses can override `_on_tokens_expired` to add custom cleanup logic.
128
+ """
129
+ current = now().timestamp()
130
+
131
+ with self.conn:
132
+ # Delete expired tokens and get their run_ids and active_until timestamps
133
+ query = """
134
+ DELETE FROM token_store
135
+ WHERE active_until < :current
136
+ RETURNING run_id, active_until;
137
+ """
138
+ rows = self.conn.execute(query, {"current": current}).fetchall()
139
+ expired_records = [
140
+ (int64_to_uint64(row["run_id"]), row["active_until"]) for row in rows
141
+ ]
142
+
143
+ # Hook for subclasses
144
+ if expired_records:
145
+ self._on_tokens_expired(expired_records)
146
+
147
+ def _on_tokens_expired(self, expired_records: list[tuple[int, float]]) -> None:
148
+ """Handle cleanup of expired tokens.
149
+
150
+ Override in subclasses to add custom cleanup logic.
151
+
152
+ Parameters
153
+ ----------
154
+ expired_records : list[tuple[int, float]]
155
+ List of tuples containing (run_id, active_until timestamp)
156
+ for expired tokens.
157
+ """
@@ -18,7 +18,6 @@
18
18
  import hashlib
19
19
  import json
20
20
  from pathlib import Path
21
- from typing import Optional
22
21
 
23
22
  from .ffs import Ffs
24
23
 
@@ -59,7 +58,7 @@ class DiskFfs(Ffs): # pylint: disable=R0904
59
58
 
60
59
  return content_hash
61
60
 
62
- def get(self, key: str) -> Optional[tuple[bytes, dict[str, str]]]:
61
+ def get(self, key: str) -> tuple[bytes, dict[str, str]] | None:
63
62
  """Return tuple containing the object content and metadata.
64
63
 
65
64
  Parameters
flwr/supercore/ffs/ffs.py CHANGED
@@ -16,7 +16,6 @@
16
16
 
17
17
 
18
18
  import abc
19
- from typing import Optional
20
19
 
21
20
 
22
21
  class Ffs(abc.ABC): # pylint: disable=R0904
@@ -40,7 +39,7 @@ class Ffs(abc.ABC): # pylint: disable=R0904
40
39
  """
41
40
 
42
41
  @abc.abstractmethod
43
- def get(self, key: str) -> Optional[tuple[bytes, dict[str, str]]]:
42
+ def get(self, key: str) -> tuple[bytes, dict[str, str]] | None:
44
43
  """Return tuple containing the object content and metadata.
45
44
 
46
45
  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
 
@@ -35,7 +34,7 @@ class FfsFactory:
35
34
 
36
35
  def __init__(self, base_dir: str) -> None:
37
36
  self.base_dir = base_dir
38
- self.ffs_instance: Optional[Ffs] = None
37
+ self.ffs_instance: Ffs | None = None
39
38
 
40
39
  def ffs(self) -> Ffs:
41
40
  """Return a Ffs instance and create it, if necessary."""
@@ -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,
@@ -48,9 +47,6 @@ class InMemoryObjectStore(ObjectStore):
48
47
  self.verify = verify
49
48
  self.store: dict[str, ObjectEntry] = {}
50
49
  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
50
  # Mapping each run ID to a set of object IDs that are used in that run
55
51
  self.run_objects_mapping: dict[int, set[str]] = {}
56
52
 
@@ -157,7 +153,7 @@ class InMemoryObjectStore(ObjectStore):
157
153
  self.store[object_id].content = object_content
158
154
  self.store[object_id].is_available = True
159
155
 
160
- def get(self, object_id: str) -> Optional[bytes]:
156
+ def get(self, object_id: str) -> bytes | None:
161
157
  """Get an object from the store."""
162
158
  with self.lock_store:
163
159
  # Check if the object ID is pre-registered
@@ -215,7 +211,6 @@ class InMemoryObjectStore(ObjectStore):
215
211
  """Clear the store."""
216
212
  with self.lock_store:
217
213
  self.store.clear()
218
- self.msg_descendant_objects_mapping.clear()
219
214
  self.run_objects_mapping.clear()
220
215
 
221
216
  def __contains__(self, object_id: str) -> bool:
@@ -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,19 +16,30 @@
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
21
+ from flwr.supercore.constant import FLWR_IN_MEMORY_DB_NAME
22
22
 
23
23
  from .in_memory_object_store import InMemoryObjectStore
24
24
  from .object_store import ObjectStore
25
+ from .sqlite_object_store import SqliteObjectStore
25
26
 
26
27
 
27
28
  class ObjectStoreFactory:
28
- """Factory class that creates ObjectStore instances."""
29
+ """Factory class that creates ObjectStore instances.
29
30
 
30
- def __init__(self) -> None:
31
- self.store_instance: Optional[ObjectStore] = None
31
+ Parameters
32
+ ----------
33
+ database : str (default: FLWR_IN_MEMORY_DB_NAME)
34
+ A string representing the path to the database file that will be opened.
35
+ Note that passing ":memory:" will open a connection to a database that is
36
+ in RAM, instead of on disk. And FLWR_IN_MEMORY_DB_NAME will create an
37
+ Python-based in-memory ObjectStore.
38
+ """
39
+
40
+ def __init__(self, database: str = FLWR_IN_MEMORY_DB_NAME) -> None:
41
+ self.database = database
42
+ self.store_instance: ObjectStore | None = None
32
43
 
33
44
  def store(self) -> ObjectStore:
34
45
  """Return an ObjectStore instance and create it, if necessary.
@@ -38,7 +49,15 @@ class ObjectStoreFactory:
38
49
  ObjectStore
39
50
  An ObjectStore instance for storing objects by object_id.
40
51
  """
41
- if self.store_instance is None:
42
- self.store_instance = InMemoryObjectStore()
43
- log(DEBUG, "Using InMemoryObjectStore")
44
- return self.store_instance
52
+ # InMemoryObjectStore
53
+ if self.database == FLWR_IN_MEMORY_DB_NAME:
54
+ if self.store_instance is None:
55
+ self.store_instance = InMemoryObjectStore()
56
+ log(DEBUG, "Using InMemoryObjectStore")
57
+ return self.store_instance
58
+
59
+ # SqliteObjectStore
60
+ store = SqliteObjectStore(self.database)
61
+ store.initialize()
62
+ log(DEBUG, "Using SqliteObjectStore")
63
+ return store
@@ -0,0 +1,253 @@
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 SQLite ObjectStore implementation."""
16
+
17
+
18
+ from typing import cast
19
+
20
+ from flwr.common.inflatable import (
21
+ get_object_id,
22
+ is_valid_sha256_hash,
23
+ iterate_object_tree,
24
+ )
25
+ from flwr.common.inflatable_utils import validate_object_content
26
+ from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
27
+ from flwr.supercore.sqlite_mixin import SqliteMixin
28
+ from flwr.supercore.utils import uint64_to_int64
29
+
30
+ from .object_store import NoObjectInStoreError, ObjectStore
31
+
32
+ SQL_CREATE_OBJECTS = """
33
+ CREATE TABLE IF NOT EXISTS objects (
34
+ object_id TEXT PRIMARY KEY,
35
+ content BLOB,
36
+ is_available INTEGER NOT NULL CHECK (is_available IN (0,1)),
37
+ ref_count INTEGER NOT NULL
38
+ );
39
+ """
40
+ SQL_CREATE_OBJECT_CHILDREN = """
41
+ CREATE TABLE IF NOT EXISTS object_children (
42
+ parent_id TEXT NOT NULL,
43
+ child_id TEXT NOT NULL,
44
+ FOREIGN KEY (parent_id) REFERENCES objects(object_id) ON DELETE CASCADE,
45
+ FOREIGN KEY (child_id) REFERENCES objects(object_id) ON DELETE CASCADE,
46
+ PRIMARY KEY (parent_id, child_id)
47
+ );
48
+ """
49
+ SQL_CREATE_RUN_OBJECTS = """
50
+ CREATE TABLE IF NOT EXISTS run_objects (
51
+ run_id INTEGER NOT NULL,
52
+ object_id TEXT NOT NULL,
53
+ FOREIGN KEY (object_id) REFERENCES objects(object_id) ON DELETE CASCADE,
54
+ PRIMARY KEY (run_id, object_id)
55
+ );
56
+ """
57
+
58
+
59
+ class SqliteObjectStore(ObjectStore, SqliteMixin):
60
+ """SQLite-based implementation of the ObjectStore interface."""
61
+
62
+ def __init__(self, database_path: str, verify: bool = True) -> None:
63
+ super().__init__(database_path)
64
+ self.verify = verify
65
+
66
+ def get_sql_statements(self) -> tuple[str, ...]:
67
+ """Return SQL statements for ObjectStore tables."""
68
+ return (
69
+ SQL_CREATE_OBJECTS,
70
+ SQL_CREATE_OBJECT_CHILDREN,
71
+ SQL_CREATE_RUN_OBJECTS,
72
+ )
73
+
74
+ def preregister(self, run_id: int, object_tree: ObjectTree) -> list[str]:
75
+ """Identify and preregister missing objects in the `ObjectStore`."""
76
+ new_objects = []
77
+ for tree_node in iterate_object_tree(object_tree):
78
+ obj_id = tree_node.object_id
79
+ if not is_valid_sha256_hash(obj_id):
80
+ raise ValueError(f"Invalid object ID format: {obj_id}")
81
+
82
+ child_ids = [child.object_id for child in tree_node.children]
83
+ with self.conn:
84
+ row = self.conn.execute(
85
+ "SELECT object_id, is_available FROM objects WHERE object_id=?",
86
+ (obj_id,),
87
+ ).fetchone()
88
+ if row is None:
89
+ # Insert new object
90
+ self.conn.execute(
91
+ "INSERT INTO objects"
92
+ "(object_id, content, is_available, ref_count) "
93
+ "VALUES (?, ?, ?, ?)",
94
+ (obj_id, b"", 0, 0),
95
+ )
96
+ for cid in child_ids:
97
+ self.conn.execute(
98
+ "INSERT INTO object_children(parent_id, child_id) "
99
+ "VALUES (?, ?)",
100
+ (obj_id, cid),
101
+ )
102
+ self.conn.execute(
103
+ "UPDATE objects SET ref_count = ref_count + 1 "
104
+ "WHERE object_id = ?",
105
+ (cid,),
106
+ )
107
+ new_objects.append(obj_id)
108
+ else:
109
+ # Add to the list of new objects if not available
110
+ if not row["is_available"]:
111
+ new_objects.append(obj_id)
112
+
113
+ # Ensure run mapping
114
+ self.conn.execute(
115
+ "INSERT OR IGNORE INTO run_objects(run_id, object_id) "
116
+ "VALUES (?, ?)",
117
+ (uint64_to_int64(run_id), obj_id),
118
+ )
119
+ return new_objects
120
+
121
+ def get_object_tree(self, object_id: str) -> ObjectTree:
122
+ """Get the object tree for a given object ID."""
123
+ with self.conn:
124
+ row = self.conn.execute(
125
+ "SELECT object_id FROM objects WHERE object_id=?", (object_id,)
126
+ ).fetchone()
127
+ if not row:
128
+ raise NoObjectInStoreError(
129
+ f"Object {object_id} was not pre-registered."
130
+ )
131
+ children = self.query(
132
+ "SELECT child_id FROM object_children WHERE parent_id=?", (object_id,)
133
+ )
134
+
135
+ # Build the object trees of all children
136
+ try:
137
+ child_trees = [self.get_object_tree(ch["child_id"]) for ch in children]
138
+ except NoObjectInStoreError as e:
139
+ # Raise an error if any child object is missing
140
+ # This indicates an integrity issue
141
+ raise NoObjectInStoreError(
142
+ f"Object tree for object ID '{object_id}' contains missing "
143
+ "children. This may indicate a corrupted object store."
144
+ ) from e
145
+
146
+ # Create and return the ObjectTree for the current object
147
+ return ObjectTree(object_id=object_id, children=child_trees)
148
+
149
+ def put(self, object_id: str, object_content: bytes) -> None:
150
+ """Put an object into the store."""
151
+ if self.verify:
152
+ # Verify object_id and object_content match
153
+ object_id_from_content = get_object_id(object_content)
154
+ if object_id != object_id_from_content:
155
+ raise ValueError(f"Object ID {object_id} does not match content hash")
156
+
157
+ # Validate object content
158
+ validate_object_content(content=object_content)
159
+
160
+ with self.conn:
161
+ # Only allow adding the object if it has been preregistered
162
+ row = self.conn.execute(
163
+ "SELECT is_available FROM objects WHERE object_id=?", (object_id,)
164
+ ).fetchone()
165
+ if row is None:
166
+ raise NoObjectInStoreError(
167
+ f"Object with ID '{object_id}' was not pre-registered."
168
+ )
169
+
170
+ # Return if object is already present in the store
171
+ if row["is_available"]:
172
+ return
173
+
174
+ # Update the object entry in the store
175
+ self.conn.execute(
176
+ "UPDATE objects SET content=?, is_available=1 WHERE object_id=?",
177
+ (object_content, object_id),
178
+ )
179
+
180
+ def get(self, object_id: str) -> bytes | None:
181
+ """Get an object from the store."""
182
+ rows = self.query("SELECT content FROM objects WHERE object_id=?", (object_id,))
183
+ return rows[0]["content"] if rows else None
184
+
185
+ def delete(self, object_id: str) -> None:
186
+ """Delete an object and its unreferenced descendants from the store."""
187
+ with self.conn:
188
+ row = self.conn.execute(
189
+ "SELECT ref_count FROM objects WHERE object_id=?", (object_id,)
190
+ ).fetchone()
191
+
192
+ # If the object is not in the store, nothing to delete
193
+ if row is None:
194
+ return
195
+
196
+ # Skip deletion if there are still references
197
+ if row["ref_count"] > 0:
198
+ return
199
+
200
+ # Deleting will cascade via FK, but we need to decrement children first
201
+ children = self.conn.execute(
202
+ "SELECT child_id FROM object_children WHERE parent_id=?", (object_id,)
203
+ ).fetchall()
204
+ child_ids = [child["child_id"] for child in children]
205
+
206
+ if child_ids:
207
+ placeholders = ", ".join("?" for _ in child_ids)
208
+ query = f"""
209
+ UPDATE objects SET ref_count = ref_count - 1
210
+ WHERE object_id IN ({placeholders})
211
+ """
212
+ self.conn.execute(query, child_ids)
213
+
214
+ self.conn.execute("DELETE FROM objects WHERE object_id=?", (object_id,))
215
+
216
+ # Recursively clean children
217
+ for child_id in child_ids:
218
+ self.delete(child_id)
219
+
220
+ def delete_objects_in_run(self, run_id: int) -> None:
221
+ """Delete all objects that were registered in a specific run."""
222
+ run_id_sint = uint64_to_int64(run_id)
223
+ with self.conn:
224
+ objs = self.conn.execute(
225
+ "SELECT object_id FROM run_objects WHERE run_id=?", (run_id_sint,)
226
+ ).fetchall()
227
+ for obj in objs:
228
+ object_id = obj["object_id"]
229
+ row = self.conn.execute(
230
+ "SELECT ref_count FROM objects WHERE object_id=?", (object_id,)
231
+ ).fetchone()
232
+ if row and row["ref_count"] == 0:
233
+ self.delete(object_id)
234
+ self.conn.execute("DELETE FROM run_objects WHERE run_id=?", (run_id_sint,))
235
+
236
+ def clear(self) -> None:
237
+ """Clear the store."""
238
+ with self.conn:
239
+ self.conn.execute("DELETE FROM object_children;")
240
+ self.conn.execute("DELETE FROM run_objects;")
241
+ self.conn.execute("DELETE FROM objects;")
242
+
243
+ def __contains__(self, object_id: str) -> bool:
244
+ """Check if an object_id is in the store."""
245
+ row = self.conn.execute(
246
+ "SELECT 1 FROM objects WHERE object_id=?", (object_id,)
247
+ ).fetchone()
248
+ return row is not None
249
+
250
+ def __len__(self) -> int:
251
+ """Return the number of objects in the store."""
252
+ row = self.conn.execute("SELECT COUNT(*) AS cnt FROM objects;").fetchone()
253
+ return cast(int, row["cnt"])
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  # ==============================================================================
15
- """Flower AppIO service."""
15
+ """Cryptographic primitives for the Flower infrastructure."""