flwr 1.24.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.
- flwr/__init__.py +1 -1
- 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 +37 -19
- flwr/cli/app_cmd/publish.py +25 -75
- flwr/cli/app_cmd/review.py +25 -66
- flwr/cli/auth_plugin/auth_plugin.py +5 -10
- flwr/cli/auth_plugin/noop_auth_plugin.py +1 -2
- flwr/cli/auth_plugin/oidc_cli_plugin.py +38 -38
- flwr/cli/build.py +15 -28
- flwr/cli/config/__init__.py +21 -0
- flwr/cli/config/ls.py +71 -0
- flwr/cli/config_migration.py +297 -0
- flwr/cli/config_utils.py +63 -156
- flwr/cli/constant.py +71 -0
- flwr/cli/federation/__init__.py +0 -2
- flwr/cli/federation/ls.py +256 -64
- flwr/cli/flower_config.py +429 -0
- flwr/cli/install.py +23 -62
- flwr/cli/log.py +23 -37
- flwr/cli/login/login.py +29 -63
- flwr/cli/ls.py +72 -61
- flwr/cli/new/new.py +98 -309
- flwr/cli/pull.py +19 -37
- flwr/cli/run/run.py +87 -100
- flwr/cli/run_utils.py +23 -5
- flwr/cli/stop.py +33 -74
- flwr/cli/supernode/ls.py +35 -62
- flwr/cli/supernode/register.py +31 -80
- flwr/cli/supernode/unregister.py +24 -70
- flwr/cli/typing.py +200 -0
- flwr/cli/utils.py +160 -412
- flwr/client/grpc_adapter_client/connection.py +2 -2
- flwr/client/grpc_rere_client/connection.py +9 -6
- flwr/client/grpc_rere_client/grpc_adapter.py +1 -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/rest_client/connection.py +6 -4
- flwr/client/run_info_store.py +2 -1
- flwr/clientapp/client_app.py +2 -1
- flwr/common/__init__.py +3 -2
- flwr/common/args.py +5 -5
- flwr/common/config.py +12 -17
- flwr/common/constant.py +3 -16
- flwr/common/context.py +2 -1
- flwr/common/exit/exit.py +4 -4
- flwr/common/exit/exit_code.py +6 -0
- flwr/common/grpc.py +2 -1
- flwr/common/logger.py +1 -1
- flwr/common/message.py +1 -1
- flwr/common/retry_invoker.py +13 -5
- flwr/common/secure_aggregation/ndarrays_arithmetic.py +5 -2
- flwr/common/serde.py +13 -5
- flwr/common/telemetry.py +1 -1
- flwr/common/typing.py +10 -3
- 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/proto/clientappio_pb2.py +2 -2
- flwr/proto/clientappio_pb2_grpc.py +104 -88
- flwr/proto/clientappio_pb2_grpc.pyi +140 -80
- flwr/proto/federation_pb2.py +5 -3
- flwr/proto/federation_pb2.pyi +32 -2
- flwr/proto/fleet_pb2.py +10 -10
- flwr/proto/fleet_pb2.pyi +5 -1
- flwr/proto/run_pb2.py +18 -26
- flwr/proto/run_pb2.pyi +10 -58
- flwr/proto/serverappio_pb2.py +2 -2
- flwr/proto/serverappio_pb2_grpc.py +138 -207
- flwr/proto/serverappio_pb2_grpc.pyi +189 -155
- flwr/proto/simulationio_pb2.py +2 -2
- flwr/proto/simulationio_pb2_grpc.py +62 -90
- flwr/proto/simulationio_pb2_grpc.pyi +95 -55
- flwr/server/app.py +7 -13
- flwr/server/compat/grid_client_proxy.py +2 -1
- flwr/server/grid/grpc_grid.py +5 -5
- flwr/server/serverapp/app.py +11 -4
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -1
- flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +13 -12
- flwr/server/superlink/fleet/message_handler/message_handler.py +42 -2
- flwr/server/superlink/linkstate/__init__.py +2 -2
- flwr/server/superlink/linkstate/in_memory_linkstate.py +36 -10
- flwr/server/superlink/linkstate/linkstate.py +34 -21
- flwr/server/superlink/linkstate/linkstate_factory.py +16 -8
- flwr/server/superlink/linkstate/{sqlite_linkstate.py → sql_linkstate.py} +471 -516
- flwr/server/superlink/linkstate/utils.py +49 -2
- flwr/server/superlink/serverappio/serverappio_servicer.py +1 -33
- flwr/server/superlink/simulation/simulationio_servicer.py +0 -19
- flwr/server/utils/validator.py +1 -1
- flwr/server/workflow/default_workflows.py +2 -1
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
- flwr/serverapp/strategy/bulyan.py +7 -1
- flwr/serverapp/strategy/dp_fixed_clipping.py +9 -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 +3 -12
- flwr/simulation/simulationio_connection.py +3 -3
- flwr/{common → supercore}/address.py +7 -33
- flwr/supercore/app_utils.py +2 -1
- flwr/supercore/constant.py +27 -2
- flwr/supercore/corestate/{sqlite_corestate.py → sql_corestate.py} +19 -23
- 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/{common → supercore}/date.py +0 -11
- flwr/supercore/ffs/disk_ffs.py +1 -1
- flwr/supercore/object_store/object_store_factory.py +14 -6
- flwr/supercore/object_store/{sqlite_object_store.py → sql_object_store.py} +115 -117
- flwr/supercore/sql_mixin.py +315 -0
- flwr/{cli/new/templates → supercore/state}/__init__.py +2 -2
- flwr/{cli/new/templates/app/code/flwr_tune → supercore/state/alembic}/__init__.py +2 -2
- flwr/supercore/state/alembic/env.py +103 -0
- flwr/supercore/state/alembic/script.py.mako +43 -0
- flwr/supercore/state/alembic/utils.py +239 -0
- flwr/{cli/new/templates/app → supercore/state/alembic/versions}/__init__.py +2 -2
- flwr/supercore/state/alembic/versions/rev_2026_01_28_initialize_migration_of_state_tables.py +200 -0
- flwr/supercore/state/schema/README.md +121 -0
- flwr/{cli/new/templates/app/code → supercore/state/schema}/__init__.py +2 -2
- 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/superexec/run_superexec.py +2 -2
- flwr/supercore/utils.py +225 -0
- flwr/superlink/federation/federation_manager.py +2 -2
- flwr/superlink/federation/noop_federation_manager.py +8 -6
- flwr/superlink/servicer/control/control_grpc.py +2 -0
- flwr/superlink/servicer/control/control_servicer.py +106 -21
- flwr/supernode/cli/flower_supernode.py +2 -1
- flwr/supernode/nodestate/in_memory_nodestate.py +62 -1
- flwr/supernode/nodestate/nodestate.py +45 -0
- flwr/supernode/runtime/run_clientapp.py +14 -14
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +13 -5
- flwr/supernode/start_client_internal.py +17 -10
- {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/METADATA +8 -8
- {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/RECORD +144 -184
- flwr/cli/federation/show.py +0 -317
- flwr/cli/new/templates/app/.gitignore.tpl +0 -163
- flwr/cli/new/templates/app/LICENSE.tpl +0 -202
- flwr/cli/new/templates/app/README.baseline.md.tpl +0 -127
- flwr/cli/new/templates/app/README.flowertune.md.tpl +0 -68
- flwr/cli/new/templates/app/README.md.tpl +0 -37
- flwr/cli/new/templates/app/code/__init__.baseline.py.tpl +0 -1
- flwr/cli/new/templates/app/code/__init__.py.tpl +0 -1
- flwr/cli/new/templates/app/code/__init__.pytorch_legacy_api.py.tpl +0 -1
- flwr/cli/new/templates/app/code/client.baseline.py.tpl +0 -75
- flwr/cli/new/templates/app/code/client.huggingface.py.tpl +0 -93
- flwr/cli/new/templates/app/code/client.jax.py.tpl +0 -71
- flwr/cli/new/templates/app/code/client.mlx.py.tpl +0 -102
- flwr/cli/new/templates/app/code/client.numpy.py.tpl +0 -46
- flwr/cli/new/templates/app/code/client.pytorch.py.tpl +0 -80
- flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +0 -55
- flwr/cli/new/templates/app/code/client.sklearn.py.tpl +0 -108
- flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +0 -82
- flwr/cli/new/templates/app/code/client.xgboost.py.tpl +0 -110
- flwr/cli/new/templates/app/code/dataset.baseline.py.tpl +0 -36
- flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +0 -92
- flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +0 -87
- flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +0 -56
- flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +0 -73
- flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +0 -78
- flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -66
- flwr/cli/new/templates/app/code/server.baseline.py.tpl +0 -43
- flwr/cli/new/templates/app/code/server.huggingface.py.tpl +0 -42
- flwr/cli/new/templates/app/code/server.jax.py.tpl +0 -39
- flwr/cli/new/templates/app/code/server.mlx.py.tpl +0 -41
- flwr/cli/new/templates/app/code/server.numpy.py.tpl +0 -38
- flwr/cli/new/templates/app/code/server.pytorch.py.tpl +0 -41
- flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +0 -31
- flwr/cli/new/templates/app/code/server.sklearn.py.tpl +0 -44
- flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +0 -38
- flwr/cli/new/templates/app/code/server.xgboost.py.tpl +0 -56
- flwr/cli/new/templates/app/code/strategy.baseline.py.tpl +0 -1
- flwr/cli/new/templates/app/code/task.huggingface.py.tpl +0 -98
- flwr/cli/new/templates/app/code/task.jax.py.tpl +0 -57
- flwr/cli/new/templates/app/code/task.mlx.py.tpl +0 -102
- flwr/cli/new/templates/app/code/task.numpy.py.tpl +0 -7
- flwr/cli/new/templates/app/code/task.pytorch.py.tpl +0 -99
- flwr/cli/new/templates/app/code/task.pytorch_legacy_api.py.tpl +0 -111
- flwr/cli/new/templates/app/code/task.sklearn.py.tpl +0 -67
- flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +0 -52
- flwr/cli/new/templates/app/code/task.xgboost.py.tpl +0 -67
- flwr/cli/new/templates/app/code/utils.baseline.py.tpl +0 -1
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +0 -146
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +0 -80
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +0 -65
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +0 -52
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +0 -56
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +0 -49
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +0 -53
- flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +0 -53
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +0 -52
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +0 -53
- flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +0 -61
- flwr/common/pyproject.py +0 -42
- flwr/supercore/sqlite_mixin.py +0 -159
- /flwr/{common → supercore}/version.py +0 -0
- {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/WHEEL +0 -0
- {flwr-1.24.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.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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,
|
|
49
|
+
def __init__(self, host: str):
|
|
50
50
|
self.access_token: str | None = None
|
|
51
51
|
self.refresh_token: str | None = None
|
|
52
|
-
self.
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
""
|
|
110
|
-
self.
|
|
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
|
-
|
|
119
|
-
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
129
|
-
self.access_token =
|
|
130
|
-
self.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
|
-
|
|
138
|
-
"
|
|
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
|
|
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
|
-
|
|
111
|
-
f"
|
|
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
|
-
|
|
120
|
-
f"
|
|
121
|
-
"a valid
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
"
|
|
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.
|
|
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
|
+
)
|