flwr 1.23.0__py3-none-any.whl → 1.25.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 (339) 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/{new/templates → app_cmd}/__init__.py +9 -1
  6. flwr/cli/app_cmd/publish.py +285 -0
  7. flwr/cli/app_cmd/review.py +262 -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/{new/templates/app/code/flwr_tune → federation}/__init__.py +10 -1
  15. flwr/cli/federation/ls.py +140 -0
  16. flwr/cli/federation/show.py +318 -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 +211 -130
  21. flwr/cli/new/new.py +123 -331
  22. flwr/cli/pull.py +10 -5
  23. flwr/cli/run/run.py +71 -29
  24. flwr/cli/run_utils.py +148 -0
  25. flwr/cli/stop.py +26 -8
  26. flwr/cli/supernode/ls.py +25 -12
  27. flwr/cli/supernode/register.py +9 -4
  28. flwr/cli/supernode/unregister.py +5 -3
  29. flwr/cli/utils.py +239 -16
  30. flwr/client/__init__.py +1 -1
  31. flwr/client/dpfedavg_numpy_client.py +4 -1
  32. flwr/client/grpc_adapter_client/connection.py +8 -9
  33. flwr/client/grpc_rere_client/connection.py +16 -14
  34. flwr/client/grpc_rere_client/grpc_adapter.py +6 -2
  35. flwr/client/grpc_rere_client/node_auth_client_interceptor.py +2 -1
  36. flwr/client/message_handler/message_handler.py +2 -2
  37. flwr/client/mod/secure_aggregation/secaggplus_mod.py +3 -3
  38. flwr/client/numpy_client.py +1 -1
  39. flwr/client/rest_client/connection.py +18 -18
  40. flwr/client/run_info_store.py +4 -5
  41. flwr/client/typing.py +1 -1
  42. flwr/clientapp/client_app.py +9 -10
  43. flwr/clientapp/mod/centraldp_mods.py +16 -17
  44. flwr/clientapp/mod/localdp_mod.py +8 -9
  45. flwr/clientapp/typing.py +1 -1
  46. flwr/clientapp/utils.py +3 -3
  47. flwr/common/address.py +1 -2
  48. flwr/common/args.py +3 -4
  49. flwr/common/config.py +13 -16
  50. flwr/common/constant.py +5 -2
  51. flwr/common/differential_privacy.py +3 -4
  52. flwr/common/event_log_plugin/event_log_plugin.py +3 -4
  53. flwr/common/exit/exit.py +15 -2
  54. flwr/common/exit/exit_code.py +19 -0
  55. flwr/common/exit/exit_handler.py +6 -2
  56. flwr/common/exit/signal_handler.py +5 -5
  57. flwr/common/grpc.py +6 -6
  58. flwr/common/inflatable_protobuf_utils.py +1 -1
  59. flwr/common/inflatable_utils.py +38 -21
  60. flwr/common/logger.py +19 -19
  61. flwr/common/message.py +4 -4
  62. flwr/common/object_ref.py +7 -7
  63. flwr/common/record/array.py +3 -3
  64. flwr/common/record/arrayrecord.py +18 -30
  65. flwr/common/record/configrecord.py +3 -3
  66. flwr/common/record/recorddict.py +5 -5
  67. flwr/common/record/typeddict.py +9 -2
  68. flwr/common/recorddict_compat.py +7 -10
  69. flwr/common/retry_invoker.py +20 -20
  70. flwr/common/secure_aggregation/ndarrays_arithmetic.py +3 -3
  71. flwr/common/serde.py +11 -4
  72. flwr/common/serde_utils.py +2 -2
  73. flwr/common/telemetry.py +9 -5
  74. flwr/common/typing.py +58 -37
  75. flwr/compat/client/app.py +38 -37
  76. flwr/compat/client/grpc_client/connection.py +11 -11
  77. flwr/compat/server/app.py +5 -6
  78. flwr/proto/appio_pb2.py +13 -3
  79. flwr/proto/appio_pb2.pyi +134 -65
  80. flwr/proto/appio_pb2_grpc.py +20 -0
  81. flwr/proto/appio_pb2_grpc.pyi +27 -0
  82. flwr/proto/clientappio_pb2.py +17 -7
  83. flwr/proto/clientappio_pb2.pyi +15 -0
  84. flwr/proto/clientappio_pb2_grpc.py +206 -40
  85. flwr/proto/clientappio_pb2_grpc.pyi +168 -53
  86. flwr/proto/control_pb2.py +71 -52
  87. flwr/proto/control_pb2.pyi +277 -111
  88. flwr/proto/control_pb2_grpc.py +249 -40
  89. flwr/proto/control_pb2_grpc.pyi +185 -52
  90. flwr/proto/error_pb2.py +13 -3
  91. flwr/proto/error_pb2.pyi +24 -6
  92. flwr/proto/error_pb2_grpc.py +20 -0
  93. flwr/proto/error_pb2_grpc.pyi +27 -0
  94. flwr/proto/fab_pb2.py +14 -4
  95. flwr/proto/fab_pb2.pyi +59 -31
  96. flwr/proto/fab_pb2_grpc.py +20 -0
  97. flwr/proto/fab_pb2_grpc.pyi +27 -0
  98. flwr/proto/federation_pb2.py +38 -0
  99. flwr/proto/federation_pb2.pyi +56 -0
  100. flwr/proto/federation_pb2_grpc.py +24 -0
  101. flwr/proto/federation_pb2_grpc.pyi +31 -0
  102. flwr/proto/fleet_pb2.py +24 -14
  103. flwr/proto/fleet_pb2.pyi +141 -61
  104. flwr/proto/fleet_pb2_grpc.py +189 -48
  105. flwr/proto/fleet_pb2_grpc.pyi +175 -61
  106. flwr/proto/grpcadapter_pb2.py +14 -4
  107. flwr/proto/grpcadapter_pb2.pyi +38 -16
  108. flwr/proto/grpcadapter_pb2_grpc.py +35 -4
  109. flwr/proto/grpcadapter_pb2_grpc.pyi +38 -7
  110. flwr/proto/heartbeat_pb2.py +17 -7
  111. flwr/proto/heartbeat_pb2.pyi +51 -22
  112. flwr/proto/heartbeat_pb2_grpc.py +20 -0
  113. flwr/proto/heartbeat_pb2_grpc.pyi +27 -0
  114. flwr/proto/log_pb2.py +13 -3
  115. flwr/proto/log_pb2.pyi +34 -11
  116. flwr/proto/log_pb2_grpc.py +20 -0
  117. flwr/proto/log_pb2_grpc.pyi +27 -0
  118. flwr/proto/message_pb2.py +15 -5
  119. flwr/proto/message_pb2.pyi +154 -86
  120. flwr/proto/message_pb2_grpc.py +20 -0
  121. flwr/proto/message_pb2_grpc.pyi +27 -0
  122. flwr/proto/node_pb2.py +15 -5
  123. flwr/proto/node_pb2.pyi +50 -25
  124. flwr/proto/node_pb2_grpc.py +20 -0
  125. flwr/proto/node_pb2_grpc.pyi +27 -0
  126. flwr/proto/recorddict_pb2.py +13 -3
  127. flwr/proto/recorddict_pb2.pyi +184 -107
  128. flwr/proto/recorddict_pb2_grpc.py +20 -0
  129. flwr/proto/recorddict_pb2_grpc.pyi +27 -0
  130. flwr/proto/run_pb2.py +40 -31
  131. flwr/proto/run_pb2.pyi +158 -84
  132. flwr/proto/run_pb2_grpc.py +20 -0
  133. flwr/proto/run_pb2_grpc.pyi +27 -0
  134. flwr/proto/serverappio_pb2.py +13 -3
  135. flwr/proto/serverappio_pb2.pyi +32 -8
  136. flwr/proto/serverappio_pb2_grpc.py +246 -65
  137. flwr/proto/serverappio_pb2_grpc.pyi +221 -85
  138. flwr/proto/simulationio_pb2.py +16 -8
  139. flwr/proto/simulationio_pb2.pyi +15 -0
  140. flwr/proto/simulationio_pb2_grpc.py +162 -41
  141. flwr/proto/simulationio_pb2_grpc.pyi +149 -55
  142. flwr/proto/transport_pb2.py +20 -10
  143. flwr/proto/transport_pb2.pyi +249 -160
  144. flwr/proto/transport_pb2_grpc.py +35 -4
  145. flwr/proto/transport_pb2_grpc.pyi +38 -8
  146. flwr/server/app.py +39 -17
  147. flwr/server/client_manager.py +4 -5
  148. flwr/server/client_proxy.py +10 -11
  149. flwr/server/compat/app.py +4 -5
  150. flwr/server/compat/app_utils.py +2 -1
  151. flwr/server/compat/grid_client_proxy.py +10 -12
  152. flwr/server/compat/legacy_context.py +3 -4
  153. flwr/server/fleet_event_log_interceptor.py +2 -1
  154. flwr/server/grid/grid.py +2 -3
  155. flwr/server/grid/grpc_grid.py +10 -8
  156. flwr/server/grid/inmemory_grid.py +4 -4
  157. flwr/server/run_serverapp.py +2 -3
  158. flwr/server/server.py +34 -39
  159. flwr/server/server_app.py +7 -8
  160. flwr/server/server_config.py +1 -2
  161. flwr/server/serverapp/app.py +34 -28
  162. flwr/server/serverapp_components.py +4 -5
  163. flwr/server/strategy/aggregate.py +9 -8
  164. flwr/server/strategy/bulyan.py +13 -11
  165. flwr/server/strategy/dp_adaptive_clipping.py +16 -20
  166. flwr/server/strategy/dp_fixed_clipping.py +12 -17
  167. flwr/server/strategy/dpfedavg_adaptive.py +3 -4
  168. flwr/server/strategy/dpfedavg_fixed.py +6 -10
  169. flwr/server/strategy/fault_tolerant_fedavg.py +14 -13
  170. flwr/server/strategy/fedadagrad.py +18 -14
  171. flwr/server/strategy/fedadam.py +16 -14
  172. flwr/server/strategy/fedavg.py +16 -17
  173. flwr/server/strategy/fedavg_android.py +15 -15
  174. flwr/server/strategy/fedavgm.py +21 -18
  175. flwr/server/strategy/fedmedian.py +2 -3
  176. flwr/server/strategy/fedopt.py +11 -10
  177. flwr/server/strategy/fedprox.py +10 -9
  178. flwr/server/strategy/fedtrimmedavg.py +12 -11
  179. flwr/server/strategy/fedxgb_bagging.py +13 -11
  180. flwr/server/strategy/fedxgb_cyclic.py +6 -6
  181. flwr/server/strategy/fedxgb_nn_avg.py +4 -4
  182. flwr/server/strategy/fedyogi.py +16 -14
  183. flwr/server/strategy/krum.py +12 -11
  184. flwr/server/strategy/qfedavg.py +16 -15
  185. flwr/server/strategy/strategy.py +6 -9
  186. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +2 -1
  187. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -2
  188. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +3 -4
  189. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +10 -12
  190. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +1 -3
  191. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +4 -4
  192. flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +3 -2
  193. flwr/server/superlink/fleet/message_handler/message_handler.py +75 -30
  194. flwr/server/superlink/fleet/rest_rere/rest_api.py +2 -2
  195. flwr/server/superlink/fleet/vce/backend/backend.py +1 -1
  196. flwr/server/superlink/fleet/vce/backend/raybackend.py +5 -5
  197. flwr/server/superlink/fleet/vce/vce_api.py +15 -9
  198. flwr/server/superlink/linkstate/in_memory_linkstate.py +148 -149
  199. flwr/server/superlink/linkstate/linkstate.py +91 -43
  200. flwr/server/superlink/linkstate/linkstate_factory.py +22 -5
  201. flwr/server/superlink/linkstate/sqlite_linkstate.py +502 -436
  202. flwr/server/superlink/linkstate/utils.py +6 -6
  203. flwr/server/superlink/serverappio/serverappio_grpc.py +1 -2
  204. flwr/server/superlink/serverappio/serverappio_servicer.py +26 -21
  205. flwr/server/superlink/simulation/simulationio_grpc.py +1 -2
  206. flwr/server/superlink/simulation/simulationio_servicer.py +18 -13
  207. flwr/server/superlink/utils.py +4 -6
  208. flwr/server/typing.py +1 -1
  209. flwr/server/utils/tensorboard.py +15 -8
  210. flwr/server/workflow/default_workflows.py +5 -5
  211. flwr/server/workflow/secure_aggregation/secagg_workflow.py +2 -4
  212. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +8 -8
  213. flwr/serverapp/strategy/bulyan.py +16 -15
  214. flwr/serverapp/strategy/dp_adaptive_clipping.py +12 -11
  215. flwr/serverapp/strategy/dp_fixed_clipping.py +11 -14
  216. flwr/serverapp/strategy/fedadagrad.py +10 -11
  217. flwr/serverapp/strategy/fedadam.py +10 -11
  218. flwr/serverapp/strategy/fedavg.py +9 -10
  219. flwr/serverapp/strategy/fedavgm.py +17 -16
  220. flwr/serverapp/strategy/fedmedian.py +2 -2
  221. flwr/serverapp/strategy/fedopt.py +10 -11
  222. flwr/serverapp/strategy/fedprox.py +7 -8
  223. flwr/serverapp/strategy/fedtrimmedavg.py +9 -9
  224. flwr/serverapp/strategy/fedxgb_bagging.py +3 -3
  225. flwr/serverapp/strategy/fedxgb_cyclic.py +9 -9
  226. flwr/serverapp/strategy/fedyogi.py +9 -11
  227. flwr/serverapp/strategy/krum.py +7 -7
  228. flwr/serverapp/strategy/multikrum.py +9 -9
  229. flwr/serverapp/strategy/qfedavg.py +17 -16
  230. flwr/serverapp/strategy/strategy.py +6 -9
  231. flwr/serverapp/strategy/strategy_utils.py +7 -8
  232. flwr/simulation/app.py +46 -42
  233. flwr/simulation/legacy_app.py +12 -12
  234. flwr/simulation/ray_transport/ray_actor.py +10 -11
  235. flwr/simulation/ray_transport/ray_client_proxy.py +11 -12
  236. flwr/simulation/run_simulation.py +43 -43
  237. flwr/simulation/simulationio_connection.py +4 -4
  238. flwr/supercore/cli/flower_superexec.py +3 -4
  239. flwr/supercore/constant.py +34 -1
  240. flwr/supercore/corestate/corestate.py +24 -3
  241. flwr/supercore/corestate/in_memory_corestate.py +138 -0
  242. flwr/supercore/corestate/sqlite_corestate.py +157 -0
  243. flwr/supercore/ffs/disk_ffs.py +1 -2
  244. flwr/supercore/ffs/ffs.py +1 -2
  245. flwr/supercore/ffs/ffs_factory.py +1 -2
  246. flwr/{common → supercore}/heartbeat.py +20 -25
  247. flwr/supercore/object_store/in_memory_object_store.py +1 -2
  248. flwr/supercore/object_store/object_store.py +1 -2
  249. flwr/supercore/object_store/object_store_factory.py +1 -2
  250. flwr/supercore/object_store/sqlite_object_store.py +8 -7
  251. flwr/supercore/primitives/asymmetric.py +1 -1
  252. flwr/supercore/primitives/asymmetric_ed25519.py +11 -1
  253. flwr/supercore/sqlite_mixin.py +37 -34
  254. flwr/supercore/superexec/plugin/base_exec_plugin.py +1 -2
  255. flwr/supercore/superexec/plugin/exec_plugin.py +3 -3
  256. flwr/supercore/superexec/run_superexec.py +9 -13
  257. flwr/supercore/utils.py +190 -0
  258. flwr/superlink/artifact_provider/artifact_provider.py +1 -2
  259. flwr/superlink/auth_plugin/auth_plugin.py +6 -9
  260. flwr/superlink/auth_plugin/noop_auth_plugin.py +6 -9
  261. flwr/{cli/new/templates/app → superlink/federation}/__init__.py +10 -1
  262. flwr/superlink/federation/federation_manager.py +64 -0
  263. flwr/superlink/federation/noop_federation_manager.py +71 -0
  264. flwr/superlink/servicer/control/control_account_auth_interceptor.py +22 -13
  265. flwr/superlink/servicer/control/control_event_log_interceptor.py +7 -7
  266. flwr/superlink/servicer/control/control_grpc.py +7 -6
  267. flwr/superlink/servicer/control/control_license_interceptor.py +3 -3
  268. flwr/superlink/servicer/control/control_servicer.py +190 -23
  269. flwr/supernode/cli/flower_supernode.py +58 -3
  270. flwr/supernode/nodestate/in_memory_nodestate.py +121 -49
  271. flwr/supernode/nodestate/nodestate.py +52 -8
  272. flwr/supernode/nodestate/nodestate_factory.py +7 -4
  273. flwr/supernode/runtime/run_clientapp.py +41 -22
  274. flwr/supernode/servicer/clientappio/clientappio_servicer.py +46 -10
  275. flwr/supernode/start_client_internal.py +165 -46
  276. {flwr-1.23.0.dist-info → flwr-1.25.0.dist-info}/METADATA +9 -11
  277. flwr-1.25.0.dist-info/RECORD +393 -0
  278. flwr/cli/new/templates/app/.gitignore.tpl +0 -163
  279. flwr/cli/new/templates/app/LICENSE.tpl +0 -202
  280. flwr/cli/new/templates/app/README.baseline.md.tpl +0 -127
  281. flwr/cli/new/templates/app/README.flowertune.md.tpl +0 -68
  282. flwr/cli/new/templates/app/README.md.tpl +0 -37
  283. flwr/cli/new/templates/app/code/__init__.baseline.py.tpl +0 -1
  284. flwr/cli/new/templates/app/code/__init__.py +0 -15
  285. flwr/cli/new/templates/app/code/__init__.py.tpl +0 -1
  286. flwr/cli/new/templates/app/code/__init__.pytorch_legacy_api.py.tpl +0 -1
  287. flwr/cli/new/templates/app/code/client.baseline.py.tpl +0 -75
  288. flwr/cli/new/templates/app/code/client.huggingface.py.tpl +0 -93
  289. flwr/cli/new/templates/app/code/client.jax.py.tpl +0 -71
  290. flwr/cli/new/templates/app/code/client.mlx.py.tpl +0 -102
  291. flwr/cli/new/templates/app/code/client.numpy.py.tpl +0 -46
  292. flwr/cli/new/templates/app/code/client.pytorch.py.tpl +0 -80
  293. flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +0 -55
  294. flwr/cli/new/templates/app/code/client.sklearn.py.tpl +0 -108
  295. flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +0 -82
  296. flwr/cli/new/templates/app/code/client.xgboost.py.tpl +0 -110
  297. flwr/cli/new/templates/app/code/dataset.baseline.py.tpl +0 -36
  298. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +0 -92
  299. flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +0 -87
  300. flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +0 -56
  301. flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +0 -73
  302. flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +0 -78
  303. flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -66
  304. flwr/cli/new/templates/app/code/server.baseline.py.tpl +0 -43
  305. flwr/cli/new/templates/app/code/server.huggingface.py.tpl +0 -42
  306. flwr/cli/new/templates/app/code/server.jax.py.tpl +0 -39
  307. flwr/cli/new/templates/app/code/server.mlx.py.tpl +0 -41
  308. flwr/cli/new/templates/app/code/server.numpy.py.tpl +0 -38
  309. flwr/cli/new/templates/app/code/server.pytorch.py.tpl +0 -41
  310. flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +0 -31
  311. flwr/cli/new/templates/app/code/server.sklearn.py.tpl +0 -44
  312. flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +0 -38
  313. flwr/cli/new/templates/app/code/server.xgboost.py.tpl +0 -56
  314. flwr/cli/new/templates/app/code/strategy.baseline.py.tpl +0 -1
  315. flwr/cli/new/templates/app/code/task.huggingface.py.tpl +0 -98
  316. flwr/cli/new/templates/app/code/task.jax.py.tpl +0 -57
  317. flwr/cli/new/templates/app/code/task.mlx.py.tpl +0 -102
  318. flwr/cli/new/templates/app/code/task.numpy.py.tpl +0 -7
  319. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +0 -98
  320. flwr/cli/new/templates/app/code/task.pytorch_legacy_api.py.tpl +0 -111
  321. flwr/cli/new/templates/app/code/task.sklearn.py.tpl +0 -67
  322. flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +0 -52
  323. flwr/cli/new/templates/app/code/task.xgboost.py.tpl +0 -67
  324. flwr/cli/new/templates/app/code/utils.baseline.py.tpl +0 -1
  325. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +0 -146
  326. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +0 -80
  327. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +0 -65
  328. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +0 -52
  329. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +0 -56
  330. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +0 -49
  331. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +0 -53
  332. flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +0 -53
  333. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +0 -52
  334. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +0 -53
  335. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +0 -61
  336. flwr/supercore/object_store/utils.py +0 -43
  337. flwr-1.23.0.dist-info/RECORD +0 -439
  338. {flwr-1.23.0.dist-info → flwr-1.25.0.dist-info}/WHEEL +0 -0
  339. {flwr-1.23.0.dist-info → flwr-1.25.0.dist-info}/entry_points.txt +0 -0
@@ -26,7 +26,7 @@ import traceback
26
26
  from logging import DEBUG, ERROR, INFO, WARNING
27
27
  from pathlib import Path
28
28
  from queue import Empty, Queue
29
- from typing import Any, Optional
29
+ from typing import Any, cast
30
30
 
31
31
  from flwr.cli.config_utils import load_and_validate
32
32
  from flwr.cli.utils import get_sha256_hash
@@ -51,7 +51,9 @@ from flwr.server.superlink.linkstate.utils import generate_rand_int_from_bytes
51
51
  from flwr.simulation.ray_transport.utils import (
52
52
  enable_tf_gpu_growth as enable_gpu_growth,
53
53
  )
54
- from flwr.supercore.constant import FLWR_IN_MEMORY_DB_NAME
54
+ from flwr.supercore.constant import FLWR_IN_MEMORY_DB_NAME, NOOP_FEDERATION
55
+ from flwr.supercore.object_store import ObjectStoreFactory
56
+ from flwr.superlink.federation import NoOpFederationManager
55
57
 
56
58
 
57
59
  def _replace_keys(d: Any, match: str, target: str) -> Any:
@@ -99,12 +101,7 @@ def run_simulation_from_cli() -> None:
99
101
  _check_ray_support(args.backend)
100
102
 
101
103
  # Load JSON config
102
- backend_config_dict = json.loads(args.backend_config)
103
-
104
- if backend_config_dict:
105
- # Backend config internally operates with `_` not with `-`
106
- backend_config_dict = _replace_keys(backend_config_dict, match="-", target="_")
107
- log(DEBUG, "backend_config_dict: %s", backend_config_dict)
104
+ backend_config = json.loads(args.backend_config)
108
105
 
109
106
  run_id = (
110
107
  generate_rand_int_from_bytes(RUN_ID_NUM_BYTES)
@@ -142,6 +139,7 @@ def run_simulation_from_cli() -> None:
142
139
 
143
140
  # Create run
144
141
  run = Run.create_empty(run_id)
142
+ run.federation = NOOP_FEDERATION
145
143
  run.override_config = override_config
146
144
 
147
145
  # Create Context
@@ -158,7 +156,7 @@ def run_simulation_from_cli() -> None:
158
156
  client_app_attr=client_app_attr,
159
157
  num_supernodes=args.num_supernodes,
160
158
  backend_name=args.backend,
161
- backend_config=backend_config_dict,
159
+ backend_config=backend_config,
162
160
  app_dir=args.app,
163
161
  run=run,
164
162
  enable_tf_gpu_growth=args.enable_tf_gpu_growth,
@@ -176,7 +174,7 @@ def run_simulation(
176
174
  client_app: ClientApp,
177
175
  num_supernodes: int,
178
176
  backend_name: str = "ray",
179
- backend_config: Optional[BackendConfig] = None,
177
+ backend_config: BackendConfig | None = None,
180
178
  enable_tf_gpu_growth: bool = False,
181
179
  verbose_logging: bool = False,
182
180
  ) -> None:
@@ -249,8 +247,8 @@ def run_simulation(
249
247
 
250
248
  # pylint: disable=too-many-arguments,too-many-positional-arguments
251
249
  def run_serverapp_th(
252
- server_app_attr: Optional[str],
253
- server_app: Optional[ServerApp],
250
+ server_app_attr: str | None,
251
+ server_app: ServerApp | None,
254
252
  server_app_context: Context,
255
253
  grid: Grid,
256
254
  app_dir: str,
@@ -267,8 +265,8 @@ def run_serverapp_th(
267
265
  exception_event: threading.Event,
268
266
  _grid: Grid,
269
267
  _server_app_dir: str,
270
- _server_app_attr: Optional[str],
271
- _server_app: Optional[ServerApp],
268
+ _server_app_attr: str | None,
269
+ _server_app: ServerApp | None,
272
270
  _ctx_queue: "Queue[Context]",
273
271
  ) -> None:
274
272
  """Run SeverApp, after check if GPU memory growth has to be set.
@@ -328,16 +326,18 @@ def _main_loop(
328
326
  enable_tf_gpu_growth: bool,
329
327
  run: Run,
330
328
  exit_event: EventType,
331
- flwr_dir: Optional[str] = None,
332
- client_app: Optional[ClientApp] = None,
333
- client_app_attr: Optional[str] = None,
334
- server_app: Optional[ServerApp] = None,
335
- server_app_attr: Optional[str] = None,
336
- server_app_context: Optional[Context] = None,
329
+ flwr_dir: str | None = None,
330
+ client_app: ClientApp | None = None,
331
+ client_app_attr: str | None = None,
332
+ server_app: ServerApp | None = None,
333
+ server_app_attr: str | None = None,
334
+ server_app_context: Context | None = None,
337
335
  ) -> Context:
338
336
  """Start ServerApp on a separate thread, then launch Simulation Engine."""
339
337
  # Initialize StateFactory
340
- state_factory = LinkStateFactory(FLWR_IN_MEMORY_DB_NAME)
338
+ state_factory = LinkStateFactory(
339
+ FLWR_IN_MEMORY_DB_NAME, NoOpFederationManager(), ObjectStoreFactory()
340
+ )
341
341
 
342
342
  f_stop = threading.Event()
343
343
  # A Threading event to indicate if an exception was raised in the ServerApp thread
@@ -428,16 +428,16 @@ def _main_loop(
428
428
  def _run_simulation(
429
429
  num_supernodes: int,
430
430
  exit_event: EventType,
431
- client_app: Optional[ClientApp] = None,
432
- server_app: Optional[ServerApp] = None,
431
+ client_app: ClientApp | None = None,
432
+ server_app: ServerApp | None = None,
433
433
  backend_name: str = "ray",
434
- backend_config: Optional[BackendConfig] = None,
435
- client_app_attr: Optional[str] = None,
436
- server_app_attr: Optional[str] = None,
437
- server_app_context: Optional[Context] = None,
434
+ backend_config: BackendConfig | None = None,
435
+ client_app_attr: str | None = None,
436
+ server_app_attr: str | None = None,
437
+ server_app_context: Context | None = None,
438
438
  app_dir: str = "",
439
- flwr_dir: Optional[str] = None,
440
- run: Optional[Run] = None,
439
+ flwr_dir: str | None = None,
440
+ run: Run | None = None,
441
441
  enable_tf_gpu_growth: bool = False,
442
442
  verbose_logging: bool = False,
443
443
  is_app: bool = False,
@@ -445,29 +445,28 @@ def _run_simulation(
445
445
  """Launch the Simulation Engine."""
446
446
  if backend_config is None:
447
447
  backend_config = {}
448
+ elif backend_config:
449
+ # Backend config internally operates with `_` not with `-`
450
+ backend_config = cast(
451
+ BackendConfig, _replace_keys(backend_config, match="-", target="_")
452
+ )
453
+ log(DEBUG, "backend_config: %s", backend_config)
448
454
 
449
- if "init_args" not in backend_config:
450
- backend_config["init_args"] = {}
451
-
455
+ # Set default init_args if not passed
456
+ backend_config.setdefault("init_args", {})
452
457
  # Set default client_resources if not passed
453
- if "client_resources" not in backend_config:
454
- backend_config["client_resources"] = {"num_cpus": 2, "num_gpus": 0}
455
-
458
+ backend_config.setdefault("client_resources", {"num_cpus": 2, "num_gpus": 0})
456
459
  # Initialization of backend config to enable GPU growth globally when set
457
- if "actor" not in backend_config:
458
- backend_config["actor"] = {"tensorflow": 0}
460
+ backend_config.setdefault("actor", {"tensorflow": 0})
459
461
 
460
462
  # Set logging level
461
463
  logger = logging.getLogger("flwr")
462
464
  if verbose_logging:
463
465
  update_console_handler(level=DEBUG, timestamps=True, colored=True)
464
466
  else:
465
- backend_config["init_args"]["logging_level"] = backend_config["init_args"].get(
466
- "logging_level", WARNING
467
- )
468
- backend_config["init_args"]["log_to_driver"] = backend_config["init_args"].get(
469
- "log_to_driver", True
470
- )
467
+ init_args = backend_config["init_args"]
468
+ init_args.setdefault("logging_level", WARNING)
469
+ init_args.setdefault("log_to_driver", True)
471
470
 
472
471
  if enable_tf_gpu_growth:
473
472
  # Check that Backend config has also enabled using GPU growth
@@ -483,6 +482,7 @@ def _run_simulation(
483
482
  if run is None:
484
483
  run_id = generate_rand_int_from_bytes(RUN_ID_NUM_BYTES)
485
484
  run = Run.create_empty(run_id=run_id)
485
+ run.federation = NOOP_FEDERATION
486
486
 
487
487
  args = (
488
488
  num_supernodes,
@@ -16,7 +16,7 @@
16
16
 
17
17
 
18
18
  from logging import DEBUG, WARNING
19
- from typing import Optional, cast
19
+ from typing import cast
20
20
 
21
21
  import grpc
22
22
 
@@ -43,12 +43,12 @@ class SimulationIoConnection:
43
43
  def __init__( # pylint: disable=too-many-arguments
44
44
  self,
45
45
  simulationio_service_address: str = SIMULATIONIO_API_DEFAULT_CLIENT_ADDRESS,
46
- root_certificates: Optional[bytes] = None,
46
+ root_certificates: bytes | None = None,
47
47
  ) -> None:
48
48
  self._addr = simulationio_service_address
49
49
  self._cert = root_certificates
50
- self._grpc_stub: Optional[SimulationIoStub] = None
51
- self._channel: Optional[grpc.Channel] = None
50
+ self._grpc_stub: SimulationIoStub | None = None
51
+ self._channel: grpc.Channel | None = None
52
52
  self._retry_invoker = _make_simple_grpc_retry_invoker()
53
53
 
54
54
  @property
@@ -17,7 +17,7 @@
17
17
 
18
18
  import argparse
19
19
  from logging import INFO
20
- from typing import Any, Optional
20
+ from typing import Any
21
21
 
22
22
  import yaml
23
23
 
@@ -54,7 +54,7 @@ except ImportError:
54
54
 
55
55
  def get_ee_plugin_and_stub_class( # pylint: disable=unused-argument
56
56
  plugin_type: str,
57
- ) -> Optional[tuple[type[ExecPlugin], type[object]]]:
57
+ ) -> tuple[type[ExecPlugin], type[object]] | None:
58
58
  """Get the EE plugin class and stub class based on the plugin type."""
59
59
  return None
60
60
 
@@ -75,7 +75,6 @@ def flower_superexec() -> None:
75
75
  # Log the first message after parsing arguments in case of `--help`
76
76
  log(INFO, "Starting Flower SuperExec")
77
77
 
78
- # Trigger telemetry event
79
78
  event(EventType.RUN_SUPEREXEC_ENTER, {"plugin_type": args.plugin_type})
80
79
 
81
80
  # Load plugin config from YAML file if provided
@@ -83,7 +82,7 @@ def flower_superexec() -> None:
83
82
  if plugin_config_path := getattr(args, "plugin_config", None):
84
83
  try:
85
84
  with open(plugin_config_path, encoding="utf-8") as file:
86
- yaml_config: Optional[dict[str, Any]] = yaml.safe_load(file)
85
+ yaml_config: dict[str, Any] | None = yaml.safe_load(file)
87
86
  if yaml_config is None or EXEC_PLUGIN_SECTION not in yaml_config:
88
87
  raise ValueError(f"Missing '{EXEC_PLUGIN_SECTION}' section.")
89
88
  plugin_config = yaml_config[EXEC_PLUGIN_SECTION]
@@ -17,6 +17,8 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ from flwr.common.constant import FLWR_DIR
21
+
20
22
  # Top-level key in YAML config for exec plugin settings
21
23
  EXEC_PLUGIN_SECTION = "exec_plugin"
22
24
 
@@ -24,9 +26,40 @@ EXEC_PLUGIN_SECTION = "exec_plugin"
24
26
  FLWR_IN_MEMORY_DB_NAME = ":flwr-in-memory:"
25
27
 
26
28
  # Constants for Hub
27
- APP_ID_PATTERN = r"^@(?P<user>[^/]+)/(?P<app>[^/]+)$"
29
+ APP_ID_PATTERN = r"^@[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$"
30
+ APP_VERSION_PATTERN = r"^\d+\.\d+\.\d+$"
28
31
  PLATFORM_API_URL = "https://api.flower.ai/v1"
29
32
 
33
+ # Specification for app publishing
34
+ APP_PUBLISH_INCLUDE_PATTERNS = (
35
+ "**/*.py",
36
+ "**/*.toml",
37
+ "**/*.md",
38
+ )
39
+ APP_PUBLISH_EXCLUDE_PATTERNS = FAB_EXCLUDE_PATTERNS = (
40
+ f"{FLWR_DIR}/**", # Exclude the .flwr directory
41
+ "**/__pycache__/**",
42
+ )
43
+ MAX_TOTAL_BYTES = 10 * 1024 * 1024 # 10 MB
44
+ MAX_FILE_BYTES = 1 * 1024 * 1024 # 1 MB
45
+ MAX_FILE_COUNT = 1000
46
+ MAX_DIR_DEPTH = 10 # relative depth (number of parts in relpath)
47
+ UTF8 = "utf-8"
48
+ MIME_MAP = {
49
+ ".py": "text/x-python; charset=utf-8",
50
+ ".md": "text/markdown; charset=utf-8",
51
+ ".toml": "application/toml; charset=utf-8",
52
+ }
53
+
54
+ # Constants for federations
55
+ NOOP_FEDERATION = "default"
56
+
57
+ # Constants for exit handling
58
+ FORCE_EXIT_TIMEOUT_SECONDS = 5 # Used in `flwr_exit` function
59
+
60
+ # Constants for message processing timing
61
+ MESSAGE_TIME_ENTRY_MAX_AGE_SECONDS = 3600
62
+
30
63
 
31
64
  class NodeStatus:
32
65
  """Event log writer types."""
@@ -16,14 +16,20 @@
16
16
 
17
17
 
18
18
  from abc import ABC, abstractmethod
19
- from typing import Optional
19
+
20
+ from ..object_store import ObjectStore
20
21
 
21
22
 
22
23
  class CoreState(ABC):
23
24
  """Abstract base class for core state."""
24
25
 
26
+ @property
27
+ @abstractmethod
28
+ def object_store(self) -> ObjectStore:
29
+ """Return the ObjectStore instance used by this CoreState."""
30
+
25
31
  @abstractmethod
26
- def create_token(self, run_id: int) -> Optional[str]:
32
+ def create_token(self, run_id: int) -> str | None:
27
33
  """Create a token for the given run ID.
28
34
 
29
35
  Parameters
@@ -66,7 +72,7 @@ class CoreState(ABC):
66
72
  """
67
73
 
68
74
  @abstractmethod
69
- def get_run_id_by_token(self, token: str) -> Optional[int]:
75
+ def get_run_id_by_token(self, token: str) -> int | None:
70
76
  """Get the run ID associated with a given token.
71
77
 
72
78
  Parameters
@@ -79,3 +85,18 @@ class CoreState(ABC):
79
85
  Optional[int]
80
86
  The run ID if the token is valid, otherwise None.
81
87
  """
88
+
89
+ @abstractmethod
90
+ def acknowledge_app_heartbeat(self, token: str) -> bool:
91
+ """Acknowledge an app heartbeat with the provided token.
92
+
93
+ Parameters
94
+ ----------
95
+ token : str
96
+ The token associated with the app.
97
+
98
+ Returns
99
+ -------
100
+ bool
101
+ True if the heartbeat is acknowledged successfully, False otherwise.
102
+ """
@@ -0,0 +1,138 @@
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
+ """In-memory CoreState implementation."""
16
+
17
+
18
+ import secrets
19
+ from dataclasses import dataclass
20
+ from threading import Lock
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
+
29
+ from ..object_store import ObjectStore
30
+ from .corestate import CoreState
31
+
32
+
33
+ @dataclass
34
+ class TokenRecord:
35
+ """Record containing token and heartbeat information."""
36
+
37
+ token: str
38
+ active_until: float
39
+
40
+
41
+ class InMemoryCoreState(CoreState):
42
+ """In-memory CoreState implementation."""
43
+
44
+ def __init__(self, object_store: ObjectStore) -> None:
45
+ self._object_store = object_store
46
+ # Store run ID to token mapping and token to run ID mapping
47
+ self.token_store: dict[int, TokenRecord] = {}
48
+ self.token_to_run_id: dict[str, int] = {}
49
+ self.lock_token_store = Lock()
50
+
51
+ @property
52
+ def object_store(self) -> ObjectStore:
53
+ """Return the ObjectStore instance used by this CoreState."""
54
+ return self._object_store
55
+
56
+ def create_token(self, run_id: int) -> str | None:
57
+ """Create a token for the given run ID."""
58
+ token = secrets.token_hex(FLWR_APP_TOKEN_LENGTH) # Generate a random token
59
+ with self.lock_token_store:
60
+ if run_id in self.token_store:
61
+ return None # Token already created for this run ID
62
+
63
+ self.token_store[run_id] = TokenRecord(
64
+ token=token, active_until=now().timestamp() + HEARTBEAT_DEFAULT_INTERVAL
65
+ )
66
+ self.token_to_run_id[token] = run_id
67
+ return token
68
+
69
+ def verify_token(self, run_id: int, token: str) -> bool:
70
+ """Verify a token for the given run ID."""
71
+ self._cleanup_expired_tokens()
72
+ with self.lock_token_store:
73
+ record = self.token_store.get(run_id)
74
+ return record is not None and record.token == token
75
+
76
+ def delete_token(self, run_id: int) -> None:
77
+ """Delete the token for the given run ID."""
78
+ with self.lock_token_store:
79
+ record = self.token_store.pop(run_id, None)
80
+ if record is not None:
81
+ self.token_to_run_id.pop(record.token, None)
82
+
83
+ def get_run_id_by_token(self, token: str) -> int | None:
84
+ """Get the run ID associated with a given token."""
85
+ self._cleanup_expired_tokens()
86
+ with self.lock_token_store:
87
+ return self.token_to_run_id.get(token)
88
+
89
+ def acknowledge_app_heartbeat(self, token: str) -> bool:
90
+ """Acknowledge an app heartbeat with the provided token."""
91
+ # Clean up expired tokens
92
+ self._cleanup_expired_tokens()
93
+
94
+ with self.lock_token_store:
95
+ # Return False if token is not found
96
+ if token not in self.token_to_run_id:
97
+ return False
98
+
99
+ # Get the run_id and update heartbeat info
100
+ run_id = self.token_to_run_id[token]
101
+ record = self.token_store[run_id]
102
+ current = now().timestamp()
103
+ record.active_until = (
104
+ current + HEARTBEAT_PATIENCE * HEARTBEAT_DEFAULT_INTERVAL
105
+ )
106
+ return True
107
+
108
+ def _cleanup_expired_tokens(self) -> None:
109
+ """Remove expired tokens and perform additional cleanup.
110
+
111
+ This method is called before token operations to ensure integrity.
112
+ Subclasses can override `_on_tokens_expired` to add custom cleanup logic.
113
+ """
114
+ with self.lock_token_store:
115
+ current = now().timestamp()
116
+ expired_records: list[tuple[int, float]] = []
117
+ for run_id, record in list(self.token_store.items()):
118
+ if record.active_until < current:
119
+ expired_records.append((run_id, record.active_until))
120
+ # Remove from both stores
121
+ del self.token_store[run_id]
122
+ self.token_to_run_id.pop(record.token, None)
123
+
124
+ # Hook for subclasses
125
+ if expired_records:
126
+ self._on_tokens_expired(expired_records)
127
+
128
+ def _on_tokens_expired(self, expired_records: list[tuple[int, float]]) -> None:
129
+ """Handle cleanup of expired tokens.
130
+
131
+ Override in subclasses to add custom cleanup logic.
132
+
133
+ Parameters
134
+ ----------
135
+ expired_records : list[tuple[int, float]]
136
+ List of tuples containing (run_id, active_until timestamp)
137
+ for expired tokens.
138
+ """
@@ -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