flwr-nightly 1.8.0.dev20240327__py3-none-any.whl → 1.8.0.dev20240401__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/client/app.py CHANGED
@@ -14,11 +14,10 @@
14
14
  # ==============================================================================
15
15
  """Flower client app."""
16
16
 
17
-
18
17
  import argparse
19
18
  import sys
20
19
  import time
21
- from logging import DEBUG, INFO, WARN
20
+ from logging import DEBUG, ERROR, INFO, WARN
22
21
  from pathlib import Path
23
22
  from typing import Callable, ContextManager, Optional, Tuple, Type, Union
24
23
 
@@ -37,7 +36,8 @@ from flwr.common.constant import (
37
36
  TRANSPORT_TYPES,
38
37
  )
39
38
  from flwr.common.exit_handlers import register_exit_handlers
40
- from flwr.common.logger import log, warn_deprecated_feature, warn_experimental_feature
39
+ from flwr.common.logger import log, warn_deprecated_feature
40
+ from flwr.common.message import Error
41
41
  from flwr.common.object_ref import load_app, validate
42
42
  from flwr.common.retry_invoker import RetryInvoker, exponential
43
43
 
@@ -385,8 +385,6 @@ def _start_client_internal(
385
385
  return ClientApp(client_fn=client_fn)
386
386
 
387
387
  load_client_app_fn = _load_client_app
388
- else:
389
- warn_experimental_feature("`load_client_app_fn`")
390
388
 
391
389
  # At this point, only `load_client_app_fn` should be used
392
390
  # Both `client` and `client_fn` must not be used directly
@@ -397,7 +395,7 @@ def _start_client_internal(
397
395
  )
398
396
 
399
397
  retry_invoker = RetryInvoker(
400
- wait_factory=exponential,
398
+ wait_gen_factory=exponential,
401
399
  recoverable_exceptions=connection_error_type,
402
400
  max_tries=max_retries,
403
401
  max_time=max_wait_time,
@@ -482,32 +480,43 @@ def _start_client_internal(
482
480
  # Retrieve context for this run
483
481
  context = node_state.retrieve_context(run_id=message.metadata.run_id)
484
482
 
485
- # Load ClientApp instance
486
- client_app: ClientApp = load_client_app_fn()
483
+ # Create an error reply message that will never be used to prevent
484
+ # the used-before-assignment linting error
485
+ reply_message = message.create_error_reply(
486
+ error=Error(code=0, reason="Unknown")
487
+ )
487
488
 
488
- # Handle task message
489
- out_message = client_app(message=message, context=context)
489
+ # Handle app loading and task message
490
+ try:
491
+ # Load ClientApp instance
492
+ client_app: ClientApp = load_client_app_fn()
490
493
 
491
- # Update node state
492
- node_state.update_context(
493
- run_id=message.metadata.run_id,
494
- context=context,
495
- )
494
+ reply_message = client_app(message=message, context=context)
495
+ # Update node state
496
+ node_state.update_context(
497
+ run_id=message.metadata.run_id,
498
+ context=context,
499
+ )
500
+ except Exception as ex: # pylint: disable=broad-exception-caught
501
+ log(ERROR, "ClientApp raised an exception", exc_info=ex)
502
+
503
+ # Legacy grpc-bidi
504
+ if transport in ["grpc-bidi", None]:
505
+ # Raise exception, crash process
506
+ raise ex
507
+
508
+ # Don't update/change NodeState
509
+
510
+ # Create error message
511
+ # Reason example: "<class 'ZeroDivisionError'>:<'division by zero'>"
512
+ reason = str(type(ex)) + ":<'" + str(ex) + "'>"
513
+ reply_message = message.create_error_reply(
514
+ error=Error(code=0, reason=reason)
515
+ )
496
516
 
497
517
  # Send
498
- send(out_message)
499
- log(
500
- INFO,
501
- "[RUN %s, ROUND %s]",
502
- out_message.metadata.run_id,
503
- out_message.metadata.group_id,
504
- )
505
- log(
506
- INFO,
507
- "Sent: %s reply to message %s",
508
- out_message.metadata.message_type,
509
- message.metadata.message_id,
510
- )
518
+ send(reply_message)
519
+ log(INFO, "Sent reply")
511
520
 
512
521
  # Unregister node
513
522
  if delete_node is not None:
flwr/client/client_app.py CHANGED
@@ -23,6 +23,7 @@ from flwr.client.message_handler.message_handler import (
23
23
  from flwr.client.mod.utils import make_ffn
24
24
  from flwr.client.typing import ClientFn, Mod
25
25
  from flwr.common import Context, Message, MessageType
26
+ from flwr.common.logger import warn_preview_feature
26
27
 
27
28
  from .typing import ClientAppCallable
28
29
 
@@ -123,6 +124,8 @@ class ClientApp:
123
124
  if self._call:
124
125
  raise _registration_error(MessageType.TRAIN)
125
126
 
127
+ warn_preview_feature("ClientApp-register-train-function")
128
+
126
129
  # Register provided function with the ClientApp object
127
130
  # Wrap mods around the wrapped step function
128
131
  self._train = make_ffn(train_fn, self._mods)
@@ -151,6 +154,8 @@ class ClientApp:
151
154
  if self._call:
152
155
  raise _registration_error(MessageType.EVALUATE)
153
156
 
157
+ warn_preview_feature("ClientApp-register-evaluate-function")
158
+
154
159
  # Register provided function with the ClientApp object
155
160
  # Wrap mods around the wrapped step function
156
161
  self._evaluate = make_ffn(evaluate_fn, self._mods)
@@ -179,6 +184,8 @@ class ClientApp:
179
184
  if self._call:
180
185
  raise _registration_error(MessageType.QUERY)
181
186
 
187
+ warn_preview_feature("ClientApp-register-query-function")
188
+
182
189
  # Register provided function with the ClientApp object
183
190
  # Wrap mods around the wrapped step function
184
191
  self._query = make_ffn(query_fn, self._mods)
@@ -15,23 +15,34 @@
15
15
  """Contextmanager for a gRPC request-response channel to the Flower server."""
16
16
 
17
17
 
18
+ import random
19
+ import threading
18
20
  from contextlib import contextmanager
19
21
  from copy import copy
20
22
  from logging import DEBUG, ERROR
21
23
  from pathlib import Path
22
- from typing import Callable, Dict, Iterator, Optional, Tuple, Union, cast
24
+ from typing import Callable, Iterator, Optional, Tuple, Union, cast
23
25
 
26
+ from flwr.client.heartbeat import start_ping_loop
24
27
  from flwr.client.message_handler.message_handler import validate_out_message
25
28
  from flwr.client.message_handler.task_handler import get_task_ins, validate_task_ins
26
29
  from flwr.common import GRPC_MAX_MESSAGE_LENGTH
30
+ from flwr.common.constant import (
31
+ PING_BASE_MULTIPLIER,
32
+ PING_CALL_TIMEOUT,
33
+ PING_DEFAULT_INTERVAL,
34
+ PING_RANDOM_RANGE,
35
+ )
27
36
  from flwr.common.grpc import create_channel
28
- from flwr.common.logger import log, warn_experimental_feature
37
+ from flwr.common.logger import log
29
38
  from flwr.common.message import Message, Metadata
30
39
  from flwr.common.retry_invoker import RetryInvoker
31
40
  from flwr.common.serde import message_from_taskins, message_to_taskres
32
41
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
33
42
  CreateNodeRequest,
34
43
  DeleteNodeRequest,
44
+ PingRequest,
45
+ PingResponse,
35
46
  PullTaskInsRequest,
36
47
  PushTaskResRequest,
37
48
  )
@@ -39,9 +50,6 @@ from flwr.proto.fleet_pb2_grpc import FleetStub # pylint: disable=E0611
39
50
  from flwr.proto.node_pb2 import Node # pylint: disable=E0611
40
51
  from flwr.proto.task_pb2 import TaskIns # pylint: disable=E0611
41
52
 
42
- KEY_NODE = "node"
43
- KEY_METADATA = "in_message_metadata"
44
-
45
53
 
46
54
  def on_channel_state_change(channel_connectivity: str) -> None:
47
55
  """Log channel connectivity."""
@@ -49,7 +57,7 @@ def on_channel_state_change(channel_connectivity: str) -> None:
49
57
 
50
58
 
51
59
  @contextmanager
52
- def grpc_request_response(
60
+ def grpc_request_response( # pylint: disable=R0914, R0915
53
61
  server_address: str,
54
62
  insecure: bool,
55
63
  retry_invoker: RetryInvoker,
@@ -95,8 +103,6 @@ def grpc_request_response(
95
103
  create_node : Optional[Callable]
96
104
  delete_node : Optional[Callable]
97
105
  """
98
- warn_experimental_feature("`grpc-rere`")
99
-
100
106
  if isinstance(root_certificates, str):
101
107
  root_certificates = Path(root_certificates).read_bytes()
102
108
 
@@ -107,47 +113,81 @@ def grpc_request_response(
107
113
  max_message_length=max_message_length,
108
114
  )
109
115
  channel.subscribe(on_channel_state_change)
110
- stub = FleetStub(channel)
111
116
 
112
- # Necessary state to validate messages to be sent
113
- state: Dict[str, Optional[Metadata]] = {KEY_METADATA: None}
114
-
115
- # Enable create_node and delete_node to store node
116
- node_store: Dict[str, Optional[Node]] = {KEY_NODE: None}
117
+ # Shared variables for inner functions
118
+ stub = FleetStub(channel)
119
+ metadata: Optional[Metadata] = None
120
+ node: Optional[Node] = None
121
+ ping_thread: Optional[threading.Thread] = None
122
+ ping_stop_event = threading.Event()
117
123
 
118
124
  ###########################################################################
119
- # receive/send functions
125
+ # ping/create_node/delete_node/receive/send functions
120
126
  ###########################################################################
121
127
 
128
+ def ping() -> None:
129
+ # Get Node
130
+ if node is None:
131
+ log(ERROR, "Node instance missing")
132
+ return
133
+
134
+ # Construct the ping request
135
+ req = PingRequest(node=node, ping_interval=PING_DEFAULT_INTERVAL)
136
+
137
+ # Call FleetAPI
138
+ res: PingResponse = stub.Ping(req, timeout=PING_CALL_TIMEOUT)
139
+
140
+ # Check if success
141
+ if not res.success:
142
+ raise RuntimeError("Ping failed unexpectedly.")
143
+
144
+ # Wait
145
+ rd = random.uniform(*PING_RANDOM_RANGE)
146
+ next_interval: float = PING_DEFAULT_INTERVAL - PING_CALL_TIMEOUT
147
+ next_interval *= PING_BASE_MULTIPLIER + rd
148
+ if not ping_stop_event.is_set():
149
+ ping_stop_event.wait(next_interval)
150
+
122
151
  def create_node() -> None:
123
152
  """Set create_node."""
153
+ # Call FleetAPI
124
154
  create_node_request = CreateNodeRequest()
125
155
  create_node_response = retry_invoker.invoke(
126
156
  stub.CreateNode,
127
157
  request=create_node_request,
128
158
  )
129
- node_store[KEY_NODE] = create_node_response.node
159
+
160
+ # Remember the node and the ping-loop thread
161
+ nonlocal node, ping_thread
162
+ node = cast(Node, create_node_response.node)
163
+ ping_thread = start_ping_loop(ping, ping_stop_event)
130
164
 
131
165
  def delete_node() -> None:
132
166
  """Set delete_node."""
133
167
  # Get Node
134
- if node_store[KEY_NODE] is None:
168
+ nonlocal node
169
+ if node is None:
135
170
  log(ERROR, "Node instance missing")
136
171
  return
137
- node: Node = cast(Node, node_store[KEY_NODE])
138
172
 
173
+ # Stop the ping-loop thread
174
+ ping_stop_event.set()
175
+ if ping_thread is not None:
176
+ ping_thread.join()
177
+
178
+ # Call FleetAPI
139
179
  delete_node_request = DeleteNodeRequest(node=node)
140
180
  retry_invoker.invoke(stub.DeleteNode, request=delete_node_request)
141
181
 
142
- del node_store[KEY_NODE]
182
+ # Cleanup
183
+ node = None
143
184
 
144
185
  def receive() -> Optional[Message]:
145
186
  """Receive next task from server."""
146
187
  # Get Node
147
- if node_store[KEY_NODE] is None:
188
+ if node is None:
148
189
  log(ERROR, "Node instance missing")
149
190
  return None
150
- node: Node = cast(Node, node_store[KEY_NODE])
151
191
 
152
192
  # Request instructions (task) from server
153
193
  request = PullTaskInsRequest(node=node)
@@ -167,7 +207,8 @@ def grpc_request_response(
167
207
  in_message = message_from_taskins(task_ins) if task_ins else None
168
208
 
169
209
  # Remember `metadata` of the in message
170
- state[KEY_METADATA] = copy(in_message.metadata) if in_message else None
210
+ nonlocal metadata
211
+ metadata = copy(in_message.metadata) if in_message else None
171
212
 
172
213
  # Return the message if available
173
214
  return in_message
@@ -175,18 +216,18 @@ def grpc_request_response(
175
216
  def send(message: Message) -> None:
176
217
  """Send task result back to server."""
177
218
  # Get Node
178
- if node_store[KEY_NODE] is None:
219
+ if node is None:
179
220
  log(ERROR, "Node instance missing")
180
221
  return
181
222
 
182
- # Get incoming message
183
- in_metadata = state[KEY_METADATA]
184
- if in_metadata is None:
223
+ # Get the metadata of the incoming message
224
+ nonlocal metadata
225
+ if metadata is None:
185
226
  log(ERROR, "No current message")
186
227
  return
187
228
 
188
229
  # Validate out message
189
- if not validate_out_message(message, in_metadata):
230
+ if not validate_out_message(message, metadata):
190
231
  log(ERROR, "Invalid out message")
191
232
  return
192
233
 
@@ -197,7 +238,8 @@ def grpc_request_response(
197
238
  request = PushTaskResRequest(task_res_list=[task_res])
198
239
  _ = retry_invoker.invoke(stub.PushTaskRes, request)
199
240
 
200
- state[KEY_METADATA] = None
241
+ # Cleanup
242
+ metadata = None
201
243
 
202
244
  try:
203
245
  # Yield methods
@@ -0,0 +1,72 @@
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
+ """Heartbeat utility functions."""
16
+
17
+
18
+ import threading
19
+ from typing import Callable
20
+
21
+ import grpc
22
+
23
+ from flwr.common.constant import PING_CALL_TIMEOUT
24
+ from flwr.common.retry_invoker import RetryInvoker, RetryState, exponential
25
+
26
+
27
+ def _ping_loop(ping_fn: Callable[[], None], stop_event: threading.Event) -> None:
28
+ def wait_fn(wait_time: float) -> None:
29
+ if not stop_event.is_set():
30
+ stop_event.wait(wait_time)
31
+
32
+ def on_backoff(state: RetryState) -> None:
33
+ err = state.exception
34
+ if not isinstance(err, grpc.RpcError):
35
+ return
36
+ status_code = err.code()
37
+ # If ping call timeout is triggered
38
+ if status_code == grpc.StatusCode.DEADLINE_EXCEEDED:
39
+ # Avoid long wait time.
40
+ if state.actual_wait is None:
41
+ return
42
+ state.actual_wait = max(state.actual_wait - PING_CALL_TIMEOUT, 0.0)
43
+
44
+ def wrapped_ping() -> None:
45
+ if not stop_event.is_set():
46
+ ping_fn()
47
+
48
+ retrier = RetryInvoker(
49
+ exponential,
50
+ grpc.RpcError,
51
+ max_tries=None,
52
+ max_time=None,
53
+ on_backoff=on_backoff,
54
+ wait_function=wait_fn,
55
+ )
56
+ while not stop_event.is_set():
57
+ retrier.invoke(wrapped_ping)
58
+
59
+
60
+ def start_ping_loop(
61
+ ping_fn: Callable[[], None], stop_event: threading.Event
62
+ ) -> threading.Thread:
63
+ """Start a ping loop in a separate thread.
64
+
65
+ This function initializes a new thread that runs a ping loop, allowing for
66
+ asynchronous ping operations. The loop can be terminated through the provided stop
67
+ event.
68
+ """
69
+ thread = threading.Thread(target=_ping_loop, args=(ping_fn, stop_event))
70
+ thread.start()
71
+
72
+ return thread
@@ -15,16 +15,25 @@
15
15
  """Contextmanager for a REST request-response channel to the Flower server."""
16
16
 
17
17
 
18
+ import random
18
19
  import sys
20
+ import threading
19
21
  from contextlib import contextmanager
20
22
  from copy import copy
21
23
  from logging import ERROR, INFO, WARN
22
- from typing import Callable, Dict, Iterator, Optional, Tuple, Union, cast
24
+ from typing import Callable, Iterator, Optional, Tuple, Union
23
25
 
26
+ from flwr.client.heartbeat import start_ping_loop
24
27
  from flwr.client.message_handler.message_handler import validate_out_message
25
28
  from flwr.client.message_handler.task_handler import get_task_ins, validate_task_ins
26
29
  from flwr.common import GRPC_MAX_MESSAGE_LENGTH
27
- from flwr.common.constant import MISSING_EXTRA_REST
30
+ from flwr.common.constant import (
31
+ MISSING_EXTRA_REST,
32
+ PING_BASE_MULTIPLIER,
33
+ PING_CALL_TIMEOUT,
34
+ PING_DEFAULT_INTERVAL,
35
+ PING_RANDOM_RANGE,
36
+ )
28
37
  from flwr.common.logger import log
29
38
  from flwr.common.message import Message, Metadata
30
39
  from flwr.common.retry_invoker import RetryInvoker
@@ -33,6 +42,8 @@ from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
33
42
  CreateNodeRequest,
34
43
  CreateNodeResponse,
35
44
  DeleteNodeRequest,
45
+ PingRequest,
46
+ PingResponse,
36
47
  PullTaskInsRequest,
37
48
  PullTaskInsResponse,
38
49
  PushTaskResRequest,
@@ -47,19 +58,15 @@ except ModuleNotFoundError:
47
58
  sys.exit(MISSING_EXTRA_REST)
48
59
 
49
60
 
50
- KEY_NODE = "node"
51
- KEY_METADATA = "in_message_metadata"
52
-
53
-
54
61
  PATH_CREATE_NODE: str = "api/v0/fleet/create-node"
55
62
  PATH_DELETE_NODE: str = "api/v0/fleet/delete-node"
56
63
  PATH_PULL_TASK_INS: str = "api/v0/fleet/pull-task-ins"
57
64
  PATH_PUSH_TASK_RES: str = "api/v0/fleet/push-task-res"
65
+ PATH_PING: str = "api/v0/fleet/ping"
58
66
 
59
67
 
60
68
  @contextmanager
61
- # pylint: disable-next=too-many-statements
62
- def http_request_response(
69
+ def http_request_response( # pylint: disable=R0914, R0915
63
70
  server_address: str,
64
71
  insecure: bool, # pylint: disable=unused-argument
65
72
  retry_invoker: RetryInvoker,
@@ -127,16 +134,71 @@ def http_request_response(
127
134
  "must be provided as a string path to the client.",
128
135
  )
129
136
 
130
- # Necessary state to validate messages to be sent
131
- state: Dict[str, Optional[Metadata]] = {KEY_METADATA: None}
132
-
133
- # Enable create_node and delete_node to store node
134
- node_store: Dict[str, Optional[Node]] = {KEY_NODE: None}
137
+ # Shared variables for inner functions
138
+ metadata: Optional[Metadata] = None
139
+ node: Optional[Node] = None
140
+ ping_thread: Optional[threading.Thread] = None
141
+ ping_stop_event = threading.Event()
135
142
 
136
143
  ###########################################################################
137
- # receive/send functions
144
+ # ping/create_node/delete_node/receive/send functions
138
145
  ###########################################################################
139
146
 
147
+ def ping() -> None:
148
+ # Get Node
149
+ if node is None:
150
+ log(ERROR, "Node instance missing")
151
+ return
152
+
153
+ # Construct the ping request
154
+ req = PingRequest(node=node, ping_interval=PING_DEFAULT_INTERVAL)
155
+ req_bytes: bytes = req.SerializeToString()
156
+
157
+ # Send the request
158
+ res = requests.post(
159
+ url=f"{base_url}/{PATH_PING}",
160
+ headers={
161
+ "Accept": "application/protobuf",
162
+ "Content-Type": "application/protobuf",
163
+ },
164
+ data=req_bytes,
165
+ verify=verify,
166
+ timeout=PING_CALL_TIMEOUT,
167
+ )
168
+
169
+ # Check status code and headers
170
+ if res.status_code != 200:
171
+ return
172
+ if "content-type" not in res.headers:
173
+ log(
174
+ WARN,
175
+ "[Node] POST /%s: missing header `Content-Type`",
176
+ PATH_PULL_TASK_INS,
177
+ )
178
+ return
179
+ if res.headers["content-type"] != "application/protobuf":
180
+ log(
181
+ WARN,
182
+ "[Node] POST /%s: header `Content-Type` has wrong value",
183
+ PATH_PULL_TASK_INS,
184
+ )
185
+ return
186
+
187
+ # Deserialize ProtoBuf from bytes
188
+ ping_res = PingResponse()
189
+ ping_res.ParseFromString(res.content)
190
+
191
+ # Check if success
192
+ if not ping_res.success:
193
+ raise RuntimeError("Ping failed unexpectedly.")
194
+
195
+ # Wait
196
+ rd = random.uniform(*PING_RANDOM_RANGE)
197
+ next_interval: float = PING_DEFAULT_INTERVAL - PING_CALL_TIMEOUT
198
+ next_interval *= PING_BASE_MULTIPLIER + rd
199
+ if not ping_stop_event.is_set():
200
+ ping_stop_event.wait(next_interval)
201
+
140
202
  def create_node() -> None:
141
203
  """Set create_node."""
142
204
  create_node_req_proto = CreateNodeRequest()
@@ -175,15 +237,25 @@ def http_request_response(
175
237
  # Deserialize ProtoBuf from bytes
176
238
  create_node_response_proto = CreateNodeResponse()
177
239
  create_node_response_proto.ParseFromString(res.content)
178
- # pylint: disable-next=no-member
179
- node_store[KEY_NODE] = create_node_response_proto.node
240
+
241
+ # Remember the node and the ping-loop thread
242
+ nonlocal node, ping_thread
243
+ node = create_node_response_proto.node
244
+ ping_thread = start_ping_loop(ping, ping_stop_event)
180
245
 
181
246
  def delete_node() -> None:
182
247
  """Set delete_node."""
183
- if node_store[KEY_NODE] is None:
248
+ nonlocal node
249
+ if node is None:
184
250
  log(ERROR, "Node instance missing")
185
251
  return
186
- node: Node = cast(Node, node_store[KEY_NODE])
252
+
253
+ # Stop the ping-loop thread
254
+ ping_stop_event.set()
255
+ if ping_thread is not None:
256
+ ping_thread.join()
257
+
258
+ # Send DeleteNode request
187
259
  delete_node_req_proto = DeleteNodeRequest(node=node)
188
260
  delete_node_req_req_bytes: bytes = delete_node_req_proto.SerializeToString()
189
261
  res = retry_invoker.invoke(
@@ -215,13 +287,15 @@ def http_request_response(
215
287
  PATH_PULL_TASK_INS,
216
288
  )
217
289
 
290
+ # Cleanup
291
+ node = None
292
+
218
293
  def receive() -> Optional[Message]:
219
294
  """Receive next task from server."""
220
295
  # Get Node
221
- if node_store[KEY_NODE] is None:
296
+ if node is None:
222
297
  log(ERROR, "Node instance missing")
223
298
  return None
224
- node: Node = cast(Node, node_store[KEY_NODE])
225
299
 
226
300
  # Request instructions (task) from server
227
301
  pull_task_ins_req_proto = PullTaskInsRequest(node=node)
@@ -273,29 +347,29 @@ def http_request_response(
273
347
  task_ins = None
274
348
 
275
349
  # Return the Message if available
350
+ nonlocal metadata
276
351
  message = None
277
- state[KEY_METADATA] = None
278
352
  if task_ins is not None:
279
353
  message = message_from_taskins(task_ins)
280
- state[KEY_METADATA] = copy(message.metadata)
354
+ metadata = copy(message.metadata)
281
355
  log(INFO, "[Node] POST /%s: success", PATH_PULL_TASK_INS)
282
356
  return message
283
357
 
284
358
  def send(message: Message) -> None:
285
359
  """Send task result back to server."""
286
360
  # Get Node
287
- if node_store[KEY_NODE] is None:
361
+ if node is None:
288
362
  log(ERROR, "Node instance missing")
289
363
  return
290
364
 
291
365
  # Get incoming message
292
- in_metadata = state[KEY_METADATA]
293
- if in_metadata is None:
366
+ nonlocal metadata
367
+ if metadata is None:
294
368
  log(ERROR, "No current message")
295
369
  return
296
370
 
297
371
  # Validate out message
298
- if not validate_out_message(message, in_metadata):
372
+ if not validate_out_message(message, metadata):
299
373
  log(ERROR, "Invalid out message")
300
374
  return
301
375
 
@@ -321,7 +395,7 @@ def http_request_response(
321
395
  timeout=None,
322
396
  )
323
397
 
324
- state[KEY_METADATA] = None
398
+ metadata = None
325
399
 
326
400
  # Check status code and headers
327
401
  if res.status_code != 200:
flwr/common/constant.py CHANGED
@@ -36,6 +36,12 @@ TRANSPORT_TYPES = [
36
36
  TRANSPORT_TYPE_VCE,
37
37
  ]
38
38
 
39
+ # Constants for ping
40
+ PING_DEFAULT_INTERVAL = 30
41
+ PING_CALL_TIMEOUT = 5
42
+ PING_BASE_MULTIPLIER = 0.8
43
+ PING_RANDOM_RANGE = (-0.1, 0.1)
44
+
39
45
 
40
46
  class MessageType:
41
47
  """Message type."""
flwr/common/logger.py CHANGED
@@ -164,13 +164,13 @@ logger = logging.getLogger(LOGGER_NAME) # pylint: disable=invalid-name
164
164
  log = logger.log # pylint: disable=invalid-name
165
165
 
166
166
 
167
- def warn_experimental_feature(name: str) -> None:
168
- """Warn the user when they use an experimental feature."""
167
+ def warn_preview_feature(name: str) -> None:
168
+ """Warn the user when they use a preview feature."""
169
169
  log(
170
170
  WARN,
171
- """EXPERIMENTAL FEATURE: %s
171
+ """PREVIEW FEATURE: %s
172
172
 
173
- This is an experimental feature. It could change significantly or be removed
173
+ This is a preview feature. It could change significantly or be removed
174
174
  entirely in future versions of Flower.
175
175
  """,
176
176
  name,
flwr/common/message.py CHANGED
@@ -297,22 +297,33 @@ class Message:
297
297
  partition_id=self.metadata.partition_id,
298
298
  )
299
299
 
300
- def create_error_reply(
301
- self,
302
- error: Error,
303
- ttl: float,
304
- ) -> Message:
300
+ def create_error_reply(self, error: Error, ttl: float | None = None) -> Message:
305
301
  """Construct a reply message indicating an error happened.
306
302
 
307
303
  Parameters
308
304
  ----------
309
305
  error : Error
310
306
  The error that was encountered.
311
- ttl : float
312
- Time-to-live for this message in seconds.
307
+ ttl : Optional[float] (default: None)
308
+ Time-to-live for this message in seconds. If unset, it will be set based
309
+ on the remaining time for the received message before it expires. This
310
+ follows the equation:
311
+
312
+ ttl = msg.meta.ttl - (reply.meta.created_at - msg.meta.created_at)
313
313
  """
314
+ # If no TTL passed, use default for message creation (will update after
315
+ # message creation)
316
+ ttl_ = DEFAULT_TTL if ttl is None else ttl
314
317
  # Create reply with error
315
- message = Message(metadata=self._create_reply_metadata(ttl), error=error)
318
+ message = Message(metadata=self._create_reply_metadata(ttl_), error=error)
319
+
320
+ if ttl is None:
321
+ # Set TTL equal to the remaining time for the received message to expire
322
+ ttl = self.metadata.ttl - (
323
+ message.metadata.created_at - self.metadata.created_at
324
+ )
325
+ message.metadata.ttl = ttl
326
+
316
327
  return message
317
328
 
318
329
  def create_reply(self, content: RecordSet, ttl: float | None = None) -> Message:
@@ -327,18 +338,31 @@ class Message:
327
338
  content : RecordSet
328
339
  The content for the reply message.
329
340
  ttl : Optional[float] (default: None)
330
- Time-to-live for this message in seconds. If unset, it will use
331
- the `common.DEFAULT_TTL` value.
341
+ Time-to-live for this message in seconds. If unset, it will be set based
342
+ on the remaining time for the received message before it expires. This
343
+ follows the equation:
344
+
345
+ ttl = msg.meta.ttl - (reply.meta.created_at - msg.meta.created_at)
332
346
 
333
347
  Returns
334
348
  -------
335
349
  Message
336
350
  A new `Message` instance representing the reply.
337
351
  """
338
- if ttl is None:
339
- ttl = DEFAULT_TTL
352
+ # If no TTL passed, use default for message creation (will update after
353
+ # message creation)
354
+ ttl_ = DEFAULT_TTL if ttl is None else ttl
340
355
 
341
- return Message(
342
- metadata=self._create_reply_metadata(ttl),
356
+ message = Message(
357
+ metadata=self._create_reply_metadata(ttl_),
343
358
  content=content,
344
359
  )
360
+
361
+ if ttl is None:
362
+ # Set TTL equal to the remaining time for the received message to expire
363
+ ttl = self.metadata.ttl - (
364
+ message.metadata.created_at - self.metadata.created_at
365
+ )
366
+ message.metadata.ttl = ttl
367
+
368
+ return message
@@ -107,7 +107,7 @@ class RetryInvoker:
107
107
 
108
108
  Parameters
109
109
  ----------
110
- wait_factory: Callable[[], Generator[float, None, None]]
110
+ wait_gen_factory: Callable[[], Generator[float, None, None]]
111
111
  A generator yielding successive wait times in seconds. If the generator
112
112
  is finite, the giveup event will be triggered when the generator raises
113
113
  `StopIteration`.
@@ -129,12 +129,12 @@ class RetryInvoker:
129
129
  data class object detailing the invocation.
130
130
  on_giveup: Optional[Callable[[RetryState], None]] (default: None)
131
131
  A callable to be executed in the event that `max_tries` or `max_time` is
132
- exceeded, `should_giveup` returns True, or `wait_factory()` generator raises
132
+ exceeded, `should_giveup` returns True, or `wait_gen_factory()` generator raises
133
133
  `StopInteration`. The parameter is a data class object detailing the
134
134
  invocation.
135
135
  jitter: Optional[Callable[[float], float]] (default: full_jitter)
136
- A function of the value yielded by `wait_factory()` returning the actual time
137
- to wait. This function helps distribute wait times stochastically to avoid
136
+ A function of the value yielded by `wait_gen_factory()` returning the actual
137
+ time to wait. This function helps distribute wait times stochastically to avoid
138
138
  timing collisions across concurrent clients. Wait times are jittered by
139
139
  default using the `full_jitter` function. To disable jittering, pass
140
140
  `jitter=None`.
@@ -142,6 +142,13 @@ class RetryInvoker:
142
142
  A function accepting an exception instance, returning whether or not
143
143
  to give up prematurely before other give-up conditions are evaluated.
144
144
  If set to None, the strategy is to never give up prematurely.
145
+ wait_function: Optional[Callable[[float], None]] (default: None)
146
+ A function that defines how to wait between retry attempts. It accepts
147
+ one argument, the wait time in seconds, allowing the use of various waiting
148
+ mechanisms (e.g., asynchronous waits or event-based synchronization) suitable
149
+ for different execution environments. If set to `None`, the `wait_function`
150
+ defaults to `time.sleep`, which is ideal for synchronous operations. Custom
151
+ functions should manage execution flow to prevent blocking or interference.
145
152
 
146
153
  Examples
147
154
  --------
@@ -159,7 +166,7 @@ class RetryInvoker:
159
166
  # pylint: disable-next=too-many-arguments
160
167
  def __init__(
161
168
  self,
162
- wait_factory: Callable[[], Generator[float, None, None]],
169
+ wait_gen_factory: Callable[[], Generator[float, None, None]],
163
170
  recoverable_exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]],
164
171
  max_tries: Optional[int],
165
172
  max_time: Optional[float],
@@ -169,8 +176,9 @@ class RetryInvoker:
169
176
  on_giveup: Optional[Callable[[RetryState], None]] = None,
170
177
  jitter: Optional[Callable[[float], float]] = full_jitter,
171
178
  should_giveup: Optional[Callable[[Exception], bool]] = None,
179
+ wait_function: Optional[Callable[[float], None]] = None,
172
180
  ) -> None:
173
- self.wait_factory = wait_factory
181
+ self.wait_gen_factory = wait_gen_factory
174
182
  self.recoverable_exceptions = recoverable_exceptions
175
183
  self.max_tries = max_tries
176
184
  self.max_time = max_time
@@ -179,6 +187,9 @@ class RetryInvoker:
179
187
  self.on_giveup = on_giveup
180
188
  self.jitter = jitter
181
189
  self.should_giveup = should_giveup
190
+ if wait_function is None:
191
+ wait_function = time.sleep
192
+ self.wait_function = wait_function
182
193
 
183
194
  # pylint: disable-next=too-many-locals
184
195
  def invoke(
@@ -212,13 +223,13 @@ class RetryInvoker:
212
223
  Raises
213
224
  ------
214
225
  Exception
215
- If the number of tries exceeds `max_tries`, if the total time
216
- exceeds `max_time`, if `wait_factory()` generator raises `StopInteration`,
226
+ If the number of tries exceeds `max_tries`, if the total time exceeds
227
+ `max_time`, if `wait_gen_factory()` generator raises `StopInteration`,
217
228
  or if the `should_giveup` returns True for a raised exception.
218
229
 
219
230
  Notes
220
231
  -----
221
- The time between retries is determined by the provided `wait_factory()`
232
+ The time between retries is determined by the provided `wait_gen_factory()`
222
233
  generator and can optionally be jittered using the `jitter` function.
223
234
  The recoverable exceptions that trigger a retry, as well as conditions to
224
235
  stop retries, are also determined by the class's initialization parameters.
@@ -231,13 +242,13 @@ class RetryInvoker:
231
242
  handler(cast(RetryState, ref_state[0]))
232
243
 
233
244
  try_cnt = 0
234
- wait_generator = self.wait_factory()
235
- start = time.time()
245
+ wait_generator = self.wait_gen_factory()
246
+ start = time.monotonic()
236
247
  ref_state: List[Optional[RetryState]] = [None]
237
248
 
238
249
  while True:
239
250
  try_cnt += 1
240
- elapsed_time = time.time() - start
251
+ elapsed_time = time.monotonic() - start
241
252
  state = RetryState(
242
253
  target=target,
243
254
  args=args,
@@ -282,7 +293,7 @@ class RetryInvoker:
282
293
  try_call_event_handler(self.on_backoff)
283
294
 
284
295
  # Sleep
285
- time.sleep(wait_time)
296
+ self.wait_function(state.actual_wait)
286
297
  else:
287
298
  # Trigger success event
288
299
  try_call_event_handler(self.on_success)
@@ -170,8 +170,24 @@ class DriverClientProxy(ClientProxy):
170
170
  )
171
171
  if len(task_res_list) == 1:
172
172
  task_res = task_res_list[0]
173
+
174
+ # This will raise an Exception if task_res carries an `error`
175
+ validate_task_res(task_res=task_res)
176
+
173
177
  return serde.recordset_from_proto(task_res.task.recordset)
174
178
 
175
179
  if timeout is not None and time.time() > start_time + timeout:
176
180
  raise RuntimeError("Timeout reached")
177
181
  time.sleep(SLEEP_TIME)
182
+
183
+
184
+ def validate_task_res(
185
+ task_res: task_pb2.TaskRes, # pylint: disable=E1101
186
+ ) -> None:
187
+ """Validate if a TaskRes is empty or not."""
188
+ if not task_res.HasField("task"):
189
+ raise ValueError("Invalid TaskRes, field `task` missing")
190
+ if task_res.task.HasField("error"):
191
+ raise ValueError("Exception during client-side task execution")
192
+ if not task_res.task.HasField("recordset"):
193
+ raise ValueError("Invalid TaskRes, both `recordset` and `error` are missing")
flwr/server/server_app.py CHANGED
@@ -18,6 +18,7 @@
18
18
  from typing import Callable, Optional
19
19
 
20
20
  from flwr.common import Context, RecordSet
21
+ from flwr.common.logger import warn_preview_feature
21
22
  from flwr.server.strategy import Strategy
22
23
 
23
24
  from .client_manager import ClientManager
@@ -120,6 +121,8 @@ class ServerApp:
120
121
  """,
121
122
  )
122
123
 
124
+ warn_preview_feature("ServerApp-register-main-function")
125
+
123
126
  # Register provided function with the ServerApp object
124
127
  self._main = main_fn
125
128
 
@@ -63,7 +63,8 @@ def ping(
63
63
  state: State, # pylint: disable=unused-argument
64
64
  ) -> PingResponse:
65
65
  """."""
66
- return PingResponse(success=True)
66
+ res = state.acknowledge_ping(request.node.node_id, request.ping_interval)
67
+ return PingResponse(success=res)
67
68
 
68
69
 
69
70
  def pull_task_ins(request: PullTaskInsRequest, state: State) -> PullTaskInsResponse:
@@ -21,6 +21,7 @@ from flwr.common.constant import MISSING_EXTRA_REST
21
21
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
22
22
  CreateNodeRequest,
23
23
  DeleteNodeRequest,
24
+ PingRequest,
24
25
  PullTaskInsRequest,
25
26
  PushTaskResRequest,
26
27
  )
@@ -152,11 +153,38 @@ async def push_task_res(request: Request) -> Response: # Check if token is need
152
153
  )
153
154
 
154
155
 
156
+ async def ping(request: Request) -> Response:
157
+ """Ping."""
158
+ _check_headers(request.headers)
159
+
160
+ # Get the request body as raw bytes
161
+ ping_request_bytes: bytes = await request.body()
162
+
163
+ # Deserialize ProtoBuf
164
+ ping_request_proto = PingRequest()
165
+ ping_request_proto.ParseFromString(ping_request_bytes)
166
+
167
+ # Get state from app
168
+ state: State = app.state.STATE_FACTORY.state()
169
+
170
+ # Handle message
171
+ ping_response_proto = message_handler.ping(request=ping_request_proto, state=state)
172
+
173
+ # Return serialized ProtoBuf
174
+ ping_response_bytes = ping_response_proto.SerializeToString()
175
+ return Response(
176
+ status_code=200,
177
+ content=ping_response_bytes,
178
+ headers={"Content-Type": "application/protobuf"},
179
+ )
180
+
181
+
155
182
  routes = [
156
183
  Route("/api/v0/fleet/create-node", create_node, methods=["POST"]),
157
184
  Route("/api/v0/fleet/delete-node", delete_node, methods=["POST"]),
158
185
  Route("/api/v0/fleet/pull-task-ins", pull_task_ins, methods=["POST"]),
159
186
  Route("/api/v0/fleet/push-task-res", push_task_res, methods=["POST"]),
187
+ Route("/api/v0/fleet/ping", ping, methods=["POST"]),
160
188
  ]
161
189
 
162
190
  app: Starlette = Starlette(
@@ -20,7 +20,7 @@ from typing import Callable, Dict, List, Tuple, Union
20
20
 
21
21
  import ray
22
22
 
23
- from flwr.client.client_app import ClientApp, LoadClientAppError
23
+ from flwr.client.client_app import ClientApp
24
24
  from flwr.common.context import Context
25
25
  from flwr.common.logger import log
26
26
  from flwr.common.message import Message
@@ -151,7 +151,6 @@ class RayBackend(Backend):
151
151
  )
152
152
 
153
153
  await future
154
-
155
154
  # Fetch result
156
155
  (
157
156
  out_mssg,
@@ -160,13 +159,15 @@ class RayBackend(Backend):
160
159
 
161
160
  return out_mssg, updated_context
162
161
 
163
- except LoadClientAppError as load_ex:
162
+ except Exception as ex:
164
163
  log(
165
164
  ERROR,
166
165
  "An exception was raised when processing a message by %s",
167
166
  self.__class__.__name__,
168
167
  )
169
- raise load_ex
168
+ # add actor back into pool
169
+ await self.pool.add_actor_back_to_pool(future)
170
+ raise ex
170
171
 
171
172
  async def terminate(self) -> None:
172
173
  """Terminate all actors in actor pool."""
@@ -14,9 +14,10 @@
14
14
  # ==============================================================================
15
15
  """Fleet Simulation Engine API."""
16
16
 
17
-
18
17
  import asyncio
19
18
  import json
19
+ import sys
20
+ import time
20
21
  import traceback
21
22
  from logging import DEBUG, ERROR, INFO, WARN
22
23
  from typing import Callable, Dict, List, Optional
@@ -24,6 +25,7 @@ from typing import Callable, Dict, List, Optional
24
25
  from flwr.client.client_app import ClientApp, LoadClientAppError
25
26
  from flwr.client.node_state import NodeState
26
27
  from flwr.common.logger import log
28
+ from flwr.common.message import Error
27
29
  from flwr.common.object_ref import load_app
28
30
  from flwr.common.serde import message_from_taskins, message_to_taskres
29
31
  from flwr.proto.task_pb2 import TaskIns # pylint: disable=E0611
@@ -59,6 +61,7 @@ async def worker(
59
61
  """Get TaskIns from queue and pass it to an actor in the pool to execute it."""
60
62
  state = state_factory.state()
61
63
  while True:
64
+ out_mssg = None
62
65
  try:
63
66
  task_ins: TaskIns = await queue.get()
64
67
  node_id = task_ins.task.consumer.node_id
@@ -82,24 +85,25 @@ async def worker(
82
85
  task_ins.run_id, context=updated_context
83
86
  )
84
87
 
85
- # Convert to TaskRes
86
- task_res = message_to_taskres(out_mssg)
87
- # Store TaskRes in state
88
- state.store_task_res(task_res)
89
-
90
88
  except asyncio.CancelledError as e:
91
- log(DEBUG, "Async worker: %s", e)
89
+ log(DEBUG, "Terminating async worker: %s", e)
92
90
  break
93
91
 
94
- except LoadClientAppError as app_ex:
95
- log(ERROR, "Async worker: %s", app_ex)
96
- log(ERROR, traceback.format_exc())
97
- raise
98
-
92
+ # Exceptions aren't raised but reported as an error message
99
93
  except Exception as ex: # pylint: disable=broad-exception-caught
100
94
  log(ERROR, ex)
101
95
  log(ERROR, traceback.format_exc())
102
- break
96
+ reason = str(type(ex)) + ":<'" + str(ex) + "'>"
97
+ error = Error(code=0, reason=reason)
98
+ out_mssg = message.create_error_reply(error=error)
99
+
100
+ finally:
101
+ if out_mssg:
102
+ # Convert to TaskRes
103
+ task_res = message_to_taskres(out_mssg)
104
+ # Store TaskRes in state
105
+ task_res.task.pushed_at = time.time()
106
+ state.store_task_res(task_res)
103
107
 
104
108
 
105
109
  async def add_taskins_to_queue(
@@ -218,7 +222,8 @@ async def run(
218
222
  await backend.terminate()
219
223
 
220
224
 
221
- # pylint: disable=too-many-arguments,unused-argument,too-many-locals
225
+ # pylint: disable=too-many-arguments,unused-argument,too-many-locals,too-many-branches
226
+ # pylint: disable=too-many-statements
222
227
  def start_vce(
223
228
  backend_name: str,
224
229
  backend_config_json_stream: str,
@@ -300,12 +305,14 @@ def start_vce(
300
305
  """Instantiate a Backend."""
301
306
  return backend_type(backend_config, work_dir=app_dir)
302
307
 
303
- log(INFO, "client_app_attr = %s", client_app_attr)
304
-
305
308
  # Load ClientApp if needed
306
309
  def _load() -> ClientApp:
307
310
 
308
311
  if client_app_attr:
312
+
313
+ if app_dir is not None:
314
+ sys.path.insert(0, app_dir)
315
+
309
316
  app: ClientApp = load_app(client_app_attr, LoadClientAppError)
310
317
 
311
318
  if not isinstance(app, ClientApp):
@@ -319,13 +326,30 @@ def start_vce(
319
326
 
320
327
  app_fn = _load
321
328
 
322
- asyncio.run(
323
- run(
324
- app_fn,
325
- backend_fn,
326
- nodes_mapping,
327
- state_factory,
328
- node_states,
329
- f_stop,
329
+ try:
330
+ # Test if ClientApp can be loaded
331
+ _ = app_fn()
332
+
333
+ # Run main simulation loop
334
+ asyncio.run(
335
+ run(
336
+ app_fn,
337
+ backend_fn,
338
+ nodes_mapping,
339
+ state_factory,
340
+ node_states,
341
+ f_stop,
342
+ )
343
+ )
344
+ except LoadClientAppError as loadapp_ex:
345
+ f_stop_delay = 10
346
+ log(
347
+ ERROR,
348
+ "LoadClientAppError exception encountered. Terminating simulation in %is",
349
+ f_stop_delay,
330
350
  )
331
- )
351
+ time.sleep(f_stop_delay)
352
+ f_stop.set() # set termination event
353
+ raise loadapp_ex
354
+ except Exception as ex:
355
+ raise ex
@@ -493,13 +493,17 @@ class BasicActorPool:
493
493
  self._future_to_actor[future] = actor
494
494
  return future
495
495
 
496
+ async def add_actor_back_to_pool(self, future: Any) -> None:
497
+ """Ad actor assigned to run future back into the pool."""
498
+ actor = self._future_to_actor.pop(future)
499
+ await self.pool.put(actor)
500
+
496
501
  async def fetch_result_and_return_actor_to_pool(
497
502
  self, future: Any
498
503
  ) -> Tuple[Message, Context]:
499
504
  """Pull result given a future and add actor back to pool."""
500
505
  # Get actor that ran job
501
- actor = self._future_to_actor.pop(future)
502
- await self.pool.put(actor)
506
+ await self.add_actor_back_to_pool(future)
503
507
  # Retrieve result for object store
504
508
  # Instead of doing ray.get(future) we await it
505
509
  _, out_mssg, updated_context = await future
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: flwr-nightly
3
- Version: 1.8.0.dev20240327
3
+ Version: 1.8.0.dev20240401
4
4
  Summary: Flower: A Friendly Federated Learning Framework
5
5
  Home-page: https://flower.ai
6
6
  License: Apache-2.0
@@ -28,14 +28,15 @@ flwr/cli/run/__init__.py,sha256=oCd6HmQDx-sqver1gecgx-uMA38BLTSiiKpl7RGNceg,789
28
28
  flwr/cli/run/run.py,sha256=C7Yh-Y0f64PEabb9733jBKIhhOUFpcRmCZJIDtv-NG8,2329
29
29
  flwr/cli/utils.py,sha256=_V2BlFVNNG2naZrq227fZ8o4TxBN_hB-4fQsen9uQoo,2300
30
30
  flwr/client/__init__.py,sha256=futk_IdY_N1h8BTve4Iru51bxm7H1gv58ZPIXWi5XUA,1187
31
- flwr/client/app.py,sha256=xCCpP-fMEFdTEaSWOP93JPIDfjRhx5Z1uI1h6YlJteo,24784
31
+ flwr/client/app.py,sha256=3k1qi_OnN17F7Lwz4zmU6O9Ijsvy9UtfCBjy0qZ3EoY,25445
32
32
  flwr/client/client.py,sha256=Vp9UkOkoHdNfn6iMYZsj_5m_GICiFfUlKEVaLad-YhM,8183
33
- flwr/client/client_app.py,sha256=30Tl_AOEi4CE8wEQbKJ3tWg4GfbsSoV1Ztc8iWE0ge4,8047
33
+ flwr/client/client_app.py,sha256=jqZGliZH7unwJ6y1lQOpGjrMvuOt0P5plVveaPeJBXw,8315
34
34
  flwr/client/dpfedavg_numpy_client.py,sha256=9Tnig4iml2J88HBKNahegjXjbfvIQyBtaIQaqjbeqsA,7435
35
35
  flwr/client/grpc_client/__init__.py,sha256=LsnbqXiJhgQcB0XzAlUQgPx011Uf7Y7yabIC1HxivJ8,735
36
36
  flwr/client/grpc_client/connection.py,sha256=w3Lble9-eCzNOR7fBUsVedVCK4ui9QPhK7i7Ew_a5Vk,8717
37
37
  flwr/client/grpc_rere_client/__init__.py,sha256=avn6W_vHEM_yZEB1S7hCZgnTbXb6ZujqRP_vAzyXu-0,752
38
- flwr/client/grpc_rere_client/connection.py,sha256=3kpnUbS06rNQ969EybGx7zZfQPc2JmCuViyrIt610V0,7421
38
+ flwr/client/grpc_rere_client/connection.py,sha256=boYRkyesnpdUWQL-DSA660tINCDGxRIPdXT8hc9uHfo,8525
39
+ flwr/client/heartbeat.py,sha256=6Ix2Du9SDlXU_nre48WIDUXDy3AVoZsGKacSq2NqT5c,2377
39
40
  flwr/client/message_handler/__init__.py,sha256=abHvBRJJiiaAMNgeILQbMOa6h8WqMK2BcnvxwQZFpic,719
40
41
  flwr/client/message_handler/message_handler.py,sha256=ml_FlduAJ5pxO31n1tKRrWfQRSxkMgKLbwXXcRsNSos,6553
41
42
  flwr/client/message_handler/task_handler.py,sha256=ZDJBKmrn2grRMNl1rU1iGs7FiMHL5VmZiSp_6h9GHVU,1824
@@ -51,11 +52,11 @@ flwr/client/node_state.py,sha256=KTTs_l4I0jBM7IsSsbAGjhfL_yZC3QANbzyvyfZBRDM,177
51
52
  flwr/client/node_state_tests.py,sha256=gPwz0zf2iuDSa11jedkur_u3Xm7lokIDG5ALD2MCvSw,2195
52
53
  flwr/client/numpy_client.py,sha256=u76GWAdHmJM88Agm2EgLQSvO8Jnk225mJTk-_TmPjFE,10283
53
54
  flwr/client/rest_client/__init__.py,sha256=ThwOnkMdzxo_UuyTI47Q7y9oSpuTgNT2OuFvJCfuDiw,735
54
- flwr/client/rest_client/connection.py,sha256=WGkml4gmrbbJ6OAmW3jARmL5nGaOoXo7JjWQbhPhizM,12422
55
+ flwr/client/rest_client/connection.py,sha256=FdK0hQUFBX4jY68wnlRlFJEKADD81fFT8ubhBI0Mm78,14447
55
56
  flwr/client/typing.py,sha256=c9EvjlEjasxn1Wqx6bGl6Xg6vM1gMFfmXht-E2i5J-k,1006
56
57
  flwr/common/__init__.py,sha256=dHOptgKxna78CEQLD5Yu0QIsoSgpIIw5AhIUZCHDWAU,3721
57
58
  flwr/common/address.py,sha256=iTAN9jtmIGMrWFnx9XZQl45ZEtQJVZZLYPRBSNVARGI,1882
58
- flwr/common/constant.py,sha256=vd4d9VRaft3j6hc3MCWuB4zu2_BVjbVOUfu7kLBy0_g,1954
59
+ flwr/common/constant.py,sha256=brSqgGzA21eDqL8WxYOb-MTYcnsAkT4g0wseyRPE7rI,2084
59
60
  flwr/common/context.py,sha256=ounF-mWPPtXGwtae3sg5EhF58ScviOa3MVqxRpGVu-8,1313
60
61
  flwr/common/date.py,sha256=UWhBZj49yX9LD4BmatS_ZFZu_-kweGh0KQJ1djyWWH4,891
61
62
  flwr/common/differential_privacy.py,sha256=WZWrL7C9XaB9l9NDkLDI5PvM7jwcoTTFu08ZVG8-M5Q,6113
@@ -63,8 +64,8 @@ flwr/common/differential_privacy_constants.py,sha256=c7b7tqgvT7yMK0XN9ndiTBs4mQf
63
64
  flwr/common/dp.py,sha256=Hc3lLHihjexbJaD_ft31gdv9XRcwOTgDBwJzICuok3A,2004
64
65
  flwr/common/exit_handlers.py,sha256=2Nt0wLhc17KQQsLPFSRAjjhUiEFfJK6tNozdGiIY4Fs,2812
65
66
  flwr/common/grpc.py,sha256=HimjpTtIY3Vfqtlq3u-CYWjqAl9rSn0uo3A8JjhUmwQ,2273
66
- flwr/common/logger.py,sha256=Plhm9fsi4ewb90eGALQZ9xBkR0cGEsckX5RLSMEaS3M,6118
67
- flwr/common/message.py,sha256=OsYdWVt5ovWTxiGyC3nav61GknoGUyH_p-zR6J-L0Dk,10532
67
+ flwr/common/logger.py,sha256=3hfKun9YISWj4i_QhxgZdnaHJc4x-QvFJQJTKHZ2KHs,6096
68
+ flwr/common/message.py,sha256=vgFSCOkPbl60iS4-XQJ8-rHL54MvNc2AwNSSxVl6qYY,11773
68
69
  flwr/common/object_ref.py,sha256=ELoUCAFO-vbjJC41CGpa-WBG2SLYe3ErW-d9YCG3zqA,4961
69
70
  flwr/common/parameter.py,sha256=-bFAUayToYDF50FZGrBC1hQYJCQDtB2bbr3ZuVLMtdE,2095
70
71
  flwr/common/pyproject.py,sha256=EI_ovbCHGmhYrdPx0RSDi5EkFZFof-8m1PA54c0ZTjc,1385
@@ -76,7 +77,7 @@ flwr/common/record/parametersrecord.py,sha256=WSqtRrYvI-mRzkEwv5s-EG-yE5uizJ8zy9
76
77
  flwr/common/record/recordset.py,sha256=o5UwLubotz1KE9HCoEIP5kK0f0dlIzpFpS1xeQvxo08,3016
77
78
  flwr/common/record/typeddict.py,sha256=2NW8JF27p1uNWaqDbJ7bMkItA5x4ygYT8aHrf8NaqnE,3879
78
79
  flwr/common/recordset_compat.py,sha256=BjxeuvlCaP94yIiKOyFFTRBUH_lprFWSLo8U8q3BDbs,13798
79
- flwr/common/retry_invoker.py,sha256=H_hKqKaEI8vZPywWmoAtJYkcUnKhlYc4kV63zRY0kWA,10856
80
+ flwr/common/retry_invoker.py,sha256=6zpjE5TXw-AuPz6Q4geVW8IU6S8sGaGaehLP3HTmlMw,11669
80
81
  flwr/common/secure_aggregation/__init__.py,sha256=29nHIUO2L8-KhNHQ2KmIgRo_4CPkq4LgLCUN0on5FgI,731
81
82
  flwr/common/secure_aggregation/crypto/__init__.py,sha256=dz7pVx2aPrHxr_AwgO5mIiTzu4PcvUxRq9NLBbFcsf8,738
82
83
  flwr/common/secure_aggregation/crypto/shamir.py,sha256=yY35ZgHlB4YyGW_buG-1X-0M-ejXuQzISgYLgC_Z9TY,2792
@@ -126,7 +127,7 @@ flwr/server/client_proxy.py,sha256=4G-oTwhb45sfWLx2uZdcXD98IZwdTS6F88xe3akCdUg,2
126
127
  flwr/server/compat/__init__.py,sha256=VxnJtJyOjNFQXMNi9hIuzNlZM5n0Hj1p3aq_Pm2udw4,892
127
128
  flwr/server/compat/app.py,sha256=3Skh76Rg80B4oME1dJOhZvn9eTfVmTNIQ0jCiZ6CzeQ,5271
128
129
  flwr/server/compat/app_utils.py,sha256=-Ey5fyRpovmp4nHglVbliITcbxzxX_0qdtZwwfMS4ZI,3450
129
- flwr/server/compat/driver_client_proxy.py,sha256=otGgR_0KOadja4s0GO3zTAO0DoDGzzt6tUq0OHFS8gI,6719
130
+ flwr/server/compat/driver_client_proxy.py,sha256=QWLl5YJwI6NVADwjQGQJqkLtCfPNT-aRH0NF9yeGEnA,7344
130
131
  flwr/server/compat/legacy_context.py,sha256=D2s7PvQoDnTexuRmf1uG9Von7GUj4Qqyr7qLklSlKAM,1766
131
132
  flwr/server/criterion.py,sha256=ypbAexbztzGUxNen9RCHF91QeqiEQix4t4Ih3E-42MM,1061
132
133
  flwr/server/driver/__init__.py,sha256=yYyVX1FcDiDFM6rw0-DSZpuRy0EoWRfG9puwlQUswFA,820
@@ -135,7 +136,7 @@ flwr/server/driver/grpc_driver.py,sha256=D2n3_Es_DHFgQsq_TjYVEz8RYJJJYoe24E1voza
135
136
  flwr/server/history.py,sha256=hDsoBaA4kUa6d1yvDVXuLluBqOBKSm0_fVDtUtYJkmg,5121
136
137
  flwr/server/run_serverapp.py,sha256=3hoXa57T4L1vOWVWPSSdZ_UyRO-uTwUIrhha6TJAXMg,5592
137
138
  flwr/server/server.py,sha256=UnBRlI6AGTj0nKeRtEQ3IalM3TJmggMKXhDyn8yKZNk,17664
138
- flwr/server/server_app.py,sha256=lYUzvzgoPSkOB_6ZxrLcpnPixKQY8Uq5XVD8Mb1Cino,4280
139
+ flwr/server/server_app.py,sha256=KgAT_HqsfseTLNnfX2ph42PBbVqQ0lFzvYrT90V34y0,4402
139
140
  flwr/server/server_config.py,sha256=CZaHVAsMvGLjpWVcLPkiYxgJN4xfIyAiUrCI3fETKY4,1349
140
141
  flwr/server/strategy/__init__.py,sha256=7eVZ3hQEg2BgA_usAeL6tsLp9T6XI1VYYoFy08Xn-ew,2836
141
142
  flwr/server/strategy/aggregate.py,sha256=QyRIJtI5gnuY1NbgrcrOvkHxGIxBvApq7d9Y4xl-6W4,13468
@@ -174,14 +175,14 @@ flwr/server/superlink/fleet/grpc_bidi/grpc_server.py,sha256=1QyBX5qcFPjMVlv7Trvn
174
175
  flwr/server/superlink/fleet/grpc_rere/__init__.py,sha256=bEJOMWbSlqkw-y5ZHtEXczhoSlAxErcRYffmTMQAV8M,758
175
176
  flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py,sha256=LC_ntiLZMIZkspwjtQ9_MZ4agzArebO4HIVJ3YOrFx8,3036
176
177
  flwr/server/superlink/fleet/message_handler/__init__.py,sha256=hEY0l61ojH8Iz30_K1btm1HJ6J49iZJSFUsVYqUTw3A,731
177
- flwr/server/superlink/fleet/message_handler/message_handler.py,sha256=N5VqopkVuXWg0h1_qzINUQ59bFKq-CXbKSi5DmSSrxs,3161
178
+ flwr/server/superlink/fleet/message_handler/message_handler.py,sha256=jz8l9xZX_ekv_TUv7pdYsByUWbvnLj1PfsOdct1gZ38,3238
178
179
  flwr/server/superlink/fleet/rest_rere/__init__.py,sha256=VKDvDq5H8koOUztpmQacVzGJXPLEEkL1Vmolxt3mvnY,735
179
- flwr/server/superlink/fleet/rest_rere/rest_api.py,sha256=7JCs7NW4Qq8W5QhXxqsQNFiCLlRY-b_iD420vH1Mu-U,5906
180
+ flwr/server/superlink/fleet/rest_rere/rest_api.py,sha256=_tGtARm4x957Fu1EWoDieqOzV9CQZTM4GgKe2GxIOvw,6734
180
181
  flwr/server/superlink/fleet/vce/__init__.py,sha256=36MHKiefnJeyjwMQzVUK4m06Ojon3WDcwZGQsAcyVhQ,783
181
182
  flwr/server/superlink/fleet/vce/backend/__init__.py,sha256=oBIzmnrSSRvH_H0vRGEGWhWzQQwqe3zn6e13RsNwlIY,1466
182
183
  flwr/server/superlink/fleet/vce/backend/backend.py,sha256=LJsKl7oixVvptcG98Rd9ejJycNWcEVB0ODvSreLGp-A,2260
183
- flwr/server/superlink/fleet/vce/backend/raybackend.py,sha256=BYgzVH1uz8nk6mOP6GhgSxjrdCe7xtkzb7nhPbKStFM,6317
184
- flwr/server/superlink/fleet/vce/vce_api.py,sha256=Yq4i9fduafnoWSHCLn0mmkCTS9oZqwycH8gbKa4bPXo,11168
184
+ flwr/server/superlink/fleet/vce/backend/raybackend.py,sha256=TaT2EpbVEsIY0EDzF8obadyZaSXjD38TFGdDPI-ytD0,6375
185
+ flwr/server/superlink/fleet/vce/vce_api.py,sha256=8zfJf2_BtvxZHI0-Konpa6sTg9l4KXOLgCIkt38C6CE,12041
185
186
  flwr/server/superlink/state/__init__.py,sha256=ij-7Ms-hyordQdRmGQxY1-nVa4OhixJ0jr7_YDkys0s,1003
186
187
  flwr/server/superlink/state/in_memory_state.py,sha256=XfdCGRzFut9xlf7AsDAhhAmBw-nKDBjmPWAI--espj0,8707
187
188
  flwr/server/superlink/state/sqlite_state.py,sha256=1SR6Zz6ud0tSSx940gTfa0vct_GH2n0cX_vnhoAEMlQ,22005
@@ -200,12 +201,12 @@ flwr/server/workflow/secure_aggregation/secaggplus_workflow.py,sha256=3TjJdhYA4x
200
201
  flwr/simulation/__init__.py,sha256=hpoKzdovrH0_Cf8HIcXxQxyUUb3BiSk-WUNLf5STHcc,1400
201
202
  flwr/simulation/app.py,sha256=WqJxdXTEuehwMW605p5NMmvBbKYx5tuqnV3Mp7jSWXM,13904
202
203
  flwr/simulation/ray_transport/__init__.py,sha256=FsaAnzC4cw4DqoouBCix6496k29jACkfeIam55BvW9g,734
203
- flwr/simulation/ray_transport/ray_actor.py,sha256=zRETW_xuCAOLRFaYnQ-q3IBSz0LIv_0RifGuhgWaYOg,19872
204
+ flwr/simulation/ray_transport/ray_actor.py,sha256=OWjgYW--fswkEDqTP9L_cZblBeUVL59vNz5gvzPAHFk,20054
204
205
  flwr/simulation/ray_transport/ray_client_proxy.py,sha256=oDu4sEPIOu39vrNi-fqDAe10xtNUXMO49bM2RWfRcyw,6738
205
206
  flwr/simulation/ray_transport/utils.py,sha256=TYdtfg1P9VfTdLMOJlifInGpxWHYs9UfUqIv2wfkRLA,2392
206
207
  flwr/simulation/run_simulation.py,sha256=HiIH6aa_v56NfKQN5ZBd94NyVfaZNyFs43_kItYsQXU,15685
207
- flwr_nightly-1.8.0.dev20240327.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
208
- flwr_nightly-1.8.0.dev20240327.dist-info/METADATA,sha256=k34dkRQ99eG3DkdjPCB3bEEllC1gMNxnYJBXZtHv6P4,15257
209
- flwr_nightly-1.8.0.dev20240327.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
210
- flwr_nightly-1.8.0.dev20240327.dist-info/entry_points.txt,sha256=utu2wybGyYJSTtsB2ktY_gmy-XtMFo9EFZdishX0zR4,320
211
- flwr_nightly-1.8.0.dev20240327.dist-info/RECORD,,
208
+ flwr_nightly-1.8.0.dev20240401.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
209
+ flwr_nightly-1.8.0.dev20240401.dist-info/METADATA,sha256=9EApt2x0ajI0-A4ms8brLZ45cWsrgTPSmrKs6X1maPs,15257
210
+ flwr_nightly-1.8.0.dev20240401.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
211
+ flwr_nightly-1.8.0.dev20240401.dist-info/entry_points.txt,sha256=utu2wybGyYJSTtsB2ktY_gmy-XtMFo9EFZdishX0zR4,320
212
+ flwr_nightly-1.8.0.dev20240401.dist-info/RECORD,,