flwr-nightly 1.20.0.dev20250718__py3-none-any.whl → 1.20.0.dev20250721__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.
flwr/cli/build.py CHANGED
@@ -184,7 +184,7 @@ def build_fab(app: Path) -> tuple[bytes, str, dict[str, Any]]:
184
184
  # Read the file content manually
185
185
  file_contents = file_path.read_bytes()
186
186
 
187
- archive_path = str(file_path.relative_to(app))
187
+ archive_path = str(file_path.relative_to(app)).replace("\\", "/")
188
188
  write_to_zip(fab_file, archive_path, file_contents)
189
189
 
190
190
  # Calculate file info
@@ -29,6 +29,7 @@ from flwr.common.logger import log
29
29
  from flwr.common.message import Message
30
30
  from flwr.common.retry_invoker import RetryInvoker
31
31
  from flwr.common.typing import Fab, Run
32
+ from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
32
33
 
33
34
 
34
35
  @contextmanager
@@ -43,12 +44,15 @@ def grpc_adapter( # pylint: disable=R0913,too-many-positional-arguments
43
44
  ] = None,
44
45
  ) -> Iterator[
45
46
  tuple[
46
- Callable[[], Optional[Message]],
47
- Callable[[Message], None],
47
+ Callable[[], Optional[tuple[Message, ObjectTree]]],
48
+ Callable[[Message, ObjectTree], set[str]],
48
49
  Callable[[], Optional[int]],
49
50
  Callable[[], None],
50
51
  Callable[[int], Run],
51
52
  Callable[[str, int], Fab],
53
+ Callable[[int, str], bytes],
54
+ Callable[[int, str, bytes], None],
55
+ Callable[[int, str], None],
52
56
  ]
53
57
  ]:
54
58
  """Primitives for request/response-based interaction with a server via GrpcAdapter.
@@ -77,12 +81,15 @@ def grpc_adapter( # pylint: disable=R0913,too-many-positional-arguments
77
81
 
78
82
  Returns
79
83
  -------
80
- receive : Callable
81
- send : Callable
84
+ receive : Callable[[], Optional[tuple[Message, ObjectTree]]]
85
+ send : Callable[[Message, ObjectTree], set[str]]
82
86
  create_node : Optional[Callable]
83
87
  delete_node : Optional[Callable]
84
88
  get_run : Optional[Callable]
85
89
  get_fab : Optional[Callable]
90
+ pull_object : Callable[[str], bytes]
91
+ push_object : Callable[[str, bytes], None]
92
+ confirm_message_received : Callable[[str], None]
86
93
  """
87
94
  if authentication_keys is not None:
88
95
  log(ERROR, "Client authentication is not supported for this transport type.")
@@ -17,7 +17,7 @@
17
17
 
18
18
  from collections.abc import Iterator, Sequence
19
19
  from contextlib import contextmanager
20
- from logging import DEBUG, ERROR
20
+ from logging import ERROR
21
21
  from pathlib import Path
22
22
  from typing import Callable, Optional, Union, cast
23
23
 
@@ -28,28 +28,18 @@ from flwr.common import GRPC_MAX_MESSAGE_LENGTH
28
28
  from flwr.common.constant import HEARTBEAT_CALL_TIMEOUT, HEARTBEAT_DEFAULT_INTERVAL
29
29
  from flwr.common.grpc import create_channel, on_channel_state_change
30
30
  from flwr.common.heartbeat import HeartbeatSender
31
- from flwr.common.inflatable import (
32
- get_all_nested_objects,
33
- get_object_tree,
34
- iterate_object_tree,
35
- no_object_id_recompute,
36
- )
37
31
  from flwr.common.inflatable_protobuf_utils import (
32
+ make_confirm_message_received_fn_protobuf,
38
33
  make_pull_object_fn_protobuf,
39
34
  make_push_object_fn_protobuf,
40
35
  )
41
- from flwr.common.inflatable_utils import (
42
- inflate_object_from_contents,
43
- pull_objects,
44
- push_objects,
45
- )
46
36
  from flwr.common.logger import log
47
37
  from flwr.common.message import Message, remove_content_from_message
48
38
  from flwr.common.retry_invoker import RetryInvoker, _wrap_stub
49
39
  from flwr.common.secure_aggregation.crypto.symmetric_encryption import (
50
40
  generate_key_pairs,
51
41
  )
52
- from flwr.common.serde import message_to_proto, run_from_proto
42
+ from flwr.common.serde import message_from_proto, message_to_proto, run_from_proto
53
43
  from flwr.common.typing import Fab, Run, RunNotRunningException
54
44
  from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
55
45
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
@@ -65,9 +55,7 @@ from flwr.proto.heartbeat_pb2 import ( # pylint: disable=E0611
65
55
  SendNodeHeartbeatRequest,
66
56
  SendNodeHeartbeatResponse,
67
57
  )
68
- from flwr.proto.message_pb2 import ( # pylint: disable=E0611
69
- ConfirmMessageReceivedRequest,
70
- )
58
+ from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
71
59
  from flwr.proto.node_pb2 import Node # pylint: disable=E0611
72
60
  from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611
73
61
 
@@ -88,12 +76,15 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
88
76
  adapter_cls: Optional[Union[type[FleetStub], type[GrpcAdapter]]] = None,
89
77
  ) -> Iterator[
90
78
  tuple[
91
- Callable[[], Optional[Message]],
92
- Callable[[Message], None],
79
+ Callable[[], Optional[tuple[Message, ObjectTree]]],
80
+ Callable[[Message, ObjectTree], set[str]],
93
81
  Callable[[], Optional[int]],
94
82
  Callable[[], None],
95
83
  Callable[[int], Run],
96
84
  Callable[[str, int], Fab],
85
+ Callable[[int, str], bytes],
86
+ Callable[[int, str, bytes], None],
87
+ Callable[[int, str], None],
97
88
  ]
98
89
  ]:
99
90
  """Primitives for request/response-based interaction with a server.
@@ -136,6 +127,9 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
136
127
  create_node : Optional[Callable]
137
128
  delete_node : Optional[Callable]
138
129
  get_run : Optional[Callable]
130
+ pull_object : Callable[[str], bytes]
131
+ push_object : Callable[[str, bytes], None]
132
+ confirm_message_received : Callable[[str], None]
139
133
  """
140
134
  if isinstance(root_certificates, str):
141
135
  root_certificates = Path(root_certificates).read_bytes()
@@ -246,98 +240,53 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
246
240
  # Cleanup
247
241
  node = None
248
242
 
249
- def receive() -> Optional[Message]:
250
- """Receive next message from server."""
243
+ def receive() -> Optional[tuple[Message, ObjectTree]]:
244
+ """Pull a message with its ObjectTree from SuperLink."""
251
245
  # Get Node
252
246
  if node is None:
253
247
  log(ERROR, "Node instance missing")
254
248
  return None
255
249
 
256
- # Request instructions (message) from server
250
+ # Try to pull a message with its object tree from SuperLink
257
251
  request = PullMessagesRequest(node=node)
258
252
  response: PullMessagesResponse = stub.PullMessages(request=request)
259
253
 
260
- # Get the current Messages
261
- message_proto = (
262
- None if len(response.messages_list) == 0 else response.messages_list[0]
263
- )
254
+ # If no messages are available, return None
255
+ if len(response.messages_list) == 0:
256
+ return None
264
257
 
265
- # Discard the current message if not valid
266
- if message_proto is not None and not (
267
- message_proto.metadata.dst_node_id == node.node_id
268
- ):
269
- message_proto = None
258
+ # Get the current Message and its object tree
259
+ message_proto = response.messages_list[0]
260
+ object_tree = response.message_object_trees[0]
270
261
 
271
262
  # Construct the Message
272
- in_message: Optional[Message] = None
273
-
274
- if message_proto:
275
- msg_id = message_proto.metadata.message_id
276
- run_id = message_proto.metadata.run_id
277
- object_tree = response.message_object_trees[0]
278
- all_object_contents = pull_objects(
279
- [tree.object_id for tree in iterate_object_tree(object_tree)],
280
- pull_object_fn=make_pull_object_fn_protobuf(
281
- pull_object_protobuf=stub.PullObject,
282
- node=node,
283
- run_id=run_id,
284
- ),
285
- )
286
-
287
- # Confirm that the message has been received
288
- stub.ConfirmMessageReceived(
289
- ConfirmMessageReceivedRequest(
290
- node=node, run_id=run_id, message_object_id=msg_id
291
- )
292
- )
293
-
294
- in_message = cast(
295
- Message, inflate_object_from_contents(msg_id, all_object_contents)
296
- )
297
- # The deflated message doesn't contain the message_id (its own object_id)
298
- # Inject
299
- in_message.metadata.__dict__["_message_id"] = msg_id
263
+ in_message = message_from_proto(message_proto)
300
264
 
301
- # Return the message if available
302
- return in_message
265
+ # Return the Message and its object tree
266
+ return in_message, object_tree
303
267
 
304
- def send(message: Message) -> None:
305
- """Send message reply to server."""
268
+ def send(message: Message, object_tree: ObjectTree) -> set[str]:
269
+ """Send the message with its ObjectTree to SuperLink."""
306
270
  # Get Node
307
271
  if node is None:
308
272
  log(ERROR, "Node instance missing")
309
- return
273
+ return set()
310
274
 
311
- with no_object_id_recompute():
312
- # Get all nested objects
313
- all_objects = get_all_nested_objects(message)
314
- object_tree = get_object_tree(message)
275
+ # Remove the content from the message if it has
276
+ if message.has_content():
277
+ message = remove_content_from_message(message)
315
278
 
316
- # Serialize Message
317
- message_proto = message_to_proto(
318
- message=remove_content_from_message(message)
319
- )
320
- request = PushMessagesRequest(
321
- node=node,
322
- messages_list=[message_proto],
323
- message_object_trees=[object_tree],
324
- )
325
- response: PushMessagesResponse = stub.PushMessages(request=request)
326
-
327
- if response.objects_to_push:
328
- objs_to_push = set(
329
- response.objects_to_push[message.object_id].object_ids
330
- )
331
- push_objects(
332
- all_objects,
333
- push_object_fn=make_push_object_fn_protobuf(
334
- push_object_protobuf=stub.PushObject,
335
- node=node,
336
- run_id=message.metadata.run_id,
337
- ),
338
- object_ids_to_push=objs_to_push,
339
- )
340
- log(DEBUG, "Pushed %s objects to servicer.", len(objs_to_push))
279
+ # Send the message with its ObjectTree to SuperLink
280
+ request = PushMessagesRequest(
281
+ node=node,
282
+ messages_list=[message_to_proto(message)],
283
+ message_object_trees=[object_tree],
284
+ )
285
+ response: PushMessagesResponse = stub.PushMessages(request=request)
286
+
287
+ # Get and return the object IDs to push
288
+ object_ids_to_push = response.objects_to_push[object_tree.object_id]
289
+ return set(object_ids_to_push.object_ids)
341
290
 
342
291
  def get_run(run_id: int) -> Run:
343
292
  # Call FleetAPI
@@ -354,9 +303,58 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
354
303
 
355
304
  return Fab(get_fab_response.fab.hash_str, get_fab_response.fab.content)
356
305
 
306
+ def pull_object(run_id: int, object_id: str) -> bytes:
307
+ """Pull the object from the SuperLink."""
308
+ # Check Node
309
+ if node is None:
310
+ raise RuntimeError("Node instance missing")
311
+
312
+ fn = make_pull_object_fn_protobuf(
313
+ pull_object_protobuf=stub.PullObject,
314
+ node=node,
315
+ run_id=run_id,
316
+ )
317
+ return fn(object_id)
318
+
319
+ def push_object(run_id: int, object_id: str, contents: bytes) -> None:
320
+ """Push the object to the SuperLink."""
321
+ # Check Node
322
+ if node is None:
323
+ raise RuntimeError("Node instance missing")
324
+
325
+ fn = make_push_object_fn_protobuf(
326
+ push_object_protobuf=stub.PushObject,
327
+ node=node,
328
+ run_id=run_id,
329
+ )
330
+ fn(object_id, contents)
331
+
332
+ def confirm_message_received(run_id: int, object_id: str) -> None:
333
+ """Confirm that the message has been received."""
334
+ # Check Node
335
+ if node is None:
336
+ raise RuntimeError("Node instance missing")
337
+
338
+ fn = make_confirm_message_received_fn_protobuf(
339
+ confirm_message_received_protobuf=stub.ConfirmMessageReceived,
340
+ node=node,
341
+ run_id=run_id,
342
+ )
343
+ fn(object_id)
344
+
357
345
  try:
358
346
  # Yield methods
359
- yield (receive, send, create_node, delete_node, get_run, get_fab)
347
+ yield (
348
+ receive,
349
+ send,
350
+ create_node,
351
+ delete_node,
352
+ get_run,
353
+ get_fab,
354
+ pull_object,
355
+ push_object,
356
+ confirm_message_received,
357
+ )
360
358
  except Exception as exc: # pylint: disable=broad-except
361
359
  log(ERROR, exc)
362
360
  # Cleanup
@@ -17,8 +17,8 @@
17
17
 
18
18
  from collections.abc import Iterator
19
19
  from contextlib import contextmanager
20
- from logging import DEBUG, ERROR, INFO, WARN
21
- from typing import Callable, Optional, TypeVar, Union, cast
20
+ from logging import ERROR, WARN
21
+ from typing import Callable, Optional, TypeVar, Union
22
22
 
23
23
  from cryptography.hazmat.primitives.asymmetric import ec
24
24
  from google.protobuf.message import Message as GrpcMessage
@@ -28,25 +28,15 @@ from flwr.common import GRPC_MAX_MESSAGE_LENGTH
28
28
  from flwr.common.constant import HEARTBEAT_DEFAULT_INTERVAL
29
29
  from flwr.common.exit import ExitCode, flwr_exit
30
30
  from flwr.common.heartbeat import HeartbeatSender
31
- from flwr.common.inflatable import (
32
- get_all_nested_objects,
33
- get_object_tree,
34
- iterate_object_tree,
35
- no_object_id_recompute,
36
- )
37
31
  from flwr.common.inflatable_protobuf_utils import (
32
+ make_confirm_message_received_fn_protobuf,
38
33
  make_pull_object_fn_protobuf,
39
34
  make_push_object_fn_protobuf,
40
35
  )
41
- from flwr.common.inflatable_utils import (
42
- inflate_object_from_contents,
43
- pull_objects,
44
- push_objects,
45
- )
46
36
  from flwr.common.logger import log
47
37
  from flwr.common.message import Message, remove_content_from_message
48
38
  from flwr.common.retry_invoker import RetryInvoker
49
- from flwr.common.serde import message_to_proto, run_from_proto
39
+ from flwr.common.serde import message_from_proto, message_to_proto, run_from_proto
50
40
  from flwr.common.typing import Fab, Run
51
41
  from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
52
42
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
@@ -66,6 +56,7 @@ from flwr.proto.heartbeat_pb2 import ( # pylint: disable=E0611
66
56
  from flwr.proto.message_pb2 import ( # pylint: disable=E0611
67
57
  ConfirmMessageReceivedRequest,
68
58
  ConfirmMessageReceivedResponse,
59
+ ObjectTree,
69
60
  PullObjectRequest,
70
61
  PullObjectResponse,
71
62
  PushObjectRequest,
@@ -108,12 +99,15 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
108
99
  ] = None,
109
100
  ) -> Iterator[
110
101
  tuple[
111
- Callable[[], Optional[Message]],
112
- Callable[[Message], None],
102
+ Callable[[], Optional[tuple[Message, ObjectTree]]],
103
+ Callable[[Message, ObjectTree], set[str]],
113
104
  Callable[[], Optional[int]],
114
105
  Callable[[], None],
115
106
  Callable[[int], Run],
116
107
  Callable[[str, int], Fab],
108
+ Callable[[int, str], bytes],
109
+ Callable[[int, str, bytes], None],
110
+ Callable[[int, str], None],
117
111
  ]
118
112
  ]:
119
113
  """Primitives for request/response-based interaction with a server.
@@ -149,6 +143,9 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
149
143
  create_node : Optional[Callable]
150
144
  delete_node : Optional[Callable]
151
145
  get_run : Optional[Callable]
146
+ pull_object : Callable[[str], bytes]
147
+ push_object : Callable[[str, bytes], None]
148
+ confirm_message_received : Callable[[str], None]
152
149
  """
153
150
  log(
154
151
  WARN,
@@ -230,6 +227,38 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
230
227
  grpc_res.ParseFromString(res.content)
231
228
  return grpc_res
232
229
 
230
+ def _pull_object_protobuf(request: PullObjectRequest) -> PullObjectResponse:
231
+ res = _request(
232
+ req=request,
233
+ res_type=PullObjectResponse,
234
+ api_path=PATH_PULL_OBJECT,
235
+ )
236
+ if res is None:
237
+ raise ValueError(f"{PullObjectResponse.__name__} is None.")
238
+ return res
239
+
240
+ def _push_object_protobuf(request: PushObjectRequest) -> PushObjectResponse:
241
+ res = _request(
242
+ req=request,
243
+ res_type=PushObjectResponse,
244
+ api_path=PATH_PUSH_OBJECT,
245
+ )
246
+ if res is None:
247
+ raise ValueError(f"{PushObjectResponse.__name__} is None.")
248
+ return res
249
+
250
+ def _confirm_message_received_protobuf(
251
+ request: ConfirmMessageReceivedRequest,
252
+ ) -> ConfirmMessageReceivedResponse:
253
+ res = _request(
254
+ req=request,
255
+ res_type=ConfirmMessageReceivedResponse,
256
+ api_path=PATH_CONFIRM_MESSAGE_RECEIVED,
257
+ )
258
+ if res is None:
259
+ raise ValueError(f"{ConfirmMessageReceivedResponse.__name__} is None.")
260
+ return res
261
+
233
262
  def send_node_heartbeat() -> bool:
234
263
  # Get Node
235
264
  if node is None:
@@ -277,8 +306,7 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
277
306
  """Set delete_node."""
278
307
  nonlocal node
279
308
  if node is None:
280
- log(ERROR, "Node instance missing")
281
- return
309
+ raise RuntimeError("Node instance missing")
282
310
 
283
311
  # Stop the heartbeat sender
284
312
  heartbeat_sender.stop()
@@ -294,143 +322,55 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
294
322
  # Cleanup
295
323
  node = None
296
324
 
297
- def receive() -> Optional[Message]:
298
- """Receive next Message from server."""
325
+ def receive() -> Optional[tuple[Message, ObjectTree]]:
326
+ """Pull a message with its ObjectTree from SuperLink."""
299
327
  # Get Node
300
328
  if node is None:
301
- log(ERROR, "Node instance missing")
302
- return None
329
+ raise RuntimeError("Node instance missing")
303
330
 
304
- # Request instructions (message) from server
331
+ # Try to pull a message with its object tree from SuperLink
305
332
  req = PullMessagesRequest(node=node)
306
-
307
- # Send the request
308
333
  res = _request(req, PullMessagesResponse, PATH_PULL_MESSAGES)
309
334
  if res is None:
310
- return None
335
+ raise ValueError("PushMessagesResponse is None.")
311
336
 
312
- # Get the current Messages
313
- message_proto = None if len(res.messages_list) == 0 else res.messages_list[0]
337
+ # If no messages are available, return None
338
+ if len(res.messages_list) == 0:
339
+ return None
314
340
 
315
- # Discard the current message if not valid
316
- if message_proto is not None and not (
317
- message_proto.metadata.dst_node_id == node.node_id
318
- ):
319
- message_proto = None
341
+ # Get the current Message and its object tree
342
+ message_proto = res.messages_list[0]
343
+ object_tree = res.message_object_trees[0]
320
344
 
321
345
  # Construct the Message
322
- in_message: Optional[Message] = None
323
-
324
- if message_proto:
325
- log(INFO, "[Node] POST /%s: success", PATH_PULL_MESSAGES)
326
- msg_id = message_proto.metadata.message_id
327
- run_id = message_proto.metadata.run_id
328
-
329
- def fn(request: PullObjectRequest) -> PullObjectResponse:
330
- res = _request(
331
- req=request, res_type=PullObjectResponse, api_path=PATH_PULL_OBJECT
332
- )
333
- if res is None:
334
- raise ValueError("PushObjectResponse is None.")
335
- return res
336
-
337
- try:
338
- object_tree = res.message_object_trees[0]
339
- all_object_contents = pull_objects(
340
- [tree.object_id for tree in iterate_object_tree(object_tree)],
341
- pull_object_fn=make_pull_object_fn_protobuf(
342
- pull_object_protobuf=fn,
343
- node=node,
344
- run_id=run_id,
345
- ),
346
- )
347
-
348
- # Confirm that the message has been received
349
- _request(
350
- req=ConfirmMessageReceivedRequest(
351
- node=node, run_id=run_id, message_object_id=msg_id
352
- ),
353
- res_type=ConfirmMessageReceivedResponse,
354
- api_path=PATH_CONFIRM_MESSAGE_RECEIVED,
355
- )
356
- except ValueError as e:
357
- log(
358
- ERROR,
359
- "Pulling objects failed. Potential irrecoverable error: %s",
360
- str(e),
361
- )
362
- in_message = cast(
363
- Message, inflate_object_from_contents(msg_id, all_object_contents)
364
- )
365
- # The deflated message doesn't contain the message_id (its own object_id)
366
- # Inject
367
- in_message.metadata.__dict__["_message_id"] = msg_id
346
+ in_message = message_from_proto(message_proto)
368
347
 
369
- return in_message
348
+ # Return the Message and its object tree
349
+ return in_message, object_tree
370
350
 
371
- def send(message: Message) -> None:
372
- """Send Message result back to server."""
351
+ def send(message: Message, object_tree: ObjectTree) -> set[str]:
352
+ """Send the message with its ObjectTree to SuperLink."""
373
353
  # Get Node
374
354
  if node is None:
375
- log(ERROR, "Node instance missing")
376
- return
355
+ raise RuntimeError("Node instance missing")
377
356
 
378
- with no_object_id_recompute():
379
- # Get all nested objects
380
- all_objects = get_all_nested_objects(message)
381
- object_tree = get_object_tree(message)
357
+ # Remove the content from the message if it has
358
+ if message.has_content():
359
+ message = remove_content_from_message(message)
382
360
 
383
- # Serialize Message
384
- message_proto = message_to_proto(
385
- message=remove_content_from_message(message)
386
- )
387
- req = PushMessagesRequest(
388
- node=node,
389
- messages_list=[message_proto],
390
- message_object_trees=[object_tree],
391
- )
361
+ # Send the message with its ObjectTree to SuperLink
362
+ req = PushMessagesRequest(
363
+ node=node,
364
+ messages_list=[message_to_proto(message)],
365
+ message_object_trees=[object_tree],
366
+ )
367
+ res = _request(req, PushMessagesResponse, PATH_PUSH_MESSAGES)
368
+ if res is None:
369
+ raise ValueError("PushMessagesResponse is None.")
392
370
 
393
- # Send the request
394
- res = _request(req, PushMessagesResponse, PATH_PUSH_MESSAGES)
395
- if res:
396
- log(
397
- INFO,
398
- "[Node] POST /%s: success, created result %s",
399
- PATH_PUSH_MESSAGES,
400
- res.results, # pylint: disable=no-member
401
- )
402
-
403
- if res and res.objects_to_push:
404
- objs_to_push = set(res.objects_to_push[message.object_id].object_ids)
405
-
406
- def fn(request: PushObjectRequest) -> PushObjectResponse:
407
- res = _request(
408
- req=request,
409
- res_type=PushObjectResponse,
410
- api_path=PATH_PUSH_OBJECT,
411
- )
412
- if res is None:
413
- raise ValueError("PushObjectResponse is None.")
414
- return res
415
-
416
- try:
417
- push_objects(
418
- all_objects,
419
- push_object_fn=make_push_object_fn_protobuf(
420
- push_object_protobuf=fn,
421
- node=node,
422
- run_id=message_proto.metadata.run_id,
423
- ),
424
- object_ids_to_push=objs_to_push,
425
- )
426
- log(DEBUG, "Pushed %s objects to servicer.", len(objs_to_push))
427
- except ValueError as e:
428
- log(
429
- ERROR,
430
- "Pushing objects failed. Potential irrecoverable error: %s",
431
- str(e),
432
- )
433
- log(ERROR, str(e))
371
+ # Get and return the object IDs to push
372
+ object_ids_to_push = res.objects_to_push[object_tree.object_id]
373
+ return set(object_ids_to_push.object_ids)
434
374
 
435
375
  def get_run(run_id: int) -> Run:
436
376
  # Construct the request
@@ -457,9 +397,58 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
457
397
  res.fab.content,
458
398
  )
459
399
 
400
+ def pull_object(run_id: int, object_id: str) -> bytes:
401
+ """Pull the object from the SuperLink."""
402
+ # Check Node
403
+ if node is None:
404
+ raise RuntimeError("Node instance missing")
405
+
406
+ fn = make_pull_object_fn_protobuf(
407
+ pull_object_protobuf=_pull_object_protobuf,
408
+ node=node,
409
+ run_id=run_id,
410
+ )
411
+ return fn(object_id)
412
+
413
+ def push_object(run_id: int, object_id: str, contents: bytes) -> None:
414
+ """Push the object to the SuperLink."""
415
+ # Check Node
416
+ if node is None:
417
+ raise RuntimeError("Node instance missing")
418
+
419
+ fn = make_push_object_fn_protobuf(
420
+ push_object_protobuf=_push_object_protobuf,
421
+ node=node,
422
+ run_id=run_id,
423
+ )
424
+ fn(object_id, contents)
425
+
426
+ def confirm_message_received(run_id: int, object_id: str) -> None:
427
+ """Confirm that the message has been received."""
428
+ # Check Node
429
+ if node is None:
430
+ raise RuntimeError("Node instance missing")
431
+
432
+ fn = make_confirm_message_received_fn_protobuf(
433
+ confirm_message_received_protobuf=_confirm_message_received_protobuf,
434
+ node=node,
435
+ run_id=run_id,
436
+ )
437
+ fn(object_id)
438
+
460
439
  try:
461
440
  # Yield methods
462
- yield (receive, send, create_node, delete_node, get_run, get_fab)
441
+ yield (
442
+ receive,
443
+ send,
444
+ create_node,
445
+ delete_node,
446
+ get_run,
447
+ get_fab,
448
+ pull_object,
449
+ push_object,
450
+ confirm_message_received,
451
+ )
463
452
  except Exception as exc: # pylint: disable=broad-except
464
453
  log(ERROR, exc)
465
454
  # Cleanup
@@ -19,6 +19,7 @@ import os
19
19
  import random
20
20
  import threading
21
21
  import time
22
+ from collections.abc import Iterable, Iterator
22
23
  from typing import Callable, Optional, TypeVar
23
24
 
24
25
  from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
@@ -110,24 +111,61 @@ def push_objects(
110
111
  max_concurrent_pushes : int (default: MAX_CONCURRENT_PUSHES)
111
112
  The maximum number of concurrent pushes to perform.
112
113
  """
113
- if object_ids_to_push is not None:
114
- # Filter objects to push only those with IDs in the set
115
- objects = {k: v for k, v in objects.items() if k in object_ids_to_push}
116
-
117
114
  lock = threading.Lock()
118
115
 
119
- def push(obj_id: str) -> None:
116
+ def iter_dict_items() -> Iterator[tuple[str, bytes]]:
117
+ """Iterate over the dictionary items."""
118
+ for obj_id in list(objects.keys()):
119
+ # Skip the object if no need to push it
120
+ if object_ids_to_push is not None and obj_id not in object_ids_to_push:
121
+ continue
122
+
123
+ # Deflate the object content
124
+ object_content = objects[obj_id].deflate()
125
+ if not keep_objects:
126
+ with lock:
127
+ del objects[obj_id]
128
+
129
+ yield obj_id, object_content
130
+
131
+ push_object_contents_from_iterable(
132
+ iter_dict_items(),
133
+ push_object_fn,
134
+ max_concurrent_pushes=max_concurrent_pushes,
135
+ )
136
+
137
+
138
+ def push_object_contents_from_iterable(
139
+ object_contents: Iterable[tuple[str, bytes]],
140
+ push_object_fn: Callable[[str, bytes], None],
141
+ *,
142
+ max_concurrent_pushes: int = MAX_CONCURRENT_PUSHES,
143
+ ) -> None:
144
+ """Push multiple object contents to the servicer.
145
+
146
+ Parameters
147
+ ----------
148
+ object_contents : Iterable[tuple[str, bytes]]
149
+ An iterable of `(object_id, object_content)` pairs.
150
+ `object_id` is the object ID, and `object_content` is the object content.
151
+ push_object_fn : Callable[[str, bytes], None]
152
+ A function that takes an object ID and its content as bytes, and pushes
153
+ it to the servicer. This function should raise `ObjectIdNotPreregisteredError`
154
+ if the object ID is not pre-registered.
155
+ max_concurrent_pushes : int (default: MAX_CONCURRENT_PUSHES)
156
+ The maximum number of concurrent pushes to perform.
157
+ """
158
+
159
+ def push(args: tuple[str, bytes]) -> None:
120
160
  """Push a single object."""
121
- object_content = objects[obj_id].deflate()
122
- if not keep_objects:
123
- with lock:
124
- del objects[obj_id]
125
- push_object_fn(obj_id, object_content)
161
+ obj_id, obj_content = args
162
+ # Push the object using the provided function
163
+ push_object_fn(obj_id, obj_content)
126
164
 
127
- # Push all objects concurrently
165
+ # Push all object contents concurrently
128
166
  num_workers = get_num_workers(max_concurrent_pushes)
129
167
  with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor:
130
- list(executor.map(push, list(objects.keys())))
168
+ list(executor.map(push, object_contents))
131
169
 
132
170
 
133
171
  def pull_objects( # pylint: disable=too-many-arguments,too-many-locals
@@ -61,8 +61,7 @@ from flwr.server.superlink.linkstate import LinkState
61
61
  from flwr.server.superlink.utils import check_abort
62
62
  from flwr.supercore.ffs import Ffs
63
63
  from flwr.supercore.object_store import NoObjectInStoreError, ObjectStore
64
-
65
- from ...utils import store_mapping_and_register_objects
64
+ from flwr.supercore.object_store.utils import store_mapping_and_register_objects
66
65
 
67
66
 
68
67
  def create_node(
@@ -87,8 +87,7 @@ from flwr.server.superlink.utils import abort_if
87
87
  from flwr.server.utils.validator import validate_message
88
88
  from flwr.supercore.ffs import Ffs, FfsFactory
89
89
  from flwr.supercore.object_store import NoObjectInStoreError, ObjectStoreFactory
90
-
91
- from ..utils import store_mapping_and_register_objects
90
+ from flwr.supercore.object_store.utils import store_mapping_and_register_objects
92
91
 
93
92
 
94
93
  class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
@@ -0,0 +1,48 @@
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
+ """Utils for ObjectStore."""
16
+
17
+
18
+ from typing import Union
19
+
20
+ from flwr.proto.appio_pb2 import PushAppMessagesRequest # pylint: disable=E0611
21
+ from flwr.proto.fleet_pb2 import PushMessagesRequest # pylint: disable=E0611
22
+ from flwr.proto.message_pb2 import ObjectIDs # pylint: disable=E0611
23
+
24
+ from . import ObjectStore
25
+
26
+
27
+ def store_mapping_and_register_objects(
28
+ store: ObjectStore, request: Union[PushAppMessagesRequest, PushMessagesRequest]
29
+ ) -> dict[str, ObjectIDs]:
30
+ """Store Message object to descendants mapping and preregister objects."""
31
+ if not request.messages_list:
32
+ return {}
33
+
34
+ objects_to_push: dict[str, ObjectIDs] = {}
35
+
36
+ # Get run_id from the first message in the list
37
+ # All messages of a request should in the same run
38
+ run_id = request.messages_list[0].metadata.run_id
39
+
40
+ for object_tree in request.message_object_trees:
41
+ # Preregister
42
+ object_ids_just_registered = store.preregister(run_id, object_tree)
43
+ # Keep track of objects that need to be pushed
44
+ objects_to_push[object_tree.object_id] = ObjectIDs(
45
+ object_ids=object_ids_just_registered
46
+ )
47
+
48
+ return objects_to_push
@@ -32,13 +32,24 @@ from flwr.common import Context, Message
32
32
  from flwr.common.config import get_flwr_dir
33
33
  from flwr.common.constant import ErrorCode
34
34
  from flwr.common.grpc import create_channel, on_channel_state_change
35
+ from flwr.common.inflatable import (
36
+ get_all_nested_objects,
37
+ get_object_tree,
38
+ no_object_id_recompute,
39
+ )
40
+ from flwr.common.inflatable_protobuf_utils import (
41
+ make_confirm_message_received_fn_protobuf,
42
+ make_pull_object_fn_protobuf,
43
+ make_push_object_fn_protobuf,
44
+ )
45
+ from flwr.common.inflatable_utils import pull_and_inflate_object_from_tree, push_objects
35
46
  from flwr.common.logger import log
47
+ from flwr.common.message import remove_content_from_message
36
48
  from flwr.common.retry_invoker import _make_simple_grpc_retry_invoker, _wrap_stub
37
49
  from flwr.common.serde import (
38
50
  context_from_proto,
39
51
  context_to_proto,
40
52
  fab_from_proto,
41
- message_from_proto,
42
53
  message_to_proto,
43
54
  run_from_proto,
44
55
  )
@@ -61,6 +72,7 @@ from flwr.proto.clientappio_pb2 import (
61
72
  RequestTokenResponse,
62
73
  )
63
74
  from flwr.proto.clientappio_pb2_grpc import ClientAppIoStub
75
+ from flwr.proto.node_pb2 import Node # pylint: disable=E0611
64
76
  from flwr.supercore.utils import mask_string
65
77
 
66
78
 
@@ -204,12 +216,6 @@ def pull_clientappinputs(
204
216
  masked_token = mask_string(token)
205
217
  log(INFO, "[flwr-clientapp] Pull `ClientAppInputs` for token %s", masked_token)
206
218
  try:
207
- # Pull Message
208
- pull_msg_res: PullAppMessagesResponse = stub.PullMessage(
209
- PullAppMessagesRequest(token=token)
210
- )
211
- message = message_from_proto(pull_msg_res.messages_list[0])
212
-
213
219
  # Pull Context, Run and (optional) FAB
214
220
  res: PullAppInputsResponse = stub.PullClientAppInputs(
215
221
  PullAppInputsRequest(token=token)
@@ -217,6 +223,26 @@ def pull_clientappinputs(
217
223
  context = context_from_proto(res.context)
218
224
  run = run_from_proto(res.run)
219
225
  fab = fab_from_proto(res.fab) if res.fab else None
226
+
227
+ # Pull and inflate the message
228
+ pull_msg_res: PullAppMessagesResponse = stub.PullMessage(
229
+ PullAppMessagesRequest(token=token)
230
+ )
231
+ run_id = context.run_id
232
+ node = Node(node_id=context.node_id)
233
+ object_tree = pull_msg_res.message_object_trees[0]
234
+ message = pull_and_inflate_object_from_tree(
235
+ object_tree,
236
+ make_pull_object_fn_protobuf(stub.PullObject, node, run_id),
237
+ make_confirm_message_received_fn_protobuf(
238
+ stub.ConfirmMessageReceived, node, run_id
239
+ ),
240
+ return_type=Message,
241
+ )
242
+
243
+ # Set the message ID
244
+ # The deflated message doesn't contain the message_id (its own object_id)
245
+ message.metadata.__dict__["_message_id"] = object_tree.object_id
220
246
  return message, context, run, fab
221
247
  except grpc.RpcError as e:
222
248
  log(ERROR, "[PullClientAppInputs] gRPC error occurred: %s", str(e))
@@ -231,15 +257,43 @@ def push_clientappoutputs(
231
257
  log(INFO, "[flwr-clientapp] Push `ClientAppOutputs` for token %s", masked_token)
232
258
  # Set message ID
233
259
  message.metadata.__dict__["_message_id"] = message.object_id
234
- proto_message = message_to_proto(message)
260
+ proto_message = message_to_proto(remove_content_from_message(message))
235
261
  proto_context = context_to_proto(context)
236
262
 
237
263
  try:
238
264
 
239
- # Push Message
240
- _ = stub.PushMessage(
241
- PushAppMessagesRequest(token=token, messages_list=[proto_message])
242
- )
265
+ with no_object_id_recompute():
266
+ # Get object tree and all objects to push
267
+ object_tree = get_object_tree(message)
268
+
269
+ # Push Message
270
+ # This is temporary. The message should not contain its content
271
+ push_msg_res = stub.PushMessage(
272
+ PushAppMessagesRequest(
273
+ token=token,
274
+ messages_list=[proto_message],
275
+ message_object_trees=[object_tree],
276
+ )
277
+ )
278
+ del proto_message
279
+
280
+ # Retrieve the object IDs to push
281
+ object_ids_to_push = set(
282
+ push_msg_res.objects_to_push[object_tree.object_id].object_ids
283
+ )
284
+
285
+ # Push all objects
286
+ all_objects = get_all_nested_objects(message)
287
+ del message
288
+ push_objects(
289
+ all_objects,
290
+ make_push_object_fn_protobuf(
291
+ stub.PushObject,
292
+ Node(node_id=context.node_id),
293
+ run_id=context.run_id,
294
+ ),
295
+ object_ids_to_push=object_ids_to_push,
296
+ )
243
297
 
244
298
  # Push Context
245
299
  res: PushAppOutputsResponse = stub.PushClientAppOutputs(
@@ -63,6 +63,7 @@ from flwr.proto.message_pb2 import (
63
63
  # pylint: disable=E0601
64
64
  from flwr.supercore.ffs import FfsFactory
65
65
  from flwr.supercore.object_store import NoObjectInStoreError, ObjectStoreFactory
66
+ from flwr.supercore.object_store.utils import store_mapping_and_register_objects
66
67
  from flwr.supernode.nodestate import NodeStateFactory
67
68
 
68
69
 
@@ -178,8 +179,9 @@ class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
178
179
  self, request: PullAppMessagesRequest, context: grpc.ServicerContext
179
180
  ) -> PullAppMessagesResponse:
180
181
  """Pull one Message."""
181
- # Initialize state and ffs connection
182
+ # Initialize state and store connection
182
183
  state = self.state_factory.state()
184
+ store = self.objectstore_factory.store()
183
185
 
184
186
  # Validate the token
185
187
  run_id = state.get_run_id_by_token(request.token)
@@ -193,14 +195,21 @@ class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
193
195
  # Retrieve message for this run
194
196
  message = state.get_messages(run_ids=[run_id], is_reply=False)[0]
195
197
 
196
- return PullAppMessagesResponse(messages_list=[message_to_proto(message)])
198
+ # Retrieve the object tree for the message
199
+ object_tree = store.get_object_tree(message.metadata.message_id)
200
+
201
+ return PullAppMessagesResponse(
202
+ messages_list=[message_to_proto(message)],
203
+ message_object_trees=[object_tree],
204
+ )
197
205
 
198
206
  def PushMessage(
199
207
  self, request: PushAppMessagesRequest, context: grpc.ServicerContext
200
208
  ) -> PushAppMessagesResponse:
201
209
  """Push one Message."""
202
- # Initialize state connection
210
+ # Initialize state and store connection
203
211
  state = self.state_factory.state()
212
+ store = self.objectstore_factory.store()
204
213
 
205
214
  # Validate the token
206
215
  run_id = state.get_run_id_by_token(request.token)
@@ -214,7 +223,10 @@ class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
214
223
  # Save the message to the state
215
224
  state.store_message(message_from_proto(request.messages_list[0]))
216
225
 
217
- return PushAppMessagesResponse()
226
+ # Store Message object to descendants mapping and preregister objects
227
+ objects_to_push = store_mapping_and_register_objects(store, request=request)
228
+
229
+ return PushAppMessagesResponse(objects_to_push=objects_to_push)
218
230
 
219
231
  def PushObject(
220
232
  self, request: PushObjectRequest, context: grpc.ServicerContext
@@ -20,6 +20,7 @@ import subprocess
20
20
  import time
21
21
  from collections.abc import Iterator
22
22
  from contextlib import contextmanager
23
+ from functools import partial
23
24
  from logging import INFO, WARN
24
25
  from pathlib import Path
25
26
  from typing import Callable, Optional, Union
@@ -47,16 +48,17 @@ from flwr.common.constant import (
47
48
  from flwr.common.exit import ExitCode, flwr_exit
48
49
  from flwr.common.exit_handlers import register_exit_handlers
49
50
  from flwr.common.grpc import generic_create_grpc_server
50
- from flwr.common.inflatable import (
51
- get_all_nested_objects,
52
- get_object_tree,
53
- no_object_id_recompute,
51
+ from flwr.common.inflatable import iterate_object_tree
52
+ from flwr.common.inflatable_utils import (
53
+ pull_objects,
54
+ push_object_contents_from_iterable,
54
55
  )
55
56
  from flwr.common.logger import log
56
57
  from flwr.common.retry_invoker import RetryInvoker, RetryState, exponential
57
58
  from flwr.common.telemetry import EventType
58
59
  from flwr.common.typing import Fab, Run, RunNotRunningException, UserConfig
59
60
  from flwr.proto.clientappio_pb2_grpc import add_ClientAppIoServicer_to_server
61
+ from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
60
62
  from flwr.supercore.ffs import Ffs, FfsFactory
61
63
  from flwr.supercore.object_store import ObjectStore, ObjectStoreFactory
62
64
  from flwr.supernode.nodestate import NodeState, NodeStateFactory
@@ -172,7 +174,17 @@ def start_client_internal(
172
174
  max_retries=max_retries,
173
175
  max_wait_time=max_wait_time,
174
176
  ) as conn:
175
- receive, send, create_node, _, get_run, get_fab = conn
177
+ (
178
+ receive,
179
+ send,
180
+ create_node,
181
+ _,
182
+ get_run,
183
+ get_fab,
184
+ pull_object,
185
+ push_object,
186
+ confirm_message_received,
187
+ ) = conn
176
188
 
177
189
  # Call create_node fn to register node
178
190
  # and store node_id in state
@@ -192,6 +204,8 @@ def start_client_internal(
192
204
  receive=receive,
193
205
  get_run=get_run,
194
206
  get_fab=get_fab,
207
+ pull_object=pull_object,
208
+ confirm_message_received=confirm_message_received,
195
209
  )
196
210
 
197
211
  # Two isolation modes:
@@ -223,7 +237,12 @@ def start_client_internal(
223
237
  ]
224
238
  subprocess.run(command, check=False)
225
239
 
226
- _push_messages(state=state, send=send)
240
+ _push_messages(
241
+ state=state,
242
+ object_store=store,
243
+ send=send,
244
+ push_object=push_object,
245
+ )
227
246
 
228
247
  # Sleep for 3 seconds before the next iteration
229
248
  time.sleep(3)
@@ -232,11 +251,13 @@ def start_client_internal(
232
251
  def _pull_and_store_message( # pylint: disable=too-many-positional-arguments
233
252
  state: NodeState,
234
253
  ffs: Ffs,
235
- object_store: ObjectStore, # pylint: disable=unused-argument
254
+ object_store: ObjectStore,
236
255
  node_config: UserConfig,
237
- receive: Callable[[], Optional[Message]],
256
+ receive: Callable[[], Optional[tuple[Message, ObjectTree]]],
238
257
  get_run: Callable[[int], Run],
239
258
  get_fab: Callable[[str, int], Fab],
259
+ pull_object: Callable[[int, str], bytes],
260
+ confirm_message_received: Callable[[int, str], None],
240
261
  ) -> Optional[int]:
241
262
  """Pull a message from the SuperLink and store it in the state.
242
263
 
@@ -248,8 +269,9 @@ def _pull_and_store_message( # pylint: disable=too-many-positional-arguments
248
269
  message = None
249
270
  try:
250
271
  # Pull message
251
- if (message := receive()) is None:
272
+ if (recv := receive()) is None:
252
273
  return None
274
+ message, object_tree = recv
253
275
 
254
276
  # Log message reception
255
277
  log(INFO, "")
@@ -293,16 +315,22 @@ def _pull_and_store_message( # pylint: disable=too-many-positional-arguments
293
315
  )
294
316
  state.store_context(run_ctx)
295
317
 
296
- # Store the message in the state
297
- # This shall be removed after the transition to ObjectStore
318
+ # Preregister the object tree of the message
319
+ obj_ids_to_pull = object_store.preregister(run_id, object_tree)
320
+
321
+ # Store the message in the state (note this message has no content)
298
322
  state.store_message(message)
299
323
 
300
- # Store the message in ObjectStore
301
- # This is a temporary solution to store messages in ObjectStore
302
- with no_object_id_recompute():
303
- object_store.preregister(run_id, get_object_tree(message))
304
- for obj_id, obj in get_all_nested_objects(message).items():
305
- object_store.put(obj_id, obj.deflate())
324
+ # Pull and store objects of the message in the ObjectStore
325
+ obj_contents = pull_objects(
326
+ obj_ids_to_pull,
327
+ pull_object_fn=lambda obj_id: pull_object(run_id, obj_id),
328
+ )
329
+ for obj_id in list(obj_contents.keys()):
330
+ object_store.put(obj_id, obj_contents.pop(obj_id))
331
+
332
+ # Confirm that the message was received
333
+ confirm_message_received(run_id, message.metadata.message_id)
306
334
 
307
335
  except RunNotRunningException:
308
336
  if message is None:
@@ -325,7 +353,9 @@ def _pull_and_store_message( # pylint: disable=too-many-positional-arguments
325
353
 
326
354
  def _push_messages(
327
355
  state: NodeState,
328
- send: Callable[[Message], None],
356
+ object_store: ObjectStore,
357
+ send: Callable[[Message, ObjectTree], set[str]],
358
+ push_object: Callable[[int, str, bytes], None],
329
359
  ) -> None:
330
360
  """Push reply messages to the SuperLink."""
331
361
  # Get messages to send
@@ -349,9 +379,34 @@ def _push_messages(
349
379
  message.metadata.message_type,
350
380
  )
351
381
 
382
+ # Get the object tree for the message
383
+ object_tree = object_store.get_object_tree(message.metadata.message_id)
384
+
385
+ # Define the iterator for yielding object contents
386
+ # This will yield (object_id, content) pairs
387
+ def yield_object_contents(_obj_tree: ObjectTree) -> Iterator[tuple[str, bytes]]:
388
+ for tree in iterate_object_tree(_obj_tree):
389
+ while (content := object_store.get(tree.object_id)) is None:
390
+ # Wait for the content to be available
391
+ time.sleep(0.5)
392
+
393
+ yield tree.object_id, content
394
+
352
395
  # Send the message
353
396
  try:
354
- send(message)
397
+ # Send the reply message with its ObjectTree
398
+ send(message, object_tree)
399
+
400
+ # Push object contents from the ObjectStore
401
+ run_id = message.metadata.run_id
402
+ push_object_contents_from_iterable(
403
+ yield_object_contents(object_tree),
404
+ # Use functools.partial to bind run_id explicitly,
405
+ # avoiding late binding issues and satisfying flake8 (B023)
406
+ # Equivalent to:
407
+ # lambda object_id, content: push_object(run_id, object_id, content)
408
+ push_object_fn=partial(push_object, run_id),
409
+ )
355
410
  log(INFO, "Sent successfully")
356
411
  except RunNotRunningException:
357
412
  log(
@@ -369,6 +424,11 @@ def _push_messages(
369
424
  ]
370
425
  )
371
426
 
427
+ # Delete all its objects from the ObjectStore
428
+ # No need to delete objects of the message it replies to, as it is
429
+ # already deleted when the ClientApp calls `ConfirmMessageReceived`
430
+ object_store.delete(message.metadata.message_id)
431
+
372
432
 
373
433
  @contextmanager
374
434
  def _init_connection( # pylint: disable=too-many-positional-arguments
@@ -383,12 +443,15 @@ def _init_connection( # pylint: disable=too-many-positional-arguments
383
443
  max_wait_time: Optional[float] = None,
384
444
  ) -> Iterator[
385
445
  tuple[
386
- Callable[[], Optional[Message]],
387
- Callable[[Message], None],
446
+ Callable[[], Optional[tuple[Message, ObjectTree]]],
447
+ Callable[[Message, ObjectTree], set[str]],
388
448
  Callable[[], Optional[int]],
389
449
  Callable[[], None],
390
450
  Callable[[int], Run],
391
451
  Callable[[str, int], Fab],
452
+ Callable[[int, str], bytes],
453
+ Callable[[int, str, bytes], None],
454
+ Callable[[int, str], None],
392
455
  ]
393
456
  ]:
394
457
  """Establish a connection to the Fleet API server at SuperLink."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: flwr-nightly
3
- Version: 1.20.0.dev20250718
3
+ Version: 1.20.0.dev20250721
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
@@ -6,7 +6,7 @@ flwr/cli/__init__.py,sha256=EfMGmHoobET6P2blBt_eOByXL8299MgFfB7XNdaPQ6I,720
6
6
  flwr/cli/app.py,sha256=AKCP45Dkbpvdil_4Ir9S93L3HP3iUOnHmcZjscoM8uU,1856
7
7
  flwr/cli/auth_plugin/__init__.py,sha256=FyaoqPzcxlBTFfJ2sBRC5USwQLmAhFr5KuBwfMO4bmo,1052
8
8
  flwr/cli/auth_plugin/oidc_cli_plugin.py,sha256=gIhW6Jg9QAo-jL43LYPpw_kn7pdUZZae0s0H8dEgjLM,5384
9
- flwr/cli/build.py,sha256=7OrcTqrjJd-iVq-MCtCIBCfvzi1JoqwUxeZDGKDKu6I,7265
9
+ flwr/cli/build.py,sha256=hE54Q_eMdWLpVKSVC2aQaUxVaiUlWnAosGNvIPSEg6Y,7284
10
10
  flwr/cli/cli_user_auth_interceptor.py,sha256=-JqDXpeZNQVwoSG7hMKsiS5qY5k5oklNSlQOVpM0-aY,3126
11
11
  flwr/cli/config_utils.py,sha256=IAVn2uWTXpN72YYt7raLtwp8ziwZugUKSURpc471VzU,9123
12
12
  flwr/cli/constant.py,sha256=g7Ad7o3DJDkJNrWS0T3SSJETWSTkkVJWGpLM8zlbpcY,1289
@@ -81,10 +81,10 @@ flwr/client/clientapp/__init__.py,sha256=Zw9qP5nHFnJ9K1dcR4cdY0fRqN-FaMYFSHJFXoF
81
81
  flwr/client/clientapp/utils.py,sha256=LsiW1OL2VPcjom3xN29pgBQC0UrttQ-xWL_GF1fkKDo,4344
82
82
  flwr/client/dpfedavg_numpy_client.py,sha256=3hul067cT2E9jBhzp7bFnFAZ_D2nWcIUEdHYE05FpzU,7404
83
83
  flwr/client/grpc_adapter_client/__init__.py,sha256=RQWP5mFPROLHKgombiRvPXVWSoVrQ81wvZm0-lOuuBA,742
84
- flwr/client/grpc_adapter_client/connection.py,sha256=aj5tTYyE8z2hQLXPPydsJiz8gBDIWLUhfWvqYkAL1L4,3966
84
+ flwr/client/grpc_adapter_client/connection.py,sha256=JGv02EjSOAG1E5BRUD4lwXc1LLiYJ0OhInvp31qx1cU,4404
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=jndP_Z8yNGHocJfucUspk2bcrjPeI3s-nOb1tEeDlT4,13732
87
+ flwr/client/grpc_rere_client/connection.py,sha256=__WQgS02WgOXopDnuighOzWLBVQY-0YCGtVjFhLaTZQ,13603
88
88
  flwr/client/grpc_rere_client/grpc_adapter.py,sha256=dLGB5GriszAmtgvuFGuz_F7rIwpzLfDxhJ7T3Un-Ce0,6694
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
@@ -98,7 +98,7 @@ flwr/client/mod/secure_aggregation/secaggplus_mod.py,sha256=aKqjZCrikF73y3E-7h40
98
98
  flwr/client/mod/utils.py,sha256=FUgD2TfcWqSeF6jUKZ4i6Ke56U4Nrv85AeVb93s6R9g,1201
99
99
  flwr/client/numpy_client.py,sha256=Qq6ghsIAop2slKqAfgiI5NiHJ4LIxGmrik3Ror4_XVc,9581
100
100
  flwr/client/rest_client/__init__.py,sha256=MBiuK62hj439m9rtwSwI184Hth6Tt5GbmpNMyl3zkZY,735
101
- flwr/client/rest_client/connection.py,sha256=zZ-HYLPCQxKtTo1Sc-n8jkl_GB4bpfViuif8h9HFSqI,16388
101
+ flwr/client/rest_client/connection.py,sha256=nGWgvOEvXpGcpu6G2Uxz34qOnB1NutIVi4tDMgvvJ1E,15738
102
102
  flwr/client/run_info_store.py,sha256=MaJ3UQ-07hWtK67wnWu0zR29jrk0fsfgJX506dvEOfE,4042
103
103
  flwr/client/typing.py,sha256=Jw3rawDzI_-ZDcRmEQcs5gZModY7oeQlEeltYsdOhlU,1048
104
104
  flwr/clientapp/__init__.py,sha256=zGW4z49Ojzoi1hDiRC7kyhLjijUilc6fqHhtM_ATRVA,719
@@ -124,7 +124,7 @@ flwr/common/grpc.py,sha256=y70hUFvXkIf3l03xOhlb7qhS6W1UJZRSZqCdB0ir0v8,10381
124
124
  flwr/common/heartbeat.py,sha256=SyEpNDnmJ0lni0cWO67rcoJVKasCLmkNHm3dKLeNrLU,5749
125
125
  flwr/common/inflatable.py,sha256=GDL9oBKs16_yyVdlH6kBf493O5xll_h9V7XB5Mpx1Hc,9524
126
126
  flwr/common/inflatable_protobuf_utils.py,sha256=JtRqp-fV47goDM2y8JRQ7AmwwjeGaWexwoMWLcxX5gE,5056
127
- flwr/common/inflatable_utils.py,sha256=kJnLBQEaByrJPGg0O2saExjLf_GPNqFlGOt0nOc3ZUw,15982
127
+ flwr/common/inflatable_utils.py,sha256=tYNrsdUWIw94p1oBPojg2wlD_-tnspWU2tbmIRafB6U,17401
128
128
  flwr/common/logger.py,sha256=JbRf6E2vQxXzpDBq1T8IDUJo_usu3gjWEBPQ6uKcmdg,13049
129
129
  flwr/common/message.py,sha256=xAL7iZN5-n-xPQpgoSFvxNrzs8fmiiPfoU0DjNQEhRw,19953
130
130
  flwr/common/object_ref.py,sha256=p3SfTeqo3Aj16SkB-vsnNn01zswOPdGNBitcbRnqmUk,9134
@@ -289,7 +289,7 @@ flwr/server/superlink/fleet/grpc_rere/__init__.py,sha256=ahDJJ1e-lDxBpeBMgPk7YZt
289
289
  flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py,sha256=X7-z4oReIH5ghMfmMXML3SSpa2bhRsuIvt2OZs82BUk,8675
290
290
  flwr/server/superlink/fleet/grpc_rere/server_interceptor.py,sha256=9_RaYWMqFdpFi8QcE7Nv8-pRjWJ2dLHxezrwhd1tAYk,6845
291
291
  flwr/server/superlink/fleet/message_handler/__init__.py,sha256=fHsRV0KvJ8HtgSA4_YBsEzuhJLjO8p6xx4aCY2oE1p4,731
292
- flwr/server/superlink/fleet/message_handler/message_handler.py,sha256=k_okVvuvbQgnBAWtXpOf0UrXcMQmRZtKKTS-as1xjig,8667
292
+ flwr/server/superlink/fleet/message_handler/message_handler.py,sha256=3Wg20bFo1tZfkzTQUerPVSHXyOuUqNuitEib3W_Dy-U,8691
293
293
  flwr/server/superlink/fleet/rest_rere/__init__.py,sha256=Lzc93nA7tDqoy-zRUaPG316oqFiZX1HUCL5ELaXY_xw,735
294
294
  flwr/server/superlink/fleet/rest_rere/rest_api.py,sha256=mxWKwGpgHPqd7cGFqd2ASnR-KZduIzLfT-d2yiNCqQ0,9257
295
295
  flwr/server/superlink/fleet/vce/__init__.py,sha256=XOKbAWOzlCqEOQ3M2cBYkH7HKA7PxlbCJMunt-ty-DY,784
@@ -305,7 +305,7 @@ flwr/server/superlink/linkstate/sqlite_linkstate.py,sha256=E699Ak0jMF3N7i1SIeFRu
305
305
  flwr/server/superlink/linkstate/utils.py,sha256=IeLh7iGRCHU5MEWOl7iriaSE4L__8GWOa2OleXadK5M,15444
306
306
  flwr/server/superlink/serverappio/__init__.py,sha256=Fy4zJuoccZe5mZSEIpOmQvU6YeXFBa1M4eZuXXmJcn8,717
307
307
  flwr/server/superlink/serverappio/serverappio_grpc.py,sha256=zcvzDhCAnlFxAwCiJUHNm6IE7-rk5jeZqSmPgjEY3AU,2307
308
- flwr/server/superlink/serverappio/serverappio_servicer.py,sha256=7oOFTjS5uSvdH0LSw-mDYulDMSItEM_QmXFQv_6ZjOU,18228
308
+ flwr/server/superlink/serverappio/serverappio_servicer.py,sha256=m0c05XQwRWwymOqbVYWDuDKRryIT3PxGhB8OM6YPyaM,18253
309
309
  flwr/server/superlink/simulation/__init__.py,sha256=Ry8DrNaZCMcQXvUc4FoCN2m3dvUQgWjasfp015o3Ec4,718
310
310
  flwr/server/superlink/simulation/simulationio_grpc.py,sha256=VqWKxjpd4bCgPFKsgtIZPk9YcG0kc1EEmr5k20EKty4,2205
311
311
  flwr/server/superlink/simulation/simulationio_servicer.py,sha256=m1T1zvEn81jlfx9hVTqmeWxAu6APCS2YW8l5O0OQvhU,7724
@@ -343,6 +343,7 @@ flwr/supercore/object_store/__init__.py,sha256=cdfPAmjINY6iOp8oI_LdcVh2simg469Mk
343
343
  flwr/supercore/object_store/in_memory_object_store.py,sha256=CGY43syxDGrUPcdOzRH3hNrfeqmoTOY_wjo3qaAHuNk,9612
344
344
  flwr/supercore/object_store/object_store.py,sha256=wC6Pxq89a7FwmIMJE3ZLPPy2i7Gdss7-8RUapECCAPY,5099
345
345
  flwr/supercore/object_store/object_store_factory.py,sha256=QVwE2ywi7vsj2iKfvWWnNw3N_I7Rz91NUt2RpcbJ7iM,1527
346
+ flwr/supercore/object_store/utils.py,sha256=-WwBa6ejMNm9ahmNZP39IHutS0cwingmeqCoxTmATQM,1845
346
347
  flwr/supercore/utils.py,sha256=ebuHMbeA8eXisX0oMPqBK3hk7uVnIE_yiqWVz8YbkpQ,1324
347
348
  flwr/superexec/__init__.py,sha256=YFqER0IJc1XEWfsX6AxZ9LSRq0sawPYrNYki-brvTIc,715
348
349
  flwr/superexec/app.py,sha256=U2jjOHb2LGWoU7vrl9_czTzre9O2mPxu3CPGUZ86sK4,1465
@@ -364,12 +365,12 @@ flwr/supernode/nodestate/in_memory_nodestate.py,sha256=LF3AbaW0bcZHY5yKWwUJSU2RZ
364
365
  flwr/supernode/nodestate/nodestate.py,sha256=kkGFxYnLIwT4-UmlPnf6HvAUpPey2urUNrweGybAIY4,6398
365
366
  flwr/supernode/nodestate/nodestate_factory.py,sha256=UYTDCcwK_baHUmkzkJDxL0UEqvtTfOMlQRrROMCd0Xo,1430
366
367
  flwr/supernode/runtime/__init__.py,sha256=JQdqd2EMTn-ORMeTvewYYh52ls0YKP68jrps1qioxu4,718
367
- flwr/supernode/runtime/run_clientapp.py,sha256=VcgRgPhrefAFpcAMfwCFSVa3HJsrlca8PNmeJD0on-k,8668
368
+ flwr/supernode/runtime/run_clientapp.py,sha256=woAO8rXclt5eZeNHokhBChgxMf-TAzqWnHCkoiSsLVs,10765
368
369
  flwr/supernode/servicer/__init__.py,sha256=lucTzre5WPK7G1YLCfaqg3rbFWdNSb7ZTt-ca8gxdEo,717
369
370
  flwr/supernode/servicer/clientappio/__init__.py,sha256=7Oy62Y_oijqF7Dxi6tpcUQyOpLc_QpIRZ83NvwmB0Yg,813
370
- flwr/supernode/servicer/clientappio/clientappio_servicer.py,sha256=OclO38I6NhKwM2ZnHz9NjrxW4RA4_v8M4GkLzsReG2c,9477
371
- flwr/supernode/start_client_internal.py,sha256=_ZqSfL_j4qn6Cg-P6sv3k_n1ZG62J_teokBxnWrXrPE,18772
372
- flwr_nightly-1.20.0.dev20250718.dist-info/METADATA,sha256=GVGToag6Bdk8OT-FHN7080UDdDwbCEnMA3tlR7L0ZwQ,15966
373
- flwr_nightly-1.20.0.dev20250718.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
374
- flwr_nightly-1.20.0.dev20250718.dist-info/entry_points.txt,sha256=jNpDXGBGgs21RqUxelF_jwGaxtqFwm-MQyfz-ZqSjrA,367
375
- flwr_nightly-1.20.0.dev20250718.dist-info/RECORD,,
371
+ flwr/supernode/servicer/clientappio/clientappio_servicer.py,sha256=SOx719uqdkIHryBri-XpuYpALQE__hxNtDjSAdeUtug,10059
372
+ flwr/supernode/start_client_internal.py,sha256=Sa9HT-YrS_RPkY44Dv6X-EZOX8HGupJyS4EY_e0x8u8,21367
373
+ flwr_nightly-1.20.0.dev20250721.dist-info/METADATA,sha256=leBKwaZsOax_yJrfB6silj5VVDX6i3Oe_GHSF2D3yV0,15966
374
+ flwr_nightly-1.20.0.dev20250721.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
375
+ flwr_nightly-1.20.0.dev20250721.dist-info/entry_points.txt,sha256=jNpDXGBGgs21RqUxelF_jwGaxtqFwm-MQyfz-ZqSjrA,367
376
+ flwr_nightly-1.20.0.dev20250721.dist-info/RECORD,,