flwr 1.23.0__py3-none-any.whl → 1.25.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. flwr/__init__.py +16 -5
  2. flwr/app/error.py +2 -2
  3. flwr/app/exception.py +3 -3
  4. flwr/cli/app.py +19 -0
  5. flwr/cli/{new/templates → app_cmd}/__init__.py +9 -1
  6. flwr/cli/app_cmd/publish.py +285 -0
  7. flwr/cli/app_cmd/review.py +262 -0
  8. flwr/cli/auth_plugin/auth_plugin.py +4 -5
  9. flwr/cli/auth_plugin/noop_auth_plugin.py +54 -11
  10. flwr/cli/auth_plugin/oidc_cli_plugin.py +32 -9
  11. flwr/cli/build.py +60 -18
  12. flwr/cli/cli_account_auth_interceptor.py +24 -7
  13. flwr/cli/config_utils.py +101 -13
  14. flwr/cli/{new/templates/app/code/flwr_tune → federation}/__init__.py +10 -1
  15. flwr/cli/federation/ls.py +140 -0
  16. flwr/cli/federation/show.py +318 -0
  17. flwr/cli/install.py +91 -13
  18. flwr/cli/log.py +52 -9
  19. flwr/cli/login/login.py +7 -4
  20. flwr/cli/ls.py +211 -130
  21. flwr/cli/new/new.py +123 -331
  22. flwr/cli/pull.py +10 -5
  23. flwr/cli/run/run.py +71 -29
  24. flwr/cli/run_utils.py +148 -0
  25. flwr/cli/stop.py +26 -8
  26. flwr/cli/supernode/ls.py +25 -12
  27. flwr/cli/supernode/register.py +9 -4
  28. flwr/cli/supernode/unregister.py +5 -3
  29. flwr/cli/utils.py +239 -16
  30. flwr/client/__init__.py +1 -1
  31. flwr/client/dpfedavg_numpy_client.py +4 -1
  32. flwr/client/grpc_adapter_client/connection.py +8 -9
  33. flwr/client/grpc_rere_client/connection.py +16 -14
  34. flwr/client/grpc_rere_client/grpc_adapter.py +6 -2
  35. flwr/client/grpc_rere_client/node_auth_client_interceptor.py +2 -1
  36. flwr/client/message_handler/message_handler.py +2 -2
  37. flwr/client/mod/secure_aggregation/secaggplus_mod.py +3 -3
  38. flwr/client/numpy_client.py +1 -1
  39. flwr/client/rest_client/connection.py +18 -18
  40. flwr/client/run_info_store.py +4 -5
  41. flwr/client/typing.py +1 -1
  42. flwr/clientapp/client_app.py +9 -10
  43. flwr/clientapp/mod/centraldp_mods.py +16 -17
  44. flwr/clientapp/mod/localdp_mod.py +8 -9
  45. flwr/clientapp/typing.py +1 -1
  46. flwr/clientapp/utils.py +3 -3
  47. flwr/common/address.py +1 -2
  48. flwr/common/args.py +3 -4
  49. flwr/common/config.py +13 -16
  50. flwr/common/constant.py +5 -2
  51. flwr/common/differential_privacy.py +3 -4
  52. flwr/common/event_log_plugin/event_log_plugin.py +3 -4
  53. flwr/common/exit/exit.py +15 -2
  54. flwr/common/exit/exit_code.py +19 -0
  55. flwr/common/exit/exit_handler.py +6 -2
  56. flwr/common/exit/signal_handler.py +5 -5
  57. flwr/common/grpc.py +6 -6
  58. flwr/common/inflatable_protobuf_utils.py +1 -1
  59. flwr/common/inflatable_utils.py +38 -21
  60. flwr/common/logger.py +19 -19
  61. flwr/common/message.py +4 -4
  62. flwr/common/object_ref.py +7 -7
  63. flwr/common/record/array.py +3 -3
  64. flwr/common/record/arrayrecord.py +18 -30
  65. flwr/common/record/configrecord.py +3 -3
  66. flwr/common/record/recorddict.py +5 -5
  67. flwr/common/record/typeddict.py +9 -2
  68. flwr/common/recorddict_compat.py +7 -10
  69. flwr/common/retry_invoker.py +20 -20
  70. flwr/common/secure_aggregation/ndarrays_arithmetic.py +3 -3
  71. flwr/common/serde.py +11 -4
  72. flwr/common/serde_utils.py +2 -2
  73. flwr/common/telemetry.py +9 -5
  74. flwr/common/typing.py +58 -37
  75. flwr/compat/client/app.py +38 -37
  76. flwr/compat/client/grpc_client/connection.py +11 -11
  77. flwr/compat/server/app.py +5 -6
  78. flwr/proto/appio_pb2.py +13 -3
  79. flwr/proto/appio_pb2.pyi +134 -65
  80. flwr/proto/appio_pb2_grpc.py +20 -0
  81. flwr/proto/appio_pb2_grpc.pyi +27 -0
  82. flwr/proto/clientappio_pb2.py +17 -7
  83. flwr/proto/clientappio_pb2.pyi +15 -0
  84. flwr/proto/clientappio_pb2_grpc.py +206 -40
  85. flwr/proto/clientappio_pb2_grpc.pyi +168 -53
  86. flwr/proto/control_pb2.py +71 -52
  87. flwr/proto/control_pb2.pyi +277 -111
  88. flwr/proto/control_pb2_grpc.py +249 -40
  89. flwr/proto/control_pb2_grpc.pyi +185 -52
  90. flwr/proto/error_pb2.py +13 -3
  91. flwr/proto/error_pb2.pyi +24 -6
  92. flwr/proto/error_pb2_grpc.py +20 -0
  93. flwr/proto/error_pb2_grpc.pyi +27 -0
  94. flwr/proto/fab_pb2.py +14 -4
  95. flwr/proto/fab_pb2.pyi +59 -31
  96. flwr/proto/fab_pb2_grpc.py +20 -0
  97. flwr/proto/fab_pb2_grpc.pyi +27 -0
  98. flwr/proto/federation_pb2.py +38 -0
  99. flwr/proto/federation_pb2.pyi +56 -0
  100. flwr/proto/federation_pb2_grpc.py +24 -0
  101. flwr/proto/federation_pb2_grpc.pyi +31 -0
  102. flwr/proto/fleet_pb2.py +24 -14
  103. flwr/proto/fleet_pb2.pyi +141 -61
  104. flwr/proto/fleet_pb2_grpc.py +189 -48
  105. flwr/proto/fleet_pb2_grpc.pyi +175 -61
  106. flwr/proto/grpcadapter_pb2.py +14 -4
  107. flwr/proto/grpcadapter_pb2.pyi +38 -16
  108. flwr/proto/grpcadapter_pb2_grpc.py +35 -4
  109. flwr/proto/grpcadapter_pb2_grpc.pyi +38 -7
  110. flwr/proto/heartbeat_pb2.py +17 -7
  111. flwr/proto/heartbeat_pb2.pyi +51 -22
  112. flwr/proto/heartbeat_pb2_grpc.py +20 -0
  113. flwr/proto/heartbeat_pb2_grpc.pyi +27 -0
  114. flwr/proto/log_pb2.py +13 -3
  115. flwr/proto/log_pb2.pyi +34 -11
  116. flwr/proto/log_pb2_grpc.py +20 -0
  117. flwr/proto/log_pb2_grpc.pyi +27 -0
  118. flwr/proto/message_pb2.py +15 -5
  119. flwr/proto/message_pb2.pyi +154 -86
  120. flwr/proto/message_pb2_grpc.py +20 -0
  121. flwr/proto/message_pb2_grpc.pyi +27 -0
  122. flwr/proto/node_pb2.py +15 -5
  123. flwr/proto/node_pb2.pyi +50 -25
  124. flwr/proto/node_pb2_grpc.py +20 -0
  125. flwr/proto/node_pb2_grpc.pyi +27 -0
  126. flwr/proto/recorddict_pb2.py +13 -3
  127. flwr/proto/recorddict_pb2.pyi +184 -107
  128. flwr/proto/recorddict_pb2_grpc.py +20 -0
  129. flwr/proto/recorddict_pb2_grpc.pyi +27 -0
  130. flwr/proto/run_pb2.py +40 -31
  131. flwr/proto/run_pb2.pyi +158 -84
  132. flwr/proto/run_pb2_grpc.py +20 -0
  133. flwr/proto/run_pb2_grpc.pyi +27 -0
  134. flwr/proto/serverappio_pb2.py +13 -3
  135. flwr/proto/serverappio_pb2.pyi +32 -8
  136. flwr/proto/serverappio_pb2_grpc.py +246 -65
  137. flwr/proto/serverappio_pb2_grpc.pyi +221 -85
  138. flwr/proto/simulationio_pb2.py +16 -8
  139. flwr/proto/simulationio_pb2.pyi +15 -0
  140. flwr/proto/simulationio_pb2_grpc.py +162 -41
  141. flwr/proto/simulationio_pb2_grpc.pyi +149 -55
  142. flwr/proto/transport_pb2.py +20 -10
  143. flwr/proto/transport_pb2.pyi +249 -160
  144. flwr/proto/transport_pb2_grpc.py +35 -4
  145. flwr/proto/transport_pb2_grpc.pyi +38 -8
  146. flwr/server/app.py +39 -17
  147. flwr/server/client_manager.py +4 -5
  148. flwr/server/client_proxy.py +10 -11
  149. flwr/server/compat/app.py +4 -5
  150. flwr/server/compat/app_utils.py +2 -1
  151. flwr/server/compat/grid_client_proxy.py +10 -12
  152. flwr/server/compat/legacy_context.py +3 -4
  153. flwr/server/fleet_event_log_interceptor.py +2 -1
  154. flwr/server/grid/grid.py +2 -3
  155. flwr/server/grid/grpc_grid.py +10 -8
  156. flwr/server/grid/inmemory_grid.py +4 -4
  157. flwr/server/run_serverapp.py +2 -3
  158. flwr/server/server.py +34 -39
  159. flwr/server/server_app.py +7 -8
  160. flwr/server/server_config.py +1 -2
  161. flwr/server/serverapp/app.py +34 -28
  162. flwr/server/serverapp_components.py +4 -5
  163. flwr/server/strategy/aggregate.py +9 -8
  164. flwr/server/strategy/bulyan.py +13 -11
  165. flwr/server/strategy/dp_adaptive_clipping.py +16 -20
  166. flwr/server/strategy/dp_fixed_clipping.py +12 -17
  167. flwr/server/strategy/dpfedavg_adaptive.py +3 -4
  168. flwr/server/strategy/dpfedavg_fixed.py +6 -10
  169. flwr/server/strategy/fault_tolerant_fedavg.py +14 -13
  170. flwr/server/strategy/fedadagrad.py +18 -14
  171. flwr/server/strategy/fedadam.py +16 -14
  172. flwr/server/strategy/fedavg.py +16 -17
  173. flwr/server/strategy/fedavg_android.py +15 -15
  174. flwr/server/strategy/fedavgm.py +21 -18
  175. flwr/server/strategy/fedmedian.py +2 -3
  176. flwr/server/strategy/fedopt.py +11 -10
  177. flwr/server/strategy/fedprox.py +10 -9
  178. flwr/server/strategy/fedtrimmedavg.py +12 -11
  179. flwr/server/strategy/fedxgb_bagging.py +13 -11
  180. flwr/server/strategy/fedxgb_cyclic.py +6 -6
  181. flwr/server/strategy/fedxgb_nn_avg.py +4 -4
  182. flwr/server/strategy/fedyogi.py +16 -14
  183. flwr/server/strategy/krum.py +12 -11
  184. flwr/server/strategy/qfedavg.py +16 -15
  185. flwr/server/strategy/strategy.py +6 -9
  186. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +2 -1
  187. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -2
  188. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +3 -4
  189. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +10 -12
  190. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +1 -3
  191. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +4 -4
  192. flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +3 -2
  193. flwr/server/superlink/fleet/message_handler/message_handler.py +75 -30
  194. flwr/server/superlink/fleet/rest_rere/rest_api.py +2 -2
  195. flwr/server/superlink/fleet/vce/backend/backend.py +1 -1
  196. flwr/server/superlink/fleet/vce/backend/raybackend.py +5 -5
  197. flwr/server/superlink/fleet/vce/vce_api.py +15 -9
  198. flwr/server/superlink/linkstate/in_memory_linkstate.py +148 -149
  199. flwr/server/superlink/linkstate/linkstate.py +91 -43
  200. flwr/server/superlink/linkstate/linkstate_factory.py +22 -5
  201. flwr/server/superlink/linkstate/sqlite_linkstate.py +502 -436
  202. flwr/server/superlink/linkstate/utils.py +6 -6
  203. flwr/server/superlink/serverappio/serverappio_grpc.py +1 -2
  204. flwr/server/superlink/serverappio/serverappio_servicer.py +26 -21
  205. flwr/server/superlink/simulation/simulationio_grpc.py +1 -2
  206. flwr/server/superlink/simulation/simulationio_servicer.py +18 -13
  207. flwr/server/superlink/utils.py +4 -6
  208. flwr/server/typing.py +1 -1
  209. flwr/server/utils/tensorboard.py +15 -8
  210. flwr/server/workflow/default_workflows.py +5 -5
  211. flwr/server/workflow/secure_aggregation/secagg_workflow.py +2 -4
  212. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +8 -8
  213. flwr/serverapp/strategy/bulyan.py +16 -15
  214. flwr/serverapp/strategy/dp_adaptive_clipping.py +12 -11
  215. flwr/serverapp/strategy/dp_fixed_clipping.py +11 -14
  216. flwr/serverapp/strategy/fedadagrad.py +10 -11
  217. flwr/serverapp/strategy/fedadam.py +10 -11
  218. flwr/serverapp/strategy/fedavg.py +9 -10
  219. flwr/serverapp/strategy/fedavgm.py +17 -16
  220. flwr/serverapp/strategy/fedmedian.py +2 -2
  221. flwr/serverapp/strategy/fedopt.py +10 -11
  222. flwr/serverapp/strategy/fedprox.py +7 -8
  223. flwr/serverapp/strategy/fedtrimmedavg.py +9 -9
  224. flwr/serverapp/strategy/fedxgb_bagging.py +3 -3
  225. flwr/serverapp/strategy/fedxgb_cyclic.py +9 -9
  226. flwr/serverapp/strategy/fedyogi.py +9 -11
  227. flwr/serverapp/strategy/krum.py +7 -7
  228. flwr/serverapp/strategy/multikrum.py +9 -9
  229. flwr/serverapp/strategy/qfedavg.py +17 -16
  230. flwr/serverapp/strategy/strategy.py +6 -9
  231. flwr/serverapp/strategy/strategy_utils.py +7 -8
  232. flwr/simulation/app.py +46 -42
  233. flwr/simulation/legacy_app.py +12 -12
  234. flwr/simulation/ray_transport/ray_actor.py +10 -11
  235. flwr/simulation/ray_transport/ray_client_proxy.py +11 -12
  236. flwr/simulation/run_simulation.py +43 -43
  237. flwr/simulation/simulationio_connection.py +4 -4
  238. flwr/supercore/cli/flower_superexec.py +3 -4
  239. flwr/supercore/constant.py +34 -1
  240. flwr/supercore/corestate/corestate.py +24 -3
  241. flwr/supercore/corestate/in_memory_corestate.py +138 -0
  242. flwr/supercore/corestate/sqlite_corestate.py +157 -0
  243. flwr/supercore/ffs/disk_ffs.py +1 -2
  244. flwr/supercore/ffs/ffs.py +1 -2
  245. flwr/supercore/ffs/ffs_factory.py +1 -2
  246. flwr/{common → supercore}/heartbeat.py +20 -25
  247. flwr/supercore/object_store/in_memory_object_store.py +1 -2
  248. flwr/supercore/object_store/object_store.py +1 -2
  249. flwr/supercore/object_store/object_store_factory.py +1 -2
  250. flwr/supercore/object_store/sqlite_object_store.py +8 -7
  251. flwr/supercore/primitives/asymmetric.py +1 -1
  252. flwr/supercore/primitives/asymmetric_ed25519.py +11 -1
  253. flwr/supercore/sqlite_mixin.py +37 -34
  254. flwr/supercore/superexec/plugin/base_exec_plugin.py +1 -2
  255. flwr/supercore/superexec/plugin/exec_plugin.py +3 -3
  256. flwr/supercore/superexec/run_superexec.py +9 -13
  257. flwr/supercore/utils.py +190 -0
  258. flwr/superlink/artifact_provider/artifact_provider.py +1 -2
  259. flwr/superlink/auth_plugin/auth_plugin.py +6 -9
  260. flwr/superlink/auth_plugin/noop_auth_plugin.py +6 -9
  261. flwr/{cli/new/templates/app → superlink/federation}/__init__.py +10 -1
  262. flwr/superlink/federation/federation_manager.py +64 -0
  263. flwr/superlink/federation/noop_federation_manager.py +71 -0
  264. flwr/superlink/servicer/control/control_account_auth_interceptor.py +22 -13
  265. flwr/superlink/servicer/control/control_event_log_interceptor.py +7 -7
  266. flwr/superlink/servicer/control/control_grpc.py +7 -6
  267. flwr/superlink/servicer/control/control_license_interceptor.py +3 -3
  268. flwr/superlink/servicer/control/control_servicer.py +190 -23
  269. flwr/supernode/cli/flower_supernode.py +58 -3
  270. flwr/supernode/nodestate/in_memory_nodestate.py +121 -49
  271. flwr/supernode/nodestate/nodestate.py +52 -8
  272. flwr/supernode/nodestate/nodestate_factory.py +7 -4
  273. flwr/supernode/runtime/run_clientapp.py +41 -22
  274. flwr/supernode/servicer/clientappio/clientappio_servicer.py +46 -10
  275. flwr/supernode/start_client_internal.py +165 -46
  276. {flwr-1.23.0.dist-info → flwr-1.25.0.dist-info}/METADATA +9 -11
  277. flwr-1.25.0.dist-info/RECORD +393 -0
  278. flwr/cli/new/templates/app/.gitignore.tpl +0 -163
  279. flwr/cli/new/templates/app/LICENSE.tpl +0 -202
  280. flwr/cli/new/templates/app/README.baseline.md.tpl +0 -127
  281. flwr/cli/new/templates/app/README.flowertune.md.tpl +0 -68
  282. flwr/cli/new/templates/app/README.md.tpl +0 -37
  283. flwr/cli/new/templates/app/code/__init__.baseline.py.tpl +0 -1
  284. flwr/cli/new/templates/app/code/__init__.py +0 -15
  285. flwr/cli/new/templates/app/code/__init__.py.tpl +0 -1
  286. flwr/cli/new/templates/app/code/__init__.pytorch_legacy_api.py.tpl +0 -1
  287. flwr/cli/new/templates/app/code/client.baseline.py.tpl +0 -75
  288. flwr/cli/new/templates/app/code/client.huggingface.py.tpl +0 -93
  289. flwr/cli/new/templates/app/code/client.jax.py.tpl +0 -71
  290. flwr/cli/new/templates/app/code/client.mlx.py.tpl +0 -102
  291. flwr/cli/new/templates/app/code/client.numpy.py.tpl +0 -46
  292. flwr/cli/new/templates/app/code/client.pytorch.py.tpl +0 -80
  293. flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +0 -55
  294. flwr/cli/new/templates/app/code/client.sklearn.py.tpl +0 -108
  295. flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +0 -82
  296. flwr/cli/new/templates/app/code/client.xgboost.py.tpl +0 -110
  297. flwr/cli/new/templates/app/code/dataset.baseline.py.tpl +0 -36
  298. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +0 -92
  299. flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +0 -87
  300. flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +0 -56
  301. flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +0 -73
  302. flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +0 -78
  303. flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -66
  304. flwr/cli/new/templates/app/code/server.baseline.py.tpl +0 -43
  305. flwr/cli/new/templates/app/code/server.huggingface.py.tpl +0 -42
  306. flwr/cli/new/templates/app/code/server.jax.py.tpl +0 -39
  307. flwr/cli/new/templates/app/code/server.mlx.py.tpl +0 -41
  308. flwr/cli/new/templates/app/code/server.numpy.py.tpl +0 -38
  309. flwr/cli/new/templates/app/code/server.pytorch.py.tpl +0 -41
  310. flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +0 -31
  311. flwr/cli/new/templates/app/code/server.sklearn.py.tpl +0 -44
  312. flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +0 -38
  313. flwr/cli/new/templates/app/code/server.xgboost.py.tpl +0 -56
  314. flwr/cli/new/templates/app/code/strategy.baseline.py.tpl +0 -1
  315. flwr/cli/new/templates/app/code/task.huggingface.py.tpl +0 -98
  316. flwr/cli/new/templates/app/code/task.jax.py.tpl +0 -57
  317. flwr/cli/new/templates/app/code/task.mlx.py.tpl +0 -102
  318. flwr/cli/new/templates/app/code/task.numpy.py.tpl +0 -7
  319. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +0 -98
  320. flwr/cli/new/templates/app/code/task.pytorch_legacy_api.py.tpl +0 -111
  321. flwr/cli/new/templates/app/code/task.sklearn.py.tpl +0 -67
  322. flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +0 -52
  323. flwr/cli/new/templates/app/code/task.xgboost.py.tpl +0 -67
  324. flwr/cli/new/templates/app/code/utils.baseline.py.tpl +0 -1
  325. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +0 -146
  326. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +0 -80
  327. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +0 -65
  328. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +0 -52
  329. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +0 -56
  330. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +0 -49
  331. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +0 -53
  332. flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +0 -53
  333. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +0 -52
  334. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +0 -53
  335. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +0 -61
  336. flwr/supercore/object_store/utils.py +0 -43
  337. flwr-1.23.0.dist-info/RECORD +0 -439
  338. {flwr-1.23.0.dist-info → flwr-1.25.0.dist-info}/WHEEL +0 -0
  339. {flwr-1.23.0.dist-info → flwr-1.25.0.dist-info}/entry_points.txt +0 -0
flwr/cli/new/new.py CHANGED
@@ -16,92 +16,84 @@
16
16
 
17
17
 
18
18
  import io
19
- import json
20
- import re
21
19
  import zipfile
22
- from enum import Enum
23
20
  from pathlib import Path
24
- from string import Template
25
- from typing import Annotated, Optional
21
+ from typing import Annotated, cast
26
22
 
27
23
  import requests
28
24
  import typer
29
25
 
30
- from flwr.supercore.constant import APP_ID_PATTERN, PLATFORM_API_URL
26
+ from flwr.supercore.constant import PLATFORM_API_URL
27
+ from flwr.supercore.utils import parse_app_spec, request_download_link
31
28
 
32
- from ..utils import (
33
- is_valid_project_name,
34
- prompt_options,
35
- prompt_text,
36
- sanitize_project_name,
37
- )
29
+ from ..utils import prompt_options, prompt_text
38
30
 
39
31
 
40
- class MlFramework(str, Enum):
41
- """Available frameworks."""
42
-
43
- PYTORCH = "PyTorch"
44
- TENSORFLOW = "TensorFlow"
45
- SKLEARN = "sklearn"
46
- HUGGINGFACE = "HuggingFace"
47
- JAX = "JAX"
48
- MLX = "MLX"
49
- NUMPY = "NumPy"
50
- XGBOOST = "XGBoost"
51
- FLOWERTUNE = "FlowerTune"
52
- BASELINE = "Flower Baseline"
53
- PYTORCH_LEGACY_API = "PyTorch (Legacy API, deprecated)"
54
-
55
-
56
- class LlmChallengeName(str, Enum):
57
- """Available LLM challenges."""
58
-
59
- GENERALNLP = "GeneralNLP"
60
- FINANCE = "Finance"
61
- MEDICAL = "Medical"
62
- CODE = "Code"
63
-
64
-
65
- class TemplateNotFound(Exception):
66
- """Raised when template does not exist."""
67
-
68
-
69
- def load_template(name: str) -> str:
70
- """Load template from template directory and return as text."""
71
- tpl_dir = (Path(__file__).parent / "templates").absolute()
72
- tpl_file_path = tpl_dir / name
73
-
74
- if not tpl_file_path.is_file():
75
- raise TemplateNotFound(f"Template '{name}' not found")
76
-
77
- with open(tpl_file_path, encoding="utf-8") as tpl_file:
78
- return tpl_file.read()
79
-
80
-
81
- def render_template(template: str, data: dict[str, str]) -> str:
82
- """Render template."""
83
- tpl_file = load_template(template)
84
- tpl = Template(tpl_file)
85
- if ".gitignore" not in template:
86
- return tpl.substitute(data)
87
- return tpl.template
88
-
32
+ # pylint: disable=too-many-locals,too-many-branches,too-many-statements
33
+ def new(
34
+ app_spec: Annotated[
35
+ str | None,
36
+ typer.Argument(
37
+ help="Flower app specifier. Use the format "
38
+ "'@account_name/app_name' or '@account_name/app_name==x.y.z'. "
39
+ "Version is optional (defaults to latest)."
40
+ ),
41
+ ] = None,
42
+ framework: Annotated[
43
+ str | None,
44
+ typer.Option(case_sensitive=False, help="Deprecated. The ML framework to use"),
45
+ ] = None,
46
+ username: Annotated[
47
+ str | None,
48
+ typer.Option(
49
+ case_sensitive=False, help="Deprecated. The Flower username of the author"
50
+ ),
51
+ ] = None,
52
+ ) -> None:
53
+ """Create new Flower App."""
54
+ if framework is not None or username is not None:
55
+ typer.secho(
56
+ "❌ The --framework and --username options are deprecated and will be "
57
+ "removed in future versions of Flower. Please provide an app specifier "
58
+ "after `flwr new` instead, e.g., '@account_name/app_name' or "
59
+ "'@account_name/app_name==x.y.z'.",
60
+ fg=typer.colors.RED,
61
+ bold=True,
62
+ err=True,
63
+ )
64
+ raise typer.Exit(code=1)
89
65
 
90
- def create_file(file_path: Path, content: str) -> None:
91
- """Create file including all nessecary directories and write content into file."""
92
- file_path.parent.mkdir(exist_ok=True)
93
- file_path.write_text(content, encoding="utf-8")
66
+ if app_spec is None:
67
+ # Fetch recommended apps
68
+ print(
69
+ typer.style(
70
+ "\n🌸 Fetching recommended apps...",
71
+ fg=typer.colors.GREEN,
72
+ bold=True,
73
+ )
74
+ )
75
+ apps = fetch_recommended_apps()
94
76
 
77
+ if not apps:
78
+ typer.secho(
79
+ "No recommended apps found. Please provide an app specifier manually.",
80
+ fg=typer.colors.YELLOW,
81
+ )
82
+ app_spec = prompt_text("Please provide the app specifier")
83
+ else:
84
+ # Extract app_ids and show selection menu
85
+ app_ids = [app["app_id"] for app in apps]
86
+ app_spec = prompt_options(
87
+ "Select a Flower App to create by entering "
88
+ "the number from the list below:",
89
+ app_ids,
90
+ )
95
91
 
96
- def render_and_create(file_path: Path, template: str, context: dict[str, str]) -> None:
97
- """Render template and write to file."""
98
- content = render_template(template, context)
99
- create_file(file_path, content)
92
+ # Download remote app
93
+ download_remote_app_via_api(app_spec)
100
94
 
101
95
 
102
- def print_success_prompt(
103
- package_name: str, llm_challenge_str: Optional[str] = None
104
- ) -> None:
96
+ def print_success_prompt(package_name: str) -> None:
105
97
  """Print styled setup instructions for running a new Flower App after creation."""
106
98
  prompt = typer.style(
107
99
  "🎊 Flower App creation successful.\n\n"
@@ -110,10 +102,8 @@ def print_success_prompt(
110
102
  bold=True,
111
103
  )
112
104
 
113
- _add = " huggingface-cli login\n" if llm_challenge_str else ""
114
-
115
105
  prompt += typer.style(
116
- f" cd {package_name} && pip install -e .\n" + _add + "\n",
106
+ f" cd {package_name} && pip install -e .\n\n",
117
107
  fg=typer.colors.BRIGHT_CYAN,
118
108
  bold=True,
119
109
  )
@@ -140,6 +130,25 @@ def print_success_prompt(
140
130
  print(prompt)
141
131
 
142
132
 
133
+ def fetch_recommended_apps() -> list[dict[str, str]]:
134
+ """Fetch recommended apps from Platform API."""
135
+ url = f"{PLATFORM_API_URL}/hub/apps?tag=recommended"
136
+ try:
137
+ response = requests.get(url, headers={"accept": "application/json"}, timeout=10)
138
+ response.raise_for_status()
139
+ data = response.json()
140
+ apps = data.get("apps", [])
141
+ return cast(list[dict[str, str]], apps)
142
+
143
+ except requests.RequestException as e:
144
+ typer.secho(
145
+ f"❌ Failed to fetch recommended apps: {e}",
146
+ fg=typer.colors.RED,
147
+ err=True,
148
+ )
149
+ raise typer.Exit(code=1) from e
150
+
151
+
143
152
  # Security: prevent zip-slip
144
153
  def _safe_extract_zip(zf: zipfile.ZipFile, dest_dir: Path) -> None:
145
154
  """Extract ZIP file into destination directory."""
@@ -181,55 +190,36 @@ def _download_zip_to_memory(presigned_url: str) -> io.BytesIO:
181
190
  r = requests.get(presigned_url, timeout=60)
182
191
  r.raise_for_status()
183
192
  except requests.RequestException as e:
184
- raise typer.BadParameter(f"ZIP download failed: {e}") from e
193
+ typer.secho(
194
+ f"ZIP download failed: {e}",
195
+ fg=typer.colors.RED,
196
+ err=True,
197
+ )
198
+ raise typer.Exit(code=1) from e
185
199
 
186
200
  buf = io.BytesIO(r.content)
187
201
  # Validate it's a zip
188
202
  if not zipfile.is_zipfile(buf):
189
- raise typer.BadParameter("Downloaded file is not a valid ZIP")
203
+ typer.secho(
204
+ "Downloaded file is not a valid ZIP",
205
+ fg=typer.colors.RED,
206
+ err=True,
207
+ )
208
+ raise typer.Exit(code=1)
190
209
  buf.seek(0)
191
210
  return buf
192
211
 
193
212
 
194
- def _request_download_link(identifier: str) -> str:
195
- """Request download link from Flower platform API."""
196
- url = f"{PLATFORM_API_URL}/hub/fetch-zip"
197
- headers = {
198
- "Content-Type": "application/json",
199
- "Accept": "application/json",
200
- }
201
- body = {
202
- "identifier": identifier, # send raw string of identifier
203
- }
204
-
213
+ def download_remote_app_via_api(app_spec: str) -> None:
214
+ """Download App from Platform API."""
215
+ # Validate app version and ID format
205
216
  try:
206
- resp = requests.post(url, headers=headers, data=json.dumps(body), timeout=20)
207
- except requests.RequestException as e:
208
- raise typer.BadParameter(f"Unable to connect to Platform API: {e}") from e
209
-
210
- if resp.status_code == 404:
211
- raise typer.BadParameter(f"'{identifier}' not found in Platform API")
212
- if not resp.ok:
213
- raise typer.BadParameter(
214
- f"Platform API request failed with "
215
- f"status {resp.status_code}. Details: {resp.text}"
216
- )
217
+ app_id, app_version = parse_app_spec(app_spec)
218
+ except ValueError as e:
219
+ typer.secho(f" {e}", fg=typer.colors.RED, err=True)
220
+ raise typer.Exit(code=1) from e
217
221
 
218
- data = resp.json()
219
- if "zip_url" not in data:
220
- raise typer.BadParameter("Invalid response from Platform API")
221
- return str(data["zip_url"])
222
-
223
-
224
- def download_remote_app_via_api(identifier: str) -> None:
225
- """Download App from Platform API."""
226
- # Parse @user/app just to derive local dir name
227
- m = re.match(APP_ID_PATTERN, identifier)
228
- if not m:
229
- raise typer.BadParameter(
230
- "Invalid remote app ID. Expected format: '@user_name/app_name'."
231
- )
232
- app_name = m.group("app")
222
+ app_name = app_id.split("/")[1]
233
223
 
234
224
  project_dir = Path.cwd() / app_name
235
225
  if project_dir.exists():
@@ -242,230 +232,32 @@ def download_remote_app_via_api(identifier: str) -> None:
242
232
  ):
243
233
  return
244
234
 
245
- print(
246
- typer.style(
247
- f"\n🔗 Requesting download link for {identifier}...",
248
- fg=typer.colors.GREEN,
249
- bold=True,
250
- )
235
+ typer.secho(
236
+ f"\n🔗 Requesting download link for {app_id}...",
237
+ fg=typer.colors.GREEN,
238
+ bold=True,
251
239
  )
252
- presigned_url = _request_download_link(identifier)
240
+ # Fetch ZIP downloading URL
241
+ url = f"{PLATFORM_API_URL}/hub/fetch-zip"
242
+ try:
243
+ presigned_url, _ = request_download_link(app_id, app_version, url, "zip_url")
244
+ except ValueError as e:
245
+ typer.secho(f"❌ {e}", fg=typer.colors.RED, err=True)
246
+ raise typer.Exit(code=1) from e
253
247
 
254
- print(
255
- typer.style(
256
- "⬇️ Downloading ZIP into memory...",
257
- fg=typer.colors.GREEN,
258
- bold=True,
259
- )
248
+ typer.secho(
249
+ "🔽 Downloading ZIP into memory...",
250
+ fg=typer.colors.GREEN,
251
+ bold=True,
260
252
  )
261
253
  zip_buf = _download_zip_to_memory(presigned_url)
262
254
 
263
- print(
264
- typer.style(
265
- f"📦 Unpacking into {project_dir}...",
266
- fg=typer.colors.GREEN,
267
- bold=True,
268
- )
255
+ typer.secho(
256
+ f"📦 Unpacking into {project_dir}...",
257
+ fg=typer.colors.GREEN,
258
+ bold=True,
269
259
  )
270
260
  with zipfile.ZipFile(zip_buf) as zf:
271
261
  _safe_extract_zip(zf, Path.cwd())
272
262
 
273
263
  print_success_prompt(app_name)
274
-
275
-
276
- # pylint: disable=too-many-locals,too-many-branches,too-many-statements
277
- def new(
278
- app_name: Annotated[
279
- Optional[str],
280
- typer.Argument(help="The name of the Flower App"),
281
- ] = None,
282
- framework: Annotated[
283
- Optional[MlFramework],
284
- typer.Option(case_sensitive=False, help="The ML framework to use"),
285
- ] = None,
286
- username: Annotated[
287
- Optional[str],
288
- typer.Option(case_sensitive=False, help="The Flower username of the author"),
289
- ] = None,
290
- ) -> None:
291
- """Create new Flower App."""
292
- if app_name is None:
293
- app_name = prompt_text("Please provide the app name")
294
-
295
- # Download remote app
296
- if app_name and app_name.startswith("@"):
297
- download_remote_app_via_api(app_name)
298
- return
299
-
300
- if not is_valid_project_name(app_name):
301
- app_name = prompt_text(
302
- "Please provide a name that only contains "
303
- "characters in {'-', a-zA-Z', '0-9'}",
304
- predicate=is_valid_project_name,
305
- default=sanitize_project_name(app_name),
306
- )
307
-
308
- # Set project directory path
309
- package_name = re.sub(r"[-_.]+", "-", app_name).lower()
310
- import_name = package_name.replace("-", "_")
311
- project_dir = Path.cwd() / package_name
312
-
313
- if project_dir.exists():
314
- if not typer.confirm(
315
- typer.style(
316
- f"\n💬 {app_name} already exists, do you want to override it?",
317
- fg=typer.colors.MAGENTA,
318
- bold=True,
319
- )
320
- ):
321
- return
322
-
323
- if username is None:
324
- username = prompt_text("Please provide your Flower username")
325
-
326
- if framework is not None:
327
- framework_str = str(framework.value)
328
- else:
329
- framework_str = prompt_options(
330
- "Please select ML framework by typing in the number",
331
- [mlf.value for mlf in MlFramework],
332
- )
333
-
334
- llm_challenge_str = None
335
- if framework_str == MlFramework.FLOWERTUNE:
336
- llm_challenge_value = prompt_options(
337
- "Please select LLM challenge by typing in the number",
338
- sorted([challenge.value for challenge in LlmChallengeName]),
339
- )
340
- llm_challenge_str = llm_challenge_value.lower()
341
-
342
- if framework_str == MlFramework.BASELINE:
343
- framework_str = "baseline"
344
-
345
- if framework_str == MlFramework.PYTORCH_LEGACY_API:
346
- framework_str = "pytorch_legacy_api"
347
-
348
- print(
349
- typer.style(
350
- f"\n🔨 Creating Flower App {app_name}...",
351
- fg=typer.colors.GREEN,
352
- bold=True,
353
- )
354
- )
355
-
356
- context = {
357
- "framework_str": framework_str,
358
- "import_name": import_name.replace("-", "_"),
359
- "package_name": package_name,
360
- "project_name": app_name,
361
- "username": username,
362
- }
363
-
364
- template_name = framework_str.lower()
365
-
366
- # List of files to render
367
- if llm_challenge_str:
368
- files = {
369
- ".gitignore": {"template": "app/.gitignore.tpl"},
370
- "pyproject.toml": {"template": f"app/pyproject.{template_name}.toml.tpl"},
371
- "README.md": {"template": f"app/README.{template_name}.md.tpl"},
372
- f"{import_name}/__init__.py": {"template": "app/code/__init__.py.tpl"},
373
- f"{import_name}/server_app.py": {
374
- "template": "app/code/flwr_tune/server_app.py.tpl"
375
- },
376
- f"{import_name}/client_app.py": {
377
- "template": "app/code/flwr_tune/client_app.py.tpl"
378
- },
379
- f"{import_name}/models.py": {
380
- "template": "app/code/flwr_tune/models.py.tpl"
381
- },
382
- f"{import_name}/dataset.py": {
383
- "template": "app/code/flwr_tune/dataset.py.tpl"
384
- },
385
- f"{import_name}/strategy.py": {
386
- "template": "app/code/flwr_tune/strategy.py.tpl"
387
- },
388
- }
389
-
390
- # Challenge specific context
391
- fraction_train = "0.2" if llm_challenge_str == "code" else "0.1"
392
- if llm_challenge_str == "generalnlp":
393
- challenge_name = "General NLP"
394
- num_clients = "20"
395
- dataset_name = "flwrlabs/alpaca-gpt4"
396
- elif llm_challenge_str == "finance":
397
- challenge_name = "Finance"
398
- num_clients = "50"
399
- dataset_name = "flwrlabs/fingpt-sentiment-train"
400
- elif llm_challenge_str == "medical":
401
- challenge_name = "Medical"
402
- num_clients = "20"
403
- dataset_name = "flwrlabs/medical-meadow-medical-flashcards"
404
- else:
405
- challenge_name = "Code"
406
- num_clients = "10"
407
- dataset_name = "flwrlabs/code-alpaca-20k"
408
-
409
- context["llm_challenge_str"] = llm_challenge_str
410
- context["fraction_train"] = fraction_train
411
- context["challenge_name"] = challenge_name
412
- context["num_clients"] = num_clients
413
- context["dataset_name"] = dataset_name
414
- else:
415
- files = {
416
- ".gitignore": {"template": "app/.gitignore.tpl"},
417
- "README.md": {"template": "app/README.md.tpl"},
418
- "pyproject.toml": {"template": f"app/pyproject.{template_name}.toml.tpl"},
419
- f"{import_name}/__init__.py": {"template": "app/code/__init__.py.tpl"},
420
- f"{import_name}/server_app.py": {
421
- "template": f"app/code/server.{template_name}.py.tpl"
422
- },
423
- f"{import_name}/client_app.py": {
424
- "template": f"app/code/client.{template_name}.py.tpl"
425
- },
426
- }
427
-
428
- # Depending on the framework, generate task.py file
429
- frameworks_with_tasks = [
430
- MlFramework.PYTORCH.value,
431
- MlFramework.JAX.value,
432
- MlFramework.HUGGINGFACE.value,
433
- MlFramework.MLX.value,
434
- MlFramework.TENSORFLOW.value,
435
- MlFramework.SKLEARN.value,
436
- MlFramework.NUMPY.value,
437
- MlFramework.XGBOOST.value,
438
- "pytorch_legacy_api",
439
- ]
440
- if framework_str in frameworks_with_tasks:
441
- files[f"{import_name}/task.py"] = {
442
- "template": f"app/code/task.{template_name}.py.tpl"
443
- }
444
-
445
- if framework_str == "pytorch_legacy_api":
446
- # Use custom __init__ that better captures name of framework
447
- files[f"{import_name}/__init__.py"] = {
448
- "template": f"app/code/__init__.{framework_str}.py.tpl"
449
- }
450
-
451
- if framework_str == "baseline":
452
- # Include additional files for baseline template
453
- for file_name in ["model", "dataset", "strategy", "utils", "__init__"]:
454
- files[f"{import_name}/{file_name}.py"] = {
455
- "template": f"app/code/{file_name}.{template_name}.py.tpl"
456
- }
457
-
458
- # Replace README.md
459
- files["README.md"]["template"] = f"app/README.{template_name}.md.tpl"
460
-
461
- # Add LICENSE
462
- files["LICENSE"] = {"template": "app/LICENSE.tpl"}
463
-
464
- for file_path, value in files.items():
465
- render_and_create(
466
- file_path=project_dir / file_path,
467
- template=value["template"],
468
- context=context,
469
- )
470
-
471
- print_success_prompt(package_name, llm_challenge_str)
flwr/cli/pull.py CHANGED
@@ -16,7 +16,7 @@
16
16
 
17
17
 
18
18
  from pathlib import Path
19
- from typing import Annotated, Optional
19
+ from typing import Annotated
20
20
 
21
21
  import typer
22
22
 
@@ -50,22 +50,26 @@ def pull( # pylint: disable=R0914
50
50
  typer.Argument(help="Path of the Flower App to run."),
51
51
  ] = Path("."),
52
52
  federation: Annotated[
53
- Optional[str],
53
+ str | None,
54
54
  typer.Argument(help="Name of the federation."),
55
55
  ] = None,
56
56
  federation_config_overrides: Annotated[
57
- Optional[list[str]],
57
+ list[str] | None,
58
58
  typer.Option(
59
59
  "--federation-config",
60
60
  help=FEDERATION_CONFIG_HELP_MESSAGE,
61
61
  ),
62
62
  ] = None,
63
63
  ) -> None:
64
- """Pull artifacts from a Flower run."""
64
+ """Pull artifacts from a Flower run.
65
+
66
+ Retrieve a download URL for artifacts generated during a completed Flower run. The
67
+ artifacts can then be downloaded from the provided URL.
68
+ """
65
69
  typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
66
70
 
67
71
  pyproject_path = app / FAB_CONFIG_FILE if app else None
68
- config, errors, warnings = load_and_validate(path=pyproject_path)
72
+ config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
69
73
  config = process_loaded_project_config(config, errors, warnings)
70
74
  federation, federation_config = validate_federation_in_project_config(
71
75
  federation, config, federation_config_overrides
@@ -88,6 +92,7 @@ def pull( # pylint: disable=R0914
88
92
  "obtained.",
89
93
  fg=typer.colors.RED,
90
94
  bold=True,
95
+ err=True,
91
96
  )
92
97
  raise typer.Exit(code=1)
93
98