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.
- flwr/app/__init__.py +4 -1
- flwr/app/message_type.py +29 -0
- flwr/app/metadata.py +5 -2
- flwr/app/user_config.py +19 -0
- flwr/cli/app.py +31 -11
- flwr/cli/auth_plugin/oidc_cli_plugin.py +8 -1
- flwr/cli/config/__init__.py +21 -0
- flwr/cli/config/ls.py +104 -0
- flwr/cli/config_migration.py +300 -0
- flwr/cli/config_utils.py +53 -0
- flwr/cli/constant.py +67 -0
- flwr/cli/federation/__init__.py +0 -2
- flwr/cli/federation/ls.py +236 -15
- flwr/cli/flower_config.py +447 -0
- flwr/cli/log.py +21 -35
- flwr/cli/ls.py +23 -34
- flwr/cli/pull.py +16 -30
- flwr/cli/run/run.py +44 -58
- flwr/cli/stop.py +21 -30
- flwr/cli/typing.py +211 -0
- flwr/cli/utils.py +102 -1
- flwr/client/message_handler/message_handler.py +2 -1
- flwr/client/mod/centraldp_mods.py +1 -1
- flwr/client/mod/localdp_mod.py +1 -1
- flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
- flwr/client/run_info_store.py +2 -1
- flwr/clientapp/client_app.py +2 -1
- flwr/common/__init__.py +2 -1
- flwr/common/config.py +2 -1
- flwr/common/constant.py +0 -15
- flwr/common/context.py +2 -1
- flwr/common/grpc.py +2 -1
- flwr/common/serde.py +7 -5
- flwr/common/typing.py +1 -2
- flwr/compat/client/app.py +6 -9
- flwr/compat/client/grpc_client/connection.py +2 -1
- flwr/compat/common/constant.py +29 -0
- flwr/compat/server/app.py +1 -1
- flwr/server/app.py +1 -1
- flwr/server/compat/grid_client_proxy.py +2 -1
- flwr/server/grid/grpc_grid.py +2 -2
- flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +13 -12
- flwr/server/superlink/linkstate/__init__.py +2 -0
- flwr/server/superlink/linkstate/in_memory_linkstate.py +2 -10
- flwr/server/superlink/linkstate/linkstate.py +2 -21
- flwr/server/superlink/linkstate/sql_linkstate.py +221 -0
- flwr/server/superlink/linkstate/sqlite_linkstate.py +2 -24
- flwr/server/superlink/linkstate/utils.py +2 -2
- flwr/server/workflow/default_workflows.py +2 -1
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
- flwr/serverapp/strategy/fedavg.py +1 -1
- flwr/serverapp/strategy/fedxgb_cyclic.py +1 -1
- flwr/simulation/ray_transport/ray_client_proxy.py +2 -6
- flwr/simulation/run_simulation.py +2 -1
- flwr/{common → supercore}/address.py +0 -35
- flwr/supercore/constant.py +14 -0
- flwr/supercore/corestate/sql_corestate.py +153 -0
- flwr/supercore/credential_store/__init__.py +33 -0
- flwr/supercore/credential_store/credential_store.py +34 -0
- flwr/supercore/credential_store/file_credential_store.py +76 -0
- flwr/supercore/sql_mixin.py +292 -0
- flwr/supercore/sqlite_mixin.py +4 -7
- flwr/supercore/state/__init__.py +15 -0
- flwr/supercore/state/schema/README.md +125 -0
- flwr/supercore/state/schema/__init__.py +15 -0
- flwr/supercore/state/schema/corestate_tables.py +36 -0
- flwr/supercore/state/schema/linkstate_tables.py +152 -0
- flwr/supercore/state/schema/objectstore_tables.py +90 -0
- flwr/supercore/utils.py +14 -0
- flwr/superlink/servicer/control/control_servicer.py +2 -1
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +6 -4
- flwr/supernode/start_client_internal.py +3 -2
- {flwr_nightly-1.26.0.dev20251226.dist-info → flwr_nightly-1.26.0.dev20260121.dist-info}/METADATA +2 -1
- {flwr_nightly-1.26.0.dev20251226.dist-info → flwr_nightly-1.26.0.dev20260121.dist-info}/RECORD +76 -58
- flwr/cli/federation/show.py +0 -318
- flwr/common/pyproject.py +0 -42
- {flwr_nightly-1.26.0.dev20251226.dist-info → flwr_nightly-1.26.0.dev20260121.dist-info}/WHEEL +0 -0
- {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
|
]
|
flwr/app/message_type.py
ADDED
|
@@ -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
|
|
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
|
-
|
|
200
|
+
SYSTEM_MESSAGE_TYPE,
|
|
198
201
|
}
|
|
199
202
|
if message_type in valid_types:
|
|
200
203
|
return True
|
flwr/app/user_config.py
ADDED
|
@@ -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 .
|
|
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
|
-
|
|
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
|
|
90
|
-
|
|
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
|
-
"""
|
|
99
|
-
|
|
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
|
-
"
|
|
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
|
+
)
|