flwr-nightly 1.10.0.dev20240612__py3-none-any.whl → 1.10.0.dev20240619__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 (53) hide show
  1. flwr/cli/build.py +3 -1
  2. flwr/cli/config_utils.py +53 -3
  3. flwr/cli/install.py +35 -20
  4. flwr/cli/run/run.py +39 -2
  5. flwr/client/__init__.py +1 -1
  6. flwr/client/app.py +22 -10
  7. flwr/client/grpc_adapter_client/__init__.py +15 -0
  8. flwr/client/grpc_adapter_client/connection.py +94 -0
  9. flwr/client/grpc_client/connection.py +5 -1
  10. flwr/client/grpc_rere_client/connection.py +8 -1
  11. flwr/client/grpc_rere_client/grpc_adapter.py +133 -0
  12. flwr/client/mod/__init__.py +3 -3
  13. flwr/client/rest_client/connection.py +9 -1
  14. flwr/client/supernode/app.py +140 -40
  15. flwr/common/__init__.py +12 -12
  16. flwr/common/config.py +71 -0
  17. flwr/common/constant.py +15 -0
  18. flwr/common/object_ref.py +39 -5
  19. flwr/common/record/__init__.py +1 -1
  20. flwr/common/telemetry.py +4 -0
  21. flwr/common/typing.py +9 -0
  22. flwr/proto/exec_pb2.py +34 -0
  23. flwr/proto/exec_pb2.pyi +55 -0
  24. flwr/proto/exec_pb2_grpc.py +101 -0
  25. flwr/proto/exec_pb2_grpc.pyi +41 -0
  26. flwr/proto/fab_pb2.py +30 -0
  27. flwr/proto/fab_pb2.pyi +56 -0
  28. flwr/proto/fab_pb2_grpc.py +4 -0
  29. flwr/proto/fab_pb2_grpc.pyi +4 -0
  30. flwr/server/__init__.py +2 -2
  31. flwr/server/app.py +62 -25
  32. flwr/server/run_serverapp.py +4 -2
  33. flwr/server/strategy/__init__.py +2 -2
  34. flwr/server/superlink/fleet/grpc_adapter/__init__.py +15 -0
  35. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +131 -0
  36. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +4 -0
  37. flwr/server/superlink/fleet/message_handler/message_handler.py +3 -3
  38. flwr/server/superlink/fleet/vce/vce_api.py +3 -1
  39. flwr/server/superlink/state/in_memory_state.py +8 -5
  40. flwr/server/superlink/state/sqlite_state.py +6 -3
  41. flwr/server/superlink/state/state.py +5 -4
  42. flwr/simulation/__init__.py +4 -1
  43. flwr/simulation/run_simulation.py +22 -0
  44. flwr/superexec/__init__.py +21 -0
  45. flwr/superexec/app.py +178 -0
  46. flwr/superexec/exec_grpc.py +51 -0
  47. flwr/superexec/exec_servicer.py +65 -0
  48. flwr/superexec/executor.py +54 -0
  49. {flwr_nightly-1.10.0.dev20240612.dist-info → flwr_nightly-1.10.0.dev20240619.dist-info}/METADATA +1 -1
  50. {flwr_nightly-1.10.0.dev20240612.dist-info → flwr_nightly-1.10.0.dev20240619.dist-info}/RECORD +53 -34
  51. {flwr_nightly-1.10.0.dev20240612.dist-info → flwr_nightly-1.10.0.dev20240619.dist-info}/entry_points.txt +1 -0
  52. {flwr_nightly-1.10.0.dev20240612.dist-info → flwr_nightly-1.10.0.dev20240619.dist-info}/LICENSE +0 -0
  53. {flwr_nightly-1.10.0.dev20240612.dist-info → flwr_nightly-1.10.0.dev20240619.dist-info}/WHEEL +0 -0
flwr/cli/build.py CHANGED
@@ -33,7 +33,7 @@ def build(
33
33
  Optional[Path],
34
34
  typer.Option(help="The Flower project directory to bundle into a FAB"),
35
35
  ] = None,
36
- ) -> None:
36
+ ) -> str:
37
37
  """Build a Flower project into a Flower App Bundle (FAB).
38
38
 
39
39
  You can run `flwr build` without any argument to bundle the current directory:
@@ -125,6 +125,8 @@ def build(
125
125
  f"🎊 Successfully built {fab_filename}.", fg=typer.colors.GREEN, bold=True
126
126
  )
127
127
 
128
+ return fab_filename
129
+
128
130
 
129
131
  def _load_gitignore(directory: Path) -> pathspec.PathSpec:
130
132
  """Load and parse .gitignore file, returning a pathspec."""
flwr/cli/config_utils.py CHANGED
@@ -14,14 +14,56 @@
14
14
  # ==============================================================================
15
15
  """Utility to validate the `pyproject.toml` file."""
16
16
 
17
+ import zipfile
18
+ from io import BytesIO
17
19
  from pathlib import Path
18
- from typing import Any, Dict, List, Optional, Tuple
20
+ from typing import IO, Any, Dict, List, Optional, Tuple, Union
19
21
 
20
22
  import tomli
21
23
 
22
24
  from flwr.common import object_ref
23
25
 
24
26
 
27
+ def get_fab_metadata(fab_file: Union[Path, bytes]) -> Tuple[str, str]:
28
+ """Extract the fab_id and the fab_version from a FAB file or path.
29
+
30
+ Parameters
31
+ ----------
32
+ fab_file : Union[Path, bytes]
33
+ The Flower App Bundle file to validate and extract the metadata from.
34
+ It can either be a path to the file or the file itself as bytes.
35
+
36
+ Returns
37
+ -------
38
+ Tuple[str, str]
39
+ The `fab_version` and `fab_id` of the given Flower App Bundle.
40
+ """
41
+ fab_file_archive: Union[Path, IO[bytes]]
42
+ if isinstance(fab_file, bytes):
43
+ fab_file_archive = BytesIO(fab_file)
44
+ elif isinstance(fab_file, Path):
45
+ fab_file_archive = fab_file
46
+ else:
47
+ raise ValueError("fab_file must be either a Path or bytes")
48
+
49
+ with zipfile.ZipFile(fab_file_archive, "r") as zipf:
50
+ with zipf.open("pyproject.toml") as file:
51
+ toml_content = file.read().decode("utf-8")
52
+
53
+ conf = load_from_string(toml_content)
54
+ if conf is None:
55
+ raise ValueError("Invalid TOML content in pyproject.toml")
56
+
57
+ is_valid, errors, _ = validate(conf, check_module=False)
58
+ if not is_valid:
59
+ raise ValueError(errors)
60
+
61
+ return (
62
+ conf["project"]["version"],
63
+ f"{conf['flower']['publisher']}/{conf['project']['name']}",
64
+ )
65
+
66
+
25
67
  def load_and_validate(
26
68
  path: Optional[Path] = None,
27
69
  check_module: bool = True,
@@ -63,8 +105,7 @@ def load(path: Optional[Path] = None) -> Optional[Dict[str, Any]]:
63
105
  return None
64
106
 
65
107
  with toml_path.open(encoding="utf-8") as toml_file:
66
- data = tomli.loads(toml_file.read())
67
- return data
108
+ return load_from_string(toml_file.read())
68
109
 
69
110
 
70
111
  # pylint: disable=too-many-branches
@@ -128,3 +169,12 @@ def validate(
128
169
  return False, [reason], []
129
170
 
130
171
  return True, [], []
172
+
173
+
174
+ def load_from_string(toml_content: str) -> Optional[Dict[str, Any]]:
175
+ """Load TOML content from a string and return as dict."""
176
+ try:
177
+ data = tomli.loads(toml_content)
178
+ return data
179
+ except tomli.TOMLDecodeError:
180
+ return None
flwr/cli/install.py CHANGED
@@ -15,16 +15,18 @@
15
15
  """Flower command line interface `install` command."""
16
16
 
17
17
 
18
- import os
19
18
  import shutil
20
19
  import tempfile
21
20
  import zipfile
21
+ from io import BytesIO
22
22
  from pathlib import Path
23
- from typing import Optional
23
+ from typing import IO, Optional, Union
24
24
 
25
25
  import typer
26
26
  from typing_extensions import Annotated
27
27
 
28
+ from flwr.common.config import get_flwr_dir
29
+
28
30
  from .config_utils import load_and_validate
29
31
  from .utils import get_sha256_hash
30
32
 
@@ -80,11 +82,24 @@ def install(
80
82
 
81
83
 
82
84
  def install_from_fab(
83
- fab_file: Path, flwr_dir: Optional[Path], skip_prompt: bool = False
84
- ) -> None:
85
+ fab_file: Union[Path, bytes],
86
+ flwr_dir: Optional[Path],
87
+ skip_prompt: bool = False,
88
+ ) -> Path:
85
89
  """Install from a FAB file after extracting and validating."""
90
+ fab_file_archive: Union[Path, IO[bytes]]
91
+ fab_name: Optional[str]
92
+ if isinstance(fab_file, bytes):
93
+ fab_file_archive = BytesIO(fab_file)
94
+ fab_name = None
95
+ elif isinstance(fab_file, Path):
96
+ fab_file_archive = fab_file
97
+ fab_name = fab_file.stem
98
+ else:
99
+ raise ValueError("fab_file must be either a Path or bytes")
100
+
86
101
  with tempfile.TemporaryDirectory() as tmpdir:
87
- with zipfile.ZipFile(fab_file, "r") as zipf:
102
+ with zipfile.ZipFile(fab_file_archive, "r") as zipf:
88
103
  zipf.extractall(tmpdir)
89
104
  tmpdir_path = Path(tmpdir)
90
105
  info_dir = tmpdir_path / ".info"
@@ -110,15 +125,19 @@ def install_from_fab(
110
125
 
111
126
  shutil.rmtree(info_dir)
112
127
 
113
- validate_and_install(tmpdir_path, fab_file.stem, flwr_dir, skip_prompt)
128
+ installed_path = validate_and_install(
129
+ tmpdir_path, fab_name, flwr_dir, skip_prompt
130
+ )
131
+
132
+ return installed_path
114
133
 
115
134
 
116
135
  def validate_and_install(
117
136
  project_dir: Path,
118
- fab_name: str,
137
+ fab_name: Optional[str],
119
138
  flwr_dir: Optional[Path],
120
139
  skip_prompt: bool = False,
121
- ) -> None:
140
+ ) -> Path:
122
141
  """Validate TOML files and install the project to the desired directory."""
123
142
  config, _, _ = load_and_validate(project_dir / "pyproject.toml", check_module=False)
124
143
 
@@ -134,7 +153,10 @@ def validate_and_install(
134
153
  project_name = config["project"]["name"]
135
154
  version = config["project"]["version"]
136
155
 
137
- if fab_name != f"{publisher}.{project_name}.{version.replace('.', '-')}":
156
+ if (
157
+ fab_name
158
+ and fab_name != f"{publisher}.{project_name}.{version.replace('.', '-')}"
159
+ ):
138
160
  typer.secho(
139
161
  "❌ FAB file has incorrect name. The file name must follow the format "
140
162
  "`<publisher>.<project_name>.<version>.fab`.",
@@ -144,16 +166,7 @@ def validate_and_install(
144
166
  raise typer.Exit(code=1)
145
167
 
146
168
  install_dir: Path = (
147
- (
148
- Path(
149
- os.getenv(
150
- "FLWR_HOME",
151
- f"{os.getenv('XDG_DATA_HOME', os.getenv('HOME'))}/.flwr",
152
- )
153
- )
154
- if not flwr_dir
155
- else flwr_dir
156
- )
169
+ (get_flwr_dir() if not flwr_dir else flwr_dir)
157
170
  / "apps"
158
171
  / publisher
159
172
  / project_name
@@ -168,7 +181,7 @@ def validate_and_install(
168
181
  bold=True,
169
182
  )
170
183
  ):
171
- return
184
+ return install_dir
172
185
 
173
186
  install_dir.mkdir(parents=True, exist_ok=True)
174
187
 
@@ -185,6 +198,8 @@ def validate_and_install(
185
198
  bold=True,
186
199
  )
187
200
 
201
+ return install_dir
202
+
188
203
 
189
204
  def _verify_hashes(list_content: str, tmpdir: Path) -> bool:
190
205
  """Verify file hashes based on the LIST content."""
flwr/cli/run/run.py CHANGED
@@ -16,12 +16,18 @@
16
16
 
17
17
  import sys
18
18
  from enum import Enum
19
+ from logging import DEBUG
19
20
  from typing import Optional
20
21
 
21
22
  import typer
22
23
  from typing_extensions import Annotated
23
24
 
24
25
  from flwr.cli import config_utils
26
+ from flwr.common.constant import SUPEREXEC_DEFAULT_ADDRESS
27
+ from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel
28
+ from flwr.common.logger import log
29
+ from flwr.proto.exec_pb2 import StartRunRequest # pylint: disable=E0611
30
+ from flwr.proto.exec_pb2_grpc import ExecStub
25
31
  from flwr.simulation.run_simulation import _run_simulation
26
32
 
27
33
 
@@ -31,20 +37,32 @@ class Engine(str, Enum):
31
37
  SIMULATION = "simulation"
32
38
 
33
39
 
40
+ # pylint: disable-next=too-many-locals
34
41
  def run(
35
42
  engine: Annotated[
36
43
  Optional[Engine],
37
- typer.Option(case_sensitive=False, help="The ML framework to use"),
44
+ typer.Option(case_sensitive=False, help="The execution engine to run the app"),
38
45
  ] = None,
46
+ use_superexec: Annotated[
47
+ bool,
48
+ typer.Option(
49
+ case_sensitive=False, help="Use this flag to use the new SuperExec API"
50
+ ),
51
+ ] = False,
39
52
  ) -> None:
40
53
  """Run Flower project."""
54
+ if use_superexec:
55
+ _start_superexec_run()
56
+ return
57
+
41
58
  typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
42
59
 
43
60
  config, errors, warnings = config_utils.load_and_validate()
44
61
 
45
62
  if config is None:
46
63
  typer.secho(
47
- "Project configuration could not be loaded.\npyproject.toml is invalid:\n"
64
+ "Project configuration could not be loaded.\n"
65
+ "pyproject.toml is invalid:\n"
48
66
  + "\n".join([f"- {line}" for line in errors]),
49
67
  fg=typer.colors.RED,
50
68
  bold=True,
@@ -82,3 +100,22 @@ def run(
82
100
  fg=typer.colors.RED,
83
101
  bold=True,
84
102
  )
103
+
104
+
105
+ def _start_superexec_run() -> None:
106
+ def on_channel_state_change(channel_connectivity: str) -> None:
107
+ """Log channel connectivity."""
108
+ log(DEBUG, channel_connectivity)
109
+
110
+ channel = create_channel(
111
+ server_address=SUPEREXEC_DEFAULT_ADDRESS,
112
+ insecure=True,
113
+ root_certificates=None,
114
+ max_message_length=GRPC_MAX_MESSAGE_LENGTH,
115
+ interceptors=None,
116
+ )
117
+ channel.subscribe(on_channel_state_change)
118
+ stub = ExecStub(channel)
119
+
120
+ req = StartRunRequest()
121
+ stub.StartRun(req)
flwr/client/__init__.py CHANGED
@@ -28,8 +28,8 @@ __all__ = [
28
28
  "Client",
29
29
  "ClientApp",
30
30
  "ClientFn",
31
- "mod",
32
31
  "NumPyClient",
32
+ "mod",
33
33
  "run_client_app",
34
34
  "run_supernode",
35
35
  "start_client",
flwr/client/app.py CHANGED
@@ -19,7 +19,7 @@ import sys
19
19
  import time
20
20
  from dataclasses import dataclass
21
21
  from logging import DEBUG, ERROR, INFO, WARN
22
- from typing import Callable, ContextManager, Optional, Tuple, Type, Union
22
+ from typing import Callable, ContextManager, Dict, Optional, Tuple, Type, Union
23
23
 
24
24
  from cryptography.hazmat.primitives.asymmetric import ec
25
25
  from grpc import RpcError
@@ -31,6 +31,7 @@ from flwr.common import GRPC_MAX_MESSAGE_LENGTH, EventType, Message, event
31
31
  from flwr.common.address import parse_address
32
32
  from flwr.common.constant import (
33
33
  MISSING_EXTRA_REST,
34
+ TRANSPORT_TYPE_GRPC_ADAPTER,
34
35
  TRANSPORT_TYPE_GRPC_BIDI,
35
36
  TRANSPORT_TYPE_GRPC_RERE,
36
37
  TRANSPORT_TYPE_REST,
@@ -41,6 +42,7 @@ from flwr.common.logger import log, warn_deprecated_feature
41
42
  from flwr.common.message import Error
42
43
  from flwr.common.retry_invoker import RetryInvoker, RetryState, exponential
43
44
 
45
+ from .grpc_adapter_client.connection import grpc_adapter
44
46
  from .grpc_client.connection import grpc_connection
45
47
  from .grpc_rere_client.connection import grpc_request_response
46
48
  from .message_handler.message_handler import handle_control_message
@@ -177,7 +179,7 @@ def start_client(
177
179
  def _start_client_internal(
178
180
  *,
179
181
  server_address: str,
180
- load_client_app_fn: Optional[Callable[[], ClientApp]] = None,
182
+ load_client_app_fn: Optional[Callable[[str, str], ClientApp]] = None,
181
183
  client_fn: Optional[ClientFn] = None,
182
184
  client: Optional[Client] = None,
183
185
  grpc_max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
@@ -252,7 +254,7 @@ def _start_client_internal(
252
254
 
253
255
  client_fn = single_client_factory
254
256
 
255
- def _load_client_app() -> ClientApp:
257
+ def _load_client_app(_1: str, _2: str) -> ClientApp:
256
258
  return ClientApp(client_fn=client_fn)
257
259
 
258
260
  load_client_app_fn = _load_client_app
@@ -308,6 +310,8 @@ def _start_client_internal(
308
310
  )
309
311
 
310
312
  node_state = NodeState()
313
+ # run_id -> (fab_id, fab_version)
314
+ run_info: Dict[int, Tuple[str, str]] = {}
311
315
 
312
316
  while not app_state_tracker.interrupt:
313
317
  sleep_duration: int = 0
@@ -319,7 +323,6 @@ def _start_client_internal(
319
323
  root_certificates,
320
324
  authentication_keys,
321
325
  ) as conn:
322
- # pylint: disable-next=W0612
323
326
  receive, send, create_node, delete_node, get_run = conn
324
327
 
325
328
  # Register node
@@ -356,13 +359,20 @@ def _start_client_internal(
356
359
  send(out_message)
357
360
  break
358
361
 
362
+ # Get run info
363
+ run_id = message.metadata.run_id
364
+ if run_id not in run_info:
365
+ if get_run is not None:
366
+ run_info[run_id] = get_run(run_id)
367
+ # If get_run is None, i.e., in grpc-bidi mode
368
+ else:
369
+ run_info[run_id] = ("", "")
370
+
359
371
  # Register context for this run
360
- node_state.register_context(run_id=message.metadata.run_id)
372
+ node_state.register_context(run_id=run_id)
361
373
 
362
374
  # Retrieve context for this run
363
- context = node_state.retrieve_context(
364
- run_id=message.metadata.run_id
365
- )
375
+ context = node_state.retrieve_context(run_id=run_id)
366
376
 
367
377
  # Create an error reply message that will never be used to prevent
368
378
  # the used-before-assignment linting error
@@ -373,7 +383,7 @@ def _start_client_internal(
373
383
  # Handle app loading and task message
374
384
  try:
375
385
  # Load ClientApp instance
376
- client_app: ClientApp = load_client_app_fn()
386
+ client_app: ClientApp = load_client_app_fn(*run_info[run_id])
377
387
 
378
388
  # Execute ClientApp
379
389
  reply_message = client_app(message=message, context=context)
@@ -411,7 +421,7 @@ def _start_client_internal(
411
421
  else:
412
422
  # No exception, update node state
413
423
  node_state.update_context(
414
- run_id=message.metadata.run_id,
424
+ run_id=run_id,
415
425
  context=context,
416
426
  )
417
427
 
@@ -592,6 +602,8 @@ def _init_connection(transport: Optional[str], server_address: str) -> Tuple[
592
602
  connection, error_type = http_request_response, RequestsConnectionError
593
603
  elif transport == TRANSPORT_TYPE_GRPC_RERE:
594
604
  connection, error_type = grpc_request_response, RpcError
605
+ elif transport == TRANSPORT_TYPE_GRPC_ADAPTER:
606
+ connection, error_type = grpc_adapter, RpcError
595
607
  elif transport == TRANSPORT_TYPE_GRPC_BIDI:
596
608
  connection, error_type = grpc_connection, RpcError
597
609
  else:
@@ -0,0 +1,15 @@
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
+ """Client-side part of the GrpcAdapter transport layer."""
@@ -0,0 +1,94 @@
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
+ """Contextmanager for a GrpcAdapter channel to the Flower server."""
16
+
17
+
18
+ from contextlib import contextmanager
19
+ from logging import ERROR
20
+ from typing import Callable, Iterator, Optional, Tuple, Union
21
+
22
+ from cryptography.hazmat.primitives.asymmetric import ec
23
+
24
+ from flwr.client.grpc_rere_client.connection import grpc_request_response
25
+ from flwr.client.grpc_rere_client.grpc_adapter import GrpcAdapter
26
+ from flwr.common import GRPC_MAX_MESSAGE_LENGTH
27
+ from flwr.common.logger import log
28
+ from flwr.common.message import Message
29
+ from flwr.common.retry_invoker import RetryInvoker
30
+
31
+
32
+ @contextmanager
33
+ def grpc_adapter( # pylint: disable=R0913
34
+ server_address: str,
35
+ insecure: bool,
36
+ retry_invoker: RetryInvoker,
37
+ max_message_length: int = GRPC_MAX_MESSAGE_LENGTH, # pylint: disable=W0613
38
+ root_certificates: Optional[Union[bytes, str]] = None,
39
+ authentication_keys: Optional[ # pylint: disable=unused-argument
40
+ Tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]
41
+ ] = None,
42
+ ) -> Iterator[
43
+ Tuple[
44
+ Callable[[], Optional[Message]],
45
+ Callable[[Message], None],
46
+ Optional[Callable[[], None]],
47
+ Optional[Callable[[], None]],
48
+ Optional[Callable[[int], Tuple[str, str]]],
49
+ ]
50
+ ]:
51
+ """Primitives for request/response-based interaction with a server via GrpcAdapter.
52
+
53
+ Parameters
54
+ ----------
55
+ server_address : str
56
+ The IPv6 address of the server with `http://` or `https://`.
57
+ If the Flower server runs on the same machine
58
+ on port 8080, then `server_address` would be `"http://[::]:8080"`.
59
+ insecure : bool
60
+ Starts an insecure gRPC connection when True. Enables HTTPS connection
61
+ when False, using system certificates if `root_certificates` is None.
62
+ retry_invoker: RetryInvoker
63
+ `RetryInvoker` object that will try to reconnect the client to the server
64
+ after gRPC errors. If None, the client will only try to
65
+ reconnect once after a failure.
66
+ max_message_length : int
67
+ Ignored, only present to preserve API-compatibility.
68
+ root_certificates : Optional[Union[bytes, str]] (default: None)
69
+ Path of the root certificate. If provided, a secure
70
+ connection using the certificates will be established to an SSL-enabled
71
+ Flower server. Bytes won't work for the REST API.
72
+ authentication_keys : Optional[Tuple[PrivateKey, PublicKey]] (default: None)
73
+ Client authentication is not supported for this transport type.
74
+
75
+ Returns
76
+ -------
77
+ receive : Callable
78
+ send : Callable
79
+ create_node : Optional[Callable]
80
+ delete_node : Optional[Callable]
81
+ get_run : Optional[Callable]
82
+ """
83
+ if authentication_keys is not None:
84
+ log(ERROR, "Client authentication is not supported for this transport type.")
85
+ with grpc_request_response(
86
+ server_address=server_address,
87
+ insecure=insecure,
88
+ retry_invoker=retry_invoker,
89
+ max_message_length=max_message_length,
90
+ root_certificates=root_certificates,
91
+ authentication_keys=None, # Authentication is not supported
92
+ adapter_cls=GrpcAdapter,
93
+ ) as conn:
94
+ yield conn
@@ -17,7 +17,7 @@
17
17
 
18
18
  import uuid
19
19
  from contextlib import contextmanager
20
- from logging import DEBUG
20
+ from logging import DEBUG, ERROR
21
21
  from pathlib import Path
22
22
  from queue import Queue
23
23
  from typing import Callable, Iterator, Optional, Tuple, Union, cast
@@ -101,6 +101,8 @@ def grpc_connection( # pylint: disable=R0913, R0915
101
101
  The PEM-encoded root certificates as a byte string or a path string.
102
102
  If provided, a secure connection using the certificates will be
103
103
  established to an SSL-enabled Flower server.
104
+ authentication_keys : Optional[Tuple[PrivateKey, PublicKey]] (default: None)
105
+ Client authentication is not supported for this transport type.
104
106
 
105
107
  Returns
106
108
  -------
@@ -123,6 +125,8 @@ def grpc_connection( # pylint: disable=R0913, R0915
123
125
  """
124
126
  if isinstance(root_certificates, str):
125
127
  root_certificates = Path(root_certificates).read_bytes()
128
+ if authentication_keys is not None:
129
+ log(ERROR, "Client authentication is not supported for this transport type.")
126
130
 
127
131
  channel = create_channel(
128
132
  server_address=server_address,
@@ -55,6 +55,7 @@ from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=
55
55
  from flwr.proto.task_pb2 import TaskIns # pylint: disable=E0611
56
56
 
57
57
  from .client_interceptor import AuthenticateClientInterceptor
58
+ from .grpc_adapter import GrpcAdapter
58
59
 
59
60
 
60
61
  def on_channel_state_change(channel_connectivity: str) -> None:
@@ -72,7 +73,7 @@ def grpc_request_response( # pylint: disable=R0913, R0914, R0915
72
73
  authentication_keys: Optional[
73
74
  Tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]
74
75
  ] = None,
75
- adapter_cls: Optional[Type[FleetStub]] = None,
76
+ adapter_cls: Optional[Union[Type[FleetStub], Type[GrpcAdapter]]] = None,
76
77
  ) -> Iterator[
77
78
  Tuple[
78
79
  Callable[[], Optional[Message]],
@@ -106,6 +107,11 @@ def grpc_request_response( # pylint: disable=R0913, R0914, R0915
106
107
  Path of the root certificate. If provided, a secure
107
108
  connection using the certificates will be established to an SSL-enabled
108
109
  Flower server. Bytes won't work for the REST API.
110
+ authentication_keys : Optional[Tuple[PrivateKey, PublicKey]] (default: None)
111
+ Tuple containing the elliptic curve private key and public key for
112
+ authentication from the cryptography library.
113
+ Source: https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/
114
+ Used to establish an authenticated connection with the server.
109
115
 
110
116
  Returns
111
117
  -------
@@ -113,6 +119,7 @@ def grpc_request_response( # pylint: disable=R0913, R0914, R0915
113
119
  send : Callable
114
120
  create_node : Optional[Callable]
115
121
  delete_node : Optional[Callable]
122
+ get_run : Optional[Callable]
116
123
  """
117
124
  if isinstance(root_certificates, str):
118
125
  root_certificates = Path(root_certificates).read_bytes()