flwr-nightly 1.14.0.dev20241214__py3-none-any.whl → 1.15.0.dev20250107__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/log.py +9 -7
- flwr/cli/login/login.py +1 -3
- flwr/cli/ls.py +25 -22
- 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 +1 -1
- 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 +1 -1
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
- 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/run.py +11 -18
- flwr/cli/stop.py +71 -32
- flwr/cli/utils.py +81 -25
- flwr/client/app.py +11 -1
- flwr/client/client.py +0 -32
- flwr/client/clientapp/app.py +3 -1
- flwr/client/grpc_rere_client/connection.py +10 -4
- flwr/client/message_handler/message_handler.py +0 -2
- flwr/client/numpy_client.py +0 -44
- flwr/client/supernode/app.py +1 -2
- flwr/common/logger.py +16 -1
- flwr/common/record/recordset.py +1 -1
- flwr/common/retry_invoker.py +3 -1
- flwr/common/secure_aggregation/crypto/symmetric_encryption.py +45 -0
- flwr/common/telemetry.py +13 -3
- flwr/server/app.py +8 -8
- flwr/server/run_serverapp.py +8 -9
- flwr/server/serverapp/app.py +17 -2
- flwr/server/superlink/driver/serverappio_servicer.py +9 -0
- flwr/server/superlink/fleet/message_handler/message_handler.py +1 -3
- flwr/server/superlink/fleet/vce/vce_api.py +2 -2
- flwr/server/superlink/linkstate/in_memory_linkstate.py +10 -2
- flwr/server/superlink/linkstate/linkstate.py +4 -0
- flwr/server/superlink/linkstate/sqlite_linkstate.py +6 -2
- flwr/server/superlink/simulation/simulationio_servicer.py +13 -0
- flwr/simulation/app.py +15 -4
- flwr/simulation/run_simulation.py +35 -22
- flwr/simulation/simulationio_connection.py +3 -0
- {flwr_nightly-1.14.0.dev20241214.dist-info → flwr_nightly-1.15.0.dev20250107.dist-info}/METADATA +2 -2
- {flwr_nightly-1.14.0.dev20241214.dist-info → flwr_nightly-1.15.0.dev20250107.dist-info}/RECORD +46 -46
- {flwr_nightly-1.14.0.dev20241214.dist-info → flwr_nightly-1.15.0.dev20250107.dist-info}/LICENSE +0 -0
- {flwr_nightly-1.14.0.dev20241214.dist-info → flwr_nightly-1.15.0.dev20250107.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.14.0.dev20241214.dist-info → flwr_nightly-1.15.0.dev20250107.dist-info}/entry_points.txt +0 -0
flwr/cli/log.py
CHANGED
@@ -34,7 +34,7 @@ from flwr.common.logger import log as logger
|
|
34
34
|
from flwr.proto.exec_pb2 import StreamLogsRequest # pylint: disable=E0611
|
35
35
|
from flwr.proto.exec_pb2_grpc import ExecStub
|
36
36
|
|
37
|
-
from .utils import init_channel, try_obtain_cli_auth_plugin
|
37
|
+
from .utils import init_channel, try_obtain_cli_auth_plugin, unauthenticated_exc_handler
|
38
38
|
|
39
39
|
|
40
40
|
def start_stream(
|
@@ -88,8 +88,9 @@ def stream_logs(
|
|
88
88
|
latest_timestamp = 0.0
|
89
89
|
res = None
|
90
90
|
try:
|
91
|
-
|
92
|
-
|
91
|
+
with unauthenticated_exc_handler():
|
92
|
+
for res in stub.StreamLogs(req, timeout=duration):
|
93
|
+
print(res.log_output, end="")
|
93
94
|
except grpc.RpcError as e:
|
94
95
|
# pylint: disable=E1101
|
95
96
|
if e.code() != grpc.StatusCode.DEADLINE_EXCEEDED:
|
@@ -109,9 +110,10 @@ def print_logs(run_id: int, channel: grpc.Channel, timeout: int) -> None:
|
|
109
110
|
try:
|
110
111
|
while True:
|
111
112
|
try:
|
112
|
-
|
113
|
-
|
114
|
-
|
113
|
+
with unauthenticated_exc_handler():
|
114
|
+
# Enforce timeout for graceful exit
|
115
|
+
for res in stub.StreamLogs(req, timeout=timeout):
|
116
|
+
print(res.log_output)
|
115
117
|
except grpc.RpcError as e:
|
116
118
|
# pylint: disable=E1101
|
117
119
|
if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
|
@@ -170,7 +172,7 @@ def _log_with_exec_api(
|
|
170
172
|
run_id: int,
|
171
173
|
stream: bool,
|
172
174
|
) -> None:
|
173
|
-
auth_plugin = try_obtain_cli_auth_plugin(app, federation
|
175
|
+
auth_plugin = try_obtain_cli_auth_plugin(app, federation)
|
174
176
|
channel = init_channel(app, federation_config, auth_plugin)
|
175
177
|
|
176
178
|
if stream:
|
flwr/cli/login/login.py
CHANGED
@@ -65,9 +65,7 @@ def login( # pylint: disable=R0914
|
|
65
65
|
|
66
66
|
# Get the auth plugin
|
67
67
|
auth_type = login_response.login_details.get(AUTH_TYPE)
|
68
|
-
auth_plugin = try_obtain_cli_auth_plugin(
|
69
|
-
app, federation, federation_config, auth_type
|
70
|
-
)
|
68
|
+
auth_plugin = try_obtain_cli_auth_plugin(app, federation, auth_type)
|
71
69
|
if auth_plugin is None:
|
72
70
|
typer.secho(
|
73
71
|
f'❌ Authentication type "{auth_type}" not found',
|
flwr/cli/ls.py
CHANGED
@@ -19,13 +19,12 @@ import io
|
|
19
19
|
import json
|
20
20
|
from datetime import datetime, timedelta
|
21
21
|
from pathlib import Path
|
22
|
-
from typing import Annotated, Optional
|
22
|
+
from typing import Annotated, Optional
|
23
23
|
|
24
24
|
import typer
|
25
25
|
from rich.console import Console
|
26
26
|
from rich.table import Table
|
27
27
|
from rich.text import Text
|
28
|
-
from typer import Exit
|
29
28
|
|
30
29
|
from flwr.cli.config_utils import (
|
31
30
|
exit_if_no_address,
|
@@ -35,7 +34,7 @@ from flwr.cli.config_utils import (
|
|
35
34
|
)
|
36
35
|
from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat, SubStatus
|
37
36
|
from flwr.common.date import format_timedelta, isoformat8601_utc
|
38
|
-
from flwr.common.logger import
|
37
|
+
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
39
38
|
from flwr.common.serde import run_from_proto
|
40
39
|
from flwr.common.typing import Run
|
41
40
|
from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
|
@@ -44,7 +43,7 @@ from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
|
|
44
43
|
)
|
45
44
|
from flwr.proto.exec_pb2_grpc import ExecStub
|
46
45
|
|
47
|
-
from .utils import init_channel, try_obtain_cli_auth_plugin
|
46
|
+
from .utils import init_channel, try_obtain_cli_auth_plugin, unauthenticated_exc_handler
|
48
47
|
|
49
48
|
_RunListType = tuple[int, str, str, str, str, str, str, str, str]
|
50
49
|
|
@@ -81,13 +80,25 @@ def ls( # pylint: disable=too-many-locals, too-many-branches
|
|
81
80
|
),
|
82
81
|
] = CliOutputFormat.DEFAULT,
|
83
82
|
) -> None:
|
84
|
-
"""List runs.
|
83
|
+
"""List the details of one provided run ID or all runs in a Flower federation.
|
84
|
+
|
85
|
+
The following details are displayed:
|
86
|
+
|
87
|
+
- **Run ID:** Unique identifier for the run.
|
88
|
+
- **FAB:** Name of the FAB associated with the run (``{FAB_ID} (v{FAB_VERSION})``).
|
89
|
+
- **Status:** Current status of the run (pending, starting, running, finished).
|
90
|
+
- **Elapsed:** Time elapsed since the run started (``HH:MM:SS``).
|
91
|
+
- **Created At:** Timestamp when the run was created.
|
92
|
+
- **Running At:** Timestamp when the run started running.
|
93
|
+
- **Finished At:** Timestamp when the run finished.
|
94
|
+
|
95
|
+
All timestamps follow ISO 8601, UTC and are formatted as ``YYYY-MM-DD HH:MM:SSZ``.
|
96
|
+
"""
|
85
97
|
suppress_output = output_format == CliOutputFormat.JSON
|
86
98
|
captured_output = io.StringIO()
|
87
99
|
try:
|
88
100
|
if suppress_output:
|
89
101
|
redirect_output(captured_output)
|
90
|
-
|
91
102
|
# Load and validate federation config
|
92
103
|
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
93
104
|
|
@@ -104,7 +115,7 @@ def ls( # pylint: disable=too-many-locals, too-many-branches
|
|
104
115
|
raise ValueError(
|
105
116
|
"The options '--runs' and '--run-id' are mutually exclusive."
|
106
117
|
)
|
107
|
-
auth_plugin = try_obtain_cli_auth_plugin(app, federation
|
118
|
+
auth_plugin = try_obtain_cli_auth_plugin(app, federation)
|
108
119
|
channel = init_channel(app, federation_config, auth_plugin)
|
109
120
|
stub = ExecStub(channel)
|
110
121
|
|
@@ -120,6 +131,8 @@ def ls( # pylint: disable=too-many-locals, too-many-branches
|
|
120
131
|
_list_runs(stub, output_format)
|
121
132
|
|
122
133
|
except ValueError as err:
|
134
|
+
if suppress_output:
|
135
|
+
redirect_output(captured_output)
|
123
136
|
typer.secho(
|
124
137
|
f"❌ {err}",
|
125
138
|
fg=typer.colors.RED,
|
@@ -132,7 +145,7 @@ def ls( # pylint: disable=too-many-locals, too-many-branches
|
|
132
145
|
if suppress_output:
|
133
146
|
restore_output()
|
134
147
|
e_message = captured_output.getvalue()
|
135
|
-
|
148
|
+
print_json_error(e_message, err)
|
136
149
|
else:
|
137
150
|
typer.secho(
|
138
151
|
f"{err}",
|
@@ -283,7 +296,8 @@ def _list_runs(
|
|
283
296
|
output_format: str = CliOutputFormat.DEFAULT,
|
284
297
|
) -> None:
|
285
298
|
"""List all runs."""
|
286
|
-
|
299
|
+
with unauthenticated_exc_handler():
|
300
|
+
res: ListRunsResponse = stub.ListRuns(ListRunsRequest())
|
287
301
|
run_dict = {run_id: run_from_proto(proto) for run_id, proto in res.run_dict.items()}
|
288
302
|
|
289
303
|
formatted_runs = _format_runs(run_dict, res.now)
|
@@ -299,7 +313,8 @@ def _display_one_run(
|
|
299
313
|
output_format: str = CliOutputFormat.DEFAULT,
|
300
314
|
) -> None:
|
301
315
|
"""Display information about a specific run."""
|
302
|
-
|
316
|
+
with unauthenticated_exc_handler():
|
317
|
+
res: ListRunsResponse = stub.ListRuns(ListRunsRequest(run_id=run_id))
|
303
318
|
if not res.run_dict:
|
304
319
|
raise ValueError(f"Run ID {run_id} not found")
|
305
320
|
|
@@ -310,15 +325,3 @@ def _display_one_run(
|
|
310
325
|
Console().print_json(_to_json(formatted_runs))
|
311
326
|
else:
|
312
327
|
Console().print(_to_table(formatted_runs))
|
313
|
-
|
314
|
-
|
315
|
-
def _print_json_error(msg: str, e: Union[Exit, Exception]) -> None:
|
316
|
-
"""Print error message as JSON."""
|
317
|
-
Console().print_json(
|
318
|
-
json.dumps(
|
319
|
-
{
|
320
|
-
"success": False,
|
321
|
-
"error-message": remove_emojis(str(msg) + "\n" + str(e)),
|
322
|
-
}
|
323
|
-
)
|
324
|
-
)
|
@@ -8,10 +8,10 @@ version = "1.0.0"
|
|
8
8
|
description = ""
|
9
9
|
license = "Apache-2.0"
|
10
10
|
dependencies = [
|
11
|
-
"flwr[simulation]>=1.
|
11
|
+
"flwr[simulation]>=1.14.0",
|
12
12
|
"flwr-datasets[vision]>=0.3.0",
|
13
|
-
"torch==2.
|
14
|
-
"torchvision==0.
|
13
|
+
"torch==2.5.1",
|
14
|
+
"torchvision==0.20.1",
|
15
15
|
]
|
16
16
|
|
17
17
|
[tool.hatch.build.targets.wheel]
|
flwr/cli/run/run.py
CHANGED
@@ -19,7 +19,7 @@ import io
|
|
19
19
|
import json
|
20
20
|
import subprocess
|
21
21
|
from pathlib import Path
|
22
|
-
from typing import Annotated, Any, Optional
|
22
|
+
from typing import Annotated, Any, Optional
|
23
23
|
|
24
24
|
import typer
|
25
25
|
from rich.console import Console
|
@@ -37,7 +37,7 @@ from flwr.common.config import (
|
|
37
37
|
user_config_to_configsrecord,
|
38
38
|
)
|
39
39
|
from flwr.common.constant import CliOutputFormat
|
40
|
-
from flwr.common.logger import
|
40
|
+
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
41
41
|
from flwr.common.serde import (
|
42
42
|
configs_record_to_proto,
|
43
43
|
fab_to_proto,
|
@@ -48,7 +48,11 @@ from flwr.proto.exec_pb2 import StartRunRequest # pylint: disable=E0611
|
|
48
48
|
from flwr.proto.exec_pb2_grpc import ExecStub
|
49
49
|
|
50
50
|
from ..log import start_stream
|
51
|
-
from ..utils import
|
51
|
+
from ..utils import (
|
52
|
+
init_channel,
|
53
|
+
try_obtain_cli_auth_plugin,
|
54
|
+
unauthenticated_exc_handler,
|
55
|
+
)
|
52
56
|
|
53
57
|
CONN_REFRESH_PERIOD = 60 # Connection refresh period for log streaming (seconds)
|
54
58
|
|
@@ -122,7 +126,7 @@ def run(
|
|
122
126
|
if suppress_output:
|
123
127
|
restore_output()
|
124
128
|
e_message = captured_output.getvalue()
|
125
|
-
|
129
|
+
print_json_error(e_message, err)
|
126
130
|
else:
|
127
131
|
typer.secho(
|
128
132
|
f"{err}",
|
@@ -144,7 +148,7 @@ def _run_with_exec_api(
|
|
144
148
|
stream: bool,
|
145
149
|
output_format: str,
|
146
150
|
) -> None:
|
147
|
-
auth_plugin = try_obtain_cli_auth_plugin(app, federation
|
151
|
+
auth_plugin = try_obtain_cli_auth_plugin(app, federation)
|
148
152
|
channel = init_channel(app, federation_config, auth_plugin)
|
149
153
|
stub = ExecStub(channel)
|
150
154
|
|
@@ -166,7 +170,8 @@ def _run_with_exec_api(
|
|
166
170
|
override_config=user_config_to_proto(parse_config_args(config_overrides)),
|
167
171
|
federation_options=configs_record_to_proto(c_record),
|
168
172
|
)
|
169
|
-
|
173
|
+
with unauthenticated_exc_handler():
|
174
|
+
res = stub.StartRun(req)
|
170
175
|
|
171
176
|
if res.HasField("run_id"):
|
172
177
|
typer.secho(f"🎊 Successfully started run {res.run_id}", fg=typer.colors.GREEN)
|
@@ -239,15 +244,3 @@ def _run_without_exec_api(
|
|
239
244
|
check=True,
|
240
245
|
text=True,
|
241
246
|
)
|
242
|
-
|
243
|
-
|
244
|
-
def _print_json_error(msg: str, e: Union[typer.Exit, Exception]) -> None:
|
245
|
-
"""Print error message as JSON."""
|
246
|
-
Console().print_json(
|
247
|
-
json.dumps(
|
248
|
-
{
|
249
|
-
"success": False,
|
250
|
-
"error-message": remove_emojis(str(msg) + "\n" + str(e)),
|
251
|
-
}
|
252
|
-
)
|
253
|
-
)
|
flwr/cli/stop.py
CHANGED
@@ -15,10 +15,13 @@
|
|
15
15
|
"""Flower command line interface `stop` command."""
|
16
16
|
|
17
17
|
|
18
|
+
import io
|
19
|
+
import json
|
18
20
|
from pathlib import Path
|
19
21
|
from typing import Annotated, Optional
|
20
22
|
|
21
23
|
import typer
|
24
|
+
from rich.console import Console
|
22
25
|
|
23
26
|
from flwr.cli.config_utils import (
|
24
27
|
exit_if_no_address,
|
@@ -26,14 +29,15 @@ from flwr.cli.config_utils import (
|
|
26
29
|
process_loaded_project_config,
|
27
30
|
validate_federation_in_project_config,
|
28
31
|
)
|
29
|
-
from flwr.common.constant import FAB_CONFIG_FILE
|
32
|
+
from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
|
33
|
+
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
30
34
|
from flwr.proto.exec_pb2 import StopRunRequest, StopRunResponse # pylint: disable=E0611
|
31
35
|
from flwr.proto.exec_pb2_grpc import ExecStub
|
32
36
|
|
33
|
-
from .utils import init_channel, try_obtain_cli_auth_plugin
|
37
|
+
from .utils import init_channel, try_obtain_cli_auth_plugin, unauthenticated_exc_handler
|
34
38
|
|
35
39
|
|
36
|
-
def stop(
|
40
|
+
def stop( # pylint: disable=R0914
|
37
41
|
run_id: Annotated[ # pylint: disable=unused-argument
|
38
42
|
int,
|
39
43
|
typer.Argument(help="The Flower run ID to stop"),
|
@@ -46,46 +50,81 @@ def stop(
|
|
46
50
|
Optional[str],
|
47
51
|
typer.Argument(help="Name of the federation"),
|
48
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,
|
49
61
|
) -> None:
|
50
62
|
"""Stop a run."""
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
pyproject_path = app / FAB_CONFIG_FILE if app else None
|
55
|
-
config, errors, warnings = load_and_validate(path=pyproject_path)
|
56
|
-
config = process_loaded_project_config(config, errors, warnings)
|
57
|
-
federation, federation_config = validate_federation_in_project_config(
|
58
|
-
federation, config
|
59
|
-
)
|
60
|
-
exit_if_no_address(federation_config, "stop")
|
61
|
-
|
63
|
+
suppress_output = output_format == CliOutputFormat.JSON
|
64
|
+
captured_output = io.StringIO()
|
62
65
|
try:
|
63
|
-
|
64
|
-
|
65
|
-
stub = ExecStub(channel) # pylint: disable=unused-variable # noqa: F841
|
66
|
+
if suppress_output:
|
67
|
+
redirect_output(captured_output)
|
66
68
|
|
67
|
-
|
68
|
-
|
69
|
+
# Load and validate federation config
|
70
|
+
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
69
71
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
75
77
|
)
|
76
|
-
|
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
|
+
)
|
77
108
|
finally:
|
78
|
-
|
109
|
+
if suppress_output:
|
110
|
+
restore_output()
|
111
|
+
captured_output.close()
|
79
112
|
|
80
113
|
|
81
|
-
def _stop_run(
|
82
|
-
stub: ExecStub, # pylint: disable=unused-argument
|
83
|
-
run_id: int, # pylint: disable=unused-argument
|
84
|
-
) -> None:
|
114
|
+
def _stop_run(stub: ExecStub, run_id: int, output_format: str) -> None:
|
85
115
|
"""Stop a run."""
|
86
|
-
|
87
|
-
|
116
|
+
with unauthenticated_exc_handler():
|
117
|
+
response: StopRunResponse = stub.StopRun(request=StopRunRequest(run_id=run_id))
|
88
118
|
if response.success:
|
89
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)
|
90
129
|
else:
|
91
130
|
typer.secho(f"❌ Run {run_id} couldn't be stopped.", fg=typer.colors.RED)
|
flwr/cli/utils.py
CHANGED
@@ -18,15 +18,16 @@
|
|
18
18
|
import hashlib
|
19
19
|
import json
|
20
20
|
import re
|
21
|
+
from collections.abc import Iterator
|
22
|
+
from contextlib import contextmanager
|
21
23
|
from logging import DEBUG
|
22
24
|
from pathlib import Path
|
23
|
-
from typing import Any, Callable, Optional, cast
|
25
|
+
from typing import Any, Callable, Optional, Union, cast
|
24
26
|
|
25
27
|
import grpc
|
26
28
|
import typer
|
27
29
|
|
28
30
|
from flwr.cli.cli_user_auth_interceptor import CliUserAuthInterceptor
|
29
|
-
from flwr.common.address import parse_address
|
30
31
|
from flwr.common.auth_plugin import CliAuthPlugin
|
31
32
|
from flwr.common.constant import AUTH_TYPE, CREDENTIALS_DIR, FLWR_DIR
|
32
33
|
from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel
|
@@ -147,45 +148,79 @@ def sanitize_project_name(name: str) -> str:
|
|
147
148
|
return sanitized_name
|
148
149
|
|
149
150
|
|
150
|
-
def get_sha256_hash(
|
151
|
+
def get_sha256_hash(file_path_or_int: Union[Path, int]) -> str:
|
151
152
|
"""Calculate the SHA-256 hash of a file."""
|
152
153
|
sha256 = hashlib.sha256()
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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())
|
159
163
|
return sha256.hexdigest()
|
160
164
|
|
161
165
|
|
162
|
-
def get_user_auth_config_path(
|
163
|
-
|
164
|
-
) -> Path:
|
165
|
-
"""Return the path to the user auth config file."""
|
166
|
-
# Parse the server address
|
167
|
-
parsed_addr = parse_address(server_address)
|
168
|
-
if parsed_addr is None:
|
169
|
-
raise ValueError(f"Invalid server address: {server_address}")
|
170
|
-
host, port, is_v6 = parsed_addr
|
171
|
-
formatted_addr = f"[{host}]_{port}" if is_v6 else f"{host}_{port}"
|
166
|
+
def get_user_auth_config_path(root_dir: Path, federation: str) -> Path:
|
167
|
+
"""Return the path to the user auth config file.
|
172
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
|
+
"""
|
173
174
|
# Locate the credentials directory
|
174
|
-
|
175
|
+
abs_flwr_dir = root_dir.absolute() / FLWR_DIR
|
176
|
+
credentials_dir = abs_flwr_dir / CREDENTIALS_DIR
|
175
177
|
credentials_dir.mkdir(parents=True, exist_ok=True)
|
176
|
-
|
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"
|
177
215
|
|
178
216
|
|
179
217
|
def try_obtain_cli_auth_plugin(
|
180
218
|
root_dir: Path,
|
181
219
|
federation: str,
|
182
|
-
federation_config: dict[str, Any],
|
183
220
|
auth_type: Optional[str] = None,
|
184
221
|
) -> Optional[CliAuthPlugin]:
|
185
222
|
"""Load the CLI-side user auth plugin for the given auth type."""
|
186
|
-
config_path = get_user_auth_config_path(
|
187
|
-
root_dir, federation, federation_config["address"]
|
188
|
-
)
|
223
|
+
config_path = get_user_auth_config_path(root_dir, federation)
|
189
224
|
|
190
225
|
# Load the config file if it exists
|
191
226
|
config: dict[str, Any] = {}
|
@@ -244,3 +279,24 @@ def init_channel(
|
|
244
279
|
)
|
245
280
|
channel.subscribe(on_channel_state_change)
|
246
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
@@ -56,7 +56,7 @@ from flwr.common.constant import (
|
|
56
56
|
from flwr.common.logger import log, warn_deprecated_feature
|
57
57
|
from flwr.common.message import Error
|
58
58
|
from flwr.common.retry_invoker import RetryInvoker, RetryState, exponential
|
59
|
-
from flwr.common.typing import Fab, Run, UserConfig
|
59
|
+
from flwr.common.typing import Fab, Run, RunNotRunningException, UserConfig
|
60
60
|
from flwr.proto.clientappio_pb2_grpc import add_ClientAppIoServicer_to_server
|
61
61
|
from flwr.server.superlink.fleet.grpc_bidi.grpc_server import generic_create_grpc_server
|
62
62
|
from flwr.server.superlink.linkstate.utils import generate_rand_int_from_bytes
|
@@ -612,6 +612,16 @@ def start_client_internal(
|
|
612
612
|
send(reply_message)
|
613
613
|
log(INFO, "Sent reply")
|
614
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
|
+
|
615
625
|
except StopIteration:
|
616
626
|
sleep_duration = 0
|
617
627
|
break
|