flwr-nightly 1.19.0.dev20250527__py3-none-any.whl → 1.19.0.dev20250529__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.
Files changed (42) hide show
  1. flwr/cli/log.py +3 -3
  2. flwr/cli/login/login.py +3 -7
  3. flwr/cli/ls.py +3 -3
  4. flwr/cli/run/run.py +2 -6
  5. flwr/cli/stop.py +2 -2
  6. flwr/cli/utils.py +5 -4
  7. flwr/client/grpc_rere_client/connection.py +2 -0
  8. flwr/client/message_handler/message_handler.py +1 -1
  9. flwr/client/mod/comms_mods.py +36 -17
  10. flwr/common/auth_plugin/auth_plugin.py +8 -2
  11. flwr/common/inflatable.py +33 -2
  12. flwr/common/message.py +11 -0
  13. flwr/common/record/array.py +38 -1
  14. flwr/common/record/arrayrecord.py +34 -0
  15. flwr/common/serde.py +6 -1
  16. flwr/proto/fleet_pb2.py +16 -16
  17. flwr/proto/fleet_pb2.pyi +5 -5
  18. flwr/proto/message_pb2.py +10 -10
  19. flwr/proto/message_pb2.pyi +4 -4
  20. flwr/proto/serverappio_pb2.py +26 -26
  21. flwr/proto/serverappio_pb2.pyi +5 -5
  22. flwr/server/app.py +52 -56
  23. flwr/server/grid/grpc_grid.py +2 -1
  24. flwr/server/grid/inmemory_grid.py +5 -4
  25. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +1 -0
  26. flwr/server/superlink/fleet/message_handler/message_handler.py +11 -3
  27. flwr/server/superlink/fleet/rest_rere/rest_api.py +3 -1
  28. flwr/server/superlink/fleet/vce/vce_api.py +3 -0
  29. flwr/server/superlink/linkstate/in_memory_linkstate.py +14 -25
  30. flwr/server/superlink/linkstate/linkstate.py +9 -10
  31. flwr/server/superlink/linkstate/sqlite_linkstate.py +11 -21
  32. flwr/server/superlink/linkstate/utils.py +23 -23
  33. flwr/server/superlink/serverappio/serverappio_servicer.py +16 -11
  34. flwr/server/superlink/utils.py +29 -0
  35. flwr/server/utils/validator.py +2 -2
  36. flwr/supercore/object_store/in_memory_object_store.py +30 -4
  37. flwr/supercore/object_store/object_store.py +48 -1
  38. flwr/superexec/exec_servicer.py +1 -2
  39. {flwr_nightly-1.19.0.dev20250527.dist-info → flwr_nightly-1.19.0.dev20250529.dist-info}/METADATA +1 -1
  40. {flwr_nightly-1.19.0.dev20250527.dist-info → flwr_nightly-1.19.0.dev20250529.dist-info}/RECORD +42 -42
  41. {flwr_nightly-1.19.0.dev20250527.dist-info → flwr_nightly-1.19.0.dev20250529.dist-info}/WHEEL +0 -0
  42. {flwr_nightly-1.19.0.dev20250527.dist-info → flwr_nightly-1.19.0.dev20250529.dist-info}/entry_points.txt +0 -0
@@ -24,7 +24,6 @@ import time
24
24
  from collections.abc import Sequence
25
25
  from logging import DEBUG, ERROR, WARNING
26
26
  from typing import Any, Optional, Union, cast
27
- from uuid import UUID, uuid4
28
27
 
29
28
  from flwr.common import Context, Message, Metadata, log, now
30
29
  from flwr.common.constant import (
@@ -251,19 +250,15 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
251
250
 
252
251
  return result
253
252
 
254
- def store_message_ins(self, message: Message) -> Optional[UUID]:
253
+ def store_message_ins(self, message: Message) -> Optional[str]:
255
254
  """Store one Message."""
256
255
  # Validate message
257
256
  errors = validate_message(message=message, is_reply_message=False)
258
257
  if any(errors):
259
258
  log(ERROR, errors)
260
259
  return None
261
- # Create message_id
262
- message_id = uuid4()
263
260
 
264
261
  # Store Message
265
- # pylint: disable-next=W0212
266
- message.metadata._message_id = str(message_id) # type: ignore
267
262
  data = (message_to_dict(message),)
268
263
 
269
264
  # Convert values from uint64 to sint64 for SQLite
@@ -303,7 +298,7 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
303
298
  # This may need to be changed in the future version with more integrity checks.
304
299
  self.query(query, data)
305
300
 
306
- return message_id
301
+ return message.metadata.message_id
307
302
 
308
303
  def get_message_ins(self, node_id: int, limit: Optional[int]) -> list[Message]:
309
304
  """Get all Messages that have not been delivered yet."""
@@ -366,7 +361,7 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
366
361
 
367
362
  return result
368
363
 
369
- def store_message_res(self, message: Message) -> Optional[UUID]:
364
+ def store_message_res(self, message: Message) -> Optional[str]:
370
365
  """Store one Message."""
371
366
  # Validate message
372
367
  errors = validate_message(message=message, is_reply_message=True)
@@ -418,12 +413,7 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
418
413
  )
419
414
  return None
420
415
 
421
- # Create message_id
422
- message_id = uuid4()
423
-
424
416
  # Store Message
425
- # pylint: disable-next=W0212
426
- message.metadata._message_id = str(message_id) # type: ignore
427
417
  data = (message_to_dict(message),)
428
418
 
429
419
  # Convert values from uint64 to sint64 for SQLite
@@ -442,12 +432,12 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
442
432
  log(ERROR, "`run` is invalid")
443
433
  return None
444
434
 
445
- return message_id
435
+ return message.metadata.message_id
446
436
 
447
- def get_message_res(self, message_ids: set[UUID]) -> list[Message]:
437
+ def get_message_res(self, message_ids: set[str]) -> list[Message]:
448
438
  """Get reply Messages for the given Message IDs."""
449
439
  # pylint: disable-msg=too-many-locals
450
- ret: dict[UUID, Message] = {}
440
+ ret: dict[str, Message] = {}
451
441
 
452
442
  # Verify Message IDs
453
443
  current = time.time()
@@ -457,12 +447,12 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
457
447
  WHERE message_id IN ({",".join(["?"] * len(message_ids))});
458
448
  """
459
449
  rows = self.query(query, tuple(str(message_id) for message_id in message_ids))
460
- found_message_ins_dict: dict[UUID, Message] = {}
450
+ found_message_ins_dict: dict[str, Message] = {}
461
451
  for row in rows:
462
452
  convert_sint64_values_in_dict_to_uint64(
463
453
  row, ["run_id", "src_node_id", "dst_node_id"]
464
454
  )
465
- found_message_ins_dict[UUID(row["message_id"])] = dict_to_message(row)
455
+ found_message_ins_dict[row["message_id"]] = dict_to_message(row)
466
456
 
467
457
  ret = verify_message_ids(
468
458
  inquired_message_ids=message_ids,
@@ -551,7 +541,7 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
551
541
  result: dict[str, int] = rows[0]
552
542
  return result["num"]
553
543
 
554
- def delete_messages(self, message_ins_ids: set[UUID]) -> None:
544
+ def delete_messages(self, message_ins_ids: set[str]) -> None:
555
545
  """Delete a Message and its reply based on provided Message IDs."""
556
546
  if not message_ins_ids:
557
547
  return
@@ -577,7 +567,7 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
577
567
  self.conn.execute(query_1, data)
578
568
  self.conn.execute(query_2, data)
579
569
 
580
- def get_message_ids_from_run_id(self, run_id: int) -> set[UUID]:
570
+ def get_message_ids_from_run_id(self, run_id: int) -> set[str]:
581
571
  """Get all instruction Message IDs for the given run_id."""
582
572
  if self.conn is None:
583
573
  raise AttributeError("LinkState not initialized")
@@ -594,7 +584,7 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
594
584
  with self.conn:
595
585
  rows = self.conn.execute(query, data).fetchall()
596
586
 
597
- return {UUID(row["message_id"]) for row in rows}
587
+ return {row["message_id"] for row in rows}
598
588
 
599
589
  def create_node(self, heartbeat_interval: float) -> int:
600
590
  """Create, store in the link state, and return `node_id`."""
@@ -17,7 +17,7 @@
17
17
 
18
18
  from os import urandom
19
19
  from typing import Optional
20
- from uuid import UUID, uuid4
20
+ from uuid import uuid4
21
21
 
22
22
  from flwr.common import ConfigRecord, Context, Error, Message, Metadata, now, serde
23
23
  from flwr.common.constant import (
@@ -273,7 +273,7 @@ def create_message_error_unavailable_res_message(
273
273
  )
274
274
 
275
275
 
276
- def create_message_error_unavailable_ins_message(reply_to_message_id: UUID) -> Message:
276
+ def create_message_error_unavailable_ins_message(reply_to_message_id: str) -> Message:
277
277
  """Error to indicate that the enquired Message had expired before reply arrived or
278
278
  that it isn't found."""
279
279
  metadata = Metadata(
@@ -281,7 +281,7 @@ def create_message_error_unavailable_ins_message(reply_to_message_id: UUID) -> M
281
281
  message_id=str(uuid4()),
282
282
  src_node_id=SUPERLINK_NODE_ID,
283
283
  dst_node_id=SUPERLINK_NODE_ID,
284
- reply_to_message_id=str(reply_to_message_id),
284
+ reply_to_message_id=reply_to_message_id,
285
285
  group_id="", # Unknown
286
286
  message_type=MessageType.SYSTEM,
287
287
  created_at=now().timestamp(),
@@ -303,18 +303,18 @@ def message_ttl_has_expired(message_metadata: Metadata, current_time: float) ->
303
303
 
304
304
 
305
305
  def verify_message_ids(
306
- inquired_message_ids: set[UUID],
307
- found_message_ins_dict: dict[UUID, Message],
306
+ inquired_message_ids: set[str],
307
+ found_message_ins_dict: dict[str, Message],
308
308
  current_time: Optional[float] = None,
309
309
  update_set: bool = True,
310
- ) -> dict[UUID, Message]:
310
+ ) -> dict[str, Message]:
311
311
  """Verify found Messages and generate error Messages for invalid ones.
312
312
 
313
313
  Parameters
314
314
  ----------
315
- inquired_message_ids : set[UUID]
315
+ inquired_message_ids : set[str]
316
316
  Set of Message IDs for which to generate error Message if invalid.
317
- found_message_ins_dict : dict[UUID, Message]
317
+ found_message_ins_dict : dict[str, Message]
318
318
  Dictionary containing all found Message indexed by their IDs.
319
319
  current_time : Optional[float] (default: None)
320
320
  The current time to check for expiration. If set to `None`, the current time
@@ -325,7 +325,7 @@ def verify_message_ids(
325
325
 
326
326
  Returns
327
327
  -------
328
- dict[UUID, Message]
328
+ dict[str, Message]
329
329
  A dictionary of error Message indexed by the corresponding ID of the message
330
330
  they are a reply of.
331
331
  """
@@ -345,19 +345,19 @@ def verify_message_ids(
345
345
 
346
346
 
347
347
  def verify_found_message_replies(
348
- inquired_message_ids: set[UUID],
349
- found_message_ins_dict: dict[UUID, Message],
348
+ inquired_message_ids: set[str],
349
+ found_message_ins_dict: dict[str, Message],
350
350
  found_message_res_list: list[Message],
351
351
  current_time: Optional[float] = None,
352
352
  update_set: bool = True,
353
- ) -> dict[UUID, Message]:
353
+ ) -> dict[str, Message]:
354
354
  """Verify found Message replies and generate error Message for invalid ones.
355
355
 
356
356
  Parameters
357
357
  ----------
358
- inquired_message_ids : set[UUID]
358
+ inquired_message_ids : set[str]
359
359
  Set of Message IDs for which to generate error Message if invalid.
360
- found_message_ins_dict : dict[UUID, Message]
360
+ found_message_ins_dict : dict[str, Message]
361
361
  Dictionary containing all found instruction Messages indexed by their IDs.
362
362
  found_message_res_list : dict[Message, Message]
363
363
  List of found Message to be verified.
@@ -370,13 +370,13 @@ def verify_found_message_replies(
370
370
 
371
371
  Returns
372
372
  -------
373
- dict[UUID, Message]
373
+ dict[str, Message]
374
374
  A dictionary of Message indexed by the corresponding Message ID.
375
375
  """
376
- ret_dict: dict[UUID, Message] = {}
376
+ ret_dict: dict[str, Message] = {}
377
377
  current = current_time if current_time else now().timestamp()
378
378
  for message_res in found_message_res_list:
379
- message_ins_id = UUID(message_res.metadata.reply_to_message_id)
379
+ message_ins_id = message_res.metadata.reply_to_message_id
380
380
  if update_set:
381
381
  inquired_message_ids.remove(message_ins_id)
382
382
  # Check if the reply Message has expired
@@ -390,21 +390,21 @@ def verify_found_message_replies(
390
390
 
391
391
 
392
392
  def check_node_availability_for_in_message(
393
- inquired_in_message_ids: set[UUID],
394
- found_in_message_dict: dict[UUID, Message],
393
+ inquired_in_message_ids: set[str],
394
+ found_in_message_dict: dict[str, Message],
395
395
  node_id_to_online_until: dict[int, float],
396
396
  current_time: Optional[float] = None,
397
397
  update_set: bool = True,
398
- ) -> dict[UUID, Message]:
398
+ ) -> dict[str, Message]:
399
399
  """Check node availability for given Message and generate error reply Message if
400
400
  unavailable. A Message error indicating node unavailability will be generated for
401
401
  each given Message whose destination node is offline or non-existent.
402
402
 
403
403
  Parameters
404
404
  ----------
405
- inquired_in_message_ids : set[UUID]
405
+ inquired_in_message_ids : set[str]
406
406
  Set of Message IDs for which to check destination node availability.
407
- found_in_message_dict : dict[UUID, Message]
407
+ found_in_message_dict : dict[str, Message]
408
408
  Dictionary containing all found Message indexed by their IDs.
409
409
  node_id_to_online_until : dict[int, float]
410
410
  Dictionary mapping node IDs to their online-until timestamps.
@@ -417,7 +417,7 @@ def check_node_availability_for_in_message(
417
417
 
418
418
  Returns
419
419
  -------
420
- dict[UUID, Message]
420
+ dict[str, Message]
421
421
  A dictionary of error Message indexed by the corresponding Message ID.
422
422
  """
423
423
  ret_dict = {}
@@ -18,7 +18,6 @@
18
18
  import threading
19
19
  from logging import DEBUG, INFO
20
20
  from typing import Optional
21
- from uuid import UUID
22
21
 
23
22
  import grpc
24
23
 
@@ -81,6 +80,8 @@ from flwr.server.superlink.utils import abort_if
81
80
  from flwr.server.utils.validator import validate_message
82
81
  from flwr.supercore.object_store import ObjectStoreFactory
83
82
 
83
+ from ..utils import store_mapping_and_register_objects
84
+
84
85
 
85
86
  class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
86
87
  """ServerAppIo API servicer."""
@@ -140,7 +141,7 @@ class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
140
141
  request_name="PushMessages",
141
142
  detail="`messages_list` must not be empty",
142
143
  )
143
- message_ids: list[Optional[UUID]] = []
144
+ message_ids: list[Optional[str]] = []
144
145
  while request.messages_list:
145
146
  message_proto = request.messages_list.pop(0)
146
147
  message = message_from_proto(message_proto=message_proto)
@@ -156,13 +157,20 @@ class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
156
157
  detail="`Message.metadata` has mismatched `run_id`",
157
158
  )
158
159
  # Store
159
- message_id: Optional[UUID] = state.store_message_ins(message=message)
160
+ message_id: Optional[str] = state.store_message_ins(message=message)
160
161
  message_ids.append(message_id)
161
162
 
163
+ # Init store
164
+ store = self.objectstore_factory.store()
165
+
166
+ # Store Message object to descendants mapping and preregister objects
167
+ objects_to_push = store_mapping_and_register_objects(store, request=request)
168
+
162
169
  return PushInsMessagesResponse(
163
170
  message_ids=[
164
171
  str(message_id) if message_id else "" for message_id in message_ids
165
- ]
172
+ ],
173
+ objects_to_push=objects_to_push,
166
174
  )
167
175
 
168
176
  def PullMessages(
@@ -182,17 +190,14 @@ class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
182
190
  context,
183
191
  )
184
192
 
185
- # Convert each message_id str to UUID
186
- message_ids: set[UUID] = {
187
- UUID(message_id) for message_id in request.message_ids
188
- }
189
-
190
193
  # Read from state
191
- messages_res: list[Message] = state.get_message_res(message_ids=message_ids)
194
+ messages_res: list[Message] = state.get_message_res(
195
+ message_ids=set(request.message_ids)
196
+ )
192
197
 
193
198
  # Delete the instruction Messages and their replies if found
194
199
  message_ins_ids_to_delete = {
195
- UUID(msg_res.metadata.reply_to_message_id) for msg_res in messages_res
200
+ msg_res.metadata.reply_to_message_id for msg_res in messages_res
196
201
  }
197
202
 
198
203
  state.delete_messages(message_ins_ids=message_ins_ids_to_delete)
@@ -21,7 +21,11 @@ import grpc
21
21
 
22
22
  from flwr.common.constant import Status, SubStatus
23
23
  from flwr.common.typing import RunStatus
24
+ from flwr.proto.fleet_pb2 import PushMessagesRequest # pylint: disable=E0611
25
+ from flwr.proto.message_pb2 import ObjectIDs # pylint: disable=E0611
26
+ from flwr.proto.serverappio_pb2 import PushInsMessagesRequest # pylint: disable=E0611
24
27
  from flwr.server.superlink.linkstate import LinkState
28
+ from flwr.supercore.object_store import ObjectStore
25
29
 
26
30
  _STATUS_TO_MSG = {
27
31
  Status.PENDING: "Run is pending.",
@@ -63,3 +67,28 @@ def abort_if(
63
67
  """Abort context if status of the provided `run_id` is in `abort_status_list`."""
64
68
  msg = check_abort(run_id, abort_status_list, state)
65
69
  abort_grpc_context(msg, context)
70
+
71
+
72
+ def store_mapping_and_register_objects(
73
+ store: ObjectStore, request: Union[PushInsMessagesRequest, PushMessagesRequest]
74
+ ) -> dict[str, ObjectIDs]:
75
+ """Store Message object to descendants mapping and preregister objects."""
76
+ objects_to_push: dict[str, ObjectIDs] = {}
77
+ for (
78
+ message_obj_id,
79
+ descendant_obj_ids,
80
+ ) in request.msg_to_descendant_mapping.items():
81
+ descendants = list(descendant_obj_ids.object_ids)
82
+ # Store mapping
83
+ store.set_message_descendant_ids(
84
+ msg_object_id=message_obj_id, descendant_ids=descendants
85
+ )
86
+
87
+ # Preregister
88
+ object_ids_just_registered = store.preregister(descendants + [message_obj_id])
89
+ # Keep track of objects that need to be pushed
90
+ objects_to_push[message_obj_id] = ObjectIDs(
91
+ object_ids=object_ids_just_registered
92
+ )
93
+
94
+ return objects_to_push
@@ -27,8 +27,8 @@ def validate_message(message: Message, is_reply_message: bool) -> list[str]:
27
27
  validation_errors = []
28
28
  metadata = message.metadata
29
29
 
30
- if metadata.message_id != "":
31
- validation_errors.append("non-empty `metadata.message_id`")
30
+ if metadata.message_id == "":
31
+ validation_errors.append("empty `metadata.message_id`")
32
32
 
33
33
  # Created/delivered/TTL/Pushed
34
34
  if (
@@ -28,12 +28,27 @@ class InMemoryObjectStore(ObjectStore):
28
28
  def __init__(self, verify: bool = True) -> None:
29
29
  self.verify = verify
30
30
  self.store: dict[str, bytes] = {}
31
+ # Mapping the Object ID of a message to the list of children object IDs
32
+ self.msg_children_objects_mapping: dict[str, list[str]] = {}
33
+
34
+ def preregister(self, object_ids: list[str]) -> list[str]:
35
+ """Identify and preregister missing objects."""
36
+ new_objects = []
37
+ for obj_id in object_ids:
38
+ # Verify object ID format (must be a valid sha256 hash)
39
+ if not is_valid_sha256_hash(obj_id):
40
+ raise ValueError(f"Invalid object ID format: {obj_id}")
41
+ if obj_id not in self.store:
42
+ self.store[obj_id] = b""
43
+ new_objects.append(obj_id)
44
+
45
+ return new_objects
31
46
 
32
47
  def put(self, object_id: str, object_content: bytes) -> None:
33
48
  """Put an object into the store."""
34
- # Verify object ID format (must be a valid sha256 hash)
35
- if not is_valid_sha256_hash(object_id):
36
- raise ValueError(f"Invalid object ID format: {object_id}")
49
+ # Only allow adding the object if it has been preregistered
50
+ if object_id not in self.store:
51
+ raise KeyError(f"Object with id {object_id} was not preregistered.")
37
52
 
38
53
  # Verify object_id and object_content match
39
54
  if self.verify:
@@ -42,11 +57,22 @@ class InMemoryObjectStore(ObjectStore):
42
57
  raise ValueError(f"Object ID {object_id} does not match content hash")
43
58
 
44
59
  # Return if object is already present in the store
45
- if object_id in self.store:
60
+ if self.store[object_id] != b"":
46
61
  return
47
62
 
48
63
  self.store[object_id] = object_content
49
64
 
65
+ def set_message_descendant_ids(
66
+ self, msg_object_id: str, descendant_ids: list[str]
67
+ ) -> None:
68
+ """Store the mapping from a ``Message`` object ID to the object IDs of its
69
+ descendants."""
70
+ self.msg_children_objects_mapping[msg_object_id] = descendant_ids
71
+
72
+ def get_message_descendant_ids(self, msg_object_id: str) -> list[str]:
73
+ """Retrieve the object IDs of all descendants of a given Message."""
74
+ return self.msg_children_objects_mapping[msg_object_id]
75
+
50
76
  def get(self, object_id: str) -> Optional[bytes]:
51
77
  """Get an object from the store."""
52
78
  return self.store.get(object_id)
@@ -26,6 +26,23 @@ class ObjectStore(abc.ABC):
26
26
  delete objects identified by object IDs.
27
27
  """
28
28
 
29
+ @abc.abstractmethod
30
+ def preregister(self, object_ids: list[str]) -> list[str]:
31
+ """Identify and preregister missing objects in the `ObjectStore`.
32
+
33
+ Parameters
34
+ ----------
35
+ object_ids : list[str]
36
+ A list of object IDs to check against the store. Any object ID not already
37
+ present will be preregistered.
38
+
39
+ Returns
40
+ -------
41
+ list[str]
42
+ A list of object IDs that were not present in the `ObjectStore` and have now
43
+ been preregistered.
44
+ """
45
+
29
46
  @abc.abstractmethod
30
47
  def put(self, object_id: str, object_content: bytes) -> None:
31
48
  """Put an object into the store.
@@ -33,7 +50,7 @@ class ObjectStore(abc.ABC):
33
50
  Parameters
34
51
  ----------
35
52
  object_id : str
36
- The object_id under which to store the object.
53
+ The object_id under which to store the object. Must be preregistered.
37
54
  object_content : bytes
38
55
  The deflated object to store.
39
56
  """
@@ -70,6 +87,36 @@ class ObjectStore(abc.ABC):
70
87
  This method should remove all objects from the store.
71
88
  """
72
89
 
90
+ @abc.abstractmethod
91
+ def set_message_descendant_ids(
92
+ self, msg_object_id: str, descendant_ids: list[str]
93
+ ) -> None:
94
+ """Store the mapping from a ``Message`` object ID to the object IDs of its
95
+ descendants.
96
+
97
+ Parameters
98
+ ----------
99
+ msg_object_id : str
100
+ The object ID of the ``Message``.
101
+ descendant_ids : list[str]
102
+ A list of object IDs representing all descendant objects of the ``Message``.
103
+ """
104
+
105
+ @abc.abstractmethod
106
+ def get_message_descendant_ids(self, msg_object_id: str) -> list[str]:
107
+ """Retrieve the object IDs of all descendants of a given ``Message``.
108
+
109
+ Parameters
110
+ ----------
111
+ msg_object_id : str
112
+ The object ID of the ``Message``.
113
+
114
+ Returns
115
+ -------
116
+ list[str]
117
+ A list of object IDs of all descendant objects of the ``Message``.
118
+ """
119
+
73
120
  @abc.abstractmethod
74
121
  def __contains__(self, object_id: str) -> bool:
75
122
  """Check if an object_id is in the store.
@@ -19,7 +19,6 @@ import time
19
19
  from collections.abc import Generator
20
20
  from logging import ERROR, INFO
21
21
  from typing import Any, Optional
22
- from uuid import UUID
23
22
 
24
23
  import grpc
25
24
 
@@ -163,7 +162,7 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
163
162
  )
164
163
 
165
164
  if update_success:
166
- message_ids: set[UUID] = state.get_message_ids_from_run_id(request.run_id)
165
+ message_ids: set[str] = state.get_message_ids_from_run_id(request.run_id)
167
166
 
168
167
  # Delete Messages and their replies for the `run_id`
169
168
  state.delete_messages(message_ids)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: flwr-nightly
3
- Version: 1.19.0.dev20250527
3
+ Version: 1.19.0.dev20250529
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