flwr-nightly 1.11.0.dev20240811__py3-none-any.whl → 1.11.0.dev20240821__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/config_utils.py +2 -2
- flwr/cli/install.py +3 -1
- flwr/cli/run/run.py +15 -11
- flwr/client/app.py +134 -15
- flwr/client/clientapp/__init__.py +22 -0
- flwr/client/clientapp/app.py +233 -0
- flwr/client/clientapp/clientappio_servicer.py +244 -0
- flwr/client/clientapp/utils.py +108 -0
- flwr/client/grpc_adapter_client/connection.py +3 -1
- flwr/client/grpc_client/connection.py +3 -2
- flwr/client/grpc_rere_client/connection.py +15 -2
- flwr/client/node_state.py +17 -4
- flwr/client/rest_client/connection.py +21 -3
- flwr/client/supernode/app.py +37 -97
- flwr/common/__init__.py +4 -0
- flwr/common/config.py +31 -10
- flwr/common/record/configsrecord.py +49 -15
- flwr/common/record/metricsrecord.py +54 -14
- flwr/common/record/parametersrecord.py +84 -17
- flwr/common/record/recordset.py +80 -8
- flwr/common/record/typeddict.py +20 -58
- flwr/common/recordset_compat.py +6 -6
- flwr/common/serde.py +178 -1
- flwr/common/typing.py +17 -0
- flwr/proto/clientappio_pb2.py +45 -0
- flwr/proto/clientappio_pb2.pyi +132 -0
- flwr/proto/clientappio_pb2_grpc.py +135 -0
- flwr/proto/clientappio_pb2_grpc.pyi +53 -0
- flwr/proto/exec_pb2.py +16 -15
- flwr/proto/exec_pb2.pyi +7 -4
- flwr/proto/message_pb2.py +41 -0
- flwr/proto/message_pb2.pyi +125 -0
- flwr/proto/message_pb2_grpc.py +4 -0
- flwr/proto/message_pb2_grpc.pyi +4 -0
- flwr/server/app.py +15 -0
- flwr/server/driver/grpc_driver.py +1 -0
- flwr/server/run_serverapp.py +18 -2
- flwr/server/server.py +3 -1
- flwr/server/superlink/driver/driver_grpc.py +3 -0
- flwr/server/superlink/driver/driver_servicer.py +32 -4
- flwr/server/superlink/ffs/__init__.py +24 -0
- flwr/server/superlink/ffs/disk_ffs.py +107 -0
- flwr/server/superlink/ffs/ffs.py +79 -0
- flwr/server/superlink/ffs/ffs_factory.py +47 -0
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +12 -4
- flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +8 -2
- flwr/server/superlink/fleet/message_handler/message_handler.py +16 -1
- flwr/server/superlink/fleet/vce/backend/raybackend.py +5 -2
- flwr/server/superlink/fleet/vce/vce_api.py +2 -2
- flwr/server/superlink/state/in_memory_state.py +7 -5
- flwr/server/superlink/state/sqlite_state.py +17 -7
- flwr/server/superlink/state/state.py +4 -3
- flwr/server/workflow/default_workflows.py +3 -1
- flwr/simulation/run_simulation.py +5 -67
- flwr/superexec/app.py +3 -3
- flwr/superexec/deployment.py +8 -9
- flwr/superexec/exec_servicer.py +1 -1
- {flwr_nightly-1.11.0.dev20240811.dist-info → flwr_nightly-1.11.0.dev20240821.dist-info}/METADATA +2 -2
- {flwr_nightly-1.11.0.dev20240811.dist-info → flwr_nightly-1.11.0.dev20240821.dist-info}/RECORD +62 -46
- {flwr_nightly-1.11.0.dev20240811.dist-info → flwr_nightly-1.11.0.dev20240821.dist-info}/entry_points.txt +1 -0
- {flwr_nightly-1.11.0.dev20240811.dist-info → flwr_nightly-1.11.0.dev20240821.dist-info}/LICENSE +0 -0
- {flwr_nightly-1.11.0.dev20240811.dist-info → flwr_nightly-1.11.0.dev20240821.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
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
|
+
"""ClientAppIo API servicer."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from logging import DEBUG, ERROR
|
|
20
|
+
from typing import Optional, cast
|
|
21
|
+
|
|
22
|
+
import grpc
|
|
23
|
+
|
|
24
|
+
from flwr.common import Context, Message, typing
|
|
25
|
+
from flwr.common.logger import log
|
|
26
|
+
from flwr.common.serde import (
|
|
27
|
+
clientappstatus_to_proto,
|
|
28
|
+
context_from_proto,
|
|
29
|
+
context_to_proto,
|
|
30
|
+
fab_to_proto,
|
|
31
|
+
message_from_proto,
|
|
32
|
+
message_to_proto,
|
|
33
|
+
run_to_proto,
|
|
34
|
+
)
|
|
35
|
+
from flwr.common.typing import Fab, Run
|
|
36
|
+
|
|
37
|
+
# pylint: disable=E0611
|
|
38
|
+
from flwr.proto import clientappio_pb2_grpc
|
|
39
|
+
from flwr.proto.clientappio_pb2 import ( # pylint: disable=E0401
|
|
40
|
+
GetTokenRequest,
|
|
41
|
+
GetTokenResponse,
|
|
42
|
+
PullClientAppInputsRequest,
|
|
43
|
+
PullClientAppInputsResponse,
|
|
44
|
+
PushClientAppOutputsRequest,
|
|
45
|
+
PushClientAppOutputsResponse,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class ClientAppInputs:
|
|
51
|
+
"""Specify the inputs to the ClientApp."""
|
|
52
|
+
|
|
53
|
+
message: Message
|
|
54
|
+
context: Context
|
|
55
|
+
run: Run
|
|
56
|
+
fab: Optional[Fab]
|
|
57
|
+
token: int
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class ClientAppOutputs:
|
|
62
|
+
"""Specify the outputs from the ClientApp."""
|
|
63
|
+
|
|
64
|
+
message: Message
|
|
65
|
+
context: Context
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# pylint: disable=C0103,W0613,W0201
|
|
69
|
+
class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
|
|
70
|
+
"""ClientAppIo API servicer."""
|
|
71
|
+
|
|
72
|
+
def __init__(self) -> None:
|
|
73
|
+
self.clientapp_input: Optional[ClientAppInputs] = None
|
|
74
|
+
self.clientapp_output: Optional[ClientAppOutputs] = None
|
|
75
|
+
self.token_returned: bool = False
|
|
76
|
+
self.inputs_returned: bool = False
|
|
77
|
+
|
|
78
|
+
def GetToken(
|
|
79
|
+
self, request: GetTokenRequest, context: grpc.ServicerContext
|
|
80
|
+
) -> GetTokenResponse:
|
|
81
|
+
"""Get token."""
|
|
82
|
+
log(DEBUG, "ClientAppIo.GetToken")
|
|
83
|
+
|
|
84
|
+
# Fail if no ClientAppInputs are available
|
|
85
|
+
if self.clientapp_input is None:
|
|
86
|
+
context.abort(
|
|
87
|
+
grpc.StatusCode.FAILED_PRECONDITION,
|
|
88
|
+
"No inputs available.",
|
|
89
|
+
)
|
|
90
|
+
clientapp_input = cast(ClientAppInputs, self.clientapp_input)
|
|
91
|
+
|
|
92
|
+
# Fail if token was already returned in a previous call
|
|
93
|
+
if self.token_returned:
|
|
94
|
+
context.abort(
|
|
95
|
+
grpc.StatusCode.FAILED_PRECONDITION,
|
|
96
|
+
"Token already returned. A token can be returned only once.",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# If
|
|
100
|
+
# - ClientAppInputs is set, and
|
|
101
|
+
# - token hasn't been returned before,
|
|
102
|
+
# return token
|
|
103
|
+
self.token_returned = True
|
|
104
|
+
return GetTokenResponse(token=clientapp_input.token)
|
|
105
|
+
|
|
106
|
+
def PullClientAppInputs(
|
|
107
|
+
self, request: PullClientAppInputsRequest, context: grpc.ServicerContext
|
|
108
|
+
) -> PullClientAppInputsResponse:
|
|
109
|
+
"""Pull Message, Context, and Run."""
|
|
110
|
+
log(DEBUG, "ClientAppIo.PullClientAppInputs")
|
|
111
|
+
|
|
112
|
+
# Fail if no ClientAppInputs are available
|
|
113
|
+
if self.clientapp_input is None:
|
|
114
|
+
context.abort(
|
|
115
|
+
grpc.StatusCode.FAILED_PRECONDITION,
|
|
116
|
+
"No inputs available.",
|
|
117
|
+
)
|
|
118
|
+
clientapp_input = cast(ClientAppInputs, self.clientapp_input)
|
|
119
|
+
|
|
120
|
+
# Fail if token wasn't returned in a previous call
|
|
121
|
+
if not self.token_returned:
|
|
122
|
+
context.abort(
|
|
123
|
+
grpc.StatusCode.FAILED_PRECONDITION,
|
|
124
|
+
"Token hasn't been returned."
|
|
125
|
+
"Token must be returned before can be returned only once.",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Fail if token isn't matching
|
|
129
|
+
if request.token != clientapp_input.token:
|
|
130
|
+
context.abort(
|
|
131
|
+
grpc.StatusCode.INVALID_ARGUMENT,
|
|
132
|
+
"Mismatch between ClientApp and SuperNode token",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Success
|
|
136
|
+
self.inputs_returned = True
|
|
137
|
+
return PullClientAppInputsResponse(
|
|
138
|
+
message=message_to_proto(clientapp_input.message),
|
|
139
|
+
context=context_to_proto(clientapp_input.context),
|
|
140
|
+
run=run_to_proto(clientapp_input.run),
|
|
141
|
+
fab=fab_to_proto(clientapp_input.fab) if clientapp_input.fab else None,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def PushClientAppOutputs(
|
|
145
|
+
self, request: PushClientAppOutputsRequest, context: grpc.ServicerContext
|
|
146
|
+
) -> PushClientAppOutputsResponse:
|
|
147
|
+
"""Push Message and Context."""
|
|
148
|
+
log(DEBUG, "ClientAppIo.PushClientAppOutputs")
|
|
149
|
+
|
|
150
|
+
# Fail if no ClientAppInputs are available
|
|
151
|
+
if not self.clientapp_input:
|
|
152
|
+
context.abort(
|
|
153
|
+
grpc.StatusCode.FAILED_PRECONDITION,
|
|
154
|
+
"No inputs available.",
|
|
155
|
+
)
|
|
156
|
+
clientapp_input = cast(ClientAppInputs, self.clientapp_input)
|
|
157
|
+
|
|
158
|
+
# Fail if token wasn't returned in a previous call
|
|
159
|
+
if not self.token_returned:
|
|
160
|
+
context.abort(
|
|
161
|
+
grpc.StatusCode.FAILED_PRECONDITION,
|
|
162
|
+
"Token hasn't been returned."
|
|
163
|
+
"Token must be returned before can be returned only once.",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Fail if inputs weren't delivered in a previous call
|
|
167
|
+
if not self.inputs_returned:
|
|
168
|
+
context.abort(
|
|
169
|
+
grpc.StatusCode.FAILED_PRECONDITION,
|
|
170
|
+
"Inputs haven't been delivered."
|
|
171
|
+
"Inputs must be delivered before can be returned only once.",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Fail if token isn't matching
|
|
175
|
+
if request.token != clientapp_input.token:
|
|
176
|
+
context.abort(
|
|
177
|
+
grpc.StatusCode.INVALID_ARGUMENT,
|
|
178
|
+
"Mismatch between ClientApp and SuperNode token",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Preconditions met
|
|
182
|
+
try:
|
|
183
|
+
# Update Message and Context
|
|
184
|
+
self.clientapp_output = ClientAppOutputs(
|
|
185
|
+
message=message_from_proto(request.message),
|
|
186
|
+
context=context_from_proto(request.context),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Set status
|
|
190
|
+
code = typing.ClientAppOutputCode.SUCCESS
|
|
191
|
+
status = typing.ClientAppOutputStatus(code=code, message="Success")
|
|
192
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
193
|
+
log(ERROR, "ClientApp failed to push message to SuperNode, %s", e)
|
|
194
|
+
code = typing.ClientAppOutputCode.UNKNOWN_ERROR
|
|
195
|
+
status = typing.ClientAppOutputStatus(code=code, message="Unkonwn error")
|
|
196
|
+
|
|
197
|
+
# Return status to ClientApp process
|
|
198
|
+
proto_status = clientappstatus_to_proto(status=status)
|
|
199
|
+
return PushClientAppOutputsResponse(status=proto_status)
|
|
200
|
+
|
|
201
|
+
def set_inputs(
|
|
202
|
+
self, clientapp_input: ClientAppInputs, token_returned: bool
|
|
203
|
+
) -> None:
|
|
204
|
+
"""Set ClientApp inputs.
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
clientapp_input : ClientAppInputs
|
|
209
|
+
The inputs to the ClientApp.
|
|
210
|
+
token_returned : bool
|
|
211
|
+
A boolean indicating if the token has been returned.
|
|
212
|
+
Set to `True` when passing the token to `flwr-clientap`
|
|
213
|
+
and `False` otherwise.
|
|
214
|
+
"""
|
|
215
|
+
if (
|
|
216
|
+
self.clientapp_input is not None
|
|
217
|
+
or self.clientapp_output is not None
|
|
218
|
+
or self.token_returned
|
|
219
|
+
):
|
|
220
|
+
raise ValueError(
|
|
221
|
+
"ClientAppInputs and ClientAppOutputs must not be set before "
|
|
222
|
+
"calling `set_inputs`."
|
|
223
|
+
)
|
|
224
|
+
log(DEBUG, "ClientAppInputs set (token: %s)", clientapp_input.token)
|
|
225
|
+
self.clientapp_input = clientapp_input
|
|
226
|
+
self.token_returned = token_returned
|
|
227
|
+
|
|
228
|
+
def has_outputs(self) -> bool:
|
|
229
|
+
"""Check if ClientAppOutputs are available."""
|
|
230
|
+
return self.clientapp_output is not None
|
|
231
|
+
|
|
232
|
+
def get_outputs(self) -> ClientAppOutputs:
|
|
233
|
+
"""Get ClientApp outputs."""
|
|
234
|
+
if self.clientapp_output is None:
|
|
235
|
+
raise ValueError("ClientAppOutputs not set before calling `get_outputs`.")
|
|
236
|
+
|
|
237
|
+
# Set outputs to a local variable and clear state
|
|
238
|
+
output: ClientAppOutputs = self.clientapp_output
|
|
239
|
+
self.clientapp_input = None
|
|
240
|
+
self.clientapp_output = None
|
|
241
|
+
self.token_returned = False
|
|
242
|
+
self.inputs_returned = False
|
|
243
|
+
|
|
244
|
+
return output
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
"""Flower ClientApp loading utils."""
|
|
16
|
+
|
|
17
|
+
from logging import DEBUG
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Callable, Optional
|
|
20
|
+
|
|
21
|
+
from flwr.client.client_app import ClientApp, LoadClientAppError
|
|
22
|
+
from flwr.common.config import (
|
|
23
|
+
get_flwr_dir,
|
|
24
|
+
get_metadata_from_config,
|
|
25
|
+
get_project_config,
|
|
26
|
+
get_project_dir,
|
|
27
|
+
)
|
|
28
|
+
from flwr.common.logger import log
|
|
29
|
+
from flwr.common.object_ref import load_app, validate
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_load_client_app_fn(
|
|
33
|
+
default_app_ref: str,
|
|
34
|
+
app_path: Optional[str],
|
|
35
|
+
multi_app: bool,
|
|
36
|
+
flwr_dir: Optional[str] = None,
|
|
37
|
+
) -> Callable[[str, str], ClientApp]:
|
|
38
|
+
"""Get the load_client_app_fn function.
|
|
39
|
+
|
|
40
|
+
If `multi_app` is True, this function loads the specified ClientApp
|
|
41
|
+
based on `fab_id` and `fab_version`. If `fab_id` is empty, a default
|
|
42
|
+
ClientApp will be loaded.
|
|
43
|
+
|
|
44
|
+
If `multi_app` is False, it ignores `fab_id` and `fab_version` and
|
|
45
|
+
loads a default ClientApp.
|
|
46
|
+
"""
|
|
47
|
+
if not multi_app:
|
|
48
|
+
log(
|
|
49
|
+
DEBUG,
|
|
50
|
+
"Flower SuperNode will load and validate ClientApp `%s`",
|
|
51
|
+
default_app_ref,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
valid, error_msg = validate(default_app_ref, project_dir=app_path)
|
|
55
|
+
if not valid and error_msg:
|
|
56
|
+
raise LoadClientAppError(error_msg) from None
|
|
57
|
+
|
|
58
|
+
def _load(fab_id: str, fab_version: str) -> ClientApp:
|
|
59
|
+
runtime_app_dir = Path(app_path if app_path else "").absolute()
|
|
60
|
+
# If multi-app feature is disabled
|
|
61
|
+
if not multi_app:
|
|
62
|
+
# Set app reference
|
|
63
|
+
client_app_ref = default_app_ref
|
|
64
|
+
# If multi-app feature is enabled but app directory is provided
|
|
65
|
+
elif app_path is not None:
|
|
66
|
+
config = get_project_config(runtime_app_dir)
|
|
67
|
+
this_fab_version, this_fab_id = get_metadata_from_config(config)
|
|
68
|
+
|
|
69
|
+
if this_fab_version != fab_version or this_fab_id != fab_id:
|
|
70
|
+
raise LoadClientAppError(
|
|
71
|
+
f"FAB ID or version mismatch: Expected FAB ID '{this_fab_id}' and "
|
|
72
|
+
f"FAB version '{this_fab_version}', but received FAB ID '{fab_id}' "
|
|
73
|
+
f"and FAB version '{fab_version}'.",
|
|
74
|
+
) from None
|
|
75
|
+
|
|
76
|
+
# log(WARN, "FAB ID is not provided; the default ClientApp will be loaded.")
|
|
77
|
+
|
|
78
|
+
# Set app reference
|
|
79
|
+
client_app_ref = config["tool"]["flwr"]["app"]["components"]["clientapp"]
|
|
80
|
+
# If multi-app feature is enabled
|
|
81
|
+
else:
|
|
82
|
+
try:
|
|
83
|
+
runtime_app_dir = get_project_dir(
|
|
84
|
+
fab_id, fab_version, get_flwr_dir(flwr_dir)
|
|
85
|
+
)
|
|
86
|
+
config = get_project_config(runtime_app_dir)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
raise LoadClientAppError("Failed to load ClientApp") from e
|
|
89
|
+
|
|
90
|
+
# Set app reference
|
|
91
|
+
client_app_ref = config["tool"]["flwr"]["app"]["components"]["clientapp"]
|
|
92
|
+
|
|
93
|
+
# Load ClientApp
|
|
94
|
+
log(
|
|
95
|
+
DEBUG,
|
|
96
|
+
"Loading ClientApp `%s`",
|
|
97
|
+
client_app_ref,
|
|
98
|
+
)
|
|
99
|
+
client_app = load_app(client_app_ref, LoadClientAppError, runtime_app_dir)
|
|
100
|
+
|
|
101
|
+
if not isinstance(client_app, ClientApp):
|
|
102
|
+
raise LoadClientAppError(
|
|
103
|
+
f"Attribute {client_app_ref} is not of type {ClientApp}",
|
|
104
|
+
) from None
|
|
105
|
+
|
|
106
|
+
return client_app
|
|
107
|
+
|
|
108
|
+
return _load
|
|
@@ -27,7 +27,7 @@ from flwr.common import GRPC_MAX_MESSAGE_LENGTH
|
|
|
27
27
|
from flwr.common.logger import log
|
|
28
28
|
from flwr.common.message import Message
|
|
29
29
|
from flwr.common.retry_invoker import RetryInvoker
|
|
30
|
-
from flwr.common.typing import Run
|
|
30
|
+
from flwr.common.typing import Fab, Run
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
@contextmanager
|
|
@@ -47,6 +47,7 @@ def grpc_adapter( # pylint: disable=R0913
|
|
|
47
47
|
Optional[Callable[[], Optional[int]]],
|
|
48
48
|
Optional[Callable[[], None]],
|
|
49
49
|
Optional[Callable[[int], Run]],
|
|
50
|
+
Optional[Callable[[str], Fab]],
|
|
50
51
|
]
|
|
51
52
|
]:
|
|
52
53
|
"""Primitives for request/response-based interaction with a server via GrpcAdapter.
|
|
@@ -80,6 +81,7 @@ def grpc_adapter( # pylint: disable=R0913
|
|
|
80
81
|
create_node : Optional[Callable]
|
|
81
82
|
delete_node : Optional[Callable]
|
|
82
83
|
get_run : Optional[Callable]
|
|
84
|
+
get_fab : Optional[Callable]
|
|
83
85
|
"""
|
|
84
86
|
if authentication_keys is not None:
|
|
85
87
|
log(ERROR, "Client authentication is not supported for this transport type.")
|
|
@@ -38,7 +38,7 @@ from flwr.common.constant import MessageType, MessageTypeLegacy
|
|
|
38
38
|
from flwr.common.grpc import create_channel
|
|
39
39
|
from flwr.common.logger import log
|
|
40
40
|
from flwr.common.retry_invoker import RetryInvoker
|
|
41
|
-
from flwr.common.typing import Run
|
|
41
|
+
from flwr.common.typing import Fab, Run
|
|
42
42
|
from flwr.proto.transport_pb2 import ( # pylint: disable=E0611
|
|
43
43
|
ClientMessage,
|
|
44
44
|
Reason,
|
|
@@ -75,6 +75,7 @@ def grpc_connection( # pylint: disable=R0913, R0915
|
|
|
75
75
|
Optional[Callable[[], Optional[int]]],
|
|
76
76
|
Optional[Callable[[], None]],
|
|
77
77
|
Optional[Callable[[int], Run]],
|
|
78
|
+
Optional[Callable[[str], Fab]],
|
|
78
79
|
]
|
|
79
80
|
]:
|
|
80
81
|
"""Establish a gRPC connection to a gRPC server.
|
|
@@ -235,7 +236,7 @@ def grpc_connection( # pylint: disable=R0913, R0915
|
|
|
235
236
|
|
|
236
237
|
try:
|
|
237
238
|
# Yield methods
|
|
238
|
-
yield (receive, send, None, None, None)
|
|
239
|
+
yield (receive, send, None, None, None, None)
|
|
239
240
|
finally:
|
|
240
241
|
# Make sure to have a final
|
|
241
242
|
channel.close()
|
|
@@ -45,7 +45,8 @@ from flwr.common.serde import (
|
|
|
45
45
|
message_to_taskres,
|
|
46
46
|
user_config_from_proto,
|
|
47
47
|
)
|
|
48
|
-
from flwr.common.typing import Run
|
|
48
|
+
from flwr.common.typing import Fab, Run
|
|
49
|
+
from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
|
|
49
50
|
from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
|
|
50
51
|
CreateNodeRequest,
|
|
51
52
|
DeleteNodeRequest,
|
|
@@ -86,6 +87,7 @@ def grpc_request_response( # pylint: disable=R0913, R0914, R0915
|
|
|
86
87
|
Optional[Callable[[], Optional[int]]],
|
|
87
88
|
Optional[Callable[[], None]],
|
|
88
89
|
Optional[Callable[[int], Run]],
|
|
90
|
+
Optional[Callable[[str], Fab]],
|
|
89
91
|
]
|
|
90
92
|
]:
|
|
91
93
|
"""Primitives for request/response-based interaction with a server.
|
|
@@ -285,11 +287,22 @@ def grpc_request_response( # pylint: disable=R0913, R0914, R0915
|
|
|
285
287
|
run_id,
|
|
286
288
|
get_run_response.run.fab_id,
|
|
287
289
|
get_run_response.run.fab_version,
|
|
290
|
+
get_run_response.run.fab_hash,
|
|
288
291
|
user_config_from_proto(get_run_response.run.override_config),
|
|
289
292
|
)
|
|
290
293
|
|
|
294
|
+
def get_fab(fab_hash: str) -> Fab:
|
|
295
|
+
# Call FleetAPI
|
|
296
|
+
get_fab_request = GetFabRequest(hash_str=fab_hash)
|
|
297
|
+
get_fab_response: GetFabResponse = retry_invoker.invoke(
|
|
298
|
+
stub.GetFab,
|
|
299
|
+
request=get_fab_request,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return Fab(get_fab_response.fab.hash_str, get_fab_response.fab.content)
|
|
303
|
+
|
|
291
304
|
try:
|
|
292
305
|
# Yield methods
|
|
293
|
-
yield (receive, send, create_node, delete_node, get_run)
|
|
306
|
+
yield (receive, send, create_node, delete_node, get_run, get_fab)
|
|
294
307
|
except Exception as exc: # pylint: disable=broad-except
|
|
295
308
|
log(ERROR, exc)
|
flwr/client/node_state.py
CHANGED
|
@@ -20,8 +20,12 @@ from pathlib import Path
|
|
|
20
20
|
from typing import Dict, Optional
|
|
21
21
|
|
|
22
22
|
from flwr.common import Context, RecordSet
|
|
23
|
-
from flwr.common.config import
|
|
24
|
-
|
|
23
|
+
from flwr.common.config import (
|
|
24
|
+
get_fused_config,
|
|
25
|
+
get_fused_config_from_dir,
|
|
26
|
+
get_fused_config_from_fab,
|
|
27
|
+
)
|
|
28
|
+
from flwr.common.typing import Fab, Run, UserConfig
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
@dataclass()
|
|
@@ -44,12 +48,14 @@ class NodeState:
|
|
|
44
48
|
self.node_config = node_config
|
|
45
49
|
self.run_infos: Dict[int, RunInfo] = {}
|
|
46
50
|
|
|
51
|
+
# pylint: disable=too-many-arguments
|
|
47
52
|
def register_context(
|
|
48
53
|
self,
|
|
49
54
|
run_id: int,
|
|
50
55
|
run: Optional[Run] = None,
|
|
51
56
|
flwr_path: Optional[Path] = None,
|
|
52
57
|
app_dir: Optional[str] = None,
|
|
58
|
+
fab: Optional[Fab] = None,
|
|
53
59
|
) -> None:
|
|
54
60
|
"""Register new run context for this node."""
|
|
55
61
|
if run_id not in self.run_infos:
|
|
@@ -65,8 +71,15 @@ class NodeState:
|
|
|
65
71
|
else:
|
|
66
72
|
raise ValueError("The specified `app_dir` must be a directory.")
|
|
67
73
|
else:
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
if run:
|
|
75
|
+
if fab:
|
|
76
|
+
# Load pyproject.toml from FAB file and fuse
|
|
77
|
+
initial_run_config = get_fused_config_from_fab(fab.content, run)
|
|
78
|
+
else:
|
|
79
|
+
# Load pyproject.toml from installed FAB and fuse
|
|
80
|
+
initial_run_config = get_fused_config(run, flwr_path)
|
|
81
|
+
else:
|
|
82
|
+
initial_run_config = {}
|
|
70
83
|
self.run_infos[run_id] = RunInfo(
|
|
71
84
|
initial_run_config=initial_run_config,
|
|
72
85
|
context=Context(
|
|
@@ -45,7 +45,8 @@ from flwr.common.serde import (
|
|
|
45
45
|
message_to_taskres,
|
|
46
46
|
user_config_from_proto,
|
|
47
47
|
)
|
|
48
|
-
from flwr.common.typing import Run
|
|
48
|
+
from flwr.common.typing import Fab, Run
|
|
49
|
+
from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
|
|
49
50
|
from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
|
|
50
51
|
CreateNodeRequest,
|
|
51
52
|
CreateNodeResponse,
|
|
@@ -74,6 +75,7 @@ PATH_PULL_TASK_INS: str = "api/v0/fleet/pull-task-ins"
|
|
|
74
75
|
PATH_PUSH_TASK_RES: str = "api/v0/fleet/push-task-res"
|
|
75
76
|
PATH_PING: str = "api/v0/fleet/ping"
|
|
76
77
|
PATH_GET_RUN: str = "/api/v0/fleet/get-run"
|
|
78
|
+
PATH_GET_FAB: str = "/api/v0/fleet/get-fab"
|
|
77
79
|
|
|
78
80
|
T = TypeVar("T", bound=GrpcMessage)
|
|
79
81
|
|
|
@@ -97,6 +99,7 @@ def http_request_response( # pylint: disable=,R0913, R0914, R0915
|
|
|
97
99
|
Optional[Callable[[], Optional[int]]],
|
|
98
100
|
Optional[Callable[[], None]],
|
|
99
101
|
Optional[Callable[[int], Run]],
|
|
102
|
+
Optional[Callable[[str], Fab]],
|
|
100
103
|
]
|
|
101
104
|
]:
|
|
102
105
|
"""Primitives for request/response-based interaction with a server.
|
|
@@ -357,17 +360,32 @@ def http_request_response( # pylint: disable=,R0913, R0914, R0915
|
|
|
357
360
|
# Send the request
|
|
358
361
|
res = _request(req, GetRunResponse, PATH_GET_RUN)
|
|
359
362
|
if res is None:
|
|
360
|
-
return Run(run_id, "", "", {})
|
|
363
|
+
return Run(run_id, "", "", "", {})
|
|
361
364
|
|
|
362
365
|
return Run(
|
|
363
366
|
run_id,
|
|
364
367
|
res.run.fab_id,
|
|
365
368
|
res.run.fab_version,
|
|
369
|
+
res.run.fab_hash,
|
|
366
370
|
user_config_from_proto(res.run.override_config),
|
|
367
371
|
)
|
|
368
372
|
|
|
373
|
+
def get_fab(fab_hash: str) -> Fab:
|
|
374
|
+
# Construct the request
|
|
375
|
+
req = GetFabRequest(hash_str=fab_hash)
|
|
376
|
+
|
|
377
|
+
# Send the request
|
|
378
|
+
res = _request(req, GetFabResponse, PATH_GET_FAB)
|
|
379
|
+
if res is None:
|
|
380
|
+
return Fab("", b"")
|
|
381
|
+
|
|
382
|
+
return Fab(
|
|
383
|
+
res.fab.hash_str,
|
|
384
|
+
res.fab.content,
|
|
385
|
+
)
|
|
386
|
+
|
|
369
387
|
try:
|
|
370
388
|
# Yield methods
|
|
371
|
-
yield (receive, send, create_node, delete_node, get_run)
|
|
389
|
+
yield (receive, send, create_node, delete_node, get_run, get_fab)
|
|
372
390
|
except Exception as exc: # pylint: disable=broad-except
|
|
373
391
|
log(ERROR, exc)
|