flwr-nightly 1.11.0.dev20240813__py3-none-any.whl → 1.11.0.dev20240822__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.

Files changed (58) hide show
  1. flwr/cli/config_utils.py +2 -2
  2. flwr/cli/install.py +3 -1
  3. flwr/cli/run/run.py +15 -11
  4. flwr/client/app.py +132 -14
  5. flwr/client/clientapp/__init__.py +22 -0
  6. flwr/client/clientapp/app.py +233 -0
  7. flwr/client/clientapp/clientappio_servicer.py +244 -0
  8. flwr/client/clientapp/utils.py +108 -0
  9. flwr/client/grpc_rere_client/connection.py +9 -1
  10. flwr/client/node_state.py +17 -4
  11. flwr/client/rest_client/connection.py +16 -3
  12. flwr/client/supernode/__init__.py +0 -2
  13. flwr/client/supernode/app.py +36 -164
  14. flwr/common/__init__.py +4 -0
  15. flwr/common/config.py +31 -10
  16. flwr/common/record/configsrecord.py +49 -15
  17. flwr/common/record/metricsrecord.py +54 -14
  18. flwr/common/record/parametersrecord.py +84 -17
  19. flwr/common/record/recordset.py +80 -8
  20. flwr/common/record/typeddict.py +20 -58
  21. flwr/common/recordset_compat.py +6 -6
  22. flwr/common/serde.py +24 -2
  23. flwr/common/typing.py +1 -0
  24. flwr/proto/clientappio_pb2.py +17 -13
  25. flwr/proto/clientappio_pb2.pyi +24 -2
  26. flwr/proto/clientappio_pb2_grpc.py +34 -0
  27. flwr/proto/clientappio_pb2_grpc.pyi +13 -0
  28. flwr/proto/exec_pb2.py +16 -15
  29. flwr/proto/exec_pb2.pyi +7 -4
  30. flwr/proto/message_pb2.py +2 -2
  31. flwr/proto/message_pb2.pyi +4 -1
  32. flwr/server/app.py +15 -0
  33. flwr/server/driver/grpc_driver.py +1 -0
  34. flwr/server/run_serverapp.py +18 -2
  35. flwr/server/server.py +3 -1
  36. flwr/server/superlink/driver/driver_grpc.py +3 -0
  37. flwr/server/superlink/driver/driver_servicer.py +32 -4
  38. flwr/server/superlink/ffs/disk_ffs.py +6 -3
  39. flwr/server/superlink/ffs/ffs.py +3 -3
  40. flwr/server/superlink/ffs/ffs_factory.py +47 -0
  41. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +12 -4
  42. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +8 -2
  43. flwr/server/superlink/fleet/message_handler/message_handler.py +16 -1
  44. flwr/server/superlink/fleet/vce/backend/raybackend.py +5 -2
  45. flwr/server/superlink/fleet/vce/vce_api.py +2 -2
  46. flwr/server/superlink/state/in_memory_state.py +7 -5
  47. flwr/server/superlink/state/sqlite_state.py +17 -7
  48. flwr/server/superlink/state/state.py +4 -3
  49. flwr/server/workflow/default_workflows.py +3 -1
  50. flwr/simulation/run_simulation.py +5 -67
  51. flwr/superexec/app.py +3 -3
  52. flwr/superexec/deployment.py +8 -9
  53. flwr/superexec/exec_servicer.py +1 -1
  54. {flwr_nightly-1.11.0.dev20240813.dist-info → flwr_nightly-1.11.0.dev20240822.dist-info}/METADATA +2 -2
  55. {flwr_nightly-1.11.0.dev20240813.dist-info → flwr_nightly-1.11.0.dev20240822.dist-info}/RECORD +58 -53
  56. {flwr_nightly-1.11.0.dev20240813.dist-info → flwr_nightly-1.11.0.dev20240822.dist-info}/entry_points.txt +1 -1
  57. {flwr_nightly-1.11.0.dev20240813.dist-info → flwr_nightly-1.11.0.dev20240822.dist-info}/LICENSE +0 -0
  58. {flwr_nightly-1.11.0.dev20240813.dist-info → flwr_nightly-1.11.0.dev20240822.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
@@ -46,6 +46,7 @@ from flwr.common.serde import (
46
46
  user_config_from_proto,
47
47
  )
48
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,
@@ -286,12 +287,19 @@ def grpc_request_response( # pylint: disable=R0913, R0914, R0915
286
287
  run_id,
287
288
  get_run_response.run.fab_id,
288
289
  get_run_response.run.fab_version,
290
+ get_run_response.run.fab_hash,
289
291
  user_config_from_proto(get_run_response.run.override_config),
290
292
  )
291
293
 
292
294
  def get_fab(fab_hash: str) -> Fab:
293
295
  # Call FleetAPI
294
- raise NotImplementedError
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)
295
303
 
296
304
  try:
297
305
  # Yield methods
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 get_fused_config, get_fused_config_from_dir
24
- from flwr.common.typing import Run, UserConfig
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
- # Load from .fab
69
- initial_run_config = get_fused_config(run, flwr_path) if run else {}
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(
@@ -46,6 +46,7 @@ from flwr.common.serde import (
46
46
  user_config_from_proto,
47
47
  )
48
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
 
@@ -358,18 +360,29 @@ def http_request_response( # pylint: disable=,R0913, R0914, R0915
358
360
  # Send the request
359
361
  res = _request(req, GetRunResponse, PATH_GET_RUN)
360
362
  if res is None:
361
- return Run(run_id, "", "", {})
363
+ return Run(run_id, "", "", "", {})
362
364
 
363
365
  return Run(
364
366
  run_id,
365
367
  res.run.fab_id,
366
368
  res.run.fab_version,
369
+ res.run.fab_hash,
367
370
  user_config_from_proto(res.run.override_config),
368
371
  )
369
372
 
370
373
  def get_fab(fab_hash: str) -> Fab:
371
- # Call FleetAPI
372
- raise NotImplementedError
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
+ )
373
386
 
374
387
  try:
375
388
  # Yield methods
@@ -15,12 +15,10 @@
15
15
  """Flower SuperNode."""
16
16
 
17
17
 
18
- from .app import flwr_clientapp as flwr_clientapp
19
18
  from .app import run_client_app as run_client_app
20
19
  from .app import run_supernode as run_supernode
21
20
 
22
21
  __all__ = [
23
- "flwr_clientapp",
24
22
  "run_client_app",
25
23
  "run_supernode",
26
24
  ]