flwr-nightly 1.12.0.dev20241009__py3-none-any.whl → 1.12.0.dev20241011__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 +60 -29
- flwr/cli/config_utils.py +10 -0
- flwr/cli/install.py +60 -20
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +2 -2
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
- flwr/cli/run/run.py +5 -5
- flwr/client/app.py +13 -3
- flwr/client/clientapp/app.py +3 -1
- flwr/client/clientapp/utils.py +11 -5
- flwr/client/grpc_adapter_client/connection.py +1 -1
- flwr/client/grpc_client/connection.py +1 -1
- flwr/client/grpc_rere_client/connection.py +4 -1
- flwr/client/node_state.py +1 -1
- flwr/client/rest_client/connection.py +1 -1
- flwr/common/config.py +19 -5
- flwr/common/logger.py +1 -1
- flwr/common/message.py +6 -1
- flwr/common/record/configsrecord.py +6 -0
- flwr/common/recordset_compat.py +10 -0
- flwr/common/retry_invoker.py +15 -0
- flwr/server/app.py +1 -0
- flwr/server/client_manager.py +2 -0
- flwr/server/driver/driver.py +1 -1
- flwr/server/driver/grpc_driver.py +1 -1
- flwr/server/driver/inmemory_driver.py +2 -2
- flwr/server/run_serverapp.py +11 -13
- flwr/server/server_app.py +1 -1
- flwr/server/strategy/dp_adaptive_clipping.py +2 -2
- flwr/server/strategy/dpfedavg_adaptive.py +1 -1
- flwr/server/strategy/dpfedavg_fixed.py +1 -1
- flwr/server/superlink/driver/driver_servicer.py +1 -1
- flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +5 -3
- flwr/server/superlink/fleet/vce/vce_api.py +9 -6
- flwr/server/superlink/state/in_memory_state.py +1 -8
- flwr/server/superlink/state/sqlite_state.py +6 -11
- flwr/server/superlink/state/state.py +1 -7
- flwr/server/superlink/state/utils.py +0 -10
- flwr/simulation/ray_transport/ray_client_proxy.py +1 -1
- flwr/simulation/run_simulation.py +4 -4
- {flwr_nightly-1.12.0.dev20241009.dist-info → flwr_nightly-1.12.0.dev20241011.dist-info}/METADATA +1 -1
- {flwr_nightly-1.12.0.dev20241009.dist-info → flwr_nightly-1.12.0.dev20241011.dist-info}/RECORD +51 -51
- {flwr_nightly-1.12.0.dev20241009.dist-info → flwr_nightly-1.12.0.dev20241011.dist-info}/LICENSE +0 -0
- {flwr_nightly-1.12.0.dev20241009.dist-info → flwr_nightly-1.12.0.dev20241011.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.12.0.dev20241009.dist-info → flwr_nightly-1.12.0.dev20241011.dist-info}/entry_points.txt +0 -0
flwr/cli/build.py
CHANGED
|
@@ -14,26 +14,50 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Flower command line interface `build` command."""
|
|
16
16
|
|
|
17
|
+
import hashlib
|
|
17
18
|
import os
|
|
19
|
+
import shutil
|
|
20
|
+
import tempfile
|
|
18
21
|
import zipfile
|
|
19
22
|
from pathlib import Path
|
|
20
|
-
from typing import Annotated, Optional
|
|
23
|
+
from typing import Annotated, Any, Optional, Union
|
|
21
24
|
|
|
22
25
|
import pathspec
|
|
23
26
|
import tomli_w
|
|
24
27
|
import typer
|
|
25
28
|
|
|
29
|
+
from flwr.common.constant import FAB_ALLOWED_EXTENSIONS, FAB_DATE, FAB_HASH_TRUNCATION
|
|
30
|
+
|
|
26
31
|
from .config_utils import load_and_validate
|
|
27
|
-
from .utils import
|
|
32
|
+
from .utils import is_valid_project_name
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def write_to_zip(
|
|
36
|
+
zipfile_obj: zipfile.ZipFile, filename: str, contents: Union[bytes, str]
|
|
37
|
+
) -> zipfile.ZipFile:
|
|
38
|
+
"""Set a fixed date and write contents to a zip file."""
|
|
39
|
+
zip_info = zipfile.ZipInfo(filename)
|
|
40
|
+
zip_info.date_time = FAB_DATE
|
|
41
|
+
zipfile_obj.writestr(zip_info, contents)
|
|
42
|
+
return zipfile_obj
|
|
43
|
+
|
|
28
44
|
|
|
45
|
+
def get_fab_filename(conf: dict[str, Any], fab_hash: str) -> str:
|
|
46
|
+
"""Get the FAB filename based on the given config and FAB hash."""
|
|
47
|
+
publisher = conf["tool"]["flwr"]["app"]["publisher"]
|
|
48
|
+
name = conf["project"]["name"]
|
|
49
|
+
version = conf["project"]["version"].replace(".", "-")
|
|
50
|
+
fab_hash_truncated = fab_hash[:FAB_HASH_TRUNCATION]
|
|
51
|
+
return f"{publisher}.{name}.{version}.{fab_hash_truncated}.fab"
|
|
29
52
|
|
|
30
|
-
|
|
53
|
+
|
|
54
|
+
# pylint: disable=too-many-locals, too-many-statements
|
|
31
55
|
def build(
|
|
32
56
|
app: Annotated[
|
|
33
57
|
Optional[Path],
|
|
34
58
|
typer.Option(help="Path of the Flower App to bundle into a FAB"),
|
|
35
59
|
] = None,
|
|
36
|
-
) -> str:
|
|
60
|
+
) -> tuple[str, str]:
|
|
37
61
|
"""Build a Flower App into a Flower App Bundle (FAB).
|
|
38
62
|
|
|
39
63
|
You can run ``flwr build`` without any arguments to bundle the app located in the
|
|
@@ -85,16 +109,8 @@ def build(
|
|
|
85
109
|
# Load .gitignore rules if present
|
|
86
110
|
ignore_spec = _load_gitignore(app)
|
|
87
111
|
|
|
88
|
-
# Set the name of the zip file
|
|
89
|
-
fab_filename = (
|
|
90
|
-
f"{conf['tool']['flwr']['app']['publisher']}"
|
|
91
|
-
f".{conf['project']['name']}"
|
|
92
|
-
f".{conf['project']['version'].replace('.', '-')}.fab"
|
|
93
|
-
)
|
|
94
112
|
list_file_content = ""
|
|
95
113
|
|
|
96
|
-
allowed_extensions = {".py", ".toml", ".md"}
|
|
97
|
-
|
|
98
114
|
# Remove the 'federations' field from 'tool.flwr' if it exists
|
|
99
115
|
if (
|
|
100
116
|
"tool" in conf
|
|
@@ -105,38 +121,53 @@ def build(
|
|
|
105
121
|
|
|
106
122
|
toml_contents = tomli_w.dumps(conf)
|
|
107
123
|
|
|
108
|
-
with
|
|
109
|
-
|
|
124
|
+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as temp_file:
|
|
125
|
+
temp_filename = temp_file.name
|
|
126
|
+
|
|
127
|
+
with zipfile.ZipFile(temp_filename, "w", zipfile.ZIP_DEFLATED) as fab_file:
|
|
128
|
+
write_to_zip(fab_file, "pyproject.toml", toml_contents)
|
|
110
129
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
files = [
|
|
130
|
+
# Continue with adding other files
|
|
131
|
+
all_files = [
|
|
114
132
|
f
|
|
115
|
-
for f in
|
|
116
|
-
if not ignore_spec.match_file(
|
|
117
|
-
and f !=
|
|
118
|
-
and
|
|
119
|
-
and f != "pyproject.toml" # Exclude the original pyproject.toml
|
|
133
|
+
for f in app.rglob("*")
|
|
134
|
+
if not ignore_spec.match_file(f)
|
|
135
|
+
and f.name != temp_filename
|
|
136
|
+
and f.suffix in FAB_ALLOWED_EXTENSIONS
|
|
137
|
+
and f.name != "pyproject.toml" # Exclude the original pyproject.toml
|
|
120
138
|
]
|
|
121
139
|
|
|
122
|
-
for
|
|
123
|
-
|
|
140
|
+
for file_path in all_files:
|
|
141
|
+
# Read the file content manually
|
|
142
|
+
with open(file_path, "rb") as f:
|
|
143
|
+
file_contents = f.read()
|
|
144
|
+
|
|
124
145
|
archive_path = file_path.relative_to(app)
|
|
125
|
-
fab_file
|
|
146
|
+
write_to_zip(fab_file, str(archive_path), file_contents)
|
|
126
147
|
|
|
127
148
|
# Calculate file info
|
|
128
|
-
sha256_hash =
|
|
149
|
+
sha256_hash = hashlib.sha256(file_contents).hexdigest()
|
|
129
150
|
file_size_bits = os.path.getsize(file_path) * 8 # size in bits
|
|
130
151
|
list_file_content += f"{archive_path},{sha256_hash},{file_size_bits}\n"
|
|
131
152
|
|
|
132
|
-
|
|
133
|
-
|
|
153
|
+
# Add CONTENT and CONTENT.jwt to the zip file
|
|
154
|
+
write_to_zip(fab_file, ".info/CONTENT", list_file_content)
|
|
155
|
+
|
|
156
|
+
# Get hash of FAB file
|
|
157
|
+
content = Path(temp_filename).read_bytes()
|
|
158
|
+
fab_hash = hashlib.sha256(content).hexdigest()
|
|
159
|
+
|
|
160
|
+
# Set the name of the zip file
|
|
161
|
+
fab_filename = get_fab_filename(conf, fab_hash)
|
|
162
|
+
|
|
163
|
+
# Once the temporary zip file is created, rename it to the final filename
|
|
164
|
+
shutil.move(temp_filename, fab_filename)
|
|
134
165
|
|
|
135
166
|
typer.secho(
|
|
136
167
|
f"🎊 Successfully built {fab_filename}", fg=typer.colors.GREEN, bold=True
|
|
137
168
|
)
|
|
138
169
|
|
|
139
|
-
return fab_filename
|
|
170
|
+
return fab_filename, fab_hash
|
|
140
171
|
|
|
141
172
|
|
|
142
173
|
def _load_gitignore(app: Path) -> pathspec.PathSpec:
|
flwr/cli/config_utils.py
CHANGED
|
@@ -90,6 +90,16 @@ def load_and_validate(
|
|
|
90
90
|
) -> tuple[Optional[dict[str, Any]], list[str], list[str]]:
|
|
91
91
|
"""Load and validate pyproject.toml as dict.
|
|
92
92
|
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
path : Optional[Path] (default: None)
|
|
96
|
+
The path of the Flower App config file to load. By default it
|
|
97
|
+
will try to use `pyproject.toml` inside the current directory.
|
|
98
|
+
check_module: bool (default: True)
|
|
99
|
+
Whether the validity of the Python module should be checked.
|
|
100
|
+
This requires the project to be installed in the currently
|
|
101
|
+
running environment. True by default.
|
|
102
|
+
|
|
93
103
|
Returns
|
|
94
104
|
-------
|
|
95
105
|
Tuple[Optional[config], List[str], List[str]]
|
flwr/cli/install.py
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Flower command line interface `install` command."""
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
import hashlib
|
|
18
18
|
import shutil
|
|
19
19
|
import subprocess
|
|
20
20
|
import tempfile
|
|
@@ -25,7 +25,8 @@ from typing import IO, Annotated, Optional, Union
|
|
|
25
25
|
|
|
26
26
|
import typer
|
|
27
27
|
|
|
28
|
-
from flwr.common.config import get_flwr_dir
|
|
28
|
+
from flwr.common.config import get_flwr_dir, get_metadata_from_config
|
|
29
|
+
from flwr.common.constant import FAB_HASH_TRUNCATION
|
|
29
30
|
|
|
30
31
|
from .config_utils import load_and_validate
|
|
31
32
|
from .utils import get_sha256_hash
|
|
@@ -91,9 +92,11 @@ def install_from_fab(
|
|
|
91
92
|
fab_name: Optional[str]
|
|
92
93
|
if isinstance(fab_file, bytes):
|
|
93
94
|
fab_file_archive = BytesIO(fab_file)
|
|
95
|
+
fab_hash = hashlib.sha256(fab_file).hexdigest()
|
|
94
96
|
fab_name = None
|
|
95
97
|
elif isinstance(fab_file, Path):
|
|
96
98
|
fab_file_archive = fab_file
|
|
99
|
+
fab_hash = hashlib.sha256(fab_file.read_bytes()).hexdigest()
|
|
97
100
|
fab_name = fab_file.stem
|
|
98
101
|
else:
|
|
99
102
|
raise ValueError("fab_file must be either a Path or bytes")
|
|
@@ -126,14 +129,16 @@ def install_from_fab(
|
|
|
126
129
|
shutil.rmtree(info_dir)
|
|
127
130
|
|
|
128
131
|
installed_path = validate_and_install(
|
|
129
|
-
tmpdir_path, fab_name, flwr_dir, skip_prompt
|
|
132
|
+
tmpdir_path, fab_hash, fab_name, flwr_dir, skip_prompt
|
|
130
133
|
)
|
|
131
134
|
|
|
132
135
|
return installed_path
|
|
133
136
|
|
|
134
137
|
|
|
138
|
+
# pylint: disable=too-many-locals
|
|
135
139
|
def validate_and_install(
|
|
136
140
|
project_dir: Path,
|
|
141
|
+
fab_hash: str,
|
|
137
142
|
fab_name: Optional[str],
|
|
138
143
|
flwr_dir: Optional[Path],
|
|
139
144
|
skip_prompt: bool = False,
|
|
@@ -149,28 +154,17 @@ def validate_and_install(
|
|
|
149
154
|
)
|
|
150
155
|
raise typer.Exit(code=1)
|
|
151
156
|
|
|
152
|
-
|
|
153
|
-
project_name =
|
|
154
|
-
|
|
157
|
+
version, fab_id = get_metadata_from_config(config)
|
|
158
|
+
publisher, project_name = fab_id.split("/")
|
|
159
|
+
config_metadata = (publisher, project_name, version, fab_hash)
|
|
155
160
|
|
|
156
|
-
if
|
|
157
|
-
fab_name
|
|
158
|
-
and fab_name != f"{publisher}.{project_name}.{version.replace('.', '-')}"
|
|
159
|
-
):
|
|
160
|
-
typer.secho(
|
|
161
|
-
"❌ FAB file has incorrect name. The file name must follow the format "
|
|
162
|
-
"`<publisher>.<project_name>.<version>.fab`.",
|
|
163
|
-
fg=typer.colors.RED,
|
|
164
|
-
bold=True,
|
|
165
|
-
)
|
|
166
|
-
raise typer.Exit(code=1)
|
|
161
|
+
if fab_name:
|
|
162
|
+
_validate_fab_and_config_metadata(fab_name, config_metadata)
|
|
167
163
|
|
|
168
164
|
install_dir: Path = (
|
|
169
165
|
(get_flwr_dir() if not flwr_dir else flwr_dir)
|
|
170
166
|
/ "apps"
|
|
171
|
-
/ publisher
|
|
172
|
-
/ project_name
|
|
173
|
-
/ version
|
|
167
|
+
/ f"{publisher}.{project_name}.{version}.{fab_hash[:FAB_HASH_TRUNCATION]}"
|
|
174
168
|
)
|
|
175
169
|
if install_dir.exists():
|
|
176
170
|
if skip_prompt:
|
|
@@ -226,3 +220,49 @@ def _verify_hashes(list_content: str, tmpdir: Path) -> bool:
|
|
|
226
220
|
if not file_path.exists() or get_sha256_hash(file_path) != hash_expected:
|
|
227
221
|
return False
|
|
228
222
|
return True
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _validate_fab_and_config_metadata(
|
|
226
|
+
fab_name: str, config_metadata: tuple[str, str, str, str]
|
|
227
|
+
) -> None:
|
|
228
|
+
"""Validate metadata from the FAB filename and config."""
|
|
229
|
+
publisher, project_name, version, fab_hash = config_metadata
|
|
230
|
+
|
|
231
|
+
fab_name = fab_name.removesuffix(".fab")
|
|
232
|
+
|
|
233
|
+
fab_publisher, fab_project_name, fab_version, fab_shorthash = fab_name.split(".")
|
|
234
|
+
fab_version = fab_version.replace("-", ".")
|
|
235
|
+
|
|
236
|
+
# Check FAB filename format
|
|
237
|
+
if (
|
|
238
|
+
f"{fab_publisher}.{fab_project_name}.{fab_version}"
|
|
239
|
+
!= f"{publisher}.{project_name}.{version}"
|
|
240
|
+
or len(fab_shorthash) != FAB_HASH_TRUNCATION # Verify hash length
|
|
241
|
+
):
|
|
242
|
+
typer.secho(
|
|
243
|
+
"❌ FAB file has incorrect name. The file name must follow the format "
|
|
244
|
+
"`<publisher>.<project_name>.<version>.<8hexchars>.fab`.",
|
|
245
|
+
fg=typer.colors.RED,
|
|
246
|
+
bold=True,
|
|
247
|
+
)
|
|
248
|
+
raise typer.Exit(code=1)
|
|
249
|
+
|
|
250
|
+
# Verify hash is a valid hexadecimal
|
|
251
|
+
try:
|
|
252
|
+
_ = int(fab_shorthash, 16)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
typer.secho(
|
|
255
|
+
f"❌ FAB file has an invalid hexadecimal string `{fab_shorthash}`.",
|
|
256
|
+
fg=typer.colors.RED,
|
|
257
|
+
bold=True,
|
|
258
|
+
)
|
|
259
|
+
raise typer.Exit(code=1) from e
|
|
260
|
+
|
|
261
|
+
# Verify shorthash matches
|
|
262
|
+
if fab_shorthash != fab_hash[:FAB_HASH_TRUNCATION]:
|
|
263
|
+
typer.secho(
|
|
264
|
+
"❌ The hash in the FAB file name does not match the hash of the FAB.",
|
|
265
|
+
fg=typer.colors.RED,
|
|
266
|
+
bold=True,
|
|
267
|
+
)
|
|
268
|
+
raise typer.Exit(code=1)
|
|
@@ -8,13 +8,13 @@ version = "1.0.0"
|
|
|
8
8
|
description = ""
|
|
9
9
|
license = "Apache-2.0"
|
|
10
10
|
dependencies = [
|
|
11
|
-
"flwr[simulation]>=1.
|
|
11
|
+
"flwr[simulation]>=1.12.0",
|
|
12
12
|
"flwr-datasets>=0.3.0",
|
|
13
13
|
"trl==0.8.1",
|
|
14
14
|
"bitsandbytes==0.43.0",
|
|
15
15
|
"scipy==1.13.0",
|
|
16
16
|
"peft==0.6.2",
|
|
17
|
-
"transformers==4.
|
|
17
|
+
"transformers==4.43.1",
|
|
18
18
|
"sentencepiece==0.2.0",
|
|
19
19
|
"omegaconf==2.3.0",
|
|
20
20
|
"hf_transfer==0.1.8",
|
flwr/cli/run/run.py
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Flower command line interface `run` command."""
|
|
16
16
|
|
|
17
|
-
import hashlib
|
|
18
17
|
import json
|
|
19
18
|
import subprocess
|
|
20
19
|
import sys
|
|
@@ -134,6 +133,7 @@ def run(
|
|
|
134
133
|
_run_without_superexec(app, federation_config, config_overrides, federation)
|
|
135
134
|
|
|
136
135
|
|
|
136
|
+
# pylint: disable=too-many-locals
|
|
137
137
|
def _run_with_superexec(
|
|
138
138
|
app: Path,
|
|
139
139
|
federation_config: dict[str, Any],
|
|
@@ -179,9 +179,9 @@ def _run_with_superexec(
|
|
|
179
179
|
channel.subscribe(on_channel_state_change)
|
|
180
180
|
stub = ExecStub(channel)
|
|
181
181
|
|
|
182
|
-
fab_path =
|
|
183
|
-
content = fab_path.read_bytes()
|
|
184
|
-
fab = Fab(
|
|
182
|
+
fab_path, fab_hash = build(app)
|
|
183
|
+
content = Path(fab_path).read_bytes()
|
|
184
|
+
fab = Fab(fab_hash, content)
|
|
185
185
|
|
|
186
186
|
req = StartRunRequest(
|
|
187
187
|
fab=fab_to_proto(fab),
|
|
@@ -193,7 +193,7 @@ def _run_with_superexec(
|
|
|
193
193
|
res = stub.StartRun(req)
|
|
194
194
|
|
|
195
195
|
# Delete FAB file once it has been sent to the SuperExec
|
|
196
|
-
fab_path.unlink()
|
|
196
|
+
Path(fab_path).unlink()
|
|
197
197
|
typer.secho(f"🎊 Successfully started run {res.run_id}", fg=typer.colors.GREEN)
|
|
198
198
|
|
|
199
199
|
if stream:
|
flwr/client/app.py
CHANGED
|
@@ -132,6 +132,11 @@ def start_client(
|
|
|
132
132
|
- 'grpc-bidi': gRPC, bidirectional streaming
|
|
133
133
|
- 'grpc-rere': gRPC, request-response (experimental)
|
|
134
134
|
- 'rest': HTTP (experimental)
|
|
135
|
+
authentication_keys : Optional[Tuple[PrivateKey, PublicKey]] (default: None)
|
|
136
|
+
Tuple containing the elliptic curve private key and public key for
|
|
137
|
+
authentication from the cryptography library.
|
|
138
|
+
Source: https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/
|
|
139
|
+
Used to establish an authenticated connection with the server.
|
|
135
140
|
max_retries: Optional[int] (default: None)
|
|
136
141
|
The maximum number of times the client will try to connect to the
|
|
137
142
|
server before giving up in case of a connection error. If set to None,
|
|
@@ -197,7 +202,7 @@ def start_client_internal(
|
|
|
197
202
|
*,
|
|
198
203
|
server_address: str,
|
|
199
204
|
node_config: UserConfig,
|
|
200
|
-
load_client_app_fn: Optional[Callable[[str, str], ClientApp]] = None,
|
|
205
|
+
load_client_app_fn: Optional[Callable[[str, str, str], ClientApp]] = None,
|
|
201
206
|
client_fn: Optional[ClientFnExt] = None,
|
|
202
207
|
client: Optional[Client] = None,
|
|
203
208
|
grpc_max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
|
|
@@ -249,6 +254,11 @@ def start_client_internal(
|
|
|
249
254
|
- 'grpc-bidi': gRPC, bidirectional streaming
|
|
250
255
|
- 'grpc-rere': gRPC, request-response (experimental)
|
|
251
256
|
- 'rest': HTTP (experimental)
|
|
257
|
+
authentication_keys : Optional[Tuple[PrivateKey, PublicKey]] (default: None)
|
|
258
|
+
Tuple containing the elliptic curve private key and public key for
|
|
259
|
+
authentication from the cryptography library.
|
|
260
|
+
Source: https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/
|
|
261
|
+
Used to establish an authenticated connection with the server.
|
|
252
262
|
max_retries: Optional[int] (default: None)
|
|
253
263
|
The maximum number of times the client will try to connect to the
|
|
254
264
|
server before giving up in case of a connection error. If set to None,
|
|
@@ -288,7 +298,7 @@ def start_client_internal(
|
|
|
288
298
|
|
|
289
299
|
client_fn = single_client_factory
|
|
290
300
|
|
|
291
|
-
def _load_client_app(_1: str, _2: str) -> ClientApp:
|
|
301
|
+
def _load_client_app(_1: str, _2: str, _3: str) -> ClientApp:
|
|
292
302
|
return ClientApp(client_fn=client_fn)
|
|
293
303
|
|
|
294
304
|
load_client_app_fn = _load_client_app
|
|
@@ -519,7 +529,7 @@ def start_client_internal(
|
|
|
519
529
|
else:
|
|
520
530
|
# Load ClientApp instance
|
|
521
531
|
client_app: ClientApp = load_client_app_fn(
|
|
522
|
-
fab_id, fab_version
|
|
532
|
+
fab_id, fab_version, run.fab_hash
|
|
523
533
|
)
|
|
524
534
|
|
|
525
535
|
# Execute ClientApp
|
flwr/client/clientapp/app.py
CHANGED
|
@@ -133,7 +133,9 @@ def run_clientapp( # pylint: disable=R0914
|
|
|
133
133
|
|
|
134
134
|
try:
|
|
135
135
|
# Load ClientApp
|
|
136
|
-
client_app: ClientApp = load_client_app_fn(
|
|
136
|
+
client_app: ClientApp = load_client_app_fn(
|
|
137
|
+
run.fab_id, run.fab_version, fab.hash_str if fab else ""
|
|
138
|
+
)
|
|
137
139
|
|
|
138
140
|
# Execute ClientApp
|
|
139
141
|
reply_message = client_app(message=message, context=context)
|
flwr/client/clientapp/utils.py
CHANGED
|
@@ -34,7 +34,7 @@ def get_load_client_app_fn(
|
|
|
34
34
|
app_path: Optional[str],
|
|
35
35
|
multi_app: bool,
|
|
36
36
|
flwr_dir: Optional[str] = None,
|
|
37
|
-
) -> Callable[[str, str], ClientApp]:
|
|
37
|
+
) -> Callable[[str, str, str], ClientApp]:
|
|
38
38
|
"""Get the load_client_app_fn function.
|
|
39
39
|
|
|
40
40
|
If `multi_app` is True, this function loads the specified ClientApp
|
|
@@ -55,13 +55,14 @@ def get_load_client_app_fn(
|
|
|
55
55
|
if not valid and error_msg:
|
|
56
56
|
raise LoadClientAppError(error_msg) from None
|
|
57
57
|
|
|
58
|
-
def _load(fab_id: str, fab_version: str) -> ClientApp:
|
|
58
|
+
def _load(fab_id: str, fab_version: str, fab_hash: str) -> ClientApp:
|
|
59
59
|
runtime_app_dir = Path(app_path if app_path else "").absolute()
|
|
60
60
|
# If multi-app feature is disabled
|
|
61
61
|
if not multi_app:
|
|
62
62
|
# Set app reference
|
|
63
63
|
client_app_ref = default_app_ref
|
|
64
|
-
# If multi-app feature is enabled but app directory is provided
|
|
64
|
+
# If multi-app feature is enabled but app directory is provided.
|
|
65
|
+
# `fab_hash` is not required since the app is loaded from `runtime_app_dir`.
|
|
65
66
|
elif app_path is not None:
|
|
66
67
|
config = get_project_config(runtime_app_dir)
|
|
67
68
|
this_fab_version, this_fab_id = get_metadata_from_config(config)
|
|
@@ -81,11 +82,16 @@ def get_load_client_app_fn(
|
|
|
81
82
|
else:
|
|
82
83
|
try:
|
|
83
84
|
runtime_app_dir = get_project_dir(
|
|
84
|
-
fab_id, fab_version, get_flwr_dir(flwr_dir)
|
|
85
|
+
fab_id, fab_version, fab_hash, get_flwr_dir(flwr_dir)
|
|
85
86
|
)
|
|
86
87
|
config = get_project_config(runtime_app_dir)
|
|
87
88
|
except Exception as e:
|
|
88
|
-
raise LoadClientAppError(
|
|
89
|
+
raise LoadClientAppError(
|
|
90
|
+
"Failed to load ClientApp."
|
|
91
|
+
"Possible reasons for error include mismatched "
|
|
92
|
+
"`fab_id`, `fab_version`, or `fab_hash` in "
|
|
93
|
+
f"{str(get_flwr_dir(flwr_dir).resolve())}."
|
|
94
|
+
) from e
|
|
89
95
|
|
|
90
96
|
# Set app reference
|
|
91
97
|
client_app_ref = config["tool"]["flwr"]["app"]["components"]["clientapp"]
|
|
@@ -60,7 +60,7 @@ def on_channel_state_change(channel_connectivity: str) -> None:
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
@contextmanager
|
|
63
|
-
def grpc_connection( # pylint: disable=R0913,
|
|
63
|
+
def grpc_connection( # pylint: disable=R0913,R0915,too-many-positional-arguments
|
|
64
64
|
server_address: str,
|
|
65
65
|
insecure: bool,
|
|
66
66
|
retry_invoker: RetryInvoker, # pylint: disable=unused-argument
|
|
@@ -71,7 +71,7 @@ def on_channel_state_change(channel_connectivity: str) -> None:
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
@contextmanager
|
|
74
|
-
def grpc_request_response( # pylint: disable=R0913,
|
|
74
|
+
def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
75
75
|
server_address: str,
|
|
76
76
|
insecure: bool,
|
|
77
77
|
retry_invoker: RetryInvoker,
|
|
@@ -120,6 +120,9 @@ def grpc_request_response( # pylint: disable=R0913, R0914, R0915
|
|
|
120
120
|
authentication from the cryptography library.
|
|
121
121
|
Source: https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/
|
|
122
122
|
Used to establish an authenticated connection with the server.
|
|
123
|
+
adapter_cls: Optional[Union[type[FleetStub], type[GrpcAdapter]]] (default: None)
|
|
124
|
+
A GrpcStub Class that can be used to send messages. By default the FleetStub
|
|
125
|
+
will be used.
|
|
123
126
|
|
|
124
127
|
Returns
|
|
125
128
|
-------
|
flwr/client/node_state.py
CHANGED
|
@@ -82,7 +82,7 @@ T = TypeVar("T", bound=GrpcMessage)
|
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
@contextmanager
|
|
85
|
-
def http_request_response( # pylint: disable
|
|
85
|
+
def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
86
86
|
server_address: str,
|
|
87
87
|
insecure: bool, # pylint: disable=unused-argument
|
|
88
88
|
retry_invoker: RetryInvoker,
|
flwr/common/config.py
CHANGED
|
@@ -22,7 +22,12 @@ from typing import Any, Optional, Union, cast, get_args
|
|
|
22
22
|
import tomli
|
|
23
23
|
|
|
24
24
|
from flwr.cli.config_utils import get_fab_config, validate_fields
|
|
25
|
-
from flwr.common.constant import
|
|
25
|
+
from flwr.common.constant import (
|
|
26
|
+
APP_DIR,
|
|
27
|
+
FAB_CONFIG_FILE,
|
|
28
|
+
FAB_HASH_TRUNCATION,
|
|
29
|
+
FLWR_HOME,
|
|
30
|
+
)
|
|
26
31
|
from flwr.common.typing import Run, UserConfig, UserConfigValue
|
|
27
32
|
|
|
28
33
|
|
|
@@ -39,7 +44,10 @@ def get_flwr_dir(provided_path: Optional[str] = None) -> Path:
|
|
|
39
44
|
|
|
40
45
|
|
|
41
46
|
def get_project_dir(
|
|
42
|
-
fab_id: str,
|
|
47
|
+
fab_id: str,
|
|
48
|
+
fab_version: str,
|
|
49
|
+
fab_hash: str,
|
|
50
|
+
flwr_dir: Optional[Union[str, Path]] = None,
|
|
43
51
|
) -> Path:
|
|
44
52
|
"""Return the project directory based on the given fab_id and fab_version."""
|
|
45
53
|
# Check the fab_id
|
|
@@ -50,7 +58,11 @@ def get_project_dir(
|
|
|
50
58
|
publisher, project_name = fab_id.split("/")
|
|
51
59
|
if flwr_dir is None:
|
|
52
60
|
flwr_dir = get_flwr_dir()
|
|
53
|
-
return
|
|
61
|
+
return (
|
|
62
|
+
Path(flwr_dir)
|
|
63
|
+
/ APP_DIR
|
|
64
|
+
/ f"{publisher}.{project_name}.{fab_version}.{fab_hash[:FAB_HASH_TRUNCATION]}"
|
|
65
|
+
)
|
|
54
66
|
|
|
55
67
|
|
|
56
68
|
def get_project_config(project_dir: Union[str, Path]) -> dict[str, Any]:
|
|
@@ -127,7 +139,7 @@ def get_fused_config(run: Run, flwr_dir: Optional[Path]) -> UserConfig:
|
|
|
127
139
|
if not run.fab_id or not run.fab_version:
|
|
128
140
|
return {}
|
|
129
141
|
|
|
130
|
-
project_dir = get_project_dir(run.fab_id, run.fab_version, flwr_dir)
|
|
142
|
+
project_dir = get_project_dir(run.fab_id, run.fab_version, run.fab_hash, flwr_dir)
|
|
131
143
|
|
|
132
144
|
# Return empty dict if project directory does not exist
|
|
133
145
|
if not project_dir.is_dir():
|
|
@@ -194,6 +206,7 @@ def parse_config_args(
|
|
|
194
206
|
# Regular expression to capture key-value pairs with possible quoted values
|
|
195
207
|
pattern = re.compile(r"(\S+?)=(\'[^\']*\'|\"[^\"]*\"|\S+)")
|
|
196
208
|
|
|
209
|
+
flat_overrides = {}
|
|
197
210
|
for config_line in config:
|
|
198
211
|
if config_line:
|
|
199
212
|
# .toml files aren't allowed alongside other configs
|
|
@@ -205,8 +218,9 @@ def parse_config_args(
|
|
|
205
218
|
matches = pattern.findall(config_line)
|
|
206
219
|
toml_str = "\n".join(f"{k} = {v}" for k, v in matches)
|
|
207
220
|
overrides.update(tomli.loads(toml_str))
|
|
221
|
+
flat_overrides = flatten_dict(overrides)
|
|
208
222
|
|
|
209
|
-
return
|
|
223
|
+
return flat_overrides
|
|
210
224
|
|
|
211
225
|
|
|
212
226
|
def get_metadata_from_config(config: dict[str, Any]) -> tuple[str, str]:
|