flwr 1.19.0__py3-none-any.whl → 1.20.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 (94) hide show
  1. flwr/cli/build.py +15 -5
  2. flwr/cli/new/new.py +12 -4
  3. flwr/cli/new/templates/app/README.flowertune.md.tpl +2 -0
  4. flwr/cli/new/templates/app/README.md.tpl +5 -0
  5. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +14 -3
  6. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +13 -1
  7. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +21 -2
  8. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +18 -1
  9. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +19 -2
  10. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +18 -1
  11. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +20 -3
  12. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +18 -1
  13. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +18 -1
  14. flwr/cli/run/run.py +45 -38
  15. flwr/cli/utils.py +12 -5
  16. flwr/client/grpc_adapter_client/connection.py +11 -4
  17. flwr/client/grpc_rere_client/connection.py +92 -117
  18. flwr/client/rest_client/connection.py +131 -164
  19. flwr/common/constant.py +3 -1
  20. flwr/common/exit/exit_code.py +16 -1
  21. flwr/common/grpc.py +12 -1
  22. flwr/common/{inflatable_grpc_utils.py → inflatable_protobuf_utils.py} +52 -10
  23. flwr/common/inflatable_utils.py +191 -24
  24. flwr/common/record/array.py +101 -22
  25. flwr/common/record/arraychunk.py +59 -0
  26. flwr/common/serde.py +0 -28
  27. flwr/compat/client/app.py +14 -31
  28. flwr/proto/appio_pb2.py +43 -0
  29. flwr/proto/appio_pb2.pyi +151 -0
  30. flwr/proto/appio_pb2_grpc.py +4 -0
  31. flwr/proto/appio_pb2_grpc.pyi +4 -0
  32. flwr/proto/clientappio_pb2.py +12 -19
  33. flwr/proto/clientappio_pb2.pyi +23 -101
  34. flwr/proto/clientappio_pb2_grpc.py +269 -28
  35. flwr/proto/clientappio_pb2_grpc.pyi +114 -20
  36. flwr/proto/fleet_pb2.py +12 -20
  37. flwr/proto/fleet_pb2.pyi +6 -36
  38. flwr/proto/serverappio_pb2.py +8 -31
  39. flwr/proto/serverappio_pb2.pyi +0 -152
  40. flwr/proto/serverappio_pb2_grpc.py +39 -38
  41. flwr/proto/serverappio_pb2_grpc.pyi +21 -20
  42. flwr/server/app.py +1 -1
  43. flwr/server/fleet_event_log_interceptor.py +4 -0
  44. flwr/server/grid/grpc_grid.py +91 -54
  45. flwr/server/serverapp/app.py +27 -17
  46. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +8 -0
  47. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +1 -1
  48. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +2 -5
  49. flwr/server/superlink/fleet/message_handler/message_handler.py +10 -16
  50. flwr/server/superlink/fleet/rest_rere/rest_api.py +1 -2
  51. flwr/server/superlink/serverappio/serverappio_grpc.py +1 -1
  52. flwr/server/superlink/serverappio/serverappio_servicer.py +35 -43
  53. flwr/server/superlink/simulation/simulationio_grpc.py +1 -1
  54. flwr/server/superlink/simulation/simulationio_servicer.py +1 -1
  55. flwr/server/superlink/utils.py +0 -35
  56. flwr/simulation/app.py +8 -0
  57. flwr/simulation/run_simulation.py +17 -0
  58. flwr/{server/superlink → supercore}/ffs/disk_ffs.py +1 -1
  59. flwr/supercore/grpc_health/__init__.py +22 -0
  60. flwr/supercore/grpc_health/simple_health_servicer.py +38 -0
  61. flwr/supercore/license_plugin/__init__.py +22 -0
  62. flwr/supercore/license_plugin/license_plugin.py +26 -0
  63. flwr/supercore/object_store/in_memory_object_store.py +31 -31
  64. flwr/supercore/object_store/object_store.py +20 -42
  65. flwr/supercore/object_store/utils.py +43 -0
  66. flwr/supercore/scheduler/__init__.py +22 -0
  67. flwr/supercore/scheduler/plugin.py +71 -0
  68. flwr/supercore/utils.py +32 -0
  69. flwr/superexec/deployment.py +1 -2
  70. flwr/superexec/exec_event_log_interceptor.py +4 -0
  71. flwr/superexec/exec_grpc.py +18 -2
  72. flwr/superexec/exec_license_interceptor.py +82 -0
  73. flwr/superexec/exec_servicer.py +10 -1
  74. flwr/superexec/exec_user_auth_interceptor.py +10 -2
  75. flwr/superexec/executor.py +1 -1
  76. flwr/superexec/simulation.py +1 -2
  77. flwr/supernode/cli/flower_supernode.py +0 -7
  78. flwr/supernode/cli/flwr_clientapp.py +10 -3
  79. flwr/supernode/nodestate/in_memory_nodestate.py +11 -2
  80. flwr/supernode/nodestate/nodestate.py +15 -0
  81. flwr/supernode/runtime/run_clientapp.py +110 -33
  82. flwr/supernode/scheduler/__init__.py +22 -0
  83. flwr/supernode/scheduler/simple_clientapp_scheduler_plugin.py +49 -0
  84. flwr/supernode/servicer/clientappio/__init__.py +1 -3
  85. flwr/supernode/servicer/clientappio/clientappio_servicer.py +223 -164
  86. flwr/supernode/start_client_internal.py +202 -104
  87. {flwr-1.19.0.dist-info → flwr-1.20.0.dist-info}/METADATA +2 -1
  88. {flwr-1.19.0.dist-info → flwr-1.20.0.dist-info}/RECORD +93 -78
  89. flwr/common/inflatable_rest_utils.py +0 -99
  90. /flwr/{server/superlink → supercore}/ffs/__init__.py +0 -0
  91. /flwr/{server/superlink → supercore}/ffs/ffs.py +0 -0
  92. /flwr/{server/superlink → supercore}/ffs/ffs_factory.py +0 -0
  93. {flwr-1.19.0.dist-info → flwr-1.20.0.dist-info}/WHEEL +0 -0
  94. {flwr-1.19.0.dist-info → flwr-1.20.0.dist-info}/entry_points.txt +0 -0
@@ -20,11 +20,7 @@ from typing import Optional, Union
20
20
  import grpc
21
21
 
22
22
  from flwr.common.constant import Status, SubStatus
23
- from flwr.common.inflatable import iterate_object_tree
24
23
  from flwr.common.typing import RunStatus
25
- from flwr.proto.fleet_pb2 import PushMessagesRequest # pylint: disable=E0611
26
- from flwr.proto.message_pb2 import ObjectIDs # pylint: disable=E0611
27
- from flwr.proto.serverappio_pb2 import PushInsMessagesRequest # pylint: disable=E0611
28
24
  from flwr.server.superlink.linkstate import LinkState
29
25
  from flwr.supercore.object_store import ObjectStore
30
26
 
@@ -74,34 +70,3 @@ def abort_if(
74
70
  """Abort context if status of the provided `run_id` is in `abort_status_list`."""
75
71
  msg = check_abort(run_id, abort_status_list, state, store)
76
72
  abort_grpc_context(msg, context)
77
-
78
-
79
- def store_mapping_and_register_objects(
80
- store: ObjectStore, request: Union[PushInsMessagesRequest, PushMessagesRequest]
81
- ) -> dict[str, ObjectIDs]:
82
- """Store Message object to descendants mapping and preregister objects."""
83
- if not request.messages_list:
84
- return {}
85
-
86
- objects_to_push: dict[str, ObjectIDs] = {}
87
-
88
- # Get run_id from the first message in the list
89
- # All messages of a request should in the same run
90
- run_id = request.messages_list[0].metadata.run_id
91
-
92
- for object_tree in request.message_object_trees:
93
- all_object_ids = [obj.object_id for obj in iterate_object_tree(object_tree)]
94
- msg_object_id, descendant_ids = all_object_ids[-1], all_object_ids[:-1]
95
- # Store mapping
96
- store.set_message_descendant_ids(
97
- msg_object_id=msg_object_id, descendant_ids=descendant_ids
98
- )
99
-
100
- # Preregister
101
- object_ids_just_registered = store.preregister(run_id, object_tree)
102
- # Keep track of objects that need to be pushed
103
- objects_to_push[msg_object_id] = ObjectIDs(
104
- object_ids=object_ids_just_registered
105
- )
106
-
107
- return objects_to_push
flwr/simulation/app.py CHANGED
@@ -16,6 +16,7 @@
16
16
 
17
17
 
18
18
  import argparse
19
+ import gc
19
20
  from logging import DEBUG, ERROR, INFO
20
21
  from queue import Queue
21
22
  from time import sleep
@@ -271,6 +272,13 @@ def run_simulation_process( # pylint: disable=R0914, disable=W0212, disable=R09
271
272
  )
272
273
  )
273
274
 
275
+ # Clean up the Context if it exists
276
+ try:
277
+ del updated_context
278
+ except NameError:
279
+ pass
280
+ gc.collect()
281
+
274
282
  # Stop the loop if `flwr-simulation` is expected to process a single run
275
283
  if run_once:
276
284
  break
@@ -19,6 +19,7 @@ import argparse
19
19
  import asyncio
20
20
  import json
21
21
  import logging
22
+ import platform
22
23
  import sys
23
24
  import threading
24
25
  import traceback
@@ -63,6 +64,18 @@ def _replace_keys(d: Any, match: str, target: str) -> Any:
63
64
  return d
64
65
 
65
66
 
67
+ def _check_ray_support(backend_name: str) -> None:
68
+ if backend_name.lower() == "ray":
69
+ if platform.system() == "Windows":
70
+ log(
71
+ WARNING,
72
+ "Ray support on Windows is experimental "
73
+ "and may not work as expected. "
74
+ "On Windows, Flower Simulations run best in WSL2: "
75
+ "https://learn.microsoft.com/en-us/windows/wsl/about",
76
+ )
77
+
78
+
66
79
  # Entry point from CLI
67
80
  # pylint: disable=too-many-locals
68
81
  def run_simulation_from_cli() -> None:
@@ -82,6 +95,8 @@ def run_simulation_from_cli() -> None:
82
95
  code_example='TF_FORCE_GPU_ALLOW_GROWTH="true" flower-simulation <...>',
83
96
  )
84
97
 
98
+ _check_ray_support(args.backend)
99
+
85
100
  # Load JSON config
86
101
  backend_config_dict = json.loads(args.backend_config)
87
102
 
@@ -208,6 +223,8 @@ def run_simulation(
208
223
  "\n\tflwr.simulation.run_simulationt(...)",
209
224
  )
210
225
 
226
+ _check_ray_support(backend_name)
227
+
211
228
  _ = _run_simulation(
212
229
  num_supernodes=num_supernodes,
213
230
  client_app=client_app,
@@ -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,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
+ """GRPC health servicers."""
16
+
17
+
18
+ from .simple_health_servicer import SimpleHealthServicer
19
+
20
+ __all__ = [
21
+ "SimpleHealthServicer",
22
+ ]
@@ -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.RpcContext
32
+ ) -> HealthCheckResponse:
33
+ """Return a HealthCheckResponse with SERVING status."""
34
+ return HealthCheckResponse(status=HealthCheckResponse.SERVING)
35
+
36
+ def Watch(self, request: HealthCheckRequest, context: grpc.RpcContext) -> 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
@@ -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 App Scheduler."""
16
+
17
+
18
+ from .plugin import SchedulerPlugin
19
+
20
+ __all__ = [
21
+ "SchedulerPlugin",
22
+ ]
@@ -0,0 +1,71 @@
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 SchedulerPlugin."""
16
+
17
+
18
+ from abc import ABC, abstractmethod
19
+ from collections.abc import Sequence
20
+ from typing import Callable, Optional
21
+
22
+ from flwr.common.typing import Run
23
+
24
+
25
+ class SchedulerPlugin(ABC):
26
+ """Abstract base class for Scheduler plugins."""
27
+
28
+ def __init__(
29
+ self,
30
+ appio_api_address: str,
31
+ flwr_dir: str,
32
+ get_run: Callable[[int], Run],
33
+ ) -> None:
34
+ self.appio_api_address = appio_api_address
35
+ self.flwr_dir = flwr_dir
36
+ self.get_run = get_run
37
+
38
+ @abstractmethod
39
+ def select_run_id(self, candidate_run_ids: Sequence[int]) -> Optional[int]:
40
+ """Select a run ID to execute from a sequence of candidates.
41
+
42
+ A candidate run ID is one that has at least one pending message and is
43
+ not currently in progress (i.e., not associated with a token).
44
+
45
+ Parameters
46
+ ----------
47
+ candidate_run_ids : Sequence[int]
48
+ A sequence of candidate run IDs to choose from.
49
+
50
+ Returns
51
+ -------
52
+ Optional[int]
53
+ The selected run ID, or None if no suitable candidate is found.
54
+ """
55
+
56
+ @abstractmethod
57
+ def launch_app(self, token: str, run_id: int) -> None:
58
+ """Launch the application associated with a given run ID and token.
59
+
60
+ This method starts the application process using the given `token`.
61
+ The `run_id` is used solely for bookkeeping purposes, allowing any
62
+ scheduler implementation to associate this launch with a specific run.
63
+
64
+ Parameters
65
+ ----------
66
+ token : str
67
+ The token required to run the application.
68
+ run_id : int
69
+ The ID of the run associated with the token, used for tracking or
70
+ logging purposes.
71
+ """
@@ -0,0 +1,32 @@
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
+ """Utility functions for the infrastructure."""
16
+
17
+
18
+ def mask_string(value: str, head: int = 4, tail: int = 4) -> str:
19
+ """Mask a string by preserving only the head and tail characters.
20
+
21
+ Mask a string for safe display by preserving the head and tail characters,
22
+ and replacing the middle with '...'. Useful for logging tokens, secrets,
23
+ or IDs without exposing sensitive data.
24
+
25
+ Notes
26
+ -----
27
+ If the string is shorter than the combined length of `head` and `tail`,
28
+ the original string is returned unchanged.
29
+ """
30
+ if len(value) <= head + tail:
31
+ return value
32
+ return f"{value[:head]}...{value[-tail:]}"
@@ -31,9 +31,8 @@ from flwr.common.constant import (
31
31
  )
32
32
  from flwr.common.logger import log
33
33
  from flwr.common.typing import Fab, RunStatus, UserConfig
34
- from flwr.server.superlink.ffs import Ffs
35
- from flwr.server.superlink.ffs.ffs_factory import FfsFactory
36
34
  from flwr.server.superlink.linkstate import LinkState, LinkStateFactory
35
+ from flwr.supercore.ffs import Ffs, FfsFactory
37
36
 
38
37
  from .executor import Executor
39
38
 
@@ -44,6 +44,10 @@ class ExecEventLogInterceptor(grpc.ServerInterceptor): # type: ignore
44
44
  Continue RPC call if event logger is enabled on the SuperLink, else, terminate
45
45
  RPC call by setting context to abort.
46
46
  """
47
+ # Only apply to Exec service
48
+ if not handler_call_details.method.startswith("/flwr.proto.Exec/"):
49
+ return continuation(handler_call_details)
50
+
47
51
  # One of the method handlers in
48
52
  # `flwr.superexec.exec_servicer.ExecServicer`
49
53
  method_handler: grpc.RpcMethodHandler = continuation(handler_call_details)
@@ -23,21 +23,31 @@ import grpc
23
23
  from flwr.common import GRPC_MAX_MESSAGE_LENGTH
24
24
  from flwr.common.auth_plugin import ExecAuthPlugin, ExecAuthzPlugin
25
25
  from flwr.common.event_log_plugin import EventLogWriterPlugin
26
+ from flwr.common.exit import ExitCode, flwr_exit
26
27
  from flwr.common.grpc import generic_create_grpc_server
27
28
  from flwr.common.logger import log
28
29
  from flwr.common.typing import UserConfig
29
30
  from flwr.proto.exec_pb2_grpc import add_ExecServicer_to_server
30
- from flwr.server.superlink.ffs.ffs_factory import FfsFactory
31
31
  from flwr.server.superlink.linkstate import LinkStateFactory
32
+ from flwr.supercore.ffs import FfsFactory
33
+ from flwr.supercore.license_plugin import LicensePlugin
32
34
  from flwr.supercore.object_store import ObjectStoreFactory
33
35
  from flwr.superexec.exec_event_log_interceptor import ExecEventLogInterceptor
36
+ from flwr.superexec.exec_license_interceptor import ExecLicenseInterceptor
34
37
  from flwr.superexec.exec_user_auth_interceptor import ExecUserAuthInterceptor
35
38
 
36
39
  from .exec_servicer import ExecServicer
37
40
  from .executor import Executor
38
41
 
42
+ try:
43
+ from flwr.ee import get_license_plugin
44
+ except ImportError:
39
45
 
40
- # pylint: disable-next=too-many-arguments, too-many-positional-arguments
46
+ def get_license_plugin() -> Optional[LicensePlugin]:
47
+ """Return the license plugin."""
48
+
49
+
50
+ # pylint: disable-next=too-many-arguments,too-many-positional-arguments,too-many-locals
41
51
  def run_exec_api_grpc(
42
52
  address: str,
43
53
  executor: Executor,
@@ -53,6 +63,10 @@ def run_exec_api_grpc(
53
63
  """Run Exec API (gRPC, request-response)."""
54
64
  executor.set_config(config)
55
65
 
66
+ license_plugin: Optional[LicensePlugin] = get_license_plugin()
67
+ if license_plugin and not license_plugin.check_license():
68
+ flwr_exit(ExitCode.SUPERLINK_LICENSE_INVALID)
69
+
56
70
  exec_servicer: grpc.Server = ExecServicer(
57
71
  linkstate_factory=state_factory,
58
72
  ffs_factory=ffs_factory,
@@ -61,6 +75,8 @@ def run_exec_api_grpc(
61
75
  auth_plugin=auth_plugin,
62
76
  )
63
77
  interceptors: list[grpc.ServerInterceptor] = []
78
+ if license_plugin is not None:
79
+ interceptors.append(ExecLicenseInterceptor(license_plugin))
64
80
  if auth_plugin is not None and authz_plugin is not None:
65
81
  interceptors.append(ExecUserAuthInterceptor(auth_plugin, authz_plugin))
66
82
  # Event log interceptor must be added after user auth interceptor