flwr 1.16.0__py3-none-any.whl → 1.17.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 (98) hide show
  1. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +1 -1
  2. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
  3. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
  4. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  5. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  6. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  7. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  8. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
  9. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  10. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
  11. flwr/cli/run/run.py +5 -9
  12. flwr/client/app.py +6 -4
  13. flwr/client/client_app.py +162 -99
  14. flwr/client/clientapp/app.py +2 -2
  15. flwr/client/grpc_client/connection.py +24 -21
  16. flwr/client/message_handler/message_handler.py +27 -27
  17. flwr/client/mod/__init__.py +2 -2
  18. flwr/client/mod/centraldp_mods.py +7 -7
  19. flwr/client/mod/comms_mods.py +16 -22
  20. flwr/client/mod/localdp_mod.py +4 -4
  21. flwr/client/mod/secure_aggregation/secaggplus_mod.py +31 -31
  22. flwr/client/run_info_store.py +2 -2
  23. flwr/common/__init__.py +12 -4
  24. flwr/common/config.py +4 -4
  25. flwr/common/constant.py +6 -6
  26. flwr/common/context.py +4 -4
  27. flwr/common/event_log_plugin/event_log_plugin.py +3 -3
  28. flwr/common/logger.py +2 -2
  29. flwr/common/message.py +327 -102
  30. flwr/common/record/__init__.py +8 -4
  31. flwr/common/record/arrayrecord.py +626 -0
  32. flwr/common/record/{configsrecord.py → configrecord.py} +75 -29
  33. flwr/common/record/conversion_utils.py +1 -1
  34. flwr/common/record/{metricsrecord.py → metricrecord.py} +78 -32
  35. flwr/common/record/recorddict.py +288 -0
  36. flwr/common/recorddict_compat.py +410 -0
  37. flwr/common/secure_aggregation/secaggplus_constants.py +1 -1
  38. flwr/common/serde.py +66 -71
  39. flwr/common/typing.py +8 -8
  40. flwr/proto/exec_pb2.py +3 -3
  41. flwr/proto/exec_pb2.pyi +3 -3
  42. flwr/proto/message_pb2.py +12 -12
  43. flwr/proto/message_pb2.pyi +9 -9
  44. flwr/proto/recorddict_pb2.py +70 -0
  45. flwr/proto/{recordset_pb2.pyi → recorddict_pb2.pyi} +35 -35
  46. flwr/proto/run_pb2.py +31 -31
  47. flwr/proto/run_pb2.pyi +3 -3
  48. flwr/server/__init__.py +3 -1
  49. flwr/server/app.py +56 -1
  50. flwr/server/compat/__init__.py +2 -2
  51. flwr/server/compat/app.py +11 -11
  52. flwr/server/compat/app_utils.py +16 -16
  53. flwr/server/compat/{driver_client_proxy.py → grid_client_proxy.py} +39 -39
  54. flwr/server/fleet_event_log_interceptor.py +94 -0
  55. flwr/server/{driver → grid}/__init__.py +8 -7
  56. flwr/server/{driver/driver.py → grid/grid.py} +47 -18
  57. flwr/server/{driver/grpc_driver.py → grid/grpc_grid.py} +87 -64
  58. flwr/server/{driver/inmemory_driver.py → grid/inmemory_grid.py} +24 -34
  59. flwr/server/run_serverapp.py +4 -4
  60. flwr/server/server_app.py +38 -18
  61. flwr/server/serverapp/app.py +10 -10
  62. flwr/server/superlink/fleet/vce/backend/backend.py +2 -2
  63. flwr/server/superlink/fleet/vce/backend/raybackend.py +2 -2
  64. flwr/server/superlink/fleet/vce/vce_api.py +1 -3
  65. flwr/server/superlink/linkstate/in_memory_linkstate.py +33 -8
  66. flwr/server/superlink/linkstate/linkstate.py +4 -4
  67. flwr/server/superlink/linkstate/sqlite_linkstate.py +61 -27
  68. flwr/server/superlink/linkstate/utils.py +93 -27
  69. flwr/server/superlink/{driver → serverappio}/__init__.py +1 -1
  70. flwr/server/superlink/{driver → serverappio}/serverappio_grpc.py +1 -1
  71. flwr/server/superlink/{driver → serverappio}/serverappio_servicer.py +4 -4
  72. flwr/server/superlink/simulation/simulationio_servicer.py +2 -2
  73. flwr/server/typing.py +3 -3
  74. flwr/server/utils/validator.py +4 -4
  75. flwr/server/workflow/default_workflows.py +48 -57
  76. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +48 -50
  77. flwr/simulation/app.py +2 -2
  78. flwr/simulation/ray_transport/ray_actor.py +4 -2
  79. flwr/simulation/ray_transport/ray_client_proxy.py +34 -32
  80. flwr/simulation/run_simulation.py +15 -15
  81. flwr/superexec/deployment.py +4 -4
  82. flwr/superexec/exec_event_log_interceptor.py +135 -0
  83. flwr/superexec/exec_grpc.py +10 -4
  84. flwr/superexec/exec_servicer.py +2 -2
  85. flwr/superexec/exec_user_auth_interceptor.py +18 -2
  86. flwr/superexec/executor.py +3 -3
  87. flwr/superexec/simulation.py +3 -3
  88. {flwr-1.16.0.dist-info → flwr-1.17.0.dist-info}/METADATA +2 -2
  89. {flwr-1.16.0.dist-info → flwr-1.17.0.dist-info}/RECORD +94 -92
  90. flwr/common/record/parametersrecord.py +0 -339
  91. flwr/common/record/recordset.py +0 -209
  92. flwr/common/recordset_compat.py +0 -418
  93. flwr/proto/recordset_pb2.py +0 -70
  94. /flwr/proto/{recordset_pb2_grpc.py → recorddict_pb2_grpc.py} +0 -0
  95. /flwr/proto/{recordset_pb2_grpc.pyi → recorddict_pb2_grpc.pyi} +0 -0
  96. {flwr-1.16.0.dist-info → flwr-1.17.0.dist-info}/LICENSE +0 -0
  97. {flwr-1.16.0.dist-info → flwr-1.17.0.dist-info}/WHEEL +0 -0
  98. {flwr-1.16.0.dist-info → flwr-1.17.0.dist-info}/entry_points.txt +0 -0
@@ -35,7 +35,7 @@ warnings.filterwarnings("ignore", category=UserWarning)
35
35
  # pylint: disable=too-many-arguments
36
36
  # pylint: disable=too-many-instance-attributes
37
37
  class FlowerClient(NumPyClient):
38
- """Standard Flower client for CNN training."""
38
+ """Flower client for LLM fine-tuning."""
39
39
 
40
40
  def __init__(
41
41
  self,
@@ -8,7 +8,7 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.16.0",
11
+ "flwr[simulation]>=1.17.0",
12
12
  "flwr-datasets[vision]>=0.5.0",
13
13
  "torch==2.5.1",
14
14
  "torchvision==0.20.1",
@@ -8,7 +8,7 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.16.0",
11
+ "flwr[simulation]>=1.17.0",
12
12
  "flwr-datasets>=0.5.0",
13
13
  "torch==2.3.1",
14
14
  "trl==0.8.1",
@@ -8,7 +8,7 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.16.0",
11
+ "flwr[simulation]>=1.17.0",
12
12
  "flwr-datasets>=0.5.0",
13
13
  "torch==2.5.1",
14
14
  "transformers>=4.30.0,<5.0",
@@ -8,7 +8,7 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.16.0",
11
+ "flwr[simulation]>=1.17.0",
12
12
  "jax==0.4.30",
13
13
  "jaxlib==0.4.30",
14
14
  "scikit-learn==1.6.1",
@@ -8,7 +8,7 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.16.0",
11
+ "flwr[simulation]>=1.17.0",
12
12
  "flwr-datasets[vision]>=0.5.0",
13
13
  "mlx==0.21.1",
14
14
  ]
@@ -8,7 +8,7 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.16.0",
11
+ "flwr[simulation]>=1.17.0",
12
12
  "numpy>=2.0.2",
13
13
  ]
14
14
 
@@ -8,7 +8,7 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.16.0",
11
+ "flwr[simulation]>=1.17.0",
12
12
  "flwr-datasets[vision]>=0.5.0",
13
13
  "torch==2.5.1",
14
14
  "torchvision==0.20.1",
@@ -8,7 +8,7 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.16.0",
11
+ "flwr[simulation]>=1.17.0",
12
12
  "flwr-datasets[vision]>=0.5.0",
13
13
  "scikit-learn>=1.6.1",
14
14
  ]
@@ -8,7 +8,7 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.16.0",
11
+ "flwr[simulation]>=1.17.0",
12
12
  "flwr-datasets[vision]>=0.5.0",
13
13
  "tensorflow>=2.11.1,<2.18.0",
14
14
  ]
flwr/cli/run/run.py CHANGED
@@ -35,15 +35,11 @@ from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
35
35
  from flwr.common.config import (
36
36
  flatten_dict,
37
37
  parse_config_args,
38
- user_config_to_configsrecord,
38
+ user_config_to_configrecord,
39
39
  )
40
40
  from flwr.common.constant import CliOutputFormat
41
41
  from flwr.common.logger import print_json_error, redirect_output, restore_output
42
- from flwr.common.serde import (
43
- configs_record_to_proto,
44
- fab_to_proto,
45
- user_config_to_proto,
46
- )
42
+ from flwr.common.serde import config_record_to_proto, fab_to_proto, user_config_to_proto
47
43
  from flwr.common.typing import Fab
48
44
  from flwr.proto.exec_pb2 import StartRunRequest # pylint: disable=E0611
49
45
  from flwr.proto.exec_pb2_grpc import ExecStub
@@ -171,14 +167,14 @@ def _run_with_exec_api(
171
167
 
172
168
  fab = Fab(fab_hash, content)
173
169
 
174
- # Construct a `ConfigsRecord` out of a flattened `UserConfig`
170
+ # Construct a `ConfigRecord` out of a flattened `UserConfig`
175
171
  fed_conf = flatten_dict(federation_config.get("options", {}))
176
- c_record = user_config_to_configsrecord(fed_conf)
172
+ c_record = user_config_to_configrecord(fed_conf)
177
173
 
178
174
  req = StartRunRequest(
179
175
  fab=fab_to_proto(fab),
180
176
  override_config=user_config_to_proto(parse_config_args(config_overrides)),
181
- federation_options=configs_record_to_proto(c_record),
177
+ federation_options=config_record_to_proto(c_record),
182
178
  )
183
179
  with unauthenticated_exc_handler():
184
180
  res = stub.StartRun(req)
flwr/client/app.py CHANGED
@@ -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
flwr/client/client_app.py CHANGED
@@ -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(
@@ -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,27 +143,41 @@ 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
  --------
@@ -172,42 +187,52 @@ class ClientApp:
172
187
  >>>
173
188
  >>> @app.train()
174
189
  >>> 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())
190
+ >>> print("Executing default train function")
191
+ >>> # Create and return an echo reply message
192
+ >>> return Message(message.content, reply_to=message)
178
193
 
179
- Registering a train function with a function-specific modifier:
194
+ Registering a train function with a custom action name:
195
+
196
+ >>> app = ClientApp()
197
+ >>>
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:
180
206
 
181
207
  >>> from flwr.client.mod import message_size_mod
182
208
  >>>
183
209
  >>> app = ClientApp()
184
210
  >>>
211
+ >>> # Using the `mods` argument to apply a function-specific mod.
185
212
  >>> @app.train(mods=[message_size_mod])
186
213
  >>> def train(message: Message, context: Context) -> Message:
187
- >>> print("ClientApp training running with message size mod")
188
- >>> return message.create_reply(content=message.content())
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)
189
217
  """
190
-
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)
195
-
196
- warn_preview_feature("ClientApp-register-train-function")
197
-
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 []))
201
-
202
- # Return provided function unmodified
203
- return train_fn
204
-
205
- return train_decorator
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
  --------
@@ -217,43 +242,52 @@ class ClientApp:
217
242
  >>>
218
243
  >>> @app.evaluate()
219
244
  >>> 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())
245
+ >>> print("Executing default evaluate function")
246
+ >>> # Create and return an echo reply message
247
+ >>> return Message(message.content, reply_to=message)
223
248
 
224
- Registering an evaluate function with a function-specific modifier:
249
+ Registering an evaluate function with a custom action name:
250
+
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:
225
261
 
226
262
  >>> from flwr.client.mod import message_size_mod
227
263
  >>>
228
264
  >>> app = ClientApp()
229
265
  >>>
266
+ >>> # Using the `mods` argument to apply a function-specific mod.
230
267
  >>> @app.evaluate(mods=[message_size_mod])
231
268
  >>> 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())
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)
235
272
  """
236
-
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
-
242
- warn_preview_feature("ClientApp-register-evaluate-function")
243
-
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 []))
247
-
248
- # Return provided function unmodified
249
- return evaluate_fn
250
-
251
- return evaluate_decorator
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
  --------
@@ -263,38 +297,35 @@ class ClientApp:
263
297
  >>>
264
298
  >>> @app.query()
265
299
  >>> 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())
300
+ >>> print("Executing default query function")
301
+ >>> # Create and return an echo reply message
302
+ >>> return Message(message.content, reply_to=message)
303
+
304
+ Registering a query function with a custom action name:
269
305
 
270
- Registering a query function with a function-specific modifier:
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:
271
316
 
272
317
  >>> from flwr.client.mod import message_size_mod
273
318
  >>>
274
319
  >>> app = ClientApp()
275
320
  >>>
321
+ >>> # Using the `mods` argument to apply a function-specific mod.
276
322
  >>> @app.query(mods=[message_size_mod])
277
323
  >>> 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())
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)
281
327
  """
282
-
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)
287
-
288
- warn_preview_feature("ClientApp-register-query-function")
289
-
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 []))
293
-
294
- # Return provided function unmodified
295
- return query_fn
296
-
297
- return query_decorator
328
+ return _get_decorator(self, MessageType.QUERY, action, mods)
298
329
 
299
330
  def lifespan(
300
331
  self,
@@ -325,7 +356,6 @@ class ClientApp:
325
356
  lifespan_fn: Callable[[Context], Iterator[None]]
326
357
  ) -> Callable[[Context], Iterator[None]]:
327
358
  """Register the lifespan fn with the ServerApp object."""
328
- warn_preview_feature("ClientApp-register-lifespan-function")
329
359
 
330
360
  @contextmanager
331
361
  def decorated_lifespan(context: Context) -> Iterator[None]:
@@ -365,6 +395,41 @@ class LoadClientAppError(Exception):
365
395
  """Error when trying to load `ClientApp`."""
366
396
 
367
397
 
398
+ def _get_decorator(
399
+ app: ClientApp, category: str, action: str, mods: Optional[list[Mod]]
400
+ ) -> Callable[[ClientAppCallable], ClientAppCallable]:
401
+ """Get the decorator for the given category and action."""
402
+ # pylint: disable=protected-access
403
+ if app._call:
404
+ raise _registration_error(category)
405
+
406
+ def decorator(fn: ClientAppCallable) -> ClientAppCallable:
407
+
408
+ # Check if the name is a valid Python identifier
409
+ if not action.isidentifier():
410
+ raise ValueError(
411
+ f"Cannot register {category} function with name '{action}'. "
412
+ "The name must follow Python's function naming rules."
413
+ )
414
+
415
+ # Check if the name is already registered
416
+ full_name = f"{category}.{action}" # Full name of the message type
417
+ if full_name in app._registered_funcs:
418
+ raise ValueError(
419
+ f"Cannot register {category} function with name '{action}'. "
420
+ f"A {category} function with the name '{action}' is already registered."
421
+ )
422
+
423
+ # Register provided function with the ClientApp object
424
+ app._registered_funcs[full_name] = make_ffn(fn, app._mods + (mods or []))
425
+
426
+ # Return provided function unmodified
427
+ return fn
428
+
429
+ # pylint: enable=protected-access
430
+ return decorator
431
+
432
+
368
433
  def _registration_error(fn_name: str) -> ValueError:
369
434
  return ValueError(
370
435
  f"""Use either `@app.{fn_name}()` or `client_fn`, but not both.
@@ -389,8 +454,6 @@ def _registration_error(fn_name: str) -> ValueError:
389
454
  >>> def {fn_name}(message: Message, context: Context) -> Message:
390
455
  >>> print("ClientApp {fn_name} running")
391
456
  >>> # Create and return an echo reply message
392
- >>> return message.create_reply(
393
- >>> content=message.content()
394
- >>> )
457
+ >>> return Message(message.content, reply_to=message)
395
458
  """,
396
459
  )
@@ -152,8 +152,8 @@ def run_clientapp( # pylint: disable=R0914
152
152
  log(ERROR, "%s raised an exception", exc_entity, exc_info=ex)
153
153
 
154
154
  # Create error message
155
- reply_message = message.create_error_reply(
156
- error=Error(code=e_code, reason=reason)
155
+ reply_message = Message(
156
+ Error(code=e_code, reason=reason), reply_to=message
157
157
  )
158
158
 
159
159
  # Push Message and Context to SuperNode