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
@@ -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
+ )
@@ -25,6 +25,7 @@ import grpc
25
25
  from flwr.common import now
26
26
  from flwr.common.auth_plugin import ExecAuthPlugin
27
27
  from flwr.common.constant import (
28
+ FAB_MAX_SIZE,
28
29
  LOG_STREAM_INTERVAL,
29
30
  RUN_ID_NOT_FOUND_MESSAGE,
30
31
  Status,
@@ -52,8 +53,8 @@ from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
52
53
  StreamLogsRequest,
53
54
  StreamLogsResponse,
54
55
  )
55
- from flwr.server.superlink.ffs.ffs_factory import FfsFactory
56
56
  from flwr.server.superlink.linkstate import LinkState, LinkStateFactory
57
+ from flwr.supercore.ffs import FfsFactory
57
58
  from flwr.supercore.object_store import ObjectStore, ObjectStoreFactory
58
59
 
59
60
  from .exec_user_auth_interceptor import shared_account_info
@@ -84,6 +85,14 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
84
85
  """Create run ID."""
85
86
  log(INFO, "ExecServicer.StartRun")
86
87
 
88
+ if len(request.fab.content) > FAB_MAX_SIZE:
89
+ log(
90
+ ERROR,
91
+ "FAB size exceeds maximum allowed size of %d bytes.",
92
+ FAB_MAX_SIZE,
93
+ )
94
+ return StartRunResponse()
95
+
87
96
  flwr_aid = shared_account_info.get().flwr_aid if self.auth_plugin else None
88
97
  run_id = self.executor.start_run(
89
98
  request.fab.content,
@@ -72,6 +72,10 @@ class ExecUserAuthInterceptor(grpc.ServerInterceptor): # type: ignore
72
72
  by validating auth metadata sent by the user. Continue RPC call if user is
73
73
  authenticated, else, terminate RPC call by setting context to abort.
74
74
  """
75
+ # Only apply to Exec service
76
+ if not handler_call_details.method.startswith("/flwr.proto.Exec/"):
77
+ return continuation(handler_call_details)
78
+
75
79
  # One of the method handlers in
76
80
  # `flwr.superexec.exec_servicer.ExecServicer`
77
81
  method_handler: grpc.RpcMethodHandler = continuation(handler_call_details)
@@ -108,7 +112,9 @@ class ExecUserAuthInterceptor(grpc.ServerInterceptor): # type: ignore
108
112
  # Check if the user is authorized
109
113
  if not self.authz_plugin.verify_user_authorization(account_info):
110
114
  context.abort(
111
- grpc.StatusCode.PERMISSION_DENIED, "User not authorized"
115
+ grpc.StatusCode.PERMISSION_DENIED,
116
+ "❗️ User not authorized. "
117
+ "Please contact the SuperLink administrator.",
112
118
  )
113
119
  raise grpc.RpcError()
114
120
  return call(request, context) # type: ignore
@@ -127,7 +133,9 @@ class ExecUserAuthInterceptor(grpc.ServerInterceptor): # type: ignore
127
133
  # Check if the user is authorized
128
134
  if not self.authz_plugin.verify_user_authorization(account_info):
129
135
  context.abort(
130
- grpc.StatusCode.PERMISSION_DENIED, "User not authorized"
136
+ grpc.StatusCode.PERMISSION_DENIED,
137
+ "❗️ User not authorized. "
138
+ "Please contact the SuperLink administrator.",
131
139
  )
132
140
  raise grpc.RpcError()
133
141
 
@@ -22,8 +22,8 @@ from typing import Optional
22
22
 
23
23
  from flwr.common import ConfigRecord
24
24
  from flwr.common.typing import UserConfig
25
- from flwr.server.superlink.ffs.ffs_factory import FfsFactory
26
25
  from flwr.server.superlink.linkstate import LinkStateFactory
26
+ from flwr.supercore.ffs import FfsFactory
27
27
 
28
28
 
29
29
  @dataclass
@@ -25,9 +25,8 @@ from flwr.cli.config_utils import get_fab_metadata
25
25
  from flwr.common import ConfigRecord, Context, RecordDict
26
26
  from flwr.common.logger import log
27
27
  from flwr.common.typing import Fab, UserConfig
28
- from flwr.server.superlink.ffs import Ffs
29
- from flwr.server.superlink.ffs.ffs_factory import FfsFactory
30
28
  from flwr.server.superlink.linkstate import LinkState, LinkStateFactory
29
+ from flwr.supercore.ffs import Ffs, FfsFactory
31
30
 
32
31
  from .executor import Executor
33
32
 
@@ -40,7 +40,6 @@ from flwr.common.constant import (
40
40
  TRANSPORT_TYPE_REST,
41
41
  )
42
42
  from flwr.common.exit import ExitCode, flwr_exit
43
- from flwr.common.exit_handlers import register_exit_handlers
44
43
  from flwr.common.logger import log
45
44
  from flwr.supernode.start_client_internal import start_client_internal
46
45
 
@@ -66,12 +65,6 @@ def flower_supernode() -> None:
66
65
 
67
66
  log(DEBUG, "Isolation mode: %s", args.isolation)
68
67
 
69
- # Register handlers for graceful shutdown
70
- register_exit_handlers(
71
- event_type=EventType.RUN_SUPERNODE_LEAVE,
72
- exit_message="SuperNode terminated gracefully.",
73
- )
74
-
75
68
  start_client_internal(
76
69
  server_address=args.superlink,
77
70
  transport=args.transport,
@@ -22,6 +22,7 @@ from flwr.common.args import add_args_flwr_app_common
22
22
  from flwr.common.constant import CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS
23
23
  from flwr.common.exit import ExitCode, flwr_exit
24
24
  from flwr.common.logger import log
25
+ from flwr.supercore.utils import mask_string
25
26
  from flwr.supernode.runtime.run_clientapp import run_clientapp
26
27
 
27
28
 
@@ -40,11 +41,11 @@ def flwr_clientapp() -> None:
40
41
  "`flwr-clientapp` will attempt to connect to SuperNode's "
41
42
  "ClientAppIo API at %s with token %s",
42
43
  args.clientappio_api_address,
43
- args.token,
44
+ mask_string(args.token) if args.token else "None",
44
45
  )
45
46
  run_clientapp(
46
47
  clientappio_api_address=args.clientappio_api_address,
47
- run_once=(args.token is not None),
48
+ run_once=(args.token is not None) or args.run_once,
48
49
  token=args.token,
49
50
  flwr_dir=args.flwr_dir,
50
51
  certificates=None,
@@ -66,7 +67,7 @@ def _parse_args_run_flwr_clientapp() -> argparse.ArgumentParser:
66
67
  )
67
68
  parser.add_argument(
68
69
  "--token",
69
- type=int,
70
+ type=str,
70
71
  required=False,
71
72
  help="Unique token generated by SuperNode for each ClientApp execution",
72
73
  )
@@ -77,5 +78,11 @@ def _parse_args_run_flwr_clientapp() -> argparse.ArgumentParser:
77
78
  help="The PID of the parent process. When set, the process will terminate "
78
79
  "when the parent process exits.",
79
80
  )
81
+ parser.add_argument(
82
+ "--run-once",
83
+ action="store_true",
84
+ help="When set, this process will start a single ClientApp for a pending "
85
+ "message. If there is no pending message, the process will exit.",
86
+ )
80
87
  add_args_flwr_app_common(parser=parser)
81
88
  return parser
@@ -51,8 +51,9 @@ class InMemoryNodeState(NodeState): # pylint: disable=too-many-instance-attribu
51
51
  # Store run ID to Context mapping
52
52
  self.ctx_store: dict[int, Context] = {}
53
53
  self.lock_ctx_store = Lock()
54
- # Store run ID to token mapping
54
+ # Store run ID to token mapping and token to run ID mapping
55
55
  self.token_store: dict[int, str] = {}
56
+ self.token_to_run_id: dict[str, int] = {}
56
57
  self.lock_token_store = Lock()
57
58
 
58
59
  def set_node_id(self, node_id: Optional[int]) -> None:
@@ -177,6 +178,7 @@ class InMemoryNodeState(NodeState): # pylint: disable=too-many-instance-attribu
177
178
  if run_id in self.token_store:
178
179
  raise ValueError("Token already created for this run ID")
179
180
  self.token_store[run_id] = token
181
+ self.token_to_run_id[token] = run_id
180
182
  return token
181
183
 
182
184
  def verify_token(self, run_id: int, token: str) -> bool:
@@ -187,4 +189,11 @@ class InMemoryNodeState(NodeState): # pylint: disable=too-many-instance-attribu
187
189
  def delete_token(self, run_id: int) -> None:
188
190
  """Delete the token for the given run ID."""
189
191
  with self.lock_token_store:
190
- self.token_store.pop(run_id, None)
192
+ token = self.token_store.pop(run_id, None)
193
+ if token is not None:
194
+ self.token_to_run_id.pop(token, None)
195
+
196
+ def get_run_id_by_token(self, token: str) -> Optional[int]:
197
+ """Get the run ID associated with a given token."""
198
+ with self.lock_token_store:
199
+ return self.token_to_run_id.get(token)
@@ -210,3 +210,18 @@ class NodeState(ABC):
210
210
  run_id : int
211
211
  The ID of the run for which to delete the token.
212
212
  """
213
+
214
+ @abstractmethod
215
+ def get_run_id_by_token(self, token: str) -> Optional[int]:
216
+ """Get the run ID associated with a given token.
217
+
218
+ Parameters
219
+ ----------
220
+ token : str
221
+ The token to look up.
222
+
223
+ Returns
224
+ -------
225
+ Optional[int]
226
+ The run ID if the token is valid, otherwise None.
227
+ """
@@ -32,34 +32,54 @@ from flwr.common import Context, Message
32
32
  from flwr.common.config import get_flwr_dir
33
33
  from flwr.common.constant import ErrorCode
34
34
  from flwr.common.grpc import create_channel, on_channel_state_change
35
+ from flwr.common.inflatable import (
36
+ get_all_nested_objects,
37
+ get_object_tree,
38
+ no_object_id_recompute,
39
+ )
40
+ from flwr.common.inflatable_protobuf_utils import (
41
+ make_confirm_message_received_fn_protobuf,
42
+ make_pull_object_fn_protobuf,
43
+ make_push_object_fn_protobuf,
44
+ )
45
+ from flwr.common.inflatable_utils import pull_and_inflate_object_from_tree, push_objects
35
46
  from flwr.common.logger import log
47
+ from flwr.common.message import remove_content_from_message
36
48
  from flwr.common.retry_invoker import _make_simple_grpc_retry_invoker, _wrap_stub
37
49
  from flwr.common.serde import (
38
50
  context_from_proto,
39
51
  context_to_proto,
40
52
  fab_from_proto,
41
- message_from_proto,
42
53
  message_to_proto,
43
54
  run_from_proto,
44
55
  )
45
56
  from flwr.common.typing import Fab, Run
57
+ from flwr.proto.appio_pb2 import ( # pylint: disable=E0611
58
+ PullAppInputsRequest,
59
+ PullAppInputsResponse,
60
+ PullAppMessagesRequest,
61
+ PullAppMessagesResponse,
62
+ PushAppMessagesRequest,
63
+ PushAppOutputsRequest,
64
+ PushAppOutputsResponse,
65
+ )
46
66
 
47
67
  # pylint: disable=E0611
48
68
  from flwr.proto.clientappio_pb2 import (
49
- GetTokenRequest,
50
- GetTokenResponse,
51
- PullClientAppInputsRequest,
52
- PullClientAppInputsResponse,
53
- PushClientAppOutputsRequest,
54
- PushClientAppOutputsResponse,
69
+ GetRunIdsWithPendingMessagesRequest,
70
+ GetRunIdsWithPendingMessagesResponse,
71
+ RequestTokenRequest,
72
+ RequestTokenResponse,
55
73
  )
56
74
  from flwr.proto.clientappio_pb2_grpc import ClientAppIoStub
75
+ from flwr.proto.node_pb2 import Node # pylint: disable=E0611
76
+ from flwr.supercore.utils import mask_string
57
77
 
58
78
 
59
79
  def run_clientapp( # pylint: disable=R0913, R0914, R0917
60
80
  clientappio_api_address: str,
61
81
  run_once: bool,
62
- token: Optional[int] = None,
82
+ token: Optional[str] = None,
63
83
  flwr_dir: Optional[str] = None,
64
84
  certificates: Optional[bytes] = None,
65
85
  parent_pid: Optional[int] = None,
@@ -84,9 +104,8 @@ def run_clientapp( # pylint: disable=R0913, R0914, R0917
84
104
 
85
105
  while True:
86
106
  # If token is not set, loop until token is received from SuperNode
87
- while token is None:
107
+ if token is None:
88
108
  token = get_token(stub)
89
- time.sleep(1)
90
109
 
91
110
  # Pull Message, Context, Run and (optional) FAB from SuperNode
92
111
  message, context, run, fab = pull_clientappinputs(stub=stub, token=token)
@@ -172,34 +191,58 @@ def start_parent_process_monitor(
172
191
  threading.Thread(target=monitor, daemon=True).start()
173
192
 
174
193
 
175
- def get_token(stub: grpc.Channel) -> Optional[int]:
194
+ def get_token(stub: ClientAppIoStub) -> str:
176
195
  """Get a token from SuperNode."""
177
196
  log(DEBUG, "[flwr-clientapp] Request token")
178
- try:
179
- res: GetTokenResponse = stub.GetToken(GetTokenRequest())
180
- log(DEBUG, "[GetToken] Received token: %s", res.token)
181
- return res.token
182
- except grpc.RpcError as e:
183
- if e.code() == grpc.StatusCode.FAILED_PRECONDITION: # pylint: disable=no-member
184
- log(DEBUG, "[GetToken] No token available yet")
185
- else:
186
- log(ERROR, "[GetToken] gRPC error occurred: %s", str(e))
187
- return None
197
+ while True:
198
+ res: GetRunIdsWithPendingMessagesResponse = stub.GetRunIdsWithPendingMessages(
199
+ GetRunIdsWithPendingMessagesRequest()
200
+ )
201
+
202
+ for run_id in res.run_ids:
203
+ tk_res: RequestTokenResponse = stub.RequestToken(
204
+ RequestTokenRequest(run_id=run_id)
205
+ )
206
+ if tk_res.token:
207
+ return tk_res.token
208
+
209
+ time.sleep(1) # Wait before retrying to get run IDs
188
210
 
189
211
 
190
212
  def pull_clientappinputs(
191
- stub: grpc.Channel, token: int
213
+ stub: ClientAppIoStub, token: str
192
214
  ) -> tuple[Message, Context, Run, Optional[Fab]]:
193
215
  """Pull ClientAppInputs from SuperNode."""
194
- log(INFO, "[flwr-clientapp] Pull `ClientAppInputs` for token %s", token)
216
+ masked_token = mask_string(token)
217
+ log(INFO, "[flwr-clientapp] Pull `ClientAppInputs` for token %s", masked_token)
195
218
  try:
196
- res: PullClientAppInputsResponse = stub.PullClientAppInputs(
197
- PullClientAppInputsRequest(token=token)
219
+ # Pull Context, Run and (optional) FAB
220
+ res: PullAppInputsResponse = stub.PullClientAppInputs(
221
+ PullAppInputsRequest(token=token)
198
222
  )
199
- message = message_from_proto(res.message)
200
223
  context = context_from_proto(res.context)
201
224
  run = run_from_proto(res.run)
202
225
  fab = fab_from_proto(res.fab) if res.fab else None
226
+
227
+ # Pull and inflate the message
228
+ pull_msg_res: PullAppMessagesResponse = stub.PullMessage(
229
+ PullAppMessagesRequest(token=token)
230
+ )
231
+ run_id = context.run_id
232
+ node = Node(node_id=context.node_id)
233
+ object_tree = pull_msg_res.message_object_trees[0]
234
+ message = pull_and_inflate_object_from_tree(
235
+ object_tree,
236
+ make_pull_object_fn_protobuf(stub.PullObject, node, run_id),
237
+ make_confirm_message_received_fn_protobuf(
238
+ stub.ConfirmMessageReceived, node, run_id
239
+ ),
240
+ return_type=Message,
241
+ )
242
+
243
+ # Set the message ID
244
+ # The deflated message doesn't contain the message_id (its own object_id)
245
+ message.metadata.__dict__["_message_id"] = object_tree.object_id
203
246
  return message, context, run, fab
204
247
  except grpc.RpcError as e:
205
248
  log(ERROR, "[PullClientAppInputs] gRPC error occurred: %s", str(e))
@@ -207,18 +250,52 @@ def pull_clientappinputs(
207
250
 
208
251
 
209
252
  def push_clientappoutputs(
210
- stub: grpc.Channel, token: int, message: Message, context: Context
211
- ) -> PushClientAppOutputsResponse:
253
+ stub: ClientAppIoStub, token: str, message: Message, context: Context
254
+ ) -> PushAppOutputsResponse:
212
255
  """Push ClientAppOutputs to SuperNode."""
213
- log(INFO, "[flwr-clientapp] Push `ClientAppOutputs` for token %s", token)
214
- proto_message = message_to_proto(message)
256
+ masked_token = mask_string(token)
257
+ log(INFO, "[flwr-clientapp] Push `ClientAppOutputs` for token %s", masked_token)
258
+ # Set message ID
259
+ message.metadata.__dict__["_message_id"] = message.object_id
260
+ proto_message = message_to_proto(remove_content_from_message(message))
215
261
  proto_context = context_to_proto(context)
216
262
 
217
263
  try:
218
- res: PushClientAppOutputsResponse = stub.PushClientAppOutputs(
219
- PushClientAppOutputsRequest(
220
- token=token, message=proto_message, context=proto_context
264
+
265
+ with no_object_id_recompute():
266
+ # Get object tree and all objects to push
267
+ object_tree = get_object_tree(message)
268
+
269
+ # Push Message
270
+ # This is temporary. The message should not contain its content
271
+ push_msg_res = stub.PushMessage(
272
+ PushAppMessagesRequest(
273
+ token=token,
274
+ messages_list=[proto_message],
275
+ message_object_trees=[object_tree],
276
+ )
221
277
  )
278
+ del proto_message
279
+
280
+ # Retrieve the object IDs to push
281
+ object_ids_to_push = set(push_msg_res.objects_to_push)
282
+
283
+ # Push all objects
284
+ all_objects = get_all_nested_objects(message)
285
+ del message
286
+ push_objects(
287
+ all_objects,
288
+ make_push_object_fn_protobuf(
289
+ stub.PushObject,
290
+ Node(node_id=context.node_id),
291
+ run_id=context.run_id,
292
+ ),
293
+ object_ids_to_push=object_ids_to_push,
294
+ )
295
+
296
+ # Push Context
297
+ res: PushAppOutputsResponse = stub.PushClientAppOutputs(
298
+ PushAppOutputsRequest(token=token, context=proto_context)
222
299
  )
223
300
  return res
224
301
  except grpc.RpcError as e:
@@ -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 ClientApp Scheduler."""
16
+
17
+
18
+ from .simple_clientapp_scheduler_plugin import SimpleClientAppSchedulerPlugin
19
+
20
+ __all__ = [
21
+ "SimpleClientAppSchedulerPlugin",
22
+ ]
@@ -0,0 +1,49 @@
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 ClientApp Scheduler plugin."""
16
+
17
+
18
+ import os
19
+ import subprocess
20
+ from collections.abc import Sequence
21
+ from typing import Optional
22
+
23
+ from flwr.supercore.scheduler import SchedulerPlugin
24
+
25
+
26
+ class SimpleClientAppSchedulerPlugin(SchedulerPlugin):
27
+ """Simple Flower ClientApp Scheduler plugin.
28
+
29
+ The plugin always selects the first candidate run ID.
30
+ """
31
+
32
+ def select_run_id(self, candidate_run_ids: Sequence[int]) -> Optional[int]:
33
+ """Select a run ID to execute from a sequence of candidates."""
34
+ if not candidate_run_ids:
35
+ return None
36
+ return candidate_run_ids[0]
37
+
38
+ def launch_app(self, token: str, run_id: int) -> None:
39
+ """Launch the application associated with a given run ID and token."""
40
+ cmds = ["flwr-clientapp", "--insecure"]
41
+ cmds += ["--clientappio-api-address", self.appio_api_address]
42
+ cmds += ["--token", token]
43
+ cmds += ["--parent-pid", str(os.getpid())]
44
+ cmds += ["--flwr-dir", self.flwr_dir]
45
+ # Launch the client app without waiting for it to complete.
46
+ # Since we don't need to manage the process, we intentionally avoid using
47
+ # a `with` statement. Suppress the pylint warning for it in this case.
48
+ # pylint: disable-next=consider-using-with
49
+ subprocess.Popen(cmds)
@@ -15,10 +15,8 @@
15
15
  """ClientAppIo API Servicer."""
16
16
 
17
17
 
18
- from .clientappio_servicer import ClientAppInputs, ClientAppIoServicer, ClientAppOutputs
18
+ from .clientappio_servicer import ClientAppIoServicer
19
19
 
20
20
  __all__ = [
21
- "ClientAppInputs",
22
21
  "ClientAppIoServicer",
23
- "ClientAppOutputs",
24
22
  ]