flwr 1.13.0__py3-none-any.whl → 1.14.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/build.py +1 -37
- flwr/cli/cli_user_auth_interceptor.py +86 -0
- flwr/cli/config_utils.py +19 -2
- flwr/cli/example.py +1 -0
- flwr/cli/install.py +2 -19
- flwr/cli/log.py +18 -36
- flwr/cli/login/__init__.py +22 -0
- flwr/cli/login/login.py +81 -0
- flwr/cli/ls.py +205 -106
- flwr/cli/new/__init__.py +1 -0
- flwr/cli/new/new.py +25 -14
- flwr/cli/new/templates/app/.gitignore.tpl +3 -0
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +3 -3
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +2 -3
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
- flwr/cli/run/__init__.py +1 -0
- flwr/cli/run/run.py +89 -39
- flwr/cli/stop.py +130 -0
- flwr/cli/utils.py +172 -8
- flwr/client/app.py +14 -3
- flwr/client/client.py +1 -32
- flwr/client/clientapp/app.py +4 -8
- flwr/client/clientapp/utils.py +1 -0
- flwr/client/grpc_adapter_client/connection.py +1 -1
- flwr/client/grpc_client/connection.py +1 -1
- flwr/client/grpc_rere_client/connection.py +13 -7
- flwr/client/message_handler/message_handler.py +1 -2
- 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 +3 -3
- flwr/client/supernode/app.py +2 -2
- flwr/common/address.py +1 -0
- flwr/common/args.py +1 -0
- flwr/common/auth_plugin/__init__.py +24 -0
- flwr/common/auth_plugin/auth_plugin.py +111 -0
- flwr/common/config.py +3 -1
- flwr/common/constant.py +17 -1
- flwr/common/logger.py +40 -0
- 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/retry_invoker.py +77 -0
- flwr/common/secure_aggregation/secaggplus_utils.py +2 -2
- flwr/common/telemetry.py +15 -4
- flwr/common/typing.py +12 -0
- flwr/common/version.py +1 -0
- flwr/proto/exec_pb2.py +38 -14
- flwr/proto/exec_pb2.pyi +107 -2
- flwr/proto/exec_pb2_grpc.py +102 -0
- flwr/proto/exec_pb2_grpc.pyi +39 -0
- flwr/proto/fab_pb2.py +4 -4
- flwr/proto/fab_pb2.pyi +4 -1
- flwr/proto/serverappio_pb2.py +18 -18
- flwr/proto/serverappio_pb2.pyi +8 -2
- flwr/proto/serverappio_pb2_grpc.py +34 -0
- flwr/proto/serverappio_pb2_grpc.pyi +13 -0
- flwr/proto/simulationio_pb2.py +2 -2
- flwr/proto/simulationio_pb2_grpc.py +34 -0
- flwr/proto/simulationio_pb2_grpc.pyi +13 -0
- flwr/server/app.py +62 -7
- flwr/server/compat/app_utils.py +7 -1
- flwr/server/driver/grpc_driver.py +11 -63
- flwr/server/driver/inmemory_driver.py +5 -1
- flwr/server/run_serverapp.py +8 -9
- flwr/server/serverapp/app.py +25 -10
- flwr/server/strategy/dpfedavg_fixed.py +1 -0
- flwr/server/superlink/driver/serverappio_grpc.py +1 -0
- flwr/server/superlink/driver/serverappio_servicer.py +82 -23
- flwr/server/superlink/ffs/disk_ffs.py +1 -0
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -0
- flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -0
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +32 -12
- flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +12 -11
- flwr/server/superlink/fleet/message_handler/message_handler.py +32 -5
- flwr/server/superlink/fleet/rest_rere/rest_api.py +4 -1
- 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/linkstate/in_memory_linkstate.py +21 -30
- flwr/server/superlink/linkstate/linkstate.py +17 -2
- flwr/server/superlink/linkstate/sqlite_linkstate.py +30 -49
- flwr/server/superlink/simulation/simulationio_servicer.py +33 -0
- flwr/server/superlink/utils.py +65 -0
- flwr/simulation/app.py +59 -52
- 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 +3 -0
- flwr/superexec/app.py +1 -0
- flwr/superexec/deployment.py +1 -0
- flwr/superexec/exec_grpc.py +19 -1
- flwr/superexec/exec_servicer.py +76 -2
- flwr/superexec/exec_user_auth_interceptor.py +101 -0
- flwr/superexec/executor.py +1 -0
- {flwr-1.13.0.dist-info → flwr-1.14.0.dist-info}/METADATA +8 -8
- {flwr-1.13.0.dist-info → flwr-1.14.0.dist-info}/RECORD +112 -112
- 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.0.dist-info → flwr-1.14.0.dist-info}/LICENSE +0 -0
- {flwr-1.13.0.dist-info → flwr-1.14.0.dist-info}/WHEEL +0 -0
- {flwr-1.13.0.dist-info → flwr-1.14.0.dist-info}/entry_points.txt +0 -0
flwr/cli/run/run.py
CHANGED
|
@@ -14,28 +14,30 @@
|
|
|
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
|
)
|
|
32
34
|
from flwr.common.config import (
|
|
33
35
|
flatten_dict,
|
|
34
36
|
parse_config_args,
|
|
35
37
|
user_config_to_configsrecord,
|
|
36
38
|
)
|
|
37
|
-
from flwr.common.
|
|
38
|
-
from flwr.common.logger import
|
|
39
|
+
from flwr.common.constant import CliOutputFormat
|
|
40
|
+
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
|
39
41
|
from flwr.common.serde import (
|
|
40
42
|
configs_record_to_proto,
|
|
41
43
|
fab_to_proto,
|
|
@@ -46,15 +48,15 @@ from flwr.proto.exec_pb2 import StartRunRequest # pylint: disable=E0611
|
|
|
46
48
|
from flwr.proto.exec_pb2_grpc import ExecStub
|
|
47
49
|
|
|
48
50
|
from ..log import start_stream
|
|
51
|
+
from ..utils import (
|
|
52
|
+
init_channel,
|
|
53
|
+
try_obtain_cli_auth_plugin,
|
|
54
|
+
unauthenticated_exc_handler,
|
|
55
|
+
)
|
|
49
56
|
|
|
50
57
|
CONN_REFRESH_PERIOD = 60 # Connection refresh period for log streaming (seconds)
|
|
51
58
|
|
|
52
59
|
|
|
53
|
-
def on_channel_state_change(channel_connectivity: str) -> None:
|
|
54
|
-
"""Log channel connectivity."""
|
|
55
|
-
log(DEBUG, channel_connectivity)
|
|
56
|
-
|
|
57
|
-
|
|
58
60
|
# pylint: disable-next=too-many-locals
|
|
59
61
|
def run(
|
|
60
62
|
app: Annotated[
|
|
@@ -85,46 +87,74 @@ def run(
|
|
|
85
87
|
"logs are not streamed by default.",
|
|
86
88
|
),
|
|
87
89
|
] = False,
|
|
90
|
+
output_format: Annotated[
|
|
91
|
+
str,
|
|
92
|
+
typer.Option(
|
|
93
|
+
"--format",
|
|
94
|
+
case_sensitive=False,
|
|
95
|
+
help="Format output using 'default' view or 'json'",
|
|
96
|
+
),
|
|
97
|
+
] = CliOutputFormat.DEFAULT,
|
|
88
98
|
) -> None:
|
|
89
99
|
"""Run Flower App."""
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
suppress_output = output_format == CliOutputFormat.JSON
|
|
101
|
+
captured_output = io.StringIO()
|
|
102
|
+
try:
|
|
103
|
+
if suppress_output:
|
|
104
|
+
redirect_output(captured_output)
|
|
105
|
+
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
|
106
|
+
|
|
107
|
+
pyproject_path = app / "pyproject.toml" if app else None
|
|
108
|
+
config, errors, warnings = load_and_validate(path=pyproject_path)
|
|
109
|
+
config = process_loaded_project_config(config, errors, warnings)
|
|
110
|
+
federation, federation_config = validate_federation_in_project_config(
|
|
111
|
+
federation, config
|
|
112
|
+
)
|
|
104
113
|
|
|
105
|
-
|
|
114
|
+
if "address" in federation_config:
|
|
115
|
+
_run_with_exec_api(
|
|
116
|
+
app,
|
|
117
|
+
federation,
|
|
118
|
+
federation_config,
|
|
119
|
+
config_overrides,
|
|
120
|
+
stream,
|
|
121
|
+
output_format,
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
_run_without_exec_api(app, federation_config, config_overrides, federation)
|
|
125
|
+
except (typer.Exit, Exception) as err: # pylint: disable=broad-except
|
|
126
|
+
if suppress_output:
|
|
127
|
+
restore_output()
|
|
128
|
+
e_message = captured_output.getvalue()
|
|
129
|
+
print_json_error(e_message, err)
|
|
130
|
+
else:
|
|
131
|
+
typer.secho(
|
|
132
|
+
f"{err}",
|
|
133
|
+
fg=typer.colors.RED,
|
|
134
|
+
bold=True,
|
|
135
|
+
)
|
|
136
|
+
finally:
|
|
137
|
+
if suppress_output:
|
|
138
|
+
restore_output()
|
|
139
|
+
captured_output.close()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# pylint: disable-next=R0913, R0914, R0917
|
|
106
143
|
def _run_with_exec_api(
|
|
107
144
|
app: Path,
|
|
145
|
+
federation: str,
|
|
108
146
|
federation_config: dict[str, Any],
|
|
109
147
|
config_overrides: Optional[list[str]],
|
|
110
148
|
stream: bool,
|
|
149
|
+
output_format: str,
|
|
111
150
|
) -> 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)
|
|
151
|
+
auth_plugin = try_obtain_cli_auth_plugin(app, federation)
|
|
152
|
+
channel = init_channel(app, federation_config, auth_plugin)
|
|
124
153
|
stub = ExecStub(channel)
|
|
125
154
|
|
|
126
155
|
fab_path, fab_hash = build(app)
|
|
127
156
|
content = Path(fab_path).read_bytes()
|
|
157
|
+
fab_id, fab_version = get_fab_metadata(Path(fab_path))
|
|
128
158
|
|
|
129
159
|
# Delete FAB file once the bytes is computed
|
|
130
160
|
Path(fab_path).unlink()
|
|
@@ -140,9 +170,29 @@ def _run_with_exec_api(
|
|
|
140
170
|
override_config=user_config_to_proto(parse_config_args(config_overrides)),
|
|
141
171
|
federation_options=configs_record_to_proto(c_record),
|
|
142
172
|
)
|
|
143
|
-
|
|
173
|
+
with unauthenticated_exc_handler():
|
|
174
|
+
res = stub.StartRun(req)
|
|
144
175
|
|
|
145
|
-
|
|
176
|
+
if res.HasField("run_id"):
|
|
177
|
+
typer.secho(f"🎊 Successfully started run {res.run_id}", fg=typer.colors.GREEN)
|
|
178
|
+
else:
|
|
179
|
+
typer.secho("❌ Failed to start run", fg=typer.colors.RED)
|
|
180
|
+
raise typer.Exit(code=1)
|
|
181
|
+
|
|
182
|
+
if output_format == CliOutputFormat.JSON:
|
|
183
|
+
run_output = json.dumps(
|
|
184
|
+
{
|
|
185
|
+
"success": res.HasField("run_id"),
|
|
186
|
+
"run-id": res.run_id if res.HasField("run_id") else None,
|
|
187
|
+
"fab-id": fab_id,
|
|
188
|
+
"fab-name": fab_id.rsplit("/", maxsplit=1)[-1],
|
|
189
|
+
"fab-version": fab_version,
|
|
190
|
+
"fab-hash": fab_hash[:8],
|
|
191
|
+
"fab-filename": fab_path,
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
restore_output()
|
|
195
|
+
Console().print_json(run_output)
|
|
146
196
|
|
|
147
197
|
if stream:
|
|
148
198
|
start_stream(res.run_id, channel, CONN_REFRESH_PERIOD)
|
flwr/cli/stop.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
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.common.constant import FAB_CONFIG_FILE, CliOutputFormat
|
|
33
|
+
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
|
34
|
+
from flwr.proto.exec_pb2 import StopRunRequest, StopRunResponse # pylint: disable=E0611
|
|
35
|
+
from flwr.proto.exec_pb2_grpc import ExecStub
|
|
36
|
+
|
|
37
|
+
from .utils import init_channel, try_obtain_cli_auth_plugin, unauthenticated_exc_handler
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def stop( # pylint: disable=R0914
|
|
41
|
+
run_id: Annotated[ # pylint: disable=unused-argument
|
|
42
|
+
int,
|
|
43
|
+
typer.Argument(help="The Flower run ID to stop"),
|
|
44
|
+
],
|
|
45
|
+
app: Annotated[
|
|
46
|
+
Path,
|
|
47
|
+
typer.Argument(help="Path of the Flower project"),
|
|
48
|
+
] = Path("."),
|
|
49
|
+
federation: Annotated[
|
|
50
|
+
Optional[str],
|
|
51
|
+
typer.Argument(help="Name of the federation"),
|
|
52
|
+
] = None,
|
|
53
|
+
output_format: Annotated[
|
|
54
|
+
str,
|
|
55
|
+
typer.Option(
|
|
56
|
+
"--format",
|
|
57
|
+
case_sensitive=False,
|
|
58
|
+
help="Format output using 'default' view or 'json'",
|
|
59
|
+
),
|
|
60
|
+
] = CliOutputFormat.DEFAULT,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Stop a run."""
|
|
63
|
+
suppress_output = output_format == CliOutputFormat.JSON
|
|
64
|
+
captured_output = io.StringIO()
|
|
65
|
+
try:
|
|
66
|
+
if suppress_output:
|
|
67
|
+
redirect_output(captured_output)
|
|
68
|
+
|
|
69
|
+
# Load and validate federation config
|
|
70
|
+
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
|
71
|
+
|
|
72
|
+
pyproject_path = app / FAB_CONFIG_FILE if app else None
|
|
73
|
+
config, errors, warnings = load_and_validate(path=pyproject_path)
|
|
74
|
+
config = process_loaded_project_config(config, errors, warnings)
|
|
75
|
+
federation, federation_config = validate_federation_in_project_config(
|
|
76
|
+
federation, config
|
|
77
|
+
)
|
|
78
|
+
exit_if_no_address(federation_config, "stop")
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
auth_plugin = try_obtain_cli_auth_plugin(app, federation)
|
|
82
|
+
channel = init_channel(app, federation_config, auth_plugin)
|
|
83
|
+
stub = ExecStub(channel) # pylint: disable=unused-variable # noqa: F841
|
|
84
|
+
|
|
85
|
+
typer.secho(f"✋ Stopping run ID {run_id}...", fg=typer.colors.GREEN)
|
|
86
|
+
_stop_run(stub=stub, run_id=run_id, output_format=output_format)
|
|
87
|
+
|
|
88
|
+
except ValueError as err:
|
|
89
|
+
typer.secho(
|
|
90
|
+
f"❌ {err}",
|
|
91
|
+
fg=typer.colors.RED,
|
|
92
|
+
bold=True,
|
|
93
|
+
)
|
|
94
|
+
raise typer.Exit(code=1) from err
|
|
95
|
+
finally:
|
|
96
|
+
channel.close()
|
|
97
|
+
except (typer.Exit, Exception) as err: # pylint: disable=broad-except
|
|
98
|
+
if suppress_output:
|
|
99
|
+
restore_output()
|
|
100
|
+
e_message = captured_output.getvalue()
|
|
101
|
+
print_json_error(e_message, err)
|
|
102
|
+
else:
|
|
103
|
+
typer.secho(
|
|
104
|
+
f"{err}",
|
|
105
|
+
fg=typer.colors.RED,
|
|
106
|
+
bold=True,
|
|
107
|
+
)
|
|
108
|
+
finally:
|
|
109
|
+
if suppress_output:
|
|
110
|
+
restore_output()
|
|
111
|
+
captured_output.close()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _stop_run(stub: ExecStub, run_id: int, output_format: str) -> None:
|
|
115
|
+
"""Stop a run."""
|
|
116
|
+
with unauthenticated_exc_handler():
|
|
117
|
+
response: StopRunResponse = stub.StopRun(request=StopRunRequest(run_id=run_id))
|
|
118
|
+
if response.success:
|
|
119
|
+
typer.secho(f"✅ Run {run_id} successfully stopped.", fg=typer.colors.GREEN)
|
|
120
|
+
if output_format == CliOutputFormat.JSON:
|
|
121
|
+
run_output = json.dumps(
|
|
122
|
+
{
|
|
123
|
+
"success": True,
|
|
124
|
+
"run-id": run_id,
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
restore_output()
|
|
128
|
+
Console().print_json(run_output)
|
|
129
|
+
else:
|
|
130
|
+
typer.secho(f"❌ Run {run_id} couldn't be stopped.", fg=typer.colors.RED)
|
flwr/cli/utils.py
CHANGED
|
@@ -14,13 +14,35 @@
|
|
|
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
|
|
23
|
+
from logging import DEBUG
|
|
19
24
|
from pathlib import Path
|
|
20
|
-
from typing import Callable, Optional, cast
|
|
25
|
+
from typing import Any, Callable, Optional, Union, cast
|
|
21
26
|
|
|
27
|
+
import grpc
|
|
22
28
|
import typer
|
|
23
29
|
|
|
30
|
+
from flwr.cli.cli_user_auth_interceptor import CliUserAuthInterceptor
|
|
31
|
+
from flwr.common.auth_plugin import CliAuthPlugin
|
|
32
|
+
from flwr.common.constant import AUTH_TYPE, CREDENTIALS_DIR, FLWR_DIR
|
|
33
|
+
from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel
|
|
34
|
+
from flwr.common.logger import log
|
|
35
|
+
|
|
36
|
+
from .config_utils import validate_certificate_in_federation_config
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
from flwr.ee import get_cli_auth_plugins
|
|
40
|
+
except ImportError:
|
|
41
|
+
|
|
42
|
+
def get_cli_auth_plugins() -> dict[str, type[CliAuthPlugin]]:
|
|
43
|
+
"""Return all CLI authentication plugins."""
|
|
44
|
+
raise NotImplementedError("No authentication plugins are currently supported.")
|
|
45
|
+
|
|
24
46
|
|
|
25
47
|
def prompt_text(
|
|
26
48
|
text: str,
|
|
@@ -126,13 +148,155 @@ def sanitize_project_name(name: str) -> str:
|
|
|
126
148
|
return sanitized_name
|
|
127
149
|
|
|
128
150
|
|
|
129
|
-
def get_sha256_hash(
|
|
151
|
+
def get_sha256_hash(file_path_or_int: Union[Path, int]) -> str:
|
|
130
152
|
"""Calculate the SHA-256 hash of a file."""
|
|
131
153
|
sha256 = hashlib.sha256()
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
154
|
+
if isinstance(file_path_or_int, Path):
|
|
155
|
+
with open(file_path_or_int, "rb") as f:
|
|
156
|
+
while True:
|
|
157
|
+
data = f.read(65536) # Read in 64kB blocks
|
|
158
|
+
if not data:
|
|
159
|
+
break
|
|
160
|
+
sha256.update(data)
|
|
161
|
+
elif isinstance(file_path_or_int, int):
|
|
162
|
+
sha256.update(str(file_path_or_int).encode())
|
|
138
163
|
return sha256.hexdigest()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def get_user_auth_config_path(root_dir: Path, federation: str) -> Path:
|
|
167
|
+
"""Return the path to the user auth config file.
|
|
168
|
+
|
|
169
|
+
Additionally, a `.gitignore` file will be created in the Flower directory to
|
|
170
|
+
include the `.credentials` folder to be excluded from git. If the `.gitignore`
|
|
171
|
+
file already exists, a warning will be displayed if the `.credentials` entry is
|
|
172
|
+
not found.
|
|
173
|
+
"""
|
|
174
|
+
# Locate the credentials directory
|
|
175
|
+
abs_flwr_dir = root_dir.absolute() / FLWR_DIR
|
|
176
|
+
credentials_dir = abs_flwr_dir / CREDENTIALS_DIR
|
|
177
|
+
credentials_dir.mkdir(parents=True, exist_ok=True)
|
|
178
|
+
|
|
179
|
+
# Determine the absolute path of the Flower directory for .gitignore
|
|
180
|
+
gitignore_path = abs_flwr_dir / ".gitignore"
|
|
181
|
+
credential_entry = CREDENTIALS_DIR
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
if gitignore_path.exists():
|
|
185
|
+
with open(gitignore_path, encoding="utf-8") as gitignore_file:
|
|
186
|
+
lines = gitignore_file.read().splitlines()
|
|
187
|
+
|
|
188
|
+
# Warn if .credentials is not already in .gitignore
|
|
189
|
+
if credential_entry not in lines:
|
|
190
|
+
typer.secho(
|
|
191
|
+
f"`.gitignore` exists, but `{credential_entry}` entry not found. "
|
|
192
|
+
"Consider adding it to your `.gitignore` to exclude Flower "
|
|
193
|
+
"credentials from git.",
|
|
194
|
+
fg=typer.colors.YELLOW,
|
|
195
|
+
bold=True,
|
|
196
|
+
)
|
|
197
|
+
else:
|
|
198
|
+
typer.secho(
|
|
199
|
+
f"Creating a new `.gitignore` with `{credential_entry}` entry...",
|
|
200
|
+
fg=typer.colors.BLUE,
|
|
201
|
+
)
|
|
202
|
+
# Create a new .gitignore with .credentials
|
|
203
|
+
with open(gitignore_path, "w", encoding="utf-8") as gitignore_file:
|
|
204
|
+
gitignore_file.write(f"{credential_entry}\n")
|
|
205
|
+
except Exception as err:
|
|
206
|
+
typer.secho(
|
|
207
|
+
"❌ An error occurred while handling `.gitignore.` "
|
|
208
|
+
f"Please check the permissions of `{gitignore_path}` and try again.",
|
|
209
|
+
fg=typer.colors.RED,
|
|
210
|
+
bold=True,
|
|
211
|
+
)
|
|
212
|
+
raise typer.Exit(code=1) from err
|
|
213
|
+
|
|
214
|
+
return credentials_dir / f"{federation}.json"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def try_obtain_cli_auth_plugin(
|
|
218
|
+
root_dir: Path,
|
|
219
|
+
federation: str,
|
|
220
|
+
auth_type: Optional[str] = None,
|
|
221
|
+
) -> Optional[CliAuthPlugin]:
|
|
222
|
+
"""Load the CLI-side user auth plugin for the given auth type."""
|
|
223
|
+
config_path = get_user_auth_config_path(root_dir, federation)
|
|
224
|
+
|
|
225
|
+
# Load the config file if it exists
|
|
226
|
+
config: dict[str, Any] = {}
|
|
227
|
+
if config_path.exists():
|
|
228
|
+
with config_path.open("r", encoding="utf-8") as file:
|
|
229
|
+
config = json.load(file)
|
|
230
|
+
# This is the case when the user auth is not enabled
|
|
231
|
+
elif auth_type is None:
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
# Get the auth type from the config if not provided
|
|
235
|
+
if auth_type is None:
|
|
236
|
+
if AUTH_TYPE not in config:
|
|
237
|
+
return None
|
|
238
|
+
auth_type = config[AUTH_TYPE]
|
|
239
|
+
|
|
240
|
+
# Retrieve auth plugin class and instantiate it
|
|
241
|
+
try:
|
|
242
|
+
all_plugins: dict[str, type[CliAuthPlugin]] = get_cli_auth_plugins()
|
|
243
|
+
auth_plugin_class = all_plugins[auth_type]
|
|
244
|
+
return auth_plugin_class(config_path)
|
|
245
|
+
except KeyError:
|
|
246
|
+
typer.echo(f"❌ Unknown user authentication type: {auth_type}")
|
|
247
|
+
raise typer.Exit(code=1) from None
|
|
248
|
+
except ImportError:
|
|
249
|
+
typer.echo("❌ No authentication plugins are currently supported.")
|
|
250
|
+
raise typer.Exit(code=1) from None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def init_channel(
|
|
254
|
+
app: Path, federation_config: dict[str, Any], auth_plugin: Optional[CliAuthPlugin]
|
|
255
|
+
) -> grpc.Channel:
|
|
256
|
+
"""Initialize gRPC channel to the Exec API."""
|
|
257
|
+
|
|
258
|
+
def on_channel_state_change(channel_connectivity: str) -> None:
|
|
259
|
+
"""Log channel connectivity."""
|
|
260
|
+
log(DEBUG, channel_connectivity)
|
|
261
|
+
|
|
262
|
+
insecure, root_certificates_bytes = validate_certificate_in_federation_config(
|
|
263
|
+
app, federation_config
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Initialize the CLI-side user auth interceptor
|
|
267
|
+
interceptors: list[grpc.UnaryUnaryClientInterceptor] = []
|
|
268
|
+
if auth_plugin is not None:
|
|
269
|
+
auth_plugin.load_tokens()
|
|
270
|
+
interceptors = CliUserAuthInterceptor(auth_plugin)
|
|
271
|
+
|
|
272
|
+
# Create the gRPC channel
|
|
273
|
+
channel = create_channel(
|
|
274
|
+
server_address=federation_config["address"],
|
|
275
|
+
insecure=insecure,
|
|
276
|
+
root_certificates=root_certificates_bytes,
|
|
277
|
+
max_message_length=GRPC_MAX_MESSAGE_LENGTH,
|
|
278
|
+
interceptors=interceptors or None,
|
|
279
|
+
)
|
|
280
|
+
channel.subscribe(on_channel_state_change)
|
|
281
|
+
return channel
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@contextmanager
|
|
285
|
+
def unauthenticated_exc_handler() -> Iterator[None]:
|
|
286
|
+
"""Context manager to handle gRPC UNAUTHENTICATED errors.
|
|
287
|
+
|
|
288
|
+
It catches grpc.RpcError exceptions with UNAUTHENTICATED status, informs the user,
|
|
289
|
+
and exits the application. All other exceptions will be allowed to escape.
|
|
290
|
+
"""
|
|
291
|
+
try:
|
|
292
|
+
yield
|
|
293
|
+
except grpc.RpcError as e:
|
|
294
|
+
if e.code() != grpc.StatusCode.UNAUTHENTICATED:
|
|
295
|
+
raise
|
|
296
|
+
typer.secho(
|
|
297
|
+
"❌ Authentication failed. Please run `flwr login`"
|
|
298
|
+
" to authenticate and try again.",
|
|
299
|
+
fg=typer.colors.RED,
|
|
300
|
+
bold=True,
|
|
301
|
+
)
|
|
302
|
+
raise typer.Exit(code=1) from None
|
flwr/client/app.py
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Flower client app."""
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
import signal
|
|
18
19
|
import subprocess
|
|
19
20
|
import sys
|
|
@@ -55,7 +56,7 @@ from flwr.common.constant import (
|
|
|
55
56
|
from flwr.common.logger import log, warn_deprecated_feature
|
|
56
57
|
from flwr.common.message import Error
|
|
57
58
|
from flwr.common.retry_invoker import RetryInvoker, RetryState, exponential
|
|
58
|
-
from flwr.common.typing import Fab, Run, UserConfig
|
|
59
|
+
from flwr.common.typing import Fab, Run, RunNotRunningException, UserConfig
|
|
59
60
|
from flwr.proto.clientappio_pb2_grpc import add_ClientAppIoServicer_to_server
|
|
60
61
|
from flwr.server.superlink.fleet.grpc_bidi.grpc_server import generic_create_grpc_server
|
|
61
62
|
from flwr.server.superlink.linkstate.utils import generate_rand_int_from_bytes
|
|
@@ -474,7 +475,7 @@ def start_client_internal(
|
|
|
474
475
|
|
|
475
476
|
run: Run = runs[run_id]
|
|
476
477
|
if get_fab is not None and run.fab_hash:
|
|
477
|
-
fab = get_fab(run.fab_hash)
|
|
478
|
+
fab = get_fab(run.fab_hash, run_id)
|
|
478
479
|
if not isolation:
|
|
479
480
|
# If `ClientApp` runs in the same process, install the FAB
|
|
480
481
|
install_from_fab(fab.content, flwr_path, True)
|
|
@@ -611,6 +612,16 @@ def start_client_internal(
|
|
|
611
612
|
send(reply_message)
|
|
612
613
|
log(INFO, "Sent reply")
|
|
613
614
|
|
|
615
|
+
except RunNotRunningException:
|
|
616
|
+
log(INFO, "")
|
|
617
|
+
log(
|
|
618
|
+
INFO,
|
|
619
|
+
"SuperNode aborted sending the reply message. "
|
|
620
|
+
"Run ID %s is not in `RUNNING` status.",
|
|
621
|
+
run_id,
|
|
622
|
+
)
|
|
623
|
+
log(INFO, "")
|
|
624
|
+
|
|
614
625
|
except StopIteration:
|
|
615
626
|
sleep_duration = 0
|
|
616
627
|
break
|
|
@@ -752,7 +763,7 @@ def _init_connection(transport: Optional[str], server_address: str) -> tuple[
|
|
|
752
763
|
Optional[Callable[[], Optional[int]]],
|
|
753
764
|
Optional[Callable[[], None]],
|
|
754
765
|
Optional[Callable[[int], Run]],
|
|
755
|
-
Optional[Callable[[str], Fab]],
|
|
766
|
+
Optional[Callable[[str, int], Fab]],
|
|
756
767
|
]
|
|
757
768
|
],
|
|
758
769
|
],
|
flwr/client/client.py
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Flower client (abstract base class)."""
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
# Needed to `Client` class can return a type of `Client` (not needed in py3.11+)
|
|
18
19
|
from __future__ import annotations
|
|
19
20
|
|
|
@@ -21,7 +22,6 @@ from abc import ABC
|
|
|
21
22
|
|
|
22
23
|
from flwr.common import (
|
|
23
24
|
Code,
|
|
24
|
-
Context,
|
|
25
25
|
EvaluateIns,
|
|
26
26
|
EvaluateRes,
|
|
27
27
|
FitIns,
|
|
@@ -33,14 +33,11 @@ from flwr.common import (
|
|
|
33
33
|
Parameters,
|
|
34
34
|
Status,
|
|
35
35
|
)
|
|
36
|
-
from flwr.common.logger import warn_deprecated_feature_with_example
|
|
37
36
|
|
|
38
37
|
|
|
39
38
|
class Client(ABC):
|
|
40
39
|
"""Abstract base class for Flower clients."""
|
|
41
40
|
|
|
42
|
-
_context: Context
|
|
43
|
-
|
|
44
41
|
def get_properties(self, ins: GetPropertiesIns) -> GetPropertiesRes:
|
|
45
42
|
"""Return set of client's properties.
|
|
46
43
|
|
|
@@ -142,34 +139,6 @@ class Client(ABC):
|
|
|
142
139
|
metrics={},
|
|
143
140
|
)
|
|
144
141
|
|
|
145
|
-
@property
|
|
146
|
-
def context(self) -> Context:
|
|
147
|
-
"""Getter for `Context` client attribute."""
|
|
148
|
-
warn_deprecated_feature_with_example(
|
|
149
|
-
"Accessing the context via the client's attribute is deprecated.",
|
|
150
|
-
example_message="Instead, pass it to the client's "
|
|
151
|
-
"constructor in your `client_fn()` which already "
|
|
152
|
-
"receives a context object.",
|
|
153
|
-
code_example="def client_fn(context: Context) -> Client:\n\n"
|
|
154
|
-
"\t\t# Your existing client_fn\n\n"
|
|
155
|
-
"\t\t# Pass `context` to the constructor\n"
|
|
156
|
-
"\t\treturn FlowerClient(context).to_client()",
|
|
157
|
-
)
|
|
158
|
-
return self._context
|
|
159
|
-
|
|
160
|
-
@context.setter
|
|
161
|
-
def context(self, context: Context) -> None:
|
|
162
|
-
"""Setter for `Context` client attribute."""
|
|
163
|
-
self._context = context
|
|
164
|
-
|
|
165
|
-
def get_context(self) -> Context:
|
|
166
|
-
"""Get the run context from this client."""
|
|
167
|
-
return self.context
|
|
168
|
-
|
|
169
|
-
def set_context(self, context: Context) -> None:
|
|
170
|
-
"""Apply a run context to this client."""
|
|
171
|
-
self.context = context
|
|
172
|
-
|
|
173
142
|
def to_client(self) -> Client:
|
|
174
143
|
"""Return client (itself)."""
|
|
175
144
|
return self
|
flwr/client/clientapp/app.py
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Flower ClientApp process."""
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
import argparse
|
|
18
19
|
import sys
|
|
19
20
|
import time
|
|
@@ -31,6 +32,7 @@ from flwr.common.constant import CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS, ErrorCo
|
|
|
31
32
|
from flwr.common.grpc import create_channel
|
|
32
33
|
from flwr.common.logger import log
|
|
33
34
|
from flwr.common.message import Error
|
|
35
|
+
from flwr.common.retry_invoker import _make_simple_grpc_retry_invoker, _wrap_stub
|
|
34
36
|
from flwr.common.serde import (
|
|
35
37
|
context_from_proto,
|
|
36
38
|
context_to_proto,
|
|
@@ -105,9 +107,9 @@ def run_clientapp( # pylint: disable=R0914
|
|
|
105
107
|
|
|
106
108
|
# Resolve directory where FABs are installed
|
|
107
109
|
flwr_dir_ = get_flwr_dir(flwr_dir)
|
|
108
|
-
|
|
109
110
|
try:
|
|
110
111
|
stub = ClientAppIoStub(channel)
|
|
112
|
+
_wrap_stub(stub, _make_simple_grpc_retry_invoker())
|
|
111
113
|
|
|
112
114
|
while True:
|
|
113
115
|
# If token is not set, loop until token is received from SuperNode
|
|
@@ -138,6 +140,7 @@ def run_clientapp( # pylint: disable=R0914
|
|
|
138
140
|
|
|
139
141
|
# Execute ClientApp
|
|
140
142
|
reply_message = client_app(message=message, context=context)
|
|
143
|
+
|
|
141
144
|
except Exception as ex: # pylint: disable=broad-exception-caught
|
|
142
145
|
# Don't update/change NodeState
|
|
143
146
|
|
|
@@ -252,12 +255,5 @@ def _parse_args_run_flwr_clientapp() -> argparse.ArgumentParser:
|
|
|
252
255
|
required=False,
|
|
253
256
|
help="Unique token generated by SuperNode for each ClientApp execution",
|
|
254
257
|
)
|
|
255
|
-
parser.add_argument(
|
|
256
|
-
"--root-certificates",
|
|
257
|
-
metavar="ROOT_CERT",
|
|
258
|
-
type=str,
|
|
259
|
-
help="Specifies the path to the PEM-encoded root certificate file for "
|
|
260
|
-
"establishing secure HTTPS connections.",
|
|
261
|
-
)
|
|
262
258
|
add_args_flwr_app_common(parser=parser)
|
|
263
259
|
return parser
|
flwr/client/clientapp/utils.py
CHANGED