flwr 1.22.0__py3-none-any.whl → 1.23.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 (108) hide show
  1. flwr/cli/app.py +15 -1
  2. flwr/cli/auth_plugin/__init__.py +15 -6
  3. flwr/cli/auth_plugin/auth_plugin.py +95 -0
  4. flwr/cli/auth_plugin/noop_auth_plugin.py +58 -0
  5. flwr/cli/auth_plugin/oidc_cli_plugin.py +16 -25
  6. flwr/cli/build.py +118 -47
  7. flwr/cli/{cli_user_auth_interceptor.py → cli_account_auth_interceptor.py} +6 -5
  8. flwr/cli/log.py +2 -2
  9. flwr/cli/login/login.py +34 -23
  10. flwr/cli/ls.py +13 -9
  11. flwr/cli/new/new.py +187 -35
  12. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
  13. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
  14. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  15. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  16. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  17. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  18. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
  19. flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +1 -1
  20. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  21. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
  22. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +1 -1
  23. flwr/cli/pull.py +2 -2
  24. flwr/cli/run/run.py +11 -7
  25. flwr/cli/stop.py +2 -2
  26. flwr/cli/supernode/__init__.py +25 -0
  27. flwr/cli/supernode/ls.py +260 -0
  28. flwr/cli/supernode/register.py +185 -0
  29. flwr/cli/supernode/unregister.py +138 -0
  30. flwr/cli/utils.py +92 -69
  31. flwr/client/__init__.py +2 -1
  32. flwr/client/grpc_adapter_client/connection.py +6 -8
  33. flwr/client/grpc_rere_client/connection.py +59 -31
  34. flwr/client/grpc_rere_client/grpc_adapter.py +28 -12
  35. flwr/client/grpc_rere_client/{client_interceptor.py → node_auth_client_interceptor.py} +3 -6
  36. flwr/client/mod/secure_aggregation/secaggplus_mod.py +7 -5
  37. flwr/client/rest_client/connection.py +82 -37
  38. flwr/clientapp/__init__.py +1 -2
  39. flwr/{client/clientapp → clientapp}/utils.py +1 -1
  40. flwr/common/constant.py +53 -13
  41. flwr/common/exit/exit_code.py +20 -10
  42. flwr/common/inflatable_utils.py +10 -10
  43. flwr/common/record/array.py +3 -3
  44. flwr/common/record/arrayrecord.py +10 -1
  45. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +1 -89
  46. flwr/common/serde.py +4 -2
  47. flwr/common/typing.py +7 -6
  48. flwr/compat/client/app.py +1 -1
  49. flwr/compat/client/grpc_client/connection.py +2 -2
  50. flwr/proto/control_pb2.py +48 -35
  51. flwr/proto/control_pb2.pyi +71 -5
  52. flwr/proto/control_pb2_grpc.py +102 -0
  53. flwr/proto/control_pb2_grpc.pyi +39 -0
  54. flwr/proto/fab_pb2.py +11 -7
  55. flwr/proto/fab_pb2.pyi +21 -1
  56. flwr/proto/fleet_pb2.py +31 -23
  57. flwr/proto/fleet_pb2.pyi +63 -23
  58. flwr/proto/fleet_pb2_grpc.py +98 -28
  59. flwr/proto/fleet_pb2_grpc.pyi +45 -13
  60. flwr/proto/node_pb2.py +3 -1
  61. flwr/proto/node_pb2.pyi +48 -0
  62. flwr/server/app.py +139 -114
  63. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +17 -7
  64. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +132 -38
  65. flwr/server/superlink/fleet/grpc_rere/{server_interceptor.py → node_auth_server_interceptor.py} +27 -51
  66. flwr/server/superlink/fleet/message_handler/message_handler.py +67 -22
  67. flwr/server/superlink/fleet/rest_rere/rest_api.py +52 -31
  68. flwr/server/superlink/fleet/vce/backend/backend.py +1 -1
  69. flwr/server/superlink/fleet/vce/backend/raybackend.py +1 -1
  70. flwr/server/superlink/fleet/vce/vce_api.py +18 -5
  71. flwr/server/superlink/linkstate/in_memory_linkstate.py +167 -73
  72. flwr/server/superlink/linkstate/linkstate.py +107 -24
  73. flwr/server/superlink/linkstate/linkstate_factory.py +2 -1
  74. flwr/server/superlink/linkstate/sqlite_linkstate.py +306 -255
  75. flwr/server/superlink/linkstate/utils.py +3 -54
  76. flwr/server/superlink/serverappio/serverappio_servicer.py +2 -2
  77. flwr/server/superlink/simulation/simulationio_servicer.py +1 -1
  78. flwr/server/utils/validator.py +2 -3
  79. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +4 -2
  80. flwr/simulation/ray_transport/ray_actor.py +1 -1
  81. flwr/simulation/ray_transport/ray_client_proxy.py +1 -1
  82. flwr/simulation/run_simulation.py +3 -2
  83. flwr/supercore/constant.py +22 -0
  84. flwr/supercore/object_store/in_memory_object_store.py +0 -4
  85. flwr/supercore/object_store/object_store_factory.py +26 -6
  86. flwr/supercore/object_store/sqlite_object_store.py +252 -0
  87. flwr/{client/clientapp → supercore/primitives}/__init__.py +1 -1
  88. flwr/supercore/primitives/asymmetric.py +117 -0
  89. flwr/supercore/primitives/asymmetric_ed25519.py +165 -0
  90. flwr/supercore/sqlite_mixin.py +156 -0
  91. flwr/supercore/utils.py +20 -0
  92. flwr/{common → superlink}/auth_plugin/__init__.py +6 -6
  93. flwr/superlink/auth_plugin/auth_plugin.py +91 -0
  94. flwr/superlink/auth_plugin/noop_auth_plugin.py +87 -0
  95. flwr/superlink/servicer/control/{control_user_auth_interceptor.py → control_account_auth_interceptor.py} +19 -19
  96. flwr/superlink/servicer/control/control_event_log_interceptor.py +1 -1
  97. flwr/superlink/servicer/control/control_grpc.py +13 -11
  98. flwr/superlink/servicer/control/control_servicer.py +152 -60
  99. flwr/supernode/cli/flower_supernode.py +19 -26
  100. flwr/supernode/runtime/run_clientapp.py +2 -2
  101. flwr/supernode/servicer/clientappio/clientappio_servicer.py +1 -1
  102. flwr/supernode/start_client_internal.py +17 -9
  103. {flwr-1.22.0.dist-info → flwr-1.23.0.dist-info}/METADATA +1 -1
  104. {flwr-1.22.0.dist-info → flwr-1.23.0.dist-info}/RECORD +107 -96
  105. flwr/common/auth_plugin/auth_plugin.py +0 -149
  106. /flwr/{client → clientapp}/client_app.py +0 -0
  107. {flwr-1.22.0.dist-info → flwr-1.23.0.dist-info}/WHEEL +0 -0
  108. {flwr-1.22.0.dist-info → flwr-1.23.0.dist-info}/entry_points.txt +0 -0
flwr/cli/login/login.py CHANGED
@@ -20,6 +20,7 @@ from typing import Annotated, Optional
20
20
 
21
21
  import typer
22
22
 
23
+ from flwr.cli.auth_plugin import LoginError, NoOpCliAuthPlugin
23
24
  from flwr.cli.config_utils import (
24
25
  exit_if_no_address,
25
26
  get_insecure_flag,
@@ -28,14 +29,19 @@ from flwr.cli.config_utils import (
28
29
  validate_federation_in_project_config,
29
30
  )
30
31
  from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
31
- from flwr.common.typing import UserAuthLoginDetails
32
+ from flwr.common.typing import AccountAuthLoginDetails
32
33
  from flwr.proto.control_pb2 import ( # pylint: disable=E0611
33
34
  GetLoginDetailsRequest,
34
35
  GetLoginDetailsResponse,
35
36
  )
36
37
  from flwr.proto.control_pb2_grpc import ControlStub
37
38
 
38
- from ..utils import flwr_cli_grpc_exc_handler, init_channel, try_obtain_cli_auth_plugin
39
+ from ..utils import (
40
+ account_auth_enabled,
41
+ flwr_cli_grpc_exc_handler,
42
+ init_channel,
43
+ load_cli_auth_plugin,
44
+ )
39
45
 
40
46
 
41
47
  def login( # pylint: disable=R0914
@@ -67,12 +73,13 @@ def login( # pylint: disable=R0914
67
73
  )
68
74
  exit_if_no_address(federation_config, "login")
69
75
 
70
- # Check if `enable-user-auth` is set to `true`
71
- if not federation_config.get("enable-user-auth", False):
76
+ # Check if `enable-account-auth` is set to `true`
77
+
78
+ if not account_auth_enabled(federation_config):
72
79
  typer.secho(
73
- f"❌ User authentication is not enabled for the federation '{federation}'. "
74
- "To enable it, set `enable-user-auth = true` in the federation "
75
- "configuration.",
80
+ "❌ Account authentication is not enabled for the federation "
81
+ f"'{federation}'. To enable it, set `enable-account-auth = true` "
82
+ "in the federation configuration.",
76
83
  fg=typer.colors.RED,
77
84
  bold=True,
78
85
  )
@@ -88,7 +95,7 @@ def login( # pylint: disable=R0914
88
95
  )
89
96
  raise typer.Exit(code=1)
90
97
 
91
- channel = init_channel(app, federation_config, None)
98
+ channel = init_channel(app, federation_config, NoOpCliAuthPlugin(Path()))
92
99
  stub = ControlStub(channel)
93
100
 
94
101
  login_request = GetLoginDetailsRequest()
@@ -96,28 +103,32 @@ def login( # pylint: disable=R0914
96
103
  login_response: GetLoginDetailsResponse = stub.GetLoginDetails(login_request)
97
104
 
98
105
  # Get the auth plugin
99
- auth_type = login_response.auth_type
100
- auth_plugin = try_obtain_cli_auth_plugin(
101
- app, federation, federation_config, auth_type
102
- )
103
- if auth_plugin is None:
104
- typer.secho(
105
- f'❌ Authentication type "{auth_type}" not found',
106
- fg=typer.colors.RED,
107
- bold=True,
108
- )
109
- raise typer.Exit(code=1)
106
+ authn_type = login_response.authn_type
107
+ auth_plugin = load_cli_auth_plugin(app, federation, federation_config, authn_type)
110
108
 
111
109
  # Login
112
- details = UserAuthLoginDetails(
113
- auth_type=login_response.auth_type,
110
+ details = AccountAuthLoginDetails(
111
+ authn_type=login_response.authn_type,
114
112
  device_code=login_response.device_code,
115
113
  verification_uri_complete=login_response.verification_uri_complete,
116
114
  expires_in=login_response.expires_in,
117
115
  interval=login_response.interval,
118
116
  )
119
- with flwr_cli_grpc_exc_handler():
120
- credentials = auth_plugin.login(details, stub)
117
+ try:
118
+ with flwr_cli_grpc_exc_handler():
119
+ credentials = auth_plugin.login(details, stub)
120
+ typer.secho(
121
+ "✅ Login successful.",
122
+ fg=typer.colors.GREEN,
123
+ bold=False,
124
+ )
125
+ except LoginError as e:
126
+ typer.secho(
127
+ f"❌ Login failed: {e.message}",
128
+ fg=typer.colors.RED,
129
+ bold=True,
130
+ )
131
+ raise typer.Exit(code=1) from None
121
132
 
122
133
  # Store the tokens
123
134
  auth_plugin.store_tokens(credentials)
flwr/cli/ls.py CHANGED
@@ -19,7 +19,7 @@ import io
19
19
  import json
20
20
  from datetime import datetime, timedelta
21
21
  from pathlib import Path
22
- from typing import Annotated, Optional
22
+ from typing import Annotated, Optional, cast
23
23
 
24
24
  import typer
25
25
  from rich.console import Console
@@ -44,12 +44,13 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
44
44
  )
45
45
  from flwr.proto.control_pb2_grpc import ControlStub
46
46
 
47
- from .utils import flwr_cli_grpc_exc_handler, init_channel, try_obtain_cli_auth_plugin
47
+ from .utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
48
48
 
49
49
  _RunListType = tuple[int, str, str, str, str, str, str, str, str]
50
50
 
51
51
 
52
52
  def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
53
+ ctx: typer.Context,
53
54
  app: Annotated[
54
55
  Path,
55
56
  typer.Argument(help="Path of the Flower project"),
@@ -102,6 +103,9 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
102
103
 
103
104
  All timestamps follow ISO 8601, UTC and are formatted as ``YYYY-MM-DD HH:MM:SSZ``.
104
105
  """
106
+ # Resolve command used (list or ls)
107
+ command_name = cast(str, ctx.command.name) if ctx.command else "list"
108
+
105
109
  suppress_output = output_format == CliOutputFormat.JSON
106
110
  captured_output = io.StringIO()
107
111
  try:
@@ -116,14 +120,14 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
116
120
  federation, federation_config = validate_federation_in_project_config(
117
121
  federation, config, federation_config_overrides
118
122
  )
119
- exit_if_no_address(federation_config, "ls")
123
+ exit_if_no_address(federation_config, command_name)
120
124
  channel = None
121
125
  try:
122
126
  if runs and run_id is not None:
123
127
  raise ValueError(
124
128
  "The options '--runs' and '--run-id' are mutually exclusive."
125
129
  )
126
- auth_plugin = try_obtain_cli_auth_plugin(app, federation, federation_config)
130
+ auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
127
131
  channel = init_channel(app, federation_config, auth_plugin)
128
132
  stub = ControlStub(channel)
129
133
 
@@ -216,14 +220,14 @@ def _to_table(run_list: list[_RunListType]) -> Table:
216
220
 
217
221
  # Add columns
218
222
  table.add_column(
219
- Text("Run ID", justify="center"), style="bright_white", overflow="fold"
223
+ Text("Run ID", justify="center"), style="bright_black", no_wrap=True
220
224
  )
221
- table.add_column(Text("FAB", justify="center"), style="dim white")
225
+ table.add_column(Text("FAB", justify="center"), style="bright_black")
222
226
  table.add_column(Text("Status", justify="center"))
223
227
  table.add_column(Text("Elapsed", justify="center"), style="blue")
224
- table.add_column(Text("Created At", justify="center"), style="dim white")
225
- table.add_column(Text("Running At", justify="center"), style="dim white")
226
- table.add_column(Text("Finished At", justify="center"), style="dim white")
228
+ table.add_column(Text("Created At", justify="center"), style="bright_black")
229
+ table.add_column(Text("Running At", justify="center"), style="bright_black")
230
+ table.add_column(Text("Finished At", justify="center"), style="bright_black")
227
231
 
228
232
  for row in run_list:
229
233
  (
flwr/cli/new/new.py CHANGED
@@ -15,14 +15,20 @@
15
15
  """Flower command line interface `new` command."""
16
16
 
17
17
 
18
+ import io
19
+ import json
18
20
  import re
21
+ import zipfile
19
22
  from enum import Enum
20
23
  from pathlib import Path
21
24
  from string import Template
22
25
  from typing import Annotated, Optional
23
26
 
27
+ import requests
24
28
  import typer
25
29
 
30
+ from flwr.supercore.constant import APP_ID_PATTERN, PLATFORM_API_URL
31
+
26
32
  from ..utils import (
27
33
  is_valid_project_name,
28
34
  prompt_options,
@@ -93,6 +99,180 @@ def render_and_create(file_path: Path, template: str, context: dict[str, str]) -
93
99
  create_file(file_path, content)
94
100
 
95
101
 
102
+ def print_success_prompt(
103
+ package_name: str, llm_challenge_str: Optional[str] = None
104
+ ) -> None:
105
+ """Print styled setup instructions for running a new Flower App after creation."""
106
+ prompt = typer.style(
107
+ "🎊 Flower App creation successful.\n\n"
108
+ "To run your Flower App, first install its dependencies:\n\n",
109
+ fg=typer.colors.GREEN,
110
+ bold=True,
111
+ )
112
+
113
+ _add = " huggingface-cli login\n" if llm_challenge_str else ""
114
+
115
+ prompt += typer.style(
116
+ f" cd {package_name} && pip install -e .\n" + _add + "\n",
117
+ fg=typer.colors.BRIGHT_CYAN,
118
+ bold=True,
119
+ )
120
+
121
+ prompt += typer.style(
122
+ "then, run the app:\n\n ",
123
+ fg=typer.colors.GREEN,
124
+ bold=True,
125
+ )
126
+
127
+ prompt += typer.style(
128
+ "\tflwr run .\n\n",
129
+ fg=typer.colors.BRIGHT_CYAN,
130
+ bold=True,
131
+ )
132
+
133
+ prompt += typer.style(
134
+ "💡 Check the README in your app directory to learn how to\n"
135
+ "customize it and how to run it using the Deployment Runtime.\n",
136
+ fg=typer.colors.GREEN,
137
+ bold=True,
138
+ )
139
+
140
+ print(prompt)
141
+
142
+
143
+ # Security: prevent zip-slip
144
+ def _safe_extract_zip(zf: zipfile.ZipFile, dest_dir: Path) -> None:
145
+ """Extract ZIP file into destination directory."""
146
+ dest_dir = dest_dir.resolve()
147
+
148
+ def _is_within_directory(base: Path, target: Path) -> bool:
149
+ try:
150
+ target.relative_to(base)
151
+ return True
152
+ except ValueError:
153
+ return False
154
+
155
+ for member in zf.infolist():
156
+ # Skip directory placeholders;
157
+ # ZipInfo can represent them as names ending with '/'.
158
+ if member.is_dir():
159
+ target_path = (dest_dir / member.filename).resolve()
160
+ if not _is_within_directory(dest_dir, target_path):
161
+ raise ValueError(f"Unsafe path in zip: {member.filename}")
162
+ target_path.mkdir(parents=True, exist_ok=True)
163
+ continue
164
+
165
+ # Files
166
+ target_path = (dest_dir / member.filename).resolve()
167
+ if not _is_within_directory(dest_dir, target_path):
168
+ raise ValueError(f"Unsafe path in zip: {member.filename}")
169
+
170
+ # Ensure parent exists
171
+ target_path.parent.mkdir(parents=True, exist_ok=True)
172
+
173
+ # Extract
174
+ with zf.open(member, "r") as src, open(target_path, "wb") as dst:
175
+ dst.write(src.read())
176
+
177
+
178
+ def _download_zip_to_memory(presigned_url: str) -> io.BytesIO:
179
+ """Download ZIP file from Platform API to memory."""
180
+ try:
181
+ r = requests.get(presigned_url, timeout=60)
182
+ r.raise_for_status()
183
+ except requests.RequestException as e:
184
+ raise typer.BadParameter(f"ZIP download failed: {e}") from e
185
+
186
+ buf = io.BytesIO(r.content)
187
+ # Validate it's a zip
188
+ if not zipfile.is_zipfile(buf):
189
+ raise typer.BadParameter("Downloaded file is not a valid ZIP")
190
+ buf.seek(0)
191
+ return buf
192
+
193
+
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:
225
+ """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")
233
+
234
+ project_dir = Path.cwd() / app_name
235
+ if project_dir.exists():
236
+ if not typer.confirm(
237
+ typer.style(
238
+ f"\n💬 {app_name} already exists, do you want to override it?",
239
+ fg=typer.colors.MAGENTA,
240
+ bold=True,
241
+ )
242
+ ):
243
+ return
244
+
245
+ print(
246
+ typer.style(
247
+ f"\n🔗 Requesting download link for {identifier}...",
248
+ fg=typer.colors.GREEN,
249
+ bold=True,
250
+ )
251
+ )
252
+ presigned_url = _request_download_link(identifier)
253
+
254
+ print(
255
+ typer.style(
256
+ "⬇️ Downloading ZIP into memory...",
257
+ fg=typer.colors.GREEN,
258
+ bold=True,
259
+ )
260
+ )
261
+ zip_buf = _download_zip_to_memory(presigned_url)
262
+
263
+ print(
264
+ typer.style(
265
+ f"📦 Unpacking into {project_dir}...",
266
+ fg=typer.colors.GREEN,
267
+ bold=True,
268
+ )
269
+ )
270
+ with zipfile.ZipFile(zip_buf) as zf:
271
+ _safe_extract_zip(zf, Path.cwd())
272
+
273
+ print_success_prompt(app_name)
274
+
275
+
96
276
  # pylint: disable=too-many-locals,too-many-branches,too-many-statements
97
277
  def new(
98
278
  app_name: Annotated[
@@ -111,6 +291,12 @@ def new(
111
291
  """Create new Flower App."""
112
292
  if app_name is None:
113
293
  app_name = prompt_text("Please provide the app name")
294
+
295
+ # Download remote app
296
+ if app_name and app_name.startswith("@"):
297
+ download_remote_app_via_api(app_name)
298
+ return
299
+
114
300
  if not is_valid_project_name(app_name):
115
301
  app_name = prompt_text(
116
302
  "Please provide a name that only contains "
@@ -282,38 +468,4 @@ def new(
282
468
  context=context,
283
469
  )
284
470
 
285
- prompt = typer.style(
286
- "🎊 Flower App creation successful.\n\n"
287
- "To run your Flower App, first install its dependencies:\n\n",
288
- fg=typer.colors.GREEN,
289
- bold=True,
290
- )
291
-
292
- _add = " huggingface-cli login\n" if llm_challenge_str else ""
293
-
294
- prompt += typer.style(
295
- f" cd {package_name} && pip install -e .\n" + _add + "\n",
296
- fg=typer.colors.BRIGHT_CYAN,
297
- bold=True,
298
- )
299
-
300
- prompt += typer.style(
301
- "then, run the app:\n\n ",
302
- fg=typer.colors.GREEN,
303
- bold=True,
304
- )
305
-
306
- prompt += typer.style(
307
- "\tflwr run .\n\n",
308
- fg=typer.colors.BRIGHT_CYAN,
309
- bold=True,
310
- )
311
-
312
- prompt += typer.style(
313
- "💡 Check the README in your app directory to learn how to\n"
314
- "customize it and how to run it using the Deployment Runtime.\n",
315
- fg=typer.colors.GREEN,
316
- bold=True,
317
- )
318
-
319
- print(prompt)
471
+ print_success_prompt(package_name, llm_challenge_str)
@@ -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.22.0",
17
+ "flwr[simulation]>=1.23.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.22.0",
17
+ "flwr[simulation]>=1.23.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.22.0",
17
+ "flwr[simulation]>=1.23.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.22.0",
17
+ "flwr[simulation]>=1.23.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.22.0",
17
+ "flwr[simulation]>=1.23.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.22.0",
17
+ "flwr[simulation]>=1.23.0",
18
18
  "numpy>=2.0.2",
19
19
  ]
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.22.0",
17
+ "flwr[simulation]>=1.23.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.22.0",
17
+ "flwr[simulation]>=1.23.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.22.0",
17
+ "flwr[simulation]>=1.23.0",
18
18
  "flwr-datasets[vision]>=0.5.0",
19
19
  "scikit-learn>=1.6.1",
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.22.0",
17
+ "flwr[simulation]>=1.23.0",
18
18
  "flwr-datasets[vision]>=0.5.0",
19
19
  "tensorflow>=2.11.1,<2.18.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.22.0",
17
+ "flwr[simulation]>=1.23.0",
18
18
  "flwr-datasets>=0.5.0",
19
19
  "xgboost>=2.0.0",
20
20
  ]
flwr/cli/pull.py CHANGED
@@ -34,7 +34,7 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
34
34
  )
35
35
  from flwr.proto.control_pb2_grpc import ControlStub
36
36
 
37
- from .utils import flwr_cli_grpc_exc_handler, init_channel, try_obtain_cli_auth_plugin
37
+ from .utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
38
38
 
39
39
 
40
40
  def pull( # pylint: disable=R0914
@@ -74,7 +74,7 @@ def pull( # pylint: disable=R0914
74
74
  channel = None
75
75
  try:
76
76
 
77
- auth_plugin = try_obtain_cli_auth_plugin(app, federation, federation_config)
77
+ auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
78
78
  channel = init_channel(app, federation_config, auth_plugin)
79
79
  stub = ControlStub(channel)
80
80
  with flwr_cli_grpc_exc_handler():
flwr/cli/run/run.py CHANGED
@@ -15,16 +15,18 @@
15
15
  """Flower command line interface `run` command."""
16
16
 
17
17
 
18
+ import hashlib
18
19
  import io
19
20
  import json
20
21
  import subprocess
21
22
  from pathlib import Path
22
- from typing import Annotated, Any, Optional
23
+ from typing import Annotated, Any, Optional, cast
23
24
 
24
25
  import typer
25
26
  from rich.console import Console
26
27
 
27
- from flwr.cli.build import build_fab, get_fab_filename
28
+ from flwr.cli.build import build_fab_from_disk, get_fab_filename
29
+ from flwr.cli.config_utils import load as load_toml
28
30
  from flwr.cli.config_utils import (
29
31
  load_and_validate,
30
32
  process_loaded_project_config,
@@ -37,7 +39,7 @@ from flwr.common.config import (
37
39
  parse_config_args,
38
40
  user_config_to_configrecord,
39
41
  )
40
- from flwr.common.constant import CliOutputFormat
42
+ from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
41
43
  from flwr.common.logger import print_json_error, redirect_output, restore_output
42
44
  from flwr.common.serde import config_record_to_proto, fab_to_proto, user_config_to_proto
43
45
  from flwr.common.typing import Fab
@@ -45,7 +47,7 @@ from flwr.proto.control_pb2 import StartRunRequest # pylint: disable=E0611
45
47
  from flwr.proto.control_pb2_grpc import ControlStub
46
48
 
47
49
  from ..log import start_stream
48
- from ..utils import flwr_cli_grpc_exc_handler, init_channel, try_obtain_cli_auth_plugin
50
+ from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
49
51
 
50
52
  CONN_REFRESH_PERIOD = 60 # Connection refresh period for log streaming (seconds)
51
53
 
@@ -148,14 +150,16 @@ def _run_with_control_api(
148
150
  ) -> None:
149
151
  channel = None
150
152
  try:
151
- auth_plugin = try_obtain_cli_auth_plugin(app, federation, federation_config)
153
+ auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
152
154
  channel = init_channel(app, federation_config, auth_plugin)
153
155
  stub = ControlStub(channel)
154
156
 
155
- fab_bytes, fab_hash, config = build_fab(app)
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))
156
160
  fab_id, fab_version = get_metadata_from_config(config)
157
161
 
158
- fab = Fab(fab_hash, fab_bytes)
162
+ fab = Fab(fab_hash, fab_bytes, {})
159
163
 
160
164
  # Construct a `ConfigRecord` out of a flattened `UserConfig`
161
165
  fed_config = flatten_dict(federation_config.get("options", {}))
flwr/cli/stop.py CHANGED
@@ -38,7 +38,7 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
38
38
  )
39
39
  from flwr.proto.control_pb2_grpc import ControlStub
40
40
 
41
- from .utils import flwr_cli_grpc_exc_handler, init_channel, try_obtain_cli_auth_plugin
41
+ from .utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
42
42
 
43
43
 
44
44
  def stop( # pylint: disable=R0914
@@ -89,7 +89,7 @@ def stop( # pylint: disable=R0914
89
89
  exit_if_no_address(federation_config, "stop")
90
90
  channel = None
91
91
  try:
92
- auth_plugin = try_obtain_cli_auth_plugin(app, federation, federation_config)
92
+ auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
93
93
  channel = init_channel(app, federation_config, auth_plugin)
94
94
  stub = ControlStub(channel) # pylint: disable=unused-variable # noqa: F841
95
95
 
@@ -0,0 +1,25 @@
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 `supernode` command."""
16
+
17
+ from .ls import ls as ls
18
+ from .register import register as register
19
+ from .unregister import unregister as unregister
20
+
21
+ __all__ = [
22
+ "ls",
23
+ "register",
24
+ "unregister",
25
+ ]