flwr 1.19.0__py3-none-any.whl → 1.21.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 (167) hide show
  1. flwr/__init__.py +4 -1
  2. flwr/app/__init__.py +28 -0
  3. flwr/app/exception.py +31 -0
  4. flwr/cli/auth_plugin/oidc_cli_plugin.py +4 -4
  5. flwr/cli/build.py +15 -5
  6. flwr/cli/cli_user_auth_interceptor.py +1 -1
  7. flwr/cli/config_utils.py +3 -3
  8. flwr/cli/constant.py +25 -8
  9. flwr/cli/log.py +9 -9
  10. flwr/cli/login/login.py +3 -3
  11. flwr/cli/ls.py +5 -5
  12. flwr/cli/new/new.py +23 -4
  13. flwr/cli/new/templates/app/README.flowertune.md.tpl +2 -0
  14. flwr/cli/new/templates/app/README.md.tpl +5 -0
  15. flwr/cli/new/templates/app/code/__init__.pytorch_msg_api.py.tpl +1 -0
  16. flwr/cli/new/templates/app/code/client.pytorch_msg_api.py.tpl +80 -0
  17. flwr/cli/new/templates/app/code/server.pytorch_msg_api.py.tpl +41 -0
  18. flwr/cli/new/templates/app/code/task.pytorch_msg_api.py.tpl +98 -0
  19. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +14 -3
  20. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +13 -1
  21. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +21 -2
  22. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +18 -1
  23. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +19 -2
  24. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +18 -1
  25. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +20 -3
  26. flwr/cli/new/templates/app/pyproject.pytorch_msg_api.toml.tpl +53 -0
  27. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +18 -1
  28. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +18 -1
  29. flwr/cli/run/run.py +53 -50
  30. flwr/cli/stop.py +7 -4
  31. flwr/cli/utils.py +29 -11
  32. flwr/client/grpc_adapter_client/connection.py +11 -4
  33. flwr/client/grpc_rere_client/connection.py +93 -129
  34. flwr/client/rest_client/connection.py +134 -164
  35. flwr/clientapp/__init__.py +10 -0
  36. flwr/clientapp/mod/__init__.py +26 -0
  37. flwr/clientapp/mod/centraldp_mods.py +132 -0
  38. flwr/common/args.py +20 -6
  39. flwr/common/auth_plugin/__init__.py +4 -4
  40. flwr/common/auth_plugin/auth_plugin.py +7 -7
  41. flwr/common/constant.py +26 -5
  42. flwr/common/event_log_plugin/event_log_plugin.py +1 -1
  43. flwr/common/exit/__init__.py +4 -0
  44. flwr/common/exit/exit.py +8 -1
  45. flwr/common/exit/exit_code.py +42 -8
  46. flwr/common/exit/exit_handler.py +62 -0
  47. flwr/common/{exit_handlers.py → exit/signal_handler.py} +20 -37
  48. flwr/common/grpc.py +1 -1
  49. flwr/common/{inflatable_grpc_utils.py → inflatable_protobuf_utils.py} +52 -10
  50. flwr/common/inflatable_utils.py +191 -24
  51. flwr/common/logger.py +1 -1
  52. flwr/common/record/array.py +101 -22
  53. flwr/common/record/arraychunk.py +59 -0
  54. flwr/common/retry_invoker.py +30 -11
  55. flwr/common/serde.py +0 -28
  56. flwr/common/telemetry.py +4 -0
  57. flwr/compat/client/app.py +14 -31
  58. flwr/compat/server/app.py +2 -2
  59. flwr/proto/appio_pb2.py +51 -0
  60. flwr/proto/appio_pb2.pyi +195 -0
  61. flwr/proto/appio_pb2_grpc.py +4 -0
  62. flwr/proto/appio_pb2_grpc.pyi +4 -0
  63. flwr/proto/clientappio_pb2.py +4 -19
  64. flwr/proto/clientappio_pb2.pyi +0 -125
  65. flwr/proto/clientappio_pb2_grpc.py +269 -29
  66. flwr/proto/clientappio_pb2_grpc.pyi +114 -21
  67. flwr/proto/control_pb2.py +62 -0
  68. flwr/proto/{exec_pb2_grpc.py → control_pb2_grpc.py} +54 -54
  69. flwr/proto/{exec_pb2_grpc.pyi → control_pb2_grpc.pyi} +28 -28
  70. flwr/proto/fleet_pb2.py +12 -20
  71. flwr/proto/fleet_pb2.pyi +6 -36
  72. flwr/proto/serverappio_pb2.py +8 -31
  73. flwr/proto/serverappio_pb2.pyi +0 -152
  74. flwr/proto/serverappio_pb2_grpc.py +107 -38
  75. flwr/proto/serverappio_pb2_grpc.pyi +47 -20
  76. flwr/proto/simulationio_pb2.py +4 -11
  77. flwr/proto/simulationio_pb2.pyi +0 -58
  78. flwr/proto/simulationio_pb2_grpc.py +129 -27
  79. flwr/proto/simulationio_pb2_grpc.pyi +52 -13
  80. flwr/server/app.py +130 -153
  81. flwr/server/fleet_event_log_interceptor.py +4 -0
  82. flwr/server/grid/grpc_grid.py +94 -54
  83. flwr/server/grid/inmemory_grid.py +1 -0
  84. flwr/server/serverapp/app.py +165 -144
  85. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +8 -0
  86. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +1 -1
  87. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +2 -5
  88. flwr/server/superlink/fleet/message_handler/message_handler.py +10 -16
  89. flwr/server/superlink/fleet/rest_rere/rest_api.py +1 -2
  90. flwr/server/superlink/fleet/vce/backend/raybackend.py +3 -1
  91. flwr/server/superlink/fleet/vce/vce_api.py +6 -6
  92. flwr/server/superlink/linkstate/in_memory_linkstate.py +34 -0
  93. flwr/server/superlink/linkstate/linkstate.py +2 -1
  94. flwr/server/superlink/linkstate/sqlite_linkstate.py +45 -0
  95. flwr/server/superlink/serverappio/serverappio_grpc.py +2 -2
  96. flwr/server/superlink/serverappio/serverappio_servicer.py +95 -48
  97. flwr/server/superlink/simulation/simulationio_grpc.py +1 -1
  98. flwr/server/superlink/simulation/simulationio_servicer.py +98 -22
  99. flwr/server/superlink/utils.py +0 -35
  100. flwr/serverapp/__init__.py +12 -0
  101. flwr/serverapp/dp_fixed_clipping.py +352 -0
  102. flwr/serverapp/exception.py +38 -0
  103. flwr/serverapp/strategy/__init__.py +38 -0
  104. flwr/serverapp/strategy/dp_fixed_clipping.py +352 -0
  105. flwr/serverapp/strategy/fedadagrad.py +162 -0
  106. flwr/serverapp/strategy/fedadam.py +181 -0
  107. flwr/serverapp/strategy/fedavg.py +295 -0
  108. flwr/serverapp/strategy/fedopt.py +218 -0
  109. flwr/serverapp/strategy/fedyogi.py +173 -0
  110. flwr/serverapp/strategy/result.py +105 -0
  111. flwr/serverapp/strategy/strategy.py +285 -0
  112. flwr/serverapp/strategy/strategy_utils.py +251 -0
  113. flwr/serverapp/strategy/strategy_utils_tests.py +304 -0
  114. flwr/simulation/app.py +159 -154
  115. flwr/simulation/run_simulation.py +17 -0
  116. flwr/supercore/app_utils.py +58 -0
  117. flwr/supercore/cli/__init__.py +22 -0
  118. flwr/supercore/cli/flower_superexec.py +141 -0
  119. flwr/supercore/corestate/__init__.py +22 -0
  120. flwr/supercore/corestate/corestate.py +81 -0
  121. flwr/{server/superlink → supercore}/ffs/disk_ffs.py +1 -1
  122. flwr/supercore/grpc_health/__init__.py +25 -0
  123. flwr/supercore/grpc_health/health_server.py +53 -0
  124. flwr/supercore/grpc_health/simple_health_servicer.py +38 -0
  125. flwr/supercore/license_plugin/__init__.py +22 -0
  126. flwr/supercore/license_plugin/license_plugin.py +26 -0
  127. flwr/supercore/object_store/in_memory_object_store.py +31 -31
  128. flwr/supercore/object_store/object_store.py +20 -42
  129. flwr/supercore/object_store/utils.py +43 -0
  130. flwr/{superexec → supercore/superexec}/__init__.py +1 -1
  131. flwr/supercore/superexec/plugin/__init__.py +28 -0
  132. flwr/supercore/superexec/plugin/base_exec_plugin.py +53 -0
  133. flwr/supercore/superexec/plugin/clientapp_exec_plugin.py +28 -0
  134. flwr/supercore/superexec/plugin/exec_plugin.py +71 -0
  135. flwr/supercore/superexec/plugin/serverapp_exec_plugin.py +28 -0
  136. flwr/supercore/superexec/plugin/simulation_exec_plugin.py +28 -0
  137. flwr/supercore/superexec/run_superexec.py +185 -0
  138. flwr/supercore/utils.py +32 -0
  139. flwr/superlink/servicer/__init__.py +15 -0
  140. flwr/superlink/servicer/control/__init__.py +22 -0
  141. flwr/{superexec/exec_event_log_interceptor.py → superlink/servicer/control/control_event_log_interceptor.py} +9 -5
  142. flwr/{superexec/exec_grpc.py → superlink/servicer/control/control_grpc.py} +39 -28
  143. flwr/superlink/servicer/control/control_license_interceptor.py +82 -0
  144. flwr/{superexec/exec_servicer.py → superlink/servicer/control/control_servicer.py} +79 -31
  145. flwr/{superexec/exec_user_auth_interceptor.py → superlink/servicer/control/control_user_auth_interceptor.py} +18 -10
  146. flwr/supernode/cli/flower_supernode.py +3 -7
  147. flwr/supernode/cli/flwr_clientapp.py +20 -16
  148. flwr/supernode/nodestate/in_memory_nodestate.py +13 -4
  149. flwr/supernode/nodestate/nodestate.py +3 -44
  150. flwr/supernode/runtime/run_clientapp.py +129 -115
  151. flwr/supernode/servicer/clientappio/__init__.py +1 -3
  152. flwr/supernode/servicer/clientappio/clientappio_servicer.py +217 -165
  153. flwr/supernode/start_client_internal.py +205 -148
  154. {flwr-1.19.0.dist-info → flwr-1.21.0.dist-info}/METADATA +5 -3
  155. {flwr-1.19.0.dist-info → flwr-1.21.0.dist-info}/RECORD +161 -117
  156. {flwr-1.19.0.dist-info → flwr-1.21.0.dist-info}/entry_points.txt +1 -0
  157. flwr/common/inflatable_rest_utils.py +0 -99
  158. flwr/proto/exec_pb2.py +0 -62
  159. flwr/superexec/app.py +0 -45
  160. flwr/superexec/deployment.py +0 -192
  161. flwr/superexec/executor.py +0 -100
  162. flwr/superexec/simulation.py +0 -130
  163. /flwr/proto/{exec_pb2.pyi → control_pb2.pyi} +0 -0
  164. /flwr/{server/superlink → supercore}/ffs/__init__.py +0 -0
  165. /flwr/{server/superlink → supercore}/ffs/ffs.py +0 -0
  166. /flwr/{server/superlink → supercore}/ffs/ffs_factory.py +0 -0
  167. {flwr-1.19.0.dist-info → flwr-1.21.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,22 @@
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 CoreState."""
16
+
17
+
18
+ from .corestate import CoreState
19
+
20
+ __all__ = [
21
+ "CoreState",
22
+ ]
@@ -0,0 +1,81 @@
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
+ """Abstract base class CoreState."""
16
+
17
+
18
+ from abc import ABC, abstractmethod
19
+ from typing import Optional
20
+
21
+
22
+ class CoreState(ABC):
23
+ """Abstract base class for core state."""
24
+
25
+ @abstractmethod
26
+ def create_token(self, run_id: int) -> Optional[str]:
27
+ """Create a token for the given run ID.
28
+
29
+ Parameters
30
+ ----------
31
+ run_id : int
32
+ The ID of the run for which to create a token.
33
+
34
+ Returns
35
+ -------
36
+ str
37
+ The newly generated token if one does not already exist
38
+ for the given run ID, otherwise None.
39
+ """
40
+
41
+ @abstractmethod
42
+ def verify_token(self, run_id: int, token: str) -> bool:
43
+ """Verify a token for the given run ID.
44
+
45
+ Parameters
46
+ ----------
47
+ run_id : int
48
+ The ID of the run for which to verify the token.
49
+ token : str
50
+ The token to verify.
51
+
52
+ Returns
53
+ -------
54
+ bool
55
+ True if the token is valid for the run ID, False otherwise.
56
+ """
57
+
58
+ @abstractmethod
59
+ def delete_token(self, run_id: int) -> None:
60
+ """Delete the token for the given run ID.
61
+
62
+ Parameters
63
+ ----------
64
+ run_id : int
65
+ The ID of the run for which to delete the token.
66
+ """
67
+
68
+ @abstractmethod
69
+ def get_run_id_by_token(self, token: str) -> Optional[int]:
70
+ """Get the run ID associated with a given token.
71
+
72
+ Parameters
73
+ ----------
74
+ token : str
75
+ The token to look up.
76
+
77
+ Returns
78
+ -------
79
+ Optional[int]
80
+ The run ID if the token is valid, otherwise None.
81
+ """
@@ -20,7 +20,7 @@ import json
20
20
  from pathlib import Path
21
21
  from typing import Optional
22
22
 
23
- from flwr.server.superlink.ffs.ffs import Ffs
23
+ from .ffs import Ffs
24
24
 
25
25
 
26
26
  class DiskFfs(Ffs): # pylint: disable=R0904
@@ -0,0 +1,25 @@
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
+ """GRPC health servicers."""
16
+
17
+
18
+ from .health_server import add_args_health, run_health_server_grpc_no_tls
19
+ from .simple_health_servicer import SimpleHealthServicer
20
+
21
+ __all__ = [
22
+ "SimpleHealthServicer",
23
+ "add_args_health",
24
+ "run_health_server_grpc_no_tls",
25
+ ]
@@ -0,0 +1,53 @@
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
+ """Health servers."""
16
+
17
+
18
+ import argparse
19
+ from logging import INFO
20
+
21
+ import grpc
22
+ from grpc_health.v1.health_pb2_grpc import add_HealthServicer_to_server
23
+
24
+ from flwr.common.grpc import generic_create_grpc_server
25
+ from flwr.common.logger import log
26
+
27
+ from .simple_health_servicer import SimpleHealthServicer
28
+
29
+
30
+ def run_health_server_grpc_no_tls(address: str) -> grpc.Server:
31
+ """Run gRPC health server with no TLS."""
32
+ health_server = generic_create_grpc_server(
33
+ servicer_and_add_fn=(
34
+ SimpleHealthServicer(),
35
+ add_HealthServicer_to_server,
36
+ ),
37
+ server_address=address,
38
+ certificates=None,
39
+ )
40
+ log(INFO, "Starting gRPC health server on %s", address)
41
+ health_server.start()
42
+ return health_server
43
+
44
+
45
+ def add_args_health(parser: argparse.ArgumentParser) -> None:
46
+ """Add arguments for health server."""
47
+ parser.add_argument(
48
+ "--health-server-address",
49
+ type=str,
50
+ default=None,
51
+ help="Health service gRPC server address (IPv4, IPv6, or a domain name) "
52
+ "with no TLS. If not set, the health server will not be started.",
53
+ )
@@ -0,0 +1,38 @@
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
+ """Simple gRPC health servicers."""
16
+
17
+
18
+ import grpc
19
+
20
+ # pylint: disable=E0611
21
+ from grpc_health.v1.health_pb2 import HealthCheckRequest, HealthCheckResponse
22
+ from grpc_health.v1.health_pb2_grpc import HealthServicer
23
+
24
+ # pylint: enable=E0611
25
+
26
+
27
+ class SimpleHealthServicer(HealthServicer): # type: ignore
28
+ """A simple gRPC health servicer that always returns SERVING."""
29
+
30
+ def Check(
31
+ self, request: HealthCheckRequest, context: grpc.ServicerContext
32
+ ) -> HealthCheckResponse:
33
+ """Return a HealthCheckResponse with SERVING status."""
34
+ return HealthCheckResponse(status=HealthCheckResponse.SERVING)
35
+
36
+ def Watch(self, request: HealthCheckRequest, context: grpc.ServicerContext) -> None:
37
+ """Watch the health status (not implemented)."""
38
+ context.abort(grpc.StatusCode.UNIMPLEMENTED, "Watch is not implemented")
@@ -0,0 +1,22 @@
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 license plugin components."""
16
+
17
+
18
+ from .license_plugin import LicensePlugin as LicensePlugin
19
+
20
+ __all__ = [
21
+ "LicensePlugin",
22
+ ]
@@ -0,0 +1,26 @@
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
+ """Abstract class for Flower License Plugin."""
16
+
17
+
18
+ from abc import ABC, abstractmethod
19
+
20
+
21
+ class LicensePlugin(ABC):
22
+ """Abstract Flower License Plugin class."""
23
+
24
+ @abstractmethod
25
+ def check_license(self) -> bool:
26
+ """Check if the license is valid."""
@@ -20,7 +20,6 @@ from dataclasses import dataclass
20
20
  from typing import Optional
21
21
 
22
22
  from flwr.common.inflatable import (
23
- get_object_children_ids_from_object_content,
24
23
  get_object_id,
25
24
  is_valid_sha256_hash,
26
25
  iterate_object_tree,
@@ -37,6 +36,7 @@ class ObjectEntry:
37
36
 
38
37
  content: bytes
39
38
  is_available: bool
39
+ child_object_ids: list[str] # List of child object IDs
40
40
  ref_count: int # Number of references (direct parents) to this object
41
41
  runs: set[int] # Set of run IDs that used this object
42
42
 
@@ -70,6 +70,9 @@ class InMemoryObjectStore(ObjectStore):
70
70
  self.store[obj_id] = ObjectEntry(
71
71
  content=b"", # Initially empty content
72
72
  is_available=False, # Initially not available
73
+ child_object_ids=[ # List of child object IDs
74
+ child.object_id for child in tree_node.children
75
+ ],
73
76
  ref_count=0, # Reference count starts at 0
74
77
  runs={run_id}, # Start with the current run ID
75
78
  )
@@ -102,6 +105,32 @@ class InMemoryObjectStore(ObjectStore):
102
105
 
103
106
  return new_objects
104
107
 
108
+ def get_object_tree(self, object_id: str) -> ObjectTree:
109
+ """Get the object tree for a given object ID."""
110
+ with self.lock_store:
111
+ # Raise an exception if there's no object with the given ID
112
+ if not (object_entry := self.store.get(object_id)):
113
+ raise NoObjectInStoreError(
114
+ f"Object with ID '{object_id}' was not pre-registered."
115
+ )
116
+
117
+ # Build the object trees of all children
118
+ try:
119
+ child_trees = [
120
+ self.get_object_tree(child_id)
121
+ for child_id in object_entry.child_object_ids
122
+ ]
123
+ except NoObjectInStoreError as e:
124
+ # Raise an error if any child object is missing
125
+ # This indicates an integrity issue
126
+ raise NoObjectInStoreError(
127
+ f"Object tree for object ID '{object_id}' contains missing "
128
+ "children. This may indicate a corrupted object store."
129
+ ) from e
130
+
131
+ # Create and return the ObjectTree for the current object
132
+ return ObjectTree(object_id=object_id, children=child_trees)
133
+
105
134
  def put(self, object_id: str, object_content: bytes) -> None:
106
135
  """Put an object into the store."""
107
136
  if self.verify:
@@ -128,29 +157,6 @@ class InMemoryObjectStore(ObjectStore):
128
157
  self.store[object_id].content = object_content
129
158
  self.store[object_id].is_available = True
130
159
 
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
160
  def get(self, object_id: str) -> Optional[bytes]:
155
161
  """Get an object from the store."""
156
162
  with self.lock_store:
@@ -177,10 +183,7 @@ class InMemoryObjectStore(ObjectStore):
177
183
  self.run_objects_mapping[run_id].discard(object_id)
178
184
 
179
185
  # 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:
186
+ for child_id in object_entry.child_object_ids:
184
187
  self.store[child_id].ref_count -= 1
185
188
 
186
189
  # Recursively try to delete the child object
@@ -205,9 +208,6 @@ class InMemoryObjectStore(ObjectStore):
205
208
  # Delete the message object and its unreferenced descendants
206
209
  self.delete(object_id)
207
210
 
208
- # Delete the message's descendants mapping
209
- self.delete_message_descendant_ids(object_id)
210
-
211
211
  # Remove the run from the mapping
212
212
  del self.run_objects_mapping[run_id]
213
213
 
@@ -60,6 +60,22 @@ class ObjectStore(abc.ABC):
60
60
  in the `ObjectStore`, or were preregistered but are not yet available.
61
61
  """
62
62
 
63
+ @abc.abstractmethod
64
+ def get_object_tree(self, object_id: str) -> ObjectTree:
65
+ """Get the object tree for a given object ID.
66
+
67
+ Parameters
68
+ ----------
69
+ object_id : str
70
+ The ID of the object for which to retrieve the object tree.
71
+
72
+ Returns
73
+ -------
74
+ ObjectTree
75
+ An ObjectTree representing the hierarchical structure of the object with
76
+ the given ID and its descendants.
77
+ """
78
+
63
79
  @abc.abstractmethod
64
80
  def put(self, object_id: str, object_content: bytes) -> None:
65
81
  """Put an object into the store.
@@ -83,8 +99,10 @@ class ObjectStore(abc.ABC):
83
99
 
84
100
  Returns
85
101
  -------
86
- bytes
87
- The object stored under the given object_id.
102
+ Optional[bytes]
103
+ The object stored under the given object_id if it exists, else None.
104
+ The returned bytes will be b"" if the object is not yet available,
105
+ but has been preregistered.
88
106
  """
89
107
 
90
108
  @abc.abstractmethod
@@ -126,46 +144,6 @@ class ObjectStore(abc.ABC):
126
144
  This method should remove all objects from the store.
127
145
  """
128
146
 
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
147
  @abc.abstractmethod
170
148
  def __contains__(self, object_id: str) -> bool:
171
149
  """Check if an object_id is in the store.
@@ -0,0 +1,43 @@
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
+ """Utils for ObjectStore."""
16
+
17
+
18
+ from typing import Union
19
+
20
+ from flwr.proto.appio_pb2 import PushAppMessagesRequest # pylint: disable=E0611
21
+ from flwr.proto.fleet_pb2 import PushMessagesRequest # pylint: disable=E0611
22
+
23
+ from . import ObjectStore
24
+
25
+
26
+ def store_mapping_and_register_objects(
27
+ store: ObjectStore, request: Union[PushAppMessagesRequest, PushMessagesRequest]
28
+ ) -> set[str]:
29
+ """Store Message object to descendants mapping and preregister objects."""
30
+ if not request.messages_list:
31
+ return set()
32
+ objects_to_push: set[str] = set()
33
+ # Get run_id from the first message in the list
34
+ # All messages of a request should in the same run
35
+ run_id = request.messages_list[0].metadata.run_id
36
+
37
+ for object_tree in request.message_object_trees:
38
+ # Preregister
39
+ unavailable_obj_ids = store.preregister(run_id, object_tree)
40
+ # Keep track of objects that need to be pushed
41
+ objects_to_push |= set(unavailable_obj_ids)
42
+
43
+ return objects_to_push
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  # ==============================================================================
15
- """Flower SuperExec service."""
15
+ """Flower SuperExec."""
@@ -0,0 +1,28 @@
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 SuperExec plugins."""
16
+
17
+
18
+ from .clientapp_exec_plugin import ClientAppExecPlugin
19
+ from .exec_plugin import ExecPlugin
20
+ from .serverapp_exec_plugin import ServerAppExecPlugin
21
+ from .simulation_exec_plugin import SimulationExecPlugin
22
+
23
+ __all__ = [
24
+ "ClientAppExecPlugin",
25
+ "ExecPlugin",
26
+ "ServerAppExecPlugin",
27
+ "SimulationExecPlugin",
28
+ ]
@@ -0,0 +1,53 @@
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
+ """Simple base Flower SuperExec plugin for app processes."""
16
+
17
+
18
+ import os
19
+ import subprocess
20
+ from collections.abc import Sequence
21
+ from typing import Optional
22
+
23
+ from .exec_plugin import ExecPlugin
24
+
25
+
26
+ class BaseExecPlugin(ExecPlugin):
27
+ """Simple Flower SuperExec plugin for app processes.
28
+
29
+ The plugin always selects the first candidate run ID.
30
+ """
31
+
32
+ # Placeholders to be defined in subclasses
33
+ command = ""
34
+ appio_api_address_arg = ""
35
+
36
+ def select_run_id(self, candidate_run_ids: Sequence[int]) -> Optional[int]:
37
+ """Select a run ID to execute from a sequence of candidates."""
38
+ if not candidate_run_ids:
39
+ return None
40
+ return candidate_run_ids[0]
41
+
42
+ def launch_app(self, token: str, run_id: int) -> None:
43
+ """Launch the application associated with a given run ID and token."""
44
+ cmds = [self.command, "--insecure"]
45
+ cmds += [self.appio_api_address_arg, self.appio_api_address]
46
+ cmds += ["--token", token]
47
+ cmds += ["--parent-pid", str(os.getpid())]
48
+ cmds += ["--flwr-dir", self.flwr_dir]
49
+ # Launch the client app without waiting for it to complete.
50
+ # Since we don't need to manage the process, we intentionally avoid using
51
+ # a `with` statement. Suppress the pylint warning for it in this case.
52
+ # pylint: disable-next=consider-using-with
53
+ subprocess.Popen(cmds)
@@ -0,0 +1,28 @@
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
+ """Simple Flower SuperExec plugin for ClientApp."""
16
+
17
+
18
+ from .base_exec_plugin import BaseExecPlugin
19
+
20
+
21
+ class ClientAppExecPlugin(BaseExecPlugin):
22
+ """Simple Flower SuperExec plugin for ClientApp.
23
+
24
+ The plugin always selects the first candidate run ID.
25
+ """
26
+
27
+ command = "flwr-clientapp"
28
+ appio_api_address_arg = "--clientappio-api-address"