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
@@ -15,26 +15,26 @@
15
15
  """Flower CLI account auth plugin for OIDC."""
16
16
 
17
17
 
18
- import json
19
18
  import time
19
+ import webbrowser
20
20
  from collections.abc import Sequence
21
- from pathlib import Path
22
- from typing import Any
23
21
 
22
+ import click
24
23
  import typer
25
24
 
26
- from flwr.common.constant import (
27
- ACCESS_TOKEN_KEY,
28
- AUTHN_TYPE_JSON_KEY,
29
- REFRESH_TOKEN_KEY,
30
- AuthnType,
25
+ from flwr.cli.constant import (
26
+ ACCESS_TOKEN_STORE_KEY,
27
+ AUTHN_TYPE_STORE_KEY,
28
+ REFRESH_TOKEN_STORE_KEY,
31
29
  )
30
+ from flwr.common.constant import ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, AuthnType
32
31
  from flwr.common.typing import AccountAuthCredentials, AccountAuthLoginDetails
33
32
  from flwr.proto.control_pb2 import ( # pylint: disable=E0611
34
33
  GetAuthTokensRequest,
35
34
  GetAuthTokensResponse,
36
35
  )
37
36
  from flwr.proto.control_pb2_grpc import ControlStub
37
+ from flwr.supercore.credential_store import get_credential_store
38
38
 
39
39
  from .auth_plugin import CliAuthPlugin, LoginError
40
40
 
@@ -46,10 +46,11 @@ class OidcCliPlugin(CliAuthPlugin):
46
46
  access to Flower SuperLink.
47
47
  """
48
48
 
49
- def __init__(self, credentials_path: Path):
49
+ def __init__(self, host: str):
50
50
  self.access_token: str | None = None
51
51
  self.refresh_token: str | None = None
52
- self.credentials_path = credentials_path
52
+ self.host = host
53
+ self.store = get_credential_store()
53
54
 
54
55
  @staticmethod
55
56
  def login(
@@ -75,11 +76,17 @@ class OidcCliPlugin(CliAuthPlugin):
75
76
  LoginError
76
77
  If authentication times out.
77
78
  """
79
+ # Prompt user to login via browser
80
+ webbrowser.open(login_details.verification_uri_complete)
78
81
  typer.secho(
79
- "Please log into your Flower account here: "
82
+ "A browser window has been opened for you to "
83
+ "log into your Flower account.\n"
84
+ "If it did not open automatically, use this URL:\n"
80
85
  f"{login_details.verification_uri_complete}",
81
86
  fg=typer.colors.BLUE,
82
87
  )
88
+
89
+ # Wait for user to complete login
83
90
  start_time = time.time()
84
91
  time.sleep(login_details.interval)
85
92
 
@@ -102,45 +109,38 @@ class OidcCliPlugin(CliAuthPlugin):
102
109
  raise LoginError("Process timed out.")
103
110
 
104
111
  def store_tokens(self, credentials: AccountAuthCredentials) -> None:
105
- """Store authentication tokens to the `credentials_path`.
112
+ """Store authentication tokens to the credential store."""
113
+ host = self.host
114
+ # Retrieve tokens
115
+ access_token = credentials.access_token
116
+ refresh_token = credentials.refresh_token
106
117
 
107
- The credentials, including tokens, will be saved as a JSON file
108
- at `credentials_path`.
109
- """
110
- self.access_token = credentials.access_token
111
- self.refresh_token = credentials.refresh_token
112
- json_dict = {
113
- AUTHN_TYPE_JSON_KEY: AuthnType.OIDC,
114
- ACCESS_TOKEN_KEY: credentials.access_token,
115
- REFRESH_TOKEN_KEY: credentials.refresh_token,
116
- }
118
+ # Store tokens in the credential store
119
+ self.store.set(AUTHN_TYPE_STORE_KEY % host, AuthnType.OIDC.encode("utf-8"))
120
+ self.store.set(ACCESS_TOKEN_STORE_KEY % host, access_token.encode("utf-8"))
121
+ self.store.set(REFRESH_TOKEN_STORE_KEY % host, refresh_token.encode("utf-8"))
117
122
 
118
- with open(self.credentials_path, "w", encoding="utf-8") as file:
119
- json.dump(json_dict, file, indent=4)
123
+ # Update internal state
124
+ self.access_token = access_token
125
+ self.refresh_token = refresh_token
120
126
 
121
127
  def load_tokens(self) -> None:
122
- """Load authentication tokens from the `credentials_path`."""
123
- with open(self.credentials_path, encoding="utf-8") as file:
124
- json_dict: dict[str, Any] = json.load(file)
125
- access_token = json_dict.get(ACCESS_TOKEN_KEY)
126
- refresh_token = json_dict.get(REFRESH_TOKEN_KEY)
128
+ """Load authentication tokens from the credential store."""
129
+ access_token_bytes = self.store.get(ACCESS_TOKEN_STORE_KEY % self.host)
130
+ refresh_token_bytes = self.store.get(REFRESH_TOKEN_STORE_KEY % self.host)
127
131
 
128
- if isinstance(access_token, str) and isinstance(refresh_token, str):
129
- self.access_token = access_token
130
- self.refresh_token = refresh_token
132
+ if access_token_bytes is not None and refresh_token_bytes is not None:
133
+ self.access_token = access_token_bytes.decode("utf-8")
134
+ self.refresh_token = refresh_token_bytes.decode("utf-8")
131
135
 
132
136
  def write_tokens_to_metadata(
133
137
  self, metadata: Sequence[tuple[str, str | bytes]]
134
138
  ) -> Sequence[tuple[str, str | bytes]]:
135
139
  """Write authentication tokens to the provided metadata."""
136
140
  if self.access_token is None or self.refresh_token is None:
137
- typer.secho(
138
- "Missing authentication tokens. Please login first.",
139
- fg=typer.colors.RED,
140
- bold=True,
141
- err=True,
141
+ raise click.ClickException(
142
+ "Missing authentication tokens. Please login first."
142
143
  )
143
- raise typer.Exit(code=1)
144
144
 
145
145
  return list(metadata) + [
146
146
  (ACCESS_TOKEN_KEY, self.access_token),
flwr/cli/build.py CHANGED
@@ -21,6 +21,7 @@ from io import BytesIO
21
21
  from pathlib import Path
22
22
  from typing import Annotated, Any
23
23
 
24
+ import click
24
25
  import pathspec
25
26
  import tomli
26
27
  import tomli_w
@@ -70,7 +71,7 @@ def get_fab_filename(config: dict[str, Any], fab_hash: str) -> str:
70
71
  Parameters
71
72
  ----------
72
73
  config : dict[str, Any]
73
- The project configuration dictionary.
74
+ The Flower App configuration dictionary.
74
75
  fab_hash : str
75
76
  The SHA-256 hash of the FAB file.
76
77
 
@@ -105,43 +106,29 @@ def build(
105
106
  if app is None:
106
107
  app = Path.cwd()
107
108
 
108
- app = app.resolve()
109
+ app = app.expanduser().resolve()
109
110
  if not app.is_dir():
110
- typer.secho(
111
- f"The path {app} is not a valid path to a Flower app.",
112
- fg=typer.colors.RED,
113
- bold=True,
114
- err=True,
111
+ raise click.ClickException(
112
+ f"The path {app} is not a valid path to a Flower app."
115
113
  )
116
- raise typer.Exit(code=1)
117
114
 
118
115
  if not is_valid_project_name(app.name):
119
- typer.secho(
120
- f"The project name {app.name} is invalid, "
121
- "a valid project name must start with a letter, "
122
- "and can only contain letters, digits, and hyphens.",
123
- fg=typer.colors.RED,
124
- bold=True,
125
- err=True,
116
+ raise click.ClickException(
117
+ f"The Flower App name {app.name} is invalid, "
118
+ "a valid app name must start with a letter, "
119
+ "and can only contain letters, digits, and hyphens."
126
120
  )
127
- raise typer.Exit(code=1)
128
121
 
129
- config, errors, warnings = load_and_validate(app / "pyproject.toml")
130
- if config is None:
131
- typer.secho(
132
- "Project configuration could not be loaded.\npyproject.toml is invalid:\n"
133
- + "\n".join([f"- {line}" for line in errors]),
134
- fg=typer.colors.RED,
135
- bold=True,
136
- err=True,
137
- )
138
- raise typer.Exit(code=1)
122
+ try:
123
+ config, warnings = load_and_validate(app / "pyproject.toml")
124
+ except ValueError as e:
125
+ raise click.ClickException(str(e)) from None
139
126
 
140
127
  if warnings:
141
128
  typer.secho(
142
- "Project configuration is missing the following "
129
+ "Flower App configuration (pyproject.toml) is missing the following "
143
130
  "recommended properties:\n" + "\n".join([f"- {line}" for line in warnings]),
144
- fg=typer.colors.RED,
131
+ fg=typer.colors.YELLOW,
145
132
  bold=True,
146
133
  )
147
134
 
@@ -0,0 +1,21 @@
1
+ # Copyright 2026 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 `config` command."""
16
+
17
+ from .ls import ls as ls
18
+
19
+ __all__ = [
20
+ "ls",
21
+ ]
flwr/cli/config/ls.py ADDED
@@ -0,0 +1,71 @@
1
+ # Copyright 2026 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 `config list` command."""
16
+
17
+
18
+ from typing import Annotated
19
+
20
+ import typer
21
+
22
+ from flwr.common.constant import CliOutputFormat
23
+
24
+ from ..constant import SuperLinkConnectionTomlKey
25
+ from ..flower_config import read_flower_config
26
+ from ..utils import cli_output_handler, print_json_to_stdout
27
+
28
+
29
+ def ls(
30
+ output_format: Annotated[
31
+ str,
32
+ typer.Option(
33
+ "--format",
34
+ case_sensitive=False,
35
+ help="Format output using 'default' view or 'json'",
36
+ ),
37
+ ] = CliOutputFormat.DEFAULT,
38
+ ) -> None:
39
+ """List all SuperLink connections (alias: ls)."""
40
+ with cli_output_handler(output_format=output_format) as is_json:
41
+ # Load Flower Config
42
+ config, config_path = read_flower_config()
43
+
44
+ # Get `superlink` tables
45
+ superlink_connections = config.get(SuperLinkConnectionTomlKey.SUPERLINK, {})
46
+
47
+ # Get default, then pop from dict
48
+ default = superlink_connections.pop(SuperLinkConnectionTomlKey.DEFAULT, None)
49
+
50
+ connection_names = list(superlink_connections.keys())
51
+
52
+ if is_json:
53
+ conn = {
54
+ SuperLinkConnectionTomlKey.SUPERLINK: connection_names,
55
+ SuperLinkConnectionTomlKey.DEFAULT: default,
56
+ }
57
+ print_json_to_stdout(conn)
58
+ else:
59
+ typer.secho("Flower Config file: ", fg=typer.colors.BLUE, nl=False)
60
+ typer.secho(f"{config_path}", fg=typer.colors.GREEN)
61
+ typer.secho("SuperLink connections:", fg=typer.colors.BLUE)
62
+ # List SuperLink connections and highlight default
63
+ for k in connection_names:
64
+ typer.secho(f" {k}", fg=typer.colors.GREEN, nl=False)
65
+ if k == default:
66
+ typer.secho(
67
+ f" ({SuperLinkConnectionTomlKey.DEFAULT})",
68
+ fg=typer.colors.WHITE,
69
+ nl=False,
70
+ )
71
+ typer.echo()
@@ -0,0 +1,297 @@
1
+ # Copyright 2026 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
+ """Utilities for migrating old TOML configurations to Flower config."""
16
+
17
+
18
+ import re
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ import click
23
+ import typer
24
+
25
+ from .config_utils import load_and_validate, validate_federation_in_project_config
26
+ from .flower_config import (
27
+ init_flwr_config,
28
+ parse_superlink_connection,
29
+ set_default_superlink_connection,
30
+ write_superlink_connection,
31
+ )
32
+
33
+ CONFIG_MIGRATION_NOTICE = """
34
+ #######################################################################
35
+ # CONFIGURATION MIGRATION NOTICE:
36
+ #
37
+ # What was previously called "federation config" for SuperLink
38
+ # connections in pyproject.toml has been renamed and moved.
39
+ #
40
+ # These settings are now **SuperLink connection configuration**
41
+ # and are defined in the Flower configuration file.
42
+ #
43
+ # The entries below are commented out intentionally and are kept
44
+ # only as a migration reference.
45
+ #
46
+ # Docs: https://flower.ai/docs/framework/ref-flower-configuration.html
47
+ #######################################################################
48
+
49
+ """
50
+
51
+ CLI_NOTICE = (
52
+ typer.style("\n🌸 Heads up from Flower!\n\n", fg=typer.colors.MAGENTA, bold=True)
53
+ + "We detected legacy usage of this command that relies on connection\n"
54
+ + "settings from your pyproject.toml.\n\n"
55
+ + "Flower will migrate any relevant settings to the new Flower config.\n\n"
56
+ + "Learn more: https://flower.ai/docs/framework/ref-flower-configuration.html\n"
57
+ )
58
+
59
+
60
+ def _is_legacy_usage(positional_arg_1: str | None, args: list[str]) -> bool:
61
+ """Check if legacy usage is detected in the given arguments."""
62
+ if positional_arg_1 is None:
63
+ return False
64
+
65
+ # If one and only one extra argument is given, assume legacy usage
66
+ if len(args) == 1:
67
+ return True
68
+
69
+ # If the first positional argument looks like a path, assume legacy usage
70
+ pth = Path(positional_arg_1)
71
+ if pth.is_absolute() or len(pth.parts) > 1 or positional_arg_1 in (".", ".."):
72
+ return True
73
+
74
+ # Lastly, check if a pyproject.toml file exists at the given path
75
+ if (pth / "pyproject.toml").exists():
76
+ return True
77
+
78
+ return False
79
+
80
+
81
+ def _is_migratable(app: Path) -> tuple[bool, str | None]:
82
+ """Check if the given app path contains legacy TOML configuration.
83
+
84
+ Parameters
85
+ ----------
86
+ app : Path
87
+ Path to the Flower App.
88
+
89
+ Returns
90
+ -------
91
+ tuple[bool, str | None]
92
+ Returns (True, None) if migratable, else (False, reason).
93
+ """
94
+ toml_path = app / "pyproject.toml"
95
+ if not toml_path.exists():
96
+ return False, f"No pyproject.toml found in '{app}'"
97
+
98
+ try:
99
+ config, _ = load_and_validate(toml_path, check_module=False)
100
+ except ValueError as e:
101
+ return False, str(e)
102
+
103
+ try:
104
+ _ = config["tool"]["flwr"]["federations"]
105
+ return True, None
106
+ except KeyError:
107
+ return False, f"No '[tool.flwr.federations]' section found in '{toml_path}'"
108
+
109
+
110
+ def _migrate_pyproject_toml_to_flower_config(
111
+ app: Path, toml_federation: str | None
112
+ ) -> tuple[list[str], str | None]:
113
+ """Migrate old TOML configuration to Flower config."""
114
+ # Load and validate the old TOML configuration
115
+ toml_path = app / "pyproject.toml"
116
+ config, _ = load_and_validate(toml_path, check_module=False)
117
+ validate_federation_in_project_config(toml_federation, config)
118
+
119
+ # Construct SuperLinkConnection
120
+ toml_federations: dict[str, Any] = config["tool"]["flwr"]["federations"]
121
+ migrated_conn_names: list[str] = []
122
+ for name, toml_fed_config in toml_federations.items():
123
+ if isinstance(toml_fed_config, dict):
124
+ # Resolve relative root-certificates path
125
+ if cert_path := toml_fed_config.get("root-certificates"):
126
+ if not Path(cert_path).is_absolute():
127
+ toml_fed_config["root-certificates"] = str(
128
+ (app / cert_path).expanduser().resolve()
129
+ )
130
+ # Parse and write SuperLink connection
131
+ conn = parse_superlink_connection(toml_fed_config, name)
132
+ write_superlink_connection(conn)
133
+ migrated_conn_names.append(name)
134
+
135
+ # Set default federation if applicable
136
+ default_toml_federation: str | None = toml_federations.get("default")
137
+ if default_toml_federation in toml_federations:
138
+ set_default_superlink_connection(default_toml_federation)
139
+
140
+ return migrated_conn_names, default_toml_federation
141
+
142
+
143
+ def _comment_out_legacy_toml_config(app: Path) -> None:
144
+ """Comment out legacy TOML configuration in pyproject.toml."""
145
+ # Read pyproject.toml lines
146
+ toml_path = app / "pyproject.toml"
147
+ lines = toml_path.read_text(encoding="utf-8").splitlines(keepends=True)
148
+ section_pattern = re.compile(r"\s*\[(.*)\]")
149
+
150
+ # Comment out the [tool.flwr.federations] section
151
+ notice_added = False
152
+ in_federation_section = False
153
+ with toml_path.open("w", encoding="utf-8") as f:
154
+ for line in lines:
155
+ # Detect section headers
156
+ if match := section_pattern.match(line):
157
+ section = match.group(1)
158
+ in_federation_section = section.startswith("tool.flwr.federations")
159
+
160
+ # Comment out lines in the federation section
161
+ if in_federation_section:
162
+ if not notice_added:
163
+ f.write(CONFIG_MIGRATION_NOTICE)
164
+ notice_added = True
165
+ # Preserve empty lines and comment out others
166
+ f.write(f"# {line}" if line.strip() != "" else line)
167
+ else:
168
+ f.write(line)
169
+
170
+
171
+ def migrate(
172
+ positional_arg_1: str | None,
173
+ args: list[str],
174
+ ignore_legacy_usage: bool = False,
175
+ ) -> None:
176
+ """Migrate legacy TOML configuration to Flower config.
177
+
178
+ Migrates SuperLink connection settings from `[tool.flwr.federations]` in
179
+ pyproject.toml to the new Flower config format when legacy usage is detected
180
+ or the migration is applicable.
181
+
182
+ `flwr run` should call `migrate(app, [], ignore_legacy_usage=True)` to skip
183
+ legacy usage check. Other CLI commands should call `migrate(superlink, ctx.args)`.
184
+
185
+ Parameters
186
+ ----------
187
+ positional_arg_1 : str | None
188
+ The first positional argument.
189
+ args : list[str]
190
+ Additional arguments. In legacy usage, this is the TOML federation name.
191
+ ignore_legacy_usage : bool (default: False)
192
+ Set to `True` only for `flwr run` command to skip legacy usage check.
193
+
194
+ Raises
195
+ ------
196
+ click.UsageError
197
+ If more than one extra argument is provided.
198
+ click.ClickException
199
+ If legacy usage detected but migration fails.
200
+
201
+ Examples
202
+ --------
203
+ The following usages will trigger migration if applicable:
204
+ - `flwr <CMD> . my-federation`
205
+ - `flwr <CMD> ./my-app`
206
+ - `flwr <CMD>`
207
+
208
+ The following usages will NOT trigger migration:
209
+ - `flwr <CMD> named-conn`
210
+
211
+ Notes
212
+ -----
213
+ This function will NOT return when legacy usage is detected to force the user
214
+ to adapt to the new usage pattern after migration.
215
+ """
216
+ # Initialize Flower config
217
+ init_flwr_config()
218
+
219
+ # Trigger the same typer error when detecting unexpected extra args
220
+ if len(args) > 1:
221
+ raise click.UsageError(f"Got unexpected extra arguments ({' '.join(args[1:])})")
222
+
223
+ # Determine app path for migration
224
+ arg1 = positional_arg_1
225
+ app = Path(arg1) if arg1 else Path(".")
226
+ app = app.expanduser().resolve()
227
+
228
+ # Check if migration is applicable and if legacy usage is detected
229
+ is_migratable, reason = _is_migratable(app)
230
+ is_legacy = _is_legacy_usage(arg1, args) if not ignore_legacy_usage else False
231
+
232
+ # Print notice once if legacy usage detected or migration is applicable
233
+ if is_legacy or is_migratable:
234
+ typer.echo(CLI_NOTICE)
235
+
236
+ if not is_migratable:
237
+ # Raise error if legacy usage is detected but migration is not applicable
238
+ if is_legacy:
239
+ raise click.ClickException(
240
+ f"Cannot migrate configuration:\n{reason}. \nThis is expected if the "
241
+ "migration has been previously carried out. Use `--help` after your "
242
+ "command to see the new usage pattern."
243
+ )
244
+ return # Nothing to migrate
245
+
246
+ # Perform migration
247
+ toml_federation = args[0] if len(args) == 1 else None
248
+ try:
249
+ migrated_conns, default_conn = _migrate_pyproject_toml_to_flower_config(
250
+ app, toml_federation
251
+ )
252
+ except Exception as e:
253
+ raise click.ClickException(
254
+ f"Failed to migrate legacy TOML configuration to Flower config:\n{e!r}"
255
+ ) from e
256
+
257
+ typer.secho("✅ Migration completed successfully!\n", fg=typer.colors.GREEN)
258
+
259
+ # Print migrated connections
260
+ typer.secho("Migrated SuperLink connections:", fg=typer.colors.BLUE)
261
+ for conn_name in migrated_conns:
262
+ typer.secho(f" {conn_name}", fg=typer.colors.GREEN, nl=False)
263
+ if conn_name == default_conn:
264
+ typer.secho(" (default)", fg=typer.colors.WHITE, nl=False)
265
+ typer.echo()
266
+
267
+ _comment_out_legacy_toml_config(app)
268
+
269
+ if is_legacy:
270
+ # print usage
271
+ typer.secho("\nYou should now use the Flower CLI as follows:")
272
+ ctx = click.get_current_context()
273
+ typer.secho(ctx.get_usage() + "\n", bold=True)
274
+
275
+ # Abort if legacy usage is detected to force user to adapt to new usage
276
+ raise typer.Exit(code=1)
277
+
278
+
279
+ def warn_if_federation_config_overrides(
280
+ federation_config_overrides: list[str] | None,
281
+ ) -> None:
282
+ """Warn if federation config overrides are provided.
283
+
284
+ Parameters
285
+ ----------
286
+ federation_config_overrides : list[str] | None
287
+ List of federation config override strings.
288
+ """
289
+ if federation_config_overrides is None:
290
+ return
291
+
292
+ typer.secho(
293
+ "⚠️ Warning: `--federation-config` option is deprecated and will be ignored.\n"
294
+ "Use Flower configuration files to set SuperLink connections.",
295
+ fg=typer.colors.YELLOW,
296
+ bold=True,
297
+ )