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/log.py CHANGED
@@ -18,7 +18,7 @@
18
18
  import time
19
19
  from logging import DEBUG, ERROR, INFO
20
20
  from pathlib import Path
21
- from typing import Annotated, Any, Optional, cast
21
+ from typing import Annotated, Any, cast
22
22
 
23
23
  import grpc
24
24
  import typer
@@ -39,13 +39,27 @@ from .utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
39
39
 
40
40
 
41
41
  class AllLogsRetrieved(BaseException):
42
- """Raised when all logs are retrieved."""
42
+ """Exception raised when all available logs have been retrieved.
43
+
44
+ This exception is used internally to signal that the log stream has reached the end
45
+ and all logs have been successfully retrieved.
46
+ """
43
47
 
44
48
 
45
49
  def start_stream(
46
50
  run_id: int, channel: grpc.Channel, refresh_period: int = CONN_REFRESH_PERIOD
47
51
  ) -> None:
48
- """Start log streaming for a given run ID."""
52
+ """Start log streaming for a given run ID.
53
+
54
+ Parameters
55
+ ----------
56
+ run_id : int
57
+ The unique identifier of the run to stream logs from.
58
+ channel : grpc.Channel
59
+ The gRPC channel for communication.
60
+ refresh_period : int (default: CONN_REFRESH_PERIOD)
61
+ Connection refresh period in seconds.
62
+ """
49
63
  stub = ControlStub(channel)
50
64
  after_timestamp = 0.0
51
65
  try:
@@ -111,7 +125,17 @@ def stream_logs(
111
125
 
112
126
 
113
127
  def print_logs(run_id: int, channel: grpc.Channel, timeout: int) -> None:
114
- """Print logs from the beginning of a run."""
128
+ """Print logs from the beginning of a run.
129
+
130
+ Parameters
131
+ ----------
132
+ run_id : int
133
+ The unique identifier of the run to retrieve logs from.
134
+ channel : grpc.Channel
135
+ The gRPC channel for communication.
136
+ timeout : int
137
+ Timeout duration in seconds for the log retrieval request.
138
+ """
115
139
  stub = ControlStub(channel)
116
140
  req = StreamLogsRequest(run_id=run_id, after_timestamp=0.0)
117
141
 
@@ -143,11 +167,11 @@ def log(
143
167
  typer.Argument(help="Path of the Flower project to run"),
144
168
  ] = Path("."),
145
169
  federation: Annotated[
146
- Optional[str],
170
+ str | None,
147
171
  typer.Argument(help="Name of the federation to run the app on"),
148
172
  ] = None,
149
173
  federation_config_overrides: Annotated[
150
- Optional[list[str]],
174
+ list[str] | None,
151
175
  typer.Option(
152
176
  "--federation-config",
153
177
  help=FEDERATION_CONFIG_HELP_MESSAGE,
@@ -161,11 +185,15 @@ def log(
161
185
  ),
162
186
  ] = True,
163
187
  ) -> None:
164
- """Get logs from a Flower project run."""
188
+ """Get logs from a run.
189
+
190
+ Retrieve and display logs from a Flower run. Logs can be streamed in real-time (with
191
+ --stream) or printed once (with --show).
192
+ """
165
193
  typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
166
194
 
167
195
  pyproject_path = app / "pyproject.toml" if app else None
168
- config, errors, warnings = load_and_validate(path=pyproject_path)
196
+ config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
169
197
  config = process_loaded_project_config(config, errors, warnings)
170
198
  federation, federation_config = validate_federation_in_project_config(
171
199
  federation, config, federation_config_overrides
@@ -175,7 +203,7 @@ def log(
175
203
  try:
176
204
  _log_with_control_api(app, federation, federation_config, run_id, stream)
177
205
  except Exception as err: # pylint: disable=broad-except
178
- typer.secho(str(err), fg=typer.colors.RED, bold=True)
206
+ typer.secho(str(err), fg=typer.colors.RED, bold=True, err=True)
179
207
  raise typer.Exit(code=1) from None
180
208
 
181
209
 
@@ -186,6 +214,21 @@ def _log_with_control_api(
186
214
  run_id: int,
187
215
  stream: bool,
188
216
  ) -> None:
217
+ """Retrieve logs using the Control API.
218
+
219
+ Parameters
220
+ ----------
221
+ app : Path
222
+ Path to the Flower app directory.
223
+ federation : str
224
+ Name of the federation.
225
+ federation_config : dict[str, Any]
226
+ Federation configuration dictionary.
227
+ run_id : int
228
+ The unique identifier of the run to retrieve logs from.
229
+ stream : bool
230
+ If True, stream logs continuously; if False, print once.
231
+ """
189
232
  auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
190
233
  channel = init_channel(app, federation_config, auth_plugin)
191
234
 
flwr/cli/login/login.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,11 +50,11 @@ def login( # 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 to login into."),
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,
@@ -65,7 +65,7 @@ def login( # pylint: disable=R0914
65
65
  typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
66
66
 
67
67
  pyproject_path = app / "pyproject.toml" if app else None
68
- config, errors, warnings = load_and_validate(path=pyproject_path)
68
+ config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
69
69
 
70
70
  config = process_loaded_project_config(config, errors, warnings)
71
71
  federation, federation_config = validate_federation_in_project_config(
@@ -82,6 +82,7 @@ def login( # pylint: disable=R0914
82
82
  "in the federation configuration.",
83
83
  fg=typer.colors.RED,
84
84
  bold=True,
85
+ err=True,
85
86
  )
86
87
  raise typer.Exit(code=1)
87
88
  # Check if insecure flag is set to `True`
@@ -92,6 +93,7 @@ def login( # pylint: disable=R0914
92
93
  "`true` in the federation configuration.",
93
94
  fg=typer.colors.RED,
94
95
  bold=True,
96
+ err=True,
95
97
  )
96
98
  raise typer.Exit(code=1)
97
99
 
@@ -127,6 +129,7 @@ def login( # pylint: disable=R0914
127
129
  f"❌ Login failed: {e.message}",
128
130
  fg=typer.colors.RED,
129
131
  bold=True,
132
+ err=True,
130
133
  )
131
134
  raise typer.Exit(code=1) from None
132
135
 
flwr/cli/ls.py CHANGED
@@ -17,9 +17,8 @@
17
17
 
18
18
  import io
19
19
  import json
20
- from datetime import datetime, timedelta
21
20
  from pathlib import Path
22
- from typing import Annotated, Optional, cast
21
+ from typing import Annotated, cast
23
22
 
24
23
  import typer
25
24
  from rich.console import Console
@@ -33,21 +32,19 @@ from flwr.cli.config_utils import (
33
32
  validate_federation_in_project_config,
34
33
  )
35
34
  from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
36
- from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat, SubStatus
37
- from flwr.common.date import format_timedelta, isoformat8601_utc
35
+ from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat, Status, SubStatus
38
36
  from flwr.common.logger import print_json_error, redirect_output, restore_output
39
37
  from flwr.common.serde import run_from_proto
40
- from flwr.common.typing import Run
41
38
  from flwr.proto.control_pb2 import ( # pylint: disable=E0611
42
39
  ListRunsRequest,
43
40
  ListRunsResponse,
44
41
  )
45
42
  from flwr.proto.control_pb2_grpc import ControlStub
43
+ from flwr.supercore.utils import humanize_bytes, humanize_duration
46
44
 
45
+ from .run_utils import RunRow, format_runs
47
46
  from .utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
48
47
 
49
- _RunListType = tuple[int, str, str, str, str, str, str, str, str]
50
-
51
48
 
52
49
  def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
53
50
  ctx: typer.Context,
@@ -56,11 +53,11 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
56
53
  typer.Argument(help="Path of the Flower project"),
57
54
  ] = Path("."),
58
55
  federation: Annotated[
59
- Optional[str],
56
+ str | None,
60
57
  typer.Argument(help="Name of the federation"),
61
58
  ] = None,
62
59
  federation_config_overrides: Annotated[
63
- Optional[list[str]],
60
+ list[str] | None,
64
61
  typer.Option(
65
62
  "--federation-config",
66
63
  help=FEDERATION_CONFIG_HELP_MESSAGE,
@@ -74,7 +71,7 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
74
71
  ),
75
72
  ] = False,
76
73
  run_id: Annotated[
77
- Optional[int],
74
+ int | None,
78
75
  typer.Option(
79
76
  "--run-id",
80
77
  help="Specific run ID to display",
@@ -94,12 +91,11 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
94
91
  The following details are displayed:
95
92
 
96
93
  - **Run ID:** Unique identifier for the run.
97
- - **FAB:** Name of the FAB associated with the run (``{FAB_ID} (v{FAB_VERSION})``).
94
+ - **Federation:** The federation to which the run belongs.
95
+ - **App:** The App associated with the run (``<APP_ID>==<APP_VERSION>``).
98
96
  - **Status:** Current status of the run (pending, starting, running, finished).
99
97
  - **Elapsed:** Time elapsed since the run started (``HH:MM:SS``).
100
- - **Created At:** Timestamp when the run was created.
101
- - **Running At:** Timestamp when the run started running.
102
- - **Finished At:** Timestamp when the run finished.
98
+ - **Status Changed @:** Timestamp of the most recent status change.
103
99
 
104
100
  All timestamps follow ISO 8601, UTC and are formatted as ``YYYY-MM-DD HH:MM:SSZ``.
105
101
  """
@@ -115,7 +111,7 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
115
111
  typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
116
112
 
117
113
  pyproject_path = app / FAB_CONFIG_FILE if app else None
118
- config, errors, warnings = load_and_validate(path=pyproject_path)
114
+ config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
119
115
  config = process_loaded_project_config(config, errors, warnings)
120
116
  federation, federation_config = validate_federation_in_project_config(
121
117
  federation, config, federation_config_overrides
@@ -143,7 +139,10 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
143
139
  if output_format == CliOutputFormat.JSON:
144
140
  Console().print_json(_to_json(formatted_runs))
145
141
  else:
146
- Console().print(_to_table(formatted_runs))
142
+ if run_id is not None:
143
+ Console().print(_to_detail_table(formatted_runs[0]))
144
+ else:
145
+ Console().print(_to_table(formatted_runs))
147
146
  finally:
148
147
  if channel:
149
148
  channel.close()
@@ -157,6 +156,7 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
157
156
  f"{err}",
158
157
  fg=typer.colors.RED,
159
158
  bold=True,
159
+ err=True,
160
160
  )
161
161
  finally:
162
162
  if suppress_output:
@@ -164,156 +164,237 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
164
164
  captured_output.close()
165
165
 
166
166
 
167
- def _format_runs(run_dict: dict[int, Run], now_isoformat: str) -> list[_RunListType]:
168
- """Format runs to a list."""
169
-
170
- def _format_datetime(dt: Optional[datetime]) -> str:
171
- return isoformat8601_utc(dt).replace("T", " ") if dt else "N/A"
172
-
173
- run_list: list[_RunListType] = []
174
-
175
- # Add rows
176
- for run in sorted(
177
- run_dict.values(), key=lambda x: datetime.fromisoformat(x.pending_at)
178
- ):
179
- # Combine status and sub-status into a single string
180
- if run.status.sub_status == "":
181
- status_text = run.status.status
182
- else:
183
- status_text = f"{run.status.status}:{run.status.sub_status}"
184
-
185
- # Convert isoformat to datetime
186
- pending_at = datetime.fromisoformat(run.pending_at) if run.pending_at else None
187
- running_at = datetime.fromisoformat(run.running_at) if run.running_at else None
188
- finished_at = (
189
- datetime.fromisoformat(run.finished_at) if run.finished_at else None
190
- )
167
+ def _get_status_style(status_text: str) -> str:
168
+ """Determine the display style/color for a status.
191
169
 
192
- # Calculate elapsed time
193
- elapsed_time = timedelta()
194
- if running_at:
195
- if finished_at:
196
- end_time = finished_at
197
- else:
198
- end_time = datetime.fromisoformat(now_isoformat)
199
- elapsed_time = end_time - running_at
200
-
201
- run_list.append(
202
- (
203
- run.run_id,
204
- run.fab_id,
205
- run.fab_version,
206
- run.fab_hash,
207
- status_text,
208
- format_timedelta(elapsed_time),
209
- _format_datetime(pending_at),
210
- _format_datetime(running_at),
211
- _format_datetime(finished_at),
212
- )
213
- )
214
- return run_list
170
+ Parameters
171
+ ----------
172
+ status_text : str
173
+ The status text to determine color for.
215
174
 
216
-
217
- def _to_table(run_list: list[_RunListType]) -> Table:
218
- """Format the provided run list to a rich Table."""
175
+ Returns
176
+ -------
177
+ str
178
+ Color name for rich console styling (e.g., 'green', 'red', 'blue').
179
+ """
180
+ status = status_text.lower()
181
+ sub_status = status_text.rsplit(":", maxsplit=1)[-1]
182
+
183
+ if sub_status == SubStatus.COMPLETED: # finished:completed
184
+ return "green"
185
+ if sub_status == SubStatus.FAILED: # finished:failed
186
+ return "red"
187
+ if sub_status == SubStatus.STOPPED: # finished:stopped
188
+ return "yellow"
189
+ if status in (Status.STARTING, Status.RUNNING): # starting, running
190
+ return "blue"
191
+ return "bright_black" # pending
192
+
193
+
194
+ def _to_table(run_list: list[RunRow]) -> Table:
195
+ """Format the provided run list to a rich Table.
196
+
197
+ Parameters
198
+ ----------
199
+ run_list : list[RunRow]
200
+ List of run information to display.
201
+
202
+ Returns
203
+ -------
204
+ Table
205
+ Rich Table object with formatted run information.
206
+ """
219
207
  table = Table(header_style="bold cyan", show_lines=True)
220
208
 
221
209
  # Add columns
222
- table.add_column(
223
- Text("Run ID", justify="center"), style="bright_black", no_wrap=True
224
- )
225
- table.add_column(Text("FAB", justify="center"), style="bright_black")
210
+ table.add_column(Text("Run ID", justify="center"), no_wrap=True)
211
+ table.add_column(Text("Federation", justify="center"))
212
+ table.add_column(Text("App", justify="center"))
226
213
  table.add_column(Text("Status", justify="center"))
227
214
  table.add_column(Text("Elapsed", justify="center"), style="blue")
228
- table.add_column(Text("Created At", justify="center"), style="bright_black")
229
- table.add_column(Text("Running At", justify="center"), style="bright_black")
230
- table.add_column(Text("Finished At", justify="center"), style="bright_black")
215
+ table.add_column(Text("Status Changed @", justify="center"))
231
216
 
232
217
  for row in run_list:
233
- (
234
- run_id,
235
- fab_id,
236
- fab_version,
237
- _,
238
- status_text,
239
- elapsed,
240
- created_at,
241
- running_at,
242
- finished_at,
243
- ) = row
244
- # Style the status based on its value
245
- sub_status = status_text.rsplit(":", maxsplit=1)[-1]
246
- if sub_status == SubStatus.COMPLETED:
247
- status_style = "green"
248
- elif sub_status == SubStatus.FAILED:
249
- status_style = "red"
218
+ status_style = _get_status_style(row.status_text)
219
+
220
+ # Use the most recent timestamp
221
+ if row.finished_at != "N/A":
222
+ status_changed_at = row.finished_at
223
+ elif row.running_at != "N/A":
224
+ status_changed_at = row.running_at
225
+ elif row.starting_at != "N/A":
226
+ status_changed_at = row.starting_at
250
227
  else:
251
- status_style = "yellow"
228
+ status_changed_at = row.pending_at
252
229
 
253
230
  formatted_row = (
254
- f"[bold]{run_id}[/bold]",
255
- f"{fab_id} (v{fab_version})",
256
- f"[{status_style}]{status_text}[/{status_style}]",
257
- elapsed,
258
- created_at,
259
- running_at,
260
- finished_at,
231
+ f"[bold]{row.run_id}[/bold]",
232
+ row.federation,
233
+ f"@{row.fab_id}=={row.fab_version}",
234
+ f"[{status_style}]{row.status_text}[/{status_style}]",
235
+ humanize_duration(row.elapsed),
236
+ status_changed_at,
261
237
  )
262
238
  table.add_row(*formatted_row)
263
239
 
264
240
  return table
265
241
 
266
242
 
267
- def _to_json(run_list: list[_RunListType]) -> str:
268
- """Format run status list to a JSON formatted string."""
243
+ def _to_detail_table(run: RunRow) -> Table:
244
+ """Format a single run's details in a vertical table layout.
245
+
246
+ Parameters
247
+ ----------
248
+ run : RunRow
249
+ The run information to display.
250
+
251
+ Returns
252
+ -------
253
+ Table
254
+ Rich Table object with detailed run information in vertical format.
255
+ """
256
+ status_style = _get_status_style(run.status_text)
257
+
258
+ # Create vertical table with field names on the left
259
+ table = Table(show_header=False, show_lines=False)
260
+ table.add_column("Field", style="bold cyan", no_wrap=True)
261
+ table.add_column("Value")
262
+
263
+ # Add rows with all details
264
+ table.add_row("Run ID", f"[bold]{run.run_id}[/bold]")
265
+ table.add_row("Federation", run.federation)
266
+ table.add_row("App", f"@{run.fab_id}=={run.fab_version}")
267
+ table.add_row("FAB Hash", f"{run.fab_hash[:8]}...{run.fab_hash[-8:]}")
268
+ table.add_row("Status", f"[{status_style}]{run.status_text}[/{status_style}]")
269
+ table.add_row("Elapsed", f"[blue]{humanize_duration(run.elapsed)}[/blue]")
270
+ table.add_row("Pending At", run.pending_at)
271
+ table.add_row("Starting At", run.starting_at)
272
+ table.add_row("Running At", run.running_at)
273
+ table.add_row("Finished At", run.finished_at)
274
+ table.add_row(
275
+ "Network traffic (inbound)",
276
+ f"[blue]{humanize_bytes(run.network_traffic_inbound)}[/blue]",
277
+ )
278
+ table.add_row(
279
+ "Network traffic (outbound)",
280
+ f"[blue]{humanize_bytes(run.network_traffic_outbound)}[/blue]",
281
+ )
282
+ table.add_row(
283
+ "Network Traffic (total)",
284
+ "[blue]"
285
+ f"{humanize_bytes(run.network_traffic_inbound + run.network_traffic_outbound)}"
286
+ "[/blue]",
287
+ )
288
+ table.add_row(
289
+ "Compute Time (ServerApp)",
290
+ f"[blue]{humanize_duration(run.compute_time_serverapp)}[/blue]",
291
+ )
292
+ table.add_row(
293
+ "Compute Time (ClientApp)",
294
+ f"[blue]{humanize_duration(run.compute_time_clientapp)}[/blue]",
295
+ )
296
+ table.add_row(
297
+ "Compute Time (total)",
298
+ "[blue]"
299
+ f"{humanize_duration(run.compute_time_serverapp + run.compute_time_clientapp)}"
300
+ "[/blue]",
301
+ )
302
+
303
+ return table
304
+
305
+
306
+ def _to_json(run_list: list[RunRow]) -> str:
307
+ """Format run status list to a JSON formatted string.
308
+
309
+ Parameters
310
+ ----------
311
+ run_list : list[RunRow]
312
+ List of run information to serialize.
313
+
314
+ Returns
315
+ -------
316
+ str
317
+ JSON string containing formatted run information.
318
+ """
269
319
  runs_list = []
270
320
  for row in run_list:
271
- (
272
- run_id,
273
- fab_id,
274
- fab_version,
275
- fab_hash,
276
- status_text,
277
- elapsed,
278
- created_at,
279
- running_at,
280
- finished_at,
281
- ) = row
282
321
  runs_list.append(
283
322
  {
284
- "run-id": run_id,
285
- "fab-id": fab_id,
286
- "fab-name": fab_id.split("/")[-1],
287
- "fab-version": fab_version,
288
- "fab-hash": fab_hash[:8],
289
- "status": status_text,
290
- "elapsed": elapsed,
291
- "created-at": created_at,
292
- "running-at": running_at,
293
- "finished-at": finished_at,
323
+ "run-id": f"{row.run_id}",
324
+ "federation": row.federation,
325
+ "fab-id": row.fab_id,
326
+ "fab-name": row.fab_id.split("/")[-1],
327
+ "fab-version": row.fab_version,
328
+ "fab-hash": row.fab_hash[:8],
329
+ "status": row.status_text,
330
+ "elapsed": row.elapsed,
331
+ "pending-at": row.pending_at,
332
+ "starting-at": row.starting_at,
333
+ "running-at": row.running_at,
334
+ "finished-at": row.finished_at,
335
+ "network-traffic": {
336
+ "inbound-bytes": row.network_traffic_inbound,
337
+ "outbound-bytes": row.network_traffic_outbound,
338
+ "total-bytes": row.network_traffic_inbound
339
+ + row.network_traffic_outbound,
340
+ },
341
+ "compute-time": {
342
+ "serverapp-seconds": row.compute_time_serverapp,
343
+ "clientapp-seconds": row.compute_time_clientapp,
344
+ "total-seconds": row.compute_time_serverapp
345
+ + row.compute_time_clientapp,
346
+ },
294
347
  }
295
348
  )
296
349
 
297
350
  return json.dumps({"success": True, "runs": runs_list})
298
351
 
299
352
 
300
- def _list_runs(stub: ControlStub) -> list[_RunListType]:
301
- """List all runs."""
353
+ def _list_runs(stub: ControlStub) -> list[RunRow]:
354
+ """List all runs.
355
+
356
+ Parameters
357
+ ----------
358
+ stub : ControlStub
359
+ The gRPC stub for Control API communication.
360
+
361
+ Returns
362
+ -------
363
+ list[RunRow]
364
+ List of formatted run information for all runs.
365
+ """
302
366
  with flwr_cli_grpc_exc_handler():
303
367
  res: ListRunsResponse = stub.ListRuns(ListRunsRequest())
304
- run_dict = {run_id: run_from_proto(proto) for run_id, proto in res.run_dict.items()}
368
+ runs = [run_from_proto(proto) for proto in res.run_dict.values()]
369
+
370
+ return format_runs(runs, res.now)
305
371
 
306
- return _format_runs(run_dict, res.now)
307
372
 
373
+ def _display_one_run(stub: ControlStub, run_id: int) -> list[RunRow]:
374
+ """Display information about a specific run.
308
375
 
309
- def _display_one_run(stub: ControlStub, run_id: int) -> list[_RunListType]:
310
- """Display information about a specific run."""
376
+ Parameters
377
+ ----------
378
+ stub : ControlStub
379
+ The gRPC stub for Control API communication.
380
+ run_id : int
381
+ The unique identifier of the run to display.
382
+
383
+ Returns
384
+ -------
385
+ list[RunRow]
386
+ List containing the formatted run information (single item).
387
+
388
+ Raises
389
+ ------
390
+ ValueError
391
+ If the run_id is not found.
392
+ """
311
393
  with flwr_cli_grpc_exc_handler():
312
394
  res: ListRunsResponse = stub.ListRuns(ListRunsRequest(run_id=run_id))
313
395
  if not res.run_dict:
314
396
  # This won't be reached as an gRPC error is raised if run_id is invalid
315
397
  raise ValueError(f"Run ID {run_id} not found")
316
398
 
317
- run_dict = {run_id: run_from_proto(proto) for run_id, proto in res.run_dict.items()}
318
-
319
- return _format_runs(run_dict, res.now)
399
+ runs = [run_from_proto(proto) for proto in res.run_dict.values()]
400
+ return format_runs(runs, res.now)