flwr 1.18.0__py3-none-any.whl → 1.19.0__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/app/__init__.py +15 -0
- flwr/app/error.py +68 -0
- flwr/app/metadata.py +223 -0
- flwr/cli/build.py +82 -57
- flwr/cli/log.py +3 -3
- flwr/cli/login/login.py +3 -7
- flwr/cli/ls.py +15 -36
- flwr/cli/new/templates/app/code/client.baseline.py.tpl +1 -1
- flwr/cli/new/templates/app/code/model.baseline.py.tpl +1 -1
- flwr/cli/new/templates/app/code/server.baseline.py.tpl +2 -3
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +14 -17
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
- flwr/cli/run/run.py +10 -18
- flwr/cli/stop.py +2 -2
- flwr/cli/utils.py +31 -5
- flwr/client/__init__.py +2 -2
- flwr/client/client_app.py +1 -1
- flwr/client/clientapp/__init__.py +0 -7
- flwr/client/grpc_adapter_client/connection.py +4 -4
- flwr/client/grpc_rere_client/connection.py +130 -60
- flwr/client/grpc_rere_client/grpc_adapter.py +34 -6
- flwr/client/message_handler/message_handler.py +1 -1
- flwr/client/mod/comms_mods.py +36 -17
- flwr/client/rest_client/connection.py +173 -67
- flwr/clientapp/__init__.py +15 -0
- flwr/common/__init__.py +2 -2
- flwr/common/auth_plugin/__init__.py +2 -0
- flwr/common/auth_plugin/auth_plugin.py +29 -3
- flwr/common/constant.py +36 -7
- flwr/common/event_log_plugin/event_log_plugin.py +3 -3
- flwr/common/exit_handlers.py +30 -0
- flwr/common/heartbeat.py +165 -0
- flwr/common/inflatable.py +290 -0
- flwr/common/inflatable_grpc_utils.py +99 -0
- flwr/common/inflatable_rest_utils.py +99 -0
- flwr/common/inflatable_utils.py +341 -0
- flwr/common/message.py +110 -242
- flwr/common/record/__init__.py +2 -1
- flwr/common/record/array.py +323 -0
- flwr/common/record/arrayrecord.py +103 -225
- flwr/common/record/configrecord.py +59 -4
- flwr/common/record/conversion_utils.py +1 -1
- flwr/common/record/metricrecord.py +55 -4
- flwr/common/record/recorddict.py +69 -1
- flwr/common/recorddict_compat.py +2 -2
- flwr/common/retry_invoker.py +5 -1
- flwr/common/serde.py +59 -183
- flwr/common/serde_utils.py +175 -0
- flwr/common/typing.py +5 -3
- flwr/compat/__init__.py +15 -0
- flwr/compat/client/__init__.py +15 -0
- flwr/{client → compat/client}/app.py +19 -159
- flwr/compat/common/__init__.py +15 -0
- flwr/compat/server/__init__.py +15 -0
- flwr/compat/server/app.py +174 -0
- flwr/compat/simulation/__init__.py +15 -0
- flwr/proto/fleet_pb2.py +32 -27
- flwr/proto/fleet_pb2.pyi +49 -35
- flwr/proto/fleet_pb2_grpc.py +117 -13
- flwr/proto/fleet_pb2_grpc.pyi +47 -6
- flwr/proto/heartbeat_pb2.py +33 -0
- flwr/proto/heartbeat_pb2.pyi +66 -0
- flwr/proto/heartbeat_pb2_grpc.py +4 -0
- flwr/proto/heartbeat_pb2_grpc.pyi +4 -0
- flwr/proto/message_pb2.py +28 -11
- flwr/proto/message_pb2.pyi +125 -0
- flwr/proto/recorddict_pb2.py +16 -28
- flwr/proto/recorddict_pb2.pyi +46 -64
- flwr/proto/run_pb2.py +24 -32
- flwr/proto/run_pb2.pyi +4 -52
- flwr/proto/serverappio_pb2.py +32 -23
- flwr/proto/serverappio_pb2.pyi +45 -3
- flwr/proto/serverappio_pb2_grpc.py +138 -34
- flwr/proto/serverappio_pb2_grpc.pyi +54 -13
- flwr/proto/simulationio_pb2.py +12 -11
- flwr/proto/simulationio_pb2_grpc.py +35 -0
- flwr/proto/simulationio_pb2_grpc.pyi +14 -0
- flwr/server/__init__.py +1 -1
- flwr/server/app.py +68 -186
- flwr/server/compat/app_utils.py +50 -28
- flwr/server/fleet_event_log_interceptor.py +2 -2
- flwr/server/grid/grpc_grid.py +104 -34
- flwr/server/grid/inmemory_grid.py +5 -4
- flwr/server/serverapp/app.py +18 -0
- flwr/server/superlink/ffs/__init__.py +2 -0
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +13 -3
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +101 -7
- flwr/server/superlink/fleet/message_handler/message_handler.py +135 -18
- flwr/server/superlink/fleet/rest_rere/rest_api.py +72 -11
- flwr/server/superlink/fleet/vce/vce_api.py +6 -3
- flwr/server/superlink/linkstate/in_memory_linkstate.py +138 -43
- flwr/server/superlink/linkstate/linkstate.py +53 -20
- flwr/server/superlink/linkstate/sqlite_linkstate.py +149 -55
- flwr/server/superlink/linkstate/utils.py +33 -29
- flwr/server/superlink/serverappio/serverappio_grpc.py +3 -0
- flwr/server/superlink/serverappio/serverappio_servicer.py +211 -57
- flwr/server/superlink/simulation/simulationio_servicer.py +25 -1
- flwr/server/superlink/utils.py +44 -2
- flwr/server/utils/validator.py +2 -2
- flwr/serverapp/__init__.py +15 -0
- flwr/simulation/app.py +17 -0
- flwr/supercore/__init__.py +15 -0
- flwr/supercore/object_store/__init__.py +24 -0
- flwr/supercore/object_store/in_memory_object_store.py +229 -0
- flwr/supercore/object_store/object_store.py +192 -0
- flwr/supercore/object_store/object_store_factory.py +44 -0
- flwr/superexec/deployment.py +6 -2
- flwr/superexec/exec_event_log_interceptor.py +4 -4
- flwr/superexec/exec_grpc.py +7 -3
- flwr/superexec/exec_servicer.py +125 -23
- flwr/superexec/exec_user_auth_interceptor.py +37 -8
- flwr/superexec/executor.py +4 -0
- flwr/superexec/simulation.py +7 -1
- flwr/superlink/__init__.py +15 -0
- flwr/{client/supernode → supernode}/__init__.py +0 -7
- flwr/{client/nodestate/nodestate.py → supernode/cli/__init__.py} +7 -14
- flwr/{client/supernode/app.py → supernode/cli/flower_supernode.py} +3 -12
- flwr/supernode/cli/flwr_clientapp.py +81 -0
- flwr/supernode/nodestate/in_memory_nodestate.py +190 -0
- flwr/supernode/nodestate/nodestate.py +212 -0
- flwr/supernode/runtime/__init__.py +15 -0
- flwr/{client/clientapp/app.py → supernode/runtime/run_clientapp.py} +25 -56
- flwr/supernode/servicer/__init__.py +15 -0
- flwr/supernode/servicer/clientappio/__init__.py +24 -0
- flwr/supernode/start_client_internal.py +491 -0
- {flwr-1.18.0.dist-info → flwr-1.19.0.dist-info}/METADATA +5 -4
- {flwr-1.18.0.dist-info → flwr-1.19.0.dist-info}/RECORD +141 -108
- {flwr-1.18.0.dist-info → flwr-1.19.0.dist-info}/WHEEL +1 -1
- {flwr-1.18.0.dist-info → flwr-1.19.0.dist-info}/entry_points.txt +2 -2
- flwr/client/heartbeat.py +0 -74
- flwr/client/nodestate/in_memory_nodestate.py +0 -38
- /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_factory.py +0 -0
- /flwr/{client/clientapp → supernode/servicer/clientappio}/clientappio_servicer.py +0 -0
|
@@ -0,0 +1,229 @@
|
|
|
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
|
+
import threading
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
from flwr.common.inflatable import (
|
|
23
|
+
get_object_children_ids_from_object_content,
|
|
24
|
+
get_object_id,
|
|
25
|
+
is_valid_sha256_hash,
|
|
26
|
+
iterate_object_tree,
|
|
27
|
+
)
|
|
28
|
+
from flwr.common.inflatable_utils import validate_object_content
|
|
29
|
+
from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
|
|
30
|
+
|
|
31
|
+
from .object_store import NoObjectInStoreError, ObjectStore
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ObjectEntry:
|
|
36
|
+
"""Data class representing an object entry in the store."""
|
|
37
|
+
|
|
38
|
+
content: bytes
|
|
39
|
+
is_available: bool
|
|
40
|
+
ref_count: int # Number of references (direct parents) to this object
|
|
41
|
+
runs: set[int] # Set of run IDs that used this object
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class InMemoryObjectStore(ObjectStore):
|
|
45
|
+
"""In-memory implementation of the ObjectStore interface."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, verify: bool = True) -> None:
|
|
48
|
+
self.verify = verify
|
|
49
|
+
self.store: dict[str, ObjectEntry] = {}
|
|
50
|
+
self.lock_store = threading.RLock()
|
|
51
|
+
# Mapping the Object ID of a message to the list of descendant object IDs
|
|
52
|
+
self.msg_descendant_objects_mapping: dict[str, list[str]] = {}
|
|
53
|
+
self.lock_msg_mapping = threading.RLock()
|
|
54
|
+
# Mapping each run ID to a set of object IDs that are used in that run
|
|
55
|
+
self.run_objects_mapping: dict[int, set[str]] = {}
|
|
56
|
+
|
|
57
|
+
def preregister(self, run_id: int, object_tree: ObjectTree) -> list[str]:
|
|
58
|
+
"""Identify and preregister missing objects."""
|
|
59
|
+
new_objects = []
|
|
60
|
+
if run_id not in self.run_objects_mapping:
|
|
61
|
+
self.run_objects_mapping[run_id] = set()
|
|
62
|
+
|
|
63
|
+
for tree_node in iterate_object_tree(object_tree):
|
|
64
|
+
obj_id = tree_node.object_id
|
|
65
|
+
# Verify object ID format (must be a valid sha256 hash)
|
|
66
|
+
if not is_valid_sha256_hash(obj_id):
|
|
67
|
+
raise ValueError(f"Invalid object ID format: {obj_id}")
|
|
68
|
+
with self.lock_store:
|
|
69
|
+
if obj_id not in self.store:
|
|
70
|
+
self.store[obj_id] = ObjectEntry(
|
|
71
|
+
content=b"", # Initially empty content
|
|
72
|
+
is_available=False, # Initially not available
|
|
73
|
+
ref_count=0, # Reference count starts at 0
|
|
74
|
+
runs={run_id}, # Start with the current run ID
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Increment the reference count for all its children
|
|
78
|
+
# Post-order traversal ensures that children are registered
|
|
79
|
+
# before parents
|
|
80
|
+
for child_node in tree_node.children:
|
|
81
|
+
child_id = child_node.object_id
|
|
82
|
+
self.store[child_id].ref_count += 1
|
|
83
|
+
|
|
84
|
+
# Add the object ID to the run's mapping
|
|
85
|
+
self.run_objects_mapping[run_id].add(obj_id)
|
|
86
|
+
|
|
87
|
+
# Add to the list of new objects
|
|
88
|
+
new_objects.append(obj_id)
|
|
89
|
+
else:
|
|
90
|
+
# Object is in store, retrieve it
|
|
91
|
+
obj_entry = self.store[obj_id]
|
|
92
|
+
|
|
93
|
+
# Add to the list of new objects if not available
|
|
94
|
+
if not obj_entry.is_available:
|
|
95
|
+
new_objects.append(obj_id)
|
|
96
|
+
|
|
97
|
+
# If the object is already registered but not in this run,
|
|
98
|
+
# add the run ID to its runs
|
|
99
|
+
if obj_id not in self.run_objects_mapping[run_id]:
|
|
100
|
+
obj_entry.runs.add(run_id)
|
|
101
|
+
self.run_objects_mapping[run_id].add(obj_id)
|
|
102
|
+
|
|
103
|
+
return new_objects
|
|
104
|
+
|
|
105
|
+
def put(self, object_id: str, object_content: bytes) -> None:
|
|
106
|
+
"""Put an object into the store."""
|
|
107
|
+
if self.verify:
|
|
108
|
+
# Verify object_id and object_content match
|
|
109
|
+
object_id_from_content = get_object_id(object_content)
|
|
110
|
+
if object_id != object_id_from_content:
|
|
111
|
+
raise ValueError(f"Object ID {object_id} does not match content hash")
|
|
112
|
+
|
|
113
|
+
# Validate object content
|
|
114
|
+
validate_object_content(content=object_content)
|
|
115
|
+
|
|
116
|
+
with self.lock_store:
|
|
117
|
+
# Only allow adding the object if it has been preregistered
|
|
118
|
+
if object_id not in self.store:
|
|
119
|
+
raise NoObjectInStoreError(
|
|
120
|
+
f"Object with ID '{object_id}' was not pre-registered."
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Return if object is already present in the store
|
|
124
|
+
if self.store[object_id].is_available:
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
# Update the object entry in the store
|
|
128
|
+
self.store[object_id].content = object_content
|
|
129
|
+
self.store[object_id].is_available = True
|
|
130
|
+
|
|
131
|
+
def set_message_descendant_ids(
|
|
132
|
+
self, msg_object_id: str, descendant_ids: list[str]
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Store the mapping from a ``Message`` object ID to the object IDs of its
|
|
135
|
+
descendants."""
|
|
136
|
+
with self.lock_msg_mapping:
|
|
137
|
+
self.msg_descendant_objects_mapping[msg_object_id] = descendant_ids
|
|
138
|
+
|
|
139
|
+
def get_message_descendant_ids(self, msg_object_id: str) -> list[str]:
|
|
140
|
+
"""Retrieve the object IDs of all descendants of a given Message."""
|
|
141
|
+
with self.lock_msg_mapping:
|
|
142
|
+
if msg_object_id not in self.msg_descendant_objects_mapping:
|
|
143
|
+
raise NoObjectInStoreError(
|
|
144
|
+
f"No message registered in Object Store with ID '{msg_object_id}'. "
|
|
145
|
+
"Mapping to descendants could not be found."
|
|
146
|
+
)
|
|
147
|
+
return self.msg_descendant_objects_mapping[msg_object_id]
|
|
148
|
+
|
|
149
|
+
def delete_message_descendant_ids(self, msg_object_id: str) -> None:
|
|
150
|
+
"""Delete the mapping from a ``Message`` object ID to its descendants."""
|
|
151
|
+
with self.lock_msg_mapping:
|
|
152
|
+
self.msg_descendant_objects_mapping.pop(msg_object_id, None)
|
|
153
|
+
|
|
154
|
+
def get(self, object_id: str) -> Optional[bytes]:
|
|
155
|
+
"""Get an object from the store."""
|
|
156
|
+
with self.lock_store:
|
|
157
|
+
# Check if the object ID is pre-registered
|
|
158
|
+
if object_id not in self.store:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
# Return content (if not yet available, it will b"")
|
|
162
|
+
return self.store[object_id].content
|
|
163
|
+
|
|
164
|
+
def delete(self, object_id: str) -> None:
|
|
165
|
+
"""Delete an object and its unreferenced descendants from the store."""
|
|
166
|
+
with self.lock_store:
|
|
167
|
+
# If the object is not in the store, nothing to delete
|
|
168
|
+
if (object_entry := self.store.get(object_id)) is None:
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
# Delete the object if it has no references left
|
|
172
|
+
if object_entry.ref_count == 0:
|
|
173
|
+
del self.store[object_id]
|
|
174
|
+
|
|
175
|
+
# Remove the object from the run's mapping
|
|
176
|
+
for run_id in object_entry.runs:
|
|
177
|
+
self.run_objects_mapping[run_id].discard(object_id)
|
|
178
|
+
|
|
179
|
+
# Decrease the reference count of its children
|
|
180
|
+
children_ids = get_object_children_ids_from_object_content(
|
|
181
|
+
object_entry.content
|
|
182
|
+
)
|
|
183
|
+
for child_id in children_ids:
|
|
184
|
+
self.store[child_id].ref_count -= 1
|
|
185
|
+
|
|
186
|
+
# Recursively try to delete the child object
|
|
187
|
+
self.delete(child_id)
|
|
188
|
+
|
|
189
|
+
def delete_objects_in_run(self, run_id: int) -> None:
|
|
190
|
+
"""Delete all objects that were registered in a specific run."""
|
|
191
|
+
with self.lock_store:
|
|
192
|
+
if run_id not in self.run_objects_mapping:
|
|
193
|
+
return
|
|
194
|
+
for object_id in list(self.run_objects_mapping[run_id]):
|
|
195
|
+
# Check if the object is still in the store
|
|
196
|
+
if (object_entry := self.store.get(object_id)) is None:
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
# Remove the run ID from the object's runs
|
|
200
|
+
object_entry.runs.discard(run_id)
|
|
201
|
+
|
|
202
|
+
# Only message objects are allowed to have a `ref_count` of 0,
|
|
203
|
+
# and every message object must have a `ref_count` of 0
|
|
204
|
+
if object_entry.ref_count == 0:
|
|
205
|
+
# Delete the message object and its unreferenced descendants
|
|
206
|
+
self.delete(object_id)
|
|
207
|
+
|
|
208
|
+
# Delete the message's descendants mapping
|
|
209
|
+
self.delete_message_descendant_ids(object_id)
|
|
210
|
+
|
|
211
|
+
# Remove the run from the mapping
|
|
212
|
+
del self.run_objects_mapping[run_id]
|
|
213
|
+
|
|
214
|
+
def clear(self) -> None:
|
|
215
|
+
"""Clear the store."""
|
|
216
|
+
with self.lock_store:
|
|
217
|
+
self.store.clear()
|
|
218
|
+
self.msg_descendant_objects_mapping.clear()
|
|
219
|
+
self.run_objects_mapping.clear()
|
|
220
|
+
|
|
221
|
+
def __contains__(self, object_id: str) -> bool:
|
|
222
|
+
"""Check if an object_id is in the store."""
|
|
223
|
+
with self.lock_store:
|
|
224
|
+
return object_id in self.store
|
|
225
|
+
|
|
226
|
+
def __len__(self) -> int:
|
|
227
|
+
"""Get the number of objects in the store."""
|
|
228
|
+
with self.lock_store:
|
|
229
|
+
return len(self.store)
|
|
@@ -0,0 +1,192 @@
|
|
|
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
|
+
from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NoObjectInStoreError(Exception):
|
|
25
|
+
"""Error when trying to access an element in the ObjectStore that does not exist."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, message: str):
|
|
28
|
+
super().__init__(message)
|
|
29
|
+
self.message = message
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
"""Return formatted exception message string."""
|
|
33
|
+
return f"NoObjectInStoreError: {self.message}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ObjectStore(abc.ABC):
|
|
37
|
+
"""Abstract base class for `ObjectStore` implementations.
|
|
38
|
+
|
|
39
|
+
This class defines the interface for an object store that can store, retrieve, and
|
|
40
|
+
delete objects identified by object IDs.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
@abc.abstractmethod
|
|
44
|
+
def preregister(self, run_id: int, object_tree: ObjectTree) -> list[str]:
|
|
45
|
+
"""Identify and preregister missing objects in the `ObjectStore`.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
run_id : int
|
|
50
|
+
The ID of the run for which to preregister objects.
|
|
51
|
+
object_tree : ObjectTree
|
|
52
|
+
The object tree containing the IDs of objects to preregister.
|
|
53
|
+
This tree should contain all objects that are expected to be
|
|
54
|
+
stored in the `ObjectStore`.
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
list[str]
|
|
59
|
+
A list of object IDs that were either not previously preregistered
|
|
60
|
+
in the `ObjectStore`, or were preregistered but are not yet available.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
@abc.abstractmethod
|
|
64
|
+
def put(self, object_id: str, object_content: bytes) -> None:
|
|
65
|
+
"""Put an object into the store.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
object_id : str
|
|
70
|
+
The object_id under which to store the object. Must be preregistered.
|
|
71
|
+
object_content : bytes
|
|
72
|
+
The deflated object to store.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
@abc.abstractmethod
|
|
76
|
+
def get(self, object_id: str) -> Optional[bytes]:
|
|
77
|
+
"""Get an object from the store.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
object_id : str
|
|
82
|
+
The object_id under which the object is stored.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
bytes
|
|
87
|
+
The object stored under the given object_id.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
@abc.abstractmethod
|
|
91
|
+
def delete(self, object_id: str) -> None:
|
|
92
|
+
"""Delete an object and its unreferenced descendants from the store.
|
|
93
|
+
|
|
94
|
+
This method attempts to recursively delete the specified object and its
|
|
95
|
+
descendants, if they are not referenced by any other object.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
object_id : str
|
|
100
|
+
The object_id under which the object is stored.
|
|
101
|
+
|
|
102
|
+
Notes
|
|
103
|
+
-----
|
|
104
|
+
The object of the given object_id will NOT be deleted if it is still referenced
|
|
105
|
+
by any other object in the store.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
@abc.abstractmethod
|
|
109
|
+
def delete_objects_in_run(self, run_id: int) -> None:
|
|
110
|
+
"""Delete all objects that were registered in a specific run.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
run_id : int
|
|
115
|
+
The ID of the run for which to delete objects.
|
|
116
|
+
|
|
117
|
+
Notes
|
|
118
|
+
-----
|
|
119
|
+
Objects that are still registered in other runs will NOT be deleted.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
@abc.abstractmethod
|
|
123
|
+
def clear(self) -> None:
|
|
124
|
+
"""Clear the store.
|
|
125
|
+
|
|
126
|
+
This method should remove all objects from the store.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
@abc.abstractmethod
|
|
130
|
+
def set_message_descendant_ids(
|
|
131
|
+
self, msg_object_id: str, descendant_ids: list[str]
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Store the mapping from a ``Message`` object ID to the object IDs of its
|
|
134
|
+
descendants.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
msg_object_id : str
|
|
139
|
+
The object ID of the ``Message``.
|
|
140
|
+
descendant_ids : list[str]
|
|
141
|
+
A list of object IDs representing all descendant objects of the ``Message``.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
@abc.abstractmethod
|
|
145
|
+
def get_message_descendant_ids(self, msg_object_id: str) -> list[str]:
|
|
146
|
+
"""Retrieve the object IDs of all descendants of a given ``Message``.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
msg_object_id : str
|
|
151
|
+
The object ID of the ``Message``.
|
|
152
|
+
|
|
153
|
+
Returns
|
|
154
|
+
-------
|
|
155
|
+
list[str]
|
|
156
|
+
A list of object IDs of all descendant objects of the ``Message``.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
@abc.abstractmethod
|
|
160
|
+
def delete_message_descendant_ids(self, msg_object_id: str) -> None:
|
|
161
|
+
"""Delete the mapping from a ``Message`` object ID to its descendants.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
msg_object_id : str
|
|
166
|
+
The object ID of the ``Message``.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
@abc.abstractmethod
|
|
170
|
+
def __contains__(self, object_id: str) -> bool:
|
|
171
|
+
"""Check if an object_id is in the store.
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
object_id : str
|
|
176
|
+
The object_id to check.
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
bool
|
|
181
|
+
True if the object_id is in the store, False otherwise.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
@abc.abstractmethod
|
|
185
|
+
def __len__(self) -> int:
|
|
186
|
+
"""Return the number of objects in the store.
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
int
|
|
191
|
+
The number of objects currently stored.
|
|
192
|
+
"""
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
"""Factory class that creates ObjectStore instances."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from logging import DEBUG
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from flwr.common.logger import log
|
|
22
|
+
|
|
23
|
+
from .in_memory_object_store import InMemoryObjectStore
|
|
24
|
+
from .object_store import ObjectStore
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ObjectStoreFactory:
|
|
28
|
+
"""Factory class that creates ObjectStore instances."""
|
|
29
|
+
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
self.store_instance: Optional[ObjectStore] = None
|
|
32
|
+
|
|
33
|
+
def store(self) -> ObjectStore:
|
|
34
|
+
"""Return an ObjectStore instance and create it, if necessary.
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
ObjectStore
|
|
39
|
+
An ObjectStore instance for storing objects by object_id.
|
|
40
|
+
"""
|
|
41
|
+
if self.store_instance is None:
|
|
42
|
+
self.store_instance = InMemoryObjectStore()
|
|
43
|
+
log(DEBUG, "Using InMemoryObjectStore")
|
|
44
|
+
return self.store_instance
|
flwr/superexec/deployment.py
CHANGED
|
@@ -132,6 +132,7 @@ class DeploymentEngine(Executor):
|
|
|
132
132
|
self,
|
|
133
133
|
fab: Fab,
|
|
134
134
|
override_config: UserConfig,
|
|
135
|
+
flwr_aid: Optional[str],
|
|
135
136
|
) -> int:
|
|
136
137
|
fab_hash = self.ffs.put(fab.content, {})
|
|
137
138
|
if fab_hash != fab.hash_str:
|
|
@@ -141,7 +142,7 @@ class DeploymentEngine(Executor):
|
|
|
141
142
|
fab_id, fab_version = get_fab_metadata(fab.content)
|
|
142
143
|
|
|
143
144
|
run_id = self.linkstate.create_run(
|
|
144
|
-
fab_id, fab_version, fab_hash, override_config, ConfigRecord()
|
|
145
|
+
fab_id, fab_version, fab_hash, override_config, ConfigRecord(), flwr_aid
|
|
145
146
|
)
|
|
146
147
|
return run_id
|
|
147
148
|
|
|
@@ -161,6 +162,7 @@ class DeploymentEngine(Executor):
|
|
|
161
162
|
fab_file: bytes,
|
|
162
163
|
override_config: UserConfig,
|
|
163
164
|
federation_options: ConfigRecord,
|
|
165
|
+
flwr_aid: Optional[str],
|
|
164
166
|
) -> Optional[int]:
|
|
165
167
|
"""Start run using the Flower Deployment Engine."""
|
|
166
168
|
run_id = None
|
|
@@ -168,7 +170,9 @@ class DeploymentEngine(Executor):
|
|
|
168
170
|
|
|
169
171
|
# Call SuperLink to create run
|
|
170
172
|
run_id = self._create_run(
|
|
171
|
-
Fab(hashlib.sha256(fab_file).hexdigest(), fab_file),
|
|
173
|
+
Fab(hashlib.sha256(fab_file).hexdigest(), fab_file),
|
|
174
|
+
override_config,
|
|
175
|
+
flwr_aid,
|
|
172
176
|
)
|
|
173
177
|
|
|
174
178
|
# Register context for the Run
|
|
@@ -24,7 +24,7 @@ from google.protobuf.message import Message as GrpcMessage
|
|
|
24
24
|
from flwr.common.event_log_plugin.event_log_plugin import EventLogWriterPlugin
|
|
25
25
|
from flwr.common.typing import LogEntry
|
|
26
26
|
|
|
27
|
-
from .exec_user_auth_interceptor import
|
|
27
|
+
from .exec_user_auth_interceptor import shared_account_info
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class ExecEventLogInterceptor(grpc.ServerInterceptor): # type: ignore
|
|
@@ -62,7 +62,7 @@ class ExecEventLogInterceptor(grpc.ServerInterceptor): # type: ignore
|
|
|
62
62
|
log_entry = self.log_plugin.compose_log_before_event(
|
|
63
63
|
request=request,
|
|
64
64
|
context=context,
|
|
65
|
-
|
|
65
|
+
account_info=shared_account_info.get(),
|
|
66
66
|
method_name=method_name,
|
|
67
67
|
)
|
|
68
68
|
self.log_plugin.write_log(log_entry)
|
|
@@ -81,7 +81,7 @@ class ExecEventLogInterceptor(grpc.ServerInterceptor): # type: ignore
|
|
|
81
81
|
log_entry = self.log_plugin.compose_log_after_event(
|
|
82
82
|
request=request,
|
|
83
83
|
context=context,
|
|
84
|
-
|
|
84
|
+
account_info=shared_account_info.get(),
|
|
85
85
|
method_name=method_name,
|
|
86
86
|
response=unary_response or error,
|
|
87
87
|
)
|
|
@@ -111,7 +111,7 @@ class ExecEventLogInterceptor(grpc.ServerInterceptor): # type: ignore
|
|
|
111
111
|
log_entry = self.log_plugin.compose_log_after_event(
|
|
112
112
|
request=request,
|
|
113
113
|
context=context,
|
|
114
|
-
|
|
114
|
+
account_info=shared_account_info.get(),
|
|
115
115
|
method_name=method_name,
|
|
116
116
|
response=stream_response or error,
|
|
117
117
|
)
|
flwr/superexec/exec_grpc.py
CHANGED
|
@@ -21,7 +21,7 @@ from typing import Optional
|
|
|
21
21
|
import grpc
|
|
22
22
|
|
|
23
23
|
from flwr.common import GRPC_MAX_MESSAGE_LENGTH
|
|
24
|
-
from flwr.common.auth_plugin import ExecAuthPlugin
|
|
24
|
+
from flwr.common.auth_plugin import ExecAuthPlugin, ExecAuthzPlugin
|
|
25
25
|
from flwr.common.event_log_plugin import EventLogWriterPlugin
|
|
26
26
|
from flwr.common.grpc import generic_create_grpc_server
|
|
27
27
|
from flwr.common.logger import log
|
|
@@ -29,6 +29,7 @@ from flwr.common.typing import UserConfig
|
|
|
29
29
|
from flwr.proto.exec_pb2_grpc import add_ExecServicer_to_server
|
|
30
30
|
from flwr.server.superlink.ffs.ffs_factory import FfsFactory
|
|
31
31
|
from flwr.server.superlink.linkstate import LinkStateFactory
|
|
32
|
+
from flwr.supercore.object_store import ObjectStoreFactory
|
|
32
33
|
from flwr.superexec.exec_event_log_interceptor import ExecEventLogInterceptor
|
|
33
34
|
from flwr.superexec.exec_user_auth_interceptor import ExecUserAuthInterceptor
|
|
34
35
|
|
|
@@ -42,9 +43,11 @@ def run_exec_api_grpc(
|
|
|
42
43
|
executor: Executor,
|
|
43
44
|
state_factory: LinkStateFactory,
|
|
44
45
|
ffs_factory: FfsFactory,
|
|
46
|
+
objectstore_factory: ObjectStoreFactory,
|
|
45
47
|
certificates: Optional[tuple[bytes, bytes, bytes]],
|
|
46
48
|
config: UserConfig,
|
|
47
49
|
auth_plugin: Optional[ExecAuthPlugin] = None,
|
|
50
|
+
authz_plugin: Optional[ExecAuthzPlugin] = None,
|
|
48
51
|
event_log_plugin: Optional[EventLogWriterPlugin] = None,
|
|
49
52
|
) -> grpc.Server:
|
|
50
53
|
"""Run Exec API (gRPC, request-response)."""
|
|
@@ -53,12 +56,13 @@ def run_exec_api_grpc(
|
|
|
53
56
|
exec_servicer: grpc.Server = ExecServicer(
|
|
54
57
|
linkstate_factory=state_factory,
|
|
55
58
|
ffs_factory=ffs_factory,
|
|
59
|
+
objectstore_factory=objectstore_factory,
|
|
56
60
|
executor=executor,
|
|
57
61
|
auth_plugin=auth_plugin,
|
|
58
62
|
)
|
|
59
63
|
interceptors: list[grpc.ServerInterceptor] = []
|
|
60
|
-
if auth_plugin is not None:
|
|
61
|
-
interceptors.append(ExecUserAuthInterceptor(auth_plugin))
|
|
64
|
+
if auth_plugin is not None and authz_plugin is not None:
|
|
65
|
+
interceptors.append(ExecUserAuthInterceptor(auth_plugin, authz_plugin))
|
|
62
66
|
# Event log interceptor must be added after user auth interceptor
|
|
63
67
|
if event_log_plugin is not None:
|
|
64
68
|
interceptors.append(ExecEventLogInterceptor(event_log_plugin))
|