flwr-nightly 1.10.0.dev20240612__py3-none-any.whl → 1.10.0.dev20240614__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 +38 -1
- flwr/client/app.py +18 -10
- flwr/client/supernode/app.py +141 -38
- flwr/common/config.py +28 -0
- flwr/common/constant.py +2 -0
- flwr/common/telemetry.py +4 -0
- flwr/proto/exec_pb2.py +30 -0
- flwr/proto/exec_pb2.pyi +32 -0
- flwr/proto/exec_pb2_grpc.py +67 -0
- flwr/proto/exec_pb2_grpc.pyi +27 -0
- flwr/server/app.py +15 -18
- 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 +54 -0
- flwr/superexec/executor.py +54 -0
- {flwr_nightly-1.10.0.dev20240612.dist-info → flwr_nightly-1.10.0.dev20240614.dist-info}/METADATA +1 -1
- {flwr_nightly-1.10.0.dev20240612.dist-info → flwr_nightly-1.10.0.dev20240614.dist-info}/RECORD +25 -15
- {flwr_nightly-1.10.0.dev20240612.dist-info → flwr_nightly-1.10.0.dev20240614.dist-info}/entry_points.txt +1 -0
- {flwr_nightly-1.10.0.dev20240612.dist-info → flwr_nightly-1.10.0.dev20240614.dist-info}/LICENSE +0 -0
- {flwr_nightly-1.10.0.dev20240612.dist-info → flwr_nightly-1.10.0.dev20240614.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
44
|
typer.Option(case_sensitive=False, help="The ML framework to use"),
|
|
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/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
|
|
@@ -177,7 +177,7 @@ def start_client(
|
|
|
177
177
|
def _start_client_internal(
|
|
178
178
|
*,
|
|
179
179
|
server_address: str,
|
|
180
|
-
load_client_app_fn: Optional[Callable[[], ClientApp]] = None,
|
|
180
|
+
load_client_app_fn: Optional[Callable[[str, str], ClientApp]] = None,
|
|
181
181
|
client_fn: Optional[ClientFn] = None,
|
|
182
182
|
client: Optional[Client] = None,
|
|
183
183
|
grpc_max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
|
|
@@ -252,7 +252,7 @@ def _start_client_internal(
|
|
|
252
252
|
|
|
253
253
|
client_fn = single_client_factory
|
|
254
254
|
|
|
255
|
-
def _load_client_app() -> ClientApp:
|
|
255
|
+
def _load_client_app(_1: str, _2: str) -> ClientApp:
|
|
256
256
|
return ClientApp(client_fn=client_fn)
|
|
257
257
|
|
|
258
258
|
load_client_app_fn = _load_client_app
|
|
@@ -308,6 +308,8 @@ def _start_client_internal(
|
|
|
308
308
|
)
|
|
309
309
|
|
|
310
310
|
node_state = NodeState()
|
|
311
|
+
# run_id -> (fab_id, fab_version)
|
|
312
|
+
run_info: Dict[int, Tuple[str, str]] = {}
|
|
311
313
|
|
|
312
314
|
while not app_state_tracker.interrupt:
|
|
313
315
|
sleep_duration: int = 0
|
|
@@ -319,7 +321,6 @@ def _start_client_internal(
|
|
|
319
321
|
root_certificates,
|
|
320
322
|
authentication_keys,
|
|
321
323
|
) as conn:
|
|
322
|
-
# pylint: disable-next=W0612
|
|
323
324
|
receive, send, create_node, delete_node, get_run = conn
|
|
324
325
|
|
|
325
326
|
# Register node
|
|
@@ -356,13 +357,20 @@ def _start_client_internal(
|
|
|
356
357
|
send(out_message)
|
|
357
358
|
break
|
|
358
359
|
|
|
360
|
+
# Get run info
|
|
361
|
+
run_id = message.metadata.run_id
|
|
362
|
+
if run_id not in run_info:
|
|
363
|
+
if get_run is not None:
|
|
364
|
+
run_info[run_id] = get_run(run_id)
|
|
365
|
+
# If get_run is None, i.e., in grpc-bidi mode
|
|
366
|
+
else:
|
|
367
|
+
run_info[run_id] = ("", "")
|
|
368
|
+
|
|
359
369
|
# Register context for this run
|
|
360
|
-
node_state.register_context(run_id=
|
|
370
|
+
node_state.register_context(run_id=run_id)
|
|
361
371
|
|
|
362
372
|
# Retrieve context for this run
|
|
363
|
-
context = node_state.retrieve_context(
|
|
364
|
-
run_id=message.metadata.run_id
|
|
365
|
-
)
|
|
373
|
+
context = node_state.retrieve_context(run_id=run_id)
|
|
366
374
|
|
|
367
375
|
# Create an error reply message that will never be used to prevent
|
|
368
376
|
# the used-before-assignment linting error
|
|
@@ -373,7 +381,7 @@ def _start_client_internal(
|
|
|
373
381
|
# Handle app loading and task message
|
|
374
382
|
try:
|
|
375
383
|
# Load ClientApp instance
|
|
376
|
-
client_app: ClientApp = load_client_app_fn()
|
|
384
|
+
client_app: ClientApp = load_client_app_fn(*run_info[run_id])
|
|
377
385
|
|
|
378
386
|
# Execute ClientApp
|
|
379
387
|
reply_message = client_app(message=message, context=context)
|
|
@@ -411,7 +419,7 @@ def _start_client_internal(
|
|
|
411
419
|
else:
|
|
412
420
|
# No exception, update node state
|
|
413
421
|
node_state.update_context(
|
|
414
|
-
run_id=
|
|
422
|
+
run_id=run_id,
|
|
415
423
|
context=context,
|
|
416
424
|
)
|
|
417
425
|
|
flwr/client/supernode/app.py
CHANGED
|
@@ -20,6 +20,7 @@ from logging import DEBUG, INFO, WARN
|
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
from typing import Callable, Optional, Tuple
|
|
22
22
|
|
|
23
|
+
import tomli
|
|
23
24
|
from cryptography.exceptions import UnsupportedAlgorithm
|
|
24
25
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
25
26
|
from cryptography.hazmat.primitives.serialization import (
|
|
@@ -27,8 +28,10 @@ from cryptography.hazmat.primitives.serialization import (
|
|
|
27
28
|
load_ssh_public_key,
|
|
28
29
|
)
|
|
29
30
|
|
|
31
|
+
from flwr.cli.config_utils import validate_fields
|
|
30
32
|
from flwr.client.client_app import ClientApp, LoadClientAppError
|
|
31
33
|
from flwr.common import EventType, event
|
|
34
|
+
from flwr.common.config import get_flwr_dir
|
|
32
35
|
from flwr.common.exit_handlers import register_exit_handlers
|
|
33
36
|
from flwr.common.logger import log, warn_deprecated_feature
|
|
34
37
|
from flwr.common.object_ref import load_app, validate
|
|
@@ -44,11 +47,23 @@ def run_supernode() -> None:
|
|
|
44
47
|
|
|
45
48
|
event(EventType.RUN_SUPERNODE_ENTER)
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
args = _parse_args_run_supernode().parse_args()
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
_warn_deprecated_server_arg(args)
|
|
53
|
+
|
|
54
|
+
root_certificates = _get_certificates(args)
|
|
55
|
+
load_fn = _get_load_client_app_fn(args, multi_app=True)
|
|
56
|
+
authentication_keys = _try_setup_client_authentication(args)
|
|
57
|
+
|
|
58
|
+
_start_client_internal(
|
|
59
|
+
server_address=args.server,
|
|
60
|
+
load_client_app_fn=load_fn,
|
|
61
|
+
transport="rest" if args.rest else "grpc-rere",
|
|
62
|
+
root_certificates=root_certificates,
|
|
63
|
+
insecure=args.insecure,
|
|
64
|
+
authentication_keys=authentication_keys,
|
|
65
|
+
max_retries=args.max_retries,
|
|
66
|
+
max_wait_time=args.max_wait_time,
|
|
52
67
|
)
|
|
53
68
|
|
|
54
69
|
# Graceful shutdown
|
|
@@ -65,6 +80,27 @@ def run_client_app() -> None:
|
|
|
65
80
|
|
|
66
81
|
args = _parse_args_run_client_app().parse_args()
|
|
67
82
|
|
|
83
|
+
_warn_deprecated_server_arg(args)
|
|
84
|
+
|
|
85
|
+
root_certificates = _get_certificates(args)
|
|
86
|
+
load_fn = _get_load_client_app_fn(args, multi_app=False)
|
|
87
|
+
authentication_keys = _try_setup_client_authentication(args)
|
|
88
|
+
|
|
89
|
+
_start_client_internal(
|
|
90
|
+
server_address=args.superlink,
|
|
91
|
+
load_client_app_fn=load_fn,
|
|
92
|
+
transport="rest" if args.rest else "grpc-rere",
|
|
93
|
+
root_certificates=root_certificates,
|
|
94
|
+
insecure=args.insecure,
|
|
95
|
+
authentication_keys=authentication_keys,
|
|
96
|
+
max_retries=args.max_retries,
|
|
97
|
+
max_wait_time=args.max_wait_time,
|
|
98
|
+
)
|
|
99
|
+
register_exit_handlers(event_type=EventType.RUN_CLIENT_APP_LEAVE)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _warn_deprecated_server_arg(args: argparse.Namespace) -> None:
|
|
103
|
+
"""Warn about the deprecated argument `--server`."""
|
|
68
104
|
if args.server != ADDRESS_FLEET_API_GRPC_RERE:
|
|
69
105
|
warn = "Passing flag --server is deprecated. Use --superlink instead."
|
|
70
106
|
warn_deprecated_feature(warn)
|
|
@@ -82,27 +118,6 @@ def run_client_app() -> None:
|
|
|
82
118
|
else:
|
|
83
119
|
args.superlink = args.server
|
|
84
120
|
|
|
85
|
-
root_certificates = _get_certificates(args)
|
|
86
|
-
log(
|
|
87
|
-
DEBUG,
|
|
88
|
-
"Flower will load ClientApp `%s`",
|
|
89
|
-
getattr(args, "client-app"),
|
|
90
|
-
)
|
|
91
|
-
load_fn = _get_load_client_app_fn(args)
|
|
92
|
-
authentication_keys = _try_setup_client_authentication(args)
|
|
93
|
-
|
|
94
|
-
_start_client_internal(
|
|
95
|
-
server_address=args.superlink,
|
|
96
|
-
load_client_app_fn=load_fn,
|
|
97
|
-
transport="rest" if args.rest else "grpc-rere",
|
|
98
|
-
root_certificates=root_certificates,
|
|
99
|
-
insecure=args.insecure,
|
|
100
|
-
authentication_keys=authentication_keys,
|
|
101
|
-
max_retries=args.max_retries,
|
|
102
|
-
max_wait_time=args.max_wait_time,
|
|
103
|
-
)
|
|
104
|
-
register_exit_handlers(event_type=EventType.RUN_CLIENT_APP_LEAVE)
|
|
105
|
-
|
|
106
121
|
|
|
107
122
|
def _get_certificates(args: argparse.Namespace) -> Optional[bytes]:
|
|
108
123
|
"""Load certificates if specified in args."""
|
|
@@ -140,24 +155,112 @@ def _get_certificates(args: argparse.Namespace) -> Optional[bytes]:
|
|
|
140
155
|
|
|
141
156
|
|
|
142
157
|
def _get_load_client_app_fn(
|
|
143
|
-
args: argparse.Namespace,
|
|
144
|
-
) -> Callable[[], ClientApp]:
|
|
145
|
-
"""Get the load_client_app_fn function.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
158
|
+
args: argparse.Namespace, multi_app: bool
|
|
159
|
+
) -> Callable[[str, str], ClientApp]:
|
|
160
|
+
"""Get the load_client_app_fn function.
|
|
161
|
+
|
|
162
|
+
If `multi_app` is True, this function loads the specified ClientApp
|
|
163
|
+
based on `fab_id` and `fab_version`. If `fab_id` is empty, a default
|
|
164
|
+
ClientApp will be loaded.
|
|
165
|
+
|
|
166
|
+
If `multi_app` is False, it ignores `fab_id` and `fab_version` and
|
|
167
|
+
loads a default ClientApp.
|
|
168
|
+
"""
|
|
169
|
+
# Find the Flower directory containing Flower Apps (only for multi-app)
|
|
170
|
+
flwr_dir = Path("")
|
|
171
|
+
if "flwr_dir" in args:
|
|
172
|
+
if args.flwr_dir is None:
|
|
173
|
+
flwr_dir = get_flwr_dir()
|
|
174
|
+
else:
|
|
175
|
+
flwr_dir = Path(args.flwr_dir)
|
|
176
|
+
|
|
177
|
+
sys.path.insert(0, str(flwr_dir))
|
|
149
178
|
|
|
150
|
-
|
|
151
|
-
valid, error_msg = validate(app_ref)
|
|
152
|
-
if not valid and error_msg:
|
|
153
|
-
raise LoadClientAppError(error_msg) from None
|
|
179
|
+
default_app_ref: str = getattr(args, "client-app")
|
|
154
180
|
|
|
155
|
-
|
|
156
|
-
|
|
181
|
+
if not multi_app:
|
|
182
|
+
log(
|
|
183
|
+
DEBUG,
|
|
184
|
+
"Flower SuperNode will load and validate ClientApp `%s`",
|
|
185
|
+
getattr(args, "client-app"),
|
|
186
|
+
)
|
|
187
|
+
valid, error_msg = validate(default_app_ref)
|
|
188
|
+
if not valid and error_msg:
|
|
189
|
+
raise LoadClientAppError(error_msg) from None
|
|
190
|
+
|
|
191
|
+
def _load(fab_id: str, fab_version: str) -> ClientApp:
|
|
192
|
+
# If multi-app feature is disabled
|
|
193
|
+
if not multi_app:
|
|
194
|
+
# Set sys.path
|
|
195
|
+
sys.path[0] = args.dir
|
|
196
|
+
|
|
197
|
+
# Set app reference
|
|
198
|
+
client_app_ref = default_app_ref
|
|
199
|
+
# If multi-app feature is enabled but the fab id is not specified
|
|
200
|
+
elif fab_id == "":
|
|
201
|
+
if default_app_ref == "":
|
|
202
|
+
raise LoadClientAppError(
|
|
203
|
+
"Invalid FAB ID: The FAB ID is empty.",
|
|
204
|
+
) from None
|
|
205
|
+
|
|
206
|
+
log(WARN, "FAB ID is not provided; the default ClientApp will be loaded.")
|
|
207
|
+
# Set sys.path
|
|
208
|
+
sys.path[0] = args.dir
|
|
209
|
+
|
|
210
|
+
# Set app reference
|
|
211
|
+
client_app_ref = default_app_ref
|
|
212
|
+
# If multi-app feature is enabled
|
|
213
|
+
else:
|
|
214
|
+
# Check the fab_id
|
|
215
|
+
if fab_id.count("/") != 1:
|
|
216
|
+
raise LoadClientAppError(
|
|
217
|
+
f"Invalid FAB ID: {fab_id}",
|
|
218
|
+
) from None
|
|
219
|
+
username, project_name = fab_id.split("/")
|
|
220
|
+
|
|
221
|
+
# Locate the directory
|
|
222
|
+
project_dir = flwr_dir / "apps" / username / project_name / fab_version
|
|
223
|
+
|
|
224
|
+
# Check if the directory exists
|
|
225
|
+
if not project_dir.exists():
|
|
226
|
+
raise LoadClientAppError(
|
|
227
|
+
f"Invalid Flower App directory: {project_dir}",
|
|
228
|
+
) from None
|
|
229
|
+
|
|
230
|
+
# Load pyproject.toml file
|
|
231
|
+
toml_path = project_dir / "pyproject.toml"
|
|
232
|
+
if not toml_path.is_file():
|
|
233
|
+
raise LoadClientAppError(
|
|
234
|
+
f"Cannot find pyproject.toml in {project_dir}",
|
|
235
|
+
) from None
|
|
236
|
+
with open(toml_path, encoding="utf-8") as toml_file:
|
|
237
|
+
config = tomli.loads(toml_file.read())
|
|
238
|
+
|
|
239
|
+
# Validate pyproject.toml fields
|
|
240
|
+
is_valid, errors, _ = validate_fields(config)
|
|
241
|
+
if not is_valid:
|
|
242
|
+
error_msg = "\n".join([f" - {error}" for error in errors])
|
|
243
|
+
raise LoadClientAppError(
|
|
244
|
+
f"Invalid pyproject.toml:\n{error_msg}",
|
|
245
|
+
) from None
|
|
246
|
+
|
|
247
|
+
# Set sys.path
|
|
248
|
+
sys.path[0] = str(project_dir)
|
|
249
|
+
|
|
250
|
+
# Set app reference
|
|
251
|
+
client_app_ref = config["flower"]["components"]["clientapp"]
|
|
252
|
+
|
|
253
|
+
# Load ClientApp
|
|
254
|
+
log(
|
|
255
|
+
DEBUG,
|
|
256
|
+
"Loading ClientApp `%s`",
|
|
257
|
+
client_app_ref,
|
|
258
|
+
)
|
|
259
|
+
client_app = load_app(client_app_ref, LoadClientAppError)
|
|
157
260
|
|
|
158
261
|
if not isinstance(client_app, ClientApp):
|
|
159
262
|
raise LoadClientAppError(
|
|
160
|
-
f"Attribute {
|
|
263
|
+
f"Attribute {client_app_ref} is not of type {ClientApp}",
|
|
161
264
|
) from None
|
|
162
265
|
|
|
163
266
|
return client_app
|
flwr/common/config.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
"""Provide functions for managing global Flower config."""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_flwr_dir() -> Path:
|
|
22
|
+
"""Return the Flower home directory based on env variables."""
|
|
23
|
+
return Path(
|
|
24
|
+
os.getenv(
|
|
25
|
+
"FLWR_HOME",
|
|
26
|
+
f"{os.getenv('XDG_DATA_HOME', os.getenv('HOME'))}/.flwr",
|
|
27
|
+
)
|
|
28
|
+
)
|
flwr/common/constant.py
CHANGED
flwr/common/telemetry.py
CHANGED
|
@@ -164,6 +164,10 @@ class EventType(str, Enum):
|
|
|
164
164
|
RUN_SUPERNODE_ENTER = auto()
|
|
165
165
|
RUN_SUPERNODE_LEAVE = auto()
|
|
166
166
|
|
|
167
|
+
# SuperExec
|
|
168
|
+
RUN_SUPEREXEC_ENTER = auto()
|
|
169
|
+
RUN_SUPEREXEC_LEAVE = auto()
|
|
170
|
+
|
|
167
171
|
|
|
168
172
|
# Use the ThreadPoolExecutor with max_workers=1 to have a queue
|
|
169
173
|
# and also ensure that telemetry calls are not blocking.
|
flwr/proto/exec_pb2.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: flwr/proto/exec.proto
|
|
4
|
+
# Protobuf Python Version: 4.25.0
|
|
5
|
+
"""Generated protocol buffer code."""
|
|
6
|
+
from google.protobuf import descriptor as _descriptor
|
|
7
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
8
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
9
|
+
from google.protobuf.internal import builder as _builder
|
|
10
|
+
# @@protoc_insertion_point(imports)
|
|
11
|
+
|
|
12
|
+
_sym_db = _symbol_database.Default()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x66lwr/proto/exec.proto\x12\nflwr.proto\"#\n\x0fStartRunRequest\x12\x10\n\x08\x66\x61\x62_file\x18\x01 \x01(\x0c\"\"\n\x10StartRunResponse\x12\x0e\n\x06run_id\x18\x01 \x01(\x12\x32O\n\x04\x45xec\x12G\n\x08StartRun\x12\x1b.flwr.proto.StartRunRequest\x1a\x1c.flwr.proto.StartRunResponse\"\x00\x62\x06proto3')
|
|
18
|
+
|
|
19
|
+
_globals = globals()
|
|
20
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
21
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'flwr.proto.exec_pb2', _globals)
|
|
22
|
+
if _descriptor._USE_C_DESCRIPTORS == False:
|
|
23
|
+
DESCRIPTOR._options = None
|
|
24
|
+
_globals['_STARTRUNREQUEST']._serialized_start=37
|
|
25
|
+
_globals['_STARTRUNREQUEST']._serialized_end=72
|
|
26
|
+
_globals['_STARTRUNRESPONSE']._serialized_start=74
|
|
27
|
+
_globals['_STARTRUNRESPONSE']._serialized_end=108
|
|
28
|
+
_globals['_EXEC']._serialized_start=110
|
|
29
|
+
_globals['_EXEC']._serialized_end=189
|
|
30
|
+
# @@protoc_insertion_point(module_scope)
|