flwr-nightly 1.13.0.dev20241111__py3-none-any.whl → 1.14.0.dev20241126__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/install.py +0 -16
- flwr/cli/ls.py +228 -0
- flwr/cli/new/new.py +23 -13
- flwr/cli/new/templates/app/README.md.tpl +11 -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 +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/run.py +4 -2
- flwr/client/app.py +50 -14
- flwr/client/clientapp/app.py +40 -23
- flwr/client/grpc_rere_client/connection.py +7 -12
- flwr/client/rest_client/connection.py +4 -14
- flwr/client/supernode/app.py +31 -53
- flwr/common/args.py +85 -16
- flwr/common/constant.py +24 -6
- flwr/common/date.py +18 -0
- flwr/common/grpc.py +4 -1
- flwr/common/serde.py +10 -0
- flwr/common/typing.py +31 -10
- flwr/proto/exec_pb2.py +22 -13
- flwr/proto/exec_pb2.pyi +44 -0
- flwr/proto/exec_pb2_grpc.py +34 -0
- flwr/proto/exec_pb2_grpc.pyi +13 -0
- flwr/proto/run_pb2.py +30 -30
- flwr/proto/run_pb2.pyi +18 -1
- flwr/server/app.py +47 -77
- flwr/server/driver/grpc_driver.py +66 -16
- flwr/server/run_serverapp.py +8 -238
- flwr/server/serverapp/app.py +49 -29
- flwr/server/superlink/fleet/rest_rere/rest_api.py +10 -9
- flwr/server/superlink/linkstate/in_memory_linkstate.py +71 -46
- flwr/server/superlink/linkstate/linkstate.py +19 -5
- flwr/server/superlink/linkstate/sqlite_linkstate.py +81 -113
- flwr/server/superlink/linkstate/utils.py +193 -3
- flwr/simulation/app.py +52 -91
- flwr/simulation/legacy_app.py +21 -1
- flwr/simulation/run_simulation.py +7 -18
- flwr/simulation/simulationio_connection.py +2 -2
- flwr/superexec/deployment.py +12 -6
- flwr/superexec/exec_servicer.py +31 -2
- flwr/superexec/simulation.py +11 -46
- {flwr_nightly-1.13.0.dev20241111.dist-info → flwr_nightly-1.14.0.dev20241126.dist-info}/METADATA +5 -4
- {flwr_nightly-1.13.0.dev20241111.dist-info → flwr_nightly-1.14.0.dev20241126.dist-info}/RECORD +53 -52
- {flwr_nightly-1.13.0.dev20241111.dist-info → flwr_nightly-1.14.0.dev20241126.dist-info}/LICENSE +0 -0
- {flwr_nightly-1.13.0.dev20241111.dist-info → flwr_nightly-1.14.0.dev20241126.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.13.0.dev20241111.dist-info → flwr_nightly-1.14.0.dev20241126.dist-info}/entry_points.txt +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/install.py
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
|
|
17
17
|
import hashlib
|
|
18
18
|
import shutil
|
|
19
|
-
import subprocess
|
|
20
19
|
import tempfile
|
|
21
20
|
import zipfile
|
|
22
21
|
from io import BytesIO
|
|
@@ -188,21 +187,6 @@ def validate_and_install(
|
|
|
188
187
|
else:
|
|
189
188
|
shutil.copy2(item, install_dir / item.name)
|
|
190
189
|
|
|
191
|
-
try:
|
|
192
|
-
subprocess.run(
|
|
193
|
-
["pip", "install", "-e", install_dir, "--no-deps"],
|
|
194
|
-
capture_output=True,
|
|
195
|
-
text=True,
|
|
196
|
-
check=True,
|
|
197
|
-
)
|
|
198
|
-
except subprocess.CalledProcessError as e:
|
|
199
|
-
typer.secho(
|
|
200
|
-
f"❌ Failed to `pip install` package(s) from {install_dir}:\n{e.stderr}",
|
|
201
|
-
fg=typer.colors.RED,
|
|
202
|
-
bold=True,
|
|
203
|
-
)
|
|
204
|
-
raise typer.Exit(code=1) from e
|
|
205
|
-
|
|
206
190
|
typer.secho(
|
|
207
191
|
f"🎊 Successfully installed {project_name} to {install_dir}.",
|
|
208
192
|
fg=typer.colors.GREEN,
|
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 ls` 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/new/new.py
CHANGED
|
@@ -268,20 +268,30 @@ def new(
|
|
|
268
268
|
context=context,
|
|
269
269
|
)
|
|
270
270
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
bold=True,
|
|
277
|
-
)
|
|
271
|
+
prompt = typer.style(
|
|
272
|
+
"🎊 Flower App creation successful.\n\n"
|
|
273
|
+
"To run your Flower App, use the following command:\n\n",
|
|
274
|
+
fg=typer.colors.GREEN,
|
|
275
|
+
bold=True,
|
|
278
276
|
)
|
|
279
277
|
|
|
280
278
|
_add = " huggingface-cli login\n" if llm_challenge_str else ""
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
279
|
+
prompt += typer.style(
|
|
280
|
+
_add + f" flwr run {package_name}\n\n",
|
|
281
|
+
fg=typer.colors.BRIGHT_CYAN,
|
|
282
|
+
bold=True,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
prompt += typer.style(
|
|
286
|
+
"If you haven't installed all dependencies yet, follow these steps:\n\n",
|
|
287
|
+
fg=typer.colors.GREEN,
|
|
288
|
+
bold=True,
|
|
287
289
|
)
|
|
290
|
+
|
|
291
|
+
prompt += typer.style(
|
|
292
|
+
f" cd {package_name}\n" + " pip install -e .\n" + _add + " flwr run .\n",
|
|
293
|
+
fg=typer.colors.BRIGHT_CYAN,
|
|
294
|
+
bold=True,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
print(prompt)
|
|
@@ -14,7 +14,18 @@ In the `$project_name` directory, use `flwr run` to run a local simulation:
|
|
|
14
14
|
flwr run .
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
Refer to the [How to Run Simulations](https://flower.ai/docs/framework/how-to-run-simulations.html) guide in the documentation for advice on how to optimize your simulations.
|
|
18
|
+
|
|
17
19
|
## Run with the Deployment Engine
|
|
18
20
|
|
|
19
21
|
> \[!NOTE\]
|
|
20
22
|
> An update to this example will show how to run this Flower application with the Deployment Engine and TLS certificates, or with Docker.
|
|
23
|
+
|
|
24
|
+
## Resources
|
|
25
|
+
|
|
26
|
+
- Flower website: [flower.ai](https://flower.ai/)
|
|
27
|
+
- Check the documentation: [flower.ai/docs](https://flower.ai/docs/)
|
|
28
|
+
- Give Flower a ⭐️ on GitHub: [GitHub](https://github.com/adap/flower)
|
|
29
|
+
- Join the Flower community!
|
|
30
|
+
- [Flower Slack](https://flower.ai/join-slack/)
|
|
31
|
+
- [Flower Discuss](https://discuss.flower.ai/)
|
flwr/cli/run/run.py
CHANGED
|
@@ -125,6 +125,10 @@ def _run_with_exec_api(
|
|
|
125
125
|
|
|
126
126
|
fab_path, fab_hash = build(app)
|
|
127
127
|
content = Path(fab_path).read_bytes()
|
|
128
|
+
|
|
129
|
+
# Delete FAB file once the bytes is computed
|
|
130
|
+
Path(fab_path).unlink()
|
|
131
|
+
|
|
128
132
|
fab = Fab(fab_hash, content)
|
|
129
133
|
|
|
130
134
|
# Construct a `ConfigsRecord` out of a flattened `UserConfig`
|
|
@@ -138,8 +142,6 @@ def _run_with_exec_api(
|
|
|
138
142
|
)
|
|
139
143
|
res = stub.StartRun(req)
|
|
140
144
|
|
|
141
|
-
# Delete FAB file once it has been sent to the Exec API
|
|
142
|
-
Path(fab_path).unlink()
|
|
143
145
|
typer.secho(f"🎊 Successfully started run {res.run_id}", fg=typer.colors.GREEN)
|
|
144
146
|
|
|
145
147
|
if stream:
|
flwr/client/app.py
CHANGED
|
@@ -37,11 +37,14 @@ from flwr.client.typing import ClientFnExt
|
|
|
37
37
|
from flwr.common import GRPC_MAX_MESSAGE_LENGTH, Context, EventType, Message, event
|
|
38
38
|
from flwr.common.address import parse_address
|
|
39
39
|
from flwr.common.constant import (
|
|
40
|
-
|
|
40
|
+
CLIENT_OCTET,
|
|
41
|
+
CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS,
|
|
41
42
|
ISOLATION_MODE_PROCESS,
|
|
42
43
|
ISOLATION_MODE_SUBPROCESS,
|
|
44
|
+
MAX_RETRY_DELAY,
|
|
43
45
|
MISSING_EXTRA_REST,
|
|
44
46
|
RUN_ID_NUM_BYTES,
|
|
47
|
+
SERVER_OCTET,
|
|
45
48
|
TRANSPORT_TYPE_GRPC_ADAPTER,
|
|
46
49
|
TRANSPORT_TYPE_GRPC_BIDI,
|
|
47
50
|
TRANSPORT_TYPE_GRPC_RERE,
|
|
@@ -102,6 +105,11 @@ def start_client(
|
|
|
102
105
|
) -> None:
|
|
103
106
|
"""Start a Flower client node which connects to a Flower server.
|
|
104
107
|
|
|
108
|
+
Warning
|
|
109
|
+
-------
|
|
110
|
+
This function is deprecated since 1.13.0. Use :code:`flower-supernode` command
|
|
111
|
+
instead to start a SuperNode.
|
|
112
|
+
|
|
105
113
|
Parameters
|
|
106
114
|
----------
|
|
107
115
|
server_address : str
|
|
@@ -176,6 +184,17 @@ def start_client(
|
|
|
176
184
|
>>> root_certificates=Path("/crts/root.pem").read_bytes(),
|
|
177
185
|
>>> )
|
|
178
186
|
"""
|
|
187
|
+
msg = (
|
|
188
|
+
"flwr.client.start_client() is deprecated."
|
|
189
|
+
"\n\tInstead, use the `flower-supernode` CLI command to start a SuperNode "
|
|
190
|
+
"as shown below:"
|
|
191
|
+
"\n\n\t\t$ flower-supernode --insecure --superlink='<IP>:<PORT>'"
|
|
192
|
+
"\n\n\tTo view all available options, run:"
|
|
193
|
+
"\n\n\t\t$ flower-supernode --help"
|
|
194
|
+
"\n\n\tUsing `start_client()` is deprecated."
|
|
195
|
+
)
|
|
196
|
+
warn_deprecated_feature(name=msg)
|
|
197
|
+
|
|
179
198
|
event(EventType.START_CLIENT_ENTER)
|
|
180
199
|
start_client_internal(
|
|
181
200
|
server_address=server_address,
|
|
@@ -216,7 +235,7 @@ def start_client_internal(
|
|
|
216
235
|
max_wait_time: Optional[float] = None,
|
|
217
236
|
flwr_path: Optional[Path] = None,
|
|
218
237
|
isolation: Optional[str] = None,
|
|
219
|
-
|
|
238
|
+
clientappio_api_address: Optional[str] = CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS,
|
|
220
239
|
) -> None:
|
|
221
240
|
"""Start a Flower client node which connects to a Flower server.
|
|
222
241
|
|
|
@@ -274,9 +293,11 @@ def start_client_internal(
|
|
|
274
293
|
`process`. Defaults to `None`, which runs the `ClientApp` in the same process
|
|
275
294
|
as the SuperNode. If `subprocess`, the `ClientApp` runs in a subprocess started
|
|
276
295
|
by the SueprNode and communicates using gRPC at the address
|
|
277
|
-
`
|
|
278
|
-
process and communicates using gRPC at the address
|
|
279
|
-
|
|
296
|
+
`clientappio_api_address`. If `process`, the `ClientApp` runs in a separate
|
|
297
|
+
isolated process and communicates using gRPC at the address
|
|
298
|
+
`clientappio_api_address`.
|
|
299
|
+
clientappio_api_address : Optional[str]
|
|
300
|
+
(default: `CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS`)
|
|
280
301
|
The SuperNode gRPC server address.
|
|
281
302
|
"""
|
|
282
303
|
if insecure is None:
|
|
@@ -304,15 +325,16 @@ def start_client_internal(
|
|
|
304
325
|
load_client_app_fn = _load_client_app
|
|
305
326
|
|
|
306
327
|
if isolation:
|
|
307
|
-
if
|
|
328
|
+
if clientappio_api_address is None:
|
|
308
329
|
raise ValueError(
|
|
309
|
-
f"`
|
|
330
|
+
f"`clientappio_api_address` required when `isolation` is "
|
|
310
331
|
f"{ISOLATION_MODE_SUBPROCESS} or {ISOLATION_MODE_PROCESS}",
|
|
311
332
|
)
|
|
312
333
|
_clientappio_grpc_server, clientappio_servicer = run_clientappio_api_grpc(
|
|
313
|
-
address=
|
|
334
|
+
address=clientappio_api_address,
|
|
335
|
+
certificates=None,
|
|
314
336
|
)
|
|
315
|
-
|
|
337
|
+
clientappio_api_address = cast(str, clientappio_api_address)
|
|
316
338
|
|
|
317
339
|
# At this point, only `load_client_app_fn` should be used
|
|
318
340
|
# Both `client` and `client_fn` must not be used directly
|
|
@@ -346,7 +368,7 @@ def start_client_internal(
|
|
|
346
368
|
)
|
|
347
369
|
|
|
348
370
|
retry_invoker = RetryInvoker(
|
|
349
|
-
wait_gen_factory=exponential,
|
|
371
|
+
wait_gen_factory=lambda: exponential(max_delay=MAX_RETRY_DELAY),
|
|
350
372
|
recoverable_exceptions=connection_error_type,
|
|
351
373
|
max_tries=max_retries + 1 if max_retries is not None else None,
|
|
352
374
|
max_time=max_wait_time,
|
|
@@ -448,7 +470,7 @@ def start_client_internal(
|
|
|
448
470
|
runs[run_id] = get_run(run_id)
|
|
449
471
|
# If get_run is None, i.e., in grpc-bidi mode
|
|
450
472
|
else:
|
|
451
|
-
runs[run_id] = Run(run_id
|
|
473
|
+
runs[run_id] = Run.create_empty(run_id=run_id)
|
|
452
474
|
|
|
453
475
|
run: Run = runs[run_id]
|
|
454
476
|
if get_fab is not None and run.fab_hash:
|
|
@@ -508,14 +530,24 @@ def start_client_internal(
|
|
|
508
530
|
)
|
|
509
531
|
|
|
510
532
|
if start_subprocess:
|
|
533
|
+
_octet, _colon, _port = (
|
|
534
|
+
clientappio_api_address.rpartition(":")
|
|
535
|
+
)
|
|
536
|
+
io_address = (
|
|
537
|
+
f"{CLIENT_OCTET}:{_port}"
|
|
538
|
+
if _octet == SERVER_OCTET
|
|
539
|
+
else clientappio_api_address
|
|
540
|
+
)
|
|
511
541
|
# Start ClientApp subprocess
|
|
512
542
|
command = [
|
|
513
543
|
"flwr-clientapp",
|
|
514
|
-
"--
|
|
515
|
-
|
|
544
|
+
"--clientappio-api-address",
|
|
545
|
+
io_address,
|
|
516
546
|
"--token",
|
|
517
547
|
str(token),
|
|
518
548
|
]
|
|
549
|
+
command.append("--insecure")
|
|
550
|
+
|
|
519
551
|
subprocess.run(
|
|
520
552
|
command,
|
|
521
553
|
stdout=None,
|
|
@@ -783,7 +815,10 @@ class _AppStateTracker:
|
|
|
783
815
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
784
816
|
|
|
785
817
|
|
|
786
|
-
def run_clientappio_api_grpc(
|
|
818
|
+
def run_clientappio_api_grpc(
|
|
819
|
+
address: str,
|
|
820
|
+
certificates: Optional[tuple[bytes, bytes, bytes]],
|
|
821
|
+
) -> tuple[grpc.Server, ClientAppIoServicer]:
|
|
787
822
|
"""Run ClientAppIo API gRPC server."""
|
|
788
823
|
clientappio_servicer: grpc.Server = ClientAppIoServicer()
|
|
789
824
|
clientappio_add_servicer_to_server_fn = add_ClientAppIoServicer_to_server
|
|
@@ -794,6 +829,7 @@ def run_clientappio_api_grpc(address: str) -> tuple[grpc.Server, ClientAppIoServ
|
|
|
794
829
|
),
|
|
795
830
|
server_address=address,
|
|
796
831
|
max_message_length=GRPC_MAX_MESSAGE_LENGTH,
|
|
832
|
+
certificates=certificates,
|
|
797
833
|
)
|
|
798
834
|
log(INFO, "Starting Flower ClientAppIo gRPC server on %s", address)
|
|
799
835
|
clientappio_grpc_server.start()
|