flwr 1.18.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 (174) 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 +94 -59
  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/new.py +12 -4
  9. flwr/cli/new/templates/app/README.flowertune.md.tpl +2 -0
  10. flwr/cli/new/templates/app/README.md.tpl +5 -0
  11. flwr/cli/new/templates/app/code/client.baseline.py.tpl +1 -1
  12. flwr/cli/new/templates/app/code/model.baseline.py.tpl +1 -1
  13. flwr/cli/new/templates/app/code/server.baseline.py.tpl +2 -3
  14. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +25 -17
  15. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +13 -1
  16. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +21 -2
  17. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +18 -1
  18. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +19 -2
  19. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +18 -1
  20. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +20 -3
  21. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +18 -1
  22. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +18 -1
  23. flwr/cli/run/run.py +48 -49
  24. flwr/cli/stop.py +2 -2
  25. flwr/cli/utils.py +38 -5
  26. flwr/client/__init__.py +2 -2
  27. flwr/client/client_app.py +1 -1
  28. flwr/client/clientapp/__init__.py +0 -7
  29. flwr/client/grpc_adapter_client/connection.py +15 -8
  30. flwr/client/grpc_rere_client/connection.py +142 -97
  31. flwr/client/grpc_rere_client/grpc_adapter.py +34 -6
  32. flwr/client/message_handler/message_handler.py +1 -1
  33. flwr/client/mod/comms_mods.py +36 -17
  34. flwr/client/rest_client/connection.py +176 -103
  35. flwr/clientapp/__init__.py +15 -0
  36. flwr/common/__init__.py +2 -2
  37. flwr/common/auth_plugin/__init__.py +2 -0
  38. flwr/common/auth_plugin/auth_plugin.py +29 -3
  39. flwr/common/constant.py +39 -8
  40. flwr/common/event_log_plugin/event_log_plugin.py +3 -3
  41. flwr/common/exit/exit_code.py +16 -1
  42. flwr/common/exit_handlers.py +30 -0
  43. flwr/common/grpc.py +12 -1
  44. flwr/common/heartbeat.py +165 -0
  45. flwr/common/inflatable.py +290 -0
  46. flwr/common/inflatable_protobuf_utils.py +141 -0
  47. flwr/common/inflatable_utils.py +508 -0
  48. flwr/common/message.py +110 -242
  49. flwr/common/record/__init__.py +2 -1
  50. flwr/common/record/array.py +402 -0
  51. flwr/common/record/arraychunk.py +59 -0
  52. flwr/common/record/arrayrecord.py +103 -225
  53. flwr/common/record/configrecord.py +59 -4
  54. flwr/common/record/conversion_utils.py +1 -1
  55. flwr/common/record/metricrecord.py +55 -4
  56. flwr/common/record/recorddict.py +69 -1
  57. flwr/common/recorddict_compat.py +2 -2
  58. flwr/common/retry_invoker.py +5 -1
  59. flwr/common/serde.py +59 -211
  60. flwr/common/serde_utils.py +175 -0
  61. flwr/common/typing.py +5 -3
  62. flwr/compat/__init__.py +15 -0
  63. flwr/compat/client/__init__.py +15 -0
  64. flwr/{client → compat/client}/app.py +28 -185
  65. flwr/compat/common/__init__.py +15 -0
  66. flwr/compat/server/__init__.py +15 -0
  67. flwr/compat/server/app.py +174 -0
  68. flwr/compat/simulation/__init__.py +15 -0
  69. flwr/proto/appio_pb2.py +43 -0
  70. flwr/proto/appio_pb2.pyi +151 -0
  71. flwr/proto/appio_pb2_grpc.py +4 -0
  72. flwr/proto/appio_pb2_grpc.pyi +4 -0
  73. flwr/proto/clientappio_pb2.py +12 -19
  74. flwr/proto/clientappio_pb2.pyi +23 -101
  75. flwr/proto/clientappio_pb2_grpc.py +269 -28
  76. flwr/proto/clientappio_pb2_grpc.pyi +114 -20
  77. flwr/proto/fleet_pb2.py +24 -27
  78. flwr/proto/fleet_pb2.pyi +19 -35
  79. flwr/proto/fleet_pb2_grpc.py +117 -13
  80. flwr/proto/fleet_pb2_grpc.pyi +47 -6
  81. flwr/proto/heartbeat_pb2.py +33 -0
  82. flwr/proto/heartbeat_pb2.pyi +66 -0
  83. flwr/proto/heartbeat_pb2_grpc.py +4 -0
  84. flwr/proto/heartbeat_pb2_grpc.pyi +4 -0
  85. flwr/proto/message_pb2.py +28 -11
  86. flwr/proto/message_pb2.pyi +125 -0
  87. flwr/proto/recorddict_pb2.py +16 -28
  88. flwr/proto/recorddict_pb2.pyi +46 -64
  89. flwr/proto/run_pb2.py +24 -32
  90. flwr/proto/run_pb2.pyi +4 -52
  91. flwr/proto/serverappio_pb2.py +9 -23
  92. flwr/proto/serverappio_pb2.pyi +0 -110
  93. flwr/proto/serverappio_pb2_grpc.py +177 -72
  94. flwr/proto/serverappio_pb2_grpc.pyi +75 -33
  95. flwr/proto/simulationio_pb2.py +12 -11
  96. flwr/proto/simulationio_pb2_grpc.py +35 -0
  97. flwr/proto/simulationio_pb2_grpc.pyi +14 -0
  98. flwr/server/__init__.py +1 -1
  99. flwr/server/app.py +69 -187
  100. flwr/server/compat/app_utils.py +50 -28
  101. flwr/server/fleet_event_log_interceptor.py +6 -2
  102. flwr/server/grid/grpc_grid.py +148 -41
  103. flwr/server/grid/inmemory_grid.py +5 -4
  104. flwr/server/serverapp/app.py +45 -17
  105. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +21 -3
  106. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +102 -8
  107. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +2 -5
  108. flwr/server/superlink/fleet/message_handler/message_handler.py +130 -19
  109. flwr/server/superlink/fleet/rest_rere/rest_api.py +73 -13
  110. flwr/server/superlink/fleet/vce/vce_api.py +6 -3
  111. flwr/server/superlink/linkstate/in_memory_linkstate.py +138 -43
  112. flwr/server/superlink/linkstate/linkstate.py +53 -20
  113. flwr/server/superlink/linkstate/sqlite_linkstate.py +149 -55
  114. flwr/server/superlink/linkstate/utils.py +33 -29
  115. flwr/server/superlink/serverappio/serverappio_grpc.py +4 -1
  116. flwr/server/superlink/serverappio/serverappio_servicer.py +230 -84
  117. flwr/server/superlink/simulation/simulationio_grpc.py +1 -1
  118. flwr/server/superlink/simulation/simulationio_servicer.py +26 -2
  119. flwr/server/superlink/utils.py +9 -2
  120. flwr/server/utils/validator.py +2 -2
  121. flwr/serverapp/__init__.py +15 -0
  122. flwr/simulation/app.py +25 -0
  123. flwr/simulation/run_simulation.py +17 -0
  124. flwr/supercore/__init__.py +15 -0
  125. flwr/{server/superlink → supercore}/ffs/__init__.py +2 -0
  126. flwr/{server/superlink → supercore}/ffs/disk_ffs.py +1 -1
  127. flwr/supercore/grpc_health/__init__.py +22 -0
  128. flwr/supercore/grpc_health/simple_health_servicer.py +38 -0
  129. flwr/supercore/license_plugin/__init__.py +22 -0
  130. flwr/supercore/license_plugin/license_plugin.py +26 -0
  131. flwr/supercore/object_store/__init__.py +24 -0
  132. flwr/supercore/object_store/in_memory_object_store.py +229 -0
  133. flwr/supercore/object_store/object_store.py +170 -0
  134. flwr/supercore/object_store/object_store_factory.py +44 -0
  135. flwr/supercore/object_store/utils.py +43 -0
  136. flwr/supercore/scheduler/__init__.py +22 -0
  137. flwr/supercore/scheduler/plugin.py +71 -0
  138. flwr/{client/nodestate/nodestate.py → supercore/utils.py} +14 -13
  139. flwr/superexec/deployment.py +7 -4
  140. flwr/superexec/exec_event_log_interceptor.py +8 -4
  141. flwr/superexec/exec_grpc.py +25 -5
  142. flwr/superexec/exec_license_interceptor.py +82 -0
  143. flwr/superexec/exec_servicer.py +135 -24
  144. flwr/superexec/exec_user_auth_interceptor.py +45 -8
  145. flwr/superexec/executor.py +5 -1
  146. flwr/superexec/simulation.py +8 -3
  147. flwr/superlink/__init__.py +15 -0
  148. flwr/{client/supernode → supernode}/__init__.py +0 -7
  149. flwr/supernode/cli/__init__.py +24 -0
  150. flwr/{client/supernode/app.py → supernode/cli/flower_supernode.py} +3 -19
  151. flwr/supernode/cli/flwr_clientapp.py +88 -0
  152. flwr/supernode/nodestate/in_memory_nodestate.py +199 -0
  153. flwr/supernode/nodestate/nodestate.py +227 -0
  154. flwr/supernode/runtime/__init__.py +15 -0
  155. flwr/{client/clientapp/app.py → supernode/runtime/run_clientapp.py} +135 -89
  156. flwr/supernode/scheduler/__init__.py +22 -0
  157. flwr/supernode/scheduler/simple_clientapp_scheduler_plugin.py +49 -0
  158. flwr/supernode/servicer/__init__.py +15 -0
  159. flwr/supernode/servicer/clientappio/__init__.py +22 -0
  160. flwr/supernode/servicer/clientappio/clientappio_servicer.py +303 -0
  161. flwr/supernode/start_client_internal.py +589 -0
  162. {flwr-1.18.0.dist-info → flwr-1.20.0.dist-info}/METADATA +6 -4
  163. {flwr-1.18.0.dist-info → flwr-1.20.0.dist-info}/RECORD +171 -123
  164. {flwr-1.18.0.dist-info → flwr-1.20.0.dist-info}/WHEEL +1 -1
  165. {flwr-1.18.0.dist-info → flwr-1.20.0.dist-info}/entry_points.txt +2 -2
  166. flwr/client/clientapp/clientappio_servicer.py +0 -244
  167. flwr/client/heartbeat.py +0 -74
  168. flwr/client/nodestate/in_memory_nodestate.py +0 -38
  169. /flwr/{client → compat/client}/grpc_client/__init__.py +0 -0
  170. /flwr/{client → compat/client}/grpc_client/connection.py +0 -0
  171. /flwr/{server/superlink → supercore}/ffs/ffs.py +0 -0
  172. /flwr/{server/superlink → supercore}/ffs/ffs_factory.py +0 -0
  173. /flwr/{client → supernode}/nodestate/__init__.py +0 -0
  174. /flwr/{client → supernode}/nodestate/nodestate_factory.py +0 -0
@@ -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
@@ -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
+ """
@@ -12,20 +12,21 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  # ==============================================================================
15
- """Abstract base class NodeState."""
15
+ """Utility functions for the infrastructure."""
16
16
 
17
17
 
18
- import abc
19
- from typing import Optional
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
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.
21
24
 
22
- class NodeState(abc.ABC):
23
- """Abstract NodeState."""
24
-
25
- @abc.abstractmethod
26
- def set_node_id(self, node_id: Optional[int]) -> None:
27
- """Set the node ID."""
28
-
29
- @abc.abstractmethod
30
- def get_node_id(self) -> int:
31
- """Get the node ID."""
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
 
@@ -132,6 +131,7 @@ class DeploymentEngine(Executor):
132
131
  self,
133
132
  fab: Fab,
134
133
  override_config: UserConfig,
134
+ flwr_aid: Optional[str],
135
135
  ) -> int:
136
136
  fab_hash = self.ffs.put(fab.content, {})
137
137
  if fab_hash != fab.hash_str:
@@ -141,7 +141,7 @@ class DeploymentEngine(Executor):
141
141
  fab_id, fab_version = get_fab_metadata(fab.content)
142
142
 
143
143
  run_id = self.linkstate.create_run(
144
- fab_id, fab_version, fab_hash, override_config, ConfigRecord()
144
+ fab_id, fab_version, fab_hash, override_config, ConfigRecord(), flwr_aid
145
145
  )
146
146
  return run_id
147
147
 
@@ -161,6 +161,7 @@ class DeploymentEngine(Executor):
161
161
  fab_file: bytes,
162
162
  override_config: UserConfig,
163
163
  federation_options: ConfigRecord,
164
+ flwr_aid: Optional[str],
164
165
  ) -> Optional[int]:
165
166
  """Start run using the Flower Deployment Engine."""
166
167
  run_id = None
@@ -168,7 +169,9 @@ class DeploymentEngine(Executor):
168
169
 
169
170
  # Call SuperLink to create run
170
171
  run_id = self._create_run(
171
- Fab(hashlib.sha256(fab_file).hexdigest(), fab_file), override_config
172
+ Fab(hashlib.sha256(fab_file).hexdigest(), fab_file),
173
+ override_config,
174
+ flwr_aid,
172
175
  )
173
176
 
174
177
  # 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
@@ -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)
@@ -62,7 +66,7 @@ class ExecEventLogInterceptor(grpc.ServerInterceptor): # type: ignore
62
66
  log_entry = self.log_plugin.compose_log_before_event(
63
67
  request=request,
64
68
  context=context,
65
- user_info=shared_user_info.get(),
69
+ account_info=shared_account_info.get(),
66
70
  method_name=method_name,
67
71
  )
68
72
  self.log_plugin.write_log(log_entry)
@@ -81,7 +85,7 @@ class ExecEventLogInterceptor(grpc.ServerInterceptor): # type: ignore
81
85
  log_entry = self.log_plugin.compose_log_after_event(
82
86
  request=request,
83
87
  context=context,
84
- user_info=shared_user_info.get(),
88
+ account_info=shared_account_info.get(),
85
89
  method_name=method_name,
86
90
  response=unary_response or error,
87
91
  )
@@ -111,7 +115,7 @@ class ExecEventLogInterceptor(grpc.ServerInterceptor): # type: ignore
111
115
  log_entry = self.log_plugin.compose_log_after_event(
112
116
  request=request,
113
117
  context=context,
114
- user_info=shared_user_info.get(),
118
+ account_info=shared_account_info.get(),
115
119
  method_name=method_name,
116
120
  response=stream_response or error,
117
121
  )
@@ -21,44 +21,64 @@ 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
+ 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
34
+ from flwr.supercore.object_store import ObjectStoreFactory
32
35
  from flwr.superexec.exec_event_log_interceptor import ExecEventLogInterceptor
36
+ from flwr.superexec.exec_license_interceptor import ExecLicenseInterceptor
33
37
  from flwr.superexec.exec_user_auth_interceptor import ExecUserAuthInterceptor
34
38
 
35
39
  from .exec_servicer import ExecServicer
36
40
  from .executor import Executor
37
41
 
42
+ try:
43
+ from flwr.ee import get_license_plugin
44
+ except ImportError:
38
45
 
39
- # 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
40
51
  def run_exec_api_grpc(
41
52
  address: str,
42
53
  executor: Executor,
43
54
  state_factory: LinkStateFactory,
44
55
  ffs_factory: FfsFactory,
56
+ objectstore_factory: ObjectStoreFactory,
45
57
  certificates: Optional[tuple[bytes, bytes, bytes]],
46
58
  config: UserConfig,
47
59
  auth_plugin: Optional[ExecAuthPlugin] = None,
60
+ authz_plugin: Optional[ExecAuthzPlugin] = None,
48
61
  event_log_plugin: Optional[EventLogWriterPlugin] = None,
49
62
  ) -> grpc.Server:
50
63
  """Run Exec API (gRPC, request-response)."""
51
64
  executor.set_config(config)
52
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
+
53
70
  exec_servicer: grpc.Server = ExecServicer(
54
71
  linkstate_factory=state_factory,
55
72
  ffs_factory=ffs_factory,
73
+ objectstore_factory=objectstore_factory,
56
74
  executor=executor,
57
75
  auth_plugin=auth_plugin,
58
76
  )
59
77
  interceptors: list[grpc.ServerInterceptor] = []
60
- if auth_plugin is not None:
61
- interceptors.append(ExecUserAuthInterceptor(auth_plugin))
78
+ if license_plugin is not None:
79
+ interceptors.append(ExecLicenseInterceptor(license_plugin))
80
+ if auth_plugin is not None and authz_plugin is not None:
81
+ interceptors.append(ExecUserAuthInterceptor(auth_plugin, authz_plugin))
62
82
  # Event log interceptor must be added after user auth interceptor
63
83
  if event_log_plugin is not None:
64
84
  interceptors.append(ExecEventLogInterceptor(event_log_plugin))
@@ -0,0 +1,82 @@
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 Exec API license interceptor."""
16
+
17
+
18
+ from collections.abc import Iterator
19
+ from typing import Any, Callable, Union
20
+
21
+ import grpc
22
+ from google.protobuf.message import Message as GrpcMessage
23
+
24
+ from flwr.supercore.license_plugin import LicensePlugin
25
+
26
+
27
+ class ExecLicenseInterceptor(grpc.ServerInterceptor): # type: ignore
28
+ """Exec API interceptor for license checking."""
29
+
30
+ def __init__(self, license_plugin: LicensePlugin) -> None:
31
+ """Initialize the interceptor with a license plugin."""
32
+ self.license_plugin = license_plugin
33
+
34
+ def intercept_service(
35
+ self,
36
+ continuation: Callable[[Any], Any],
37
+ handler_call_details: grpc.HandlerCallDetails,
38
+ ) -> grpc.RpcMethodHandler:
39
+ """Flower server interceptor license logic.
40
+
41
+ Intercept all unary-unary/unary-stream calls from users and check the license.
42
+ Continue RPC call if license check is enabled and passes, else, terminate RPC
43
+ call by setting context to abort.
44
+ """
45
+ # Only apply to Exec service
46
+ if not handler_call_details.method.startswith("/flwr.proto.Exec/"):
47
+ return continuation(handler_call_details)
48
+
49
+ # One of the method handlers in
50
+ # `flwr.superexec.exec_servicer.ExecServicer`
51
+ method_handler: grpc.RpcMethodHandler = continuation(handler_call_details)
52
+ return self._generic_license_unary_method_handler(method_handler)
53
+
54
+ def _generic_license_unary_method_handler(
55
+ self, method_handler: grpc.RpcMethodHandler
56
+ ) -> grpc.RpcMethodHandler:
57
+ def _generic_method_handler(
58
+ request: GrpcMessage,
59
+ context: grpc.ServicerContext,
60
+ ) -> Union[GrpcMessage, Iterator[GrpcMessage]]:
61
+ """Handle the method call with license checking."""
62
+ call = method_handler.unary_unary or method_handler.unary_stream
63
+
64
+ if not self.license_plugin.check_license():
65
+ context.abort(
66
+ grpc.StatusCode.PERMISSION_DENIED,
67
+ "❗️ License check failed. Please contact the SuperLink "
68
+ "administrator.",
69
+ )
70
+ raise grpc.RpcError()
71
+
72
+ return call(request, context) # type: ignore
73
+
74
+ if method_handler.unary_unary:
75
+ message_handler = grpc.unary_unary_rpc_method_handler
76
+ else:
77
+ message_handler = grpc.unary_stream_rpc_method_handler
78
+ return message_handler(
79
+ _generic_method_handler,
80
+ request_deserializer=method_handler.request_deserializer,
81
+ response_serializer=method_handler.response_serializer,
82
+ )