flwr-nightly 1.14.0.dev20241202__py3-none-any.whl → 1.14.0.dev20241214__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.

Potentially problematic release.


This version of flwr-nightly might be problematic. Click here for more details.

Files changed (108) hide show
  1. flwr/cli/app.py +5 -0
  2. flwr/cli/build.py +1 -0
  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 +1 -0
  7. flwr/cli/log.py +11 -31
  8. flwr/cli/login/__init__.py +22 -0
  9. flwr/cli/login/login.py +83 -0
  10. flwr/cli/ls.py +198 -102
  11. flwr/cli/new/__init__.py +1 -0
  12. flwr/cli/new/new.py +2 -1
  13. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +2 -2
  14. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -2
  15. flwr/cli/run/__init__.py +1 -0
  16. flwr/cli/run/run.py +96 -39
  17. flwr/cli/stop.py +91 -0
  18. flwr/cli/utils.py +109 -1
  19. flwr/client/app.py +3 -2
  20. flwr/client/client.py +1 -0
  21. flwr/client/clientapp/app.py +1 -0
  22. flwr/client/clientapp/utils.py +1 -0
  23. flwr/client/grpc_adapter_client/connection.py +1 -1
  24. flwr/client/grpc_client/connection.py +1 -1
  25. flwr/client/grpc_rere_client/connection.py +3 -3
  26. flwr/client/message_handler/message_handler.py +1 -0
  27. flwr/client/mod/comms_mods.py +1 -0
  28. flwr/client/mod/localdp_mod.py +1 -1
  29. flwr/client/nodestate/__init__.py +1 -0
  30. flwr/client/nodestate/nodestate.py +1 -0
  31. flwr/client/nodestate/nodestate_factory.py +1 -0
  32. flwr/client/rest_client/connection.py +3 -3
  33. flwr/client/supernode/app.py +1 -0
  34. flwr/common/address.py +1 -0
  35. flwr/common/args.py +1 -0
  36. flwr/common/auth_plugin/__init__.py +24 -0
  37. flwr/common/auth_plugin/auth_plugin.py +111 -0
  38. flwr/common/config.py +3 -1
  39. flwr/common/constant.py +17 -1
  40. flwr/common/logger.py +25 -0
  41. flwr/common/message.py +1 -0
  42. flwr/common/object_ref.py +57 -54
  43. flwr/common/pyproject.py +1 -0
  44. flwr/common/record/__init__.py +1 -0
  45. flwr/common/record/parametersrecord.py +1 -0
  46. flwr/common/retry_invoker.py +75 -0
  47. flwr/common/secure_aggregation/secaggplus_utils.py +2 -2
  48. flwr/common/telemetry.py +2 -1
  49. flwr/common/typing.py +12 -0
  50. flwr/common/version.py +1 -0
  51. flwr/proto/exec_pb2.py +38 -14
  52. flwr/proto/exec_pb2.pyi +107 -2
  53. flwr/proto/exec_pb2_grpc.py +102 -0
  54. flwr/proto/exec_pb2_grpc.pyi +39 -0
  55. flwr/proto/fab_pb2.py +4 -4
  56. flwr/proto/fab_pb2.pyi +4 -1
  57. flwr/proto/serverappio_pb2.py +18 -18
  58. flwr/proto/serverappio_pb2.pyi +8 -2
  59. flwr/proto/serverappio_pb2_grpc.py +34 -0
  60. flwr/proto/serverappio_pb2_grpc.pyi +13 -0
  61. flwr/proto/simulationio_pb2.py +2 -2
  62. flwr/proto/simulationio_pb2_grpc.py +34 -0
  63. flwr/proto/simulationio_pb2_grpc.pyi +13 -0
  64. flwr/server/app.py +53 -1
  65. flwr/server/compat/app_utils.py +7 -1
  66. flwr/server/driver/grpc_driver.py +11 -63
  67. flwr/server/driver/inmemory_driver.py +5 -1
  68. flwr/server/serverapp/app.py +9 -2
  69. flwr/server/strategy/dpfedavg_fixed.py +1 -0
  70. flwr/server/superlink/driver/serverappio_grpc.py +1 -0
  71. flwr/server/superlink/driver/serverappio_servicer.py +73 -23
  72. flwr/server/superlink/ffs/disk_ffs.py +1 -0
  73. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -0
  74. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -0
  75. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +32 -12
  76. flwr/server/superlink/fleet/message_handler/message_handler.py +31 -2
  77. flwr/server/superlink/fleet/rest_rere/rest_api.py +4 -1
  78. flwr/server/superlink/fleet/vce/__init__.py +1 -0
  79. flwr/server/superlink/fleet/vce/backend/__init__.py +1 -0
  80. flwr/server/superlink/fleet/vce/backend/raybackend.py +1 -0
  81. flwr/server/superlink/linkstate/in_memory_linkstate.py +14 -30
  82. flwr/server/superlink/linkstate/linkstate.py +13 -2
  83. flwr/server/superlink/linkstate/sqlite_linkstate.py +24 -44
  84. flwr/server/superlink/simulation/simulationio_servicer.py +20 -0
  85. flwr/server/superlink/utils.py +65 -0
  86. flwr/simulation/app.py +1 -0
  87. flwr/simulation/ray_transport/ray_actor.py +1 -0
  88. flwr/simulation/ray_transport/utils.py +1 -0
  89. flwr/simulation/run_simulation.py +1 -0
  90. flwr/superexec/app.py +1 -0
  91. flwr/superexec/deployment.py +1 -0
  92. flwr/superexec/exec_grpc.py +19 -1
  93. flwr/superexec/exec_servicer.py +76 -2
  94. flwr/superexec/exec_user_auth_interceptor.py +101 -0
  95. flwr/superexec/executor.py +1 -0
  96. {flwr_nightly-1.14.0.dev20241202.dist-info → flwr_nightly-1.14.0.dev20241214.dist-info}/METADATA +8 -7
  97. {flwr_nightly-1.14.0.dev20241202.dist-info → flwr_nightly-1.14.0.dev20241214.dist-info}/RECORD +100 -100
  98. flwr/proto/common_pb2.py +0 -36
  99. flwr/proto/common_pb2.pyi +0 -121
  100. flwr/proto/common_pb2_grpc.py +0 -4
  101. flwr/proto/common_pb2_grpc.pyi +0 -4
  102. flwr/proto/control_pb2.py +0 -27
  103. flwr/proto/control_pb2.pyi +0 -7
  104. flwr/proto/control_pb2_grpc.py +0 -135
  105. flwr/proto/control_pb2_grpc.pyi +0 -53
  106. {flwr_nightly-1.14.0.dev20241202.dist-info → flwr_nightly-1.14.0.dev20241214.dist-info}/LICENSE +0 -0
  107. {flwr_nightly-1.14.0.dev20241202.dist-info → flwr_nightly-1.14.0.dev20241214.dist-info}/WHEEL +0 -0
  108. {flwr_nightly-1.14.0.dev20241202.dist-info → flwr_nightly-1.14.0.dev20241214.dist-info}/entry_points.txt +0 -0
flwr/cli/stop.py ADDED
@@ -0,0 +1,91 @@
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
+ from pathlib import Path
19
+ from typing import Annotated, Optional
20
+
21
+ import typer
22
+
23
+ from flwr.cli.config_utils import (
24
+ exit_if_no_address,
25
+ load_and_validate,
26
+ process_loaded_project_config,
27
+ validate_federation_in_project_config,
28
+ )
29
+ from flwr.common.constant import FAB_CONFIG_FILE
30
+ from flwr.proto.exec_pb2 import StopRunRequest, StopRunResponse # pylint: disable=E0611
31
+ from flwr.proto.exec_pb2_grpc import ExecStub
32
+
33
+ from .utils import init_channel, try_obtain_cli_auth_plugin
34
+
35
+
36
+ def stop(
37
+ run_id: Annotated[ # pylint: disable=unused-argument
38
+ int,
39
+ typer.Argument(help="The Flower run ID to stop"),
40
+ ],
41
+ app: Annotated[
42
+ Path,
43
+ typer.Argument(help="Path of the Flower project"),
44
+ ] = Path("."),
45
+ federation: Annotated[
46
+ Optional[str],
47
+ typer.Argument(help="Name of the federation"),
48
+ ] = None,
49
+ ) -> None:
50
+ """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
+
62
+ 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
+
67
+ typer.secho(f"✋ Stopping run ID {run_id}...", fg=typer.colors.GREEN)
68
+ _stop_run(stub, run_id=run_id)
69
+
70
+ except ValueError as err:
71
+ typer.secho(
72
+ f"❌ {err}",
73
+ fg=typer.colors.RED,
74
+ bold=True,
75
+ )
76
+ raise typer.Exit(code=1) from err
77
+ finally:
78
+ channel.close()
79
+
80
+
81
+ def _stop_run(
82
+ stub: ExecStub, # pylint: disable=unused-argument
83
+ run_id: int, # pylint: disable=unused-argument
84
+ ) -> None:
85
+ """Stop a run."""
86
+ response: StopRunResponse = stub.StopRun(request=StopRunRequest(run_id=run_id))
87
+
88
+ if response.success:
89
+ typer.secho(f"✅ Run {run_id} successfully stopped.", fg=typer.colors.GREEN)
90
+ else:
91
+ typer.secho(f"❌ Run {run_id} couldn't be stopped.", fg=typer.colors.RED)
flwr/cli/utils.py CHANGED
@@ -14,13 +14,34 @@
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 logging import DEBUG
19
22
  from pathlib import Path
20
- from typing import Callable, Optional, cast
23
+ from typing import Any, Callable, Optional, cast
21
24
 
25
+ import grpc
22
26
  import typer
23
27
 
28
+ from flwr.cli.cli_user_auth_interceptor import CliUserAuthInterceptor
29
+ from flwr.common.address import parse_address
30
+ from flwr.common.auth_plugin import CliAuthPlugin
31
+ from flwr.common.constant import AUTH_TYPE, CREDENTIALS_DIR, FLWR_DIR
32
+ from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel
33
+ from flwr.common.logger import log
34
+
35
+ from .config_utils import validate_certificate_in_federation_config
36
+
37
+ try:
38
+ from flwr.ee import get_cli_auth_plugins
39
+ except ImportError:
40
+
41
+ def get_cli_auth_plugins() -> dict[str, type[CliAuthPlugin]]:
42
+ """Return all CLI authentication plugins."""
43
+ raise NotImplementedError("No authentication plugins are currently supported.")
44
+
24
45
 
25
46
  def prompt_text(
26
47
  text: str,
@@ -136,3 +157,90 @@ def get_sha256_hash(file_path: Path) -> str:
136
157
  break
137
158
  sha256.update(data)
138
159
  return sha256.hexdigest()
160
+
161
+
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}"
172
+
173
+ # Locate the credentials directory
174
+ credentials_dir = root_dir.absolute() / FLWR_DIR / CREDENTIALS_DIR
175
+ credentials_dir.mkdir(parents=True, exist_ok=True)
176
+ return credentials_dir / f"{federation}_{formatted_addr}.json"
177
+
178
+
179
+ def try_obtain_cli_auth_plugin(
180
+ root_dir: Path,
181
+ federation: str,
182
+ federation_config: dict[str, Any],
183
+ auth_type: Optional[str] = None,
184
+ ) -> Optional[CliAuthPlugin]:
185
+ """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
+ )
189
+
190
+ # Load the config file if it exists
191
+ config: dict[str, Any] = {}
192
+ if config_path.exists():
193
+ with config_path.open("r", encoding="utf-8") as file:
194
+ config = json.load(file)
195
+ # This is the case when the user auth is not enabled
196
+ elif auth_type is None:
197
+ return None
198
+
199
+ # Get the auth type from the config if not provided
200
+ if auth_type is None:
201
+ if AUTH_TYPE not in config:
202
+ return None
203
+ auth_type = config[AUTH_TYPE]
204
+
205
+ # Retrieve auth plugin class and instantiate it
206
+ try:
207
+ all_plugins: dict[str, type[CliAuthPlugin]] = get_cli_auth_plugins()
208
+ auth_plugin_class = all_plugins[auth_type]
209
+ return auth_plugin_class(config_path)
210
+ except KeyError:
211
+ typer.echo(f"❌ Unknown user authentication type: {auth_type}")
212
+ raise typer.Exit(code=1) from None
213
+ except ImportError:
214
+ typer.echo("❌ No authentication plugins are currently supported.")
215
+ raise typer.Exit(code=1) from None
216
+
217
+
218
+ def init_channel(
219
+ app: Path, federation_config: dict[str, Any], auth_plugin: Optional[CliAuthPlugin]
220
+ ) -> grpc.Channel:
221
+ """Initialize gRPC channel to the Exec API."""
222
+
223
+ def on_channel_state_change(channel_connectivity: str) -> None:
224
+ """Log channel connectivity."""
225
+ log(DEBUG, channel_connectivity)
226
+
227
+ insecure, root_certificates_bytes = validate_certificate_in_federation_config(
228
+ app, federation_config
229
+ )
230
+
231
+ # Initialize the CLI-side user auth interceptor
232
+ interceptors: list[grpc.UnaryUnaryClientInterceptor] = []
233
+ if auth_plugin is not None:
234
+ auth_plugin.load_tokens()
235
+ interceptors = CliUserAuthInterceptor(auth_plugin)
236
+
237
+ # Create the gRPC channel
238
+ channel = create_channel(
239
+ server_address=federation_config["address"],
240
+ insecure=insecure,
241
+ root_certificates=root_certificates_bytes,
242
+ max_message_length=GRPC_MAX_MESSAGE_LENGTH,
243
+ interceptors=interceptors or None,
244
+ )
245
+ channel.subscribe(on_channel_state_change)
246
+ return channel
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
@@ -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)
@@ -752,7 +753,7 @@ def _init_connection(transport: Optional[str], server_address: str) -> tuple[
752
753
  Optional[Callable[[], Optional[int]]],
753
754
  Optional[Callable[[], None]],
754
755
  Optional[Callable[[int], Run]],
755
- Optional[Callable[[str], Fab]],
756
+ Optional[Callable[[str, int], Fab]],
756
757
  ]
757
758
  ],
758
759
  ],
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
 
@@ -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
@@ -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
@@ -48,7 +48,7 @@ def grpc_adapter( # pylint: disable=R0913,too-many-positional-arguments
48
48
  Optional[Callable[[], Optional[int]]],
49
49
  Optional[Callable[[], None]],
50
50
  Optional[Callable[[int], Run]],
51
- Optional[Callable[[str], Fab]],
51
+ Optional[Callable[[str, int], Fab]],
52
52
  ]
53
53
  ]:
54
54
  """Primitives for request/response-based interaction with a server via GrpcAdapter.
@@ -76,7 +76,7 @@ def grpc_connection( # pylint: disable=R0913,R0915,too-many-positional-argument
76
76
  Optional[Callable[[], Optional[int]]],
77
77
  Optional[Callable[[], None]],
78
78
  Optional[Callable[[int], Run]],
79
- Optional[Callable[[str], Fab]],
79
+ Optional[Callable[[str, int], Fab]],
80
80
  ]
81
81
  ]:
82
82
  """Establish a gRPC connection to a gRPC server.
@@ -84,7 +84,7 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
84
84
  Optional[Callable[[], Optional[int]]],
85
85
  Optional[Callable[[], None]],
86
86
  Optional[Callable[[int], Run]],
87
- Optional[Callable[[str], Fab]],
87
+ Optional[Callable[[str, int], Fab]],
88
88
  ]
89
89
  ]:
90
90
  """Primitives for request/response-based interaction with a server.
@@ -290,9 +290,9 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
290
290
  # Return fab_id and fab_version
291
291
  return run_from_proto(get_run_response.run)
292
292
 
293
- def get_fab(fab_hash: str) -> Fab:
293
+ def get_fab(fab_hash: str, run_id: int) -> Fab:
294
294
  # Call FleetAPI
295
- get_fab_request = GetFabRequest(node=node, hash_str=fab_hash)
295
+ get_fab_request = GetFabRequest(node=node, hash_str=fab_hash, run_id=run_id)
296
296
  get_fab_response: GetFabResponse = retry_invoker.invoke(
297
297
  stub.GetFab,
298
298
  request=get_fab_request,
@@ -14,6 +14,7 @@
14
14
  # ==============================================================================
15
15
  """Client-side message handler."""
16
16
 
17
+
17
18
  from logging import WARN
18
19
  from typing import Optional, cast
19
20
 
@@ -14,6 +14,7 @@
14
14
  # ==============================================================================
15
15
  """Mods that report statistics about message communication."""
16
16
 
17
+
17
18
  from logging import INFO
18
19
 
19
20
  import numpy as np
@@ -136,7 +136,7 @@ class LocalDpMod:
136
136
  fit_res.parameters = ndarrays_to_parameters(client_to_server_params)
137
137
 
138
138
  # Add noise to model params
139
- add_localdp_gaussian_noise_to_params(
139
+ fit_res.parameters = add_localdp_gaussian_noise_to_params(
140
140
  fit_res.parameters, self.sensitivity, self.epsilon, self.delta
141
141
  )
142
142
 
@@ -14,6 +14,7 @@
14
14
  # ==============================================================================
15
15
  """Flower NodeState."""
16
16
 
17
+
17
18
  from .in_memory_nodestate import InMemoryNodeState as InMemoryNodeState
18
19
  from .nodestate import NodeState as NodeState
19
20
  from .nodestate_factory import NodeStateFactory as NodeStateFactory
@@ -14,6 +14,7 @@
14
14
  # ==============================================================================
15
15
  """Abstract base class NodeState."""
16
16
 
17
+
17
18
  import abc
18
19
  from typing import Optional
19
20
 
@@ -14,6 +14,7 @@
14
14
  # ==============================================================================
15
15
  """Factory class that creates NodeState instances."""
16
16
 
17
+
17
18
  import threading
18
19
  from typing import Optional
19
20
 
@@ -96,7 +96,7 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
96
96
  Optional[Callable[[], Optional[int]]],
97
97
  Optional[Callable[[], None]],
98
98
  Optional[Callable[[int], Run]],
99
- Optional[Callable[[str], Fab]],
99
+ Optional[Callable[[str, int], Fab]],
100
100
  ]
101
101
  ]:
102
102
  """Primitives for request/response-based interaction with a server.
@@ -361,9 +361,9 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
361
361
 
362
362
  return run_from_proto(res.run)
363
363
 
364
- def get_fab(fab_hash: str) -> Fab:
364
+ def get_fab(fab_hash: str, run_id: int) -> Fab:
365
365
  # Construct the request
366
- req = GetFabRequest(node=node, hash_str=fab_hash)
366
+ req = GetFabRequest(node=node, hash_str=fab_hash, run_id=run_id)
367
367
 
368
368
  # Send the request
369
369
  res = _request(req, GetFabResponse, PATH_GET_FAB)
@@ -14,6 +14,7 @@
14
14
  # ==============================================================================
15
15
  """Flower SuperNode."""
16
16
 
17
+
17
18
  import argparse
18
19
  import sys
19
20
  from logging import DEBUG, ERROR, INFO, WARN
flwr/common/address.py CHANGED
@@ -14,6 +14,7 @@
14
14
  # ==============================================================================
15
15
  """Flower IP address utils."""
16
16
 
17
+
17
18
  import socket
18
19
  from ipaddress import ip_address
19
20
  from typing import Optional
flwr/common/args.py CHANGED
@@ -14,6 +14,7 @@
14
14
  # ==============================================================================
15
15
  """Common Flower arguments."""
16
16
 
17
+
17
18
  import argparse
18
19
  import sys
19
20
  from logging import DEBUG, ERROR, WARN
@@ -0,0 +1,24 @@
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
+ """Auth plugin components."""
16
+
17
+
18
+ from .auth_plugin import CliAuthPlugin as CliAuthPlugin
19
+ from .auth_plugin import ExecAuthPlugin as ExecAuthPlugin
20
+
21
+ __all__ = [
22
+ "CliAuthPlugin",
23
+ "ExecAuthPlugin",
24
+ ]
@@ -0,0 +1,111 @@
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
+ """Abstract classes for Flower User Auth Plugin."""
16
+
17
+
18
+ from abc import ABC, abstractmethod
19
+ from collections.abc import Sequence
20
+ from pathlib import Path
21
+ from typing import Any, Optional, Union
22
+
23
+ from flwr.proto.exec_pb2_grpc import ExecStub
24
+
25
+
26
+ class ExecAuthPlugin(ABC):
27
+ """Abstract Flower Auth Plugin class for ExecServicer.
28
+
29
+ Parameters
30
+ ----------
31
+ config : dict[str, Any]
32
+ The authentication configuration loaded from a YAML file.
33
+ """
34
+
35
+ @abstractmethod
36
+ def __init__(self, config: dict[str, Any]):
37
+ """Abstract constructor."""
38
+
39
+ @abstractmethod
40
+ def get_login_details(self) -> dict[str, str]:
41
+ """Get the login details."""
42
+
43
+ @abstractmethod
44
+ def validate_tokens_in_metadata(
45
+ self, metadata: Sequence[tuple[str, Union[str, bytes]]]
46
+ ) -> bool:
47
+ """Validate authentication tokens in the provided metadata."""
48
+
49
+ @abstractmethod
50
+ def get_auth_tokens(self, auth_details: dict[str, str]) -> dict[str, str]:
51
+ """Get authentication tokens."""
52
+
53
+ @abstractmethod
54
+ def refresh_tokens(
55
+ self, metadata: Sequence[tuple[str, Union[str, bytes]]]
56
+ ) -> Optional[Sequence[tuple[str, Union[str, bytes]]]]:
57
+ """Refresh authentication tokens in the provided metadata."""
58
+
59
+
60
+ class CliAuthPlugin(ABC):
61
+ """Abstract Flower Auth Plugin class for CLI.
62
+
63
+ Parameters
64
+ ----------
65
+ user_auth_config_path : Path
66
+ The path to the user's authentication configuration file.
67
+ """
68
+
69
+ @staticmethod
70
+ @abstractmethod
71
+ def login(
72
+ login_details: dict[str, str],
73
+ exec_stub: ExecStub,
74
+ ) -> dict[str, Any]:
75
+ """Authenticate the user with the SuperLink.
76
+
77
+ Parameters
78
+ ----------
79
+ login_details : dict[str, str]
80
+ A dictionary containing the user's login details.
81
+ exec_stub : ExecStub
82
+ An instance of `ExecStub` used for communication with the SuperLink.
83
+
84
+ Returns
85
+ -------
86
+ user_auth_config : dict[str, Any]
87
+ A dictionary containing the user's authentication configuration
88
+ in JSON format.
89
+ """
90
+
91
+ @abstractmethod
92
+ def __init__(self, user_auth_config_path: Path):
93
+ """Abstract constructor."""
94
+
95
+ @abstractmethod
96
+ def store_tokens(self, user_auth_config: dict[str, Any]) -> None:
97
+ """Store authentication tokens from the provided user_auth_config.
98
+
99
+ The configuration, including tokens, will be saved as a JSON file
100
+ at `user_auth_config_path`.
101
+ """
102
+
103
+ @abstractmethod
104
+ def load_tokens(self) -> None:
105
+ """Load authentication tokens from the user_auth_config_path."""
106
+
107
+ @abstractmethod
108
+ def write_tokens_to_metadata(
109
+ self, metadata: Sequence[tuple[str, Union[str, bytes]]]
110
+ ) -> Sequence[tuple[str, Union[str, bytes]]]:
111
+ """Write authentication tokens to the provided metadata."""
flwr/common/config.py CHANGED
@@ -14,6 +14,7 @@
14
14
  # ==============================================================================
15
15
  """Provide functions for managing global Flower config."""
16
16
 
17
+
17
18
  import os
18
19
  import re
19
20
  from pathlib import Path
@@ -27,6 +28,7 @@ from flwr.common.constant import (
27
28
  APP_DIR,
28
29
  FAB_CONFIG_FILE,
29
30
  FAB_HASH_TRUNCATION,
31
+ FLWR_DIR,
30
32
  FLWR_HOME,
31
33
  )
32
34
  from flwr.common.typing import Run, UserConfig, UserConfigValue
@@ -38,7 +40,7 @@ def get_flwr_dir(provided_path: Optional[str] = None) -> Path:
38
40
  return Path(
39
41
  os.getenv(
40
42
  FLWR_HOME,
41
- Path(f"{os.getenv('XDG_DATA_HOME', os.getenv('HOME'))}") / ".flwr",
43
+ Path(f"{os.getenv('XDG_DATA_HOME', os.getenv('HOME'))}") / FLWR_DIR,
42
44
  )
43
45
  )
44
46
  return Path(provided_path).absolute()
flwr/common/constant.py CHANGED
@@ -80,7 +80,8 @@ FAB_ALLOWED_EXTENSIONS = {".py", ".toml", ".md"}
80
80
  FAB_CONFIG_FILE = "pyproject.toml"
81
81
  FAB_DATE = (2024, 10, 1, 0, 0, 0)
82
82
  FAB_HASH_TRUNCATION = 8
83
- FLWR_HOME = "FLWR_HOME"
83
+ FLWR_DIR = ".flwr" # The default Flower directory: ~/.flwr/
84
+ FLWR_HOME = "FLWR_HOME" # If set, override the default Flower directory
84
85
 
85
86
  # Constants entries in Node config for Simulation
86
87
  PARTITION_ID_KEY = "partition-id"
@@ -110,6 +111,10 @@ LOG_UPLOAD_INTERVAL = 0.2 # Minimum interval between two log uploads
110
111
  # Retry configurations
111
112
  MAX_RETRY_DELAY = 20 # Maximum delay duration between two consecutive retries.
112
113
 
114
+ # Constants for user authentication
115
+ CREDENTIALS_DIR = ".credentials"
116
+ AUTH_TYPE = "auth_type"
117
+
113
118
 
114
119
  class MessageType:
115
120
  """Message type."""
@@ -181,3 +186,14 @@ class SubStatus:
181
186
  def __new__(cls) -> SubStatus:
182
187
  """Prevent instantiation."""
183
188
  raise TypeError(f"{cls.__name__} cannot be instantiated.")
189
+
190
+
191
+ class CliOutputFormat:
192
+ """Define output format for `flwr` CLI commands."""
193
+
194
+ DEFAULT = "default"
195
+ JSON = "json"
196
+
197
+ def __new__(cls) -> CliOutputFormat:
198
+ """Prevent instantiation."""
199
+ raise TypeError(f"{cls.__name__} cannot be instantiated.")
flwr/common/logger.py CHANGED
@@ -16,9 +16,11 @@
16
16
 
17
17
 
18
18
  import logging
19
+ import re
19
20
  import sys
20
21
  import threading
21
22
  import time
23
+ from io import StringIO
22
24
  from logging import WARN, LogRecord
23
25
  from logging.handlers import HTTPHandler
24
26
  from queue import Empty, Queue
@@ -303,6 +305,13 @@ def restore_output() -> None:
303
305
  console_handler.stream = sys.stdout
304
306
 
305
307
 
308
+ def redirect_output(output_buffer: StringIO) -> None:
309
+ """Redirect stdout and stderr to text I/O buffer."""
310
+ sys.stdout = output_buffer
311
+ sys.stderr = output_buffer
312
+ console_handler.stream = sys.stdout
313
+
314
+
306
315
  def _log_uploader(
307
316
  log_queue: Queue[Optional[str]], node_id: int, run_id: int, stub: ServerAppIoStub
308
317
  ) -> None:
@@ -366,3 +375,19 @@ def stop_log_uploader(
366
375
  """Stop the log uploader thread."""
367
376
  log_queue.put(None)
368
377
  log_uploader.join()
378
+
379
+
380
+ def remove_emojis(text: str) -> str:
381
+ """Remove emojis from the provided text."""
382
+ emoji_pattern = re.compile(
383
+ "["
384
+ "\U0001F600-\U0001F64F" # Emoticons
385
+ "\U0001F300-\U0001F5FF" # Symbols & Pictographs
386
+ "\U0001F680-\U0001F6FF" # Transport & Map Symbols
387
+ "\U0001F1E0-\U0001F1FF" # Flags
388
+ "\U00002702-\U000027B0" # Dingbats
389
+ "\U000024C2-\U0001F251"
390
+ "]+",
391
+ flags=re.UNICODE,
392
+ )
393
+ return emoji_pattern.sub(r"", text)
flwr/common/message.py CHANGED
@@ -14,6 +14,7 @@
14
14
  # ==============================================================================
15
15
  """Message."""
16
16
 
17
+
17
18
  from __future__ import annotations
18
19
 
19
20
  import time