flwr-nightly 1.8.0.dev20240314__py3-none-any.whl → 1.11.0.dev20240813__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.

Potentially problematic release.


This version of flwr-nightly might be problematic. Click here for more details.

Files changed (237) hide show
  1. flwr/cli/app.py +7 -0
  2. flwr/cli/build.py +150 -0
  3. flwr/cli/config_utils.py +219 -0
  4. flwr/cli/example.py +3 -1
  5. flwr/cli/install.py +227 -0
  6. flwr/cli/new/new.py +179 -48
  7. flwr/cli/new/templates/app/.gitignore.tpl +160 -0
  8. flwr/cli/new/templates/app/README.flowertune.md.tpl +56 -0
  9. flwr/cli/new/templates/app/README.md.tpl +1 -5
  10. flwr/cli/new/templates/app/code/__init__.py.tpl +1 -1
  11. flwr/cli/new/templates/app/code/client.huggingface.py.tpl +65 -0
  12. flwr/cli/new/templates/app/code/client.jax.py.tpl +56 -0
  13. flwr/cli/new/templates/app/code/client.mlx.py.tpl +93 -0
  14. flwr/cli/new/templates/app/code/client.numpy.py.tpl +3 -2
  15. flwr/cli/new/templates/app/code/client.pytorch.py.tpl +23 -11
  16. flwr/cli/new/templates/app/code/client.sklearn.py.tpl +97 -0
  17. flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +60 -1
  18. flwr/cli/new/templates/app/code/flwr_tune/__init__.py +15 -0
  19. flwr/cli/new/templates/app/code/flwr_tune/app.py.tpl +89 -0
  20. flwr/cli/new/templates/app/code/flwr_tune/client.py.tpl +126 -0
  21. flwr/cli/new/templates/app/code/flwr_tune/config.yaml.tpl +34 -0
  22. flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +57 -0
  23. flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +59 -0
  24. flwr/cli/new/templates/app/code/flwr_tune/server.py.tpl +48 -0
  25. flwr/cli/new/templates/app/code/flwr_tune/static_config.yaml.tpl +11 -0
  26. flwr/cli/new/templates/app/code/server.huggingface.py.tpl +23 -0
  27. flwr/cli/new/templates/app/code/server.jax.py.tpl +20 -0
  28. flwr/cli/new/templates/app/code/server.mlx.py.tpl +20 -0
  29. flwr/cli/new/templates/app/code/server.numpy.py.tpl +17 -9
  30. flwr/cli/new/templates/app/code/server.pytorch.py.tpl +21 -18
  31. flwr/cli/new/templates/app/code/server.sklearn.py.tpl +24 -0
  32. flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +29 -1
  33. flwr/cli/new/templates/app/code/task.huggingface.py.tpl +99 -0
  34. flwr/cli/new/templates/app/code/task.jax.py.tpl +57 -0
  35. flwr/cli/new/templates/app/code/task.mlx.py.tpl +102 -0
  36. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +28 -23
  37. flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +53 -0
  38. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +39 -0
  39. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +38 -0
  40. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +34 -0
  41. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +39 -0
  42. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +25 -12
  43. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +29 -14
  44. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +33 -0
  45. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +29 -14
  46. flwr/cli/run/run.py +168 -17
  47. flwr/cli/utils.py +75 -4
  48. flwr/client/__init__.py +6 -1
  49. flwr/client/app.py +239 -248
  50. flwr/client/client_app.py +70 -9
  51. flwr/client/dpfedavg_numpy_client.py +1 -1
  52. flwr/client/grpc_adapter_client/__init__.py +15 -0
  53. flwr/client/grpc_adapter_client/connection.py +97 -0
  54. flwr/client/grpc_client/connection.py +18 -5
  55. flwr/client/grpc_rere_client/__init__.py +1 -1
  56. flwr/client/grpc_rere_client/client_interceptor.py +158 -0
  57. flwr/client/grpc_rere_client/connection.py +127 -33
  58. flwr/client/grpc_rere_client/grpc_adapter.py +140 -0
  59. flwr/client/heartbeat.py +74 -0
  60. flwr/client/message_handler/__init__.py +1 -1
  61. flwr/client/message_handler/message_handler.py +7 -7
  62. flwr/client/mod/__init__.py +5 -5
  63. flwr/client/mod/centraldp_mods.py +4 -2
  64. flwr/client/mod/comms_mods.py +4 -4
  65. flwr/client/mod/localdp_mod.py +9 -4
  66. flwr/client/mod/secure_aggregation/__init__.py +1 -1
  67. flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
  68. flwr/client/mod/utils.py +1 -1
  69. flwr/client/node_state.py +60 -10
  70. flwr/client/node_state_tests.py +4 -3
  71. flwr/client/rest_client/__init__.py +1 -1
  72. flwr/client/rest_client/connection.py +177 -157
  73. flwr/client/supernode/__init__.py +26 -0
  74. flwr/client/supernode/app.py +464 -0
  75. flwr/client/typing.py +1 -0
  76. flwr/common/__init__.py +13 -11
  77. flwr/common/address.py +1 -1
  78. flwr/common/config.py +193 -0
  79. flwr/common/constant.py +42 -1
  80. flwr/common/context.py +26 -1
  81. flwr/common/date.py +1 -1
  82. flwr/common/dp.py +1 -1
  83. flwr/common/grpc.py +6 -2
  84. flwr/common/logger.py +79 -8
  85. flwr/common/message.py +167 -105
  86. flwr/common/object_ref.py +126 -25
  87. flwr/common/record/__init__.py +1 -1
  88. flwr/common/record/parametersrecord.py +0 -1
  89. flwr/common/record/recordset.py +78 -27
  90. flwr/common/recordset_compat.py +8 -1
  91. flwr/common/retry_invoker.py +25 -13
  92. flwr/common/secure_aggregation/__init__.py +1 -1
  93. flwr/common/secure_aggregation/crypto/__init__.py +1 -1
  94. flwr/common/secure_aggregation/crypto/shamir.py +1 -1
  95. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +21 -2
  96. flwr/common/secure_aggregation/ndarrays_arithmetic.py +1 -1
  97. flwr/common/secure_aggregation/quantization.py +1 -1
  98. flwr/common/secure_aggregation/secaggplus_constants.py +1 -1
  99. flwr/common/secure_aggregation/secaggplus_utils.py +1 -1
  100. flwr/common/serde.py +209 -3
  101. flwr/common/telemetry.py +25 -0
  102. flwr/common/typing.py +38 -0
  103. flwr/common/version.py +14 -0
  104. flwr/proto/clientappio_pb2.py +41 -0
  105. flwr/proto/clientappio_pb2.pyi +110 -0
  106. flwr/proto/clientappio_pb2_grpc.py +101 -0
  107. flwr/proto/clientappio_pb2_grpc.pyi +40 -0
  108. flwr/proto/common_pb2.py +36 -0
  109. flwr/proto/common_pb2.pyi +121 -0
  110. flwr/proto/common_pb2_grpc.py +4 -0
  111. flwr/proto/common_pb2_grpc.pyi +4 -0
  112. flwr/proto/driver_pb2.py +26 -19
  113. flwr/proto/driver_pb2.pyi +34 -0
  114. flwr/proto/driver_pb2_grpc.py +70 -0
  115. flwr/proto/driver_pb2_grpc.pyi +28 -0
  116. flwr/proto/exec_pb2.py +43 -0
  117. flwr/proto/exec_pb2.pyi +95 -0
  118. flwr/proto/exec_pb2_grpc.py +101 -0
  119. flwr/proto/exec_pb2_grpc.pyi +41 -0
  120. flwr/proto/fab_pb2.py +30 -0
  121. flwr/proto/fab_pb2.pyi +56 -0
  122. flwr/proto/fab_pb2_grpc.py +4 -0
  123. flwr/proto/fab_pb2_grpc.pyi +4 -0
  124. flwr/proto/fleet_pb2.py +29 -23
  125. flwr/proto/fleet_pb2.pyi +33 -0
  126. flwr/proto/fleet_pb2_grpc.py +102 -0
  127. flwr/proto/fleet_pb2_grpc.pyi +35 -0
  128. flwr/proto/grpcadapter_pb2.py +32 -0
  129. flwr/proto/grpcadapter_pb2.pyi +43 -0
  130. flwr/proto/grpcadapter_pb2_grpc.py +66 -0
  131. flwr/proto/grpcadapter_pb2_grpc.pyi +24 -0
  132. flwr/proto/message_pb2.py +41 -0
  133. flwr/proto/message_pb2.pyi +122 -0
  134. flwr/proto/message_pb2_grpc.py +4 -0
  135. flwr/proto/message_pb2_grpc.pyi +4 -0
  136. flwr/proto/run_pb2.py +35 -0
  137. flwr/proto/run_pb2.pyi +76 -0
  138. flwr/proto/run_pb2_grpc.py +4 -0
  139. flwr/proto/run_pb2_grpc.pyi +4 -0
  140. flwr/proto/task_pb2.py +7 -8
  141. flwr/proto/task_pb2.pyi +8 -5
  142. flwr/server/__init__.py +4 -8
  143. flwr/server/app.py +298 -350
  144. flwr/server/compat/app.py +6 -57
  145. flwr/server/compat/app_utils.py +5 -4
  146. flwr/server/compat/driver_client_proxy.py +29 -48
  147. flwr/server/compat/legacy_context.py +5 -4
  148. flwr/server/driver/__init__.py +2 -0
  149. flwr/server/driver/driver.py +22 -132
  150. flwr/server/driver/grpc_driver.py +224 -74
  151. flwr/server/driver/inmemory_driver.py +183 -0
  152. flwr/server/history.py +20 -20
  153. flwr/server/run_serverapp.py +121 -34
  154. flwr/server/server.py +11 -7
  155. flwr/server/server_app.py +59 -10
  156. flwr/server/serverapp_components.py +52 -0
  157. flwr/server/strategy/__init__.py +2 -2
  158. flwr/server/strategy/bulyan.py +1 -1
  159. flwr/server/strategy/dp_adaptive_clipping.py +3 -3
  160. flwr/server/strategy/dp_fixed_clipping.py +4 -3
  161. flwr/server/strategy/dpfedavg_adaptive.py +1 -1
  162. flwr/server/strategy/dpfedavg_fixed.py +1 -1
  163. flwr/server/strategy/fedadagrad.py +1 -1
  164. flwr/server/strategy/fedadam.py +1 -1
  165. flwr/server/strategy/fedavg_android.py +1 -1
  166. flwr/server/strategy/fedavgm.py +1 -1
  167. flwr/server/strategy/fedmedian.py +1 -1
  168. flwr/server/strategy/fedopt.py +1 -1
  169. flwr/server/strategy/fedprox.py +1 -1
  170. flwr/server/strategy/fedxgb_bagging.py +1 -1
  171. flwr/server/strategy/fedxgb_cyclic.py +1 -1
  172. flwr/server/strategy/fedxgb_nn_avg.py +1 -1
  173. flwr/server/strategy/fedyogi.py +1 -1
  174. flwr/server/strategy/krum.py +1 -1
  175. flwr/server/strategy/qfedavg.py +1 -1
  176. flwr/server/superlink/driver/__init__.py +1 -1
  177. flwr/server/superlink/driver/driver_grpc.py +1 -1
  178. flwr/server/superlink/driver/driver_servicer.py +51 -4
  179. flwr/server/superlink/ffs/__init__.py +24 -0
  180. flwr/server/superlink/ffs/disk_ffs.py +104 -0
  181. flwr/server/superlink/ffs/ffs.py +79 -0
  182. flwr/server/superlink/fleet/__init__.py +1 -1
  183. flwr/server/superlink/fleet/grpc_adapter/__init__.py +15 -0
  184. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +131 -0
  185. flwr/server/superlink/fleet/grpc_bidi/__init__.py +1 -1
  186. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -1
  187. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +1 -1
  188. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +1 -1
  189. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +8 -2
  190. flwr/server/superlink/fleet/grpc_rere/__init__.py +1 -1
  191. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +30 -2
  192. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +214 -0
  193. flwr/server/superlink/fleet/message_handler/__init__.py +1 -1
  194. flwr/server/superlink/fleet/message_handler/message_handler.py +42 -2
  195. flwr/server/superlink/fleet/rest_rere/__init__.py +1 -1
  196. flwr/server/superlink/fleet/rest_rere/rest_api.py +59 -1
  197. flwr/server/superlink/fleet/vce/backend/__init__.py +1 -1
  198. flwr/server/superlink/fleet/vce/backend/backend.py +5 -5
  199. flwr/server/superlink/fleet/vce/backend/raybackend.py +53 -56
  200. flwr/server/superlink/fleet/vce/vce_api.py +190 -127
  201. flwr/server/superlink/state/__init__.py +1 -1
  202. flwr/server/superlink/state/in_memory_state.py +159 -42
  203. flwr/server/superlink/state/sqlite_state.py +243 -39
  204. flwr/server/superlink/state/state.py +81 -6
  205. flwr/server/superlink/state/state_factory.py +11 -2
  206. flwr/server/superlink/state/utils.py +62 -0
  207. flwr/server/typing.py +2 -0
  208. flwr/server/utils/__init__.py +1 -1
  209. flwr/server/utils/tensorboard.py +1 -1
  210. flwr/server/utils/validator.py +23 -9
  211. flwr/server/workflow/default_workflows.py +67 -25
  212. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +18 -6
  213. flwr/simulation/__init__.py +7 -4
  214. flwr/simulation/app.py +67 -36
  215. flwr/simulation/ray_transport/__init__.py +1 -1
  216. flwr/simulation/ray_transport/ray_actor.py +20 -46
  217. flwr/simulation/ray_transport/ray_client_proxy.py +36 -16
  218. flwr/simulation/run_simulation.py +308 -92
  219. flwr/superexec/__init__.py +21 -0
  220. flwr/superexec/app.py +184 -0
  221. flwr/superexec/deployment.py +185 -0
  222. flwr/superexec/exec_grpc.py +55 -0
  223. flwr/superexec/exec_servicer.py +70 -0
  224. flwr/superexec/executor.py +75 -0
  225. flwr/superexec/simulation.py +193 -0
  226. {flwr_nightly-1.8.0.dev20240314.dist-info → flwr_nightly-1.11.0.dev20240813.dist-info}/METADATA +10 -6
  227. flwr_nightly-1.11.0.dev20240813.dist-info/RECORD +288 -0
  228. flwr_nightly-1.11.0.dev20240813.dist-info/entry_points.txt +10 -0
  229. flwr/cli/flower_toml.py +0 -140
  230. flwr/cli/new/templates/app/flower.toml.tpl +0 -13
  231. flwr/cli/new/templates/app/requirements.numpy.txt.tpl +0 -2
  232. flwr/cli/new/templates/app/requirements.pytorch.txt.tpl +0 -4
  233. flwr/cli/new/templates/app/requirements.tensorflow.txt.tpl +0 -4
  234. flwr_nightly-1.8.0.dev20240314.dist-info/RECORD +0 -211
  235. flwr_nightly-1.8.0.dev20240314.dist-info/entry_points.txt +0 -9
  236. {flwr_nightly-1.8.0.dev20240314.dist-info → flwr_nightly-1.11.0.dev20240813.dist-info}/LICENSE +0 -0
  237. {flwr_nightly-1.8.0.dev20240314.dist-info → flwr_nightly-1.11.0.dev20240813.dist-info}/WHEEL +0 -0
flwr/common/message.py CHANGED
@@ -16,12 +16,15 @@
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
- from dataclasses import dataclass
19
+ import time
20
+ import warnings
21
+ from typing import Optional, cast
20
22
 
21
23
  from .record import RecordSet
22
24
 
25
+ DEFAULT_TTL = 3600
26
+
23
27
 
24
- @dataclass
25
28
  class Metadata: # pylint: disable=too-many-instance-attributes
26
29
  """A dataclass holding metadata associated with the current message.
27
30
 
@@ -40,27 +43,13 @@ class Metadata: # pylint: disable=too-many-instance-attributes
40
43
  group_id : str
41
44
  An identifier for grouping messages. In some settings,
42
45
  this is used as the FL round.
43
- ttl : str
44
- Time-to-live for this message.
46
+ ttl : float
47
+ Time-to-live for this message in seconds.
45
48
  message_type : str
46
49
  A string that encodes the action to be executed on
47
50
  the receiving end.
48
- partition_id : Optional[int]
49
- An identifier that can be used when loading a particular
50
- data partition for a ClientApp. Making use of this identifier
51
- is more relevant when conducting simulations.
52
51
  """
53
52
 
54
- _run_id: int
55
- _message_id: str
56
- _src_node_id: int
57
- _dst_node_id: int
58
- _reply_to_message: str
59
- _group_id: str
60
- _ttl: str
61
- _message_type: str
62
- _partition_id: int | None
63
-
64
53
  def __init__( # pylint: disable=too-many-arguments
65
54
  self,
66
55
  run_id: int,
@@ -69,92 +58,103 @@ class Metadata: # pylint: disable=too-many-instance-attributes
69
58
  dst_node_id: int,
70
59
  reply_to_message: str,
71
60
  group_id: str,
72
- ttl: str,
61
+ ttl: float,
73
62
  message_type: str,
74
- partition_id: int | None = None,
75
63
  ) -> None:
76
- self._run_id = run_id
77
- self._message_id = message_id
78
- self._src_node_id = src_node_id
79
- self._dst_node_id = dst_node_id
80
- self._reply_to_message = reply_to_message
81
- self._group_id = group_id
82
- self._ttl = ttl
83
- self._message_type = message_type
84
- self._partition_id = partition_id
64
+ var_dict = {
65
+ "_run_id": run_id,
66
+ "_message_id": message_id,
67
+ "_src_node_id": src_node_id,
68
+ "_dst_node_id": dst_node_id,
69
+ "_reply_to_message": reply_to_message,
70
+ "_group_id": group_id,
71
+ "_ttl": ttl,
72
+ "_message_type": message_type,
73
+ }
74
+ self.__dict__.update(var_dict)
85
75
 
86
76
  @property
87
77
  def run_id(self) -> int:
88
78
  """An identifier for the current run."""
89
- return self._run_id
79
+ return cast(int, self.__dict__["_run_id"])
90
80
 
91
81
  @property
92
82
  def message_id(self) -> str:
93
83
  """An identifier for the current message."""
94
- return self._message_id
84
+ return cast(str, self.__dict__["_message_id"])
95
85
 
96
86
  @property
97
87
  def src_node_id(self) -> int:
98
88
  """An identifier for the node sending this message."""
99
- return self._src_node_id
89
+ return cast(int, self.__dict__["_src_node_id"])
100
90
 
101
91
  @property
102
92
  def reply_to_message(self) -> str:
103
93
  """An identifier for the message this message replies to."""
104
- return self._reply_to_message
94
+ return cast(str, self.__dict__["_reply_to_message"])
105
95
 
106
96
  @property
107
97
  def dst_node_id(self) -> int:
108
98
  """An identifier for the node receiving this message."""
109
- return self._dst_node_id
99
+ return cast(int, self.__dict__["_dst_node_id"])
110
100
 
111
101
  @dst_node_id.setter
112
102
  def dst_node_id(self, value: int) -> None:
113
103
  """Set dst_node_id."""
114
- self._dst_node_id = value
104
+ self.__dict__["_dst_node_id"] = value
115
105
 
116
106
  @property
117
107
  def group_id(self) -> str:
118
108
  """An identifier for grouping messages."""
119
- return self._group_id
109
+ return cast(str, self.__dict__["_group_id"])
120
110
 
121
111
  @group_id.setter
122
112
  def group_id(self, value: str) -> None:
123
113
  """Set group_id."""
124
- self._group_id = value
114
+ self.__dict__["_group_id"] = value
125
115
 
126
116
  @property
127
- def ttl(self) -> str:
117
+ def created_at(self) -> float:
118
+ """Unix timestamp when the message was created."""
119
+ return cast(float, self.__dict__["_created_at"])
120
+
121
+ @created_at.setter
122
+ def created_at(self, value: float) -> None:
123
+ """Set creation timestamp for this message."""
124
+ self.__dict__["_created_at"] = value
125
+
126
+ @property
127
+ def ttl(self) -> float:
128
128
  """Time-to-live for this message."""
129
- return self._ttl
129
+ return cast(float, self.__dict__["_ttl"])
130
130
 
131
131
  @ttl.setter
132
- def ttl(self, value: str) -> None:
132
+ def ttl(self, value: float) -> None:
133
133
  """Set ttl."""
134
- self._ttl = value
134
+ self.__dict__["_ttl"] = value
135
135
 
136
136
  @property
137
137
  def message_type(self) -> str:
138
138
  """A string that encodes the action to be executed on the receiving end."""
139
- return self._message_type
139
+ return cast(str, self.__dict__["_message_type"])
140
140
 
141
141
  @message_type.setter
142
142
  def message_type(self, value: str) -> None:
143
143
  """Set message_type."""
144
- self._message_type = value
144
+ self.__dict__["_message_type"] = value
145
145
 
146
- @property
147
- def partition_id(self) -> int | None:
148
- """An identifier telling which data partition a ClientApp should use."""
149
- return self._partition_id
146
+ def __repr__(self) -> str:
147
+ """Return a string representation of this instance."""
148
+ view = ", ".join([f"{k.lstrip('_')}={v!r}" for k, v in self.__dict__.items()])
149
+ return f"{self.__class__.__qualname__}({view})"
150
150
 
151
- @partition_id.setter
152
- def partition_id(self, value: int) -> None:
153
- """Set patition_id."""
154
- self._partition_id = value
151
+ def __eq__(self, other: object) -> bool:
152
+ """Compare two instances of the class."""
153
+ if not isinstance(other, self.__class__):
154
+ raise NotImplementedError
155
+ return self.__dict__ == other.__dict__
155
156
 
156
157
 
157
- @dataclass
158
158
  class Error:
159
159
  """A dataclass that stores information about an error that occurred.
160
160
 
@@ -166,25 +166,35 @@ class Error:
166
166
  A reason for why the error arose (e.g. an exception stack-trace)
167
167
  """
168
168
 
169
- _code: int
170
- _reason: str | None = None
171
-
172
169
  def __init__(self, code: int, reason: str | None = None) -> None:
173
- self._code = code
174
- self._reason = reason
170
+ var_dict = {
171
+ "_code": code,
172
+ "_reason": reason,
173
+ }
174
+ self.__dict__.update(var_dict)
175
175
 
176
176
  @property
177
177
  def code(self) -> int:
178
178
  """Error code."""
179
- return self._code
179
+ return cast(int, self.__dict__["_code"])
180
180
 
181
181
  @property
182
182
  def reason(self) -> str | None:
183
183
  """Reason reported about the error."""
184
- return self._reason
184
+ return cast(Optional[str], self.__dict__["_reason"])
185
+
186
+ def __repr__(self) -> str:
187
+ """Return a string representation of this instance."""
188
+ view = ", ".join([f"{k.lstrip('_')}={v!r}" for k, v in self.__dict__.items()])
189
+ return f"{self.__class__.__qualname__}({view})"
190
+
191
+ def __eq__(self, other: object) -> bool:
192
+ """Compare two instances of the class."""
193
+ if not isinstance(other, self.__class__):
194
+ raise NotImplementedError
195
+ return self.__dict__ == other.__dict__
185
196
 
186
197
 
187
- @dataclass
188
198
  class Message:
189
199
  """State of your application from the viewpoint of the entity using it.
190
200
 
@@ -200,105 +210,108 @@ class Message:
200
210
  when processing another message.
201
211
  """
202
212
 
203
- _metadata: Metadata
204
- _content: RecordSet | None = None
205
- _error: Error | None = None
206
-
207
213
  def __init__(
208
214
  self,
209
215
  metadata: Metadata,
210
216
  content: RecordSet | None = None,
211
217
  error: Error | None = None,
212
218
  ) -> None:
213
- self._metadata = metadata
214
-
215
219
  if not (content is None) ^ (error is None):
216
220
  raise ValueError("Either `content` or `error` must be set, but not both.")
217
221
 
218
- self._content = content
219
- self._error = error
222
+ metadata.created_at = time.time() # Set the message creation timestamp
223
+ var_dict = {
224
+ "_metadata": metadata,
225
+ "_content": content,
226
+ "_error": error,
227
+ }
228
+ self.__dict__.update(var_dict)
220
229
 
221
230
  @property
222
231
  def metadata(self) -> Metadata:
223
232
  """A dataclass including information about the message to be executed."""
224
- return self._metadata
233
+ return cast(Metadata, self.__dict__["_metadata"])
225
234
 
226
235
  @property
227
236
  def content(self) -> RecordSet:
228
237
  """The content of this message."""
229
- if self._content is None:
238
+ if self.__dict__["_content"] is None:
230
239
  raise ValueError(
231
240
  "Message content is None. Use <message>.has_content() "
232
241
  "to check if a message has content."
233
242
  )
234
- return self._content
243
+ return cast(RecordSet, self.__dict__["_content"])
235
244
 
236
245
  @content.setter
237
246
  def content(self, value: RecordSet) -> None:
238
247
  """Set content."""
239
- if self._error is None:
240
- self._content = value
248
+ if self.__dict__["_error"] is None:
249
+ self.__dict__["_content"] = value
241
250
  else:
242
251
  raise ValueError("A message with an error set cannot have content.")
243
252
 
244
253
  @property
245
254
  def error(self) -> Error:
246
255
  """Error captured by this message."""
247
- if self._error is None:
256
+ if self.__dict__["_error"] is None:
248
257
  raise ValueError(
249
258
  "Message error is None. Use <message>.has_error() "
250
259
  "to check first if a message carries an error."
251
260
  )
252
- return self._error
261
+ return cast(Error, self.__dict__["_error"])
253
262
 
254
263
  @error.setter
255
264
  def error(self, value: Error) -> None:
256
265
  """Set error."""
257
266
  if self.has_content():
258
267
  raise ValueError("A message with content set cannot carry an error.")
259
- self._error = value
268
+ self.__dict__["_error"] = value
260
269
 
261
270
  def has_content(self) -> bool:
262
271
  """Return True if message has content, else False."""
263
- return self._content is not None
272
+ return self.__dict__["_content"] is not None
264
273
 
265
274
  def has_error(self) -> bool:
266
275
  """Return True if message has an error, else False."""
267
- return self._error is not None
268
-
269
- def _create_reply_metadata(self, ttl: str) -> Metadata:
270
- """Construct metadata for a reply message."""
271
- return Metadata(
272
- run_id=self.metadata.run_id,
273
- message_id="",
274
- src_node_id=self.metadata.dst_node_id,
275
- dst_node_id=self.metadata.src_node_id,
276
- reply_to_message=self.metadata.message_id,
277
- group_id=self.metadata.group_id,
278
- ttl=ttl,
279
- message_type=self.metadata.message_type,
280
- partition_id=self.metadata.partition_id,
281
- )
276
+ return self.__dict__["_error"] is not None
282
277
 
283
- def create_error_reply(
284
- self,
285
- error: Error,
286
- ttl: str,
287
- ) -> Message:
278
+ def create_error_reply(self, error: Error, ttl: float | None = None) -> Message:
288
279
  """Construct a reply message indicating an error happened.
289
280
 
290
281
  Parameters
291
282
  ----------
292
283
  error : Error
293
284
  The error that was encountered.
294
- ttl : str
295
- Time-to-live for this message.
285
+ ttl : Optional[float] (default: None)
286
+ Time-to-live for this message in seconds. If unset, it will be set based
287
+ on the remaining time for the received message before it expires. This
288
+ follows the equation:
289
+
290
+ ttl = msg.meta.ttl - (reply.meta.created_at - msg.meta.created_at)
296
291
  """
292
+ if ttl:
293
+ warnings.warn(
294
+ "A custom TTL was set, but note that the SuperLink does not enforce "
295
+ "the TTL yet. The SuperLink will start enforcing the TTL in a future "
296
+ "version of Flower.",
297
+ stacklevel=2,
298
+ )
299
+ # If no TTL passed, use default for message creation (will update after
300
+ # message creation)
301
+ ttl_ = DEFAULT_TTL if ttl is None else ttl
297
302
  # Create reply with error
298
- message = Message(metadata=self._create_reply_metadata(ttl), error=error)
303
+ message = Message(metadata=_create_reply_metadata(self, ttl_), error=error)
304
+
305
+ if ttl is None:
306
+ # Set TTL equal to the remaining time for the received message to expire
307
+ ttl = self.metadata.ttl - (
308
+ message.metadata.created_at - self.metadata.created_at
309
+ )
310
+ message.metadata.ttl = ttl
311
+
299
312
  return message
300
313
 
301
- def create_reply(self, content: RecordSet, ttl: str) -> Message:
314
+ def create_reply(self, content: RecordSet, ttl: float | None = None) -> Message:
302
315
  """Create a reply to this message with specified content and TTL.
303
316
 
304
317
  The method generates a new `Message` as a reply to this message.
@@ -309,15 +322,64 @@ class Message:
309
322
  ----------
310
323
  content : RecordSet
311
324
  The content for the reply message.
312
- ttl : str
313
- Time-to-live for this message.
325
+ ttl : Optional[float] (default: None)
326
+ Time-to-live for this message in seconds. If unset, it will be set based
327
+ on the remaining time for the received message before it expires. This
328
+ follows the equation:
329
+
330
+ ttl = msg.meta.ttl - (reply.meta.created_at - msg.meta.created_at)
314
331
 
315
332
  Returns
316
333
  -------
317
334
  Message
318
335
  A new `Message` instance representing the reply.
319
336
  """
320
- return Message(
321
- metadata=self._create_reply_metadata(ttl),
337
+ if ttl:
338
+ warnings.warn(
339
+ "A custom TTL was set, but note that the SuperLink does not enforce "
340
+ "the TTL yet. The SuperLink will start enforcing the TTL in a future "
341
+ "version of Flower.",
342
+ stacklevel=2,
343
+ )
344
+ # If no TTL passed, use default for message creation (will update after
345
+ # message creation)
346
+ ttl_ = DEFAULT_TTL if ttl is None else ttl
347
+
348
+ message = Message(
349
+ metadata=_create_reply_metadata(self, ttl_),
322
350
  content=content,
323
351
  )
352
+
353
+ if ttl is None:
354
+ # Set TTL equal to the remaining time for the received message to expire
355
+ ttl = self.metadata.ttl - (
356
+ message.metadata.created_at - self.metadata.created_at
357
+ )
358
+ message.metadata.ttl = ttl
359
+
360
+ return message
361
+
362
+ def __repr__(self) -> str:
363
+ """Return a string representation of this instance."""
364
+ view = ", ".join(
365
+ [
366
+ f"{k.lstrip('_')}={v!r}"
367
+ for k, v in self.__dict__.items()
368
+ if v is not None
369
+ ]
370
+ )
371
+ return f"{self.__class__.__qualname__}({view})"
372
+
373
+
374
+ def _create_reply_metadata(msg: Message, ttl: float) -> Metadata:
375
+ """Construct metadata for a reply message."""
376
+ return Metadata(
377
+ run_id=msg.metadata.run_id,
378
+ message_id="",
379
+ src_node_id=msg.metadata.dst_node_id,
380
+ dst_node_id=msg.metadata.src_node_id,
381
+ reply_to_message=msg.metadata.message_id,
382
+ group_id=msg.metadata.group_id,
383
+ ttl=ttl,
384
+ message_type=msg.metadata.message_type,
385
+ )
flwr/common/object_ref.py CHANGED
@@ -17,8 +17,13 @@
17
17
 
18
18
  import ast
19
19
  import importlib
20
+ import sys
20
21
  from importlib.util import find_spec
21
- from typing import Any, Optional, Tuple, Type
22
+ from logging import WARN
23
+ from pathlib import Path
24
+ from typing import Any, Optional, Tuple, Type, Union
25
+
26
+ from .logger import log
22
27
 
23
28
  OBJECT_REF_HELP_STR = """
24
29
  \n\nThe object reference string should have the form <module>:<attribute>. Valid
@@ -28,21 +33,41 @@ attribute.
28
33
  """
29
34
 
30
35
 
36
+ _current_sys_path: Optional[str] = None
37
+
38
+
31
39
  def validate(
32
40
  module_attribute_str: str,
41
+ check_module: bool = True,
42
+ project_dir: Optional[Union[str, Path]] = None,
33
43
  ) -> Tuple[bool, Optional[str]]:
34
44
  """Validate object reference.
35
45
 
36
- The object reference string should have the form <module>:<attribute>. Valid
37
- examples include `client:app` and `project.package.module:wrapper.app`. It must
38
- refer to a module on the PYTHONPATH and the module needs to have the specified
39
- attribute.
46
+ Parameters
47
+ ----------
48
+ module_attribute_str : str
49
+ The reference to the object. It should have the form `<module>:<attribute>`.
50
+ Valid examples include `client:app` and `project.package.module:wrapper.app`.
51
+ It must refer to a module on the PYTHONPATH or in the provided `project_dir`
52
+ and the module needs to have the specified attribute.
53
+ check_module : bool (default: True)
54
+ Flag indicating whether to verify the existence of the module and the
55
+ specified attribute within it.
56
+ project_dir : Optional[Union[str, Path]] (default: None)
57
+ The directory containing the module. If None, the current working directory
58
+ is used. If `check_module` is True, the `project_dir` will be inserted into
59
+ the system path, and the previously inserted `project_dir` will be removed.
40
60
 
41
61
  Returns
42
62
  -------
43
63
  Tuple[bool, Optional[str]]
44
64
  A boolean indicating whether an object reference is valid and
45
65
  the reason why it might not be.
66
+
67
+ Note
68
+ ----
69
+ This function will modify `sys.path` by inserting the provided `project_dir`
70
+ and removing the previously inserted `project_dir`.
46
71
  """
47
72
  module_str, _, attributes_str = module_attribute_str.partition(":")
48
73
  if not module_str:
@@ -56,15 +81,21 @@ def validate(
56
81
  f"Missing attribute in {module_attribute_str}{OBJECT_REF_HELP_STR}",
57
82
  )
58
83
 
59
- # Load module
60
- module = find_spec(module_str)
61
- if module and module.origin:
62
- if not _find_attribute_in_module(module.origin, attributes_str):
63
- return (
64
- False,
65
- f"Unable to find attribute {attributes_str} in module {module_str}"
66
- f"{OBJECT_REF_HELP_STR}",
67
- )
84
+ if check_module:
85
+ # Set the system path
86
+ _set_sys_path(project_dir)
87
+
88
+ # Load module
89
+ module = find_spec(module_str)
90
+ if module and module.origin:
91
+ if not _find_attribute_in_module(module.origin, attributes_str):
92
+ return (
93
+ False,
94
+ f"Unable to find attribute {attributes_str} in module {module_str}"
95
+ f"{OBJECT_REF_HELP_STR}",
96
+ )
97
+ return (True, None)
98
+ else:
68
99
  return (True, None)
69
100
 
70
101
  return (
@@ -73,44 +104,114 @@ def validate(
73
104
  )
74
105
 
75
106
 
76
- def load_app(
107
+ def load_app( # pylint: disable= too-many-branches
77
108
  module_attribute_str: str,
78
109
  error_type: Type[Exception],
110
+ project_dir: Optional[Union[str, Path]] = None,
79
111
  ) -> Any:
80
112
  """Return the object specified in a module attribute string.
81
113
 
82
- The module/attribute string should have the form <module>:<attribute>. Valid
83
- examples include `client:app` and `project.package.module:wrapper.app`. It must
84
- refer to a module on the PYTHONPATH, the module needs to have the specified
85
- attribute.
114
+ Parameters
115
+ ----------
116
+ module_attribute_str : str
117
+ The reference to the object. It should have the form `<module>:<attribute>`.
118
+ Valid examples include `client:app` and `project.package.module:wrapper.app`.
119
+ It must refer to a module on the PYTHONPATH or in the provided `project_dir`
120
+ and the module needs to have the specified attribute.
121
+ error_type : Type[Exception]
122
+ The type of exception to be raised if the provided `module_attribute_str` is
123
+ in an invalid format.
124
+ project_dir : Optional[Union[str, Path]], optional (default=None)
125
+ The directory containing the module. If None, the current working directory
126
+ is used. The `project_dir` will be inserted into the system path, and the
127
+ previously inserted `project_dir` will be removed.
128
+
129
+ Returns
130
+ -------
131
+ Any
132
+ The object specified by the module attribute string.
133
+
134
+ Note
135
+ ----
136
+ This function will modify `sys.path` by inserting the provided `project_dir`
137
+ and removing the previously inserted `project_dir`.
86
138
  """
87
- valid, error_msg = validate(module_attribute_str)
139
+ valid, error_msg = validate(module_attribute_str, check_module=False)
88
140
  if not valid and error_msg:
89
141
  raise error_type(error_msg) from None
90
142
 
91
143
  module_str, _, attributes_str = module_attribute_str.partition(":")
92
144
 
93
145
  try:
94
- module = importlib.import_module(module_str)
95
- except ModuleNotFoundError:
146
+ _set_sys_path(project_dir)
147
+
148
+ if module_str not in sys.modules:
149
+ module = importlib.import_module(module_str)
150
+ # Hack: `tabnet` does not work with `importlib.reload`
151
+ elif "tabnet" in sys.modules:
152
+ log(
153
+ WARN,
154
+ "Cannot reload module `%s` from disk due to compatibility issues "
155
+ "with the `tabnet` library. The module will be loaded from the "
156
+ "cache instead. If you experience issues, consider restarting "
157
+ "the application.",
158
+ module_str,
159
+ )
160
+ module = sys.modules[module_str]
161
+ else:
162
+ module = sys.modules[module_str]
163
+
164
+ if project_dir is None:
165
+ project_dir = Path.cwd()
166
+
167
+ # Reload cached modules in the project directory
168
+ for m in list(sys.modules.values()):
169
+ path: Optional[str] = getattr(m, "__file__", None)
170
+ if path is not None and path.startswith(str(project_dir)):
171
+ importlib.reload(m)
172
+
173
+ except ModuleNotFoundError as err:
96
174
  raise error_type(
97
175
  f"Unable to load module {module_str}{OBJECT_REF_HELP_STR}",
98
- ) from None
176
+ ) from err
99
177
 
100
178
  # Recursively load attribute
101
179
  attribute = module
102
180
  try:
103
181
  for attribute_str in attributes_str.split("."):
104
182
  attribute = getattr(attribute, attribute_str)
105
- except AttributeError:
183
+ except AttributeError as err:
106
184
  raise error_type(
107
185
  f"Unable to load attribute {attributes_str} from module {module_str}"
108
186
  f"{OBJECT_REF_HELP_STR}",
109
- ) from None
187
+ ) from err
110
188
 
111
189
  return attribute
112
190
 
113
191
 
192
+ def _set_sys_path(directory: Optional[Union[str, Path]]) -> None:
193
+ """Set the system path."""
194
+ if directory is None:
195
+ directory = Path.cwd()
196
+ else:
197
+ directory = Path(directory).absolute()
198
+
199
+ # If the directory has already been added to `sys.path`, return
200
+ if str(directory) in sys.path:
201
+ return
202
+
203
+ # Remove the old path if it exists and is not `""`.
204
+ global _current_sys_path # pylint: disable=global-statement
205
+ if _current_sys_path is not None:
206
+ sys.path.remove(_current_sys_path)
207
+
208
+ # Add the new path to sys.path
209
+ sys.path.insert(0, str(directory))
210
+
211
+ # Update the current_sys_path
212
+ _current_sys_path = str(directory)
213
+
214
+
114
215
  def _find_attribute_in_module(file_path: str, attribute_name: str) -> bool:
115
216
  """Check if attribute_name exists in module's abstract symbolic tree."""
116
217
  with open(file_path, encoding="utf-8") as file:
@@ -22,9 +22,9 @@ from .recordset import RecordSet
22
22
 
23
23
  __all__ = [
24
24
  "Array",
25
- "array_from_numpy",
26
25
  "ConfigsRecord",
27
26
  "MetricsRecord",
28
27
  "ParametersRecord",
29
28
  "RecordSet",
29
+ "array_from_numpy",
30
30
  ]
@@ -82,7 +82,6 @@ def _check_value(value: Array) -> None:
82
82
  )
83
83
 
84
84
 
85
- @dataclass
86
85
  class ParametersRecord(TypedDict[str, Array]):
87
86
  """Parameters record.
88
87