flwr-nightly 1.15.0.dev20250104__py3-none-any.whl → 1.15.0.dev20250123__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.
- flwr/cli/cli_user_auth_interceptor.py +6 -2
- flwr/cli/config_utils.py +23 -146
- flwr/cli/constant.py +27 -0
- flwr/cli/install.py +1 -1
- flwr/cli/log.py +17 -2
- flwr/cli/login/login.py +20 -5
- flwr/cli/ls.py +10 -2
- flwr/cli/run/run.py +20 -10
- flwr/cli/stop.py +9 -1
- flwr/cli/utils.py +4 -4
- flwr/client/app.py +36 -48
- flwr/client/clientapp/app.py +4 -6
- flwr/client/clientapp/utils.py +1 -1
- flwr/client/grpc_client/connection.py +0 -6
- flwr/client/grpc_rere_client/client_interceptor.py +19 -119
- flwr/client/grpc_rere_client/connection.py +34 -24
- flwr/client/grpc_rere_client/grpc_adapter.py +16 -0
- flwr/client/rest_client/connection.py +34 -26
- flwr/client/supernode/app.py +14 -20
- flwr/common/auth_plugin/auth_plugin.py +34 -23
- flwr/common/config.py +152 -15
- flwr/common/constant.py +11 -8
- flwr/common/exit/__init__.py +24 -0
- flwr/common/exit/exit.py +99 -0
- flwr/common/exit/exit_code.py +93 -0
- flwr/common/exit_handlers.py +24 -10
- flwr/common/grpc.py +161 -3
- flwr/common/logger.py +1 -1
- flwr/common/secure_aggregation/crypto/symmetric_encryption.py +45 -0
- flwr/common/serde.py +6 -4
- flwr/common/typing.py +20 -0
- flwr/proto/clientappio_pb2.py +13 -3
- flwr/proto/clientappio_pb2_grpc.py +63 -12
- flwr/proto/error_pb2.py +13 -3
- flwr/proto/error_pb2_grpc.py +20 -0
- flwr/proto/exec_pb2.py +27 -29
- flwr/proto/exec_pb2.pyi +27 -54
- flwr/proto/exec_pb2_grpc.py +105 -24
- flwr/proto/fab_pb2.py +13 -3
- flwr/proto/fab_pb2_grpc.py +20 -0
- flwr/proto/fleet_pb2.py +54 -31
- flwr/proto/fleet_pb2.pyi +84 -0
- flwr/proto/fleet_pb2_grpc.py +207 -28
- flwr/proto/fleet_pb2_grpc.pyi +26 -0
- flwr/proto/grpcadapter_pb2.py +14 -4
- flwr/proto/grpcadapter_pb2_grpc.py +35 -4
- flwr/proto/log_pb2.py +13 -3
- flwr/proto/log_pb2_grpc.py +20 -0
- flwr/proto/message_pb2.py +15 -5
- flwr/proto/message_pb2_grpc.py +20 -0
- flwr/proto/node_pb2.py +15 -5
- flwr/proto/node_pb2.pyi +1 -4
- flwr/proto/node_pb2_grpc.py +20 -0
- flwr/proto/recordset_pb2.py +18 -8
- flwr/proto/recordset_pb2_grpc.py +20 -0
- flwr/proto/run_pb2.py +16 -6
- flwr/proto/run_pb2_grpc.py +20 -0
- flwr/proto/serverappio_pb2.py +32 -14
- flwr/proto/serverappio_pb2.pyi +56 -0
- flwr/proto/serverappio_pb2_grpc.py +261 -44
- flwr/proto/serverappio_pb2_grpc.pyi +20 -0
- flwr/proto/simulationio_pb2.py +13 -3
- flwr/proto/simulationio_pb2_grpc.py +105 -24
- flwr/proto/task_pb2.py +13 -3
- flwr/proto/task_pb2_grpc.py +20 -0
- flwr/proto/transport_pb2.py +20 -10
- flwr/proto/transport_pb2_grpc.py +35 -4
- flwr/server/app.py +87 -38
- flwr/server/compat/app_utils.py +0 -1
- flwr/server/compat/driver_client_proxy.py +1 -2
- flwr/server/driver/grpc_driver.py +5 -2
- flwr/server/driver/inmemory_driver.py +2 -1
- flwr/server/serverapp/app.py +5 -6
- flwr/server/superlink/driver/serverappio_grpc.py +1 -1
- flwr/server/superlink/driver/serverappio_servicer.py +132 -14
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +20 -88
- flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +2 -165
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +38 -0
- flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +95 -168
- flwr/server/superlink/fleet/message_handler/message_handler.py +66 -5
- flwr/server/superlink/fleet/rest_rere/rest_api.py +28 -3
- flwr/server/superlink/fleet/vce/vce_api.py +2 -2
- flwr/server/superlink/linkstate/in_memory_linkstate.py +40 -48
- flwr/server/superlink/linkstate/linkstate.py +15 -22
- flwr/server/superlink/linkstate/sqlite_linkstate.py +80 -99
- flwr/server/superlink/linkstate/utils.py +18 -8
- flwr/server/superlink/simulation/simulationio_grpc.py +1 -1
- flwr/server/utils/validator.py +9 -34
- flwr/simulation/app.py +4 -6
- flwr/simulation/legacy_app.py +4 -2
- flwr/simulation/run_simulation.py +1 -1
- flwr/superexec/exec_grpc.py +1 -1
- flwr/superexec/exec_servicer.py +23 -2
- {flwr_nightly-1.15.0.dev20250104.dist-info → flwr_nightly-1.15.0.dev20250123.dist-info}/METADATA +7 -7
- {flwr_nightly-1.15.0.dev20250104.dist-info → flwr_nightly-1.15.0.dev20250123.dist-info}/RECORD +98 -94
- {flwr_nightly-1.15.0.dev20250104.dist-info → flwr_nightly-1.15.0.dev20250123.dist-info}/LICENSE +0 -0
- {flwr_nightly-1.15.0.dev20250104.dist-info → flwr_nightly-1.15.0.dev20250123.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.15.0.dev20250104.dist-info → flwr_nightly-1.15.0.dev20250123.dist-info}/entry_points.txt +0 -0
@@ -16,7 +16,6 @@
|
|
16
16
|
|
17
17
|
|
18
18
|
import random
|
19
|
-
import sys
|
20
19
|
import threading
|
21
20
|
from collections.abc import Iterator
|
22
21
|
from contextlib import contextmanager
|
@@ -26,22 +25,22 @@ from typing import Callable, Optional, TypeVar, Union
|
|
26
25
|
|
27
26
|
from cryptography.hazmat.primitives.asymmetric import ec
|
28
27
|
from google.protobuf.message import Message as GrpcMessage
|
28
|
+
from requests.exceptions import ConnectionError as RequestsConnectionError
|
29
29
|
|
30
30
|
from flwr.client.heartbeat import start_ping_loop
|
31
31
|
from flwr.client.message_handler.message_handler import validate_out_message
|
32
|
-
from flwr.client.message_handler.task_handler import get_task_ins, validate_task_ins
|
33
32
|
from flwr.common import GRPC_MAX_MESSAGE_LENGTH
|
34
33
|
from flwr.common.constant import (
|
35
|
-
MISSING_EXTRA_REST,
|
36
34
|
PING_BASE_MULTIPLIER,
|
37
35
|
PING_CALL_TIMEOUT,
|
38
36
|
PING_DEFAULT_INTERVAL,
|
39
37
|
PING_RANDOM_RANGE,
|
40
38
|
)
|
39
|
+
from flwr.common.exit import ExitCode, flwr_exit
|
41
40
|
from flwr.common.logger import log
|
42
41
|
from flwr.common.message import Message, Metadata
|
43
42
|
from flwr.common.retry_invoker import RetryInvoker
|
44
|
-
from flwr.common.serde import
|
43
|
+
from flwr.common.serde import message_from_proto, message_to_proto, run_from_proto
|
45
44
|
from flwr.common.typing import Fab, Run
|
46
45
|
from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
|
47
46
|
from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
|
@@ -51,25 +50,26 @@ from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
|
|
51
50
|
DeleteNodeResponse,
|
52
51
|
PingRequest,
|
53
52
|
PingResponse,
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
53
|
+
PullMessagesRequest,
|
54
|
+
PullMessagesResponse,
|
55
|
+
PushMessagesRequest,
|
56
|
+
PushMessagesResponse,
|
58
57
|
)
|
59
58
|
from flwr.proto.node_pb2 import Node # pylint: disable=E0611
|
60
59
|
from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611
|
61
|
-
from flwr.proto.task_pb2 import TaskIns # pylint: disable=E0611
|
62
60
|
|
63
61
|
try:
|
64
62
|
import requests
|
65
63
|
except ModuleNotFoundError:
|
66
|
-
|
64
|
+
flwr_exit(ExitCode.COMMON_MISSING_EXTRA_REST)
|
67
65
|
|
68
66
|
|
69
67
|
PATH_CREATE_NODE: str = "api/v0/fleet/create-node"
|
70
68
|
PATH_DELETE_NODE: str = "api/v0/fleet/delete-node"
|
71
69
|
PATH_PULL_TASK_INS: str = "api/v0/fleet/pull-task-ins"
|
70
|
+
PATH_PULL_MESSAGES: str = "/api/v0/fleet/pull-messages"
|
72
71
|
PATH_PUSH_TASK_RES: str = "api/v0/fleet/push-task-res"
|
72
|
+
PATH_PUSH_MESSAGES: str = "/api/v0/fleet/push-messages"
|
73
73
|
PATH_PING: str = "api/v0/fleet/ping"
|
74
74
|
PATH_GET_RUN: str = "/api/v0/fleet/get-run"
|
75
75
|
PATH_GET_FAB: str = "/api/v0/fleet/get-fab"
|
@@ -286,29 +286,28 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
286
286
|
log(ERROR, "Node instance missing")
|
287
287
|
return None
|
288
288
|
|
289
|
-
# Request instructions (
|
290
|
-
req =
|
289
|
+
# Request instructions (message) from server
|
290
|
+
req = PullMessagesRequest(node=node)
|
291
291
|
|
292
292
|
# Send the request
|
293
|
-
res = _request(req,
|
293
|
+
res = _request(req, PullMessagesResponse, PATH_PULL_MESSAGES)
|
294
294
|
if res is None:
|
295
295
|
return None
|
296
296
|
|
297
|
-
# Get the current
|
298
|
-
|
297
|
+
# Get the current Messages
|
298
|
+
message_proto = None if len(res.messages_list) == 0 else res.messages_list[0]
|
299
299
|
|
300
|
-
# Discard the current
|
301
|
-
if
|
302
|
-
|
303
|
-
and validate_task_ins(task_ins)
|
300
|
+
# Discard the current message if not valid
|
301
|
+
if message_proto is not None and not (
|
302
|
+
message_proto.metadata.dst_node_id == node.node_id
|
304
303
|
):
|
305
|
-
|
304
|
+
message_proto = None
|
306
305
|
|
307
306
|
# Return the Message if available
|
308
307
|
nonlocal metadata
|
309
308
|
message = None
|
310
|
-
if
|
311
|
-
message =
|
309
|
+
if message_proto is not None:
|
310
|
+
message = message_from_proto(message_proto)
|
312
311
|
metadata = copy(message.metadata)
|
313
312
|
log(INFO, "[Node] POST /%s: success", PATH_PULL_TASK_INS)
|
314
313
|
return message
|
@@ -332,14 +331,14 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
332
331
|
return
|
333
332
|
metadata = None
|
334
333
|
|
335
|
-
#
|
336
|
-
|
334
|
+
# Serialize ProtoBuf to bytes
|
335
|
+
message_proto = message_to_proto(message=message)
|
337
336
|
|
338
337
|
# Serialize ProtoBuf to bytes
|
339
|
-
req =
|
338
|
+
req = PushMessagesRequest(node=node, messages_list=[message_proto])
|
340
339
|
|
341
340
|
# Send the request
|
342
|
-
res = _request(req,
|
341
|
+
res = _request(req, PushMessagesResponse, PATH_PUSH_MESSAGES)
|
343
342
|
if res is None:
|
344
343
|
return
|
345
344
|
|
@@ -380,3 +379,12 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
380
379
|
yield (receive, send, create_node, delete_node, get_run, get_fab)
|
381
380
|
except Exception as exc: # pylint: disable=broad-except
|
382
381
|
log(ERROR, exc)
|
382
|
+
# Cleanup
|
383
|
+
finally:
|
384
|
+
try:
|
385
|
+
if node is not None:
|
386
|
+
# Disable retrying
|
387
|
+
retry_invoker.max_tries = 1
|
388
|
+
delete_node()
|
389
|
+
except RequestsConnectionError:
|
390
|
+
pass
|
flwr/client/supernode/app.py
CHANGED
@@ -40,6 +40,7 @@ from flwr.common.constant import (
|
|
40
40
|
TRANSPORT_TYPE_GRPC_RERE,
|
41
41
|
TRANSPORT_TYPE_REST,
|
42
42
|
)
|
43
|
+
from flwr.common.exit import ExitCode, flwr_exit
|
43
44
|
from flwr.common.exit_handlers import register_exit_handlers
|
44
45
|
from flwr.common.logger import log, warn_deprecated_feature
|
45
46
|
|
@@ -86,6 +87,12 @@ def run_supernode() -> None:
|
|
86
87
|
|
87
88
|
log(DEBUG, "Isolation mode: %s", args.isolation)
|
88
89
|
|
90
|
+
# Register handlers for graceful shutdown
|
91
|
+
register_exit_handlers(
|
92
|
+
event_type=EventType.RUN_SUPERNODE_LEAVE,
|
93
|
+
exit_message="SuperNode terminated gracefully.",
|
94
|
+
)
|
95
|
+
|
89
96
|
start_client_internal(
|
90
97
|
server_address=args.superlink,
|
91
98
|
load_client_app_fn=load_fn,
|
@@ -103,11 +110,6 @@ def run_supernode() -> None:
|
|
103
110
|
clientappio_api_address=args.clientappio_api_address,
|
104
111
|
)
|
105
112
|
|
106
|
-
# Graceful shutdown
|
107
|
-
register_exit_handlers(
|
108
|
-
event_type=EventType.RUN_SUPERNODE_LEAVE,
|
109
|
-
)
|
110
|
-
|
111
113
|
|
112
114
|
def run_client_app() -> None:
|
113
115
|
"""Run Flower client app."""
|
@@ -280,11 +282,7 @@ def _try_setup_client_authentication(
|
|
280
282
|
return None
|
281
283
|
|
282
284
|
if not args.auth_supernode_private_key or not args.auth_supernode_public_key:
|
283
|
-
|
284
|
-
"Authentication requires file paths to both "
|
285
|
-
"'--auth-supernode-private-key' and '--auth-supernode-public-key'"
|
286
|
-
"to be provided (providing only one of them is not sufficient)."
|
287
|
-
)
|
285
|
+
flwr_exit(ExitCode.SUPERNODE_NODE_AUTH_KEYS_REQUIRED)
|
288
286
|
|
289
287
|
try:
|
290
288
|
ssh_private_key = load_ssh_private_key(
|
@@ -294,11 +292,9 @@ def _try_setup_client_authentication(
|
|
294
292
|
if not isinstance(ssh_private_key, ec.EllipticCurvePrivateKey):
|
295
293
|
raise ValueError()
|
296
294
|
except (ValueError, UnsupportedAlgorithm):
|
297
|
-
|
298
|
-
|
299
|
-
"
|
300
|
-
"curve private and public key pair. Please ensure that the file "
|
301
|
-
"path points to a valid private key file and try again."
|
295
|
+
flwr_exit(
|
296
|
+
ExitCode.SUPERNODE_NODE_AUTH_KEYS_INVALID,
|
297
|
+
"Unable to parse the private key file.",
|
302
298
|
)
|
303
299
|
|
304
300
|
try:
|
@@ -308,11 +304,9 @@ def _try_setup_client_authentication(
|
|
308
304
|
if not isinstance(ssh_public_key, ec.EllipticCurvePublicKey):
|
309
305
|
raise ValueError()
|
310
306
|
except (ValueError, UnsupportedAlgorithm):
|
311
|
-
|
312
|
-
|
313
|
-
"
|
314
|
-
"curve private and public key pair. Please ensure that the file "
|
315
|
-
"path points to a valid public key file and try again."
|
307
|
+
flwr_exit(
|
308
|
+
ExitCode.SUPERNODE_NODE_AUTH_KEYS_INVALID,
|
309
|
+
"Unable to parse the public key file.",
|
316
310
|
)
|
317
311
|
|
318
312
|
return (
|
@@ -18,26 +18,32 @@
|
|
18
18
|
from abc import ABC, abstractmethod
|
19
19
|
from collections.abc import Sequence
|
20
20
|
from pathlib import Path
|
21
|
-
from typing import
|
21
|
+
from typing import Optional, Union
|
22
22
|
|
23
23
|
from flwr.proto.exec_pb2_grpc import ExecStub
|
24
24
|
|
25
|
+
from ..typing import UserAuthCredentials, UserAuthLoginDetails
|
26
|
+
|
25
27
|
|
26
28
|
class ExecAuthPlugin(ABC):
|
27
29
|
"""Abstract Flower Auth Plugin class for ExecServicer.
|
28
30
|
|
29
31
|
Parameters
|
30
32
|
----------
|
31
|
-
|
32
|
-
|
33
|
+
user_auth_config_path : Path
|
34
|
+
Path to the YAML file containing the authentication configuration.
|
33
35
|
"""
|
34
36
|
|
35
37
|
@abstractmethod
|
36
|
-
def __init__(
|
38
|
+
def __init__(
|
39
|
+
self,
|
40
|
+
user_auth_config_path: Path,
|
41
|
+
verify_tls_cert: bool,
|
42
|
+
):
|
37
43
|
"""Abstract constructor."""
|
38
44
|
|
39
45
|
@abstractmethod
|
40
|
-
def get_login_details(self) ->
|
46
|
+
def get_login_details(self) -> Optional[UserAuthLoginDetails]:
|
41
47
|
"""Get the login details."""
|
42
48
|
|
43
49
|
@abstractmethod
|
@@ -47,7 +53,7 @@ class ExecAuthPlugin(ABC):
|
|
47
53
|
"""Validate authentication tokens in the provided metadata."""
|
48
54
|
|
49
55
|
@abstractmethod
|
50
|
-
def get_auth_tokens(self,
|
56
|
+
def get_auth_tokens(self, device_code: str) -> Optional[UserAuthCredentials]:
|
51
57
|
"""Get authentication tokens."""
|
52
58
|
|
53
59
|
@abstractmethod
|
@@ -62,50 +68,55 @@ class CliAuthPlugin(ABC):
|
|
62
68
|
|
63
69
|
Parameters
|
64
70
|
----------
|
65
|
-
|
66
|
-
|
71
|
+
credentials_path : Path
|
72
|
+
Path to the user's authentication credentials file.
|
67
73
|
"""
|
68
74
|
|
69
75
|
@staticmethod
|
70
76
|
@abstractmethod
|
71
77
|
def login(
|
72
|
-
login_details:
|
78
|
+
login_details: UserAuthLoginDetails,
|
73
79
|
exec_stub: ExecStub,
|
74
|
-
) ->
|
75
|
-
"""Authenticate the user
|
80
|
+
) -> UserAuthCredentials:
|
81
|
+
"""Authenticate the user and retrieve authentication credentials.
|
76
82
|
|
77
83
|
Parameters
|
78
84
|
----------
|
79
|
-
login_details :
|
80
|
-
|
85
|
+
login_details : UserAuthLoginDetails
|
86
|
+
An object containing the user's login details.
|
81
87
|
exec_stub : ExecStub
|
82
|
-
|
88
|
+
A stub for executing RPC calls to the server.
|
83
89
|
|
84
90
|
Returns
|
85
91
|
-------
|
86
|
-
|
87
|
-
|
88
|
-
in JSON format.
|
92
|
+
UserAuthCredentials
|
93
|
+
The authentication credentials obtained after login.
|
89
94
|
"""
|
90
95
|
|
91
96
|
@abstractmethod
|
92
|
-
def __init__(self,
|
97
|
+
def __init__(self, credentials_path: Path):
|
93
98
|
"""Abstract constructor."""
|
94
99
|
|
95
100
|
@abstractmethod
|
96
|
-
def store_tokens(self,
|
97
|
-
"""Store authentication tokens
|
101
|
+
def store_tokens(self, credentials: UserAuthCredentials) -> None:
|
102
|
+
"""Store authentication tokens to the `credentials_path`.
|
98
103
|
|
99
|
-
The
|
100
|
-
at `
|
104
|
+
The credentials, including tokens, will be saved as a JSON file
|
105
|
+
at `credentials_path`.
|
101
106
|
"""
|
102
107
|
|
103
108
|
@abstractmethod
|
104
109
|
def load_tokens(self) -> None:
|
105
|
-
"""Load authentication tokens from the
|
110
|
+
"""Load authentication tokens from the `credentials_path`."""
|
106
111
|
|
107
112
|
@abstractmethod
|
108
113
|
def write_tokens_to_metadata(
|
109
114
|
self, metadata: Sequence[tuple[str, Union[str, bytes]]]
|
110
115
|
) -> Sequence[tuple[str, Union[str, bytes]]]:
|
111
116
|
"""Write authentication tokens to the provided metadata."""
|
117
|
+
|
118
|
+
@abstractmethod
|
119
|
+
def read_tokens_from_metadata(
|
120
|
+
self, metadata: Sequence[tuple[str, Union[str, bytes]]]
|
121
|
+
) -> Optional[UserAuthCredentials]:
|
122
|
+
"""Read authentication tokens from the provided metadata."""
|
flwr/common/config.py
CHANGED
@@ -17,13 +17,13 @@
|
|
17
17
|
|
18
18
|
import os
|
19
19
|
import re
|
20
|
+
import zipfile
|
21
|
+
from io import BytesIO
|
20
22
|
from pathlib import Path
|
21
|
-
from typing import Any, Optional, Union, cast, get_args
|
23
|
+
from typing import IO, Any, Optional, TypeVar, Union, cast, get_args
|
22
24
|
|
23
25
|
import tomli
|
24
26
|
|
25
|
-
from flwr.cli.config_utils import get_fab_config, validate_fields
|
26
|
-
from flwr.common import ConfigsRecord
|
27
27
|
from flwr.common.constant import (
|
28
28
|
APP_DIR,
|
29
29
|
FAB_CONFIG_FILE,
|
@@ -33,6 +33,10 @@ from flwr.common.constant import (
|
|
33
33
|
)
|
34
34
|
from flwr.common.typing import Run, UserConfig, UserConfigValue
|
35
35
|
|
36
|
+
from . import ConfigsRecord, object_ref
|
37
|
+
|
38
|
+
T_dict = TypeVar("T_dict", bound=dict[str, Any]) # pylint: disable=invalid-name
|
39
|
+
|
36
40
|
|
37
41
|
def get_flwr_dir(provided_path: Optional[str] = None) -> Path:
|
38
42
|
"""Return the Flower home directory based on env variables."""
|
@@ -80,7 +84,7 @@ def get_project_config(project_dir: Union[str, Path]) -> dict[str, Any]:
|
|
80
84
|
config = tomli.loads(toml_file.read())
|
81
85
|
|
82
86
|
# Validate pyproject.toml fields
|
83
|
-
is_valid, errors, _ =
|
87
|
+
is_valid, errors, _ = validate_fields_in_config(config)
|
84
88
|
if not is_valid:
|
85
89
|
error_msg = "\n".join([f" - {error}" for error in errors])
|
86
90
|
raise ValueError(
|
@@ -91,19 +95,28 @@ def get_project_config(project_dir: Union[str, Path]) -> dict[str, Any]:
|
|
91
95
|
|
92
96
|
|
93
97
|
def fuse_dicts(
|
94
|
-
main_dict:
|
95
|
-
override_dict:
|
96
|
-
|
98
|
+
main_dict: T_dict,
|
99
|
+
override_dict: T_dict,
|
100
|
+
check_keys: bool = True,
|
101
|
+
) -> T_dict:
|
97
102
|
"""Merge a config with the overrides.
|
98
103
|
|
99
|
-
|
100
|
-
|
104
|
+
If `check_keys` is set to True, an error will be raised if the override
|
105
|
+
dictionary contains keys that are not present in the main dictionary.
|
106
|
+
Otherwise, only the keys present in the main dictionary will be updated.
|
101
107
|
"""
|
102
|
-
|
108
|
+
if not isinstance(main_dict, dict) or not isinstance(override_dict, dict):
|
109
|
+
raise ValueError("Both dictionaries must be of type dict")
|
110
|
+
|
111
|
+
fused_dict = cast(T_dict, main_dict.copy())
|
103
112
|
|
104
113
|
for key, value in override_dict.items():
|
105
114
|
if key in main_dict:
|
115
|
+
if isinstance(value, dict):
|
116
|
+
fused_dict[key] = fuse_dicts(main_dict[key], value)
|
106
117
|
fused_dict[key] = value
|
118
|
+
elif check_keys:
|
119
|
+
raise ValueError(f"Key '{key}' is not present in the main dictionary")
|
107
120
|
|
108
121
|
return fused_dict
|
109
122
|
|
@@ -192,8 +205,8 @@ def unflatten_dict(flat_dict: dict[str, Any]) -> dict[str, Any]:
|
|
192
205
|
|
193
206
|
|
194
207
|
def parse_config_args(
|
195
|
-
config: Optional[list[str]],
|
196
|
-
) ->
|
208
|
+
config: Optional[list[str]], flatten: bool = True
|
209
|
+
) -> dict[str, Any]:
|
197
210
|
"""Parse separator separated list of key-value pairs separated by '='."""
|
198
211
|
overrides: UserConfig = {}
|
199
212
|
|
@@ -221,16 +234,16 @@ def parse_config_args(
|
|
221
234
|
matches = pattern.findall(config_line)
|
222
235
|
toml_str = "\n".join(f"{k} = {v}" for k, v in matches)
|
223
236
|
overrides.update(tomli.loads(toml_str))
|
224
|
-
flat_overrides = flatten_dict(overrides)
|
237
|
+
flat_overrides = flatten_dict(overrides) if flatten else overrides
|
225
238
|
|
226
239
|
return flat_overrides
|
227
240
|
|
228
241
|
|
229
242
|
def get_metadata_from_config(config: dict[str, Any]) -> tuple[str, str]:
|
230
|
-
"""Extract `
|
243
|
+
"""Extract `fab_id` and `fab_version` from a project config."""
|
231
244
|
return (
|
232
|
-
config["project"]["version"],
|
233
245
|
f"{config['tool']['flwr']['app']['publisher']}/{config['project']['name']}",
|
246
|
+
config["project"]["version"],
|
234
247
|
)
|
235
248
|
|
236
249
|
|
@@ -241,3 +254,127 @@ def user_config_to_configsrecord(config: UserConfig) -> ConfigsRecord:
|
|
241
254
|
c_record[k] = v
|
242
255
|
|
243
256
|
return c_record
|
257
|
+
|
258
|
+
|
259
|
+
def get_fab_config(fab_file: Union[Path, bytes]) -> dict[str, Any]:
|
260
|
+
"""Extract the config from a FAB file or path.
|
261
|
+
|
262
|
+
Parameters
|
263
|
+
----------
|
264
|
+
fab_file : Union[Path, bytes]
|
265
|
+
The Flower App Bundle file to validate and extract the metadata from.
|
266
|
+
It can either be a path to the file or the file itself as bytes.
|
267
|
+
|
268
|
+
Returns
|
269
|
+
-------
|
270
|
+
Dict[str, Any]
|
271
|
+
The `config` of the given Flower App Bundle.
|
272
|
+
"""
|
273
|
+
fab_file_archive: Union[Path, IO[bytes]]
|
274
|
+
if isinstance(fab_file, bytes):
|
275
|
+
fab_file_archive = BytesIO(fab_file)
|
276
|
+
elif isinstance(fab_file, Path):
|
277
|
+
fab_file_archive = fab_file
|
278
|
+
else:
|
279
|
+
raise ValueError("fab_file must be either a Path or bytes")
|
280
|
+
|
281
|
+
with zipfile.ZipFile(fab_file_archive, "r") as zipf:
|
282
|
+
with zipf.open("pyproject.toml") as file:
|
283
|
+
toml_content = file.read().decode("utf-8")
|
284
|
+
try:
|
285
|
+
conf = tomli.loads(toml_content)
|
286
|
+
except tomli.TOMLDecodeError:
|
287
|
+
raise ValueError("Invalid TOML content in pyproject.toml") from None
|
288
|
+
|
289
|
+
is_valid, errors, _ = validate_config(conf, check_module=False)
|
290
|
+
if not is_valid:
|
291
|
+
raise ValueError(errors)
|
292
|
+
|
293
|
+
return conf
|
294
|
+
|
295
|
+
|
296
|
+
def _validate_run_config(config_dict: dict[str, Any], errors: list[str]) -> None:
|
297
|
+
for key, value in config_dict.items():
|
298
|
+
if isinstance(value, dict):
|
299
|
+
_validate_run_config(config_dict[key], errors)
|
300
|
+
elif not isinstance(value, get_args(UserConfigValue)):
|
301
|
+
raise ValueError(
|
302
|
+
f"The value for key {key} needs to be of type `int`, `float`, "
|
303
|
+
"`bool, `str`, or a `dict` of those.",
|
304
|
+
)
|
305
|
+
|
306
|
+
|
307
|
+
# pylint: disable=too-many-branches
|
308
|
+
def validate_fields_in_config(
|
309
|
+
config: dict[str, Any]
|
310
|
+
) -> tuple[bool, list[str], list[str]]:
|
311
|
+
"""Validate pyproject.toml fields."""
|
312
|
+
errors = []
|
313
|
+
warnings = []
|
314
|
+
|
315
|
+
if "project" not in config:
|
316
|
+
errors.append("Missing [project] section")
|
317
|
+
else:
|
318
|
+
if "name" not in config["project"]:
|
319
|
+
errors.append('Property "name" missing in [project]')
|
320
|
+
if "version" not in config["project"]:
|
321
|
+
errors.append('Property "version" missing in [project]')
|
322
|
+
if "description" not in config["project"]:
|
323
|
+
warnings.append('Recommended property "description" missing in [project]')
|
324
|
+
if "license" not in config["project"]:
|
325
|
+
warnings.append('Recommended property "license" missing in [project]')
|
326
|
+
if "authors" not in config["project"]:
|
327
|
+
warnings.append('Recommended property "authors" missing in [project]')
|
328
|
+
|
329
|
+
if (
|
330
|
+
"tool" not in config
|
331
|
+
or "flwr" not in config["tool"]
|
332
|
+
or "app" not in config["tool"]["flwr"]
|
333
|
+
):
|
334
|
+
errors.append("Missing [tool.flwr.app] section")
|
335
|
+
else:
|
336
|
+
if "publisher" not in config["tool"]["flwr"]["app"]:
|
337
|
+
errors.append('Property "publisher" missing in [tool.flwr.app]')
|
338
|
+
if "config" in config["tool"]["flwr"]["app"]:
|
339
|
+
_validate_run_config(config["tool"]["flwr"]["app"]["config"], errors)
|
340
|
+
if "components" not in config["tool"]["flwr"]["app"]:
|
341
|
+
errors.append("Missing [tool.flwr.app.components] section")
|
342
|
+
else:
|
343
|
+
if "serverapp" not in config["tool"]["flwr"]["app"]["components"]:
|
344
|
+
errors.append(
|
345
|
+
'Property "serverapp" missing in [tool.flwr.app.components]'
|
346
|
+
)
|
347
|
+
if "clientapp" not in config["tool"]["flwr"]["app"]["components"]:
|
348
|
+
errors.append(
|
349
|
+
'Property "clientapp" missing in [tool.flwr.app.components]'
|
350
|
+
)
|
351
|
+
|
352
|
+
return len(errors) == 0, errors, warnings
|
353
|
+
|
354
|
+
|
355
|
+
def validate_config(
|
356
|
+
config: dict[str, Any],
|
357
|
+
check_module: bool = True,
|
358
|
+
project_dir: Optional[Union[str, Path]] = None,
|
359
|
+
) -> tuple[bool, list[str], list[str]]:
|
360
|
+
"""Validate pyproject.toml."""
|
361
|
+
is_valid, errors, warnings = validate_fields_in_config(config)
|
362
|
+
|
363
|
+
if not is_valid:
|
364
|
+
return False, errors, warnings
|
365
|
+
|
366
|
+
# Validate serverapp
|
367
|
+
serverapp_ref = config["tool"]["flwr"]["app"]["components"]["serverapp"]
|
368
|
+
is_valid, reason = object_ref.validate(serverapp_ref, check_module, project_dir)
|
369
|
+
|
370
|
+
if not is_valid and isinstance(reason, str):
|
371
|
+
return False, [reason], []
|
372
|
+
|
373
|
+
# Validate clientapp
|
374
|
+
clientapp_ref = config["tool"]["flwr"]["app"]["components"]["clientapp"]
|
375
|
+
is_valid, reason = object_ref.validate(clientapp_ref, check_module, project_dir)
|
376
|
+
|
377
|
+
if not is_valid and isinstance(reason, str):
|
378
|
+
return False, [reason], []
|
379
|
+
|
380
|
+
return True, [], []
|
flwr/common/constant.py
CHANGED
@@ -17,14 +17,6 @@
|
|
17
17
|
|
18
18
|
from __future__ import annotations
|
19
19
|
|
20
|
-
MISSING_EXTRA_REST = """
|
21
|
-
Extra dependencies required for using the REST-based Fleet API are missing.
|
22
|
-
|
23
|
-
To use the REST API, install `flwr` with the `rest` extra:
|
24
|
-
|
25
|
-
`pip install flwr[rest]`.
|
26
|
-
"""
|
27
|
-
|
28
20
|
TRANSPORT_TYPE_GRPC_BIDI = "grpc-bidi"
|
29
21
|
TRANSPORT_TYPE_GRPC_RERE = "grpc-rere"
|
30
22
|
TRANSPORT_TYPE_GRPC_ADAPTER = "grpc-adapter"
|
@@ -83,6 +75,9 @@ FAB_HASH_TRUNCATION = 8
|
|
83
75
|
FLWR_DIR = ".flwr" # The default Flower directory: ~/.flwr/
|
84
76
|
FLWR_HOME = "FLWR_HOME" # If set, override the default Flower directory
|
85
77
|
|
78
|
+
# Constant for SuperLink
|
79
|
+
SUPERLINK_NODE_ID = 1
|
80
|
+
|
86
81
|
# Constants entries in Node config for Simulation
|
87
82
|
PARTITION_ID_KEY = "partition-id"
|
88
83
|
NUM_PARTITIONS_KEY = "num-partitions"
|
@@ -114,6 +109,14 @@ MAX_RETRY_DELAY = 20 # Maximum delay duration between two consecutive retries.
|
|
114
109
|
# Constants for user authentication
|
115
110
|
CREDENTIALS_DIR = ".credentials"
|
116
111
|
AUTH_TYPE = "auth_type"
|
112
|
+
ACCESS_TOKEN_KEY = "access_token"
|
113
|
+
REFRESH_TOKEN_KEY = "refresh_token"
|
114
|
+
|
115
|
+
# Constants for node authentication
|
116
|
+
PUBLIC_KEY_HEADER = "public-key-bin" # Must end with "-bin" for binary data
|
117
|
+
SIGNATURE_HEADER = "signature-bin" # Must end with "-bin" for binary data
|
118
|
+
TIMESTAMP_HEADER = "timestamp"
|
119
|
+
TIMESTAMP_TOLERANCE = 10 # Tolerance for timestamp verification
|
117
120
|
|
118
121
|
|
119
122
|
class MessageType:
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
# ==============================================================================
|
15
|
+
"""Flower exit functionality."""
|
16
|
+
|
17
|
+
|
18
|
+
from .exit import flwr_exit
|
19
|
+
from .exit_code import ExitCode
|
20
|
+
|
21
|
+
__all__ = [
|
22
|
+
"ExitCode",
|
23
|
+
"flwr_exit",
|
24
|
+
]
|