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.
Files changed (143) hide show
  1. flwr/app/__init__.py +15 -0
  2. flwr/app/error.py +68 -0
  3. flwr/app/metadata.py +223 -0
  4. flwr/cli/build.py +82 -57
  5. flwr/cli/log.py +3 -3
  6. flwr/cli/login/login.py +3 -7
  7. flwr/cli/ls.py +15 -36
  8. flwr/cli/new/templates/app/code/client.baseline.py.tpl +1 -1
  9. flwr/cli/new/templates/app/code/model.baseline.py.tpl +1 -1
  10. flwr/cli/new/templates/app/code/server.baseline.py.tpl +2 -3
  11. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +14 -17
  12. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
  13. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  14. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  15. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  16. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  17. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
  18. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  19. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
  20. flwr/cli/run/run.py +10 -18
  21. flwr/cli/stop.py +2 -2
  22. flwr/cli/utils.py +31 -5
  23. flwr/client/__init__.py +2 -2
  24. flwr/client/client_app.py +1 -1
  25. flwr/client/clientapp/__init__.py +0 -7
  26. flwr/client/grpc_adapter_client/connection.py +4 -4
  27. flwr/client/grpc_rere_client/connection.py +130 -60
  28. flwr/client/grpc_rere_client/grpc_adapter.py +34 -6
  29. flwr/client/message_handler/message_handler.py +1 -1
  30. flwr/client/mod/comms_mods.py +36 -17
  31. flwr/client/rest_client/connection.py +173 -67
  32. flwr/clientapp/__init__.py +15 -0
  33. flwr/common/__init__.py +2 -2
  34. flwr/common/auth_plugin/__init__.py +2 -0
  35. flwr/common/auth_plugin/auth_plugin.py +29 -3
  36. flwr/common/constant.py +36 -7
  37. flwr/common/event_log_plugin/event_log_plugin.py +3 -3
  38. flwr/common/exit_handlers.py +30 -0
  39. flwr/common/heartbeat.py +165 -0
  40. flwr/common/inflatable.py +290 -0
  41. flwr/common/inflatable_grpc_utils.py +99 -0
  42. flwr/common/inflatable_rest_utils.py +99 -0
  43. flwr/common/inflatable_utils.py +341 -0
  44. flwr/common/message.py +110 -242
  45. flwr/common/record/__init__.py +2 -1
  46. flwr/common/record/array.py +323 -0
  47. flwr/common/record/arrayrecord.py +103 -225
  48. flwr/common/record/configrecord.py +59 -4
  49. flwr/common/record/conversion_utils.py +1 -1
  50. flwr/common/record/metricrecord.py +55 -4
  51. flwr/common/record/recorddict.py +69 -1
  52. flwr/common/recorddict_compat.py +2 -2
  53. flwr/common/retry_invoker.py +5 -1
  54. flwr/common/serde.py +59 -183
  55. flwr/common/serde_utils.py +175 -0
  56. flwr/common/typing.py +5 -3
  57. flwr/compat/__init__.py +15 -0
  58. flwr/compat/client/__init__.py +15 -0
  59. flwr/{client → compat/client}/app.py +19 -159
  60. flwr/compat/common/__init__.py +15 -0
  61. flwr/compat/server/__init__.py +15 -0
  62. flwr/compat/server/app.py +174 -0
  63. flwr/compat/simulation/__init__.py +15 -0
  64. flwr/proto/fleet_pb2.py +32 -27
  65. flwr/proto/fleet_pb2.pyi +49 -35
  66. flwr/proto/fleet_pb2_grpc.py +117 -13
  67. flwr/proto/fleet_pb2_grpc.pyi +47 -6
  68. flwr/proto/heartbeat_pb2.py +33 -0
  69. flwr/proto/heartbeat_pb2.pyi +66 -0
  70. flwr/proto/heartbeat_pb2_grpc.py +4 -0
  71. flwr/proto/heartbeat_pb2_grpc.pyi +4 -0
  72. flwr/proto/message_pb2.py +28 -11
  73. flwr/proto/message_pb2.pyi +125 -0
  74. flwr/proto/recorddict_pb2.py +16 -28
  75. flwr/proto/recorddict_pb2.pyi +46 -64
  76. flwr/proto/run_pb2.py +24 -32
  77. flwr/proto/run_pb2.pyi +4 -52
  78. flwr/proto/serverappio_pb2.py +32 -23
  79. flwr/proto/serverappio_pb2.pyi +45 -3
  80. flwr/proto/serverappio_pb2_grpc.py +138 -34
  81. flwr/proto/serverappio_pb2_grpc.pyi +54 -13
  82. flwr/proto/simulationio_pb2.py +12 -11
  83. flwr/proto/simulationio_pb2_grpc.py +35 -0
  84. flwr/proto/simulationio_pb2_grpc.pyi +14 -0
  85. flwr/server/__init__.py +1 -1
  86. flwr/server/app.py +68 -186
  87. flwr/server/compat/app_utils.py +50 -28
  88. flwr/server/fleet_event_log_interceptor.py +2 -2
  89. flwr/server/grid/grpc_grid.py +104 -34
  90. flwr/server/grid/inmemory_grid.py +5 -4
  91. flwr/server/serverapp/app.py +18 -0
  92. flwr/server/superlink/ffs/__init__.py +2 -0
  93. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +13 -3
  94. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +101 -7
  95. flwr/server/superlink/fleet/message_handler/message_handler.py +135 -18
  96. flwr/server/superlink/fleet/rest_rere/rest_api.py +72 -11
  97. flwr/server/superlink/fleet/vce/vce_api.py +6 -3
  98. flwr/server/superlink/linkstate/in_memory_linkstate.py +138 -43
  99. flwr/server/superlink/linkstate/linkstate.py +53 -20
  100. flwr/server/superlink/linkstate/sqlite_linkstate.py +149 -55
  101. flwr/server/superlink/linkstate/utils.py +33 -29
  102. flwr/server/superlink/serverappio/serverappio_grpc.py +3 -0
  103. flwr/server/superlink/serverappio/serverappio_servicer.py +211 -57
  104. flwr/server/superlink/simulation/simulationio_servicer.py +25 -1
  105. flwr/server/superlink/utils.py +44 -2
  106. flwr/server/utils/validator.py +2 -2
  107. flwr/serverapp/__init__.py +15 -0
  108. flwr/simulation/app.py +17 -0
  109. flwr/supercore/__init__.py +15 -0
  110. flwr/supercore/object_store/__init__.py +24 -0
  111. flwr/supercore/object_store/in_memory_object_store.py +229 -0
  112. flwr/supercore/object_store/object_store.py +192 -0
  113. flwr/supercore/object_store/object_store_factory.py +44 -0
  114. flwr/superexec/deployment.py +6 -2
  115. flwr/superexec/exec_event_log_interceptor.py +4 -4
  116. flwr/superexec/exec_grpc.py +7 -3
  117. flwr/superexec/exec_servicer.py +125 -23
  118. flwr/superexec/exec_user_auth_interceptor.py +37 -8
  119. flwr/superexec/executor.py +4 -0
  120. flwr/superexec/simulation.py +7 -1
  121. flwr/superlink/__init__.py +15 -0
  122. flwr/{client/supernode → supernode}/__init__.py +0 -7
  123. flwr/{client/nodestate/nodestate.py → supernode/cli/__init__.py} +7 -14
  124. flwr/{client/supernode/app.py → supernode/cli/flower_supernode.py} +3 -12
  125. flwr/supernode/cli/flwr_clientapp.py +81 -0
  126. flwr/supernode/nodestate/in_memory_nodestate.py +190 -0
  127. flwr/supernode/nodestate/nodestate.py +212 -0
  128. flwr/supernode/runtime/__init__.py +15 -0
  129. flwr/{client/clientapp/app.py → supernode/runtime/run_clientapp.py} +25 -56
  130. flwr/supernode/servicer/__init__.py +15 -0
  131. flwr/supernode/servicer/clientappio/__init__.py +24 -0
  132. flwr/supernode/start_client_internal.py +491 -0
  133. {flwr-1.18.0.dist-info → flwr-1.19.0.dist-info}/METADATA +5 -4
  134. {flwr-1.18.0.dist-info → flwr-1.19.0.dist-info}/RECORD +141 -108
  135. {flwr-1.18.0.dist-info → flwr-1.19.0.dist-info}/WHEEL +1 -1
  136. {flwr-1.18.0.dist-info → flwr-1.19.0.dist-info}/entry_points.txt +2 -2
  137. flwr/client/heartbeat.py +0 -74
  138. flwr/client/nodestate/in_memory_nodestate.py +0 -38
  139. /flwr/{client → compat/client}/grpc_client/__init__.py +0 -0
  140. /flwr/{client → compat/client}/grpc_client/connection.py +0 -0
  141. /flwr/{client → supernode}/nodestate/__init__.py +0 -0
  142. /flwr/{client → supernode}/nodestate/nodestate_factory.py +0 -0
  143. /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
@@ -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), override_config
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 shared_user_info
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
- user_info=shared_user_info.get(),
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
- user_info=shared_user_info.get(),
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
- user_info=shared_user_info.get(),
114
+ account_info=shared_account_info.get(),
115
115
  method_name=method_name,
116
116
  response=stream_response or error,
117
117
  )
@@ -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))