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.
- flwr/client/__init__.py +2 -2
- flwr/client/grpc_adapter_client/connection.py +4 -4
- flwr/client/grpc_rere_client/connection.py +4 -4
- flwr/client/rest_client/connection.py +4 -4
- flwr/client/start_client_internal.py +495 -0
- flwr/client/supernode/app.py +1 -9
- flwr/common/inflatable.py +23 -0
- flwr/common/inflatable_grpc_utils.py +2 -0
- flwr/common/message.py +82 -1
- flwr/common/serde.py +8 -56
- flwr/common/serde_utils.py +50 -0
- flwr/{client → compat/client}/app.py +13 -11
- flwr/server/app.py +12 -1
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +6 -1
- flwr/server/superlink/linkstate/sqlite_linkstate.py +2 -6
- flwr/server/superlink/serverappio/serverappio_grpc.py +3 -0
- flwr/server/superlink/serverappio/serverappio_servicer.py +6 -1
- flwr/supercore/object_store/__init__.py +23 -0
- flwr/supercore/object_store/in_memory_object_store.py +65 -0
- flwr/supercore/object_store/object_store.py +86 -0
- flwr/supercore/object_store/object_store_factory.py +44 -0
- flwr/{client → supernode}/nodestate/in_memory_nodestate.py +1 -1
- {flwr_nightly-1.19.0.dev20250520.dist-info → flwr_nightly-1.19.0.dev20250522.dist-info}/METADATA +1 -1
- {flwr_nightly-1.19.0.dev20250520.dist-info → flwr_nightly-1.19.0.dev20250522.dist-info}/RECORD +31 -26
- /flwr/{client → compat/client}/grpc_client/__init__.py +0 -0
- /flwr/{client → compat/client}/grpc_client/connection.py +0 -0
- /flwr/{client → supernode}/nodestate/__init__.py +0 -0
- /flwr/{client → supernode}/nodestate/nodestate.py +0 -0
- /flwr/{client → supernode}/nodestate/nodestate_factory.py +0 -0
- {flwr_nightly-1.19.0.dev20250520.dist-info → flwr_nightly-1.19.0.dev20250522.dist-info}/WHEEL +0 -0
- {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
|
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
|
|
flwr/common/serde_utils.py
CHANGED
@@ -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.
|
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,
|
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
|
-
|
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,
|
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
|
+
"""
|