flwr 1.15.2__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 (120) hide show
  1. flwr/cli/build.py +2 -0
  2. flwr/cli/log.py +20 -21
  3. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +1 -1
  4. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
  5. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
  6. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  7. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  8. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  9. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  10. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
  11. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  12. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
  13. flwr/cli/run/run.py +5 -9
  14. flwr/client/app.py +6 -4
  15. flwr/client/client_app.py +260 -86
  16. flwr/client/clientapp/app.py +6 -2
  17. flwr/client/grpc_client/connection.py +24 -21
  18. flwr/client/message_handler/message_handler.py +28 -28
  19. flwr/client/mod/__init__.py +2 -2
  20. flwr/client/mod/centraldp_mods.py +7 -7
  21. flwr/client/mod/comms_mods.py +16 -22
  22. flwr/client/mod/localdp_mod.py +4 -4
  23. flwr/client/mod/secure_aggregation/secaggplus_mod.py +31 -31
  24. flwr/client/rest_client/connection.py +4 -6
  25. flwr/client/run_info_store.py +2 -2
  26. flwr/client/supernode/__init__.py +0 -2
  27. flwr/client/supernode/app.py +1 -11
  28. flwr/common/__init__.py +12 -4
  29. flwr/common/address.py +35 -0
  30. flwr/common/args.py +8 -2
  31. flwr/common/auth_plugin/auth_plugin.py +2 -1
  32. flwr/common/config.py +4 -4
  33. flwr/common/constant.py +16 -0
  34. flwr/common/context.py +4 -4
  35. flwr/common/event_log_plugin/__init__.py +22 -0
  36. flwr/common/event_log_plugin/event_log_plugin.py +60 -0
  37. flwr/common/grpc.py +1 -1
  38. flwr/common/logger.py +2 -2
  39. flwr/common/message.py +338 -102
  40. flwr/common/object_ref.py +0 -10
  41. flwr/common/record/__init__.py +8 -4
  42. flwr/common/record/arrayrecord.py +626 -0
  43. flwr/common/record/{configsrecord.py → configrecord.py} +75 -29
  44. flwr/common/record/conversion_utils.py +9 -18
  45. flwr/common/record/{metricsrecord.py → metricrecord.py} +78 -32
  46. flwr/common/record/recorddict.py +288 -0
  47. flwr/common/recorddict_compat.py +410 -0
  48. flwr/common/secure_aggregation/quantization.py +5 -1
  49. flwr/common/secure_aggregation/secaggplus_constants.py +1 -1
  50. flwr/common/serde.py +67 -190
  51. flwr/common/telemetry.py +0 -10
  52. flwr/common/typing.py +44 -8
  53. flwr/proto/exec_pb2.py +3 -3
  54. flwr/proto/exec_pb2.pyi +3 -3
  55. flwr/proto/message_pb2.py +12 -12
  56. flwr/proto/message_pb2.pyi +9 -9
  57. flwr/proto/recorddict_pb2.py +70 -0
  58. flwr/proto/{recordset_pb2.pyi → recorddict_pb2.pyi} +35 -35
  59. flwr/proto/run_pb2.py +31 -31
  60. flwr/proto/run_pb2.pyi +3 -3
  61. flwr/server/__init__.py +3 -1
  62. flwr/server/app.py +74 -3
  63. flwr/server/compat/__init__.py +2 -2
  64. flwr/server/compat/app.py +15 -12
  65. flwr/server/compat/app_utils.py +26 -18
  66. flwr/server/compat/{driver_client_proxy.py → grid_client_proxy.py} +41 -41
  67. flwr/server/fleet_event_log_interceptor.py +94 -0
  68. flwr/server/{driver → grid}/__init__.py +8 -7
  69. flwr/server/{driver/driver.py → grid/grid.py} +48 -19
  70. flwr/server/{driver/grpc_driver.py → grid/grpc_grid.py} +88 -56
  71. flwr/server/{driver/inmemory_driver.py → grid/inmemory_grid.py} +41 -54
  72. flwr/server/run_serverapp.py +6 -17
  73. flwr/server/server_app.py +126 -33
  74. flwr/server/serverapp/app.py +10 -10
  75. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +2 -2
  76. flwr/server/superlink/fleet/message_handler/message_handler.py +8 -12
  77. flwr/server/superlink/fleet/vce/backend/backend.py +3 -3
  78. flwr/server/superlink/fleet/vce/backend/raybackend.py +2 -2
  79. flwr/server/superlink/fleet/vce/vce_api.py +33 -38
  80. flwr/server/superlink/linkstate/in_memory_linkstate.py +171 -132
  81. flwr/server/superlink/linkstate/linkstate.py +51 -64
  82. flwr/server/superlink/linkstate/sqlite_linkstate.py +253 -285
  83. flwr/server/superlink/linkstate/utils.py +171 -133
  84. flwr/server/superlink/{driver → serverappio}/__init__.py +1 -1
  85. flwr/server/superlink/{driver → serverappio}/serverappio_grpc.py +1 -1
  86. flwr/server/superlink/{driver → serverappio}/serverappio_servicer.py +27 -29
  87. flwr/server/superlink/simulation/simulationio_servicer.py +2 -2
  88. flwr/server/typing.py +3 -3
  89. flwr/server/utils/__init__.py +2 -2
  90. flwr/server/utils/validator.py +53 -68
  91. flwr/server/workflow/default_workflows.py +52 -58
  92. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +48 -50
  93. flwr/simulation/app.py +2 -2
  94. flwr/simulation/ray_transport/ray_actor.py +4 -2
  95. flwr/simulation/ray_transport/ray_client_proxy.py +34 -32
  96. flwr/simulation/run_simulation.py +15 -15
  97. flwr/superexec/app.py +0 -14
  98. flwr/superexec/deployment.py +4 -4
  99. flwr/superexec/exec_event_log_interceptor.py +135 -0
  100. flwr/superexec/exec_grpc.py +10 -4
  101. flwr/superexec/exec_servicer.py +6 -6
  102. flwr/superexec/exec_user_auth_interceptor.py +22 -4
  103. flwr/superexec/executor.py +3 -3
  104. flwr/superexec/simulation.py +3 -3
  105. {flwr-1.15.2.dist-info → flwr-1.17.0.dist-info}/METADATA +5 -5
  106. {flwr-1.15.2.dist-info → flwr-1.17.0.dist-info}/RECORD +111 -112
  107. {flwr-1.15.2.dist-info → flwr-1.17.0.dist-info}/entry_points.txt +0 -3
  108. flwr/client/message_handler/task_handler.py +0 -37
  109. flwr/common/record/parametersrecord.py +0 -204
  110. flwr/common/record/recordset.py +0 -202
  111. flwr/common/recordset_compat.py +0 -418
  112. flwr/proto/recordset_pb2.py +0 -70
  113. flwr/proto/task_pb2.py +0 -33
  114. flwr/proto/task_pb2.pyi +0 -100
  115. flwr/proto/task_pb2_grpc.py +0 -4
  116. flwr/proto/task_pb2_grpc.pyi +0 -4
  117. /flwr/proto/{recordset_pb2_grpc.py → recorddict_pb2_grpc.py} +0 -0
  118. /flwr/proto/{recordset_pb2_grpc.pyi → recorddict_pb2_grpc.pyi} +0 -0
  119. {flwr-1.15.2.dist-info → flwr-1.17.0.dist-info}/LICENSE +0 -0
  120. {flwr-1.15.2.dist-info → flwr-1.17.0.dist-info}/WHEEL +0 -0
flwr/client/client_app.py CHANGED
@@ -16,6 +16,8 @@
16
16
 
17
17
 
18
18
  import inspect
19
+ from collections.abc import Iterator
20
+ from contextlib import contextmanager
19
21
  from typing import Callable, Optional
20
22
 
21
23
  from flwr.client.client import Client
@@ -25,10 +27,13 @@ from flwr.client.message_handler.message_handler import (
25
27
  from flwr.client.mod.utils import make_ffn
26
28
  from flwr.client.typing import ClientFnExt, Mod
27
29
  from flwr.common import Context, Message, MessageType
28
- 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
29
32
 
30
33
  from .typing import ClientAppCallable
31
34
 
35
+ DEFAULT_ACTION = "default"
36
+
32
37
 
33
38
  def _alert_erroneous_client_fn() -> None:
34
39
  raise ValueError(
@@ -71,6 +76,11 @@ def _inspect_maybe_adapt_client_fn_signature(client_fn: ClientFnExt) -> ClientFn
71
76
  return client_fn
72
77
 
73
78
 
79
+ @contextmanager
80
+ def _empty_lifespan(_: Context) -> Iterator[None]:
81
+ yield
82
+
83
+
74
84
  class ClientAppException(Exception):
75
85
  """Exception raised when an exception is raised while executing a ClientApp."""
76
86
 
@@ -95,15 +105,6 @@ class ClientApp:
95
105
  >>> return FlowerClient().to_client()
96
106
  >>>
97
107
  >>> app = ClientApp(client_fn)
98
-
99
- If the above code is in a Python module called `client`, it can be started as
100
- follows:
101
-
102
- >>> flower-client-app client:app --insecure
103
-
104
- In this `client:app` example, `client` refers to the Python module `client.py` in
105
- which the previous code lives in and `app` refers to the global attribute `app` that
106
- points to an object of type `ClientApp`.
107
108
  """
108
109
 
109
110
  def __init__(
@@ -112,6 +113,7 @@ class ClientApp:
112
113
  mods: Optional[list[Mod]] = None,
113
114
  ) -> None:
114
115
  self._mods: list[Mod] = mods if mods is not None else []
116
+ self._registered_funcs: dict[str, ClientAppCallable] = {}
115
117
 
116
118
  # Create wrapper function for `handle`
117
119
  self._call: Optional[ClientAppCallable] = None
@@ -131,129 +133,303 @@ class ClientApp:
131
133
  # Wrap mods around the wrapped handle function
132
134
  self._call = make_ffn(ffn, mods if mods is not None else [])
133
135
 
134
- # Step functions
135
- self._train: Optional[ClientAppCallable] = None
136
- self._evaluate: Optional[ClientAppCallable] = None
137
- self._query: Optional[ClientAppCallable] = None
136
+ # Lifespan function
137
+ self._lifespan = _empty_lifespan
138
138
 
139
139
  def __call__(self, message: Message, context: Context) -> Message:
140
140
  """Execute `ClientApp`."""
141
- # Execute message using `client_fn`
142
- if self._call:
143
- return self._call(message, context)
144
-
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}")
161
-
162
- def train(self) -> Callable[[ClientAppCallable], ClientAppCallable]:
163
- """Return a decorator that registers the train fn with the client app.
141
+ with self._lifespan(context):
142
+ # Execute message using `client_fn`
143
+ if self._call:
144
+ return self._call(message, context)
145
+
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}'")
164
+
165
+ def train(
166
+ self, action: str = DEFAULT_ACTION, *, mods: Optional[list[Mod]] = None
167
+ ) -> Callable[[ClientAppCallable], ClientAppCallable]:
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``.
164
181
 
165
182
  Examples
166
183
  --------
184
+ Registering a train function:
185
+
167
186
  >>> app = ClientApp()
168
187
  >>>
169
188
  >>> @app.train()
170
189
  >>> def train(message: Message, context: Context) -> Message:
171
- >>> print("ClientApp training running")
172
- >>> # Create and return an echo reply message
173
- >>> return message.create_reply(content=message.content())
174
- """
190
+ >>> print("Executing default train function")
191
+ >>> # Create and return an echo reply message
192
+ >>> return Message(message.content, reply_to=message)
175
193
 
176
- def train_decorator(train_fn: ClientAppCallable) -> ClientAppCallable:
177
- """Register the train fn with the ServerApp object."""
178
- if self._call:
179
- raise _registration_error(MessageType.TRAIN)
194
+ Registering a train function with a custom action name:
180
195
 
181
- warn_preview_feature("ClientApp-register-train-function")
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)
182
204
 
183
- # Register provided function with the ClientApp object
184
- # Wrap mods around the wrapped step function
185
- self._train = make_ffn(train_fn, self._mods)
205
+ Registering a train function with a function-specific Flower Mod:
186
206
 
187
- # Return provided function unmodified
188
- return train_fn
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)
219
+
220
+ def evaluate(
221
+ self, action: str = DEFAULT_ACTION, *, mods: Optional[list[Mod]] = None
222
+ ) -> Callable[[ClientAppCallable], ClientAppCallable]:
223
+ """Register an evaluate function with the ``ClientApp``.
189
224
 
190
- return train_decorator
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.
191
231
 
192
- def evaluate(self) -> Callable[[ClientAppCallable], ClientAppCallable]:
193
- """Return a decorator that registers the evaluate fn with the client app.
232
+ Returns
233
+ -------
234
+ Callable[[ClientAppCallable], ClientAppCallable]
235
+ A decorator that registers an evaluate function with the ``ClientApp``.
194
236
 
195
237
  Examples
196
238
  --------
239
+ Registering an evaluate function:
240
+
197
241
  >>> app = ClientApp()
198
242
  >>>
199
243
  >>> @app.evaluate()
200
244
  >>> def evaluate(message: Message, context: Context) -> Message:
201
- >>> print("ClientApp evaluation running")
202
- >>> # Create and return an echo reply message
203
- >>> return message.create_reply(content=message.content())
204
- """
245
+ >>> print("Executing default evaluate function")
246
+ >>> # Create and return an echo reply message
247
+ >>> return Message(message.content, reply_to=message)
205
248
 
206
- def evaluate_decorator(evaluate_fn: ClientAppCallable) -> ClientAppCallable:
207
- """Register the evaluate fn with the ServerApp object."""
208
- if self._call:
209
- raise _registration_error(MessageType.EVALUATE)
249
+ Registering an evaluate function with a custom action name:
210
250
 
211
- warn_preview_feature("ClientApp-register-evaluate-function")
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)
212
259
 
213
- # Register provided function with the ClientApp object
214
- # Wrap mods around the wrapped step function
215
- self._evaluate = make_ffn(evaluate_fn, self._mods)
260
+ Registering an evaluate function with a function-specific Flower Mod:
216
261
 
217
- # Return provided function unmodified
218
- return evaluate_fn
262
+ >>> from flwr.client.mod import message_size_mod
263
+ >>>
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)
274
+
275
+ def query(
276
+ self, action: str = DEFAULT_ACTION, *, mods: Optional[list[Mod]] = None
277
+ ) -> Callable[[ClientAppCallable], ClientAppCallable]:
278
+ """Register a query function with the ``ClientApp``.
219
279
 
220
- return evaluate_decorator
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.
221
286
 
222
- def query(self) -> Callable[[ClientAppCallable], ClientAppCallable]:
223
- """Return a decorator that registers the query fn with the client app.
287
+ Returns
288
+ -------
289
+ Callable[[ClientAppCallable], ClientAppCallable]
290
+ A decorator that registers a query function with the ``ClientApp``.
224
291
 
225
292
  Examples
226
293
  --------
294
+ Registering a query function:
295
+
227
296
  >>> app = ClientApp()
228
297
  >>>
229
298
  >>> @app.query()
230
299
  >>> def query(message: Message, context: Context) -> Message:
231
- >>> print("ClientApp query running")
232
- >>> # Create and return an echo reply message
233
- >>> 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:
305
+
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
318
+ >>>
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)
234
327
  """
328
+ return _get_decorator(self, MessageType.QUERY, action, mods)
235
329
 
236
- def query_decorator(query_fn: ClientAppCallable) -> ClientAppCallable:
237
- """Register the query fn with the ServerApp object."""
238
- if self._call:
239
- raise _registration_error(MessageType.QUERY)
330
+ def lifespan(
331
+ self,
332
+ ) -> Callable[
333
+ [Callable[[Context], Iterator[None]]], Callable[[Context], Iterator[None]]
334
+ ]:
335
+ """Return a decorator that registers the lifespan fn with the client app.
336
+
337
+ The decorated function should accept a `Context` object and use `yield`
338
+ to define enter and exit behavior.
339
+
340
+ Examples
341
+ --------
342
+ >>> app = ClientApp()
343
+ >>>
344
+ >>> @app.lifespan()
345
+ >>> def lifespan(context: Context) -> None:
346
+ >>> # Perform initialization tasks before the app starts
347
+ >>> print("Initializing ClientApp")
348
+ >>>
349
+ >>> yield # ClientApp is running
350
+ >>>
351
+ >>> # Perform cleanup tasks after the app stops
352
+ >>> print("Cleaning up ClientApp")
353
+ """
240
354
 
241
- warn_preview_feature("ClientApp-register-query-function")
355
+ def lifespan_decorator(
356
+ lifespan_fn: Callable[[Context], Iterator[None]]
357
+ ) -> Callable[[Context], Iterator[None]]:
358
+ """Register the lifespan fn with the ServerApp object."""
359
+
360
+ @contextmanager
361
+ def decorated_lifespan(context: Context) -> Iterator[None]:
362
+ # Execute the code before `yield` in lifespan_fn
363
+ try:
364
+ if not isinstance(it := lifespan_fn(context), Iterator):
365
+ raise StopIteration
366
+ next(it)
367
+ except StopIteration:
368
+ raise RuntimeError(
369
+ "lifespan function should yield at least once."
370
+ ) from None
371
+
372
+ try:
373
+ # Enter the context
374
+ yield
375
+ finally:
376
+ try:
377
+ # Execute the code after `yield` in lifespan_fn
378
+ next(it)
379
+ except StopIteration:
380
+ pass
381
+ else:
382
+ raise RuntimeError("lifespan function should only yield once.")
242
383
 
243
384
  # Register provided function with the ClientApp object
244
- # Wrap mods around the wrapped step function
245
- self._query = make_ffn(query_fn, self._mods)
385
+ # Ignore mypy error because of different argument names (`_` vs `context`)
386
+ self._lifespan = decorated_lifespan # type: ignore
246
387
 
247
388
  # Return provided function unmodified
248
- return query_fn
389
+ return lifespan_fn
249
390
 
250
- return query_decorator
391
+ return lifespan_decorator
251
392
 
252
393
 
253
394
  class LoadClientAppError(Exception):
254
395
  """Error when trying to load `ClientApp`."""
255
396
 
256
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
+
257
433
  def _registration_error(fn_name: str) -> ValueError:
258
434
  return ValueError(
259
435
  f"""Use either `@app.{fn_name}()` or `client_fn`, but not both.
@@ -278,8 +454,6 @@ def _registration_error(fn_name: str) -> ValueError:
278
454
  >>> def {fn_name}(message: Message, context: Context) -> Message:
279
455
  >>> print("ClientApp {fn_name} running")
280
456
  >>> # Create and return an echo reply message
281
- >>> return message.create_reply(
282
- >>> content=message.content()
283
- >>> )
457
+ >>> return Message(message.content, reply_to=message)
284
458
  """,
285
459
  )
@@ -16,6 +16,7 @@
16
16
 
17
17
 
18
18
  import argparse
19
+ import gc
19
20
  import time
20
21
  from logging import DEBUG, ERROR, INFO
21
22
  from typing import Optional
@@ -151,8 +152,8 @@ def run_clientapp( # pylint: disable=R0914
151
152
  log(ERROR, "%s raised an exception", exc_entity, exc_info=ex)
152
153
 
153
154
  # Create error message
154
- reply_message = message.create_error_reply(
155
- error=Error(code=e_code, reason=reason)
155
+ reply_message = Message(
156
+ Error(code=e_code, reason=reason), reply_to=message
156
157
  )
157
158
 
158
159
  # Push Message and Context to SuperNode
@@ -160,6 +161,9 @@ def run_clientapp( # pylint: disable=R0914
160
161
  stub=stub, token=token, message=reply_message, context=context
161
162
  )
162
163
 
164
+ del client_app, message, context, run, fab, reply_message
165
+ gc.collect()
166
+
163
167
  # Reset token to `None` to prevent flwr-clientapp from trying to pull the
164
168
  # same inputs again
165
169
  token = None
@@ -28,16 +28,18 @@ from cryptography.hazmat.primitives.asymmetric import ec
28
28
  from flwr.common import (
29
29
  DEFAULT_TTL,
30
30
  GRPC_MAX_MESSAGE_LENGTH,
31
- ConfigsRecord,
31
+ ConfigRecord,
32
32
  Message,
33
33
  Metadata,
34
- RecordSet,
34
+ RecordDict,
35
+ now,
35
36
  )
36
- from flwr.common import recordset_compat as compat
37
+ from flwr.common import recorddict_compat as compat
37
38
  from flwr.common import serde
38
39
  from flwr.common.constant import MessageType, MessageTypeLegacy
39
40
  from flwr.common.grpc import create_channel, on_channel_state_change
40
41
  from flwr.common.logger import log
42
+ from flwr.common.message import make_message
41
43
  from flwr.common.retry_invoker import RetryInvoker
42
44
  from flwr.common.typing import Fab, Run
43
45
  from flwr.proto.transport_pb2 import ( # pylint: disable=E0611
@@ -139,32 +141,32 @@ def grpc_connection( # pylint: disable=R0913,R0915,too-many-positional-argument
139
141
  # Receive ServerMessage proto
140
142
  proto = next(server_message_iterator)
141
143
 
142
- # ServerMessage proto --> *Ins --> RecordSet
144
+ # ServerMessage proto --> *Ins --> RecordDict
143
145
  field = proto.WhichOneof("msg")
144
146
  message_type = ""
145
147
  if field == "get_properties_ins":
146
- recordset = compat.getpropertiesins_to_recordset(
148
+ recorddict = compat.getpropertiesins_to_recorddict(
147
149
  serde.get_properties_ins_from_proto(proto.get_properties_ins)
148
150
  )
149
151
  message_type = MessageTypeLegacy.GET_PROPERTIES
150
152
  elif field == "get_parameters_ins":
151
- recordset = compat.getparametersins_to_recordset(
153
+ recorddict = compat.getparametersins_to_recorddict(
152
154
  serde.get_parameters_ins_from_proto(proto.get_parameters_ins)
153
155
  )
154
156
  message_type = MessageTypeLegacy.GET_PARAMETERS
155
157
  elif field == "fit_ins":
156
- recordset = compat.fitins_to_recordset(
158
+ recorddict = compat.fitins_to_recorddict(
157
159
  serde.fit_ins_from_proto(proto.fit_ins), False
158
160
  )
159
161
  message_type = MessageType.TRAIN
160
162
  elif field == "evaluate_ins":
161
- recordset = compat.evaluateins_to_recordset(
163
+ recorddict = compat.evaluateins_to_recorddict(
162
164
  serde.evaluate_ins_from_proto(proto.evaluate_ins), False
163
165
  )
164
166
  message_type = MessageType.EVALUATE
165
167
  elif field == "reconnect_ins":
166
- recordset = RecordSet()
167
- recordset.configs_records["config"] = ConfigsRecord(
168
+ recorddict = RecordDict()
169
+ recorddict.config_records["config"] = ConfigRecord(
168
170
  {"seconds": proto.reconnect_ins.seconds}
169
171
  )
170
172
  message_type = "reconnect"
@@ -175,45 +177,46 @@ def grpc_connection( # pylint: disable=R0913,R0915,too-many-positional-argument
175
177
  )
176
178
 
177
179
  # Construct Message
178
- return Message(
180
+ return make_message(
179
181
  metadata=Metadata(
180
182
  run_id=0,
181
183
  message_id=str(uuid.uuid4()),
182
184
  src_node_id=0,
183
185
  dst_node_id=0,
184
- reply_to_message="",
186
+ reply_to_message_id="",
185
187
  group_id="",
188
+ created_at=now().timestamp(),
186
189
  ttl=DEFAULT_TTL,
187
190
  message_type=message_type,
188
191
  ),
189
- content=recordset,
192
+ content=recorddict,
190
193
  )
191
194
 
192
195
  def send(message: Message) -> None:
193
- # Retrieve RecordSet and message_type
194
- recordset = message.content
196
+ # Retrieve RecordDict and message_type
197
+ recorddict = message.content
195
198
  message_type = message.metadata.message_type
196
199
 
197
- # RecordSet --> *Res --> *Res proto -> ClientMessage proto
200
+ # RecordDict --> *Res --> *Res proto -> ClientMessage proto
198
201
  if message_type == MessageTypeLegacy.GET_PROPERTIES:
199
- getpropres = compat.recordset_to_getpropertiesres(recordset)
202
+ getpropres = compat.recorddict_to_getpropertiesres(recorddict)
200
203
  msg_proto = ClientMessage(
201
204
  get_properties_res=serde.get_properties_res_to_proto(getpropres)
202
205
  )
203
206
  elif message_type == MessageTypeLegacy.GET_PARAMETERS:
204
- getparamres = compat.recordset_to_getparametersres(recordset, False)
207
+ getparamres = compat.recorddict_to_getparametersres(recorddict, False)
205
208
  msg_proto = ClientMessage(
206
209
  get_parameters_res=serde.get_parameters_res_to_proto(getparamres)
207
210
  )
208
211
  elif message_type == MessageType.TRAIN:
209
- fitres = compat.recordset_to_fitres(recordset, False)
212
+ fitres = compat.recorddict_to_fitres(recorddict, False)
210
213
  msg_proto = ClientMessage(fit_res=serde.fit_res_to_proto(fitres))
211
214
  elif message_type == MessageType.EVALUATE:
212
- evalres = compat.recordset_to_evaluateres(recordset)
215
+ evalres = compat.recorddict_to_evaluateres(recorddict)
213
216
  msg_proto = ClientMessage(evaluate_res=serde.evaluate_res_to_proto(evalres))
214
217
  elif message_type == "reconnect":
215
218
  reason = cast(
216
- Reason.ValueType, recordset.configs_records["config"]["reason"]
219
+ Reason.ValueType, recorddict.config_records["config"]["reason"]
217
220
  )
218
221
  msg_proto = ClientMessage(
219
222
  disconnect_res=ClientMessage.DisconnectRes(reason=reason)