flwr 1.22.0__py3-none-any.whl → 1.24.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 (301) hide show
  1. flwr/__init__.py +16 -5
  2. flwr/app/error.py +2 -2
  3. flwr/app/exception.py +3 -3
  4. flwr/cli/app.py +34 -1
  5. flwr/cli/app_cmd/__init__.py +23 -0
  6. flwr/cli/app_cmd/publish.py +285 -0
  7. flwr/cli/app_cmd/review.py +252 -0
  8. flwr/cli/auth_plugin/__init__.py +15 -6
  9. flwr/cli/auth_plugin/auth_plugin.py +94 -0
  10. flwr/cli/auth_plugin/noop_auth_plugin.py +101 -0
  11. flwr/cli/auth_plugin/oidc_cli_plugin.py +46 -32
  12. flwr/cli/build.py +166 -53
  13. flwr/cli/{cli_user_auth_interceptor.py → cli_account_auth_interceptor.py} +29 -11
  14. flwr/cli/config_utils.py +101 -13
  15. flwr/cli/federation/__init__.py +24 -0
  16. flwr/cli/federation/ls.py +140 -0
  17. flwr/cli/federation/show.py +317 -0
  18. flwr/cli/install.py +91 -13
  19. flwr/cli/log.py +54 -11
  20. flwr/cli/login/login.py +41 -27
  21. flwr/cli/ls.py +177 -133
  22. flwr/cli/new/new.py +175 -40
  23. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +1 -0
  24. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
  25. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
  26. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  27. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  28. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  29. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  30. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
  31. flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +1 -1
  32. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  33. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +2 -2
  34. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +1 -1
  35. flwr/cli/pull.py +12 -7
  36. flwr/cli/run/run.py +82 -31
  37. flwr/cli/run_utils.py +130 -0
  38. flwr/cli/stop.py +27 -9
  39. flwr/cli/supernode/__init__.py +25 -0
  40. flwr/cli/supernode/ls.py +268 -0
  41. flwr/cli/supernode/register.py +190 -0
  42. flwr/cli/supernode/unregister.py +140 -0
  43. flwr/cli/utils.py +464 -81
  44. flwr/client/__init__.py +2 -1
  45. flwr/client/dpfedavg_numpy_client.py +4 -1
  46. flwr/client/grpc_adapter_client/connection.py +12 -15
  47. flwr/client/grpc_rere_client/connection.py +68 -41
  48. flwr/client/grpc_rere_client/grpc_adapter.py +34 -14
  49. flwr/client/grpc_rere_client/{client_interceptor.py → node_auth_client_interceptor.py} +5 -7
  50. flwr/client/message_handler/message_handler.py +2 -2
  51. flwr/client/mod/secure_aggregation/secaggplus_mod.py +10 -8
  52. flwr/client/numpy_client.py +1 -1
  53. flwr/client/rest_client/connection.py +94 -51
  54. flwr/client/run_info_store.py +4 -5
  55. flwr/client/typing.py +1 -1
  56. flwr/clientapp/__init__.py +1 -2
  57. flwr/{client → clientapp}/client_app.py +9 -10
  58. flwr/clientapp/mod/centraldp_mods.py +16 -17
  59. flwr/clientapp/mod/localdp_mod.py +8 -9
  60. flwr/clientapp/typing.py +1 -1
  61. flwr/{client/clientapp → clientapp}/utils.py +4 -4
  62. flwr/common/address.py +1 -2
  63. flwr/common/args.py +3 -4
  64. flwr/common/config.py +13 -16
  65. flwr/common/constant.py +56 -13
  66. flwr/common/differential_privacy.py +3 -4
  67. flwr/common/event_log_plugin/event_log_plugin.py +3 -4
  68. flwr/common/exit/exit.py +15 -2
  69. flwr/common/exit/exit_code.py +39 -10
  70. flwr/common/exit/exit_handler.py +6 -2
  71. flwr/common/exit/signal_handler.py +5 -5
  72. flwr/common/grpc.py +6 -6
  73. flwr/common/inflatable_protobuf_utils.py +1 -1
  74. flwr/common/inflatable_utils.py +48 -31
  75. flwr/common/logger.py +19 -19
  76. flwr/common/message.py +4 -4
  77. flwr/common/object_ref.py +7 -7
  78. flwr/common/record/array.py +6 -6
  79. flwr/common/record/arrayrecord.py +18 -21
  80. flwr/common/record/configrecord.py +3 -3
  81. flwr/common/record/recorddict.py +5 -5
  82. flwr/common/record/typeddict.py +9 -2
  83. flwr/common/recorddict_compat.py +7 -10
  84. flwr/common/retry_invoker.py +20 -20
  85. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +1 -89
  86. flwr/common/secure_aggregation/ndarrays_arithmetic.py +3 -3
  87. flwr/common/serde.py +9 -6
  88. flwr/common/serde_utils.py +2 -2
  89. flwr/common/telemetry.py +9 -5
  90. flwr/common/typing.py +59 -43
  91. flwr/compat/client/app.py +39 -38
  92. flwr/compat/client/grpc_client/connection.py +13 -13
  93. flwr/compat/server/app.py +5 -6
  94. flwr/proto/appio_pb2.py +13 -3
  95. flwr/proto/appio_pb2.pyi +134 -65
  96. flwr/proto/appio_pb2_grpc.py +20 -0
  97. flwr/proto/appio_pb2_grpc.pyi +27 -0
  98. flwr/proto/clientappio_pb2.py +17 -7
  99. flwr/proto/clientappio_pb2.pyi +15 -0
  100. flwr/proto/clientappio_pb2_grpc.py +206 -40
  101. flwr/proto/clientappio_pb2_grpc.pyi +168 -53
  102. flwr/proto/control_pb2.py +72 -40
  103. flwr/proto/control_pb2.pyi +319 -87
  104. flwr/proto/control_pb2_grpc.py +339 -28
  105. flwr/proto/control_pb2_grpc.pyi +209 -37
  106. flwr/proto/error_pb2.py +13 -3
  107. flwr/proto/error_pb2.pyi +24 -6
  108. flwr/proto/error_pb2_grpc.py +20 -0
  109. flwr/proto/error_pb2_grpc.pyi +27 -0
  110. flwr/proto/fab_pb2.py +24 -10
  111. flwr/proto/fab_pb2.pyi +68 -20
  112. flwr/proto/fab_pb2_grpc.py +20 -0
  113. flwr/proto/fab_pb2_grpc.pyi +27 -0
  114. flwr/proto/federation_pb2.py +38 -0
  115. flwr/proto/federation_pb2.pyi +56 -0
  116. flwr/proto/federation_pb2_grpc.py +24 -0
  117. flwr/proto/federation_pb2_grpc.pyi +31 -0
  118. flwr/proto/fleet_pb2.py +45 -27
  119. flwr/proto/fleet_pb2.pyi +186 -70
  120. flwr/proto/fleet_pb2_grpc.py +277 -66
  121. flwr/proto/fleet_pb2_grpc.pyi +201 -55
  122. flwr/proto/grpcadapter_pb2.py +14 -4
  123. flwr/proto/grpcadapter_pb2.pyi +38 -16
  124. flwr/proto/grpcadapter_pb2_grpc.py +35 -4
  125. flwr/proto/grpcadapter_pb2_grpc.pyi +38 -7
  126. flwr/proto/heartbeat_pb2.py +17 -7
  127. flwr/proto/heartbeat_pb2.pyi +51 -22
  128. flwr/proto/heartbeat_pb2_grpc.py +20 -0
  129. flwr/proto/heartbeat_pb2_grpc.pyi +27 -0
  130. flwr/proto/log_pb2.py +13 -3
  131. flwr/proto/log_pb2.pyi +34 -11
  132. flwr/proto/log_pb2_grpc.py +20 -0
  133. flwr/proto/log_pb2_grpc.pyi +27 -0
  134. flwr/proto/message_pb2.py +15 -5
  135. flwr/proto/message_pb2.pyi +154 -86
  136. flwr/proto/message_pb2_grpc.py +20 -0
  137. flwr/proto/message_pb2_grpc.pyi +27 -0
  138. flwr/proto/node_pb2.py +16 -4
  139. flwr/proto/node_pb2.pyi +77 -4
  140. flwr/proto/node_pb2_grpc.py +20 -0
  141. flwr/proto/node_pb2_grpc.pyi +27 -0
  142. flwr/proto/recorddict_pb2.py +13 -3
  143. flwr/proto/recorddict_pb2.pyi +184 -107
  144. flwr/proto/recorddict_pb2_grpc.py +20 -0
  145. flwr/proto/recorddict_pb2_grpc.pyi +27 -0
  146. flwr/proto/run_pb2.py +40 -31
  147. flwr/proto/run_pb2.pyi +149 -84
  148. flwr/proto/run_pb2_grpc.py +20 -0
  149. flwr/proto/run_pb2_grpc.pyi +27 -0
  150. flwr/proto/serverappio_pb2.py +13 -3
  151. flwr/proto/serverappio_pb2.pyi +32 -8
  152. flwr/proto/serverappio_pb2_grpc.py +246 -65
  153. flwr/proto/serverappio_pb2_grpc.pyi +221 -85
  154. flwr/proto/simulationio_pb2.py +16 -8
  155. flwr/proto/simulationio_pb2.pyi +15 -0
  156. flwr/proto/simulationio_pb2_grpc.py +162 -41
  157. flwr/proto/simulationio_pb2_grpc.pyi +149 -55
  158. flwr/proto/transport_pb2.py +20 -10
  159. flwr/proto/transport_pb2.pyi +249 -160
  160. flwr/proto/transport_pb2_grpc.py +35 -4
  161. flwr/proto/transport_pb2_grpc.pyi +38 -8
  162. flwr/server/app.py +173 -127
  163. flwr/server/client_manager.py +4 -5
  164. flwr/server/client_proxy.py +10 -11
  165. flwr/server/compat/app.py +4 -5
  166. flwr/server/compat/app_utils.py +2 -1
  167. flwr/server/compat/grid_client_proxy.py +10 -12
  168. flwr/server/compat/legacy_context.py +3 -4
  169. flwr/server/fleet_event_log_interceptor.py +2 -1
  170. flwr/server/grid/grid.py +2 -3
  171. flwr/server/grid/grpc_grid.py +10 -8
  172. flwr/server/grid/inmemory_grid.py +4 -4
  173. flwr/server/run_serverapp.py +2 -3
  174. flwr/server/server.py +34 -39
  175. flwr/server/server_app.py +7 -8
  176. flwr/server/server_config.py +1 -2
  177. flwr/server/serverapp/app.py +34 -28
  178. flwr/server/serverapp_components.py +4 -5
  179. flwr/server/strategy/aggregate.py +9 -8
  180. flwr/server/strategy/bulyan.py +13 -11
  181. flwr/server/strategy/dp_adaptive_clipping.py +16 -20
  182. flwr/server/strategy/dp_fixed_clipping.py +12 -17
  183. flwr/server/strategy/dpfedavg_adaptive.py +3 -4
  184. flwr/server/strategy/dpfedavg_fixed.py +6 -10
  185. flwr/server/strategy/fault_tolerant_fedavg.py +14 -13
  186. flwr/server/strategy/fedadagrad.py +18 -14
  187. flwr/server/strategy/fedadam.py +16 -14
  188. flwr/server/strategy/fedavg.py +16 -17
  189. flwr/server/strategy/fedavg_android.py +15 -15
  190. flwr/server/strategy/fedavgm.py +21 -18
  191. flwr/server/strategy/fedmedian.py +2 -3
  192. flwr/server/strategy/fedopt.py +11 -10
  193. flwr/server/strategy/fedprox.py +10 -9
  194. flwr/server/strategy/fedtrimmedavg.py +12 -11
  195. flwr/server/strategy/fedxgb_bagging.py +13 -11
  196. flwr/server/strategy/fedxgb_cyclic.py +6 -6
  197. flwr/server/strategy/fedxgb_nn_avg.py +4 -4
  198. flwr/server/strategy/fedyogi.py +16 -14
  199. flwr/server/strategy/krum.py +12 -11
  200. flwr/server/strategy/qfedavg.py +16 -15
  201. flwr/server/strategy/strategy.py +6 -9
  202. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +19 -8
  203. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -2
  204. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +3 -4
  205. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +10 -12
  206. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +1 -3
  207. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +136 -42
  208. flwr/server/superlink/fleet/grpc_rere/{server_interceptor.py → node_auth_server_interceptor.py} +28 -51
  209. flwr/server/superlink/fleet/message_handler/message_handler.py +100 -49
  210. flwr/server/superlink/fleet/rest_rere/rest_api.py +54 -33
  211. flwr/server/superlink/fleet/vce/backend/backend.py +2 -2
  212. flwr/server/superlink/fleet/vce/backend/raybackend.py +6 -6
  213. flwr/server/superlink/fleet/vce/vce_api.py +32 -13
  214. flwr/server/superlink/linkstate/in_memory_linkstate.py +266 -207
  215. flwr/server/superlink/linkstate/linkstate.py +161 -62
  216. flwr/server/superlink/linkstate/linkstate_factory.py +24 -6
  217. flwr/server/superlink/linkstate/sqlite_linkstate.py +698 -638
  218. flwr/server/superlink/linkstate/utils.py +9 -60
  219. flwr/server/superlink/serverappio/serverappio_grpc.py +1 -2
  220. flwr/server/superlink/serverappio/serverappio_servicer.py +28 -23
  221. flwr/server/superlink/simulation/simulationio_grpc.py +1 -2
  222. flwr/server/superlink/simulation/simulationio_servicer.py +19 -14
  223. flwr/server/superlink/utils.py +4 -6
  224. flwr/server/typing.py +1 -1
  225. flwr/server/utils/tensorboard.py +15 -8
  226. flwr/server/utils/validator.py +2 -3
  227. flwr/server/workflow/default_workflows.py +5 -5
  228. flwr/server/workflow/secure_aggregation/secagg_workflow.py +2 -4
  229. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +12 -10
  230. flwr/serverapp/strategy/bulyan.py +16 -15
  231. flwr/serverapp/strategy/dp_adaptive_clipping.py +12 -11
  232. flwr/serverapp/strategy/dp_fixed_clipping.py +11 -14
  233. flwr/serverapp/strategy/fedadagrad.py +10 -11
  234. flwr/serverapp/strategy/fedadam.py +10 -11
  235. flwr/serverapp/strategy/fedavg.py +9 -10
  236. flwr/serverapp/strategy/fedavgm.py +17 -16
  237. flwr/serverapp/strategy/fedmedian.py +2 -2
  238. flwr/serverapp/strategy/fedopt.py +10 -11
  239. flwr/serverapp/strategy/fedprox.py +7 -8
  240. flwr/serverapp/strategy/fedtrimmedavg.py +9 -9
  241. flwr/serverapp/strategy/fedxgb_bagging.py +3 -3
  242. flwr/serverapp/strategy/fedxgb_cyclic.py +9 -9
  243. flwr/serverapp/strategy/fedyogi.py +9 -11
  244. flwr/serverapp/strategy/krum.py +7 -7
  245. flwr/serverapp/strategy/multikrum.py +9 -9
  246. flwr/serverapp/strategy/qfedavg.py +17 -16
  247. flwr/serverapp/strategy/strategy.py +6 -9
  248. flwr/serverapp/strategy/strategy_utils.py +7 -8
  249. flwr/simulation/app.py +46 -42
  250. flwr/simulation/legacy_app.py +12 -12
  251. flwr/simulation/ray_transport/ray_actor.py +11 -12
  252. flwr/simulation/ray_transport/ray_client_proxy.py +12 -13
  253. flwr/simulation/run_simulation.py +44 -43
  254. flwr/simulation/simulationio_connection.py +4 -4
  255. flwr/supercore/cli/flower_superexec.py +3 -4
  256. flwr/supercore/constant.py +52 -0
  257. flwr/supercore/corestate/corestate.py +24 -3
  258. flwr/supercore/corestate/in_memory_corestate.py +138 -0
  259. flwr/supercore/corestate/sqlite_corestate.py +157 -0
  260. flwr/supercore/ffs/disk_ffs.py +1 -2
  261. flwr/supercore/ffs/ffs.py +1 -2
  262. flwr/supercore/ffs/ffs_factory.py +1 -2
  263. flwr/{common → supercore}/heartbeat.py +20 -25
  264. flwr/supercore/object_store/in_memory_object_store.py +1 -6
  265. flwr/supercore/object_store/object_store.py +1 -2
  266. flwr/supercore/object_store/object_store_factory.py +27 -8
  267. flwr/supercore/object_store/sqlite_object_store.py +253 -0
  268. flwr/{client/clientapp → supercore/primitives}/__init__.py +1 -1
  269. flwr/supercore/primitives/asymmetric.py +117 -0
  270. flwr/supercore/primitives/asymmetric_ed25519.py +175 -0
  271. flwr/supercore/sqlite_mixin.py +159 -0
  272. flwr/supercore/superexec/plugin/base_exec_plugin.py +1 -2
  273. flwr/supercore/superexec/plugin/exec_plugin.py +3 -3
  274. flwr/supercore/superexec/run_superexec.py +9 -13
  275. flwr/supercore/utils.py +20 -0
  276. flwr/superlink/artifact_provider/artifact_provider.py +1 -2
  277. flwr/{common → superlink}/auth_plugin/__init__.py +6 -6
  278. flwr/superlink/auth_plugin/auth_plugin.py +88 -0
  279. flwr/superlink/auth_plugin/noop_auth_plugin.py +84 -0
  280. flwr/superlink/federation/__init__.py +24 -0
  281. flwr/superlink/federation/federation_manager.py +64 -0
  282. flwr/superlink/federation/noop_federation_manager.py +71 -0
  283. flwr/superlink/servicer/control/{control_user_auth_interceptor.py → control_account_auth_interceptor.py} +41 -32
  284. flwr/superlink/servicer/control/control_event_log_interceptor.py +7 -7
  285. flwr/superlink/servicer/control/control_grpc.py +18 -17
  286. flwr/superlink/servicer/control/control_license_interceptor.py +3 -3
  287. flwr/superlink/servicer/control/control_servicer.py +239 -63
  288. flwr/supernode/cli/flower_supernode.py +74 -26
  289. flwr/supernode/nodestate/in_memory_nodestate.py +60 -49
  290. flwr/supernode/nodestate/nodestate.py +7 -8
  291. flwr/supernode/nodestate/nodestate_factory.py +7 -4
  292. flwr/supernode/runtime/run_clientapp.py +43 -24
  293. flwr/supernode/servicer/clientappio/clientappio_servicer.py +40 -10
  294. flwr/supernode/start_client_internal.py +175 -51
  295. {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/METADATA +8 -8
  296. flwr-1.24.0.dist-info/RECORD +454 -0
  297. flwr/common/auth_plugin/auth_plugin.py +0 -149
  298. flwr/supercore/object_store/utils.py +0 -43
  299. flwr-1.22.0.dist-info/RECORD +0 -428
  300. {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/WHEEL +0 -0
  301. {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/entry_points.txt +0 -0
flwr/cli/build.py CHANGED
@@ -19,28 +19,45 @@ import hashlib
19
19
  import zipfile
20
20
  from io import BytesIO
21
21
  from pathlib import Path
22
- from typing import Annotated, Any, Optional, Union
22
+ from typing import Annotated, Any
23
23
 
24
24
  import pathspec
25
+ import tomli
25
26
  import tomli_w
26
27
  import typer
27
28
 
28
29
  from flwr.common.constant import (
29
- FAB_ALLOWED_EXTENSIONS,
30
+ FAB_CONFIG_FILE,
30
31
  FAB_DATE,
32
+ FAB_EXCLUDE_PATTERNS,
31
33
  FAB_HASH_TRUNCATION,
34
+ FAB_INCLUDE_PATTERNS,
32
35
  FAB_MAX_SIZE,
33
36
  )
34
37
 
35
- from .config_utils import load as load_toml
36
38
  from .config_utils import load_and_validate
37
- from .utils import is_valid_project_name
39
+ from .utils import build_pathspec, is_valid_project_name, load_gitignore_patterns
38
40
 
39
41
 
40
42
  def write_to_zip(
41
- zipfile_obj: zipfile.ZipFile, filename: str, contents: Union[bytes, str]
43
+ zipfile_obj: zipfile.ZipFile, filename: str, contents: bytes | str
42
44
  ) -> zipfile.ZipFile:
43
- """Set a fixed date and write contents to a zip file."""
45
+ """Set a fixed date and write contents to a zip file.
46
+
47
+ Parameters
48
+ ----------
49
+ zipfile_obj : zipfile.ZipFile
50
+ The ZipFile object to write to.
51
+ filename : str
52
+ Name of the file within the zip archive.
53
+ contents : bytes | str
54
+ The file contents to write.
55
+
56
+ Returns
57
+ -------
58
+ ZipFile
59
+ The modified ZipFile object.
60
+ """
44
61
  zip_info = zipfile.ZipInfo(filename)
45
62
  zip_info.date_time = FAB_DATE
46
63
  zipfile_obj.writestr(zip_info, contents)
@@ -48,7 +65,21 @@ def write_to_zip(
48
65
 
49
66
 
50
67
  def get_fab_filename(config: dict[str, Any], fab_hash: str) -> str:
51
- """Get the FAB filename based on the given config and FAB hash."""
68
+ """Get the FAB filename based on the given config and FAB hash.
69
+
70
+ Parameters
71
+ ----------
72
+ config : dict[str, Any]
73
+ The project configuration dictionary.
74
+ fab_hash : str
75
+ The SHA-256 hash of the FAB file.
76
+
77
+ Returns
78
+ -------
79
+ str
80
+ The formatted FAB filename in the pattern:
81
+ <publisher>.<name>.<version>.<hash_prefix>.fab
82
+ """
52
83
  publisher = config["tool"]["flwr"]["app"]["publisher"]
53
84
  name = config["project"]["name"]
54
85
  version = config["project"]["version"].replace(".", "-")
@@ -59,7 +90,7 @@ def get_fab_filename(config: dict[str, Any], fab_hash: str) -> str:
59
90
  # pylint: disable=too-many-locals, too-many-statements
60
91
  def build(
61
92
  app: Annotated[
62
- Optional[Path],
93
+ Path | None,
63
94
  typer.Option(help="Path of the Flower App to bundle into a FAB"),
64
95
  ] = None,
65
96
  ) -> None:
@@ -80,6 +111,7 @@ def build(
80
111
  f"❌ The path {app} is not a valid path to a Flower app.",
81
112
  fg=typer.colors.RED,
82
113
  bold=True,
114
+ err=True,
83
115
  )
84
116
  raise typer.Exit(code=1)
85
117
 
@@ -90,6 +122,7 @@ def build(
90
122
  "and can only contain letters, digits, and hyphens.",
91
123
  fg=typer.colors.RED,
92
124
  bold=True,
125
+ err=True,
93
126
  )
94
127
  raise typer.Exit(code=1)
95
128
 
@@ -100,6 +133,7 @@ def build(
100
133
  + "\n".join([f"- {line}" for line in errors]),
101
134
  fg=typer.colors.RED,
102
135
  bold=True,
136
+ err=True,
103
137
  )
104
138
  raise typer.Exit(code=1)
105
139
 
@@ -112,7 +146,10 @@ def build(
112
146
  )
113
147
 
114
148
  # Build FAB
115
- fab_bytes, fab_hash, _ = build_fab(app)
149
+ fab_bytes = build_fab_from_disk(app)
150
+
151
+ # Calculate hash for filename
152
+ fab_hash = hashlib.sha256(fab_bytes).hexdigest()
116
153
 
117
154
  # Get the name of the zip file
118
155
  fab_filename = get_fab_filename(config, fab_hash)
@@ -125,11 +162,10 @@ def build(
125
162
  )
126
163
 
127
164
 
128
- def build_fab(app: Path) -> tuple[bytes, str, dict[str, Any]]:
129
- """Build a FAB in memory and return the bytes, hash, and config.
165
+ def build_fab_from_disk(app: Path) -> bytes:
166
+ """Build a FAB from files on disk and return the FAB as bytes.
130
167
 
131
- This function assumes that the provided path points to a valid Flower app and
132
- bundles it into a FAB without performing additional validation.
168
+ This function reads files from disk and bundles them into a FAB.
133
169
 
134
170
  Parameters
135
171
  ----------
@@ -138,18 +174,67 @@ def build_fab(app: Path) -> tuple[bytes, str, dict[str, Any]]:
138
174
 
139
175
  Returns
140
176
  -------
141
- tuple[bytes, str, dict[str, Any]]
142
- A tuple containing:
143
- - the FAB as bytes
144
- - the SHA256 hash of the FAB
145
- - the project configuration (with the 'federations' field removed)
177
+ bytes
178
+ The FAB as bytes.
146
179
  """
147
180
  app = app.resolve()
148
181
 
149
- # Load the pyproject.toml file
150
- config = load_toml(app / "pyproject.toml")
151
- if config is None:
152
- raise ValueError("Project configuration could not be loaded.")
182
+ # Collect all files recursively (including pyproject.toml and .gitignore)
183
+ all_files = [f for f in app.rglob("*") if f.is_file()]
184
+
185
+ # Create dict mapping relative paths to Path objects
186
+ files_dict: dict[str, bytes | Path] = {
187
+ # Ensure consistent path separators across platforms
188
+ str(file_path.relative_to(app)).replace("\\", "/"): file_path
189
+ for file_path in all_files
190
+ }
191
+
192
+ # Build FAB from the files dict
193
+ return build_fab_from_files(files_dict)
194
+
195
+
196
+ def build_fab_from_files(files: dict[str, bytes | Path]) -> bytes:
197
+ r"""Build a FAB from in-memory files and return the FAB as bytes.
198
+
199
+ This is the core FAB building function that works with in-memory data.
200
+ It accepts either bytes or Path objects as file contents, applies filtering
201
+ rules (include/exclude patterns), and builds the FAB.
202
+
203
+ Parameters
204
+ ----------
205
+ files : dict[str, Union[bytes, Path]]
206
+ Dictionary mapping relative file paths to their contents.
207
+ - Keys: Relative paths (strings)
208
+ - Values: Either bytes (file contents) or Path (will be read)
209
+ Must include "pyproject.toml" and optionally ".gitignore".
210
+
211
+ Returns
212
+ -------
213
+ bytes
214
+ The FAB as bytes.
215
+
216
+ Examples
217
+ --------
218
+ Build a FAB from in-memory files::
219
+
220
+ files = {
221
+ "pyproject.toml": b"[project]\nname = 'myapp'\n...",
222
+ ".gitignore": b"*.pyc\n__pycache__/\n",
223
+ "src/client.py": Path("/path/to/client.py"),
224
+ "src/server.py": b"print('hello')",
225
+ "README.md": b"# My App\n",
226
+ }
227
+ fab_bytes = build_fab_from_files(files)
228
+ """
229
+
230
+ def to_bytes(content: bytes | Path) -> bytes:
231
+ return content.read_bytes() if isinstance(content, Path) else content
232
+
233
+ # Extract, load, and parse pyproject.toml
234
+ if FAB_CONFIG_FILE not in files:
235
+ raise ValueError(f"{FAB_CONFIG_FILE} not found in files")
236
+ pyproject_content = to_bytes(files[FAB_CONFIG_FILE])
237
+ config = tomli.loads(pyproject_content.decode("utf-8"))
153
238
 
154
239
  # Remove the 'federations' field if it exists
155
240
  if (
@@ -159,18 +244,22 @@ def build_fab(app: Path) -> tuple[bytes, str, dict[str, Any]]:
159
244
  ):
160
245
  del config["tool"]["flwr"]["federations"]
161
246
 
162
- # Load .gitignore rules if present
163
- ignore_spec = _load_gitignore(app)
247
+ # Extract and load .gitignore if present
248
+ gitignore_content = None
249
+ if ".gitignore" in files:
250
+ gitignore_content = to_bytes(files[".gitignore"])
251
+
252
+ # Get exclude and include specs
253
+ exclude_spec = get_fab_exclude_pathspec(gitignore_content)
254
+ include_spec = get_fab_include_pathspec()
164
255
 
165
- # Search for all files in the app directory
166
- all_files = [
167
- f
168
- for f in app.rglob("*")
169
- if not ignore_spec.match_file(f)
170
- and f.suffix in FAB_ALLOWED_EXTENSIONS
171
- and f.name != "pyproject.toml" # Exclude the original pyproject.toml
256
+ # Filter files based on include/exclude specs
257
+ filtered_paths = [
258
+ path.replace("\\", "/") # Ensure consistent path separators across platforms
259
+ for path in files.keys()
260
+ if include_spec.match_file(path) and not exclude_spec.match_file(path)
172
261
  ]
173
- all_files.sort()
262
+ filtered_paths.sort() # Sort for deterministic output
174
263
 
175
264
  # Create a zip file in memory
176
265
  list_file_content = ""
@@ -178,41 +267,65 @@ def build_fab(app: Path) -> tuple[bytes, str, dict[str, Any]]:
178
267
  fab_buffer = BytesIO()
179
268
  with zipfile.ZipFile(fab_buffer, "w", zipfile.ZIP_DEFLATED) as fab_file:
180
269
  # Add pyproject.toml
181
- write_to_zip(fab_file, "pyproject.toml", tomli_w.dumps(config))
270
+ write_to_zip(fab_file, FAB_CONFIG_FILE, tomli_w.dumps(config))
271
+
272
+ for file_path in filtered_paths:
182
273
 
183
- for file_path in all_files:
184
- # Read the file content manually
185
- file_contents = file_path.read_bytes()
274
+ # Get file contents as bytes
275
+ file_content = to_bytes(files[file_path])
186
276
 
187
- archive_path = str(file_path.relative_to(app)).replace("\\", "/")
188
- write_to_zip(fab_file, archive_path, file_contents)
277
+ # Write file to FAB
278
+ write_to_zip(fab_file, file_path, file_content)
189
279
 
190
- # Calculate file info
191
- sha256_hash = hashlib.sha256(file_contents).hexdigest()
192
- file_size_bits = len(file_contents) * 8 # size in bits
193
- list_file_content += f"{archive_path},{sha256_hash},{file_size_bits}\n"
280
+ # Calculate file info for CONTENT manifest
281
+ sha256_hash = hashlib.sha256(file_content).hexdigest()
282
+ file_size_bits = len(file_content) * 8 # size in bits
283
+ list_file_content += f"{file_path},{sha256_hash},{file_size_bits}\n"
194
284
 
195
- # Add CONTENT and CONTENT.jwt to the zip file
285
+ # Add CONTENT manifest to the zip file
196
286
  write_to_zip(fab_file, ".info/CONTENT", list_file_content)
197
287
 
198
288
  fab_bytes = fab_buffer.getvalue()
289
+
290
+ # Validate FAB size
199
291
  if len(fab_bytes) > FAB_MAX_SIZE:
200
292
  raise ValueError(
201
- f"FAB size exceeds maximum allowed size of {FAB_MAX_SIZE:,} bytes."
293
+ f"FAB size exceeds maximum allowed size of {FAB_MAX_SIZE:,} bytes. "
202
294
  "To reduce the package size, consider ignoring unnecessary files "
203
295
  "via your `.gitignore` file or excluding them from the build."
204
296
  )
205
297
 
206
- fab_hash = hashlib.sha256(fab_bytes).hexdigest()
298
+ return fab_bytes
299
+
207
300
 
208
- return fab_bytes, fab_hash, config
301
+ def get_fab_include_pathspec() -> pathspec.PathSpec:
302
+ """Get the PathSpec for files to include in a FAB.
209
303
 
304
+ Returns
305
+ -------
306
+ PathSpec
307
+ PathSpec object with default include patterns for FAB files.
308
+ """
309
+ return build_pathspec(FAB_INCLUDE_PATTERNS)
310
+
311
+
312
+ def get_fab_exclude_pathspec(gitignore_content: bytes | None) -> pathspec.PathSpec:
313
+ """Get the PathSpec for files to exclude from a FAB.
314
+
315
+ If gitignore_content is provided, its patterns will be combined with the default
316
+ exclude patterns.
210
317
 
211
- def _load_gitignore(app: Path) -> pathspec.PathSpec:
212
- """Load and parse .gitignore file, returning a pathspec."""
213
- gitignore_path = app / ".gitignore"
214
- patterns = ["__pycache__/"] # Default pattern
215
- if gitignore_path.exists():
216
- with open(gitignore_path, encoding="UTF-8") as file:
217
- patterns.extend(file.readlines())
318
+ Parameters
319
+ ----------
320
+ gitignore_content : bytes | None
321
+ Optional gitignore file content as bytes.
322
+
323
+ Returns
324
+ -------
325
+ PathSpec
326
+ PathSpec object with combined exclude patterns.
327
+ """
328
+ patterns = list(FAB_EXCLUDE_PATTERNS)
329
+ if gitignore_content:
330
+ patterns += load_gitignore_patterns(gitignore_content)
218
331
  return pathspec.PathSpec.from_lines("gitwildmatch", patterns)
@@ -15,26 +15,29 @@
15
15
  """Flower run interceptor."""
16
16
 
17
17
 
18
- from typing import Any, Callable, Union
18
+ from collections.abc import Callable
19
+ from typing import Any
19
20
 
20
21
  import grpc
21
22
 
22
- from flwr.common.auth_plugin import CliAuthPlugin
23
23
  from flwr.proto.control_pb2 import ( # pylint: disable=E0611
24
24
  StartRunRequest,
25
25
  StreamLogsRequest,
26
26
  )
27
27
 
28
- Request = Union[
29
- StartRunRequest,
30
- StreamLogsRequest,
31
- ]
28
+ from .auth_plugin import CliAuthPlugin
29
+
30
+ Request = StartRunRequest | StreamLogsRequest
32
31
 
33
32
 
34
- class CliUserAuthInterceptor(
33
+ class CliAccountAuthInterceptor(
35
34
  grpc.UnaryUnaryClientInterceptor, grpc.UnaryStreamClientInterceptor # type: ignore
36
35
  ):
37
- """CLI interceptor for user authentication."""
36
+ """CLI interceptor for account authentication.
37
+
38
+ This interceptor adds authentication tokens to gRPC metadata for CLI requests and
39
+ handles token refresh from response metadata.
40
+ """
38
41
 
39
42
  def __init__(self, auth_plugin: CliAuthPlugin):
40
43
  self.auth_plugin = auth_plugin
@@ -45,7 +48,22 @@ class CliUserAuthInterceptor(
45
48
  client_call_details: grpc.ClientCallDetails,
46
49
  request: Request,
47
50
  ) -> grpc.Call:
48
- """Send and receive tokens via metadata."""
51
+ """Send and receive tokens via metadata.
52
+
53
+ Parameters
54
+ ----------
55
+ continuation : Callable[[Any, Any], Any]
56
+ The next interceptor or handler in the chain.
57
+ client_call_details : grpc.ClientCallDetails
58
+ Details of the RPC call as a NamedTuple.
59
+ request : Request
60
+ The RPC request object.
61
+
62
+ Returns
63
+ -------
64
+ grpc.Call
65
+ The RPC response.
66
+ """
49
67
  new_metadata = self.auth_plugin.write_tokens_to_metadata(
50
68
  client_call_details.metadata or []
51
69
  )
@@ -69,7 +87,7 @@ class CliUserAuthInterceptor(
69
87
  client_call_details: grpc.ClientCallDetails,
70
88
  request: Request,
71
89
  ) -> grpc.Call:
72
- """Intercept a unary-unary call for user authentication.
90
+ """Intercept a unary-unary call for account authentication.
73
91
 
74
92
  This method intercepts a unary-unary RPC call initiated from the CLI and adds
75
93
  the required authentication tokens to the RPC metadata.
@@ -82,7 +100,7 @@ class CliUserAuthInterceptor(
82
100
  client_call_details: grpc.ClientCallDetails,
83
101
  request: Request,
84
102
  ) -> grpc.Call:
85
- """Intercept a unary-stream call for user authentication.
103
+ """Intercept a unary-stream call for account authentication.
86
104
 
87
105
  This method intercepts a unary-stream RPC call initiated from the CLI and adds
88
106
  the required authentication tokens to the RPC metadata.
flwr/cli/config_utils.py CHANGED
@@ -16,7 +16,7 @@
16
16
 
17
17
 
18
18
  from pathlib import Path
19
- from typing import Any, Optional, Union
19
+ from typing import Any
20
20
 
21
21
  import tomli
22
22
  import typer
@@ -30,7 +30,7 @@ from flwr.common.config import (
30
30
  )
31
31
 
32
32
 
33
- def get_fab_metadata(fab_file: Union[Path, bytes]) -> tuple[str, str]:
33
+ def get_fab_metadata(fab_file: Path | bytes) -> tuple[str, str]:
34
34
  """Extract the fab_id and the fab_version from a FAB file or path.
35
35
 
36
36
  Parameters
@@ -48,9 +48,9 @@ def get_fab_metadata(fab_file: Union[Path, bytes]) -> tuple[str, str]:
48
48
 
49
49
 
50
50
  def load_and_validate(
51
- path: Optional[Path] = None,
51
+ path: Path | None = None,
52
52
  check_module: bool = True,
53
- ) -> tuple[Optional[dict[str, Any]], list[str], list[str]]:
53
+ ) -> tuple[dict[str, Any] | None, list[str], list[str]]:
54
54
  """Load and validate pyproject.toml as dict.
55
55
 
56
56
  Parameters
@@ -89,8 +89,20 @@ def load_and_validate(
89
89
  return (config, errors, warnings)
90
90
 
91
91
 
92
- def load(toml_path: Path) -> Optional[dict[str, Any]]:
93
- """Load pyproject.toml and return as dict."""
92
+ def load(toml_path: Path) -> dict[str, Any] | None:
93
+ """Load pyproject.toml and return as dict.
94
+
95
+ Parameters
96
+ ----------
97
+ toml_path : Path
98
+ Path to the pyproject.toml file.
99
+
100
+ Returns
101
+ -------
102
+ dict[str, Any] | None
103
+ Parsed TOML configuration as dictionary, or None if file doesn't exist
104
+ or has invalid TOML syntax.
105
+ """
94
106
  if not toml_path.is_file():
95
107
  return None
96
108
 
@@ -102,12 +114,31 @@ def load(toml_path: Path) -> Optional[dict[str, Any]]:
102
114
 
103
115
 
104
116
  def process_loaded_project_config(
105
- config: Union[dict[str, Any], None], errors: list[str], warnings: list[str]
117
+ config: dict[str, Any] | None, errors: list[str], warnings: list[str]
106
118
  ) -> dict[str, Any]:
107
119
  """Process and return the loaded project configuration.
108
120
 
109
121
  This function handles errors and warnings from the `load_and_validate` function,
110
122
  exits on critical issues, and returns the validated configuration.
123
+
124
+ Parameters
125
+ ----------
126
+ config : dict[str, Any] | None
127
+ The loaded configuration dictionary, or None if loading failed.
128
+ errors : list[str]
129
+ List of error messages from validation.
130
+ warnings : list[str]
131
+ List of warning messages from validation.
132
+
133
+ Returns
134
+ -------
135
+ dict[str, Any]
136
+ The validated configuration dictionary.
137
+
138
+ Raises
139
+ ------
140
+ typer.Exit
141
+ If config is None or contains critical errors.
111
142
  """
112
143
  if config is None:
113
144
  typer.secho(
@@ -116,6 +147,7 @@ def process_loaded_project_config(
116
147
  + "\n".join([f"- {line}" for line in errors]),
117
148
  fg=typer.colors.RED,
118
149
  bold=True,
150
+ err=True,
119
151
  )
120
152
  raise typer.Exit(code=1)
121
153
 
@@ -133,11 +165,32 @@ def process_loaded_project_config(
133
165
 
134
166
 
135
167
  def validate_federation_in_project_config(
136
- federation: Optional[str],
168
+ federation: str | None,
137
169
  config: dict[str, Any],
138
- overrides: Optional[list[str]] = None,
170
+ overrides: list[str] | None = None,
139
171
  ) -> tuple[str, dict[str, Any]]:
140
- """Validate the federation name in the Flower project configuration."""
172
+ """Validate the federation name in the Flower project configuration.
173
+
174
+ Parameters
175
+ ----------
176
+ federation : str | None
177
+ Name of the federation, or None to use default from config.
178
+ config : dict[str, Any]
179
+ The project configuration dictionary.
180
+ overrides : list[str] | None
181
+ List of configuration override strings. Default is None.
182
+
183
+ Returns
184
+ -------
185
+ tuple[str, dict[str, Any]]
186
+ A tuple of (federation_name, federation_config).
187
+
188
+ Raises
189
+ ------
190
+ typer.Exit
191
+ If no federation name provided and no default found, or if federation
192
+ doesn't exist in config.
193
+ """
141
194
  federation = federation or config["tool"]["flwr"]["federations"].get("default")
142
195
 
143
196
  if federation is None:
@@ -147,6 +200,7 @@ def validate_federation_in_project_config(
147
200
  "`options.num-supernodes` value).",
148
201
  fg=typer.colors.RED,
149
202
  bold=True,
203
+ err=True,
150
204
  )
151
205
  raise typer.Exit(code=1)
152
206
 
@@ -162,6 +216,7 @@ def validate_federation_in_project_config(
162
216
  + "\n".join(available_feds),
163
217
  fg=typer.colors.RED,
164
218
  bold=True,
219
+ err=True,
165
220
  )
166
221
  raise typer.Exit(code=1)
167
222
 
@@ -175,7 +230,7 @@ def validate_federation_in_project_config(
175
230
 
176
231
  def validate_certificate_in_federation_config(
177
232
  app: Path, federation_config: dict[str, Any]
178
- ) -> tuple[bool, Optional[bytes]]:
233
+ ) -> tuple[bool, bytes | None]:
179
234
  """Validate the certificates in the Flower project configuration.
180
235
 
181
236
  Accepted configurations:
@@ -207,6 +262,7 @@ def validate_certificate_in_federation_config(
207
262
  "is set to `True`.",
208
263
  fg=typer.colors.RED,
209
264
  bold=True,
265
+ err=True,
210
266
  )
211
267
  raise typer.Exit(code=1)
212
268
 
@@ -218,6 +274,7 @@ def validate_certificate_in_federation_config(
218
274
  f"❌ Failed to read certificate file `{root_certificates}`: {e}",
219
275
  fg=typer.colors.RED,
220
276
  bold=True,
277
+ err=True,
221
278
  )
222
279
  raise typer.Exit(code=1) from e
223
280
  else:
@@ -227,19 +284,49 @@ def validate_certificate_in_federation_config(
227
284
 
228
285
 
229
286
  def exit_if_no_address(federation_config: dict[str, Any], cmd: str) -> None:
230
- """Exit if the provided federation_config has no "address" key."""
287
+ """Exit if the provided federation_config has no "address" key.
288
+
289
+ Parameters
290
+ ----------
291
+ federation_config : dict[str, Any]
292
+ The federation configuration dictionary to check.
293
+ cmd : str
294
+ The command name to display in the error message.
295
+
296
+ Raises
297
+ ------
298
+ typer.Exit
299
+ If 'address' key is not present in federation_config.
300
+ """
231
301
  if "address" not in federation_config:
232
302
  typer.secho(
233
303
  f"❌ `flwr {cmd}` currently works with a SuperLink. Ensure that the "
234
304
  "correct SuperLink (Control API) address is provided in `pyproject.toml`.",
235
305
  fg=typer.colors.RED,
236
306
  bold=True,
307
+ err=True,
237
308
  )
238
309
  raise typer.Exit(code=1)
239
310
 
240
311
 
241
312
  def get_insecure_flag(federation_config: dict[str, Any]) -> bool:
242
- """Extract and validate the `insecure` flag from the federation configuration."""
313
+ """Extract and validate the `insecure` flag from the federation configuration.
314
+
315
+ Parameters
316
+ ----------
317
+ federation_config : dict[str, Any]
318
+ The federation configuration dictionary.
319
+
320
+ Returns
321
+ -------
322
+ bool
323
+ The insecure flag value. Returns False if not specified.
324
+
325
+ Raises
326
+ ------
327
+ typer.Exit
328
+ If insecure value is not a boolean type.
329
+ """
243
330
  insecure_value = federation_config.get("insecure")
244
331
 
245
332
  if insecure_value is None:
@@ -252,5 +339,6 @@ def get_insecure_flag(federation_config: dict[str, Any]) -> bool:
252
339
  "(`insecure = true` or `insecure = false`)",
253
340
  fg=typer.colors.RED,
254
341
  bold=True,
342
+ err=True,
255
343
  )
256
344
  raise typer.Exit(code=1)
@@ -0,0 +1,24 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Flower command line interface `federation` command."""
16
+
17
+
18
+ from .ls import ls as ls
19
+ from .show import show as show
20
+
21
+ __all__ = [
22
+ "ls",
23
+ "show",
24
+ ]