flwr-nightly 1.14.0.dev20241214__py3-none-any.whl → 1.15.0.dev20250107__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 (46) hide show
  1. flwr/cli/log.py +9 -7
  2. flwr/cli/login/login.py +1 -3
  3. flwr/cli/ls.py +25 -22
  4. flwr/cli/new/templates/app/.gitignore.tpl +3 -0
  5. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
  6. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
  7. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  8. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  9. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  10. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  11. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
  12. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  13. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
  14. flwr/cli/run/run.py +11 -18
  15. flwr/cli/stop.py +71 -32
  16. flwr/cli/utils.py +81 -25
  17. flwr/client/app.py +11 -1
  18. flwr/client/client.py +0 -32
  19. flwr/client/clientapp/app.py +3 -1
  20. flwr/client/grpc_rere_client/connection.py +10 -4
  21. flwr/client/message_handler/message_handler.py +0 -2
  22. flwr/client/numpy_client.py +0 -44
  23. flwr/client/supernode/app.py +1 -2
  24. flwr/common/logger.py +16 -1
  25. flwr/common/record/recordset.py +1 -1
  26. flwr/common/retry_invoker.py +3 -1
  27. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +45 -0
  28. flwr/common/telemetry.py +13 -3
  29. flwr/server/app.py +8 -8
  30. flwr/server/run_serverapp.py +8 -9
  31. flwr/server/serverapp/app.py +17 -2
  32. flwr/server/superlink/driver/serverappio_servicer.py +9 -0
  33. flwr/server/superlink/fleet/message_handler/message_handler.py +1 -3
  34. flwr/server/superlink/fleet/vce/vce_api.py +2 -2
  35. flwr/server/superlink/linkstate/in_memory_linkstate.py +10 -2
  36. flwr/server/superlink/linkstate/linkstate.py +4 -0
  37. flwr/server/superlink/linkstate/sqlite_linkstate.py +6 -2
  38. flwr/server/superlink/simulation/simulationio_servicer.py +13 -0
  39. flwr/simulation/app.py +15 -4
  40. flwr/simulation/run_simulation.py +35 -22
  41. flwr/simulation/simulationio_connection.py +3 -0
  42. {flwr_nightly-1.14.0.dev20241214.dist-info → flwr_nightly-1.15.0.dev20250107.dist-info}/METADATA +2 -2
  43. {flwr_nightly-1.14.0.dev20241214.dist-info → flwr_nightly-1.15.0.dev20250107.dist-info}/RECORD +46 -46
  44. {flwr_nightly-1.14.0.dev20241214.dist-info → flwr_nightly-1.15.0.dev20250107.dist-info}/LICENSE +0 -0
  45. {flwr_nightly-1.14.0.dev20241214.dist-info → flwr_nightly-1.15.0.dev20250107.dist-info}/WHEEL +0 -0
  46. {flwr_nightly-1.14.0.dev20241214.dist-info → flwr_nightly-1.15.0.dev20250107.dist-info}/entry_points.txt +0 -0
flwr/cli/log.py CHANGED
@@ -34,7 +34,7 @@ from flwr.common.logger import log as logger
34
34
  from flwr.proto.exec_pb2 import StreamLogsRequest # pylint: disable=E0611
35
35
  from flwr.proto.exec_pb2_grpc import ExecStub
36
36
 
37
- from .utils import init_channel, try_obtain_cli_auth_plugin
37
+ from .utils import init_channel, try_obtain_cli_auth_plugin, unauthenticated_exc_handler
38
38
 
39
39
 
40
40
  def start_stream(
@@ -88,8 +88,9 @@ def stream_logs(
88
88
  latest_timestamp = 0.0
89
89
  res = None
90
90
  try:
91
- for res in stub.StreamLogs(req, timeout=duration):
92
- print(res.log_output, end="")
91
+ with unauthenticated_exc_handler():
92
+ for res in stub.StreamLogs(req, timeout=duration):
93
+ print(res.log_output, end="")
93
94
  except grpc.RpcError as e:
94
95
  # pylint: disable=E1101
95
96
  if e.code() != grpc.StatusCode.DEADLINE_EXCEEDED:
@@ -109,9 +110,10 @@ def print_logs(run_id: int, channel: grpc.Channel, timeout: int) -> None:
109
110
  try:
110
111
  while True:
111
112
  try:
112
- # Enforce timeout for graceful exit
113
- for res in stub.StreamLogs(req, timeout=timeout):
114
- print(res.log_output)
113
+ with unauthenticated_exc_handler():
114
+ # Enforce timeout for graceful exit
115
+ for res in stub.StreamLogs(req, timeout=timeout):
116
+ print(res.log_output)
115
117
  except grpc.RpcError as e:
116
118
  # pylint: disable=E1101
117
119
  if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
@@ -170,7 +172,7 @@ def _log_with_exec_api(
170
172
  run_id: int,
171
173
  stream: bool,
172
174
  ) -> None:
173
- auth_plugin = try_obtain_cli_auth_plugin(app, federation, federation_config)
175
+ auth_plugin = try_obtain_cli_auth_plugin(app, federation)
174
176
  channel = init_channel(app, federation_config, auth_plugin)
175
177
 
176
178
  if stream:
flwr/cli/login/login.py CHANGED
@@ -65,9 +65,7 @@ def login( # pylint: disable=R0914
65
65
 
66
66
  # Get the auth plugin
67
67
  auth_type = login_response.login_details.get(AUTH_TYPE)
68
- auth_plugin = try_obtain_cli_auth_plugin(
69
- app, federation, federation_config, auth_type
70
- )
68
+ auth_plugin = try_obtain_cli_auth_plugin(app, federation, auth_type)
71
69
  if auth_plugin is None:
72
70
  typer.secho(
73
71
  f'❌ Authentication type "{auth_type}" not found',
flwr/cli/ls.py CHANGED
@@ -19,13 +19,12 @@ import io
19
19
  import json
20
20
  from datetime import datetime, timedelta
21
21
  from pathlib import Path
22
- from typing import Annotated, Optional, Union
22
+ from typing import Annotated, Optional
23
23
 
24
24
  import typer
25
25
  from rich.console import Console
26
26
  from rich.table import Table
27
27
  from rich.text import Text
28
- from typer import Exit
29
28
 
30
29
  from flwr.cli.config_utils import (
31
30
  exit_if_no_address,
@@ -35,7 +34,7 @@ from flwr.cli.config_utils import (
35
34
  )
36
35
  from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat, SubStatus
37
36
  from flwr.common.date import format_timedelta, isoformat8601_utc
38
- from flwr.common.logger import redirect_output, remove_emojis, restore_output
37
+ from flwr.common.logger import print_json_error, redirect_output, restore_output
39
38
  from flwr.common.serde import run_from_proto
40
39
  from flwr.common.typing import Run
41
40
  from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
@@ -44,7 +43,7 @@ from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
44
43
  )
45
44
  from flwr.proto.exec_pb2_grpc import ExecStub
46
45
 
47
- from .utils import init_channel, try_obtain_cli_auth_plugin
46
+ from .utils import init_channel, try_obtain_cli_auth_plugin, unauthenticated_exc_handler
48
47
 
49
48
  _RunListType = tuple[int, str, str, str, str, str, str, str, str]
50
49
 
@@ -81,13 +80,25 @@ def ls( # pylint: disable=too-many-locals, too-many-branches
81
80
  ),
82
81
  ] = CliOutputFormat.DEFAULT,
83
82
  ) -> None:
84
- """List runs."""
83
+ """List the details of one provided run ID or all runs in a Flower federation.
84
+
85
+ The following details are displayed:
86
+
87
+ - **Run ID:** Unique identifier for the run.
88
+ - **FAB:** Name of the FAB associated with the run (``{FAB_ID} (v{FAB_VERSION})``).
89
+ - **Status:** Current status of the run (pending, starting, running, finished).
90
+ - **Elapsed:** Time elapsed since the run started (``HH:MM:SS``).
91
+ - **Created At:** Timestamp when the run was created.
92
+ - **Running At:** Timestamp when the run started running.
93
+ - **Finished At:** Timestamp when the run finished.
94
+
95
+ All timestamps follow ISO 8601, UTC and are formatted as ``YYYY-MM-DD HH:MM:SSZ``.
96
+ """
85
97
  suppress_output = output_format == CliOutputFormat.JSON
86
98
  captured_output = io.StringIO()
87
99
  try:
88
100
  if suppress_output:
89
101
  redirect_output(captured_output)
90
-
91
102
  # Load and validate federation config
92
103
  typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
93
104
 
@@ -104,7 +115,7 @@ def ls( # pylint: disable=too-many-locals, too-many-branches
104
115
  raise ValueError(
105
116
  "The options '--runs' and '--run-id' are mutually exclusive."
106
117
  )
107
- auth_plugin = try_obtain_cli_auth_plugin(app, federation, federation_config)
118
+ auth_plugin = try_obtain_cli_auth_plugin(app, federation)
108
119
  channel = init_channel(app, federation_config, auth_plugin)
109
120
  stub = ExecStub(channel)
110
121
 
@@ -120,6 +131,8 @@ def ls( # pylint: disable=too-many-locals, too-many-branches
120
131
  _list_runs(stub, output_format)
121
132
 
122
133
  except ValueError as err:
134
+ if suppress_output:
135
+ redirect_output(captured_output)
123
136
  typer.secho(
124
137
  f"❌ {err}",
125
138
  fg=typer.colors.RED,
@@ -132,7 +145,7 @@ def ls( # pylint: disable=too-many-locals, too-many-branches
132
145
  if suppress_output:
133
146
  restore_output()
134
147
  e_message = captured_output.getvalue()
135
- _print_json_error(e_message, err)
148
+ print_json_error(e_message, err)
136
149
  else:
137
150
  typer.secho(
138
151
  f"{err}",
@@ -283,7 +296,8 @@ def _list_runs(
283
296
  output_format: str = CliOutputFormat.DEFAULT,
284
297
  ) -> None:
285
298
  """List all runs."""
286
- res: ListRunsResponse = stub.ListRuns(ListRunsRequest())
299
+ with unauthenticated_exc_handler():
300
+ res: ListRunsResponse = stub.ListRuns(ListRunsRequest())
287
301
  run_dict = {run_id: run_from_proto(proto) for run_id, proto in res.run_dict.items()}
288
302
 
289
303
  formatted_runs = _format_runs(run_dict, res.now)
@@ -299,7 +313,8 @@ def _display_one_run(
299
313
  output_format: str = CliOutputFormat.DEFAULT,
300
314
  ) -> None:
301
315
  """Display information about a specific run."""
302
- res: ListRunsResponse = stub.ListRuns(ListRunsRequest(run_id=run_id))
316
+ with unauthenticated_exc_handler():
317
+ res: ListRunsResponse = stub.ListRuns(ListRunsRequest(run_id=run_id))
303
318
  if not res.run_dict:
304
319
  raise ValueError(f"Run ID {run_id} not found")
305
320
 
@@ -310,15 +325,3 @@ def _display_one_run(
310
325
  Console().print_json(_to_json(formatted_runs))
311
326
  else:
312
327
  Console().print(_to_table(formatted_runs))
313
-
314
-
315
- def _print_json_error(msg: str, e: Union[Exit, Exception]) -> None:
316
- """Print error message as JSON."""
317
- Console().print_json(
318
- json.dumps(
319
- {
320
- "success": False,
321
- "error-message": remove_emojis(str(msg) + "\n" + str(e)),
322
- }
323
- )
324
- )
@@ -3,6 +3,9 @@ __pycache__/
3
3
  *.py[cod]
4
4
  *$py.class
5
5
 
6
+ # Flower directory
7
+ .flwr
8
+
6
9
  # C extensions
7
10
  *.so
8
11
 
@@ -8,7 +8,7 @@ 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.14.0",
12
12
  "flwr-datasets[vision]>=0.3.0",
13
13
  "torch==2.2.1",
14
14
  "torchvision==0.17.1",
@@ -8,7 +8,7 @@ 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.14.0",
12
12
  "flwr-datasets>=0.3.0",
13
13
  "torch==2.3.1",
14
14
  "trl==0.8.1",
@@ -8,7 +8,7 @@ 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.14.0",
12
12
  "flwr-datasets>=0.3.0",
13
13
  "torch==2.2.1",
14
14
  "transformers>=4.30.0,<5.0",
@@ -8,7 +8,7 @@ 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.14.0",
12
12
  "jax==0.4.30",
13
13
  "jaxlib==0.4.30",
14
14
  "scikit-learn==1.3.2",
@@ -8,7 +8,7 @@ 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.14.0",
12
12
  "flwr-datasets[vision]>=0.3.0",
13
13
  "mlx==0.21.1",
14
14
  ]
@@ -8,7 +8,7 @@ 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.14.0",
12
12
  "numpy>=1.21.0",
13
13
  ]
14
14
 
@@ -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.14.0",
12
12
  "flwr-datasets[vision]>=0.3.0",
13
- "torch==2.2.1",
14
- "torchvision==0.17.1",
13
+ "torch==2.5.1",
14
+ "torchvision==0.20.1",
15
15
  ]
16
16
 
17
17
  [tool.hatch.build.targets.wheel]
@@ -8,7 +8,7 @@ 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.14.0",
12
12
  "flwr-datasets[vision]>=0.3.0",
13
13
  "scikit-learn>=1.1.1",
14
14
  ]
@@ -8,7 +8,7 @@ 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.14.0",
12
12
  "flwr-datasets[vision]>=0.3.0",
13
13
  "tensorflow>=2.11.1,<2.18.0",
14
14
  ]
flwr/cli/run/run.py CHANGED
@@ -19,7 +19,7 @@ import io
19
19
  import json
20
20
  import subprocess
21
21
  from pathlib import Path
22
- from typing import Annotated, Any, Optional, Union
22
+ from typing import Annotated, Any, Optional
23
23
 
24
24
  import typer
25
25
  from rich.console import Console
@@ -37,7 +37,7 @@ from flwr.common.config import (
37
37
  user_config_to_configsrecord,
38
38
  )
39
39
  from flwr.common.constant import CliOutputFormat
40
- from flwr.common.logger import redirect_output, remove_emojis, restore_output
40
+ from flwr.common.logger import print_json_error, redirect_output, restore_output
41
41
  from flwr.common.serde import (
42
42
  configs_record_to_proto,
43
43
  fab_to_proto,
@@ -48,7 +48,11 @@ from flwr.proto.exec_pb2 import StartRunRequest # pylint: disable=E0611
48
48
  from flwr.proto.exec_pb2_grpc import ExecStub
49
49
 
50
50
  from ..log import start_stream
51
- from ..utils import init_channel, try_obtain_cli_auth_plugin
51
+ from ..utils import (
52
+ init_channel,
53
+ try_obtain_cli_auth_plugin,
54
+ unauthenticated_exc_handler,
55
+ )
52
56
 
53
57
  CONN_REFRESH_PERIOD = 60 # Connection refresh period for log streaming (seconds)
54
58
 
@@ -122,7 +126,7 @@ def run(
122
126
  if suppress_output:
123
127
  restore_output()
124
128
  e_message = captured_output.getvalue()
125
- _print_json_error(e_message, err)
129
+ print_json_error(e_message, err)
126
130
  else:
127
131
  typer.secho(
128
132
  f"{err}",
@@ -144,7 +148,7 @@ def _run_with_exec_api(
144
148
  stream: bool,
145
149
  output_format: str,
146
150
  ) -> None:
147
- auth_plugin = try_obtain_cli_auth_plugin(app, federation, federation_config)
151
+ auth_plugin = try_obtain_cli_auth_plugin(app, federation)
148
152
  channel = init_channel(app, federation_config, auth_plugin)
149
153
  stub = ExecStub(channel)
150
154
 
@@ -166,7 +170,8 @@ def _run_with_exec_api(
166
170
  override_config=user_config_to_proto(parse_config_args(config_overrides)),
167
171
  federation_options=configs_record_to_proto(c_record),
168
172
  )
169
- res = stub.StartRun(req)
173
+ with unauthenticated_exc_handler():
174
+ res = stub.StartRun(req)
170
175
 
171
176
  if res.HasField("run_id"):
172
177
  typer.secho(f"🎊 Successfully started run {res.run_id}", fg=typer.colors.GREEN)
@@ -239,15 +244,3 @@ def _run_without_exec_api(
239
244
  check=True,
240
245
  text=True,
241
246
  )
242
-
243
-
244
- def _print_json_error(msg: str, e: Union[typer.Exit, Exception]) -> None:
245
- """Print error message as JSON."""
246
- Console().print_json(
247
- json.dumps(
248
- {
249
- "success": False,
250
- "error-message": remove_emojis(str(msg) + "\n" + str(e)),
251
- }
252
- )
253
- )
flwr/cli/stop.py CHANGED
@@ -15,10 +15,13 @@
15
15
  """Flower command line interface `stop` command."""
16
16
 
17
17
 
18
+ import io
19
+ import json
18
20
  from pathlib import Path
19
21
  from typing import Annotated, Optional
20
22
 
21
23
  import typer
24
+ from rich.console import Console
22
25
 
23
26
  from flwr.cli.config_utils import (
24
27
  exit_if_no_address,
@@ -26,14 +29,15 @@ from flwr.cli.config_utils import (
26
29
  process_loaded_project_config,
27
30
  validate_federation_in_project_config,
28
31
  )
29
- from flwr.common.constant import FAB_CONFIG_FILE
32
+ from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
33
+ from flwr.common.logger import print_json_error, redirect_output, restore_output
30
34
  from flwr.proto.exec_pb2 import StopRunRequest, StopRunResponse # pylint: disable=E0611
31
35
  from flwr.proto.exec_pb2_grpc import ExecStub
32
36
 
33
- from .utils import init_channel, try_obtain_cli_auth_plugin
37
+ from .utils import init_channel, try_obtain_cli_auth_plugin, unauthenticated_exc_handler
34
38
 
35
39
 
36
- def stop(
40
+ def stop( # pylint: disable=R0914
37
41
  run_id: Annotated[ # pylint: disable=unused-argument
38
42
  int,
39
43
  typer.Argument(help="The Flower run ID to stop"),
@@ -46,46 +50,81 @@ def stop(
46
50
  Optional[str],
47
51
  typer.Argument(help="Name of the federation"),
48
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,
49
61
  ) -> None:
50
62
  """Stop a run."""
51
- # Load and validate federation config
52
- typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
53
-
54
- pyproject_path = app / FAB_CONFIG_FILE if app else None
55
- config, errors, warnings = load_and_validate(path=pyproject_path)
56
- config = process_loaded_project_config(config, errors, warnings)
57
- federation, federation_config = validate_federation_in_project_config(
58
- federation, config
59
- )
60
- exit_if_no_address(federation_config, "stop")
61
-
63
+ suppress_output = output_format == CliOutputFormat.JSON
64
+ captured_output = io.StringIO()
62
65
  try:
63
- auth_plugin = try_obtain_cli_auth_plugin(app, federation, federation_config)
64
- channel = init_channel(app, federation_config, auth_plugin)
65
- stub = ExecStub(channel) # pylint: disable=unused-variable # noqa: F841
66
+ if suppress_output:
67
+ redirect_output(captured_output)
66
68
 
67
- typer.secho(f"✋ Stopping run ID {run_id}...", fg=typer.colors.GREEN)
68
- _stop_run(stub, run_id=run_id)
69
+ # Load and validate federation config
70
+ typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
69
71
 
70
- except ValueError as err:
71
- typer.secho(
72
- f"❌ {err}",
73
- fg=typer.colors.RED,
74
- bold=True,
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
75
77
  )
76
- raise typer.Exit(code=1) from err
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
+ )
77
108
  finally:
78
- channel.close()
109
+ if suppress_output:
110
+ restore_output()
111
+ captured_output.close()
79
112
 
80
113
 
81
- def _stop_run(
82
- stub: ExecStub, # pylint: disable=unused-argument
83
- run_id: int, # pylint: disable=unused-argument
84
- ) -> None:
114
+ def _stop_run(stub: ExecStub, run_id: int, output_format: str) -> None:
85
115
  """Stop a run."""
86
- response: StopRunResponse = stub.StopRun(request=StopRunRequest(run_id=run_id))
87
-
116
+ with unauthenticated_exc_handler():
117
+ response: StopRunResponse = stub.StopRun(request=StopRunRequest(run_id=run_id))
88
118
  if response.success:
89
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)
90
129
  else:
91
130
  typer.secho(f"❌ Run {run_id} couldn't be stopped.", fg=typer.colors.RED)
flwr/cli/utils.py CHANGED
@@ -18,15 +18,16 @@
18
18
  import hashlib
19
19
  import json
20
20
  import re
21
+ from collections.abc import Iterator
22
+ from contextlib import contextmanager
21
23
  from logging import DEBUG
22
24
  from pathlib import Path
23
- from typing import Any, Callable, Optional, cast
25
+ from typing import Any, Callable, Optional, Union, cast
24
26
 
25
27
  import grpc
26
28
  import typer
27
29
 
28
30
  from flwr.cli.cli_user_auth_interceptor import CliUserAuthInterceptor
29
- from flwr.common.address import parse_address
30
31
  from flwr.common.auth_plugin import CliAuthPlugin
31
32
  from flwr.common.constant import AUTH_TYPE, CREDENTIALS_DIR, FLWR_DIR
32
33
  from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel
@@ -147,45 +148,79 @@ def sanitize_project_name(name: str) -> str:
147
148
  return sanitized_name
148
149
 
149
150
 
150
- def get_sha256_hash(file_path: Path) -> str:
151
+ def get_sha256_hash(file_path_or_int: Union[Path, int]) -> str:
151
152
  """Calculate the SHA-256 hash of a file."""
152
153
  sha256 = hashlib.sha256()
153
- with open(file_path, "rb") as f:
154
- while True:
155
- data = f.read(65536) # Read in 64kB blocks
156
- if not data:
157
- break
158
- 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())
159
163
  return sha256.hexdigest()
160
164
 
161
165
 
162
- def get_user_auth_config_path(
163
- root_dir: Path, federation: str, server_address: str
164
- ) -> Path:
165
- """Return the path to the user auth config file."""
166
- # Parse the server address
167
- parsed_addr = parse_address(server_address)
168
- if parsed_addr is None:
169
- raise ValueError(f"Invalid server address: {server_address}")
170
- host, port, is_v6 = parsed_addr
171
- formatted_addr = f"[{host}]_{port}" if is_v6 else f"{host}_{port}"
166
+ def get_user_auth_config_path(root_dir: Path, federation: str) -> Path:
167
+ """Return the path to the user auth config file.
172
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
+ """
173
174
  # Locate the credentials directory
174
- credentials_dir = root_dir.absolute() / FLWR_DIR / CREDENTIALS_DIR
175
+ abs_flwr_dir = root_dir.absolute() / FLWR_DIR
176
+ credentials_dir = abs_flwr_dir / CREDENTIALS_DIR
175
177
  credentials_dir.mkdir(parents=True, exist_ok=True)
176
- return credentials_dir / f"{federation}_{formatted_addr}.json"
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"
177
215
 
178
216
 
179
217
  def try_obtain_cli_auth_plugin(
180
218
  root_dir: Path,
181
219
  federation: str,
182
- federation_config: dict[str, Any],
183
220
  auth_type: Optional[str] = None,
184
221
  ) -> Optional[CliAuthPlugin]:
185
222
  """Load the CLI-side user auth plugin for the given auth type."""
186
- config_path = get_user_auth_config_path(
187
- root_dir, federation, federation_config["address"]
188
- )
223
+ config_path = get_user_auth_config_path(root_dir, federation)
189
224
 
190
225
  # Load the config file if it exists
191
226
  config: dict[str, Any] = {}
@@ -244,3 +279,24 @@ def init_channel(
244
279
  )
245
280
  channel.subscribe(on_channel_state_change)
246
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
@@ -56,7 +56,7 @@ from flwr.common.constant import (
56
56
  from flwr.common.logger import log, warn_deprecated_feature
57
57
  from flwr.common.message import Error
58
58
  from flwr.common.retry_invoker import RetryInvoker, RetryState, exponential
59
- from flwr.common.typing import Fab, Run, UserConfig
59
+ from flwr.common.typing import Fab, Run, RunNotRunningException, UserConfig
60
60
  from flwr.proto.clientappio_pb2_grpc import add_ClientAppIoServicer_to_server
61
61
  from flwr.server.superlink.fleet.grpc_bidi.grpc_server import generic_create_grpc_server
62
62
  from flwr.server.superlink.linkstate.utils import generate_rand_int_from_bytes
@@ -612,6 +612,16 @@ def start_client_internal(
612
612
  send(reply_message)
613
613
  log(INFO, "Sent reply")
614
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
+
615
625
  except StopIteration:
616
626
  sleep_duration = 0
617
627
  break