flwr-nightly 1.26.0.dev20260122__py3-none-any.whl → 1.26.0.dev20260126__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/cli/app_cmd/publish.py +18 -44
- flwr/cli/app_cmd/review.py +8 -25
- flwr/cli/auth_plugin/oidc_cli_plugin.py +3 -6
- flwr/cli/build.py +8 -19
- flwr/cli/config/ls.py +8 -13
- flwr/cli/config_utils.py +19 -171
- flwr/cli/federation/ls.py +3 -7
- flwr/cli/flower_config.py +28 -47
- flwr/cli/install.py +18 -57
- flwr/cli/log.py +2 -2
- flwr/cli/login/login.py +8 -21
- flwr/cli/ls.py +3 -7
- flwr/cli/new/new.py +9 -29
- flwr/cli/pull.py +3 -7
- flwr/cli/run/run.py +6 -15
- flwr/cli/stop.py +5 -17
- flwr/cli/supernode/register.py +6 -22
- flwr/cli/supernode/unregister.py +3 -13
- flwr/cli/utils.py +66 -169
- flwr/common/config.py +5 -9
- flwr/common/constant.py +2 -0
- flwr/server/superlink/fleet/message_handler/message_handler.py +4 -4
- flwr/server/superlink/linkstate/__init__.py +0 -2
- flwr/server/superlink/linkstate/sql_linkstate.py +38 -10
- flwr/supercore/object_store/object_store_factory.py +4 -4
- flwr/supercore/object_store/sql_object_store.py +171 -6
- flwr/superlink/servicer/control/control_servicer.py +11 -12
- {flwr_nightly-1.26.0.dev20260122.dist-info → flwr_nightly-1.26.0.dev20260126.dist-info}/METADATA +2 -2
- {flwr_nightly-1.26.0.dev20260122.dist-info → flwr_nightly-1.26.0.dev20260126.dist-info}/RECORD +31 -35
- flwr/server/superlink/linkstate/sqlite_linkstate.py +0 -1302
- flwr/supercore/corestate/sqlite_corestate.py +0 -157
- flwr/supercore/object_store/sqlite_object_store.py +0 -253
- flwr/supercore/sqlite_mixin.py +0 -156
- {flwr_nightly-1.26.0.dev20260122.dist-info → flwr_nightly-1.26.0.dev20260126.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.26.0.dev20260122.dist-info → flwr_nightly-1.26.0.dev20260126.dist-info}/entry_points.txt +0 -0
flwr/cli/app_cmd/publish.py
CHANGED
|
@@ -19,6 +19,7 @@ from contextlib import ExitStack
|
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
from typing import IO, Annotated
|
|
21
21
|
|
|
22
|
+
import click
|
|
22
23
|
import requests
|
|
23
24
|
import typer
|
|
24
25
|
from requests import Response
|
|
@@ -62,12 +63,7 @@ def publish(
|
|
|
62
63
|
auth_plugin = load_cli_auth_plugin_from_connection(SUPERGRID_ADDRESS)
|
|
63
64
|
auth_plugin.load_tokens()
|
|
64
65
|
if not isinstance(auth_plugin, OidcCliPlugin) or not auth_plugin.access_token:
|
|
65
|
-
|
|
66
|
-
"❌ Please log in before publishing app.",
|
|
67
|
-
fg=typer.colors.RED,
|
|
68
|
-
err=True,
|
|
69
|
-
)
|
|
70
|
-
raise typer.Exit(code=1)
|
|
66
|
+
raise click.ClickException("Please log in before publishing app.")
|
|
71
67
|
|
|
72
68
|
# Load token from the plugin
|
|
73
69
|
token = auth_plugin.access_token
|
|
@@ -82,19 +78,17 @@ def publish(
|
|
|
82
78
|
try:
|
|
83
79
|
resp = _post_files(files_param, token)
|
|
84
80
|
except requests.RequestException as err:
|
|
85
|
-
|
|
86
|
-
raise typer.Exit(code=1) from err
|
|
81
|
+
raise click.ClickException(f"Network error: {err}") from err
|
|
87
82
|
|
|
88
83
|
if resp.ok:
|
|
89
84
|
typer.secho("🎊 Upload successful", fg=typer.colors.GREEN, bold=True)
|
|
90
85
|
return # success
|
|
91
86
|
|
|
92
87
|
# Error path:
|
|
93
|
-
msg = f"
|
|
88
|
+
msg = f"Upload failed with status {resp.status_code}"
|
|
94
89
|
if resp.text:
|
|
95
90
|
msg += f": {resp.text}"
|
|
96
|
-
|
|
97
|
-
raise typer.Exit(code=1)
|
|
91
|
+
raise click.ClickException(msg)
|
|
98
92
|
|
|
99
93
|
|
|
100
94
|
def _depth_of(relative_path_to_root: Path) -> int:
|
|
@@ -138,14 +132,9 @@ def _collect_file_paths(root: Path) -> list[Path]:
|
|
|
138
132
|
|
|
139
133
|
# Check max depth
|
|
140
134
|
if _depth_of(relative_path) > MAX_DIR_DEPTH:
|
|
141
|
-
|
|
142
|
-
f"
|
|
143
|
-
f"exceeds the maximum directory depth "
|
|
144
|
-
f"of {MAX_DIR_DEPTH}.",
|
|
145
|
-
fg=typer.colors.RED,
|
|
146
|
-
err=True,
|
|
135
|
+
raise click.ClickException(
|
|
136
|
+
f"'{path}' exceeds the maximum directory depth of {MAX_DIR_DEPTH}."
|
|
147
137
|
)
|
|
148
|
-
raise typer.Exit(code=2)
|
|
149
138
|
|
|
150
139
|
file_paths.append(path)
|
|
151
140
|
|
|
@@ -160,21 +149,15 @@ def _validate_files(file_paths: list[Path]) -> None:
|
|
|
160
149
|
Checks file count, individual file size, total size, and UTF-8 encoding.
|
|
161
150
|
"""
|
|
162
151
|
if len(file_paths) == 0:
|
|
163
|
-
|
|
152
|
+
raise click.ClickException(
|
|
164
153
|
"Nothing to upload: no files matched after applying .gitignore and "
|
|
165
|
-
"allowed extensions."
|
|
166
|
-
fg=typer.colors.RED,
|
|
167
|
-
err=True,
|
|
154
|
+
"allowed extensions."
|
|
168
155
|
)
|
|
169
|
-
raise typer.Exit(code=2)
|
|
170
156
|
|
|
171
157
|
if len(file_paths) > MAX_FILE_COUNT:
|
|
172
|
-
|
|
173
|
-
f"Too many files: {len(file_paths)} > allowed maximum of {MAX_FILE_COUNT}."
|
|
174
|
-
fg=typer.colors.RED,
|
|
175
|
-
err=True,
|
|
158
|
+
raise click.ClickException(
|
|
159
|
+
f"Too many files: {len(file_paths)} > allowed maximum of {MAX_FILE_COUNT}."
|
|
176
160
|
)
|
|
177
|
-
raise typer.Exit(code=2)
|
|
178
161
|
|
|
179
162
|
# Calculate files size
|
|
180
163
|
total_size = 0
|
|
@@ -184,34 +167,25 @@ def _validate_files(file_paths: list[Path]) -> None:
|
|
|
184
167
|
|
|
185
168
|
# Check single file size
|
|
186
169
|
if file_size > MAX_FILE_BYTES:
|
|
187
|
-
|
|
170
|
+
raise click.ClickException(
|
|
188
171
|
f"File too large: '{path.as_posix()}' is {file_size:,} bytes, "
|
|
189
|
-
f"exceeding the per-file limit of {MAX_FILE_BYTES:,} bytes."
|
|
190
|
-
fg=typer.colors.RED,
|
|
191
|
-
err=True,
|
|
172
|
+
f"exceeding the per-file limit of {MAX_FILE_BYTES:,} bytes."
|
|
192
173
|
)
|
|
193
|
-
raise typer.Exit(code=2)
|
|
194
174
|
|
|
195
175
|
# Ensure we can decode as UTF-8.
|
|
196
176
|
try:
|
|
197
177
|
path.read_text(encoding=UTF8)
|
|
198
178
|
except UnicodeDecodeError as err:
|
|
199
|
-
|
|
200
|
-
f"Encoding error: '{path}' is not UTF-8 encoded."
|
|
201
|
-
|
|
202
|
-
err=True,
|
|
203
|
-
)
|
|
204
|
-
raise typer.Exit(code=2) from err
|
|
179
|
+
raise click.ClickException(
|
|
180
|
+
f"Encoding error: '{path}' is not UTF-8 encoded."
|
|
181
|
+
) from err
|
|
205
182
|
|
|
206
183
|
# Check total files size
|
|
207
184
|
if total_size > MAX_TOTAL_BYTES:
|
|
208
|
-
|
|
185
|
+
raise click.ClickException(
|
|
209
186
|
"Total size of all files is too large: "
|
|
210
|
-
f"{total_size:,} bytes > {MAX_TOTAL_BYTES:,} bytes."
|
|
211
|
-
fg=typer.colors.RED,
|
|
212
|
-
err=True,
|
|
187
|
+
f"{total_size:,} bytes > {MAX_TOTAL_BYTES:,} bytes."
|
|
213
188
|
)
|
|
214
|
-
raise typer.Exit(code=2)
|
|
215
189
|
|
|
216
190
|
# Print validation passed prompt
|
|
217
191
|
typer.echo(typer.style("✅ Validation passed", fg=typer.colors.GREEN, bold=True))
|
flwr/cli/app_cmd/review.py
CHANGED
|
@@ -21,6 +21,7 @@ import re
|
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
from typing import Annotated
|
|
23
23
|
|
|
24
|
+
import click
|
|
24
25
|
import requests
|
|
25
26
|
import typer
|
|
26
27
|
from cryptography.exceptions import UnsupportedAlgorithm
|
|
@@ -60,12 +61,7 @@ def review(
|
|
|
60
61
|
|
|
61
62
|
auth_plugin.load_tokens()
|
|
62
63
|
if not isinstance(auth_plugin, OidcCliPlugin) or not auth_plugin.access_token:
|
|
63
|
-
|
|
64
|
-
"❌ Please log in before reviewing app.",
|
|
65
|
-
fg=typer.colors.RED,
|
|
66
|
-
err=True,
|
|
67
|
-
)
|
|
68
|
-
raise typer.Exit(code=1)
|
|
64
|
+
raise click.ClickException("Please log in before reviewing app.")
|
|
69
65
|
|
|
70
66
|
# Load token from the plugin
|
|
71
67
|
token = auth_plugin.access_token
|
|
@@ -74,8 +70,7 @@ def review(
|
|
|
74
70
|
try:
|
|
75
71
|
app_id, app_version = parse_app_spec(app_spec)
|
|
76
72
|
except ValueError as e:
|
|
77
|
-
|
|
78
|
-
raise typer.Exit(code=1) from e
|
|
73
|
+
raise click.ClickException(str(e)) from e
|
|
79
74
|
|
|
80
75
|
# Download FAB
|
|
81
76
|
typer.secho("Downloading FAB... ", fg=typer.colors.BLUE)
|
|
@@ -83,8 +78,7 @@ def review(
|
|
|
83
78
|
try:
|
|
84
79
|
presigned_url, _ = request_download_link(app_id, app_version, url, "fab_url")
|
|
85
80
|
except ValueError as e:
|
|
86
|
-
|
|
87
|
-
raise typer.Exit(code=1) from e
|
|
81
|
+
raise click.ClickException(str(e)) from e
|
|
88
82
|
|
|
89
83
|
fab_bytes = _download_fab(presigned_url)
|
|
90
84
|
|
|
@@ -170,12 +164,7 @@ def _download_fab(url: str) -> bytes:
|
|
|
170
164
|
r = requests.get(url, timeout=60)
|
|
171
165
|
r.raise_for_status()
|
|
172
166
|
except requests.RequestException as e:
|
|
173
|
-
|
|
174
|
-
f"❌ FAB download failed: {e}",
|
|
175
|
-
fg=typer.colors.RED,
|
|
176
|
-
err=True,
|
|
177
|
-
)
|
|
178
|
-
raise typer.Exit(code=1) from e
|
|
167
|
+
raise click.ClickException(f"FAB download failed: {e}") from e
|
|
179
168
|
return r.content
|
|
180
169
|
|
|
181
170
|
|
|
@@ -209,20 +198,14 @@ def _submit_review(
|
|
|
209
198
|
try:
|
|
210
199
|
resp = requests.post(url, headers=headers, json=payload, timeout=120)
|
|
211
200
|
except requests.RequestException as e:
|
|
212
|
-
|
|
213
|
-
f"❌ Network error while submitting review: {e}",
|
|
214
|
-
fg=typer.colors.RED,
|
|
215
|
-
err=True,
|
|
216
|
-
)
|
|
217
|
-
raise typer.Exit(code=1) from e
|
|
201
|
+
raise click.ClickException(f"Network error while submitting review: {e}") from e
|
|
218
202
|
|
|
219
203
|
if resp.ok:
|
|
220
204
|
typer.secho("🎊 Review submitted", fg=typer.colors.GREEN, bold=True)
|
|
221
205
|
return
|
|
222
206
|
|
|
223
207
|
# Error path:
|
|
224
|
-
msg = f"
|
|
208
|
+
msg = f"Review submission failed (HTTP {resp.status_code})"
|
|
225
209
|
if resp.text:
|
|
226
210
|
msg += f": {resp.text}"
|
|
227
|
-
|
|
228
|
-
raise typer.Exit(code=1)
|
|
211
|
+
raise click.ClickException(msg)
|
|
@@ -19,6 +19,7 @@ import time
|
|
|
19
19
|
import webbrowser
|
|
20
20
|
from collections.abc import Sequence
|
|
21
21
|
|
|
22
|
+
import click
|
|
22
23
|
import typer
|
|
23
24
|
|
|
24
25
|
from flwr.cli.constant import (
|
|
@@ -137,13 +138,9 @@ class OidcCliPlugin(CliAuthPlugin):
|
|
|
137
138
|
) -> Sequence[tuple[str, str | bytes]]:
|
|
138
139
|
"""Write authentication tokens to the provided metadata."""
|
|
139
140
|
if self.access_token is None or self.refresh_token is None:
|
|
140
|
-
|
|
141
|
-
"
|
|
142
|
-
fg=typer.colors.RED,
|
|
143
|
-
bold=True,
|
|
144
|
-
err=True,
|
|
141
|
+
raise click.ClickException(
|
|
142
|
+
"Missing authentication tokens. Please login first."
|
|
145
143
|
)
|
|
146
|
-
raise typer.Exit(code=1)
|
|
147
144
|
|
|
148
145
|
return list(metadata) + [
|
|
149
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
|
|
@@ -107,35 +108,23 @@ def build(
|
|
|
107
108
|
|
|
108
109
|
app = app.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"
|
|
116
|
+
raise click.ClickException(
|
|
117
|
+
f"The project name {app.name} is invalid, "
|
|
121
118
|
"a valid project name must start with a letter, "
|
|
122
|
-
"and can only contain letters, digits, and hyphens."
|
|
123
|
-
fg=typer.colors.RED,
|
|
124
|
-
bold=True,
|
|
125
|
-
err=True,
|
|
119
|
+
"and can only contain letters, digits, and hyphens."
|
|
126
120
|
)
|
|
127
|
-
raise typer.Exit(code=1)
|
|
128
121
|
|
|
129
122
|
config, errors, warnings = load_and_validate(app / "pyproject.toml")
|
|
130
123
|
if config is None:
|
|
131
|
-
|
|
124
|
+
raise click.ClickException(
|
|
132
125
|
"Project configuration could not be loaded.\npyproject.toml is invalid:\n"
|
|
133
|
-
+ "\n".join([f"- {line}" for line in errors])
|
|
134
|
-
fg=typer.colors.RED,
|
|
135
|
-
bold=True,
|
|
136
|
-
err=True,
|
|
126
|
+
+ "\n".join([f"- {line}" for line in errors])
|
|
137
127
|
)
|
|
138
|
-
raise typer.Exit(code=1)
|
|
139
128
|
|
|
140
129
|
if warnings:
|
|
141
130
|
typer.secho(
|
flwr/cli/config/ls.py
CHANGED
|
@@ -19,6 +19,7 @@ import io
|
|
|
19
19
|
import json
|
|
20
20
|
from typing import Annotated
|
|
21
21
|
|
|
22
|
+
import click
|
|
22
23
|
import typer
|
|
23
24
|
from rich.console import Console
|
|
24
25
|
|
|
@@ -66,6 +67,8 @@ def ls(
|
|
|
66
67
|
}
|
|
67
68
|
Console().print_json(json.dumps(conn))
|
|
68
69
|
else:
|
|
70
|
+
typer.secho("Flower Config file: ", fg=typer.colors.BLUE, nl=False)
|
|
71
|
+
typer.secho(f"{config_path}", fg=typer.colors.GREEN)
|
|
69
72
|
typer.secho("SuperLink connections:", fg=typer.colors.BLUE)
|
|
70
73
|
# List SuperLink connections and highlight default
|
|
71
74
|
for k in connection_names:
|
|
@@ -77,26 +80,18 @@ def ls(
|
|
|
77
80
|
nl=False,
|
|
78
81
|
)
|
|
79
82
|
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
83
|
|
|
88
84
|
except Exception as err: # pylint: disable=broad-except
|
|
85
|
+
# log the error if json format requested
|
|
89
86
|
if suppress_output:
|
|
90
87
|
restore_output()
|
|
91
88
|
e_message = captured_output.getvalue()
|
|
92
89
|
print_json_error(e_message, err)
|
|
93
90
|
else:
|
|
94
|
-
|
|
95
|
-
f"
|
|
96
|
-
f"connections in the Flower configuration file ({config_path}): {err}"
|
|
97
|
-
|
|
98
|
-
err=True,
|
|
99
|
-
)
|
|
91
|
+
raise click.ClickException(
|
|
92
|
+
f"An unexpected error occurred while listing the SuperLink "
|
|
93
|
+
f"connections in the Flower configuration file ({config_path}): {err}"
|
|
94
|
+
) from err
|
|
100
95
|
|
|
101
96
|
finally:
|
|
102
97
|
if suppress_output:
|
flwr/cli/config_utils.py
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from typing import Any
|
|
20
20
|
|
|
21
|
+
import click
|
|
21
22
|
import tomli
|
|
22
|
-
import typer
|
|
23
23
|
|
|
24
24
|
from flwr.cli.typing import SuperLinkConnection
|
|
25
25
|
from flwr.common.config import (
|
|
@@ -114,57 +114,6 @@ def load(toml_path: Path) -> dict[str, Any] | None:
|
|
|
114
114
|
return None
|
|
115
115
|
|
|
116
116
|
|
|
117
|
-
def process_loaded_project_config(
|
|
118
|
-
config: dict[str, Any] | None, errors: list[str], warnings: list[str]
|
|
119
|
-
) -> dict[str, Any]:
|
|
120
|
-
"""Process and return the loaded project configuration.
|
|
121
|
-
|
|
122
|
-
This function handles errors and warnings from the `load_and_validate` function,
|
|
123
|
-
exits on critical issues, and returns the validated configuration.
|
|
124
|
-
|
|
125
|
-
Parameters
|
|
126
|
-
----------
|
|
127
|
-
config : dict[str, Any] | None
|
|
128
|
-
The loaded configuration dictionary, or None if loading failed.
|
|
129
|
-
errors : list[str]
|
|
130
|
-
List of error messages from validation.
|
|
131
|
-
warnings : list[str]
|
|
132
|
-
List of warning messages from validation.
|
|
133
|
-
|
|
134
|
-
Returns
|
|
135
|
-
-------
|
|
136
|
-
dict[str, Any]
|
|
137
|
-
The validated configuration dictionary.
|
|
138
|
-
|
|
139
|
-
Raises
|
|
140
|
-
------
|
|
141
|
-
typer.Exit
|
|
142
|
-
If config is None or contains critical errors.
|
|
143
|
-
"""
|
|
144
|
-
if config is None:
|
|
145
|
-
typer.secho(
|
|
146
|
-
"Project configuration could not be loaded.\n"
|
|
147
|
-
"pyproject.toml is invalid:\n"
|
|
148
|
-
+ "\n".join([f"- {line}" for line in errors]),
|
|
149
|
-
fg=typer.colors.RED,
|
|
150
|
-
bold=True,
|
|
151
|
-
err=True,
|
|
152
|
-
)
|
|
153
|
-
raise typer.Exit(code=1)
|
|
154
|
-
|
|
155
|
-
if warnings:
|
|
156
|
-
typer.secho(
|
|
157
|
-
"Project configuration is missing the following "
|
|
158
|
-
"recommended properties:\n" + "\n".join([f"- {line}" for line in warnings]),
|
|
159
|
-
fg=typer.colors.RED,
|
|
160
|
-
bold=True,
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
typer.secho("Success", fg=typer.colors.GREEN)
|
|
164
|
-
|
|
165
|
-
return config
|
|
166
|
-
|
|
167
|
-
|
|
168
117
|
def validate_federation_in_project_config(
|
|
169
118
|
federation: str | None,
|
|
170
119
|
config: dict[str, Any],
|
|
@@ -188,22 +137,18 @@ def validate_federation_in_project_config(
|
|
|
188
137
|
|
|
189
138
|
Raises
|
|
190
139
|
------
|
|
191
|
-
|
|
140
|
+
click.ClickException
|
|
192
141
|
If no federation name provided and no default found, or if federation
|
|
193
142
|
doesn't exist in config.
|
|
194
143
|
"""
|
|
195
144
|
federation = federation or config["tool"]["flwr"]["federations"].get("default")
|
|
196
145
|
|
|
197
146
|
if federation is None:
|
|
198
|
-
|
|
199
|
-
"
|
|
147
|
+
raise click.ClickException(
|
|
148
|
+
"No federation name was provided and the project's `pyproject.toml` "
|
|
200
149
|
"doesn't declare a default federation (with an Control API address or an "
|
|
201
|
-
"`options.num-supernodes` value)."
|
|
202
|
-
fg=typer.colors.RED,
|
|
203
|
-
bold=True,
|
|
204
|
-
err=True,
|
|
150
|
+
"`options.num-supernodes` value)."
|
|
205
151
|
)
|
|
206
|
-
raise typer.Exit(code=1)
|
|
207
152
|
|
|
208
153
|
# Validate the federation exists in the configuration
|
|
209
154
|
federation_config = config["tool"]["flwr"]["federations"].get(federation)
|
|
@@ -211,15 +156,11 @@ def validate_federation_in_project_config(
|
|
|
211
156
|
available_feds = {
|
|
212
157
|
fed for fed in config["tool"]["flwr"]["federations"] if fed != "default"
|
|
213
158
|
}
|
|
214
|
-
|
|
215
|
-
f"
|
|
159
|
+
raise click.ClickException(
|
|
160
|
+
f"There is no `{federation}` federation declared in the "
|
|
216
161
|
"`pyproject.toml`.\n The following federations were found:\n\n"
|
|
217
|
-
+ "\n".join(available_feds)
|
|
218
|
-
fg=typer.colors.RED,
|
|
219
|
-
bold=True,
|
|
220
|
-
err=True,
|
|
162
|
+
+ "\n".join(available_feds)
|
|
221
163
|
)
|
|
222
|
-
raise typer.Exit(code=1)
|
|
223
164
|
|
|
224
165
|
# Override the federation configuration if provided
|
|
225
166
|
if overrides:
|
|
@@ -229,61 +170,6 @@ def validate_federation_in_project_config(
|
|
|
229
170
|
return federation, federation_config
|
|
230
171
|
|
|
231
172
|
|
|
232
|
-
def validate_certificate_in_federation_config(
|
|
233
|
-
app: Path, federation_config: dict[str, Any]
|
|
234
|
-
) -> tuple[bool, bytes | None]:
|
|
235
|
-
"""Validate the certificates in the Flower project configuration.
|
|
236
|
-
|
|
237
|
-
Accepted configurations:
|
|
238
|
-
1. TLS enabled and gRPC will load(*) the trusted certificate bundle:
|
|
239
|
-
- Only `address` is provided. `root-certificates` and `insecure` not set.
|
|
240
|
-
- `address` is provided and `insecure` set to `false`. `root-certificates` not
|
|
241
|
-
set.
|
|
242
|
-
(*)gRPC uses a multi-step fallback mechanism to load the trusted certificate
|
|
243
|
-
bundle in the following sequence:
|
|
244
|
-
a. A configured file path (if set via configuration or environment),
|
|
245
|
-
b. An override callback (if registered via
|
|
246
|
-
`grpc_set_ssl_roots_override_callback`),
|
|
247
|
-
c. The OS trust store (if available),
|
|
248
|
-
d. A bundled default certificate file.
|
|
249
|
-
2. TLS enabled with self-signed certificates:
|
|
250
|
-
- `address` and `root-certificates` are provided. `insecure` not set.
|
|
251
|
-
- `address` and `root-certificates` are provided. `insecure` set to `false`.
|
|
252
|
-
3. TLS disabled. This is not recommended and should only be used for prototyping:
|
|
253
|
-
- `address` is provided and `insecure = true`. If `root-certificates` is
|
|
254
|
-
set, exit with an error.
|
|
255
|
-
"""
|
|
256
|
-
insecure = get_insecure_flag(federation_config)
|
|
257
|
-
|
|
258
|
-
# Process root certificates
|
|
259
|
-
if root_certificates := federation_config.get("root-certificates"):
|
|
260
|
-
if insecure:
|
|
261
|
-
typer.secho(
|
|
262
|
-
"❌ `root-certificates` were provided but the `insecure` parameter "
|
|
263
|
-
"is set to `True`.",
|
|
264
|
-
fg=typer.colors.RED,
|
|
265
|
-
bold=True,
|
|
266
|
-
err=True,
|
|
267
|
-
)
|
|
268
|
-
raise typer.Exit(code=1)
|
|
269
|
-
|
|
270
|
-
# TLS is enabled with self-signed certificates: attempt to read the file
|
|
271
|
-
try:
|
|
272
|
-
root_certificates_bytes = (app / root_certificates).read_bytes()
|
|
273
|
-
except Exception as e:
|
|
274
|
-
typer.secho(
|
|
275
|
-
f"❌ Failed to read certificate file `{root_certificates}`: {e}",
|
|
276
|
-
fg=typer.colors.RED,
|
|
277
|
-
bold=True,
|
|
278
|
-
err=True,
|
|
279
|
-
)
|
|
280
|
-
raise typer.Exit(code=1) from e
|
|
281
|
-
else:
|
|
282
|
-
root_certificates_bytes = None
|
|
283
|
-
|
|
284
|
-
return insecure, root_certificates_bytes
|
|
285
|
-
|
|
286
|
-
|
|
287
173
|
def load_certificate_in_connection(
|
|
288
174
|
connection: SuperLinkConnection,
|
|
289
175
|
) -> bytes | None:
|
|
@@ -304,64 +190,30 @@ def load_certificate_in_connection(
|
|
|
304
190
|
------
|
|
305
191
|
ValueError
|
|
306
192
|
If required TLS settings are missing.
|
|
307
|
-
|
|
193
|
+
click.ClickException
|
|
308
194
|
If the configuration is invalid or the certificate file cannot be read.
|
|
309
195
|
"""
|
|
310
196
|
# Process root certificates
|
|
311
197
|
if root_certificates := connection.root_certificates:
|
|
312
198
|
if connection.insecure:
|
|
313
|
-
|
|
314
|
-
"
|
|
315
|
-
"is set to `True`."
|
|
316
|
-
fg=typer.colors.RED,
|
|
317
|
-
bold=True,
|
|
318
|
-
err=True,
|
|
199
|
+
raise click.ClickException(
|
|
200
|
+
"`root-certificates` were provided but the `insecure` parameter "
|
|
201
|
+
"is set to `True`."
|
|
319
202
|
)
|
|
320
|
-
raise typer.Exit(code=1)
|
|
321
203
|
|
|
322
204
|
# TLS is enabled with self-signed certificates: attempt to read the file
|
|
323
205
|
try:
|
|
324
206
|
root_certificates_bytes = Path(root_certificates).read_bytes()
|
|
325
207
|
except Exception as e:
|
|
326
|
-
|
|
327
|
-
f"
|
|
328
|
-
|
|
329
|
-
bold=True,
|
|
330
|
-
err=True,
|
|
331
|
-
)
|
|
332
|
-
raise typer.Exit(code=1) from e
|
|
208
|
+
raise click.ClickException(
|
|
209
|
+
f"Failed to read certificate file `{root_certificates}`: {e}"
|
|
210
|
+
) from e
|
|
333
211
|
else:
|
|
334
212
|
root_certificates_bytes = None
|
|
335
213
|
|
|
336
214
|
return root_certificates_bytes
|
|
337
215
|
|
|
338
216
|
|
|
339
|
-
def exit_if_no_address(federation_config: dict[str, Any], cmd: str) -> None:
|
|
340
|
-
"""Exit if the provided federation_config has no "address" key.
|
|
341
|
-
|
|
342
|
-
Parameters
|
|
343
|
-
----------
|
|
344
|
-
federation_config : dict[str, Any]
|
|
345
|
-
The federation configuration dictionary to check.
|
|
346
|
-
cmd : str
|
|
347
|
-
The command name to display in the error message.
|
|
348
|
-
|
|
349
|
-
Raises
|
|
350
|
-
------
|
|
351
|
-
typer.Exit
|
|
352
|
-
If 'address' key is not present in federation_config.
|
|
353
|
-
"""
|
|
354
|
-
if "address" not in federation_config:
|
|
355
|
-
typer.secho(
|
|
356
|
-
f"❌ `flwr {cmd}` currently works with a SuperLink. Ensure that the "
|
|
357
|
-
"correct SuperLink (Control API) address is provided in `pyproject.toml`.",
|
|
358
|
-
fg=typer.colors.RED,
|
|
359
|
-
bold=True,
|
|
360
|
-
err=True,
|
|
361
|
-
)
|
|
362
|
-
raise typer.Exit(code=1)
|
|
363
|
-
|
|
364
|
-
|
|
365
217
|
def get_insecure_flag(federation_config: dict[str, Any]) -> bool:
|
|
366
218
|
"""Extract and validate the `insecure` flag from the federation configuration.
|
|
367
219
|
|
|
@@ -377,7 +229,7 @@ def get_insecure_flag(federation_config: dict[str, Any]) -> bool:
|
|
|
377
229
|
|
|
378
230
|
Raises
|
|
379
231
|
------
|
|
380
|
-
|
|
232
|
+
click.ClickException
|
|
381
233
|
If insecure value is not a boolean type.
|
|
382
234
|
"""
|
|
383
235
|
insecure_value = federation_config.get("insecure")
|
|
@@ -387,11 +239,7 @@ def get_insecure_flag(federation_config: dict[str, Any]) -> bool:
|
|
|
387
239
|
return False
|
|
388
240
|
if isinstance(insecure_value, bool):
|
|
389
241
|
return insecure_value
|
|
390
|
-
|
|
391
|
-
"
|
|
392
|
-
"(`insecure = true` or `insecure = false`)"
|
|
393
|
-
fg=typer.colors.RED,
|
|
394
|
-
bold=True,
|
|
395
|
-
err=True,
|
|
242
|
+
raise click.ClickException(
|
|
243
|
+
"Invalid type for `insecure`: expected a boolean if provided. "
|
|
244
|
+
"(`insecure = true` or `insecure = false`)"
|
|
396
245
|
)
|
|
397
|
-
raise typer.Exit(code=1)
|
flwr/cli/federation/ls.py
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import io
|
|
19
19
|
from typing import Annotated, Any
|
|
20
20
|
|
|
21
|
+
import click
|
|
21
22
|
import typer
|
|
22
23
|
from rich.console import Console
|
|
23
24
|
from rich.table import Table
|
|
@@ -112,18 +113,13 @@ def ls( # pylint: disable=R0914, R0913, R0917, R0912
|
|
|
112
113
|
finally:
|
|
113
114
|
if channel:
|
|
114
115
|
channel.close()
|
|
115
|
-
except
|
|
116
|
+
except Exception as err: # pylint: disable=broad-except
|
|
116
117
|
if suppress_output:
|
|
117
118
|
restore_output()
|
|
118
119
|
e_message = captured_output.getvalue()
|
|
119
120
|
print_json_error(e_message, err)
|
|
120
121
|
else:
|
|
121
|
-
|
|
122
|
-
f"{err}",
|
|
123
|
-
fg=typer.colors.RED,
|
|
124
|
-
bold=True,
|
|
125
|
-
err=True,
|
|
126
|
-
)
|
|
122
|
+
raise click.ClickException(str(err)) from None
|
|
127
123
|
finally:
|
|
128
124
|
if suppress_output:
|
|
129
125
|
restore_output()
|