flwr-nightly 1.8.0.dev20240315__py3-none-any.whl → 1.11.0.dev20240813__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.

Potentially problematic release.


This version of flwr-nightly might be problematic. Click here for more details.

Files changed (237) hide show
  1. flwr/cli/app.py +7 -0
  2. flwr/cli/build.py +150 -0
  3. flwr/cli/config_utils.py +219 -0
  4. flwr/cli/example.py +3 -1
  5. flwr/cli/install.py +227 -0
  6. flwr/cli/new/new.py +179 -48
  7. flwr/cli/new/templates/app/.gitignore.tpl +160 -0
  8. flwr/cli/new/templates/app/README.flowertune.md.tpl +56 -0
  9. flwr/cli/new/templates/app/README.md.tpl +1 -5
  10. flwr/cli/new/templates/app/code/__init__.py.tpl +1 -1
  11. flwr/cli/new/templates/app/code/client.huggingface.py.tpl +65 -0
  12. flwr/cli/new/templates/app/code/client.jax.py.tpl +56 -0
  13. flwr/cli/new/templates/app/code/client.mlx.py.tpl +93 -0
  14. flwr/cli/new/templates/app/code/client.numpy.py.tpl +3 -2
  15. flwr/cli/new/templates/app/code/client.pytorch.py.tpl +23 -11
  16. flwr/cli/new/templates/app/code/client.sklearn.py.tpl +97 -0
  17. flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +60 -1
  18. flwr/cli/new/templates/app/code/flwr_tune/__init__.py +15 -0
  19. flwr/cli/new/templates/app/code/flwr_tune/app.py.tpl +89 -0
  20. flwr/cli/new/templates/app/code/flwr_tune/client.py.tpl +126 -0
  21. flwr/cli/new/templates/app/code/flwr_tune/config.yaml.tpl +34 -0
  22. flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +57 -0
  23. flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +59 -0
  24. flwr/cli/new/templates/app/code/flwr_tune/server.py.tpl +48 -0
  25. flwr/cli/new/templates/app/code/flwr_tune/static_config.yaml.tpl +11 -0
  26. flwr/cli/new/templates/app/code/server.huggingface.py.tpl +23 -0
  27. flwr/cli/new/templates/app/code/server.jax.py.tpl +20 -0
  28. flwr/cli/new/templates/app/code/server.mlx.py.tpl +20 -0
  29. flwr/cli/new/templates/app/code/server.numpy.py.tpl +17 -9
  30. flwr/cli/new/templates/app/code/server.pytorch.py.tpl +21 -18
  31. flwr/cli/new/templates/app/code/server.sklearn.py.tpl +24 -0
  32. flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +29 -1
  33. flwr/cli/new/templates/app/code/task.huggingface.py.tpl +99 -0
  34. flwr/cli/new/templates/app/code/task.jax.py.tpl +57 -0
  35. flwr/cli/new/templates/app/code/task.mlx.py.tpl +102 -0
  36. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +28 -23
  37. flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +53 -0
  38. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +39 -0
  39. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +38 -0
  40. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +34 -0
  41. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +39 -0
  42. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +25 -12
  43. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +29 -14
  44. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +33 -0
  45. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +29 -14
  46. flwr/cli/run/run.py +168 -17
  47. flwr/cli/utils.py +75 -4
  48. flwr/client/__init__.py +6 -1
  49. flwr/client/app.py +239 -248
  50. flwr/client/client_app.py +70 -9
  51. flwr/client/dpfedavg_numpy_client.py +1 -1
  52. flwr/client/grpc_adapter_client/__init__.py +15 -0
  53. flwr/client/grpc_adapter_client/connection.py +97 -0
  54. flwr/client/grpc_client/connection.py +18 -5
  55. flwr/client/grpc_rere_client/__init__.py +1 -1
  56. flwr/client/grpc_rere_client/client_interceptor.py +158 -0
  57. flwr/client/grpc_rere_client/connection.py +127 -33
  58. flwr/client/grpc_rere_client/grpc_adapter.py +140 -0
  59. flwr/client/heartbeat.py +74 -0
  60. flwr/client/message_handler/__init__.py +1 -1
  61. flwr/client/message_handler/message_handler.py +7 -7
  62. flwr/client/mod/__init__.py +5 -5
  63. flwr/client/mod/centraldp_mods.py +4 -2
  64. flwr/client/mod/comms_mods.py +4 -4
  65. flwr/client/mod/localdp_mod.py +9 -4
  66. flwr/client/mod/secure_aggregation/__init__.py +1 -1
  67. flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
  68. flwr/client/mod/utils.py +1 -1
  69. flwr/client/node_state.py +60 -10
  70. flwr/client/node_state_tests.py +4 -3
  71. flwr/client/rest_client/__init__.py +1 -1
  72. flwr/client/rest_client/connection.py +177 -157
  73. flwr/client/supernode/__init__.py +26 -0
  74. flwr/client/supernode/app.py +464 -0
  75. flwr/client/typing.py +1 -0
  76. flwr/common/__init__.py +13 -11
  77. flwr/common/address.py +1 -1
  78. flwr/common/config.py +193 -0
  79. flwr/common/constant.py +42 -1
  80. flwr/common/context.py +26 -1
  81. flwr/common/date.py +1 -1
  82. flwr/common/dp.py +1 -1
  83. flwr/common/grpc.py +6 -2
  84. flwr/common/logger.py +79 -8
  85. flwr/common/message.py +167 -105
  86. flwr/common/object_ref.py +126 -25
  87. flwr/common/record/__init__.py +1 -1
  88. flwr/common/record/parametersrecord.py +0 -1
  89. flwr/common/record/recordset.py +78 -27
  90. flwr/common/recordset_compat.py +8 -1
  91. flwr/common/retry_invoker.py +25 -13
  92. flwr/common/secure_aggregation/__init__.py +1 -1
  93. flwr/common/secure_aggregation/crypto/__init__.py +1 -1
  94. flwr/common/secure_aggregation/crypto/shamir.py +1 -1
  95. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +21 -2
  96. flwr/common/secure_aggregation/ndarrays_arithmetic.py +1 -1
  97. flwr/common/secure_aggregation/quantization.py +1 -1
  98. flwr/common/secure_aggregation/secaggplus_constants.py +1 -1
  99. flwr/common/secure_aggregation/secaggplus_utils.py +1 -1
  100. flwr/common/serde.py +209 -3
  101. flwr/common/telemetry.py +25 -0
  102. flwr/common/typing.py +38 -0
  103. flwr/common/version.py +14 -0
  104. flwr/proto/clientappio_pb2.py +41 -0
  105. flwr/proto/clientappio_pb2.pyi +110 -0
  106. flwr/proto/clientappio_pb2_grpc.py +101 -0
  107. flwr/proto/clientappio_pb2_grpc.pyi +40 -0
  108. flwr/proto/common_pb2.py +36 -0
  109. flwr/proto/common_pb2.pyi +121 -0
  110. flwr/proto/common_pb2_grpc.py +4 -0
  111. flwr/proto/common_pb2_grpc.pyi +4 -0
  112. flwr/proto/driver_pb2.py +26 -19
  113. flwr/proto/driver_pb2.pyi +34 -0
  114. flwr/proto/driver_pb2_grpc.py +70 -0
  115. flwr/proto/driver_pb2_grpc.pyi +28 -0
  116. flwr/proto/exec_pb2.py +43 -0
  117. flwr/proto/exec_pb2.pyi +95 -0
  118. flwr/proto/exec_pb2_grpc.py +101 -0
  119. flwr/proto/exec_pb2_grpc.pyi +41 -0
  120. flwr/proto/fab_pb2.py +30 -0
  121. flwr/proto/fab_pb2.pyi +56 -0
  122. flwr/proto/fab_pb2_grpc.py +4 -0
  123. flwr/proto/fab_pb2_grpc.pyi +4 -0
  124. flwr/proto/fleet_pb2.py +29 -23
  125. flwr/proto/fleet_pb2.pyi +33 -0
  126. flwr/proto/fleet_pb2_grpc.py +102 -0
  127. flwr/proto/fleet_pb2_grpc.pyi +35 -0
  128. flwr/proto/grpcadapter_pb2.py +32 -0
  129. flwr/proto/grpcadapter_pb2.pyi +43 -0
  130. flwr/proto/grpcadapter_pb2_grpc.py +66 -0
  131. flwr/proto/grpcadapter_pb2_grpc.pyi +24 -0
  132. flwr/proto/message_pb2.py +41 -0
  133. flwr/proto/message_pb2.pyi +122 -0
  134. flwr/proto/message_pb2_grpc.py +4 -0
  135. flwr/proto/message_pb2_grpc.pyi +4 -0
  136. flwr/proto/run_pb2.py +35 -0
  137. flwr/proto/run_pb2.pyi +76 -0
  138. flwr/proto/run_pb2_grpc.py +4 -0
  139. flwr/proto/run_pb2_grpc.pyi +4 -0
  140. flwr/proto/task_pb2.py +7 -8
  141. flwr/proto/task_pb2.pyi +8 -5
  142. flwr/server/__init__.py +4 -8
  143. flwr/server/app.py +298 -350
  144. flwr/server/compat/app.py +6 -57
  145. flwr/server/compat/app_utils.py +5 -4
  146. flwr/server/compat/driver_client_proxy.py +29 -48
  147. flwr/server/compat/legacy_context.py +5 -4
  148. flwr/server/driver/__init__.py +2 -0
  149. flwr/server/driver/driver.py +22 -132
  150. flwr/server/driver/grpc_driver.py +224 -74
  151. flwr/server/driver/inmemory_driver.py +183 -0
  152. flwr/server/history.py +20 -20
  153. flwr/server/run_serverapp.py +121 -34
  154. flwr/server/server.py +11 -7
  155. flwr/server/server_app.py +59 -10
  156. flwr/server/serverapp_components.py +52 -0
  157. flwr/server/strategy/__init__.py +2 -2
  158. flwr/server/strategy/bulyan.py +1 -1
  159. flwr/server/strategy/dp_adaptive_clipping.py +3 -3
  160. flwr/server/strategy/dp_fixed_clipping.py +4 -3
  161. flwr/server/strategy/dpfedavg_adaptive.py +1 -1
  162. flwr/server/strategy/dpfedavg_fixed.py +1 -1
  163. flwr/server/strategy/fedadagrad.py +1 -1
  164. flwr/server/strategy/fedadam.py +1 -1
  165. flwr/server/strategy/fedavg_android.py +1 -1
  166. flwr/server/strategy/fedavgm.py +1 -1
  167. flwr/server/strategy/fedmedian.py +1 -1
  168. flwr/server/strategy/fedopt.py +1 -1
  169. flwr/server/strategy/fedprox.py +1 -1
  170. flwr/server/strategy/fedxgb_bagging.py +1 -1
  171. flwr/server/strategy/fedxgb_cyclic.py +1 -1
  172. flwr/server/strategy/fedxgb_nn_avg.py +1 -1
  173. flwr/server/strategy/fedyogi.py +1 -1
  174. flwr/server/strategy/krum.py +1 -1
  175. flwr/server/strategy/qfedavg.py +1 -1
  176. flwr/server/superlink/driver/__init__.py +1 -1
  177. flwr/server/superlink/driver/driver_grpc.py +1 -1
  178. flwr/server/superlink/driver/driver_servicer.py +51 -4
  179. flwr/server/superlink/ffs/__init__.py +24 -0
  180. flwr/server/superlink/ffs/disk_ffs.py +104 -0
  181. flwr/server/superlink/ffs/ffs.py +79 -0
  182. flwr/server/superlink/fleet/__init__.py +1 -1
  183. flwr/server/superlink/fleet/grpc_adapter/__init__.py +15 -0
  184. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +131 -0
  185. flwr/server/superlink/fleet/grpc_bidi/__init__.py +1 -1
  186. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -1
  187. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +1 -1
  188. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +1 -1
  189. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +8 -2
  190. flwr/server/superlink/fleet/grpc_rere/__init__.py +1 -1
  191. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +30 -2
  192. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +214 -0
  193. flwr/server/superlink/fleet/message_handler/__init__.py +1 -1
  194. flwr/server/superlink/fleet/message_handler/message_handler.py +42 -2
  195. flwr/server/superlink/fleet/rest_rere/__init__.py +1 -1
  196. flwr/server/superlink/fleet/rest_rere/rest_api.py +59 -1
  197. flwr/server/superlink/fleet/vce/backend/__init__.py +1 -1
  198. flwr/server/superlink/fleet/vce/backend/backend.py +5 -5
  199. flwr/server/superlink/fleet/vce/backend/raybackend.py +53 -56
  200. flwr/server/superlink/fleet/vce/vce_api.py +190 -127
  201. flwr/server/superlink/state/__init__.py +1 -1
  202. flwr/server/superlink/state/in_memory_state.py +159 -42
  203. flwr/server/superlink/state/sqlite_state.py +243 -39
  204. flwr/server/superlink/state/state.py +81 -6
  205. flwr/server/superlink/state/state_factory.py +11 -2
  206. flwr/server/superlink/state/utils.py +62 -0
  207. flwr/server/typing.py +2 -0
  208. flwr/server/utils/__init__.py +1 -1
  209. flwr/server/utils/tensorboard.py +1 -1
  210. flwr/server/utils/validator.py +23 -9
  211. flwr/server/workflow/default_workflows.py +67 -25
  212. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +18 -6
  213. flwr/simulation/__init__.py +7 -4
  214. flwr/simulation/app.py +67 -36
  215. flwr/simulation/ray_transport/__init__.py +1 -1
  216. flwr/simulation/ray_transport/ray_actor.py +20 -46
  217. flwr/simulation/ray_transport/ray_client_proxy.py +36 -16
  218. flwr/simulation/run_simulation.py +308 -92
  219. flwr/superexec/__init__.py +21 -0
  220. flwr/superexec/app.py +184 -0
  221. flwr/superexec/deployment.py +185 -0
  222. flwr/superexec/exec_grpc.py +55 -0
  223. flwr/superexec/exec_servicer.py +70 -0
  224. flwr/superexec/executor.py +75 -0
  225. flwr/superexec/simulation.py +193 -0
  226. {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.11.0.dev20240813.dist-info}/METADATA +10 -6
  227. flwr_nightly-1.11.0.dev20240813.dist-info/RECORD +288 -0
  228. flwr_nightly-1.11.0.dev20240813.dist-info/entry_points.txt +10 -0
  229. flwr/cli/flower_toml.py +0 -140
  230. flwr/cli/new/templates/app/flower.toml.tpl +0 -13
  231. flwr/cli/new/templates/app/requirements.numpy.txt.tpl +0 -2
  232. flwr/cli/new/templates/app/requirements.pytorch.txt.tpl +0 -4
  233. flwr/cli/new/templates/app/requirements.tensorflow.txt.tpl +0 -4
  234. flwr_nightly-1.8.0.dev20240315.dist-info/RECORD +0 -211
  235. flwr_nightly-1.8.0.dev20240315.dist-info/entry_points.txt +0 -9
  236. {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.11.0.dev20240813.dist-info}/LICENSE +0 -0
  237. {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.11.0.dev20240813.dist-info}/WHEEL +0 -0
flwr/client/app.py CHANGED
@@ -14,33 +14,37 @@
14
14
  # ==============================================================================
15
15
  """Flower client app."""
16
16
 
17
-
18
- import argparse
17
+ import signal
19
18
  import sys
20
19
  import time
21
- from logging import DEBUG, INFO, WARN
20
+ from dataclasses import dataclass
21
+ from logging import ERROR, INFO, WARN
22
22
  from pathlib import Path
23
- from typing import Callable, ContextManager, Optional, Tuple, Type, Union
23
+ from typing import Callable, ContextManager, Dict, Optional, Tuple, Type, Union
24
24
 
25
+ from cryptography.hazmat.primitives.asymmetric import ec
25
26
  from grpc import RpcError
26
27
 
27
28
  from flwr.client.client import Client
28
29
  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
30
+ from flwr.client.typing import ClientFnExt
31
+ from flwr.common import GRPC_MAX_MESSAGE_LENGTH, Context, EventType, Message, event
31
32
  from flwr.common.address import parse_address
32
33
  from flwr.common.constant import (
33
34
  MISSING_EXTRA_REST,
35
+ TRANSPORT_TYPE_GRPC_ADAPTER,
34
36
  TRANSPORT_TYPE_GRPC_BIDI,
35
37
  TRANSPORT_TYPE_GRPC_RERE,
36
38
  TRANSPORT_TYPE_REST,
37
39
  TRANSPORT_TYPES,
40
+ ErrorCode,
38
41
  )
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
42
+ from flwr.common.logger import log, warn_deprecated_feature
43
+ from flwr.common.message import Error
44
+ from flwr.common.retry_invoker import RetryInvoker, RetryState, exponential
45
+ from flwr.common.typing import Fab, Run, UserConfig
43
46
 
47
+ from .grpc_adapter_client.connection import grpc_adapter
44
48
  from .grpc_client.connection import grpc_connection
45
49
  from .grpc_rere_client.connection import grpc_request_response
46
50
  from .message_handler.message_handler import handle_control_message
@@ -48,144 +52,8 @@ from .node_state import NodeState
48
52
  from .numpy_client import NumPyClient
49
53
 
50
54
 
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
185
-
186
-
187
55
  def _check_actionable_client(
188
- client: Optional[Client], client_fn: Optional[ClientFn]
56
+ client: Optional[Client], client_fn: Optional[ClientFnExt]
189
57
  ) -> None:
190
58
  if client_fn is None and client is None:
191
59
  raise ValueError(
@@ -206,12 +74,15 @@ def _check_actionable_client(
206
74
  def start_client(
207
75
  *,
208
76
  server_address: str,
209
- client_fn: Optional[ClientFn] = None,
77
+ client_fn: Optional[ClientFnExt] = None,
210
78
  client: Optional[Client] = None,
211
79
  grpc_max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
212
80
  root_certificates: Optional[Union[bytes, str]] = None,
213
81
  insecure: Optional[bool] = None,
214
82
  transport: Optional[str] = None,
83
+ authentication_keys: Optional[
84
+ Tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]
85
+ ] = None,
215
86
  max_retries: Optional[int] = None,
216
87
  max_wait_time: Optional[float] = None,
217
88
  ) -> None:
@@ -223,7 +94,7 @@ def start_client(
223
94
  The IPv4 or IPv6 address of the server. If the Flower
224
95
  server runs on the same machine on port 8080, then `server_address`
225
96
  would be `"[::]:8080"`.
226
- client_fn : Optional[ClientFn]
97
+ client_fn : Optional[ClientFnExt]
227
98
  A callable that instantiates a Client. (default: None)
228
99
  client : Optional[flwr.client.Client]
229
100
  An implementation of the abstract base
@@ -267,8 +138,8 @@ def start_client(
267
138
 
268
139
  Starting an SSL-enabled gRPC client using system certificates:
269
140
 
270
- >>> def client_fn(cid: str):
271
- >>> return FlowerClient()
141
+ >>> def client_fn(context: Context):
142
+ >>> return FlowerClient().to_client()
272
143
  >>>
273
144
  >>> start_client(
274
145
  >>> server_address=localhost:8080,
@@ -289,6 +160,7 @@ def start_client(
289
160
  event(EventType.START_CLIENT_ENTER)
290
161
  _start_client_internal(
291
162
  server_address=server_address,
163
+ node_config={},
292
164
  load_client_app_fn=None,
293
165
  client_fn=client_fn,
294
166
  client=client,
@@ -296,6 +168,7 @@ def start_client(
296
168
  root_certificates=root_certificates,
297
169
  insecure=insecure,
298
170
  transport=transport,
171
+ authentication_keys=authentication_keys,
299
172
  max_retries=max_retries,
300
173
  max_wait_time=max_wait_time,
301
174
  )
@@ -309,15 +182,20 @@ def start_client(
309
182
  def _start_client_internal(
310
183
  *,
311
184
  server_address: str,
312
- load_client_app_fn: Optional[Callable[[], ClientApp]] = None,
313
- client_fn: Optional[ClientFn] = None,
185
+ node_config: UserConfig,
186
+ load_client_app_fn: Optional[Callable[[str, str], ClientApp]] = None,
187
+ client_fn: Optional[ClientFnExt] = None,
314
188
  client: Optional[Client] = None,
315
189
  grpc_max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
316
190
  root_certificates: Optional[Union[bytes, str]] = None,
317
191
  insecure: Optional[bool] = None,
318
192
  transport: Optional[str] = None,
193
+ authentication_keys: Optional[
194
+ Tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]
195
+ ] = None,
319
196
  max_retries: Optional[int] = None,
320
197
  max_wait_time: Optional[float] = None,
198
+ flwr_path: Optional[Path] = None,
321
199
  ) -> None:
322
200
  """Start a Flower client node which connects to a Flower server.
323
201
 
@@ -327,9 +205,11 @@ def _start_client_internal(
327
205
  The IPv4 or IPv6 address of the server. If the Flower
328
206
  server runs on the same machine on port 8080, then `server_address`
329
207
  would be `"[::]:8080"`.
208
+ node_config: UserConfig
209
+ The configuration of the node.
330
210
  load_client_app_fn : Optional[Callable[[], ClientApp]] (default: None)
331
211
  A function that can be used to load a `ClientApp` instance.
332
- client_fn : Optional[ClientFn]
212
+ client_fn : Optional[ClientFnExt]
333
213
  A callable that instantiates a Client. (default: None)
334
214
  client : Optional[flwr.client.Client]
335
215
  An implementation of the abstract base
@@ -361,6 +241,8 @@ def _start_client_internal(
361
241
  The maximum duration before the client stops trying to
362
242
  connect to the server in case of connection error.
363
243
  If set to None, there is no limit to the total time.
244
+ flwr_path: Optional[Path] (default: None)
245
+ The fully resolved path containing installed Flower Apps.
364
246
  """
365
247
  if insecure is None:
366
248
  insecure = root_certificates is None
@@ -371,7 +253,7 @@ def _start_client_internal(
371
253
  if client_fn is None:
372
254
  # Wrap `Client` instance in `client_fn`
373
255
  def single_client_factory(
374
- cid: str, # pylint: disable=unused-argument
256
+ context: Context, # pylint: disable=unused-argument
375
257
  ) -> Client:
376
258
  if client is None: # Added this to keep mypy happy
377
259
  raise ValueError(
@@ -381,12 +263,10 @@ def _start_client_internal(
381
263
 
382
264
  client_fn = single_client_factory
383
265
 
384
- def _load_client_app() -> ClientApp:
266
+ def _load_client_app(_1: str, _2: str) -> ClientApp:
385
267
  return ClientApp(client_fn=client_fn)
386
268
 
387
269
  load_client_app_fn = _load_client_app
388
- else:
389
- warn_experimental_feature("`load_client_app_fn`")
390
270
 
391
271
  # At this point, only `load_client_app_fn` should be used
392
272
  # Both `client` and `client_fn` must not be used directly
@@ -396,10 +276,33 @@ def _start_client_internal(
396
276
  transport, server_address
397
277
  )
398
278
 
279
+ app_state_tracker = _AppStateTracker()
280
+
281
+ def _on_sucess(retry_state: RetryState) -> None:
282
+ app_state_tracker.is_connected = True
283
+ if retry_state.tries > 1:
284
+ log(
285
+ INFO,
286
+ "Connection successful after %.2f seconds and %s tries.",
287
+ retry_state.elapsed_time,
288
+ retry_state.tries,
289
+ )
290
+
291
+ def _on_backoff(retry_state: RetryState) -> None:
292
+ app_state_tracker.is_connected = False
293
+ if retry_state.tries == 1:
294
+ log(WARN, "Connection attempt failed, retrying...")
295
+ else:
296
+ log(
297
+ WARN,
298
+ "Connection attempt failed, retrying in %.2f seconds",
299
+ retry_state.actual_wait,
300
+ )
301
+
399
302
  retry_invoker = RetryInvoker(
400
- wait_factory=exponential,
303
+ wait_gen_factory=exponential,
401
304
  recoverable_exceptions=connection_error_type,
402
- max_tries=max_retries,
305
+ max_tries=max_retries + 1 if max_retries is not None else None,
403
306
  max_time=max_wait_time,
404
307
  on_giveup=lambda retry_state: (
405
308
  log(
@@ -411,30 +314,16 @@ def _start_client_internal(
411
314
  if retry_state.tries > 1
412
315
  else None
413
316
  ),
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
- ),
317
+ on_success=_on_sucess,
318
+ on_backoff=_on_backoff,
433
319
  )
434
320
 
435
- node_state = NodeState()
321
+ # NodeState gets initialized when the first connection is established
322
+ node_state: Optional[NodeState] = None
323
+
324
+ runs: Dict[int, Run] = {}
436
325
 
437
- while True:
326
+ while not app_state_tracker.interrupt:
438
327
  sleep_duration: int = 0
439
328
  with connection(
440
329
  address,
@@ -442,80 +331,154 @@ def _start_client_internal(
442
331
  retry_invoker,
443
332
  grpc_max_message_length,
444
333
  root_certificates,
334
+ authentication_keys,
445
335
  ) 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
336
+ receive, send, create_node, delete_node, get_run, _ = conn
337
+
338
+ # Register node when connecting the first time
339
+ if node_state is None:
340
+ if create_node is None:
341
+ if transport not in ["grpc-bidi", None]:
342
+ raise NotImplementedError(
343
+ "All transports except `grpc-bidi` require "
344
+ "an implementation for `create_node()`.'"
345
+ )
346
+ # gRPC-bidi doesn't have the concept of node_id,
347
+ # so we set it to -1
348
+ node_state = NodeState(
349
+ node_id=-1,
350
+ node_config={},
351
+ )
352
+ else:
353
+ # Call create_node fn to register node
354
+ node_id: Optional[int] = ( # pylint: disable=assignment-from-none
355
+ create_node()
356
+ ) # pylint: disable=not-callable
357
+ if node_id is None:
358
+ raise ValueError("Node registration failed")
359
+ node_state = NodeState(
360
+ node_id=node_id,
361
+ node_config=node_config,
362
+ )
363
+
364
+ app_state_tracker.register_signal_handler()
365
+ while not app_state_tracker.interrupt:
366
+ try:
367
+ # Receive
368
+ message = receive()
369
+ if message is None:
370
+ time.sleep(3) # Wait for 3s before asking again
371
+ continue
372
+
373
+ log(INFO, "")
374
+ if len(message.metadata.group_id) > 0:
375
+ log(
376
+ INFO,
377
+ "[RUN %s, ROUND %s]",
378
+ message.metadata.run_id,
379
+ message.metadata.group_id,
380
+ )
381
+ log(
382
+ INFO,
383
+ "Received: %s message %s",
384
+ message.metadata.message_type,
385
+ message.metadata.message_id,
386
+ )
387
+
388
+ # Handle control message
389
+ out_message, sleep_duration = handle_control_message(message)
390
+ if out_message:
391
+ send(out_message)
392
+ break
393
+
394
+ # Get run info
395
+ run_id = message.metadata.run_id
396
+ if run_id not in runs:
397
+ if get_run is not None:
398
+ runs[run_id] = get_run(run_id)
399
+ # If get_run is None, i.e., in grpc-bidi mode
400
+ else:
401
+ runs[run_id] = Run(run_id, "", "", {})
402
+
403
+ # Register context for this run
404
+ node_state.register_context(
405
+ run_id=run_id, run=runs[run_id], flwr_path=flwr_path
406
+ )
407
+
408
+ # Retrieve context for this run
409
+ context = node_state.retrieve_context(run_id=run_id)
478
410
 
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
- )
411
+ # Create an error reply message that will never be used to prevent
412
+ # the used-before-assignment linting error
413
+ reply_message = message.create_error_reply(
414
+ error=Error(code=ErrorCode.UNKNOWN, reason="Unknown")
415
+ )
416
+
417
+ # Handle app loading and task message
418
+ try:
419
+ # Load ClientApp instance
420
+ run: Run = runs[run_id]
421
+ client_app: ClientApp = load_client_app_fn(
422
+ run.fab_id, run.fab_version
423
+ )
424
+
425
+ # Execute ClientApp
426
+ reply_message = client_app(message=message, context=context)
427
+ except Exception as ex: # pylint: disable=broad-exception-caught
428
+
429
+ # Legacy grpc-bidi
430
+ if transport in ["grpc-bidi", None]:
431
+ log(ERROR, "Client raised an exception.", exc_info=ex)
432
+ # Raise exception, crash process
433
+ raise ex
434
+
435
+ # Don't update/change NodeState
436
+
437
+ e_code = ErrorCode.CLIENT_APP_RAISED_EXCEPTION
438
+ # Ex fmt: "<class 'ZeroDivisionError'>:<'division by zero'>"
439
+ reason = str(type(ex)) + ":<'" + str(ex) + "'>"
440
+ exc_entity = "ClientApp"
441
+ if isinstance(ex, LoadClientAppError):
442
+ reason = (
443
+ "An exception was raised when attempting to load "
444
+ "`ClientApp`"
445
+ )
446
+ e_code = ErrorCode.LOAD_CLIENT_APP_EXCEPTION
447
+ exc_entity = "SuperNode"
448
+
449
+ if not app_state_tracker.interrupt:
450
+ log(
451
+ ERROR, "%s raised an exception", exc_entity, exc_info=ex
452
+ )
453
+
454
+ # Create error message
455
+ reply_message = message.create_error_reply(
456
+ error=Error(code=e_code, reason=reason)
457
+ )
458
+ else:
459
+ # No exception, update node state
460
+ node_state.update_context(
461
+ run_id=run_id,
462
+ context=context,
463
+ )
464
+
465
+ # Send
466
+ send(reply_message)
467
+ log(INFO, "Sent reply")
468
+
469
+ except StopIteration:
470
+ sleep_duration = 0
471
+ break
511
472
 
512
473
  # Unregister node
513
- if delete_node is not None:
474
+ if delete_node is not None and app_state_tracker.is_connected:
514
475
  delete_node() # pylint: disable=not-callable
515
476
 
516
477
  if sleep_duration == 0:
517
478
  log(INFO, "Disconnect and shut down")
479
+ del app_state_tracker
518
480
  break
481
+
519
482
  # Sleep and reconnect afterwards
520
483
  log(
521
484
  INFO,
@@ -628,13 +591,22 @@ def start_numpy_client(
628
591
 
629
592
  def _init_connection(transport: Optional[str], server_address: str) -> Tuple[
630
593
  Callable[
631
- [str, bool, RetryInvoker, int, Union[bytes, str, None]],
594
+ [
595
+ str,
596
+ bool,
597
+ RetryInvoker,
598
+ int,
599
+ Union[bytes, str, None],
600
+ Optional[Tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]],
601
+ ],
632
602
  ContextManager[
633
603
  Tuple[
634
604
  Callable[[], Optional[Message]],
635
605
  Callable[[Message], None],
606
+ Optional[Callable[[], Optional[int]]],
636
607
  Optional[Callable[[], None]],
637
- Optional[Callable[[], None]],
608
+ Optional[Callable[[int], Run]],
609
+ Optional[Callable[[str], Fab]],
638
610
  ]
639
611
  ],
640
612
  ],
@@ -668,6 +640,8 @@ def _init_connection(transport: Optional[str], server_address: str) -> Tuple[
668
640
  connection, error_type = http_request_response, RequestsConnectionError
669
641
  elif transport == TRANSPORT_TYPE_GRPC_RERE:
670
642
  connection, error_type = grpc_request_response, RpcError
643
+ elif transport == TRANSPORT_TYPE_GRPC_ADAPTER:
644
+ connection, error_type = grpc_adapter, RpcError
671
645
  elif transport == TRANSPORT_TYPE_GRPC_BIDI:
672
646
  connection, error_type = grpc_connection, RpcError
673
647
  else:
@@ -676,3 +650,20 @@ def _init_connection(transport: Optional[str], server_address: str) -> Tuple[
676
650
  )
677
651
 
678
652
  return connection, address, error_type
653
+
654
+
655
+ @dataclass
656
+ class _AppStateTracker:
657
+ interrupt: bool = False
658
+ is_connected: bool = False
659
+
660
+ def register_signal_handler(self) -> None:
661
+ """Register handlers for exit signals."""
662
+
663
+ def signal_handler(sig, frame): # type: ignore
664
+ # pylint: disable=unused-argument
665
+ self.interrupt = True
666
+ raise StopIteration from None
667
+
668
+ signal.signal(signal.SIGINT, signal_handler)
669
+ signal.signal(signal.SIGTERM, signal_handler)