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/common/message.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.
@@ -17,19 +17,47 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- import time
21
20
  from logging import WARNING
22
- from typing import Optional, cast
21
+ from typing import Any, Optional, cast, overload
23
22
 
24
- from .constant import MESSAGE_TTL_TOLERANCE
23
+ from flwr.common.date import now
24
+ from flwr.common.logger import warn_deprecated_feature
25
+
26
+ from .constant import MESSAGE_TTL_TOLERANCE, MessageType, MessageTypeLegacy
25
27
  from .logger import log
26
- from .record import RecordSet
28
+ from .record import RecordDict
27
29
 
28
30
  DEFAULT_TTL = 43200 # This is 12 hours
31
+ MESSAGE_INIT_ERROR_MESSAGE = (
32
+ "Invalid arguments for Message. Expected one of the documented "
33
+ "signatures: Message(content: RecordDict, dst_node_id: int, message_type: str,"
34
+ " *, [ttl: float, group_id: str]) or Message(content: RecordDict | error: Error,"
35
+ " *, reply_to: Message, [ttl: float])."
36
+ )
37
+
38
+
39
+ class _WarningTracker:
40
+ """A class to track warnings for deprecated properties."""
41
+
42
+ def __init__(self) -> None:
43
+ # These variables are used to ensure that the deprecation warnings
44
+ # for the deprecated properties/class are logged only once.
45
+ self.create_error_reply_logged = False
46
+ self.create_reply_logged = False
47
+
48
+
49
+ _warning_tracker = _WarningTracker()
50
+
51
+
52
+ class MessageInitializationError(TypeError):
53
+ """Error raised when initializing a message with invalid arguments."""
54
+
55
+ def __init__(self, message: str | None = None) -> None:
56
+ super().__init__(message or MESSAGE_INIT_ERROR_MESSAGE)
29
57
 
30
58
 
31
59
  class Metadata: # pylint: disable=too-many-instance-attributes
32
- """A dataclass holding metadata associated with the current message.
60
+ """The class representing metadata associated with the current message.
33
61
 
34
62
  Parameters
35
63
  ----------
@@ -41,11 +69,13 @@ class Metadata: # pylint: disable=too-many-instance-attributes
41
69
  An identifier for the node sending this message.
42
70
  dst_node_id : int
43
71
  An identifier for the node receiving this message.
44
- reply_to_message : str
45
- An identifier for the message this message replies to.
72
+ reply_to_message_id : str
73
+ An identifier for the message to which this message is a reply.
46
74
  group_id : str
47
75
  An identifier for grouping messages. In some settings,
48
76
  this is used as the FL round.
77
+ created_at : float
78
+ Unix timestamp when the message was created.
49
79
  ttl : float
50
80
  Time-to-live for this message in seconds.
51
81
  message_type : str
@@ -59,8 +89,9 @@ class Metadata: # pylint: disable=too-many-instance-attributes
59
89
  message_id: str,
60
90
  src_node_id: int,
61
91
  dst_node_id: int,
62
- reply_to_message: str,
92
+ reply_to_message_id: str,
63
93
  group_id: str,
94
+ created_at: float,
64
95
  ttl: float,
65
96
  message_type: str,
66
97
  ) -> None:
@@ -69,12 +100,14 @@ class Metadata: # pylint: disable=too-many-instance-attributes
69
100
  "_message_id": message_id,
70
101
  "_src_node_id": src_node_id,
71
102
  "_dst_node_id": dst_node_id,
72
- "_reply_to_message": reply_to_message,
103
+ "_reply_to_message_id": reply_to_message_id,
73
104
  "_group_id": group_id,
105
+ "_created_at": created_at,
74
106
  "_ttl": ttl,
75
107
  "_message_type": message_type,
76
108
  }
77
109
  self.__dict__.update(var_dict)
110
+ self.message_type = message_type # Trigger validation
78
111
 
79
112
  @property
80
113
  def run_id(self) -> int:
@@ -92,9 +125,9 @@ class Metadata: # pylint: disable=too-many-instance-attributes
92
125
  return cast(int, self.__dict__["_src_node_id"])
93
126
 
94
127
  @property
95
- def reply_to_message(self) -> str:
96
- """An identifier for the message this message replies to."""
97
- return cast(str, self.__dict__["_reply_to_message"])
128
+ def reply_to_message_id(self) -> str:
129
+ """An identifier for the message to which this message is a reply."""
130
+ return cast(str, self.__dict__["_reply_to_message_id"])
98
131
 
99
132
  @property
100
133
  def dst_node_id(self) -> int:
@@ -123,7 +156,7 @@ class Metadata: # pylint: disable=too-many-instance-attributes
123
156
 
124
157
  @created_at.setter
125
158
  def created_at(self, value: float) -> None:
126
- """Set creation timestamp for this message."""
159
+ """Set creation timestamp of this message."""
127
160
  self.__dict__["_created_at"] = value
128
161
 
129
162
  @property
@@ -154,6 +187,17 @@ class Metadata: # pylint: disable=too-many-instance-attributes
154
187
  @message_type.setter
155
188
  def message_type(self, value: str) -> None:
156
189
  """Set message_type."""
190
+ # Validate message type
191
+ if validate_legacy_message_type(value):
192
+ pass # Backward compatibility for legacy message types
193
+ elif not validate_message_type(value):
194
+ raise ValueError(
195
+ f"Invalid message type: '{value}'. "
196
+ "Expected format: '<category>' or '<category>.<action>', "
197
+ "where <category> must be 'train', 'evaluate', or 'query', "
198
+ "and <action> must be a valid Python identifier."
199
+ )
200
+
157
201
  self.__dict__["_message_type"] = value
158
202
 
159
203
  def __repr__(self) -> str:
@@ -169,7 +213,7 @@ class Metadata: # pylint: disable=too-many-instance-attributes
169
213
 
170
214
 
171
215
  class Error:
172
- """A dataclass that stores information about an error that occurred.
216
+ """The class storing information about an error that occurred.
173
217
 
174
218
  Parameters
175
219
  ----------
@@ -209,31 +253,148 @@ class Error:
209
253
 
210
254
 
211
255
  class Message:
212
- """State of your application from the viewpoint of the entity using it.
256
+ """Represents a message exchanged between ClientApp and ServerApp.
257
+
258
+ This class encapsulates the payload and metadata necessary for communication
259
+ between a ClientApp and a ServerApp.
213
260
 
214
261
  Parameters
215
262
  ----------
216
- metadata : Metadata
217
- A dataclass including information about the message to be executed.
218
- content : Optional[RecordSet]
263
+ content : Optional[RecordDict] (default: None)
219
264
  Holds records either sent by another entity (e.g. sent by the server-side
220
265
  logic to a client, or vice-versa) or that will be sent to it.
221
- error : Optional[Error]
266
+ error : Optional[Error] (default: None)
222
267
  A dataclass that captures information about an error that took place
223
268
  when processing another message.
269
+ dst_node_id : Optional[int] (default: None)
270
+ An identifier for the node receiving this message.
271
+ message_type : Optional[str] (default: None)
272
+ A string that encodes the action to be executed on
273
+ the receiving end.
274
+ ttl : Optional[float] (default: None)
275
+ Time-to-live (TTL) for this message in seconds. If `None` (default),
276
+ the TTL is set to 43,200 seconds (12 hours).
277
+ group_id : Optional[str] (default: None)
278
+ An identifier for grouping messages. In some settings, this is used as
279
+ the FL round.
280
+ reply_to : Optional[Message] (default: None)
281
+ The instruction message to which this message is a reply. This message does
282
+ not retain the original message's content but derives its metadata from it.
224
283
  """
225
284
 
226
- def __init__(
285
+ @overload
286
+ def __init__( # pylint: disable=too-many-arguments # noqa: E704
227
287
  self,
228
- metadata: Metadata,
229
- content: RecordSet | None = None,
288
+ content: RecordDict,
289
+ dst_node_id: int,
290
+ message_type: str,
291
+ *,
292
+ ttl: float | None = None,
293
+ group_id: str | None = None,
294
+ ) -> None: ...
295
+
296
+ @overload
297
+ def __init__( # noqa: E704
298
+ self, content: RecordDict, *, reply_to: Message, ttl: float | None = None
299
+ ) -> None: ...
300
+
301
+ @overload
302
+ def __init__( # noqa: E704
303
+ self, error: Error, *, reply_to: Message, ttl: float | None = None
304
+ ) -> None: ...
305
+
306
+ def __init__( # pylint: disable=too-many-arguments
307
+ self,
308
+ *args: Any,
309
+ dst_node_id: int | None = None,
310
+ message_type: str | None = None,
311
+ content: RecordDict | None = None,
230
312
  error: Error | None = None,
313
+ ttl: float | None = None,
314
+ group_id: str | None = None,
315
+ reply_to: Message | None = None,
316
+ metadata: Metadata | None = None,
231
317
  ) -> None:
232
- if not (content is None) ^ (error is None):
233
- raise ValueError("Either `content` or `error` must be set, but not both.")
318
+ # Set positional arguments
319
+ content, error, dst_node_id, message_type = _extract_positional_args(
320
+ *args,
321
+ content=content,
322
+ error=error,
323
+ dst_node_id=dst_node_id,
324
+ message_type=message_type,
325
+ )
326
+ _check_arg_types(
327
+ dst_node_id=dst_node_id,
328
+ message_type=message_type,
329
+ content=content,
330
+ error=error,
331
+ ttl=ttl,
332
+ group_id=group_id,
333
+ reply_to=reply_to,
334
+ metadata=metadata,
335
+ )
336
+
337
+ # Set metadata directly (This is for internal use only)
338
+ if metadata is not None:
339
+ # When metadata is set, all other arguments must be None,
340
+ # except `content`, `error`, or `content_or_error`
341
+ if any(
342
+ x is not None
343
+ for x in [dst_node_id, message_type, ttl, group_id, reply_to]
344
+ ):
345
+ raise MessageInitializationError(
346
+ f"Invalid arguments for {Message.__qualname__}. "
347
+ "Expected only `metadata` to be set when creating a message "
348
+ "with provided metadata."
349
+ )
350
+
351
+ # Create metadata for an instruction message
352
+ elif reply_to is None:
353
+ # Check arguments
354
+ # `content`, `dst_node_id` and `message_type` must be set
355
+ if not (
356
+ isinstance(content, RecordDict)
357
+ and isinstance(dst_node_id, int)
358
+ and isinstance(message_type, str)
359
+ ):
360
+ raise MessageInitializationError()
361
+
362
+ # Set metadata
363
+ metadata = Metadata(
364
+ run_id=0, # Will be set before pushed
365
+ message_id="", # Will be set by the SuperLink
366
+ src_node_id=0, # Will be set before pushed
367
+ dst_node_id=dst_node_id,
368
+ # Instruction messages do not reply to any message
369
+ reply_to_message_id="",
370
+ group_id=group_id or "",
371
+ created_at=now().timestamp(),
372
+ ttl=ttl or DEFAULT_TTL,
373
+ message_type=message_type,
374
+ )
375
+
376
+ # Create metadata for a reply message
377
+ else:
378
+ # Check arguments
379
+ # `dst_node_id`, `message_type` and `group_id` must not be set
380
+ if any(x is not None for x in [dst_node_id, message_type, group_id]):
381
+ raise MessageInitializationError()
382
+
383
+ # Set metadata
384
+ current = now().timestamp()
385
+ metadata = Metadata(
386
+ run_id=reply_to.metadata.run_id,
387
+ message_id="", # Will be set by the SuperLink
388
+ src_node_id=reply_to.metadata.dst_node_id,
389
+ dst_node_id=reply_to.metadata.src_node_id,
390
+ reply_to_message_id=reply_to.metadata.message_id,
391
+ group_id=reply_to.metadata.group_id,
392
+ created_at=current,
393
+ ttl=_limit_reply_ttl(current, ttl, reply_to),
394
+ message_type=reply_to.metadata.message_type,
395
+ )
234
396
 
235
- metadata.created_at = time.time() # Set the message creation timestamp
236
- metadata.delivered_at = ""
397
+ metadata.delivered_at = "" # Backward compatibility
237
398
  var_dict = {
238
399
  "_metadata": metadata,
239
400
  "_content": content,
@@ -247,17 +408,17 @@ class Message:
247
408
  return cast(Metadata, self.__dict__["_metadata"])
248
409
 
249
410
  @property
250
- def content(self) -> RecordSet:
411
+ def content(self) -> RecordDict:
251
412
  """The content of this message."""
252
413
  if self.__dict__["_content"] is None:
253
414
  raise ValueError(
254
415
  "Message content is None. Use <message>.has_content() "
255
416
  "to check if a message has content."
256
417
  )
257
- return cast(RecordSet, self.__dict__["_content"])
418
+ return cast(RecordDict, self.__dict__["_content"])
258
419
 
259
420
  @content.setter
260
- def content(self, value: RecordSet) -> None:
421
+ def content(self, value: RecordDict) -> None:
261
422
  """Set content."""
262
423
  if self.__dict__["_error"] is None:
263
424
  self.__dict__["_content"] = value
@@ -308,33 +469,27 @@ class Message:
308
469
  message : Message
309
470
  A Message containing only the relevant error and metadata.
310
471
  """
311
- # If no TTL passed, use default for message creation (will update after
312
- # message creation)
313
- ttl_ = DEFAULT_TTL if ttl is None else ttl
314
- # Create reply with error
315
- message = Message(metadata=_create_reply_metadata(self, ttl_), error=error)
316
-
317
- if ttl is None:
318
- # Set TTL equal to the remaining time for the received message to expire
319
- ttl = self.metadata.ttl - (
320
- message.metadata.created_at - self.metadata.created_at
472
+ if not _warning_tracker.create_error_reply_logged:
473
+ _warning_tracker.create_error_reply_logged = True
474
+ warn_deprecated_feature(
475
+ "`Message.create_error_reply` is deprecated. "
476
+ "Instead of calling `some_message.create_error_reply(some_error, "
477
+ "ttl=...)`, use `Message(some_error, reply_to=some_message, ttl=...)`."
321
478
  )
322
- message.metadata.ttl = ttl
479
+ if ttl is not None:
480
+ return Message(error, reply_to=self, ttl=ttl)
481
+ return Message(error, reply_to=self)
323
482
 
324
- self._limit_message_res_ttl(message)
325
-
326
- return message
327
-
328
- def create_reply(self, content: RecordSet, ttl: float | None = None) -> Message:
483
+ def create_reply(self, content: RecordDict, ttl: float | None = None) -> Message:
329
484
  """Create a reply to this message with specified content and TTL.
330
485
 
331
486
  The method generates a new `Message` as a reply to this message.
332
487
  It inherits 'run_id', 'src_node_id', 'dst_node_id', and 'message_type' from
333
- this message and sets 'reply_to_message' to the ID of this message.
488
+ this message and sets 'reply_to_message_id' to the ID of this message.
334
489
 
335
490
  Parameters
336
491
  ----------
337
- content : RecordSet
492
+ content : RecordDict
338
493
  The content for the reply message.
339
494
  ttl : Optional[float] (default: None)
340
495
  Time-to-live for this message in seconds. If unset, it will be set based
@@ -348,25 +503,16 @@ class Message:
348
503
  Message
349
504
  A new `Message` instance representing the reply.
350
505
  """
351
- # If no TTL passed, use default for message creation (will update after
352
- # message creation)
353
- ttl_ = DEFAULT_TTL if ttl is None else ttl
354
-
355
- message = Message(
356
- metadata=_create_reply_metadata(self, ttl_),
357
- content=content,
358
- )
359
-
360
- if ttl is None:
361
- # Set TTL equal to the remaining time for the received message to expire
362
- ttl = self.metadata.ttl - (
363
- message.metadata.created_at - self.metadata.created_at
506
+ if not _warning_tracker.create_reply_logged:
507
+ _warning_tracker.create_reply_logged = True
508
+ warn_deprecated_feature(
509
+ "`Message.create_reply` is deprecated. "
510
+ "Instead of calling `some_message.create_reply(some_content, ttl=...)`"
511
+ ", use `Message(some_content, reply_to=some_message, ttl=...)`."
364
512
  )
365
- message.metadata.ttl = ttl
366
-
367
- self._limit_message_res_ttl(message)
368
-
369
- return message
513
+ if ttl is not None:
514
+ return Message(content, reply_to=self, ttl=ttl)
515
+ return Message(content, reply_to=self)
370
516
 
371
517
  def __repr__(self) -> str:
372
518
  """Return a string representation of this instance."""
@@ -379,41 +525,137 @@ class Message:
379
525
  )
380
526
  return f"{self.__class__.__qualname__}({view})"
381
527
 
382
- def _limit_message_res_ttl(self, message: Message) -> None:
383
- """Limit the TTL of the provided Message to not exceed the expiration time of
384
- this Message it replies to.
385
528
 
386
- Parameters
387
- ----------
388
- message : Message
389
- The reply Message to limit the TTL for.
390
- """
391
- # Calculate the maximum allowed TTL
392
- max_allowed_ttl = (
393
- self.metadata.created_at + self.metadata.ttl - message.metadata.created_at
529
+ def make_message(
530
+ metadata: Metadata, content: RecordDict | None = None, error: Error | None = None
531
+ ) -> Message:
532
+ """Create a message with the provided metadata, content, and error."""
533
+ return Message(metadata=metadata, content=content, error=error) # type: ignore
534
+
535
+
536
+ def _limit_reply_ttl(
537
+ current: float, reply_ttl: float | None, reply_to: Message
538
+ ) -> float:
539
+ """Limit the TTL of a reply message such that it does exceed the expiration time of
540
+ the message it replies to."""
541
+ # Calculate the maximum allowed TTL
542
+ max_allowed_ttl = reply_to.metadata.created_at + reply_to.metadata.ttl - current
543
+
544
+ if reply_ttl is not None and reply_ttl - max_allowed_ttl > MESSAGE_TTL_TOLERANCE:
545
+ log(
546
+ WARNING,
547
+ "The reply TTL of %.2f seconds exceeded the "
548
+ "allowed maximum of %.2f seconds. "
549
+ "The TTL has been updated to the allowed maximum.",
550
+ reply_ttl,
551
+ max_allowed_ttl,
394
552
  )
395
-
396
- if message.metadata.ttl - max_allowed_ttl > MESSAGE_TTL_TOLERANCE:
397
- log(
398
- WARNING,
399
- "The reply TTL of %.2f seconds exceeded the "
400
- "allowed maximum of %.2f seconds. "
401
- "The TTL has been updated to the allowed maximum.",
402
- message.metadata.ttl,
403
- max_allowed_ttl,
404
- )
405
- message.metadata.ttl = max_allowed_ttl
406
-
407
-
408
- def _create_reply_metadata(msg: Message, ttl: float) -> Metadata:
409
- """Construct metadata for a reply message."""
410
- return Metadata(
411
- run_id=msg.metadata.run_id,
412
- message_id="",
413
- src_node_id=msg.metadata.dst_node_id,
414
- dst_node_id=msg.metadata.src_node_id,
415
- reply_to_message=msg.metadata.message_id,
416
- group_id=msg.metadata.group_id,
417
- ttl=ttl,
418
- message_type=msg.metadata.message_type,
419
- )
553
+ return max_allowed_ttl
554
+
555
+ return reply_ttl or max_allowed_ttl
556
+
557
+
558
+ def _extract_positional_args(
559
+ *args: Any,
560
+ content: RecordDict | None,
561
+ error: Error | None,
562
+ dst_node_id: int | None,
563
+ message_type: str | None,
564
+ ) -> tuple[RecordDict | None, Error | None, int | None, str | None]:
565
+ """Extract positional arguments for the `Message` constructor."""
566
+ content_or_error = args[0] if args else None
567
+ if len(args) > 1:
568
+ if dst_node_id is not None:
569
+ raise MessageInitializationError()
570
+ dst_node_id = args[1]
571
+ if len(args) > 2:
572
+ if message_type is not None:
573
+ raise MessageInitializationError()
574
+ message_type = args[2]
575
+ if len(args) > 3:
576
+ raise MessageInitializationError()
577
+
578
+ # One and only one of `content_or_error`, `content` and `error` must be set
579
+ if sum(x is not None for x in [content_or_error, content, error]) != 1:
580
+ raise MessageInitializationError()
581
+
582
+ # Set `content` or `error` based on `content_or_error`
583
+ if content_or_error is not None: # This means `content` and `error` are None
584
+ if isinstance(content_or_error, RecordDict):
585
+ content = content_or_error
586
+ elif isinstance(content_or_error, Error):
587
+ error = content_or_error
588
+ else:
589
+ raise MessageInitializationError()
590
+ return content, error, dst_node_id, message_type
591
+
592
+
593
+ def _check_arg_types( # pylint: disable=too-many-arguments, R0917
594
+ dst_node_id: int | None = None,
595
+ message_type: str | None = None,
596
+ content: RecordDict | None = None,
597
+ error: Error | None = None,
598
+ ttl: float | None = None,
599
+ group_id: str | None = None,
600
+ reply_to: Message | None = None,
601
+ metadata: Metadata | None = None,
602
+ ) -> None:
603
+ """Check argument types for the `Message` constructor."""
604
+ # pylint: disable=too-many-boolean-expressions
605
+ if (
606
+ (dst_node_id is None or isinstance(dst_node_id, int))
607
+ and (message_type is None or isinstance(message_type, str))
608
+ and (content is None or isinstance(content, RecordDict))
609
+ and (error is None or isinstance(error, Error))
610
+ and (ttl is None or isinstance(ttl, (int, float)))
611
+ and (group_id is None or isinstance(group_id, str))
612
+ and (reply_to is None or isinstance(reply_to, Message))
613
+ and (metadata is None or isinstance(metadata, Metadata))
614
+ ):
615
+ return
616
+ raise MessageInitializationError()
617
+
618
+
619
+ def validate_message_type(message_type: str) -> bool:
620
+ """Validate if the message type is valid.
621
+
622
+ A valid message type format must be one of the following:
623
+
624
+ - "<category>"
625
+ - "<category>.<action>"
626
+
627
+ where `category` must be one of "train", "evaluate", or "query",
628
+ and `action` must be a valid Python identifier.
629
+ """
630
+ # Check if conforming to the format "<category>"
631
+ valid_types = {
632
+ MessageType.TRAIN,
633
+ MessageType.EVALUATE,
634
+ MessageType.QUERY,
635
+ MessageType.SYSTEM,
636
+ }
637
+ if message_type in valid_types:
638
+ return True
639
+
640
+ # Check if conforming to the format "<category>.<action>"
641
+ if message_type.count(".") != 1:
642
+ return False
643
+
644
+ category, action = message_type.split(".")
645
+ if category in valid_types and action.isidentifier():
646
+ return True
647
+
648
+ return False
649
+
650
+
651
+ def validate_legacy_message_type(message_type: str) -> bool:
652
+ """Validate if the legacy message type is valid."""
653
+ # Backward compatibility for legacy message types
654
+ if message_type in (
655
+ MessageTypeLegacy.GET_PARAMETERS,
656
+ MessageTypeLegacy.GET_PROPERTIES,
657
+ "reconnect",
658
+ ):
659
+ return True
660
+
661
+ return False
flwr/common/object_ref.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.
flwr/common/parameter.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/common/pyproject.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.
@@ -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.
@@ -15,17 +15,21 @@
15
15
  """Record APIs."""
16
16
 
17
17
 
18
- from .configsrecord import ConfigsRecord
18
+ from .arrayrecord import Array, ArrayRecord, ParametersRecord
19
+ from .configrecord import ConfigRecord, ConfigsRecord
19
20
  from .conversion_utils import array_from_numpy
20
- from .metricsrecord import MetricsRecord
21
- from .parametersrecord import Array, ParametersRecord
22
- from .recordset import RecordSet
21
+ from .metricrecord import MetricRecord, MetricsRecord
22
+ from .recorddict import RecordDict, RecordSet
23
23
 
24
24
  __all__ = [
25
25
  "Array",
26
+ "ArrayRecord",
27
+ "ConfigRecord",
26
28
  "ConfigsRecord",
29
+ "MetricRecord",
27
30
  "MetricsRecord",
28
31
  "ParametersRecord",
32
+ "RecordDict",
29
33
  "RecordSet",
30
34
  "array_from_numpy",
31
35
  ]