flwr 1.14.0__py3-none-any.whl → 1.15.1__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 (103) hide show
  1. flwr/cli/auth_plugin/__init__.py +31 -0
  2. flwr/cli/auth_plugin/oidc_cli_plugin.py +150 -0
  3. flwr/cli/cli_user_auth_interceptor.py +6 -2
  4. flwr/cli/config_utils.py +24 -147
  5. flwr/cli/constant.py +27 -0
  6. flwr/cli/install.py +1 -1
  7. flwr/cli/log.py +18 -3
  8. flwr/cli/login/login.py +43 -8
  9. flwr/cli/ls.py +14 -5
  10. flwr/cli/new/templates/app/README.md.tpl +3 -2
  11. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +4 -4
  12. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +2 -2
  13. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +4 -4
  14. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +2 -2
  15. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +2 -2
  16. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +2 -2
  17. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +4 -4
  18. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +3 -3
  19. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +2 -2
  20. flwr/cli/run/run.py +21 -11
  21. flwr/cli/stop.py +13 -4
  22. flwr/cli/utils.py +54 -40
  23. flwr/client/app.py +36 -48
  24. flwr/client/clientapp/app.py +19 -25
  25. flwr/client/clientapp/utils.py +1 -1
  26. flwr/client/grpc_client/connection.py +1 -12
  27. flwr/client/grpc_rere_client/client_interceptor.py +19 -119
  28. flwr/client/grpc_rere_client/connection.py +46 -36
  29. flwr/client/grpc_rere_client/grpc_adapter.py +12 -12
  30. flwr/client/message_handler/task_handler.py +0 -17
  31. flwr/client/rest_client/connection.py +34 -26
  32. flwr/client/supernode/app.py +18 -72
  33. flwr/common/args.py +25 -47
  34. flwr/common/auth_plugin/auth_plugin.py +34 -23
  35. flwr/common/config.py +166 -16
  36. flwr/common/constant.py +24 -9
  37. flwr/common/differential_privacy.py +2 -1
  38. flwr/common/exit/__init__.py +24 -0
  39. flwr/common/exit/exit.py +99 -0
  40. flwr/common/exit/exit_code.py +93 -0
  41. flwr/common/exit_handlers.py +32 -30
  42. flwr/common/grpc.py +167 -4
  43. flwr/common/logger.py +26 -7
  44. flwr/common/object_ref.py +0 -14
  45. flwr/common/record/recordset.py +1 -1
  46. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +45 -0
  47. flwr/common/serde.py +6 -4
  48. flwr/common/typing.py +20 -0
  49. flwr/proto/clientappio_pb2.py +1 -1
  50. flwr/proto/error_pb2.py +1 -1
  51. flwr/proto/exec_pb2.py +13 -25
  52. flwr/proto/exec_pb2.pyi +27 -54
  53. flwr/proto/fab_pb2.py +1 -1
  54. flwr/proto/fleet_pb2.py +31 -31
  55. flwr/proto/fleet_pb2.pyi +23 -23
  56. flwr/proto/fleet_pb2_grpc.py +30 -30
  57. flwr/proto/fleet_pb2_grpc.pyi +20 -20
  58. flwr/proto/grpcadapter_pb2.py +1 -1
  59. flwr/proto/log_pb2.py +1 -1
  60. flwr/proto/message_pb2.py +1 -1
  61. flwr/proto/node_pb2.py +3 -3
  62. flwr/proto/node_pb2.pyi +1 -4
  63. flwr/proto/recordset_pb2.py +1 -1
  64. flwr/proto/run_pb2.py +1 -1
  65. flwr/proto/serverappio_pb2.py +24 -25
  66. flwr/proto/serverappio_pb2.pyi +26 -32
  67. flwr/proto/serverappio_pb2_grpc.py +28 -28
  68. flwr/proto/serverappio_pb2_grpc.pyi +16 -16
  69. flwr/proto/simulationio_pb2.py +1 -1
  70. flwr/proto/task_pb2.py +1 -1
  71. flwr/proto/transport_pb2.py +1 -1
  72. flwr/server/app.py +116 -128
  73. flwr/server/compat/app_utils.py +0 -1
  74. flwr/server/compat/driver_client_proxy.py +1 -2
  75. flwr/server/driver/grpc_driver.py +32 -27
  76. flwr/server/driver/inmemory_driver.py +2 -1
  77. flwr/server/serverapp/app.py +12 -10
  78. flwr/server/superlink/driver/serverappio_grpc.py +1 -1
  79. flwr/server/superlink/driver/serverappio_servicer.py +74 -48
  80. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +20 -88
  81. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +2 -165
  82. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +25 -24
  83. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +110 -168
  84. flwr/server/superlink/fleet/message_handler/message_handler.py +37 -24
  85. flwr/server/superlink/fleet/rest_rere/rest_api.py +16 -18
  86. flwr/server/superlink/fleet/vce/vce_api.py +2 -2
  87. flwr/server/superlink/linkstate/in_memory_linkstate.py +45 -75
  88. flwr/server/superlink/linkstate/linkstate.py +17 -38
  89. flwr/server/superlink/linkstate/sqlite_linkstate.py +81 -145
  90. flwr/server/superlink/linkstate/utils.py +18 -8
  91. flwr/server/superlink/simulation/simulationio_grpc.py +1 -1
  92. flwr/server/utils/validator.py +9 -34
  93. flwr/simulation/app.py +4 -6
  94. flwr/simulation/legacy_app.py +4 -2
  95. flwr/simulation/run_simulation.py +1 -1
  96. flwr/simulation/simulationio_connection.py +2 -1
  97. flwr/superexec/exec_grpc.py +1 -1
  98. flwr/superexec/exec_servicer.py +23 -2
  99. {flwr-1.14.0.dist-info → flwr-1.15.1.dist-info}/METADATA +8 -8
  100. {flwr-1.14.0.dist-info → flwr-1.15.1.dist-info}/RECORD +103 -97
  101. {flwr-1.14.0.dist-info → flwr-1.15.1.dist-info}/LICENSE +0 -0
  102. {flwr-1.14.0.dist-info → flwr-1.15.1.dist-info}/WHEEL +0 -0
  103. {flwr-1.14.0.dist-info → flwr-1.15.1.dist-info}/entry_points.txt +0 -0
@@ -18,8 +18,9 @@ Refer to the [How to Run Simulations](https://flower.ai/docs/framework/how-to-ru
18
18
 
19
19
  ## Run with the Deployment Engine
20
20
 
21
- > \[!NOTE\]
22
- > An update to this example will show how to run this Flower application with the Deployment Engine and TLS certificates, or with Docker.
21
+ Follow this [how-to guide](https://flower.ai/docs/framework/how-to-run-flower-with-deployment-engine.html) to run the same app in this example but with Flower's Deployment Engine. After that, you might be intersted in setting up [secure TLS-enabled communications](https://flower.ai/docs/framework/how-to-enable-tls-connections.html) and [SuperNode authentication](https://flower.ai/docs/framework/how-to-authenticate-supernodes.html) in your federation.
22
+
23
+ You can run Flower on Docker too! Check out the [Flower with Docker](https://flower.ai/docs/framework/docker/index.html) documentation.
23
24
 
24
25
  ## Resources
25
26
 
@@ -8,10 +8,10 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.13.1",
12
- "flwr-datasets[vision]>=0.3.0",
13
- "torch==2.2.1",
14
- "torchvision==0.17.1",
11
+ "flwr[simulation]>=1.15.1",
12
+ "flwr-datasets[vision]>=0.5.0",
13
+ "torch==2.5.1",
14
+ "torchvision==0.20.1",
15
15
  ]
16
16
 
17
17
  [tool.hatch.metadata]
@@ -8,8 +8,8 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.13.1",
12
- "flwr-datasets>=0.3.0",
11
+ "flwr[simulation]>=1.15.1",
12
+ "flwr-datasets>=0.5.0",
13
13
  "torch==2.3.1",
14
14
  "trl==0.8.1",
15
15
  "bitsandbytes==0.45.0",
@@ -8,13 +8,13 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.13.1",
12
- "flwr-datasets>=0.3.0",
13
- "torch==2.2.1",
11
+ "flwr[simulation]>=1.15.1",
12
+ "flwr-datasets>=0.5.0",
13
+ "torch==2.5.1",
14
14
  "transformers>=4.30.0,<5.0",
15
15
  "evaluate>=0.4.0,<1.0",
16
16
  "datasets>=2.0.0, <3.0",
17
- "scikit-learn>=1.3.1, <2.0",
17
+ "scikit-learn>=1.6.1, <2.0",
18
18
  ]
19
19
 
20
20
  [tool.hatch.build.targets.wheel]
@@ -8,10 +8,10 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.13.1",
11
+ "flwr[simulation]>=1.15.1",
12
12
  "jax==0.4.30",
13
13
  "jaxlib==0.4.30",
14
- "scikit-learn==1.3.2",
14
+ "scikit-learn==1.6.1",
15
15
  ]
16
16
 
17
17
  [tool.hatch.build.targets.wheel]
@@ -8,8 +8,8 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.13.1",
12
- "flwr-datasets[vision]>=0.3.0",
11
+ "flwr[simulation]>=1.15.1",
12
+ "flwr-datasets[vision]>=0.5.0",
13
13
  "mlx==0.21.1",
14
14
  ]
15
15
 
@@ -8,8 +8,8 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.13.1",
12
- "numpy>=1.21.0",
11
+ "flwr[simulation]>=1.15.1",
12
+ "numpy>=2.0.2",
13
13
  ]
14
14
 
15
15
  [tool.hatch.build.targets.wheel]
@@ -8,10 +8,10 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.13.1",
12
- "flwr-datasets[vision]>=0.3.0",
13
- "torch==2.2.1",
14
- "torchvision==0.17.1",
11
+ "flwr[simulation]>=1.15.1",
12
+ "flwr-datasets[vision]>=0.5.0",
13
+ "torch==2.5.1",
14
+ "torchvision==0.20.1",
15
15
  ]
16
16
 
17
17
  [tool.hatch.build.targets.wheel]
@@ -8,9 +8,9 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.13.1",
12
- "flwr-datasets[vision]>=0.3.0",
13
- "scikit-learn>=1.1.1",
11
+ "flwr[simulation]>=1.15.1",
12
+ "flwr-datasets[vision]>=0.5.0",
13
+ "scikit-learn>=1.6.1",
14
14
  ]
15
15
 
16
16
  [tool.hatch.build.targets.wheel]
@@ -8,8 +8,8 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.13.1",
12
- "flwr-datasets[vision]>=0.3.0",
11
+ "flwr[simulation]>=1.15.1",
12
+ "flwr-datasets[vision]>=0.5.0",
13
13
  "tensorflow>=2.11.1,<2.18.0",
14
14
  ]
15
15
 
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()
@@ -148,7 +158,7 @@ def _run_with_exec_api(
148
158
  stream: bool,
149
159
  output_format: str,
150
160
  ) -> None:
151
- auth_plugin = try_obtain_cli_auth_plugin(app, federation)
161
+ auth_plugin = try_obtain_cli_auth_plugin(app, federation, federation_config)
152
162
  channel = init_channel(app, federation_config, auth_plugin)
153
163
  stub = ExecStub(channel)
154
164
 
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,12 +81,12 @@ 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
+ channel = None
80
88
  try:
81
- auth_plugin = try_obtain_cli_auth_plugin(app, federation)
89
+ auth_plugin = try_obtain_cli_auth_plugin(app, federation, federation_config)
82
90
  channel = init_channel(app, federation_config, auth_plugin)
83
91
  stub = ExecStub(channel) # pylint: disable=unused-variable # noqa: F841
84
92
 
@@ -93,7 +101,8 @@ def stop( # pylint: disable=R0914
93
101
  )
94
102
  raise typer.Exit(code=1) from err
95
103
  finally:
96
- channel.close()
104
+ if channel:
105
+ channel.close()
97
106
  except (typer.Exit, Exception) as err: # pylint: disable=broad-except
98
107
  if suppress_output:
99
108
  restore_output()
flwr/cli/utils.py CHANGED
@@ -20,7 +20,6 @@ import json
20
20
  import re
21
21
  from collections.abc import Iterator
22
22
  from contextlib import contextmanager
23
- from logging import DEBUG
24
23
  from pathlib import Path
25
24
  from typing import Any, Callable, Optional, Union, cast
26
25
 
@@ -29,20 +28,16 @@ import typer
29
28
 
30
29
  from flwr.cli.cli_user_auth_interceptor import CliUserAuthInterceptor
31
30
  from flwr.common.auth_plugin import CliAuthPlugin
32
- from flwr.common.constant import AUTH_TYPE, CREDENTIALS_DIR, FLWR_DIR
33
- from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel
34
- from flwr.common.logger import log
35
-
31
+ from flwr.common.constant import AUTH_TYPE_JSON_KEY, CREDENTIALS_DIR, FLWR_DIR
32
+ from flwr.common.grpc import (
33
+ GRPC_MAX_MESSAGE_LENGTH,
34
+ create_channel,
35
+ on_channel_state_change,
36
+ )
37
+
38
+ from .auth_plugin import get_cli_auth_plugins
36
39
  from .config_utils import validate_certificate_in_federation_config
37
40
 
38
- try:
39
- from flwr.ee import get_cli_auth_plugins
40
- except ImportError:
41
-
42
- def get_cli_auth_plugins() -> dict[str, type[CliAuthPlugin]]:
43
- """Return all CLI authentication plugins."""
44
- raise NotImplementedError("No authentication plugins are currently supported.")
45
-
46
41
 
47
42
  def prompt_text(
48
43
  text: str,
@@ -217,25 +212,42 @@ def get_user_auth_config_path(root_dir: Path, federation: str) -> Path:
217
212
  def try_obtain_cli_auth_plugin(
218
213
  root_dir: Path,
219
214
  federation: str,
215
+ federation_config: dict[str, Any],
220
216
  auth_type: Optional[str] = None,
221
217
  ) -> Optional[CliAuthPlugin]:
222
218
  """Load the CLI-side user auth plugin for the given auth type."""
223
- config_path = get_user_auth_config_path(root_dir, federation)
224
-
225
- # Load the config file if it exists
226
- config: dict[str, Any] = {}
227
- if config_path.exists():
228
- with config_path.open("r", encoding="utf-8") as file:
229
- config = json.load(file)
230
- # This is the case when the user auth is not enabled
231
- elif auth_type is None:
219
+ # Check if user auth is enabled
220
+ if not federation_config.get("enable-user-auth", False):
232
221
  return None
233
222
 
223
+ # Check if TLS is enabled. If not, raise an error
224
+ if federation_config.get("root-certificates") is None:
225
+ typer.secho(
226
+ "❌ User authentication requires TLS to be enabled. "
227
+ "Please provide 'root-certificates' in the federation"
228
+ " configuration.",
229
+ fg=typer.colors.RED,
230
+ bold=True,
231
+ )
232
+ raise typer.Exit(code=1)
233
+
234
+ config_path = get_user_auth_config_path(root_dir, federation)
235
+
234
236
  # Get the auth type from the config if not provided
237
+ # auth_type will be None for all CLI commands except login
235
238
  if auth_type is None:
236
- if AUTH_TYPE not in config:
237
- return None
238
- auth_type = config[AUTH_TYPE]
239
+ try:
240
+ with config_path.open("r", encoding="utf-8") as file:
241
+ json_file = json.load(file)
242
+ auth_type = json_file[AUTH_TYPE_JSON_KEY]
243
+ except (FileNotFoundError, KeyError):
244
+ typer.secho(
245
+ "❌ Missing or invalid credentials for user authentication. "
246
+ "Please run `flwr login` to authenticate.",
247
+ fg=typer.colors.RED,
248
+ bold=True,
249
+ )
250
+ raise typer.Exit(code=1) from None
239
251
 
240
252
  # Retrieve auth plugin class and instantiate it
241
253
  try:
@@ -254,11 +266,6 @@ def init_channel(
254
266
  app: Path, federation_config: dict[str, Any], auth_plugin: Optional[CliAuthPlugin]
255
267
  ) -> grpc.Channel:
256
268
  """Initialize gRPC channel to the Exec API."""
257
-
258
- def on_channel_state_change(channel_connectivity: str) -> None:
259
- """Log channel connectivity."""
260
- log(DEBUG, channel_connectivity)
261
-
262
269
  insecure, root_certificates_bytes = validate_certificate_in_federation_config(
263
270
  app, federation_config
264
271
  )
@@ -267,7 +274,7 @@ def init_channel(
267
274
  interceptors: list[grpc.UnaryUnaryClientInterceptor] = []
268
275
  if auth_plugin is not None:
269
276
  auth_plugin.load_tokens()
270
- interceptors = CliUserAuthInterceptor(auth_plugin)
277
+ interceptors.append(CliUserAuthInterceptor(auth_plugin))
271
278
 
272
279
  # Create the gRPC channel
273
280
  channel = create_channel(
@@ -291,12 +298,19 @@ def unauthenticated_exc_handler() -> Iterator[None]:
291
298
  try:
292
299
  yield
293
300
  except grpc.RpcError as e:
294
- if e.code() != grpc.StatusCode.UNAUTHENTICATED:
295
- raise
296
- typer.secho(
297
- " Authentication failed. Please run `flwr login`"
298
- " to authenticate and try again.",
299
- fg=typer.colors.RED,
300
- bold=True,
301
- )
302
- raise typer.Exit(code=1) from None
301
+ if e.code() == grpc.StatusCode.UNAUTHENTICATED:
302
+ typer.secho(
303
+ "❌ Authentication failed. Please run `flwr login`"
304
+ " to authenticate and try again.",
305
+ fg=typer.colors.RED,
306
+ bold=True,
307
+ )
308
+ raise typer.Exit(code=1) from None
309
+ if e.code() == grpc.StatusCode.UNIMPLEMENTED:
310
+ typer.secho(
311
+ "❌ User authentication is not enabled on this SuperLink.",
312
+ fg=typer.colors.RED,
313
+ bold=True,
314
+ )
315
+ raise typer.Exit(code=1) from None
316
+ raise
flwr/client/app.py CHANGED
@@ -15,13 +15,14 @@
15
15
  """Flower client app."""
16
16
 
17
17
 
18
- import signal
19
- import subprocess
18
+ import multiprocessing
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
+ from os import urandom
25
26
  from pathlib import Path
26
27
  from typing import Callable, Optional, Union, cast
27
28
 
@@ -33,6 +34,7 @@ from flwr.cli.config_utils import get_fab_metadata
33
34
  from flwr.cli.install import install_from_fab
34
35
  from flwr.client.client import Client
35
36
  from flwr.client.client_app import ClientApp, LoadClientAppError
37
+ from flwr.client.clientapp.app import flwr_clientapp
36
38
  from flwr.client.nodestate.nodestate_factory import NodeStateFactory
37
39
  from flwr.client.typing import ClientFnExt
38
40
  from flwr.common import GRPC_MAX_MESSAGE_LENGTH, Context, EventType, Message, event
@@ -43,7 +45,6 @@ from flwr.common.constant import (
43
45
  ISOLATION_MODE_PROCESS,
44
46
  ISOLATION_MODE_SUBPROCESS,
45
47
  MAX_RETRY_DELAY,
46
- MISSING_EXTRA_REST,
47
48
  RUN_ID_NUM_BYTES,
48
49
  SERVER_OCTET,
49
50
  TRANSPORT_TYPE_GRPC_ADAPTER,
@@ -53,13 +54,13 @@ from flwr.common.constant import (
53
54
  TRANSPORT_TYPES,
54
55
  ErrorCode,
55
56
  )
57
+ from flwr.common.exit import ExitCode, flwr_exit
58
+ from flwr.common.grpc import generic_create_grpc_server
56
59
  from flwr.common.logger import log, warn_deprecated_feature
57
60
  from flwr.common.message import Error
58
61
  from flwr.common.retry_invoker import RetryInvoker, RetryState, exponential
59
62
  from flwr.common.typing import Fab, Run, RunNotRunningException, UserConfig
60
63
  from flwr.proto.clientappio_pb2_grpc import add_ClientAppIoServicer_to_server
61
- from flwr.server.superlink.fleet.grpc_bidi.grpc_server import generic_create_grpc_server
62
- from flwr.server.superlink.linkstate.utils import generate_rand_int_from_bytes
63
64
 
64
65
  from .clientapp.clientappio_servicer import ClientAppInputs, ClientAppIoServicer
65
66
  from .grpc_adapter_client.connection import grpc_adapter
@@ -345,10 +346,7 @@ def start_client_internal(
345
346
  transport, server_address
346
347
  )
347
348
 
348
- app_state_tracker = _AppStateTracker()
349
-
350
349
  def _on_sucess(retry_state: RetryState) -> None:
351
- app_state_tracker.is_connected = True
352
350
  if retry_state.tries > 1:
353
351
  log(
354
352
  INFO,
@@ -358,7 +356,6 @@ def start_client_internal(
358
356
  )
359
357
 
360
358
  def _on_backoff(retry_state: RetryState) -> None:
361
- app_state_tracker.is_connected = False
362
359
  if retry_state.tries == 1:
363
360
  log(WARN, "Connection attempt failed, retrying...")
364
361
  else:
@@ -391,10 +388,11 @@ def start_client_internal(
391
388
  run_info_store: Optional[DeprecatedRunInfoStore] = None
392
389
  state_factory = NodeStateFactory()
393
390
  state = state_factory.state()
391
+ mp_spawn_context = multiprocessing.get_context("spawn")
394
392
 
395
393
  runs: dict[int, Run] = {}
396
394
 
397
- while not app_state_tracker.interrupt:
395
+ while True:
398
396
  sleep_duration: int = 0
399
397
  with connection(
400
398
  address,
@@ -433,9 +431,8 @@ def start_client_internal(
433
431
  node_config=node_config,
434
432
  )
435
433
 
436
- app_state_tracker.register_signal_handler()
437
434
  # pylint: disable=too-many-nested-blocks
438
- while not app_state_tracker.interrupt:
435
+ while True:
439
436
  try:
440
437
  # Receive
441
438
  message = receive()
@@ -513,7 +510,7 @@ def start_client_internal(
513
510
  # Docker container.
514
511
 
515
512
  # Generate SuperNode token
516
- token: int = generate_rand_int_from_bytes(RUN_ID_NUM_BYTES)
513
+ token = int.from_bytes(urandom(RUN_ID_NUM_BYTES), "little")
517
514
 
518
515
  # Mode 1: SuperNode starts ClientApp as subprocess
519
516
  start_subprocess = isolation == ISOLATION_MODE_SUBPROCESS
@@ -549,12 +546,13 @@ def start_client_internal(
549
546
  ]
550
547
  command.append("--insecure")
551
548
 
552
- subprocess.run(
553
- command,
554
- stdout=None,
555
- stderr=None,
556
- check=True,
549
+ proc = mp_spawn_context.Process(
550
+ target=_run_flwr_clientapp,
551
+ args=(command, os.getpid()),
552
+ daemon=True,
557
553
  )
554
+ proc.start()
555
+ proc.join()
558
556
  else:
559
557
  # Wait for output to become available
560
558
  while not clientappio_servicer.has_outputs():
@@ -592,10 +590,7 @@ def start_client_internal(
592
590
  e_code = ErrorCode.LOAD_CLIENT_APP_EXCEPTION
593
591
  exc_entity = "SuperNode"
594
592
 
595
- if not app_state_tracker.interrupt:
596
- log(
597
- ERROR, "%s raised an exception", exc_entity, exc_info=ex
598
- )
593
+ log(ERROR, "%s raised an exception", exc_entity, exc_info=ex)
599
594
 
600
595
  # Create error message
601
596
  reply_message = message.create_error_reply(
@@ -621,19 +616,14 @@ def start_client_internal(
621
616
  run_id,
622
617
  )
623
618
  log(INFO, "")
624
-
625
- except StopIteration:
626
- sleep_duration = 0
627
- break
628
619
  # pylint: enable=too-many-nested-blocks
629
620
 
630
621
  # Unregister node
631
- if delete_node is not None and app_state_tracker.is_connected:
622
+ if delete_node is not None:
632
623
  delete_node() # pylint: disable=not-callable
633
624
 
634
625
  if sleep_duration == 0:
635
626
  log(INFO, "Disconnect and shut down")
636
- del app_state_tracker
637
627
  break
638
628
 
639
629
  # Sleep and reconnect afterwards
@@ -773,7 +763,10 @@ def _init_connection(transport: Optional[str], server_address: str) -> tuple[
773
763
  # Parse IP address
774
764
  parsed_address = parse_address(server_address)
775
765
  if not parsed_address:
776
- 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
+ )
777
770
  host, port, is_v6 = parsed_address
778
771
  address = f"[{host}]:{port}" if is_v6 else f"{host}:{port}"
779
772
 
@@ -788,12 +781,9 @@ def _init_connection(transport: Optional[str], server_address: str) -> tuple[
788
781
 
789
782
  from .rest_client.connection import http_request_response
790
783
  except ModuleNotFoundError:
791
- sys.exit(MISSING_EXTRA_REST)
784
+ flwr_exit(ExitCode.COMMON_MISSING_EXTRA_REST)
792
785
  if server_address[:4] != "http":
793
- sys.exit(
794
- "When using the REST API, please provide `https://` or "
795
- "`http://` before the server address (e.g. `http://127.0.0.1:8080`)"
796
- )
786
+ flwr_exit(ExitCode.SUPERNODE_REST_ADDRESS_INVALID)
797
787
  connection, error_type = http_request_response, RequestsConnectionError
798
788
  elif transport == TRANSPORT_TYPE_GRPC_RERE:
799
789
  connection, error_type = grpc_request_response, RpcError
@@ -809,21 +799,19 @@ def _init_connection(transport: Optional[str], server_address: str) -> tuple[
809
799
  return connection, address, error_type
810
800
 
811
801
 
812
- @dataclass
813
- class _AppStateTracker:
814
- interrupt: bool = False
815
- is_connected: bool = False
816
-
817
- def register_signal_handler(self) -> None:
818
- """Register handlers for exit signals."""
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)
819
809
 
820
- def signal_handler(sig, frame): # type: ignore
821
- # pylint: disable=unused-argument
822
- self.interrupt = True
823
- raise StopIteration from None
810
+ threading.Thread(target=main_process_monitor, daemon=True).start()
824
811
 
825
- signal.signal(signal.SIGINT, signal_handler)
826
- signal.signal(signal.SIGTERM, signal_handler)
812
+ # Run the command
813
+ sys.argv = args
814
+ flwr_clientapp()
827
815
 
828
816
 
829
817
  def run_clientappio_api_grpc(