flwr-nightly 1.23.0.dev20250930__py3-none-any.whl → 1.26.0.dev20260121__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 (375) hide show
  1. flwr/__init__.py +17 -6
  2. flwr/app/__init__.py +4 -1
  3. flwr/app/error.py +2 -2
  4. flwr/app/exception.py +3 -3
  5. flwr/app/message_type.py +29 -0
  6. flwr/app/metadata.py +5 -2
  7. flwr/app/user_config.py +19 -0
  8. flwr/cli/app.py +62 -9
  9. flwr/cli/{new/templates/app/code → app_cmd}/__init__.py +9 -1
  10. flwr/cli/app_cmd/publish.py +285 -0
  11. flwr/cli/app_cmd/review.py +262 -0
  12. flwr/cli/auth_plugin/__init__.py +13 -6
  13. flwr/cli/auth_plugin/auth_plugin.py +26 -15
  14. flwr/cli/auth_plugin/noop_auth_plugin.py +101 -0
  15. flwr/cli/auth_plugin/oidc_cli_plugin.py +52 -32
  16. flwr/cli/build.py +166 -53
  17. flwr/cli/{cli_user_auth_interceptor.py → cli_account_auth_interceptor.py} +27 -10
  18. flwr/cli/config/__init__.py +21 -0
  19. flwr/cli/config/ls.py +104 -0
  20. flwr/cli/config_migration.py +300 -0
  21. flwr/cli/config_utils.py +154 -13
  22. flwr/cli/constant.py +67 -0
  23. flwr/cli/{new/templates/app/code/flwr_tune → federation}/__init__.py +8 -1
  24. flwr/cli/federation/ls.py +361 -0
  25. flwr/cli/flower_config.py +447 -0
  26. flwr/cli/install.py +91 -13
  27. flwr/cli/log.py +65 -36
  28. flwr/cli/login/login.py +41 -27
  29. flwr/cli/ls.py +232 -158
  30. flwr/cli/new/new.py +188 -244
  31. flwr/cli/pull.py +25 -34
  32. flwr/cli/run/run.py +106 -74
  33. flwr/cli/run_utils.py +148 -0
  34. flwr/cli/stop.py +46 -37
  35. flwr/cli/supernode/__init__.py +25 -0
  36. flwr/cli/supernode/ls.py +273 -0
  37. flwr/cli/supernode/register.py +190 -0
  38. flwr/cli/supernode/unregister.py +140 -0
  39. flwr/cli/typing.py +211 -0
  40. flwr/cli/utils.py +428 -80
  41. flwr/client/__init__.py +2 -1
  42. flwr/client/dpfedavg_numpy_client.py +4 -1
  43. flwr/client/grpc_adapter_client/connection.py +14 -17
  44. flwr/client/grpc_rere_client/connection.py +73 -43
  45. flwr/client/grpc_rere_client/grpc_adapter.py +35 -15
  46. flwr/client/grpc_rere_client/{client_interceptor.py → node_auth_client_interceptor.py} +5 -7
  47. flwr/client/message_handler/message_handler.py +4 -3
  48. flwr/client/mod/centraldp_mods.py +1 -1
  49. flwr/client/mod/localdp_mod.py +1 -1
  50. flwr/client/mod/secure_aggregation/secaggplus_mod.py +11 -9
  51. flwr/client/numpy_client.py +1 -1
  52. flwr/client/rest_client/connection.py +99 -54
  53. flwr/client/run_info_store.py +6 -6
  54. flwr/client/typing.py +1 -1
  55. flwr/clientapp/__init__.py +1 -2
  56. flwr/{client → clientapp}/client_app.py +11 -11
  57. flwr/clientapp/mod/centraldp_mods.py +16 -17
  58. flwr/clientapp/mod/localdp_mod.py +8 -9
  59. flwr/clientapp/typing.py +1 -1
  60. flwr/{client/clientapp → clientapp}/utils.py +4 -4
  61. flwr/common/__init__.py +3 -2
  62. flwr/common/args.py +3 -4
  63. flwr/common/config.py +15 -17
  64. flwr/common/constant.py +56 -28
  65. flwr/common/context.py +2 -1
  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 +16 -3
  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 +8 -7
  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 +5 -5
  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 +8 -5
  87. flwr/common/serde.py +22 -11
  88. flwr/common/serde_utils.py +2 -2
  89. flwr/common/telemetry.py +10 -6
  90. flwr/common/typing.py +65 -44
  91. flwr/compat/client/app.py +45 -47
  92. flwr/compat/client/grpc_client/connection.py +15 -14
  93. flwr/compat/common/constant.py +29 -0
  94. flwr/compat/server/app.py +6 -7
  95. flwr/proto/appio_pb2.py +13 -3
  96. flwr/proto/appio_pb2.pyi +134 -65
  97. flwr/proto/appio_pb2_grpc.py +20 -0
  98. flwr/proto/appio_pb2_grpc.pyi +27 -0
  99. flwr/proto/clientappio_pb2.py +17 -7
  100. flwr/proto/clientappio_pb2.pyi +15 -0
  101. flwr/proto/clientappio_pb2_grpc.py +206 -40
  102. flwr/proto/clientappio_pb2_grpc.pyi +168 -53
  103. flwr/proto/control_pb2.py +72 -40
  104. flwr/proto/control_pb2.pyi +319 -87
  105. flwr/proto/control_pb2_grpc.py +339 -28
  106. flwr/proto/control_pb2_grpc.pyi +209 -37
  107. flwr/proto/error_pb2.py +13 -3
  108. flwr/proto/error_pb2.pyi +24 -6
  109. flwr/proto/error_pb2_grpc.py +20 -0
  110. flwr/proto/error_pb2_grpc.pyi +27 -0
  111. flwr/proto/fab_pb2.py +24 -10
  112. flwr/proto/fab_pb2.pyi +68 -20
  113. flwr/proto/fab_pb2_grpc.py +20 -0
  114. flwr/proto/fab_pb2_grpc.pyi +27 -0
  115. flwr/proto/federation_pb2.py +38 -0
  116. flwr/proto/federation_pb2.pyi +56 -0
  117. flwr/proto/federation_pb2_grpc.py +24 -0
  118. flwr/proto/federation_pb2_grpc.pyi +31 -0
  119. flwr/proto/fleet_pb2.py +45 -27
  120. flwr/proto/fleet_pb2.pyi +190 -70
  121. flwr/proto/fleet_pb2_grpc.py +277 -66
  122. flwr/proto/fleet_pb2_grpc.pyi +201 -55
  123. flwr/proto/grpcadapter_pb2.py +14 -4
  124. flwr/proto/grpcadapter_pb2.pyi +38 -16
  125. flwr/proto/grpcadapter_pb2_grpc.py +35 -4
  126. flwr/proto/grpcadapter_pb2_grpc.pyi +38 -7
  127. flwr/proto/heartbeat_pb2.py +17 -7
  128. flwr/proto/heartbeat_pb2.pyi +51 -22
  129. flwr/proto/heartbeat_pb2_grpc.py +20 -0
  130. flwr/proto/heartbeat_pb2_grpc.pyi +27 -0
  131. flwr/proto/log_pb2.py +13 -3
  132. flwr/proto/log_pb2.pyi +34 -11
  133. flwr/proto/log_pb2_grpc.py +20 -0
  134. flwr/proto/log_pb2_grpc.pyi +27 -0
  135. flwr/proto/message_pb2.py +15 -5
  136. flwr/proto/message_pb2.pyi +154 -86
  137. flwr/proto/message_pb2_grpc.py +20 -0
  138. flwr/proto/message_pb2_grpc.pyi +27 -0
  139. flwr/proto/node_pb2.py +16 -4
  140. flwr/proto/node_pb2.pyi +77 -4
  141. flwr/proto/node_pb2_grpc.py +20 -0
  142. flwr/proto/node_pb2_grpc.pyi +27 -0
  143. flwr/proto/recorddict_pb2.py +13 -3
  144. flwr/proto/recorddict_pb2.pyi +184 -107
  145. flwr/proto/recorddict_pb2_grpc.py +20 -0
  146. flwr/proto/recorddict_pb2_grpc.pyi +27 -0
  147. flwr/proto/run_pb2.py +40 -31
  148. flwr/proto/run_pb2.pyi +158 -84
  149. flwr/proto/run_pb2_grpc.py +20 -0
  150. flwr/proto/run_pb2_grpc.pyi +27 -0
  151. flwr/proto/serverappio_pb2.py +13 -3
  152. flwr/proto/serverappio_pb2.pyi +32 -8
  153. flwr/proto/serverappio_pb2_grpc.py +246 -65
  154. flwr/proto/serverappio_pb2_grpc.pyi +221 -85
  155. flwr/proto/simulationio_pb2.py +16 -8
  156. flwr/proto/simulationio_pb2.pyi +15 -0
  157. flwr/proto/simulationio_pb2_grpc.py +162 -41
  158. flwr/proto/simulationio_pb2_grpc.pyi +149 -55
  159. flwr/proto/transport_pb2.py +20 -10
  160. flwr/proto/transport_pb2.pyi +249 -160
  161. flwr/proto/transport_pb2_grpc.py +35 -4
  162. flwr/proto/transport_pb2_grpc.pyi +38 -8
  163. flwr/server/app.py +175 -128
  164. flwr/server/client_manager.py +4 -5
  165. flwr/server/client_proxy.py +10 -11
  166. flwr/server/compat/app.py +4 -5
  167. flwr/server/compat/app_utils.py +2 -1
  168. flwr/server/compat/grid_client_proxy.py +12 -13
  169. flwr/server/compat/legacy_context.py +3 -4
  170. flwr/server/fleet_event_log_interceptor.py +2 -1
  171. flwr/server/grid/grid.py +2 -3
  172. flwr/server/grid/grpc_grid.py +12 -10
  173. flwr/server/grid/inmemory_grid.py +4 -4
  174. flwr/server/run_serverapp.py +2 -3
  175. flwr/server/server.py +34 -39
  176. flwr/server/server_app.py +7 -8
  177. flwr/server/server_config.py +1 -2
  178. flwr/server/serverapp/app.py +34 -28
  179. flwr/server/serverapp_components.py +4 -5
  180. flwr/server/strategy/aggregate.py +9 -8
  181. flwr/server/strategy/bulyan.py +13 -11
  182. flwr/server/strategy/dp_adaptive_clipping.py +16 -20
  183. flwr/server/strategy/dp_fixed_clipping.py +12 -17
  184. flwr/server/strategy/dpfedavg_adaptive.py +3 -4
  185. flwr/server/strategy/dpfedavg_fixed.py +6 -10
  186. flwr/server/strategy/fault_tolerant_fedavg.py +14 -13
  187. flwr/server/strategy/fedadagrad.py +18 -14
  188. flwr/server/strategy/fedadam.py +16 -14
  189. flwr/server/strategy/fedavg.py +16 -17
  190. flwr/server/strategy/fedavg_android.py +15 -15
  191. flwr/server/strategy/fedavgm.py +21 -18
  192. flwr/server/strategy/fedmedian.py +2 -3
  193. flwr/server/strategy/fedopt.py +11 -10
  194. flwr/server/strategy/fedprox.py +10 -9
  195. flwr/server/strategy/fedtrimmedavg.py +12 -11
  196. flwr/server/strategy/fedxgb_bagging.py +13 -11
  197. flwr/server/strategy/fedxgb_cyclic.py +6 -6
  198. flwr/server/strategy/fedxgb_nn_avg.py +4 -4
  199. flwr/server/strategy/fedyogi.py +16 -14
  200. flwr/server/strategy/krum.py +12 -11
  201. flwr/server/strategy/qfedavg.py +16 -15
  202. flwr/server/strategy/strategy.py +6 -9
  203. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +20 -9
  204. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -2
  205. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +3 -4
  206. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +10 -12
  207. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +1 -3
  208. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +136 -42
  209. flwr/server/superlink/fleet/grpc_rere/{server_interceptor.py → node_auth_server_interceptor.py} +28 -50
  210. flwr/server/superlink/fleet/message_handler/message_handler.py +141 -51
  211. flwr/server/superlink/fleet/rest_rere/rest_api.py +54 -33
  212. flwr/server/superlink/fleet/vce/backend/backend.py +2 -2
  213. flwr/server/superlink/fleet/vce/backend/raybackend.py +6 -6
  214. flwr/server/superlink/fleet/vce/vce_api.py +32 -13
  215. flwr/server/superlink/linkstate/__init__.py +2 -0
  216. flwr/server/superlink/linkstate/in_memory_linkstate.py +293 -208
  217. flwr/server/superlink/linkstate/linkstate.py +176 -64
  218. flwr/server/superlink/linkstate/linkstate_factory.py +24 -6
  219. flwr/server/superlink/linkstate/sql_linkstate.py +221 -0
  220. flwr/server/superlink/linkstate/sqlite_linkstate.py +743 -648
  221. flwr/server/superlink/linkstate/utils.py +11 -62
  222. flwr/server/superlink/serverappio/serverappio_grpc.py +1 -2
  223. flwr/server/superlink/serverappio/serverappio_servicer.py +28 -23
  224. flwr/server/superlink/simulation/simulationio_grpc.py +1 -2
  225. flwr/server/superlink/simulation/simulationio_servicer.py +19 -14
  226. flwr/server/superlink/utils.py +4 -6
  227. flwr/server/typing.py +1 -1
  228. flwr/server/utils/tensorboard.py +15 -8
  229. flwr/server/utils/validator.py +2 -3
  230. flwr/server/workflow/default_workflows.py +7 -6
  231. flwr/server/workflow/secure_aggregation/secagg_workflow.py +2 -4
  232. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +13 -11
  233. flwr/serverapp/strategy/bulyan.py +16 -15
  234. flwr/serverapp/strategy/dp_adaptive_clipping.py +12 -11
  235. flwr/serverapp/strategy/dp_fixed_clipping.py +11 -14
  236. flwr/serverapp/strategy/fedadagrad.py +10 -11
  237. flwr/serverapp/strategy/fedadam.py +10 -11
  238. flwr/serverapp/strategy/fedavg.py +10 -11
  239. flwr/serverapp/strategy/fedavgm.py +17 -16
  240. flwr/serverapp/strategy/fedmedian.py +2 -2
  241. flwr/serverapp/strategy/fedopt.py +10 -11
  242. flwr/serverapp/strategy/fedprox.py +7 -8
  243. flwr/serverapp/strategy/fedtrimmedavg.py +9 -9
  244. flwr/serverapp/strategy/fedxgb_bagging.py +3 -3
  245. flwr/serverapp/strategy/fedxgb_cyclic.py +10 -10
  246. flwr/serverapp/strategy/fedyogi.py +9 -11
  247. flwr/serverapp/strategy/krum.py +7 -7
  248. flwr/serverapp/strategy/multikrum.py +9 -9
  249. flwr/serverapp/strategy/qfedavg.py +17 -16
  250. flwr/serverapp/strategy/strategy.py +6 -9
  251. flwr/serverapp/strategy/strategy_utils.py +7 -8
  252. flwr/simulation/app.py +46 -42
  253. flwr/simulation/legacy_app.py +12 -12
  254. flwr/simulation/ray_transport/ray_actor.py +11 -12
  255. flwr/simulation/ray_transport/ray_client_proxy.py +14 -19
  256. flwr/simulation/run_simulation.py +46 -44
  257. flwr/simulation/simulationio_connection.py +4 -4
  258. flwr/{common → supercore}/address.py +1 -37
  259. flwr/supercore/cli/flower_superexec.py +3 -4
  260. flwr/supercore/constant.py +69 -0
  261. flwr/supercore/corestate/corestate.py +24 -3
  262. flwr/supercore/corestate/in_memory_corestate.py +138 -0
  263. flwr/supercore/corestate/sql_corestate.py +153 -0
  264. flwr/supercore/corestate/sqlite_corestate.py +157 -0
  265. flwr/supercore/credential_store/__init__.py +33 -0
  266. flwr/supercore/credential_store/credential_store.py +34 -0
  267. flwr/supercore/credential_store/file_credential_store.py +76 -0
  268. flwr/{common → supercore}/date.py +0 -11
  269. flwr/supercore/ffs/disk_ffs.py +1 -2
  270. flwr/supercore/ffs/ffs.py +1 -2
  271. flwr/supercore/ffs/ffs_factory.py +1 -2
  272. flwr/{common → supercore}/heartbeat.py +20 -25
  273. flwr/supercore/object_store/in_memory_object_store.py +1 -6
  274. flwr/supercore/object_store/object_store.py +1 -2
  275. flwr/supercore/object_store/object_store_factory.py +27 -8
  276. flwr/supercore/object_store/sqlite_object_store.py +253 -0
  277. flwr/{cli/new/templates/app → supercore/primitives}/__init__.py +1 -1
  278. flwr/supercore/primitives/asymmetric.py +117 -0
  279. flwr/supercore/primitives/asymmetric_ed25519.py +175 -0
  280. flwr/supercore/sql_mixin.py +292 -0
  281. flwr/supercore/sqlite_mixin.py +156 -0
  282. flwr/{client/clientapp → supercore/state}/__init__.py +2 -2
  283. flwr/supercore/state/schema/README.md +125 -0
  284. flwr/{cli/new/templates → supercore/state/schema}/__init__.py +2 -2
  285. flwr/supercore/state/schema/corestate_tables.py +36 -0
  286. flwr/supercore/state/schema/linkstate_tables.py +152 -0
  287. flwr/supercore/state/schema/objectstore_tables.py +90 -0
  288. flwr/supercore/superexec/plugin/base_exec_plugin.py +1 -2
  289. flwr/supercore/superexec/plugin/exec_plugin.py +3 -3
  290. flwr/supercore/superexec/run_superexec.py +9 -13
  291. flwr/supercore/utils.py +224 -0
  292. flwr/superlink/artifact_provider/artifact_provider.py +1 -2
  293. flwr/superlink/auth_plugin/__init__.py +5 -2
  294. flwr/superlink/auth_plugin/auth_plugin.py +20 -19
  295. flwr/superlink/auth_plugin/noop_auth_plugin.py +84 -0
  296. flwr/superlink/federation/__init__.py +24 -0
  297. flwr/superlink/federation/federation_manager.py +64 -0
  298. flwr/superlink/federation/noop_federation_manager.py +71 -0
  299. flwr/superlink/servicer/control/{control_user_auth_interceptor.py → control_account_auth_interceptor.py} +41 -32
  300. flwr/superlink/servicer/control/control_event_log_interceptor.py +7 -7
  301. flwr/superlink/servicer/control/control_grpc.py +20 -17
  302. flwr/superlink/servicer/control/control_license_interceptor.py +3 -3
  303. flwr/superlink/servicer/control/control_servicer.py +328 -68
  304. flwr/supernode/cli/flower_supernode.py +74 -26
  305. flwr/supernode/nodestate/in_memory_nodestate.py +121 -49
  306. flwr/supernode/nodestate/nodestate.py +52 -8
  307. flwr/supernode/nodestate/nodestate_factory.py +7 -4
  308. flwr/supernode/runtime/run_clientapp.py +43 -24
  309. flwr/supernode/servicer/clientappio/clientappio_servicer.py +48 -10
  310. flwr/supernode/start_client_internal.py +185 -57
  311. {flwr_nightly-1.23.0.dev20250930.dist-info → flwr_nightly-1.26.0.dev20260121.dist-info}/METADATA +10 -11
  312. flwr_nightly-1.26.0.dev20260121.dist-info/RECORD +411 -0
  313. flwr/cli/new/templates/app/.gitignore.tpl +0 -163
  314. flwr/cli/new/templates/app/LICENSE.tpl +0 -202
  315. flwr/cli/new/templates/app/README.baseline.md.tpl +0 -127
  316. flwr/cli/new/templates/app/README.flowertune.md.tpl +0 -68
  317. flwr/cli/new/templates/app/README.md.tpl +0 -37
  318. flwr/cli/new/templates/app/code/__init__.baseline.py.tpl +0 -1
  319. flwr/cli/new/templates/app/code/__init__.py.tpl +0 -1
  320. flwr/cli/new/templates/app/code/__init__.pytorch_legacy_api.py.tpl +0 -1
  321. flwr/cli/new/templates/app/code/client.baseline.py.tpl +0 -75
  322. flwr/cli/new/templates/app/code/client.huggingface.py.tpl +0 -93
  323. flwr/cli/new/templates/app/code/client.jax.py.tpl +0 -71
  324. flwr/cli/new/templates/app/code/client.mlx.py.tpl +0 -102
  325. flwr/cli/new/templates/app/code/client.numpy.py.tpl +0 -46
  326. flwr/cli/new/templates/app/code/client.pytorch.py.tpl +0 -80
  327. flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +0 -55
  328. flwr/cli/new/templates/app/code/client.sklearn.py.tpl +0 -108
  329. flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +0 -82
  330. flwr/cli/new/templates/app/code/client.xgboost.py.tpl +0 -110
  331. flwr/cli/new/templates/app/code/dataset.baseline.py.tpl +0 -36
  332. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +0 -92
  333. flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +0 -87
  334. flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +0 -56
  335. flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +0 -73
  336. flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +0 -78
  337. flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -66
  338. flwr/cli/new/templates/app/code/server.baseline.py.tpl +0 -43
  339. flwr/cli/new/templates/app/code/server.huggingface.py.tpl +0 -42
  340. flwr/cli/new/templates/app/code/server.jax.py.tpl +0 -39
  341. flwr/cli/new/templates/app/code/server.mlx.py.tpl +0 -41
  342. flwr/cli/new/templates/app/code/server.numpy.py.tpl +0 -38
  343. flwr/cli/new/templates/app/code/server.pytorch.py.tpl +0 -41
  344. flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +0 -31
  345. flwr/cli/new/templates/app/code/server.sklearn.py.tpl +0 -44
  346. flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +0 -38
  347. flwr/cli/new/templates/app/code/server.xgboost.py.tpl +0 -56
  348. flwr/cli/new/templates/app/code/strategy.baseline.py.tpl +0 -1
  349. flwr/cli/new/templates/app/code/task.huggingface.py.tpl +0 -98
  350. flwr/cli/new/templates/app/code/task.jax.py.tpl +0 -57
  351. flwr/cli/new/templates/app/code/task.mlx.py.tpl +0 -102
  352. flwr/cli/new/templates/app/code/task.numpy.py.tpl +0 -7
  353. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +0 -98
  354. flwr/cli/new/templates/app/code/task.pytorch_legacy_api.py.tpl +0 -111
  355. flwr/cli/new/templates/app/code/task.sklearn.py.tpl +0 -67
  356. flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +0 -52
  357. flwr/cli/new/templates/app/code/task.xgboost.py.tpl +0 -67
  358. flwr/cli/new/templates/app/code/utils.baseline.py.tpl +0 -1
  359. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +0 -146
  360. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +0 -80
  361. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +0 -65
  362. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +0 -52
  363. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +0 -56
  364. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +0 -49
  365. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +0 -53
  366. flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +0 -53
  367. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +0 -52
  368. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +0 -53
  369. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +0 -61
  370. flwr/common/pyproject.py +0 -42
  371. flwr/supercore/object_store/utils.py +0 -43
  372. flwr_nightly-1.23.0.dev20250930.dist-info/RECORD +0 -429
  373. /flwr/{common → supercore}/version.py +0 -0
  374. {flwr_nightly-1.23.0.dev20250930.dist-info → flwr_nightly-1.26.0.dev20260121.dist-info}/WHEEL +0 -0
  375. {flwr_nightly-1.23.0.dev20250930.dist-info → flwr_nightly-1.26.0.dev20260121.dist-info}/entry_points.txt +0 -0
@@ -18,14 +18,12 @@
18
18
  import argparse
19
19
  from logging import DEBUG, INFO, WARN
20
20
  from pathlib import Path
21
- from typing import Optional
22
21
 
22
+ import yaml
23
23
  from cryptography.exceptions import UnsupportedAlgorithm
24
- from cryptography.hazmat.primitives.asymmetric import ec
25
- from cryptography.hazmat.primitives.serialization import (
26
- load_ssh_private_key,
27
- load_ssh_public_key,
28
- )
24
+ from cryptography.hazmat.primitives.asymmetric import ec, ed25519
25
+ from cryptography.hazmat.primitives.serialization import load_ssh_private_key
26
+ from cryptography.hazmat.primitives.serialization.ssh import load_ssh_public_key
29
27
 
30
28
  from flwr.common import EventType, event
31
29
  from flwr.common.args import try_obtain_root_certificates
@@ -61,9 +59,19 @@ def flower_supernode() -> None:
61
59
  "Ignoring `--flwr-dir`.",
62
60
  )
63
61
 
62
+ trusted_entities = _try_obtain_trusted_entities(args.trusted_entities)
63
+ if trusted_entities:
64
+ _validate_public_keys_ed25519(trusted_entities)
64
65
  root_certificates = try_obtain_root_certificates(args, args.superlink)
65
66
  authentication_keys = _try_setup_client_authentication(args)
66
67
 
68
+ # Warn if authentication keys are provided but transport is not grpc-rere
69
+ if authentication_keys is not None and args.transport != TRANSPORT_TYPE_GRPC_RERE:
70
+ log(
71
+ WARN,
72
+ "SuperNode Authentication is only supported with the grpc-rere transport.",
73
+ )
74
+
67
75
  log(DEBUG, "Isolation mode: %s", args.isolation)
68
76
 
69
77
  start_client_internal(
@@ -81,6 +89,7 @@ def flower_supernode() -> None:
81
89
  isolation=args.isolation,
82
90
  clientappio_api_address=args.clientappio_api_address,
83
91
  health_server_address=args.health_server_address,
92
+ trusted_entities=trusted_entities,
84
93
  )
85
94
 
86
95
 
@@ -120,6 +129,18 @@ def _parse_args_run_supernode() -> argparse.ArgumentParser:
120
129
  help="ClientAppIo API (gRPC) server address (IPv4, IPv6, or a domain name). "
121
130
  f"By default, it is set to {CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS}.",
122
131
  )
132
+ parser.add_argument(
133
+ "--trusted-entities",
134
+ type=Path,
135
+ default=None,
136
+ metavar="YAML_FILE",
137
+ help=(
138
+ "Path to a YAML file defining trusted entities. "
139
+ "The file must map public key IDs to public keys. "
140
+ "Example: { fpk_UUID1: 'ssh-ed25519 <key1> [comment1]', "
141
+ "fpk_UUID2: 'ssh-ed25519 <key2> [comment2]' }"
142
+ ),
143
+ )
123
144
  add_args_health(parser)
124
145
 
125
146
  return parser
@@ -188,12 +209,12 @@ def _parse_args_common(parser: argparse.ArgumentParser) -> None:
188
209
  parser.add_argument(
189
210
  "--auth-supernode-private-key",
190
211
  type=str,
191
- help="The SuperNode's private key (as a path str) to enable authentication.",
212
+ help="Path to the SuperNode's private key to enable authentication.",
192
213
  )
193
214
  parser.add_argument(
194
215
  "--auth-supernode-public-key",
195
216
  type=str,
196
- help="The SuperNode's public key (as a path str) to enable authentication.",
217
+ help="This argument is deprecated and will be removed in a future release.",
197
218
  )
198
219
  parser.add_argument(
199
220
  "--node-config",
@@ -206,13 +227,10 @@ def _parse_args_common(parser: argparse.ArgumentParser) -> None:
206
227
 
207
228
  def _try_setup_client_authentication(
208
229
  args: argparse.Namespace,
209
- ) -> Optional[tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]]:
210
- if not args.auth_supernode_private_key and not args.auth_supernode_public_key:
230
+ ) -> tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey] | None:
231
+ if not args.auth_supernode_private_key:
211
232
  return None
212
233
 
213
- if not args.auth_supernode_private_key or not args.auth_supernode_public_key:
214
- flwr_exit(ExitCode.SUPERNODE_NODE_AUTH_KEYS_REQUIRED)
215
-
216
234
  try:
217
235
  ssh_private_key = load_ssh_private_key(
218
236
  Path(args.auth_supernode_private_key).read_bytes(),
@@ -222,23 +240,53 @@ def _try_setup_client_authentication(
222
240
  raise ValueError()
223
241
  except (ValueError, UnsupportedAlgorithm):
224
242
  flwr_exit(
225
- ExitCode.SUPERNODE_NODE_AUTH_KEYS_INVALID,
243
+ ExitCode.SUPERNODE_NODE_AUTH_KEY_INVALID,
226
244
  "Unable to parse the private key file.",
227
245
  )
228
246
 
229
- try:
230
- ssh_public_key = load_ssh_public_key(
231
- Path(args.auth_supernode_public_key).read_bytes()
247
+ if args.auth_supernode_public_key:
248
+ log(
249
+ WARN,
250
+ "The `--auth-supernode-public-key` flag is deprecated and will be "
251
+ "removed in a future release. The public key is now derived from the "
252
+ "private key provided by `--auth-supernode-private-key`.",
232
253
  )
233
- if not isinstance(ssh_public_key, ec.EllipticCurvePublicKey):
234
- raise ValueError()
235
- except (ValueError, UnsupportedAlgorithm):
254
+ return ssh_private_key, ssh_private_key.public_key()
255
+
256
+
257
+ def _try_obtain_trusted_entities(
258
+ trusted_entities_path: Path | None,
259
+ ) -> dict[str, str] | None:
260
+ """Validate and return the trust entities."""
261
+ if not trusted_entities_path:
262
+ return None
263
+ if not trusted_entities_path.is_file():
236
264
  flwr_exit(
237
- ExitCode.SUPERNODE_NODE_AUTH_KEYS_INVALID,
238
- "Unable to parse the public key file.",
265
+ ExitCode.SUPERNODE_INVALID_TRUSTED_ENTITIES,
266
+ "Path argument `--trusted-entities` does not point to a file.",
239
267
  )
268
+ try:
269
+ with trusted_entities_path.open("r", encoding="utf-8") as f:
270
+ trusted_entities = yaml.safe_load(f)
271
+ if not isinstance(trusted_entities, dict):
272
+ raise ValueError("Invalid trusted entities format.")
273
+ except (yaml.YAMLError, ValueError) as e:
274
+ flwr_exit(
275
+ ExitCode.SUPERNODE_INVALID_TRUSTED_ENTITIES,
276
+ f"Failed to read YAML file '{trusted_entities_path}': {e}",
277
+ )
278
+ return trusted_entities
240
279
 
241
- return (
242
- ssh_private_key,
243
- ssh_public_key,
244
- )
280
+
281
+ def _validate_public_keys_ed25519(trusted_entities: dict[str, str]) -> None:
282
+ """Validate public keys for the trust entities are Ed25519."""
283
+ for public_key_id in trusted_entities.keys():
284
+ verifier_public_key = load_ssh_public_key(
285
+ trusted_entities[public_key_id].encode("utf-8")
286
+ )
287
+ if not isinstance(verifier_public_key, ed25519.Ed25519PublicKey):
288
+ flwr_exit(
289
+ ExitCode.SUPERNODE_INVALID_TRUSTED_ENTITIES,
290
+ "The provided public key associated with "
291
+ f"trusted entity {public_key_id} is not Ed25519.",
292
+ )
@@ -15,18 +15,28 @@
15
15
  """In-memory NodeState implementation."""
16
16
 
17
17
 
18
- import secrets
19
18
  from collections.abc import Sequence
20
19
  from dataclasses import dataclass
21
- from threading import Lock
22
- from typing import Optional
23
-
24
- from flwr.common import Context, Message
25
- from flwr.common.constant import FLWR_APP_TOKEN_LENGTH
20
+ from threading import Lock, RLock
21
+
22
+ from flwr.common import Context, Error, Message, now
23
+ from flwr.common.constant import ErrorCode
24
+ from flwr.common.inflatable import (
25
+ get_all_nested_objects,
26
+ get_object_tree,
27
+ no_object_id_recompute,
28
+ )
26
29
  from flwr.common.typing import Run
30
+ from flwr.supercore.constant import MESSAGE_TIME_ENTRY_MAX_AGE_SECONDS
31
+ from flwr.supercore.corestate.in_memory_corestate import InMemoryCoreState
32
+ from flwr.supercore.object_store import ObjectStore
27
33
 
28
34
  from .nodestate import NodeState
29
35
 
36
+ CLIENT_APP_CRASHED_ERROR = Error(
37
+ ErrorCode.CLIENT_APP_CRASHED, "ClientApp stopped responding."
38
+ )
39
+
30
40
 
31
41
  @dataclass
32
42
  class MessageEntry:
@@ -36,27 +46,37 @@ class MessageEntry:
36
46
  is_retrieved: bool = False
37
47
 
38
48
 
39
- class InMemoryNodeState(NodeState): # pylint: disable=too-many-instance-attributes
49
+ @dataclass
50
+ class TimeEntry:
51
+ """Data class to represent a time entry."""
52
+
53
+ starting_at: float
54
+ finished_at: float | None = None
55
+
56
+
57
+ class InMemoryNodeState(
58
+ NodeState, InMemoryCoreState
59
+ ): # pylint: disable=too-many-instance-attributes
40
60
  """In-memory NodeState implementation."""
41
61
 
42
- def __init__(self) -> None:
62
+ def __init__(self, object_store: ObjectStore) -> None:
63
+ super().__init__(object_store)
43
64
  # Store node_id
44
- self.node_id: Optional[int] = None
65
+ self.node_id: int | None = None
45
66
  # Store Object ID to MessageEntry mapping
46
67
  self.msg_store: dict[str, MessageEntry] = {}
47
- self.lock_msg_store = Lock()
68
+ self.lock_msg_store = RLock()
48
69
  # Store run ID to Run mapping
49
70
  self.run_store: dict[int, Run] = {}
50
71
  self.lock_run_store = Lock()
51
72
  # Store run ID to Context mapping
52
73
  self.ctx_store: dict[int, Context] = {}
53
74
  self.lock_ctx_store = Lock()
54
- # Store run ID to token mapping and token to run ID mapping
55
- self.token_store: dict[int, str] = {}
56
- self.token_to_run_id: dict[str, int] = {}
57
- self.lock_token_store = Lock()
75
+ # Store msg ID to TimeEntry mapping
76
+ self.time_store: dict[str, TimeEntry] = {}
77
+ self.lock_time_store = Lock()
58
78
 
59
- def set_node_id(self, node_id: Optional[int]) -> None:
79
+ def set_node_id(self, node_id: int | None) -> None:
60
80
  """Set the node ID."""
61
81
  self.node_id = node_id
62
82
 
@@ -66,8 +86,10 @@ class InMemoryNodeState(NodeState): # pylint: disable=too-many-instance-attribu
66
86
  raise ValueError("Node ID not set")
67
87
  return self.node_id
68
88
 
69
- def store_message(self, message: Message) -> Optional[str]:
89
+ def store_message(self, message: Message) -> str | None:
70
90
  """Store a message."""
91
+ # No need to check for expired tokens here
92
+ # The ClientAppIo servicer will first verify the token before storing messages
71
93
  with self.lock_msg_store:
72
94
  msg_id = message.metadata.message_id
73
95
  if msg_id == "" or msg_id in self.msg_store:
@@ -78,13 +100,14 @@ class InMemoryNodeState(NodeState): # pylint: disable=too-many-instance-attribu
78
100
  def get_messages(
79
101
  self,
80
102
  *,
81
- run_ids: Optional[Sequence[int]] = None,
82
- is_reply: Optional[bool] = None,
83
- limit: Optional[int] = None,
103
+ run_ids: Sequence[int] | None = None,
104
+ is_reply: bool | None = None,
105
+ limit: int | None = None,
84
106
  ) -> Sequence[Message]:
85
107
  """Retrieve messages based on the specified filters."""
86
- selected_messages: list[Message] = []
108
+ self._cleanup_expired_tokens()
87
109
 
110
+ selected_messages: list[Message] = []
88
111
  with self.lock_msg_store:
89
112
  # Iterate through all messages in the store
90
113
  for object_id in list(self.msg_store.keys()):
@@ -122,7 +145,7 @@ class InMemoryNodeState(NodeState): # pylint: disable=too-many-instance-attribu
122
145
  def delete_messages(
123
146
  self,
124
147
  *,
125
- message_ids: Optional[Sequence[str]] = None,
148
+ message_ids: Sequence[str] | None = None,
126
149
  ) -> None:
127
150
  """Delete messages based on the specified filters."""
128
151
  with self.lock_msg_store:
@@ -140,7 +163,7 @@ class InMemoryNodeState(NodeState): # pylint: disable=too-many-instance-attribu
140
163
  with self.lock_run_store:
141
164
  self.run_store[run.run_id] = run
142
165
 
143
- def get_run(self, run_id: int) -> Optional[Run]:
166
+ def get_run(self, run_id: int) -> Run | None:
144
167
  """Retrieve a run by its ID."""
145
168
  with self.lock_run_store:
146
169
  return self.run_store.get(run_id)
@@ -150,7 +173,7 @@ class InMemoryNodeState(NodeState): # pylint: disable=too-many-instance-attribu
150
173
  with self.lock_ctx_store:
151
174
  self.ctx_store[context.run_id] = context
152
175
 
153
- def get_context(self, run_id: int) -> Optional[Context]:
176
+ def get_context(self, run_id: int) -> Context | None:
154
177
  """Retrieve a context by its run ID."""
155
178
  with self.lock_ctx_store:
156
179
  return self.ctx_store.get(run_id)
@@ -171,29 +194,78 @@ class InMemoryNodeState(NodeState): # pylint: disable=too-many-instance-attribu
171
194
  ret -= set(self.token_store.keys())
172
195
  return list(ret)
173
196
 
174
- def create_token(self, run_id: int) -> Optional[str]:
175
- """Create a token for the given run ID."""
176
- token = secrets.token_hex(FLWR_APP_TOKEN_LENGTH) # Generate a random token
177
- with self.lock_token_store:
178
- if run_id in self.token_store:
179
- return None # Token already created for this run ID
180
- self.token_store[run_id] = token
181
- self.token_to_run_id[token] = run_id
182
- return token
183
-
184
- def verify_token(self, run_id: int, token: str) -> bool:
185
- """Verify a token for the given run ID."""
186
- with self.lock_token_store:
187
- return self.token_store.get(run_id) == token
188
-
189
- def delete_token(self, run_id: int) -> None:
190
- """Delete the token for the given run ID."""
191
- with self.lock_token_store:
192
- token = self.token_store.pop(run_id, None)
193
- if token is not None:
194
- self.token_to_run_id.pop(token, None)
195
-
196
- def get_run_id_by_token(self, token: str) -> Optional[int]:
197
- """Get the run ID associated with a given token."""
198
- with self.lock_token_store:
199
- return self.token_to_run_id.get(token)
197
+ def _on_tokens_expired(self, expired_records: list[tuple[int, float]]) -> None:
198
+ """Insert error replies for messages associated with expired tokens."""
199
+ with self.lock_msg_store:
200
+ # Find all retrieved messages associated with expired run IDs
201
+ expired_run_ids = {run_id for run_id, _ in expired_records}
202
+ messages_to_reply: list[Message] = []
203
+ for entry in self.msg_store.values():
204
+ msg = entry.message
205
+ if msg.metadata.run_id in expired_run_ids and entry.is_retrieved:
206
+ messages_to_reply.append(msg)
207
+
208
+ # Create and store error replies for each message
209
+ for msg in messages_to_reply:
210
+ error_reply = Message(CLIENT_APP_CRASHED_ERROR, reply_to=msg)
211
+
212
+ # Insert objects of the error reply into the object store
213
+ with no_object_id_recompute():
214
+ # pylint: disable-next=W0212
215
+ error_reply.metadata._message_id = error_reply.object_id # type: ignore
216
+ object_tree = get_object_tree(error_reply)
217
+ self.object_store.preregister(msg.metadata.run_id, object_tree)
218
+ for obj_id, obj in get_all_nested_objects(error_reply).items():
219
+ self.object_store.put(obj_id, obj.deflate())
220
+
221
+ # Store the error reply message
222
+ self.store_message(error_reply)
223
+
224
+ def record_message_processing_start(self, message_id: str) -> None:
225
+ """Record the start time of message processing based on the message ID."""
226
+ with self.lock_time_store:
227
+ self.time_store[message_id] = TimeEntry(starting_at=now().timestamp())
228
+
229
+ def record_message_processing_end(self, message_id: str) -> None:
230
+ """Record the end time of message processing based on the message ID."""
231
+ with self.lock_time_store:
232
+ if message_id not in self.time_store:
233
+ raise ValueError(
234
+ f"Cannot record end time: Message ID {message_id} not found."
235
+ )
236
+ entry = self.time_store[message_id]
237
+ entry.finished_at = now().timestamp()
238
+
239
+ def get_message_processing_duration(self, message_id: str) -> float:
240
+ """Get the message processing duration based on the message ID."""
241
+ # Cleanup old message processing times
242
+ self._cleanup_old_message_times()
243
+ with self.lock_time_store:
244
+ if message_id not in self.time_store:
245
+ raise ValueError(f"Message ID {message_id} not found.")
246
+
247
+ entry = self.time_store[message_id]
248
+ if entry.starting_at is None or entry.finished_at is None:
249
+ raise ValueError(
250
+ f"Start time or end time for message ID {message_id} is missing."
251
+ )
252
+
253
+ duration = entry.finished_at - entry.starting_at
254
+ return duration
255
+
256
+ def _cleanup_old_message_times(self) -> None:
257
+ """Remove time entries older than MESSAGE_TIME_ENTRY_MAX_AGE_SECONDS."""
258
+ with self.lock_time_store:
259
+ cutoff = now().timestamp() - MESSAGE_TIME_ENTRY_MAX_AGE_SECONDS
260
+ # Find message IDs for entries that have a finishing_at time
261
+ # before the cutoff, and those that don't exist in msg_store
262
+ to_delete = [
263
+ msg_id
264
+ for msg_id, entry in self.time_store.items()
265
+ if (entry.finished_at and entry.finished_at < cutoff)
266
+ or msg_id not in self.msg_store
267
+ ]
268
+
269
+ # Delete the identified entries
270
+ for msg_id in to_delete:
271
+ del self.time_store[msg_id]
@@ -17,7 +17,6 @@
17
17
 
18
18
  from abc import abstractmethod
19
19
  from collections.abc import Sequence
20
- from typing import Optional
21
20
 
22
21
  from flwr.common import Context, Message
23
22
  from flwr.common.typing import Run
@@ -36,7 +35,7 @@ class NodeState(CoreState):
36
35
  """Get the node ID."""
37
36
 
38
37
  @abstractmethod
39
- def store_message(self, message: Message) -> Optional[str]:
38
+ def store_message(self, message: Message) -> str | None:
40
39
  """Store a message.
41
40
 
42
41
  Parameters
@@ -54,9 +53,9 @@ class NodeState(CoreState):
54
53
  def get_messages(
55
54
  self,
56
55
  *,
57
- run_ids: Optional[Sequence[int]] = None,
58
- is_reply: Optional[bool] = None,
59
- limit: Optional[int] = None,
56
+ run_ids: Sequence[int] | None = None,
57
+ is_reply: bool | None = None,
58
+ limit: int | None = None,
60
59
  ) -> Sequence[Message]:
61
60
  """Retrieve messages based on the specified filters.
62
61
 
@@ -89,7 +88,7 @@ class NodeState(CoreState):
89
88
  def delete_messages(
90
89
  self,
91
90
  *,
92
- message_ids: Optional[Sequence[str]] = None,
91
+ message_ids: Sequence[str] | None = None,
93
92
  ) -> None:
94
93
  """Delete messages based on the specified filters.
95
94
 
@@ -118,7 +117,7 @@ class NodeState(CoreState):
118
117
  """
119
118
 
120
119
  @abstractmethod
121
- def get_run(self, run_id: int) -> Optional[Run]:
120
+ def get_run(self, run_id: int) -> Run | None:
122
121
  """Retrieve a run by its ID.
123
122
 
124
123
  Parameters
@@ -143,7 +142,7 @@ class NodeState(CoreState):
143
142
  """
144
143
 
145
144
  @abstractmethod
146
- def get_context(self, run_id: int) -> Optional[Context]:
145
+ def get_context(self, run_id: int) -> Context | None:
147
146
  """Retrieve a context by its run ID.
148
147
 
149
148
  Parameters
@@ -169,3 +168,48 @@ class NodeState(CoreState):
169
168
  Sequence[int]
170
169
  Sequence of run IDs with pending messages.
171
170
  """
171
+
172
+ @abstractmethod
173
+ def record_message_processing_start(self, message_id: str) -> None:
174
+ """Record the start time of message processing based on the message ID.
175
+
176
+ Parameters
177
+ ----------
178
+ message_id : str
179
+ The ID of the message associated with the start time.
180
+ """
181
+
182
+ @abstractmethod
183
+ def record_message_processing_end(self, message_id: str) -> None:
184
+ """Record the end time of message processing based on the message ID.
185
+
186
+ Parameters
187
+ ----------
188
+ message_id : str
189
+ The ID of the message associated with the end time.
190
+
191
+ Raises
192
+ ------
193
+ ValueError
194
+ If the message ID is not found.
195
+ """
196
+
197
+ @abstractmethod
198
+ def get_message_processing_duration(self, message_id: str) -> float:
199
+ """Get the message processing duration based on the message ID.
200
+
201
+ Parameters
202
+ ----------
203
+ message_id : str
204
+ The ID of the message.
205
+
206
+ Returns
207
+ -------
208
+ float
209
+ The processing duration in seconds.
210
+
211
+ Raises
212
+ ------
213
+ ValueError
214
+ If the message ID is not found, or if start/end times are missing.
215
+ """
@@ -16,7 +16,8 @@
16
16
 
17
17
 
18
18
  import threading
19
- from typing import Optional
19
+
20
+ from flwr.supercore.object_store import ObjectStoreFactory
20
21
 
21
22
  from .in_memory_nodestate import InMemoryNodeState
22
23
  from .nodestate import NodeState
@@ -25,8 +26,9 @@ from .nodestate import NodeState
25
26
  class NodeStateFactory:
26
27
  """Factory class that creates NodeState instances."""
27
28
 
28
- def __init__(self) -> None:
29
- self.state_instance: Optional[NodeState] = None
29
+ def __init__(self, objectstore_factory: ObjectStoreFactory) -> None:
30
+ self.objectstore_factory = objectstore_factory
31
+ self.state_instance: NodeState | None = None
30
32
  self.lock = threading.RLock()
31
33
 
32
34
  def state(self) -> NodeState:
@@ -34,5 +36,6 @@ class NodeStateFactory:
34
36
  # Lock access to NodeStateFactory to prevent returning different instances
35
37
  with self.lock:
36
38
  if self.state_instance is None:
37
- self.state_instance = InMemoryNodeState()
39
+ object_store = self.objectstore_factory.store()
40
+ self.state_instance = InMemoryNodeState(object_store)
38
41
  return self.state_instance
@@ -15,19 +15,18 @@
15
15
  """Flower ClientApp process."""
16
16
 
17
17
 
18
- import gc
19
18
  from logging import DEBUG, ERROR, INFO
20
- from typing import Optional
21
19
 
22
20
  import grpc
23
21
 
24
22
  from flwr.app.error import Error
25
23
  from flwr.cli.install import install_from_fab
26
- from flwr.client.client_app import ClientApp, LoadClientAppError
27
- from flwr.client.clientapp.utils import get_load_client_app_fn
24
+ from flwr.clientapp.client_app import ClientApp, LoadClientAppError
25
+ from flwr.clientapp.utils import get_load_client_app_fn
28
26
  from flwr.common import Context, Message
29
27
  from flwr.common.config import get_flwr_dir
30
28
  from flwr.common.constant import ErrorCode
29
+ from flwr.common.exit import ExitCode, flwr_exit, register_signal_handlers
31
30
  from flwr.common.grpc import create_channel, on_channel_state_change
32
31
  from flwr.common.inflatable import (
33
32
  get_all_nested_objects,
@@ -50,6 +49,7 @@ from flwr.common.serde import (
50
49
  message_to_proto,
51
50
  run_from_proto,
52
51
  )
52
+ from flwr.common.telemetry import EventType, event
53
53
  from flwr.common.typing import Fab, Run
54
54
  from flwr.proto.appio_pb2 import ( # pylint: disable=E0611
55
55
  PullAppInputsRequest,
@@ -63,27 +63,41 @@ from flwr.proto.appio_pb2 import ( # pylint: disable=E0611
63
63
  from flwr.proto.clientappio_pb2_grpc import ClientAppIoStub
64
64
  from flwr.proto.node_pb2 import Node # pylint: disable=E0611
65
65
  from flwr.supercore.app_utils import start_parent_process_monitor
66
+ from flwr.supercore.heartbeat import HeartbeatSender, make_app_heartbeat_fn_grpc
66
67
  from flwr.supercore.utils import mask_string
67
68
 
68
69
 
69
70
  def run_clientapp( # pylint: disable=R0913, R0914, R0917
70
71
  clientappio_api_address: str,
71
72
  token: str,
72
- flwr_dir: Optional[str] = None,
73
- certificates: Optional[bytes] = None,
74
- parent_pid: Optional[int] = None,
73
+ flwr_dir: str | None = None,
74
+ certificates: bytes | None = None,
75
+ parent_pid: int | None = None,
75
76
  ) -> None:
76
77
  """Run Flower ClientApp process."""
77
78
  # Monitor the main process in case of SIGKILL
78
79
  if parent_pid is not None:
79
80
  start_parent_process_monitor(parent_pid)
80
81
 
82
+ event(EventType.FLWR_CLIENTAPP_RUN_ENTER)
83
+
81
84
  channel = create_channel(
82
85
  server_address=clientappio_api_address,
83
86
  insecure=(certificates is None),
84
87
  root_certificates=certificates,
85
88
  )
86
89
  channel.subscribe(on_channel_state_change)
90
+ heartbeat_sender = None
91
+
92
+ def on_exit() -> None:
93
+ if heartbeat_sender is not None and heartbeat_sender.is_running:
94
+ heartbeat_sender.stop()
95
+ channel.close()
96
+
97
+ register_signal_handlers(
98
+ event_type=EventType.FLWR_CLIENTAPP_RUN_LEAVE,
99
+ exit_handlers=[on_exit],
100
+ )
87
101
 
88
102
  # Resolve directory where FABs are installed
89
103
  flwr_dir_ = get_flwr_dir(flwr_dir)
@@ -91,22 +105,27 @@ def run_clientapp( # pylint: disable=R0913, R0914, R0917
91
105
  stub = ClientAppIoStub(channel)
92
106
  _wrap_stub(stub, _make_simple_grpc_retry_invoker())
93
107
 
108
+ # Start app heartbeat
109
+ heartbeat_sender = HeartbeatSender(make_app_heartbeat_fn_grpc(stub, token))
110
+ heartbeat_sender.start()
111
+
94
112
  # Pull Message, Context, Run and (optional) FAB from SuperNode
95
113
  message, context, run, fab = pull_clientappinputs(stub=stub, token=token)
96
114
 
97
- # Install FAB, if provided
98
- if fab:
99
- log(DEBUG, "[flwr-clientapp] Start FAB installation.")
100
- install_from_fab(fab.content, flwr_dir=flwr_dir_, skip_prompt=True)
115
+ try:
101
116
 
102
- load_client_app_fn = get_load_client_app_fn(
103
- default_app_ref="",
104
- app_path=None,
105
- multi_app=True,
106
- flwr_dir=str(flwr_dir_),
107
- )
117
+ # Install FAB, if provided
118
+ if fab:
119
+ log(DEBUG, "[flwr-clientapp] Start FAB installation.")
120
+ install_from_fab(fab.content, flwr_dir=flwr_dir_, skip_prompt=True)
121
+
122
+ load_client_app_fn = get_load_client_app_fn(
123
+ default_app_ref="",
124
+ app_path=None,
125
+ multi_app=True,
126
+ flwr_dir=str(flwr_dir_),
127
+ )
108
128
 
109
- try:
110
129
  # Load ClientApp
111
130
  log(DEBUG, "[flwr-clientapp] Start `ClientApp` Loading.")
112
131
  client_app: ClientApp = load_client_app_fn(
@@ -137,18 +156,18 @@ def run_clientapp( # pylint: disable=R0913, R0914, R0917
137
156
  stub=stub, token=token, message=reply_message, context=context
138
157
  )
139
158
 
140
- del client_app, message, context, run, fab, reply_message
141
- gc.collect()
142
-
143
159
  except grpc.RpcError as e:
144
160
  log(ERROR, "GRPC error occurred: %s", str(e))
145
- finally:
146
- channel.close()
161
+
162
+ flwr_exit(
163
+ code=ExitCode.SUCCESS,
164
+ event_type=EventType.FLWR_CLIENTAPP_RUN_LEAVE,
165
+ )
147
166
 
148
167
 
149
168
  def pull_clientappinputs(
150
169
  stub: ClientAppIoStub, token: str
151
- ) -> tuple[Message, Context, Run, Optional[Fab]]:
170
+ ) -> tuple[Message, Context, Run, Fab | None]:
152
171
  """Pull ClientAppInputs from SuperNode."""
153
172
  masked_token = mask_string(token)
154
173
  log(INFO, "[flwr-clientapp] Pull `ClientAppInputs` for token %s", masked_token)