flwr 1.23.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 (292) 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 +19 -0
  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/auth_plugin.py +4 -5
  9. flwr/cli/auth_plugin/noop_auth_plugin.py +54 -11
  10. flwr/cli/auth_plugin/oidc_cli_plugin.py +32 -9
  11. flwr/cli/build.py +60 -18
  12. flwr/cli/cli_account_auth_interceptor.py +24 -7
  13. flwr/cli/config_utils.py +101 -13
  14. flwr/cli/federation/__init__.py +24 -0
  15. flwr/cli/federation/ls.py +140 -0
  16. flwr/cli/federation/show.py +317 -0
  17. flwr/cli/install.py +91 -13
  18. flwr/cli/log.py +52 -9
  19. flwr/cli/login/login.py +7 -4
  20. flwr/cli/ls.py +170 -130
  21. flwr/cli/new/new.py +33 -50
  22. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +1 -0
  23. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
  24. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
  25. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  26. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  27. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  28. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  29. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
  30. flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +1 -1
  31. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  32. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +2 -2
  33. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +1 -1
  34. flwr/cli/pull.py +10 -5
  35. flwr/cli/run/run.py +77 -30
  36. flwr/cli/run_utils.py +130 -0
  37. flwr/cli/stop.py +25 -7
  38. flwr/cli/supernode/ls.py +16 -8
  39. flwr/cli/supernode/register.py +9 -4
  40. flwr/cli/supernode/unregister.py +5 -3
  41. flwr/cli/utils.py +376 -16
  42. flwr/client/__init__.py +1 -1
  43. flwr/client/dpfedavg_numpy_client.py +4 -1
  44. flwr/client/grpc_adapter_client/connection.py +6 -7
  45. flwr/client/grpc_rere_client/connection.py +10 -11
  46. flwr/client/grpc_rere_client/grpc_adapter.py +6 -2
  47. flwr/client/grpc_rere_client/node_auth_client_interceptor.py +2 -1
  48. flwr/client/message_handler/message_handler.py +2 -2
  49. flwr/client/mod/secure_aggregation/secaggplus_mod.py +3 -3
  50. flwr/client/numpy_client.py +1 -1
  51. flwr/client/rest_client/connection.py +12 -14
  52. flwr/client/run_info_store.py +4 -5
  53. flwr/client/typing.py +1 -1
  54. flwr/clientapp/client_app.py +9 -10
  55. flwr/clientapp/mod/centraldp_mods.py +16 -17
  56. flwr/clientapp/mod/localdp_mod.py +8 -9
  57. flwr/clientapp/typing.py +1 -1
  58. flwr/clientapp/utils.py +3 -3
  59. flwr/common/address.py +1 -2
  60. flwr/common/args.py +3 -4
  61. flwr/common/config.py +13 -16
  62. flwr/common/constant.py +5 -2
  63. flwr/common/differential_privacy.py +3 -4
  64. flwr/common/event_log_plugin/event_log_plugin.py +3 -4
  65. flwr/common/exit/exit.py +15 -2
  66. flwr/common/exit/exit_code.py +19 -0
  67. flwr/common/exit/exit_handler.py +6 -2
  68. flwr/common/exit/signal_handler.py +5 -5
  69. flwr/common/grpc.py +6 -6
  70. flwr/common/inflatable_protobuf_utils.py +1 -1
  71. flwr/common/inflatable_utils.py +38 -21
  72. flwr/common/logger.py +19 -19
  73. flwr/common/message.py +4 -4
  74. flwr/common/object_ref.py +7 -7
  75. flwr/common/record/array.py +3 -3
  76. flwr/common/record/arrayrecord.py +18 -30
  77. flwr/common/record/configrecord.py +3 -3
  78. flwr/common/record/recorddict.py +5 -5
  79. flwr/common/record/typeddict.py +9 -2
  80. flwr/common/recorddict_compat.py +7 -10
  81. flwr/common/retry_invoker.py +20 -20
  82. flwr/common/secure_aggregation/ndarrays_arithmetic.py +3 -3
  83. flwr/common/serde.py +5 -4
  84. flwr/common/serde_utils.py +2 -2
  85. flwr/common/telemetry.py +9 -5
  86. flwr/common/typing.py +52 -37
  87. flwr/compat/client/app.py +38 -37
  88. flwr/compat/client/grpc_client/connection.py +11 -11
  89. flwr/compat/server/app.py +5 -6
  90. flwr/proto/appio_pb2.py +13 -3
  91. flwr/proto/appio_pb2.pyi +134 -65
  92. flwr/proto/appio_pb2_grpc.py +20 -0
  93. flwr/proto/appio_pb2_grpc.pyi +27 -0
  94. flwr/proto/clientappio_pb2.py +17 -7
  95. flwr/proto/clientappio_pb2.pyi +15 -0
  96. flwr/proto/clientappio_pb2_grpc.py +206 -40
  97. flwr/proto/clientappio_pb2_grpc.pyi +168 -53
  98. flwr/proto/control_pb2.py +71 -52
  99. flwr/proto/control_pb2.pyi +277 -111
  100. flwr/proto/control_pb2_grpc.py +249 -40
  101. flwr/proto/control_pb2_grpc.pyi +185 -52
  102. flwr/proto/error_pb2.py +13 -3
  103. flwr/proto/error_pb2.pyi +24 -6
  104. flwr/proto/error_pb2_grpc.py +20 -0
  105. flwr/proto/error_pb2_grpc.pyi +27 -0
  106. flwr/proto/fab_pb2.py +14 -4
  107. flwr/proto/fab_pb2.pyi +59 -31
  108. flwr/proto/fab_pb2_grpc.py +20 -0
  109. flwr/proto/fab_pb2_grpc.pyi +27 -0
  110. flwr/proto/federation_pb2.py +38 -0
  111. flwr/proto/federation_pb2.pyi +56 -0
  112. flwr/proto/federation_pb2_grpc.py +24 -0
  113. flwr/proto/federation_pb2_grpc.pyi +31 -0
  114. flwr/proto/fleet_pb2.py +14 -4
  115. flwr/proto/fleet_pb2.pyi +137 -61
  116. flwr/proto/fleet_pb2_grpc.py +189 -48
  117. flwr/proto/fleet_pb2_grpc.pyi +175 -61
  118. flwr/proto/grpcadapter_pb2.py +14 -4
  119. flwr/proto/grpcadapter_pb2.pyi +38 -16
  120. flwr/proto/grpcadapter_pb2_grpc.py +35 -4
  121. flwr/proto/grpcadapter_pb2_grpc.pyi +38 -7
  122. flwr/proto/heartbeat_pb2.py +17 -7
  123. flwr/proto/heartbeat_pb2.pyi +51 -22
  124. flwr/proto/heartbeat_pb2_grpc.py +20 -0
  125. flwr/proto/heartbeat_pb2_grpc.pyi +27 -0
  126. flwr/proto/log_pb2.py +13 -3
  127. flwr/proto/log_pb2.pyi +34 -11
  128. flwr/proto/log_pb2_grpc.py +20 -0
  129. flwr/proto/log_pb2_grpc.pyi +27 -0
  130. flwr/proto/message_pb2.py +15 -5
  131. flwr/proto/message_pb2.pyi +154 -86
  132. flwr/proto/message_pb2_grpc.py +20 -0
  133. flwr/proto/message_pb2_grpc.pyi +27 -0
  134. flwr/proto/node_pb2.py +15 -5
  135. flwr/proto/node_pb2.pyi +50 -25
  136. flwr/proto/node_pb2_grpc.py +20 -0
  137. flwr/proto/node_pb2_grpc.pyi +27 -0
  138. flwr/proto/recorddict_pb2.py +13 -3
  139. flwr/proto/recorddict_pb2.pyi +184 -107
  140. flwr/proto/recorddict_pb2_grpc.py +20 -0
  141. flwr/proto/recorddict_pb2_grpc.pyi +27 -0
  142. flwr/proto/run_pb2.py +40 -31
  143. flwr/proto/run_pb2.pyi +149 -84
  144. flwr/proto/run_pb2_grpc.py +20 -0
  145. flwr/proto/run_pb2_grpc.pyi +27 -0
  146. flwr/proto/serverappio_pb2.py +13 -3
  147. flwr/proto/serverappio_pb2.pyi +32 -8
  148. flwr/proto/serverappio_pb2_grpc.py +246 -65
  149. flwr/proto/serverappio_pb2_grpc.pyi +221 -85
  150. flwr/proto/simulationio_pb2.py +16 -8
  151. flwr/proto/simulationio_pb2.pyi +15 -0
  152. flwr/proto/simulationio_pb2_grpc.py +162 -41
  153. flwr/proto/simulationio_pb2_grpc.pyi +149 -55
  154. flwr/proto/transport_pb2.py +20 -10
  155. flwr/proto/transport_pb2.pyi +249 -160
  156. flwr/proto/transport_pb2_grpc.py +35 -4
  157. flwr/proto/transport_pb2_grpc.pyi +38 -8
  158. flwr/server/app.py +38 -17
  159. flwr/server/client_manager.py +4 -5
  160. flwr/server/client_proxy.py +10 -11
  161. flwr/server/compat/app.py +4 -5
  162. flwr/server/compat/app_utils.py +2 -1
  163. flwr/server/compat/grid_client_proxy.py +10 -12
  164. flwr/server/compat/legacy_context.py +3 -4
  165. flwr/server/fleet_event_log_interceptor.py +2 -1
  166. flwr/server/grid/grid.py +2 -3
  167. flwr/server/grid/grpc_grid.py +10 -8
  168. flwr/server/grid/inmemory_grid.py +4 -4
  169. flwr/server/run_serverapp.py +2 -3
  170. flwr/server/server.py +34 -39
  171. flwr/server/server_app.py +7 -8
  172. flwr/server/server_config.py +1 -2
  173. flwr/server/serverapp/app.py +34 -28
  174. flwr/server/serverapp_components.py +4 -5
  175. flwr/server/strategy/aggregate.py +9 -8
  176. flwr/server/strategy/bulyan.py +13 -11
  177. flwr/server/strategy/dp_adaptive_clipping.py +16 -20
  178. flwr/server/strategy/dp_fixed_clipping.py +12 -17
  179. flwr/server/strategy/dpfedavg_adaptive.py +3 -4
  180. flwr/server/strategy/dpfedavg_fixed.py +6 -10
  181. flwr/server/strategy/fault_tolerant_fedavg.py +14 -13
  182. flwr/server/strategy/fedadagrad.py +18 -14
  183. flwr/server/strategy/fedadam.py +16 -14
  184. flwr/server/strategy/fedavg.py +16 -17
  185. flwr/server/strategy/fedavg_android.py +15 -15
  186. flwr/server/strategy/fedavgm.py +21 -18
  187. flwr/server/strategy/fedmedian.py +2 -3
  188. flwr/server/strategy/fedopt.py +11 -10
  189. flwr/server/strategy/fedprox.py +10 -9
  190. flwr/server/strategy/fedtrimmedavg.py +12 -11
  191. flwr/server/strategy/fedxgb_bagging.py +13 -11
  192. flwr/server/strategy/fedxgb_cyclic.py +6 -6
  193. flwr/server/strategy/fedxgb_nn_avg.py +4 -4
  194. flwr/server/strategy/fedyogi.py +16 -14
  195. flwr/server/strategy/krum.py +12 -11
  196. flwr/server/strategy/qfedavg.py +16 -15
  197. flwr/server/strategy/strategy.py +6 -9
  198. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +2 -1
  199. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -2
  200. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +3 -4
  201. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +10 -12
  202. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +1 -3
  203. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +4 -4
  204. flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +3 -2
  205. flwr/server/superlink/fleet/message_handler/message_handler.py +34 -28
  206. flwr/server/superlink/fleet/rest_rere/rest_api.py +2 -2
  207. flwr/server/superlink/fleet/vce/backend/backend.py +1 -1
  208. flwr/server/superlink/fleet/vce/backend/raybackend.py +5 -5
  209. flwr/server/superlink/fleet/vce/vce_api.py +15 -9
  210. flwr/server/superlink/linkstate/in_memory_linkstate.py +115 -150
  211. flwr/server/superlink/linkstate/linkstate.py +59 -43
  212. flwr/server/superlink/linkstate/linkstate_factory.py +22 -5
  213. flwr/server/superlink/linkstate/sqlite_linkstate.py +447 -438
  214. flwr/server/superlink/linkstate/utils.py +6 -6
  215. flwr/server/superlink/serverappio/serverappio_grpc.py +1 -2
  216. flwr/server/superlink/serverappio/serverappio_servicer.py +26 -21
  217. flwr/server/superlink/simulation/simulationio_grpc.py +1 -2
  218. flwr/server/superlink/simulation/simulationio_servicer.py +18 -13
  219. flwr/server/superlink/utils.py +4 -6
  220. flwr/server/typing.py +1 -1
  221. flwr/server/utils/tensorboard.py +15 -8
  222. flwr/server/workflow/default_workflows.py +5 -5
  223. flwr/server/workflow/secure_aggregation/secagg_workflow.py +2 -4
  224. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +8 -8
  225. flwr/serverapp/strategy/bulyan.py +16 -15
  226. flwr/serverapp/strategy/dp_adaptive_clipping.py +12 -11
  227. flwr/serverapp/strategy/dp_fixed_clipping.py +11 -14
  228. flwr/serverapp/strategy/fedadagrad.py +10 -11
  229. flwr/serverapp/strategy/fedadam.py +10 -11
  230. flwr/serverapp/strategy/fedavg.py +9 -10
  231. flwr/serverapp/strategy/fedavgm.py +17 -16
  232. flwr/serverapp/strategy/fedmedian.py +2 -2
  233. flwr/serverapp/strategy/fedopt.py +10 -11
  234. flwr/serverapp/strategy/fedprox.py +7 -8
  235. flwr/serverapp/strategy/fedtrimmedavg.py +9 -9
  236. flwr/serverapp/strategy/fedxgb_bagging.py +3 -3
  237. flwr/serverapp/strategy/fedxgb_cyclic.py +9 -9
  238. flwr/serverapp/strategy/fedyogi.py +9 -11
  239. flwr/serverapp/strategy/krum.py +7 -7
  240. flwr/serverapp/strategy/multikrum.py +9 -9
  241. flwr/serverapp/strategy/qfedavg.py +17 -16
  242. flwr/serverapp/strategy/strategy.py +6 -9
  243. flwr/serverapp/strategy/strategy_utils.py +7 -8
  244. flwr/simulation/app.py +46 -42
  245. flwr/simulation/legacy_app.py +12 -12
  246. flwr/simulation/ray_transport/ray_actor.py +10 -11
  247. flwr/simulation/ray_transport/ray_client_proxy.py +11 -12
  248. flwr/simulation/run_simulation.py +43 -43
  249. flwr/simulation/simulationio_connection.py +4 -4
  250. flwr/supercore/cli/flower_superexec.py +3 -4
  251. flwr/supercore/constant.py +31 -1
  252. flwr/supercore/corestate/corestate.py +24 -3
  253. flwr/supercore/corestate/in_memory_corestate.py +138 -0
  254. flwr/supercore/corestate/sqlite_corestate.py +157 -0
  255. flwr/supercore/ffs/disk_ffs.py +1 -2
  256. flwr/supercore/ffs/ffs.py +1 -2
  257. flwr/supercore/ffs/ffs_factory.py +1 -2
  258. flwr/{common → supercore}/heartbeat.py +20 -25
  259. flwr/supercore/object_store/in_memory_object_store.py +1 -2
  260. flwr/supercore/object_store/object_store.py +1 -2
  261. flwr/supercore/object_store/object_store_factory.py +1 -2
  262. flwr/supercore/object_store/sqlite_object_store.py +8 -7
  263. flwr/supercore/primitives/asymmetric.py +1 -1
  264. flwr/supercore/primitives/asymmetric_ed25519.py +11 -1
  265. flwr/supercore/sqlite_mixin.py +37 -34
  266. flwr/supercore/superexec/plugin/base_exec_plugin.py +1 -2
  267. flwr/supercore/superexec/plugin/exec_plugin.py +3 -3
  268. flwr/supercore/superexec/run_superexec.py +9 -13
  269. flwr/superlink/artifact_provider/artifact_provider.py +1 -2
  270. flwr/superlink/auth_plugin/auth_plugin.py +6 -9
  271. flwr/superlink/auth_plugin/noop_auth_plugin.py +6 -9
  272. flwr/superlink/federation/__init__.py +24 -0
  273. flwr/superlink/federation/federation_manager.py +64 -0
  274. flwr/superlink/federation/noop_federation_manager.py +71 -0
  275. flwr/superlink/servicer/control/control_account_auth_interceptor.py +22 -13
  276. flwr/superlink/servicer/control/control_event_log_interceptor.py +7 -7
  277. flwr/superlink/servicer/control/control_grpc.py +5 -6
  278. flwr/superlink/servicer/control/control_license_interceptor.py +3 -3
  279. flwr/superlink/servicer/control/control_servicer.py +102 -18
  280. flwr/supernode/cli/flower_supernode.py +58 -3
  281. flwr/supernode/nodestate/in_memory_nodestate.py +60 -49
  282. flwr/supernode/nodestate/nodestate.py +7 -8
  283. flwr/supernode/nodestate/nodestate_factory.py +7 -4
  284. flwr/supernode/runtime/run_clientapp.py +41 -22
  285. flwr/supernode/servicer/clientappio/clientappio_servicer.py +40 -10
  286. flwr/supernode/start_client_internal.py +158 -42
  287. {flwr-1.23.0.dist-info → flwr-1.24.0.dist-info}/METADATA +8 -8
  288. flwr-1.24.0.dist-info/RECORD +454 -0
  289. flwr/supercore/object_store/utils.py +0 -43
  290. flwr-1.23.0.dist-info/RECORD +0 -439
  291. {flwr-1.23.0.dist-info → flwr-1.24.0.dist-info}/WHEEL +0 -0
  292. {flwr-1.23.0.dist-info → flwr-1.24.0.dist-info}/entry_points.txt +0 -0
flwr/cli/utils.py CHANGED
@@ -18,15 +18,18 @@
18
18
  import hashlib
19
19
  import json
20
20
  import re
21
- from collections.abc import Iterator
21
+ from collections.abc import Callable, Iterable, Iterator
22
22
  from contextlib import contextmanager
23
23
  from pathlib import Path
24
- from typing import Any, Callable, Optional, Union, cast
24
+ from typing import Any, cast
25
25
 
26
26
  import grpc
27
+ import pathspec
28
+ import requests
27
29
  import typer
28
30
 
29
31
  from flwr.common.constant import (
32
+ ACCESS_TOKEN_KEY,
30
33
  AUTHN_TYPE_JSON_KEY,
31
34
  CREDENTIALS_DIR,
32
35
  FLWR_DIR,
@@ -36,6 +39,7 @@ from flwr.common.constant import (
36
39
  PUBLIC_KEY_ALREADY_IN_USE_MESSAGE,
37
40
  PUBLIC_KEY_NOT_VALID,
38
41
  PULL_UNFINISHED_RUN_MESSAGE,
42
+ REFRESH_TOKEN_KEY,
39
43
  RUN_ID_NOT_FOUND_MESSAGE,
40
44
  AuthnType,
41
45
  )
@@ -44,6 +48,8 @@ from flwr.common.grpc import (
44
48
  create_channel,
45
49
  on_channel_state_change,
46
50
  )
51
+ from flwr.common.version import package_version as flwr_version
52
+ from flwr.supercore.constant import APP_ID_PATTERN, APP_VERSION_PATTERN
47
53
 
48
54
  from .auth_plugin import CliAuthPlugin, get_cli_plugin_class
49
55
  from .cli_account_auth_interceptor import CliAccountAuthInterceptor
@@ -53,9 +59,24 @@ from .config_utils import validate_certificate_in_federation_config
53
59
  def prompt_text(
54
60
  text: str,
55
61
  predicate: Callable[[str], bool] = lambda _: True,
56
- default: Optional[str] = None,
62
+ default: str | None = None,
57
63
  ) -> str:
58
- """Ask user to enter text input."""
64
+ """Ask user to enter text input.
65
+
66
+ Parameters
67
+ ----------
68
+ text : str
69
+ The prompt text to display to the user.
70
+ predicate : Callable[[str], bool] (default: lambda _: True)
71
+ A function to validate the user input. Default accepts all non-empty strings.
72
+ default : str | None (default: None)
73
+ Default value to use if user presses enter without input.
74
+
75
+ Returns
76
+ -------
77
+ str
78
+ The validated user input.
79
+ """
59
80
  while True:
60
81
  result = typer.prompt(
61
82
  typer.style(f"\n💬 {text}", fg=typer.colors.MAGENTA, bold=True),
@@ -69,7 +90,20 @@ def prompt_text(
69
90
 
70
91
 
71
92
  def prompt_options(text: str, options: list[str]) -> str:
72
- """Ask user to select one of the given options and return the selected item."""
93
+ """Ask user to select one of the given options and return the selected item.
94
+
95
+ Parameters
96
+ ----------
97
+ text : str
98
+ The prompt text to display to the user.
99
+ options : list[str]
100
+ List of options to present to the user.
101
+
102
+ Returns
103
+ -------
104
+ str
105
+ The selected option from the list.
106
+ """
73
107
  # Turn options into a list with index as in " [ 0] quickstart-pytorch"
74
108
  options_formatted = [
75
109
  " [ "
@@ -127,9 +161,19 @@ def is_valid_project_name(name: str) -> bool:
127
161
  def sanitize_project_name(name: str) -> str:
128
162
  """Sanitize the given string to make it a valid Python project name.
129
163
 
130
- This version replaces spaces, dots, slashes, and underscores with dashes, removes
164
+ This function replaces spaces, dots, slashes, and underscores with dashes, removes
131
165
  any characters not allowed in Python project names, makes the string lowercase, and
132
166
  ensures it starts with a valid character.
167
+
168
+ Parameters
169
+ ----------
170
+ name : str
171
+ The project name to sanitize.
172
+
173
+ Returns
174
+ -------
175
+ str
176
+ The sanitized project name that is valid for Python projects.
133
177
  """
134
178
  # Replace whitespace with '_'
135
179
  name_with_hyphens = re.sub(r"[ ./_]", "-", name)
@@ -154,8 +198,19 @@ def sanitize_project_name(name: str) -> str:
154
198
  return sanitized_name
155
199
 
156
200
 
157
- def get_sha256_hash(file_path_or_int: Union[Path, int]) -> str:
158
- """Calculate the SHA-256 hash of a file."""
201
+ def get_sha256_hash(file_path_or_int: Path | int) -> str:
202
+ """Calculate the SHA-256 hash of a file or integer.
203
+
204
+ Parameters
205
+ ----------
206
+ file_path_or_int : Path | int
207
+ Either a path to a file to hash, or an integer to convert to string and hash.
208
+
209
+ Returns
210
+ -------
211
+ str
212
+ The SHA-256 hash as a hexadecimal string.
213
+ """
159
214
  sha256 = hashlib.sha256()
160
215
  if isinstance(file_path_or_int, Path):
161
216
  with open(file_path_or_int, "rb") as f:
@@ -214,6 +269,7 @@ def get_account_auth_config_path(root_dir: Path, federation: str) -> Path:
214
269
  f"Please check the permissions of `{gitignore_path}` and try again.",
215
270
  fg=typer.colors.RED,
216
271
  bold=True,
272
+ err=True,
217
273
  )
218
274
  raise typer.Exit(code=1) from err
219
275
 
@@ -221,7 +277,18 @@ def get_account_auth_config_path(root_dir: Path, federation: str) -> Path:
221
277
 
222
278
 
223
279
  def account_auth_enabled(federation_config: dict[str, Any]) -> bool:
224
- """Check if account authentication is enabled in the federation config."""
280
+ """Check if account authentication is enabled in the federation config.
281
+
282
+ Parameters
283
+ ----------
284
+ federation_config : dict[str, Any]
285
+ The federation configuration dictionary.
286
+
287
+ Returns
288
+ -------
289
+ bool
290
+ True if account authentication is enabled, False otherwise.
291
+ """
225
292
  enabled: bool = federation_config.get("enable-user-auth", False)
226
293
  enabled |= federation_config.get("enable-account-auth", False)
227
294
  if "enable-user-auth" in federation_config:
@@ -235,7 +302,18 @@ def account_auth_enabled(federation_config: dict[str, Any]) -> bool:
235
302
 
236
303
 
237
304
  def retrieve_authn_type(config_path: Path) -> str:
238
- """Retrieve the auth type from the config file or return NOOP if not found."""
305
+ """Retrieve the auth type from the config file or return NOOP if not found.
306
+
307
+ Parameters
308
+ ----------
309
+ config_path : Path
310
+ Path to the authentication configuration file.
311
+
312
+ Returns
313
+ -------
314
+ str
315
+ The authentication type string, or AuthnType.NOOP if not found.
316
+ """
239
317
  try:
240
318
  with config_path.open("r", encoding="utf-8") as file:
241
319
  json_file = json.load(file)
@@ -249,9 +327,31 @@ def load_cli_auth_plugin(
249
327
  root_dir: Path,
250
328
  federation: str,
251
329
  federation_config: dict[str, Any],
252
- authn_type: Optional[str] = None,
330
+ authn_type: str | None = None,
253
331
  ) -> CliAuthPlugin:
254
- """Load the CLI-side account auth plugin for the given authn type."""
332
+ """Load the CLI-side account auth plugin for the given authn type.
333
+
334
+ Parameters
335
+ ----------
336
+ root_dir : Path
337
+ Root directory of the Flower project.
338
+ federation : str
339
+ Name of the federation.
340
+ federation_config : dict[str, Any]
341
+ Federation configuration dictionary.
342
+ authn_type : str | None
343
+ Authentication type. If None, will be determined from config.
344
+
345
+ Returns
346
+ -------
347
+ CliAuthPlugin
348
+ The loaded authentication plugin instance.
349
+
350
+ Raises
351
+ ------
352
+ typer.Exit
353
+ If the authentication type is unknown.
354
+ """
255
355
  # Find the path to the account auth config file
256
356
  config_path = get_account_auth_config_path(root_dir, federation)
257
357
 
@@ -275,7 +375,22 @@ def load_cli_auth_plugin(
275
375
  def init_channel(
276
376
  app: Path, federation_config: dict[str, Any], auth_plugin: CliAuthPlugin
277
377
  ) -> grpc.Channel:
278
- """Initialize gRPC channel to the Control API."""
378
+ """Initialize gRPC channel to the Control API.
379
+
380
+ Parameters
381
+ ----------
382
+ app : Path
383
+ Path to the Flower app directory.
384
+ federation_config : dict[str, Any]
385
+ Federation configuration dictionary containing address and TLS settings.
386
+ auth_plugin : CliAuthPlugin
387
+ Authentication plugin instance for handling credentials.
388
+
389
+ Returns
390
+ -------
391
+ grpc.Channel
392
+ Configured gRPC channel with authentication interceptors.
393
+ """
279
394
  insecure, root_certificates_bytes = validate_certificate_in_federation_config(
280
395
  app, federation_config
281
396
  )
@@ -299,9 +414,22 @@ def init_channel(
299
414
  def flwr_cli_grpc_exc_handler() -> Iterator[None]: # pylint: disable=too-many-branches
300
415
  """Context manager to handle specific gRPC errors.
301
416
 
302
- It catches grpc.RpcError exceptions with UNAUTHENTICATED, UNIMPLEMENTED,
303
- UNAVAILABLE, and PERMISSION_DENIED statuses, informs the user, and exits the
304
- application. All other exceptions will be allowed to escape.
417
+ Catches grpc.RpcError exceptions with UNAUTHENTICATED, UNIMPLEMENTED,
418
+ UNAVAILABLE, PERMISSION_DENIED, NOT_FOUND, and FAILED_PRECONDITION statuses,
419
+ informs the user, and exits the application. All other exceptions will be
420
+ allowed to escape.
421
+
422
+ Yields
423
+ ------
424
+ None
425
+ Context manager yields nothing.
426
+
427
+ Raises
428
+ ------
429
+ typer.Exit
430
+ On handled gRPC error statuses with appropriate exit code.
431
+ grpc.RpcError
432
+ For unhandled gRPC error statuses.
305
433
  """
306
434
  try:
307
435
  yield
@@ -312,6 +440,7 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]: # pylint: disable=too-many-b
312
440
  " to authenticate and try again.",
313
441
  fg=typer.colors.RED,
314
442
  bold=True,
443
+ err=True,
315
444
  )
316
445
  raise typer.Exit(code=1) from None
317
446
  if e.code() == grpc.StatusCode.UNIMPLEMENTED:
@@ -320,12 +449,14 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]: # pylint: disable=too-many-b
320
449
  "❌ Account authentication is not enabled on this SuperLink.",
321
450
  fg=typer.colors.RED,
322
451
  bold=True,
452
+ err=True,
323
453
  )
324
454
  elif e.details() == NO_ARTIFACT_PROVIDER_MESSAGE: # pylint: disable=E1101
325
455
  typer.secho(
326
456
  "❌ The SuperLink does not support `flwr pull` command.",
327
457
  fg=typer.colors.RED,
328
458
  bold=True,
459
+ err=True,
329
460
  )
330
461
  else:
331
462
  typer.secho(
@@ -335,6 +466,7 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]: # pylint: disable=too-many-b
335
466
  "the CLI and SuperLink are compatible.",
336
467
  fg=typer.colors.RED,
337
468
  bold=True,
469
+ err=True,
338
470
  )
339
471
  raise typer.Exit(code=1) from None
340
472
  if e.code() == grpc.StatusCode.PERMISSION_DENIED:
@@ -342,6 +474,7 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]: # pylint: disable=too-many-b
342
474
  "❌ Permission denied.",
343
475
  fg=typer.colors.RED,
344
476
  bold=True,
477
+ err=True,
345
478
  )
346
479
  # pylint: disable-next=E1101
347
480
  typer.secho(e.details(), fg=typer.colors.RED, bold=True)
@@ -352,6 +485,7 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]: # pylint: disable=too-many-b
352
485
  "connection and 'address' in the federation configuration.",
353
486
  fg=typer.colors.RED,
354
487
  bold=True,
488
+ err=True,
355
489
  )
356
490
  raise typer.Exit(code=1) from None
357
491
  if e.code() == grpc.StatusCode.NOT_FOUND:
@@ -360,6 +494,7 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]: # pylint: disable=too-many-b
360
494
  "❌ Run ID not found.",
361
495
  fg=typer.colors.RED,
362
496
  bold=True,
497
+ err=True,
363
498
  )
364
499
  raise typer.Exit(code=1) from None
365
500
  if e.details() == NODE_NOT_FOUND_MESSAGE: # pylint: disable=E1101
@@ -367,6 +502,7 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]: # pylint: disable=too-many-b
367
502
  "❌ Node ID not found for this account.",
368
503
  fg=typer.colors.RED,
369
504
  bold=True,
505
+ err=True,
370
506
  )
371
507
  raise typer.Exit(code=1) from None
372
508
  if e.code() == grpc.StatusCode.FAILED_PRECONDITION:
@@ -376,6 +512,7 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]: # pylint: disable=too-many-b
376
512
  "the run is finished. You can check the run status with `flwr ls`.",
377
513
  fg=typer.colors.RED,
378
514
  bold=True,
515
+ err=True,
379
516
  )
380
517
  raise typer.Exit(code=1) from None
381
518
  if (
@@ -386,6 +523,7 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]: # pylint: disable=too-many-b
386
523
  "SuperNode.",
387
524
  fg=typer.colors.RED,
388
525
  bold=True,
526
+ err=True,
389
527
  )
390
528
  raise typer.Exit(code=1) from None
391
529
  if e.details() == PUBLIC_KEY_NOT_VALID: # pylint: disable=E1101
@@ -394,6 +532,228 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]: # pylint: disable=too-many-b
394
532
  "NIST EC public key.",
395
533
  fg=typer.colors.RED,
396
534
  bold=True,
535
+ err=True,
397
536
  )
398
537
  raise typer.Exit(code=1) from None
538
+
539
+ # Log details from grpc error directly
540
+ typer.secho(
541
+ f"❌ {e.details()}",
542
+ fg=typer.colors.RED,
543
+ bold=True,
544
+ err=True,
545
+ )
546
+ raise typer.Exit(code=1) from None
399
547
  raise
548
+
549
+
550
+ def request_download_link(
551
+ app_id: str, app_version: str | None, in_url: str, out_url: str
552
+ ) -> str:
553
+ """Request a download link for the given app from the Flower platform API.
554
+
555
+ Parameters
556
+ ----------
557
+ app_id : str
558
+ The application identifier.
559
+ app_version : str | None
560
+ The application version, or None for latest.
561
+ in_url : str
562
+ The API endpoint URL.
563
+ out_url : str
564
+ The key name for the download URL in the response.
565
+
566
+ Returns
567
+ -------
568
+ str
569
+ The download URL for the application.
570
+
571
+ Raises
572
+ ------
573
+ typer.Exit
574
+ If connection fails, app not found, or API request fails.
575
+ """
576
+ headers = {
577
+ "Content-Type": "application/json",
578
+ "Accept": "application/json",
579
+ }
580
+ body = {
581
+ "app_id": app_id, # send raw string of app_id
582
+ "app_version": app_version,
583
+ "flwr_version": flwr_version,
584
+ }
585
+ try:
586
+ resp = requests.post(in_url, headers=headers, data=json.dumps(body), timeout=20)
587
+ except requests.RequestException as e:
588
+ typer.secho(
589
+ f"Unable to connect to Platform API: {e}",
590
+ fg=typer.colors.RED,
591
+ err=True,
592
+ )
593
+ raise typer.Exit(code=1) from e
594
+
595
+ if resp.status_code == 404:
596
+ error_message = resp.json()["detail"]
597
+ if isinstance(error_message, dict):
598
+ available_app_versions = error_message["available_app_versions"]
599
+ available_versions_str = (
600
+ ", ".join(map(str, available_app_versions))
601
+ if available_app_versions
602
+ else "None"
603
+ )
604
+ typer.secho(
605
+ f"{app_id}=={app_version} not found in Platform API. "
606
+ f"Available app versions for {app_id}: {available_versions_str}",
607
+ fg=typer.colors.RED,
608
+ err=True,
609
+ )
610
+ else:
611
+ typer.secho(
612
+ f"{app_id} not found in Platform API.",
613
+ fg=typer.colors.RED,
614
+ err=True,
615
+ )
616
+ raise typer.Exit(code=1)
617
+
618
+ if not resp.ok:
619
+ typer.secho(
620
+ f"Platform API request failed with "
621
+ f"status {resp.status_code}. Details: {resp.text}",
622
+ fg=typer.colors.RED,
623
+ err=True,
624
+ )
625
+ raise typer.Exit(code=1)
626
+
627
+ data = resp.json()
628
+ if out_url not in data:
629
+ typer.secho(
630
+ "Invalid response from Platform API",
631
+ fg=typer.colors.RED,
632
+ err=True,
633
+ )
634
+ raise typer.Exit(code=1)
635
+ return str(data[out_url])
636
+
637
+
638
+ def build_pathspec(patterns: Iterable[str]) -> pathspec.PathSpec:
639
+ """Build a PathSpec from a list of GitIgnore-style patterns.
640
+
641
+ Parameters
642
+ ----------
643
+ patterns : Iterable[str]
644
+ Iterable of GitIgnore-style pattern strings.
645
+
646
+ Returns
647
+ -------
648
+ pathspec.PathSpec
649
+ Compiled PathSpec object for pattern matching.
650
+ """
651
+ return pathspec.PathSpec.from_lines("gitwildmatch", patterns)
652
+
653
+
654
+ def load_gitignore_patterns(file: Path | bytes) -> list[str]:
655
+ """Load gitignore patterns from .gitignore file bytes.
656
+
657
+ Parameters
658
+ ----------
659
+ file : Path | bytes
660
+ The path to a .gitignore file or its bytes content.
661
+
662
+ Returns
663
+ -------
664
+ list[str]
665
+ List of gitignore patterns.
666
+ Returns empty list if content can't be decoded or the file does not exist.
667
+ """
668
+ try:
669
+ if isinstance(file, Path):
670
+ content = file.read_text(encoding="utf-8")
671
+ else:
672
+ content = file.decode("utf-8")
673
+ patterns = [
674
+ line.strip()
675
+ for line in content.splitlines()
676
+ if line.strip() and not line.strip().startswith("#")
677
+ ]
678
+ return patterns
679
+ except (UnicodeDecodeError, OSError):
680
+ return []
681
+
682
+
683
+ def validate_credentials_content(creds_path: Path) -> str:
684
+ """Load and validate the credentials file content.
685
+
686
+ Ensures required keys exist:
687
+ - AUTHN_TYPE_JSON_KEY
688
+ - ACCESS_TOKEN_KEY
689
+ - REFRESH_TOKEN_KEY
690
+ """
691
+ try:
692
+ creds: dict[str, str] = json.loads(creds_path.read_text(encoding="utf-8"))
693
+ except (OSError, json.JSONDecodeError) as err:
694
+ typer.secho(
695
+ f"Invalid credentials file at '{creds_path}': {err}",
696
+ fg=typer.colors.RED,
697
+ err=True,
698
+ )
699
+ raise typer.Exit(code=1) from err
700
+
701
+ required_keys = [AUTHN_TYPE_JSON_KEY, ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY]
702
+ missing = [key for key in required_keys if key not in creds]
703
+
704
+ if missing:
705
+ typer.secho(
706
+ f"Credentials file '{creds_path}' is missing "
707
+ f"required key(s): {', '.join(missing)}. Please log in again.",
708
+ fg=typer.colors.RED,
709
+ err=True,
710
+ )
711
+ raise typer.Exit(code=1)
712
+
713
+ return creds[ACCESS_TOKEN_KEY]
714
+
715
+
716
+ def parse_app_spec(app_spec: str) -> tuple[str, str | None]:
717
+ """Parse app specification string into app ID and version.
718
+
719
+ Parameters
720
+ ----------
721
+ app_spec : str
722
+ The app specification string in the format '@account/app' or
723
+ '@account/app==x.y.z' (digits only).
724
+
725
+ Returns
726
+ -------
727
+ tuple[str, str | None]
728
+ A tuple containing the app ID and optional version.
729
+
730
+ Raises
731
+ ------
732
+ typer.Exit
733
+ If the app specification format is invalid.
734
+ """
735
+ if "==" in app_spec:
736
+ app_id, app_version = app_spec.split("==")
737
+
738
+ # Validate app version format
739
+ if not re.match(APP_VERSION_PATTERN, app_version):
740
+ typer.secho(
741
+ "❌ Invalid app version. Expected format: x.y.z (digits only).",
742
+ fg=typer.colors.RED,
743
+ err=True,
744
+ )
745
+ raise typer.Exit(code=1)
746
+ else:
747
+ app_id = app_spec
748
+ app_version = None
749
+
750
+ # Validate app_id format
751
+ if not re.match(APP_ID_PATTERN, app_id):
752
+ typer.secho(
753
+ "❌ Invalid remote app ID. Expected format: '@account/app'.",
754
+ fg=typer.colors.RED,
755
+ err=True,
756
+ )
757
+ raise typer.Exit(code=1)
758
+
759
+ return app_id, app_version
flwr/client/__init__.py CHANGED
@@ -15,7 +15,7 @@
15
15
  """Flower client."""
16
16
 
17
17
 
18
- from flwr.clientapp import ClientApp
18
+ from flwr.clientapp.client_app import ClientApp
19
19
 
20
20
  from ..compat.client.app import start_client as start_client # Deprecated
21
21
  from ..compat.client.app import start_numpy_client as start_numpy_client # Deprecated
@@ -120,7 +120,10 @@ class DPFedAvgNumPyClient(NumPyClient):
120
120
  updated_params, num_examples, metrics = self.client.fit(parameters, config)
121
121
 
122
122
  # Update = updated model - original model
123
- update = [np.subtract(x, y) for (x, y) in zip(updated_params, original_params)]
123
+ update = [
124
+ np.subtract(x, y)
125
+ for (x, y) in zip(updated_params, original_params, strict=True)
126
+ ]
124
127
 
125
128
  if "dpfedavg_clip_norm" not in config:
126
129
  raise KeyError("Clipping threshold not supplied by the server.")
@@ -15,10 +15,9 @@
15
15
  """Contextmanager for a GrpcAdapter channel to the Flower server."""
16
16
 
17
17
 
18
- from collections.abc import Iterator
18
+ from collections.abc import Callable, Iterator
19
19
  from contextlib import contextmanager
20
20
  from logging import ERROR
21
- from typing import Callable, Optional, Union
22
21
 
23
22
  from cryptography.hazmat.primitives.asymmetric import ec
24
23
 
@@ -38,14 +37,14 @@ def grpc_adapter( # pylint: disable=R0913,too-many-positional-arguments
38
37
  insecure: bool,
39
38
  retry_invoker: RetryInvoker,
40
39
  max_message_length: int = GRPC_MAX_MESSAGE_LENGTH, # pylint: disable=W0613
41
- root_certificates: Optional[Union[bytes, str]] = None,
42
- authentication_keys: Optional[ # pylint: disable=unused-argument
43
- tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]
44
- ] = None,
40
+ root_certificates: bytes | str | None = None,
41
+ authentication_keys: (
42
+ tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey] | None
43
+ ) = None,
45
44
  ) -> Iterator[
46
45
  tuple[
47
46
  int,
48
- Callable[[], Optional[tuple[Message, ObjectTree]]],
47
+ Callable[[], tuple[Message, ObjectTree] | None],
49
48
  Callable[[Message, ObjectTree], set[str]],
50
49
  Callable[[int], Run],
51
50
  Callable[[str, int], Fab],
@@ -15,11 +15,10 @@
15
15
  """Contextmanager for a gRPC request-response channel to the Flower server."""
16
16
 
17
17
 
18
- from collections.abc import Iterator, Sequence
18
+ from collections.abc import Callable, Iterator, Sequence
19
19
  from contextlib import contextmanager
20
20
  from logging import ERROR
21
21
  from pathlib import Path
22
- from typing import Callable, Optional, Union
23
22
 
24
23
  import grpc
25
24
  from cryptography.hazmat.primitives.asymmetric import ec
@@ -27,7 +26,6 @@ from cryptography.hazmat.primitives.asymmetric import ec
27
26
  from flwr.common import GRPC_MAX_MESSAGE_LENGTH
28
27
  from flwr.common.constant import HEARTBEAT_CALL_TIMEOUT, HEARTBEAT_DEFAULT_INTERVAL
29
28
  from flwr.common.grpc import create_channel, on_channel_state_change
30
- from flwr.common.heartbeat import HeartbeatSender
31
29
  from flwr.common.inflatable_protobuf_utils import (
32
30
  make_confirm_message_received_fn_protobuf,
33
31
  make_pull_object_fn_protobuf,
@@ -63,6 +61,7 @@ from flwr.proto.heartbeat_pb2 import ( # pylint: disable=E0611
63
61
  from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
64
62
  from flwr.proto.node_pb2 import Node # pylint: disable=E0611
65
63
  from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611
64
+ from flwr.supercore.heartbeat import HeartbeatSender
66
65
  from flwr.supercore.primitives.asymmetric import generate_key_pairs, public_key_to_bytes
67
66
 
68
67
  from .grpc_adapter import GrpcAdapter
@@ -75,15 +74,15 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
75
74
  insecure: bool,
76
75
  retry_invoker: RetryInvoker,
77
76
  max_message_length: int = GRPC_MAX_MESSAGE_LENGTH, # pylint: disable=W0613
78
- root_certificates: Optional[Union[bytes, str]] = None,
79
- authentication_keys: Optional[
80
- tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]
81
- ] = None,
82
- adapter_cls: Optional[Union[type[FleetStub], type[GrpcAdapter]]] = None,
77
+ root_certificates: bytes | str | None = None,
78
+ authentication_keys: (
79
+ tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey] | None
80
+ ) = None,
81
+ adapter_cls: type[FleetStub] | type[GrpcAdapter] | None = None,
83
82
  ) -> Iterator[
84
83
  tuple[
85
84
  int,
86
- Callable[[], Optional[tuple[Message, ObjectTree]]],
85
+ Callable[[], tuple[Message, ObjectTree] | None],
87
86
  Callable[[Message, ObjectTree], set[str]],
88
87
  Callable[[int], Run],
89
88
  Callable[[str, int], Fab],
@@ -163,7 +162,7 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
163
162
  if adapter_cls is None:
164
163
  adapter_cls = FleetStub
165
164
  stub = adapter_cls(channel)
166
- node: Optional[Node] = None
165
+ node: Node | None = None
167
166
 
168
167
  # Wrap stub
169
168
  _wrap_stub(stub, retry_invoker)
@@ -253,7 +252,7 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
253
252
  # Cleanup
254
253
  node = None
255
254
 
256
- def receive() -> Optional[tuple[Message, ObjectTree]]:
255
+ def receive() -> tuple[Message, ObjectTree] | None:
257
256
  """Pull a message with its ObjectTree from SuperLink."""
258
257
  # Get Node
259
258
  if node is None: