flwr-nightly 1.8.0.dev20240314__py3-none-any.whl → 1.15.0.dev20250114__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (311) 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 +132 -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 +298 -19
  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 +1 -1
  169. flwr/proto/recordset_pb2.py +35 -33
  170. flwr/proto/recordset_pb2.pyi +40 -14
  171. flwr/proto/run_pb2.py +64 -0
  172. flwr/proto/run_pb2.pyi +268 -0
  173. flwr/proto/run_pb2_grpc.py +4 -0
  174. flwr/proto/run_pb2_grpc.pyi +4 -0
  175. flwr/proto/serverappio_pb2.py +52 -0
  176. flwr/proto/{driver_pb2.pyi → serverappio_pb2.pyi} +62 -20
  177. flwr/proto/serverappio_pb2_grpc.py +410 -0
  178. flwr/proto/serverappio_pb2_grpc.pyi +160 -0
  179. flwr/proto/simulationio_pb2.py +38 -0
  180. flwr/proto/simulationio_pb2.pyi +65 -0
  181. flwr/proto/simulationio_pb2_grpc.py +239 -0
  182. flwr/proto/simulationio_pb2_grpc.pyi +94 -0
  183. flwr/proto/task_pb2.py +7 -8
  184. flwr/proto/task_pb2.pyi +8 -5
  185. flwr/proto/transport_pb2.py +8 -8
  186. flwr/proto/transport_pb2.pyi +9 -6
  187. flwr/server/__init__.py +2 -10
  188. flwr/server/app.py +579 -402
  189. flwr/server/client_manager.py +8 -6
  190. flwr/server/compat/app.py +6 -62
  191. flwr/server/compat/app_utils.py +14 -8
  192. flwr/server/compat/driver_client_proxy.py +25 -58
  193. flwr/server/compat/legacy_context.py +5 -4
  194. flwr/server/driver/__init__.py +2 -0
  195. flwr/server/driver/driver.py +36 -131
  196. flwr/server/driver/grpc_driver.py +217 -81
  197. flwr/server/driver/inmemory_driver.py +182 -0
  198. flwr/server/history.py +28 -29
  199. flwr/server/run_serverapp.py +15 -126
  200. flwr/server/server.py +50 -44
  201. flwr/server/server_app.py +59 -10
  202. flwr/server/serverapp/__init__.py +22 -0
  203. flwr/server/serverapp/app.py +256 -0
  204. flwr/server/serverapp_components.py +52 -0
  205. flwr/server/strategy/__init__.py +2 -2
  206. flwr/server/strategy/aggregate.py +37 -23
  207. flwr/server/strategy/bulyan.py +9 -9
  208. flwr/server/strategy/dp_adaptive_clipping.py +25 -25
  209. flwr/server/strategy/dp_fixed_clipping.py +23 -22
  210. flwr/server/strategy/dpfedavg_adaptive.py +8 -8
  211. flwr/server/strategy/dpfedavg_fixed.py +13 -12
  212. flwr/server/strategy/fault_tolerant_fedavg.py +11 -11
  213. flwr/server/strategy/fedadagrad.py +9 -9
  214. flwr/server/strategy/fedadam.py +20 -10
  215. flwr/server/strategy/fedavg.py +16 -16
  216. flwr/server/strategy/fedavg_android.py +17 -17
  217. flwr/server/strategy/fedavgm.py +9 -9
  218. flwr/server/strategy/fedmedian.py +5 -5
  219. flwr/server/strategy/fedopt.py +6 -6
  220. flwr/server/strategy/fedprox.py +7 -7
  221. flwr/server/strategy/fedtrimmedavg.py +8 -8
  222. flwr/server/strategy/fedxgb_bagging.py +12 -12
  223. flwr/server/strategy/fedxgb_cyclic.py +10 -10
  224. flwr/server/strategy/fedxgb_nn_avg.py +6 -6
  225. flwr/server/strategy/fedyogi.py +9 -9
  226. flwr/server/strategy/krum.py +9 -9
  227. flwr/server/strategy/qfedavg.py +16 -16
  228. flwr/server/strategy/strategy.py +10 -10
  229. flwr/server/superlink/driver/__init__.py +2 -2
  230. flwr/server/superlink/driver/serverappio_grpc.py +61 -0
  231. flwr/server/superlink/driver/serverappio_servicer.py +363 -0
  232. flwr/server/superlink/ffs/__init__.py +24 -0
  233. flwr/server/superlink/ffs/disk_ffs.py +108 -0
  234. flwr/server/superlink/ffs/ffs.py +79 -0
  235. flwr/server/superlink/ffs/ffs_factory.py +47 -0
  236. flwr/server/superlink/fleet/__init__.py +1 -1
  237. flwr/server/superlink/fleet/grpc_adapter/__init__.py +15 -0
  238. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +162 -0
  239. flwr/server/superlink/fleet/grpc_bidi/__init__.py +1 -1
  240. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +4 -2
  241. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +3 -2
  242. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +1 -1
  243. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +5 -154
  244. flwr/server/superlink/fleet/grpc_rere/__init__.py +1 -1
  245. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +120 -13
  246. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +228 -0
  247. flwr/server/superlink/fleet/message_handler/__init__.py +1 -1
  248. flwr/server/superlink/fleet/message_handler/message_handler.py +153 -9
  249. flwr/server/superlink/fleet/rest_rere/__init__.py +1 -1
  250. flwr/server/superlink/fleet/rest_rere/rest_api.py +119 -81
  251. flwr/server/superlink/fleet/vce/__init__.py +1 -0
  252. flwr/server/superlink/fleet/vce/backend/__init__.py +4 -4
  253. flwr/server/superlink/fleet/vce/backend/backend.py +8 -9
  254. flwr/server/superlink/fleet/vce/backend/raybackend.py +87 -68
  255. flwr/server/superlink/fleet/vce/vce_api.py +208 -146
  256. flwr/server/superlink/linkstate/__init__.py +28 -0
  257. flwr/server/superlink/linkstate/in_memory_linkstate.py +581 -0
  258. flwr/server/superlink/linkstate/linkstate.py +389 -0
  259. flwr/server/superlink/{state/state_factory.py → linkstate/linkstate_factory.py} +19 -10
  260. flwr/server/superlink/linkstate/sqlite_linkstate.py +1236 -0
  261. flwr/server/superlink/linkstate/utils.py +389 -0
  262. flwr/server/superlink/simulation/__init__.py +15 -0
  263. flwr/server/superlink/simulation/simulationio_grpc.py +65 -0
  264. flwr/server/superlink/simulation/simulationio_servicer.py +186 -0
  265. flwr/server/superlink/utils.py +65 -0
  266. flwr/server/typing.py +2 -0
  267. flwr/server/utils/__init__.py +1 -1
  268. flwr/server/utils/tensorboard.py +5 -5
  269. flwr/server/utils/validator.py +31 -11
  270. flwr/server/workflow/default_workflows.py +70 -26
  271. flwr/server/workflow/secure_aggregation/secagg_workflow.py +1 -0
  272. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +40 -27
  273. flwr/simulation/__init__.py +12 -5
  274. flwr/simulation/app.py +247 -315
  275. flwr/simulation/legacy_app.py +402 -0
  276. flwr/simulation/ray_transport/__init__.py +1 -1
  277. flwr/simulation/ray_transport/ray_actor.py +42 -67
  278. flwr/simulation/ray_transport/ray_client_proxy.py +37 -17
  279. flwr/simulation/ray_transport/utils.py +1 -0
  280. flwr/simulation/run_simulation.py +306 -163
  281. flwr/simulation/simulationio_connection.py +89 -0
  282. flwr/superexec/__init__.py +15 -0
  283. flwr/superexec/app.py +59 -0
  284. flwr/superexec/deployment.py +188 -0
  285. flwr/superexec/exec_grpc.py +80 -0
  286. flwr/superexec/exec_servicer.py +231 -0
  287. flwr/superexec/exec_user_auth_interceptor.py +101 -0
  288. flwr/superexec/executor.py +96 -0
  289. flwr/superexec/simulation.py +124 -0
  290. {flwr_nightly-1.8.0.dev20240314.dist-info → flwr_nightly-1.15.0.dev20250114.dist-info}/METADATA +33 -26
  291. flwr_nightly-1.15.0.dev20250114.dist-info/RECORD +328 -0
  292. flwr_nightly-1.15.0.dev20250114.dist-info/entry_points.txt +12 -0
  293. flwr/cli/flower_toml.py +0 -140
  294. flwr/cli/new/templates/app/flower.toml.tpl +0 -13
  295. flwr/cli/new/templates/app/requirements.numpy.txt.tpl +0 -2
  296. flwr/cli/new/templates/app/requirements.pytorch.txt.tpl +0 -4
  297. flwr/cli/new/templates/app/requirements.tensorflow.txt.tpl +0 -4
  298. flwr/client/node_state.py +0 -48
  299. flwr/client/node_state_tests.py +0 -65
  300. flwr/proto/driver_pb2.py +0 -44
  301. flwr/proto/driver_pb2_grpc.py +0 -169
  302. flwr/proto/driver_pb2_grpc.pyi +0 -66
  303. flwr/server/superlink/driver/driver_grpc.py +0 -54
  304. flwr/server/superlink/driver/driver_servicer.py +0 -129
  305. flwr/server/superlink/state/in_memory_state.py +0 -230
  306. flwr/server/superlink/state/sqlite_state.py +0 -630
  307. flwr/server/superlink/state/state.py +0 -154
  308. flwr_nightly-1.8.0.dev20240314.dist-info/RECORD +0 -211
  309. flwr_nightly-1.8.0.dev20240314.dist-info/entry_points.txt +0 -9
  310. {flwr_nightly-1.8.0.dev20240314.dist-info → flwr_nightly-1.15.0.dev20250114.dist-info}/LICENSE +0 -0
  311. {flwr_nightly-1.8.0.dev20240314.dist-info → flwr_nightly-1.15.0.dev20250114.dist-info}/WHEEL +0 -0
flwr/cli/run/run.py CHANGED
@@ -14,55 +14,233 @@
14
14
  # ==============================================================================
15
15
  """Flower command line interface `run` command."""
16
16
 
17
- import sys
17
+
18
+ import io
19
+ import json
20
+ import subprocess
21
+ from pathlib import Path
22
+ from typing import Annotated, Any, Optional
18
23
 
19
24
  import typer
25
+ from rich.console import Console
20
26
 
21
- from flwr.cli import flower_toml
22
- from flwr.simulation.run_simulation import _run_simulation
27
+ from flwr.cli.build import build
28
+ from flwr.cli.config_utils import (
29
+ get_fab_metadata,
30
+ load_and_validate,
31
+ process_loaded_project_config,
32
+ validate_federation_in_project_config,
33
+ )
34
+ from flwr.common.config import (
35
+ flatten_dict,
36
+ parse_config_args,
37
+ user_config_to_configsrecord,
38
+ )
39
+ from flwr.common.constant import CliOutputFormat
40
+ from flwr.common.logger import print_json_error, redirect_output, restore_output
41
+ from flwr.common.serde import (
42
+ configs_record_to_proto,
43
+ fab_to_proto,
44
+ user_config_to_proto,
45
+ )
46
+ from flwr.common.typing import Fab
47
+ from flwr.proto.exec_pb2 import StartRunRequest # pylint: disable=E0611
48
+ from flwr.proto.exec_pb2_grpc import ExecStub
23
49
 
50
+ from ..log import start_stream
51
+ from ..utils import (
52
+ init_channel,
53
+ try_obtain_cli_auth_plugin,
54
+ unauthenticated_exc_handler,
55
+ )
24
56
 
25
- def run() -> None:
26
- """Run Flower project."""
27
- typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
57
+ CONN_REFRESH_PERIOD = 60 # Connection refresh period for log streaming (seconds)
28
58
 
29
- config, errors, warnings = flower_toml.load_and_validate_with_defaults()
30
59
 
31
- if config is None:
32
- typer.secho(
33
- "Project configuration could not be loaded.\nflower.toml is invalid:\n"
34
- + "\n".join([f"- {line}" for line in errors]),
35
- fg=typer.colors.RED,
36
- bold=True,
37
- )
38
- sys.exit()
60
+ # pylint: disable-next=too-many-locals
61
+ def run(
62
+ app: Annotated[
63
+ Path,
64
+ typer.Argument(help="Path of the Flower App to run."),
65
+ ] = Path("."),
66
+ federation: Annotated[
67
+ Optional[str],
68
+ typer.Argument(help="Name of the federation to run the app on."),
69
+ ] = None,
70
+ config_overrides: Annotated[
71
+ Optional[list[str]],
72
+ typer.Option(
73
+ "--run-config",
74
+ "-c",
75
+ help="Override configuration key-value pairs, should be of the format:\n\n"
76
+ '`--run-config \'key1="value1" key2="value2"\' '
77
+ "--run-config 'key3=\"value3\"'`\n\n"
78
+ "Note that `key1`, `key2`, and `key3` in this example need to exist "
79
+ "inside the `pyproject.toml` in order to be properly overriden.",
80
+ ),
81
+ ] = None,
82
+ stream: Annotated[
83
+ bool,
84
+ typer.Option(
85
+ "--stream",
86
+ help="Use `--stream` with `flwr run` to display logs;\n "
87
+ "logs are not streamed by default.",
88
+ ),
89
+ ] = False,
90
+ output_format: Annotated[
91
+ str,
92
+ typer.Option(
93
+ "--format",
94
+ case_sensitive=False,
95
+ help="Format output using 'default' view or 'json'",
96
+ ),
97
+ ] = CliOutputFormat.DEFAULT,
98
+ ) -> None:
99
+ """Run Flower App."""
100
+ suppress_output = output_format == CliOutputFormat.JSON
101
+ captured_output = io.StringIO()
102
+ try:
103
+ if suppress_output:
104
+ redirect_output(captured_output)
105
+ typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
39
106
 
40
- if warnings:
41
- typer.secho(
42
- "Project configuration is missing the following "
43
- "recommended properties:\n" + "\n".join([f"- {line}" for line in warnings]),
44
- fg=typer.colors.RED,
45
- bold=True,
107
+ pyproject_path = app / "pyproject.toml" if app else None
108
+ config, errors, warnings = load_and_validate(path=pyproject_path)
109
+ config = process_loaded_project_config(config, errors, warnings)
110
+ federation, federation_config = validate_federation_in_project_config(
111
+ federation, config
46
112
  )
47
113
 
48
- typer.secho("Success", fg=typer.colors.GREEN)
114
+ if "address" in federation_config:
115
+ _run_with_exec_api(
116
+ app,
117
+ federation,
118
+ federation_config,
119
+ config_overrides,
120
+ stream,
121
+ output_format,
122
+ )
123
+ else:
124
+ _run_without_exec_api(app, federation_config, config_overrides, federation)
125
+ except (typer.Exit, Exception) as err: # pylint: disable=broad-except
126
+ if suppress_output:
127
+ restore_output()
128
+ e_message = captured_output.getvalue()
129
+ print_json_error(e_message, err)
130
+ else:
131
+ typer.secho(
132
+ f"{err}",
133
+ fg=typer.colors.RED,
134
+ bold=True,
135
+ )
136
+ finally:
137
+ if suppress_output:
138
+ restore_output()
139
+ captured_output.close()
49
140
 
50
- server_app_ref = config["flower"]["components"]["serverapp"]
51
- client_app_ref = config["flower"]["components"]["clientapp"]
52
- engine = config["flower"]["engine"]["name"]
53
141
 
54
- if engine == "simulation":
55
- num_supernodes = config["flower"]["engine"]["simulation"]["supernode"]["num"]
142
+ # pylint: disable-next=R0913, R0914, R0917
143
+ def _run_with_exec_api(
144
+ app: Path,
145
+ federation: str,
146
+ federation_config: dict[str, Any],
147
+ config_overrides: Optional[list[str]],
148
+ stream: bool,
149
+ output_format: str,
150
+ ) -> None:
151
+ auth_plugin = try_obtain_cli_auth_plugin(app, federation)
152
+ channel = init_channel(app, federation_config, auth_plugin)
153
+ stub = ExecStub(channel)
56
154
 
57
- typer.secho("Starting run... ", fg=typer.colors.BLUE)
58
- _run_simulation(
59
- server_app_attr=server_app_ref,
60
- client_app_attr=client_app_ref,
61
- num_supernodes=num_supernodes,
62
- )
155
+ fab_path, fab_hash = build(app)
156
+ content = Path(fab_path).read_bytes()
157
+ fab_id, fab_version = get_fab_metadata(Path(fab_path))
158
+
159
+ # Delete FAB file once the bytes is computed
160
+ Path(fab_path).unlink()
161
+
162
+ fab = Fab(fab_hash, content)
163
+
164
+ # Construct a `ConfigsRecord` out of a flattened `UserConfig`
165
+ fed_conf = flatten_dict(federation_config.get("options", {}))
166
+ c_record = user_config_to_configsrecord(fed_conf)
167
+
168
+ req = StartRunRequest(
169
+ fab=fab_to_proto(fab),
170
+ override_config=user_config_to_proto(parse_config_args(config_overrides)),
171
+ federation_options=configs_record_to_proto(c_record),
172
+ )
173
+ with unauthenticated_exc_handler():
174
+ res = stub.StartRun(req)
175
+
176
+ if res.HasField("run_id"):
177
+ typer.secho(f"🎊 Successfully started run {res.run_id}", fg=typer.colors.GREEN)
63
178
  else:
179
+ typer.secho("❌ Failed to start run", fg=typer.colors.RED)
180
+ raise typer.Exit(code=1)
181
+
182
+ if output_format == CliOutputFormat.JSON:
183
+ run_output = json.dumps(
184
+ {
185
+ "success": res.HasField("run_id"),
186
+ "run-id": res.run_id if res.HasField("run_id") else None,
187
+ "fab-id": fab_id,
188
+ "fab-name": fab_id.rsplit("/", maxsplit=1)[-1],
189
+ "fab-version": fab_version,
190
+ "fab-hash": fab_hash[:8],
191
+ "fab-filename": fab_path,
192
+ }
193
+ )
194
+ restore_output()
195
+ Console().print_json(run_output)
196
+
197
+ if stream:
198
+ start_stream(res.run_id, channel, CONN_REFRESH_PERIOD)
199
+
200
+
201
+ def _run_without_exec_api(
202
+ app: Optional[Path],
203
+ federation_config: dict[str, Any],
204
+ config_overrides: Optional[list[str]],
205
+ federation: str,
206
+ ) -> None:
207
+ try:
208
+ num_supernodes = federation_config["options"]["num-supernodes"]
209
+ verbose: Optional[bool] = federation_config["options"].get("verbose")
210
+ backend_cfg = federation_config["options"].get("backend", {})
211
+ except KeyError as err:
64
212
  typer.secho(
65
- f"Engine '{engine}' is not yet supported in `flwr run`",
213
+ " The project's `pyproject.toml` needs to declare the number of"
214
+ " SuperNodes in the simulation. To simulate 10 SuperNodes,"
215
+ " use the following notation:\n\n"
216
+ f"[tool.flwr.federations.{federation}]\n"
217
+ "options.num-supernodes = 10\n",
66
218
  fg=typer.colors.RED,
67
219
  bold=True,
68
220
  )
221
+ raise typer.Exit(code=1) from err
222
+
223
+ command = [
224
+ "flower-simulation",
225
+ "--app",
226
+ f"{app}",
227
+ "--num-supernodes",
228
+ f"{num_supernodes}",
229
+ ]
230
+
231
+ if backend_cfg:
232
+ # Stringify as JSON
233
+ command.extend(["--backend-config", json.dumps(backend_cfg)])
234
+
235
+ if verbose:
236
+ command.extend(["--verbose"])
237
+
238
+ if config_overrides:
239
+ command.extend(["--run-config", f"{' '.join(config_overrides)}"])
240
+
241
+ # Run the simulation
242
+ subprocess.run(
243
+ command,
244
+ check=True,
245
+ text=True,
246
+ )
flwr/cli/stop.py ADDED
@@ -0,0 +1,130 @@
1
+ # Copyright 2024 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 command line interface `stop` command."""
16
+
17
+
18
+ import io
19
+ import json
20
+ from pathlib import Path
21
+ from typing import Annotated, Optional
22
+
23
+ import typer
24
+ from rich.console import Console
25
+
26
+ from flwr.cli.config_utils import (
27
+ exit_if_no_address,
28
+ load_and_validate,
29
+ process_loaded_project_config,
30
+ validate_federation_in_project_config,
31
+ )
32
+ from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
33
+ from flwr.common.logger import print_json_error, redirect_output, restore_output
34
+ from flwr.proto.exec_pb2 import StopRunRequest, StopRunResponse # pylint: disable=E0611
35
+ from flwr.proto.exec_pb2_grpc import ExecStub
36
+
37
+ from .utils import init_channel, try_obtain_cli_auth_plugin, unauthenticated_exc_handler
38
+
39
+
40
+ def stop( # pylint: disable=R0914
41
+ run_id: Annotated[ # pylint: disable=unused-argument
42
+ int,
43
+ typer.Argument(help="The Flower run ID to stop"),
44
+ ],
45
+ app: Annotated[
46
+ Path,
47
+ typer.Argument(help="Path of the Flower project"),
48
+ ] = Path("."),
49
+ federation: Annotated[
50
+ Optional[str],
51
+ typer.Argument(help="Name of the federation"),
52
+ ] = None,
53
+ output_format: Annotated[
54
+ str,
55
+ typer.Option(
56
+ "--format",
57
+ case_sensitive=False,
58
+ help="Format output using 'default' view or 'json'",
59
+ ),
60
+ ] = CliOutputFormat.DEFAULT,
61
+ ) -> None:
62
+ """Stop a run."""
63
+ suppress_output = output_format == CliOutputFormat.JSON
64
+ captured_output = io.StringIO()
65
+ try:
66
+ if suppress_output:
67
+ redirect_output(captured_output)
68
+
69
+ # Load and validate federation config
70
+ typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
71
+
72
+ pyproject_path = app / FAB_CONFIG_FILE if app else None
73
+ config, errors, warnings = load_and_validate(path=pyproject_path)
74
+ config = process_loaded_project_config(config, errors, warnings)
75
+ federation, federation_config = validate_federation_in_project_config(
76
+ federation, config
77
+ )
78
+ exit_if_no_address(federation_config, "stop")
79
+
80
+ try:
81
+ auth_plugin = try_obtain_cli_auth_plugin(app, federation)
82
+ channel = init_channel(app, federation_config, auth_plugin)
83
+ stub = ExecStub(channel) # pylint: disable=unused-variable # noqa: F841
84
+
85
+ typer.secho(f"✋ Stopping run ID {run_id}...", fg=typer.colors.GREEN)
86
+ _stop_run(stub=stub, run_id=run_id, output_format=output_format)
87
+
88
+ except ValueError as err:
89
+ typer.secho(
90
+ f"❌ {err}",
91
+ fg=typer.colors.RED,
92
+ bold=True,
93
+ )
94
+ raise typer.Exit(code=1) from err
95
+ finally:
96
+ channel.close()
97
+ except (typer.Exit, Exception) as err: # pylint: disable=broad-except
98
+ if suppress_output:
99
+ restore_output()
100
+ e_message = captured_output.getvalue()
101
+ print_json_error(e_message, err)
102
+ else:
103
+ typer.secho(
104
+ f"{err}",
105
+ fg=typer.colors.RED,
106
+ bold=True,
107
+ )
108
+ finally:
109
+ if suppress_output:
110
+ restore_output()
111
+ captured_output.close()
112
+
113
+
114
+ def _stop_run(stub: ExecStub, run_id: int, output_format: str) -> None:
115
+ """Stop a run."""
116
+ with unauthenticated_exc_handler():
117
+ response: StopRunResponse = stub.StopRun(request=StopRunRequest(run_id=run_id))
118
+ if response.success:
119
+ typer.secho(f"✅ Run {run_id} successfully stopped.", fg=typer.colors.GREEN)
120
+ if output_format == CliOutputFormat.JSON:
121
+ run_output = json.dumps(
122
+ {
123
+ "success": True,
124
+ "run-id": run_id,
125
+ }
126
+ )
127
+ restore_output()
128
+ Console().print_json(run_output)
129
+ else:
130
+ typer.secho(f"❌ Run {run_id} couldn't be stopped.", fg=typer.colors.RED)
flwr/cli/utils.py CHANGED
@@ -14,25 +14,55 @@
14
14
  # ==============================================================================
15
15
  """Flower command line interface utils."""
16
16
 
17
- from typing import List, cast
18
17
 
18
+ import hashlib
19
+ import json
20
+ import re
21
+ from collections.abc import Iterator
22
+ from contextlib import contextmanager
23
+ from logging import DEBUG
24
+ from pathlib import Path
25
+ from typing import Any, Callable, Optional, Union, cast
26
+
27
+ import grpc
19
28
  import typer
20
29
 
30
+ from flwr.cli.cli_user_auth_interceptor import CliUserAuthInterceptor
31
+ from flwr.common.auth_plugin import CliAuthPlugin
32
+ from flwr.common.constant import AUTH_TYPE, CREDENTIALS_DIR, FLWR_DIR
33
+ from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel
34
+ from flwr.common.logger import log
35
+
36
+ from .config_utils import validate_certificate_in_federation_config
37
+
38
+ try:
39
+ from flwr.ee import get_cli_auth_plugins
40
+ except ImportError:
21
41
 
22
- def prompt_text(text: str) -> str:
42
+ def get_cli_auth_plugins() -> dict[str, type[CliAuthPlugin]]:
43
+ """Return all CLI authentication plugins."""
44
+ raise NotImplementedError("No authentication plugins are currently supported.")
45
+
46
+
47
+ def prompt_text(
48
+ text: str,
49
+ predicate: Callable[[str], bool] = lambda _: True,
50
+ default: Optional[str] = None,
51
+ ) -> str:
23
52
  """Ask user to enter text input."""
24
53
  while True:
25
54
  result = typer.prompt(
26
- typer.style(f"\n💬 {text}", fg=typer.colors.MAGENTA, bold=True)
55
+ typer.style(f"\n💬 {text}", fg=typer.colors.MAGENTA, bold=True),
56
+ default=default,
27
57
  )
28
- if len(result) > 0:
58
+ if predicate(result) and len(result) > 0:
29
59
  break
30
60
  print(typer.style("❌ Invalid entry", fg=typer.colors.RED, bold=True))
31
61
 
32
62
  return cast(str, result)
33
63
 
34
64
 
35
- def prompt_options(text: str, options: List[str]) -> str:
65
+ def prompt_options(text: str, options: list[str]) -> str:
36
66
  """Ask user to select one of the given options and return the selected item."""
37
67
  # Turn options into a list with index as in " [ 0] quickstart-pytorch"
38
68
  options_formatted = [
@@ -65,3 +95,208 @@ def prompt_options(text: str, options: List[str]) -> str:
65
95
 
66
96
  result = options[int(index)]
67
97
  return result
98
+
99
+
100
+ def is_valid_project_name(name: str) -> bool:
101
+ """Check if the given string is a valid Python project name.
102
+
103
+ A valid project name must start with a letter and can only contain letters, digits,
104
+ and hyphens.
105
+ """
106
+ if not name:
107
+ return False
108
+
109
+ # Check if the first character is a letter
110
+ if not name[0].isalpha():
111
+ return False
112
+
113
+ # Check if the rest of the characters are valid (letter, digit, or dash)
114
+ for char in name[1:]:
115
+ if not (char.isalnum() or char in "-"):
116
+ return False
117
+
118
+ return True
119
+
120
+
121
+ def sanitize_project_name(name: str) -> str:
122
+ """Sanitize the given string to make it a valid Python project name.
123
+
124
+ This version replaces spaces, dots, slashes, and underscores with dashes, removes
125
+ any characters not allowed in Python project names, makes the string lowercase, and
126
+ ensures it starts with a valid character.
127
+ """
128
+ # Replace whitespace with '_'
129
+ name_with_hyphens = re.sub(r"[ ./_]", "-", name)
130
+
131
+ # Allowed characters in a module name: letters, digits, underscore
132
+ allowed_chars = set(
133
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"
134
+ )
135
+
136
+ # Make the string lowercase
137
+ sanitized_name = name_with_hyphens.lower()
138
+
139
+ # Remove any characters not allowed in Python module names
140
+ sanitized_name = "".join(c for c in sanitized_name if c in allowed_chars)
141
+
142
+ # Ensure the first character is a letter or underscore
143
+ while sanitized_name and (
144
+ sanitized_name[0].isdigit() or sanitized_name[0] not in allowed_chars
145
+ ):
146
+ sanitized_name = sanitized_name[1:]
147
+
148
+ return sanitized_name
149
+
150
+
151
+ def get_sha256_hash(file_path_or_int: Union[Path, int]) -> str:
152
+ """Calculate the SHA-256 hash of a file."""
153
+ sha256 = hashlib.sha256()
154
+ if isinstance(file_path_or_int, Path):
155
+ with open(file_path_or_int, "rb") as f:
156
+ while True:
157
+ data = f.read(65536) # Read in 64kB blocks
158
+ if not data:
159
+ break
160
+ sha256.update(data)
161
+ elif isinstance(file_path_or_int, int):
162
+ sha256.update(str(file_path_or_int).encode())
163
+ return sha256.hexdigest()
164
+
165
+
166
+ def get_user_auth_config_path(root_dir: Path, federation: str) -> Path:
167
+ """Return the path to the user auth config file.
168
+
169
+ Additionally, a `.gitignore` file will be created in the Flower directory to
170
+ include the `.credentials` folder to be excluded from git. If the `.gitignore`
171
+ file already exists, a warning will be displayed if the `.credentials` entry is
172
+ not found.
173
+ """
174
+ # Locate the credentials directory
175
+ abs_flwr_dir = root_dir.absolute() / FLWR_DIR
176
+ credentials_dir = abs_flwr_dir / CREDENTIALS_DIR
177
+ credentials_dir.mkdir(parents=True, exist_ok=True)
178
+
179
+ # Determine the absolute path of the Flower directory for .gitignore
180
+ gitignore_path = abs_flwr_dir / ".gitignore"
181
+ credential_entry = CREDENTIALS_DIR
182
+
183
+ try:
184
+ if gitignore_path.exists():
185
+ with open(gitignore_path, encoding="utf-8") as gitignore_file:
186
+ lines = gitignore_file.read().splitlines()
187
+
188
+ # Warn if .credentials is not already in .gitignore
189
+ if credential_entry not in lines:
190
+ typer.secho(
191
+ f"`.gitignore` exists, but `{credential_entry}` entry not found. "
192
+ "Consider adding it to your `.gitignore` to exclude Flower "
193
+ "credentials from git.",
194
+ fg=typer.colors.YELLOW,
195
+ bold=True,
196
+ )
197
+ else:
198
+ typer.secho(
199
+ f"Creating a new `.gitignore` with `{credential_entry}` entry...",
200
+ fg=typer.colors.BLUE,
201
+ )
202
+ # Create a new .gitignore with .credentials
203
+ with open(gitignore_path, "w", encoding="utf-8") as gitignore_file:
204
+ gitignore_file.write(f"{credential_entry}\n")
205
+ except Exception as err:
206
+ typer.secho(
207
+ "❌ An error occurred while handling `.gitignore.` "
208
+ f"Please check the permissions of `{gitignore_path}` and try again.",
209
+ fg=typer.colors.RED,
210
+ bold=True,
211
+ )
212
+ raise typer.Exit(code=1) from err
213
+
214
+ return credentials_dir / f"{federation}.json"
215
+
216
+
217
+ def try_obtain_cli_auth_plugin(
218
+ root_dir: Path,
219
+ federation: str,
220
+ auth_type: Optional[str] = None,
221
+ ) -> Optional[CliAuthPlugin]:
222
+ """Load the CLI-side user auth plugin for the given auth type."""
223
+ config_path = get_user_auth_config_path(root_dir, federation)
224
+
225
+ # Load the config file if it exists
226
+ json_file: dict[str, Any] = {}
227
+ if config_path.exists():
228
+ with config_path.open("r", encoding="utf-8") as file:
229
+ json_file = json.load(file)
230
+ # This is the case when the user auth is not enabled
231
+ elif auth_type is None:
232
+ return None
233
+
234
+ # Get the auth type from the config if not provided
235
+ if auth_type is None:
236
+ if AUTH_TYPE not in json_file:
237
+ return None
238
+ auth_type = json_file[AUTH_TYPE]
239
+
240
+ # Retrieve auth plugin class and instantiate it
241
+ try:
242
+ all_plugins: dict[str, type[CliAuthPlugin]] = get_cli_auth_plugins()
243
+ auth_plugin_class = all_plugins[auth_type]
244
+ return auth_plugin_class(config_path)
245
+ except KeyError:
246
+ typer.echo(f"❌ Unknown user authentication type: {auth_type}")
247
+ raise typer.Exit(code=1) from None
248
+ except ImportError:
249
+ typer.echo("❌ No authentication plugins are currently supported.")
250
+ raise typer.Exit(code=1) from None
251
+
252
+
253
+ def init_channel(
254
+ app: Path, federation_config: dict[str, Any], auth_plugin: Optional[CliAuthPlugin]
255
+ ) -> grpc.Channel:
256
+ """Initialize gRPC channel to the Exec API."""
257
+
258
+ def on_channel_state_change(channel_connectivity: str) -> None:
259
+ """Log channel connectivity."""
260
+ log(DEBUG, channel_connectivity)
261
+
262
+ insecure, root_certificates_bytes = validate_certificate_in_federation_config(
263
+ app, federation_config
264
+ )
265
+
266
+ # Initialize the CLI-side user auth interceptor
267
+ interceptors: list[grpc.UnaryUnaryClientInterceptor] = []
268
+ if auth_plugin is not None:
269
+ auth_plugin.load_tokens()
270
+ interceptors = CliUserAuthInterceptor(auth_plugin)
271
+
272
+ # Create the gRPC channel
273
+ channel = create_channel(
274
+ server_address=federation_config["address"],
275
+ insecure=insecure,
276
+ root_certificates=root_certificates_bytes,
277
+ max_message_length=GRPC_MAX_MESSAGE_LENGTH,
278
+ interceptors=interceptors or None,
279
+ )
280
+ channel.subscribe(on_channel_state_change)
281
+ return channel
282
+
283
+
284
+ @contextmanager
285
+ def unauthenticated_exc_handler() -> Iterator[None]:
286
+ """Context manager to handle gRPC UNAUTHENTICATED errors.
287
+
288
+ It catches grpc.RpcError exceptions with UNAUTHENTICATED status, informs the user,
289
+ and exits the application. All other exceptions will be allowed to escape.
290
+ """
291
+ try:
292
+ yield
293
+ except grpc.RpcError as e:
294
+ if e.code() != grpc.StatusCode.UNAUTHENTICATED:
295
+ raise
296
+ typer.secho(
297
+ "❌ Authentication failed. Please run `flwr login`"
298
+ " to authenticate and try again.",
299
+ fg=typer.colors.RED,
300
+ bold=True,
301
+ )
302
+ raise typer.Exit(code=1) from None
flwr/client/__init__.py CHANGED
@@ -15,20 +15,21 @@
15
15
  """Flower client."""
16
16
 
17
17
 
18
- from .app import run_client_app as run_client_app
19
18
  from .app import start_client as start_client
20
19
  from .app import start_numpy_client as start_numpy_client
21
20
  from .client import Client as Client
22
21
  from .client_app import ClientApp as ClientApp
23
22
  from .numpy_client import NumPyClient as NumPyClient
24
23
  from .typing import ClientFn as ClientFn
24
+ from .typing import ClientFnExt as ClientFnExt
25
25
 
26
26
  __all__ = [
27
27
  "Client",
28
28
  "ClientApp",
29
29
  "ClientFn",
30
+ "ClientFnExt",
30
31
  "NumPyClient",
31
- "run_client_app",
32
+ "mod",
32
33
  "start_client",
33
34
  "start_numpy_client",
34
35
  ]