flwr-nightly 1.8.0.dev20240328__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
@@ -36,7 +36,7 @@ from flwr.common.constant import (
36
36
  TRANSPORT_TYPES,
37
37
  )
38
38
  from flwr.common.exit_handlers import register_exit_handlers
39
- from flwr.common.logger import log, warn_deprecated_feature, warn_experimental_feature
39
+ from flwr.common.logger import log, warn_deprecated_feature
40
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
@@ -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,
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,
@@ -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)
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(
@@ -223,6 +223,7 @@ async def run(
223
223
 
224
224
 
225
225
  # pylint: disable=too-many-arguments,unused-argument,too-many-locals,too-many-branches
226
+ # pylint: disable=too-many-statements
226
227
  def start_vce(
227
228
  backend_name: str,
228
229
  backend_config_json_stream: str,
@@ -341,6 +342,13 @@ def start_vce(
341
342
  )
342
343
  )
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,
350
+ )
351
+ time.sleep(f_stop_delay)
344
352
  f_stop.set() # set termination event
345
353
  raise loadapp_ex
346
354
  except Exception as ex:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: flwr-nightly
3
- Version: 1.8.0.dev20240328
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=bRrLIfn3MIZKfsVq8AlgYmhpI08UTioaOlyx6YLaLR4,25536
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,7 +64,7 @@ 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/logger.py,sha256=3hfKun9YISWj4i_QhxgZdnaHJc4x-QvFJQJTKHZ2KHs,6096
67
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
@@ -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
@@ -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
184
  flwr/server/superlink/fleet/vce/backend/raybackend.py,sha256=TaT2EpbVEsIY0EDzF8obadyZaSXjD38TFGdDPI-ytD0,6375
184
- flwr/server/superlink/fleet/vce/vce_api.py,sha256=EV4ISvHZPucVLD3lYVFF5fQ4yyxmoaoZMdp_1B2k6J8,11789
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
@@ -204,8 +205,8 @@ flwr/simulation/ray_transport/ray_actor.py,sha256=OWjgYW--fswkEDqTP9L_cZblBeUVL5
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.dev20240328.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
208
- flwr_nightly-1.8.0.dev20240328.dist-info/METADATA,sha256=8FreyB01iNsm1LCt2zh74ZE46oAFcKAieUIrZQEtTgI,15257
209
- flwr_nightly-1.8.0.dev20240328.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
210
- flwr_nightly-1.8.0.dev20240328.dist-info/entry_points.txt,sha256=utu2wybGyYJSTtsB2ktY_gmy-XtMFo9EFZdishX0zR4,320
211
- flwr_nightly-1.8.0.dev20240328.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,,