flwr-nightly 1.8.0.dev20240315__py3-none-any.whl → 1.15.0.dev20250115__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (312) hide show
  1. flwr/cli/app.py +16 -2
  2. flwr/cli/build.py +181 -0
  3. flwr/cli/cli_user_auth_interceptor.py +90 -0
  4. flwr/cli/config_utils.py +343 -0
  5. flwr/cli/example.py +4 -1
  6. flwr/cli/install.py +253 -0
  7. flwr/cli/log.py +182 -0
  8. flwr/{server/superlink/state → cli/login}/__init__.py +4 -10
  9. flwr/cli/login/login.py +88 -0
  10. flwr/cli/ls.py +327 -0
  11. flwr/cli/new/__init__.py +1 -0
  12. flwr/cli/new/new.py +210 -66
  13. flwr/cli/new/templates/app/.gitignore.tpl +163 -0
  14. flwr/cli/new/templates/app/LICENSE.tpl +202 -0
  15. flwr/cli/new/templates/app/README.baseline.md.tpl +127 -0
  16. flwr/cli/new/templates/app/README.flowertune.md.tpl +66 -0
  17. flwr/cli/new/templates/app/README.md.tpl +16 -32
  18. flwr/cli/new/templates/app/code/__init__.baseline.py.tpl +1 -0
  19. flwr/cli/new/templates/app/code/__init__.py.tpl +1 -1
  20. flwr/cli/new/templates/app/code/client.baseline.py.tpl +58 -0
  21. flwr/cli/new/templates/app/code/client.huggingface.py.tpl +55 -0
  22. flwr/cli/new/templates/app/code/client.jax.py.tpl +50 -0
  23. flwr/cli/new/templates/app/code/client.mlx.py.tpl +73 -0
  24. flwr/cli/new/templates/app/code/client.numpy.py.tpl +7 -7
  25. flwr/cli/new/templates/app/code/client.pytorch.py.tpl +30 -21
  26. flwr/cli/new/templates/app/code/client.sklearn.py.tpl +63 -0
  27. flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +57 -1
  28. flwr/cli/new/templates/app/code/dataset.baseline.py.tpl +36 -0
  29. flwr/cli/new/templates/app/code/flwr_tune/__init__.py +15 -0
  30. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +126 -0
  31. flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +87 -0
  32. flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +78 -0
  33. flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +94 -0
  34. flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +83 -0
  35. flwr/cli/new/templates/app/code/model.baseline.py.tpl +80 -0
  36. flwr/cli/new/templates/app/code/server.baseline.py.tpl +46 -0
  37. flwr/cli/new/templates/app/code/server.huggingface.py.tpl +38 -0
  38. flwr/cli/new/templates/app/code/server.jax.py.tpl +26 -0
  39. flwr/cli/new/templates/app/code/server.mlx.py.tpl +31 -0
  40. flwr/cli/new/templates/app/code/server.numpy.py.tpl +22 -9
  41. flwr/cli/new/templates/app/code/server.pytorch.py.tpl +21 -18
  42. flwr/cli/new/templates/app/code/server.sklearn.py.tpl +36 -0
  43. flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +29 -1
  44. flwr/cli/new/templates/app/code/strategy.baseline.py.tpl +1 -0
  45. flwr/cli/new/templates/app/code/task.huggingface.py.tpl +102 -0
  46. flwr/cli/new/templates/app/code/task.jax.py.tpl +57 -0
  47. flwr/cli/new/templates/app/code/task.mlx.py.tpl +102 -0
  48. flwr/cli/new/templates/app/code/task.numpy.py.tpl +7 -0
  49. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +29 -24
  50. flwr/cli/new/templates/app/code/task.sklearn.py.tpl +67 -0
  51. flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +53 -0
  52. flwr/cli/new/templates/app/code/utils.baseline.py.tpl +1 -0
  53. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +138 -0
  54. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +68 -0
  55. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +46 -0
  56. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +35 -0
  57. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +39 -0
  58. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +25 -12
  59. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +29 -14
  60. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +35 -0
  61. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +29 -14
  62. flwr/cli/run/__init__.py +1 -0
  63. flwr/cli/run/run.py +212 -34
  64. flwr/cli/stop.py +130 -0
  65. flwr/cli/utils.py +240 -5
  66. flwr/client/__init__.py +3 -2
  67. flwr/client/app.py +432 -255
  68. flwr/client/client.py +1 -11
  69. flwr/client/client_app.py +74 -13
  70. flwr/client/clientapp/__init__.py +22 -0
  71. flwr/client/clientapp/app.py +259 -0
  72. flwr/client/clientapp/clientappio_servicer.py +244 -0
  73. flwr/client/clientapp/utils.py +115 -0
  74. flwr/client/dpfedavg_numpy_client.py +7 -8
  75. flwr/client/grpc_adapter_client/__init__.py +15 -0
  76. flwr/client/grpc_adapter_client/connection.py +98 -0
  77. flwr/client/grpc_client/connection.py +21 -7
  78. flwr/client/grpc_rere_client/__init__.py +1 -1
  79. flwr/client/grpc_rere_client/client_interceptor.py +176 -0
  80. flwr/client/grpc_rere_client/connection.py +163 -56
  81. flwr/client/grpc_rere_client/grpc_adapter.py +167 -0
  82. flwr/client/heartbeat.py +74 -0
  83. flwr/client/message_handler/__init__.py +1 -1
  84. flwr/client/message_handler/message_handler.py +10 -11
  85. flwr/client/mod/__init__.py +5 -5
  86. flwr/client/mod/centraldp_mods.py +4 -2
  87. flwr/client/mod/comms_mods.py +5 -4
  88. flwr/client/mod/localdp_mod.py +10 -5
  89. flwr/client/mod/secure_aggregation/__init__.py +1 -1
  90. flwr/client/mod/secure_aggregation/secaggplus_mod.py +26 -26
  91. flwr/client/mod/utils.py +2 -4
  92. flwr/client/nodestate/__init__.py +26 -0
  93. flwr/client/nodestate/in_memory_nodestate.py +38 -0
  94. flwr/client/nodestate/nodestate.py +31 -0
  95. flwr/client/nodestate/nodestate_factory.py +38 -0
  96. flwr/client/numpy_client.py +8 -31
  97. flwr/client/rest_client/__init__.py +1 -1
  98. flwr/client/rest_client/connection.py +199 -176
  99. flwr/client/run_info_store.py +112 -0
  100. flwr/client/supernode/__init__.py +24 -0
  101. flwr/client/supernode/app.py +321 -0
  102. flwr/client/typing.py +1 -0
  103. flwr/common/__init__.py +17 -11
  104. flwr/common/address.py +47 -3
  105. flwr/common/args.py +153 -0
  106. flwr/common/auth_plugin/__init__.py +24 -0
  107. flwr/common/auth_plugin/auth_plugin.py +121 -0
  108. flwr/common/config.py +243 -0
  109. flwr/common/constant.py +135 -1
  110. flwr/common/context.py +32 -2
  111. flwr/common/date.py +22 -4
  112. flwr/common/differential_privacy.py +2 -2
  113. flwr/common/dp.py +2 -4
  114. flwr/common/exit_handlers.py +3 -3
  115. flwr/common/grpc.py +164 -5
  116. flwr/common/logger.py +230 -12
  117. flwr/common/message.py +191 -106
  118. flwr/common/object_ref.py +179 -44
  119. flwr/common/pyproject.py +1 -0
  120. flwr/common/record/__init__.py +2 -1
  121. flwr/common/record/configsrecord.py +58 -18
  122. flwr/common/record/metricsrecord.py +57 -17
  123. flwr/common/record/parametersrecord.py +88 -20
  124. flwr/common/record/recordset.py +153 -30
  125. flwr/common/record/typeddict.py +30 -55
  126. flwr/common/recordset_compat.py +31 -12
  127. flwr/common/retry_invoker.py +123 -30
  128. flwr/common/secure_aggregation/__init__.py +1 -1
  129. flwr/common/secure_aggregation/crypto/__init__.py +1 -1
  130. flwr/common/secure_aggregation/crypto/shamir.py +11 -11
  131. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +68 -4
  132. flwr/common/secure_aggregation/ndarrays_arithmetic.py +17 -17
  133. flwr/common/secure_aggregation/quantization.py +8 -8
  134. flwr/common/secure_aggregation/secaggplus_constants.py +1 -1
  135. flwr/common/secure_aggregation/secaggplus_utils.py +10 -12
  136. flwr/common/serde.py +304 -23
  137. flwr/common/telemetry.py +65 -29
  138. flwr/common/typing.py +120 -19
  139. flwr/common/version.py +17 -3
  140. flwr/proto/clientappio_pb2.py +45 -0
  141. flwr/proto/clientappio_pb2.pyi +132 -0
  142. flwr/proto/clientappio_pb2_grpc.py +135 -0
  143. flwr/proto/clientappio_pb2_grpc.pyi +53 -0
  144. flwr/proto/exec_pb2.py +62 -0
  145. flwr/proto/exec_pb2.pyi +212 -0
  146. flwr/proto/exec_pb2_grpc.py +237 -0
  147. flwr/proto/exec_pb2_grpc.pyi +93 -0
  148. flwr/proto/fab_pb2.py +31 -0
  149. flwr/proto/fab_pb2.pyi +65 -0
  150. flwr/proto/fab_pb2_grpc.py +4 -0
  151. flwr/proto/fab_pb2_grpc.pyi +4 -0
  152. flwr/proto/fleet_pb2.py +42 -23
  153. flwr/proto/fleet_pb2.pyi +123 -1
  154. flwr/proto/fleet_pb2_grpc.py +170 -0
  155. flwr/proto/fleet_pb2_grpc.pyi +61 -0
  156. flwr/proto/grpcadapter_pb2.py +32 -0
  157. flwr/proto/grpcadapter_pb2.pyi +43 -0
  158. flwr/proto/grpcadapter_pb2_grpc.py +66 -0
  159. flwr/proto/grpcadapter_pb2_grpc.pyi +24 -0
  160. flwr/proto/log_pb2.py +29 -0
  161. flwr/proto/log_pb2.pyi +39 -0
  162. flwr/proto/log_pb2_grpc.py +4 -0
  163. flwr/proto/log_pb2_grpc.pyi +4 -0
  164. flwr/proto/message_pb2.py +41 -0
  165. flwr/proto/message_pb2.pyi +128 -0
  166. flwr/proto/message_pb2_grpc.py +4 -0
  167. flwr/proto/message_pb2_grpc.pyi +4 -0
  168. flwr/proto/node_pb2.py +2 -2
  169. flwr/proto/node_pb2.pyi +1 -4
  170. flwr/proto/recordset_pb2.py +35 -33
  171. flwr/proto/recordset_pb2.pyi +40 -14
  172. flwr/proto/run_pb2.py +64 -0
  173. flwr/proto/run_pb2.pyi +268 -0
  174. flwr/proto/run_pb2_grpc.py +4 -0
  175. flwr/proto/run_pb2_grpc.pyi +4 -0
  176. flwr/proto/serverappio_pb2.py +52 -0
  177. flwr/proto/{driver_pb2.pyi → serverappio_pb2.pyi} +62 -20
  178. flwr/proto/serverappio_pb2_grpc.py +410 -0
  179. flwr/proto/serverappio_pb2_grpc.pyi +160 -0
  180. flwr/proto/simulationio_pb2.py +38 -0
  181. flwr/proto/simulationio_pb2.pyi +65 -0
  182. flwr/proto/simulationio_pb2_grpc.py +239 -0
  183. flwr/proto/simulationio_pb2_grpc.pyi +94 -0
  184. flwr/proto/task_pb2.py +7 -8
  185. flwr/proto/task_pb2.pyi +8 -5
  186. flwr/proto/transport_pb2.py +8 -8
  187. flwr/proto/transport_pb2.pyi +9 -6
  188. flwr/server/__init__.py +2 -10
  189. flwr/server/app.py +579 -402
  190. flwr/server/client_manager.py +8 -6
  191. flwr/server/compat/app.py +6 -62
  192. flwr/server/compat/app_utils.py +14 -9
  193. flwr/server/compat/driver_client_proxy.py +25 -59
  194. flwr/server/compat/legacy_context.py +5 -4
  195. flwr/server/driver/__init__.py +2 -0
  196. flwr/server/driver/driver.py +36 -131
  197. flwr/server/driver/grpc_driver.py +220 -81
  198. flwr/server/driver/inmemory_driver.py +183 -0
  199. flwr/server/history.py +28 -29
  200. flwr/server/run_serverapp.py +15 -126
  201. flwr/server/server.py +50 -44
  202. flwr/server/server_app.py +59 -10
  203. flwr/server/serverapp/__init__.py +22 -0
  204. flwr/server/serverapp/app.py +256 -0
  205. flwr/server/serverapp_components.py +52 -0
  206. flwr/server/strategy/__init__.py +2 -2
  207. flwr/server/strategy/aggregate.py +37 -23
  208. flwr/server/strategy/bulyan.py +9 -9
  209. flwr/server/strategy/dp_adaptive_clipping.py +25 -25
  210. flwr/server/strategy/dp_fixed_clipping.py +23 -22
  211. flwr/server/strategy/dpfedavg_adaptive.py +8 -8
  212. flwr/server/strategy/dpfedavg_fixed.py +13 -12
  213. flwr/server/strategy/fault_tolerant_fedavg.py +11 -11
  214. flwr/server/strategy/fedadagrad.py +9 -9
  215. flwr/server/strategy/fedadam.py +20 -10
  216. flwr/server/strategy/fedavg.py +16 -16
  217. flwr/server/strategy/fedavg_android.py +17 -17
  218. flwr/server/strategy/fedavgm.py +9 -9
  219. flwr/server/strategy/fedmedian.py +5 -5
  220. flwr/server/strategy/fedopt.py +6 -6
  221. flwr/server/strategy/fedprox.py +7 -7
  222. flwr/server/strategy/fedtrimmedavg.py +8 -8
  223. flwr/server/strategy/fedxgb_bagging.py +12 -12
  224. flwr/server/strategy/fedxgb_cyclic.py +10 -10
  225. flwr/server/strategy/fedxgb_nn_avg.py +6 -6
  226. flwr/server/strategy/fedyogi.py +9 -9
  227. flwr/server/strategy/krum.py +9 -9
  228. flwr/server/strategy/qfedavg.py +16 -16
  229. flwr/server/strategy/strategy.py +10 -10
  230. flwr/server/superlink/driver/__init__.py +2 -2
  231. flwr/server/superlink/driver/serverappio_grpc.py +61 -0
  232. flwr/server/superlink/driver/serverappio_servicer.py +361 -0
  233. flwr/server/superlink/ffs/__init__.py +24 -0
  234. flwr/server/superlink/ffs/disk_ffs.py +108 -0
  235. flwr/server/superlink/ffs/ffs.py +79 -0
  236. flwr/server/superlink/ffs/ffs_factory.py +47 -0
  237. flwr/server/superlink/fleet/__init__.py +1 -1
  238. flwr/server/superlink/fleet/grpc_adapter/__init__.py +15 -0
  239. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +162 -0
  240. flwr/server/superlink/fleet/grpc_bidi/__init__.py +1 -1
  241. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +4 -2
  242. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +3 -2
  243. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +1 -1
  244. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +5 -154
  245. flwr/server/superlink/fleet/grpc_rere/__init__.py +1 -1
  246. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +120 -13
  247. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +228 -0
  248. flwr/server/superlink/fleet/message_handler/__init__.py +1 -1
  249. flwr/server/superlink/fleet/message_handler/message_handler.py +156 -13
  250. flwr/server/superlink/fleet/rest_rere/__init__.py +1 -1
  251. flwr/server/superlink/fleet/rest_rere/rest_api.py +119 -81
  252. flwr/server/superlink/fleet/vce/__init__.py +1 -0
  253. flwr/server/superlink/fleet/vce/backend/__init__.py +4 -4
  254. flwr/server/superlink/fleet/vce/backend/backend.py +8 -9
  255. flwr/server/superlink/fleet/vce/backend/raybackend.py +87 -68
  256. flwr/server/superlink/fleet/vce/vce_api.py +208 -146
  257. flwr/server/superlink/linkstate/__init__.py +28 -0
  258. flwr/server/superlink/linkstate/in_memory_linkstate.py +569 -0
  259. flwr/server/superlink/linkstate/linkstate.py +376 -0
  260. flwr/server/superlink/{state/state_factory.py → linkstate/linkstate_factory.py} +19 -10
  261. flwr/server/superlink/linkstate/sqlite_linkstate.py +1196 -0
  262. flwr/server/superlink/linkstate/utils.py +399 -0
  263. flwr/server/superlink/simulation/__init__.py +15 -0
  264. flwr/server/superlink/simulation/simulationio_grpc.py +65 -0
  265. flwr/server/superlink/simulation/simulationio_servicer.py +186 -0
  266. flwr/server/superlink/utils.py +65 -0
  267. flwr/server/typing.py +2 -0
  268. flwr/server/utils/__init__.py +1 -1
  269. flwr/server/utils/tensorboard.py +5 -5
  270. flwr/server/utils/validator.py +40 -45
  271. flwr/server/workflow/default_workflows.py +70 -26
  272. flwr/server/workflow/secure_aggregation/secagg_workflow.py +1 -0
  273. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +40 -27
  274. flwr/simulation/__init__.py +12 -5
  275. flwr/simulation/app.py +247 -315
  276. flwr/simulation/legacy_app.py +404 -0
  277. flwr/simulation/ray_transport/__init__.py +1 -1
  278. flwr/simulation/ray_transport/ray_actor.py +42 -67
  279. flwr/simulation/ray_transport/ray_client_proxy.py +37 -17
  280. flwr/simulation/ray_transport/utils.py +1 -0
  281. flwr/simulation/run_simulation.py +306 -163
  282. flwr/simulation/simulationio_connection.py +89 -0
  283. flwr/superexec/__init__.py +15 -0
  284. flwr/superexec/app.py +59 -0
  285. flwr/superexec/deployment.py +188 -0
  286. flwr/superexec/exec_grpc.py +80 -0
  287. flwr/superexec/exec_servicer.py +231 -0
  288. flwr/superexec/exec_user_auth_interceptor.py +101 -0
  289. flwr/superexec/executor.py +96 -0
  290. flwr/superexec/simulation.py +124 -0
  291. {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.15.0.dev20250115.dist-info}/METADATA +33 -26
  292. flwr_nightly-1.15.0.dev20250115.dist-info/RECORD +328 -0
  293. flwr_nightly-1.15.0.dev20250115.dist-info/entry_points.txt +12 -0
  294. flwr/cli/flower_toml.py +0 -140
  295. flwr/cli/new/templates/app/flower.toml.tpl +0 -13
  296. flwr/cli/new/templates/app/requirements.numpy.txt.tpl +0 -2
  297. flwr/cli/new/templates/app/requirements.pytorch.txt.tpl +0 -4
  298. flwr/cli/new/templates/app/requirements.tensorflow.txt.tpl +0 -4
  299. flwr/client/node_state.py +0 -48
  300. flwr/client/node_state_tests.py +0 -65
  301. flwr/proto/driver_pb2.py +0 -44
  302. flwr/proto/driver_pb2_grpc.py +0 -169
  303. flwr/proto/driver_pb2_grpc.pyi +0 -66
  304. flwr/server/superlink/driver/driver_grpc.py +0 -54
  305. flwr/server/superlink/driver/driver_servicer.py +0 -129
  306. flwr/server/superlink/state/in_memory_state.py +0 -230
  307. flwr/server/superlink/state/sqlite_state.py +0 -630
  308. flwr/server/superlink/state/state.py +0 -154
  309. flwr_nightly-1.8.0.dev20240315.dist-info/RECORD +0 -211
  310. flwr_nightly-1.8.0.dev20240315.dist-info/entry_points.txt +0 -9
  311. {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.15.0.dev20250115.dist-info}/LICENSE +0 -0
  312. {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.15.0.dev20250115.dist-info}/WHEEL +0 -0
@@ -15,65 +15,188 @@
15
15
  """RecordSet."""
16
16
 
17
17
 
18
+ from __future__ import annotations
19
+
18
20
  from dataclasses import dataclass
19
- from typing import Callable, Dict, Optional, Type, TypeVar
21
+ from typing import cast
20
22
 
21
23
  from .configsrecord import ConfigsRecord
22
24
  from .metricsrecord import MetricsRecord
23
25
  from .parametersrecord import ParametersRecord
24
26
  from .typeddict import TypedDict
25
27
 
26
- T = TypeVar("T")
27
-
28
28
 
29
29
  @dataclass
30
- class RecordSet:
31
- """RecordSet stores groups of parameters, metrics and configs."""
30
+ class RecordSetData:
31
+ """Inner data container for the RecordSet class."""
32
32
 
33
- _parameters_records: TypedDict[str, ParametersRecord]
34
- _metrics_records: TypedDict[str, MetricsRecord]
35
- _configs_records: TypedDict[str, ConfigsRecord]
33
+ parameters_records: TypedDict[str, ParametersRecord]
34
+ metrics_records: TypedDict[str, MetricsRecord]
35
+ configs_records: TypedDict[str, ConfigsRecord]
36
36
 
37
37
  def __init__(
38
38
  self,
39
- parameters_records: Optional[Dict[str, ParametersRecord]] = None,
40
- metrics_records: Optional[Dict[str, MetricsRecord]] = None,
41
- configs_records: Optional[Dict[str, ConfigsRecord]] = None,
39
+ parameters_records: dict[str, ParametersRecord] | None = None,
40
+ metrics_records: dict[str, MetricsRecord] | None = None,
41
+ configs_records: dict[str, ConfigsRecord] | None = None,
42
42
  ) -> None:
43
- def _get_check_fn(__t: Type[T]) -> Callable[[T], None]:
44
- def _check_fn(__v: T) -> None:
45
- if not isinstance(__v, __t):
46
- raise TypeError(f"Expected `{__t}`, but `{type(__v)}` was passed.")
47
-
48
- return _check_fn
49
-
50
- self._parameters_records = TypedDict[str, ParametersRecord](
51
- _get_check_fn(str), _get_check_fn(ParametersRecord)
43
+ self.parameters_records = TypedDict[str, ParametersRecord](
44
+ self._check_fn_str, self._check_fn_params
52
45
  )
53
- self._metrics_records = TypedDict[str, MetricsRecord](
54
- _get_check_fn(str), _get_check_fn(MetricsRecord)
46
+ self.metrics_records = TypedDict[str, MetricsRecord](
47
+ self._check_fn_str, self._check_fn_metrics
55
48
  )
56
- self._configs_records = TypedDict[str, ConfigsRecord](
57
- _get_check_fn(str), _get_check_fn(ConfigsRecord)
49
+ self.configs_records = TypedDict[str, ConfigsRecord](
50
+ self._check_fn_str, self._check_fn_configs
58
51
  )
59
52
  if parameters_records is not None:
60
- self._parameters_records.update(parameters_records)
53
+ self.parameters_records.update(parameters_records)
61
54
  if metrics_records is not None:
62
- self._metrics_records.update(metrics_records)
55
+ self.metrics_records.update(metrics_records)
63
56
  if configs_records is not None:
64
- self._configs_records.update(configs_records)
57
+ self.configs_records.update(configs_records)
58
+
59
+ def _check_fn_str(self, key: str) -> None:
60
+ if not isinstance(key, str):
61
+ raise TypeError(
62
+ f"Expected `{str.__name__}`, but "
63
+ f"received `{type(key).__name__}` for the key."
64
+ )
65
+
66
+ def _check_fn_params(self, record: ParametersRecord) -> None:
67
+ if not isinstance(record, ParametersRecord):
68
+ raise TypeError(
69
+ f"Expected `{ParametersRecord.__name__}`, but "
70
+ f"received `{type(record).__name__}` for the value."
71
+ )
72
+
73
+ def _check_fn_metrics(self, record: MetricsRecord) -> None:
74
+ if not isinstance(record, MetricsRecord):
75
+ raise TypeError(
76
+ f"Expected `{MetricsRecord.__name__}`, but "
77
+ f"received `{type(record).__name__}` for the value."
78
+ )
79
+
80
+ def _check_fn_configs(self, record: ConfigsRecord) -> None:
81
+ if not isinstance(record, ConfigsRecord):
82
+ raise TypeError(
83
+ f"Expected `{ConfigsRecord.__name__}`, but "
84
+ f"received `{type(record).__name__}` for the value."
85
+ )
86
+
87
+
88
+ class RecordSet:
89
+ """RecordSet stores groups of parameters, metrics and configs.
90
+
91
+ A :code:`RecordSet` is the unified mechanism by which parameters,
92
+ metrics and configs can be either stored as part of a
93
+ `flwr.common.Context <flwr.common.Context.html>`_ in your apps
94
+ or communicated as part of a
95
+ `flwr.common.Message <flwr.common.Message.html>`_ between your apps.
96
+
97
+ Parameters
98
+ ----------
99
+ parameters_records : Optional[Dict[str, ParametersRecord]]
100
+ A dictionary of :code:`ParametersRecords` that can be used to record
101
+ and communicate model parameters and high-dimensional arrays.
102
+ metrics_records : Optional[Dict[str, MetricsRecord]]
103
+ A dictionary of :code:`MetricsRecord` that can be used to record
104
+ and communicate scalar-valued metrics that are the result of performing
105
+ and action, for example, by a :code:`ClientApp`.
106
+ configs_records : Optional[Dict[str, ConfigsRecord]]
107
+ A dictionary of :code:`ConfigsRecord` that can be used to record
108
+ and communicate configuration values to an entity (e.g. to a
109
+ :code:`ClientApp`)
110
+ for it to adjust how an action is performed.
111
+
112
+ Examples
113
+ --------
114
+ A :code:`RecordSet` can hold three types of records, each designed
115
+ with an specific purpose. What is common to all of them is that they
116
+ are Python dictionaries designed to ensure that each key-value pair
117
+ adheres to specified data types.
118
+
119
+ Let's see an example.
120
+
121
+ >>> from flwr.common import RecordSet
122
+ >>> from flwr.common import ConfigsRecord, MetricsRecord, ParametersRecord
123
+ >>>
124
+ >>> # Let's begin with an empty record
125
+ >>> my_recordset = RecordSet()
126
+ >>>
127
+ >>> # We can create a ConfigsRecord
128
+ >>> c_record = ConfigsRecord({"lr": 0.1, "batch-size": 128})
129
+ >>> # Adding it to the record_set would look like this
130
+ >>> my_recordset.configs_records["my_config"] = c_record
131
+ >>>
132
+ >>> # We can create a MetricsRecord following a similar process
133
+ >>> m_record = MetricsRecord({"accuracy": 0.93, "losses": [0.23, 0.1]})
134
+ >>> # Adding it to the record_set would look like this
135
+ >>> my_recordset.metrics_records["my_metrics"] = m_record
136
+
137
+ Adding a :code:`ParametersRecord` follows the same steps as above but first,
138
+ the array needs to be serialized and represented as a :code:`flwr.common.Array`.
139
+ If the array is a :code:`NumPy` array, you can use the built-in utility function
140
+ `array_from_numpy <flwr.common.array_from_numpy.html>`_. It is often possible to
141
+ convert an array first to :code:`NumPy` and then use the aforementioned function.
142
+
143
+ >>> from flwr.common import array_from_numpy
144
+ >>> # Creating a ParametersRecord would look like this
145
+ >>> arr_np = np.random.randn(3, 3)
146
+ >>>
147
+ >>> # You can use the built-in tool to serialize the array
148
+ >>> arr = array_from_numpy(arr_np)
149
+ >>>
150
+ >>> # Finally, create the record
151
+ >>> p_record = ParametersRecord({"my_array": arr})
152
+ >>>
153
+ >>> # Adding it to the record_set would look like this
154
+ >>> my_recordset.parameters_records["my_parameters"] = p_record
155
+
156
+ For additional examples on how to construct each of the records types shown
157
+ above, please refer to the documentation for :code:`ConfigsRecord`,
158
+ :code:`MetricsRecord` and :code:`ParametersRecord`.
159
+ """
160
+
161
+ def __init__(
162
+ self,
163
+ parameters_records: dict[str, ParametersRecord] | None = None,
164
+ metrics_records: dict[str, MetricsRecord] | None = None,
165
+ configs_records: dict[str, ConfigsRecord] | None = None,
166
+ ) -> None:
167
+ data = RecordSetData(
168
+ parameters_records=parameters_records,
169
+ metrics_records=metrics_records,
170
+ configs_records=configs_records,
171
+ )
172
+ self.__dict__["_data"] = data
65
173
 
66
174
  @property
67
175
  def parameters_records(self) -> TypedDict[str, ParametersRecord]:
68
176
  """Dictionary holding ParametersRecord instances."""
69
- return self._parameters_records
177
+ data = cast(RecordSetData, self.__dict__["_data"])
178
+ return data.parameters_records
70
179
 
71
180
  @property
72
181
  def metrics_records(self) -> TypedDict[str, MetricsRecord]:
73
182
  """Dictionary holding MetricsRecord instances."""
74
- return self._metrics_records
183
+ data = cast(RecordSetData, self.__dict__["_data"])
184
+ return data.metrics_records
75
185
 
76
186
  @property
77
187
  def configs_records(self) -> TypedDict[str, ConfigsRecord]:
78
188
  """Dictionary holding ConfigsRecord instances."""
79
- return self._configs_records
189
+ data = cast(RecordSetData, self.__dict__["_data"])
190
+ return data.configs_records
191
+
192
+ def __repr__(self) -> str:
193
+ """Return a string representation of this instance."""
194
+ flds = ("parameters_records", "metrics_records", "configs_records")
195
+ view = ", ".join([f"{fld}={getattr(self, fld)!r}" for fld in flds])
196
+ return f"{self.__class__.__qualname__}({view})"
197
+
198
+ def __eq__(self, other: object) -> bool:
199
+ """Compare two instances of the class."""
200
+ if not isinstance(other, self.__class__):
201
+ raise NotImplementedError
202
+ return self.__dict__ == other.__dict__
@@ -15,99 +15,74 @@
15
15
  """Typed dict base class for *Records."""
16
16
 
17
17
 
18
- from typing import Any, Callable, Dict, Generic, Iterator, Tuple, TypeVar, cast
18
+ from collections.abc import ItemsView, Iterator, KeysView, MutableMapping, ValuesView
19
+ from typing import Callable, Generic, TypeVar, cast
19
20
 
20
21
  K = TypeVar("K") # Key type
21
22
  V = TypeVar("V") # Value type
22
23
 
23
24
 
24
- class TypedDict(Generic[K, V]):
25
+ class TypedDict(MutableMapping[K, V], Generic[K, V]):
25
26
  """Typed dictionary."""
26
27
 
27
28
  def __init__(
28
29
  self, check_key_fn: Callable[[K], None], check_value_fn: Callable[[V], None]
29
30
  ):
30
- self._data: Dict[K, V] = {}
31
- self._check_key_fn = check_key_fn
32
- self._check_value_fn = check_value_fn
31
+ self.__dict__["_check_key_fn"] = check_key_fn
32
+ self.__dict__["_check_value_fn"] = check_value_fn
33
+ self.__dict__["_data"] = {}
33
34
 
34
35
  def __setitem__(self, key: K, value: V) -> None:
35
36
  """Set the given key to the given value after type checking."""
36
37
  # Check the types of key and value
37
- self._check_key_fn(key)
38
- self._check_value_fn(value)
38
+ cast(Callable[[K], None], self.__dict__["_check_key_fn"])(key)
39
+ cast(Callable[[V], None], self.__dict__["_check_value_fn"])(value)
40
+
39
41
  # Set key-value pair
40
- self._data[key] = value
42
+ cast(dict[K, V], self.__dict__["_data"])[key] = value
41
43
 
42
44
  def __delitem__(self, key: K) -> None:
43
45
  """Remove the item with the specified key."""
44
- del self._data[key]
46
+ del cast(dict[K, V], self.__dict__["_data"])[key]
45
47
 
46
48
  def __getitem__(self, item: K) -> V:
47
49
  """Return the value for the specified key."""
48
- return self._data[item]
50
+ return cast(dict[K, V], self.__dict__["_data"])[item]
49
51
 
50
52
  def __iter__(self) -> Iterator[K]:
51
53
  """Yield an iterator over the keys of the dictionary."""
52
- return iter(self._data)
54
+ return iter(cast(dict[K, V], self.__dict__["_data"]))
53
55
 
54
56
  def __repr__(self) -> str:
55
57
  """Return a string representation of the dictionary."""
56
- return self._data.__repr__()
58
+ return cast(dict[K, V], self.__dict__["_data"]).__repr__()
57
59
 
58
60
  def __len__(self) -> int:
59
61
  """Return the number of items in the dictionary."""
60
- return len(self._data)
62
+ return len(cast(dict[K, V], self.__dict__["_data"]))
61
63
 
62
- def __contains__(self, key: K) -> bool:
64
+ def __contains__(self, key: object) -> bool:
63
65
  """Check if the dictionary contains the specified key."""
64
- return key in self._data
66
+ return key in cast(dict[K, V], self.__dict__["_data"])
65
67
 
66
68
  def __eq__(self, other: object) -> bool:
67
69
  """Compare this instance to another dictionary or TypedDict."""
70
+ data = cast(dict[K, V], self.__dict__["_data"])
68
71
  if isinstance(other, TypedDict):
69
- return self._data == other._data
72
+ other_data = cast(dict[K, V], other.__dict__["_data"])
73
+ return data == other_data
70
74
  if isinstance(other, dict):
71
- return self._data == other
75
+ return data == other
72
76
  return NotImplemented
73
77
 
74
- def items(self) -> Iterator[Tuple[K, V]]:
75
- """R.items() -> a set-like object providing a view on R's items."""
76
- return cast(Iterator[Tuple[K, V]], self._data.items())
77
-
78
- def keys(self) -> Iterator[K]:
79
- """R.keys() -> a set-like object providing a view on R's keys."""
80
- return cast(Iterator[K], self._data.keys())
81
-
82
- def values(self) -> Iterator[V]:
83
- """R.values() -> an object providing a view on R's values."""
84
- return cast(Iterator[V], self._data.values())
85
-
86
- def update(self, *args: Any, **kwargs: Any) -> None:
87
- """R.update([E, ]**F) -> None.
88
-
89
- Update R from dict/iterable E and F.
90
- """
91
- for key, value in dict(*args, **kwargs).items():
92
- self[key] = value
93
-
94
- def pop(self, key: K) -> V:
95
- """R.pop(k[,d]) -> v, remove specified key and return the corresponding value.
96
-
97
- If key is not found, d is returned if given, otherwise KeyError is raised.
98
- """
99
- return self._data.pop(key)
100
-
101
- def get(self, key: K, default: V) -> V:
102
- """R.get(k[,d]) -> R[k] if k in R, else d.
103
-
104
- d defaults to None.
105
- """
106
- return self._data.get(key, default)
78
+ def keys(self) -> KeysView[K]:
79
+ """D.keys() -> a set-like object providing a view on D's keys."""
80
+ return cast(dict[K, V], self.__dict__["_data"]).keys()
107
81
 
108
- def clear(self) -> None:
109
- """R.clear() -> None.
82
+ def values(self) -> ValuesView[V]:
83
+ """D.values() -> an object providing a view on D's values."""
84
+ return cast(dict[K, V], self.__dict__["_data"]).values()
110
85
 
111
- Remove all items from R.
112
- """
113
- self._data.clear()
86
+ def items(self) -> ItemsView[K, V]:
87
+ """D.items() -> a set-like object providing a view on D's items."""
88
+ return cast(dict[K, V], self.__dict__["_data"]).items()
@@ -15,7 +15,9 @@
15
15
  """RecordSet utilities."""
16
16
 
17
17
 
18
- from typing import Dict, Mapping, OrderedDict, Tuple, Union, cast, get_args
18
+ from collections import OrderedDict
19
+ from collections.abc import Mapping
20
+ from typing import Union, cast, get_args
19
21
 
20
22
  from . import Array, ConfigsRecord, MetricsRecord, ParametersRecord, RecordSet
21
23
  from .typing import (
@@ -35,6 +37,8 @@ from .typing import (
35
37
  Status,
36
38
  )
37
39
 
40
+ EMPTY_TENSOR_KEY = "_empty"
41
+
38
42
 
39
43
  def parametersrecord_to_parameters(
40
44
  record: ParametersRecord, keep_input: bool
@@ -55,11 +59,17 @@ def parametersrecord_to_parameters(
55
59
  keep_input : bool
56
60
  A boolean indicating whether entries in the record should be deleted from the
57
61
  input dictionary immediately after adding them to the record.
62
+
63
+ Returns
64
+ -------
65
+ parameters : Parameters
66
+ The parameters in the legacy format Parameters.
58
67
  """
59
68
  parameters = Parameters(tensors=[], tensor_type="")
60
69
 
61
70
  for key in list(record.keys()):
62
- parameters.tensors.append(record[key].data)
71
+ if key != EMPTY_TENSOR_KEY:
72
+ parameters.tensors.append(record[key].data)
63
73
 
64
74
  if not parameters.tensor_type:
65
75
  # Setting from first array in record. Recall the warning in the docstrings
@@ -89,6 +99,11 @@ def parameters_to_parametersrecord(
89
99
  A boolean indicating whether parameters should be deleted from the input
90
100
  Parameters object (i.e. a list of serialized NumPy arrays) immediately after
91
101
  adding them to the record.
102
+
103
+ Returns
104
+ -------
105
+ ParametersRecord
106
+ The ParametersRecord containing the provided parameters.
92
107
  """
93
108
  tensor_type = parameters.tensor_type
94
109
 
@@ -103,12 +118,16 @@ def parameters_to_parametersrecord(
103
118
  data=tensor, dtype="", stype=tensor_type, shape=[]
104
119
  )
105
120
 
121
+ if num_arrays == 0:
122
+ ordered_dict[EMPTY_TENSOR_KEY] = Array(
123
+ data=b"", dtype="", stype=tensor_type, shape=[]
124
+ )
106
125
  return ParametersRecord(ordered_dict, keep_input=keep_input)
107
126
 
108
127
 
109
128
  def _check_mapping_from_recordscalartype_to_scalar(
110
129
  record_data: Mapping[str, Union[ConfigsRecordValues, MetricsRecordValues]]
111
- ) -> Dict[str, Scalar]:
130
+ ) -> dict[str, Scalar]:
112
131
  """Check mapping `common.*RecordValues` into `common.Scalar` is possible."""
113
132
  for value in record_data.values():
114
133
  if not isinstance(value, get_args(Scalar)):
@@ -119,14 +138,14 @@ def _check_mapping_from_recordscalartype_to_scalar(
119
138
  "supported by the `common.RecordSet` infrastructure. "
120
139
  f"You used type: {type(value)}"
121
140
  )
122
- return cast(Dict[str, Scalar], record_data)
141
+ return cast(dict[str, Scalar], record_data)
123
142
 
124
143
 
125
144
  def _recordset_to_fit_or_evaluate_ins_components(
126
145
  recordset: RecordSet,
127
146
  ins_str: str,
128
147
  keep_input: bool,
129
- ) -> Tuple[Parameters, Dict[str, Scalar]]:
148
+ ) -> tuple[Parameters, dict[str, Scalar]]:
130
149
  """Derive Fit/Evaluate Ins from a RecordSet."""
131
150
  # get Array and construct Parameters
132
151
  parameters_record = recordset.parameters_records[f"{ins_str}.parameters"]
@@ -138,7 +157,7 @@ def _recordset_to_fit_or_evaluate_ins_components(
138
157
  # get config dict
139
158
  config_record = recordset.configs_records[f"{ins_str}.config"]
140
159
  # pylint: disable-next=protected-access
141
- config_dict = _check_mapping_from_recordscalartype_to_scalar(config_record._data)
160
+ config_dict = _check_mapping_from_recordscalartype_to_scalar(config_record)
142
161
 
143
162
  return parameters, config_dict
144
163
 
@@ -162,7 +181,7 @@ def _fit_or_evaluate_ins_to_recordset(
162
181
  def _embed_status_into_recordset(
163
182
  res_str: str, status: Status, recordset: RecordSet
164
183
  ) -> RecordSet:
165
- status_dict: Dict[str, ConfigsRecordValues] = {
184
+ status_dict: dict[str, ConfigsRecordValues] = {
166
185
  "code": int(status.code.value),
167
186
  "message": status.message,
168
187
  }
@@ -206,7 +225,7 @@ def recordset_to_fitres(recordset: RecordSet, keep_input: bool) -> FitRes:
206
225
  )
207
226
  configs_record = recordset.configs_records[f"{ins_str}.metrics"]
208
227
  # pylint: disable-next=protected-access
209
- metrics = _check_mapping_from_recordscalartype_to_scalar(configs_record._data)
228
+ metrics = _check_mapping_from_recordscalartype_to_scalar(configs_record)
210
229
  status = _extract_status_from_recordset(ins_str, recordset)
211
230
 
212
231
  return FitRes(
@@ -267,7 +286,7 @@ def recordset_to_evaluateres(recordset: RecordSet) -> EvaluateRes:
267
286
  configs_record = recordset.configs_records[f"{ins_str}.metrics"]
268
287
 
269
288
  # pylint: disable-next=protected-access
270
- metrics = _check_mapping_from_recordscalartype_to_scalar(configs_record._data)
289
+ metrics = _check_mapping_from_recordscalartype_to_scalar(configs_record)
271
290
  status = _extract_status_from_recordset(ins_str, recordset)
272
291
 
273
292
  return EvaluateRes(
@@ -307,7 +326,7 @@ def recordset_to_getparametersins(recordset: RecordSet) -> GetParametersIns:
307
326
  """Derive GetParametersIns from a RecordSet object."""
308
327
  config_record = recordset.configs_records["getparametersins.config"]
309
328
  # pylint: disable-next=protected-access
310
- config_dict = _check_mapping_from_recordscalartype_to_scalar(config_record._data)
329
+ config_dict = _check_mapping_from_recordscalartype_to_scalar(config_record)
311
330
 
312
331
  return GetParametersIns(config=config_dict)
313
332
 
@@ -358,7 +377,7 @@ def recordset_to_getpropertiesins(recordset: RecordSet) -> GetPropertiesIns:
358
377
  """Derive GetPropertiesIns from a RecordSet object."""
359
378
  config_record = recordset.configs_records["getpropertiesins.config"]
360
379
  # pylint: disable-next=protected-access
361
- config_dict = _check_mapping_from_recordscalartype_to_scalar(config_record._data)
380
+ config_dict = _check_mapping_from_recordscalartype_to_scalar(config_record)
362
381
 
363
382
  return GetPropertiesIns(config=config_dict)
364
383
 
@@ -377,7 +396,7 @@ def recordset_to_getpropertiesres(recordset: RecordSet) -> GetPropertiesRes:
377
396
  res_str = "getpropertiesres"
378
397
  config_record = recordset.configs_records[f"{res_str}.properties"]
379
398
  # pylint: disable-next=protected-access
380
- properties = _check_mapping_from_recordscalartype_to_scalar(config_record._data)
399
+ properties = _check_mapping_from_recordscalartype_to_scalar(config_record)
381
400
 
382
401
  status = _extract_status_from_recordset(res_str, recordset=recordset)
383
402