flwr-nightly 1.20.0.dev20250722__py3-none-any.whl → 1.20.0.dev20250724__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.
@@ -31,6 +31,7 @@ class ExitCode:
31
31
  SUPERLINK_THREAD_CRASH = 100
32
32
  SUPERLINK_LICENSE_INVALID = 101
33
33
  SUPERLINK_LICENSE_MISSING = 102
34
+ SUPERLINK_LICENSE_URL_INVALID = 103
34
35
 
35
36
  # ServerApp-specific exit codes (200-299)
36
37
 
@@ -70,6 +71,10 @@ EXIT_CODE_HELP = {
70
71
  "The license is missing. Please specify the license key by setting the "
71
72
  "environment variable `FLWR_LICENSE_KEY`."
72
73
  ),
74
+ ExitCode.SUPERLINK_LICENSE_URL_INVALID: (
75
+ "The license URL is invalid. Please ensure that the `FLWR_LICENSE_URL` "
76
+ "environment variable is set to a valid URL."
77
+ ),
73
78
  # ServerApp-specific exit codes (200-299)
74
79
  # SuperNode-specific exit codes (300-399)
75
80
  ExitCode.SUPERNODE_REST_ADDRESS_INVALID: (
@@ -18,7 +18,7 @@ from flwr.proto import message_pb2 as flwr_dot_proto_dot_message__pb2
18
18
  from flwr.proto import appio_pb2 as flwr_dot_proto_dot_appio__pb2
19
19
 
20
20
 
21
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x66lwr/proto/clientappio.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x18\x66lwr/proto/message.proto\x1a\x16\x66lwr/proto/appio.proto\"%\n#GetRunIdsWithPendingMessagesRequest\"7\n$GetRunIdsWithPendingMessagesResponse\x12\x0f\n\x07run_ids\x18\x01 \x03(\x04\"%\n\x13RequestTokenRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"%\n\x14RequestTokenResponse\x12\r\n\x05token\x18\x01 \x01(\t2\xec\x06\n\x0b\x43lientAppIo\x12\x83\x01\n\x1cGetRunIdsWithPendingMessages\x12/.flwr.proto.GetRunIdsWithPendingMessagesRequest\x1a\x30.flwr.proto.GetRunIdsWithPendingMessagesResponse\"\x00\x12S\n\x0cRequestToken\x12\x1f.flwr.proto.RequestTokenRequest\x1a .flwr.proto.RequestTokenResponse\"\x00\x12\\\n\x13PullClientAppInputs\x12 .flwr.proto.PullAppInputsRequest\x1a!.flwr.proto.PullAppInputsResponse\"\x00\x12_\n\x14PushClientAppOutputs\x12!.flwr.proto.PushAppOutputsRequest\x1a\".flwr.proto.PushAppOutputsResponse\"\x00\x12X\n\x0bPushMessage\x12\".flwr.proto.PushAppMessagesRequest\x1a#.flwr.proto.PushAppMessagesResponse\"\x00\x12X\n\x0bPullMessage\x12\".flwr.proto.PullAppMessagesRequest\x1a#.flwr.proto.PullAppMessagesResponse\"\x00\x12M\n\nPushObject\x12\x1d.flwr.proto.PushObjectRequest\x1a\x1e.flwr.proto.PushObjectResponse\"\x00\x12M\n\nPullObject\x12\x1d.flwr.proto.PullObjectRequest\x1a\x1e.flwr.proto.PullObjectResponse\"\x00\x12q\n\x16\x43onfirmMessageReceived\x12).flwr.proto.ConfirmMessageReceivedRequest\x1a*.flwr.proto.ConfirmMessageReceivedResponse\"\x00\x62\x06proto3')
21
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x66lwr/proto/clientappio.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x18\x66lwr/proto/message.proto\x1a\x16\x66lwr/proto/appio.proto\"%\n#GetRunIdsWithPendingMessagesRequest\"7\n$GetRunIdsWithPendingMessagesResponse\x12\x0f\n\x07run_ids\x18\x01 \x03(\x04\"%\n\x13RequestTokenRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"%\n\x14RequestTokenResponse\x12\r\n\x05token\x18\x01 \x01(\t2\xaf\x07\n\x0b\x43lientAppIo\x12\x83\x01\n\x1cGetRunIdsWithPendingMessages\x12/.flwr.proto.GetRunIdsWithPendingMessagesRequest\x1a\x30.flwr.proto.GetRunIdsWithPendingMessagesResponse\"\x00\x12S\n\x0cRequestToken\x12\x1f.flwr.proto.RequestTokenRequest\x1a .flwr.proto.RequestTokenResponse\"\x00\x12\x41\n\x06GetRun\x12\x19.flwr.proto.GetRunRequest\x1a\x1a.flwr.proto.GetRunResponse\"\x00\x12\\\n\x13PullClientAppInputs\x12 .flwr.proto.PullAppInputsRequest\x1a!.flwr.proto.PullAppInputsResponse\"\x00\x12_\n\x14PushClientAppOutputs\x12!.flwr.proto.PushAppOutputsRequest\x1a\".flwr.proto.PushAppOutputsResponse\"\x00\x12X\n\x0bPushMessage\x12\".flwr.proto.PushAppMessagesRequest\x1a#.flwr.proto.PushAppMessagesResponse\"\x00\x12X\n\x0bPullMessage\x12\".flwr.proto.PullAppMessagesRequest\x1a#.flwr.proto.PullAppMessagesResponse\"\x00\x12M\n\nPushObject\x12\x1d.flwr.proto.PushObjectRequest\x1a\x1e.flwr.proto.PushObjectResponse\"\x00\x12M\n\nPullObject\x12\x1d.flwr.proto.PullObjectRequest\x1a\x1e.flwr.proto.PullObjectResponse\"\x00\x12q\n\x16\x43onfirmMessageReceived\x12).flwr.proto.ConfirmMessageReceivedRequest\x1a*.flwr.proto.ConfirmMessageReceivedResponse\"\x00\x62\x06proto3')
22
22
 
23
23
  _globals = globals()
24
24
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -34,5 +34,5 @@ if _descriptor._USE_C_DESCRIPTORS == False:
34
34
  _globals['_REQUESTTOKENRESPONSE']._serialized_start=273
35
35
  _globals['_REQUESTTOKENRESPONSE']._serialized_end=310
36
36
  _globals['_CLIENTAPPIO']._serialized_start=313
37
- _globals['_CLIENTAPPIO']._serialized_end=1189
37
+ _globals['_CLIENTAPPIO']._serialized_end=1256
38
38
  # @@protoc_insertion_point(module_scope)
@@ -5,6 +5,7 @@ import grpc
5
5
  from flwr.proto import appio_pb2 as flwr_dot_proto_dot_appio__pb2
6
6
  from flwr.proto import clientappio_pb2 as flwr_dot_proto_dot_clientappio__pb2
7
7
  from flwr.proto import message_pb2 as flwr_dot_proto_dot_message__pb2
8
+ from flwr.proto import run_pb2 as flwr_dot_proto_dot_run__pb2
8
9
 
9
10
 
10
11
  class ClientAppIoStub(object):
@@ -26,6 +27,11 @@ class ClientAppIoStub(object):
26
27
  request_serializer=flwr_dot_proto_dot_clientappio__pb2.RequestTokenRequest.SerializeToString,
27
28
  response_deserializer=flwr_dot_proto_dot_clientappio__pb2.RequestTokenResponse.FromString,
28
29
  )
30
+ self.GetRun = channel.unary_unary(
31
+ '/flwr.proto.ClientAppIo/GetRun',
32
+ request_serializer=flwr_dot_proto_dot_run__pb2.GetRunRequest.SerializeToString,
33
+ response_deserializer=flwr_dot_proto_dot_run__pb2.GetRunResponse.FromString,
34
+ )
29
35
  self.PullClientAppInputs = channel.unary_unary(
30
36
  '/flwr.proto.ClientAppIo/PullClientAppInputs',
31
37
  request_serializer=flwr_dot_proto_dot_appio__pb2.PullAppInputsRequest.SerializeToString,
@@ -80,6 +86,13 @@ class ClientAppIoServicer(object):
80
86
  context.set_details('Method not implemented!')
81
87
  raise NotImplementedError('Method not implemented!')
82
88
 
89
+ def GetRun(self, request, context):
90
+ """Get run details
91
+ """
92
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
93
+ context.set_details('Method not implemented!')
94
+ raise NotImplementedError('Method not implemented!')
95
+
83
96
  def PullClientAppInputs(self, request, context):
84
97
  """Pull client app inputs
85
98
  """
@@ -142,6 +155,11 @@ def add_ClientAppIoServicer_to_server(servicer, server):
142
155
  request_deserializer=flwr_dot_proto_dot_clientappio__pb2.RequestTokenRequest.FromString,
143
156
  response_serializer=flwr_dot_proto_dot_clientappio__pb2.RequestTokenResponse.SerializeToString,
144
157
  ),
158
+ 'GetRun': grpc.unary_unary_rpc_method_handler(
159
+ servicer.GetRun,
160
+ request_deserializer=flwr_dot_proto_dot_run__pb2.GetRunRequest.FromString,
161
+ response_serializer=flwr_dot_proto_dot_run__pb2.GetRunResponse.SerializeToString,
162
+ ),
145
163
  'PullClientAppInputs': grpc.unary_unary_rpc_method_handler(
146
164
  servicer.PullClientAppInputs,
147
165
  request_deserializer=flwr_dot_proto_dot_appio__pb2.PullAppInputsRequest.FromString,
@@ -221,6 +239,23 @@ class ClientAppIo(object):
221
239
  options, channel_credentials,
222
240
  insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
223
241
 
242
+ @staticmethod
243
+ def GetRun(request,
244
+ target,
245
+ options=(),
246
+ channel_credentials=None,
247
+ call_credentials=None,
248
+ insecure=False,
249
+ compression=None,
250
+ wait_for_ready=None,
251
+ timeout=None,
252
+ metadata=None):
253
+ return grpc.experimental.unary_unary(request, target, '/flwr.proto.ClientAppIo/GetRun',
254
+ flwr_dot_proto_dot_run__pb2.GetRunRequest.SerializeToString,
255
+ flwr_dot_proto_dot_run__pb2.GetRunResponse.FromString,
256
+ options, channel_credentials,
257
+ insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
258
+
224
259
  @staticmethod
225
260
  def PullClientAppInputs(request,
226
261
  target,
@@ -6,6 +6,7 @@ import abc
6
6
  import flwr.proto.appio_pb2
7
7
  import flwr.proto.clientappio_pb2
8
8
  import flwr.proto.message_pb2
9
+ import flwr.proto.run_pb2
9
10
  import grpc
10
11
 
11
12
  class ClientAppIoStub:
@@ -20,6 +21,11 @@ class ClientAppIoStub:
20
21
  flwr.proto.clientappio_pb2.RequestTokenResponse]
21
22
  """Request token"""
22
23
 
24
+ GetRun: grpc.UnaryUnaryMultiCallable[
25
+ flwr.proto.run_pb2.GetRunRequest,
26
+ flwr.proto.run_pb2.GetRunResponse]
27
+ """Get run details"""
28
+
23
29
  PullClientAppInputs: grpc.UnaryUnaryMultiCallable[
24
30
  flwr.proto.appio_pb2.PullAppInputsRequest,
25
31
  flwr.proto.appio_pb2.PullAppInputsResponse]
@@ -73,6 +79,14 @@ class ClientAppIoServicer(metaclass=abc.ABCMeta):
73
79
  """Request token"""
74
80
  pass
75
81
 
82
+ @abc.abstractmethod
83
+ def GetRun(self,
84
+ request: flwr.proto.run_pb2.GetRunRequest,
85
+ context: grpc.ServicerContext,
86
+ ) -> flwr.proto.run_pb2.GetRunResponse:
87
+ """Get run details"""
88
+ pass
89
+
76
90
  @abc.abstractmethod
77
91
  def PullClientAppInputs(self,
78
92
  request: flwr.proto.appio_pb2.PullAppInputsRequest,
@@ -22,10 +22,13 @@ from typing import Optional, cast
22
22
 
23
23
  import grpc
24
24
 
25
- from flwr.common import Message, RecordDict
25
+ from flwr.app.error import Error
26
+ from flwr.common import Message, Metadata, RecordDict, now
26
27
  from flwr.common.constant import (
27
28
  SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS,
28
29
  SUPERLINK_NODE_ID,
30
+ ErrorCode,
31
+ MessageType,
29
32
  )
30
33
  from flwr.common.grpc import create_channel, on_channel_state_change
31
34
  from flwr.common.inflatable import (
@@ -39,12 +42,13 @@ from flwr.common.inflatable_protobuf_utils import (
39
42
  make_push_object_fn_protobuf,
40
43
  )
41
44
  from flwr.common.inflatable_utils import (
45
+ ObjectUnavailableError,
42
46
  inflate_object_from_contents,
43
47
  pull_objects,
44
48
  push_objects,
45
49
  )
46
50
  from flwr.common.logger import log, warn_deprecated_feature
47
- from flwr.common.message import remove_content_from_message
51
+ from flwr.common.message import make_message, remove_content_from_message
48
52
  from flwr.common.retry_invoker import _make_simple_grpc_retry_invoker, _wrap_stub
49
53
  from flwr.common.serde import message_to_proto, run_from_proto
50
54
  from flwr.common.typing import Run
@@ -260,15 +264,15 @@ class GrpcGrid(Grid):
260
264
  run_id = cast(Run, self._run).run_id
261
265
  message_ids: list[str] = []
262
266
  try:
263
- for msg in messages:
264
- # Populate metadata
265
- msg.metadata.__dict__["_run_id"] = run_id
266
- msg.metadata.__dict__["_src_node_id"] = self.node.node_id
267
- msg.metadata.__dict__["_message_id"] = msg.object_id
268
- # Check message
269
- self._check_message(msg)
270
- # Try pushing message and its objects
271
- with no_object_id_recompute():
267
+ with no_object_id_recompute():
268
+ for msg in messages:
269
+ # Populate metadata
270
+ msg.metadata.__dict__["_run_id"] = run_id
271
+ msg.metadata.__dict__["_src_node_id"] = self.node.node_id
272
+ msg.metadata.__dict__["_message_id"] = msg.object_id
273
+ # Check message
274
+ self._check_message(msg)
275
+ # Try pushing message and its objects
272
276
  message_ids.append(self._try_push_message(run_id, msg))
273
277
 
274
278
  except grpc.RpcError as e:
@@ -307,16 +311,42 @@ class GrpcGrid(Grid):
307
311
  inflated_msgs: list[Message] = []
308
312
  for msg_proto, msg_tree in zip(res.messages_list, res.message_object_trees):
309
313
  msg_id = msg_proto.metadata.message_id
310
- all_object_contents = pull_objects(
311
- object_ids=[
312
- tree.object_id for tree in iterate_object_tree(msg_tree)
313
- ],
314
- pull_object_fn=make_pull_object_fn_protobuf(
315
- pull_object_protobuf=self._stub.PullObject,
316
- node=self.node,
317
- run_id=run_id,
318
- ),
319
- )
314
+ try:
315
+ all_object_contents = pull_objects(
316
+ object_ids=[
317
+ tree.object_id for tree in iterate_object_tree(msg_tree)
318
+ ],
319
+ pull_object_fn=make_pull_object_fn_protobuf(
320
+ pull_object_protobuf=self._stub.PullObject,
321
+ node=self.node,
322
+ run_id=run_id,
323
+ ),
324
+ )
325
+ except ObjectUnavailableError as e:
326
+ # An ObjectUnavailableError indicates that the object is not yet
327
+ # available. If this point has been reached, it means that the
328
+ # Grid has tried to pull the object for the maximum number of times
329
+ # or for the maximum time allowed, so we return an inflated message
330
+ # with an error
331
+ inflated_msgs.append(
332
+ make_message(
333
+ metadata=Metadata(
334
+ run_id=run_id,
335
+ message_id="",
336
+ src_node_id=self.node.node_id,
337
+ dst_node_id=self.node.node_id,
338
+ message_type=MessageType.SYSTEM,
339
+ group_id="",
340
+ ttl=0,
341
+ reply_to_message_id=msg_proto.metadata.reply_to_message_id,
342
+ created_at=now().timestamp(),
343
+ ),
344
+ error=Error(
345
+ code=ErrorCode.MESSAGE_UNAVAILABLE, reason=(str(e))
346
+ ),
347
+ )
348
+ )
349
+ continue
320
350
 
321
351
  # Confirm that the message has been received
322
352
  self._stub.ConfirmMessageReceived(
@@ -21,9 +21,6 @@ import grpc
21
21
 
22
22
  from flwr.common.constant import Status, SubStatus
23
23
  from flwr.common.typing import RunStatus
24
- from flwr.proto.appio_pb2 import PushAppMessagesRequest # pylint: disable=E0611
25
- from flwr.proto.fleet_pb2 import PushMessagesRequest # pylint: disable=E0611
26
- from flwr.proto.message_pb2 import ObjectIDs # pylint: disable=E0611
27
24
  from flwr.server.superlink.linkstate import LinkState
28
25
  from flwr.supercore.object_store import ObjectStore
29
26
 
@@ -73,27 +70,3 @@ def abort_if(
73
70
  """Abort context if status of the provided `run_id` is in `abort_status_list`."""
74
71
  msg = check_abort(run_id, abort_status_list, state, store)
75
72
  abort_grpc_context(msg, context)
76
-
77
-
78
- def store_mapping_and_register_objects(
79
- store: ObjectStore, request: Union[PushAppMessagesRequest, PushMessagesRequest]
80
- ) -> dict[str, ObjectIDs]:
81
- """Store Message object to descendants mapping and preregister objects."""
82
- if not request.messages_list:
83
- return {}
84
-
85
- objects_to_push: dict[str, ObjectIDs] = {}
86
-
87
- # Get run_id from the first message in the list
88
- # All messages of a request should in the same run
89
- run_id = request.messages_list[0].metadata.run_id
90
-
91
- for object_tree in request.message_object_trees:
92
- # Preregister
93
- object_ids_just_registered = store.preregister(run_id, object_tree)
94
- # Keep track of objects that need to be pushed
95
- objects_to_push[object_tree.object_id] = ObjectIDs(
96
- object_ids=object_ids_just_registered
97
- )
98
-
99
- 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,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)
@@ -59,6 +59,7 @@ from flwr.proto.message_pb2 import (
59
59
  PushObjectRequest,
60
60
  PushObjectResponse,
61
61
  )
62
+ from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611
62
63
 
63
64
  # pylint: disable=E0601
64
65
  from flwr.supercore.ffs import FfsFactory
@@ -118,6 +119,23 @@ class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
118
119
  # Return the token
119
120
  return RequestTokenResponse(token=token)
120
121
 
122
+ def GetRun(
123
+ self, request: GetRunRequest, context: grpc.ServicerContext
124
+ ) -> GetRunResponse:
125
+ """Get run information."""
126
+ log(DEBUG, "ClientAppIo.GetRun")
127
+
128
+ # Initialize state connection
129
+ state = self.state_factory.state()
130
+
131
+ # Retrieve run information
132
+ run = state.get_run(request.run_id)
133
+
134
+ if run is None:
135
+ return GetRunResponse()
136
+
137
+ return GetRunResponse(run=run_to_proto(run))
138
+
121
139
  def PullClientAppInputs(
122
140
  self, request: PullAppInputsRequest, context: grpc.ServicerContext
123
141
  ) -> PullAppInputsResponse:
@@ -237,6 +237,12 @@ def start_client_internal(
237
237
  ]
238
238
  subprocess.run(command, check=False)
239
239
 
240
+ # No message has been pulled therefore we can skip the push stage.
241
+ if run_id is None:
242
+ # If no message was received, wait for a while
243
+ time.sleep(3)
244
+ continue
245
+
240
246
  _push_messages(
241
247
  state=state,
242
248
  object_store=store,
@@ -244,9 +250,6 @@ def start_client_internal(
244
250
  push_object=push_object,
245
251
  )
246
252
 
247
- # Sleep for 3 seconds before the next iteration
248
- time.sleep(3)
249
-
250
253
 
251
254
  def _pull_and_store_message( # pylint: disable=too-many-positional-arguments
252
255
  state: NodeState,
@@ -358,8 +361,10 @@ def _push_messages(
358
361
  push_object: Callable[[int, str, bytes], None],
359
362
  ) -> None:
360
363
  """Push reply messages to the SuperLink."""
361
- # Get messages to send
362
- reply_messages = state.get_messages(is_reply=True)
364
+ # This is to ensure that only one message is processed at a time
365
+ # Wait until a reply message is available
366
+ while not (reply_messages := state.get_messages(is_reply=True)):
367
+ time.sleep(0.5)
363
368
 
364
369
  for message in reply_messages:
365
370
  # Log message sending
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: flwr-nightly
3
- Version: 1.20.0.dev20250722
3
+ Version: 1.20.0.dev20250724
4
4
  Summary: Flower: A Friendly Federated AI Framework
5
5
  License: Apache-2.0
6
6
  Keywords: Artificial Intelligence,Federated AI,Federated Analytics,Federated Evaluation,Federated Learning,Flower,Machine Learning
@@ -118,7 +118,7 @@ flwr/common/event_log_plugin/__init__.py,sha256=ts3VAL3Fk6Grp1EK_1Qg_V-BfOof9F86
118
118
  flwr/common/event_log_plugin/event_log_plugin.py,sha256=eK8OaDFagQRwqpb9eV0cJcm2ErtEBpMxFbhxJNx6n5w,2061
119
119
  flwr/common/exit/__init__.py,sha256=-ZOJYLaNnR729a7VzZiFsLiqngzKQh3xc27svYStZ_Q,826
120
120
  flwr/common/exit/exit.py,sha256=mJgbqMlVlwAgYtq-Vedj53wO4VxcDcy_P-GzqGK-1GQ,3452
121
- flwr/common/exit/exit_code.py,sha256=qpOQsh2-TNJosxrGpR-rKnLBiv5lnl_2sClNoDblAW4,3882
121
+ flwr/common/exit/exit_code.py,sha256=NbjJEywRb5YzDE7Xmo3Kd29inDEQbIUpskY9IEIGomE,4110
122
122
  flwr/common/exit_handlers.py,sha256=IaqJ60fXZuu7McaRYnoYKtlbH9t4Yl9goNExKqtmQbs,4304
123
123
  flwr/common/grpc.py,sha256=y70hUFvXkIf3l03xOhlb7qhS6W1UJZRSZqCdB0ir0v8,10381
124
124
  flwr/common/heartbeat.py,sha256=SyEpNDnmJ0lni0cWO67rcoJVKasCLmkNHm3dKLeNrLU,5749
@@ -168,10 +168,10 @@ flwr/proto/appio_pb2.py,sha256=wCmsFJphNn5VHV2ew6-Wz58OclbsNfiwkwbssX8Ojf0,3853
168
168
  flwr/proto/appio_pb2.pyi,sha256=_75EI4-BtZFW_tQcjLYhULmZPlFh1WJGVpL4vOROljU,7736
169
169
  flwr/proto/appio_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
170
170
  flwr/proto/appio_pb2_grpc.pyi,sha256=ff2TSiLVnG6IVQcTGzb2DIH3XRSoAvAo_RMcvbMFyc0,76
171
- flwr/proto/clientappio_pb2.py,sha256=VHXByon0L0U0768mSbp5ek2iWC6uujowc4sw7IHZHg8,3256
171
+ flwr/proto/clientappio_pb2.py,sha256=3b87t4_AFgeDopgpYhhOVBrdh1HG74W2HC-y0C0q_eY,3349
172
172
  flwr/proto/clientappio_pb2.pyi,sha256=gA-WU5lCYxhMciAUiCg__DrAzHPtytqRLyflpToWHqc,2033
173
- flwr/proto/clientappio_pb2_grpc.py,sha256=4jYgxz3mNl20u3rJuGTe2TkeiLoVSwnRnXCGXhbCxgw,16263
174
- flwr/proto/clientappio_pb2_grpc.pyi,sha256=oa6AwB04qozIdwRLg2K43Zmf9BY10wlcnoJwPAxGHpc,4536
173
+ flwr/proto/clientappio_pb2_grpc.py,sha256=CrB1caDEuPxgjQxDEozLyApzNMqzD8z1pWg310xCTGU,17856
174
+ flwr/proto/clientappio_pb2_grpc.pyi,sha256=FkS3ZcsU1iUuoS7ji84o8L5D9u-e5vsVR7O-ESqD5LI,4939
175
175
  flwr/proto/error_pb2.py,sha256=PQVWrfjVPo88ql_KgV9nCxyQNCcV9PVfmcw7sOzTMro,1084
176
176
  flwr/proto/error_pb2.pyi,sha256=ZNH4HhJTU_KfMXlyCeg8FwU-fcUYxTqEmoJPtWtHikc,734
177
177
  flwr/proto/error_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
@@ -242,7 +242,7 @@ flwr/server/criterion.py,sha256=G4e-6B48Pc7d5rmGVUpIzNKb6UF88O3VmTRuUltgjzM,1061
242
242
  flwr/server/fleet_event_log_interceptor.py,sha256=gtVPr2yQp3U2GnabIdFnuok18VerEcidKj1lKmDYGoY,3950
243
243
  flwr/server/grid/__init__.py,sha256=aWZHezoR2UGMJISB_gPMCm2N_2GSbm97A3lAp7ruhRQ,888
244
244
  flwr/server/grid/grid.py,sha256=naGCYt5J6dnmUvrcGkdNyKPe3MBd-0awGm1ALmgahqY,6625
245
- flwr/server/grid/grpc_grid.py,sha256=2AL1Niz77cOWGX0e7Y9eJFpOK962tDqyn8QQyjSN0qM,13735
245
+ flwr/server/grid/grpc_grid.py,sha256=anUMektTtlAWcvu6wf7dsoiuBF5OiXfKuginHx0RWT0,15292
246
246
  flwr/server/grid/inmemory_grid.py,sha256=RjejYT-d-hHuTs1KSs_5wvOdAWKLus8w5_UAcnGt4iw,6168
247
247
  flwr/server/history.py,sha256=cCkFhBN4GoHsYYNk5GG1Y089eKJh2DH_ZJbYPwLaGyk,5026
248
248
  flwr/server/run_serverapp.py,sha256=v0p6jXj2dFxlRUdoEeF1mnaFd9XRQi6dZCflPY6d3qI,2063
@@ -309,7 +309,7 @@ flwr/server/superlink/serverappio/serverappio_servicer.py,sha256=m0c05XQwRWwymOq
309
309
  flwr/server/superlink/simulation/__init__.py,sha256=Ry8DrNaZCMcQXvUc4FoCN2m3dvUQgWjasfp015o3Ec4,718
310
310
  flwr/server/superlink/simulation/simulationio_grpc.py,sha256=VqWKxjpd4bCgPFKsgtIZPk9YcG0kc1EEmr5k20EKty4,2205
311
311
  flwr/server/superlink/simulation/simulationio_servicer.py,sha256=m1T1zvEn81jlfx9hVTqmeWxAu6APCS2YW8l5O0OQvhU,7724
312
- flwr/server/superlink/utils.py,sha256=e_-BZmW19xRuClGwBxK1GNSM_WCDGM4A8TW6fHoLPF0,3532
312
+ flwr/server/superlink/utils.py,sha256=zXmyU2o535b9dgz-TvFklzfuQk4irNnMtiK8vT4Dm1c,2454
313
313
  flwr/server/typing.py,sha256=LvO6gq7H6TAWhA9JFx0WyqHxU7FycyvhSsLjBLPgpts,1011
314
314
  flwr/server/utils/__init__.py,sha256=U4gM84-uUFddarODDQkO6SjNUuGhFcsHJZMjSEbezkU,884
315
315
  flwr/server/utils/tensorboard.py,sha256=3z3MeF0cu_U6ghNgRd0UQ5bunyDQKxCLpIpEdGMoCJ0,5466
@@ -344,6 +344,8 @@ flwr/supercore/object_store/in_memory_object_store.py,sha256=CGY43syxDGrUPcdOzRH
344
344
  flwr/supercore/object_store/object_store.py,sha256=J-rI3X7ET-F6dqOyM-UfHKCCQtPJ_EnYW62H_1txts0,5252
345
345
  flwr/supercore/object_store/object_store_factory.py,sha256=QVwE2ywi7vsj2iKfvWWnNw3N_I7Rz91NUt2RpcbJ7iM,1527
346
346
  flwr/supercore/object_store/utils.py,sha256=-WwBa6ejMNm9ahmNZP39IHutS0cwingmeqCoxTmATQM,1845
347
+ flwr/supercore/scheduler/__init__.py,sha256=E4GviiNJoZKz1dOao8ZGRHExsiM23GtOrkpMrTHy3n8,787
348
+ flwr/supercore/scheduler/plugin.py,sha256=kIv0JUrHP-ghrcGT-pbporL9A2mUz8PxASw6KgxCRv8,2460
347
349
  flwr/supercore/utils.py,sha256=ebuHMbeA8eXisX0oMPqBK3hk7uVnIE_yiqWVz8YbkpQ,1324
348
350
  flwr/superexec/__init__.py,sha256=YFqER0IJc1XEWfsX6AxZ9LSRq0sawPYrNYki-brvTIc,715
349
351
  flwr/superexec/app.py,sha256=U2jjOHb2LGWoU7vrl9_czTzre9O2mPxu3CPGUZ86sK4,1465
@@ -366,11 +368,13 @@ flwr/supernode/nodestate/nodestate.py,sha256=kkGFxYnLIwT4-UmlPnf6HvAUpPey2urUNrw
366
368
  flwr/supernode/nodestate/nodestate_factory.py,sha256=UYTDCcwK_baHUmkzkJDxL0UEqvtTfOMlQRrROMCd0Xo,1430
367
369
  flwr/supernode/runtime/__init__.py,sha256=JQdqd2EMTn-ORMeTvewYYh52ls0YKP68jrps1qioxu4,718
368
370
  flwr/supernode/runtime/run_clientapp.py,sha256=woAO8rXclt5eZeNHokhBChgxMf-TAzqWnHCkoiSsLVs,10765
371
+ flwr/supernode/scheduler/__init__.py,sha256=nQLi5ROVCMz8ii_WsZn4MAqKHXI40Eb3tq5-9zZbmpg,850
372
+ flwr/supernode/scheduler/simple_clientapp_scheduler_plugin.py,sha256=nnNuKhelrAEGCnWZ3aAkqJrhPu7xlVQ-oO1Eih9shTU,2000
369
373
  flwr/supernode/servicer/__init__.py,sha256=lucTzre5WPK7G1YLCfaqg3rbFWdNSb7ZTt-ca8gxdEo,717
370
374
  flwr/supernode/servicer/clientappio/__init__.py,sha256=7Oy62Y_oijqF7Dxi6tpcUQyOpLc_QpIRZ83NvwmB0Yg,813
371
- flwr/supernode/servicer/clientappio/clientappio_servicer.py,sha256=SOx719uqdkIHryBri-XpuYpALQE__hxNtDjSAdeUtug,10059
372
- flwr/supernode/start_client_internal.py,sha256=j0tGOaki1LNNMKzvnb2h8mGM0nE3ys-GzwAh0r6rgWI,21731
373
- flwr_nightly-1.20.0.dev20250722.dist-info/METADATA,sha256=nF2Mzn-x3-xwXJ-gLIXU9zZgS7XXOIrU1_MUkX-MpiY,15966
374
- flwr_nightly-1.20.0.dev20250722.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
375
- flwr_nightly-1.20.0.dev20250722.dist-info/entry_points.txt,sha256=jNpDXGBGgs21RqUxelF_jwGaxtqFwm-MQyfz-ZqSjrA,367
376
- flwr_nightly-1.20.0.dev20250722.dist-info/RECORD,,
375
+ flwr/supernode/servicer/clientappio/clientappio_servicer.py,sha256=ClPoKco7Tjj_cxRPhZlQSrOvcGa8sJwGs26LUNZnI3Y,10608
376
+ flwr/supernode/start_client_internal.py,sha256=VC7rV4QaX5Vk4Lw0DDodhwsKvGiHbr0mqrV0H77h1UI,21999
377
+ flwr_nightly-1.20.0.dev20250724.dist-info/METADATA,sha256=q7m01BPLI60ilXLKUeH5jYl-Eet8A98qD9F-sAK5mdg,15966
378
+ flwr_nightly-1.20.0.dev20250724.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
379
+ flwr_nightly-1.20.0.dev20250724.dist-info/entry_points.txt,sha256=jNpDXGBGgs21RqUxelF_jwGaxtqFwm-MQyfz-ZqSjrA,367
380
+ flwr_nightly-1.20.0.dev20250724.dist-info/RECORD,,