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/login/login.py CHANGED
@@ -15,20 +15,14 @@
15
15
  """Flower command line interface `login` command."""
16
16
 
17
17
 
18
- from pathlib import Path
19
- from typing import Annotated
18
+ from typing import Annotated, cast
20
19
 
20
+ import click
21
21
  import typer
22
22
 
23
23
  from flwr.cli.auth_plugin import LoginError, NoOpCliAuthPlugin
24
- from flwr.cli.config_utils import (
25
- exit_if_no_address,
26
- get_insecure_flag,
27
- load_and_validate,
28
- process_loaded_project_config,
29
- validate_federation_in_project_config,
30
- )
31
24
  from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
25
+ from flwr.cli.utils import init_channel_from_connection
32
26
  from flwr.common.typing import AccountAuthLoginDetails
33
27
  from flwr.proto.control_pb2 import ( # pylint: disable=E0611
34
28
  GetLoginDetailsRequest,
@@ -36,68 +30,45 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
36
30
  )
37
31
  from flwr.proto.control_pb2_grpc import ControlStub
38
32
 
39
- from ..utils import (
40
- account_auth_enabled,
41
- flwr_cli_grpc_exc_handler,
42
- init_channel,
43
- load_cli_auth_plugin,
44
- )
33
+ from ..config_migration import migrate, warn_if_federation_config_overrides
34
+ from ..flower_config import read_superlink_connection
35
+ from ..utils import flwr_cli_grpc_exc_handler, load_cli_auth_plugin_from_connection
45
36
 
46
37
 
47
- def login( # pylint: disable=R0914
48
- app: Annotated[
49
- Path,
50
- typer.Argument(help="Path of the Flower App to run."),
51
- ] = Path("."),
52
- federation: Annotated[
38
+ def login(
39
+ ctx: typer.Context,
40
+ superlink: Annotated[
53
41
  str | None,
54
- typer.Argument(help="Name of the federation to login into."),
42
+ typer.Argument(help="Name of the SuperLink connection."),
55
43
  ] = None,
56
44
  federation_config_overrides: Annotated[
57
45
  list[str] | None,
58
46
  typer.Option(
59
47
  "--federation-config",
60
48
  help=FEDERATION_CONFIG_HELP_MESSAGE,
49
+ hidden=True,
61
50
  ),
62
51
  ] = None,
63
52
  ) -> None:
64
53
  """Login to Flower SuperLink."""
65
- typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
54
+ # Warn `--federation-config` is ignored
55
+ warn_if_federation_config_overrides(federation_config_overrides)
66
56
 
67
- pyproject_path = app / "pyproject.toml" if app else None
68
- config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
57
+ # Migrate legacy usage if any
58
+ migrate(superlink, args=ctx.args)
69
59
 
70
- config = process_loaded_project_config(config, errors, warnings)
71
- federation, federation_config = validate_federation_in_project_config(
72
- federation, config, federation_config_overrides
73
- )
74
- exit_if_no_address(federation_config, "login")
60
+ # Read superlink connection configuration
61
+ superlink_connection = read_superlink_connection(superlink)
62
+ superlink = superlink_connection.name
75
63
 
76
- # Check if `enable-account-auth` is set to `true`
77
-
78
- if not account_auth_enabled(federation_config):
79
- typer.secho(
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.",
83
- fg=typer.colors.RED,
84
- bold=True,
85
- err=True,
86
- )
87
- raise typer.Exit(code=1)
88
64
  # Check if insecure flag is set to `True`
89
- insecure = get_insecure_flag(federation_config)
90
- if insecure:
91
- typer.secho(
92
- "`flwr login` requires TLS to be enabled. `insecure` must NOT be set to "
93
- "`true` in the federation configuration.",
94
- fg=typer.colors.RED,
95
- bold=True,
96
- err=True,
65
+ if superlink_connection.insecure:
66
+ raise click.ClickException(
67
+ "`flwr login` requires TLS to be enabled. `insecure` must NOT be set to "
68
+ "`true` in the federation configuration."
97
69
  )
98
- raise typer.Exit(code=1)
99
70
 
100
- channel = init_channel(app, federation_config, NoOpCliAuthPlugin(Path()))
71
+ channel = init_channel_from_connection(superlink_connection, NoOpCliAuthPlugin())
101
72
  stub = ControlStub(channel)
102
73
 
103
74
  login_request = GetLoginDetailsRequest()
@@ -105,8 +76,9 @@ def login( # pylint: disable=R0914
105
76
  login_response: GetLoginDetailsResponse = stub.GetLoginDetails(login_request)
106
77
 
107
78
  # Get the auth plugin
108
- authn_type = login_response.authn_type
109
- auth_plugin = load_cli_auth_plugin(app, federation, federation_config, authn_type)
79
+ authn_plugin = load_cli_auth_plugin_from_connection(
80
+ cast(str, superlink_connection.address), login_response.authn_type
81
+ )
110
82
 
111
83
  # Login
112
84
  details = AccountAuthLoginDetails(
@@ -118,20 +90,14 @@ def login( # pylint: disable=R0914
118
90
  )
119
91
  try:
120
92
  with flwr_cli_grpc_exc_handler():
121
- credentials = auth_plugin.login(details, stub)
93
+ credentials = authn_plugin.login(details, stub)
122
94
  typer.secho(
123
95
  "✅ Login successful.",
124
96
  fg=typer.colors.GREEN,
125
97
  bold=False,
126
98
  )
127
99
  except LoginError as e:
128
- typer.secho(
129
- f"❌ Login failed: {e.message}",
130
- fg=typer.colors.RED,
131
- bold=True,
132
- err=True,
133
- )
134
- raise typer.Exit(code=1) from None
100
+ raise click.ClickException(f"Login failed: {e.message}") from None
135
101
 
136
102
  # Store the tokens
137
- auth_plugin.store_tokens(credentials)
103
+ authn_plugin.store_tokens(credentials)
flwr/cli/ls.py CHANGED
@@ -15,25 +15,18 @@
15
15
  """Flower command line interface `ls` command."""
16
16
 
17
17
 
18
- import io
19
18
  import json
20
- from pathlib import Path
21
- from typing import Annotated, cast
19
+ from typing import Annotated
22
20
 
23
21
  import typer
24
22
  from rich.console import Console
25
23
  from rich.table import Table
26
24
  from rich.text import Text
27
25
 
28
- from flwr.cli.config_utils import (
29
- exit_if_no_address,
30
- load_and_validate,
31
- process_loaded_project_config,
32
- validate_federation_in_project_config,
33
- )
26
+ from flwr.cli.config_migration import migrate, warn_if_federation_config_overrides
34
27
  from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
35
- from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat, Status, SubStatus
36
- from flwr.common.logger import print_json_error, redirect_output, restore_output
28
+ from flwr.cli.flower_config import read_superlink_connection
29
+ from flwr.common.constant import CliOutputFormat, Status, SubStatus
37
30
  from flwr.common.serde import run_from_proto
38
31
  from flwr.proto.control_pb2 import ( # pylint: disable=E0611
39
32
  ListRunsRequest,
@@ -43,24 +36,26 @@ from flwr.proto.control_pb2_grpc import ControlStub
43
36
  from flwr.supercore.utils import humanize_bytes, humanize_duration
44
37
 
45
38
  from .run_utils import RunRow, format_runs
46
- from .utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
39
+ from .utils import (
40
+ cli_output_handler,
41
+ flwr_cli_grpc_exc_handler,
42
+ init_channel_from_connection,
43
+ print_json_to_stdout,
44
+ )
47
45
 
48
46
 
49
47
  def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
50
48
  ctx: typer.Context,
51
- app: Annotated[
52
- Path,
53
- typer.Argument(help="Path of the Flower project"),
54
- ] = Path("."),
55
- federation: Annotated[
49
+ superlink: Annotated[
56
50
  str | None,
57
- typer.Argument(help="Name of the federation"),
51
+ typer.Argument(help="Name of the SuperLink connection."),
58
52
  ] = None,
59
53
  federation_config_overrides: Annotated[
60
54
  list[str] | None,
61
55
  typer.Option(
62
56
  "--federation-config",
63
57
  help=FEDERATION_CONFIG_HELP_MESSAGE,
58
+ hidden=True,
64
59
  ),
65
60
  ] = None,
66
61
  runs: Annotated[
@@ -86,7 +81,7 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
86
81
  ),
87
82
  ] = CliOutputFormat.DEFAULT,
88
83
  ) -> None:
89
- """List the details of one provided run ID or all runs in a Flower federation.
84
+ """List the details of one provided run ID or all runs (alias: ls).
90
85
 
91
86
  The following details are displayed:
92
87
 
@@ -99,32 +94,23 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
99
94
 
100
95
  All timestamps follow ISO 8601, UTC and are formatted as ``YYYY-MM-DD HH:MM:SSZ``.
101
96
  """
102
- # Resolve command used (list or ls)
103
- command_name = cast(str, ctx.command.name) if ctx.command else "list"
104
-
105
- suppress_output = output_format == CliOutputFormat.JSON
106
- captured_output = io.StringIO()
107
- try:
108
- if suppress_output:
109
- redirect_output(captured_output)
110
- # Load and validate federation config
111
- typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
112
-
113
- pyproject_path = app / FAB_CONFIG_FILE if app else None
114
- config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
115
- config = process_loaded_project_config(config, errors, warnings)
116
- federation, federation_config = validate_federation_in_project_config(
117
- federation, config, federation_config_overrides
118
- )
119
- exit_if_no_address(federation_config, command_name)
97
+ with cli_output_handler(output_format=output_format) as is_json:
98
+ # Warn `--federation-config` is ignored
99
+ warn_if_federation_config_overrides(federation_config_overrides)
100
+
101
+ # Migrate legacy usage if any
102
+ migrate(superlink, args=ctx.args)
103
+
104
+ # Read superlink connection configuration
105
+ superlink_connection = read_superlink_connection(superlink)
120
106
  channel = None
107
+
121
108
  try:
122
109
  if runs and run_id is not None:
123
110
  raise ValueError(
124
111
  "The options '--runs' and '--run-id' are mutually exclusive."
125
112
  )
126
- auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
127
- channel = init_channel(app, federation_config, auth_plugin)
113
+ channel = init_channel_from_connection(superlink_connection)
128
114
  stub = ControlStub(channel)
129
115
 
130
116
  # Display information about a specific run ID
@@ -135,9 +121,9 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
135
121
  else:
136
122
  typer.echo("📄 Listing all runs...")
137
123
  formatted_runs = _list_runs(stub)
138
- restore_output()
139
- if output_format == CliOutputFormat.JSON:
140
- Console().print_json(_to_json(formatted_runs))
124
+
125
+ if is_json:
126
+ print_json_to_stdout(_to_json(formatted_runs))
141
127
  else:
142
128
  if run_id is not None:
143
129
  Console().print(_to_detail_table(formatted_runs[0]))
@@ -146,22 +132,6 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
146
132
  finally:
147
133
  if channel:
148
134
  channel.close()
149
- except (typer.Exit, Exception) as err: # pylint: disable=broad-except
150
- if suppress_output:
151
- restore_output()
152
- e_message = captured_output.getvalue()
153
- print_json_error(e_message, err)
154
- else:
155
- typer.secho(
156
- f"{err}",
157
- fg=typer.colors.RED,
158
- bold=True,
159
- err=True,
160
- )
161
- finally:
162
- if suppress_output:
163
- restore_output()
164
- captured_output.close()
165
135
 
166
136
 
167
137
  def _get_status_style(status_text: str) -> str:
flwr/cli/new/new.py CHANGED
@@ -20,6 +20,7 @@ import zipfile
20
20
  from pathlib import Path
21
21
  from typing import Annotated, cast
22
22
 
23
+ import click
23
24
  import requests
24
25
  import typer
25
26
 
@@ -52,16 +53,12 @@ def new(
52
53
  ) -> None:
53
54
  """Create new Flower App."""
54
55
  if framework is not None or username is not None:
55
- typer.secho(
56
- "The --framework and --username options are deprecated and will be "
56
+ raise click.ClickException(
57
+ "The --framework and --username options are deprecated and will be "
57
58
  "removed in future versions of Flower. Please provide an app specifier "
58
59
  "after `flwr new` instead, e.g., '@account_name/app_name' or "
59
- "'@account_name/app_name==x.y.z'.",
60
- fg=typer.colors.RED,
61
- bold=True,
62
- err=True,
60
+ "'@account_name/app_name==x.y.z'."
63
61
  )
64
- raise typer.Exit(code=1)
65
62
 
66
63
  if app_spec is None:
67
64
  # Fetch recommended apps
@@ -141,12 +138,7 @@ def fetch_recommended_apps() -> list[dict[str, str]]:
141
138
  return cast(list[dict[str, str]], apps)
142
139
 
143
140
  except requests.RequestException as e:
144
- typer.secho(
145
- f"❌ Failed to fetch recommended apps: {e}",
146
- fg=typer.colors.RED,
147
- err=True,
148
- )
149
- raise typer.Exit(code=1) from e
141
+ raise click.ClickException(f"Failed to fetch recommended apps: {e}") from e
150
142
 
151
143
 
152
144
  # Security: prevent zip-slip
@@ -190,22 +182,12 @@ def _download_zip_to_memory(presigned_url: str) -> io.BytesIO:
190
182
  r = requests.get(presigned_url, timeout=60)
191
183
  r.raise_for_status()
192
184
  except requests.RequestException as e:
193
- typer.secho(
194
- f"ZIP download failed: {e}",
195
- fg=typer.colors.RED,
196
- err=True,
197
- )
198
- raise typer.Exit(code=1) from e
185
+ raise click.ClickException(f"ZIP download failed: {e}") from e
199
186
 
200
187
  buf = io.BytesIO(r.content)
201
188
  # Validate it's a zip
202
189
  if not zipfile.is_zipfile(buf):
203
- typer.secho(
204
- "Downloaded file is not a valid ZIP",
205
- fg=typer.colors.RED,
206
- err=True,
207
- )
208
- raise typer.Exit(code=1)
190
+ raise click.ClickException("Downloaded file is not a valid ZIP")
209
191
  buf.seek(0)
210
192
  return buf
211
193
 
@@ -216,8 +198,7 @@ def download_remote_app_via_api(app_spec: str) -> None:
216
198
  try:
217
199
  app_id, app_version = parse_app_spec(app_spec)
218
200
  except ValueError as e:
219
- typer.secho(f"❌ {e}", fg=typer.colors.RED, err=True)
220
- raise typer.Exit(code=1) from e
201
+ raise click.ClickException(str(e)) from e
221
202
 
222
203
  app_name = app_id.split("/")[1]
223
204
 
@@ -242,8 +223,7 @@ def download_remote_app_via_api(app_spec: str) -> None:
242
223
  try:
243
224
  presigned_url, _ = request_download_link(app_id, app_version, url, "zip_url")
244
225
  except ValueError as e:
245
- typer.secho(f"❌ {e}", fg=typer.colors.RED, err=True)
246
- raise typer.Exit(code=1) from e
226
+ raise click.ClickException(str(e)) from e
247
227
 
248
228
  typer.secho(
249
229
  "🔽 Downloading ZIP into memory...",
flwr/cli/pull.py CHANGED
@@ -15,49 +15,39 @@
15
15
  """Flower command line interface `pull` command."""
16
16
 
17
17
 
18
- from pathlib import Path
19
18
  from typing import Annotated
20
19
 
20
+ import click
21
21
  import typer
22
22
 
23
- from flwr.cli.config_utils import (
24
- exit_if_no_address,
25
- load_and_validate,
26
- process_loaded_project_config,
27
- validate_federation_in_project_config,
28
- )
23
+ from flwr.cli.config_migration import migrate, warn_if_federation_config_overrides
29
24
  from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
30
- from flwr.common.constant import FAB_CONFIG_FILE
25
+ from flwr.cli.flower_config import read_superlink_connection
31
26
  from flwr.proto.control_pb2 import ( # pylint: disable=E0611
32
27
  PullArtifactsRequest,
33
28
  PullArtifactsResponse,
34
29
  )
35
30
  from flwr.proto.control_pb2_grpc import ControlStub
36
31
 
37
- from .utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
32
+ from .utils import flwr_cli_grpc_exc_handler, init_channel_from_connection
38
33
 
39
34
 
40
35
  def pull( # pylint: disable=R0914
36
+ ctx: typer.Context,
41
37
  run_id: Annotated[
42
38
  int,
43
- typer.Option(
44
- "--run-id",
45
- help="Run ID to pull artifacts from.",
46
- ),
39
+ typer.Argument(help="Run ID to pull artifacts from."),
47
40
  ],
48
- app: Annotated[
49
- Path,
50
- typer.Argument(help="Path of the Flower App to run."),
51
- ] = Path("."),
52
- federation: Annotated[
41
+ superlink: Annotated[
53
42
  str | None,
54
- typer.Argument(help="Name of the federation."),
43
+ typer.Argument(help="Name of the SuperLink connection."),
55
44
  ] = None,
56
45
  federation_config_overrides: Annotated[
57
46
  list[str] | None,
58
47
  typer.Option(
59
48
  "--federation-config",
60
49
  help=FEDERATION_CONFIG_HELP_MESSAGE,
50
+ hidden=True,
61
51
  ),
62
52
  ] = None,
63
53
  ) -> None:
@@ -66,20 +56,17 @@ def pull( # pylint: disable=R0914
66
56
  Retrieve a download URL for artifacts generated during a completed Flower run. The
67
57
  artifacts can then be downloaded from the provided URL.
68
58
  """
69
- typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
59
+ # Warn `--federation-config` is ignored
60
+ warn_if_federation_config_overrides(federation_config_overrides)
70
61
 
71
- pyproject_path = app / FAB_CONFIG_FILE if app else None
72
- config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
73
- config = process_loaded_project_config(config, errors, warnings)
74
- federation, federation_config = validate_federation_in_project_config(
75
- federation, config, federation_config_overrides
76
- )
77
- exit_if_no_address(federation_config, "pull")
62
+ # Migrate legacy usage if any
63
+ migrate(superlink, args=ctx.args)
64
+
65
+ # Read superlink connection configuration
66
+ superlink_connection = read_superlink_connection(superlink)
78
67
  channel = None
79
68
  try:
80
-
81
- auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
82
- channel = init_channel(app, federation_config, auth_plugin)
69
+ channel = init_channel_from_connection(superlink_connection)
83
70
  stub = ControlStub(channel)
84
71
  with flwr_cli_grpc_exc_handler():
85
72
  res: PullArtifactsResponse = stub.PullArtifacts(
@@ -87,14 +74,9 @@ def pull( # pylint: disable=R0914
87
74
  )
88
75
 
89
76
  if not res.url:
90
- typer.secho(
91
- f"A download URL for artifacts from run {run_id} couldn't be "
92
- "obtained.",
93
- fg=typer.colors.RED,
94
- bold=True,
95
- err=True,
77
+ raise click.ClickException(
78
+ f"A download URL for artifacts from run {run_id} couldn't be obtained."
96
79
  )
97
- raise typer.Exit(code=1)
98
80
 
99
81
  typer.secho(
100
82
  f"✅ Artifacts for run {run_id} can be downloaded from: {res.url}",