flwr-nightly 1.13.0.dev20241106__py3-none-any.whl → 1.13.0.dev20241117__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.
Potentially problematic release.
This version of flwr-nightly might be problematic. Click here for more details.
- flwr/cli/app.py +2 -0
- flwr/cli/build.py +37 -0
- flwr/cli/install.py +5 -3
- flwr/cli/ls.py +228 -0
- flwr/cli/run/run.py +16 -5
- flwr/client/app.py +68 -19
- flwr/client/clientapp/app.py +51 -35
- flwr/client/grpc_rere_client/connection.py +2 -12
- flwr/client/nodestate/__init__.py +25 -0
- flwr/client/nodestate/in_memory_nodestate.py +38 -0
- flwr/client/nodestate/nodestate.py +30 -0
- flwr/client/nodestate/nodestate_factory.py +37 -0
- flwr/client/rest_client/connection.py +4 -14
- flwr/client/supernode/app.py +57 -53
- flwr/common/args.py +148 -0
- flwr/common/config.py +10 -0
- flwr/common/constant.py +21 -7
- flwr/common/date.py +18 -0
- flwr/common/logger.py +6 -2
- flwr/common/object_ref.py +47 -16
- flwr/common/serde.py +10 -0
- flwr/common/typing.py +32 -11
- flwr/proto/exec_pb2.py +23 -17
- flwr/proto/exec_pb2.pyi +50 -20
- flwr/proto/exec_pb2_grpc.py +34 -0
- flwr/proto/exec_pb2_grpc.pyi +13 -0
- flwr/proto/run_pb2.py +32 -27
- flwr/proto/run_pb2.pyi +44 -1
- 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 +83 -87
- flwr/server/driver/driver.py +1 -1
- flwr/server/driver/grpc_driver.py +6 -20
- flwr/server/driver/inmemory_driver.py +1 -3
- flwr/server/run_serverapp.py +8 -238
- flwr/server/serverapp/app.py +44 -89
- flwr/server/strategy/aggregate.py +4 -4
- flwr/server/superlink/fleet/rest_rere/rest_api.py +10 -9
- flwr/server/superlink/linkstate/in_memory_linkstate.py +76 -62
- flwr/server/superlink/linkstate/linkstate.py +24 -9
- flwr/server/superlink/linkstate/sqlite_linkstate.py +87 -128
- flwr/server/superlink/linkstate/utils.py +191 -32
- flwr/server/superlink/simulation/simulationio_servicer.py +22 -1
- flwr/simulation/__init__.py +3 -1
- flwr/simulation/app.py +245 -352
- flwr/simulation/legacy_app.py +402 -0
- flwr/simulation/run_simulation.py +8 -19
- flwr/simulation/simulationio_connection.py +2 -2
- flwr/superexec/deployment.py +13 -7
- flwr/superexec/exec_servicer.py +32 -3
- flwr/superexec/executor.py +4 -3
- flwr/superexec/simulation.py +52 -145
- {flwr_nightly-1.13.0.dev20241106.dist-info → flwr_nightly-1.13.0.dev20241117.dist-info}/METADATA +10 -7
- {flwr_nightly-1.13.0.dev20241106.dist-info → flwr_nightly-1.13.0.dev20241117.dist-info}/RECORD +58 -51
- {flwr_nightly-1.13.0.dev20241106.dist-info → flwr_nightly-1.13.0.dev20241117.dist-info}/entry_points.txt +1 -0
- {flwr_nightly-1.13.0.dev20241106.dist-info → flwr_nightly-1.13.0.dev20241117.dist-info}/LICENSE +0 -0
- {flwr_nightly-1.13.0.dev20241106.dist-info → flwr_nightly-1.13.0.dev20241117.dist-info}/WHEEL +0 -0
flwr/cli/app.py
CHANGED
|
@@ -20,6 +20,7 @@ from typer.main import get_command
|
|
|
20
20
|
from .build import build
|
|
21
21
|
from .install import install
|
|
22
22
|
from .log import log
|
|
23
|
+
from .ls import ls
|
|
23
24
|
from .new import new
|
|
24
25
|
from .run import run
|
|
25
26
|
|
|
@@ -37,6 +38,7 @@ app.command()(run)
|
|
|
37
38
|
app.command()(build)
|
|
38
39
|
app.command()(install)
|
|
39
40
|
app.command()(log)
|
|
41
|
+
app.command()(ls)
|
|
40
42
|
|
|
41
43
|
typer_click_object = get_command(app)
|
|
42
44
|
|
flwr/cli/build.py
CHANGED
|
@@ -19,14 +19,18 @@ import os
|
|
|
19
19
|
import shutil
|
|
20
20
|
import tempfile
|
|
21
21
|
import zipfile
|
|
22
|
+
from logging import DEBUG, ERROR
|
|
22
23
|
from pathlib import Path
|
|
23
24
|
from typing import Annotated, Any, Optional, Union
|
|
24
25
|
|
|
25
26
|
import pathspec
|
|
26
27
|
import tomli_w
|
|
27
28
|
import typer
|
|
29
|
+
from hatchling.builders.wheel import WheelBuilder
|
|
30
|
+
from hatchling.metadata.core import ProjectMetadata
|
|
28
31
|
|
|
29
32
|
from flwr.common.constant import FAB_ALLOWED_EXTENSIONS, FAB_DATE, FAB_HASH_TRUNCATION
|
|
33
|
+
from flwr.common.logger import log
|
|
30
34
|
|
|
31
35
|
from .config_utils import load_and_validate
|
|
32
36
|
from .utils import is_valid_project_name
|
|
@@ -51,6 +55,27 @@ def get_fab_filename(conf: dict[str, Any], fab_hash: str) -> str:
|
|
|
51
55
|
return f"{publisher}.{name}.{version}.{fab_hash_truncated}.fab"
|
|
52
56
|
|
|
53
57
|
|
|
58
|
+
def _build_app_wheel(app: Path) -> Path:
|
|
59
|
+
"""Build app as a wheel and return its path."""
|
|
60
|
+
# Path to your project directory
|
|
61
|
+
app_dir = str(app.resolve())
|
|
62
|
+
try:
|
|
63
|
+
|
|
64
|
+
# Initialize the WheelBuilder
|
|
65
|
+
builder = WheelBuilder(
|
|
66
|
+
app_dir, metadata=ProjectMetadata(root=app_dir, plugin_manager=None)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Build
|
|
70
|
+
whl_path = Path(next(builder.build(directory=app_dir)))
|
|
71
|
+
log(DEBUG, "Wheel succesfully built: %s", str(whl_path))
|
|
72
|
+
except Exception as ex:
|
|
73
|
+
log(ERROR, "Exception encountered when building wheel.", exc_info=ex)
|
|
74
|
+
raise typer.Exit(code=1) from ex
|
|
75
|
+
|
|
76
|
+
return whl_path
|
|
77
|
+
|
|
78
|
+
|
|
54
79
|
# pylint: disable=too-many-locals, too-many-statements
|
|
55
80
|
def build(
|
|
56
81
|
app: Annotated[
|
|
@@ -106,6 +131,12 @@ def build(
|
|
|
106
131
|
bold=True,
|
|
107
132
|
)
|
|
108
133
|
|
|
134
|
+
# Build wheel
|
|
135
|
+
whl_path = _build_app_wheel(app)
|
|
136
|
+
|
|
137
|
+
# Add path to .whl to `[tool.flwr.app]`
|
|
138
|
+
conf["tool"]["flwr"]["app"]["whl"] = str(whl_path.name)
|
|
139
|
+
|
|
109
140
|
# Load .gitignore rules if present
|
|
110
141
|
ignore_spec = _load_gitignore(app)
|
|
111
142
|
|
|
@@ -137,6 +168,9 @@ def build(
|
|
|
137
168
|
and f.name != "pyproject.toml" # Exclude the original pyproject.toml
|
|
138
169
|
]
|
|
139
170
|
|
|
171
|
+
# Include FAB .whl
|
|
172
|
+
all_files.append(whl_path)
|
|
173
|
+
|
|
140
174
|
for file_path in all_files:
|
|
141
175
|
# Read the file content manually
|
|
142
176
|
with open(file_path, "rb") as f:
|
|
@@ -153,6 +187,9 @@ def build(
|
|
|
153
187
|
# Add CONTENT and CONTENT.jwt to the zip file
|
|
154
188
|
write_to_zip(fab_file, ".info/CONTENT", list_file_content)
|
|
155
189
|
|
|
190
|
+
# Erase FAB .whl in app directory
|
|
191
|
+
whl_path.unlink()
|
|
192
|
+
|
|
156
193
|
# Get hash of FAB file
|
|
157
194
|
content = Path(temp_filename).read_bytes()
|
|
158
195
|
fab_hash = hashlib.sha256(content).hexdigest()
|
flwr/cli/install.py
CHANGED
|
@@ -188,23 +188,25 @@ def validate_and_install(
|
|
|
188
188
|
else:
|
|
189
189
|
shutil.copy2(item, install_dir / item.name)
|
|
190
190
|
|
|
191
|
+
whl_file = config["tool"]["flwr"]["app"]["whl"]
|
|
192
|
+
install_whl = install_dir / whl_file
|
|
191
193
|
try:
|
|
192
194
|
subprocess.run(
|
|
193
|
-
["pip", "install", "
|
|
195
|
+
["pip", "install", "--no-deps", install_whl],
|
|
194
196
|
capture_output=True,
|
|
195
197
|
text=True,
|
|
196
198
|
check=True,
|
|
197
199
|
)
|
|
198
200
|
except subprocess.CalledProcessError as e:
|
|
199
201
|
typer.secho(
|
|
200
|
-
f"❌ Failed to
|
|
202
|
+
f"❌ Failed to install {project_name}:\n{e.stderr}",
|
|
201
203
|
fg=typer.colors.RED,
|
|
202
204
|
bold=True,
|
|
203
205
|
)
|
|
204
206
|
raise typer.Exit(code=1) from e
|
|
205
207
|
|
|
206
208
|
typer.secho(
|
|
207
|
-
f"🎊 Successfully installed {project_name}
|
|
209
|
+
f"🎊 Successfully installed {project_name}.",
|
|
208
210
|
fg=typer.colors.GREEN,
|
|
209
211
|
bold=True,
|
|
210
212
|
)
|
flwr/cli/ls.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
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 `ls` command."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from datetime import datetime, timedelta
|
|
19
|
+
from logging import DEBUG
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Annotated, Any, Optional
|
|
22
|
+
|
|
23
|
+
import grpc
|
|
24
|
+
import typer
|
|
25
|
+
from rich.console import Console
|
|
26
|
+
from rich.table import Table
|
|
27
|
+
from rich.text import Text
|
|
28
|
+
|
|
29
|
+
from flwr.cli.config_utils import (
|
|
30
|
+
load_and_validate,
|
|
31
|
+
validate_certificate_in_federation_config,
|
|
32
|
+
validate_federation_in_project_config,
|
|
33
|
+
validate_project_config,
|
|
34
|
+
)
|
|
35
|
+
from flwr.common.constant import FAB_CONFIG_FILE, SubStatus
|
|
36
|
+
from flwr.common.date import format_timedelta, isoformat8601_utc
|
|
37
|
+
from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel
|
|
38
|
+
from flwr.common.logger import log
|
|
39
|
+
from flwr.common.serde import run_from_proto
|
|
40
|
+
from flwr.common.typing import Run
|
|
41
|
+
from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
|
|
42
|
+
ListRunsRequest,
|
|
43
|
+
ListRunsResponse,
|
|
44
|
+
)
|
|
45
|
+
from flwr.proto.exec_pb2_grpc import ExecStub
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def ls(
|
|
49
|
+
app: Annotated[
|
|
50
|
+
Path,
|
|
51
|
+
typer.Argument(help="Path of the Flower project"),
|
|
52
|
+
] = Path("."),
|
|
53
|
+
federation: Annotated[
|
|
54
|
+
Optional[str],
|
|
55
|
+
typer.Argument(help="Name of the federation"),
|
|
56
|
+
] = None,
|
|
57
|
+
runs: Annotated[
|
|
58
|
+
bool,
|
|
59
|
+
typer.Option(
|
|
60
|
+
"--runs",
|
|
61
|
+
help="List all runs",
|
|
62
|
+
),
|
|
63
|
+
] = False,
|
|
64
|
+
run_id: Annotated[
|
|
65
|
+
Optional[int],
|
|
66
|
+
typer.Option(
|
|
67
|
+
"--run-id",
|
|
68
|
+
help="Specific run ID to display",
|
|
69
|
+
),
|
|
70
|
+
] = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""List runs."""
|
|
73
|
+
# Load and validate federation config
|
|
74
|
+
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
|
75
|
+
|
|
76
|
+
pyproject_path = app / FAB_CONFIG_FILE if app else None
|
|
77
|
+
config, errors, warnings = load_and_validate(path=pyproject_path)
|
|
78
|
+
config = validate_project_config(config, errors, warnings)
|
|
79
|
+
federation, federation_config = validate_federation_in_project_config(
|
|
80
|
+
federation, config
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if "address" not in federation_config:
|
|
84
|
+
typer.secho(
|
|
85
|
+
"❌ `flwr log` currently works with Exec API. Ensure that the correct"
|
|
86
|
+
"Exec API address is provided in the `pyproject.toml`.",
|
|
87
|
+
fg=typer.colors.RED,
|
|
88
|
+
bold=True,
|
|
89
|
+
)
|
|
90
|
+
raise typer.Exit(code=1)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
if runs and run_id is not None:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
"The options '--runs' and '--run-id' are mutually exclusive."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
channel = _init_channel(app, federation_config)
|
|
99
|
+
stub = ExecStub(channel)
|
|
100
|
+
|
|
101
|
+
# Display information about a specific run ID
|
|
102
|
+
if run_id is not None:
|
|
103
|
+
typer.echo(f"🔍 Displaying information for run ID {run_id}...")
|
|
104
|
+
_display_one_run(stub, run_id)
|
|
105
|
+
# By default, list all runs
|
|
106
|
+
else:
|
|
107
|
+
typer.echo("📄 Listing all runs...")
|
|
108
|
+
_list_runs(stub)
|
|
109
|
+
|
|
110
|
+
except ValueError as err:
|
|
111
|
+
typer.secho(
|
|
112
|
+
f"❌ {err}",
|
|
113
|
+
fg=typer.colors.RED,
|
|
114
|
+
bold=True,
|
|
115
|
+
)
|
|
116
|
+
raise typer.Exit(code=1) from err
|
|
117
|
+
finally:
|
|
118
|
+
channel.close()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def on_channel_state_change(channel_connectivity: str) -> None:
|
|
122
|
+
"""Log channel connectivity."""
|
|
123
|
+
log(DEBUG, channel_connectivity)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _init_channel(app: Path, federation_config: dict[str, Any]) -> grpc.Channel:
|
|
127
|
+
"""Initialize gRPC channel to the Exec API."""
|
|
128
|
+
insecure, root_certificates_bytes = validate_certificate_in_federation_config(
|
|
129
|
+
app, federation_config
|
|
130
|
+
)
|
|
131
|
+
channel = create_channel(
|
|
132
|
+
server_address=federation_config["address"],
|
|
133
|
+
insecure=insecure,
|
|
134
|
+
root_certificates=root_certificates_bytes,
|
|
135
|
+
max_message_length=GRPC_MAX_MESSAGE_LENGTH,
|
|
136
|
+
interceptors=None,
|
|
137
|
+
)
|
|
138
|
+
channel.subscribe(on_channel_state_change)
|
|
139
|
+
return channel
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _format_run_table(run_dict: dict[int, Run], now_isoformat: str) -> Table:
|
|
143
|
+
"""Format run status as a rich Table."""
|
|
144
|
+
table = Table(header_style="bold cyan", show_lines=True)
|
|
145
|
+
|
|
146
|
+
def _format_datetime(dt: Optional[datetime]) -> str:
|
|
147
|
+
return isoformat8601_utc(dt).replace("T", " ") if dt else "N/A"
|
|
148
|
+
|
|
149
|
+
# Add columns
|
|
150
|
+
table.add_column(
|
|
151
|
+
Text("Run ID", justify="center"), style="bright_white", overflow="fold"
|
|
152
|
+
)
|
|
153
|
+
table.add_column(Text("FAB", justify="center"), style="dim white")
|
|
154
|
+
table.add_column(Text("Status", justify="center"))
|
|
155
|
+
table.add_column(Text("Elapsed", justify="center"), style="blue")
|
|
156
|
+
table.add_column(Text("Created At", justify="center"), style="dim white")
|
|
157
|
+
table.add_column(Text("Running At", justify="center"), style="dim white")
|
|
158
|
+
table.add_column(Text("Finished At", justify="center"), style="dim white")
|
|
159
|
+
|
|
160
|
+
# Add rows
|
|
161
|
+
for run in sorted(
|
|
162
|
+
run_dict.values(), key=lambda x: datetime.fromisoformat(x.pending_at)
|
|
163
|
+
):
|
|
164
|
+
# Combine status and sub-status into a single string
|
|
165
|
+
if run.status.sub_status == "":
|
|
166
|
+
status_text = run.status.status
|
|
167
|
+
else:
|
|
168
|
+
status_text = f"{run.status.status}:{run.status.sub_status}"
|
|
169
|
+
|
|
170
|
+
# Style the status based on its value
|
|
171
|
+
sub_status = run.status.sub_status
|
|
172
|
+
if sub_status == SubStatus.COMPLETED:
|
|
173
|
+
status_style = "green"
|
|
174
|
+
elif sub_status == SubStatus.FAILED:
|
|
175
|
+
status_style = "red"
|
|
176
|
+
else:
|
|
177
|
+
status_style = "yellow"
|
|
178
|
+
|
|
179
|
+
# Convert isoformat to datetime
|
|
180
|
+
pending_at = datetime.fromisoformat(run.pending_at) if run.pending_at else None
|
|
181
|
+
running_at = datetime.fromisoformat(run.running_at) if run.running_at else None
|
|
182
|
+
finished_at = (
|
|
183
|
+
datetime.fromisoformat(run.finished_at) if run.finished_at else None
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Calculate elapsed time
|
|
187
|
+
elapsed_time = timedelta()
|
|
188
|
+
if running_at:
|
|
189
|
+
if finished_at:
|
|
190
|
+
end_time = finished_at
|
|
191
|
+
else:
|
|
192
|
+
end_time = datetime.fromisoformat(now_isoformat)
|
|
193
|
+
elapsed_time = end_time - running_at
|
|
194
|
+
|
|
195
|
+
table.add_row(
|
|
196
|
+
f"[bold]{run.run_id}[/bold]",
|
|
197
|
+
f"{run.fab_id} (v{run.fab_version})",
|
|
198
|
+
f"[{status_style}]{status_text}[/{status_style}]",
|
|
199
|
+
format_timedelta(elapsed_time),
|
|
200
|
+
_format_datetime(pending_at),
|
|
201
|
+
_format_datetime(running_at),
|
|
202
|
+
_format_datetime(finished_at),
|
|
203
|
+
)
|
|
204
|
+
return table
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _list_runs(
|
|
208
|
+
stub: ExecStub,
|
|
209
|
+
) -> None:
|
|
210
|
+
"""List all runs."""
|
|
211
|
+
res: ListRunsResponse = stub.ListRuns(ListRunsRequest())
|
|
212
|
+
run_dict = {run_id: run_from_proto(proto) for run_id, proto in res.run_dict.items()}
|
|
213
|
+
|
|
214
|
+
Console().print(_format_run_table(run_dict, res.now))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _display_one_run(
|
|
218
|
+
stub: ExecStub,
|
|
219
|
+
run_id: int,
|
|
220
|
+
) -> None:
|
|
221
|
+
"""Display information about a specific run."""
|
|
222
|
+
res: ListRunsResponse = stub.ListRuns(ListRunsRequest(run_id=run_id))
|
|
223
|
+
if not res.run_dict:
|
|
224
|
+
raise ValueError(f"Run ID {run_id} not found")
|
|
225
|
+
|
|
226
|
+
run_dict = {run_id: run_from_proto(proto) for run_id, proto in res.run_dict.items()}
|
|
227
|
+
|
|
228
|
+
Console().print(_format_run_table(run_dict, res.now))
|
flwr/cli/run/run.py
CHANGED
|
@@ -29,10 +29,18 @@ from flwr.cli.config_utils import (
|
|
|
29
29
|
validate_federation_in_project_config,
|
|
30
30
|
validate_project_config,
|
|
31
31
|
)
|
|
32
|
-
from flwr.common.config import
|
|
32
|
+
from flwr.common.config import (
|
|
33
|
+
flatten_dict,
|
|
34
|
+
parse_config_args,
|
|
35
|
+
user_config_to_configsrecord,
|
|
36
|
+
)
|
|
33
37
|
from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel
|
|
34
38
|
from flwr.common.logger import log
|
|
35
|
-
from flwr.common.serde import
|
|
39
|
+
from flwr.common.serde import (
|
|
40
|
+
configs_record_to_proto,
|
|
41
|
+
fab_to_proto,
|
|
42
|
+
user_config_to_proto,
|
|
43
|
+
)
|
|
36
44
|
from flwr.common.typing import Fab
|
|
37
45
|
from flwr.proto.exec_pb2 import StartRunRequest # pylint: disable=E0611
|
|
38
46
|
from flwr.proto.exec_pb2_grpc import ExecStub
|
|
@@ -94,6 +102,7 @@ def run(
|
|
|
94
102
|
_run_without_exec_api(app, federation_config, config_overrides, federation)
|
|
95
103
|
|
|
96
104
|
|
|
105
|
+
# pylint: disable-next=too-many-locals
|
|
97
106
|
def _run_with_exec_api(
|
|
98
107
|
app: Path,
|
|
99
108
|
federation_config: dict[str, Any],
|
|
@@ -118,12 +127,14 @@ def _run_with_exec_api(
|
|
|
118
127
|
content = Path(fab_path).read_bytes()
|
|
119
128
|
fab = Fab(fab_hash, content)
|
|
120
129
|
|
|
130
|
+
# Construct a `ConfigsRecord` out of a flattened `UserConfig`
|
|
131
|
+
fed_conf = flatten_dict(federation_config.get("options", {}))
|
|
132
|
+
c_record = user_config_to_configsrecord(fed_conf)
|
|
133
|
+
|
|
121
134
|
req = StartRunRequest(
|
|
122
135
|
fab=fab_to_proto(fab),
|
|
123
136
|
override_config=user_config_to_proto(parse_config_args(config_overrides)),
|
|
124
|
-
|
|
125
|
-
flatten_dict(federation_config.get("options"))
|
|
126
|
-
),
|
|
137
|
+
federation_options=configs_record_to_proto(c_record),
|
|
127
138
|
)
|
|
128
139
|
res = stub.StartRun(req)
|
|
129
140
|
|
flwr/client/app.py
CHANGED
|
@@ -32,15 +32,18 @@ from flwr.cli.config_utils import get_fab_metadata
|
|
|
32
32
|
from flwr.cli.install import install_from_fab
|
|
33
33
|
from flwr.client.client import Client
|
|
34
34
|
from flwr.client.client_app import ClientApp, LoadClientAppError
|
|
35
|
+
from flwr.client.nodestate.nodestate_factory import NodeStateFactory
|
|
35
36
|
from flwr.client.typing import ClientFnExt
|
|
36
37
|
from flwr.common import GRPC_MAX_MESSAGE_LENGTH, Context, EventType, Message, event
|
|
37
38
|
from flwr.common.address import parse_address
|
|
38
39
|
from flwr.common.constant import (
|
|
39
|
-
|
|
40
|
+
CLIENT_OCTET,
|
|
41
|
+
CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS,
|
|
40
42
|
ISOLATION_MODE_PROCESS,
|
|
41
43
|
ISOLATION_MODE_SUBPROCESS,
|
|
42
44
|
MISSING_EXTRA_REST,
|
|
43
45
|
RUN_ID_NUM_BYTES,
|
|
46
|
+
SERVER_OCTET,
|
|
44
47
|
TRANSPORT_TYPE_GRPC_ADAPTER,
|
|
45
48
|
TRANSPORT_TYPE_GRPC_BIDI,
|
|
46
49
|
TRANSPORT_TYPE_GRPC_RERE,
|
|
@@ -101,6 +104,11 @@ def start_client(
|
|
|
101
104
|
) -> None:
|
|
102
105
|
"""Start a Flower client node which connects to a Flower server.
|
|
103
106
|
|
|
107
|
+
Warning
|
|
108
|
+
-------
|
|
109
|
+
This function is deprecated since 1.13.0. Use :code:`flower-supernode` command
|
|
110
|
+
instead to start a SuperNode.
|
|
111
|
+
|
|
104
112
|
Parameters
|
|
105
113
|
----------
|
|
106
114
|
server_address : str
|
|
@@ -175,6 +183,17 @@ def start_client(
|
|
|
175
183
|
>>> root_certificates=Path("/crts/root.pem").read_bytes(),
|
|
176
184
|
>>> )
|
|
177
185
|
"""
|
|
186
|
+
msg = (
|
|
187
|
+
"flwr.client.start_client() is deprecated."
|
|
188
|
+
"\n\tInstead, use the `flower-supernode` CLI command to start a SuperNode "
|
|
189
|
+
"as shown below:"
|
|
190
|
+
"\n\n\t\t$ flower-supernode --insecure --superlink='<IP>:<PORT>'"
|
|
191
|
+
"\n\n\tTo view all available options, run:"
|
|
192
|
+
"\n\n\t\t$ flower-supernode --help"
|
|
193
|
+
"\n\n\tUsing `start_client()` is deprecated."
|
|
194
|
+
)
|
|
195
|
+
warn_deprecated_feature(name=msg)
|
|
196
|
+
|
|
178
197
|
event(EventType.START_CLIENT_ENTER)
|
|
179
198
|
start_client_internal(
|
|
180
199
|
server_address=server_address,
|
|
@@ -215,7 +234,9 @@ def start_client_internal(
|
|
|
215
234
|
max_wait_time: Optional[float] = None,
|
|
216
235
|
flwr_path: Optional[Path] = None,
|
|
217
236
|
isolation: Optional[str] = None,
|
|
218
|
-
|
|
237
|
+
clientappio_api_address: Optional[str] = CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS,
|
|
238
|
+
certificates: Optional[tuple[bytes, bytes, bytes]] = None,
|
|
239
|
+
ssl_ca_certfile: Optional[str] = None,
|
|
219
240
|
) -> None:
|
|
220
241
|
"""Start a Flower client node which connects to a Flower server.
|
|
221
242
|
|
|
@@ -273,10 +294,16 @@ def start_client_internal(
|
|
|
273
294
|
`process`. Defaults to `None`, which runs the `ClientApp` in the same process
|
|
274
295
|
as the SuperNode. If `subprocess`, the `ClientApp` runs in a subprocess started
|
|
275
296
|
by the SueprNode and communicates using gRPC at the address
|
|
276
|
-
`
|
|
277
|
-
process and communicates using gRPC at the address
|
|
278
|
-
|
|
297
|
+
`clientappio_api_address`. If `process`, the `ClientApp` runs in a separate
|
|
298
|
+
isolated process and communicates using gRPC at the address
|
|
299
|
+
`clientappio_api_address`.
|
|
300
|
+
clientappio_api_address : Optional[str]
|
|
301
|
+
(default: `CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS`)
|
|
279
302
|
The SuperNode gRPC server address.
|
|
303
|
+
certificates : Optional[Tuple[bytes, bytes, bytes]] (default: None)
|
|
304
|
+
Tuple containing the CA certificate, server certificate, and server private key.
|
|
305
|
+
ssl_ca_certfile : Optional[str] (default: None)
|
|
306
|
+
The path to the CA certificate file used by `flwr-clientapp` in subprocess mode.
|
|
280
307
|
"""
|
|
281
308
|
if insecure is None:
|
|
282
309
|
insecure = root_certificates is None
|
|
@@ -303,15 +330,16 @@ def start_client_internal(
|
|
|
303
330
|
load_client_app_fn = _load_client_app
|
|
304
331
|
|
|
305
332
|
if isolation:
|
|
306
|
-
if
|
|
333
|
+
if clientappio_api_address is None:
|
|
307
334
|
raise ValueError(
|
|
308
|
-
f"`
|
|
335
|
+
f"`clientappio_api_address` required when `isolation` is "
|
|
309
336
|
f"{ISOLATION_MODE_SUBPROCESS} or {ISOLATION_MODE_PROCESS}",
|
|
310
337
|
)
|
|
311
338
|
_clientappio_grpc_server, clientappio_servicer = run_clientappio_api_grpc(
|
|
312
|
-
address=
|
|
339
|
+
address=clientappio_api_address,
|
|
340
|
+
certificates=certificates,
|
|
313
341
|
)
|
|
314
|
-
|
|
342
|
+
clientappio_api_address = cast(str, clientappio_api_address)
|
|
315
343
|
|
|
316
344
|
# At this point, only `load_client_app_fn` should be used
|
|
317
345
|
# Both `client` and `client_fn` must not be used directly
|
|
@@ -365,6 +393,8 @@ def start_client_internal(
|
|
|
365
393
|
|
|
366
394
|
# DeprecatedRunInfoStore gets initialized when the first connection is established
|
|
367
395
|
run_info_store: Optional[DeprecatedRunInfoStore] = None
|
|
396
|
+
state_factory = NodeStateFactory()
|
|
397
|
+
state = state_factory.state()
|
|
368
398
|
|
|
369
399
|
runs: dict[int, Run] = {}
|
|
370
400
|
|
|
@@ -396,13 +426,14 @@ def start_client_internal(
|
|
|
396
426
|
)
|
|
397
427
|
else:
|
|
398
428
|
# Call create_node fn to register node
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
429
|
+
# and store node_id in state
|
|
430
|
+
if (node_id := create_node()) is None:
|
|
431
|
+
raise ValueError(
|
|
432
|
+
"Failed to register SuperNode with the SuperLink"
|
|
433
|
+
)
|
|
434
|
+
state.set_node_id(node_id)
|
|
404
435
|
run_info_store = DeprecatedRunInfoStore(
|
|
405
|
-
node_id=
|
|
436
|
+
node_id=state.get_node_id(),
|
|
406
437
|
node_config=node_config,
|
|
407
438
|
)
|
|
408
439
|
|
|
@@ -444,7 +475,7 @@ def start_client_internal(
|
|
|
444
475
|
runs[run_id] = get_run(run_id)
|
|
445
476
|
# If get_run is None, i.e., in grpc-bidi mode
|
|
446
477
|
else:
|
|
447
|
-
runs[run_id] = Run(run_id
|
|
478
|
+
runs[run_id] = Run.create_empty(run_id=run_id)
|
|
448
479
|
|
|
449
480
|
run: Run = runs[run_id]
|
|
450
481
|
if get_fab is not None and run.fab_hash:
|
|
@@ -504,14 +535,28 @@ def start_client_internal(
|
|
|
504
535
|
)
|
|
505
536
|
|
|
506
537
|
if start_subprocess:
|
|
538
|
+
_octet, _colon, _port = (
|
|
539
|
+
clientappio_api_address.rpartition(":")
|
|
540
|
+
)
|
|
541
|
+
io_address = (
|
|
542
|
+
f"{CLIENT_OCTET}:{_port}"
|
|
543
|
+
if _octet == SERVER_OCTET
|
|
544
|
+
else clientappio_api_address
|
|
545
|
+
)
|
|
507
546
|
# Start ClientApp subprocess
|
|
508
547
|
command = [
|
|
509
548
|
"flwr-clientapp",
|
|
510
|
-
"--
|
|
511
|
-
|
|
549
|
+
"--clientappio-api-address",
|
|
550
|
+
io_address,
|
|
512
551
|
"--token",
|
|
513
552
|
str(token),
|
|
514
553
|
]
|
|
554
|
+
if ssl_ca_certfile:
|
|
555
|
+
command.append("--root-certificates")
|
|
556
|
+
command.append(ssl_ca_certfile)
|
|
557
|
+
else:
|
|
558
|
+
command.append("--insecure")
|
|
559
|
+
|
|
515
560
|
subprocess.run(
|
|
516
561
|
command,
|
|
517
562
|
stdout=None,
|
|
@@ -779,7 +824,10 @@ class _AppStateTracker:
|
|
|
779
824
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
780
825
|
|
|
781
826
|
|
|
782
|
-
def run_clientappio_api_grpc(
|
|
827
|
+
def run_clientappio_api_grpc(
|
|
828
|
+
address: str,
|
|
829
|
+
certificates: Optional[tuple[bytes, bytes, bytes]],
|
|
830
|
+
) -> tuple[grpc.Server, ClientAppIoServicer]:
|
|
783
831
|
"""Run ClientAppIo API gRPC server."""
|
|
784
832
|
clientappio_servicer: grpc.Server = ClientAppIoServicer()
|
|
785
833
|
clientappio_add_servicer_to_server_fn = add_ClientAppIoServicer_to_server
|
|
@@ -790,6 +838,7 @@ def run_clientappio_api_grpc(address: str) -> tuple[grpc.Server, ClientAppIoServ
|
|
|
790
838
|
),
|
|
791
839
|
server_address=address,
|
|
792
840
|
max_message_length=GRPC_MAX_MESSAGE_LENGTH,
|
|
841
|
+
certificates=certificates,
|
|
793
842
|
)
|
|
794
843
|
log(INFO, "Starting Flower ClientAppIo gRPC server on %s", address)
|
|
795
844
|
clientappio_grpc_server.start()
|