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/new/new.py CHANGED
@@ -16,23 +16,24 @@
16
16
 
17
17
 
18
18
  import io
19
- import json
20
19
  import re
21
20
  import zipfile
22
21
  from enum import Enum
23
22
  from pathlib import Path
24
23
  from string import Template
25
- from typing import Annotated, Optional
24
+ from typing import Annotated
26
25
 
27
26
  import requests
28
27
  import typer
29
28
 
30
- from flwr.supercore.constant import APP_ID_PATTERN, PLATFORM_API_URL
29
+ from flwr.supercore.constant import PLATFORM_API_URL
31
30
 
32
31
  from ..utils import (
33
32
  is_valid_project_name,
33
+ parse_app_spec,
34
34
  prompt_options,
35
35
  prompt_text,
36
+ request_download_link,
36
37
  sanitize_project_name,
37
38
  )
38
39
 
@@ -100,7 +101,7 @@ def render_and_create(file_path: Path, template: str, context: dict[str, str]) -
100
101
 
101
102
 
102
103
  def print_success_prompt(
103
- package_name: str, llm_challenge_str: Optional[str] = None
104
+ package_name: str, llm_challenge_str: str | None = None
104
105
  ) -> None:
105
106
  """Print styled setup instructions for running a new Flower App after creation."""
106
107
  prompt = typer.style(
@@ -181,55 +182,31 @@ def _download_zip_to_memory(presigned_url: str) -> io.BytesIO:
181
182
  r = requests.get(presigned_url, timeout=60)
182
183
  r.raise_for_status()
183
184
  except requests.RequestException as e:
184
- raise typer.BadParameter(f"ZIP download failed: {e}") from e
185
+ typer.secho(
186
+ f"ZIP download failed: {e}",
187
+ fg=typer.colors.RED,
188
+ err=True,
189
+ )
190
+ raise typer.Exit(code=1) from e
185
191
 
186
192
  buf = io.BytesIO(r.content)
187
193
  # Validate it's a zip
188
194
  if not zipfile.is_zipfile(buf):
189
- raise typer.BadParameter("Downloaded file is not a valid ZIP")
195
+ typer.secho(
196
+ "Downloaded file is not a valid ZIP",
197
+ fg=typer.colors.RED,
198
+ err=True,
199
+ )
200
+ raise typer.Exit(code=1)
190
201
  buf.seek(0)
191
202
  return buf
192
203
 
193
204
 
194
- def _request_download_link(identifier: str) -> str:
195
- """Request download link from Flower platform API."""
196
- url = f"{PLATFORM_API_URL}/hub/fetch-zip"
197
- headers = {
198
- "Content-Type": "application/json",
199
- "Accept": "application/json",
200
- }
201
- body = {
202
- "identifier": identifier, # send raw string of identifier
203
- }
204
-
205
- try:
206
- resp = requests.post(url, headers=headers, data=json.dumps(body), timeout=20)
207
- except requests.RequestException as e:
208
- raise typer.BadParameter(f"Unable to connect to Platform API: {e}") from e
209
-
210
- if resp.status_code == 404:
211
- raise typer.BadParameter(f"'{identifier}' not found in Platform API")
212
- if not resp.ok:
213
- raise typer.BadParameter(
214
- f"Platform API request failed with "
215
- f"status {resp.status_code}. Details: {resp.text}"
216
- )
217
-
218
- data = resp.json()
219
- if "zip_url" not in data:
220
- raise typer.BadParameter("Invalid response from Platform API")
221
- return str(data["zip_url"])
222
-
223
-
224
- def download_remote_app_via_api(identifier: str) -> None:
205
+ def download_remote_app_via_api(app_spec: str) -> None:
225
206
  """Download App from Platform API."""
226
- # Parse @user/app just to derive local dir name
227
- m = re.match(APP_ID_PATTERN, identifier)
228
- if not m:
229
- raise typer.BadParameter(
230
- "Invalid remote app ID. Expected format: '@user_name/app_name'."
231
- )
232
- app_name = m.group("app")
207
+ # Validate app version and ID format
208
+ app_id, app_version = parse_app_spec(app_spec)
209
+ app_name = app_id.split("/")[1]
233
210
 
234
211
  project_dir = Path.cwd() / app_name
235
212
  if project_dir.exists():
@@ -244,12 +221,14 @@ def download_remote_app_via_api(identifier: str) -> None:
244
221
 
245
222
  print(
246
223
  typer.style(
247
- f"\n🔗 Requesting download link for {identifier}...",
224
+ f"\n🔗 Requesting download link for {app_id}...",
248
225
  fg=typer.colors.GREEN,
249
226
  bold=True,
250
227
  )
251
228
  )
252
- presigned_url = _request_download_link(identifier)
229
+ # Fetch ZIP downloading URL
230
+ url = f"{PLATFORM_API_URL}/hub/fetch-zip"
231
+ presigned_url = request_download_link(app_id, app_version, url, "zip_url")
253
232
 
254
233
  print(
255
234
  typer.style(
@@ -276,15 +255,19 @@ def download_remote_app_via_api(identifier: str) -> None:
276
255
  # pylint: disable=too-many-locals,too-many-branches,too-many-statements
277
256
  def new(
278
257
  app_name: Annotated[
279
- Optional[str],
280
- typer.Argument(help="The name of the Flower App"),
258
+ str | None,
259
+ typer.Argument(
260
+ help="Flower app name. For remote apps, use the format "
261
+ "'@account_name/app_name' or '@account_name/app_name==x.y.z'. "
262
+ "Version is optional (defaults to latest)."
263
+ ),
281
264
  ] = None,
282
265
  framework: Annotated[
283
- Optional[MlFramework],
266
+ MlFramework | None,
284
267
  typer.Option(case_sensitive=False, help="The ML framework to use"),
285
268
  ] = None,
286
269
  username: Annotated[
287
- Optional[str],
270
+ str | None,
288
271
  typer.Option(case_sensitive=False, help="The Flower username of the author"),
289
272
  ] = None,
290
273
  ) -> None:
@@ -84,6 +84,7 @@ def train(net, trainloader, epochs, lr, device):
84
84
  def test(net, testloader, device):
85
85
  """Validate the model on the test set."""
86
86
  net.to(device)
87
+ net.eval()
87
88
  criterion = torch.nn.CrossEntropyLoss()
88
89
  correct, loss = 0, 0.0
89
90
  with torch.no_grad():
@@ -14,7 +14,7 @@ description = ""
14
14
  license = "Apache-2.0"
15
15
  # Dependencies for your Flower App
16
16
  dependencies = [
17
- "flwr[simulation]>=1.23.0",
17
+ "flwr[simulation]>=1.24.0",
18
18
  "flwr-datasets[vision]>=0.5.0",
19
19
  "torch==2.8.0",
20
20
  "torchvision==0.23.0",
@@ -14,7 +14,7 @@ description = ""
14
14
  license = "Apache-2.0"
15
15
  # Dependencies for your Flower App
16
16
  dependencies = [
17
- "flwr[simulation]>=1.23.0",
17
+ "flwr[simulation]>=1.24.0",
18
18
  "flwr-datasets>=0.5.0",
19
19
  "torch==2.4.0",
20
20
  "trl==0.8.1",
@@ -14,7 +14,7 @@ description = ""
14
14
  license = "Apache-2.0"
15
15
  # Dependencies for your Flower App
16
16
  dependencies = [
17
- "flwr[simulation]>=1.23.0",
17
+ "flwr[simulation]>=1.24.0",
18
18
  "flwr-datasets>=0.5.0",
19
19
  "torch>=2.7.1",
20
20
  "transformers>=4.30.0,<5.0",
@@ -14,7 +14,7 @@ description = ""
14
14
  license = "Apache-2.0"
15
15
  # Dependencies for your Flower App
16
16
  dependencies = [
17
- "flwr[simulation]>=1.23.0",
17
+ "flwr[simulation]>=1.24.0",
18
18
  "jax==0.4.30",
19
19
  "jaxlib==0.4.30",
20
20
  "scikit-learn==1.6.1",
@@ -14,7 +14,7 @@ description = ""
14
14
  license = "Apache-2.0"
15
15
  # Dependencies for your Flower App
16
16
  dependencies = [
17
- "flwr[simulation]>=1.23.0",
17
+ "flwr[simulation]>=1.24.0",
18
18
  "flwr-datasets[vision]>=0.5.0",
19
19
  "mlx==0.29.0",
20
20
  ]
@@ -14,7 +14,7 @@ description = ""
14
14
  license = "Apache-2.0"
15
15
  # Dependencies for your Flower App
16
16
  dependencies = [
17
- "flwr[simulation]>=1.23.0",
17
+ "flwr[simulation]>=1.24.0",
18
18
  "numpy>=2.0.2",
19
19
  ]
20
20
 
@@ -14,10 +14,10 @@ description = ""
14
14
  license = "Apache-2.0"
15
15
  # Dependencies for your Flower App
16
16
  dependencies = [
17
- "flwr[simulation]>=1.23.0",
17
+ "flwr[simulation]>=1.24.0",
18
18
  "flwr-datasets[vision]>=0.5.0",
19
- "torch==2.7.1",
20
- "torchvision==0.22.1",
19
+ "torch>=2.7.1",
20
+ "torchvision>=0.22.1",
21
21
  ]
22
22
 
23
23
  [tool.hatch.build.targets.wheel]
@@ -14,7 +14,7 @@ description = ""
14
14
  license = "Apache-2.0"
15
15
  # Dependencies for your Flower App
16
16
  dependencies = [
17
- "flwr[simulation]>=1.23.0",
17
+ "flwr[simulation]>=1.24.0",
18
18
  "flwr-datasets[vision]>=0.5.0",
19
19
  "torch==2.7.1",
20
20
  "torchvision==0.22.1",
@@ -14,7 +14,7 @@ description = ""
14
14
  license = "Apache-2.0"
15
15
  # Dependencies for your Flower App
16
16
  dependencies = [
17
- "flwr[simulation]>=1.23.0",
17
+ "flwr[simulation]>=1.24.0",
18
18
  "flwr-datasets[vision]>=0.5.0",
19
19
  "scikit-learn>=1.6.1",
20
20
  ]
@@ -14,9 +14,9 @@ description = ""
14
14
  license = "Apache-2.0"
15
15
  # Dependencies for your Flower App
16
16
  dependencies = [
17
- "flwr[simulation]>=1.23.0",
17
+ "flwr[simulation]>=1.24.0",
18
18
  "flwr-datasets[vision]>=0.5.0",
19
- "tensorflow>=2.11.1,<2.18.0",
19
+ "tensorflow>=2.18.0",
20
20
  ]
21
21
 
22
22
  [tool.hatch.build.targets.wheel]
@@ -14,7 +14,7 @@ description = ""
14
14
  license = "Apache-2.0"
15
15
  # Dependencies for your Flower App
16
16
  dependencies = [
17
- "flwr[simulation]>=1.23.0",
17
+ "flwr[simulation]>=1.24.0",
18
18
  "flwr-datasets>=0.5.0",
19
19
  "xgboost>=2.0.0",
20
20
  ]
flwr/cli/pull.py CHANGED
@@ -16,7 +16,7 @@
16
16
 
17
17
 
18
18
  from pathlib import Path
19
- from typing import Annotated, Optional
19
+ from typing import Annotated
20
20
 
21
21
  import typer
22
22
 
@@ -50,22 +50,26 @@ def pull( # pylint: disable=R0914
50
50
  typer.Argument(help="Path of the Flower App to run."),
51
51
  ] = Path("."),
52
52
  federation: Annotated[
53
- Optional[str],
53
+ str | None,
54
54
  typer.Argument(help="Name of the federation."),
55
55
  ] = None,
56
56
  federation_config_overrides: Annotated[
57
- Optional[list[str]],
57
+ list[str] | None,
58
58
  typer.Option(
59
59
  "--federation-config",
60
60
  help=FEDERATION_CONFIG_HELP_MESSAGE,
61
61
  ),
62
62
  ] = None,
63
63
  ) -> None:
64
- """Pull artifacts from a Flower run."""
64
+ """Pull artifacts from a Flower run.
65
+
66
+ Retrieve a download URL for artifacts generated during a completed Flower run. The
67
+ artifacts can then be downloaded from the provided URL.
68
+ """
65
69
  typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
66
70
 
67
71
  pyproject_path = app / FAB_CONFIG_FILE if app else None
68
- config, errors, warnings = load_and_validate(path=pyproject_path)
72
+ config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
69
73
  config = process_loaded_project_config(config, errors, warnings)
70
74
  federation, federation_config = validate_federation_in_project_config(
71
75
  federation, config, federation_config_overrides
@@ -88,6 +92,7 @@ def pull( # pylint: disable=R0914
88
92
  "obtained.",
89
93
  fg=typer.colors.RED,
90
94
  bold=True,
95
+ err=True,
91
96
  )
92
97
  raise typer.Exit(code=1)
93
98
 
flwr/cli/run/run.py CHANGED
@@ -20,7 +20,7 @@ import io
20
20
  import json
21
21
  import subprocess
22
22
  from pathlib import Path
23
- from typing import Annotated, Any, Optional, cast
23
+ from typing import Annotated, Any, cast
24
24
 
25
25
  import typer
26
26
  from rich.console import Console
@@ -45,25 +45,31 @@ from flwr.common.serde import config_record_to_proto, fab_to_proto, user_config_
45
45
  from flwr.common.typing import Fab
46
46
  from flwr.proto.control_pb2 import StartRunRequest # pylint: disable=E0611
47
47
  from flwr.proto.control_pb2_grpc import ControlStub
48
+ from flwr.supercore.constant import NOOP_FEDERATION
48
49
 
49
50
  from ..log import start_stream
50
- from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
51
+ from ..utils import (
52
+ flwr_cli_grpc_exc_handler,
53
+ init_channel,
54
+ load_cli_auth_plugin,
55
+ parse_app_spec,
56
+ )
51
57
 
52
58
  CONN_REFRESH_PERIOD = 60 # Connection refresh period for log streaming (seconds)
53
59
 
54
60
 
55
- # pylint: disable-next=too-many-locals, R0913, R0917
61
+ # pylint: disable-next=too-many-locals, too-many-branches, R0913, R0917
56
62
  def run(
57
63
  app: Annotated[
58
64
  Path,
59
65
  typer.Argument(help="Path of the Flower App to run."),
60
66
  ] = Path("."),
61
67
  federation: Annotated[
62
- Optional[str],
68
+ str | None,
63
69
  typer.Argument(help="Name of the federation to run the app on."),
64
70
  ] = None,
65
71
  run_config_overrides: Annotated[
66
- Optional[list[str]],
72
+ list[str] | None,
67
73
  typer.Option(
68
74
  "--run-config",
69
75
  "-c",
@@ -71,7 +77,7 @@ def run(
71
77
  ),
72
78
  ] = None,
73
79
  federation_config_overrides: Annotated[
74
- Optional[list[str]],
80
+ list[str] | None,
75
81
  typer.Option(
76
82
  "--federation-config",
77
83
  help=FEDERATION_CONFIG_HELP_MESSAGE,
@@ -100,11 +106,25 @@ def run(
100
106
  try:
101
107
  if suppress_output:
102
108
  redirect_output(captured_output)
109
+
110
+ # Determine if app is remote
111
+ app_spec = None
112
+ if (app_str := str(app)).startswith("@"):
113
+ # Validate app version and ID format
114
+ _ = parse_app_spec(app_str)
115
+ app_spec = app_str
116
+ is_remote_app = app_spec is not None
117
+
103
118
  typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
104
119
 
105
- pyproject_path = app / "pyproject.toml" if app else None
106
- config, errors, warnings = load_and_validate(path=pyproject_path)
120
+ # Disable the validation for remote apps
121
+ pyproject_path = app / "pyproject.toml" if not is_remote_app else None
122
+ # `./pyproject.toml` will be loaded when `pyproject_path` is None
123
+ config, errors, warnings = load_and_validate(
124
+ pyproject_path, check_module=not is_remote_app
125
+ )
107
126
  config = process_loaded_project_config(config, errors, warnings)
127
+
108
128
  federation, federation_config = validate_federation_in_project_config(
109
129
  federation, config, federation_config_overrides
110
130
  )
@@ -117,6 +137,7 @@ def run(
117
137
  run_config_overrides,
118
138
  stream,
119
139
  output_format,
140
+ app_spec,
120
141
  )
121
142
  else:
122
143
  _run_without_control_api(
@@ -132,6 +153,7 @@ def run(
132
153
  f"{err}",
133
154
  fg=typer.colors.RED,
134
155
  bold=True,
156
+ err=True,
135
157
  )
136
158
  finally:
137
159
  if suppress_output:
@@ -144,22 +166,32 @@ def _run_with_control_api(
144
166
  app: Path,
145
167
  federation: str,
146
168
  federation_config: dict[str, Any],
147
- config_overrides: Optional[list[str]],
169
+ config_overrides: list[str] | None,
148
170
  stream: bool,
149
171
  output_format: str,
172
+ app_spec: str | None,
150
173
  ) -> None:
151
174
  channel = None
175
+ is_remote_app = app_spec is not None
152
176
  try:
153
177
  auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
154
178
  channel = init_channel(app, federation_config, auth_plugin)
155
179
  stub = ControlStub(channel)
156
180
 
157
- fab_bytes = build_fab_from_disk(app)
158
- fab_hash = hashlib.sha256(fab_bytes).hexdigest()
159
- config = cast(dict[str, Any], load_toml(app / FAB_CONFIG_FILE))
160
- fab_id, fab_version = get_metadata_from_config(config)
181
+ # Build FAB if local app
182
+ if not is_remote_app:
183
+ fab_bytes = build_fab_from_disk(app)
184
+ fab_hash = hashlib.sha256(fab_bytes).hexdigest()
185
+ config = cast(dict[str, Any], load_toml(app / FAB_CONFIG_FILE))
186
+ fab_id, fab_version = get_metadata_from_config(config)
187
+ fab = Fab(fab_hash, fab_bytes, {})
188
+ # Skip FAB build if remote app
189
+ else:
190
+ # Use empty values for FAB
191
+ fab_id = fab_version = fab_hash = ""
192
+ fab = Fab(fab_hash, b"", {})
161
193
 
162
- fab = Fab(fab_hash, fab_bytes, {})
194
+ real_federation: str = federation_config.get("federation", NOOP_FEDERATION)
163
195
 
164
196
  # Construct a `ConfigRecord` out of a flattened `UserConfig`
165
197
  fed_config = flatten_dict(federation_config.get("options", {}))
@@ -168,7 +200,9 @@ def _run_with_control_api(
168
200
  req = StartRunRequest(
169
201
  fab=fab_to_proto(fab),
170
202
  override_config=user_config_to_proto(parse_config_args(config_overrides)),
203
+ federation=real_federation,
171
204
  federation_options=config_record_to_proto(c_record),
205
+ app_spec=app_spec or "",
172
206
  )
173
207
  with flwr_cli_grpc_exc_handler():
174
208
  res = stub.StartRun(req)
@@ -178,23 +212,35 @@ def _run_with_control_api(
178
212
  f"🎊 Successfully started run {res.run_id}", fg=typer.colors.GREEN
179
213
  )
180
214
  else:
181
- typer.secho("❌ Failed to start run", fg=typer.colors.RED)
215
+ if is_remote_app:
216
+ typer.secho(
217
+ "❌ Failed to start run. Please check that the provided "
218
+ "app identifier (@account_name/app_name) is correct.",
219
+ fg=typer.colors.RED,
220
+ err=True,
221
+ )
222
+ else:
223
+ typer.secho("❌ Failed to start run", fg=typer.colors.RED, err=True)
182
224
  raise typer.Exit(code=1)
183
225
 
184
226
  if output_format == CliOutputFormat.JSON:
185
- run_output = json.dumps(
186
- {
187
- "success": res.HasField("run_id"),
188
- "run-id": res.run_id if res.HasField("run_id") else None,
189
- "fab-id": fab_id,
190
- "fab-name": fab_id.rsplit("/", maxsplit=1)[-1],
191
- "fab-version": fab_version,
192
- "fab-hash": fab_hash[:8],
193
- "fab-filename": get_fab_filename(config, fab_hash),
194
- }
195
- )
227
+ # Only include FAB metadata if we actually built a local FAB
228
+ payload: dict[str, Any] = {
229
+ "success": res.HasField("run_id"),
230
+ "run-id": res.run_id if res.HasField("run_id") else None,
231
+ }
232
+ if not is_remote_app:
233
+ payload.update(
234
+ {
235
+ "fab-id": fab_id,
236
+ "fab-name": fab_id.rsplit("/", maxsplit=1)[-1],
237
+ "fab-version": fab_version,
238
+ "fab-hash": fab_hash[:8],
239
+ "fab-filename": get_fab_filename(config, fab_hash),
240
+ }
241
+ )
196
242
  restore_output()
197
- Console().print_json(run_output)
243
+ Console().print_json(json.dumps(payload))
198
244
 
199
245
  if stream:
200
246
  start_stream(res.run_id, channel, CONN_REFRESH_PERIOD)
@@ -204,14 +250,14 @@ def _run_with_control_api(
204
250
 
205
251
 
206
252
  def _run_without_control_api(
207
- app: Optional[Path],
253
+ app: Path | None,
208
254
  federation_config: dict[str, Any],
209
- config_overrides: Optional[list[str]],
255
+ config_overrides: list[str] | None,
210
256
  federation: str,
211
257
  ) -> None:
212
258
  try:
213
259
  num_supernodes = federation_config["options"]["num-supernodes"]
214
- verbose: Optional[bool] = federation_config["options"].get("verbose")
260
+ verbose: bool | None = federation_config["options"].get("verbose")
215
261
  backend_cfg = federation_config["options"].get("backend", {})
216
262
  except KeyError as err:
217
263
  typer.secho(
@@ -222,6 +268,7 @@ def _run_without_control_api(
222
268
  "options.num-supernodes = 10\n",
223
269
  fg=typer.colors.RED,
224
270
  bold=True,
271
+ err=True,
225
272
  )
226
273
  raise typer.Exit(code=1) from err
227
274
 
flwr/cli/run_utils.py ADDED
@@ -0,0 +1,130 @@
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 utils."""
16
+
17
+
18
+ from dataclasses import dataclass
19
+ from datetime import datetime, timedelta
20
+
21
+ from flwr.common.date import format_timedelta, isoformat8601_utc
22
+ from flwr.common.typing import Run
23
+
24
+
25
+ @dataclass
26
+ class RunRow: # pylint: disable=too-many-instance-attributes
27
+ """Represents a single run's data for display.
28
+
29
+ Attributes
30
+ ----------
31
+ run_id : int
32
+ The unique identifier for the run.
33
+ federation : str
34
+ The federation name.
35
+ fab_id : str
36
+ The Flower App Bundle identifier.
37
+ fab_version : str
38
+ The FAB version string.
39
+ fab_hash : str
40
+ The SHA-256 hash of the FAB.
41
+ status_text : str
42
+ The formatted status text.
43
+ elapsed : str
44
+ The formatted elapsed time.
45
+ pending_at : str
46
+ Timestamp when run entered pending state.
47
+ starting_at : str
48
+ Timestamp when run entered starting state.
49
+ running_at : str
50
+ Timestamp when run entered running state.
51
+ finished_at : str
52
+ Timestamp when run finished.
53
+ """
54
+
55
+ run_id: int
56
+ federation: str
57
+ fab_id: str
58
+ fab_version: str
59
+ fab_hash: str
60
+ status_text: str
61
+ elapsed: str
62
+ pending_at: str
63
+ starting_at: str
64
+ running_at: str
65
+ finished_at: str
66
+
67
+
68
+ def format_runs(runs: list[Run], now_isoformat: str) -> list[RunRow]:
69
+ """Format runs to a list of RunRow objects.
70
+
71
+ Parameters
72
+ ----------
73
+ runs : list[Run]
74
+ List of Run objects to format.
75
+ now_isoformat : str
76
+ Current timestamp in ISO format for calculating elapsed time.
77
+
78
+ Returns
79
+ -------
80
+ list[RunRow]
81
+ List of formatted RunRow objects sorted by pending_at timestamp.
82
+ """
83
+
84
+ def _format_datetime(dt: datetime | None) -> str:
85
+ return isoformat8601_utc(dt).replace("T", " ") if dt else "N/A"
86
+
87
+ run_list: list[RunRow] = []
88
+
89
+ # Add rows
90
+ for run in sorted(runs, key=lambda x: datetime.fromisoformat(x.pending_at)):
91
+ # Combine status and sub-status into a single string
92
+ if run.status.sub_status == "":
93
+ status_text = run.status.status
94
+ else:
95
+ status_text = f"{run.status.status}:{run.status.sub_status}"
96
+
97
+ # Convert isoformat to datetime
98
+ pending_at = datetime.fromisoformat(run.pending_at) if run.pending_at else None
99
+ starting_at = (
100
+ datetime.fromisoformat(run.starting_at) if run.starting_at else None
101
+ )
102
+ running_at = datetime.fromisoformat(run.running_at) if run.running_at else None
103
+ finished_at = (
104
+ datetime.fromisoformat(run.finished_at) if run.finished_at else None
105
+ )
106
+
107
+ # Calculate elapsed time
108
+ elapsed_time = timedelta()
109
+ if running_at:
110
+ if finished_at:
111
+ end_time = finished_at
112
+ else:
113
+ end_time = datetime.fromisoformat(now_isoformat)
114
+ elapsed_time = end_time - running_at
115
+
116
+ row = RunRow(
117
+ run_id=run.run_id,
118
+ federation=run.federation,
119
+ fab_id=run.fab_id,
120
+ fab_version=run.fab_version,
121
+ fab_hash=run.fab_hash,
122
+ status_text=status_text,
123
+ elapsed=format_timedelta(elapsed_time),
124
+ pending_at=_format_datetime(pending_at),
125
+ starting_at=_format_datetime(starting_at),
126
+ running_at=_format_datetime(running_at),
127
+ finished_at=_format_datetime(finished_at),
128
+ )
129
+ run_list.append(row)
130
+ return run_list