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.
- flwr/cli/build.py +3 -1
- flwr/cli/config_utils.py +53 -3
- flwr/cli/install.py +35 -20
- flwr/cli/run/run.py +39 -2
- flwr/client/__init__.py +1 -1
- flwr/client/app.py +22 -10
- flwr/client/grpc_adapter_client/__init__.py +15 -0
- flwr/client/grpc_adapter_client/connection.py +94 -0
- flwr/client/grpc_client/connection.py +5 -1
- flwr/client/grpc_rere_client/connection.py +8 -1
- flwr/client/grpc_rere_client/grpc_adapter.py +133 -0
- flwr/client/mod/__init__.py +3 -3
- flwr/client/rest_client/connection.py +9 -1
- flwr/client/supernode/app.py +140 -40
- flwr/common/__init__.py +12 -12
- flwr/common/config.py +71 -0
- flwr/common/constant.py +15 -0
- flwr/common/object_ref.py +39 -5
- flwr/common/record/__init__.py +1 -1
- flwr/common/telemetry.py +4 -0
- flwr/common/typing.py +9 -0
- flwr/proto/exec_pb2.py +34 -0
- flwr/proto/exec_pb2.pyi +55 -0
- flwr/proto/exec_pb2_grpc.py +101 -0
- flwr/proto/exec_pb2_grpc.pyi +41 -0
- flwr/proto/fab_pb2.py +30 -0
- flwr/proto/fab_pb2.pyi +56 -0
- flwr/proto/fab_pb2_grpc.py +4 -0
- flwr/proto/fab_pb2_grpc.pyi +4 -0
- flwr/server/__init__.py +2 -2
- flwr/server/app.py +62 -25
- flwr/server/run_serverapp.py +4 -2
- flwr/server/strategy/__init__.py +2 -2
- flwr/server/superlink/fleet/grpc_adapter/__init__.py +15 -0
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +131 -0
- flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +4 -0
- flwr/server/superlink/fleet/message_handler/message_handler.py +3 -3
- flwr/server/superlink/fleet/vce/vce_api.py +3 -1
- flwr/server/superlink/state/in_memory_state.py +8 -5
- flwr/server/superlink/state/sqlite_state.py +6 -3
- flwr/server/superlink/state/state.py +5 -4
- flwr/simulation/__init__.py +4 -1
- flwr/simulation/run_simulation.py +22 -0
- flwr/superexec/__init__.py +21 -0
- flwr/superexec/app.py +178 -0
- flwr/superexec/exec_grpc.py +51 -0
- flwr/superexec/exec_servicer.py +65 -0
- flwr/superexec/executor.py +54 -0
- {flwr_nightly-1.10.0.dev20240612.dist-info → flwr_nightly-1.10.0.dev20240619.dist-info}/METADATA +1 -1
- {flwr_nightly-1.10.0.dev20240612.dist-info → flwr_nightly-1.10.0.dev20240619.dist-info}/RECORD +53 -34
- {flwr_nightly-1.10.0.dev20240612.dist-info → flwr_nightly-1.10.0.dev20240619.dist-info}/entry_points.txt +1 -0
- {flwr_nightly-1.10.0.dev20240612.dist-info → flwr_nightly-1.10.0.dev20240619.dist-info}/LICENSE +0 -0
- {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
|
-
) ->
|
|
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
|
-
|
|
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,
|
|
84
|
-
|
|
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(
|
|
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(
|
|
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
|
-
) ->
|
|
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
|
|
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
|
|
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.\
|
|
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
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=
|
|
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=
|
|
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()
|