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.
Files changed (82) hide show
  1. flwr/cli/config_utils.py +23 -146
  2. flwr/cli/constant.py +27 -0
  3. flwr/cli/install.py +1 -1
  4. flwr/cli/log.py +17 -2
  5. flwr/cli/login/login.py +9 -1
  6. flwr/cli/ls.py +10 -2
  7. flwr/cli/run/run.py +20 -10
  8. flwr/cli/stop.py +9 -1
  9. flwr/client/app.py +23 -43
  10. flwr/client/clientapp/app.py +4 -6
  11. flwr/client/clientapp/utils.py +1 -1
  12. flwr/client/grpc_client/connection.py +0 -6
  13. flwr/client/grpc_rere_client/client_interceptor.py +19 -125
  14. flwr/client/grpc_rere_client/connection.py +10 -0
  15. flwr/client/rest_client/connection.py +12 -3
  16. flwr/client/supernode/app.py +14 -20
  17. flwr/common/auth_plugin/auth_plugin.py +1 -0
  18. flwr/common/config.py +152 -15
  19. flwr/common/constant.py +9 -8
  20. flwr/common/exit/__init__.py +24 -0
  21. flwr/common/exit/exit.py +99 -0
  22. flwr/common/exit/exit_code.py +93 -0
  23. flwr/common/exit_handlers.py +24 -10
  24. flwr/common/grpc.py +7 -0
  25. flwr/common/logger.py +1 -1
  26. flwr/common/serde.py +6 -4
  27. flwr/proto/clientappio_pb2.py +13 -3
  28. flwr/proto/clientappio_pb2_grpc.py +63 -12
  29. flwr/proto/error_pb2.py +13 -3
  30. flwr/proto/error_pb2_grpc.py +20 -0
  31. flwr/proto/exec_pb2.py +15 -5
  32. flwr/proto/exec_pb2_grpc.py +105 -24
  33. flwr/proto/fab_pb2.py +13 -3
  34. flwr/proto/fab_pb2_grpc.py +20 -0
  35. flwr/proto/fleet_pb2.py +15 -5
  36. flwr/proto/fleet_pb2_grpc.py +147 -36
  37. flwr/proto/grpcadapter_pb2.py +14 -4
  38. flwr/proto/grpcadapter_pb2_grpc.py +35 -4
  39. flwr/proto/log_pb2.py +13 -3
  40. flwr/proto/log_pb2_grpc.py +20 -0
  41. flwr/proto/message_pb2.py +15 -5
  42. flwr/proto/message_pb2_grpc.py +20 -0
  43. flwr/proto/node_pb2.py +15 -5
  44. flwr/proto/node_pb2.pyi +1 -4
  45. flwr/proto/node_pb2_grpc.py +20 -0
  46. flwr/proto/recordset_pb2.py +18 -8
  47. flwr/proto/recordset_pb2_grpc.py +20 -0
  48. flwr/proto/run_pb2.py +16 -6
  49. flwr/proto/run_pb2_grpc.py +20 -0
  50. flwr/proto/serverappio_pb2.py +32 -14
  51. flwr/proto/serverappio_pb2.pyi +56 -0
  52. flwr/proto/serverappio_pb2_grpc.py +261 -44
  53. flwr/proto/serverappio_pb2_grpc.pyi +20 -0
  54. flwr/proto/simulationio_pb2.py +13 -3
  55. flwr/proto/simulationio_pb2_grpc.py +105 -24
  56. flwr/proto/task_pb2.py +13 -3
  57. flwr/proto/task_pb2_grpc.py +20 -0
  58. flwr/proto/transport_pb2.py +20 -10
  59. flwr/proto/transport_pb2_grpc.py +35 -4
  60. flwr/server/app.py +40 -11
  61. flwr/server/compat/app_utils.py +0 -1
  62. flwr/server/compat/driver_client_proxy.py +1 -2
  63. flwr/server/driver/grpc_driver.py +5 -2
  64. flwr/server/driver/inmemory_driver.py +2 -1
  65. flwr/server/serverapp/app.py +5 -6
  66. flwr/server/superlink/driver/serverappio_servicer.py +110 -6
  67. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +20 -88
  68. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +95 -169
  69. flwr/server/superlink/fleet/message_handler/message_handler.py +4 -5
  70. flwr/server/superlink/fleet/rest_rere/rest_api.py +2 -3
  71. flwr/server/superlink/linkstate/in_memory_linkstate.py +14 -26
  72. flwr/server/superlink/linkstate/linkstate.py +5 -18
  73. flwr/server/superlink/linkstate/sqlite_linkstate.py +30 -70
  74. flwr/server/superlink/linkstate/utils.py +18 -8
  75. flwr/server/utils/validator.py +9 -34
  76. flwr/simulation/app.py +4 -6
  77. flwr/simulation/legacy_app.py +4 -2
  78. {flwr_nightly-1.15.0.dev20250114.dist-info → flwr_nightly-1.15.0.dev20250123.dist-info}/METADATA +4 -4
  79. {flwr_nightly-1.15.0.dev20250114.dist-info → flwr_nightly-1.15.0.dev20250123.dist-info}/RECORD +82 -78
  80. {flwr_nightly-1.15.0.dev20250114.dist-info → flwr_nightly-1.15.0.dev20250123.dist-info}/LICENSE +0 -0
  81. {flwr_nightly-1.15.0.dev20250114.dist-info → flwr_nightly-1.15.0.dev20250123.dist-info}/WHEEL +0 -0
  82. {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 IO, Any, Optional, Union, get_args
19
+ from typing import Any, Optional, Union
22
20
 
23
21
  import tomli
24
22
  import typer
25
23
 
26
- from flwr.common import object_ref
27
- from flwr.common.typing import UserConfigValue
28
-
29
-
30
- def get_fab_config(fab_file: Union[Path, bytes]) -> dict[str, Any]:
31
- """Extract the config from a FAB file or path.
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
- conf = get_fab_config(fab_file)
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 = validate(config, check_module, path.parent)
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(encoding="utf-8") as toml_file:
137
- return load_from_string(toml_file.read())
138
-
139
-
140
- def _validate_run_config(config_dict: dict[str, Any], errors: list[str]) -> None:
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], config: dict[str, Any]
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
- version, fab_id = get_metadata_from_config(config)
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
- _log_with_exec_api(app, federation, federation_config, run_id, stream)
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
- config_overrides: Annotated[
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 key-value pairs, should be of the format:\n\n"
76
- '`--run-config \'key1="value1" key2="value2"\' '
77
- "--run-config 'key3=\"value3\"'`\n\n"
78
- "Note that `key1`, `key2`, and `key3` in this example need to exist "
79
- "inside the `pyproject.toml` in order to be properly overriden.",
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
- config_overrides,
127
+ run_config_overrides,
120
128
  stream,
121
129
  output_format,
122
130
  )
123
131
  else:
124
- _run_without_exec_api(app, federation_config, config_overrides, federation)
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 signal
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 not app_state_tracker.interrupt:
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 not app_state_tracker.interrupt:
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
- if not app_state_tracker.interrupt:
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 and app_state_tracker.is_connected:
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
- sys.exit(f"Server address ({server_address}) cannot be parsed.")
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
- sys.exit(MISSING_EXTRA_REST)
784
+ flwr_exit(ExitCode.COMMON_MISSING_EXTRA_REST)
795
785
  if server_address[:4] != "http":
796
- sys.exit(
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
- @dataclass
816
- class _AppStateTracker:
817
- interrupt: bool = False
818
- is_connected: bool = False
819
-
820
- def register_signal_handler(self) -> None:
821
- """Register handlers for exit signals."""
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
- def _run_flwr_clientapp(args: list[str]) -> None:
812
+ # Run the command
833
813
  sys.argv = args
834
814
  flwr_clientapp()
835
815
 
@@ -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
- log(
65
- ERROR,
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(