flwr 1.13.1__py3-none-any.whl → 1.15.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/cli/app.py +5 -0
- flwr/cli/auth_plugin/__init__.py +31 -0
- flwr/cli/auth_plugin/oidc_cli_plugin.py +150 -0
- flwr/cli/build.py +1 -0
- flwr/cli/cli_user_auth_interceptor.py +90 -0
- flwr/cli/config_utils.py +43 -149
- flwr/cli/constant.py +27 -0
- flwr/cli/example.py +1 -0
- flwr/cli/install.py +2 -1
- flwr/cli/log.py +34 -37
- flwr/cli/login/__init__.py +22 -0
- flwr/cli/login/login.py +116 -0
- flwr/cli/ls.py +214 -106
- flwr/cli/new/__init__.py +1 -0
- flwr/cli/new/new.py +2 -1
- flwr/cli/new/templates/app/.gitignore.tpl +3 -0
- flwr/cli/new/templates/app/README.md.tpl +3 -2
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +4 -4
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +4 -4
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +4 -4
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +2 -2
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +3 -4
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +2 -2
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +4 -4
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +3 -3
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +2 -2
- flwr/cli/run/__init__.py +1 -0
- flwr/cli/run/run.py +103 -43
- flwr/cli/stop.py +139 -0
- flwr/cli/utils.py +186 -8
- flwr/client/app.py +49 -50
- flwr/client/client.py +1 -32
- flwr/client/clientapp/app.py +23 -26
- flwr/client/clientapp/utils.py +2 -1
- flwr/client/grpc_adapter_client/connection.py +1 -1
- flwr/client/grpc_client/connection.py +2 -13
- flwr/client/grpc_rere_client/client_interceptor.py +19 -119
- flwr/client/grpc_rere_client/connection.py +59 -43
- flwr/client/grpc_rere_client/grpc_adapter.py +12 -12
- flwr/client/message_handler/message_handler.py +1 -2
- flwr/client/message_handler/task_handler.py +0 -17
- flwr/client/mod/comms_mods.py +1 -0
- flwr/client/mod/localdp_mod.py +1 -1
- flwr/client/nodestate/__init__.py +1 -0
- flwr/client/nodestate/nodestate.py +1 -0
- flwr/client/nodestate/nodestate_factory.py +1 -0
- flwr/client/numpy_client.py +0 -44
- flwr/client/rest_client/connection.py +37 -29
- flwr/client/supernode/app.py +20 -74
- flwr/common/address.py +1 -0
- flwr/common/args.py +26 -47
- flwr/common/auth_plugin/__init__.py +24 -0
- flwr/common/auth_plugin/auth_plugin.py +122 -0
- flwr/common/config.py +169 -17
- flwr/common/constant.py +38 -9
- flwr/common/differential_privacy.py +2 -1
- flwr/common/exit/__init__.py +24 -0
- flwr/common/exit/exit.py +99 -0
- flwr/common/exit/exit_code.py +93 -0
- flwr/common/exit_handlers.py +24 -10
- flwr/common/grpc.py +167 -4
- flwr/common/logger.py +66 -7
- flwr/common/message.py +1 -0
- flwr/common/object_ref.py +57 -54
- flwr/common/pyproject.py +1 -0
- flwr/common/record/__init__.py +1 -0
- flwr/common/record/parametersrecord.py +1 -0
- flwr/common/record/recordset.py +1 -1
- flwr/common/retry_invoker.py +77 -0
- flwr/common/secure_aggregation/crypto/symmetric_encryption.py +45 -0
- flwr/common/secure_aggregation/secaggplus_utils.py +2 -2
- flwr/common/serde.py +6 -4
- flwr/common/telemetry.py +15 -4
- flwr/common/typing.py +32 -0
- flwr/common/version.py +1 -0
- flwr/proto/clientappio_pb2.py +1 -1
- flwr/proto/error_pb2.py +1 -1
- flwr/proto/exec_pb2.py +27 -15
- flwr/proto/exec_pb2.pyi +80 -2
- flwr/proto/exec_pb2_grpc.py +102 -0
- flwr/proto/exec_pb2_grpc.pyi +39 -0
- flwr/proto/fab_pb2.py +5 -5
- flwr/proto/fab_pb2.pyi +4 -1
- flwr/proto/fleet_pb2.py +31 -31
- flwr/proto/fleet_pb2.pyi +23 -23
- flwr/proto/fleet_pb2_grpc.py +30 -30
- flwr/proto/fleet_pb2_grpc.pyi +20 -20
- flwr/proto/grpcadapter_pb2.py +1 -1
- flwr/proto/log_pb2.py +1 -1
- flwr/proto/message_pb2.py +1 -1
- flwr/proto/node_pb2.py +3 -3
- flwr/proto/node_pb2.pyi +1 -4
- flwr/proto/recordset_pb2.py +1 -1
- flwr/proto/run_pb2.py +1 -1
- flwr/proto/serverappio_pb2.py +24 -25
- flwr/proto/serverappio_pb2.pyi +32 -32
- flwr/proto/serverappio_pb2_grpc.py +62 -28
- flwr/proto/serverappio_pb2_grpc.pyi +29 -16
- flwr/proto/simulationio_pb2.py +3 -3
- flwr/proto/simulationio_pb2_grpc.py +34 -0
- flwr/proto/simulationio_pb2_grpc.pyi +13 -0
- flwr/proto/task_pb2.py +1 -1
- flwr/proto/transport_pb2.py +1 -1
- flwr/server/app.py +152 -112
- flwr/server/compat/app_utils.py +7 -2
- flwr/server/compat/driver_client_proxy.py +1 -2
- flwr/server/driver/grpc_driver.py +38 -85
- flwr/server/driver/inmemory_driver.py +7 -2
- flwr/server/run_serverapp.py +8 -9
- flwr/server/serverapp/app.py +37 -13
- flwr/server/strategy/dpfedavg_fixed.py +1 -0
- flwr/server/superlink/driver/serverappio_grpc.py +2 -1
- flwr/server/superlink/driver/serverappio_servicer.py +148 -63
- flwr/server/superlink/ffs/disk_ffs.py +1 -0
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +20 -87
- flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -0
- flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +2 -165
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +56 -35
- flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +99 -169
- flwr/server/superlink/fleet/message_handler/message_handler.py +69 -29
- flwr/server/superlink/fleet/rest_rere/rest_api.py +20 -19
- flwr/server/superlink/fleet/vce/__init__.py +1 -0
- flwr/server/superlink/fleet/vce/backend/__init__.py +1 -0
- flwr/server/superlink/fleet/vce/backend/raybackend.py +1 -0
- flwr/server/superlink/fleet/vce/vce_api.py +2 -2
- flwr/server/superlink/linkstate/in_memory_linkstate.py +60 -99
- flwr/server/superlink/linkstate/linkstate.py +30 -36
- flwr/server/superlink/linkstate/sqlite_linkstate.py +105 -188
- flwr/server/superlink/linkstate/utils.py +18 -8
- flwr/server/superlink/simulation/simulationio_grpc.py +1 -1
- flwr/server/superlink/simulation/simulationio_servicer.py +33 -0
- flwr/server/superlink/utils.py +65 -0
- flwr/server/utils/validator.py +9 -34
- flwr/simulation/app.py +20 -10
- flwr/simulation/legacy_app.py +4 -2
- flwr/simulation/ray_transport/ray_actor.py +1 -0
- flwr/simulation/ray_transport/utils.py +1 -0
- flwr/simulation/run_simulation.py +36 -22
- flwr/simulation/simulationio_connection.py +5 -1
- flwr/superexec/app.py +1 -0
- flwr/superexec/deployment.py +1 -0
- flwr/superexec/exec_grpc.py +20 -2
- flwr/superexec/exec_servicer.py +97 -2
- flwr/superexec/exec_user_auth_interceptor.py +101 -0
- flwr/superexec/executor.py +1 -0
- {flwr-1.13.1.dist-info → flwr-1.15.0.dist-info}/METADATA +14 -13
- {flwr-1.13.1.dist-info → flwr-1.15.0.dist-info}/RECORD +150 -144
- flwr/proto/common_pb2.py +0 -36
- flwr/proto/common_pb2.pyi +0 -121
- flwr/proto/common_pb2_grpc.py +0 -4
- flwr/proto/common_pb2_grpc.pyi +0 -4
- flwr/proto/control_pb2.py +0 -27
- flwr/proto/control_pb2.pyi +0 -7
- flwr/proto/control_pb2_grpc.py +0 -135
- flwr/proto/control_pb2_grpc.pyi +0 -53
- {flwr-1.13.1.dist-info → flwr-1.15.0.dist-info}/LICENSE +0 -0
- {flwr-1.13.1.dist-info → flwr-1.15.0.dist-info}/WHEEL +0 -0
- {flwr-1.13.1.dist-info → flwr-1.15.0.dist-info}/entry_points.txt +0 -0
flwr/cli/run/run.py
CHANGED
|
@@ -14,28 +14,31 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Flower command line interface `run` command."""
|
|
16
16
|
|
|
17
|
+
|
|
18
|
+
import io
|
|
17
19
|
import json
|
|
18
20
|
import subprocess
|
|
19
|
-
from logging import DEBUG
|
|
20
21
|
from pathlib import Path
|
|
21
22
|
from typing import Annotated, Any, Optional
|
|
22
23
|
|
|
23
24
|
import typer
|
|
25
|
+
from rich.console import Console
|
|
24
26
|
|
|
25
27
|
from flwr.cli.build import build
|
|
26
28
|
from flwr.cli.config_utils import (
|
|
29
|
+
get_fab_metadata,
|
|
27
30
|
load_and_validate,
|
|
28
|
-
|
|
31
|
+
process_loaded_project_config,
|
|
29
32
|
validate_federation_in_project_config,
|
|
30
|
-
validate_project_config,
|
|
31
33
|
)
|
|
34
|
+
from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
|
|
32
35
|
from flwr.common.config import (
|
|
33
36
|
flatten_dict,
|
|
34
37
|
parse_config_args,
|
|
35
38
|
user_config_to_configsrecord,
|
|
36
39
|
)
|
|
37
|
-
from flwr.common.
|
|
38
|
-
from flwr.common.logger import
|
|
40
|
+
from flwr.common.constant import CliOutputFormat
|
|
41
|
+
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
|
39
42
|
from flwr.common.serde import (
|
|
40
43
|
configs_record_to_proto,
|
|
41
44
|
fab_to_proto,
|
|
@@ -46,16 +49,16 @@ from flwr.proto.exec_pb2 import StartRunRequest # pylint: disable=E0611
|
|
|
46
49
|
from flwr.proto.exec_pb2_grpc import ExecStub
|
|
47
50
|
|
|
48
51
|
from ..log import start_stream
|
|
52
|
+
from ..utils import (
|
|
53
|
+
init_channel,
|
|
54
|
+
try_obtain_cli_auth_plugin,
|
|
55
|
+
unauthenticated_exc_handler,
|
|
56
|
+
)
|
|
49
57
|
|
|
50
58
|
CONN_REFRESH_PERIOD = 60 # Connection refresh period for log streaming (seconds)
|
|
51
59
|
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
"""Log channel connectivity."""
|
|
55
|
-
log(DEBUG, channel_connectivity)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# pylint: disable-next=too-many-locals
|
|
61
|
+
# pylint: disable-next=too-many-locals, R0913, R0917
|
|
59
62
|
def run(
|
|
60
63
|
app: Annotated[
|
|
61
64
|
Path,
|
|
@@ -65,16 +68,23 @@ def run(
|
|
|
65
68
|
Optional[str],
|
|
66
69
|
typer.Argument(help="Name of the federation to run the app on."),
|
|
67
70
|
] = None,
|
|
68
|
-
|
|
71
|
+
run_config_overrides: Annotated[
|
|
69
72
|
Optional[list[str]],
|
|
70
73
|
typer.Option(
|
|
71
74
|
"--run-config",
|
|
72
75
|
"-c",
|
|
73
|
-
help="Override configuration
|
|
74
|
-
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
76
|
+
help="Override run configuration values in the format:\n\n"
|
|
77
|
+
"`--run-config 'key1=value1 key2=value2' --run-config 'key3=value3'`\n\n"
|
|
78
|
+
"Values can be of any type supported in TOML, such as bool, int, "
|
|
79
|
+
"float, or string. Ensure that the keys (`key1`, `key2`, `key3` "
|
|
80
|
+
"in this example) exist in `pyproject.toml` for proper overriding.",
|
|
81
|
+
),
|
|
82
|
+
] = None,
|
|
83
|
+
federation_config_overrides: Annotated[
|
|
84
|
+
Optional[list[str]],
|
|
85
|
+
typer.Option(
|
|
86
|
+
"--federation-config",
|
|
87
|
+
help=FEDERATION_CONFIG_HELP_MESSAGE,
|
|
78
88
|
),
|
|
79
89
|
] = None,
|
|
80
90
|
stream: Annotated[
|
|
@@ -85,46 +95,76 @@ def run(
|
|
|
85
95
|
"logs are not streamed by default.",
|
|
86
96
|
),
|
|
87
97
|
] = False,
|
|
98
|
+
output_format: Annotated[
|
|
99
|
+
str,
|
|
100
|
+
typer.Option(
|
|
101
|
+
"--format",
|
|
102
|
+
case_sensitive=False,
|
|
103
|
+
help="Format output using 'default' view or 'json'",
|
|
104
|
+
),
|
|
105
|
+
] = CliOutputFormat.DEFAULT,
|
|
88
106
|
) -> None:
|
|
89
107
|
"""Run Flower App."""
|
|
90
|
-
|
|
108
|
+
suppress_output = output_format == CliOutputFormat.JSON
|
|
109
|
+
captured_output = io.StringIO()
|
|
110
|
+
try:
|
|
111
|
+
if suppress_output:
|
|
112
|
+
redirect_output(captured_output)
|
|
113
|
+
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
|
91
114
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
115
|
+
pyproject_path = app / "pyproject.toml" if app else None
|
|
116
|
+
config, errors, warnings = load_and_validate(path=pyproject_path)
|
|
117
|
+
config = process_loaded_project_config(config, errors, warnings)
|
|
118
|
+
federation, federation_config = validate_federation_in_project_config(
|
|
119
|
+
federation, config, federation_config_overrides
|
|
120
|
+
)
|
|
98
121
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
122
|
+
if "address" in federation_config:
|
|
123
|
+
_run_with_exec_api(
|
|
124
|
+
app,
|
|
125
|
+
federation,
|
|
126
|
+
federation_config,
|
|
127
|
+
run_config_overrides,
|
|
128
|
+
stream,
|
|
129
|
+
output_format,
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
_run_without_exec_api(
|
|
133
|
+
app, federation_config, run_config_overrides, federation
|
|
134
|
+
)
|
|
135
|
+
except (typer.Exit, Exception) as err: # pylint: disable=broad-except
|
|
136
|
+
if suppress_output:
|
|
137
|
+
restore_output()
|
|
138
|
+
e_message = captured_output.getvalue()
|
|
139
|
+
print_json_error(e_message, err)
|
|
140
|
+
else:
|
|
141
|
+
typer.secho(
|
|
142
|
+
f"{err}",
|
|
143
|
+
fg=typer.colors.RED,
|
|
144
|
+
bold=True,
|
|
145
|
+
)
|
|
146
|
+
finally:
|
|
147
|
+
if suppress_output:
|
|
148
|
+
restore_output()
|
|
149
|
+
captured_output.close()
|
|
103
150
|
|
|
104
151
|
|
|
105
|
-
# pylint: disable-next=
|
|
152
|
+
# pylint: disable-next=R0913, R0914, R0917
|
|
106
153
|
def _run_with_exec_api(
|
|
107
154
|
app: Path,
|
|
155
|
+
federation: str,
|
|
108
156
|
federation_config: dict[str, Any],
|
|
109
157
|
config_overrides: Optional[list[str]],
|
|
110
158
|
stream: bool,
|
|
159
|
+
output_format: str,
|
|
111
160
|
) -> None:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
app, federation_config
|
|
115
|
-
)
|
|
116
|
-
channel = create_channel(
|
|
117
|
-
server_address=federation_config["address"],
|
|
118
|
-
insecure=insecure,
|
|
119
|
-
root_certificates=root_certificates_bytes,
|
|
120
|
-
max_message_length=GRPC_MAX_MESSAGE_LENGTH,
|
|
121
|
-
interceptors=None,
|
|
122
|
-
)
|
|
123
|
-
channel.subscribe(on_channel_state_change)
|
|
161
|
+
auth_plugin = try_obtain_cli_auth_plugin(app, federation, federation_config)
|
|
162
|
+
channel = init_channel(app, federation_config, auth_plugin)
|
|
124
163
|
stub = ExecStub(channel)
|
|
125
164
|
|
|
126
165
|
fab_path, fab_hash = build(app)
|
|
127
166
|
content = Path(fab_path).read_bytes()
|
|
167
|
+
fab_id, fab_version = get_fab_metadata(Path(fab_path))
|
|
128
168
|
|
|
129
169
|
# Delete FAB file once the bytes is computed
|
|
130
170
|
Path(fab_path).unlink()
|
|
@@ -140,9 +180,29 @@ def _run_with_exec_api(
|
|
|
140
180
|
override_config=user_config_to_proto(parse_config_args(config_overrides)),
|
|
141
181
|
federation_options=configs_record_to_proto(c_record),
|
|
142
182
|
)
|
|
143
|
-
|
|
183
|
+
with unauthenticated_exc_handler():
|
|
184
|
+
res = stub.StartRun(req)
|
|
144
185
|
|
|
145
|
-
|
|
186
|
+
if res.HasField("run_id"):
|
|
187
|
+
typer.secho(f"🎊 Successfully started run {res.run_id}", fg=typer.colors.GREEN)
|
|
188
|
+
else:
|
|
189
|
+
typer.secho("❌ Failed to start run", fg=typer.colors.RED)
|
|
190
|
+
raise typer.Exit(code=1)
|
|
191
|
+
|
|
192
|
+
if output_format == CliOutputFormat.JSON:
|
|
193
|
+
run_output = json.dumps(
|
|
194
|
+
{
|
|
195
|
+
"success": res.HasField("run_id"),
|
|
196
|
+
"run-id": res.run_id if res.HasField("run_id") else None,
|
|
197
|
+
"fab-id": fab_id,
|
|
198
|
+
"fab-name": fab_id.rsplit("/", maxsplit=1)[-1],
|
|
199
|
+
"fab-version": fab_version,
|
|
200
|
+
"fab-hash": fab_hash[:8],
|
|
201
|
+
"fab-filename": fab_path,
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
restore_output()
|
|
205
|
+
Console().print_json(run_output)
|
|
146
206
|
|
|
147
207
|
if stream:
|
|
148
208
|
start_stream(res.run_id, channel, CONN_REFRESH_PERIOD)
|
flwr/cli/stop.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Copyright 2024 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 `stop` command."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
import io
|
|
19
|
+
import json
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Annotated, Optional
|
|
22
|
+
|
|
23
|
+
import typer
|
|
24
|
+
from rich.console import Console
|
|
25
|
+
|
|
26
|
+
from flwr.cli.config_utils import (
|
|
27
|
+
exit_if_no_address,
|
|
28
|
+
load_and_validate,
|
|
29
|
+
process_loaded_project_config,
|
|
30
|
+
validate_federation_in_project_config,
|
|
31
|
+
)
|
|
32
|
+
from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
|
|
33
|
+
from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
|
|
34
|
+
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
|
35
|
+
from flwr.proto.exec_pb2 import StopRunRequest, StopRunResponse # pylint: disable=E0611
|
|
36
|
+
from flwr.proto.exec_pb2_grpc import ExecStub
|
|
37
|
+
|
|
38
|
+
from .utils import init_channel, try_obtain_cli_auth_plugin, unauthenticated_exc_handler
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def stop( # pylint: disable=R0914
|
|
42
|
+
run_id: Annotated[ # pylint: disable=unused-argument
|
|
43
|
+
int,
|
|
44
|
+
typer.Argument(help="The Flower run ID to stop"),
|
|
45
|
+
],
|
|
46
|
+
app: Annotated[
|
|
47
|
+
Path,
|
|
48
|
+
typer.Argument(help="Path of the Flower project"),
|
|
49
|
+
] = Path("."),
|
|
50
|
+
federation: Annotated[
|
|
51
|
+
Optional[str],
|
|
52
|
+
typer.Argument(help="Name of the federation"),
|
|
53
|
+
] = None,
|
|
54
|
+
federation_config_overrides: Annotated[
|
|
55
|
+
Optional[list[str]],
|
|
56
|
+
typer.Option(
|
|
57
|
+
"--federation-config",
|
|
58
|
+
help=FEDERATION_CONFIG_HELP_MESSAGE,
|
|
59
|
+
),
|
|
60
|
+
] = None,
|
|
61
|
+
output_format: Annotated[
|
|
62
|
+
str,
|
|
63
|
+
typer.Option(
|
|
64
|
+
"--format",
|
|
65
|
+
case_sensitive=False,
|
|
66
|
+
help="Format output using 'default' view or 'json'",
|
|
67
|
+
),
|
|
68
|
+
] = CliOutputFormat.DEFAULT,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Stop a run."""
|
|
71
|
+
suppress_output = output_format == CliOutputFormat.JSON
|
|
72
|
+
captured_output = io.StringIO()
|
|
73
|
+
try:
|
|
74
|
+
if suppress_output:
|
|
75
|
+
redirect_output(captured_output)
|
|
76
|
+
|
|
77
|
+
# Load and validate federation config
|
|
78
|
+
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
|
79
|
+
|
|
80
|
+
pyproject_path = app / FAB_CONFIG_FILE if app else None
|
|
81
|
+
config, errors, warnings = load_and_validate(path=pyproject_path)
|
|
82
|
+
config = process_loaded_project_config(config, errors, warnings)
|
|
83
|
+
federation, federation_config = validate_federation_in_project_config(
|
|
84
|
+
federation, config, federation_config_overrides
|
|
85
|
+
)
|
|
86
|
+
exit_if_no_address(federation_config, "stop")
|
|
87
|
+
channel = None
|
|
88
|
+
try:
|
|
89
|
+
auth_plugin = try_obtain_cli_auth_plugin(app, federation, federation_config)
|
|
90
|
+
channel = init_channel(app, federation_config, auth_plugin)
|
|
91
|
+
stub = ExecStub(channel) # pylint: disable=unused-variable # noqa: F841
|
|
92
|
+
|
|
93
|
+
typer.secho(f"✋ Stopping run ID {run_id}...", fg=typer.colors.GREEN)
|
|
94
|
+
_stop_run(stub=stub, run_id=run_id, output_format=output_format)
|
|
95
|
+
|
|
96
|
+
except ValueError as err:
|
|
97
|
+
typer.secho(
|
|
98
|
+
f"❌ {err}",
|
|
99
|
+
fg=typer.colors.RED,
|
|
100
|
+
bold=True,
|
|
101
|
+
)
|
|
102
|
+
raise typer.Exit(code=1) from err
|
|
103
|
+
finally:
|
|
104
|
+
if channel:
|
|
105
|
+
channel.close()
|
|
106
|
+
except (typer.Exit, Exception) as err: # pylint: disable=broad-except
|
|
107
|
+
if suppress_output:
|
|
108
|
+
restore_output()
|
|
109
|
+
e_message = captured_output.getvalue()
|
|
110
|
+
print_json_error(e_message, err)
|
|
111
|
+
else:
|
|
112
|
+
typer.secho(
|
|
113
|
+
f"{err}",
|
|
114
|
+
fg=typer.colors.RED,
|
|
115
|
+
bold=True,
|
|
116
|
+
)
|
|
117
|
+
finally:
|
|
118
|
+
if suppress_output:
|
|
119
|
+
restore_output()
|
|
120
|
+
captured_output.close()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _stop_run(stub: ExecStub, run_id: int, output_format: str) -> None:
|
|
124
|
+
"""Stop a run."""
|
|
125
|
+
with unauthenticated_exc_handler():
|
|
126
|
+
response: StopRunResponse = stub.StopRun(request=StopRunRequest(run_id=run_id))
|
|
127
|
+
if response.success:
|
|
128
|
+
typer.secho(f"✅ Run {run_id} successfully stopped.", fg=typer.colors.GREEN)
|
|
129
|
+
if output_format == CliOutputFormat.JSON:
|
|
130
|
+
run_output = json.dumps(
|
|
131
|
+
{
|
|
132
|
+
"success": True,
|
|
133
|
+
"run-id": run_id,
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
restore_output()
|
|
137
|
+
Console().print_json(run_output)
|
|
138
|
+
else:
|
|
139
|
+
typer.secho(f"❌ Run {run_id} couldn't be stopped.", fg=typer.colors.RED)
|
flwr/cli/utils.py
CHANGED
|
@@ -14,13 +14,30 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Flower command line interface utils."""
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
import hashlib
|
|
19
|
+
import json
|
|
18
20
|
import re
|
|
21
|
+
from collections.abc import Iterator
|
|
22
|
+
from contextlib import contextmanager
|
|
19
23
|
from pathlib import Path
|
|
20
|
-
from typing import Callable, Optional, cast
|
|
24
|
+
from typing import Any, Callable, Optional, Union, cast
|
|
21
25
|
|
|
26
|
+
import grpc
|
|
22
27
|
import typer
|
|
23
28
|
|
|
29
|
+
from flwr.cli.cli_user_auth_interceptor import CliUserAuthInterceptor
|
|
30
|
+
from flwr.common.auth_plugin import CliAuthPlugin
|
|
31
|
+
from flwr.common.constant import AUTH_TYPE_KEY, CREDENTIALS_DIR, FLWR_DIR
|
|
32
|
+
from flwr.common.grpc import (
|
|
33
|
+
GRPC_MAX_MESSAGE_LENGTH,
|
|
34
|
+
create_channel,
|
|
35
|
+
on_channel_state_change,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from .auth_plugin import get_cli_auth_plugins
|
|
39
|
+
from .config_utils import validate_certificate_in_federation_config
|
|
40
|
+
|
|
24
41
|
|
|
25
42
|
def prompt_text(
|
|
26
43
|
text: str,
|
|
@@ -126,13 +143,174 @@ def sanitize_project_name(name: str) -> str:
|
|
|
126
143
|
return sanitized_name
|
|
127
144
|
|
|
128
145
|
|
|
129
|
-
def get_sha256_hash(
|
|
146
|
+
def get_sha256_hash(file_path_or_int: Union[Path, int]) -> str:
|
|
130
147
|
"""Calculate the SHA-256 hash of a file."""
|
|
131
148
|
sha256 = hashlib.sha256()
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
149
|
+
if isinstance(file_path_or_int, Path):
|
|
150
|
+
with open(file_path_or_int, "rb") as f:
|
|
151
|
+
while True:
|
|
152
|
+
data = f.read(65536) # Read in 64kB blocks
|
|
153
|
+
if not data:
|
|
154
|
+
break
|
|
155
|
+
sha256.update(data)
|
|
156
|
+
elif isinstance(file_path_or_int, int):
|
|
157
|
+
sha256.update(str(file_path_or_int).encode())
|
|
138
158
|
return sha256.hexdigest()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def get_user_auth_config_path(root_dir: Path, federation: str) -> Path:
|
|
162
|
+
"""Return the path to the user auth config file.
|
|
163
|
+
|
|
164
|
+
Additionally, a `.gitignore` file will be created in the Flower directory to
|
|
165
|
+
include the `.credentials` folder to be excluded from git. If the `.gitignore`
|
|
166
|
+
file already exists, a warning will be displayed if the `.credentials` entry is
|
|
167
|
+
not found.
|
|
168
|
+
"""
|
|
169
|
+
# Locate the credentials directory
|
|
170
|
+
abs_flwr_dir = root_dir.absolute() / FLWR_DIR
|
|
171
|
+
credentials_dir = abs_flwr_dir / CREDENTIALS_DIR
|
|
172
|
+
credentials_dir.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
|
|
174
|
+
# Determine the absolute path of the Flower directory for .gitignore
|
|
175
|
+
gitignore_path = abs_flwr_dir / ".gitignore"
|
|
176
|
+
credential_entry = CREDENTIALS_DIR
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
if gitignore_path.exists():
|
|
180
|
+
with open(gitignore_path, encoding="utf-8") as gitignore_file:
|
|
181
|
+
lines = gitignore_file.read().splitlines()
|
|
182
|
+
|
|
183
|
+
# Warn if .credentials is not already in .gitignore
|
|
184
|
+
if credential_entry not in lines:
|
|
185
|
+
typer.secho(
|
|
186
|
+
f"`.gitignore` exists, but `{credential_entry}` entry not found. "
|
|
187
|
+
"Consider adding it to your `.gitignore` to exclude Flower "
|
|
188
|
+
"credentials from git.",
|
|
189
|
+
fg=typer.colors.YELLOW,
|
|
190
|
+
bold=True,
|
|
191
|
+
)
|
|
192
|
+
else:
|
|
193
|
+
typer.secho(
|
|
194
|
+
f"Creating a new `.gitignore` with `{credential_entry}` entry...",
|
|
195
|
+
fg=typer.colors.BLUE,
|
|
196
|
+
)
|
|
197
|
+
# Create a new .gitignore with .credentials
|
|
198
|
+
with open(gitignore_path, "w", encoding="utf-8") as gitignore_file:
|
|
199
|
+
gitignore_file.write(f"{credential_entry}\n")
|
|
200
|
+
except Exception as err:
|
|
201
|
+
typer.secho(
|
|
202
|
+
"❌ An error occurred while handling `.gitignore.` "
|
|
203
|
+
f"Please check the permissions of `{gitignore_path}` and try again.",
|
|
204
|
+
fg=typer.colors.RED,
|
|
205
|
+
bold=True,
|
|
206
|
+
)
|
|
207
|
+
raise typer.Exit(code=1) from err
|
|
208
|
+
|
|
209
|
+
return credentials_dir / f"{federation}.json"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def try_obtain_cli_auth_plugin(
|
|
213
|
+
root_dir: Path,
|
|
214
|
+
federation: str,
|
|
215
|
+
federation_config: dict[str, Any],
|
|
216
|
+
auth_type: Optional[str] = None,
|
|
217
|
+
) -> Optional[CliAuthPlugin]:
|
|
218
|
+
"""Load the CLI-side user auth plugin for the given auth type."""
|
|
219
|
+
# Check if user auth is enabled
|
|
220
|
+
if not federation_config.get("enable-user-auth", False):
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
# Check if TLS is enabled. If not, raise an error
|
|
224
|
+
if federation_config.get("root-certificates") is None:
|
|
225
|
+
typer.secho(
|
|
226
|
+
"❌ User authentication requires TLS to be enabled. "
|
|
227
|
+
"Please provide 'root-certificates' in the federation"
|
|
228
|
+
" configuration.",
|
|
229
|
+
fg=typer.colors.RED,
|
|
230
|
+
bold=True,
|
|
231
|
+
)
|
|
232
|
+
raise typer.Exit(code=1)
|
|
233
|
+
|
|
234
|
+
config_path = get_user_auth_config_path(root_dir, federation)
|
|
235
|
+
|
|
236
|
+
# Get the auth type from the config if not provided
|
|
237
|
+
# auth_type will be None for all CLI commands except login
|
|
238
|
+
if auth_type is None:
|
|
239
|
+
try:
|
|
240
|
+
with config_path.open("r", encoding="utf-8") as file:
|
|
241
|
+
json_file = json.load(file)
|
|
242
|
+
auth_type = json_file[AUTH_TYPE_KEY]
|
|
243
|
+
except (FileNotFoundError, KeyError):
|
|
244
|
+
typer.secho(
|
|
245
|
+
"❌ Missing or invalid credentials for user authentication. "
|
|
246
|
+
"Please run `flwr login` to authenticate.",
|
|
247
|
+
fg=typer.colors.RED,
|
|
248
|
+
bold=True,
|
|
249
|
+
)
|
|
250
|
+
raise typer.Exit(code=1) from None
|
|
251
|
+
|
|
252
|
+
# Retrieve auth plugin class and instantiate it
|
|
253
|
+
try:
|
|
254
|
+
all_plugins: dict[str, type[CliAuthPlugin]] = get_cli_auth_plugins()
|
|
255
|
+
auth_plugin_class = all_plugins[auth_type]
|
|
256
|
+
return auth_plugin_class(config_path)
|
|
257
|
+
except KeyError:
|
|
258
|
+
typer.echo(f"❌ Unknown user authentication type: {auth_type}")
|
|
259
|
+
raise typer.Exit(code=1) from None
|
|
260
|
+
except ImportError:
|
|
261
|
+
typer.echo("❌ No authentication plugins are currently supported.")
|
|
262
|
+
raise typer.Exit(code=1) from None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def init_channel(
|
|
266
|
+
app: Path, federation_config: dict[str, Any], auth_plugin: Optional[CliAuthPlugin]
|
|
267
|
+
) -> grpc.Channel:
|
|
268
|
+
"""Initialize gRPC channel to the Exec API."""
|
|
269
|
+
insecure, root_certificates_bytes = validate_certificate_in_federation_config(
|
|
270
|
+
app, federation_config
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Initialize the CLI-side user auth interceptor
|
|
274
|
+
interceptors: list[grpc.UnaryUnaryClientInterceptor] = []
|
|
275
|
+
if auth_plugin is not None:
|
|
276
|
+
auth_plugin.load_tokens()
|
|
277
|
+
interceptors.append(CliUserAuthInterceptor(auth_plugin))
|
|
278
|
+
|
|
279
|
+
# Create the gRPC channel
|
|
280
|
+
channel = create_channel(
|
|
281
|
+
server_address=federation_config["address"],
|
|
282
|
+
insecure=insecure,
|
|
283
|
+
root_certificates=root_certificates_bytes,
|
|
284
|
+
max_message_length=GRPC_MAX_MESSAGE_LENGTH,
|
|
285
|
+
interceptors=interceptors or None,
|
|
286
|
+
)
|
|
287
|
+
channel.subscribe(on_channel_state_change)
|
|
288
|
+
return channel
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@contextmanager
|
|
292
|
+
def unauthenticated_exc_handler() -> Iterator[None]:
|
|
293
|
+
"""Context manager to handle gRPC UNAUTHENTICATED errors.
|
|
294
|
+
|
|
295
|
+
It catches grpc.RpcError exceptions with UNAUTHENTICATED status, informs the user,
|
|
296
|
+
and exits the application. All other exceptions will be allowed to escape.
|
|
297
|
+
"""
|
|
298
|
+
try:
|
|
299
|
+
yield
|
|
300
|
+
except grpc.RpcError as e:
|
|
301
|
+
if e.code() == grpc.StatusCode.UNAUTHENTICATED:
|
|
302
|
+
typer.secho(
|
|
303
|
+
"❌ Authentication failed. Please run `flwr login`"
|
|
304
|
+
" to authenticate and try again.",
|
|
305
|
+
fg=typer.colors.RED,
|
|
306
|
+
bold=True,
|
|
307
|
+
)
|
|
308
|
+
raise typer.Exit(code=1) from None
|
|
309
|
+
if e.code() == grpc.StatusCode.UNIMPLEMENTED:
|
|
310
|
+
typer.secho(
|
|
311
|
+
"❌ User authentication is not enabled on this SuperLink.",
|
|
312
|
+
fg=typer.colors.RED,
|
|
313
|
+
bold=True,
|
|
314
|
+
)
|
|
315
|
+
raise typer.Exit(code=1) from None
|
|
316
|
+
raise
|