flwr 1.17.0__py3-none-any.whl → 1.19.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 (286) hide show
  1. flwr/__init__.py +1 -1
  2. flwr/app/__init__.py +15 -0
  3. flwr/app/error.py +68 -0
  4. flwr/app/metadata.py +223 -0
  5. flwr/cli/__init__.py +1 -1
  6. flwr/cli/app.py +21 -2
  7. flwr/cli/build.py +83 -58
  8. flwr/cli/cli_user_auth_interceptor.py +1 -1
  9. flwr/cli/config_utils.py +53 -17
  10. flwr/cli/example.py +1 -1
  11. flwr/cli/install.py +1 -1
  12. flwr/cli/log.py +4 -4
  13. flwr/cli/login/__init__.py +1 -1
  14. flwr/cli/login/login.py +15 -8
  15. flwr/cli/ls.py +16 -37
  16. flwr/cli/new/__init__.py +1 -1
  17. flwr/cli/new/new.py +4 -4
  18. flwr/cli/new/templates/__init__.py +1 -1
  19. flwr/cli/new/templates/app/__init__.py +1 -1
  20. flwr/cli/new/templates/app/code/__init__.py +1 -1
  21. flwr/cli/new/templates/app/code/client.baseline.py.tpl +1 -1
  22. flwr/cli/new/templates/app/code/flwr_tune/__init__.py +1 -1
  23. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +4 -4
  24. flwr/cli/new/templates/app/code/model.baseline.py.tpl +1 -1
  25. flwr/cli/new/templates/app/code/server.baseline.py.tpl +2 -3
  26. flwr/cli/new/templates/app/code/task.sklearn.py.tpl +1 -1
  27. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +14 -17
  28. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +4 -4
  29. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  30. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  31. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  32. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  33. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
  34. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  35. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
  36. flwr/cli/run/__init__.py +1 -1
  37. flwr/cli/run/run.py +11 -19
  38. flwr/cli/stop.py +3 -3
  39. flwr/cli/utils.py +42 -17
  40. flwr/client/__init__.py +3 -3
  41. flwr/client/client.py +1 -1
  42. flwr/client/client_app.py +140 -138
  43. flwr/client/clientapp/__init__.py +1 -8
  44. flwr/client/clientapp/utils.py +1 -1
  45. flwr/client/dpfedavg_numpy_client.py +1 -1
  46. flwr/client/grpc_adapter_client/__init__.py +1 -1
  47. flwr/client/grpc_adapter_client/connection.py +5 -5
  48. flwr/client/grpc_rere_client/__init__.py +1 -1
  49. flwr/client/grpc_rere_client/client_interceptor.py +1 -1
  50. flwr/client/grpc_rere_client/connection.py +131 -61
  51. flwr/client/grpc_rere_client/grpc_adapter.py +35 -7
  52. flwr/client/message_handler/__init__.py +1 -1
  53. flwr/client/message_handler/message_handler.py +2 -2
  54. flwr/client/mod/__init__.py +1 -1
  55. flwr/client/mod/centraldp_mods.py +1 -1
  56. flwr/client/mod/comms_mods.py +39 -20
  57. flwr/client/mod/localdp_mod.py +6 -6
  58. flwr/client/mod/secure_aggregation/__init__.py +1 -1
  59. flwr/client/mod/secure_aggregation/secagg_mod.py +1 -1
  60. flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
  61. flwr/client/mod/utils.py +1 -1
  62. flwr/client/numpy_client.py +1 -1
  63. flwr/client/rest_client/__init__.py +1 -1
  64. flwr/client/rest_client/connection.py +174 -68
  65. flwr/client/run_info_store.py +1 -1
  66. flwr/client/typing.py +1 -1
  67. flwr/clientapp/__init__.py +15 -0
  68. flwr/common/__init__.py +3 -3
  69. flwr/common/address.py +1 -1
  70. flwr/common/args.py +1 -1
  71. flwr/common/auth_plugin/__init__.py +3 -1
  72. flwr/common/auth_plugin/auth_plugin.py +30 -4
  73. flwr/common/config.py +1 -1
  74. flwr/common/constant.py +37 -8
  75. flwr/common/context.py +1 -1
  76. flwr/common/date.py +1 -1
  77. flwr/common/differential_privacy.py +1 -1
  78. flwr/common/differential_privacy_constants.py +1 -1
  79. flwr/common/dp.py +1 -1
  80. flwr/common/event_log_plugin/event_log_plugin.py +3 -3
  81. flwr/common/exit/exit.py +6 -6
  82. flwr/common/exit_handlers.py +31 -1
  83. flwr/common/grpc.py +1 -1
  84. flwr/common/heartbeat.py +165 -0
  85. flwr/common/inflatable.py +290 -0
  86. flwr/common/inflatable_grpc_utils.py +99 -0
  87. flwr/common/inflatable_rest_utils.py +99 -0
  88. flwr/common/inflatable_utils.py +341 -0
  89. flwr/common/logger.py +1 -1
  90. flwr/common/message.py +137 -252
  91. flwr/common/object_ref.py +1 -1
  92. flwr/common/parameter.py +1 -1
  93. flwr/common/pyproject.py +1 -1
  94. flwr/common/record/__init__.py +3 -2
  95. flwr/common/record/array.py +323 -0
  96. flwr/common/record/arrayrecord.py +121 -243
  97. flwr/common/record/configrecord.py +71 -16
  98. flwr/common/record/conversion_utils.py +2 -2
  99. flwr/common/record/metricrecord.py +71 -20
  100. flwr/common/record/recorddict.py +207 -90
  101. flwr/common/record/typeddict.py +1 -1
  102. flwr/common/recorddict_compat.py +2 -2
  103. flwr/common/retry_invoker.py +15 -11
  104. flwr/common/secure_aggregation/__init__.py +1 -1
  105. flwr/common/secure_aggregation/crypto/__init__.py +1 -1
  106. flwr/common/secure_aggregation/crypto/shamir.py +52 -30
  107. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +1 -1
  108. flwr/common/secure_aggregation/ndarrays_arithmetic.py +1 -1
  109. flwr/common/secure_aggregation/quantization.py +1 -1
  110. flwr/common/secure_aggregation/secaggplus_constants.py +1 -1
  111. flwr/common/secure_aggregation/secaggplus_utils.py +1 -1
  112. flwr/common/serde.py +60 -184
  113. flwr/common/serde_utils.py +175 -0
  114. flwr/common/telemetry.py +2 -2
  115. flwr/common/typing.py +6 -4
  116. flwr/common/version.py +1 -1
  117. flwr/compat/__init__.py +15 -0
  118. flwr/compat/client/__init__.py +15 -0
  119. flwr/{client → compat/client}/app.py +71 -211
  120. flwr/{client → compat/client}/grpc_client/__init__.py +1 -1
  121. flwr/{client → compat/client}/grpc_client/connection.py +13 -13
  122. flwr/compat/common/__init__.py +15 -0
  123. flwr/compat/server/__init__.py +15 -0
  124. flwr/compat/server/app.py +174 -0
  125. flwr/compat/simulation/__init__.py +15 -0
  126. flwr/proto/__init__.py +1 -1
  127. flwr/proto/fleet_pb2.py +32 -27
  128. flwr/proto/fleet_pb2.pyi +49 -35
  129. flwr/proto/fleet_pb2_grpc.py +117 -13
  130. flwr/proto/fleet_pb2_grpc.pyi +47 -6
  131. flwr/proto/heartbeat_pb2.py +33 -0
  132. flwr/proto/heartbeat_pb2.pyi +66 -0
  133. flwr/proto/heartbeat_pb2_grpc.py +4 -0
  134. flwr/proto/heartbeat_pb2_grpc.pyi +4 -0
  135. flwr/proto/message_pb2.py +28 -11
  136. flwr/proto/message_pb2.pyi +125 -0
  137. flwr/proto/recorddict_pb2.py +16 -28
  138. flwr/proto/recorddict_pb2.pyi +46 -64
  139. flwr/proto/run_pb2.py +24 -32
  140. flwr/proto/run_pb2.pyi +4 -52
  141. flwr/proto/serverappio_pb2.py +32 -23
  142. flwr/proto/serverappio_pb2.pyi +45 -3
  143. flwr/proto/serverappio_pb2_grpc.py +138 -34
  144. flwr/proto/serverappio_pb2_grpc.pyi +54 -13
  145. flwr/proto/simulationio_pb2.py +12 -11
  146. flwr/proto/simulationio_pb2_grpc.py +35 -0
  147. flwr/proto/simulationio_pb2_grpc.pyi +14 -0
  148. flwr/server/__init__.py +2 -2
  149. flwr/server/app.py +69 -187
  150. flwr/server/client_manager.py +1 -1
  151. flwr/server/client_proxy.py +1 -1
  152. flwr/server/compat/__init__.py +1 -1
  153. flwr/server/compat/app.py +1 -1
  154. flwr/server/compat/app_utils.py +51 -29
  155. flwr/server/compat/legacy_context.py +1 -1
  156. flwr/server/criterion.py +1 -1
  157. flwr/server/fleet_event_log_interceptor.py +2 -2
  158. flwr/server/grid/grid.py +3 -3
  159. flwr/server/grid/grpc_grid.py +104 -34
  160. flwr/server/grid/inmemory_grid.py +5 -4
  161. flwr/server/history.py +1 -1
  162. flwr/server/run_serverapp.py +1 -1
  163. flwr/server/server.py +1 -1
  164. flwr/server/server_app.py +65 -58
  165. flwr/server/server_config.py +1 -1
  166. flwr/server/serverapp/__init__.py +1 -1
  167. flwr/server/serverapp/app.py +19 -1
  168. flwr/server/serverapp_components.py +1 -1
  169. flwr/server/strategy/__init__.py +1 -1
  170. flwr/server/strategy/aggregate.py +1 -1
  171. flwr/server/strategy/bulyan.py +2 -2
  172. flwr/server/strategy/dp_adaptive_clipping.py +17 -17
  173. flwr/server/strategy/dp_fixed_clipping.py +17 -17
  174. flwr/server/strategy/dpfedavg_adaptive.py +1 -1
  175. flwr/server/strategy/dpfedavg_fixed.py +1 -1
  176. flwr/server/strategy/fault_tolerant_fedavg.py +1 -1
  177. flwr/server/strategy/fedadagrad.py +1 -1
  178. flwr/server/strategy/fedadam.py +1 -1
  179. flwr/server/strategy/fedavg.py +1 -1
  180. flwr/server/strategy/fedavg_android.py +1 -1
  181. flwr/server/strategy/fedavgm.py +1 -1
  182. flwr/server/strategy/fedmedian.py +1 -1
  183. flwr/server/strategy/fedopt.py +1 -1
  184. flwr/server/strategy/fedprox.py +1 -1
  185. flwr/server/strategy/fedtrimmedavg.py +1 -1
  186. flwr/server/strategy/fedxgb_bagging.py +1 -1
  187. flwr/server/strategy/fedxgb_cyclic.py +1 -1
  188. flwr/server/strategy/fedxgb_nn_avg.py +3 -2
  189. flwr/server/strategy/fedyogi.py +1 -1
  190. flwr/server/strategy/krum.py +1 -1
  191. flwr/server/strategy/qfedavg.py +1 -1
  192. flwr/server/strategy/strategy.py +1 -1
  193. flwr/server/superlink/__init__.py +1 -1
  194. flwr/server/superlink/ffs/__init__.py +3 -1
  195. flwr/server/superlink/ffs/disk_ffs.py +1 -1
  196. flwr/server/superlink/ffs/ffs.py +1 -1
  197. flwr/server/superlink/ffs/ffs_factory.py +1 -1
  198. flwr/server/superlink/fleet/__init__.py +1 -1
  199. flwr/server/superlink/fleet/grpc_adapter/__init__.py +1 -1
  200. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +14 -4
  201. flwr/server/superlink/fleet/grpc_bidi/__init__.py +1 -1
  202. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -1
  203. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +1 -1
  204. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +1 -1
  205. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +13 -13
  206. flwr/server/superlink/fleet/grpc_rere/__init__.py +1 -1
  207. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +102 -8
  208. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +1 -1
  209. flwr/server/superlink/fleet/message_handler/__init__.py +1 -1
  210. flwr/server/superlink/fleet/message_handler/message_handler.py +136 -19
  211. flwr/server/superlink/fleet/rest_rere/__init__.py +1 -1
  212. flwr/server/superlink/fleet/rest_rere/rest_api.py +73 -12
  213. flwr/server/superlink/fleet/vce/__init__.py +1 -1
  214. flwr/server/superlink/fleet/vce/backend/__init__.py +1 -1
  215. flwr/server/superlink/fleet/vce/backend/backend.py +1 -1
  216. flwr/server/superlink/fleet/vce/backend/raybackend.py +1 -1
  217. flwr/server/superlink/fleet/vce/vce_api.py +7 -4
  218. flwr/server/superlink/linkstate/__init__.py +1 -1
  219. flwr/server/superlink/linkstate/in_memory_linkstate.py +139 -44
  220. flwr/server/superlink/linkstate/linkstate.py +54 -21
  221. flwr/server/superlink/linkstate/linkstate_factory.py +1 -1
  222. flwr/server/superlink/linkstate/sqlite_linkstate.py +150 -56
  223. flwr/server/superlink/linkstate/utils.py +34 -30
  224. flwr/server/superlink/serverappio/serverappio_grpc.py +3 -0
  225. flwr/server/superlink/serverappio/serverappio_servicer.py +211 -57
  226. flwr/server/superlink/simulation/__init__.py +1 -1
  227. flwr/server/superlink/simulation/simulationio_grpc.py +1 -1
  228. flwr/server/superlink/simulation/simulationio_servicer.py +26 -2
  229. flwr/server/superlink/utils.py +45 -3
  230. flwr/server/typing.py +1 -1
  231. flwr/server/utils/__init__.py +1 -1
  232. flwr/server/utils/tensorboard.py +1 -1
  233. flwr/server/utils/validator.py +3 -3
  234. flwr/server/workflow/__init__.py +1 -1
  235. flwr/server/workflow/constant.py +1 -1
  236. flwr/server/workflow/default_workflows.py +1 -1
  237. flwr/server/workflow/secure_aggregation/__init__.py +1 -1
  238. flwr/server/workflow/secure_aggregation/secagg_workflow.py +1 -1
  239. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
  240. flwr/serverapp/__init__.py +15 -0
  241. flwr/simulation/__init__.py +1 -1
  242. flwr/simulation/app.py +18 -1
  243. flwr/simulation/legacy_app.py +1 -1
  244. flwr/simulation/ray_transport/__init__.py +1 -1
  245. flwr/simulation/ray_transport/ray_actor.py +1 -1
  246. flwr/simulation/ray_transport/ray_client_proxy.py +1 -1
  247. flwr/simulation/ray_transport/utils.py +1 -1
  248. flwr/simulation/run_simulation.py +2 -2
  249. flwr/simulation/simulationio_connection.py +1 -1
  250. flwr/supercore/__init__.py +15 -0
  251. flwr/supercore/object_store/__init__.py +24 -0
  252. flwr/supercore/object_store/in_memory_object_store.py +229 -0
  253. flwr/supercore/object_store/object_store.py +192 -0
  254. flwr/supercore/object_store/object_store_factory.py +44 -0
  255. flwr/superexec/__init__.py +1 -1
  256. flwr/superexec/app.py +1 -1
  257. flwr/superexec/deployment.py +7 -3
  258. flwr/superexec/exec_event_log_interceptor.py +4 -4
  259. flwr/superexec/exec_grpc.py +8 -4
  260. flwr/superexec/exec_servicer.py +126 -24
  261. flwr/superexec/exec_user_auth_interceptor.py +38 -9
  262. flwr/superexec/executor.py +5 -1
  263. flwr/superexec/simulation.py +8 -2
  264. flwr/superlink/__init__.py +15 -0
  265. flwr/{client/supernode → supernode}/__init__.py +1 -8
  266. flwr/{client/nodestate/nodestate.py → supernode/cli/__init__.py} +8 -15
  267. flwr/{client/supernode/app.py → supernode/cli/flower_supernode.py} +4 -13
  268. flwr/supernode/cli/flwr_clientapp.py +81 -0
  269. flwr/{client → supernode}/nodestate/__init__.py +1 -1
  270. flwr/supernode/nodestate/in_memory_nodestate.py +190 -0
  271. flwr/supernode/nodestate/nodestate.py +212 -0
  272. flwr/{client → supernode}/nodestate/nodestate_factory.py +1 -1
  273. flwr/supernode/runtime/__init__.py +15 -0
  274. flwr/{client/clientapp/app.py → supernode/runtime/run_clientapp.py} +26 -57
  275. flwr/supernode/servicer/__init__.py +15 -0
  276. flwr/supernode/servicer/clientappio/__init__.py +24 -0
  277. flwr/{client/clientapp → supernode/servicer/clientappio}/clientappio_servicer.py +1 -1
  278. flwr/supernode/start_client_internal.py +491 -0
  279. {flwr-1.17.0.dist-info → flwr-1.19.0.dist-info}/METADATA +6 -5
  280. flwr-1.19.0.dist-info/RECORD +365 -0
  281. {flwr-1.17.0.dist-info → flwr-1.19.0.dist-info}/WHEEL +1 -1
  282. {flwr-1.17.0.dist-info → flwr-1.19.0.dist-info}/entry_points.txt +2 -2
  283. flwr/client/heartbeat.py +0 -74
  284. flwr/client/nodestate/in_memory_nodestate.py +0 -38
  285. flwr-1.17.0.dist-info/LICENSE +0 -202
  286. flwr-1.17.0.dist-info/RECORD +0 -333
@@ -0,0 +1,81 @@
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
+ """`flwr-clientapp` command."""
16
+
17
+
18
+ import argparse
19
+ from logging import DEBUG, INFO
20
+
21
+ from flwr.common.args import add_args_flwr_app_common
22
+ from flwr.common.constant import CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS
23
+ from flwr.common.exit import ExitCode, flwr_exit
24
+ from flwr.common.logger import log
25
+ from flwr.supernode.runtime.run_clientapp import run_clientapp
26
+
27
+
28
+ def flwr_clientapp() -> None:
29
+ """Run process-isolated Flower ClientApp."""
30
+ args = _parse_args_run_flwr_clientapp().parse_args()
31
+ if not args.insecure:
32
+ flwr_exit(
33
+ ExitCode.COMMON_TLS_NOT_SUPPORTED,
34
+ "flwr-clientapp does not support TLS yet.",
35
+ )
36
+
37
+ log(INFO, "Start `flwr-clientapp` process")
38
+ log(
39
+ DEBUG,
40
+ "`flwr-clientapp` will attempt to connect to SuperNode's "
41
+ "ClientAppIo API at %s with token %s",
42
+ args.clientappio_api_address,
43
+ args.token,
44
+ )
45
+ run_clientapp(
46
+ clientappio_api_address=args.clientappio_api_address,
47
+ run_once=(args.token is not None),
48
+ token=args.token,
49
+ flwr_dir=args.flwr_dir,
50
+ certificates=None,
51
+ parent_pid=args.parent_pid,
52
+ )
53
+
54
+
55
+ def _parse_args_run_flwr_clientapp() -> argparse.ArgumentParser:
56
+ """Parse flwr-clientapp command line arguments."""
57
+ parser = argparse.ArgumentParser(
58
+ description="Run a Flower ClientApp",
59
+ )
60
+ parser.add_argument(
61
+ "--clientappio-api-address",
62
+ default=CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS,
63
+ type=str,
64
+ help="Address of SuperNode's ClientAppIo API (IPv4, IPv6, or a domain name)."
65
+ f"By default, it is set to {CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS}.",
66
+ )
67
+ parser.add_argument(
68
+ "--token",
69
+ type=int,
70
+ required=False,
71
+ help="Unique token generated by SuperNode for each ClientApp execution",
72
+ )
73
+ parser.add_argument(
74
+ "--parent-pid",
75
+ type=int,
76
+ default=None,
77
+ help="The PID of the parent process. When set, the process will terminate "
78
+ "when the parent process exits.",
79
+ )
80
+ add_args_flwr_app_common(parser=parser)
81
+ return parser
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Flower Labs GmbH. All Rights Reserved.
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -0,0 +1,190 @@
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
+ """In-memory NodeState implementation."""
16
+
17
+
18
+ import secrets
19
+ from collections.abc import Sequence
20
+ from dataclasses import dataclass
21
+ from threading import Lock
22
+ from typing import Optional
23
+
24
+ from flwr.common import Context, Message
25
+ from flwr.common.constant import FLWR_APP_TOKEN_LENGTH
26
+ from flwr.common.typing import Run
27
+
28
+ from .nodestate import NodeState
29
+
30
+
31
+ @dataclass
32
+ class MessageEntry:
33
+ """Data class to represent a message entry."""
34
+
35
+ message: Message
36
+ is_retrieved: bool = False
37
+
38
+
39
+ class InMemoryNodeState(NodeState): # pylint: disable=too-many-instance-attributes
40
+ """In-memory NodeState implementation."""
41
+
42
+ def __init__(self) -> None:
43
+ # Store node_id
44
+ self.node_id: Optional[int] = None
45
+ # Store Object ID to MessageEntry mapping
46
+ self.msg_store: dict[str, MessageEntry] = {}
47
+ self.lock_msg_store = Lock()
48
+ # Store run ID to Run mapping
49
+ self.run_store: dict[int, Run] = {}
50
+ self.lock_run_store = Lock()
51
+ # Store run ID to Context mapping
52
+ self.ctx_store: dict[int, Context] = {}
53
+ self.lock_ctx_store = Lock()
54
+ # Store run ID to token mapping
55
+ self.token_store: dict[int, str] = {}
56
+ self.lock_token_store = Lock()
57
+
58
+ def set_node_id(self, node_id: Optional[int]) -> None:
59
+ """Set the node ID."""
60
+ self.node_id = node_id
61
+
62
+ def get_node_id(self) -> int:
63
+ """Get the node ID."""
64
+ if self.node_id is None:
65
+ raise ValueError("Node ID not set")
66
+ return self.node_id
67
+
68
+ def store_message(self, message: Message) -> Optional[str]:
69
+ """Store a message."""
70
+ with self.lock_msg_store:
71
+ msg_id = message.metadata.message_id
72
+ if msg_id == "" or msg_id in self.msg_store:
73
+ return None
74
+ self.msg_store[msg_id] = MessageEntry(message=message)
75
+ return msg_id
76
+
77
+ def get_messages(
78
+ self,
79
+ *,
80
+ run_ids: Optional[Sequence[int]] = None,
81
+ is_reply: Optional[bool] = None,
82
+ limit: Optional[int] = None,
83
+ ) -> Sequence[Message]:
84
+ """Retrieve messages based on the specified filters."""
85
+ selected_messages: list[Message] = []
86
+
87
+ with self.lock_msg_store:
88
+ # Iterate through all messages in the store
89
+ for object_id in list(self.msg_store.keys()):
90
+ entry = self.msg_store[object_id]
91
+ message = entry.message
92
+
93
+ # Skip messages that have already been retrieved
94
+ if entry.is_retrieved:
95
+ continue
96
+
97
+ # Skip messages whose run_id doesn't match the filter
98
+ if run_ids is not None:
99
+ if message.metadata.run_id not in run_ids:
100
+ continue
101
+
102
+ # If is_reply filter is set, filter for reply/non-reply messages
103
+ if is_reply is not None:
104
+ is_reply_message = message.metadata.reply_to_message_id != ""
105
+ # XOR logic to filter mismatched types (reply vs non-reply)
106
+ if is_reply ^ is_reply_message:
107
+ continue
108
+
109
+ # Add the message to the result set
110
+ selected_messages.append(message)
111
+
112
+ # Mark the message as retrieved
113
+ entry.is_retrieved = True
114
+
115
+ # Stop if the number of collected messages reaches the limit
116
+ if limit is not None and len(selected_messages) >= limit:
117
+ break
118
+
119
+ return selected_messages
120
+
121
+ def delete_messages(
122
+ self,
123
+ *,
124
+ message_ids: Optional[Sequence[str]] = None,
125
+ ) -> None:
126
+ """Delete messages based on the specified filters."""
127
+ with self.lock_msg_store:
128
+ if message_ids is None:
129
+ # If no message IDs are provided, clear the entire store
130
+ self.msg_store.clear()
131
+ return
132
+
133
+ # Remove specified messages from the store
134
+ for msg_id in message_ids:
135
+ self.msg_store.pop(msg_id, None)
136
+
137
+ def store_run(self, run: Run) -> None:
138
+ """Store a run."""
139
+ with self.lock_run_store:
140
+ self.run_store[run.run_id] = run
141
+
142
+ def get_run(self, run_id: int) -> Optional[Run]:
143
+ """Retrieve a run by its ID."""
144
+ with self.lock_run_store:
145
+ return self.run_store.get(run_id)
146
+
147
+ def store_context(self, context: Context) -> None:
148
+ """Store a context."""
149
+ with self.lock_ctx_store:
150
+ self.ctx_store[context.run_id] = context
151
+
152
+ def get_context(self, run_id: int) -> Optional[Context]:
153
+ """Retrieve a context by its run ID."""
154
+ with self.lock_ctx_store:
155
+ return self.ctx_store.get(run_id)
156
+
157
+ def get_run_ids_with_pending_messages(self) -> Sequence[int]:
158
+ """Retrieve run IDs that have at least one pending message."""
159
+ # Collect run IDs from messages
160
+ with self.lock_msg_store:
161
+ ret = {
162
+ entry.message.metadata.run_id
163
+ for entry in self.msg_store.values()
164
+ if entry.message.metadata.reply_to_message_id == ""
165
+ and not entry.is_retrieved
166
+ }
167
+
168
+ # Remove run IDs that have tokens stored (indicating they are in progress)
169
+ with self.lock_token_store:
170
+ ret -= set(self.token_store.keys())
171
+ return list(ret)
172
+
173
+ def create_token(self, run_id: int) -> str:
174
+ """Create a token for the given run ID."""
175
+ token = secrets.token_hex(FLWR_APP_TOKEN_LENGTH) # Generate a random token
176
+ with self.lock_token_store:
177
+ if run_id in self.token_store:
178
+ raise ValueError("Token already created for this run ID")
179
+ self.token_store[run_id] = token
180
+ return token
181
+
182
+ def verify_token(self, run_id: int, token: str) -> bool:
183
+ """Verify a token for the given run ID."""
184
+ with self.lock_token_store:
185
+ return self.token_store.get(run_id) == token
186
+
187
+ def delete_token(self, run_id: int) -> None:
188
+ """Delete the token for the given run ID."""
189
+ with self.lock_token_store:
190
+ self.token_store.pop(run_id, None)
@@ -0,0 +1,212 @@
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 NodeState."""
16
+
17
+
18
+ from abc import ABC, abstractmethod
19
+ from collections.abc import Sequence
20
+ from typing import Optional
21
+
22
+ from flwr.common import Context, Message
23
+ from flwr.common.typing import Run
24
+
25
+
26
+ class NodeState(ABC):
27
+ """Abstract base class for node state."""
28
+
29
+ @abstractmethod
30
+ def set_node_id(self, node_id: int) -> None:
31
+ """Set the node ID."""
32
+
33
+ @abstractmethod
34
+ def get_node_id(self) -> int:
35
+ """Get the node ID."""
36
+
37
+ @abstractmethod
38
+ def store_message(self, message: Message) -> Optional[str]:
39
+ """Store a message.
40
+
41
+ Parameters
42
+ ----------
43
+ message : Message
44
+ The message to store.
45
+
46
+ Returns
47
+ -------
48
+ Optional[str]
49
+ The object ID of the stored message, or None if storage failed.
50
+ """
51
+
52
+ @abstractmethod
53
+ def get_messages(
54
+ self,
55
+ *,
56
+ run_ids: Optional[Sequence[int]] = None,
57
+ is_reply: Optional[bool] = None,
58
+ limit: Optional[int] = None,
59
+ ) -> Sequence[Message]:
60
+ """Retrieve messages based on the specified filters.
61
+
62
+ If a filter is set to None, it is ignored.
63
+ If multiple filters are provided, they are combined using AND logic.
64
+
65
+ Parameters
66
+ ----------
67
+ run_ids : Optional[Sequence[int]] (default: None)
68
+ Sequence of run IDs to filter by. If a sequence is provided,
69
+ it is treated as an OR condition.
70
+ is_reply : Optional[bool] (default: None)
71
+ If True, filter for reply messages; if False, filter for non-reply
72
+ (instruction) messages.
73
+ limit : Optional[int] (default: None)
74
+ Maximum number of messages to return. If None, no limit is applied.
75
+
76
+ Returns
77
+ -------
78
+ Sequence[Message]
79
+ A sequence of messages matching the specified filters.
80
+
81
+ Notes
82
+ -----
83
+ **IMPORTANT:** Retrieved messages will **NOT** be returned again by subsequent
84
+ calls to this method, even if the filters match them.
85
+ """
86
+
87
+ @abstractmethod
88
+ def delete_messages(
89
+ self,
90
+ *,
91
+ message_ids: Optional[Sequence[str]] = None,
92
+ ) -> None:
93
+ """Delete messages based on the specified filters.
94
+
95
+ If a filter is set to None, it is ignored.
96
+ If multiple filters are provided, they are combined using AND logic.
97
+
98
+ Parameters
99
+ ----------
100
+ message_ids : Optional[Sequence[str]] (default: None)
101
+ Sequence of message (object) IDs to filter by. If a sequence is provided,
102
+ it is treated as an OR condition.
103
+
104
+ Notes
105
+ -----
106
+ **IMPORTANT:** **All messages** will be deleted if no filters are provided.
107
+ """
108
+
109
+ @abstractmethod
110
+ def store_run(self, run: Run) -> None:
111
+ """Store a run.
112
+
113
+ Parameters
114
+ ----------
115
+ run : Run
116
+ The `Run` instance to store.
117
+ """
118
+
119
+ @abstractmethod
120
+ def get_run(self, run_id: int) -> Optional[Run]:
121
+ """Retrieve a run by its ID.
122
+
123
+ Parameters
124
+ ----------
125
+ run_id : int
126
+ The ID of the run to retrieve.
127
+
128
+ Returns
129
+ -------
130
+ Optional[Run]
131
+ The `Run` instance if found, otherwise None.
132
+ """
133
+
134
+ @abstractmethod
135
+ def store_context(self, context: Context) -> None:
136
+ """Store a context.
137
+
138
+ Parameters
139
+ ----------
140
+ context : Context
141
+ The context to store.
142
+ """
143
+
144
+ @abstractmethod
145
+ def get_context(self, run_id: int) -> Optional[Context]:
146
+ """Retrieve a context by its run ID.
147
+
148
+ Parameters
149
+ ----------
150
+ run_id : int
151
+ The ID of the run with which the context is associated.
152
+
153
+ Returns
154
+ -------
155
+ Optional[Context]
156
+ The `Context` instance if found, otherwise None.
157
+ """
158
+
159
+ @abstractmethod
160
+ def get_run_ids_with_pending_messages(self) -> Sequence[int]:
161
+ """Retrieve run IDs that have at least one pending message.
162
+
163
+ Run IDs that are currently in progress (i.e., those associated with tokens)
164
+ will not be returned, even if they have pending messages.
165
+
166
+ Returns
167
+ -------
168
+ Sequence[int]
169
+ Sequence of run IDs with pending messages.
170
+ """
171
+
172
+ @abstractmethod
173
+ def create_token(self, run_id: int) -> str:
174
+ """Create a token for the given run ID.
175
+
176
+ Parameters
177
+ ----------
178
+ run_id : int
179
+ The ID of the run for which to create a token.
180
+
181
+ Returns
182
+ -------
183
+ str
184
+ A unique token associated with the run ID.
185
+ """
186
+
187
+ @abstractmethod
188
+ def verify_token(self, run_id: int, token: str) -> bool:
189
+ """Verify a token for the given run ID.
190
+
191
+ Parameters
192
+ ----------
193
+ run_id : int
194
+ The ID of the run for which to verify the token.
195
+ token : str
196
+ The token to verify.
197
+
198
+ Returns
199
+ -------
200
+ bool
201
+ True if the token is valid for the run ID, False otherwise.
202
+ """
203
+
204
+ @abstractmethod
205
+ def delete_token(self, run_id: int) -> None:
206
+ """Delete the token for the given run ID.
207
+
208
+ Parameters
209
+ ----------
210
+ run_id : int
211
+ The ID of the run for which to delete the token.
212
+ """
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Flower Labs GmbH. All Rights Reserved.
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -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 components."""
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Flower Labs GmbH. All Rights Reserved.
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -15,24 +15,24 @@
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
34
35
  from flwr.common.logger import log
35
- from flwr.common.message import Error
36
36
  from flwr.common.retry_invoker import _make_simple_grpc_retry_invoker, _wrap_stub
37
37
  from flwr.common.serde import (
38
38
  context_from_proto,
@@ -55,43 +55,20 @@ from flwr.proto.clientappio_pb2 import (
55
55
  )
56
56
  from flwr.proto.clientappio_pb2_grpc import ClientAppIoStub
57
57
 
58
- from .utils import get_load_client_app_fn
59
58
 
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
- )
85
-
86
-
87
- def run_clientapp( # pylint: disable=R0914
59
+ def run_clientapp( # pylint: disable=R0913, R0914, R0917
88
60
  clientappio_api_address: str,
89
61
  run_once: bool,
90
62
  token: Optional[int] = None,
91
63
  flwr_dir: Optional[str] = None,
92
64
  certificates: Optional[bytes] = None,
65
+ parent_pid: Optional[int] = None,
93
66
  ) -> None:
94
67
  """Run Flower ClientApp process."""
68
+ # Monitor the main process in case of SIGKILL
69
+ if parent_pid is not None:
70
+ start_parent_process_monitor(parent_pid)
71
+
95
72
  channel = create_channel(
96
73
  server_address=clientappio_api_address,
97
74
  insecure=(certificates is None),
@@ -181,6 +158,20 @@ def run_clientapp( # pylint: disable=R0914
181
158
  channel.close()
182
159
 
183
160
 
161
+ def start_parent_process_monitor(
162
+ parent_pid: int,
163
+ ) -> None:
164
+ """Monitor the parent process and exit if it terminates."""
165
+
166
+ def monitor() -> None:
167
+ while True:
168
+ time.sleep(0.2)
169
+ if os.getppid() != parent_pid:
170
+ os.kill(os.getpid(), 9)
171
+
172
+ threading.Thread(target=monitor, daemon=True).start()
173
+
174
+
184
175
  def get_token(stub: grpc.Channel) -> Optional[int]:
185
176
  """Get a token from SuperNode."""
186
177
  log(DEBUG, "[flwr-clientapp] Request token")
@@ -233,25 +224,3 @@ def push_clientappoutputs(
233
224
  except grpc.RpcError as e:
234
225
  log(ERROR, "[PushClientAppOutputs] gRPC error occurred: %s", str(e))
235
226
  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,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,24 @@
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 ClientAppInputs, ClientAppIoServicer, ClientAppOutputs
19
+
20
+ __all__ = [
21
+ "ClientAppInputs",
22
+ "ClientAppIoServicer",
23
+ "ClientAppOutputs",
24
+ ]
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Flower Labs GmbH. All Rights Reserved.
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.