flwr 1.25.0__py3-none-any.whl → 1.26.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +18 -69
- 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 +28 -58
- flwr/cli/new/new.py +9 -29
- flwr/cli/pull.py +19 -37
- flwr/cli/run/run.py +85 -93
- flwr/cli/run_utils.py +1 -1
- flwr/cli/stop.py +32 -73
- flwr/cli/supernode/ls.py +25 -57
- 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 -275
- flwr/client/grpc_rere_client/connection.py +3 -3
- 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/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 +7 -5
- flwr/common/telemetry.py +1 -1
- flwr/common/typing.py +4 -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/run_pb2.py +5 -13
- flwr/proto/run_pb2.pyi +0 -57
- 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 +6 -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 +6 -5
- flwr/server/superlink/linkstate/__init__.py +2 -2
- flwr/server/superlink/linkstate/in_memory_linkstate.py +2 -10
- flwr/server/superlink/linkstate/linkstate.py +2 -21
- flwr/server/superlink/linkstate/linkstate_factory.py +16 -8
- flwr/server/superlink/linkstate/{sqlite_linkstate.py → sql_linkstate.py} +432 -534
- 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 +24 -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/supercore/state/__init__.py +15 -0
- flwr/supercore/state/alembic/__init__.py +15 -0
- 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/supercore/state/alembic/versions/__init__.py +15 -0
- 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/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/superexec/run_superexec.py +2 -2
- flwr/supercore/utils.py +36 -1
- flwr/superlink/federation/federation_manager.py +2 -2
- flwr/superlink/federation/noop_federation_manager.py +8 -6
- flwr/superlink/servicer/control/control_servicer.py +19 -17
- flwr/supernode/cli/flower_supernode.py +2 -1
- flwr/supernode/runtime/run_clientapp.py +14 -14
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +10 -8
- flwr/supernode/start_client_internal.py +10 -6
- {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/METADATA +7 -5
- {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/RECORD +137 -116
- flwr/cli/federation/show.py +0 -318
- flwr/common/pyproject.py +0 -42
- flwr/supercore/sqlite_mixin.py +0 -159
- /flwr/{common → supercore}/version.py +0 -0
- {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/WHEEL +0 -0
- {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/entry_points.txt +0 -0
flwr/cli/utils.py
CHANGED
|
@@ -18,20 +18,25 @@
|
|
|
18
18
|
import hashlib
|
|
19
19
|
import json
|
|
20
20
|
import re
|
|
21
|
+
import sys
|
|
21
22
|
from collections.abc import Callable, Iterable, Iterator
|
|
22
23
|
from contextlib import contextmanager
|
|
24
|
+
from io import StringIO
|
|
23
25
|
from pathlib import Path
|
|
24
26
|
from typing import Any, cast
|
|
25
27
|
|
|
28
|
+
import click
|
|
26
29
|
import grpc
|
|
27
30
|
import pathspec
|
|
28
31
|
import typer
|
|
32
|
+
from rich.console import Console
|
|
29
33
|
|
|
34
|
+
from flwr.cli.typing import SuperLinkConnection
|
|
30
35
|
from flwr.common.constant import (
|
|
31
36
|
ACCESS_TOKEN_KEY,
|
|
32
37
|
AUTHN_TYPE_JSON_KEY,
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
FEDERATION_NOT_FOUND_MESSAGE,
|
|
39
|
+
FEDERATION_NOT_SPECIFIED_MESSAGE,
|
|
35
40
|
NO_ACCOUNT_AUTH_MESSAGE,
|
|
36
41
|
NO_ARTIFACT_PROVIDER_MESSAGE,
|
|
37
42
|
NODE_NOT_FOUND_MESSAGE,
|
|
@@ -41,16 +46,67 @@ from flwr.common.constant import (
|
|
|
41
46
|
REFRESH_TOKEN_KEY,
|
|
42
47
|
RUN_ID_NOT_FOUND_MESSAGE,
|
|
43
48
|
AuthnType,
|
|
49
|
+
CliOutputFormat,
|
|
44
50
|
)
|
|
45
51
|
from flwr.common.grpc import (
|
|
46
52
|
GRPC_MAX_MESSAGE_LENGTH,
|
|
47
53
|
create_channel,
|
|
48
54
|
on_channel_state_change,
|
|
49
55
|
)
|
|
56
|
+
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
|
57
|
+
from flwr.supercore.credential_store import get_credential_store
|
|
50
58
|
|
|
51
59
|
from .auth_plugin import CliAuthPlugin, get_cli_plugin_class
|
|
52
60
|
from .cli_account_auth_interceptor import CliAccountAuthInterceptor
|
|
53
|
-
from .config_utils import
|
|
61
|
+
from .config_utils import load_certificate_in_connection
|
|
62
|
+
from .constant import AUTHN_TYPE_STORE_KEY
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def print_json_to_stdout(data: str | Any) -> None:
|
|
66
|
+
"""Print JSON data to stdout, bypassing any output redirection.
|
|
67
|
+
|
|
68
|
+
Use this function within the `cli_output_handler` context manager to print JSON
|
|
69
|
+
output directly to the terminal, even when stdout is being captured.
|
|
70
|
+
"""
|
|
71
|
+
if isinstance(data, str):
|
|
72
|
+
Console(file=sys.__stdout__).print_json(data)
|
|
73
|
+
else:
|
|
74
|
+
Console(file=sys.__stdout__).print_json(data=data)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@contextmanager # docsig: ignore=SIG503
|
|
78
|
+
def cli_output_handler(
|
|
79
|
+
output_format: str = CliOutputFormat.DEFAULT,
|
|
80
|
+
) -> Iterator[bool]:
|
|
81
|
+
"""Context manager for handling CLI output in different formats.
|
|
82
|
+
|
|
83
|
+
This context manager provides consistent output handling for CLI commands by:
|
|
84
|
+
- Redirecting stdout/stderr when JSON format is requested
|
|
85
|
+
- Catching and handling exceptions appropriately based on the output format
|
|
86
|
+
|
|
87
|
+
Use the `print_json_to_stdout()` utility function to print JSON output that bypasses
|
|
88
|
+
output redirection.
|
|
89
|
+
"""
|
|
90
|
+
is_json = output_format == CliOutputFormat.JSON
|
|
91
|
+
captured_output = StringIO()
|
|
92
|
+
|
|
93
|
+
if is_json:
|
|
94
|
+
redirect_output(captured_output)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
yield is_json
|
|
98
|
+
except Exception as err: # pylint: disable=broad-except
|
|
99
|
+
if is_json:
|
|
100
|
+
restore_output()
|
|
101
|
+
print_json_error(captured_output.getvalue(), err)
|
|
102
|
+
else:
|
|
103
|
+
if isinstance(err, typer.Exit):
|
|
104
|
+
raise # Allow typer.Exit to escape normally
|
|
105
|
+
raise click.ClickException(str(err)) from None
|
|
106
|
+
finally:
|
|
107
|
+
if is_json:
|
|
108
|
+
restore_output()
|
|
109
|
+
captured_output.close()
|
|
54
110
|
|
|
55
111
|
|
|
56
112
|
def prompt_text(
|
|
@@ -155,46 +211,6 @@ def is_valid_project_name(name: str) -> bool:
|
|
|
155
211
|
return True
|
|
156
212
|
|
|
157
213
|
|
|
158
|
-
def sanitize_project_name(name: str) -> str:
|
|
159
|
-
"""Sanitize the given string to make it a valid Python project name.
|
|
160
|
-
|
|
161
|
-
This function replaces spaces, dots, slashes, and underscores with dashes, removes
|
|
162
|
-
any characters not allowed in Python project names, makes the string lowercase, and
|
|
163
|
-
ensures it starts with a valid character.
|
|
164
|
-
|
|
165
|
-
Parameters
|
|
166
|
-
----------
|
|
167
|
-
name : str
|
|
168
|
-
The project name to sanitize.
|
|
169
|
-
|
|
170
|
-
Returns
|
|
171
|
-
-------
|
|
172
|
-
str
|
|
173
|
-
The sanitized project name that is valid for Python projects.
|
|
174
|
-
"""
|
|
175
|
-
# Replace whitespace with '_'
|
|
176
|
-
name_with_hyphens = re.sub(r"[ ./_]", "-", name)
|
|
177
|
-
|
|
178
|
-
# Allowed characters in a module name: letters, digits, underscore
|
|
179
|
-
allowed_chars = set(
|
|
180
|
-
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
# Make the string lowercase
|
|
184
|
-
sanitized_name = name_with_hyphens.lower()
|
|
185
|
-
|
|
186
|
-
# Remove any characters not allowed in Python module names
|
|
187
|
-
sanitized_name = "".join(c for c in sanitized_name if c in allowed_chars)
|
|
188
|
-
|
|
189
|
-
# Ensure the first character is a letter or underscore
|
|
190
|
-
while sanitized_name and (
|
|
191
|
-
sanitized_name[0].isdigit() or sanitized_name[0] not in allowed_chars
|
|
192
|
-
):
|
|
193
|
-
sanitized_name = sanitized_name[1:]
|
|
194
|
-
|
|
195
|
-
return sanitized_name
|
|
196
|
-
|
|
197
|
-
|
|
198
214
|
def get_sha256_hash(file_path_or_int: Path | int) -> str:
|
|
199
215
|
"""Calculate the SHA-256 hash of a file or integer.
|
|
200
216
|
|
|
@@ -221,121 +237,27 @@ def get_sha256_hash(file_path_or_int: Path | int) -> str:
|
|
|
221
237
|
return sha256.hexdigest()
|
|
222
238
|
|
|
223
239
|
|
|
224
|
-
def
|
|
225
|
-
"""
|
|
226
|
-
|
|
227
|
-
Additionally, a `.gitignore` file will be created in the Flower directory to
|
|
228
|
-
include the `.credentials` folder to be excluded from git. If the `.gitignore`
|
|
229
|
-
file already exists, a warning will be displayed if the `.credentials` entry is
|
|
230
|
-
not found.
|
|
231
|
-
"""
|
|
232
|
-
# Locate the credentials directory
|
|
233
|
-
abs_flwr_dir = root_dir.absolute() / FLWR_DIR
|
|
234
|
-
credentials_dir = abs_flwr_dir / CREDENTIALS_DIR
|
|
235
|
-
credentials_dir.mkdir(parents=True, exist_ok=True)
|
|
236
|
-
|
|
237
|
-
# Determine the absolute path of the Flower directory for .gitignore
|
|
238
|
-
gitignore_path = abs_flwr_dir / ".gitignore"
|
|
239
|
-
credential_entry = CREDENTIALS_DIR
|
|
240
|
-
|
|
241
|
-
try:
|
|
242
|
-
if gitignore_path.exists():
|
|
243
|
-
with open(gitignore_path, encoding="utf-8") as gitignore_file:
|
|
244
|
-
lines = gitignore_file.read().splitlines()
|
|
245
|
-
|
|
246
|
-
# Warn if .credentials is not already in .gitignore
|
|
247
|
-
if credential_entry not in lines:
|
|
248
|
-
typer.secho(
|
|
249
|
-
f"`.gitignore` exists, but `{credential_entry}` entry not found. "
|
|
250
|
-
"Consider adding it to your `.gitignore` to exclude Flower "
|
|
251
|
-
"credentials from git.",
|
|
252
|
-
fg=typer.colors.YELLOW,
|
|
253
|
-
bold=True,
|
|
254
|
-
)
|
|
255
|
-
else:
|
|
256
|
-
typer.secho(
|
|
257
|
-
f"Creating a new `.gitignore` with `{credential_entry}` entry...",
|
|
258
|
-
fg=typer.colors.BLUE,
|
|
259
|
-
)
|
|
260
|
-
# Create a new .gitignore with .credentials
|
|
261
|
-
with open(gitignore_path, "w", encoding="utf-8") as gitignore_file:
|
|
262
|
-
gitignore_file.write(f"{credential_entry}\n")
|
|
263
|
-
except Exception as err:
|
|
264
|
-
typer.secho(
|
|
265
|
-
"❌ An error occurred while handling `.gitignore.` "
|
|
266
|
-
f"Please check the permissions of `{gitignore_path}` and try again.",
|
|
267
|
-
fg=typer.colors.RED,
|
|
268
|
-
bold=True,
|
|
269
|
-
err=True,
|
|
270
|
-
)
|
|
271
|
-
raise typer.Exit(code=1) from err
|
|
272
|
-
|
|
273
|
-
return credentials_dir / f"{federation}.json"
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
def account_auth_enabled(federation_config: dict[str, Any]) -> bool:
|
|
277
|
-
"""Check if account authentication is enabled in the federation config.
|
|
278
|
-
|
|
279
|
-
Parameters
|
|
280
|
-
----------
|
|
281
|
-
federation_config : dict[str, Any]
|
|
282
|
-
The federation configuration dictionary.
|
|
283
|
-
|
|
284
|
-
Returns
|
|
285
|
-
-------
|
|
286
|
-
bool
|
|
287
|
-
True if account authentication is enabled, False otherwise.
|
|
288
|
-
"""
|
|
289
|
-
enabled: bool = federation_config.get("enable-user-auth", False)
|
|
290
|
-
enabled |= federation_config.get("enable-account-auth", False)
|
|
291
|
-
if "enable-user-auth" in federation_config:
|
|
292
|
-
typer.secho(
|
|
293
|
-
"`enable-user-auth` is deprecated and will be removed in a future "
|
|
294
|
-
"release. Please use `enable-account-auth` instead.",
|
|
295
|
-
fg=typer.colors.YELLOW,
|
|
296
|
-
bold=True,
|
|
297
|
-
)
|
|
298
|
-
return enabled
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def retrieve_authn_type(config_path: Path) -> str:
|
|
302
|
-
"""Retrieve the auth type from the config file or return NOOP if not found.
|
|
240
|
+
def get_authn_type(host: str) -> str:
|
|
241
|
+
"""Retrieve the authentication type for the given host from the credential store.
|
|
303
242
|
|
|
304
|
-
|
|
305
|
-
----------
|
|
306
|
-
config_path : Path
|
|
307
|
-
Path to the authentication configuration file.
|
|
308
|
-
|
|
309
|
-
Returns
|
|
310
|
-
-------
|
|
311
|
-
str
|
|
312
|
-
The authentication type string, or AuthnType.NOOP if not found.
|
|
243
|
+
`AuthnType.NOOP` is returned if no authentication type is found.
|
|
313
244
|
"""
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
authn_type: str = json_file[AUTHN_TYPE_JSON_KEY]
|
|
318
|
-
return authn_type
|
|
319
|
-
except (FileNotFoundError, KeyError):
|
|
245
|
+
store = get_credential_store()
|
|
246
|
+
authn_type = store.get(AUTHN_TYPE_STORE_KEY % host)
|
|
247
|
+
if authn_type is None:
|
|
320
248
|
return AuthnType.NOOP
|
|
249
|
+
return authn_type.decode("utf-8")
|
|
321
250
|
|
|
322
251
|
|
|
323
|
-
def
|
|
324
|
-
|
|
325
|
-
federation: str,
|
|
326
|
-
federation_config: dict[str, Any],
|
|
327
|
-
authn_type: str | None = None,
|
|
252
|
+
def load_cli_auth_plugin_from_connection(
|
|
253
|
+
host: str, authn_type: str | None = None
|
|
328
254
|
) -> CliAuthPlugin:
|
|
329
|
-
"""Load the CLI-side account auth plugin for the given
|
|
255
|
+
"""Load the CLI-side account auth plugin for the given connection.
|
|
330
256
|
|
|
331
257
|
Parameters
|
|
332
258
|
----------
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
federation : str
|
|
336
|
-
Name of the federation.
|
|
337
|
-
federation_config : dict[str, Any]
|
|
338
|
-
Federation configuration dictionary.
|
|
259
|
+
host : str
|
|
260
|
+
The SuperLink Control API address.
|
|
339
261
|
authn_type : str | None
|
|
340
262
|
Authentication type. If None, will be determined from config.
|
|
341
263
|
|
|
@@ -346,41 +268,48 @@ def load_cli_auth_plugin(
|
|
|
346
268
|
|
|
347
269
|
Raises
|
|
348
270
|
------
|
|
349
|
-
|
|
271
|
+
click.ClickException
|
|
350
272
|
If the authentication type is unknown.
|
|
351
273
|
"""
|
|
352
|
-
# Find the path to the account auth config file
|
|
353
|
-
config_path = get_account_auth_config_path(root_dir, federation)
|
|
354
|
-
|
|
355
274
|
# Determine the auth type if not provided
|
|
356
275
|
# Only `flwr login` command can provide `authn_type` explicitly, as it can query the
|
|
357
276
|
# SuperLink for the auth type.
|
|
358
277
|
if authn_type is None:
|
|
359
|
-
authn_type =
|
|
360
|
-
if account_auth_enabled(federation_config):
|
|
361
|
-
authn_type = retrieve_authn_type(config_path)
|
|
278
|
+
authn_type = get_authn_type(host)
|
|
362
279
|
|
|
363
280
|
# Retrieve auth plugin class and instantiate it
|
|
364
281
|
try:
|
|
365
282
|
auth_plugin_class = get_cli_plugin_class(authn_type)
|
|
366
|
-
return auth_plugin_class(
|
|
283
|
+
return auth_plugin_class(host)
|
|
367
284
|
except ValueError:
|
|
368
|
-
|
|
369
|
-
|
|
285
|
+
raise click.ClickException(
|
|
286
|
+
f"Unknown account authentication type: {authn_type}"
|
|
287
|
+
) from None
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def require_superlink_address(connection: SuperLinkConnection) -> str:
|
|
291
|
+
"""Return the SuperLink address or exit if it is not configured."""
|
|
292
|
+
if connection.address is None:
|
|
293
|
+
cmd = click.get_current_context().command.name
|
|
294
|
+
raise click.ClickException(
|
|
295
|
+
f"`flwr {cmd}` currently works with a SuperLink. Ensure that the "
|
|
296
|
+
"correct SuperLink (Control API) address is provided SuperLink connection "
|
|
297
|
+
"you are using. Check your Flower configuration file. You may use `flwr "
|
|
298
|
+
"config list` to see its location in the file system."
|
|
299
|
+
)
|
|
300
|
+
return connection.address
|
|
370
301
|
|
|
371
302
|
|
|
372
|
-
def
|
|
373
|
-
|
|
303
|
+
def init_channel_from_connection(
|
|
304
|
+
connection: SuperLinkConnection, auth_plugin: CliAuthPlugin | None = None
|
|
374
305
|
) -> grpc.Channel:
|
|
375
306
|
"""Initialize gRPC channel to the Control API.
|
|
376
307
|
|
|
377
308
|
Parameters
|
|
378
309
|
----------
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
Federation configuration dictionary containing address and TLS settings.
|
|
383
|
-
auth_plugin : CliAuthPlugin
|
|
310
|
+
connection : SuperLinkConnection
|
|
311
|
+
SuperLink connection configuration.
|
|
312
|
+
auth_plugin : CliAuthPlugin | None (default: None)
|
|
384
313
|
Authentication plugin instance for handling credentials.
|
|
385
314
|
|
|
386
315
|
Returns
|
|
@@ -388,17 +317,20 @@ def init_channel(
|
|
|
388
317
|
grpc.Channel
|
|
389
318
|
Configured gRPC channel with authentication interceptors.
|
|
390
319
|
"""
|
|
391
|
-
|
|
392
|
-
app, federation_config
|
|
393
|
-
)
|
|
320
|
+
address = require_superlink_address(connection)
|
|
394
321
|
|
|
322
|
+
root_certificates_bytes = load_certificate_in_connection(connection)
|
|
323
|
+
|
|
324
|
+
# Load authentication plugin
|
|
325
|
+
if auth_plugin is None:
|
|
326
|
+
auth_plugin = load_cli_auth_plugin_from_connection(address)
|
|
395
327
|
# Load tokens
|
|
396
328
|
auth_plugin.load_tokens()
|
|
397
329
|
|
|
398
330
|
# Create the gRPC channel
|
|
399
331
|
channel = create_channel(
|
|
400
|
-
server_address=
|
|
401
|
-
insecure=insecure,
|
|
332
|
+
server_address=address,
|
|
333
|
+
insecure=connection.insecure,
|
|
402
334
|
root_certificates=root_certificates_bytes,
|
|
403
335
|
max_message_length=GRPC_MAX_MESSAGE_LENGTH,
|
|
404
336
|
interceptors=[CliAccountAuthInterceptor(auth_plugin)],
|
|
@@ -423,7 +355,7 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]: # pylint: disable=too-many-b
|
|
|
423
355
|
|
|
424
356
|
Raises
|
|
425
357
|
------
|
|
426
|
-
|
|
358
|
+
click.ClickException
|
|
427
359
|
On handled gRPC error statuses with appropriate exit code.
|
|
428
360
|
grpc.RpcError
|
|
429
361
|
For unhandled gRPC error statuses.
|
|
@@ -432,115 +364,74 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]: # pylint: disable=too-many-b
|
|
|
432
364
|
yield
|
|
433
365
|
except grpc.RpcError as e:
|
|
434
366
|
if e.code() == grpc.StatusCode.UNAUTHENTICATED:
|
|
435
|
-
|
|
436
|
-
"
|
|
437
|
-
" to authenticate and try again."
|
|
438
|
-
|
|
439
|
-
bold=True,
|
|
440
|
-
err=True,
|
|
441
|
-
)
|
|
442
|
-
raise typer.Exit(code=1) from None
|
|
367
|
+
raise click.ClickException(
|
|
368
|
+
"Authentication failed. Please run `flwr login`"
|
|
369
|
+
" to authenticate and try again."
|
|
370
|
+
) from None
|
|
443
371
|
if e.code() == grpc.StatusCode.UNIMPLEMENTED:
|
|
444
372
|
if e.details() == NO_ACCOUNT_AUTH_MESSAGE: # pylint: disable=E1101
|
|
445
|
-
|
|
446
|
-
"
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
"❌ The SuperLink cannot process this request. Please verify that "
|
|
461
|
-
"you set the address to its Control API endpoint correctly in your "
|
|
462
|
-
"`pyproject.toml`, and ensure that the Flower versions used by "
|
|
463
|
-
"the CLI and SuperLink are compatible.",
|
|
464
|
-
fg=typer.colors.RED,
|
|
465
|
-
bold=True,
|
|
466
|
-
err=True,
|
|
467
|
-
)
|
|
468
|
-
raise typer.Exit(code=1) from None
|
|
373
|
+
raise click.ClickException(
|
|
374
|
+
"Account authentication is not enabled on this SuperLink."
|
|
375
|
+
) from None
|
|
376
|
+
if e.details() == NO_ARTIFACT_PROVIDER_MESSAGE: # pylint: disable=E1101
|
|
377
|
+
raise click.ClickException(
|
|
378
|
+
"The SuperLink does not support `flwr pull` command."
|
|
379
|
+
) from None
|
|
380
|
+
raise click.ClickException(
|
|
381
|
+
"The SuperLink cannot process this request. Please verify that "
|
|
382
|
+
"you set the address to its Control API endpoint correctly in your "
|
|
383
|
+
"SuperLink connection in your Flower Configuration file. You may use "
|
|
384
|
+
"`flwr config list` to see its location in the file system. "
|
|
385
|
+
"Additonally, ensure that the Flower versions used by the CLI and "
|
|
386
|
+
"SuperLink are compatible."
|
|
387
|
+
) from None
|
|
469
388
|
if e.code() == grpc.StatusCode.PERMISSION_DENIED:
|
|
470
|
-
typer.secho(
|
|
471
|
-
"❌ Permission denied.",
|
|
472
|
-
fg=typer.colors.RED,
|
|
473
|
-
bold=True,
|
|
474
|
-
err=True,
|
|
475
|
-
)
|
|
476
389
|
# pylint: disable-next=E1101
|
|
477
|
-
|
|
478
|
-
raise typer.Exit(code=1) from None
|
|
390
|
+
raise click.ClickException(f"Permission denied.\n{e.details()}") from None
|
|
479
391
|
if e.code() == grpc.StatusCode.UNAVAILABLE:
|
|
480
|
-
|
|
392
|
+
raise click.ClickException(
|
|
481
393
|
"Connection to the SuperLink is unavailable. Please check your network "
|
|
482
|
-
"connection and 'address' in the
|
|
483
|
-
|
|
484
|
-
bold=True,
|
|
485
|
-
err=True,
|
|
486
|
-
)
|
|
487
|
-
raise typer.Exit(code=1) from None
|
|
394
|
+
"connection and 'address' in the SuperLink connection configuration."
|
|
395
|
+
) from None
|
|
488
396
|
if e.code() == grpc.StatusCode.NOT_FOUND:
|
|
489
397
|
if e.details() == RUN_ID_NOT_FOUND_MESSAGE: # pylint: disable=E1101
|
|
490
|
-
|
|
491
|
-
"❌ Run ID not found.",
|
|
492
|
-
fg=typer.colors.RED,
|
|
493
|
-
bold=True,
|
|
494
|
-
err=True,
|
|
495
|
-
)
|
|
496
|
-
raise typer.Exit(code=1) from None
|
|
398
|
+
raise click.ClickException("Run ID not found.") from None
|
|
497
399
|
if e.details() == NODE_NOT_FOUND_MESSAGE: # pylint: disable=E1101
|
|
498
|
-
|
|
499
|
-
"
|
|
500
|
-
|
|
501
|
-
bold=True,
|
|
502
|
-
err=True,
|
|
503
|
-
)
|
|
504
|
-
raise typer.Exit(code=1) from None
|
|
400
|
+
raise click.ClickException(
|
|
401
|
+
"Node ID not found for this account."
|
|
402
|
+
) from None
|
|
505
403
|
if e.code() == grpc.StatusCode.FAILED_PRECONDITION:
|
|
506
404
|
if e.details() == PULL_UNFINISHED_RUN_MESSAGE: # pylint: disable=E1101
|
|
507
|
-
|
|
508
|
-
"
|
|
509
|
-
"the run is finished. You can check the run status with `flwr ls`."
|
|
510
|
-
|
|
511
|
-
bold=True,
|
|
512
|
-
err=True,
|
|
513
|
-
)
|
|
514
|
-
raise typer.Exit(code=1) from None
|
|
405
|
+
raise click.ClickException(
|
|
406
|
+
"Run is not finished yet. Artifacts can only be pulled after "
|
|
407
|
+
"the run is finished. You can check the run status with `flwr ls`."
|
|
408
|
+
) from None
|
|
515
409
|
if (
|
|
516
410
|
e.details() == PUBLIC_KEY_ALREADY_IN_USE_MESSAGE
|
|
517
411
|
): # pylint: disable=E1101
|
|
518
|
-
|
|
519
|
-
"
|
|
520
|
-
|
|
521
|
-
fg=typer.colors.RED,
|
|
522
|
-
bold=True,
|
|
523
|
-
err=True,
|
|
524
|
-
)
|
|
525
|
-
raise typer.Exit(code=1) from None
|
|
412
|
+
raise click.ClickException(
|
|
413
|
+
"The provided public key is already in use by another SuperNode."
|
|
414
|
+
) from None
|
|
526
415
|
if e.details() == PUBLIC_KEY_NOT_VALID: # pylint: disable=E1101
|
|
527
|
-
|
|
528
|
-
"
|
|
529
|
-
"NIST EC public key."
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
416
|
+
raise click.ClickException(
|
|
417
|
+
"The provided public key is invalid. Please provide a valid "
|
|
418
|
+
"NIST EC public key."
|
|
419
|
+
) from None
|
|
420
|
+
if e.details() == FEDERATION_NOT_SPECIFIED_MESSAGE: # pylint: disable=E1101
|
|
421
|
+
raise click.ClickException(
|
|
422
|
+
"No federation specified. "
|
|
423
|
+
"Please use the `--federation` flag or set a default federation "
|
|
424
|
+
"in your SuperLink connection configuration."
|
|
425
|
+
) from None
|
|
426
|
+
patten = re.compile(FEDERATION_NOT_FOUND_MESSAGE.replace("%s", "(.+)"))
|
|
427
|
+
if m := patten.match(e.details()): # pylint: disable=E1101
|
|
428
|
+
raise click.ClickException(
|
|
429
|
+
f"Federation '{m.group(1)}' does not exist. "
|
|
430
|
+
"Please verify the federation name and try again."
|
|
431
|
+
) from None
|
|
535
432
|
|
|
536
433
|
# Log details from grpc error directly
|
|
537
|
-
|
|
538
|
-
f"❌ {e.details()}",
|
|
539
|
-
fg=typer.colors.RED,
|
|
540
|
-
bold=True,
|
|
541
|
-
err=True,
|
|
542
|
-
)
|
|
543
|
-
raise typer.Exit(code=1) from None
|
|
434
|
+
raise click.ClickException(f"{e.details()}") from None
|
|
544
435
|
raise
|
|
545
436
|
|
|
546
437
|
|
|
@@ -600,23 +491,17 @@ def validate_credentials_content(creds_path: Path) -> str:
|
|
|
600
491
|
try:
|
|
601
492
|
creds: dict[str, str] = json.loads(creds_path.read_text(encoding="utf-8"))
|
|
602
493
|
except (OSError, json.JSONDecodeError) as err:
|
|
603
|
-
|
|
604
|
-
f"Invalid credentials file at '{creds_path}': {err}"
|
|
605
|
-
|
|
606
|
-
err=True,
|
|
607
|
-
)
|
|
608
|
-
raise typer.Exit(code=1) from err
|
|
494
|
+
raise click.ClickException(
|
|
495
|
+
f"Invalid credentials file at '{creds_path}': {err}"
|
|
496
|
+
) from err
|
|
609
497
|
|
|
610
498
|
required_keys = [AUTHN_TYPE_JSON_KEY, ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY]
|
|
611
499
|
missing = [key for key in required_keys if key not in creds]
|
|
612
500
|
|
|
613
501
|
if missing:
|
|
614
|
-
|
|
502
|
+
raise click.ClickException(
|
|
615
503
|
f"Credentials file '{creds_path}' is missing "
|
|
616
|
-
f"required key(s): {', '.join(missing)}. Please log in again."
|
|
617
|
-
fg=typer.colors.RED,
|
|
618
|
-
err=True,
|
|
504
|
+
f"required key(s): {', '.join(missing)}. Please log in again."
|
|
619
505
|
)
|
|
620
|
-
raise typer.Exit(code=1)
|
|
621
506
|
|
|
622
507
|
return creds[ACCESS_TOKEN_KEY]
|
|
@@ -33,7 +33,7 @@ from flwr.common.inflatable_protobuf_utils import (
|
|
|
33
33
|
)
|
|
34
34
|
from flwr.common.logger import log
|
|
35
35
|
from flwr.common.message import Message, remove_content_from_message
|
|
36
|
-
from flwr.common.retry_invoker import RetryInvoker,
|
|
36
|
+
from flwr.common.retry_invoker import RetryInvoker, wrap_stub
|
|
37
37
|
from flwr.common.serde import (
|
|
38
38
|
fab_from_proto,
|
|
39
39
|
message_from_proto,
|
|
@@ -136,7 +136,7 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
136
136
|
confirm_message_received : Callable[[str], None]
|
|
137
137
|
"""
|
|
138
138
|
if isinstance(root_certificates, str):
|
|
139
|
-
root_certificates = Path(root_certificates).read_bytes()
|
|
139
|
+
root_certificates = Path(root_certificates).expanduser().read_bytes()
|
|
140
140
|
|
|
141
141
|
# Automatic node auth: generate keys if user didn't provide any
|
|
142
142
|
self_registered = False
|
|
@@ -165,7 +165,7 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
165
165
|
node: Node | None = None
|
|
166
166
|
|
|
167
167
|
# Wrap stub
|
|
168
|
-
|
|
168
|
+
wrap_stub(stub, retry_invoker)
|
|
169
169
|
###########################################################################
|
|
170
170
|
# SuperNode functions
|
|
171
171
|
###########################################################################
|
|
@@ -32,7 +32,6 @@ from flwr.common.constant import (
|
|
|
32
32
|
GRPC_ADAPTER_METADATA_MESSAGE_QUALNAME_KEY,
|
|
33
33
|
GRPC_ADAPTER_METADATA_SHOULD_EXIT_KEY,
|
|
34
34
|
)
|
|
35
|
-
from flwr.common.version import package_name, package_version
|
|
36
35
|
from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
|
|
37
36
|
from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
|
|
38
37
|
ActivateNodeRequest,
|
|
@@ -64,6 +63,7 @@ from flwr.proto.message_pb2 import ( # pylint: disable=E0611
|
|
|
64
63
|
)
|
|
65
64
|
from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611
|
|
66
65
|
from flwr.supercore.constant import FORCE_EXIT_TIMEOUT_SECONDS
|
|
66
|
+
from flwr.supercore.version import package_name, package_version
|
|
67
67
|
|
|
68
68
|
T = TypeVar("T", bound=GrpcMessage)
|
|
69
69
|
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
from logging import WARN
|
|
19
19
|
from typing import cast
|
|
20
20
|
|
|
21
|
+
from flwr.app.message_type import MessageType
|
|
21
22
|
from flwr.client.client import (
|
|
22
23
|
maybe_call_evaluate,
|
|
23
24
|
maybe_call_fit,
|
|
@@ -27,7 +28,7 @@ from flwr.client.client import (
|
|
|
27
28
|
from flwr.client.numpy_client import NumPyClient
|
|
28
29
|
from flwr.client.typing import ClientFnExt
|
|
29
30
|
from flwr.common import ConfigRecord, Context, Message, Metadata, RecordDict, log
|
|
30
|
-
from flwr.common.constant import
|
|
31
|
+
from flwr.common.constant import MessageTypeLegacy
|
|
31
32
|
from flwr.common.recorddict_compat import (
|
|
32
33
|
evaluateres_to_recorddict,
|
|
33
34
|
fitres_to_recorddict,
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
|
|
18
18
|
from logging import INFO
|
|
19
19
|
|
|
20
|
+
from flwr.app.message_type import MessageType
|
|
20
21
|
from flwr.client.typing import ClientAppCallable
|
|
21
22
|
from flwr.common import ndarrays_to_parameters, parameters_to_ndarrays
|
|
22
23
|
from flwr.common import recorddict_compat as compat
|
|
23
|
-
from flwr.common.constant import MessageType
|
|
24
24
|
from flwr.common.context import Context
|
|
25
25
|
from flwr.common.differential_privacy import (
|
|
26
26
|
compute_adaptive_clip_model_update,
|
flwr/client/mod/localdp_mod.py
CHANGED
|
@@ -19,10 +19,10 @@ from logging import INFO
|
|
|
19
19
|
|
|
20
20
|
import numpy as np
|
|
21
21
|
|
|
22
|
+
from flwr.app.message_type import MessageType
|
|
22
23
|
from flwr.client.typing import ClientAppCallable
|
|
23
24
|
from flwr.common import ndarrays_to_parameters, parameters_to_ndarrays
|
|
24
25
|
from flwr.common import recorddict_compat as compat
|
|
25
|
-
from flwr.common.constant import MessageType
|
|
26
26
|
from flwr.common.context import Context
|
|
27
27
|
from flwr.common.differential_privacy import (
|
|
28
28
|
add_localdp_gaussian_noise_to_params,
|
|
@@ -20,6 +20,7 @@ from dataclasses import dataclass, field
|
|
|
20
20
|
from logging import DEBUG, WARNING
|
|
21
21
|
from typing import Any, cast
|
|
22
22
|
|
|
23
|
+
from flwr.app.message_type import MessageType
|
|
23
24
|
from flwr.client.typing import ClientAppCallable
|
|
24
25
|
from flwr.common import (
|
|
25
26
|
ConfigRecord,
|
|
@@ -31,7 +32,6 @@ from flwr.common import (
|
|
|
31
32
|
parameters_to_ndarrays,
|
|
32
33
|
)
|
|
33
34
|
from flwr.common import recorddict_compat as compat
|
|
34
|
-
from flwr.common.constant import MessageType
|
|
35
35
|
from flwr.common.logger import log
|
|
36
36
|
from flwr.common.secure_aggregation.crypto.shamir import create_shares
|
|
37
37
|
from flwr.common.secure_aggregation.crypto.symmetric_encryption import (
|