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
flwr/common/config.py CHANGED
@@ -20,7 +20,7 @@ import re
20
20
  import zipfile
21
21
  from io import BytesIO
22
22
  from pathlib import Path
23
- from typing import IO, Any, Optional, TypeVar, Union, cast, get_args
23
+ from typing import IO, Any, TypeVar, cast, get_args
24
24
 
25
25
  import tomli
26
26
  import typer
@@ -39,7 +39,7 @@ from . import ConfigRecord, object_ref
39
39
  T_dict = TypeVar("T_dict", bound=dict[str, Any]) # pylint: disable=invalid-name
40
40
 
41
41
 
42
- def get_flwr_dir(provided_path: Optional[str] = None) -> Path:
42
+ def get_flwr_dir(provided_path: str | None = None) -> Path:
43
43
  """Return the Flower home directory based on env variables."""
44
44
  if provided_path is None or not Path(provided_path).is_dir():
45
45
  return Path(
@@ -55,7 +55,7 @@ def get_project_dir(
55
55
  fab_id: str,
56
56
  fab_version: str,
57
57
  fab_hash: str,
58
- flwr_dir: Optional[Union[str, Path]] = None,
58
+ flwr_dir: str | Path | None = None,
59
59
  ) -> Path:
60
60
  """Return the project directory based on the given fab_id and fab_version."""
61
61
  # Check the fab_id
@@ -73,7 +73,7 @@ def get_project_dir(
73
73
  )
74
74
 
75
75
 
76
- def get_project_config(project_dir: Union[str, Path]) -> dict[str, Any]:
76
+ def get_project_config(project_dir: str | Path) -> dict[str, Any]:
77
77
  """Return pyproject.toml in the given project directory."""
78
78
  # Load pyproject.toml file
79
79
  toml_path = Path(project_dir) / FAB_CONFIG_FILE
@@ -134,7 +134,7 @@ def get_fused_config_from_dir(
134
134
  return fuse_dicts(flat_default_config, override_config)
135
135
 
136
136
 
137
- def get_fused_config_from_fab(fab_file: Union[Path, bytes], run: Run) -> UserConfig:
137
+ def get_fused_config_from_fab(fab_file: Path | bytes, run: Run) -> UserConfig:
138
138
  """Fuse default config in a `FAB` with overrides in a `Run`.
139
139
 
140
140
  This enables obtaining a run-config without having to install the FAB. This
@@ -146,7 +146,7 @@ def get_fused_config_from_fab(fab_file: Union[Path, bytes], run: Run) -> UserCon
146
146
  return fuse_dicts(flat_config_flat, run.override_config)
147
147
 
148
148
 
149
- def get_fused_config(run: Run, flwr_dir: Optional[Path]) -> UserConfig:
149
+ def get_fused_config(run: Run, flwr_dir: Path | None) -> UserConfig:
150
150
  """Merge the overrides from a `Run` with the config from a FAB.
151
151
 
152
152
  Get the config using the fab_id and the fab_version, remove the nesting by adding
@@ -165,9 +165,7 @@ def get_fused_config(run: Run, flwr_dir: Optional[Path]) -> UserConfig:
165
165
  return get_fused_config_from_dir(project_dir, run.override_config)
166
166
 
167
167
 
168
- def flatten_dict(
169
- raw_dict: Optional[dict[str, Any]], parent_key: str = ""
170
- ) -> UserConfig:
168
+ def flatten_dict(raw_dict: dict[str, Any] | None, parent_key: str = "") -> UserConfig:
171
169
  """Flatten dict by joining nested keys with a given separator."""
172
170
  if raw_dict is None:
173
171
  return {}
@@ -205,9 +203,7 @@ def unflatten_dict(flat_dict: dict[str, Any]) -> dict[str, Any]:
205
203
  return unflattened_dict
206
204
 
207
205
 
208
- def parse_config_args(
209
- config: Optional[list[str]], flatten: bool = True
210
- ) -> dict[str, Any]:
206
+ def parse_config_args(config: list[str] | None, flatten: bool = True) -> dict[str, Any]:
211
207
  """Parse separator separated list of key-value pairs separated by '='."""
212
208
  overrides: UserConfig = {}
213
209
 
@@ -246,6 +242,7 @@ def parse_config_args(
246
242
  "space-separated key-value pairs.",
247
243
  fg=typer.colors.RED,
248
244
  bold=True,
245
+ err=True,
249
246
  )
250
247
  raise typer.Exit(code=1) from err
251
248
 
@@ -269,7 +266,7 @@ def user_config_to_configrecord(config: UserConfig) -> ConfigRecord:
269
266
  return c_record
270
267
 
271
268
 
272
- def get_fab_config(fab_file: Union[Path, bytes]) -> dict[str, Any]:
269
+ def get_fab_config(fab_file: Path | bytes) -> dict[str, Any]:
273
270
  """Extract the config from a FAB file or path.
274
271
 
275
272
  Parameters
@@ -283,7 +280,7 @@ def get_fab_config(fab_file: Union[Path, bytes]) -> dict[str, Any]:
283
280
  Dict[str, Any]
284
281
  The `config` of the given Flower App Bundle.
285
282
  """
286
- fab_file_archive: Union[Path, IO[bytes]]
283
+ fab_file_archive: Path | IO[bytes]
287
284
  if isinstance(fab_file, bytes):
288
285
  fab_file_archive = BytesIO(fab_file)
289
286
  elif isinstance(fab_file, Path):
@@ -319,7 +316,7 @@ def _validate_run_config(config_dict: dict[str, Any], errors: list[str]) -> None
319
316
 
320
317
  # pylint: disable=too-many-branches
321
318
  def validate_fields_in_config(
322
- config: dict[str, Any]
319
+ config: dict[str, Any],
323
320
  ) -> tuple[bool, list[str], list[str]]:
324
321
  """Validate pyproject.toml fields."""
325
322
  errors = []
@@ -368,7 +365,7 @@ def validate_fields_in_config(
368
365
  def validate_config(
369
366
  config: dict[str, Any],
370
367
  check_module: bool = True,
371
- project_dir: Optional[Union[str, Path]] = None,
368
+ project_dir: str | Path | None = None,
372
369
  ) -> tuple[bool, list[str], list[str]]:
373
370
  """Validate pyproject.toml."""
374
371
  is_valid, errors, warnings = validate_fields_in_config(config)
flwr/common/constant.py CHANGED
@@ -17,6 +17,8 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ import os
21
+
20
22
  TRANSPORT_TYPE_GRPC_BIDI = "grpc-bidi"
21
23
  TRANSPORT_TYPE_GRPC_RERE = "grpc-rere"
22
24
  TRANSPORT_TYPE_GRPC_ADAPTER = "grpc-adapter"
@@ -60,7 +62,9 @@ HEARTBEAT_DEFAULT_INTERVAL = 30
60
62
  HEARTBEAT_CALL_TIMEOUT = 5
61
63
  HEARTBEAT_BASE_MULTIPLIER = 0.8
62
64
  HEARTBEAT_RANDOM_RANGE = (-0.1, 0.1)
63
- HEARTBEAT_MAX_INTERVAL = 1e300
65
+ HEARTBEAT_MIN_INTERVAL = 10
66
+ HEARTBEAT_MAX_INTERVAL = 1800 # 30 minutes
67
+ HEARTBEAT_INTERVAL_INF = 1e300 # Large value, disabling heartbeats
64
68
  HEARTBEAT_PATIENCE = 2
65
69
  RUN_FAILURE_DETAILS_NO_HEARTBEAT = "No heartbeat received from the run."
66
70
 
@@ -70,13 +74,24 @@ NODE_ID_NUM_BYTES = 8
70
74
 
71
75
  # Constants for FAB
72
76
  APP_DIR = "apps"
73
- FAB_ALLOWED_EXTENSIONS = {".py", ".toml", ".md"}
74
77
  FAB_CONFIG_FILE = "pyproject.toml"
75
78
  FAB_DATE = (2024, 10, 1, 0, 0, 0)
76
79
  FAB_HASH_TRUNCATION = 8
77
80
  FAB_MAX_SIZE = 10 * 1024 * 1024 # 10 MB
78
81
  FLWR_DIR = ".flwr" # The default Flower directory: ~/.flwr/
79
82
  FLWR_HOME = "FLWR_HOME" # If set, override the default Flower directory
83
+ # FAB file include patterns (gitignore-style patterns)
84
+ FAB_INCLUDE_PATTERNS = (
85
+ "**/*.py",
86
+ "**/*.toml",
87
+ "**/*.md",
88
+ )
89
+ # FAB file exclude patterns (gitignore-style patterns)
90
+ FAB_EXCLUDE_PATTERNS = (
91
+ f"{FLWR_DIR}/**", # Exclude the .flwr directory
92
+ "**/__pycache__/**",
93
+ FAB_CONFIG_FILE, # Exclude the original pyproject.toml
94
+ )
80
95
 
81
96
  # Constant for SuperLink
82
97
  SUPERLINK_NODE_ID = 1
@@ -109,14 +124,14 @@ LOG_UPLOAD_INTERVAL = 0.2 # Minimum interval between two log uploads
109
124
  # Retry configurations
110
125
  MAX_RETRY_DELAY = 20 # Maximum delay duration between two consecutive retries.
111
126
 
112
- # Constants for user authentication
127
+ # Constants for account authentication
113
128
  CREDENTIALS_DIR = ".credentials"
114
- AUTH_TYPE_JSON_KEY = "auth-type" # For key name in JSON file
115
- AUTH_TYPE_YAML_KEY = "auth_type" # For key name in YAML file
129
+ AUTHN_TYPE_JSON_KEY = "authn-type" # For key name in JSON file
130
+ AUTHN_TYPE_YAML_KEY = "authn_type" # For key name in YAML file
116
131
  ACCESS_TOKEN_KEY = "flwr-oidc-access-token"
117
132
  REFRESH_TOKEN_KEY = "flwr-oidc-refresh-token"
118
133
 
119
- # Constants for user authorization
134
+ # Constants for account authorization
120
135
  AUTHZ_TYPE_YAML_KEY = "authz_type" # For key name in YAML file
121
136
 
122
137
  # Constants for node authentication
@@ -135,7 +150,9 @@ GC_THRESHOLD = 200_000_000 # 200 MB
135
150
  # Constants for Inflatable
136
151
  HEAD_BODY_DIVIDER = b"\x00"
137
152
  HEAD_VALUE_DIVIDER = " "
138
- MAX_ARRAY_CHUNK_SIZE = 20_971_520 # 20 MB
153
+ FLWR_PRIVATE_MAX_ARRAY_CHUNK_SIZE = int(
154
+ os.getenv("FLWR_PRIVATE_MAX_ARRAY_CHUNK_SIZE", "5242880")
155
+ ) # 5 MB
139
156
 
140
157
  # Constants for serialization
141
158
  INT64_MAX_VALUE = 9223372036854775807 # (1 << 63) - 1
@@ -144,8 +161,12 @@ INT64_MAX_VALUE = 9223372036854775807 # (1 << 63) - 1
144
161
  FLWR_APP_TOKEN_LENGTH = 128 # Length of the token used
145
162
 
146
163
  # Constants for object pushing and pulling
147
- MAX_CONCURRENT_PUSHES = 8 # Default maximum number of concurrent pushes
148
- MAX_CONCURRENT_PULLS = 8 # Default maximum number of concurrent pulls
164
+ FLWR_PRIVATE_MAX_CONCURRENT_OBJ_PUSHES = int(
165
+ os.getenv("FLWR_PRIVATE_MAX_CONCURRENT_OBJ_PUSHES", "2")
166
+ ) # Default maximum number of concurrent pushes
167
+ FLWR_PRIVATE_MAX_CONCURRENT_OBJ_PULLS = int(
168
+ os.getenv("FLWR_PRIVATE_MAX_CONCURRENT_OBJ_PULLS", "2")
169
+ ) # Default maximum number of concurrent pulls
149
170
  PULL_MAX_TIME = 7200 # Default maximum time to wait for pulling objects
150
171
  PULL_MAX_TRIES_PER_OBJECT = 500 # Default maximum number of tries to pull an object
151
172
  PULL_INITIAL_BACKOFF = 1 # Initial backoff time for pulling objects
@@ -154,9 +175,13 @@ PULL_BACKOFF_CAP = 10 # Maximum backoff time for pulling objects
154
175
 
155
176
  # ControlServicer constants
156
177
  RUN_ID_NOT_FOUND_MESSAGE = "Run ID not found"
157
- NO_USER_AUTH_MESSAGE = "ControlServicer initialized without user authentication"
178
+ NO_ACCOUNT_AUTH_MESSAGE = "ControlServicer initialized without account authentication"
158
179
  NO_ARTIFACT_PROVIDER_MESSAGE = "ControlServicer initialized without artifact provider"
159
180
  PULL_UNFINISHED_RUN_MESSAGE = "Cannot pull artifacts for an unfinished run"
181
+ SUPERNODE_NOT_CREATED_FROM_CLI_MESSAGE = "Invalid SuperNode credentials"
182
+ PUBLIC_KEY_ALREADY_IN_USE_MESSAGE = "Public key already in use"
183
+ PUBLIC_KEY_NOT_VALID = "The provided public key is not valid"
184
+ NODE_NOT_FOUND_MESSAGE = "Node ID not found for account"
160
185
 
161
186
 
162
187
  class MessageType:
@@ -203,6 +228,8 @@ class ErrorCode:
203
228
  REPLY_MESSAGE_UNAVAILABLE = 4
204
229
  NODE_UNAVAILABLE = 5
205
230
  MOD_FAILED_PRECONDITION = 6
231
+ INVALID_FAB = 7
232
+ CLIENT_APP_CRASHED = 8
206
233
 
207
234
  def __new__(cls) -> ErrorCode:
208
235
  """Prevent instantiation."""
@@ -245,12 +272,23 @@ class CliOutputFormat:
245
272
  raise TypeError(f"{cls.__name__} cannot be instantiated.")
246
273
 
247
274
 
248
- class AuthType:
249
- """User authentication types."""
275
+ class AuthnType:
276
+ """Account authentication types."""
250
277
 
278
+ NOOP = "noop"
251
279
  OIDC = "oidc"
252
280
 
253
- def __new__(cls) -> AuthType:
281
+ def __new__(cls) -> AuthnType:
282
+ """Prevent instantiation."""
283
+ raise TypeError(f"{cls.__name__} cannot be instantiated.")
284
+
285
+
286
+ class AuthzType:
287
+ """Account authorization types."""
288
+
289
+ NOOP = "noop"
290
+
291
+ def __new__(cls) -> AuthzType:
254
292
  """Prevent instantiation."""
255
293
  raise TypeError(f"{cls.__name__} cannot be instantiated.")
256
294
 
@@ -281,3 +319,8 @@ class ExecPluginType:
281
319
  """Return all SuperExec plugin types."""
282
320
  # Filter all constants (uppercase) of the class
283
321
  return [v for k, v in vars(ExecPluginType).items() if k.isupper()]
322
+
323
+
324
+ # Constants for No-op auth plugins
325
+ NOOP_FLWR_AID = "<id:none>"
326
+ NOOP_ACCOUNT_NAME = "<name:none>"
@@ -16,7 +16,6 @@
16
16
 
17
17
 
18
18
  from logging import WARNING
19
- from typing import Optional
20
19
 
21
20
  import numpy as np
22
21
 
@@ -70,7 +69,7 @@ def compute_clip_model_update(
70
69
  """Compute model update (param1 - param2) and clip it.
71
70
 
72
71
  Then add the clipped value to param1."""
73
- model_update = [np.subtract(x, y) for (x, y) in zip(param1, param2)]
72
+ model_update = [np.subtract(x, y) for (x, y) in zip(param1, param2, strict=True)]
74
73
  clip_inputs_inplace(model_update, clipping_norm)
75
74
 
76
75
  for i, _ in enumerate(param2):
@@ -98,7 +97,7 @@ def compute_adaptive_clip_model_update(
98
97
  model update = param1 - param2
99
98
  Return the norm_bit
100
99
  """
101
- model_update = [np.subtract(x, y) for (x, y) in zip(param1, param2)]
100
+ model_update = [np.subtract(x, y) for (x, y) in zip(param1, param2, strict=True)]
102
101
  norm_bit = adaptive_clip_inputs_inplace(model_update, clipping_norm)
103
102
 
104
103
  for i, _ in enumerate(param2):
@@ -125,7 +124,7 @@ def add_gaussian_noise_to_params(
125
124
  def compute_adaptive_noise_params(
126
125
  noise_multiplier: float,
127
126
  num_sampled_clients: float,
128
- clipped_count_stddev: Optional[float],
127
+ clipped_count_stddev: float | None,
129
128
  ) -> tuple[float, float]:
130
129
  """Compute noising parameters for the adaptive clipping.
131
130
 
@@ -16,7 +16,6 @@
16
16
 
17
17
 
18
18
  from abc import ABC, abstractmethod
19
- from typing import Optional, Union
20
19
 
21
20
  import grpc
22
21
  from google.protobuf.message import Message as GrpcMessage
@@ -36,7 +35,7 @@ class EventLogWriterPlugin(ABC):
36
35
  self,
37
36
  request: GrpcMessage,
38
37
  context: grpc.ServicerContext,
39
- account_info: Optional[AccountInfo],
38
+ account_info: AccountInfo | None,
40
39
  method_name: str,
41
40
  ) -> LogEntry:
42
41
  """Compose pre-event log entry from the provided request and context."""
@@ -46,9 +45,9 @@ class EventLogWriterPlugin(ABC):
46
45
  self,
47
46
  request: GrpcMessage,
48
47
  context: grpc.ServicerContext,
49
- account_info: Optional[AccountInfo],
48
+ account_info: AccountInfo | None,
50
49
  method_name: str,
51
- response: Optional[Union[GrpcMessage, BaseException]],
50
+ response: GrpcMessage | BaseException | None,
52
51
  ) -> LogEntry:
53
52
  """Compose post-event log entry from the provided response and context."""
54
53
 
flwr/common/exit/exit.py CHANGED
@@ -15,14 +15,16 @@
15
15
  """Unified exit function."""
16
16
 
17
17
 
18
- from __future__ import annotations
19
-
18
+ import os
20
19
  import sys
20
+ import threading
21
+ import time
21
22
  from logging import ERROR, INFO
22
23
  from typing import Any, NoReturn
23
24
 
24
25
  from flwr.common import EventType, event
25
26
  from flwr.common.version import package_version
27
+ from flwr.supercore.constant import FORCE_EXIT_TIMEOUT_SECONDS
26
28
 
27
29
  from ..logger import log
28
30
  from .exit_code import EXIT_CODE_HELP
@@ -53,6 +55,10 @@ def flwr_exit(
53
55
  - `<message>`: Optional context or additional information about the exit.
54
56
  - `<short-help-message>`: A brief explanation for the given exit code.
55
57
  - `<help-page-url>`: A URL providing detailed documentation and resolution steps.
58
+
59
+ Notes
60
+ -----
61
+ This function MUST be called from the main thread.
56
62
  """
57
63
  is_error = not 0 <= code < 100 # 0-99 are success exit codes
58
64
 
@@ -84,6 +90,13 @@ def flwr_exit(
84
90
  # Trigger exit handlers
85
91
  trigger_exit_handlers()
86
92
 
93
+ # Start a daemon thread to force exit if graceful exit fails
94
+ def force_exit() -> None:
95
+ time.sleep(FORCE_EXIT_TIMEOUT_SECONDS)
96
+ os._exit(sys_exit_code)
97
+
98
+ threading.Thread(target=force_exit, daemon=True).start()
99
+
87
100
  # Exit
88
101
  sys.exit(sys_exit_code)
89
102
 
@@ -38,20 +38,29 @@ class ExitCode:
38
38
  SERVERAPP_STRATEGY_PRECONDITION_UNMET = 200
39
39
  SERVERAPP_EXCEPTION = 201
40
40
  SERVERAPP_STRATEGY_AGGREGATION_ERROR = 202
41
+ SERVERAPP_RUN_START_REJECTED = 203
41
42
 
42
43
  # SuperNode-specific exit codes (300-399)
43
44
  SUPERNODE_REST_ADDRESS_INVALID = 300
44
- SUPERNODE_NODE_AUTH_KEYS_REQUIRED = 301
45
- SUPERNODE_NODE_AUTH_KEYS_INVALID = 302
45
+ # SUPERNODE_NODE_AUTH_KEYS_REQUIRED = 301 --- DELETED ---
46
+ SUPERNODE_NODE_AUTH_KEY_INVALID = 302
47
+ SUPERNODE_STARTED_WITHOUT_TLS_BUT_NODE_AUTH_ENABLED = 303
48
+ SUPERNODE_INVALID_TRUSTED_ENTITIES = 304
46
49
 
47
50
  # SuperExec-specific exit codes (400-499)
48
51
  SUPEREXEC_INVALID_PLUGIN_CONFIG = 400
49
52
 
53
+ # FlowerCLI-specific exit codes (500-599)
54
+ FLWRCLI_NODE_AUTH_PUBLIC_KEY_INVALID = 500
55
+
50
56
  # Common exit codes (600-699)
51
57
  COMMON_ADDRESS_INVALID = 600
52
58
  COMMON_MISSING_EXTRA_REST = 601
53
59
  COMMON_TLS_NOT_SUPPORTED = 602
54
60
 
61
+ # Simulation exit codes (700-799)
62
+ SIMULATION_EXCEPTION = 700
63
+
55
64
  def __new__(cls) -> ExitCode:
56
65
  """Prevent instantiation."""
57
66
  raise TypeError(f"{cls.__name__} cannot be instantiated.")
@@ -97,25 +106,41 @@ EXIT_CODE_HELP = {
97
106
  "The strategy encountered an error during aggregation. Please check the logs "
98
107
  "for more details."
99
108
  ),
109
+ ExitCode.SERVERAPP_RUN_START_REJECTED: (
110
+ "The SuperLink rejected the request to start the run. This may occur if the "
111
+ "run has been stopped, the run ID or FAB is invalid, or the run failed to "
112
+ "start within the allowed time."
113
+ ),
100
114
  # SuperNode-specific exit codes (300-399)
101
115
  ExitCode.SUPERNODE_REST_ADDRESS_INVALID: (
102
116
  "When using the REST API, please provide `https://` or "
103
117
  "`http://` before the server address (e.g. `http://127.0.0.1:8080`)"
104
118
  ),
105
- ExitCode.SUPERNODE_NODE_AUTH_KEYS_REQUIRED: (
106
- "Node authentication requires file paths to both "
107
- "'--auth-supernode-private-key' and '--auth-supernode-public-key' "
108
- "to be provided (providing only one of them is not sufficient)."
109
- ),
110
- ExitCode.SUPERNODE_NODE_AUTH_KEYS_INVALID: (
111
- "Node authentication requires elliptic curve private and public key pair. "
112
- "Please ensure that the file path points to a valid private/public key "
119
+ ExitCode.SUPERNODE_NODE_AUTH_KEY_INVALID: (
120
+ "Node authentication requires elliptic curve private key. "
121
+ "Please ensure that the file path points to a valid private key "
113
122
  "file and try again."
114
123
  ),
124
+ ExitCode.SUPERNODE_STARTED_WITHOUT_TLS_BUT_NODE_AUTH_ENABLED: (
125
+ "The private key for SuperNode authentication was provided, but TLS is not "
126
+ "enabled. Node authentication can only be used when TLS is enabled."
127
+ ),
128
+ ExitCode.SUPERNODE_INVALID_TRUSTED_ENTITIES: (
129
+ "Failed to read the trusted entities YAML file. "
130
+ "Please ensure that a valid file is provided using "
131
+ "the `--trusted-entities` option."
132
+ ),
115
133
  # SuperExec-specific exit codes (400-499)
116
134
  ExitCode.SUPEREXEC_INVALID_PLUGIN_CONFIG: (
117
135
  "The YAML configuration for the SuperExec plugin is invalid."
118
136
  ),
137
+ # FlowerCLI-specific exit codes (500-599)
138
+ ExitCode.FLWRCLI_NODE_AUTH_PUBLIC_KEY_INVALID: (
139
+ "Node authentication requires a valid elliptic curve public key in the "
140
+ "SSH format and following a NIST standard elliptic curve (e.g. SECP384R1). "
141
+ "Please ensure that the file path points to a valid public key "
142
+ "file and try again."
143
+ ),
119
144
  # Common exit codes (600-699)
120
145
  ExitCode.COMMON_ADDRESS_INVALID: (
121
146
  "Please provide a valid URL, IPv4 or IPv6 address."
@@ -128,4 +153,8 @@ To use the REST API, install `flwr` with the `rest` extra:
128
153
  `pip install "flwr[rest]"`.
129
154
  """,
130
155
  ExitCode.COMMON_TLS_NOT_SUPPORTED: "Please use the '--insecure' flag.",
156
+ # Simulation exit codes (700-799)
157
+ ExitCode.SIMULATION_EXCEPTION: (
158
+ "An unhandled exception occurred when running the simulation."
159
+ ),
131
160
  }
@@ -17,7 +17,7 @@
17
17
 
18
18
  import signal
19
19
  import threading
20
- from typing import Callable
20
+ from collections.abc import Callable
21
21
 
22
22
  from .exit_code import ExitCode
23
23
 
@@ -58,5 +58,9 @@ def trigger_exit_handlers() -> None:
58
58
  """Trigger all registered exit handlers in LIFO order."""
59
59
  with _lock_handlers:
60
60
  for handler in reversed(registered_exit_handlers):
61
- handler()
61
+ try:
62
+ handler()
63
+ except Exception: # pylint: disable=broad-exception-caught
64
+ # Ignore exceptions in exit handlers
65
+ pass
62
66
  registered_exit_handlers.clear()
@@ -16,9 +16,9 @@
16
16
 
17
17
 
18
18
  import signal
19
+ from collections.abc import Callable
19
20
  from threading import Thread
20
21
  from types import FrameType
21
- from typing import Callable, Optional
22
22
 
23
23
  from grpc import Server
24
24
 
@@ -40,10 +40,10 @@ if hasattr(signal, "SIGQUIT"):
40
40
 
41
41
  def register_signal_handlers(
42
42
  event_type: EventType,
43
- exit_message: Optional[str] = None,
44
- grpc_servers: Optional[list[Server]] = None,
45
- bckg_threads: Optional[list[Thread]] = None,
46
- exit_handlers: Optional[list[Callable[[], None]]] = None,
43
+ exit_message: str | None = None,
44
+ grpc_servers: list[Server] | None = None,
45
+ bckg_threads: list[Thread] | None = None,
46
+ exit_handlers: list[Callable[[], None]] | None = None,
47
47
  ) -> None:
48
48
  """Register exit handlers for `SIGINT`, `SIGTERM` and `SIGQUIT` signals.
49
49
 
flwr/common/grpc.py CHANGED
@@ -18,9 +18,9 @@
18
18
  import concurrent.futures
19
19
  import os
20
20
  import sys
21
- from collections.abc import Sequence
21
+ from collections.abc import Callable, Sequence
22
22
  from logging import DEBUG, ERROR
23
- from typing import Any, Callable, Optional
23
+ from typing import Any
24
24
 
25
25
  import grpc
26
26
 
@@ -46,9 +46,9 @@ if "GRPC_VERBOSITY" not in os.environ:
46
46
  def create_channel(
47
47
  server_address: str,
48
48
  insecure: bool,
49
- root_certificates: Optional[bytes] = None,
49
+ root_certificates: bytes | None = None,
50
50
  max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
51
- interceptors: Optional[Sequence[grpc.UnaryUnaryClientInterceptor]] = None,
51
+ interceptors: Sequence[grpc.UnaryUnaryClientInterceptor] | None = None,
52
52
  ) -> grpc.Channel:
53
53
  """Create a gRPC channel, either secure or insecure."""
54
54
  # Check for conflicting parameters
@@ -104,8 +104,8 @@ def generic_create_grpc_server( # pylint: disable=too-many-arguments, R0914, R0
104
104
  max_concurrent_workers: int = 1000,
105
105
  max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
106
106
  keepalive_time_ms: int = 210000,
107
- certificates: Optional[tuple[bytes, bytes, bytes]] = None,
108
- interceptors: Optional[Sequence[grpc.ServerInterceptor]] = None,
107
+ certificates: tuple[bytes, bytes, bytes] | None = None,
108
+ interceptors: Sequence[grpc.ServerInterceptor] | None = None,
109
109
  ) -> grpc.Server:
110
110
  """Create a gRPC server with a single servicer.
111
111
 
@@ -15,7 +15,7 @@
15
15
  """InflatableObject gRPC utils."""
16
16
 
17
17
 
18
- from typing import Callable
18
+ from collections.abc import Callable
19
19
 
20
20
  from flwr.proto.message_pb2 import ( # pylint: disable=E0611
21
21
  ConfirmMessageReceivedRequest,