flwr-nightly 1.8.0.dev20240315__py3-none-any.whl → 1.15.0.dev20250115__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (312) hide show
  1. flwr/cli/app.py +16 -2
  2. flwr/cli/build.py +181 -0
  3. flwr/cli/cli_user_auth_interceptor.py +90 -0
  4. flwr/cli/config_utils.py +343 -0
  5. flwr/cli/example.py +4 -1
  6. flwr/cli/install.py +253 -0
  7. flwr/cli/log.py +182 -0
  8. flwr/{server/superlink/state → cli/login}/__init__.py +4 -10
  9. flwr/cli/login/login.py +88 -0
  10. flwr/cli/ls.py +327 -0
  11. flwr/cli/new/__init__.py +1 -0
  12. flwr/cli/new/new.py +210 -66
  13. flwr/cli/new/templates/app/.gitignore.tpl +163 -0
  14. flwr/cli/new/templates/app/LICENSE.tpl +202 -0
  15. flwr/cli/new/templates/app/README.baseline.md.tpl +127 -0
  16. flwr/cli/new/templates/app/README.flowertune.md.tpl +66 -0
  17. flwr/cli/new/templates/app/README.md.tpl +16 -32
  18. flwr/cli/new/templates/app/code/__init__.baseline.py.tpl +1 -0
  19. flwr/cli/new/templates/app/code/__init__.py.tpl +1 -1
  20. flwr/cli/new/templates/app/code/client.baseline.py.tpl +58 -0
  21. flwr/cli/new/templates/app/code/client.huggingface.py.tpl +55 -0
  22. flwr/cli/new/templates/app/code/client.jax.py.tpl +50 -0
  23. flwr/cli/new/templates/app/code/client.mlx.py.tpl +73 -0
  24. flwr/cli/new/templates/app/code/client.numpy.py.tpl +7 -7
  25. flwr/cli/new/templates/app/code/client.pytorch.py.tpl +30 -21
  26. flwr/cli/new/templates/app/code/client.sklearn.py.tpl +63 -0
  27. flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +57 -1
  28. flwr/cli/new/templates/app/code/dataset.baseline.py.tpl +36 -0
  29. flwr/cli/new/templates/app/code/flwr_tune/__init__.py +15 -0
  30. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +126 -0
  31. flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +87 -0
  32. flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +78 -0
  33. flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +94 -0
  34. flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +83 -0
  35. flwr/cli/new/templates/app/code/model.baseline.py.tpl +80 -0
  36. flwr/cli/new/templates/app/code/server.baseline.py.tpl +46 -0
  37. flwr/cli/new/templates/app/code/server.huggingface.py.tpl +38 -0
  38. flwr/cli/new/templates/app/code/server.jax.py.tpl +26 -0
  39. flwr/cli/new/templates/app/code/server.mlx.py.tpl +31 -0
  40. flwr/cli/new/templates/app/code/server.numpy.py.tpl +22 -9
  41. flwr/cli/new/templates/app/code/server.pytorch.py.tpl +21 -18
  42. flwr/cli/new/templates/app/code/server.sklearn.py.tpl +36 -0
  43. flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +29 -1
  44. flwr/cli/new/templates/app/code/strategy.baseline.py.tpl +1 -0
  45. flwr/cli/new/templates/app/code/task.huggingface.py.tpl +102 -0
  46. flwr/cli/new/templates/app/code/task.jax.py.tpl +57 -0
  47. flwr/cli/new/templates/app/code/task.mlx.py.tpl +102 -0
  48. flwr/cli/new/templates/app/code/task.numpy.py.tpl +7 -0
  49. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +29 -24
  50. flwr/cli/new/templates/app/code/task.sklearn.py.tpl +67 -0
  51. flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +53 -0
  52. flwr/cli/new/templates/app/code/utils.baseline.py.tpl +1 -0
  53. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +138 -0
  54. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +68 -0
  55. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +46 -0
  56. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +35 -0
  57. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +39 -0
  58. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +25 -12
  59. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +29 -14
  60. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +35 -0
  61. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +29 -14
  62. flwr/cli/run/__init__.py +1 -0
  63. flwr/cli/run/run.py +212 -34
  64. flwr/cli/stop.py +130 -0
  65. flwr/cli/utils.py +240 -5
  66. flwr/client/__init__.py +3 -2
  67. flwr/client/app.py +432 -255
  68. flwr/client/client.py +1 -11
  69. flwr/client/client_app.py +74 -13
  70. flwr/client/clientapp/__init__.py +22 -0
  71. flwr/client/clientapp/app.py +259 -0
  72. flwr/client/clientapp/clientappio_servicer.py +244 -0
  73. flwr/client/clientapp/utils.py +115 -0
  74. flwr/client/dpfedavg_numpy_client.py +7 -8
  75. flwr/client/grpc_adapter_client/__init__.py +15 -0
  76. flwr/client/grpc_adapter_client/connection.py +98 -0
  77. flwr/client/grpc_client/connection.py +21 -7
  78. flwr/client/grpc_rere_client/__init__.py +1 -1
  79. flwr/client/grpc_rere_client/client_interceptor.py +176 -0
  80. flwr/client/grpc_rere_client/connection.py +163 -56
  81. flwr/client/grpc_rere_client/grpc_adapter.py +167 -0
  82. flwr/client/heartbeat.py +74 -0
  83. flwr/client/message_handler/__init__.py +1 -1
  84. flwr/client/message_handler/message_handler.py +10 -11
  85. flwr/client/mod/__init__.py +5 -5
  86. flwr/client/mod/centraldp_mods.py +4 -2
  87. flwr/client/mod/comms_mods.py +5 -4
  88. flwr/client/mod/localdp_mod.py +10 -5
  89. flwr/client/mod/secure_aggregation/__init__.py +1 -1
  90. flwr/client/mod/secure_aggregation/secaggplus_mod.py +26 -26
  91. flwr/client/mod/utils.py +2 -4
  92. flwr/client/nodestate/__init__.py +26 -0
  93. flwr/client/nodestate/in_memory_nodestate.py +38 -0
  94. flwr/client/nodestate/nodestate.py +31 -0
  95. flwr/client/nodestate/nodestate_factory.py +38 -0
  96. flwr/client/numpy_client.py +8 -31
  97. flwr/client/rest_client/__init__.py +1 -1
  98. flwr/client/rest_client/connection.py +199 -176
  99. flwr/client/run_info_store.py +112 -0
  100. flwr/client/supernode/__init__.py +24 -0
  101. flwr/client/supernode/app.py +321 -0
  102. flwr/client/typing.py +1 -0
  103. flwr/common/__init__.py +17 -11
  104. flwr/common/address.py +47 -3
  105. flwr/common/args.py +153 -0
  106. flwr/common/auth_plugin/__init__.py +24 -0
  107. flwr/common/auth_plugin/auth_plugin.py +121 -0
  108. flwr/common/config.py +243 -0
  109. flwr/common/constant.py +135 -1
  110. flwr/common/context.py +32 -2
  111. flwr/common/date.py +22 -4
  112. flwr/common/differential_privacy.py +2 -2
  113. flwr/common/dp.py +2 -4
  114. flwr/common/exit_handlers.py +3 -3
  115. flwr/common/grpc.py +164 -5
  116. flwr/common/logger.py +230 -12
  117. flwr/common/message.py +191 -106
  118. flwr/common/object_ref.py +179 -44
  119. flwr/common/pyproject.py +1 -0
  120. flwr/common/record/__init__.py +2 -1
  121. flwr/common/record/configsrecord.py +58 -18
  122. flwr/common/record/metricsrecord.py +57 -17
  123. flwr/common/record/parametersrecord.py +88 -20
  124. flwr/common/record/recordset.py +153 -30
  125. flwr/common/record/typeddict.py +30 -55
  126. flwr/common/recordset_compat.py +31 -12
  127. flwr/common/retry_invoker.py +123 -30
  128. flwr/common/secure_aggregation/__init__.py +1 -1
  129. flwr/common/secure_aggregation/crypto/__init__.py +1 -1
  130. flwr/common/secure_aggregation/crypto/shamir.py +11 -11
  131. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +68 -4
  132. flwr/common/secure_aggregation/ndarrays_arithmetic.py +17 -17
  133. flwr/common/secure_aggregation/quantization.py +8 -8
  134. flwr/common/secure_aggregation/secaggplus_constants.py +1 -1
  135. flwr/common/secure_aggregation/secaggplus_utils.py +10 -12
  136. flwr/common/serde.py +304 -23
  137. flwr/common/telemetry.py +65 -29
  138. flwr/common/typing.py +120 -19
  139. flwr/common/version.py +17 -3
  140. flwr/proto/clientappio_pb2.py +45 -0
  141. flwr/proto/clientappio_pb2.pyi +132 -0
  142. flwr/proto/clientappio_pb2_grpc.py +135 -0
  143. flwr/proto/clientappio_pb2_grpc.pyi +53 -0
  144. flwr/proto/exec_pb2.py +62 -0
  145. flwr/proto/exec_pb2.pyi +212 -0
  146. flwr/proto/exec_pb2_grpc.py +237 -0
  147. flwr/proto/exec_pb2_grpc.pyi +93 -0
  148. flwr/proto/fab_pb2.py +31 -0
  149. flwr/proto/fab_pb2.pyi +65 -0
  150. flwr/proto/fab_pb2_grpc.py +4 -0
  151. flwr/proto/fab_pb2_grpc.pyi +4 -0
  152. flwr/proto/fleet_pb2.py +42 -23
  153. flwr/proto/fleet_pb2.pyi +123 -1
  154. flwr/proto/fleet_pb2_grpc.py +170 -0
  155. flwr/proto/fleet_pb2_grpc.pyi +61 -0
  156. flwr/proto/grpcadapter_pb2.py +32 -0
  157. flwr/proto/grpcadapter_pb2.pyi +43 -0
  158. flwr/proto/grpcadapter_pb2_grpc.py +66 -0
  159. flwr/proto/grpcadapter_pb2_grpc.pyi +24 -0
  160. flwr/proto/log_pb2.py +29 -0
  161. flwr/proto/log_pb2.pyi +39 -0
  162. flwr/proto/log_pb2_grpc.py +4 -0
  163. flwr/proto/log_pb2_grpc.pyi +4 -0
  164. flwr/proto/message_pb2.py +41 -0
  165. flwr/proto/message_pb2.pyi +128 -0
  166. flwr/proto/message_pb2_grpc.py +4 -0
  167. flwr/proto/message_pb2_grpc.pyi +4 -0
  168. flwr/proto/node_pb2.py +2 -2
  169. flwr/proto/node_pb2.pyi +1 -4
  170. flwr/proto/recordset_pb2.py +35 -33
  171. flwr/proto/recordset_pb2.pyi +40 -14
  172. flwr/proto/run_pb2.py +64 -0
  173. flwr/proto/run_pb2.pyi +268 -0
  174. flwr/proto/run_pb2_grpc.py +4 -0
  175. flwr/proto/run_pb2_grpc.pyi +4 -0
  176. flwr/proto/serverappio_pb2.py +52 -0
  177. flwr/proto/{driver_pb2.pyi → serverappio_pb2.pyi} +62 -20
  178. flwr/proto/serverappio_pb2_grpc.py +410 -0
  179. flwr/proto/serverappio_pb2_grpc.pyi +160 -0
  180. flwr/proto/simulationio_pb2.py +38 -0
  181. flwr/proto/simulationio_pb2.pyi +65 -0
  182. flwr/proto/simulationio_pb2_grpc.py +239 -0
  183. flwr/proto/simulationio_pb2_grpc.pyi +94 -0
  184. flwr/proto/task_pb2.py +7 -8
  185. flwr/proto/task_pb2.pyi +8 -5
  186. flwr/proto/transport_pb2.py +8 -8
  187. flwr/proto/transport_pb2.pyi +9 -6
  188. flwr/server/__init__.py +2 -10
  189. flwr/server/app.py +579 -402
  190. flwr/server/client_manager.py +8 -6
  191. flwr/server/compat/app.py +6 -62
  192. flwr/server/compat/app_utils.py +14 -9
  193. flwr/server/compat/driver_client_proxy.py +25 -59
  194. flwr/server/compat/legacy_context.py +5 -4
  195. flwr/server/driver/__init__.py +2 -0
  196. flwr/server/driver/driver.py +36 -131
  197. flwr/server/driver/grpc_driver.py +220 -81
  198. flwr/server/driver/inmemory_driver.py +183 -0
  199. flwr/server/history.py +28 -29
  200. flwr/server/run_serverapp.py +15 -126
  201. flwr/server/server.py +50 -44
  202. flwr/server/server_app.py +59 -10
  203. flwr/server/serverapp/__init__.py +22 -0
  204. flwr/server/serverapp/app.py +256 -0
  205. flwr/server/serverapp_components.py +52 -0
  206. flwr/server/strategy/__init__.py +2 -2
  207. flwr/server/strategy/aggregate.py +37 -23
  208. flwr/server/strategy/bulyan.py +9 -9
  209. flwr/server/strategy/dp_adaptive_clipping.py +25 -25
  210. flwr/server/strategy/dp_fixed_clipping.py +23 -22
  211. flwr/server/strategy/dpfedavg_adaptive.py +8 -8
  212. flwr/server/strategy/dpfedavg_fixed.py +13 -12
  213. flwr/server/strategy/fault_tolerant_fedavg.py +11 -11
  214. flwr/server/strategy/fedadagrad.py +9 -9
  215. flwr/server/strategy/fedadam.py +20 -10
  216. flwr/server/strategy/fedavg.py +16 -16
  217. flwr/server/strategy/fedavg_android.py +17 -17
  218. flwr/server/strategy/fedavgm.py +9 -9
  219. flwr/server/strategy/fedmedian.py +5 -5
  220. flwr/server/strategy/fedopt.py +6 -6
  221. flwr/server/strategy/fedprox.py +7 -7
  222. flwr/server/strategy/fedtrimmedavg.py +8 -8
  223. flwr/server/strategy/fedxgb_bagging.py +12 -12
  224. flwr/server/strategy/fedxgb_cyclic.py +10 -10
  225. flwr/server/strategy/fedxgb_nn_avg.py +6 -6
  226. flwr/server/strategy/fedyogi.py +9 -9
  227. flwr/server/strategy/krum.py +9 -9
  228. flwr/server/strategy/qfedavg.py +16 -16
  229. flwr/server/strategy/strategy.py +10 -10
  230. flwr/server/superlink/driver/__init__.py +2 -2
  231. flwr/server/superlink/driver/serverappio_grpc.py +61 -0
  232. flwr/server/superlink/driver/serverappio_servicer.py +361 -0
  233. flwr/server/superlink/ffs/__init__.py +24 -0
  234. flwr/server/superlink/ffs/disk_ffs.py +108 -0
  235. flwr/server/superlink/ffs/ffs.py +79 -0
  236. flwr/server/superlink/ffs/ffs_factory.py +47 -0
  237. flwr/server/superlink/fleet/__init__.py +1 -1
  238. flwr/server/superlink/fleet/grpc_adapter/__init__.py +15 -0
  239. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +162 -0
  240. flwr/server/superlink/fleet/grpc_bidi/__init__.py +1 -1
  241. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +4 -2
  242. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +3 -2
  243. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +1 -1
  244. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +5 -154
  245. flwr/server/superlink/fleet/grpc_rere/__init__.py +1 -1
  246. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +120 -13
  247. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +228 -0
  248. flwr/server/superlink/fleet/message_handler/__init__.py +1 -1
  249. flwr/server/superlink/fleet/message_handler/message_handler.py +156 -13
  250. flwr/server/superlink/fleet/rest_rere/__init__.py +1 -1
  251. flwr/server/superlink/fleet/rest_rere/rest_api.py +119 -81
  252. flwr/server/superlink/fleet/vce/__init__.py +1 -0
  253. flwr/server/superlink/fleet/vce/backend/__init__.py +4 -4
  254. flwr/server/superlink/fleet/vce/backend/backend.py +8 -9
  255. flwr/server/superlink/fleet/vce/backend/raybackend.py +87 -68
  256. flwr/server/superlink/fleet/vce/vce_api.py +208 -146
  257. flwr/server/superlink/linkstate/__init__.py +28 -0
  258. flwr/server/superlink/linkstate/in_memory_linkstate.py +569 -0
  259. flwr/server/superlink/linkstate/linkstate.py +376 -0
  260. flwr/server/superlink/{state/state_factory.py → linkstate/linkstate_factory.py} +19 -10
  261. flwr/server/superlink/linkstate/sqlite_linkstate.py +1196 -0
  262. flwr/server/superlink/linkstate/utils.py +399 -0
  263. flwr/server/superlink/simulation/__init__.py +15 -0
  264. flwr/server/superlink/simulation/simulationio_grpc.py +65 -0
  265. flwr/server/superlink/simulation/simulationio_servicer.py +186 -0
  266. flwr/server/superlink/utils.py +65 -0
  267. flwr/server/typing.py +2 -0
  268. flwr/server/utils/__init__.py +1 -1
  269. flwr/server/utils/tensorboard.py +5 -5
  270. flwr/server/utils/validator.py +40 -45
  271. flwr/server/workflow/default_workflows.py +70 -26
  272. flwr/server/workflow/secure_aggregation/secagg_workflow.py +1 -0
  273. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +40 -27
  274. flwr/simulation/__init__.py +12 -5
  275. flwr/simulation/app.py +247 -315
  276. flwr/simulation/legacy_app.py +404 -0
  277. flwr/simulation/ray_transport/__init__.py +1 -1
  278. flwr/simulation/ray_transport/ray_actor.py +42 -67
  279. flwr/simulation/ray_transport/ray_client_proxy.py +37 -17
  280. flwr/simulation/ray_transport/utils.py +1 -0
  281. flwr/simulation/run_simulation.py +306 -163
  282. flwr/simulation/simulationio_connection.py +89 -0
  283. flwr/superexec/__init__.py +15 -0
  284. flwr/superexec/app.py +59 -0
  285. flwr/superexec/deployment.py +188 -0
  286. flwr/superexec/exec_grpc.py +80 -0
  287. flwr/superexec/exec_servicer.py +231 -0
  288. flwr/superexec/exec_user_auth_interceptor.py +101 -0
  289. flwr/superexec/executor.py +96 -0
  290. flwr/superexec/simulation.py +124 -0
  291. {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.15.0.dev20250115.dist-info}/METADATA +33 -26
  292. flwr_nightly-1.15.0.dev20250115.dist-info/RECORD +328 -0
  293. flwr_nightly-1.15.0.dev20250115.dist-info/entry_points.txt +12 -0
  294. flwr/cli/flower_toml.py +0 -140
  295. flwr/cli/new/templates/app/flower.toml.tpl +0 -13
  296. flwr/cli/new/templates/app/requirements.numpy.txt.tpl +0 -2
  297. flwr/cli/new/templates/app/requirements.pytorch.txt.tpl +0 -4
  298. flwr/cli/new/templates/app/requirements.tensorflow.txt.tpl +0 -4
  299. flwr/client/node_state.py +0 -48
  300. flwr/client/node_state_tests.py +0 -65
  301. flwr/proto/driver_pb2.py +0 -44
  302. flwr/proto/driver_pb2_grpc.py +0 -169
  303. flwr/proto/driver_pb2_grpc.pyi +0 -66
  304. flwr/server/superlink/driver/driver_grpc.py +0 -54
  305. flwr/server/superlink/driver/driver_servicer.py +0 -129
  306. flwr/server/superlink/state/in_memory_state.py +0 -230
  307. flwr/server/superlink/state/sqlite_state.py +0 -630
  308. flwr/server/superlink/state/state.py +0 -154
  309. flwr_nightly-1.8.0.dev20240315.dist-info/RECORD +0 -211
  310. flwr_nightly-1.8.0.dev20240315.dist-info/entry_points.txt +0 -9
  311. {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.15.0.dev20250115.dist-info}/LICENSE +0 -0
  312. {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.15.0.dev20250115.dist-info}/WHEEL +0 -0
flwr/client/app.py CHANGED
@@ -15,177 +15,64 @@
15
15
  """Flower client app."""
16
16
 
17
17
 
18
- import argparse
18
+ import multiprocessing
19
+ import signal
19
20
  import sys
20
21
  import time
21
- from logging import DEBUG, INFO, WARN
22
+ from contextlib import AbstractContextManager
23
+ from dataclasses import dataclass
24
+ from logging import ERROR, INFO, WARN
25
+ from os import urandom
22
26
  from pathlib import Path
23
- from typing import Callable, ContextManager, Optional, Tuple, Type, Union
27
+ from typing import Callable, Optional, Union, cast
24
28
 
29
+ import grpc
30
+ from cryptography.hazmat.primitives.asymmetric import ec
25
31
  from grpc import RpcError
26
32
 
33
+ from flwr.cli.config_utils import get_fab_metadata
34
+ from flwr.cli.install import install_from_fab
27
35
  from flwr.client.client import Client
28
36
  from flwr.client.client_app import ClientApp, LoadClientAppError
29
- from flwr.client.typing import ClientFn
30
- from flwr.common import GRPC_MAX_MESSAGE_LENGTH, EventType, Message, event
37
+ from flwr.client.clientapp.app import flwr_clientapp
38
+ from flwr.client.nodestate.nodestate_factory import NodeStateFactory
39
+ from flwr.client.typing import ClientFnExt
40
+ from flwr.common import GRPC_MAX_MESSAGE_LENGTH, Context, EventType, Message, event
31
41
  from flwr.common.address import parse_address
32
42
  from flwr.common.constant import (
43
+ CLIENT_OCTET,
44
+ CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS,
45
+ ISOLATION_MODE_PROCESS,
46
+ ISOLATION_MODE_SUBPROCESS,
47
+ MAX_RETRY_DELAY,
33
48
  MISSING_EXTRA_REST,
49
+ RUN_ID_NUM_BYTES,
50
+ SERVER_OCTET,
51
+ TRANSPORT_TYPE_GRPC_ADAPTER,
34
52
  TRANSPORT_TYPE_GRPC_BIDI,
35
53
  TRANSPORT_TYPE_GRPC_RERE,
36
54
  TRANSPORT_TYPE_REST,
37
55
  TRANSPORT_TYPES,
56
+ ErrorCode,
38
57
  )
39
- from flwr.common.exit_handlers import register_exit_handlers
40
- from flwr.common.logger import log, warn_deprecated_feature, warn_experimental_feature
41
- from flwr.common.object_ref import load_app, validate
42
- from flwr.common.retry_invoker import RetryInvoker, exponential
43
-
58
+ from flwr.common.grpc import generic_create_grpc_server
59
+ from flwr.common.logger import log, warn_deprecated_feature
60
+ from flwr.common.message import Error
61
+ from flwr.common.retry_invoker import RetryInvoker, RetryState, exponential
62
+ from flwr.common.typing import Fab, Run, RunNotRunningException, UserConfig
63
+ from flwr.proto.clientappio_pb2_grpc import add_ClientAppIoServicer_to_server
64
+
65
+ from .clientapp.clientappio_servicer import ClientAppInputs, ClientAppIoServicer
66
+ from .grpc_adapter_client.connection import grpc_adapter
44
67
  from .grpc_client.connection import grpc_connection
45
68
  from .grpc_rere_client.connection import grpc_request_response
46
69
  from .message_handler.message_handler import handle_control_message
47
- from .node_state import NodeState
48
70
  from .numpy_client import NumPyClient
49
-
50
-
51
- def run_client_app() -> None:
52
- """Run Flower client app."""
53
- event(EventType.RUN_CLIENT_APP_ENTER)
54
-
55
- log(INFO, "Long-running Flower client starting")
56
-
57
- args = _parse_args_run_client_app().parse_args()
58
-
59
- # Obtain certificates
60
- if args.insecure:
61
- if args.root_certificates is not None:
62
- sys.exit(
63
- "Conflicting options: The '--insecure' flag disables HTTPS, "
64
- "but '--root-certificates' was also specified. Please remove "
65
- "the '--root-certificates' option when running in insecure mode, "
66
- "or omit '--insecure' to use HTTPS."
67
- )
68
- log(
69
- WARN,
70
- "Option `--insecure` was set. "
71
- "Starting insecure HTTP client connected to %s.",
72
- args.server,
73
- )
74
- root_certificates = None
75
- else:
76
- # Load the certificates if provided, or load the system certificates
77
- cert_path = args.root_certificates
78
- if cert_path is None:
79
- root_certificates = None
80
- else:
81
- root_certificates = Path(cert_path).read_bytes()
82
- log(
83
- DEBUG,
84
- "Starting secure HTTPS client connected to %s "
85
- "with the following certificates: %s.",
86
- args.server,
87
- cert_path,
88
- )
89
-
90
- log(
91
- DEBUG,
92
- "Flower will load ClientApp `%s`",
93
- getattr(args, "client-app"),
94
- )
95
-
96
- client_app_dir = args.dir
97
- if client_app_dir is not None:
98
- sys.path.insert(0, client_app_dir)
99
-
100
- app_ref: str = getattr(args, "client-app")
101
- valid, error_msg = validate(app_ref)
102
- if not valid and error_msg:
103
- raise LoadClientAppError(error_msg) from None
104
-
105
- def _load() -> ClientApp:
106
- client_app = load_app(app_ref, LoadClientAppError)
107
-
108
- if not isinstance(client_app, ClientApp):
109
- raise LoadClientAppError(
110
- f"Attribute {app_ref} is not of type {ClientApp}",
111
- ) from None
112
-
113
- return client_app
114
-
115
- _start_client_internal(
116
- server_address=args.server,
117
- load_client_app_fn=_load,
118
- transport="rest" if args.rest else "grpc-rere",
119
- root_certificates=root_certificates,
120
- insecure=args.insecure,
121
- max_retries=args.max_retries,
122
- max_wait_time=args.max_wait_time,
123
- )
124
- register_exit_handlers(event_type=EventType.RUN_CLIENT_APP_LEAVE)
125
-
126
-
127
- def _parse_args_run_client_app() -> argparse.ArgumentParser:
128
- """Parse flower-client-app command line arguments."""
129
- parser = argparse.ArgumentParser(
130
- description="Start a Flower client app",
131
- )
132
-
133
- parser.add_argument(
134
- "client-app",
135
- help="For example: `client:app` or `project.package.module:wrapper.app`",
136
- )
137
- parser.add_argument(
138
- "--insecure",
139
- action="store_true",
140
- help="Run the client without HTTPS. By default, the client runs with "
141
- "HTTPS enabled. Use this flag only if you understand the risks.",
142
- )
143
- parser.add_argument(
144
- "--rest",
145
- action="store_true",
146
- help="Use REST as a transport layer for the client.",
147
- )
148
- parser.add_argument(
149
- "--root-certificates",
150
- metavar="ROOT_CERT",
151
- type=str,
152
- help="Specifies the path to the PEM-encoded root certificate file for "
153
- "establishing secure HTTPS connections.",
154
- )
155
- parser.add_argument(
156
- "--server",
157
- default="0.0.0.0:9092",
158
- help="Server address",
159
- )
160
- parser.add_argument(
161
- "--max-retries",
162
- type=int,
163
- default=None,
164
- help="The maximum number of times the client will try to connect to the"
165
- "server before giving up in case of a connection error. By default,"
166
- "it is set to None, meaning there is no limit to the number of tries.",
167
- )
168
- parser.add_argument(
169
- "--max-wait-time",
170
- type=float,
171
- default=None,
172
- help="The maximum duration before the client stops trying to"
173
- "connect to the server in case of connection error. By default, it"
174
- "is set to None, meaning there is no limit to the total time.",
175
- )
176
- parser.add_argument(
177
- "--dir",
178
- default="",
179
- help="Add specified directory to the PYTHONPATH and load Flower "
180
- "app from there."
181
- " Default: current working directory.",
182
- )
183
-
184
- return parser
71
+ from .run_info_store import DeprecatedRunInfoStore
185
72
 
186
73
 
187
74
  def _check_actionable_client(
188
- client: Optional[Client], client_fn: Optional[ClientFn]
75
+ client: Optional[Client], client_fn: Optional[ClientFnExt]
189
76
  ) -> None:
190
77
  if client_fn is None and client is None:
191
78
  raise ValueError(
@@ -206,24 +93,32 @@ def _check_actionable_client(
206
93
  def start_client(
207
94
  *,
208
95
  server_address: str,
209
- client_fn: Optional[ClientFn] = None,
96
+ client_fn: Optional[ClientFnExt] = None,
210
97
  client: Optional[Client] = None,
211
98
  grpc_max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
212
99
  root_certificates: Optional[Union[bytes, str]] = None,
213
100
  insecure: Optional[bool] = None,
214
101
  transport: Optional[str] = None,
102
+ authentication_keys: Optional[
103
+ tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]
104
+ ] = None,
215
105
  max_retries: Optional[int] = None,
216
106
  max_wait_time: Optional[float] = None,
217
107
  ) -> None:
218
108
  """Start a Flower client node which connects to a Flower server.
219
109
 
110
+ Warning
111
+ -------
112
+ This function is deprecated since 1.13.0. Use :code:`flower-supernode` command
113
+ instead to start a SuperNode.
114
+
220
115
  Parameters
221
116
  ----------
222
117
  server_address : str
223
118
  The IPv4 or IPv6 address of the server. If the Flower
224
119
  server runs on the same machine on port 8080, then `server_address`
225
120
  would be `"[::]:8080"`.
226
- client_fn : Optional[ClientFn]
121
+ client_fn : Optional[ClientFnExt]
227
122
  A callable that instantiates a Client. (default: None)
228
123
  client : Optional[flwr.client.Client]
229
124
  An implementation of the abstract base
@@ -247,6 +142,11 @@ def start_client(
247
142
  - 'grpc-bidi': gRPC, bidirectional streaming
248
143
  - 'grpc-rere': gRPC, request-response (experimental)
249
144
  - 'rest': HTTP (experimental)
145
+ authentication_keys : Optional[Tuple[PrivateKey, PublicKey]] (default: None)
146
+ Tuple containing the elliptic curve private key and public key for
147
+ authentication from the cryptography library.
148
+ Source: https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/
149
+ Used to establish an authenticated connection with the server.
250
150
  max_retries: Optional[int] (default: None)
251
151
  The maximum number of times the client will try to connect to the
252
152
  server before giving up in case of a connection error. If set to None,
@@ -267,8 +167,8 @@ def start_client(
267
167
 
268
168
  Starting an SSL-enabled gRPC client using system certificates:
269
169
 
270
- >>> def client_fn(cid: str):
271
- >>> return FlowerClient()
170
+ >>> def client_fn(context: Context):
171
+ >>> return FlowerClient().to_client()
272
172
  >>>
273
173
  >>> start_client(
274
174
  >>> server_address=localhost:8080,
@@ -286,9 +186,21 @@ def start_client(
286
186
  >>> root_certificates=Path("/crts/root.pem").read_bytes(),
287
187
  >>> )
288
188
  """
189
+ msg = (
190
+ "flwr.client.start_client() is deprecated."
191
+ "\n\tInstead, use the `flower-supernode` CLI command to start a SuperNode "
192
+ "as shown below:"
193
+ "\n\n\t\t$ flower-supernode --insecure --superlink='<IP>:<PORT>'"
194
+ "\n\n\tTo view all available options, run:"
195
+ "\n\n\t\t$ flower-supernode --help"
196
+ "\n\n\tUsing `start_client()` is deprecated."
197
+ )
198
+ warn_deprecated_feature(name=msg)
199
+
289
200
  event(EventType.START_CLIENT_ENTER)
290
- _start_client_internal(
201
+ start_client_internal(
291
202
  server_address=server_address,
203
+ node_config={},
292
204
  load_client_app_fn=None,
293
205
  client_fn=client_fn,
294
206
  client=client,
@@ -296,6 +208,7 @@ def start_client(
296
208
  root_certificates=root_certificates,
297
209
  insecure=insecure,
298
210
  transport=transport,
211
+ authentication_keys=authentication_keys,
299
212
  max_retries=max_retries,
300
213
  max_wait_time=max_wait_time,
301
214
  )
@@ -306,18 +219,25 @@ def start_client(
306
219
  # pylint: disable=too-many-branches
307
220
  # pylint: disable=too-many-locals
308
221
  # pylint: disable=too-many-statements
309
- def _start_client_internal(
222
+ def start_client_internal(
310
223
  *,
311
224
  server_address: str,
312
- load_client_app_fn: Optional[Callable[[], ClientApp]] = None,
313
- client_fn: Optional[ClientFn] = None,
225
+ node_config: UserConfig,
226
+ load_client_app_fn: Optional[Callable[[str, str, str], ClientApp]] = None,
227
+ client_fn: Optional[ClientFnExt] = None,
314
228
  client: Optional[Client] = None,
315
229
  grpc_max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
316
230
  root_certificates: Optional[Union[bytes, str]] = None,
317
231
  insecure: Optional[bool] = None,
318
232
  transport: Optional[str] = None,
233
+ authentication_keys: Optional[
234
+ tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]
235
+ ] = None,
319
236
  max_retries: Optional[int] = None,
320
237
  max_wait_time: Optional[float] = None,
238
+ flwr_path: Optional[Path] = None,
239
+ isolation: Optional[str] = None,
240
+ clientappio_api_address: Optional[str] = CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS,
321
241
  ) -> None:
322
242
  """Start a Flower client node which connects to a Flower server.
323
243
 
@@ -327,9 +247,11 @@ def _start_client_internal(
327
247
  The IPv4 or IPv6 address of the server. If the Flower
328
248
  server runs on the same machine on port 8080, then `server_address`
329
249
  would be `"[::]:8080"`.
250
+ node_config: UserConfig
251
+ The configuration of the node.
330
252
  load_client_app_fn : Optional[Callable[[], ClientApp]] (default: None)
331
253
  A function that can be used to load a `ClientApp` instance.
332
- client_fn : Optional[ClientFn]
254
+ client_fn : Optional[ClientFnExt]
333
255
  A callable that instantiates a Client. (default: None)
334
256
  client : Optional[flwr.client.Client]
335
257
  An implementation of the abstract base
@@ -353,6 +275,11 @@ def _start_client_internal(
353
275
  - 'grpc-bidi': gRPC, bidirectional streaming
354
276
  - 'grpc-rere': gRPC, request-response (experimental)
355
277
  - 'rest': HTTP (experimental)
278
+ authentication_keys : Optional[Tuple[PrivateKey, PublicKey]] (default: None)
279
+ Tuple containing the elliptic curve private key and public key for
280
+ authentication from the cryptography library.
281
+ Source: https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/
282
+ Used to establish an authenticated connection with the server.
356
283
  max_retries: Optional[int] (default: None)
357
284
  The maximum number of times the client will try to connect to the
358
285
  server before giving up in case of a connection error. If set to None,
@@ -361,6 +288,19 @@ def _start_client_internal(
361
288
  The maximum duration before the client stops trying to
362
289
  connect to the server in case of connection error.
363
290
  If set to None, there is no limit to the total time.
291
+ flwr_path: Optional[Path] (default: None)
292
+ The fully resolved path containing installed Flower Apps.
293
+ isolation : Optional[str] (default: None)
294
+ Isolation mode for `ClientApp`. Possible values are `subprocess` and
295
+ `process`. Defaults to `None`, which runs the `ClientApp` in the same process
296
+ as the SuperNode. If `subprocess`, the `ClientApp` runs in a subprocess started
297
+ by the SueprNode and communicates using gRPC at the address
298
+ `clientappio_api_address`. If `process`, the `ClientApp` runs in a separate
299
+ isolated process and communicates using gRPC at the address
300
+ `clientappio_api_address`.
301
+ clientappio_api_address : Optional[str]
302
+ (default: `CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS`)
303
+ The SuperNode gRPC server address.
364
304
  """
365
305
  if insecure is None:
366
306
  insecure = root_certificates is None
@@ -371,7 +311,7 @@ def _start_client_internal(
371
311
  if client_fn is None:
372
312
  # Wrap `Client` instance in `client_fn`
373
313
  def single_client_factory(
374
- cid: str, # pylint: disable=unused-argument
314
+ context: Context, # pylint: disable=unused-argument
375
315
  ) -> Client:
376
316
  if client is None: # Added this to keep mypy happy
377
317
  raise ValueError(
@@ -381,12 +321,22 @@ def _start_client_internal(
381
321
 
382
322
  client_fn = single_client_factory
383
323
 
384
- def _load_client_app() -> ClientApp:
324
+ def _load_client_app(_1: str, _2: str, _3: str) -> ClientApp:
385
325
  return ClientApp(client_fn=client_fn)
386
326
 
387
327
  load_client_app_fn = _load_client_app
388
- else:
389
- warn_experimental_feature("`load_client_app_fn`")
328
+
329
+ if isolation:
330
+ if clientappio_api_address is None:
331
+ raise ValueError(
332
+ f"`clientappio_api_address` required when `isolation` is "
333
+ f"{ISOLATION_MODE_SUBPROCESS} or {ISOLATION_MODE_PROCESS}",
334
+ )
335
+ _clientappio_grpc_server, clientappio_servicer = run_clientappio_api_grpc(
336
+ address=clientappio_api_address,
337
+ certificates=None,
338
+ )
339
+ clientappio_api_address = cast(str, clientappio_api_address)
390
340
 
391
341
  # At this point, only `load_client_app_fn` should be used
392
342
  # Both `client` and `client_fn` must not be used directly
@@ -396,10 +346,33 @@ def _start_client_internal(
396
346
  transport, server_address
397
347
  )
398
348
 
349
+ app_state_tracker = _AppStateTracker()
350
+
351
+ def _on_sucess(retry_state: RetryState) -> None:
352
+ app_state_tracker.is_connected = True
353
+ if retry_state.tries > 1:
354
+ log(
355
+ INFO,
356
+ "Connection successful after %.2f seconds and %s tries.",
357
+ retry_state.elapsed_time,
358
+ retry_state.tries,
359
+ )
360
+
361
+ def _on_backoff(retry_state: RetryState) -> None:
362
+ app_state_tracker.is_connected = False
363
+ if retry_state.tries == 1:
364
+ log(WARN, "Connection attempt failed, retrying...")
365
+ else:
366
+ log(
367
+ WARN,
368
+ "Connection attempt failed, retrying in %.2f seconds",
369
+ retry_state.actual_wait,
370
+ )
371
+
399
372
  retry_invoker = RetryInvoker(
400
- wait_factory=exponential,
373
+ wait_gen_factory=lambda: exponential(max_delay=MAX_RETRY_DELAY),
401
374
  recoverable_exceptions=connection_error_type,
402
- max_tries=max_retries,
375
+ max_tries=max_retries + 1 if max_retries is not None else None,
403
376
  max_time=max_wait_time,
404
377
  on_giveup=lambda retry_state: (
405
378
  log(
@@ -411,30 +384,19 @@ def _start_client_internal(
411
384
  if retry_state.tries > 1
412
385
  else None
413
386
  ),
414
- on_success=lambda retry_state: (
415
- log(
416
- INFO,
417
- "Connection successful after %.2f seconds and %s tries.",
418
- retry_state.elapsed_time,
419
- retry_state.tries,
420
- )
421
- if retry_state.tries > 1
422
- else None
423
- ),
424
- on_backoff=lambda retry_state: (
425
- log(WARN, "Connection attempt failed, retrying...")
426
- if retry_state.tries == 1
427
- else log(
428
- DEBUG,
429
- "Connection attempt failed, retrying in %.2f seconds",
430
- retry_state.actual_wait,
431
- )
432
- ),
387
+ on_success=_on_sucess,
388
+ on_backoff=_on_backoff,
433
389
  )
434
390
 
435
- node_state = NodeState()
391
+ # DeprecatedRunInfoStore gets initialized when the first connection is established
392
+ run_info_store: Optional[DeprecatedRunInfoStore] = None
393
+ state_factory = NodeStateFactory()
394
+ state = state_factory.state()
395
+ mp_spawn_context = multiprocessing.get_context("spawn")
396
+
397
+ runs: dict[int, Run] = {}
436
398
 
437
- while True:
399
+ while not app_state_tracker.interrupt:
438
400
  sleep_duration: int = 0
439
401
  with connection(
440
402
  address,
@@ -442,80 +404,241 @@ def _start_client_internal(
442
404
  retry_invoker,
443
405
  grpc_max_message_length,
444
406
  root_certificates,
407
+ authentication_keys,
445
408
  ) as conn:
446
- receive, send, create_node, delete_node = conn
447
-
448
- # Register node
449
- if create_node is not None:
450
- create_node() # pylint: disable=not-callable
451
-
452
- while True:
453
- # Receive
454
- message = receive()
455
- if message is None:
456
- time.sleep(3) # Wait for 3s before asking again
457
- continue
458
-
459
- log(INFO, "")
460
- log(
461
- INFO,
462
- "[RUN %s, ROUND %s]",
463
- message.metadata.run_id,
464
- message.metadata.group_id,
465
- )
466
- log(
467
- INFO,
468
- "Received: %s message %s",
469
- message.metadata.message_type,
470
- message.metadata.message_id,
471
- )
472
-
473
- # Handle control message
474
- out_message, sleep_duration = handle_control_message(message)
475
- if out_message:
476
- send(out_message)
477
- break
409
+ receive, send, create_node, delete_node, get_run, get_fab = conn
410
+
411
+ # Register node when connecting the first time
412
+ if run_info_store is None:
413
+ if create_node is None:
414
+ if transport not in ["grpc-bidi", None]:
415
+ raise NotImplementedError(
416
+ "All transports except `grpc-bidi` require "
417
+ "an implementation for `create_node()`.'"
418
+ )
419
+ # gRPC-bidi doesn't have the concept of node_id,
420
+ # so we set it to -1
421
+ run_info_store = DeprecatedRunInfoStore(
422
+ node_id=-1,
423
+ node_config={},
424
+ )
425
+ else:
426
+ # Call create_node fn to register node
427
+ # and store node_id in state
428
+ if (node_id := create_node()) is None:
429
+ raise ValueError(
430
+ "Failed to register SuperNode with the SuperLink"
431
+ )
432
+ state.set_node_id(node_id)
433
+ run_info_store = DeprecatedRunInfoStore(
434
+ node_id=state.get_node_id(),
435
+ node_config=node_config,
436
+ )
437
+
438
+ app_state_tracker.register_signal_handler()
439
+ # pylint: disable=too-many-nested-blocks
440
+ while not app_state_tracker.interrupt:
441
+ try:
442
+ # Receive
443
+ message = receive()
444
+ if message is None:
445
+ time.sleep(3) # Wait for 3s before asking again
446
+ continue
447
+
448
+ log(INFO, "")
449
+ if len(message.metadata.group_id) > 0:
450
+ log(
451
+ INFO,
452
+ "[RUN %s, ROUND %s]",
453
+ message.metadata.run_id,
454
+ message.metadata.group_id,
455
+ )
456
+ log(
457
+ INFO,
458
+ "Received: %s message %s",
459
+ message.metadata.message_type,
460
+ message.metadata.message_id,
461
+ )
478
462
 
479
- # Register context for this run
480
- node_state.register_context(run_id=message.metadata.run_id)
481
-
482
- # Retrieve context for this run
483
- context = node_state.retrieve_context(run_id=message.metadata.run_id)
484
-
485
- # Load ClientApp instance
486
- client_app: ClientApp = load_client_app_fn()
487
-
488
- # Handle task message
489
- out_message = client_app(message=message, context=context)
490
-
491
- # Update node state
492
- node_state.update_context(
493
- run_id=message.metadata.run_id,
494
- context=context,
495
- )
496
-
497
- # Send
498
- send(out_message)
499
- log(
500
- INFO,
501
- "[RUN %s, ROUND %s]",
502
- out_message.metadata.run_id,
503
- out_message.metadata.group_id,
504
- )
505
- log(
506
- INFO,
507
- "Sent: %s reply to message %s",
508
- out_message.metadata.message_type,
509
- message.metadata.message_id,
510
- )
463
+ # Handle control message
464
+ out_message, sleep_duration = handle_control_message(message)
465
+ if out_message:
466
+ send(out_message)
467
+ break
468
+
469
+ # Get run info
470
+ run_id = message.metadata.run_id
471
+ if run_id not in runs:
472
+ if get_run is not None:
473
+ runs[run_id] = get_run(run_id)
474
+ # If get_run is None, i.e., in grpc-bidi mode
475
+ else:
476
+ runs[run_id] = Run.create_empty(run_id=run_id)
477
+
478
+ run: Run = runs[run_id]
479
+ if get_fab is not None and run.fab_hash:
480
+ fab = get_fab(run.fab_hash, run_id)
481
+ if not isolation:
482
+ # If `ClientApp` runs in the same process, install the FAB
483
+ install_from_fab(fab.content, flwr_path, True)
484
+ fab_id, fab_version = get_fab_metadata(fab.content)
485
+ else:
486
+ fab = None
487
+ fab_id, fab_version = run.fab_id, run.fab_version
488
+
489
+ run.fab_id, run.fab_version = fab_id, fab_version
490
+
491
+ # Register context for this run
492
+ run_info_store.register_context(
493
+ run_id=run_id,
494
+ run=run,
495
+ flwr_path=flwr_path,
496
+ fab=fab,
497
+ )
498
+
499
+ # Retrieve context for this run
500
+ context = run_info_store.retrieve_context(run_id=run_id)
501
+ # Create an error reply message that will never be used to prevent
502
+ # the used-before-assignment linting error
503
+ reply_message = message.create_error_reply(
504
+ error=Error(code=ErrorCode.UNKNOWN, reason="Unknown")
505
+ )
506
+
507
+ # Handle app loading and task message
508
+ try:
509
+ if isolation:
510
+ # Two isolation modes:
511
+ # 1. `subprocess`: SuperNode is starting the ClientApp
512
+ # process as a subprocess.
513
+ # 2. `process`: ClientApp process gets started separately
514
+ # (via `flwr-clientapp`), for example, in a separate
515
+ # Docker container.
516
+
517
+ # Generate SuperNode token
518
+ token = int.from_bytes(urandom(RUN_ID_NUM_BYTES), "little")
519
+
520
+ # Mode 1: SuperNode starts ClientApp as subprocess
521
+ start_subprocess = isolation == ISOLATION_MODE_SUBPROCESS
522
+
523
+ # Share Message and Context with servicer
524
+ clientappio_servicer.set_inputs(
525
+ clientapp_input=ClientAppInputs(
526
+ message=message,
527
+ context=context,
528
+ run=run,
529
+ fab=fab,
530
+ token=token,
531
+ ),
532
+ token_returned=start_subprocess,
533
+ )
534
+
535
+ if start_subprocess:
536
+ _octet, _colon, _port = (
537
+ clientappio_api_address.rpartition(":")
538
+ )
539
+ io_address = (
540
+ f"{CLIENT_OCTET}:{_port}"
541
+ if _octet == SERVER_OCTET
542
+ else clientappio_api_address
543
+ )
544
+ # Start ClientApp subprocess
545
+ command = [
546
+ "flwr-clientapp",
547
+ "--clientappio-api-address",
548
+ io_address,
549
+ "--token",
550
+ str(token),
551
+ ]
552
+ command.append("--insecure")
553
+
554
+ proc = mp_spawn_context.Process(
555
+ target=_run_flwr_clientapp,
556
+ args=(command,),
557
+ daemon=True,
558
+ )
559
+ proc.start()
560
+ proc.join()
561
+ else:
562
+ # Wait for output to become available
563
+ while not clientappio_servicer.has_outputs():
564
+ time.sleep(0.1)
565
+
566
+ outputs = clientappio_servicer.get_outputs()
567
+ reply_message, context = outputs.message, outputs.context
568
+ else:
569
+ # Load ClientApp instance
570
+ client_app: ClientApp = load_client_app_fn(
571
+ fab_id, fab_version, run.fab_hash
572
+ )
573
+
574
+ # Execute ClientApp
575
+ reply_message = client_app(message=message, context=context)
576
+ except Exception as ex: # pylint: disable=broad-exception-caught
577
+
578
+ # Legacy grpc-bidi
579
+ if transport in ["grpc-bidi", None]:
580
+ log(ERROR, "Client raised an exception.", exc_info=ex)
581
+ # Raise exception, crash process
582
+ raise ex
583
+
584
+ # Don't update/change DeprecatedRunInfoStore
585
+
586
+ e_code = ErrorCode.CLIENT_APP_RAISED_EXCEPTION
587
+ # Ex fmt: "<class 'ZeroDivisionError'>:<'division by zero'>"
588
+ reason = str(type(ex)) + ":<'" + str(ex) + "'>"
589
+ exc_entity = "ClientApp"
590
+ if isinstance(ex, LoadClientAppError):
591
+ reason = (
592
+ "An exception was raised when attempting to load "
593
+ "`ClientApp`"
594
+ )
595
+ e_code = ErrorCode.LOAD_CLIENT_APP_EXCEPTION
596
+ exc_entity = "SuperNode"
597
+
598
+ if not app_state_tracker.interrupt:
599
+ log(
600
+ ERROR, "%s raised an exception", exc_entity, exc_info=ex
601
+ )
602
+
603
+ # Create error message
604
+ reply_message = message.create_error_reply(
605
+ error=Error(code=e_code, reason=reason)
606
+ )
607
+ else:
608
+ # No exception, update node state
609
+ run_info_store.update_context(
610
+ run_id=run_id,
611
+ context=context,
612
+ )
613
+
614
+ # Send
615
+ send(reply_message)
616
+ log(INFO, "Sent reply")
617
+
618
+ except RunNotRunningException:
619
+ log(INFO, "")
620
+ log(
621
+ INFO,
622
+ "SuperNode aborted sending the reply message. "
623
+ "Run ID %s is not in `RUNNING` status.",
624
+ run_id,
625
+ )
626
+ log(INFO, "")
627
+
628
+ except StopIteration:
629
+ sleep_duration = 0
630
+ break
631
+ # pylint: enable=too-many-nested-blocks
511
632
 
512
633
  # Unregister node
513
- if delete_node is not None:
634
+ if delete_node is not None and app_state_tracker.is_connected:
514
635
  delete_node() # pylint: disable=not-callable
515
636
 
516
637
  if sleep_duration == 0:
517
638
  log(INFO, "Disconnect and shut down")
639
+ del app_state_tracker
518
640
  break
641
+
519
642
  # Sleep and reconnect afterwards
520
643
  log(
521
644
  INFO,
@@ -626,20 +749,29 @@ def start_numpy_client(
626
749
  )
627
750
 
628
751
 
629
- def _init_connection(transport: Optional[str], server_address: str) -> Tuple[
752
+ def _init_connection(transport: Optional[str], server_address: str) -> tuple[
630
753
  Callable[
631
- [str, bool, RetryInvoker, int, Union[bytes, str, None]],
632
- ContextManager[
633
- Tuple[
754
+ [
755
+ str,
756
+ bool,
757
+ RetryInvoker,
758
+ int,
759
+ Union[bytes, str, None],
760
+ Optional[tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]],
761
+ ],
762
+ AbstractContextManager[
763
+ tuple[
634
764
  Callable[[], Optional[Message]],
635
765
  Callable[[Message], None],
766
+ Optional[Callable[[], Optional[int]]],
636
767
  Optional[Callable[[], None]],
637
- Optional[Callable[[], None]],
768
+ Optional[Callable[[int], Run]],
769
+ Optional[Callable[[str, int], Fab]],
638
770
  ]
639
771
  ],
640
772
  ],
641
773
  str,
642
- Type[Exception],
774
+ type[Exception],
643
775
  ]:
644
776
  # Parse IP address
645
777
  parsed_address = parse_address(server_address)
@@ -668,6 +800,8 @@ def _init_connection(transport: Optional[str], server_address: str) -> Tuple[
668
800
  connection, error_type = http_request_response, RequestsConnectionError
669
801
  elif transport == TRANSPORT_TYPE_GRPC_RERE:
670
802
  connection, error_type = grpc_request_response, RpcError
803
+ elif transport == TRANSPORT_TYPE_GRPC_ADAPTER:
804
+ connection, error_type = grpc_adapter, RpcError
671
805
  elif transport == TRANSPORT_TYPE_GRPC_BIDI:
672
806
  connection, error_type = grpc_connection, RpcError
673
807
  else:
@@ -676,3 +810,46 @@ def _init_connection(transport: Optional[str], server_address: str) -> Tuple[
676
810
  )
677
811
 
678
812
  return connection, address, error_type
813
+
814
+
815
+ @dataclass
816
+ class _AppStateTracker:
817
+ interrupt: bool = False
818
+ is_connected: bool = False
819
+
820
+ def register_signal_handler(self) -> None:
821
+ """Register handlers for exit signals."""
822
+
823
+ def signal_handler(sig, frame): # type: ignore
824
+ # pylint: disable=unused-argument
825
+ self.interrupt = True
826
+ raise StopIteration from None
827
+
828
+ signal.signal(signal.SIGINT, signal_handler)
829
+ signal.signal(signal.SIGTERM, signal_handler)
830
+
831
+
832
+ def _run_flwr_clientapp(args: list[str]) -> None:
833
+ sys.argv = args
834
+ flwr_clientapp()
835
+
836
+
837
+ def run_clientappio_api_grpc(
838
+ address: str,
839
+ certificates: Optional[tuple[bytes, bytes, bytes]],
840
+ ) -> tuple[grpc.Server, ClientAppIoServicer]:
841
+ """Run ClientAppIo API gRPC server."""
842
+ clientappio_servicer: grpc.Server = ClientAppIoServicer()
843
+ clientappio_add_servicer_to_server_fn = add_ClientAppIoServicer_to_server
844
+ clientappio_grpc_server = generic_create_grpc_server(
845
+ servicer_and_add_fn=(
846
+ clientappio_servicer,
847
+ clientappio_add_servicer_to_server_fn,
848
+ ),
849
+ server_address=address,
850
+ max_message_length=GRPC_MAX_MESSAGE_LENGTH,
851
+ certificates=certificates,
852
+ )
853
+ log(INFO, "Starting Flower ClientAppIo gRPC server on %s", address)
854
+ clientappio_grpc_server.start()
855
+ return clientappio_grpc_server, clientappio_servicer