flwr-nightly 1.19.0.dev20250605__py3-none-any.whl → 1.19.0.dev20250607__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,16 +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 (
34
- pull_object_from_servicer,
35
- push_object_to_servicer,
35
+ inflate_object_from_contents,
36
+ make_pull_object_fn_grpc,
37
+ make_push_object_fn_grpc,
38
+ pull_objects,
39
+ push_objects,
36
40
  )
37
41
  from flwr.common.logger import log
38
- from flwr.common.message import (
39
- Message,
40
- get_message_to_descendant_id_mapping,
41
- remove_content_from_message,
42
- )
42
+ from flwr.common.message import Message, remove_content_from_message
43
43
  from flwr.common.retry_invoker import RetryInvoker, _wrap_stub
44
44
  from flwr.common.secure_aggregation.crypto.symmetric_encryption import (
45
45
  generate_key_pairs,
@@ -60,6 +60,7 @@ from flwr.proto.heartbeat_pb2 import ( # pylint: disable=E0611
60
60
  SendNodeHeartbeatRequest,
61
61
  SendNodeHeartbeatResponse,
62
62
  )
63
+ from flwr.proto.message_pb2 import ObjectIDs # pylint: disable=E0611
63
64
  from flwr.proto.node_pb2 import Node # pylint: disable=E0611
64
65
  from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611
65
66
 
@@ -265,21 +266,21 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
265
266
  in_message: Optional[Message] = None
266
267
 
267
268
  if message_proto:
268
- in_message = cast(
269
- Message,
270
- pull_object_from_servicer(
271
- object_id=message_proto.metadata.message_id,
272
- stub=stub,
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,
273
274
  node=node,
274
275
  run_id=message_proto.metadata.run_id,
275
276
  ),
276
277
  )
277
-
278
- if in_message:
278
+ in_message = cast(
279
+ Message, inflate_object_from_contents(msg_id, all_object_contents)
280
+ )
279
281
  # The deflated message doesn't contain the message_id (its own object_id)
280
282
  # Inject
281
- # pylint: disable-next=W0212
282
- in_message.metadata._message_id = message_proto.metadata.message_id # type: ignore
283
+ in_message.metadata.__dict__["_message_id"] = msg_id
283
284
 
284
285
  # Remember `metadata` of the in message
285
286
  nonlocal metadata
@@ -308,23 +309,30 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
308
309
  log(ERROR, "Invalid out message")
309
310
  return
310
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
+
311
318
  # Serialize Message
312
319
  message_proto = message_to_proto(message=remove_content_from_message(message))
313
- descendants_mapping = get_message_to_descendant_id_mapping(message)
314
320
  request = PushMessagesRequest(
315
321
  node=node,
316
322
  messages_list=[message_proto],
317
- msg_to_descendant_mapping=descendants_mapping,
323
+ msg_to_descendant_mapping={msg_id: ObjectIDs(object_ids=descendant_ids)},
318
324
  )
319
325
  response: PushMessagesResponse = stub.PushMessages(request=request)
320
326
 
321
327
  if response.objects_to_push:
322
328
  objs_to_push = set(response.objects_to_push[message.object_id].object_ids)
323
- push_object_to_servicer(
324
- message,
325
- stub,
326
- node,
327
- run_id=message.metadata.run_id,
329
+ push_objects(
330
+ all_objects,
331
+ push_object_fn=make_push_object_fn_grpc(
332
+ push_object_grpc=stub.PushObject,
333
+ node=node,
334
+ run_id=message.metadata.run_id,
335
+ ),
328
336
  object_ids_to_push=objs_to_push,
329
337
  )
330
338
  log(DEBUG, "Pushed %s objects to servicer.", len(objs_to_push))
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
@@ -25,6 +25,16 @@ from .constant import HEAD_BODY_DIVIDER, HEAD_VALUE_DIVIDER
25
25
  from .logger import log
26
26
 
27
27
 
28
+ class UnexpectedObjectContentError(Exception):
29
+ """Exception raised when the content of an object does not conform to the expected
30
+ structure for an InflatableObject (i.e., head, body, and values within the head)."""
31
+
32
+ def __init__(self, object_id: str, reason: str):
33
+ super().__init__(
34
+ f"Object with ID '{object_id}' has an unexpected structure. {reason}"
35
+ )
36
+
37
+
28
38
  class InflatableObject:
29
39
  """Base class for inflatable objects."""
30
40
 
@@ -117,12 +127,14 @@ def add_header_to_object_body(object_body: bytes, obj: InflatableObject) -> byte
117
127
 
118
128
  def _get_object_head(object_content: bytes) -> bytes:
119
129
  """Return object head from object content."""
120
- return object_content.split(HEAD_BODY_DIVIDER, 1)[0]
130
+ index = object_content.find(HEAD_BODY_DIVIDER)
131
+ return object_content[:index]
121
132
 
122
133
 
123
134
  def _get_object_body(object_content: bytes) -> bytes:
124
135
  """Return object body from object content."""
125
- return object_content.split(HEAD_BODY_DIVIDER, 1)[1]
136
+ index = object_content.find(HEAD_BODY_DIVIDER)
137
+ return object_content[index + len(HEAD_BODY_DIVIDER) :]
126
138
 
127
139
 
128
140
  def is_valid_sha256_hash(object_id: str) -> bool:
@@ -197,21 +209,25 @@ def get_object_head_values_from_object_content(
197
209
  return obj_type, children_ids, int(body_len)
198
210
 
199
211
 
200
- def _get_descendants_object_ids_recursively(obj: InflatableObject) -> set[str]:
212
+ def get_descendant_object_ids(obj: InflatableObject) -> set[str]:
213
+ """Get a set of object IDs of all descendants."""
214
+ descendants = set(get_all_nested_objects(obj).keys())
215
+ # Exclude Object ID of parent object
216
+ descendants.discard(obj.object_id)
217
+ return descendants
201
218
 
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
219
 
207
- descendants.add(obj.object_id)
220
+ def get_all_nested_objects(obj: InflatableObject) -> dict[str, InflatableObject]:
221
+ """Get a dictionary of all nested objects, including the object itself.
208
222
 
209
- return descendants
223
+ Each key in the dictionary is an object ID, and the entries are ordered by post-
224
+ order traversal, i.e., child objects appear before their respective parents.
225
+ """
226
+ ret: dict[str, InflatableObject] = {}
227
+ if children := obj.children:
228
+ for child in children.values():
229
+ ret.update(get_all_nested_objects(child))
210
230
 
231
+ ret[obj.object_id] = obj
211
232
 
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
233
+ return ret
@@ -15,11 +15,12 @@
15
15
  """InflatableObject utils."""
16
16
 
17
17
 
18
- from time import sleep
19
- from typing import Optional, Union
18
+ import concurrent.futures
19
+ import random
20
+ import threading
21
+ import time
22
+ from typing import Callable, Optional
20
23
 
21
- from flwr.client.grpc_rere_client.grpc_adapter import GrpcAdapter
22
- from flwr.proto.fleet_pb2_grpc import FleetStub # pylint: disable=E0611
23
24
  from flwr.proto.message_pb2 import ( # pylint: disable=E0611
24
25
  PullObjectRequest,
25
26
  PullObjectResponse,
@@ -27,13 +28,16 @@ from flwr.proto.message_pb2 import ( # pylint: disable=E0611
27
28
  PushObjectResponse,
28
29
  )
29
30
  from flwr.proto.node_pb2 import Node # pylint: disable=E0611
30
- from flwr.proto.serverappio_pb2_grpc import ServerAppIoStub # pylint: disable=E0611
31
31
 
32
- from .inflatable import (
33
- InflatableObject,
34
- get_object_head_values_from_object_content,
35
- 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,
36
39
  )
40
+ from .inflatable import InflatableObject, get_object_head_values_from_object_content
37
41
  from .message import Message
38
42
  from .record import Array, ArrayRecord, ConfigRecord, MetricRecord, RecordDict
39
43
 
@@ -48,75 +52,311 @@ inflatable_class_registry: dict[str, type[InflatableObject]] = {
48
52
  }
49
53
 
50
54
 
51
- def push_object_to_servicer(
52
- obj: InflatableObject,
53
- stub: Union[FleetStub, ServerAppIoStub, GrpcAdapter],
55
+ class ObjectUnavailableError(Exception):
56
+ """Exception raised when an object has been pre-registered but is not yet
57
+ available."""
58
+
59
+ def __init__(self, object_id: str):
60
+ super().__init__(f"Object with ID '{object_id}' is not yet available.")
61
+
62
+
63
+ class ObjectIdNotPreregisteredError(Exception):
64
+ """Exception raised when an object ID is not pre-registered."""
65
+
66
+ def __init__(self, object_id: str):
67
+ super().__init__(f"Object with ID '{object_id}' could not be found.")
68
+
69
+
70
+ def make_pull_object_fn_grpc(
71
+ pull_object_grpc: Callable[[PullObjectRequest], PullObjectResponse],
54
72
  node: Node,
55
73
  run_id: int,
56
- object_ids_to_push: Optional[set[str]] = None,
57
- ) -> set[str]:
58
- """Recursively deflate an object and push it to the servicer.
74
+ ) -> Callable[[str], bytes]:
75
+ """Create a pull object function that uses gRPC to pull objects.
59
76
 
60
- Objects with the same ID are not pushed twice. If `object_ids_to_push` is set,
61
- only objects with those IDs are pushed. It returns the set of pushed object
62
- IDs.
77
+ Parameters
78
+ ----------
79
+ pull_object_grpc : Callable[[PullObjectRequest], PullObjectResponse]
80
+ The gRPC function to pull objects, e.g., `FleetStub.PullObject`.
81
+ node : Node
82
+ The node making the request.
83
+ run_id : int
84
+ The run ID for the current operation.
85
+
86
+ Returns
87
+ -------
88
+ Callable[[str], bytes]
89
+ A function that takes an object ID and returns the object content as bytes.
90
+ The function raises `ObjectIdNotPreregisteredError` if the object ID is not
91
+ pre-registered, or `ObjectUnavailableError` if the object is not yet available.
63
92
  """
64
- pushed_object_ids: set[str] = set()
65
- # Push children if it has any
66
- if children := obj.children:
67
- for child in children.values():
68
- pushed_object_ids |= push_object_to_servicer(
69
- child, stub, node, run_id, object_ids_to_push
70
- )
71
-
72
- # Deflate object and push
73
- object_content = obj.deflate()
74
- object_id = get_object_id(object_content)
75
- # Push always if no object set is specified, or if the object is in the set
76
- if object_ids_to_push is None or object_id in object_ids_to_push:
77
- _: PushObjectResponse = stub.PushObject(
78
- PushObjectRequest(
79
- node=node,
80
- run_id=run_id,
81
- object_id=object_id,
82
- object_content=object_content,
83
- )
84
- )
85
- pushed_object_ids.add(object_id)
86
93
 
87
- return pushed_object_ids
94
+ def pull_object_fn(object_id: str) -> bytes:
95
+ request = PullObjectRequest(node=node, run_id=run_id, object_id=object_id)
96
+ response: PullObjectResponse = pull_object_grpc(request)
97
+ if not response.object_found:
98
+ raise ObjectIdNotPreregisteredError(object_id)
99
+ if not response.object_available:
100
+ raise ObjectUnavailableError(object_id)
101
+ return response.object_content
88
102
 
103
+ return pull_object_fn
89
104
 
90
- def pull_object_from_servicer(
91
- object_id: str,
92
- stub: Union[FleetStub, ServerAppIoStub, GrpcAdapter],
105
+
106
+ def make_push_object_fn_grpc(
107
+ push_object_grpc: Callable[[PushObjectRequest], PushObjectResponse],
93
108
  node: Node,
94
109
  run_id: int,
95
- ) -> InflatableObject:
96
- """Recursively inflate an object by pulling it from the servicer."""
97
- # Pull object
98
- object_available = False
99
- while not object_available:
100
- object_proto: PullObjectResponse = stub.PullObject(
101
- PullObjectRequest(node=node, run_id=run_id, object_id=object_id)
110
+ ) -> Callable[[str, bytes], None]:
111
+ """Create a push object function that uses gRPC to push objects.
112
+
113
+ Parameters
114
+ ----------
115
+ push_object_grpc : Callable[[PushObjectRequest], PushObjectResponse]
116
+ The gRPC function to push objects, e.g., `FleetStub.PushObject`.
117
+ node : Node
118
+ The node making the request.
119
+ run_id : int
120
+ The run ID for the current operation.
121
+
122
+ Returns
123
+ -------
124
+ Callable[[str, bytes], None]
125
+ A function that takes an object ID and its content as bytes, and pushes it
126
+ to the servicer. The function raises `ObjectIdNotPreregisteredError` if
127
+ the object ID is not pre-registered.
128
+ """
129
+
130
+ def push_object_fn(object_id: str, object_content: bytes) -> None:
131
+ request = PushObjectRequest(
132
+ node=node, run_id=run_id, object_id=object_id, object_content=object_content
102
133
  )
103
- object_available = object_proto.object_available
104
- object_content = object_proto.object_content
105
- sleep(0.1)
134
+ response: PushObjectResponse = push_object_grpc(request)
135
+ if not response.stored:
136
+ raise ObjectIdNotPreregisteredError(object_id)
137
+
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]
106
334
 
107
335
  # Extract object class and object_ids of children
336
+ object_content = object_contents[object_id]
108
337
  obj_type, children_obj_ids, _ = get_object_head_values_from_object_content(
109
- object_content=object_content
338
+ object_content=object_contents[object_id]
110
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
+
111
345
  # Resolve object class
112
346
  cls_type = inflatable_class_registry[obj_type]
113
347
 
114
- # Pull all children objects
348
+ # Inflate all children objects
115
349
  children: dict[str, InflatableObject] = {}
116
- for child_object_id in children_obj_ids:
117
- children[child_object_id] = pull_object_from_servicer(
118
- child_object_id, stub, node, run_id
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,
119
356
  )
120
357
 
121
358
  # Inflate object passing its children
122
- return cls_type.inflate(object_content, children=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,18 +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 (
32
- pull_object_from_servicer,
33
- push_object_to_servicer,
33
+ inflate_object_from_contents,
34
+ make_pull_object_fn_grpc,
35
+ make_push_object_fn_grpc,
36
+ pull_objects,
37
+ push_objects,
34
38
  )
35
39
  from flwr.common.logger import log, warn_deprecated_feature
36
- from flwr.common.message import (
37
- get_message_to_descendant_id_mapping,
38
- remove_content_from_message,
39
- )
40
+ from flwr.common.message import remove_content_from_message
40
41
  from flwr.common.retry_invoker import _make_simple_grpc_retry_invoker, _wrap_stub
41
42
  from flwr.common.serde import message_to_proto, run_from_proto
42
43
  from flwr.common.typing import Run
44
+ from flwr.proto.message_pb2 import ObjectIDs # pylint: disable=E0611
43
45
  from flwr.proto.node_pb2 import Node # pylint: disable=E0611
44
46
  from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611
45
47
  from flwr.proto.serverappio_pb2 import ( # pylint: disable=E0611
@@ -208,28 +210,34 @@ class GrpcGrid(Grid):
208
210
  def _try_push_message(self, run_id: int, message: Message) -> str:
209
211
  """Push one message and its associated objects."""
210
212
  # Compute mapping of message descendants
211
- 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
212
217
 
213
218
  # Call GrpcServerAppIoStub method
214
219
  res: PushInsMessagesResponse = self._stub.PushMessages(
215
220
  PushInsMessagesRequest(
216
221
  messages_list=[message_to_proto(remove_content_from_message(message))],
217
222
  run_id=run_id,
218
- msg_to_descendant_mapping=descendants_mapping,
223
+ msg_to_descendant_mapping={
224
+ msg_id: ObjectIDs(object_ids=descendant_ids)
225
+ },
219
226
  )
220
227
  )
221
228
 
222
229
  # Push objects
223
- msg_id = res.message_ids[0]
224
230
  # If Message was added to the LinkState correctly
225
231
  if msg_id is not None:
226
232
  obj_ids_to_push = set(res.objects_to_push[msg_id].object_ids)
227
233
  # Push only object that are not in the store
228
- push_object_to_servicer(
229
- message,
230
- self._stub,
231
- node=self.node,
232
- run_id=run_id,
234
+ push_objects(
235
+ all_objects,
236
+ push_object_fn=make_push_object_fn_grpc(
237
+ push_object_grpc=self._stub.PushObject,
238
+ node=self.node,
239
+ run_id=run_id,
240
+ ),
233
241
  object_ids_to_push=obj_ids_to_push,
234
242
  )
235
243
  return msg_id
@@ -289,14 +297,20 @@ class GrpcGrid(Grid):
289
297
  # Pull Messages from store
290
298
  inflated_msgs: list[Message] = []
291
299
  for msg_proto in res.messages_list:
292
-
293
- message = pull_object_from_servicer(
294
- msg_proto.metadata.message_id,
295
- self._stub,
296
- node=self.node,
297
- run_id=run_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],
303
+ pull_object_fn=make_pull_object_fn_grpc(
304
+ pull_object_grpc=self._stub.PullObject,
305
+ node=self.node,
306
+ run_id=run_id,
307
+ ),
308
+ )
309
+ message = cast(
310
+ Message, inflate_object_from_contents(msg_id, all_object_contents)
298
311
  )
299
- inflated_msgs.append(cast(Message, message))
312
+ message.metadata.__dict__["_message_id"] = msg_id
313
+ inflated_msgs.append(message)
300
314
 
301
315
  return inflated_msgs
302
316
 
@@ -23,7 +23,7 @@ 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 check_body_len_consistency, get_descendant_object_ids
27
27
  from flwr.common.logger import log
28
28
  from flwr.common.serde import (
29
29
  context_from_proto,
@@ -202,7 +202,7 @@ class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
202
202
  # Register messages generated by LinkState in the Store for consistency
203
203
  for msg_res in messages_res:
204
204
  if msg_res.metadata.src_node_id == SUPERLINK_NODE_ID:
205
- descendants = list(get_desdendant_object_ids(msg_res))
205
+ descendants = list(get_descendant_object_ids(msg_res))
206
206
  message_obj_id = msg_res.metadata.message_id
207
207
  # Store mapping
208
208
  store.set_message_descendant_ids(
@@ -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.dev20250605
3
+ Version: 1.19.0.dev20250607
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=XwhT-yrWLHHegXpiAu1YuICLQ1t-KkOh6pNYJ9R4FEE,13358
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=YGP8oJRfnkwvY6segWH1DUf_ljDIkku7-2zH66tv3HA,4337
125
+ flwr/common/inflatable.py,sha256=b8Br0TKB_yMijbICPVD4gbGMr4l0iXtH9s8MztTry4A,7717
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=ymLaqWYYkv0uWrYvxavMKEyB0TVRMSPi1tJhmvhe7_g,12392
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
@@ -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=GElRmXPxkrbnqD9lMVvJJHQF5hYJODL8uhAvvI97ItQ,17290
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
@@ -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.dev20250605.dist-info/METADATA,sha256=gsyzTuVl8GpQD7gk1RPBCbNvdugpyl08gCr1sC0kUHc,15910
361
- flwr_nightly-1.19.0.dev20250605.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
362
- flwr_nightly-1.19.0.dev20250605.dist-info/entry_points.txt,sha256=jNpDXGBGgs21RqUxelF_jwGaxtqFwm-MQyfz-ZqSjrA,367
363
- flwr_nightly-1.19.0.dev20250605.dist-info/RECORD,,
361
+ flwr_nightly-1.19.0.dev20250607.dist-info/METADATA,sha256=YhYf05eYMhWNR6GdMIdEh39UZLZMmlX3x0MifwkcURo,15910
362
+ flwr_nightly-1.19.0.dev20250607.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
363
+ flwr_nightly-1.19.0.dev20250607.dist-info/entry_points.txt,sha256=jNpDXGBGgs21RqUxelF_jwGaxtqFwm-MQyfz-ZqSjrA,367
364
+ flwr_nightly-1.19.0.dev20250607.dist-info/RECORD,,