flwr 1.25.0__py3-none-any.whl → 1.26.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 (140) hide show
  1. flwr/__init__.py +1 -1
  2. flwr/app/__init__.py +4 -1
  3. flwr/app/message_type.py +29 -0
  4. flwr/app/metadata.py +5 -2
  5. flwr/app/user_config.py +19 -0
  6. flwr/cli/app.py +37 -19
  7. flwr/cli/app_cmd/publish.py +25 -75
  8. flwr/cli/app_cmd/review.py +18 -69
  9. flwr/cli/auth_plugin/auth_plugin.py +5 -10
  10. flwr/cli/auth_plugin/noop_auth_plugin.py +1 -2
  11. flwr/cli/auth_plugin/oidc_cli_plugin.py +38 -38
  12. flwr/cli/build.py +15 -28
  13. flwr/cli/config/__init__.py +21 -0
  14. flwr/cli/config/ls.py +71 -0
  15. flwr/cli/config_migration.py +297 -0
  16. flwr/cli/config_utils.py +63 -156
  17. flwr/cli/constant.py +71 -0
  18. flwr/cli/federation/__init__.py +0 -2
  19. flwr/cli/federation/ls.py +256 -64
  20. flwr/cli/flower_config.py +429 -0
  21. flwr/cli/install.py +23 -62
  22. flwr/cli/log.py +23 -37
  23. flwr/cli/login/login.py +29 -63
  24. flwr/cli/ls.py +28 -58
  25. flwr/cli/new/new.py +9 -29
  26. flwr/cli/pull.py +19 -37
  27. flwr/cli/run/run.py +85 -93
  28. flwr/cli/run_utils.py +1 -1
  29. flwr/cli/stop.py +32 -73
  30. flwr/cli/supernode/ls.py +25 -57
  31. flwr/cli/supernode/register.py +31 -80
  32. flwr/cli/supernode/unregister.py +24 -70
  33. flwr/cli/typing.py +200 -0
  34. flwr/cli/utils.py +160 -275
  35. flwr/client/grpc_rere_client/connection.py +3 -3
  36. flwr/client/grpc_rere_client/grpc_adapter.py +1 -1
  37. flwr/client/message_handler/message_handler.py +2 -1
  38. flwr/client/mod/centraldp_mods.py +1 -1
  39. flwr/client/mod/localdp_mod.py +1 -1
  40. flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
  41. flwr/client/run_info_store.py +2 -1
  42. flwr/clientapp/client_app.py +2 -1
  43. flwr/common/__init__.py +3 -2
  44. flwr/common/args.py +5 -5
  45. flwr/common/config.py +12 -17
  46. flwr/common/constant.py +3 -16
  47. flwr/common/context.py +2 -1
  48. flwr/common/exit/exit.py +4 -4
  49. flwr/common/exit/exit_code.py +6 -0
  50. flwr/common/grpc.py +2 -1
  51. flwr/common/logger.py +1 -1
  52. flwr/common/message.py +1 -1
  53. flwr/common/retry_invoker.py +13 -5
  54. flwr/common/secure_aggregation/ndarrays_arithmetic.py +5 -2
  55. flwr/common/serde.py +7 -5
  56. flwr/common/telemetry.py +1 -1
  57. flwr/common/typing.py +4 -3
  58. flwr/compat/client/app.py +6 -9
  59. flwr/compat/client/grpc_client/connection.py +2 -1
  60. flwr/compat/common/constant.py +29 -0
  61. flwr/compat/server/app.py +1 -1
  62. flwr/proto/clientappio_pb2.py +2 -2
  63. flwr/proto/clientappio_pb2_grpc.py +104 -88
  64. flwr/proto/clientappio_pb2_grpc.pyi +140 -80
  65. flwr/proto/federation_pb2.py +5 -3
  66. flwr/proto/federation_pb2.pyi +32 -2
  67. flwr/proto/run_pb2.py +5 -13
  68. flwr/proto/run_pb2.pyi +0 -57
  69. flwr/proto/serverappio_pb2.py +2 -2
  70. flwr/proto/serverappio_pb2_grpc.py +138 -207
  71. flwr/proto/serverappio_pb2_grpc.pyi +189 -155
  72. flwr/proto/simulationio_pb2.py +2 -2
  73. flwr/proto/simulationio_pb2_grpc.py +62 -90
  74. flwr/proto/simulationio_pb2_grpc.pyi +95 -55
  75. flwr/server/app.py +6 -13
  76. flwr/server/compat/grid_client_proxy.py +2 -1
  77. flwr/server/grid/grpc_grid.py +5 -5
  78. flwr/server/serverapp/app.py +11 -4
  79. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -1
  80. flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +13 -12
  81. flwr/server/superlink/fleet/message_handler/message_handler.py +6 -5
  82. flwr/server/superlink/linkstate/__init__.py +2 -2
  83. flwr/server/superlink/linkstate/in_memory_linkstate.py +2 -10
  84. flwr/server/superlink/linkstate/linkstate.py +2 -21
  85. flwr/server/superlink/linkstate/linkstate_factory.py +16 -8
  86. flwr/server/superlink/linkstate/{sqlite_linkstate.py → sql_linkstate.py} +432 -534
  87. flwr/server/superlink/linkstate/utils.py +49 -2
  88. flwr/server/superlink/serverappio/serverappio_servicer.py +1 -33
  89. flwr/server/superlink/simulation/simulationio_servicer.py +0 -19
  90. flwr/server/utils/validator.py +1 -1
  91. flwr/server/workflow/default_workflows.py +2 -1
  92. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
  93. flwr/serverapp/strategy/bulyan.py +7 -1
  94. flwr/serverapp/strategy/dp_fixed_clipping.py +9 -1
  95. flwr/serverapp/strategy/fedavg.py +1 -1
  96. flwr/serverapp/strategy/fedxgb_cyclic.py +1 -1
  97. flwr/simulation/ray_transport/ray_client_proxy.py +2 -6
  98. flwr/simulation/run_simulation.py +3 -12
  99. flwr/simulation/simulationio_connection.py +3 -3
  100. flwr/{common → supercore}/address.py +7 -33
  101. flwr/supercore/app_utils.py +2 -1
  102. flwr/supercore/constant.py +24 -2
  103. flwr/supercore/corestate/{sqlite_corestate.py → sql_corestate.py} +19 -23
  104. flwr/supercore/credential_store/__init__.py +33 -0
  105. flwr/supercore/credential_store/credential_store.py +34 -0
  106. flwr/supercore/credential_store/file_credential_store.py +76 -0
  107. flwr/{common → supercore}/date.py +0 -11
  108. flwr/supercore/ffs/disk_ffs.py +1 -1
  109. flwr/supercore/object_store/object_store_factory.py +14 -6
  110. flwr/supercore/object_store/{sqlite_object_store.py → sql_object_store.py} +115 -117
  111. flwr/supercore/sql_mixin.py +315 -0
  112. flwr/supercore/state/__init__.py +15 -0
  113. flwr/supercore/state/alembic/__init__.py +15 -0
  114. flwr/supercore/state/alembic/env.py +103 -0
  115. flwr/supercore/state/alembic/script.py.mako +43 -0
  116. flwr/supercore/state/alembic/utils.py +239 -0
  117. flwr/supercore/state/alembic/versions/__init__.py +15 -0
  118. flwr/supercore/state/alembic/versions/rev_2026_01_28_initialize_migration_of_state_tables.py +200 -0
  119. flwr/supercore/state/schema/README.md +121 -0
  120. flwr/supercore/state/schema/__init__.py +15 -0
  121. flwr/supercore/state/schema/corestate_tables.py +36 -0
  122. flwr/supercore/state/schema/linkstate_tables.py +152 -0
  123. flwr/supercore/state/schema/objectstore_tables.py +90 -0
  124. flwr/supercore/superexec/run_superexec.py +2 -2
  125. flwr/supercore/utils.py +36 -1
  126. flwr/superlink/federation/federation_manager.py +2 -2
  127. flwr/superlink/federation/noop_federation_manager.py +8 -6
  128. flwr/superlink/servicer/control/control_servicer.py +19 -17
  129. flwr/supernode/cli/flower_supernode.py +2 -1
  130. flwr/supernode/runtime/run_clientapp.py +14 -14
  131. flwr/supernode/servicer/clientappio/clientappio_servicer.py +10 -8
  132. flwr/supernode/start_client_internal.py +10 -6
  133. {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/METADATA +7 -5
  134. {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/RECORD +137 -116
  135. flwr/cli/federation/show.py +0 -318
  136. flwr/common/pyproject.py +0 -42
  137. flwr/supercore/sqlite_mixin.py +0 -159
  138. /flwr/{common → supercore}/version.py +0 -0
  139. {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/WHEEL +0 -0
  140. {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/entry_points.txt +0 -0
flwr/cli/utils.py CHANGED
@@ -18,20 +18,25 @@
18
18
  import hashlib
19
19
  import json
20
20
  import re
21
+ import sys
21
22
  from collections.abc import Callable, Iterable, Iterator
22
23
  from contextlib import contextmanager
24
+ from io import StringIO
23
25
  from pathlib import Path
24
26
  from typing import Any, cast
25
27
 
28
+ import click
26
29
  import grpc
27
30
  import pathspec
28
31
  import typer
32
+ from rich.console import Console
29
33
 
34
+ from flwr.cli.typing import SuperLinkConnection
30
35
  from flwr.common.constant import (
31
36
  ACCESS_TOKEN_KEY,
32
37
  AUTHN_TYPE_JSON_KEY,
33
- CREDENTIALS_DIR,
34
- FLWR_DIR,
38
+ FEDERATION_NOT_FOUND_MESSAGE,
39
+ FEDERATION_NOT_SPECIFIED_MESSAGE,
35
40
  NO_ACCOUNT_AUTH_MESSAGE,
36
41
  NO_ARTIFACT_PROVIDER_MESSAGE,
37
42
  NODE_NOT_FOUND_MESSAGE,
@@ -41,16 +46,67 @@ from flwr.common.constant import (
41
46
  REFRESH_TOKEN_KEY,
42
47
  RUN_ID_NOT_FOUND_MESSAGE,
43
48
  AuthnType,
49
+ CliOutputFormat,
44
50
  )
45
51
  from flwr.common.grpc import (
46
52
  GRPC_MAX_MESSAGE_LENGTH,
47
53
  create_channel,
48
54
  on_channel_state_change,
49
55
  )
56
+ from flwr.common.logger import print_json_error, redirect_output, restore_output
57
+ from flwr.supercore.credential_store import get_credential_store
50
58
 
51
59
  from .auth_plugin import CliAuthPlugin, get_cli_plugin_class
52
60
  from .cli_account_auth_interceptor import CliAccountAuthInterceptor
53
- from .config_utils import validate_certificate_in_federation_config
61
+ from .config_utils import load_certificate_in_connection
62
+ from .constant import AUTHN_TYPE_STORE_KEY
63
+
64
+
65
+ def print_json_to_stdout(data: str | Any) -> None:
66
+ """Print JSON data to stdout, bypassing any output redirection.
67
+
68
+ Use this function within the `cli_output_handler` context manager to print JSON
69
+ output directly to the terminal, even when stdout is being captured.
70
+ """
71
+ if isinstance(data, str):
72
+ Console(file=sys.__stdout__).print_json(data)
73
+ else:
74
+ Console(file=sys.__stdout__).print_json(data=data)
75
+
76
+
77
+ @contextmanager # docsig: ignore=SIG503
78
+ def cli_output_handler(
79
+ output_format: str = CliOutputFormat.DEFAULT,
80
+ ) -> Iterator[bool]:
81
+ """Context manager for handling CLI output in different formats.
82
+
83
+ This context manager provides consistent output handling for CLI commands by:
84
+ - Redirecting stdout/stderr when JSON format is requested
85
+ - Catching and handling exceptions appropriately based on the output format
86
+
87
+ Use the `print_json_to_stdout()` utility function to print JSON output that bypasses
88
+ output redirection.
89
+ """
90
+ is_json = output_format == CliOutputFormat.JSON
91
+ captured_output = StringIO()
92
+
93
+ if is_json:
94
+ redirect_output(captured_output)
95
+
96
+ try:
97
+ yield is_json
98
+ except Exception as err: # pylint: disable=broad-except
99
+ if is_json:
100
+ restore_output()
101
+ print_json_error(captured_output.getvalue(), err)
102
+ else:
103
+ if isinstance(err, typer.Exit):
104
+ raise # Allow typer.Exit to escape normally
105
+ raise click.ClickException(str(err)) from None
106
+ finally:
107
+ if is_json:
108
+ restore_output()
109
+ captured_output.close()
54
110
 
55
111
 
56
112
  def prompt_text(
@@ -155,46 +211,6 @@ def is_valid_project_name(name: str) -> bool:
155
211
  return True
156
212
 
157
213
 
158
- def sanitize_project_name(name: str) -> str:
159
- """Sanitize the given string to make it a valid Python project name.
160
-
161
- This function replaces spaces, dots, slashes, and underscores with dashes, removes
162
- any characters not allowed in Python project names, makes the string lowercase, and
163
- ensures it starts with a valid character.
164
-
165
- Parameters
166
- ----------
167
- name : str
168
- The project name to sanitize.
169
-
170
- Returns
171
- -------
172
- str
173
- The sanitized project name that is valid for Python projects.
174
- """
175
- # Replace whitespace with '_'
176
- name_with_hyphens = re.sub(r"[ ./_]", "-", name)
177
-
178
- # Allowed characters in a module name: letters, digits, underscore
179
- allowed_chars = set(
180
- "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"
181
- )
182
-
183
- # Make the string lowercase
184
- sanitized_name = name_with_hyphens.lower()
185
-
186
- # Remove any characters not allowed in Python module names
187
- sanitized_name = "".join(c for c in sanitized_name if c in allowed_chars)
188
-
189
- # Ensure the first character is a letter or underscore
190
- while sanitized_name and (
191
- sanitized_name[0].isdigit() or sanitized_name[0] not in allowed_chars
192
- ):
193
- sanitized_name = sanitized_name[1:]
194
-
195
- return sanitized_name
196
-
197
-
198
214
  def get_sha256_hash(file_path_or_int: Path | int) -> str:
199
215
  """Calculate the SHA-256 hash of a file or integer.
200
216
 
@@ -221,121 +237,27 @@ def get_sha256_hash(file_path_or_int: Path | int) -> str:
221
237
  return sha256.hexdigest()
222
238
 
223
239
 
224
- def get_account_auth_config_path(root_dir: Path, federation: str) -> Path:
225
- """Return the path to the account auth config file.
226
-
227
- Additionally, a `.gitignore` file will be created in the Flower directory to
228
- include the `.credentials` folder to be excluded from git. If the `.gitignore`
229
- file already exists, a warning will be displayed if the `.credentials` entry is
230
- not found.
231
- """
232
- # Locate the credentials directory
233
- abs_flwr_dir = root_dir.absolute() / FLWR_DIR
234
- credentials_dir = abs_flwr_dir / CREDENTIALS_DIR
235
- credentials_dir.mkdir(parents=True, exist_ok=True)
236
-
237
- # Determine the absolute path of the Flower directory for .gitignore
238
- gitignore_path = abs_flwr_dir / ".gitignore"
239
- credential_entry = CREDENTIALS_DIR
240
-
241
- try:
242
- if gitignore_path.exists():
243
- with open(gitignore_path, encoding="utf-8") as gitignore_file:
244
- lines = gitignore_file.read().splitlines()
245
-
246
- # Warn if .credentials is not already in .gitignore
247
- if credential_entry not in lines:
248
- typer.secho(
249
- f"`.gitignore` exists, but `{credential_entry}` entry not found. "
250
- "Consider adding it to your `.gitignore` to exclude Flower "
251
- "credentials from git.",
252
- fg=typer.colors.YELLOW,
253
- bold=True,
254
- )
255
- else:
256
- typer.secho(
257
- f"Creating a new `.gitignore` with `{credential_entry}` entry...",
258
- fg=typer.colors.BLUE,
259
- )
260
- # Create a new .gitignore with .credentials
261
- with open(gitignore_path, "w", encoding="utf-8") as gitignore_file:
262
- gitignore_file.write(f"{credential_entry}\n")
263
- except Exception as err:
264
- typer.secho(
265
- "❌ An error occurred while handling `.gitignore.` "
266
- f"Please check the permissions of `{gitignore_path}` and try again.",
267
- fg=typer.colors.RED,
268
- bold=True,
269
- err=True,
270
- )
271
- raise typer.Exit(code=1) from err
272
-
273
- return credentials_dir / f"{federation}.json"
274
-
275
-
276
- def account_auth_enabled(federation_config: dict[str, Any]) -> bool:
277
- """Check if account authentication is enabled in the federation config.
278
-
279
- Parameters
280
- ----------
281
- federation_config : dict[str, Any]
282
- The federation configuration dictionary.
283
-
284
- Returns
285
- -------
286
- bool
287
- True if account authentication is enabled, False otherwise.
288
- """
289
- enabled: bool = federation_config.get("enable-user-auth", False)
290
- enabled |= federation_config.get("enable-account-auth", False)
291
- if "enable-user-auth" in federation_config:
292
- typer.secho(
293
- "`enable-user-auth` is deprecated and will be removed in a future "
294
- "release. Please use `enable-account-auth` instead.",
295
- fg=typer.colors.YELLOW,
296
- bold=True,
297
- )
298
- return enabled
299
-
300
-
301
- def retrieve_authn_type(config_path: Path) -> str:
302
- """Retrieve the auth type from the config file or return NOOP if not found.
240
+ def get_authn_type(host: str) -> str:
241
+ """Retrieve the authentication type for the given host from the credential store.
303
242
 
304
- Parameters
305
- ----------
306
- config_path : Path
307
- Path to the authentication configuration file.
308
-
309
- Returns
310
- -------
311
- str
312
- The authentication type string, or AuthnType.NOOP if not found.
243
+ `AuthnType.NOOP` is returned if no authentication type is found.
313
244
  """
314
- try:
315
- with config_path.open("r", encoding="utf-8") as file:
316
- json_file = json.load(file)
317
- authn_type: str = json_file[AUTHN_TYPE_JSON_KEY]
318
- return authn_type
319
- except (FileNotFoundError, KeyError):
245
+ store = get_credential_store()
246
+ authn_type = store.get(AUTHN_TYPE_STORE_KEY % host)
247
+ if authn_type is None:
320
248
  return AuthnType.NOOP
249
+ return authn_type.decode("utf-8")
321
250
 
322
251
 
323
- def load_cli_auth_plugin(
324
- root_dir: Path,
325
- federation: str,
326
- federation_config: dict[str, Any],
327
- authn_type: str | None = None,
252
+ def load_cli_auth_plugin_from_connection(
253
+ host: str, authn_type: str | None = None
328
254
  ) -> CliAuthPlugin:
329
- """Load the CLI-side account auth plugin for the given authn type.
255
+ """Load the CLI-side account auth plugin for the given connection.
330
256
 
331
257
  Parameters
332
258
  ----------
333
- root_dir : Path
334
- Root directory of the Flower project.
335
- federation : str
336
- Name of the federation.
337
- federation_config : dict[str, Any]
338
- Federation configuration dictionary.
259
+ host : str
260
+ The SuperLink Control API address.
339
261
  authn_type : str | None
340
262
  Authentication type. If None, will be determined from config.
341
263
 
@@ -346,41 +268,48 @@ def load_cli_auth_plugin(
346
268
 
347
269
  Raises
348
270
  ------
349
- typer.Exit
271
+ click.ClickException
350
272
  If the authentication type is unknown.
351
273
  """
352
- # Find the path to the account auth config file
353
- config_path = get_account_auth_config_path(root_dir, federation)
354
-
355
274
  # Determine the auth type if not provided
356
275
  # Only `flwr login` command can provide `authn_type` explicitly, as it can query the
357
276
  # SuperLink for the auth type.
358
277
  if authn_type is None:
359
- authn_type = AuthnType.NOOP
360
- if account_auth_enabled(federation_config):
361
- authn_type = retrieve_authn_type(config_path)
278
+ authn_type = get_authn_type(host)
362
279
 
363
280
  # Retrieve auth plugin class and instantiate it
364
281
  try:
365
282
  auth_plugin_class = get_cli_plugin_class(authn_type)
366
- return auth_plugin_class(config_path)
283
+ return auth_plugin_class(host)
367
284
  except ValueError:
368
- typer.echo(f"❌ Unknown account authentication type: {authn_type}")
369
- raise typer.Exit(code=1) from None
285
+ raise click.ClickException(
286
+ f"Unknown account authentication type: {authn_type}"
287
+ ) from None
288
+
289
+
290
+ def require_superlink_address(connection: SuperLinkConnection) -> str:
291
+ """Return the SuperLink address or exit if it is not configured."""
292
+ if connection.address is None:
293
+ cmd = click.get_current_context().command.name
294
+ raise click.ClickException(
295
+ f"`flwr {cmd}` currently works with a SuperLink. Ensure that the "
296
+ "correct SuperLink (Control API) address is provided SuperLink connection "
297
+ "you are using. Check your Flower configuration file. You may use `flwr "
298
+ "config list` to see its location in the file system."
299
+ )
300
+ return connection.address
370
301
 
371
302
 
372
- def init_channel(
373
- app: Path, federation_config: dict[str, Any], auth_plugin: CliAuthPlugin
303
+ def init_channel_from_connection(
304
+ connection: SuperLinkConnection, auth_plugin: CliAuthPlugin | None = None
374
305
  ) -> grpc.Channel:
375
306
  """Initialize gRPC channel to the Control API.
376
307
 
377
308
  Parameters
378
309
  ----------
379
- app : Path
380
- Path to the Flower app directory.
381
- federation_config : dict[str, Any]
382
- Federation configuration dictionary containing address and TLS settings.
383
- auth_plugin : CliAuthPlugin
310
+ connection : SuperLinkConnection
311
+ SuperLink connection configuration.
312
+ auth_plugin : CliAuthPlugin | None (default: None)
384
313
  Authentication plugin instance for handling credentials.
385
314
 
386
315
  Returns
@@ -388,17 +317,20 @@ def init_channel(
388
317
  grpc.Channel
389
318
  Configured gRPC channel with authentication interceptors.
390
319
  """
391
- insecure, root_certificates_bytes = validate_certificate_in_federation_config(
392
- app, federation_config
393
- )
320
+ address = require_superlink_address(connection)
394
321
 
322
+ root_certificates_bytes = load_certificate_in_connection(connection)
323
+
324
+ # Load authentication plugin
325
+ if auth_plugin is None:
326
+ auth_plugin = load_cli_auth_plugin_from_connection(address)
395
327
  # Load tokens
396
328
  auth_plugin.load_tokens()
397
329
 
398
330
  # Create the gRPC channel
399
331
  channel = create_channel(
400
- server_address=federation_config["address"],
401
- insecure=insecure,
332
+ server_address=address,
333
+ insecure=connection.insecure,
402
334
  root_certificates=root_certificates_bytes,
403
335
  max_message_length=GRPC_MAX_MESSAGE_LENGTH,
404
336
  interceptors=[CliAccountAuthInterceptor(auth_plugin)],
@@ -423,7 +355,7 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]: # pylint: disable=too-many-b
423
355
 
424
356
  Raises
425
357
  ------
426
- typer.Exit
358
+ click.ClickException
427
359
  On handled gRPC error statuses with appropriate exit code.
428
360
  grpc.RpcError
429
361
  For unhandled gRPC error statuses.
@@ -432,115 +364,74 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]: # pylint: disable=too-many-b
432
364
  yield
433
365
  except grpc.RpcError as e:
434
366
  if e.code() == grpc.StatusCode.UNAUTHENTICATED:
435
- typer.secho(
436
- "Authentication failed. Please run `flwr login`"
437
- " to authenticate and try again.",
438
- fg=typer.colors.RED,
439
- bold=True,
440
- err=True,
441
- )
442
- raise typer.Exit(code=1) from None
367
+ raise click.ClickException(
368
+ "Authentication failed. Please run `flwr login`"
369
+ " to authenticate and try again."
370
+ ) from None
443
371
  if e.code() == grpc.StatusCode.UNIMPLEMENTED:
444
372
  if e.details() == NO_ACCOUNT_AUTH_MESSAGE: # pylint: disable=E1101
445
- typer.secho(
446
- "Account authentication is not enabled on this SuperLink.",
447
- fg=typer.colors.RED,
448
- bold=True,
449
- err=True,
450
- )
451
- elif e.details() == NO_ARTIFACT_PROVIDER_MESSAGE: # pylint: disable=E1101
452
- typer.secho(
453
- "The SuperLink does not support `flwr pull` command.",
454
- fg=typer.colors.RED,
455
- bold=True,
456
- err=True,
457
- )
458
- else:
459
- typer.secho(
460
- "❌ The SuperLink cannot process this request. Please verify that "
461
- "you set the address to its Control API endpoint correctly in your "
462
- "`pyproject.toml`, and ensure that the Flower versions used by "
463
- "the CLI and SuperLink are compatible.",
464
- fg=typer.colors.RED,
465
- bold=True,
466
- err=True,
467
- )
468
- raise typer.Exit(code=1) from None
373
+ raise click.ClickException(
374
+ "Account authentication is not enabled on this SuperLink."
375
+ ) from None
376
+ if e.details() == NO_ARTIFACT_PROVIDER_MESSAGE: # pylint: disable=E1101
377
+ raise click.ClickException(
378
+ "The SuperLink does not support `flwr pull` command."
379
+ ) from None
380
+ raise click.ClickException(
381
+ "The SuperLink cannot process this request. Please verify that "
382
+ "you set the address to its Control API endpoint correctly in your "
383
+ "SuperLink connection in your Flower Configuration file. You may use "
384
+ "`flwr config list` to see its location in the file system. "
385
+ "Additonally, ensure that the Flower versions used by the CLI and "
386
+ "SuperLink are compatible."
387
+ ) from None
469
388
  if e.code() == grpc.StatusCode.PERMISSION_DENIED:
470
- typer.secho(
471
- "❌ Permission denied.",
472
- fg=typer.colors.RED,
473
- bold=True,
474
- err=True,
475
- )
476
389
  # pylint: disable-next=E1101
477
- typer.secho(e.details(), fg=typer.colors.RED, bold=True)
478
- raise typer.Exit(code=1) from None
390
+ raise click.ClickException(f"Permission denied.\n{e.details()}") from None
479
391
  if e.code() == grpc.StatusCode.UNAVAILABLE:
480
- typer.secho(
392
+ raise click.ClickException(
481
393
  "Connection to the SuperLink is unavailable. Please check your network "
482
- "connection and 'address' in the federation configuration.",
483
- fg=typer.colors.RED,
484
- bold=True,
485
- err=True,
486
- )
487
- raise typer.Exit(code=1) from None
394
+ "connection and 'address' in the SuperLink connection configuration."
395
+ ) from None
488
396
  if e.code() == grpc.StatusCode.NOT_FOUND:
489
397
  if e.details() == RUN_ID_NOT_FOUND_MESSAGE: # pylint: disable=E1101
490
- typer.secho(
491
- "❌ Run ID not found.",
492
- fg=typer.colors.RED,
493
- bold=True,
494
- err=True,
495
- )
496
- raise typer.Exit(code=1) from None
398
+ raise click.ClickException("Run ID not found.") from None
497
399
  if e.details() == NODE_NOT_FOUND_MESSAGE: # pylint: disable=E1101
498
- typer.secho(
499
- "Node ID not found for this account.",
500
- fg=typer.colors.RED,
501
- bold=True,
502
- err=True,
503
- )
504
- raise typer.Exit(code=1) from None
400
+ raise click.ClickException(
401
+ "Node ID not found for this account."
402
+ ) from None
505
403
  if e.code() == grpc.StatusCode.FAILED_PRECONDITION:
506
404
  if e.details() == PULL_UNFINISHED_RUN_MESSAGE: # pylint: disable=E1101
507
- typer.secho(
508
- "Run is not finished yet. Artifacts can only be pulled after "
509
- "the run is finished. You can check the run status with `flwr ls`.",
510
- fg=typer.colors.RED,
511
- bold=True,
512
- err=True,
513
- )
514
- raise typer.Exit(code=1) from None
405
+ raise click.ClickException(
406
+ "Run is not finished yet. Artifacts can only be pulled after "
407
+ "the run is finished. You can check the run status with `flwr ls`."
408
+ ) from None
515
409
  if (
516
410
  e.details() == PUBLIC_KEY_ALREADY_IN_USE_MESSAGE
517
411
  ): # pylint: disable=E1101
518
- typer.secho(
519
- "The provided public key is already in use by another "
520
- "SuperNode.",
521
- fg=typer.colors.RED,
522
- bold=True,
523
- err=True,
524
- )
525
- raise typer.Exit(code=1) from None
412
+ raise click.ClickException(
413
+ "The provided public key is already in use by another SuperNode."
414
+ ) from None
526
415
  if e.details() == PUBLIC_KEY_NOT_VALID: # pylint: disable=E1101
527
- typer.secho(
528
- "The provided public key is invalid. Please provide a valid "
529
- "NIST EC public key.",
530
- fg=typer.colors.RED,
531
- bold=True,
532
- err=True,
533
- )
534
- raise typer.Exit(code=1) from None
416
+ raise click.ClickException(
417
+ "The provided public key is invalid. Please provide a valid "
418
+ "NIST EC public key."
419
+ ) from None
420
+ if e.details() == FEDERATION_NOT_SPECIFIED_MESSAGE: # pylint: disable=E1101
421
+ raise click.ClickException(
422
+ "No federation specified. "
423
+ "Please use the `--federation` flag or set a default federation "
424
+ "in your SuperLink connection configuration."
425
+ ) from None
426
+ patten = re.compile(FEDERATION_NOT_FOUND_MESSAGE.replace("%s", "(.+)"))
427
+ if m := patten.match(e.details()): # pylint: disable=E1101
428
+ raise click.ClickException(
429
+ f"Federation '{m.group(1)}' does not exist. "
430
+ "Please verify the federation name and try again."
431
+ ) from None
535
432
 
536
433
  # Log details from grpc error directly
537
- typer.secho(
538
- f"❌ {e.details()}",
539
- fg=typer.colors.RED,
540
- bold=True,
541
- err=True,
542
- )
543
- raise typer.Exit(code=1) from None
434
+ raise click.ClickException(f"{e.details()}") from None
544
435
  raise
545
436
 
546
437
 
@@ -600,23 +491,17 @@ def validate_credentials_content(creds_path: Path) -> str:
600
491
  try:
601
492
  creds: dict[str, str] = json.loads(creds_path.read_text(encoding="utf-8"))
602
493
  except (OSError, json.JSONDecodeError) as err:
603
- typer.secho(
604
- f"Invalid credentials file at '{creds_path}': {err}",
605
- fg=typer.colors.RED,
606
- err=True,
607
- )
608
- raise typer.Exit(code=1) from err
494
+ raise click.ClickException(
495
+ f"Invalid credentials file at '{creds_path}': {err}"
496
+ ) from err
609
497
 
610
498
  required_keys = [AUTHN_TYPE_JSON_KEY, ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY]
611
499
  missing = [key for key in required_keys if key not in creds]
612
500
 
613
501
  if missing:
614
- typer.secho(
502
+ raise click.ClickException(
615
503
  f"Credentials file '{creds_path}' is missing "
616
- f"required key(s): {', '.join(missing)}. Please log in again.",
617
- fg=typer.colors.RED,
618
- err=True,
504
+ f"required key(s): {', '.join(missing)}. Please log in again."
619
505
  )
620
- raise typer.Exit(code=1)
621
506
 
622
507
  return creds[ACCESS_TOKEN_KEY]
@@ -33,7 +33,7 @@ from flwr.common.inflatable_protobuf_utils import (
33
33
  )
34
34
  from flwr.common.logger import log
35
35
  from flwr.common.message import Message, remove_content_from_message
36
- from flwr.common.retry_invoker import RetryInvoker, _wrap_stub
36
+ from flwr.common.retry_invoker import RetryInvoker, wrap_stub
37
37
  from flwr.common.serde import (
38
38
  fab_from_proto,
39
39
  message_from_proto,
@@ -136,7 +136,7 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
136
136
  confirm_message_received : Callable[[str], None]
137
137
  """
138
138
  if isinstance(root_certificates, str):
139
- root_certificates = Path(root_certificates).read_bytes()
139
+ root_certificates = Path(root_certificates).expanduser().read_bytes()
140
140
 
141
141
  # Automatic node auth: generate keys if user didn't provide any
142
142
  self_registered = False
@@ -165,7 +165,7 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
165
165
  node: Node | None = None
166
166
 
167
167
  # Wrap stub
168
- _wrap_stub(stub, retry_invoker)
168
+ wrap_stub(stub, retry_invoker)
169
169
  ###########################################################################
170
170
  # SuperNode functions
171
171
  ###########################################################################
@@ -32,7 +32,6 @@ from flwr.common.constant import (
32
32
  GRPC_ADAPTER_METADATA_MESSAGE_QUALNAME_KEY,
33
33
  GRPC_ADAPTER_METADATA_SHOULD_EXIT_KEY,
34
34
  )
35
- from flwr.common.version import package_name, package_version
36
35
  from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
37
36
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
38
37
  ActivateNodeRequest,
@@ -64,6 +63,7 @@ from flwr.proto.message_pb2 import ( # pylint: disable=E0611
64
63
  )
65
64
  from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611
66
65
  from flwr.supercore.constant import FORCE_EXIT_TIMEOUT_SECONDS
66
+ from flwr.supercore.version import package_name, package_version
67
67
 
68
68
  T = TypeVar("T", bound=GrpcMessage)
69
69
 
@@ -18,6 +18,7 @@
18
18
  from logging import WARN
19
19
  from typing import cast
20
20
 
21
+ from flwr.app.message_type import MessageType
21
22
  from flwr.client.client import (
22
23
  maybe_call_evaluate,
23
24
  maybe_call_fit,
@@ -27,7 +28,7 @@ from flwr.client.client import (
27
28
  from flwr.client.numpy_client import NumPyClient
28
29
  from flwr.client.typing import ClientFnExt
29
30
  from flwr.common import ConfigRecord, Context, Message, Metadata, RecordDict, log
30
- from flwr.common.constant import MessageType, MessageTypeLegacy
31
+ from flwr.common.constant import MessageTypeLegacy
31
32
  from flwr.common.recorddict_compat import (
32
33
  evaluateres_to_recorddict,
33
34
  fitres_to_recorddict,
@@ -17,10 +17,10 @@
17
17
 
18
18
  from logging import INFO
19
19
 
20
+ from flwr.app.message_type import MessageType
20
21
  from flwr.client.typing import ClientAppCallable
21
22
  from flwr.common import ndarrays_to_parameters, parameters_to_ndarrays
22
23
  from flwr.common import recorddict_compat as compat
23
- from flwr.common.constant import MessageType
24
24
  from flwr.common.context import Context
25
25
  from flwr.common.differential_privacy import (
26
26
  compute_adaptive_clip_model_update,
@@ -19,10 +19,10 @@ from logging import INFO
19
19
 
20
20
  import numpy as np
21
21
 
22
+ from flwr.app.message_type import MessageType
22
23
  from flwr.client.typing import ClientAppCallable
23
24
  from flwr.common import ndarrays_to_parameters, parameters_to_ndarrays
24
25
  from flwr.common import recorddict_compat as compat
25
- from flwr.common.constant import MessageType
26
26
  from flwr.common.context import Context
27
27
  from flwr.common.differential_privacy import (
28
28
  add_localdp_gaussian_noise_to_params,
@@ -20,6 +20,7 @@ from dataclasses import dataclass, field
20
20
  from logging import DEBUG, WARNING
21
21
  from typing import Any, cast
22
22
 
23
+ from flwr.app.message_type import MessageType
23
24
  from flwr.client.typing import ClientAppCallable
24
25
  from flwr.common import (
25
26
  ConfigRecord,
@@ -31,7 +32,6 @@ from flwr.common import (
31
32
  parameters_to_ndarrays,
32
33
  )
33
34
  from flwr.common import recorddict_compat as compat
34
- from flwr.common.constant import MessageType
35
35
  from flwr.common.logger import log
36
36
  from flwr.common.secure_aggregation.crypto.shamir import create_shares
37
37
  from flwr.common.secure_aggregation.crypto.symmetric_encryption import (