flwr-nightly 1.19.0.dev20250520__py3-none-any.whl → 1.19.0.dev20250522__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 (31) hide show
  1. flwr/client/__init__.py +2 -2
  2. flwr/client/grpc_adapter_client/connection.py +4 -4
  3. flwr/client/grpc_rere_client/connection.py +4 -4
  4. flwr/client/rest_client/connection.py +4 -4
  5. flwr/client/start_client_internal.py +495 -0
  6. flwr/client/supernode/app.py +1 -9
  7. flwr/common/inflatable.py +23 -0
  8. flwr/common/inflatable_grpc_utils.py +2 -0
  9. flwr/common/message.py +82 -1
  10. flwr/common/serde.py +8 -56
  11. flwr/common/serde_utils.py +50 -0
  12. flwr/{client → compat/client}/app.py +13 -11
  13. flwr/server/app.py +12 -1
  14. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +6 -1
  15. flwr/server/superlink/linkstate/sqlite_linkstate.py +2 -6
  16. flwr/server/superlink/serverappio/serverappio_grpc.py +3 -0
  17. flwr/server/superlink/serverappio/serverappio_servicer.py +6 -1
  18. flwr/supercore/object_store/__init__.py +23 -0
  19. flwr/supercore/object_store/in_memory_object_store.py +65 -0
  20. flwr/supercore/object_store/object_store.py +86 -0
  21. flwr/supercore/object_store/object_store_factory.py +44 -0
  22. flwr/{client → supernode}/nodestate/in_memory_nodestate.py +1 -1
  23. {flwr_nightly-1.19.0.dev20250520.dist-info → flwr_nightly-1.19.0.dev20250522.dist-info}/METADATA +1 -1
  24. {flwr_nightly-1.19.0.dev20250520.dist-info → flwr_nightly-1.19.0.dev20250522.dist-info}/RECORD +31 -26
  25. /flwr/{client → compat/client}/grpc_client/__init__.py +0 -0
  26. /flwr/{client → compat/client}/grpc_client/connection.py +0 -0
  27. /flwr/{client → supernode}/nodestate/__init__.py +0 -0
  28. /flwr/{client → supernode}/nodestate/nodestate.py +0 -0
  29. /flwr/{client → supernode}/nodestate/nodestate_factory.py +0 -0
  30. {flwr_nightly-1.19.0.dev20250520.dist-info → flwr_nightly-1.19.0.dev20250522.dist-info}/WHEEL +0 -0
  31. {flwr_nightly-1.19.0.dev20250520.dist-info → flwr_nightly-1.19.0.dev20250522.dist-info}/entry_points.txt +0 -0
flwr/common/message.py CHANGED
@@ -22,12 +22,25 @@ from typing import Any, cast, overload
22
22
 
23
23
  from flwr.common.date import now
24
24
  from flwr.common.logger import warn_deprecated_feature
25
+ from flwr.proto.message_pb2 import Message as ProtoMessage # pylint: disable=E0611
25
26
 
26
27
  from ..app.error import Error
27
28
  from ..app.metadata import Metadata
28
29
  from .constant import MESSAGE_TTL_TOLERANCE
30
+ from .inflatable import (
31
+ InflatableObject,
32
+ add_header_to_object_body,
33
+ get_object_body,
34
+ get_object_children_ids_from_object_content,
35
+ )
29
36
  from .logger import log
30
37
  from .record import RecordDict
38
+ from .serde_utils import (
39
+ error_from_proto,
40
+ error_to_proto,
41
+ metadata_from_proto,
42
+ metadata_to_proto,
43
+ )
31
44
 
32
45
  DEFAULT_TTL = 43200 # This is 12 hours
33
46
  MESSAGE_INIT_ERROR_MESSAGE = (
@@ -58,7 +71,7 @@ class MessageInitializationError(TypeError):
58
71
  super().__init__(message or MESSAGE_INIT_ERROR_MESSAGE)
59
72
 
60
73
 
61
- class Message:
74
+ class Message(InflatableObject):
62
75
  """Represents a message exchanged between ClientApp and ServerApp.
63
76
 
64
77
  This class encapsulates the payload and metadata necessary for communication
@@ -331,6 +344,74 @@ class Message:
331
344
  )
332
345
  return f"{self.__class__.__qualname__}({view})"
333
346
 
347
+ @property
348
+ def children(self) -> dict[str, InflatableObject] | None:
349
+ """Return a dictionary of a single RecordDict with its Object IDs as key."""
350
+ return {self.content.object_id: self.content} if self.has_content() else None
351
+
352
+ def deflate(self) -> bytes:
353
+ """Deflate message."""
354
+ # Store message metadata and error in object body
355
+ obj_body = ProtoMessage(
356
+ metadata=metadata_to_proto(self.metadata),
357
+ content=None,
358
+ error=error_to_proto(self.error) if self.has_error() else None,
359
+ ).SerializeToString(deterministic=True)
360
+
361
+ return add_header_to_object_body(object_body=obj_body, obj=self)
362
+
363
+ @classmethod
364
+ def inflate(
365
+ cls, object_content: bytes, children: dict[str, InflatableObject] | None = None
366
+ ) -> Message:
367
+ """Inflate an Message from bytes.
368
+
369
+ Parameters
370
+ ----------
371
+ object_content : bytes
372
+ The deflated object content of the Message.
373
+ children : Optional[dict[str, InflatableObject]] (default: None)
374
+ Dictionary of children InflatableObjects mapped to their Object IDs.
375
+ These children enable the full inflation of the Message.
376
+
377
+ Returns
378
+ -------
379
+ Message
380
+ The inflated Message.
381
+ """
382
+ if children is None:
383
+ children = {}
384
+
385
+ # Get the children id from the deflated message
386
+ children_ids = get_object_children_ids_from_object_content(object_content)
387
+
388
+ # If the message had content, only one children is possible
389
+ # If the message carried an error, the returned listed should be empty
390
+ if children_ids != list(children.keys()):
391
+ raise ValueError(
392
+ f"Mismatch in children object IDs: expected {children_ids}, but "
393
+ f"received {list(children.keys())}. The provided children must exactly "
394
+ "match the IDs specified in the object head."
395
+ )
396
+
397
+ # Inflate content
398
+ obj_body = get_object_body(object_content, cls)
399
+ proto_message = ProtoMessage.FromString(obj_body)
400
+
401
+ # Prepare content if error wasn't set in protobuf message
402
+ if proto_message.HasField("error"):
403
+ content = None
404
+ error = error_from_proto(proto_message.error)
405
+ else:
406
+ content = cast(RecordDict, children[children_ids[0]])
407
+ error = None
408
+ # Return message
409
+ return make_message(
410
+ metadata=metadata_from_proto(proto_message.metadata),
411
+ content=content,
412
+ error=error,
413
+ )
414
+
334
415
 
335
416
  def make_message(
336
417
  metadata: Metadata, content: RecordDict | None = None, error: Error | None = None
flwr/common/serde.py CHANGED
@@ -20,11 +20,9 @@ from typing import Any, cast
20
20
 
21
21
  # pylint: disable=E0611
22
22
  from flwr.proto.clientappio_pb2 import ClientAppOutputCode, ClientAppOutputStatus
23
- from flwr.proto.error_pb2 import Error as ProtoError
24
23
  from flwr.proto.fab_pb2 import Fab as ProtoFab
25
24
  from flwr.proto.message_pb2 import Context as ProtoContext
26
25
  from flwr.proto.message_pb2 import Message as ProtoMessage
27
- from flwr.proto.message_pb2 import Metadata as ProtoMetadata
28
26
  from flwr.proto.recorddict_pb2 import Array as ProtoArray
29
27
  from flwr.proto.recorddict_pb2 import ArrayRecord as ProtoArrayRecord
30
28
  from flwr.proto.recorddict_pb2 import ConfigRecord as ProtoConfigRecord
@@ -44,9 +42,6 @@ from flwr.proto.transport_pb2 import (
44
42
  Status,
45
43
  )
46
44
 
47
- from ..app.error import Error
48
- from ..app.metadata import Metadata
49
-
50
45
  # pylint: enable=E0611
51
46
  from . import (
52
47
  Array,
@@ -59,7 +54,14 @@ from . import (
59
54
  )
60
55
  from .constant import INT64_MAX_VALUE
61
56
  from .message import Message, make_message
62
- from .serde_utils import record_value_dict_from_proto, record_value_dict_to_proto
57
+ from .serde_utils import (
58
+ error_from_proto,
59
+ error_to_proto,
60
+ metadata_from_proto,
61
+ metadata_to_proto,
62
+ record_value_dict_from_proto,
63
+ record_value_dict_to_proto,
64
+ )
63
65
 
64
66
  # === Parameters message ===
65
67
 
@@ -449,21 +451,6 @@ def config_record_from_proto(record_proto: ProtoConfigRecord) -> ConfigRecord:
449
451
  )
450
452
 
451
453
 
452
- # === Error message ===
453
-
454
-
455
- def error_to_proto(error: Error) -> ProtoError:
456
- """Serialize Error to ProtoBuf."""
457
- reason = error.reason if error.reason else ""
458
- return ProtoError(code=error.code, reason=reason)
459
-
460
-
461
- def error_from_proto(error_proto: ProtoError) -> Error:
462
- """Deserialize Error from ProtoBuf."""
463
- reason = error_proto.reason if len(error_proto.reason) > 0 else None
464
- return Error(code=error_proto.code, reason=reason)
465
-
466
-
467
454
  # === RecordDict message ===
468
455
 
469
456
 
@@ -552,41 +539,6 @@ def user_config_value_from_proto(scalar_msg: Scalar) -> typing.UserConfigValue:
552
539
  return cast(typing.UserConfigValue, scalar)
553
540
 
554
541
 
555
- # === Metadata messages ===
556
-
557
-
558
- def metadata_to_proto(metadata: Metadata) -> ProtoMetadata:
559
- """Serialize `Metadata` to ProtoBuf."""
560
- proto = ProtoMetadata( # pylint: disable=E1101
561
- run_id=metadata.run_id,
562
- message_id=metadata.message_id,
563
- src_node_id=metadata.src_node_id,
564
- dst_node_id=metadata.dst_node_id,
565
- reply_to_message_id=metadata.reply_to_message_id,
566
- group_id=metadata.group_id,
567
- ttl=metadata.ttl,
568
- message_type=metadata.message_type,
569
- created_at=metadata.created_at,
570
- )
571
- return proto
572
-
573
-
574
- def metadata_from_proto(metadata_proto: ProtoMetadata) -> Metadata:
575
- """Deserialize `Metadata` from ProtoBuf."""
576
- metadata = Metadata(
577
- run_id=metadata_proto.run_id,
578
- message_id=metadata_proto.message_id,
579
- src_node_id=metadata_proto.src_node_id,
580
- dst_node_id=metadata_proto.dst_node_id,
581
- reply_to_message_id=metadata_proto.reply_to_message_id,
582
- group_id=metadata_proto.group_id,
583
- created_at=metadata_proto.created_at,
584
- ttl=metadata_proto.ttl,
585
- message_type=metadata_proto.message_type,
586
- )
587
- return metadata
588
-
589
-
590
542
  # === Message messages ===
591
543
 
592
544
 
@@ -20,6 +20,8 @@ from typing import Any, TypeVar, cast
20
20
  from google.protobuf.message import Message as GrpcMessage
21
21
 
22
22
  # pylint: disable=E0611
23
+ from flwr.proto.error_pb2 import Error as ProtoError
24
+ from flwr.proto.message_pb2 import Metadata as ProtoMetadata
23
25
  from flwr.proto.recorddict_pb2 import (
24
26
  BoolList,
25
27
  BytesList,
@@ -29,9 +31,13 @@ from flwr.proto.recorddict_pb2 import (
29
31
  UintList,
30
32
  )
31
33
 
34
+ from ..app.error import Error
35
+ from ..app.metadata import Metadata
32
36
  from .constant import INT64_MAX_VALUE
33
37
  from .record.typeddict import TypedDict
34
38
 
39
+ # pylint: enable=E0611
40
+
35
41
  _type_to_field: dict[type, str] = {
36
42
  float: "double",
37
43
  int: "sint64",
@@ -121,3 +127,47 @@ def record_value_dict_from_proto(
121
127
  ) -> dict[str, Any]:
122
128
  """Deserialize the record value dict from ProtoBuf."""
123
129
  return {k: _record_value_from_proto(v) for k, v in value_dict_proto.items()}
130
+
131
+
132
+ def error_to_proto(error: Error) -> ProtoError:
133
+ """Serialize Error to ProtoBuf."""
134
+ reason = error.reason if error.reason else ""
135
+ return ProtoError(code=error.code, reason=reason)
136
+
137
+
138
+ def error_from_proto(error_proto: ProtoError) -> Error:
139
+ """Deserialize Error from ProtoBuf."""
140
+ reason = error_proto.reason if len(error_proto.reason) > 0 else None
141
+ return Error(code=error_proto.code, reason=reason)
142
+
143
+
144
+ def metadata_to_proto(metadata: Metadata) -> ProtoMetadata:
145
+ """Serialize `Metadata` to ProtoBuf."""
146
+ proto = ProtoMetadata( # pylint: disable=E1101
147
+ run_id=metadata.run_id,
148
+ message_id=metadata.message_id,
149
+ src_node_id=metadata.src_node_id,
150
+ dst_node_id=metadata.dst_node_id,
151
+ reply_to_message_id=metadata.reply_to_message_id,
152
+ group_id=metadata.group_id,
153
+ ttl=metadata.ttl,
154
+ message_type=metadata.message_type,
155
+ created_at=metadata.created_at,
156
+ )
157
+ return proto
158
+
159
+
160
+ def metadata_from_proto(metadata_proto: ProtoMetadata) -> Metadata:
161
+ """Deserialize `Metadata` from ProtoBuf."""
162
+ metadata = Metadata(
163
+ run_id=metadata_proto.run_id,
164
+ message_id=metadata_proto.message_id,
165
+ src_node_id=metadata_proto.src_node_id,
166
+ dst_node_id=metadata_proto.dst_node_id,
167
+ reply_to_message_id=metadata_proto.reply_to_message_id,
168
+ group_id=metadata_proto.group_id,
169
+ created_at=metadata_proto.created_at,
170
+ ttl=metadata_proto.ttl,
171
+ message_type=metadata_proto.message_type,
172
+ )
173
+ return metadata
@@ -36,7 +36,15 @@ from flwr.cli.install import install_from_fab
36
36
  from flwr.client.client import Client
37
37
  from flwr.client.client_app import ClientApp, LoadClientAppError
38
38
  from flwr.client.clientapp.app import flwr_clientapp
39
- from flwr.client.nodestate.nodestate_factory import NodeStateFactory
39
+ from flwr.client.clientapp.clientappio_servicer import (
40
+ ClientAppInputs,
41
+ ClientAppIoServicer,
42
+ )
43
+ from flwr.client.grpc_adapter_client.connection import grpc_adapter
44
+ from flwr.client.grpc_rere_client.connection import grpc_request_response
45
+ from flwr.client.message_handler.message_handler import handle_control_message
46
+ from flwr.client.numpy_client import NumPyClient
47
+ from flwr.client.run_info_store import DeprecatedRunInfoStore
40
48
  from flwr.client.typing import ClientFnExt
41
49
  from flwr.common import GRPC_MAX_MESSAGE_LENGTH, Context, EventType, Message, event
42
50
  from flwr.common.address import parse_address
@@ -60,15 +68,9 @@ from flwr.common.grpc import generic_create_grpc_server
60
68
  from flwr.common.logger import log, warn_deprecated_feature
61
69
  from flwr.common.retry_invoker import RetryInvoker, RetryState, exponential
62
70
  from flwr.common.typing import Fab, Run, RunNotRunningException, UserConfig
71
+ from flwr.compat.client.grpc_client.connection import grpc_connection
63
72
  from flwr.proto.clientappio_pb2_grpc import add_ClientAppIoServicer_to_server
64
-
65
- from .clientapp.clientappio_servicer import ClientAppInputs, ClientAppIoServicer
66
- from .grpc_adapter_client.connection import grpc_adapter
67
- from .grpc_client.connection import grpc_connection
68
- from .grpc_rere_client.connection import grpc_request_response
69
- from .message_handler.message_handler import handle_control_message
70
- from .numpy_client import NumPyClient
71
- from .run_info_store import DeprecatedRunInfoStore
73
+ from flwr.supernode.nodestate import NodeStateFactory
72
74
 
73
75
 
74
76
  def _check_actionable_client(
@@ -781,7 +783,7 @@ def _init_connection(transport: Optional[str], server_address: str) -> tuple[
781
783
  try:
782
784
  from requests.exceptions import ConnectionError as RequestsConnectionError
783
785
 
784
- from .rest_client.connection import http_request_response
786
+ from flwr.client.rest_client.connection import http_request_response
785
787
  except ModuleNotFoundError:
786
788
  flwr_exit(ExitCode.COMMON_MISSING_EXTRA_REST)
787
789
  if server_address[:4] != "http":
@@ -792,7 +794,7 @@ def _init_connection(transport: Optional[str], server_address: str) -> tuple[
792
794
  elif transport == TRANSPORT_TYPE_GRPC_ADAPTER:
793
795
  connection, error_type = grpc_adapter, RpcError
794
796
  elif transport == TRANSPORT_TYPE_GRPC_BIDI:
795
- connection, error_type = grpc_connection, RpcError
797
+ connection, error_type = grpc_connection, RpcError # type: ignore[assignment]
796
798
  else:
797
799
  raise ValueError(
798
800
  f"Unknown transport type: {transport} (possible: {TRANSPORT_TYPES})"
flwr/server/app.py CHANGED
@@ -71,6 +71,7 @@ from flwr.proto.grpcadapter_pb2_grpc import add_GrpcAdapterServicer_to_server
71
71
  from flwr.server.fleet_event_log_interceptor import FleetEventLogInterceptor
72
72
  from flwr.server.serverapp.app import flwr_serverapp
73
73
  from flwr.simulation.app import flwr_simulation
74
+ from flwr.supercore.object_store import ObjectStoreFactory
74
75
  from flwr.superexec.app import load_executor
75
76
  from flwr.superexec.exec_grpc import run_exec_api_grpc
76
77
 
@@ -307,6 +308,9 @@ def run_superlink() -> None:
307
308
  # Initialize FfsFactory
308
309
  ffs_factory = FfsFactory(args.storage_dir)
309
310
 
311
+ # Initialize ObjectStoreFactory
312
+ objectstore_factory = ObjectStoreFactory()
313
+
310
314
  # Start Exec API
311
315
  executor = load_executor(args)
312
316
  exec_server: grpc.Server = run_exec_api_grpc(
@@ -343,6 +347,7 @@ def run_superlink() -> None:
343
347
  address=serverappio_address,
344
348
  state_factory=state_factory,
345
349
  ffs_factory=ffs_factory,
350
+ objectstore_factory=objectstore_factory,
346
351
  certificates=None, # ServerAppIo API doesn't support SSL yet
347
352
  )
348
353
  grpc_servers.append(serverappio_server)
@@ -421,6 +426,7 @@ def run_superlink() -> None:
421
426
  address=fleet_address,
422
427
  state_factory=state_factory,
423
428
  ffs_factory=ffs_factory,
429
+ objectstore_factory=objectstore_factory,
424
430
  certificates=certificates,
425
431
  interceptors=interceptors,
426
432
  )
@@ -430,6 +436,7 @@ def run_superlink() -> None:
430
436
  address=fleet_address,
431
437
  state_factory=state_factory,
432
438
  ffs_factory=ffs_factory,
439
+ objectstore_factory=objectstore_factory,
433
440
  certificates=certificates,
434
441
  )
435
442
  grpc_servers.append(fleet_server)
@@ -668,10 +675,11 @@ def _try_obtain_fleet_event_log_writer_plugin() -> Optional[EventLogWriterPlugin
668
675
  sys.exit("No Fleet API event log writer plugins are currently supported.")
669
676
 
670
677
 
671
- def _run_fleet_api_grpc_rere(
678
+ def _run_fleet_api_grpc_rere( # pylint: disable=R0913, R0917
672
679
  address: str,
673
680
  state_factory: LinkStateFactory,
674
681
  ffs_factory: FfsFactory,
682
+ objectstore_factory: ObjectStoreFactory,
675
683
  certificates: Optional[tuple[bytes, bytes, bytes]],
676
684
  interceptors: Optional[Sequence[grpc.ServerInterceptor]] = None,
677
685
  ) -> grpc.Server:
@@ -680,6 +688,7 @@ def _run_fleet_api_grpc_rere(
680
688
  fleet_servicer = FleetServicer(
681
689
  state_factory=state_factory,
682
690
  ffs_factory=ffs_factory,
691
+ objectstore_factory=objectstore_factory,
683
692
  )
684
693
  fleet_add_servicer_to_server_fn = add_FleetServicer_to_server
685
694
  fleet_grpc_server = generic_create_grpc_server(
@@ -700,6 +709,7 @@ def _run_fleet_api_grpc_adapter(
700
709
  address: str,
701
710
  state_factory: LinkStateFactory,
702
711
  ffs_factory: FfsFactory,
712
+ objectstore_factory: ObjectStoreFactory,
703
713
  certificates: Optional[tuple[bytes, bytes, bytes]],
704
714
  ) -> grpc.Server:
705
715
  """Run Fleet API (GrpcAdapter)."""
@@ -707,6 +717,7 @@ def _run_fleet_api_grpc_adapter(
707
717
  fleet_servicer = GrpcAdapterServicer(
708
718
  state_factory=state_factory,
709
719
  ffs_factory=ffs_factory,
720
+ objectstore_factory=objectstore_factory,
710
721
  )
711
722
  fleet_add_servicer_to_server_fn = add_GrpcAdapterServicer_to_server
712
723
  fleet_grpc_server = generic_create_grpc_server(
@@ -50,16 +50,21 @@ from flwr.server.superlink.ffs.ffs_factory import FfsFactory
50
50
  from flwr.server.superlink.fleet.message_handler import message_handler
51
51
  from flwr.server.superlink.linkstate import LinkStateFactory
52
52
  from flwr.server.superlink.utils import abort_grpc_context
53
+ from flwr.supercore.object_store import ObjectStoreFactory
53
54
 
54
55
 
55
56
  class FleetServicer(fleet_pb2_grpc.FleetServicer):
56
57
  """Fleet API servicer."""
57
58
 
58
59
  def __init__(
59
- self, state_factory: LinkStateFactory, ffs_factory: FfsFactory
60
+ self,
61
+ state_factory: LinkStateFactory,
62
+ ffs_factory: FfsFactory,
63
+ objectstore_factory: ObjectStoreFactory,
60
64
  ) -> None:
61
65
  self.state_factory = state_factory
62
66
  self.ffs_factory = ffs_factory
67
+ self.objectstore_factory = objectstore_factory
63
68
 
64
69
  def CreateNode(
65
70
  self, request: CreateNodeRequest, context: grpc.ServicerContext
@@ -40,12 +40,8 @@ from flwr.common.constant import (
40
40
  )
41
41
  from flwr.common.message import make_message
42
42
  from flwr.common.record import ConfigRecord
43
- from flwr.common.serde import (
44
- error_from_proto,
45
- error_to_proto,
46
- recorddict_from_proto,
47
- recorddict_to_proto,
48
- )
43
+ from flwr.common.serde import recorddict_from_proto, recorddict_to_proto
44
+ from flwr.common.serde_utils import error_from_proto, error_to_proto
49
45
  from flwr.common.typing import Run, RunStatus, UserConfig
50
46
 
51
47
  # pylint: disable=E0611
@@ -28,6 +28,7 @@ from flwr.proto.serverappio_pb2_grpc import ( # pylint: disable=E0611
28
28
  )
29
29
  from flwr.server.superlink.ffs.ffs_factory import FfsFactory
30
30
  from flwr.server.superlink.linkstate import LinkStateFactory
31
+ from flwr.supercore.object_store import ObjectStoreFactory
31
32
 
32
33
  from .serverappio_servicer import ServerAppIoServicer
33
34
 
@@ -36,6 +37,7 @@ def run_serverappio_api_grpc(
36
37
  address: str,
37
38
  state_factory: LinkStateFactory,
38
39
  ffs_factory: FfsFactory,
40
+ objectstore_factory: ObjectStoreFactory,
39
41
  certificates: Optional[tuple[bytes, bytes, bytes]],
40
42
  ) -> grpc.Server:
41
43
  """Run ServerAppIo API (gRPC, request-response)."""
@@ -43,6 +45,7 @@ def run_serverappio_api_grpc(
43
45
  serverappio_servicer: grpc.Server = ServerAppIoServicer(
44
46
  state_factory=state_factory,
45
47
  ffs_factory=ffs_factory,
48
+ objectstore_factory=objectstore_factory,
46
49
  )
47
50
  serverappio_add_servicer_to_server_fn = add_ServerAppIoServicer_to_server
48
51
  serverappio_grpc_server = generic_create_grpc_server(
@@ -83,16 +83,21 @@ from flwr.server.superlink.ffs.ffs_factory import FfsFactory
83
83
  from flwr.server.superlink.linkstate import LinkState, LinkStateFactory
84
84
  from flwr.server.superlink.utils import abort_if
85
85
  from flwr.server.utils.validator import validate_message
86
+ from flwr.supercore.object_store import ObjectStoreFactory
86
87
 
87
88
 
88
89
  class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
89
90
  """ServerAppIo API servicer."""
90
91
 
91
92
  def __init__(
92
- self, state_factory: LinkStateFactory, ffs_factory: FfsFactory
93
+ self,
94
+ state_factory: LinkStateFactory,
95
+ ffs_factory: FfsFactory,
96
+ objectstore_factory: ObjectStoreFactory,
93
97
  ) -> None:
94
98
  self.state_factory = state_factory
95
99
  self.ffs_factory = ffs_factory
100
+ self.objectstore_factory = objectstore_factory
96
101
  self.lock = threading.RLock()
97
102
 
98
103
  def GetNodes(
@@ -0,0 +1,23 @@
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
+ """Flower ObjectStore."""
16
+
17
+ from .object_store import ObjectStore
18
+ from .object_store_factory import ObjectStoreFactory
19
+
20
+ __all__ = [
21
+ "ObjectStore",
22
+ "ObjectStoreFactory",
23
+ ]
@@ -0,0 +1,65 @@
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
+ """Flower in-memory ObjectStore implementation."""
16
+
17
+
18
+ from typing import Optional
19
+
20
+ from flwr.common.inflatable import get_object_id, is_valid_sha256_hash
21
+
22
+ from .object_store import ObjectStore
23
+
24
+
25
+ class InMemoryObjectStore(ObjectStore):
26
+ """In-memory implementation of the ObjectStore interface."""
27
+
28
+ def __init__(self, verify: bool = True) -> None:
29
+ self.verify = verify
30
+ self.store: dict[str, bytes] = {}
31
+
32
+ def put(self, object_id: str, object_content: bytes) -> None:
33
+ """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}")
37
+
38
+ # Verify object_id and object_content match
39
+ if self.verify:
40
+ object_id_from_content = get_object_id(object_content)
41
+ if object_id != object_id_from_content:
42
+ raise ValueError(f"Object ID {object_id} does not match content hash")
43
+
44
+ # Return if object is already present in the store
45
+ if object_id in self.store:
46
+ return
47
+
48
+ self.store[object_id] = object_content
49
+
50
+ def get(self, object_id: str) -> Optional[bytes]:
51
+ """Get an object from the store."""
52
+ return self.store.get(object_id)
53
+
54
+ def delete(self, object_id: str) -> None:
55
+ """Delete an object from the store."""
56
+ if object_id in self.store:
57
+ del self.store[object_id]
58
+
59
+ def clear(self) -> None:
60
+ """Clear the store."""
61
+ self.store.clear()
62
+
63
+ def __contains__(self, object_id: str) -> bool:
64
+ """Check if an object_id is in the store."""
65
+ return object_id in self.store
@@ -0,0 +1,86 @@
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
+ """Flower abstract ObjectStore definition."""
16
+
17
+
18
+ import abc
19
+ from typing import Optional
20
+
21
+
22
+ class ObjectStore(abc.ABC):
23
+ """Abstract base class for `ObjectStore` implementations.
24
+
25
+ This class defines the interface for an object store that can store, retrieve, and
26
+ delete objects identified by object IDs.
27
+ """
28
+
29
+ @abc.abstractmethod
30
+ def put(self, object_id: str, object_content: bytes) -> None:
31
+ """Put an object into the store.
32
+
33
+ Parameters
34
+ ----------
35
+ object_id : str
36
+ The object_id under which to store the object.
37
+ object_content : bytes
38
+ The deflated object to store.
39
+ """
40
+
41
+ @abc.abstractmethod
42
+ def get(self, object_id: str) -> Optional[bytes]:
43
+ """Get an object from the store.
44
+
45
+ Parameters
46
+ ----------
47
+ object_id : str
48
+ The object_id under which the object is stored.
49
+
50
+ Returns
51
+ -------
52
+ bytes
53
+ The object stored under the given object_id.
54
+ """
55
+
56
+ @abc.abstractmethod
57
+ def delete(self, object_id: str) -> None:
58
+ """Delete an object from the store.
59
+
60
+ Parameters
61
+ ----------
62
+ object_id : str
63
+ The object_id under which the object is stored.
64
+ """
65
+
66
+ @abc.abstractmethod
67
+ def clear(self) -> None:
68
+ """Clear the store.
69
+
70
+ This method should remove all objects from the store.
71
+ """
72
+
73
+ @abc.abstractmethod
74
+ def __contains__(self, object_id: str) -> bool:
75
+ """Check if an object_id is in the store.
76
+
77
+ Parameters
78
+ ----------
79
+ object_id : str
80
+ The object_id to check.
81
+
82
+ Returns
83
+ -------
84
+ bool
85
+ True if the object_id is in the store, False otherwise.
86
+ """