flwr-nightly 1.19.0.dev20250606__py3-none-any.whl → 1.19.0.dev20250609__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.
@@ -30,18 +30,16 @@ from flwr.common import GRPC_MAX_MESSAGE_LENGTH
30
30
  from flwr.common.constant import HEARTBEAT_CALL_TIMEOUT, HEARTBEAT_DEFAULT_INTERVAL
31
31
  from flwr.common.grpc import create_channel, on_channel_state_change
32
32
  from flwr.common.heartbeat import HeartbeatSender
33
+ from flwr.common.inflatable import get_all_nested_objects
33
34
  from flwr.common.inflatable_grpc_utils import (
35
+ inflate_object_from_contents,
34
36
  make_pull_object_fn_grpc,
35
37
  make_push_object_fn_grpc,
36
- pull_object_from_servicer,
37
- push_object_to_servicer,
38
+ pull_objects,
39
+ push_objects,
38
40
  )
39
41
  from flwr.common.logger import log
40
- from flwr.common.message import (
41
- Message,
42
- get_message_to_descendant_id_mapping,
43
- remove_content_from_message,
44
- )
42
+ from flwr.common.message import Message, remove_content_from_message
45
43
  from flwr.common.retry_invoker import RetryInvoker, _wrap_stub
46
44
  from flwr.common.secure_aggregation.crypto.symmetric_encryption import (
47
45
  generate_key_pairs,
@@ -62,6 +60,7 @@ from flwr.proto.heartbeat_pb2 import ( # pylint: disable=E0611
62
60
  SendNodeHeartbeatRequest,
63
61
  SendNodeHeartbeatResponse,
64
62
  )
63
+ from flwr.proto.message_pb2 import ObjectIDs # pylint: disable=E0611
65
64
  from flwr.proto.node_pb2 import Node # pylint: disable=E0611
66
65
  from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611
67
66
 
@@ -267,23 +266,21 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
267
266
  in_message: Optional[Message] = None
268
267
 
269
268
  if message_proto:
270
- in_message = cast(
271
- Message,
272
- pull_object_from_servicer(
273
- object_id=message_proto.metadata.message_id,
274
- pull_object_fn=make_pull_object_fn_grpc(
275
- pull_object_grpc=stub.PullObject,
276
- node=node,
277
- run_id=message_proto.metadata.run_id,
278
- ),
269
+ msg_id = message_proto.metadata.message_id
270
+ all_object_contents = pull_objects(
271
+ list(response.objects_to_pull[msg_id].object_ids) + [msg_id],
272
+ pull_object_fn=make_pull_object_fn_grpc(
273
+ pull_object_grpc=stub.PullObject,
274
+ node=node,
275
+ run_id=message_proto.metadata.run_id,
279
276
  ),
280
277
  )
281
-
282
- if in_message:
278
+ in_message = cast(
279
+ Message, inflate_object_from_contents(msg_id, all_object_contents)
280
+ )
283
281
  # The deflated message doesn't contain the message_id (its own object_id)
284
282
  # Inject
285
- # pylint: disable-next=W0212
286
- in_message.metadata._message_id = message_proto.metadata.message_id # type: ignore
283
+ in_message.metadata.__dict__["_message_id"] = msg_id
287
284
 
288
285
  # Remember `metadata` of the in message
289
286
  nonlocal metadata
@@ -312,20 +309,25 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
312
309
  log(ERROR, "Invalid out message")
313
310
  return
314
311
 
312
+ # Get all nested objects
313
+ all_objects = get_all_nested_objects(message)
314
+ all_object_ids = list(all_objects.keys())
315
+ msg_id = all_object_ids[-1] # Last object is the message itself
316
+ descendant_ids = all_object_ids[:-1] # All but the last object are descendants
317
+
315
318
  # Serialize Message
316
319
  message_proto = message_to_proto(message=remove_content_from_message(message))
317
- descendants_mapping = get_message_to_descendant_id_mapping(message)
318
320
  request = PushMessagesRequest(
319
321
  node=node,
320
322
  messages_list=[message_proto],
321
- msg_to_descendant_mapping=descendants_mapping,
323
+ msg_to_descendant_mapping={msg_id: ObjectIDs(object_ids=descendant_ids)},
322
324
  )
323
325
  response: PushMessagesResponse = stub.PushMessages(request=request)
324
326
 
325
327
  if response.objects_to_push:
326
328
  objs_to_push = set(response.objects_to_push[message.object_id].object_ids)
327
- push_object_to_servicer(
328
- message,
329
+ push_objects(
330
+ all_objects,
329
331
  push_object_fn=make_push_object_fn_grpc(
330
332
  push_object_grpc=stub.PushObject,
331
333
  node=node,
flwr/common/constant.py CHANGED
@@ -138,6 +138,17 @@ HEAD_VALUE_DIVIDER = " "
138
138
  # Constants for serialization
139
139
  INT64_MAX_VALUE = 9223372036854775807 # (1 << 63) - 1
140
140
 
141
+ # Constants for `flwr-serverapp` and `flwr-clientapp` CLI commands
142
+ FLWR_APP_TOKEN_LENGTH = 128 # Length of the token used
143
+
144
+ # Constants for object pushing and pulling
145
+ MAX_CONCURRENT_PUSHES = 8 # Default maximum number of concurrent pushes
146
+ MAX_CONCURRENT_PULLS = 8 # Default maximum number of concurrent pulls
147
+ PULL_MAX_TIME = 7200 # Default maximum time to wait for pulling objects
148
+ PULL_MAX_TRIES_PER_OBJECT = 500 # Default maximum number of tries to pull an object
149
+ PULL_INITIAL_BACKOFF = 1 # Initial backoff time for pulling objects
150
+ PULL_BACKOFF_CAP = 10 # Maximum backoff time for pulling objects
151
+
141
152
 
142
153
  class MessageType:
143
154
  """Message type."""
flwr/common/inflatable.py CHANGED
@@ -18,11 +18,19 @@
18
18
  from __future__ import annotations
19
19
 
20
20
  import hashlib
21
- from logging import ERROR
22
21
  from typing import TypeVar, cast
23
22
 
24
23
  from .constant import HEAD_BODY_DIVIDER, HEAD_VALUE_DIVIDER
25
- from .logger import log
24
+
25
+
26
+ class UnexpectedObjectContentError(Exception):
27
+ """Exception raised when the content of an object does not conform to the expected
28
+ structure for an InflatableObject (i.e., head, body, and values within the head)."""
29
+
30
+ def __init__(self, object_id: str, reason: str):
31
+ super().__init__(
32
+ f"Object with ID '{object_id}' has an unexpected structure. {reason}"
33
+ )
26
34
 
27
35
 
28
36
  class InflatableObject:
@@ -117,12 +125,14 @@ def add_header_to_object_body(object_body: bytes, obj: InflatableObject) -> byte
117
125
 
118
126
  def _get_object_head(object_content: bytes) -> bytes:
119
127
  """Return object head from object content."""
120
- return object_content.split(HEAD_BODY_DIVIDER, 1)[0]
128
+ index = object_content.find(HEAD_BODY_DIVIDER)
129
+ return object_content[:index]
121
130
 
122
131
 
123
132
  def _get_object_body(object_content: bytes) -> bytes:
124
133
  """Return object body from object content."""
125
- return object_content.split(HEAD_BODY_DIVIDER, 1)[1]
134
+ index = object_content.find(HEAD_BODY_DIVIDER)
135
+ return object_content[index + len(HEAD_BODY_DIVIDER) :]
126
136
 
127
137
 
128
138
  def is_valid_sha256_hash(object_id: str) -> bool:
@@ -163,16 +173,6 @@ def get_object_body_len_from_object_content(object_content: bytes) -> int:
163
173
  return get_object_head_values_from_object_content(object_content)[2]
164
174
 
165
175
 
166
- def check_body_len_consistency(object_content: bytes) -> bool:
167
- """Check that the object body is of length as specified in the head."""
168
- try:
169
- body_len = get_object_body_len_from_object_content(object_content)
170
- return body_len == len(_get_object_body(object_content))
171
- except ValueError:
172
- log(ERROR, "Object content does match the expected format.")
173
- return False
174
-
175
-
176
176
  def get_object_head_values_from_object_content(
177
177
  object_content: bytes,
178
178
  ) -> tuple[str, list[str], int]:
@@ -197,21 +197,25 @@ def get_object_head_values_from_object_content(
197
197
  return obj_type, children_ids, int(body_len)
198
198
 
199
199
 
200
- def _get_descendants_object_ids_recursively(obj: InflatableObject) -> set[str]:
200
+ def get_descendant_object_ids(obj: InflatableObject) -> set[str]:
201
+ """Get a set of object IDs of all descendants."""
202
+ descendants = set(get_all_nested_objects(obj).keys())
203
+ # Exclude Object ID of parent object
204
+ descendants.discard(obj.object_id)
205
+ return descendants
201
206
 
202
- descendants: set[str] = set()
203
- if children := obj.children:
204
- for child in children.values():
205
- descendants |= _get_descendants_object_ids_recursively(child)
206
207
 
207
- descendants.add(obj.object_id)
208
+ def get_all_nested_objects(obj: InflatableObject) -> dict[str, InflatableObject]:
209
+ """Get a dictionary of all nested objects, including the object itself.
208
210
 
209
- return descendants
211
+ Each key in the dictionary is an object ID, and the entries are ordered by post-
212
+ order traversal, i.e., child objects appear before their respective parents.
213
+ """
214
+ ret: dict[str, InflatableObject] = {}
215
+ if children := obj.children:
216
+ for child in children.values():
217
+ ret.update(get_all_nested_objects(child))
210
218
 
219
+ ret[obj.object_id] = obj
211
220
 
212
- def get_desdendant_object_ids(obj: InflatableObject) -> set[str]:
213
- """Get a set of object IDs of all descendants."""
214
- descendants = _get_descendants_object_ids_recursively(obj)
215
- # Exclude Object ID of parent object
216
- descendants.discard(obj.object_id)
217
- return descendants
221
+ return ret
@@ -15,7 +15,10 @@
15
15
  """InflatableObject utils."""
16
16
 
17
17
 
18
- from time import sleep
18
+ import concurrent.futures
19
+ import random
20
+ import threading
21
+ import time
19
22
  from typing import Callable, Optional
20
23
 
21
24
  from flwr.proto.message_pb2 import ( # pylint: disable=E0611
@@ -26,11 +29,15 @@ from flwr.proto.message_pb2 import ( # pylint: disable=E0611
26
29
  )
27
30
  from flwr.proto.node_pb2 import Node # pylint: disable=E0611
28
31
 
29
- from .inflatable import (
30
- InflatableObject,
31
- get_object_head_values_from_object_content,
32
- get_object_id,
32
+ from .constant import (
33
+ MAX_CONCURRENT_PULLS,
34
+ MAX_CONCURRENT_PUSHES,
35
+ PULL_BACKOFF_CAP,
36
+ PULL_INITIAL_BACKOFF,
37
+ PULL_MAX_TIME,
38
+ PULL_MAX_TRIES_PER_OBJECT,
33
39
  )
40
+ from .inflatable import InflatableObject, get_object_head_values_from_object_content
34
41
  from .message import Message
35
42
  from .record import Array, ArrayRecord, ConfigRecord, MetricRecord, RecordDict
36
43
 
@@ -60,102 +67,6 @@ class ObjectIdNotPreregisteredError(Exception):
60
67
  super().__init__(f"Object with ID '{object_id}' could not be found.")
61
68
 
62
69
 
63
- def push_object_to_servicer(
64
- obj: InflatableObject,
65
- push_object_fn: Callable[[str, bytes], None],
66
- object_ids_to_push: Optional[set[str]] = None,
67
- ) -> set[str]:
68
- """Recursively deflate an object and push it to the servicer.
69
-
70
- Objects with the same ID are not pushed twice. If `object_ids_to_push` is set,
71
- only objects with those IDs are pushed. It returns the set of pushed object
72
- IDs.
73
-
74
- Parameters
75
- ----------
76
- obj : InflatableObject
77
- The object to push.
78
- push_object_fn : Callable[[str, bytes], None]
79
- A function that takes an object ID and its content as bytes, and pushes
80
- it to the servicer. This function should raise `ObjectIdNotPreregisteredError`
81
- if the object ID is not pre-registered.
82
- object_ids_to_push : Optional[set[str]] (default: None)
83
- A set of object IDs to push. If object ID of the given object is not in this
84
- set, the object will not be pushed.
85
-
86
- Returns
87
- -------
88
- set[str]
89
- A set of object IDs that were pushed to the servicer.
90
- """
91
- pushed_object_ids: set[str] = set()
92
- # Push children if it has any
93
- if children := obj.children:
94
- for child in children.values():
95
- pushed_object_ids |= push_object_to_servicer(
96
- child, push_object_fn, object_ids_to_push
97
- )
98
-
99
- # Deflate object and push
100
- object_content = obj.deflate()
101
- object_id = get_object_id(object_content)
102
- # Push always if no object set is specified, or if the object is in the set
103
- if object_ids_to_push is None or object_id in object_ids_to_push:
104
- # The function may raise an error if the object ID is not pre-registered
105
- push_object_fn(object_id, object_content)
106
- pushed_object_ids.add(object_id)
107
-
108
- return pushed_object_ids
109
-
110
-
111
- def pull_object_from_servicer(
112
- object_id: str,
113
- pull_object_fn: Callable[[str], bytes],
114
- ) -> InflatableObject:
115
- """Recursively inflate an object by pulling it from the servicer.
116
-
117
- Parameters
118
- ----------
119
- object_id : str
120
- The ID of the object to pull.
121
- pull_object_fn : Callable[[str], bytes]
122
- A function that takes an object ID and returns the object content as bytes.
123
- The function should raise `ObjectUnavailableError` if the object is not yet
124
- available, or `ObjectIdNotPreregisteredError` if the object ID is not
125
- pre-registered.
126
-
127
- Returns
128
- -------
129
- InflatableObject
130
- The pulled object.
131
- """
132
- # Pull object
133
- while True:
134
- try:
135
- # The function may raise an error if the object ID is not pre-registered
136
- object_content: bytes = pull_object_fn(object_id)
137
- break # Exit loop if object is successfully pulled
138
- except ObjectUnavailableError:
139
- sleep(0.1) # Retry after a short delay
140
-
141
- # Extract object class and object_ids of children
142
- obj_type, children_obj_ids, _ = get_object_head_values_from_object_content(
143
- object_content=object_content
144
- )
145
- # Resolve object class
146
- cls_type = inflatable_class_registry[obj_type]
147
-
148
- # Pull all children objects
149
- children: dict[str, InflatableObject] = {}
150
- for child_object_id in children_obj_ids:
151
- children[child_object_id] = pull_object_from_servicer(
152
- child_object_id, pull_object_fn
153
- )
154
-
155
- # Inflate object passing its children
156
- return cls_type.inflate(object_content, children=children)
157
-
158
-
159
70
  def make_pull_object_fn_grpc(
160
71
  pull_object_grpc: Callable[[PullObjectRequest], PullObjectResponse],
161
72
  node: Node,
@@ -225,3 +136,227 @@ def make_push_object_fn_grpc(
225
136
  raise ObjectIdNotPreregisteredError(object_id)
226
137
 
227
138
  return push_object_fn
139
+
140
+
141
+ def push_objects(
142
+ objects: dict[str, InflatableObject],
143
+ push_object_fn: Callable[[str, bytes], None],
144
+ *,
145
+ object_ids_to_push: Optional[set[str]] = None,
146
+ keep_objects: bool = False,
147
+ max_concurrent_pushes: int = MAX_CONCURRENT_PUSHES,
148
+ ) -> None:
149
+ """Push multiple objects to the servicer.
150
+
151
+ Parameters
152
+ ----------
153
+ objects : dict[str, InflatableObject]
154
+ A dictionary of objects to push, where keys are object IDs and values are
155
+ `InflatableObject` instances.
156
+ push_object_fn : Callable[[str, bytes], None]
157
+ A function that takes an object ID and its content as bytes, and pushes
158
+ it to the servicer. This function should raise `ObjectIdNotPreregisteredError`
159
+ if the object ID is not pre-registered.
160
+ object_ids_to_push : Optional[set[str]] (default: None)
161
+ A set of object IDs to push. If not provided, all objects will be pushed.
162
+ keep_objects : bool (default: False)
163
+ If `True`, the original objects will be kept in the `objects` dictionary
164
+ after pushing. If `False`, they will be removed from the dictionary to avoid
165
+ high memory usage.
166
+ max_concurrent_pushes : int (default: MAX_CONCURRENT_PUSHES)
167
+ The maximum number of concurrent pushes to perform.
168
+ """
169
+ if object_ids_to_push is not None:
170
+ # Filter objects to push only those with IDs in the set
171
+ objects = {k: v for k, v in objects.items() if k in object_ids_to_push}
172
+
173
+ lock = threading.Lock()
174
+
175
+ def push(obj_id: str) -> None:
176
+ """Push a single object."""
177
+ object_content = objects[obj_id].deflate()
178
+ if not keep_objects:
179
+ with lock:
180
+ del objects[obj_id]
181
+ push_object_fn(obj_id, object_content)
182
+
183
+ with concurrent.futures.ThreadPoolExecutor(
184
+ max_workers=max_concurrent_pushes
185
+ ) as executor:
186
+ list(executor.map(push, list(objects.keys())))
187
+
188
+
189
+ def pull_objects( # pylint: disable=too-many-arguments
190
+ object_ids: list[str],
191
+ pull_object_fn: Callable[[str], bytes],
192
+ *,
193
+ max_concurrent_pulls: int = MAX_CONCURRENT_PULLS,
194
+ max_time: Optional[float] = PULL_MAX_TIME,
195
+ max_tries_per_object: Optional[int] = PULL_MAX_TRIES_PER_OBJECT,
196
+ initial_backoff: float = PULL_INITIAL_BACKOFF,
197
+ backoff_cap: float = PULL_BACKOFF_CAP,
198
+ ) -> dict[str, bytes]:
199
+ """Pull multiple objects from the servicer.
200
+
201
+ Parameters
202
+ ----------
203
+ object_ids : list[str]
204
+ A list of object IDs to pull.
205
+ pull_object_fn : Callable[[str], bytes]
206
+ A function that takes an object ID and returns the object content as bytes.
207
+ The function should raise `ObjectUnavailableError` if the object is not yet
208
+ available, or `ObjectIdNotPreregisteredError` if the object ID is not
209
+ pre-registered.
210
+ max_concurrent_pulls : int (default: MAX_CONCURRENT_PULLS)
211
+ The maximum number of concurrent pulls to perform.
212
+ max_time : Optional[float] (default: PULL_MAX_TIME)
213
+ The maximum time to wait for all pulls to complete. If `None`, waits
214
+ indefinitely.
215
+ max_tries_per_object : Optional[int] (default: PULL_MAX_TRIES_PER_OBJECT)
216
+ The maximum number of attempts to pull each object. If `None`, pulls
217
+ indefinitely until the object is available.
218
+ initial_backoff : float (default: PULL_INITIAL_BACKOFF)
219
+ The initial backoff time in seconds for retrying pulls after an
220
+ `ObjectUnavailableError`.
221
+ backoff_cap : float (default: PULL_BACKOFF_CAP)
222
+ The maximum backoff time in seconds. Backoff times will not exceed this value.
223
+
224
+ Returns
225
+ -------
226
+ dict[str, bytes]
227
+ A dictionary where keys are object IDs and values are the pulled
228
+ object contents.
229
+ """
230
+ if max_tries_per_object is None:
231
+ max_tries_per_object = int(1e9)
232
+ if max_time is None:
233
+ max_time = float("inf")
234
+
235
+ results: dict[str, bytes] = {}
236
+ results_lock = threading.Lock()
237
+ err_to_raise: Optional[Exception] = None
238
+ early_stop = threading.Event()
239
+ start = time.monotonic()
240
+
241
+ def pull_with_retries(object_id: str) -> None:
242
+ """Attempt to pull a single object with retry and backoff."""
243
+ nonlocal err_to_raise
244
+ tries = 0
245
+ delay = initial_backoff
246
+
247
+ while not early_stop.is_set():
248
+ try:
249
+ object_content = pull_object_fn(object_id)
250
+ with results_lock:
251
+ results[object_id] = object_content
252
+ return
253
+
254
+ except ObjectUnavailableError as err:
255
+ tries += 1
256
+ if (
257
+ tries >= max_tries_per_object
258
+ or time.monotonic() - start >= max_time
259
+ ):
260
+ # Stop all work if one object exhausts retries
261
+ early_stop.set()
262
+ with results_lock:
263
+ if err_to_raise is None:
264
+ err_to_raise = err
265
+ return
266
+
267
+ # Apply exponential backoff with ±20% jitter
268
+ sleep_time = delay * (1 + random.uniform(-0.2, 0.2))
269
+ early_stop.wait(sleep_time)
270
+ delay = min(delay * 2, backoff_cap)
271
+
272
+ except ObjectIdNotPreregisteredError as err:
273
+ # Permanent failure: object ID is invalid
274
+ early_stop.set()
275
+ with results_lock:
276
+ if err_to_raise is None:
277
+ err_to_raise = err
278
+ return
279
+
280
+ # Submit all pull tasks concurrently
281
+ with concurrent.futures.ThreadPoolExecutor(
282
+ max_workers=max_concurrent_pulls
283
+ ) as executor:
284
+ futures = {
285
+ executor.submit(pull_with_retries, obj_id): obj_id for obj_id in object_ids
286
+ }
287
+
288
+ # Wait for completion
289
+ concurrent.futures.wait(futures)
290
+
291
+ if err_to_raise is not None:
292
+ raise err_to_raise
293
+
294
+ return results
295
+
296
+
297
+ def inflate_object_from_contents(
298
+ object_id: str,
299
+ object_contents: dict[str, bytes],
300
+ *,
301
+ keep_object_contents: bool = False,
302
+ objects: Optional[dict[str, InflatableObject]] = None,
303
+ ) -> InflatableObject:
304
+ """Inflate an object from object contents.
305
+
306
+ Parameters
307
+ ----------
308
+ object_id : str
309
+ The ID of the object to inflate.
310
+ object_contents : dict[str, bytes]
311
+ A dictionary mapping object IDs to their contents as bytes.
312
+ All descendant objects must be present in this dictionary.
313
+ keep_object_contents : bool (default: False)
314
+ If `True`, the object content will be kept in the `object_contents`
315
+ dictionary after inflation. If `False`, the object content will be
316
+ removed from the dictionary to save memory.
317
+ objects : Optional[dict[str, InflatableObject]] (default: None)
318
+ No need to provide this parameter. A dictionary to store already
319
+ inflated objects, mapping object IDs to their corresponding
320
+ `InflatableObject` instances.
321
+
322
+ Returns
323
+ -------
324
+ InflatableObject
325
+ The inflated object.
326
+ """
327
+ if objects is None:
328
+ # Initialize objects dictionary
329
+ objects = {}
330
+
331
+ if object_id in objects:
332
+ # If the object is already in the objects dictionary, return it
333
+ return objects[object_id]
334
+
335
+ # Extract object class and object_ids of children
336
+ object_content = object_contents[object_id]
337
+ obj_type, children_obj_ids, _ = get_object_head_values_from_object_content(
338
+ object_content=object_contents[object_id]
339
+ )
340
+
341
+ # Remove the object content from the dictionary to save memory
342
+ if not keep_object_contents:
343
+ del object_contents[object_id]
344
+
345
+ # Resolve object class
346
+ cls_type = inflatable_class_registry[obj_type]
347
+
348
+ # Inflate all children objects
349
+ children: dict[str, InflatableObject] = {}
350
+ for child_obj_id in children_obj_ids:
351
+ children[child_obj_id] = inflate_object_from_contents(
352
+ child_obj_id,
353
+ object_contents,
354
+ keep_object_contents=keep_object_contents,
355
+ objects=objects,
356
+ )
357
+
358
+ # Inflate object passing its children
359
+ obj = cls_type.inflate(object_content, children=children)
360
+ del object_content # Free memory after inflation
361
+ objects[object_id] = obj
362
+ return obj
@@ -0,0 +1,75 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """InflatableObject utilities."""
16
+
17
+
18
+ from .constant import HEAD_BODY_DIVIDER, HEAD_VALUE_DIVIDER
19
+ from .inflatable import (
20
+ UnexpectedObjectContentError,
21
+ _get_object_head,
22
+ get_object_id,
23
+ is_valid_sha256_hash,
24
+ )
25
+ from .inflatable_grpc_utils import inflatable_class_registry
26
+
27
+
28
+ def validate_object_content(content: bytes) -> None:
29
+ """Validate the deflated content of an InflatableObject."""
30
+ try:
31
+ # Check if there is a head-body divider
32
+ index = content.find(HEAD_BODY_DIVIDER)
33
+ if index == -1:
34
+ raise ValueError(
35
+ "Unexpected format for object content. Head and body "
36
+ "could not be split."
37
+ )
38
+
39
+ head = _get_object_head(content)
40
+
41
+ # check if the head has three parts:
42
+ # <object_type> <children_ids> <object_body_len>
43
+ head_decoded = head.decode(encoding="utf-8")
44
+ head_parts = head_decoded.split(HEAD_VALUE_DIVIDER)
45
+
46
+ if len(head_parts) != 3:
47
+ raise ValueError("Unexpected format for object head.")
48
+
49
+ obj_type, children_str, body_len = head_parts
50
+
51
+ # Check that children IDs are valid IDs
52
+ children = children_str.split(",")
53
+ for children_id in children:
54
+ if children_id and not is_valid_sha256_hash(children_id):
55
+ raise ValueError(
56
+ f"Detected invalid object ID ({children_id}) in children."
57
+ )
58
+
59
+ # Check that object type is recognized
60
+ if obj_type not in inflatable_class_registry:
61
+ if obj_type != "CustomDataClass": # to allow for the class in tests
62
+ raise ValueError(f"Object of type {obj_type} is not supported.")
63
+
64
+ # Check if the body length in the head matches that of the body
65
+ actual_body_len = len(content) - len(head) - len(HEAD_BODY_DIVIDER)
66
+ if actual_body_len != int(body_len):
67
+ raise ValueError(
68
+ f"Object content length expected {body_len} bytes but got "
69
+ f"{actual_body_len} bytes."
70
+ )
71
+
72
+ except ValueError as err:
73
+ raise UnexpectedObjectContentError(
74
+ object_id=get_object_id(content), reason=str(err)
75
+ ) from err
flwr/common/message.py CHANGED
@@ -32,7 +32,7 @@ from .constant import MESSAGE_TTL_TOLERANCE
32
32
  from .inflatable import (
33
33
  InflatableObject,
34
34
  add_header_to_object_body,
35
- get_desdendant_object_ids,
35
+ get_descendant_object_ids,
36
36
  get_object_body,
37
37
  get_object_children_ids_from_object_content,
38
38
  )
@@ -524,6 +524,6 @@ def get_message_to_descendant_id_mapping(message: Message) -> dict[str, ObjectID
524
524
  """Construct a mapping between message object_id and that of its descendants."""
525
525
  return {
526
526
  message.object_id: ObjectIDs(
527
- object_ids=list(get_desdendant_object_ids(message))
527
+ object_ids=list(get_descendant_object_ids(message))
528
528
  )
529
529
  }
@@ -28,20 +28,20 @@ from flwr.common.constant import (
28
28
  SUPERLINK_NODE_ID,
29
29
  )
30
30
  from flwr.common.grpc import create_channel, on_channel_state_change
31
+ from flwr.common.inflatable import get_all_nested_objects
31
32
  from flwr.common.inflatable_grpc_utils import (
33
+ inflate_object_from_contents,
32
34
  make_pull_object_fn_grpc,
33
35
  make_push_object_fn_grpc,
34
- pull_object_from_servicer,
35
- push_object_to_servicer,
36
+ pull_objects,
37
+ push_objects,
36
38
  )
37
39
  from flwr.common.logger import log, warn_deprecated_feature
38
- from flwr.common.message import (
39
- get_message_to_descendant_id_mapping,
40
- remove_content_from_message,
41
- )
40
+ from flwr.common.message import remove_content_from_message
42
41
  from flwr.common.retry_invoker import _make_simple_grpc_retry_invoker, _wrap_stub
43
42
  from flwr.common.serde import message_to_proto, run_from_proto
44
43
  from flwr.common.typing import Run
44
+ from flwr.proto.message_pb2 import ObjectIDs # pylint: disable=E0611
45
45
  from flwr.proto.node_pb2 import Node # pylint: disable=E0611
46
46
  from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611
47
47
  from flwr.proto.serverappio_pb2 import ( # pylint: disable=E0611
@@ -210,25 +210,29 @@ class GrpcGrid(Grid):
210
210
  def _try_push_message(self, run_id: int, message: Message) -> str:
211
211
  """Push one message and its associated objects."""
212
212
  # Compute mapping of message descendants
213
- descendants_mapping = get_message_to_descendant_id_mapping(message)
213
+ all_objects = get_all_nested_objects(message)
214
+ all_object_ids = list(all_objects.keys())
215
+ msg_id = all_object_ids[-1] # Last object is the message itself
216
+ descendant_ids = all_object_ids[:-1] # All but the last object are descendants
214
217
 
215
218
  # Call GrpcServerAppIoStub method
216
219
  res: PushInsMessagesResponse = self._stub.PushMessages(
217
220
  PushInsMessagesRequest(
218
221
  messages_list=[message_to_proto(remove_content_from_message(message))],
219
222
  run_id=run_id,
220
- msg_to_descendant_mapping=descendants_mapping,
223
+ msg_to_descendant_mapping={
224
+ msg_id: ObjectIDs(object_ids=descendant_ids)
225
+ },
221
226
  )
222
227
  )
223
228
 
224
229
  # Push objects
225
- msg_id = res.message_ids[0]
226
230
  # If Message was added to the LinkState correctly
227
231
  if msg_id is not None:
228
232
  obj_ids_to_push = set(res.objects_to_push[msg_id].object_ids)
229
233
  # Push only object that are not in the store
230
- push_object_to_servicer(
231
- message,
234
+ push_objects(
235
+ all_objects,
232
236
  push_object_fn=make_push_object_fn_grpc(
233
237
  push_object_grpc=self._stub.PushObject,
234
238
  node=self.node,
@@ -293,16 +297,20 @@ class GrpcGrid(Grid):
293
297
  # Pull Messages from store
294
298
  inflated_msgs: list[Message] = []
295
299
  for msg_proto in res.messages_list:
296
-
297
- message = pull_object_from_servicer(
298
- msg_proto.metadata.message_id,
300
+ msg_id = msg_proto.metadata.message_id
301
+ all_object_contents = pull_objects(
302
+ list(res.objects_to_pull[msg_id].object_ids) + [msg_id],
299
303
  pull_object_fn=make_pull_object_fn_grpc(
300
304
  pull_object_grpc=self._stub.PullObject,
301
305
  node=self.node,
302
306
  run_id=run_id,
303
307
  ),
304
308
  )
305
- inflated_msgs.append(cast(Message, message))
309
+ message = cast(
310
+ Message, inflate_object_from_contents(msg_id, all_object_contents)
311
+ )
312
+ message.metadata.__dict__["_message_id"] = msg_id
313
+ inflated_msgs.append(message)
306
314
 
307
315
  return inflated_msgs
308
316
 
@@ -21,7 +21,7 @@ import grpc
21
21
  from google.protobuf.json_format import MessageToDict
22
22
 
23
23
  from flwr.common.constant import Status
24
- from flwr.common.inflatable import check_body_len_consistency
24
+ from flwr.common.inflatable import UnexpectedObjectContentError
25
25
  from flwr.common.logger import log
26
26
  from flwr.common.typing import InvalidRunStatusException
27
27
  from flwr.proto import fleet_pb2_grpc # pylint: disable=E0611
@@ -200,12 +200,6 @@ class FleetServicer(fleet_pb2_grpc.FleetServicer):
200
200
  # Cancel insertion in ObjectStore
201
201
  context.abort(grpc.StatusCode.FAILED_PRECONDITION, "Unexpected node ID.")
202
202
 
203
- if not check_body_len_consistency(request.object_content):
204
- # Cancel insertion in ObjectStore
205
- context.abort(
206
- grpc.StatusCode.FAILED_PRECONDITION, "Unexpected object length"
207
- )
208
-
209
203
  # Init store
210
204
  store = self.objectstore_factory.store()
211
205
 
@@ -216,6 +210,9 @@ class FleetServicer(fleet_pb2_grpc.FleetServicer):
216
210
  stored = True
217
211
  except (NoObjectInStoreError, ValueError) as e:
218
212
  log(ERROR, str(e))
213
+ except UnexpectedObjectContentError as e:
214
+ # Object content is not valid
215
+ context.abort(grpc.StatusCode.FAILED_PRECONDITION, str(e))
219
216
 
220
217
  return PushObjectResponse(stored=stored)
221
218
 
@@ -23,7 +23,10 @@ import grpc
23
23
 
24
24
  from flwr.common import Message
25
25
  from flwr.common.constant import SUPERLINK_NODE_ID, Status
26
- from flwr.common.inflatable import check_body_len_consistency, get_desdendant_object_ids
26
+ from flwr.common.inflatable import (
27
+ UnexpectedObjectContentError,
28
+ get_descendant_object_ids,
29
+ )
27
30
  from flwr.common.logger import log
28
31
  from flwr.common.serde import (
29
32
  context_from_proto,
@@ -202,7 +205,7 @@ class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
202
205
  # Register messages generated by LinkState in the Store for consistency
203
206
  for msg_res in messages_res:
204
207
  if msg_res.metadata.src_node_id == SUPERLINK_NODE_ID:
205
- descendants = list(get_desdendant_object_ids(msg_res))
208
+ descendants = list(get_descendant_object_ids(msg_res))
206
209
  message_obj_id = msg_res.metadata.message_id
207
210
  # Store mapping
208
211
  store.set_message_descendant_ids(
@@ -424,12 +427,6 @@ class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
424
427
  # Cancel insertion in ObjectStore
425
428
  context.abort(grpc.StatusCode.FAILED_PRECONDITION, "Unexpected node ID.")
426
429
 
427
- if not check_body_len_consistency(request.object_content):
428
- # Cancel insertion in ObjectStore
429
- context.abort(
430
- grpc.StatusCode.FAILED_PRECONDITION, "Unexpected object length."
431
- )
432
-
433
430
  # Init store
434
431
  store = self.objectstore_factory.store()
435
432
 
@@ -440,6 +437,9 @@ class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
440
437
  stored = True
441
438
  except (NoObjectInStoreError, ValueError) as e:
442
439
  log(ERROR, str(e))
440
+ except UnexpectedObjectContentError as e:
441
+ # Object content is not valid
442
+ context.abort(grpc.StatusCode.FAILED_PRECONDITION, str(e))
443
443
 
444
444
  return PushObjectResponse(stored=stored)
445
445
 
@@ -18,6 +18,7 @@
18
18
  from typing import Optional
19
19
 
20
20
  from flwr.common.inflatable import get_object_id, is_valid_sha256_hash
21
+ from flwr.common.inflatable_utils import validate_object_content
21
22
 
22
23
  from .object_store import NoObjectInStoreError, ObjectStore
23
24
 
@@ -52,12 +53,15 @@ class InMemoryObjectStore(ObjectStore):
52
53
  f"Object with ID '{object_id}' was not pre-registered."
53
54
  )
54
55
 
55
- # Verify object_id and object_content match
56
56
  if self.verify:
57
+ # Verify object_id and object_content match
57
58
  object_id_from_content = get_object_id(object_content)
58
59
  if object_id != object_id_from_content:
59
60
  raise ValueError(f"Object ID {object_id} does not match content hash")
60
61
 
62
+ # Validate object content
63
+ validate_object_content(content=object_content)
64
+
61
65
  # Return if object is already present in the store
62
66
  if self.store[object_id] != b"":
63
67
  return
@@ -15,12 +15,14 @@
15
15
  """In-memory NodeState implementation."""
16
16
 
17
17
 
18
+ import secrets
18
19
  from collections.abc import Sequence
19
20
  from dataclasses import dataclass
20
21
  from threading import Lock
21
22
  from typing import Optional
22
23
 
23
24
  from flwr.common import Context, Message
25
+ from flwr.common.constant import FLWR_APP_TOKEN_LENGTH
24
26
  from flwr.common.typing import Run
25
27
 
26
28
  from .nodestate import NodeState
@@ -34,7 +36,7 @@ class MessageEntry:
34
36
  is_retrieved: bool = False
35
37
 
36
38
 
37
- class InMemoryNodeState(NodeState):
39
+ class InMemoryNodeState(NodeState): # pylint: disable=too-many-instance-attributes
38
40
  """In-memory NodeState implementation."""
39
41
 
40
42
  def __init__(self) -> None:
@@ -49,6 +51,9 @@ class InMemoryNodeState(NodeState):
49
51
  # Store run ID to Context mapping
50
52
  self.ctx_store: dict[int, Context] = {}
51
53
  self.lock_ctx_store = Lock()
54
+ # Store run ID to token mapping
55
+ self.token_store: dict[int, str] = {}
56
+ self.lock_token_store = Lock()
52
57
 
53
58
  def set_node_id(self, node_id: Optional[int]) -> None:
54
59
  """Set the node ID."""
@@ -148,3 +153,38 @@ class InMemoryNodeState(NodeState):
148
153
  """Retrieve a context by its run ID."""
149
154
  with self.lock_ctx_store:
150
155
  return self.ctx_store.get(run_id)
156
+
157
+ def get_run_ids_with_pending_messages(self) -> Sequence[int]:
158
+ """Retrieve run IDs that have at least one pending message."""
159
+ # Collect run IDs from messages
160
+ with self.lock_msg_store:
161
+ ret = {
162
+ entry.message.metadata.run_id
163
+ for entry in self.msg_store.values()
164
+ if entry.message.metadata.reply_to_message_id == ""
165
+ and not entry.is_retrieved
166
+ }
167
+
168
+ # Remove run IDs that have tokens stored (indicating they are in progress)
169
+ with self.lock_token_store:
170
+ ret -= set(self.token_store.keys())
171
+ return list(ret)
172
+
173
+ def create_token(self, run_id: int) -> str:
174
+ """Create a token for the given run ID."""
175
+ token = secrets.token_hex(FLWR_APP_TOKEN_LENGTH) # Generate a random token
176
+ with self.lock_token_store:
177
+ if run_id in self.token_store:
178
+ raise ValueError("Token already created for this run ID")
179
+ self.token_store[run_id] = token
180
+ return token
181
+
182
+ def verify_token(self, run_id: int, token: str) -> bool:
183
+ """Verify a token for the given run ID."""
184
+ with self.lock_token_store:
185
+ return self.token_store.get(run_id) == token
186
+
187
+ def delete_token(self, run_id: int) -> None:
188
+ """Delete the token for the given run ID."""
189
+ with self.lock_token_store:
190
+ self.token_store.pop(run_id, None)
@@ -155,3 +155,58 @@ class NodeState(ABC):
155
155
  Optional[Context]
156
156
  The `Context` instance if found, otherwise None.
157
157
  """
158
+
159
+ @abstractmethod
160
+ def get_run_ids_with_pending_messages(self) -> Sequence[int]:
161
+ """Retrieve run IDs that have at least one pending message.
162
+
163
+ Run IDs that are currently in progress (i.e., those associated with tokens)
164
+ will not be returned, even if they have pending messages.
165
+
166
+ Returns
167
+ -------
168
+ Sequence[int]
169
+ Sequence of run IDs with pending messages.
170
+ """
171
+
172
+ @abstractmethod
173
+ def create_token(self, run_id: int) -> str:
174
+ """Create a token for the given run ID.
175
+
176
+ Parameters
177
+ ----------
178
+ run_id : int
179
+ The ID of the run for which to create a token.
180
+
181
+ Returns
182
+ -------
183
+ str
184
+ A unique token associated with the run ID.
185
+ """
186
+
187
+ @abstractmethod
188
+ def verify_token(self, run_id: int, token: str) -> bool:
189
+ """Verify a token for the given run ID.
190
+
191
+ Parameters
192
+ ----------
193
+ run_id : int
194
+ The ID of the run for which to verify the token.
195
+ token : str
196
+ The token to verify.
197
+
198
+ Returns
199
+ -------
200
+ bool
201
+ True if the token is valid for the run ID, False otherwise.
202
+ """
203
+
204
+ @abstractmethod
205
+ def delete_token(self, run_id: int) -> None:
206
+ """Delete the token for the given run ID.
207
+
208
+ Parameters
209
+ ----------
210
+ run_id : int
211
+ The ID of the run for which to delete the token.
212
+ """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: flwr-nightly
3
- Version: 1.19.0.dev20250606
3
+ Version: 1.19.0.dev20250609
4
4
  Summary: Flower: A Friendly Federated AI Framework
5
5
  License: Apache-2.0
6
6
  Keywords: Artificial Intelligence,Federated AI,Federated Analytics,Federated Evaluation,Federated Learning,Flower,Machine Learning
@@ -84,7 +84,7 @@ flwr/client/grpc_adapter_client/__init__.py,sha256=RQWP5mFPROLHKgombiRvPXVWSoVrQ
84
84
  flwr/client/grpc_adapter_client/connection.py,sha256=aj5tTYyE8z2hQLXPPydsJiz8gBDIWLUhfWvqYkAL1L4,3966
85
85
  flwr/client/grpc_rere_client/__init__.py,sha256=i7iS0Lt8B7q0E2L72e4F_YrKm6ClRKnd71PNA6PW2O0,752
86
86
  flwr/client/grpc_rere_client/client_interceptor.py,sha256=zFaVHw6AxeNO-7eCKKb-RxrPa7zbM5Z-2-1Efc4adQY,2451
87
- flwr/client/grpc_rere_client/connection.py,sha256=6LF9JuAhL4XDHVtPS6MzEYm-Nrer-5iM7fEjR5XaGzM,13658
87
+ flwr/client/grpc_rere_client/connection.py,sha256=nk4xSjBKG_mvoZsCVkkGLPPc6juxBId7RNcckgA6VSE,13994
88
88
  flwr/client/grpc_rere_client/grpc_adapter.py,sha256=JvMZ7vCFTaTEo6AzKYh3zDmeQAU7VSjdysbC6t3ufWg,6351
89
89
  flwr/client/message_handler/__init__.py,sha256=0lyljDVqre3WljiZbPcwCCf8GiIaSVI_yo_ylEyPwSE,719
90
90
  flwr/client/message_handler/message_handler.py,sha256=X9SXX6et97Lw9_DGD93HKsEBGNjXClcFgc_5aLK0oiU,6541
@@ -108,7 +108,7 @@ flwr/common/args.py,sha256=-aX_jVnSaDrJR2KZ8Wq0Y3dQHII4R4MJtJOIXzVUA0c,5417
108
108
  flwr/common/auth_plugin/__init__.py,sha256=3rzPkVLn9WyB5n7HLk1XGDw3SLCqRWAU1_CnglcWPfw,970
109
109
  flwr/common/auth_plugin/auth_plugin.py,sha256=yEIapwim14tn99oGhkQKun_WifoY8hscAtU8Z6fcGXE,4796
110
110
  flwr/common/config.py,sha256=glcZDjco-amw1YfQcYTFJ4S1pt9APoexT-mf1QscuHs,13960
111
- flwr/common/constant.py,sha256=lQCel7YanrEdFzy7pHBejhrQ7ZIylJrhBD9pXY02qv0,7472
111
+ flwr/common/constant.py,sha256=A8rWHZZUH4Rxbx-bCKW5pNJ_BG4W9Xfw4fLevNhZ8l0,8077
112
112
  flwr/common/context.py,sha256=Be8obQR_OvEDy1OmshuUKxGRQ7Qx89mf5F4xlhkR10s,2407
113
113
  flwr/common/date.py,sha256=1ZT2cRSpC2DJqprOVTLXYCR_O2_OZR0zXO_brJ3LqWc,1554
114
114
  flwr/common/differential_privacy.py,sha256=FdlpdpPl_H_2HJa8CQM1iCUGBBQ5Dc8CzxmHERM-EoE,6148
@@ -122,10 +122,11 @@ flwr/common/exit/exit_code.py,sha256=PNEnCrZfOILjfDAFu5m-2YWEJBrk97xglq4zCUlqV7E
122
122
  flwr/common/exit_handlers.py,sha256=IaqJ60fXZuu7McaRYnoYKtlbH9t4Yl9goNExKqtmQbs,4304
123
123
  flwr/common/grpc.py,sha256=manTaHaPiyYngUq1ErZvvV2B2GxlXUUUGRy3jc3TBIQ,9798
124
124
  flwr/common/heartbeat.py,sha256=SyEpNDnmJ0lni0cWO67rcoJVKasCLmkNHm3dKLeNrLU,5749
125
- flwr/common/inflatable.py,sha256=9yPsSFOfNM2OIb15JQ6-wY5kblwXiC5zNX-tsp2ZwW0,7017
126
- flwr/common/inflatable_grpc_utils.py,sha256=3M3_334GM-BrjLysilPXEbkDAErh_dXVgkcmcFSQYWg,8017
125
+ flwr/common/inflatable.py,sha256=gdAICtXklkQRMrxoTYEbzJl7AeFqZtUm4JU6f2it9FM,7264
126
+ flwr/common/inflatable_grpc_utils.py,sha256=M982VQ8x1wh4QnLXR-IKWdtGuOFACvIsU6lYbbs1TI8,13040
127
+ flwr/common/inflatable_utils.py,sha256=hPwap-P9pxHcxo-EtxtrnKNiFA2HsNGJaKX2b002PnE,2892
127
128
  flwr/common/logger.py,sha256=JbRf6E2vQxXzpDBq1T8IDUJo_usu3gjWEBPQ6uKcmdg,13049
128
- flwr/common/message.py,sha256=ZdH35PznYhIzfNKPzHTL1YS5z-flTpHnjdNpQR6B8x0,19953
129
+ flwr/common/message.py,sha256=xAL7iZN5-n-xPQpgoSFvxNrzs8fmiiPfoU0DjNQEhRw,19953
129
130
  flwr/common/object_ref.py,sha256=p3SfTeqo3Aj16SkB-vsnNn01zswOPdGNBitcbRnqmUk,9134
130
131
  flwr/common/parameter.py,sha256=UVw6sOgehEFhFs4uUCMl2kfVq1PD6ncmWgPLMsZPKPE,2095
131
132
  flwr/common/pyproject.py,sha256=2SU6yJW7059SbMXgzjOdK1GZRWO6AixDH7BmdxbMvHI,1386
@@ -236,7 +237,7 @@ flwr/server/criterion.py,sha256=G4e-6B48Pc7d5rmGVUpIzNKb6UF88O3VmTRuUltgjzM,1061
236
237
  flwr/server/fleet_event_log_interceptor.py,sha256=AkL7Y5d3xm2vRhL3ahmEVVoOvAP7PA7dRgB-je4v-Ys,3774
237
238
  flwr/server/grid/__init__.py,sha256=aWZHezoR2UGMJISB_gPMCm2N_2GSbm97A3lAp7ruhRQ,888
238
239
  flwr/server/grid/grid.py,sha256=naGCYt5J6dnmUvrcGkdNyKPe3MBd-0awGm1ALmgahqY,6625
239
- flwr/server/grid/grpc_grid.py,sha256=o9oXBsAc9jp2RneY1AhOOyOO8B4sRJKP_l1UyC0j5_M,12692
240
+ flwr/server/grid/grpc_grid.py,sha256=rVzPh0BGQOI3Dzhd8upBXNLEA5bKy82eBZkU4a1qFJY,13257
240
241
  flwr/server/grid/inmemory_grid.py,sha256=RjejYT-d-hHuTs1KSs_5wvOdAWKLus8w5_UAcnGt4iw,6168
241
242
  flwr/server/history.py,sha256=cCkFhBN4GoHsYYNk5GG1Y089eKJh2DH_ZJbYPwLaGyk,5026
242
243
  flwr/server/run_serverapp.py,sha256=v0p6jXj2dFxlRUdoEeF1mnaFd9XRQi6dZCflPY6d3qI,2063
@@ -284,7 +285,7 @@ flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py,sha256=KouR9PUcrPmMtoLooF4O
284
285
  flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py,sha256=iSf0mbBAlig7G6subQwBSVjcUCgSihONKdZ1RmQPTOk,4887
285
286
  flwr/server/superlink/fleet/grpc_bidi/grpc_server.py,sha256=OsS-6GgCIzMMZDVu5Y-OKjynHVUrpdc_5OrtuB-IbU0,5174
286
287
  flwr/server/superlink/fleet/grpc_rere/__init__.py,sha256=ahDJJ1e-lDxBpeBMgPk7YZt2wB38_QltcpOC0gLbpFs,758
287
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py,sha256=SsMtj1EeOgumffWtYTQt-ii3JPldszXvP91C3axznq8,9176
288
+ flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py,sha256=iD45MSaGxPmkmPp_Y6XR-crUaQIcAcfyw0qJZwt7C0o,9106
288
289
  flwr/server/superlink/fleet/grpc_rere/server_interceptor.py,sha256=DrHubsaLgJCwCeeJPYogQTiP0xYqjxwnT9rh7OP7BoU,6984
289
290
  flwr/server/superlink/fleet/message_handler/__init__.py,sha256=fHsRV0KvJ8HtgSA4_YBsEzuhJLjO8p6xx4aCY2oE1p4,731
290
291
  flwr/server/superlink/fleet/message_handler/message_handler.py,sha256=P43PapLZJKbZ0Oo0kP_KcO5zSMvO53SakQgPMiR5d1M,6500
@@ -303,7 +304,7 @@ flwr/server/superlink/linkstate/sqlite_linkstate.py,sha256=sHJPK1w0tP0m2WCXH2F9l
303
304
  flwr/server/superlink/linkstate/utils.py,sha256=IeLh7iGRCHU5MEWOl7iriaSE4L__8GWOa2OleXadK5M,15444
304
305
  flwr/server/superlink/serverappio/__init__.py,sha256=Fy4zJuoccZe5mZSEIpOmQvU6YeXFBa1M4eZuXXmJcn8,717
305
306
  flwr/server/superlink/serverappio/serverappio_grpc.py,sha256=6-FUUt0GiLcBPljj8bBrUNeAITUoDQOLzaMihKo52hg,2326
306
- flwr/server/superlink/serverappio/serverappio_servicer.py,sha256=qInBXn7xcnNUNIXj_BkjoWfZd96By55gbTsp4onwfDQ,17290
307
+ flwr/server/superlink/serverappio/serverappio_servicer.py,sha256=mRDMklbizitbDcN6hrCSp0NpsFegdAE84MHEulW7eBQ,17232
307
308
  flwr/server/superlink/simulation/__init__.py,sha256=Ry8DrNaZCMcQXvUc4FoCN2m3dvUQgWjasfp015o3Ec4,718
308
309
  flwr/server/superlink/simulation/simulationio_grpc.py,sha256=0l0F-UjYEk6W7HZmI28PbJQLFxSi_vBHRkdchgdaSMQ,2224
309
310
  flwr/server/superlink/simulation/simulationio_servicer.py,sha256=aJezU8RSJswcmWm7Eoy0BqsU13jrcfuFwX3ljm-cORM,7719
@@ -330,7 +331,7 @@ flwr/simulation/run_simulation.py,sha256=Nvw_6hI71aE2nU95_tt1F9VSo3OJWrvA97e3XZu
330
331
  flwr/simulation/simulationio_connection.py,sha256=mzS1C6EEREwQDPceDo30anAasmTDLb9qqV2tXlBhOUA,3494
331
332
  flwr/supercore/__init__.py,sha256=pqkFoow_E6UhbBlhmoD1gmTH-33yJRhBsIZqxRPFZ7U,755
332
333
  flwr/supercore/object_store/__init__.py,sha256=cdfPAmjINY6iOp8oI_LdcVh2simg469Mkdl4LLV4kHI,911
333
- flwr/supercore/object_store/in_memory_object_store.py,sha256=OPZPNM2CLi5tZq9bVBKAzPch4CRXrUz_20Op1bRsFJc,3913
334
+ flwr/supercore/object_store/in_memory_object_store.py,sha256=8EfTJHb6-RObWmzb2ZxBgxMobCod6NP820DzrMnYdbY,4081
334
335
  flwr/supercore/object_store/object_store.py,sha256=yZ6A_JgK_aGF54zlPISLK_d9FvxpYJlI2qNfmQBdlzM,4328
335
336
  flwr/supercore/object_store/object_store_factory.py,sha256=QVwE2ywi7vsj2iKfvWWnNw3N_I7Rz91NUt2RpcbJ7iM,1527
336
337
  flwr/superexec/__init__.py,sha256=YFqER0IJc1XEWfsX6AxZ9LSRq0sawPYrNYki-brvTIc,715
@@ -348,8 +349,8 @@ flwr/supernode/cli/__init__.py,sha256=JuEMr0-s9zv-PEWKuLB9tj1ocNfroSyNJ-oyv7ati9
348
349
  flwr/supernode/cli/flower_supernode.py,sha256=ly2AQhbla2sufDaMsENaEALDEd0a4CS4D0eUrUOkHzY,8778
349
350
  flwr/supernode/cli/flwr_clientapp.py,sha256=KfVUO20ZMnUDSGZTJ9I1KkMawFsRV6kdRUmGIRNbg_8,2812
350
351
  flwr/supernode/nodestate/__init__.py,sha256=CyLLObbmmVgfRO88UCM0VMait1dL57mUauUDfuSHsbU,976
351
- flwr/supernode/nodestate/in_memory_nodestate.py,sha256=4ZiLA45fMi2bJgmfDNLtiv-gVNru95Bi48xBy7xtatA,5212
352
- flwr/supernode/nodestate/nodestate.py,sha256=SgblnKtqzTHRiODwg4QUREw1-uYPQrLzoeTBlROHf_0,4571
352
+ flwr/supernode/nodestate/in_memory_nodestate.py,sha256=vfHgG7dxgVLo0ar4ltLOveEm7YVvXxdj0Cac4CE85qQ,6903
353
+ flwr/supernode/nodestate/nodestate.py,sha256=Ro60vlm4xUm6FGNV440DrlzZ6rPsem-ULmJg0eAFmhU,6034
353
354
  flwr/supernode/nodestate/nodestate_factory.py,sha256=UYTDCcwK_baHUmkzkJDxL0UEqvtTfOMlQRrROMCd0Xo,1430
354
355
  flwr/supernode/runtime/__init__.py,sha256=JQdqd2EMTn-ORMeTvewYYh52ls0YKP68jrps1qioxu4,718
355
356
  flwr/supernode/runtime/run_clientapp.py,sha256=cvWSby7u31u97QapWHxJM-Wer6F1k6mbbD-d1gxwxZA,7962
@@ -357,7 +358,7 @@ flwr/supernode/servicer/__init__.py,sha256=lucTzre5WPK7G1YLCfaqg3rbFWdNSb7ZTt-ca
357
358
  flwr/supernode/servicer/clientappio/__init__.py,sha256=vJyOjO2FXZ2URbnthmdsgs6948wbYfdq1L1V8Um-Lr8,895
358
359
  flwr/supernode/servicer/clientappio/clientappio_servicer.py,sha256=LmzkxtNQBn5vVrHc0Bhq2WqaK6-LM2v4kfLBN0PiNNM,8522
359
360
  flwr/supernode/start_client_internal.py,sha256=5CwTNV-XmIhwR1jv3G7aQAXGhf6OFWS6U-vmxY1iKGA,16984
360
- flwr_nightly-1.19.0.dev20250606.dist-info/METADATA,sha256=OBd3qXqNO4F-2bqFuBUM8HZ1hKilS8QDmf0fH12imYs,15910
361
- flwr_nightly-1.19.0.dev20250606.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
362
- flwr_nightly-1.19.0.dev20250606.dist-info/entry_points.txt,sha256=jNpDXGBGgs21RqUxelF_jwGaxtqFwm-MQyfz-ZqSjrA,367
363
- flwr_nightly-1.19.0.dev20250606.dist-info/RECORD,,
361
+ flwr_nightly-1.19.0.dev20250609.dist-info/METADATA,sha256=HloLs2HPXsSLn_UVyxQf1G9yLasbznXJstMQ1Cr1mo4,15910
362
+ flwr_nightly-1.19.0.dev20250609.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
363
+ flwr_nightly-1.19.0.dev20250609.dist-info/entry_points.txt,sha256=jNpDXGBGgs21RqUxelF_jwGaxtqFwm-MQyfz-ZqSjrA,367
364
+ flwr_nightly-1.19.0.dev20250609.dist-info/RECORD,,