flwr-nightly 1.26.0.dev20251226__py3-none-any.whl → 1.26.0.dev20260121__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 (78) hide show
  1. flwr/app/__init__.py +4 -1
  2. flwr/app/message_type.py +29 -0
  3. flwr/app/metadata.py +5 -2
  4. flwr/app/user_config.py +19 -0
  5. flwr/cli/app.py +31 -11
  6. flwr/cli/auth_plugin/oidc_cli_plugin.py +8 -1
  7. flwr/cli/config/__init__.py +21 -0
  8. flwr/cli/config/ls.py +104 -0
  9. flwr/cli/config_migration.py +300 -0
  10. flwr/cli/config_utils.py +53 -0
  11. flwr/cli/constant.py +67 -0
  12. flwr/cli/federation/__init__.py +0 -2
  13. flwr/cli/federation/ls.py +236 -15
  14. flwr/cli/flower_config.py +447 -0
  15. flwr/cli/log.py +21 -35
  16. flwr/cli/ls.py +23 -34
  17. flwr/cli/pull.py +16 -30
  18. flwr/cli/run/run.py +44 -58
  19. flwr/cli/stop.py +21 -30
  20. flwr/cli/typing.py +211 -0
  21. flwr/cli/utils.py +102 -1
  22. flwr/client/message_handler/message_handler.py +2 -1
  23. flwr/client/mod/centraldp_mods.py +1 -1
  24. flwr/client/mod/localdp_mod.py +1 -1
  25. flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
  26. flwr/client/run_info_store.py +2 -1
  27. flwr/clientapp/client_app.py +2 -1
  28. flwr/common/__init__.py +2 -1
  29. flwr/common/config.py +2 -1
  30. flwr/common/constant.py +0 -15
  31. flwr/common/context.py +2 -1
  32. flwr/common/grpc.py +2 -1
  33. flwr/common/serde.py +7 -5
  34. flwr/common/typing.py +1 -2
  35. flwr/compat/client/app.py +6 -9
  36. flwr/compat/client/grpc_client/connection.py +2 -1
  37. flwr/compat/common/constant.py +29 -0
  38. flwr/compat/server/app.py +1 -1
  39. flwr/server/app.py +1 -1
  40. flwr/server/compat/grid_client_proxy.py +2 -1
  41. flwr/server/grid/grpc_grid.py +2 -2
  42. flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +13 -12
  43. flwr/server/superlink/linkstate/__init__.py +2 -0
  44. flwr/server/superlink/linkstate/in_memory_linkstate.py +2 -10
  45. flwr/server/superlink/linkstate/linkstate.py +2 -21
  46. flwr/server/superlink/linkstate/sql_linkstate.py +221 -0
  47. flwr/server/superlink/linkstate/sqlite_linkstate.py +2 -24
  48. flwr/server/superlink/linkstate/utils.py +2 -2
  49. flwr/server/workflow/default_workflows.py +2 -1
  50. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
  51. flwr/serverapp/strategy/fedavg.py +1 -1
  52. flwr/serverapp/strategy/fedxgb_cyclic.py +1 -1
  53. flwr/simulation/ray_transport/ray_client_proxy.py +2 -6
  54. flwr/simulation/run_simulation.py +2 -1
  55. flwr/{common → supercore}/address.py +0 -35
  56. flwr/supercore/constant.py +14 -0
  57. flwr/supercore/corestate/sql_corestate.py +153 -0
  58. flwr/supercore/credential_store/__init__.py +33 -0
  59. flwr/supercore/credential_store/credential_store.py +34 -0
  60. flwr/supercore/credential_store/file_credential_store.py +76 -0
  61. flwr/supercore/sql_mixin.py +292 -0
  62. flwr/supercore/sqlite_mixin.py +4 -7
  63. flwr/supercore/state/__init__.py +15 -0
  64. flwr/supercore/state/schema/README.md +125 -0
  65. flwr/supercore/state/schema/__init__.py +15 -0
  66. flwr/supercore/state/schema/corestate_tables.py +36 -0
  67. flwr/supercore/state/schema/linkstate_tables.py +152 -0
  68. flwr/supercore/state/schema/objectstore_tables.py +90 -0
  69. flwr/supercore/utils.py +14 -0
  70. flwr/superlink/servicer/control/control_servicer.py +2 -1
  71. flwr/supernode/servicer/clientappio/clientappio_servicer.py +6 -4
  72. flwr/supernode/start_client_internal.py +3 -2
  73. {flwr_nightly-1.26.0.dev20251226.dist-info → flwr_nightly-1.26.0.dev20260121.dist-info}/METADATA +2 -1
  74. {flwr_nightly-1.26.0.dev20251226.dist-info → flwr_nightly-1.26.0.dev20260121.dist-info}/RECORD +76 -58
  75. flwr/cli/federation/show.py +0 -318
  76. flwr/common/pyproject.py +0 -42
  77. {flwr_nightly-1.26.0.dev20251226.dist-info → flwr_nightly-1.26.0.dev20260121.dist-info}/WHEEL +0 -0
  78. {flwr_nightly-1.26.0.dev20251226.dist-info → flwr_nightly-1.26.0.dev20260121.dist-info}/entry_points.txt +0 -0
flwr/app/__init__.py CHANGED
@@ -15,7 +15,6 @@
15
15
  """Public Flower App APIs."""
16
16
 
17
17
 
18
- from flwr.common.constant import MessageType
19
18
  from flwr.common.context import Context
20
19
  from flwr.common.message import Message
21
20
  from flwr.common.record import (
@@ -27,7 +26,9 @@ from flwr.common.record import (
27
26
  )
28
27
 
29
28
  from .error import Error
29
+ from .message_type import MessageType
30
30
  from .metadata import Metadata
31
+ from .user_config import UserConfig, UserConfigValue
31
32
 
32
33
  __all__ = [
33
34
  "Array",
@@ -40,4 +41,6 @@ __all__ = [
40
41
  "Metadata",
41
42
  "MetricRecord",
42
43
  "RecordDict",
44
+ "UserConfig",
45
+ "UserConfigValue",
43
46
  ]
@@ -0,0 +1,29 @@
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
+ """MessageType constants."""
16
+
17
+ from __future__ import annotations
18
+
19
+
20
+ class MessageType:
21
+ """Message type."""
22
+
23
+ TRAIN = "train"
24
+ EVALUATE = "evaluate"
25
+ QUERY = "query"
26
+
27
+ def __new__(cls) -> MessageType:
28
+ """Prevent instantiation."""
29
+ raise TypeError(f"{cls.__name__} cannot be instantiated.")
flwr/app/metadata.py CHANGED
@@ -19,7 +19,10 @@ from __future__ import annotations
19
19
 
20
20
  from typing import cast
21
21
 
22
- from ..common.constant import MessageType, MessageTypeLegacy
22
+ from flwr.app.message_type import MessageType
23
+ from flwr.supercore.constant import SYSTEM_MESSAGE_TYPE
24
+
25
+ from ..common.constant import MessageTypeLegacy
23
26
 
24
27
 
25
28
  class Metadata: # pylint: disable=too-many-instance-attributes
@@ -194,7 +197,7 @@ def validate_message_type(message_type: str) -> bool:
194
197
  MessageType.TRAIN,
195
198
  MessageType.EVALUATE,
196
199
  MessageType.QUERY,
197
- MessageType.SYSTEM,
200
+ SYSTEM_MESSAGE_TYPE,
198
201
  }
199
202
  if message_type in valid_types:
200
203
  return True
@@ -0,0 +1,19 @@
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
+ """UserConfig type definition."""
16
+
17
+
18
+ UserConfigValue = bool | float | int | str
19
+ UserConfig = dict[str, UserConfigValue]
flwr/cli/app.py CHANGED
@@ -14,6 +14,9 @@
14
14
  # ==============================================================================
15
15
  """Flower command line interface."""
16
16
 
17
+
18
+ from typing import Any, TypedDict
19
+
17
20
  import typer
18
21
  from typer.main import get_command
19
22
 
@@ -22,8 +25,9 @@ from flwr.supercore.version import package_version
22
25
  from .app_cmd import publish as app_publish
23
26
  from .app_cmd import review as app_review
24
27
  from .build import build
28
+ from .config import ls as config_list
25
29
  from .federation import ls as federation_list
26
- from .federation import show as federation_show
30
+ from .flower_config import init_flwr_config
27
31
  from .install import install
28
32
  from .log import log
29
33
  from .login import login
@@ -36,6 +40,15 @@ from .supernode import ls as supernode_list
36
40
  from .supernode import register as supernode_register
37
41
  from .supernode import unregister as supernode_unregister
38
42
 
43
+
44
+ class CommandKwargs(TypedDict):
45
+ """Keywords for typer command to make mypy happy."""
46
+
47
+ context_settings: dict[str, Any]
48
+
49
+
50
+ ALLOW_EXTRAS: CommandKwargs = {"context_settings": {"allow_extra_args": True}}
51
+
39
52
  app = typer.Typer(
40
53
  help=typer.style(
41
54
  "flwr is the Flower command line interface.",
@@ -50,12 +63,12 @@ app.command()(new)
50
63
  app.command()(run)
51
64
  app.command()(build)
52
65
  app.command()(install)
53
- app.command()(log)
54
- app.command("list")(ls)
55
- app.command(hidden=True)(ls)
56
- app.command()(stop)
66
+ app.command(**ALLOW_EXTRAS)(log)
67
+ app.command("list", **ALLOW_EXTRAS)(ls)
68
+ app.command(hidden=True, **ALLOW_EXTRAS)(ls)
69
+ app.command(**ALLOW_EXTRAS)(stop)
57
70
  app.command()(login)
58
- app.command()(pull)
71
+ app.command(**ALLOW_EXTRAS)(pull)
59
72
 
60
73
  # Create supernode command group
61
74
  supernode_app = typer.Typer(help="Manage SuperNodes")
@@ -80,14 +93,20 @@ federation_app.command("list")(federation_list)
80
93
  # Hide "ls" command (left as alias)
81
94
  federation_app.command(hidden=True)(federation_list)
82
95
  app.add_typer(federation_app, name="federation")
83
- federation_app.command()(federation_show)
96
+
97
+ # Create config command group
98
+ config_app = typer.Typer(help="Manage Configuration")
99
+ config_app.command("list")(config_list)
100
+ # Hide "ls" command (left as alias)
101
+ config_app.command(hidden=True)(config_list)
102
+ app.add_typer(config_app, name="config")
84
103
 
85
104
  typer_click_object = get_command(app)
86
105
 
87
106
 
88
107
  @app.callback(invoke_without_command=True)
89
- def version_callback(
90
- ver: bool = typer.Option(
108
+ def main(
109
+ version: bool = typer.Option(
91
110
  None,
92
111
  "-V",
93
112
  "--version",
@@ -95,8 +114,9 @@ def version_callback(
95
114
  help="Show the version and exit.",
96
115
  ),
97
116
  ) -> None:
98
- """Print version."""
99
- if ver:
117
+ """Flower CLI."""
118
+ init_flwr_config()
119
+ if version:
100
120
  typer.secho(f"Flower version: {package_version}", fg="blue")
101
121
  raise typer.Exit()
102
122
 
@@ -17,6 +17,7 @@
17
17
 
18
18
  import json
19
19
  import time
20
+ import webbrowser
20
21
  from collections.abc import Sequence
21
22
  from pathlib import Path
22
23
  from typing import Any
@@ -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
 
@@ -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,104 @@
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
+ import io
19
+ import json
20
+ from typing import Annotated
21
+
22
+ import typer
23
+ from rich.console import Console
24
+
25
+ from flwr.common.constant import CliOutputFormat
26
+ from flwr.common.logger import print_json_error, redirect_output, restore_output
27
+
28
+ from ..constant import SuperLinkConnectionTomlKey
29
+ from ..flower_config import read_flower_config
30
+
31
+
32
+ def ls(
33
+ output_format: Annotated[
34
+ str,
35
+ typer.Option(
36
+ "--format",
37
+ case_sensitive=False,
38
+ help="Format output using 'default' view or 'json'",
39
+ ),
40
+ ] = CliOutputFormat.DEFAULT,
41
+ ) -> None:
42
+ """List all SuperLink connections."""
43
+ suppress_output = output_format == CliOutputFormat.JSON
44
+ captured_output = io.StringIO()
45
+ config_path = None
46
+
47
+ if suppress_output:
48
+ redirect_output(captured_output)
49
+
50
+ try:
51
+ # Load Flower Config
52
+ config, config_path = read_flower_config()
53
+
54
+ # Get `superlink` tables
55
+ superlink_connections = config.get(SuperLinkConnectionTomlKey.SUPERLINK, {})
56
+
57
+ # Get default, then pop from dict
58
+ default = superlink_connections.pop(SuperLinkConnectionTomlKey.DEFAULT, None)
59
+
60
+ connection_names = list(superlink_connections.keys())
61
+ restore_output()
62
+ if output_format == CliOutputFormat.JSON:
63
+ conn = {
64
+ SuperLinkConnectionTomlKey.SUPERLINK: connection_names,
65
+ SuperLinkConnectionTomlKey.DEFAULT: default,
66
+ }
67
+ Console().print_json(json.dumps(conn))
68
+ else:
69
+ typer.secho("SuperLink connections:", fg=typer.colors.BLUE)
70
+ # List SuperLink connections and highlight default
71
+ for k in connection_names:
72
+ typer.secho(f" {k}", fg=typer.colors.GREEN, nl=False)
73
+ if k == default:
74
+ typer.secho(
75
+ f" ({SuperLinkConnectionTomlKey.DEFAULT})",
76
+ fg=typer.colors.WHITE,
77
+ nl=False,
78
+ )
79
+ typer.echo()
80
+ except typer.Exit as err:
81
+ # log the error if json format requested
82
+ # else do nothing since it will be logged by typer
83
+ if suppress_output:
84
+ restore_output()
85
+ e_message = captured_output.getvalue()
86
+ print_json_error(e_message, err)
87
+
88
+ except Exception as err: # pylint: disable=broad-except
89
+ if suppress_output:
90
+ restore_output()
91
+ e_message = captured_output.getvalue()
92
+ print_json_error(e_message, err)
93
+ else:
94
+ typer.secho(
95
+ f"❌ An unexpected error occurred while listing the SuperLink "
96
+ f"connections in the Flower configuration file ({config_path}): {err}",
97
+ fg=typer.colors.RED,
98
+ err=True,
99
+ )
100
+
101
+ finally:
102
+ if suppress_output:
103
+ restore_output()
104
+ captured_output.close()
@@ -0,0 +1,300 @@
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: <link to Flower config docs>
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\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
+ config, errors, _ = load_and_validate(toml_path, check_module=False)
98
+ if config is None:
99
+ return False, f"Failed to load TOML configuration: {toml_path}"
100
+ if errors:
101
+ err_msg = f"Invalid TOML configuration found in '{toml_path}':\n"
102
+ err_msg += "\n".join(f"- {err}" for err in errors)
103
+ return False, err_msg
104
+ try:
105
+ _ = config["tool"]["flwr"]["federations"]
106
+ return True, None
107
+ except KeyError:
108
+ return False, f"No '[tool.flwr.federations]' section found in '{toml_path}'"
109
+
110
+
111
+ def _migrate_pyproject_toml_to_flower_config(
112
+ app: Path, toml_federation: str | None
113
+ ) -> tuple[list[str], str | None]:
114
+ """Migrate old TOML configuration to Flower config."""
115
+ # Load and validate the old TOML configuration
116
+ toml_path = app / "pyproject.toml"
117
+ config, _, _ = load_and_validate(toml_path, check_module=False)
118
+ if config is None:
119
+ raise ValueError(f"Failed to load TOML configuration: {toml_path}")
120
+ validate_federation_in_project_config(toml_federation, config)
121
+
122
+ # Construct SuperLinkConnection
123
+ toml_federations: dict[str, Any] = config["tool"]["flwr"]["federations"]
124
+ migrated_conn_names: list[str] = []
125
+ for name, toml_fed_config in toml_federations.items():
126
+ if isinstance(toml_fed_config, dict):
127
+ # Resolve relative root-certificates path
128
+ if cert_path := toml_fed_config.get("root-certificates"):
129
+ if not Path(cert_path).is_absolute():
130
+ toml_fed_config["root-certificates"] = str(
131
+ (app / cert_path).resolve()
132
+ )
133
+ # Parse and write SuperLink connection
134
+ conn = parse_superlink_connection(toml_fed_config, name)
135
+ write_superlink_connection(conn)
136
+ migrated_conn_names.append(name)
137
+
138
+ # Set default federation if applicable
139
+ default_toml_federation: str | None = toml_federations.get("default")
140
+ if default_toml_federation in toml_federations:
141
+ set_default_superlink_connection(default_toml_federation)
142
+
143
+ return migrated_conn_names, default_toml_federation
144
+
145
+
146
+ def _comment_out_legacy_toml_config(app: Path) -> None:
147
+ """Comment out legacy TOML configuration in pyproject.toml."""
148
+ # Read pyproject.toml lines
149
+ toml_path = app / "pyproject.toml"
150
+ lines = toml_path.read_text(encoding="utf-8").splitlines(keepends=True)
151
+ section_pattern = re.compile(r"\s*\[(.*)\]")
152
+
153
+ # Comment out the [tool.flwr.federations] section
154
+ notice_added = False
155
+ in_federation_section = False
156
+ with toml_path.open("w", encoding="utf-8") as f:
157
+ for line in lines:
158
+ # Detect section headers
159
+ if match := section_pattern.match(line):
160
+ section = match.group(1)
161
+ in_federation_section = section.startswith("tool.flwr.federations")
162
+
163
+ # Comment out lines in the federation section
164
+ if in_federation_section:
165
+ if not notice_added:
166
+ f.write(CONFIG_MIGRATION_NOTICE)
167
+ notice_added = True
168
+ # Preserve empty lines and comment out others
169
+ f.write(f"# {line}" if line.strip() != "" else line)
170
+ else:
171
+ f.write(line)
172
+
173
+
174
+ def migrate(
175
+ positional_arg_1: str | None,
176
+ args: list[str],
177
+ ignore_legacy_usage: bool = False,
178
+ ) -> None:
179
+ """Migrate legacy TOML configuration to Flower config.
180
+
181
+ Migrates SuperLink connection settings from `[tool.flwr.federations]` in
182
+ pyproject.toml to the new Flower config format when legacy usage is detected
183
+ or the migration is applicable.
184
+
185
+ `flwr run` should call `migrate(app, [], ignore_legacy_usage=True)` to skip
186
+ legacy usage check. Other CLI commands should call `migrate(superlink, ctx.args)`.
187
+
188
+ Parameters
189
+ ----------
190
+ positional_arg_1 : str | None
191
+ The first positional argument.
192
+ args : list[str]
193
+ Additional arguments. In legacy usage, this is the TOML federation name.
194
+ ignore_legacy_usage : bool (default: False)
195
+ Set to `True` only for `flwr run` command to skip legacy usage check.
196
+
197
+ Raises
198
+ ------
199
+ click.UsageError
200
+ If more than one extra argument is provided.
201
+ click.ClickException
202
+ If legacy usage detected but migration fails.
203
+
204
+ Examples
205
+ --------
206
+ The following usages will trigger migration if applicable:
207
+ - `flwr <CMD> . my-federation`
208
+ - `flwr <CMD> ./my-app`
209
+ - `flwr <CMD>`
210
+
211
+ The following usages will NOT trigger migration:
212
+ - `flwr <CMD> named-conn`
213
+
214
+ Notes
215
+ -----
216
+ This function will NOT return when legacy usage is detected to force the user
217
+ to adapt to the new usage pattern after migration.
218
+ """
219
+ # Initialize Flower config
220
+ init_flwr_config()
221
+
222
+ # Trigger the same typer error when detecting unexpected extra args
223
+ if len(args) > 1:
224
+ raise click.UsageError(f"Got unexpected extra arguments ({' '.join(args[1:])})")
225
+
226
+ # Determine app path for migration
227
+ arg1 = positional_arg_1
228
+ app = Path(arg1) if arg1 else Path(".")
229
+ app = app.resolve()
230
+
231
+ # Check if migration is applicable and if legacy usage is detected
232
+ is_migratable, reason = _is_migratable(app)
233
+ is_legacy = _is_legacy_usage(arg1, args) if not ignore_legacy_usage else False
234
+
235
+ # Print notice once if legacy usage detected or migration is applicable
236
+ if is_legacy or is_migratable:
237
+ typer.echo(CLI_NOTICE)
238
+
239
+ if not is_migratable:
240
+ # Raise error if legacy usage is detected but migration is not applicable
241
+ if is_legacy:
242
+ raise click.ClickException(
243
+ f"Cannot migrate configuration:\n{reason}. \nThis is expected if the "
244
+ "migration has been previously carried out. Use `--help` after your "
245
+ "command to see the new usage pattern."
246
+ )
247
+ return # Nothing to migrate
248
+
249
+ # Perform migration
250
+ toml_federation = args[0] if len(args) == 1 else None
251
+ try:
252
+ migrated_conns, default_conn = _migrate_pyproject_toml_to_flower_config(
253
+ app, toml_federation
254
+ )
255
+ except Exception as e:
256
+ raise click.ClickException(
257
+ f"Failed to migrate legacy TOML configuration to Flower config:\n{e!r}"
258
+ ) from e
259
+
260
+ typer.secho("✅ Migration completed successfully!\n", fg=typer.colors.GREEN)
261
+
262
+ # Print migrated connections
263
+ typer.secho("Migrated SuperLink connections:", fg=typer.colors.BLUE)
264
+ for conn_name in migrated_conns:
265
+ typer.secho(f" {conn_name}", fg=typer.colors.GREEN, nl=False)
266
+ if conn_name == default_conn:
267
+ typer.secho(" (default)", fg=typer.colors.WHITE, nl=False)
268
+ typer.echo()
269
+
270
+ _comment_out_legacy_toml_config(app)
271
+
272
+ if is_legacy:
273
+ # print usage
274
+ typer.secho("\nYou should now use the Flower CLI as follows:")
275
+ ctx = click.get_current_context()
276
+ typer.secho(ctx.get_usage() + "\n", bold=True)
277
+
278
+ # Abort if legacy usage is detected to force user to adapt to new usage
279
+ raise typer.Exit(code=1)
280
+
281
+
282
+ def warn_if_federation_config_overrides(
283
+ federation_config_overrides: list[str] | None,
284
+ ) -> None:
285
+ """Warn if federation config overrides are provided.
286
+
287
+ Parameters
288
+ ----------
289
+ federation_config_overrides : list[str] | None
290
+ List of federation config override strings.
291
+ """
292
+ if federation_config_overrides is None:
293
+ return
294
+
295
+ typer.secho(
296
+ "⚠️ Warning: `--federation-config` option is deprecated and will be ignored.\n"
297
+ "Use Flower configuration files to set SuperLink connections.",
298
+ fg=typer.colors.YELLOW,
299
+ bold=True,
300
+ )