flwr-nightly 1.15.0.dev20250114__py3-none-any.whl → 1.15.0.dev20250123__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/config_utils.py +23 -146
- flwr/cli/constant.py +27 -0
- flwr/cli/install.py +1 -1
- flwr/cli/log.py +17 -2
- flwr/cli/login/login.py +9 -1
- flwr/cli/ls.py +10 -2
- flwr/cli/run/run.py +20 -10
- flwr/cli/stop.py +9 -1
- flwr/client/app.py +23 -43
- flwr/client/clientapp/app.py +4 -6
- flwr/client/clientapp/utils.py +1 -1
- flwr/client/grpc_client/connection.py +0 -6
- flwr/client/grpc_rere_client/client_interceptor.py +19 -125
- flwr/client/grpc_rere_client/connection.py +10 -0
- flwr/client/rest_client/connection.py +12 -3
- flwr/client/supernode/app.py +14 -20
- flwr/common/auth_plugin/auth_plugin.py +1 -0
- flwr/common/config.py +152 -15
- flwr/common/constant.py +9 -8
- flwr/common/exit/__init__.py +24 -0
- flwr/common/exit/exit.py +99 -0
- flwr/common/exit/exit_code.py +93 -0
- flwr/common/exit_handlers.py +24 -10
- flwr/common/grpc.py +7 -0
- flwr/common/logger.py +1 -1
- flwr/common/serde.py +6 -4
- flwr/proto/clientappio_pb2.py +13 -3
- flwr/proto/clientappio_pb2_grpc.py +63 -12
- flwr/proto/error_pb2.py +13 -3
- flwr/proto/error_pb2_grpc.py +20 -0
- flwr/proto/exec_pb2.py +15 -5
- flwr/proto/exec_pb2_grpc.py +105 -24
- flwr/proto/fab_pb2.py +13 -3
- flwr/proto/fab_pb2_grpc.py +20 -0
- flwr/proto/fleet_pb2.py +15 -5
- flwr/proto/fleet_pb2_grpc.py +147 -36
- flwr/proto/grpcadapter_pb2.py +14 -4
- flwr/proto/grpcadapter_pb2_grpc.py +35 -4
- flwr/proto/log_pb2.py +13 -3
- flwr/proto/log_pb2_grpc.py +20 -0
- flwr/proto/message_pb2.py +15 -5
- flwr/proto/message_pb2_grpc.py +20 -0
- flwr/proto/node_pb2.py +15 -5
- flwr/proto/node_pb2.pyi +1 -4
- flwr/proto/node_pb2_grpc.py +20 -0
- flwr/proto/recordset_pb2.py +18 -8
- flwr/proto/recordset_pb2_grpc.py +20 -0
- flwr/proto/run_pb2.py +16 -6
- flwr/proto/run_pb2_grpc.py +20 -0
- flwr/proto/serverappio_pb2.py +32 -14
- flwr/proto/serverappio_pb2.pyi +56 -0
- flwr/proto/serverappio_pb2_grpc.py +261 -44
- flwr/proto/serverappio_pb2_grpc.pyi +20 -0
- flwr/proto/simulationio_pb2.py +13 -3
- flwr/proto/simulationio_pb2_grpc.py +105 -24
- flwr/proto/task_pb2.py +13 -3
- flwr/proto/task_pb2_grpc.py +20 -0
- flwr/proto/transport_pb2.py +20 -10
- flwr/proto/transport_pb2_grpc.py +35 -4
- flwr/server/app.py +40 -11
- flwr/server/compat/app_utils.py +0 -1
- flwr/server/compat/driver_client_proxy.py +1 -2
- flwr/server/driver/grpc_driver.py +5 -2
- flwr/server/driver/inmemory_driver.py +2 -1
- flwr/server/serverapp/app.py +5 -6
- flwr/server/superlink/driver/serverappio_servicer.py +110 -6
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +20 -88
- flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +95 -169
- flwr/server/superlink/fleet/message_handler/message_handler.py +4 -5
- flwr/server/superlink/fleet/rest_rere/rest_api.py +2 -3
- flwr/server/superlink/linkstate/in_memory_linkstate.py +14 -26
- flwr/server/superlink/linkstate/linkstate.py +5 -18
- flwr/server/superlink/linkstate/sqlite_linkstate.py +30 -70
- flwr/server/superlink/linkstate/utils.py +18 -8
- flwr/server/utils/validator.py +9 -34
- flwr/simulation/app.py +4 -6
- flwr/simulation/legacy_app.py +4 -2
- {flwr_nightly-1.15.0.dev20250114.dist-info → flwr_nightly-1.15.0.dev20250123.dist-info}/METADATA +4 -4
- {flwr_nightly-1.15.0.dev20250114.dist-info → flwr_nightly-1.15.0.dev20250123.dist-info}/RECORD +82 -78
- {flwr_nightly-1.15.0.dev20250114.dist-info → flwr_nightly-1.15.0.dev20250123.dist-info}/LICENSE +0 -0
- {flwr_nightly-1.15.0.dev20250114.dist-info → flwr_nightly-1.15.0.dev20250123.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.15.0.dev20250114.dist-info → flwr_nightly-1.15.0.dev20250123.dist-info}/entry_points.txt +0 -0
flwr/cli/config_utils.py
CHANGED
@@ -15,53 +15,19 @@
|
|
15
15
|
"""Utility to validate the `pyproject.toml` file."""
|
16
16
|
|
17
17
|
|
18
|
-
import zipfile
|
19
|
-
from io import BytesIO
|
20
18
|
from pathlib import Path
|
21
|
-
from typing import
|
19
|
+
from typing import Any, Optional, Union
|
22
20
|
|
23
21
|
import tomli
|
24
22
|
import typer
|
25
23
|
|
26
|
-
from flwr.common import
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
Parameters
|
34
|
-
----------
|
35
|
-
fab_file : Union[Path, bytes]
|
36
|
-
The Flower App Bundle file to validate and extract the metadata from.
|
37
|
-
It can either be a path to the file or the file itself as bytes.
|
38
|
-
|
39
|
-
Returns
|
40
|
-
-------
|
41
|
-
Dict[str, Any]
|
42
|
-
The `config` of the given Flower App Bundle.
|
43
|
-
"""
|
44
|
-
fab_file_archive: Union[Path, IO[bytes]]
|
45
|
-
if isinstance(fab_file, bytes):
|
46
|
-
fab_file_archive = BytesIO(fab_file)
|
47
|
-
elif isinstance(fab_file, Path):
|
48
|
-
fab_file_archive = fab_file
|
49
|
-
else:
|
50
|
-
raise ValueError("fab_file must be either a Path or bytes")
|
51
|
-
|
52
|
-
with zipfile.ZipFile(fab_file_archive, "r") as zipf:
|
53
|
-
with zipf.open("pyproject.toml") as file:
|
54
|
-
toml_content = file.read().decode("utf-8")
|
55
|
-
|
56
|
-
conf = load_from_string(toml_content)
|
57
|
-
if conf is None:
|
58
|
-
raise ValueError("Invalid TOML content in pyproject.toml")
|
59
|
-
|
60
|
-
is_valid, errors, _ = validate(conf, check_module=False)
|
61
|
-
if not is_valid:
|
62
|
-
raise ValueError(errors)
|
63
|
-
|
64
|
-
return conf
|
24
|
+
from flwr.common.config import (
|
25
|
+
fuse_dicts,
|
26
|
+
get_fab_config,
|
27
|
+
get_metadata_from_config,
|
28
|
+
parse_config_args,
|
29
|
+
validate_config,
|
30
|
+
)
|
65
31
|
|
66
32
|
|
67
33
|
def get_fab_metadata(fab_file: Union[Path, bytes]) -> tuple[str, str]:
|
@@ -78,12 +44,7 @@ def get_fab_metadata(fab_file: Union[Path, bytes]) -> tuple[str, str]:
|
|
78
44
|
Tuple[str, str]
|
79
45
|
The `fab_id` and `fab_version` of the given Flower App Bundle.
|
80
46
|
"""
|
81
|
-
|
82
|
-
|
83
|
-
return (
|
84
|
-
f"{conf['tool']['flwr']['app']['publisher']}/{conf['project']['name']}",
|
85
|
-
conf["project"]["version"],
|
86
|
-
)
|
47
|
+
return get_metadata_from_config(get_fab_config(fab_file))
|
87
48
|
|
88
49
|
|
89
50
|
def load_and_validate(
|
@@ -120,7 +81,7 @@ def load_and_validate(
|
|
120
81
|
]
|
121
82
|
return (None, errors, [])
|
122
83
|
|
123
|
-
is_valid, errors, warnings =
|
84
|
+
is_valid, errors, warnings = validate_config(config, check_module, path.parent)
|
124
85
|
|
125
86
|
if not is_valid:
|
126
87
|
return (None, errors, warnings)
|
@@ -133,102 +94,11 @@ def load(toml_path: Path) -> Optional[dict[str, Any]]:
|
|
133
94
|
if not toml_path.is_file():
|
134
95
|
return None
|
135
96
|
|
136
|
-
with toml_path.open(
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
for key, value in config_dict.items():
|
142
|
-
if isinstance(value, dict):
|
143
|
-
_validate_run_config(config_dict[key], errors)
|
144
|
-
elif not isinstance(value, get_args(UserConfigValue)):
|
145
|
-
raise ValueError(
|
146
|
-
f"The value for key {key} needs to be of type `int`, `float`, "
|
147
|
-
"`bool, `str`, or a `dict` of those.",
|
148
|
-
)
|
149
|
-
|
150
|
-
|
151
|
-
# pylint: disable=too-many-branches
|
152
|
-
def validate_fields(config: dict[str, Any]) -> tuple[bool, list[str], list[str]]:
|
153
|
-
"""Validate pyproject.toml fields."""
|
154
|
-
errors = []
|
155
|
-
warnings = []
|
156
|
-
|
157
|
-
if "project" not in config:
|
158
|
-
errors.append("Missing [project] section")
|
159
|
-
else:
|
160
|
-
if "name" not in config["project"]:
|
161
|
-
errors.append('Property "name" missing in [project]')
|
162
|
-
if "version" not in config["project"]:
|
163
|
-
errors.append('Property "version" missing in [project]')
|
164
|
-
if "description" not in config["project"]:
|
165
|
-
warnings.append('Recommended property "description" missing in [project]')
|
166
|
-
if "license" not in config["project"]:
|
167
|
-
warnings.append('Recommended property "license" missing in [project]')
|
168
|
-
if "authors" not in config["project"]:
|
169
|
-
warnings.append('Recommended property "authors" missing in [project]')
|
170
|
-
|
171
|
-
if (
|
172
|
-
"tool" not in config
|
173
|
-
or "flwr" not in config["tool"]
|
174
|
-
or "app" not in config["tool"]["flwr"]
|
175
|
-
):
|
176
|
-
errors.append("Missing [tool.flwr.app] section")
|
177
|
-
else:
|
178
|
-
if "publisher" not in config["tool"]["flwr"]["app"]:
|
179
|
-
errors.append('Property "publisher" missing in [tool.flwr.app]')
|
180
|
-
if "config" in config["tool"]["flwr"]["app"]:
|
181
|
-
_validate_run_config(config["tool"]["flwr"]["app"]["config"], errors)
|
182
|
-
if "components" not in config["tool"]["flwr"]["app"]:
|
183
|
-
errors.append("Missing [tool.flwr.app.components] section")
|
184
|
-
else:
|
185
|
-
if "serverapp" not in config["tool"]["flwr"]["app"]["components"]:
|
186
|
-
errors.append(
|
187
|
-
'Property "serverapp" missing in [tool.flwr.app.components]'
|
188
|
-
)
|
189
|
-
if "clientapp" not in config["tool"]["flwr"]["app"]["components"]:
|
190
|
-
errors.append(
|
191
|
-
'Property "clientapp" missing in [tool.flwr.app.components]'
|
192
|
-
)
|
193
|
-
|
194
|
-
return len(errors) == 0, errors, warnings
|
195
|
-
|
196
|
-
|
197
|
-
def validate(
|
198
|
-
config: dict[str, Any],
|
199
|
-
check_module: bool = True,
|
200
|
-
project_dir: Optional[Union[str, Path]] = None,
|
201
|
-
) -> tuple[bool, list[str], list[str]]:
|
202
|
-
"""Validate pyproject.toml."""
|
203
|
-
is_valid, errors, warnings = validate_fields(config)
|
204
|
-
|
205
|
-
if not is_valid:
|
206
|
-
return False, errors, warnings
|
207
|
-
|
208
|
-
# Validate serverapp
|
209
|
-
serverapp_ref = config["tool"]["flwr"]["app"]["components"]["serverapp"]
|
210
|
-
is_valid, reason = object_ref.validate(serverapp_ref, check_module, project_dir)
|
211
|
-
|
212
|
-
if not is_valid and isinstance(reason, str):
|
213
|
-
return False, [reason], []
|
214
|
-
|
215
|
-
# Validate clientapp
|
216
|
-
clientapp_ref = config["tool"]["flwr"]["app"]["components"]["clientapp"]
|
217
|
-
is_valid, reason = object_ref.validate(clientapp_ref, check_module, project_dir)
|
218
|
-
|
219
|
-
if not is_valid and isinstance(reason, str):
|
220
|
-
return False, [reason], []
|
221
|
-
|
222
|
-
return True, [], []
|
223
|
-
|
224
|
-
|
225
|
-
def load_from_string(toml_content: str) -> Optional[dict[str, Any]]:
|
226
|
-
"""Load TOML content from a string and return as dict."""
|
227
|
-
try:
|
228
|
-
data = tomli.loads(toml_content)
|
229
|
-
return data
|
230
|
-
except tomli.TOMLDecodeError:
|
231
|
-
return None
|
97
|
+
with toml_path.open("rb") as toml_file:
|
98
|
+
try:
|
99
|
+
return tomli.load(toml_file)
|
100
|
+
except tomli.TOMLDecodeError:
|
101
|
+
return None
|
232
102
|
|
233
103
|
|
234
104
|
def process_loaded_project_config(
|
@@ -263,7 +133,9 @@ def process_loaded_project_config(
|
|
263
133
|
|
264
134
|
|
265
135
|
def validate_federation_in_project_config(
|
266
|
-
federation: Optional[str],
|
136
|
+
federation: Optional[str],
|
137
|
+
config: dict[str, Any],
|
138
|
+
overrides: Optional[list[str]] = None,
|
267
139
|
) -> tuple[str, dict[str, Any]]:
|
268
140
|
"""Validate the federation name in the Flower project configuration."""
|
269
141
|
federation = federation or config["tool"]["flwr"]["federations"].get("default")
|
@@ -293,6 +165,11 @@ def validate_federation_in_project_config(
|
|
293
165
|
)
|
294
166
|
raise typer.Exit(code=1)
|
295
167
|
|
168
|
+
# Override the federation configuration if provided
|
169
|
+
if overrides:
|
170
|
+
overrides_dict = parse_config_args(overrides, flatten=False)
|
171
|
+
federation_config = fuse_dicts(federation_config, overrides_dict)
|
172
|
+
|
296
173
|
return federation, federation_config
|
297
174
|
|
298
175
|
|
flwr/cli/constant.py
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# Copyright 2025 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
|
+
"""Constants for CLI commands."""
|
16
|
+
|
17
|
+
|
18
|
+
# The help message for `--federation-config` option
|
19
|
+
FEDERATION_CONFIG_HELP_MESSAGE = (
|
20
|
+
"Override federation configuration values in the format:\n\n"
|
21
|
+
"`--federation-config 'key1=value1 key2=value2' --federation-config "
|
22
|
+
"'key3=value3'`\n\nValues can be of any type supported in TOML, such as "
|
23
|
+
"bool, int, float, or string. Ensure that the keys (`key1`, `key2`, `key3` "
|
24
|
+
"in this example) exist in the federation configuration under the "
|
25
|
+
"`[tool.flwr.federations.<YOUR_FEDERATION>]` table of the `pyproject.toml` "
|
26
|
+
"for proper overriding."
|
27
|
+
)
|
flwr/cli/install.py
CHANGED
@@ -154,7 +154,7 @@ def validate_and_install(
|
|
154
154
|
)
|
155
155
|
raise typer.Exit(code=1)
|
156
156
|
|
157
|
-
|
157
|
+
fab_id, version = get_metadata_from_config(config)
|
158
158
|
publisher, project_name = fab_id.split("/")
|
159
159
|
config_metadata = (publisher, project_name, version, fab_hash)
|
160
160
|
|
flwr/cli/log.py
CHANGED
@@ -29,6 +29,7 @@ from flwr.cli.config_utils import (
|
|
29
29
|
process_loaded_project_config,
|
30
30
|
validate_federation_in_project_config,
|
31
31
|
)
|
32
|
+
from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
|
32
33
|
from flwr.common.constant import CONN_RECONNECT_INTERVAL, CONN_REFRESH_PERIOD
|
33
34
|
from flwr.common.logger import log as logger
|
34
35
|
from flwr.proto.exec_pb2 import StreamLogsRequest # pylint: disable=E0611
|
@@ -57,6 +58,8 @@ def start_stream(
|
|
57
58
|
logger(ERROR, "Invalid run_id `%s`, exiting", run_id)
|
58
59
|
if e.code() == grpc.StatusCode.CANCELLED:
|
59
60
|
pass
|
61
|
+
else:
|
62
|
+
raise e
|
60
63
|
finally:
|
61
64
|
channel.close()
|
62
65
|
|
@@ -123,6 +126,7 @@ def print_logs(run_id: int, channel: grpc.Channel, timeout: int) -> None:
|
|
123
126
|
break
|
124
127
|
if e.code() == grpc.StatusCode.CANCELLED:
|
125
128
|
break
|
129
|
+
raise e
|
126
130
|
except KeyboardInterrupt:
|
127
131
|
logger(DEBUG, "Stream interrupted by user")
|
128
132
|
finally:
|
@@ -143,6 +147,13 @@ def log(
|
|
143
147
|
Optional[str],
|
144
148
|
typer.Argument(help="Name of the federation to run the app on"),
|
145
149
|
] = None,
|
150
|
+
federation_config_overrides: Annotated[
|
151
|
+
Optional[list[str]],
|
152
|
+
typer.Option(
|
153
|
+
"--federation-config",
|
154
|
+
help=FEDERATION_CONFIG_HELP_MESSAGE,
|
155
|
+
),
|
156
|
+
] = None,
|
146
157
|
stream: Annotated[
|
147
158
|
bool,
|
148
159
|
typer.Option(
|
@@ -158,11 +169,15 @@ def log(
|
|
158
169
|
config, errors, warnings = load_and_validate(path=pyproject_path)
|
159
170
|
config = process_loaded_project_config(config, errors, warnings)
|
160
171
|
federation, federation_config = validate_federation_in_project_config(
|
161
|
-
federation, config
|
172
|
+
federation, config, federation_config_overrides
|
162
173
|
)
|
163
174
|
exit_if_no_address(federation_config, "log")
|
164
175
|
|
165
|
-
|
176
|
+
try:
|
177
|
+
_log_with_exec_api(app, federation, federation_config, run_id, stream)
|
178
|
+
except Exception as err: # pylint: disable=broad-except
|
179
|
+
typer.secho(str(err), fg=typer.colors.RED, bold=True)
|
180
|
+
raise typer.Exit(code=1) from None
|
166
181
|
|
167
182
|
|
168
183
|
def _log_with_exec_api(
|
flwr/cli/login/login.py
CHANGED
@@ -26,6 +26,7 @@ from flwr.cli.config_utils import (
|
|
26
26
|
process_loaded_project_config,
|
27
27
|
validate_federation_in_project_config,
|
28
28
|
)
|
29
|
+
from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
|
29
30
|
from flwr.common.typing import UserAuthLoginDetails
|
30
31
|
from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
|
31
32
|
GetLoginDetailsRequest,
|
@@ -45,6 +46,13 @@ def login( # pylint: disable=R0914
|
|
45
46
|
Optional[str],
|
46
47
|
typer.Argument(help="Name of the federation to login into."),
|
47
48
|
] = None,
|
49
|
+
federation_config_overrides: Annotated[
|
50
|
+
Optional[list[str]],
|
51
|
+
typer.Option(
|
52
|
+
"--federation-config",
|
53
|
+
help=FEDERATION_CONFIG_HELP_MESSAGE,
|
54
|
+
),
|
55
|
+
] = None,
|
48
56
|
) -> None:
|
49
57
|
"""Login to Flower SuperLink."""
|
50
58
|
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
@@ -54,7 +62,7 @@ def login( # pylint: disable=R0914
|
|
54
62
|
|
55
63
|
config = process_loaded_project_config(config, errors, warnings)
|
56
64
|
federation, federation_config = validate_federation_in_project_config(
|
57
|
-
federation, config
|
65
|
+
federation, config, federation_config_overrides
|
58
66
|
)
|
59
67
|
exit_if_no_address(federation_config, "login")
|
60
68
|
channel = init_channel(app, federation_config, None)
|
flwr/cli/ls.py
CHANGED
@@ -32,6 +32,7 @@ from flwr.cli.config_utils import (
|
|
32
32
|
process_loaded_project_config,
|
33
33
|
validate_federation_in_project_config,
|
34
34
|
)
|
35
|
+
from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
|
35
36
|
from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat, SubStatus
|
36
37
|
from flwr.common.date import format_timedelta, isoformat8601_utc
|
37
38
|
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
@@ -48,7 +49,7 @@ from .utils import init_channel, try_obtain_cli_auth_plugin, unauthenticated_exc
|
|
48
49
|
_RunListType = tuple[int, str, str, str, str, str, str, str, str]
|
49
50
|
|
50
51
|
|
51
|
-
def ls( # pylint: disable=too-many-locals, too-many-branches
|
52
|
+
def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
|
52
53
|
app: Annotated[
|
53
54
|
Path,
|
54
55
|
typer.Argument(help="Path of the Flower project"),
|
@@ -57,6 +58,13 @@ def ls( # pylint: disable=too-many-locals, too-many-branches
|
|
57
58
|
Optional[str],
|
58
59
|
typer.Argument(help="Name of the federation"),
|
59
60
|
] = None,
|
61
|
+
federation_config_overrides: Annotated[
|
62
|
+
Optional[list[str]],
|
63
|
+
typer.Option(
|
64
|
+
"--federation-config",
|
65
|
+
help=FEDERATION_CONFIG_HELP_MESSAGE,
|
66
|
+
),
|
67
|
+
] = None,
|
60
68
|
runs: Annotated[
|
61
69
|
bool,
|
62
70
|
typer.Option(
|
@@ -106,7 +114,7 @@ def ls( # pylint: disable=too-many-locals, too-many-branches
|
|
106
114
|
config, errors, warnings = load_and_validate(path=pyproject_path)
|
107
115
|
config = process_loaded_project_config(config, errors, warnings)
|
108
116
|
federation, federation_config = validate_federation_in_project_config(
|
109
|
-
federation, config
|
117
|
+
federation, config, federation_config_overrides
|
110
118
|
)
|
111
119
|
exit_if_no_address(federation_config, "ls")
|
112
120
|
|
flwr/cli/run/run.py
CHANGED
@@ -31,6 +31,7 @@ from flwr.cli.config_utils import (
|
|
31
31
|
process_loaded_project_config,
|
32
32
|
validate_federation_in_project_config,
|
33
33
|
)
|
34
|
+
from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
|
34
35
|
from flwr.common.config import (
|
35
36
|
flatten_dict,
|
36
37
|
parse_config_args,
|
@@ -57,7 +58,7 @@ from ..utils import (
|
|
57
58
|
CONN_REFRESH_PERIOD = 60 # Connection refresh period for log streaming (seconds)
|
58
59
|
|
59
60
|
|
60
|
-
# pylint: disable-next=too-many-locals
|
61
|
+
# pylint: disable-next=too-many-locals, R0913, R0917
|
61
62
|
def run(
|
62
63
|
app: Annotated[
|
63
64
|
Path,
|
@@ -67,16 +68,23 @@ def run(
|
|
67
68
|
Optional[str],
|
68
69
|
typer.Argument(help="Name of the federation to run the app on."),
|
69
70
|
] = None,
|
70
|
-
|
71
|
+
run_config_overrides: Annotated[
|
71
72
|
Optional[list[str]],
|
72
73
|
typer.Option(
|
73
74
|
"--run-config",
|
74
75
|
"-c",
|
75
|
-
help="Override configuration
|
76
|
-
|
77
|
-
"
|
78
|
-
"
|
79
|
-
"
|
76
|
+
help="Override run configuration values in the format:\n\n"
|
77
|
+
"`--run-config 'key1=value1 key2=value2' --run-config 'key3=value3'`\n\n"
|
78
|
+
"Values can be of any type supported in TOML, such as bool, int, "
|
79
|
+
"float, or string. Ensure that the keys (`key1`, `key2`, `key3` "
|
80
|
+
"in this example) exist in `pyproject.toml` for proper overriding.",
|
81
|
+
),
|
82
|
+
] = None,
|
83
|
+
federation_config_overrides: Annotated[
|
84
|
+
Optional[list[str]],
|
85
|
+
typer.Option(
|
86
|
+
"--federation-config",
|
87
|
+
help=FEDERATION_CONFIG_HELP_MESSAGE,
|
80
88
|
),
|
81
89
|
] = None,
|
82
90
|
stream: Annotated[
|
@@ -108,7 +116,7 @@ def run(
|
|
108
116
|
config, errors, warnings = load_and_validate(path=pyproject_path)
|
109
117
|
config = process_loaded_project_config(config, errors, warnings)
|
110
118
|
federation, federation_config = validate_federation_in_project_config(
|
111
|
-
federation, config
|
119
|
+
federation, config, federation_config_overrides
|
112
120
|
)
|
113
121
|
|
114
122
|
if "address" in federation_config:
|
@@ -116,12 +124,14 @@ def run(
|
|
116
124
|
app,
|
117
125
|
federation,
|
118
126
|
federation_config,
|
119
|
-
|
127
|
+
run_config_overrides,
|
120
128
|
stream,
|
121
129
|
output_format,
|
122
130
|
)
|
123
131
|
else:
|
124
|
-
_run_without_exec_api(
|
132
|
+
_run_without_exec_api(
|
133
|
+
app, federation_config, run_config_overrides, federation
|
134
|
+
)
|
125
135
|
except (typer.Exit, Exception) as err: # pylint: disable=broad-except
|
126
136
|
if suppress_output:
|
127
137
|
restore_output()
|
flwr/cli/stop.py
CHANGED
@@ -29,6 +29,7 @@ from flwr.cli.config_utils import (
|
|
29
29
|
process_loaded_project_config,
|
30
30
|
validate_federation_in_project_config,
|
31
31
|
)
|
32
|
+
from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
|
32
33
|
from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
|
33
34
|
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
34
35
|
from flwr.proto.exec_pb2 import StopRunRequest, StopRunResponse # pylint: disable=E0611
|
@@ -50,6 +51,13 @@ def stop( # pylint: disable=R0914
|
|
50
51
|
Optional[str],
|
51
52
|
typer.Argument(help="Name of the federation"),
|
52
53
|
] = None,
|
54
|
+
federation_config_overrides: Annotated[
|
55
|
+
Optional[list[str]],
|
56
|
+
typer.Option(
|
57
|
+
"--federation-config",
|
58
|
+
help=FEDERATION_CONFIG_HELP_MESSAGE,
|
59
|
+
),
|
60
|
+
] = None,
|
53
61
|
output_format: Annotated[
|
54
62
|
str,
|
55
63
|
typer.Option(
|
@@ -73,7 +81,7 @@ def stop( # pylint: disable=R0914
|
|
73
81
|
config, errors, warnings = load_and_validate(path=pyproject_path)
|
74
82
|
config = process_loaded_project_config(config, errors, warnings)
|
75
83
|
federation, federation_config = validate_federation_in_project_config(
|
76
|
-
federation, config
|
84
|
+
federation, config, federation_config_overrides
|
77
85
|
)
|
78
86
|
exit_if_no_address(federation_config, "stop")
|
79
87
|
|
flwr/client/app.py
CHANGED
@@ -16,11 +16,11 @@
|
|
16
16
|
|
17
17
|
|
18
18
|
import multiprocessing
|
19
|
-
import
|
19
|
+
import os
|
20
20
|
import sys
|
21
|
+
import threading
|
21
22
|
import time
|
22
23
|
from contextlib import AbstractContextManager
|
23
|
-
from dataclasses import dataclass
|
24
24
|
from logging import ERROR, INFO, WARN
|
25
25
|
from os import urandom
|
26
26
|
from pathlib import Path
|
@@ -45,7 +45,6 @@ from flwr.common.constant import (
|
|
45
45
|
ISOLATION_MODE_PROCESS,
|
46
46
|
ISOLATION_MODE_SUBPROCESS,
|
47
47
|
MAX_RETRY_DELAY,
|
48
|
-
MISSING_EXTRA_REST,
|
49
48
|
RUN_ID_NUM_BYTES,
|
50
49
|
SERVER_OCTET,
|
51
50
|
TRANSPORT_TYPE_GRPC_ADAPTER,
|
@@ -55,6 +54,7 @@ from flwr.common.constant import (
|
|
55
54
|
TRANSPORT_TYPES,
|
56
55
|
ErrorCode,
|
57
56
|
)
|
57
|
+
from flwr.common.exit import ExitCode, flwr_exit
|
58
58
|
from flwr.common.grpc import generic_create_grpc_server
|
59
59
|
from flwr.common.logger import log, warn_deprecated_feature
|
60
60
|
from flwr.common.message import Error
|
@@ -346,10 +346,7 @@ def start_client_internal(
|
|
346
346
|
transport, server_address
|
347
347
|
)
|
348
348
|
|
349
|
-
app_state_tracker = _AppStateTracker()
|
350
|
-
|
351
349
|
def _on_sucess(retry_state: RetryState) -> None:
|
352
|
-
app_state_tracker.is_connected = True
|
353
350
|
if retry_state.tries > 1:
|
354
351
|
log(
|
355
352
|
INFO,
|
@@ -359,7 +356,6 @@ def start_client_internal(
|
|
359
356
|
)
|
360
357
|
|
361
358
|
def _on_backoff(retry_state: RetryState) -> None:
|
362
|
-
app_state_tracker.is_connected = False
|
363
359
|
if retry_state.tries == 1:
|
364
360
|
log(WARN, "Connection attempt failed, retrying...")
|
365
361
|
else:
|
@@ -396,7 +392,7 @@ def start_client_internal(
|
|
396
392
|
|
397
393
|
runs: dict[int, Run] = {}
|
398
394
|
|
399
|
-
while
|
395
|
+
while True:
|
400
396
|
sleep_duration: int = 0
|
401
397
|
with connection(
|
402
398
|
address,
|
@@ -435,9 +431,8 @@ def start_client_internal(
|
|
435
431
|
node_config=node_config,
|
436
432
|
)
|
437
433
|
|
438
|
-
app_state_tracker.register_signal_handler()
|
439
434
|
# pylint: disable=too-many-nested-blocks
|
440
|
-
while
|
435
|
+
while True:
|
441
436
|
try:
|
442
437
|
# Receive
|
443
438
|
message = receive()
|
@@ -553,7 +548,7 @@ def start_client_internal(
|
|
553
548
|
|
554
549
|
proc = mp_spawn_context.Process(
|
555
550
|
target=_run_flwr_clientapp,
|
556
|
-
args=(command,),
|
551
|
+
args=(command, os.getpid()),
|
557
552
|
daemon=True,
|
558
553
|
)
|
559
554
|
proc.start()
|
@@ -595,10 +590,7 @@ def start_client_internal(
|
|
595
590
|
e_code = ErrorCode.LOAD_CLIENT_APP_EXCEPTION
|
596
591
|
exc_entity = "SuperNode"
|
597
592
|
|
598
|
-
|
599
|
-
log(
|
600
|
-
ERROR, "%s raised an exception", exc_entity, exc_info=ex
|
601
|
-
)
|
593
|
+
log(ERROR, "%s raised an exception", exc_entity, exc_info=ex)
|
602
594
|
|
603
595
|
# Create error message
|
604
596
|
reply_message = message.create_error_reply(
|
@@ -624,19 +616,14 @@ def start_client_internal(
|
|
624
616
|
run_id,
|
625
617
|
)
|
626
618
|
log(INFO, "")
|
627
|
-
|
628
|
-
except StopIteration:
|
629
|
-
sleep_duration = 0
|
630
|
-
break
|
631
619
|
# pylint: enable=too-many-nested-blocks
|
632
620
|
|
633
621
|
# Unregister node
|
634
|
-
if delete_node is not None
|
622
|
+
if delete_node is not None:
|
635
623
|
delete_node() # pylint: disable=not-callable
|
636
624
|
|
637
625
|
if sleep_duration == 0:
|
638
626
|
log(INFO, "Disconnect and shut down")
|
639
|
-
del app_state_tracker
|
640
627
|
break
|
641
628
|
|
642
629
|
# Sleep and reconnect afterwards
|
@@ -776,7 +763,10 @@ def _init_connection(transport: Optional[str], server_address: str) -> tuple[
|
|
776
763
|
# Parse IP address
|
777
764
|
parsed_address = parse_address(server_address)
|
778
765
|
if not parsed_address:
|
779
|
-
|
766
|
+
flwr_exit(
|
767
|
+
ExitCode.COMMON_ADDRESS_INVALID,
|
768
|
+
f"SuperLink address ({server_address}) cannot be parsed.",
|
769
|
+
)
|
780
770
|
host, port, is_v6 = parsed_address
|
781
771
|
address = f"[{host}]:{port}" if is_v6 else f"{host}:{port}"
|
782
772
|
|
@@ -791,12 +781,9 @@ def _init_connection(transport: Optional[str], server_address: str) -> tuple[
|
|
791
781
|
|
792
782
|
from .rest_client.connection import http_request_response
|
793
783
|
except ModuleNotFoundError:
|
794
|
-
|
784
|
+
flwr_exit(ExitCode.COMMON_MISSING_EXTRA_REST)
|
795
785
|
if server_address[:4] != "http":
|
796
|
-
|
797
|
-
"When using the REST API, please provide `https://` or "
|
798
|
-
"`http://` before the server address (e.g. `http://127.0.0.1:8080`)"
|
799
|
-
)
|
786
|
+
flwr_exit(ExitCode.SUPERNODE_REST_ADDRESS_INVALID)
|
800
787
|
connection, error_type = http_request_response, RequestsConnectionError
|
801
788
|
elif transport == TRANSPORT_TYPE_GRPC_RERE:
|
802
789
|
connection, error_type = grpc_request_response, RpcError
|
@@ -812,24 +799,17 @@ def _init_connection(transport: Optional[str], server_address: str) -> tuple[
|
|
812
799
|
return connection, address, error_type
|
813
800
|
|
814
801
|
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
def signal_handler(sig, frame): # type: ignore
|
824
|
-
# pylint: disable=unused-argument
|
825
|
-
self.interrupt = True
|
826
|
-
raise StopIteration from None
|
827
|
-
|
828
|
-
signal.signal(signal.SIGINT, signal_handler)
|
829
|
-
signal.signal(signal.SIGTERM, signal_handler)
|
802
|
+
def _run_flwr_clientapp(args: list[str], main_pid: int) -> None:
|
803
|
+
# Monitor the main process in case of SIGKILL
|
804
|
+
def main_process_monitor() -> None:
|
805
|
+
while True:
|
806
|
+
time.sleep(1)
|
807
|
+
if os.getppid() != main_pid:
|
808
|
+
os.kill(os.getpid(), 9)
|
830
809
|
|
810
|
+
threading.Thread(target=main_process_monitor, daemon=True).start()
|
831
811
|
|
832
|
-
|
812
|
+
# Run the command
|
833
813
|
sys.argv = args
|
834
814
|
flwr_clientapp()
|
835
815
|
|
flwr/client/clientapp/app.py
CHANGED
@@ -16,7 +16,6 @@
|
|
16
16
|
|
17
17
|
|
18
18
|
import argparse
|
19
|
-
import sys
|
20
19
|
import time
|
21
20
|
from logging import DEBUG, ERROR, INFO
|
22
21
|
from typing import Optional
|
@@ -29,6 +28,7 @@ from flwr.common import Context, Message
|
|
29
28
|
from flwr.common.args import add_args_flwr_app_common
|
30
29
|
from flwr.common.config import get_flwr_dir
|
31
30
|
from flwr.common.constant import CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS, ErrorCode
|
31
|
+
from flwr.common.exit import ExitCode, flwr_exit
|
32
32
|
from flwr.common.grpc import create_channel
|
33
33
|
from flwr.common.logger import log
|
34
34
|
from flwr.common.message import Error
|
@@ -61,12 +61,10 @@ def flwr_clientapp() -> None:
|
|
61
61
|
"""Run process-isolated Flower ClientApp."""
|
62
62
|
args = _parse_args_run_flwr_clientapp().parse_args()
|
63
63
|
if not args.insecure:
|
64
|
-
|
65
|
-
|
66
|
-
"flwr-clientapp does not support TLS yet.
|
67
|
-
"Please use the '--insecure' flag.",
|
64
|
+
flwr_exit(
|
65
|
+
ExitCode.COMMON_TLS_NOT_SUPPORTED,
|
66
|
+
"flwr-clientapp does not support TLS yet.",
|
68
67
|
)
|
69
|
-
sys.exit(1)
|
70
68
|
|
71
69
|
log(INFO, "Starting Flower ClientApp")
|
72
70
|
log(
|