flwr 1.16.0__py3-none-any.whl → 1.18.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 (248) hide show
  1. flwr/__init__.py +1 -1
  2. flwr/cli/__init__.py +1 -1
  3. flwr/cli/app.py +21 -2
  4. flwr/cli/build.py +1 -1
  5. flwr/cli/cli_user_auth_interceptor.py +1 -1
  6. flwr/cli/config_utils.py +53 -17
  7. flwr/cli/example.py +1 -1
  8. flwr/cli/install.py +1 -1
  9. flwr/cli/log.py +1 -1
  10. flwr/cli/login/__init__.py +1 -1
  11. flwr/cli/login/login.py +12 -1
  12. flwr/cli/ls.py +1 -1
  13. flwr/cli/new/__init__.py +1 -1
  14. flwr/cli/new/new.py +4 -4
  15. flwr/cli/new/templates/__init__.py +1 -1
  16. flwr/cli/new/templates/app/__init__.py +1 -1
  17. flwr/cli/new/templates/app/code/__init__.py +1 -1
  18. flwr/cli/new/templates/app/code/flwr_tune/__init__.py +1 -1
  19. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +5 -5
  20. flwr/cli/new/templates/app/code/task.sklearn.py.tpl +1 -1
  21. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
  22. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +4 -4
  23. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  24. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  25. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  26. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  27. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
  28. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  29. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
  30. flwr/cli/run/__init__.py +1 -1
  31. flwr/cli/run/run.py +6 -10
  32. flwr/cli/stop.py +1 -1
  33. flwr/cli/utils.py +11 -12
  34. flwr/client/__init__.py +1 -1
  35. flwr/client/app.py +58 -56
  36. flwr/client/client.py +1 -1
  37. flwr/client/client_app.py +231 -166
  38. flwr/client/clientapp/__init__.py +1 -1
  39. flwr/client/clientapp/app.py +3 -3
  40. flwr/client/clientapp/clientappio_servicer.py +1 -1
  41. flwr/client/clientapp/utils.py +1 -1
  42. flwr/client/dpfedavg_numpy_client.py +1 -1
  43. flwr/client/grpc_adapter_client/__init__.py +1 -1
  44. flwr/client/grpc_adapter_client/connection.py +1 -1
  45. flwr/client/grpc_client/__init__.py +1 -1
  46. flwr/client/grpc_client/connection.py +37 -34
  47. flwr/client/grpc_rere_client/__init__.py +1 -1
  48. flwr/client/grpc_rere_client/client_interceptor.py +1 -1
  49. flwr/client/grpc_rere_client/connection.py +1 -1
  50. flwr/client/grpc_rere_client/grpc_adapter.py +1 -1
  51. flwr/client/heartbeat.py +1 -1
  52. flwr/client/message_handler/__init__.py +1 -1
  53. flwr/client/message_handler/message_handler.py +28 -28
  54. flwr/client/mod/__init__.py +3 -3
  55. flwr/client/mod/centraldp_mods.py +8 -8
  56. flwr/client/mod/comms_mods.py +17 -23
  57. flwr/client/mod/localdp_mod.py +10 -10
  58. flwr/client/mod/secure_aggregation/__init__.py +1 -1
  59. flwr/client/mod/secure_aggregation/secagg_mod.py +1 -1
  60. flwr/client/mod/secure_aggregation/secaggplus_mod.py +32 -32
  61. flwr/client/mod/utils.py +1 -1
  62. flwr/client/nodestate/__init__.py +1 -1
  63. flwr/client/nodestate/in_memory_nodestate.py +1 -1
  64. flwr/client/nodestate/nodestate.py +1 -1
  65. flwr/client/nodestate/nodestate_factory.py +1 -1
  66. flwr/client/numpy_client.py +1 -1
  67. flwr/client/rest_client/__init__.py +1 -1
  68. flwr/client/rest_client/connection.py +1 -1
  69. flwr/client/run_info_store.py +3 -3
  70. flwr/client/supernode/__init__.py +1 -1
  71. flwr/client/supernode/app.py +1 -1
  72. flwr/client/typing.py +1 -1
  73. flwr/common/__init__.py +13 -5
  74. flwr/common/address.py +1 -1
  75. flwr/common/args.py +1 -1
  76. flwr/common/auth_plugin/__init__.py +1 -1
  77. flwr/common/auth_plugin/auth_plugin.py +1 -1
  78. flwr/common/config.py +5 -5
  79. flwr/common/constant.py +7 -7
  80. flwr/common/context.py +5 -5
  81. flwr/common/date.py +1 -1
  82. flwr/common/differential_privacy.py +1 -1
  83. flwr/common/differential_privacy_constants.py +1 -1
  84. flwr/common/dp.py +1 -1
  85. flwr/common/event_log_plugin/event_log_plugin.py +3 -3
  86. flwr/common/exit/exit.py +6 -6
  87. flwr/common/exit_handlers.py +1 -1
  88. flwr/common/grpc.py +1 -1
  89. flwr/common/logger.py +3 -3
  90. flwr/common/message.py +344 -102
  91. flwr/common/object_ref.py +1 -1
  92. flwr/common/parameter.py +1 -1
  93. flwr/common/pyproject.py +1 -1
  94. flwr/common/record/__init__.py +9 -5
  95. flwr/common/record/arrayrecord.py +626 -0
  96. flwr/common/record/{configsrecord.py → configrecord.py} +83 -37
  97. flwr/common/record/conversion_utils.py +2 -2
  98. flwr/common/record/{metricsrecord.py → metricrecord.py} +90 -44
  99. flwr/common/record/recorddict.py +337 -0
  100. flwr/common/record/typeddict.py +1 -1
  101. flwr/common/recorddict_compat.py +410 -0
  102. flwr/common/retry_invoker.py +10 -10
  103. flwr/common/secure_aggregation/__init__.py +1 -1
  104. flwr/common/secure_aggregation/crypto/__init__.py +1 -1
  105. flwr/common/secure_aggregation/crypto/shamir.py +52 -30
  106. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +1 -1
  107. flwr/common/secure_aggregation/ndarrays_arithmetic.py +1 -1
  108. flwr/common/secure_aggregation/quantization.py +1 -1
  109. flwr/common/secure_aggregation/secaggplus_constants.py +2 -2
  110. flwr/common/secure_aggregation/secaggplus_utils.py +1 -1
  111. flwr/common/serde.py +67 -72
  112. flwr/common/telemetry.py +2 -2
  113. flwr/common/typing.py +9 -9
  114. flwr/common/version.py +1 -1
  115. flwr/proto/__init__.py +1 -1
  116. flwr/proto/exec_pb2.py +3 -3
  117. flwr/proto/exec_pb2.pyi +3 -3
  118. flwr/proto/message_pb2.py +12 -12
  119. flwr/proto/message_pb2.pyi +9 -9
  120. flwr/proto/recorddict_pb2.py +70 -0
  121. flwr/proto/{recordset_pb2.pyi → recorddict_pb2.pyi} +35 -35
  122. flwr/proto/run_pb2.py +31 -31
  123. flwr/proto/run_pb2.pyi +3 -3
  124. flwr/server/__init__.py +4 -2
  125. flwr/server/app.py +67 -12
  126. flwr/server/client_manager.py +1 -1
  127. flwr/server/client_proxy.py +1 -1
  128. flwr/server/compat/__init__.py +3 -3
  129. flwr/server/compat/app.py +12 -12
  130. flwr/server/compat/app_utils.py +17 -17
  131. flwr/server/compat/{driver_client_proxy.py → grid_client_proxy.py} +39 -39
  132. flwr/server/compat/legacy_context.py +1 -1
  133. flwr/server/criterion.py +1 -1
  134. flwr/server/fleet_event_log_interceptor.py +94 -0
  135. flwr/server/{driver → grid}/__init__.py +8 -7
  136. flwr/server/{driver/driver.py → grid/grid.py} +48 -19
  137. flwr/server/{driver/grpc_driver.py → grid/grpc_grid.py} +87 -64
  138. flwr/server/{driver/inmemory_driver.py → grid/inmemory_grid.py} +24 -34
  139. flwr/server/history.py +1 -1
  140. flwr/server/run_serverapp.py +5 -5
  141. flwr/server/server.py +1 -1
  142. flwr/server/server_app.py +98 -71
  143. flwr/server/server_config.py +1 -1
  144. flwr/server/serverapp/__init__.py +1 -1
  145. flwr/server/serverapp/app.py +11 -11
  146. flwr/server/serverapp_components.py +1 -1
  147. flwr/server/strategy/__init__.py +1 -1
  148. flwr/server/strategy/aggregate.py +1 -1
  149. flwr/server/strategy/bulyan.py +2 -2
  150. flwr/server/strategy/dp_adaptive_clipping.py +17 -17
  151. flwr/server/strategy/dp_fixed_clipping.py +17 -17
  152. flwr/server/strategy/dpfedavg_adaptive.py +1 -1
  153. flwr/server/strategy/dpfedavg_fixed.py +1 -1
  154. flwr/server/strategy/fault_tolerant_fedavg.py +1 -1
  155. flwr/server/strategy/fedadagrad.py +1 -1
  156. flwr/server/strategy/fedadam.py +1 -1
  157. flwr/server/strategy/fedavg.py +1 -1
  158. flwr/server/strategy/fedavg_android.py +1 -1
  159. flwr/server/strategy/fedavgm.py +1 -1
  160. flwr/server/strategy/fedmedian.py +1 -1
  161. flwr/server/strategy/fedopt.py +1 -1
  162. flwr/server/strategy/fedprox.py +1 -1
  163. flwr/server/strategy/fedtrimmedavg.py +1 -1
  164. flwr/server/strategy/fedxgb_bagging.py +1 -1
  165. flwr/server/strategy/fedxgb_cyclic.py +1 -1
  166. flwr/server/strategy/fedxgb_nn_avg.py +3 -2
  167. flwr/server/strategy/fedyogi.py +1 -1
  168. flwr/server/strategy/krum.py +1 -1
  169. flwr/server/strategy/qfedavg.py +1 -1
  170. flwr/server/strategy/strategy.py +1 -1
  171. flwr/server/superlink/__init__.py +1 -1
  172. flwr/server/superlink/ffs/__init__.py +1 -1
  173. flwr/server/superlink/ffs/disk_ffs.py +1 -1
  174. flwr/server/superlink/ffs/ffs.py +1 -1
  175. flwr/server/superlink/ffs/ffs_factory.py +1 -1
  176. flwr/server/superlink/fleet/__init__.py +1 -1
  177. flwr/server/superlink/fleet/grpc_adapter/__init__.py +1 -1
  178. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -1
  179. flwr/server/superlink/fleet/grpc_bidi/__init__.py +1 -1
  180. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -1
  181. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +1 -1
  182. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +1 -1
  183. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +13 -13
  184. flwr/server/superlink/fleet/grpc_rere/__init__.py +1 -1
  185. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +1 -1
  186. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +1 -1
  187. flwr/server/superlink/fleet/message_handler/__init__.py +1 -1
  188. flwr/server/superlink/fleet/message_handler/message_handler.py +1 -1
  189. flwr/server/superlink/fleet/rest_rere/__init__.py +1 -1
  190. flwr/server/superlink/fleet/rest_rere/rest_api.py +1 -1
  191. flwr/server/superlink/fleet/vce/__init__.py +1 -1
  192. flwr/server/superlink/fleet/vce/backend/__init__.py +1 -1
  193. flwr/server/superlink/fleet/vce/backend/backend.py +3 -3
  194. flwr/server/superlink/fleet/vce/backend/raybackend.py +3 -3
  195. flwr/server/superlink/fleet/vce/vce_api.py +2 -4
  196. flwr/server/superlink/linkstate/__init__.py +1 -1
  197. flwr/server/superlink/linkstate/in_memory_linkstate.py +34 -9
  198. flwr/server/superlink/linkstate/linkstate.py +5 -5
  199. flwr/server/superlink/linkstate/linkstate_factory.py +1 -1
  200. flwr/server/superlink/linkstate/sqlite_linkstate.py +62 -28
  201. flwr/server/superlink/linkstate/utils.py +94 -28
  202. flwr/server/superlink/{driver → serverappio}/__init__.py +1 -1
  203. flwr/server/superlink/{driver → serverappio}/serverappio_grpc.py +1 -1
  204. flwr/server/superlink/{driver → serverappio}/serverappio_servicer.py +4 -4
  205. flwr/server/superlink/simulation/__init__.py +1 -1
  206. flwr/server/superlink/simulation/simulationio_grpc.py +1 -1
  207. flwr/server/superlink/simulation/simulationio_servicer.py +3 -3
  208. flwr/server/superlink/utils.py +1 -1
  209. flwr/server/typing.py +4 -4
  210. flwr/server/utils/__init__.py +1 -1
  211. flwr/server/utils/tensorboard.py +1 -1
  212. flwr/server/utils/validator.py +5 -5
  213. flwr/server/workflow/__init__.py +1 -1
  214. flwr/server/workflow/constant.py +1 -1
  215. flwr/server/workflow/default_workflows.py +49 -58
  216. flwr/server/workflow/secure_aggregation/__init__.py +1 -1
  217. flwr/server/workflow/secure_aggregation/secagg_workflow.py +1 -1
  218. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +49 -51
  219. flwr/simulation/__init__.py +1 -1
  220. flwr/simulation/app.py +3 -3
  221. flwr/simulation/legacy_app.py +1 -1
  222. flwr/simulation/ray_transport/__init__.py +1 -1
  223. flwr/simulation/ray_transport/ray_actor.py +5 -3
  224. flwr/simulation/ray_transport/ray_client_proxy.py +35 -33
  225. flwr/simulation/ray_transport/utils.py +1 -1
  226. flwr/simulation/run_simulation.py +17 -17
  227. flwr/simulation/simulationio_connection.py +1 -1
  228. flwr/superexec/__init__.py +1 -1
  229. flwr/superexec/app.py +1 -1
  230. flwr/superexec/deployment.py +5 -5
  231. flwr/superexec/exec_event_log_interceptor.py +135 -0
  232. flwr/superexec/exec_grpc.py +11 -5
  233. flwr/superexec/exec_servicer.py +3 -3
  234. flwr/superexec/exec_user_auth_interceptor.py +19 -3
  235. flwr/superexec/executor.py +4 -4
  236. flwr/superexec/simulation.py +4 -4
  237. {flwr-1.16.0.dist-info → flwr-1.18.0.dist-info}/METADATA +3 -3
  238. flwr-1.18.0.dist-info/RECORD +332 -0
  239. flwr/common/record/parametersrecord.py +0 -339
  240. flwr/common/record/recordset.py +0 -209
  241. flwr/common/recordset_compat.py +0 -418
  242. flwr/proto/recordset_pb2.py +0 -70
  243. flwr-1.16.0.dist-info/LICENSE +0 -202
  244. flwr-1.16.0.dist-info/RECORD +0 -331
  245. /flwr/proto/{recordset_pb2_grpc.py → recorddict_pb2_grpc.py} +0 -0
  246. /flwr/proto/{recordset_pb2_grpc.pyi → recorddict_pb2_grpc.pyi} +0 -0
  247. {flwr-1.16.0.dist-info → flwr-1.18.0.dist-info}/WHEEL +0 -0
  248. {flwr-1.16.0.dist-info → flwr-1.18.0.dist-info}/entry_points.txt +0 -0
flwr/client/app.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2020 Flower Labs GmbH. All Rights Reserved.
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -158,33 +158,33 @@ def start_client(
158
158
 
159
159
  Examples
160
160
  --------
161
- Starting a gRPC client with an insecure server connection:
162
-
163
- >>> start_client(
164
- >>> server_address=localhost:8080,
165
- >>> client_fn=client_fn,
166
- >>> )
167
-
168
- Starting an SSL-enabled gRPC client using system certificates:
169
-
170
- >>> def client_fn(context: Context):
171
- >>> return FlowerClient().to_client()
172
- >>>
173
- >>> start_client(
174
- >>> server_address=localhost:8080,
175
- >>> client_fn=client_fn,
176
- >>> insecure=False,
177
- >>> )
178
-
179
- Starting an SSL-enabled gRPC client using provided certificates:
180
-
181
- >>> from pathlib import Path
182
- >>>
183
- >>> start_client(
184
- >>> server_address=localhost:8080,
185
- >>> client_fn=client_fn,
186
- >>> root_certificates=Path("/crts/root.pem").read_bytes(),
187
- >>> )
161
+ Starting a gRPC client with an insecure server connection::
162
+
163
+ start_client(
164
+ server_address=localhost:8080,
165
+ client_fn=client_fn,
166
+ )
167
+
168
+ Starting a TLS-enabled gRPC client using system certificates::
169
+
170
+ def client_fn(context: Context):
171
+ return FlowerClient().to_client()
172
+
173
+ start_client(
174
+ server_address=localhost:8080,
175
+ client_fn=client_fn,
176
+ insecure=False,
177
+ )
178
+
179
+ Starting a TLS-enabled gRPC client using provided certificates::
180
+
181
+ from pathlib import Path
182
+
183
+ start_client(
184
+ server_address=localhost:8080,
185
+ client_fn=client_fn,
186
+ root_certificates=Path("/crts/root.pem").read_bytes(),
187
+ )
188
188
  """
189
189
  msg = (
190
190
  "flwr.client.start_client() is deprecated."
@@ -495,8 +495,9 @@ def start_client_internal(
495
495
  context = run_info_store.retrieve_context(run_id=run_id)
496
496
  # Create an error reply message that will never be used to prevent
497
497
  # the used-before-assignment linting error
498
- reply_message = message.create_error_reply(
499
- error=Error(code=ErrorCode.UNKNOWN, reason="Unknown")
498
+ reply_message = Message(
499
+ Error(code=ErrorCode.UNKNOWN, reason="Unknown"),
500
+ reply_to=message,
500
501
  )
501
502
 
502
503
  # Handle app loading and task message
@@ -593,8 +594,9 @@ def start_client_internal(
593
594
  log(ERROR, "%s raised an exception", exc_entity, exc_info=ex)
594
595
 
595
596
  # Create error message
596
- reply_message = message.create_error_reply(
597
- error=Error(code=e_code, reason=reason)
597
+ reply_message = Message(
598
+ Error(code=e_code, reason=reason),
599
+ reply_to=message,
598
600
  )
599
601
  else:
600
602
  # No exception, update node state
@@ -682,30 +684,30 @@ def start_numpy_client(
682
684
 
683
685
  Examples
684
686
  --------
685
- Starting a gRPC client with an insecure server connection:
686
-
687
- >>> start_numpy_client(
688
- >>> server_address=localhost:8080,
689
- >>> client=FlowerClient(),
690
- >>> )
691
-
692
- Starting an SSL-enabled gRPC client using system certificates:
693
-
694
- >>> start_numpy_client(
695
- >>> server_address=localhost:8080,
696
- >>> client=FlowerClient(),
697
- >>> insecure=False,
698
- >>> )
699
-
700
- Starting an SSL-enabled gRPC client using provided certificates:
701
-
702
- >>> from pathlib import Path
703
- >>>
704
- >>> start_numpy_client(
705
- >>> server_address=localhost:8080,
706
- >>> client=FlowerClient(),
707
- >>> root_certificates=Path("/crts/root.pem").read_bytes(),
708
- >>> )
687
+ Starting a gRPC client with an insecure server connection::
688
+
689
+ start_numpy_client(
690
+ server_address=localhost:8080,
691
+ client=FlowerClient(),
692
+ )
693
+
694
+ Starting a TLS-enabled gRPC client using system certificates::
695
+
696
+ start_numpy_client(
697
+ server_address=localhost:8080,
698
+ client=FlowerClient(),
699
+ insecure=False,
700
+ )
701
+
702
+ Starting a TLS-enabled gRPC client using provided certificates::
703
+
704
+ from pathlib import Path
705
+
706
+ start_numpy_client(
707
+ server_address=localhost:8080,
708
+ client=FlowerClient(),
709
+ root_certificates=Path("/crts/root.pem").read_bytes(),
710
+ )
709
711
  """
710
712
  mssg = (
711
713
  "flwr.client.start_numpy_client() is deprecated. \n\tInstead, use "
flwr/client/client.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2020 Flower Labs GmbH. All Rights Reserved.
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
flwr/client/client_app.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Flower Labs GmbH. All Rights Reserved.
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -27,10 +27,13 @@ from flwr.client.message_handler.message_handler import (
27
27
  from flwr.client.mod.utils import make_ffn
28
28
  from flwr.client.typing import ClientFnExt, Mod
29
29
  from flwr.common import Context, Message, MessageType
30
- from flwr.common.logger import warn_deprecated_feature, warn_preview_feature
30
+ from flwr.common.logger import warn_deprecated_feature
31
+ from flwr.common.message import validate_message_type
31
32
 
32
33
  from .typing import ClientAppCallable
33
34
 
35
+ DEFAULT_ACTION = "default"
36
+
34
37
 
35
38
  def _alert_erroneous_client_fn() -> None:
36
39
  raise ValueError(
@@ -92,16 +95,16 @@ class ClientApp:
92
95
 
93
96
  Examples
94
97
  --------
95
- Assuming a typical `Client` implementation named `FlowerClient`, you can wrap it in
96
- a `ClientApp` as follows:
97
-
98
- >>> class FlowerClient(NumPyClient):
99
- >>> # ...
100
- >>>
101
- >>> def client_fn(context: Context):
102
- >>> return FlowerClient().to_client()
103
- >>>
104
- >>> app = ClientApp(client_fn)
98
+ Assuming a typical ``Client`` implementation named ``FlowerClient``, you can wrap
99
+ it in a ``ClientApp`` as follows::
100
+
101
+ class FlowerClient(NumPyClient):
102
+ # ...
103
+
104
+ def client_fn(context: Context):
105
+ return FlowerClient().to_client()
106
+
107
+ app = ClientApp(client_fn)
105
108
  """
106
109
 
107
110
  def __init__(
@@ -110,6 +113,7 @@ class ClientApp:
110
113
  mods: Optional[list[Mod]] = None,
111
114
  ) -> None:
112
115
  self._mods: list[Mod] = mods if mods is not None else []
116
+ self._registered_funcs: dict[str, ClientAppCallable] = {}
113
117
 
114
118
  # Create wrapper function for `handle`
115
119
  self._call: Optional[ClientAppCallable] = None
@@ -129,10 +133,7 @@ class ClientApp:
129
133
  # Wrap mods around the wrapped handle function
130
134
  self._call = make_ffn(ffn, mods if mods is not None else [])
131
135
 
132
- # Step functions
133
- self._train: Optional[ClientAppCallable] = None
134
- self._evaluate: Optional[ClientAppCallable] = None
135
- self._query: Optional[ClientAppCallable] = None
136
+ # Lifespan function
136
137
  self._lifespan = _empty_lifespan
137
138
 
138
139
  def __call__(self, message: Message, context: Context) -> Message:
@@ -142,159 +143,189 @@ class ClientApp:
142
143
  if self._call:
143
144
  return self._call(message, context)
144
145
 
145
- # Execute message using a new
146
- if message.metadata.message_type == MessageType.TRAIN:
147
- if self._train:
148
- return self._train(message, context)
149
- raise ValueError("No `train` function registered")
150
- if message.metadata.message_type == MessageType.EVALUATE:
151
- if self._evaluate:
152
- return self._evaluate(message, context)
153
- raise ValueError("No `evaluate` function registered")
154
- if message.metadata.message_type == MessageType.QUERY:
155
- if self._query:
156
- return self._query(message, context)
157
- raise ValueError("No `query` function registered")
158
-
159
- # Message type did not match one of the known message types abvoe
160
- raise ValueError(f"Unknown message_type: {message.metadata.message_type}")
146
+ # Get the category and the action
147
+ # A valid message type is of the form "<category>" or "<category>.<action>",
148
+ # where <category> must be "train"/"evaluate"/"query", and <action> is a
149
+ # valid Python identifier
150
+ if not validate_message_type(message.metadata.message_type):
151
+ raise ValueError(
152
+ f"Invalid message type: {message.metadata.message_type}"
153
+ )
154
+
155
+ category, action = message.metadata.message_type, DEFAULT_ACTION
156
+ if "." in category:
157
+ category, action = category.split(".")
158
+
159
+ # Check if the function is registered
160
+ if (full_name := f"{category}.{action}") in self._registered_funcs:
161
+ return self._registered_funcs[full_name](message, context)
162
+
163
+ raise ValueError(f"No {category} function registered with name '{action}'")
161
164
 
162
165
  def train(
163
- self, mods: Optional[list[Mod]] = None
166
+ self, action: str = DEFAULT_ACTION, *, mods: Optional[list[Mod]] = None
164
167
  ) -> Callable[[ClientAppCallable], ClientAppCallable]:
165
- """Return a decorator that registers the train fn with the client app.
168
+ """Register a train function with the ``ClientApp``.
169
+
170
+ Parameters
171
+ ----------
172
+ action : str (default: "default")
173
+ The action name used to route messages. Defaults to "default".
174
+ mods : Optional[list[Mod]] (default: None)
175
+ A list of function-specific modifiers.
176
+
177
+ Returns
178
+ -------
179
+ Callable[[ClientAppCallable], ClientAppCallable]
180
+ A decorator that registers a train function with the ``ClientApp``.
166
181
 
167
182
  Examples
168
183
  --------
169
- Registering a train function:
170
-
171
- >>> app = ClientApp()
172
- >>>
173
- >>> @app.train()
174
- >>> def train(message: Message, context: Context) -> Message:
175
- >>> print("ClientApp training running")
176
- >>> # Create and return an echo reply message
177
- >>> return message.create_reply(content=message.content())
178
-
179
- Registering a train function with a function-specific modifier:
180
-
181
- >>> from flwr.client.mod import message_size_mod
182
- >>>
183
- >>> app = ClientApp()
184
- >>>
185
- >>> @app.train(mods=[message_size_mod])
186
- >>> def train(message: Message, context: Context) -> Message:
187
- >>> print("ClientApp training running with message size mod")
188
- >>> return message.create_reply(content=message.content())
189
- """
184
+ Registering a train function::
190
185
 
191
- def train_decorator(train_fn: ClientAppCallable) -> ClientAppCallable:
192
- """Register the train fn with the ServerApp object."""
193
- if self._call:
194
- raise _registration_error(MessageType.TRAIN)
186
+ app = ClientApp()
195
187
 
196
- warn_preview_feature("ClientApp-register-train-function")
188
+ @app.train()
189
+ def train(message: Message, context: Context) -> Message:
190
+ print("Executing default train function")
191
+ # Create and return an echo reply message
192
+ return Message(message.content, reply_to=message)
197
193
 
198
- # Register provided function with the ClientApp object
199
- # Wrap mods around the wrapped step function
200
- self._train = make_ffn(train_fn, self._mods + (mods or []))
194
+ Registering a train function with a custom action name::
201
195
 
202
- # Return provided function unmodified
203
- return train_fn
196
+ app = ClientApp()
204
197
 
205
- return train_decorator
198
+ # Messages with `message_type="train.custom_action"` will be
199
+ # routed to this function.
200
+ @app.train("custom_action")
201
+ def custom_action(message: Message, context: Context) -> Message:
202
+ print("Executing train function for custom action")
203
+ return Message(message.content, reply_to=message)
204
+
205
+ Registering a train function with a function-specific Flower Mod::
206
+
207
+ from flwr.client.mod import message_size_mod
208
+
209
+ app = ClientApp()
210
+
211
+ # Using the `mods` argument to apply a function-specific mod.
212
+ @app.train(mods=[message_size_mod])
213
+ def train(message: Message, context: Context) -> Message:
214
+ print("Executing train function with message size mod")
215
+ # Create and return an echo reply message
216
+ return Message(message.content, reply_to=message)
217
+ """
218
+ return _get_decorator(self, MessageType.TRAIN, action, mods)
206
219
 
207
220
  def evaluate(
208
- self, mods: Optional[list[Mod]] = None
221
+ self, action: str = DEFAULT_ACTION, *, mods: Optional[list[Mod]] = None
209
222
  ) -> Callable[[ClientAppCallable], ClientAppCallable]:
210
- """Return a decorator that registers the evaluate fn with the client app.
223
+ """Register an evaluate function with the ``ClientApp``.
224
+
225
+ Parameters
226
+ ----------
227
+ action : str (default: "default")
228
+ The action name used to route messages. Defaults to "default".
229
+ mods : Optional[list[Mod]] (default: None)
230
+ A list of function-specific modifiers.
231
+
232
+ Returns
233
+ -------
234
+ Callable[[ClientAppCallable], ClientAppCallable]
235
+ A decorator that registers an evaluate function with the ``ClientApp``.
211
236
 
212
237
  Examples
213
238
  --------
214
- Registering an evaluate function:
215
-
216
- >>> app = ClientApp()
217
- >>>
218
- >>> @app.evaluate()
219
- >>> def evaluate(message: Message, context: Context) -> Message:
220
- >>> print("ClientApp evaluation running")
221
- >>> # Create and return an echo reply message
222
- >>> return message.create_reply(content=message.content())
223
-
224
- Registering an evaluate function with a function-specific modifier:
225
-
226
- >>> from flwr.client.mod import message_size_mod
227
- >>>
228
- >>> app = ClientApp()
229
- >>>
230
- >>> @app.evaluate(mods=[message_size_mod])
231
- >>> def evaluate(message: Message, context: Context) -> Message:
232
- >>> print("ClientApp evaluation running with message size mod")
233
- >>> # Create and return an echo reply message
234
- >>> return message.create_reply(content=message.content())
235
- """
239
+ Registering an evaluate function::
236
240
 
237
- def evaluate_decorator(evaluate_fn: ClientAppCallable) -> ClientAppCallable:
238
- """Register the evaluate fn with the ServerApp object."""
239
- if self._call:
240
- raise _registration_error(MessageType.EVALUATE)
241
+ app = ClientApp()
241
242
 
242
- warn_preview_feature("ClientApp-register-evaluate-function")
243
+ @app.evaluate()
244
+ def evaluate(message: Message, context: Context) -> Message:
245
+ print("Executing default evaluate function")
246
+ # Create and return an echo reply message
247
+ return Message(message.content, reply_to=message)
243
248
 
244
- # Register provided function with the ClientApp object
245
- # Wrap mods around the wrapped step function
246
- self._evaluate = make_ffn(evaluate_fn, self._mods + (mods or []))
249
+ Registering an evaluate function with a custom action name::
247
250
 
248
- # Return provided function unmodified
249
- return evaluate_fn
251
+ app = ClientApp()
252
+
253
+ # Messages with `message_type="evaluate.custom_action"` will be
254
+ # routed to this function.
255
+ @app.evaluate("custom_action")
256
+ def custom_action(message: Message, context: Context) -> Message:
257
+ print("Executing evaluate function for custom action")
258
+ return Message(message.content, reply_to=message)
259
+
260
+ Registering an evaluate function with a function-specific Flower Mod::
261
+
262
+ from flwr.client.mod import message_size_mod
250
263
 
251
- return evaluate_decorator
264
+ app = ClientApp()
265
+
266
+ # Using the `mods` argument to apply a function-specific mod.
267
+ @app.evaluate(mods=[message_size_mod])
268
+ def evaluate(message: Message, context: Context) -> Message:
269
+ print("Executing evaluate function with message size mod")
270
+ # Create and return an echo reply message
271
+ return Message(message.content, reply_to=message)
272
+ """
273
+ return _get_decorator(self, MessageType.EVALUATE, action, mods)
252
274
 
253
275
  def query(
254
- self, mods: Optional[list[Mod]] = None
276
+ self, action: str = DEFAULT_ACTION, *, mods: Optional[list[Mod]] = None
255
277
  ) -> Callable[[ClientAppCallable], ClientAppCallable]:
256
- """Return a decorator that registers the query fn with the client app.
278
+ """Register a query function with the ``ClientApp``.
279
+
280
+ Parameters
281
+ ----------
282
+ action : str (default: "default")
283
+ The action name used to route messages. Defaults to "default".
284
+ mods : Optional[list[Mod]] (default: None)
285
+ A list of function-specific modifiers.
286
+
287
+ Returns
288
+ -------
289
+ Callable[[ClientAppCallable], ClientAppCallable]
290
+ A decorator that registers a query function with the ``ClientApp``.
257
291
 
258
292
  Examples
259
293
  --------
260
- Registering a query function:
261
-
262
- >>> app = ClientApp()
263
- >>>
264
- >>> @app.query()
265
- >>> def query(message: Message, context: Context) -> Message:
266
- >>> print("ClientApp query running")
267
- >>> # Create and return an echo reply message
268
- >>> return message.create_reply(content=message.content())
269
-
270
- Registering a query function with a function-specific modifier:
271
-
272
- >>> from flwr.client.mod import message_size_mod
273
- >>>
274
- >>> app = ClientApp()
275
- >>>
276
- >>> @app.query(mods=[message_size_mod])
277
- >>> def query(message: Message, context: Context) -> Message:
278
- >>> print("ClientApp query running with message size mod")
279
- >>> # Create and return an echo reply message
280
- >>> return message.create_reply(content=message.content())
281
- """
294
+ Registering a query function::
282
295
 
283
- def query_decorator(query_fn: ClientAppCallable) -> ClientAppCallable:
284
- """Register the query fn with the ServerApp object."""
285
- if self._call:
286
- raise _registration_error(MessageType.QUERY)
296
+ app = ClientApp()
287
297
 
288
- warn_preview_feature("ClientApp-register-query-function")
298
+ @app.query()
299
+ def query(message: Message, context: Context) -> Message:
300
+ print("Executing default query function")
301
+ # Create and return an echo reply message
302
+ return Message(message.content, reply_to=message)
289
303
 
290
- # Register provided function with the ClientApp object
291
- # Wrap mods around the wrapped step function
292
- self._query = make_ffn(query_fn, self._mods + (mods or []))
304
+ Registering a query function with a custom action name::
293
305
 
294
- # Return provided function unmodified
295
- return query_fn
306
+ app = ClientApp()
307
+
308
+ # Messages with `message_type="query.custom_action"` will be
309
+ # routed to this function.
310
+ @app.query("custom_action")
311
+ def custom_action(message: Message, context: Context) -> Message:
312
+ print("Executing query function for custom action")
313
+ return Message(message.content, reply_to=message)
314
+
315
+ Registering a query function with a function-specific Flower Mod::
316
+
317
+ from flwr.client.mod import message_size_mod
296
318
 
297
- return query_decorator
319
+ app = ClientApp()
320
+
321
+ # Using the `mods` argument to apply a function-specific mod.
322
+ @app.query(mods=[message_size_mod])
323
+ def query(message: Message, context: Context) -> Message:
324
+ print("Executing query function with message size mod")
325
+ # Create and return an echo reply message
326
+ return Message(message.content, reply_to=message)
327
+ """
328
+ return _get_decorator(self, MessageType.QUERY, action, mods)
298
329
 
299
330
  def lifespan(
300
331
  self,
@@ -308,24 +339,25 @@ class ClientApp:
308
339
 
309
340
  Examples
310
341
  --------
311
- >>> app = ClientApp()
312
- >>>
313
- >>> @app.lifespan()
314
- >>> def lifespan(context: Context) -> None:
315
- >>> # Perform initialization tasks before the app starts
316
- >>> print("Initializing ClientApp")
317
- >>>
318
- >>> yield # ClientApp is running
319
- >>>
320
- >>> # Perform cleanup tasks after the app stops
321
- >>> print("Cleaning up ClientApp")
342
+ ::
343
+
344
+ app = ClientApp()
345
+
346
+ @app.lifespan()
347
+ def lifespan(context: Context) -> None:
348
+ # Perform initialization tasks before the app starts
349
+ print("Initializing ClientApp")
350
+
351
+ yield # ClientApp is running
352
+
353
+ # Perform cleanup tasks after the app stops
354
+ print("Cleaning up ClientApp")
322
355
  """
323
356
 
324
357
  def lifespan_decorator(
325
358
  lifespan_fn: Callable[[Context], Iterator[None]]
326
359
  ) -> Callable[[Context], Iterator[None]]:
327
360
  """Register the lifespan fn with the ServerApp object."""
328
- warn_preview_feature("ClientApp-register-lifespan-function")
329
361
 
330
362
  @contextmanager
331
363
  def decorated_lifespan(context: Context) -> Iterator[None]:
@@ -365,32 +397,65 @@ class LoadClientAppError(Exception):
365
397
  """Error when trying to load `ClientApp`."""
366
398
 
367
399
 
400
+ def _get_decorator(
401
+ app: ClientApp, category: str, action: str, mods: Optional[list[Mod]]
402
+ ) -> Callable[[ClientAppCallable], ClientAppCallable]:
403
+ """Get the decorator for the given category and action."""
404
+ # pylint: disable=protected-access
405
+ if app._call:
406
+ raise _registration_error(category)
407
+
408
+ def decorator(fn: ClientAppCallable) -> ClientAppCallable:
409
+
410
+ # Check if the name is a valid Python identifier
411
+ if not action.isidentifier():
412
+ raise ValueError(
413
+ f"Cannot register {category} function with name '{action}'. "
414
+ "The name must follow Python's function naming rules."
415
+ )
416
+
417
+ # Check if the name is already registered
418
+ full_name = f"{category}.{action}" # Full name of the message type
419
+ if full_name in app._registered_funcs:
420
+ raise ValueError(
421
+ f"Cannot register {category} function with name '{action}'. "
422
+ f"A {category} function with the name '{action}' is already registered."
423
+ )
424
+
425
+ # Register provided function with the ClientApp object
426
+ app._registered_funcs[full_name] = make_ffn(fn, app._mods + (mods or []))
427
+
428
+ # Return provided function unmodified
429
+ return fn
430
+
431
+ # pylint: enable=protected-access
432
+ return decorator
433
+
434
+
368
435
  def _registration_error(fn_name: str) -> ValueError:
369
436
  return ValueError(
370
437
  f"""Use either `@app.{fn_name}()` or `client_fn`, but not both.
371
438
 
372
439
  Use the `ClientApp` with an existing `client_fn`:
373
440
 
374
- >>> class FlowerClient(NumPyClient):
375
- >>> # ...
376
- >>>
377
- >>> def client_fn(context: Context):
378
- >>> return FlowerClient().to_client()
379
- >>>
380
- >>> app = ClientApp(
381
- >>> client_fn=client_fn,
382
- >>> )
441
+ class FlowerClient(NumPyClient):
442
+ # ...
443
+
444
+ def client_fn(context: Context):
445
+ return FlowerClient().to_client()
446
+
447
+ app = ClientApp(
448
+ client_fn=client_fn,
449
+ )
383
450
 
384
451
  Use the `ClientApp` with a custom {fn_name} function:
385
452
 
386
- >>> app = ClientApp()
387
- >>>
388
- >>> @app.{fn_name}()
389
- >>> def {fn_name}(message: Message, context: Context) -> Message:
390
- >>> print("ClientApp {fn_name} running")
391
- >>> # Create and return an echo reply message
392
- >>> return message.create_reply(
393
- >>> content=message.content()
394
- >>> )
453
+ app = ClientApp()
454
+
455
+ @app.{fn_name}()
456
+ def {fn_name}(message: Message, context: Context) -> Message:
457
+ print("ClientApp {fn_name} running")
458
+ # Create and return an echo reply message
459
+ return Message(message.content, reply_to=message)
395
460
  """,
396
461
  )
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Flower Labs GmbH. All Rights Reserved.
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.