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
@@ -15,83 +15,80 @@
15
15
  """Flower ClientApp process."""
16
16
 
17
17
 
18
- import argparse
19
18
  import gc
19
+ import os
20
+ import threading
20
21
  import time
21
22
  from logging import DEBUG, ERROR, INFO
22
23
  from typing import Optional
23
24
 
24
25
  import grpc
25
26
 
27
+ from flwr.app.error import Error
26
28
  from flwr.cli.install import install_from_fab
27
29
  from flwr.client.client_app import ClientApp, LoadClientAppError
30
+ from flwr.client.clientapp.utils import get_load_client_app_fn
28
31
  from flwr.common import Context, Message
29
- from flwr.common.args import add_args_flwr_app_common
30
32
  from flwr.common.config import get_flwr_dir
31
- from flwr.common.constant import CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS, ErrorCode
32
- from flwr.common.exit import ExitCode, flwr_exit
33
+ from flwr.common.constant import ErrorCode
33
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
34
46
  from flwr.common.logger import log
35
- from flwr.common.message import Error
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
57
-
58
- from .utils import get_load_client_app_fn
59
-
60
-
61
- def flwr_clientapp() -> None:
62
- """Run process-isolated Flower ClientApp."""
63
- args = _parse_args_run_flwr_clientapp().parse_args()
64
- if not args.insecure:
65
- flwr_exit(
66
- ExitCode.COMMON_TLS_NOT_SUPPORTED,
67
- "flwr-clientapp does not support TLS yet.",
68
- )
69
-
70
- log(INFO, "Start `flwr-clientapp` process")
71
- log(
72
- DEBUG,
73
- "`flwr-clientapp` will attempt to connect to SuperNode's "
74
- "ClientAppIo API at %s with token %s",
75
- args.clientappio_api_address,
76
- args.token,
77
- )
78
- run_clientapp(
79
- clientappio_api_address=args.clientappio_api_address,
80
- run_once=(args.token is not None),
81
- token=args.token,
82
- flwr_dir=args.flwr_dir,
83
- certificates=None,
84
- )
75
+ from flwr.proto.node_pb2 import Node # pylint: disable=E0611
76
+ from flwr.supercore.utils import mask_string
85
77
 
86
78
 
87
- def run_clientapp( # pylint: disable=R0914
79
+ def run_clientapp( # pylint: disable=R0913, R0914, R0917
88
80
  clientappio_api_address: str,
89
81
  run_once: bool,
90
- token: Optional[int] = None,
82
+ token: Optional[str] = None,
91
83
  flwr_dir: Optional[str] = None,
92
84
  certificates: Optional[bytes] = None,
85
+ parent_pid: Optional[int] = None,
93
86
  ) -> None:
94
87
  """Run Flower ClientApp process."""
88
+ # Monitor the main process in case of SIGKILL
89
+ if parent_pid is not None:
90
+ start_parent_process_monitor(parent_pid)
91
+
95
92
  channel = create_channel(
96
93
  server_address=clientappio_api_address,
97
94
  insecure=(certificates is None),
@@ -107,9 +104,8 @@ def run_clientapp( # pylint: disable=R0914
107
104
 
108
105
  while True:
109
106
  # If token is not set, loop until token is received from SuperNode
110
- while token is None:
107
+ if token is None:
111
108
  token = get_token(stub)
112
- time.sleep(1)
113
109
 
114
110
  # Pull Message, Context, Run and (optional) FAB from SuperNode
115
111
  message, context, run, fab = pull_clientappinputs(stub=stub, token=token)
@@ -181,34 +177,72 @@ def run_clientapp( # pylint: disable=R0914
181
177
  channel.close()
182
178
 
183
179
 
184
- def get_token(stub: grpc.Channel) -> Optional[int]:
180
+ def start_parent_process_monitor(
181
+ parent_pid: int,
182
+ ) -> None:
183
+ """Monitor the parent process and exit if it terminates."""
184
+
185
+ def monitor() -> None:
186
+ while True:
187
+ time.sleep(0.2)
188
+ if os.getppid() != parent_pid:
189
+ os.kill(os.getpid(), 9)
190
+
191
+ threading.Thread(target=monitor, daemon=True).start()
192
+
193
+
194
+ def get_token(stub: ClientAppIoStub) -> str:
185
195
  """Get a token from SuperNode."""
186
196
  log(DEBUG, "[flwr-clientapp] Request token")
187
- try:
188
- res: GetTokenResponse = stub.GetToken(GetTokenRequest())
189
- log(DEBUG, "[GetToken] Received token: %s", res.token)
190
- return res.token
191
- except grpc.RpcError as e:
192
- if e.code() == grpc.StatusCode.FAILED_PRECONDITION: # pylint: disable=no-member
193
- log(DEBUG, "[GetToken] No token available yet")
194
- else:
195
- log(ERROR, "[GetToken] gRPC error occurred: %s", str(e))
196
- 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
197
210
 
198
211
 
199
212
  def pull_clientappinputs(
200
- stub: grpc.Channel, token: int
213
+ stub: ClientAppIoStub, token: str
201
214
  ) -> tuple[Message, Context, Run, Optional[Fab]]:
202
215
  """Pull ClientAppInputs from SuperNode."""
203
- 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)
204
218
  try:
205
- res: PullClientAppInputsResponse = stub.PullClientAppInputs(
206
- PullClientAppInputsRequest(token=token)
219
+ # Pull Context, Run and (optional) FAB
220
+ res: PullAppInputsResponse = stub.PullClientAppInputs(
221
+ PullAppInputsRequest(token=token)
207
222
  )
208
- message = message_from_proto(res.message)
209
223
  context = context_from_proto(res.context)
210
224
  run = run_from_proto(res.run)
211
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
212
246
  return message, context, run, fab
213
247
  except grpc.RpcError as e:
214
248
  log(ERROR, "[PullClientAppInputs] gRPC error occurred: %s", str(e))
@@ -216,42 +250,54 @@ def pull_clientappinputs(
216
250
 
217
251
 
218
252
  def push_clientappoutputs(
219
- stub: grpc.Channel, token: int, message: Message, context: Context
220
- ) -> PushClientAppOutputsResponse:
253
+ stub: ClientAppIoStub, token: str, message: Message, context: Context
254
+ ) -> PushAppOutputsResponse:
221
255
  """Push ClientAppOutputs to SuperNode."""
222
- log(INFO, "[flwr-clientapp] Push `ClientAppOutputs` for token %s", token)
223
- 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))
224
261
  proto_context = context_to_proto(context)
225
262
 
226
263
  try:
227
- res: PushClientAppOutputsResponse = stub.PushClientAppOutputs(
228
- PushClientAppOutputsRequest(
229
- 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
+ )
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,
230
294
  )
295
+
296
+ # Push Context
297
+ res: PushAppOutputsResponse = stub.PushClientAppOutputs(
298
+ PushAppOutputsRequest(token=token, context=proto_context)
231
299
  )
232
300
  return res
233
301
  except grpc.RpcError as e:
234
302
  log(ERROR, "[PushClientAppOutputs] gRPC error occurred: %s", str(e))
235
303
  raise e
236
-
237
-
238
- def _parse_args_run_flwr_clientapp() -> argparse.ArgumentParser:
239
- """Parse flwr-clientapp command line arguments."""
240
- parser = argparse.ArgumentParser(
241
- description="Run a Flower ClientApp",
242
- )
243
- parser.add_argument(
244
- "--clientappio-api-address",
245
- default=CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS,
246
- type=str,
247
- help="Address of SuperNode's ClientAppIo API (IPv4, IPv6, or a domain name)."
248
- f"By default, it is set to {CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS}.",
249
- )
250
- parser.add_argument(
251
- "--token",
252
- type=int,
253
- required=False,
254
- help="Unique token generated by SuperNode for each ClientApp execution",
255
- )
256
- add_args_flwr_app_common(parser=parser)
257
- return parser
@@ -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)
@@ -0,0 +1,15 @@
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 SuperNode servicers."""
@@ -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
+ """ClientAppIo API Servicer."""
16
+
17
+
18
+ from .clientappio_servicer import ClientAppIoServicer
19
+
20
+ __all__ = [
21
+ "ClientAppIoServicer",
22
+ ]