flwr 1.13.0__py3-none-any.whl → 1.14.0__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 (120) hide show
  1. flwr/cli/app.py +5 -0
  2. flwr/cli/build.py +1 -37
  3. flwr/cli/cli_user_auth_interceptor.py +86 -0
  4. flwr/cli/config_utils.py +19 -2
  5. flwr/cli/example.py +1 -0
  6. flwr/cli/install.py +2 -19
  7. flwr/cli/log.py +18 -36
  8. flwr/cli/login/__init__.py +22 -0
  9. flwr/cli/login/login.py +81 -0
  10. flwr/cli/ls.py +205 -106
  11. flwr/cli/new/__init__.py +1 -0
  12. flwr/cli/new/new.py +25 -14
  13. flwr/cli/new/templates/app/.gitignore.tpl +3 -0
  14. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
  15. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +3 -3
  16. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  17. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  18. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +2 -3
  19. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  20. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
  21. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  22. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
  23. flwr/cli/run/__init__.py +1 -0
  24. flwr/cli/run/run.py +89 -39
  25. flwr/cli/stop.py +130 -0
  26. flwr/cli/utils.py +172 -8
  27. flwr/client/app.py +14 -3
  28. flwr/client/client.py +1 -32
  29. flwr/client/clientapp/app.py +4 -8
  30. flwr/client/clientapp/utils.py +1 -0
  31. flwr/client/grpc_adapter_client/connection.py +1 -1
  32. flwr/client/grpc_client/connection.py +1 -1
  33. flwr/client/grpc_rere_client/connection.py +13 -7
  34. flwr/client/message_handler/message_handler.py +1 -2
  35. flwr/client/mod/comms_mods.py +1 -0
  36. flwr/client/mod/localdp_mod.py +1 -1
  37. flwr/client/nodestate/__init__.py +1 -0
  38. flwr/client/nodestate/nodestate.py +1 -0
  39. flwr/client/nodestate/nodestate_factory.py +1 -0
  40. flwr/client/numpy_client.py +0 -44
  41. flwr/client/rest_client/connection.py +3 -3
  42. flwr/client/supernode/app.py +2 -2
  43. flwr/common/address.py +1 -0
  44. flwr/common/args.py +1 -0
  45. flwr/common/auth_plugin/__init__.py +24 -0
  46. flwr/common/auth_plugin/auth_plugin.py +111 -0
  47. flwr/common/config.py +3 -1
  48. flwr/common/constant.py +17 -1
  49. flwr/common/logger.py +40 -0
  50. flwr/common/message.py +1 -0
  51. flwr/common/object_ref.py +57 -54
  52. flwr/common/pyproject.py +1 -0
  53. flwr/common/record/__init__.py +1 -0
  54. flwr/common/record/parametersrecord.py +1 -0
  55. flwr/common/retry_invoker.py +77 -0
  56. flwr/common/secure_aggregation/secaggplus_utils.py +2 -2
  57. flwr/common/telemetry.py +15 -4
  58. flwr/common/typing.py +12 -0
  59. flwr/common/version.py +1 -0
  60. flwr/proto/exec_pb2.py +38 -14
  61. flwr/proto/exec_pb2.pyi +107 -2
  62. flwr/proto/exec_pb2_grpc.py +102 -0
  63. flwr/proto/exec_pb2_grpc.pyi +39 -0
  64. flwr/proto/fab_pb2.py +4 -4
  65. flwr/proto/fab_pb2.pyi +4 -1
  66. flwr/proto/serverappio_pb2.py +18 -18
  67. flwr/proto/serverappio_pb2.pyi +8 -2
  68. flwr/proto/serverappio_pb2_grpc.py +34 -0
  69. flwr/proto/serverappio_pb2_grpc.pyi +13 -0
  70. flwr/proto/simulationio_pb2.py +2 -2
  71. flwr/proto/simulationio_pb2_grpc.py +34 -0
  72. flwr/proto/simulationio_pb2_grpc.pyi +13 -0
  73. flwr/server/app.py +62 -7
  74. flwr/server/compat/app_utils.py +7 -1
  75. flwr/server/driver/grpc_driver.py +11 -63
  76. flwr/server/driver/inmemory_driver.py +5 -1
  77. flwr/server/run_serverapp.py +8 -9
  78. flwr/server/serverapp/app.py +25 -10
  79. flwr/server/strategy/dpfedavg_fixed.py +1 -0
  80. flwr/server/superlink/driver/serverappio_grpc.py +1 -0
  81. flwr/server/superlink/driver/serverappio_servicer.py +82 -23
  82. flwr/server/superlink/ffs/disk_ffs.py +1 -0
  83. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -0
  84. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -0
  85. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +32 -12
  86. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +12 -11
  87. flwr/server/superlink/fleet/message_handler/message_handler.py +32 -5
  88. flwr/server/superlink/fleet/rest_rere/rest_api.py +4 -1
  89. flwr/server/superlink/fleet/vce/__init__.py +1 -0
  90. flwr/server/superlink/fleet/vce/backend/__init__.py +1 -0
  91. flwr/server/superlink/fleet/vce/backend/raybackend.py +1 -0
  92. flwr/server/superlink/linkstate/in_memory_linkstate.py +21 -30
  93. flwr/server/superlink/linkstate/linkstate.py +17 -2
  94. flwr/server/superlink/linkstate/sqlite_linkstate.py +30 -49
  95. flwr/server/superlink/simulation/simulationio_servicer.py +33 -0
  96. flwr/server/superlink/utils.py +65 -0
  97. flwr/simulation/app.py +59 -52
  98. flwr/simulation/ray_transport/ray_actor.py +1 -0
  99. flwr/simulation/ray_transport/utils.py +1 -0
  100. flwr/simulation/run_simulation.py +36 -22
  101. flwr/simulation/simulationio_connection.py +3 -0
  102. flwr/superexec/app.py +1 -0
  103. flwr/superexec/deployment.py +1 -0
  104. flwr/superexec/exec_grpc.py +19 -1
  105. flwr/superexec/exec_servicer.py +76 -2
  106. flwr/superexec/exec_user_auth_interceptor.py +101 -0
  107. flwr/superexec/executor.py +1 -0
  108. {flwr-1.13.0.dist-info → flwr-1.14.0.dist-info}/METADATA +8 -8
  109. {flwr-1.13.0.dist-info → flwr-1.14.0.dist-info}/RECORD +112 -112
  110. flwr/proto/common_pb2.py +0 -36
  111. flwr/proto/common_pb2.pyi +0 -121
  112. flwr/proto/common_pb2_grpc.py +0 -4
  113. flwr/proto/common_pb2_grpc.pyi +0 -4
  114. flwr/proto/control_pb2.py +0 -27
  115. flwr/proto/control_pb2.pyi +0 -7
  116. flwr/proto/control_pb2_grpc.py +0 -135
  117. flwr/proto/control_pb2_grpc.pyi +0 -53
  118. {flwr-1.13.0.dist-info → flwr-1.14.0.dist-info}/LICENSE +0 -0
  119. {flwr-1.13.0.dist-info → flwr-1.14.0.dist-info}/WHEEL +0 -0
  120. {flwr-1.13.0.dist-info → flwr-1.14.0.dist-info}/entry_points.txt +0 -0
flwr/cli/run/run.py CHANGED
@@ -14,28 +14,30 @@
14
14
  # ==============================================================================
15
15
  """Flower command line interface `run` command."""
16
16
 
17
+
18
+ import io
17
19
  import json
18
20
  import subprocess
19
- from logging import DEBUG
20
21
  from pathlib import Path
21
22
  from typing import Annotated, Any, Optional
22
23
 
23
24
  import typer
25
+ from rich.console import Console
24
26
 
25
27
  from flwr.cli.build import build
26
28
  from flwr.cli.config_utils import (
29
+ get_fab_metadata,
27
30
  load_and_validate,
28
- validate_certificate_in_federation_config,
31
+ process_loaded_project_config,
29
32
  validate_federation_in_project_config,
30
- validate_project_config,
31
33
  )
32
34
  from flwr.common.config import (
33
35
  flatten_dict,
34
36
  parse_config_args,
35
37
  user_config_to_configsrecord,
36
38
  )
37
- from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel
38
- from flwr.common.logger import log
39
+ from flwr.common.constant import CliOutputFormat
40
+ from flwr.common.logger import print_json_error, redirect_output, restore_output
39
41
  from flwr.common.serde import (
40
42
  configs_record_to_proto,
41
43
  fab_to_proto,
@@ -46,15 +48,15 @@ from flwr.proto.exec_pb2 import StartRunRequest # pylint: disable=E0611
46
48
  from flwr.proto.exec_pb2_grpc import ExecStub
47
49
 
48
50
  from ..log import start_stream
51
+ from ..utils import (
52
+ init_channel,
53
+ try_obtain_cli_auth_plugin,
54
+ unauthenticated_exc_handler,
55
+ )
49
56
 
50
57
  CONN_REFRESH_PERIOD = 60 # Connection refresh period for log streaming (seconds)
51
58
 
52
59
 
53
- def on_channel_state_change(channel_connectivity: str) -> None:
54
- """Log channel connectivity."""
55
- log(DEBUG, channel_connectivity)
56
-
57
-
58
60
  # pylint: disable-next=too-many-locals
59
61
  def run(
60
62
  app: Annotated[
@@ -85,46 +87,74 @@ def run(
85
87
  "logs are not streamed by default.",
86
88
  ),
87
89
  ] = False,
90
+ output_format: Annotated[
91
+ str,
92
+ typer.Option(
93
+ "--format",
94
+ case_sensitive=False,
95
+ help="Format output using 'default' view or 'json'",
96
+ ),
97
+ ] = CliOutputFormat.DEFAULT,
88
98
  ) -> None:
89
99
  """Run Flower App."""
90
- typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
91
-
92
- pyproject_path = app / "pyproject.toml" if app else None
93
- config, errors, warnings = load_and_validate(path=pyproject_path)
94
- config = validate_project_config(config, errors, warnings)
95
- federation, federation_config = validate_federation_in_project_config(
96
- federation, config
97
- )
98
-
99
- if "address" in federation_config:
100
- _run_with_exec_api(app, federation_config, config_overrides, stream)
101
- else:
102
- _run_without_exec_api(app, federation_config, config_overrides, federation)
103
-
100
+ suppress_output = output_format == CliOutputFormat.JSON
101
+ captured_output = io.StringIO()
102
+ try:
103
+ if suppress_output:
104
+ redirect_output(captured_output)
105
+ typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
106
+
107
+ pyproject_path = app / "pyproject.toml" if app else None
108
+ config, errors, warnings = load_and_validate(path=pyproject_path)
109
+ config = process_loaded_project_config(config, errors, warnings)
110
+ federation, federation_config = validate_federation_in_project_config(
111
+ federation, config
112
+ )
104
113
 
105
- # pylint: disable-next=too-many-locals
114
+ if "address" in federation_config:
115
+ _run_with_exec_api(
116
+ app,
117
+ federation,
118
+ federation_config,
119
+ config_overrides,
120
+ stream,
121
+ output_format,
122
+ )
123
+ else:
124
+ _run_without_exec_api(app, federation_config, config_overrides, federation)
125
+ except (typer.Exit, Exception) as err: # pylint: disable=broad-except
126
+ if suppress_output:
127
+ restore_output()
128
+ e_message = captured_output.getvalue()
129
+ print_json_error(e_message, err)
130
+ else:
131
+ typer.secho(
132
+ f"{err}",
133
+ fg=typer.colors.RED,
134
+ bold=True,
135
+ )
136
+ finally:
137
+ if suppress_output:
138
+ restore_output()
139
+ captured_output.close()
140
+
141
+
142
+ # pylint: disable-next=R0913, R0914, R0917
106
143
  def _run_with_exec_api(
107
144
  app: Path,
145
+ federation: str,
108
146
  federation_config: dict[str, Any],
109
147
  config_overrides: Optional[list[str]],
110
148
  stream: bool,
149
+ output_format: str,
111
150
  ) -> None:
112
-
113
- insecure, root_certificates_bytes = validate_certificate_in_federation_config(
114
- app, federation_config
115
- )
116
- channel = create_channel(
117
- server_address=federation_config["address"],
118
- insecure=insecure,
119
- root_certificates=root_certificates_bytes,
120
- max_message_length=GRPC_MAX_MESSAGE_LENGTH,
121
- interceptors=None,
122
- )
123
- channel.subscribe(on_channel_state_change)
151
+ auth_plugin = try_obtain_cli_auth_plugin(app, federation)
152
+ channel = init_channel(app, federation_config, auth_plugin)
124
153
  stub = ExecStub(channel)
125
154
 
126
155
  fab_path, fab_hash = build(app)
127
156
  content = Path(fab_path).read_bytes()
157
+ fab_id, fab_version = get_fab_metadata(Path(fab_path))
128
158
 
129
159
  # Delete FAB file once the bytes is computed
130
160
  Path(fab_path).unlink()
@@ -140,9 +170,29 @@ def _run_with_exec_api(
140
170
  override_config=user_config_to_proto(parse_config_args(config_overrides)),
141
171
  federation_options=configs_record_to_proto(c_record),
142
172
  )
143
- res = stub.StartRun(req)
173
+ with unauthenticated_exc_handler():
174
+ res = stub.StartRun(req)
144
175
 
145
- typer.secho(f"🎊 Successfully started run {res.run_id}", fg=typer.colors.GREEN)
176
+ if res.HasField("run_id"):
177
+ typer.secho(f"🎊 Successfully started run {res.run_id}", fg=typer.colors.GREEN)
178
+ else:
179
+ typer.secho("❌ Failed to start run", fg=typer.colors.RED)
180
+ raise typer.Exit(code=1)
181
+
182
+ if output_format == CliOutputFormat.JSON:
183
+ run_output = json.dumps(
184
+ {
185
+ "success": res.HasField("run_id"),
186
+ "run-id": res.run_id if res.HasField("run_id") else None,
187
+ "fab-id": fab_id,
188
+ "fab-name": fab_id.rsplit("/", maxsplit=1)[-1],
189
+ "fab-version": fab_version,
190
+ "fab-hash": fab_hash[:8],
191
+ "fab-filename": fab_path,
192
+ }
193
+ )
194
+ restore_output()
195
+ Console().print_json(run_output)
146
196
 
147
197
  if stream:
148
198
  start_stream(res.run_id, channel, CONN_REFRESH_PERIOD)
flwr/cli/stop.py ADDED
@@ -0,0 +1,130 @@
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 `stop` command."""
16
+
17
+
18
+ import io
19
+ import json
20
+ from pathlib import Path
21
+ from typing import Annotated, Optional
22
+
23
+ import typer
24
+ from rich.console import Console
25
+
26
+ from flwr.cli.config_utils import (
27
+ exit_if_no_address,
28
+ load_and_validate,
29
+ process_loaded_project_config,
30
+ validate_federation_in_project_config,
31
+ )
32
+ from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
33
+ from flwr.common.logger import print_json_error, redirect_output, restore_output
34
+ from flwr.proto.exec_pb2 import StopRunRequest, StopRunResponse # pylint: disable=E0611
35
+ from flwr.proto.exec_pb2_grpc import ExecStub
36
+
37
+ from .utils import init_channel, try_obtain_cli_auth_plugin, unauthenticated_exc_handler
38
+
39
+
40
+ def stop( # pylint: disable=R0914
41
+ run_id: Annotated[ # pylint: disable=unused-argument
42
+ int,
43
+ typer.Argument(help="The Flower run ID to stop"),
44
+ ],
45
+ app: Annotated[
46
+ Path,
47
+ typer.Argument(help="Path of the Flower project"),
48
+ ] = Path("."),
49
+ federation: Annotated[
50
+ Optional[str],
51
+ typer.Argument(help="Name of the federation"),
52
+ ] = None,
53
+ output_format: Annotated[
54
+ str,
55
+ typer.Option(
56
+ "--format",
57
+ case_sensitive=False,
58
+ help="Format output using 'default' view or 'json'",
59
+ ),
60
+ ] = CliOutputFormat.DEFAULT,
61
+ ) -> None:
62
+ """Stop a run."""
63
+ suppress_output = output_format == CliOutputFormat.JSON
64
+ captured_output = io.StringIO()
65
+ try:
66
+ if suppress_output:
67
+ redirect_output(captured_output)
68
+
69
+ # Load and validate federation config
70
+ typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
71
+
72
+ pyproject_path = app / FAB_CONFIG_FILE if app else None
73
+ config, errors, warnings = load_and_validate(path=pyproject_path)
74
+ config = process_loaded_project_config(config, errors, warnings)
75
+ federation, federation_config = validate_federation_in_project_config(
76
+ federation, config
77
+ )
78
+ exit_if_no_address(federation_config, "stop")
79
+
80
+ try:
81
+ auth_plugin = try_obtain_cli_auth_plugin(app, federation)
82
+ channel = init_channel(app, federation_config, auth_plugin)
83
+ stub = ExecStub(channel) # pylint: disable=unused-variable # noqa: F841
84
+
85
+ typer.secho(f"✋ Stopping run ID {run_id}...", fg=typer.colors.GREEN)
86
+ _stop_run(stub=stub, run_id=run_id, output_format=output_format)
87
+
88
+ except ValueError as err:
89
+ typer.secho(
90
+ f"❌ {err}",
91
+ fg=typer.colors.RED,
92
+ bold=True,
93
+ )
94
+ raise typer.Exit(code=1) from err
95
+ finally:
96
+ channel.close()
97
+ except (typer.Exit, Exception) as err: # pylint: disable=broad-except
98
+ if suppress_output:
99
+ restore_output()
100
+ e_message = captured_output.getvalue()
101
+ print_json_error(e_message, err)
102
+ else:
103
+ typer.secho(
104
+ f"{err}",
105
+ fg=typer.colors.RED,
106
+ bold=True,
107
+ )
108
+ finally:
109
+ if suppress_output:
110
+ restore_output()
111
+ captured_output.close()
112
+
113
+
114
+ def _stop_run(stub: ExecStub, run_id: int, output_format: str) -> None:
115
+ """Stop a run."""
116
+ with unauthenticated_exc_handler():
117
+ response: StopRunResponse = stub.StopRun(request=StopRunRequest(run_id=run_id))
118
+ if response.success:
119
+ typer.secho(f"✅ Run {run_id} successfully stopped.", fg=typer.colors.GREEN)
120
+ if output_format == CliOutputFormat.JSON:
121
+ run_output = json.dumps(
122
+ {
123
+ "success": True,
124
+ "run-id": run_id,
125
+ }
126
+ )
127
+ restore_output()
128
+ Console().print_json(run_output)
129
+ else:
130
+ typer.secho(f"❌ Run {run_id} couldn't be stopped.", fg=typer.colors.RED)
flwr/cli/utils.py CHANGED
@@ -14,13 +14,35 @@
14
14
  # ==============================================================================
15
15
  """Flower command line interface utils."""
16
16
 
17
+
17
18
  import hashlib
19
+ import json
18
20
  import re
21
+ from collections.abc import Iterator
22
+ from contextlib import contextmanager
23
+ from logging import DEBUG
19
24
  from pathlib import Path
20
- from typing import Callable, Optional, cast
25
+ from typing import Any, Callable, Optional, Union, cast
21
26
 
27
+ import grpc
22
28
  import typer
23
29
 
30
+ from flwr.cli.cli_user_auth_interceptor import CliUserAuthInterceptor
31
+ 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
+
36
+ from .config_utils import validate_certificate_in_federation_config
37
+
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
+
24
46
 
25
47
  def prompt_text(
26
48
  text: str,
@@ -126,13 +148,155 @@ def sanitize_project_name(name: str) -> str:
126
148
  return sanitized_name
127
149
 
128
150
 
129
- def get_sha256_hash(file_path: Path) -> str:
151
+ def get_sha256_hash(file_path_or_int: Union[Path, int]) -> str:
130
152
  """Calculate the SHA-256 hash of a file."""
131
153
  sha256 = hashlib.sha256()
132
- with open(file_path, "rb") as f:
133
- while True:
134
- data = f.read(65536) # Read in 64kB blocks
135
- if not data:
136
- break
137
- sha256.update(data)
154
+ if isinstance(file_path_or_int, Path):
155
+ with open(file_path_or_int, "rb") as f:
156
+ while True:
157
+ data = f.read(65536) # Read in 64kB blocks
158
+ if not data:
159
+ break
160
+ sha256.update(data)
161
+ elif isinstance(file_path_or_int, int):
162
+ sha256.update(str(file_path_or_int).encode())
138
163
  return sha256.hexdigest()
164
+
165
+
166
+ def get_user_auth_config_path(root_dir: Path, federation: str) -> Path:
167
+ """Return the path to the user auth config file.
168
+
169
+ Additionally, a `.gitignore` file will be created in the Flower directory to
170
+ include the `.credentials` folder to be excluded from git. If the `.gitignore`
171
+ file already exists, a warning will be displayed if the `.credentials` entry is
172
+ not found.
173
+ """
174
+ # Locate the credentials directory
175
+ abs_flwr_dir = root_dir.absolute() / FLWR_DIR
176
+ credentials_dir = abs_flwr_dir / CREDENTIALS_DIR
177
+ credentials_dir.mkdir(parents=True, exist_ok=True)
178
+
179
+ # Determine the absolute path of the Flower directory for .gitignore
180
+ gitignore_path = abs_flwr_dir / ".gitignore"
181
+ credential_entry = CREDENTIALS_DIR
182
+
183
+ try:
184
+ if gitignore_path.exists():
185
+ with open(gitignore_path, encoding="utf-8") as gitignore_file:
186
+ lines = gitignore_file.read().splitlines()
187
+
188
+ # Warn if .credentials is not already in .gitignore
189
+ if credential_entry not in lines:
190
+ typer.secho(
191
+ f"`.gitignore` exists, but `{credential_entry}` entry not found. "
192
+ "Consider adding it to your `.gitignore` to exclude Flower "
193
+ "credentials from git.",
194
+ fg=typer.colors.YELLOW,
195
+ bold=True,
196
+ )
197
+ else:
198
+ typer.secho(
199
+ f"Creating a new `.gitignore` with `{credential_entry}` entry...",
200
+ fg=typer.colors.BLUE,
201
+ )
202
+ # Create a new .gitignore with .credentials
203
+ with open(gitignore_path, "w", encoding="utf-8") as gitignore_file:
204
+ gitignore_file.write(f"{credential_entry}\n")
205
+ except Exception as err:
206
+ typer.secho(
207
+ "❌ An error occurred while handling `.gitignore.` "
208
+ f"Please check the permissions of `{gitignore_path}` and try again.",
209
+ fg=typer.colors.RED,
210
+ bold=True,
211
+ )
212
+ raise typer.Exit(code=1) from err
213
+
214
+ return credentials_dir / f"{federation}.json"
215
+
216
+
217
+ def try_obtain_cli_auth_plugin(
218
+ root_dir: Path,
219
+ federation: str,
220
+ auth_type: Optional[str] = None,
221
+ ) -> Optional[CliAuthPlugin]:
222
+ """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:
232
+ return None
233
+
234
+ # Get the auth type from the config if not provided
235
+ if auth_type is None:
236
+ if AUTH_TYPE not in config:
237
+ return None
238
+ auth_type = config[AUTH_TYPE]
239
+
240
+ # Retrieve auth plugin class and instantiate it
241
+ try:
242
+ all_plugins: dict[str, type[CliAuthPlugin]] = get_cli_auth_plugins()
243
+ auth_plugin_class = all_plugins[auth_type]
244
+ return auth_plugin_class(config_path)
245
+ except KeyError:
246
+ typer.echo(f"❌ Unknown user authentication type: {auth_type}")
247
+ raise typer.Exit(code=1) from None
248
+ except ImportError:
249
+ typer.echo("❌ No authentication plugins are currently supported.")
250
+ raise typer.Exit(code=1) from None
251
+
252
+
253
+ def init_channel(
254
+ app: Path, federation_config: dict[str, Any], auth_plugin: Optional[CliAuthPlugin]
255
+ ) -> grpc.Channel:
256
+ """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
+ insecure, root_certificates_bytes = validate_certificate_in_federation_config(
263
+ app, federation_config
264
+ )
265
+
266
+ # Initialize the CLI-side user auth interceptor
267
+ interceptors: list[grpc.UnaryUnaryClientInterceptor] = []
268
+ if auth_plugin is not None:
269
+ auth_plugin.load_tokens()
270
+ interceptors = CliUserAuthInterceptor(auth_plugin)
271
+
272
+ # Create the gRPC channel
273
+ channel = create_channel(
274
+ server_address=federation_config["address"],
275
+ insecure=insecure,
276
+ root_certificates=root_certificates_bytes,
277
+ max_message_length=GRPC_MAX_MESSAGE_LENGTH,
278
+ interceptors=interceptors or None,
279
+ )
280
+ channel.subscribe(on_channel_state_change)
281
+ return channel
282
+
283
+
284
+ @contextmanager
285
+ def unauthenticated_exc_handler() -> Iterator[None]:
286
+ """Context manager to handle gRPC UNAUTHENTICATED errors.
287
+
288
+ It catches grpc.RpcError exceptions with UNAUTHENTICATED status, informs the user,
289
+ and exits the application. All other exceptions will be allowed to escape.
290
+ """
291
+ try:
292
+ yield
293
+ 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
flwr/client/app.py CHANGED
@@ -14,6 +14,7 @@
14
14
  # ==============================================================================
15
15
  """Flower client app."""
16
16
 
17
+
17
18
  import signal
18
19
  import subprocess
19
20
  import sys
@@ -55,7 +56,7 @@ from flwr.common.constant import (
55
56
  from flwr.common.logger import log, warn_deprecated_feature
56
57
  from flwr.common.message import Error
57
58
  from flwr.common.retry_invoker import RetryInvoker, RetryState, exponential
58
- from flwr.common.typing import Fab, Run, UserConfig
59
+ from flwr.common.typing import Fab, Run, RunNotRunningException, UserConfig
59
60
  from flwr.proto.clientappio_pb2_grpc import add_ClientAppIoServicer_to_server
60
61
  from flwr.server.superlink.fleet.grpc_bidi.grpc_server import generic_create_grpc_server
61
62
  from flwr.server.superlink.linkstate.utils import generate_rand_int_from_bytes
@@ -474,7 +475,7 @@ def start_client_internal(
474
475
 
475
476
  run: Run = runs[run_id]
476
477
  if get_fab is not None and run.fab_hash:
477
- fab = get_fab(run.fab_hash)
478
+ fab = get_fab(run.fab_hash, run_id)
478
479
  if not isolation:
479
480
  # If `ClientApp` runs in the same process, install the FAB
480
481
  install_from_fab(fab.content, flwr_path, True)
@@ -611,6 +612,16 @@ def start_client_internal(
611
612
  send(reply_message)
612
613
  log(INFO, "Sent reply")
613
614
 
615
+ except RunNotRunningException:
616
+ log(INFO, "")
617
+ log(
618
+ INFO,
619
+ "SuperNode aborted sending the reply message. "
620
+ "Run ID %s is not in `RUNNING` status.",
621
+ run_id,
622
+ )
623
+ log(INFO, "")
624
+
614
625
  except StopIteration:
615
626
  sleep_duration = 0
616
627
  break
@@ -752,7 +763,7 @@ def _init_connection(transport: Optional[str], server_address: str) -> tuple[
752
763
  Optional[Callable[[], Optional[int]]],
753
764
  Optional[Callable[[], None]],
754
765
  Optional[Callable[[int], Run]],
755
- Optional[Callable[[str], Fab]],
766
+ Optional[Callable[[str, int], Fab]],
756
767
  ]
757
768
  ],
758
769
  ],
flwr/client/client.py CHANGED
@@ -14,6 +14,7 @@
14
14
  # ==============================================================================
15
15
  """Flower client (abstract base class)."""
16
16
 
17
+
17
18
  # Needed to `Client` class can return a type of `Client` (not needed in py3.11+)
18
19
  from __future__ import annotations
19
20
 
@@ -21,7 +22,6 @@ from abc import ABC
21
22
 
22
23
  from flwr.common import (
23
24
  Code,
24
- Context,
25
25
  EvaluateIns,
26
26
  EvaluateRes,
27
27
  FitIns,
@@ -33,14 +33,11 @@ from flwr.common import (
33
33
  Parameters,
34
34
  Status,
35
35
  )
36
- from flwr.common.logger import warn_deprecated_feature_with_example
37
36
 
38
37
 
39
38
  class Client(ABC):
40
39
  """Abstract base class for Flower clients."""
41
40
 
42
- _context: Context
43
-
44
41
  def get_properties(self, ins: GetPropertiesIns) -> GetPropertiesRes:
45
42
  """Return set of client's properties.
46
43
 
@@ -142,34 +139,6 @@ class Client(ABC):
142
139
  metrics={},
143
140
  )
144
141
 
145
- @property
146
- def context(self) -> Context:
147
- """Getter for `Context` client attribute."""
148
- warn_deprecated_feature_with_example(
149
- "Accessing the context via the client's attribute is deprecated.",
150
- example_message="Instead, pass it to the client's "
151
- "constructor in your `client_fn()` which already "
152
- "receives a context object.",
153
- code_example="def client_fn(context: Context) -> Client:\n\n"
154
- "\t\t# Your existing client_fn\n\n"
155
- "\t\t# Pass `context` to the constructor\n"
156
- "\t\treturn FlowerClient(context).to_client()",
157
- )
158
- return self._context
159
-
160
- @context.setter
161
- def context(self, context: Context) -> None:
162
- """Setter for `Context` client attribute."""
163
- self._context = context
164
-
165
- def get_context(self) -> Context:
166
- """Get the run context from this client."""
167
- return self.context
168
-
169
- def set_context(self, context: Context) -> None:
170
- """Apply a run context to this client."""
171
- self.context = context
172
-
173
142
  def to_client(self) -> Client:
174
143
  """Return client (itself)."""
175
144
  return self
@@ -14,6 +14,7 @@
14
14
  # ==============================================================================
15
15
  """Flower ClientApp process."""
16
16
 
17
+
17
18
  import argparse
18
19
  import sys
19
20
  import time
@@ -31,6 +32,7 @@ from flwr.common.constant import CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS, ErrorCo
31
32
  from flwr.common.grpc import create_channel
32
33
  from flwr.common.logger import log
33
34
  from flwr.common.message import Error
35
+ from flwr.common.retry_invoker import _make_simple_grpc_retry_invoker, _wrap_stub
34
36
  from flwr.common.serde import (
35
37
  context_from_proto,
36
38
  context_to_proto,
@@ -105,9 +107,9 @@ def run_clientapp( # pylint: disable=R0914
105
107
 
106
108
  # Resolve directory where FABs are installed
107
109
  flwr_dir_ = get_flwr_dir(flwr_dir)
108
-
109
110
  try:
110
111
  stub = ClientAppIoStub(channel)
112
+ _wrap_stub(stub, _make_simple_grpc_retry_invoker())
111
113
 
112
114
  while True:
113
115
  # If token is not set, loop until token is received from SuperNode
@@ -138,6 +140,7 @@ def run_clientapp( # pylint: disable=R0914
138
140
 
139
141
  # Execute ClientApp
140
142
  reply_message = client_app(message=message, context=context)
143
+
141
144
  except Exception as ex: # pylint: disable=broad-exception-caught
142
145
  # Don't update/change NodeState
143
146
 
@@ -252,12 +255,5 @@ def _parse_args_run_flwr_clientapp() -> argparse.ArgumentParser:
252
255
  required=False,
253
256
  help="Unique token generated by SuperNode for each ClientApp execution",
254
257
  )
255
- parser.add_argument(
256
- "--root-certificates",
257
- metavar="ROOT_CERT",
258
- type=str,
259
- help="Specifies the path to the PEM-encoded root certificate file for "
260
- "establishing secure HTTPS connections.",
261
- )
262
258
  add_args_flwr_app_common(parser=parser)
263
259
  return parser
@@ -14,6 +14,7 @@
14
14
  # ==============================================================================
15
15
  """Flower ClientApp loading utils."""
16
16
 
17
+
17
18
  from logging import DEBUG
18
19
  from pathlib import Path
19
20
  from typing import Callable, Optional