flwr 1.23.0__py3-none-any.whl → 1.24.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 (292) 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/app_cmd/__init__.py +23 -0
  6. flwr/cli/app_cmd/publish.py +285 -0
  7. flwr/cli/app_cmd/review.py +252 -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/federation/__init__.py +24 -0
  15. flwr/cli/federation/ls.py +140 -0
  16. flwr/cli/federation/show.py +317 -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 +170 -130
  21. flwr/cli/new/new.py +33 -50
  22. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +1 -0
  23. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
  24. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
  25. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  26. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  27. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  28. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  29. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
  30. flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +1 -1
  31. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  32. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +2 -2
  33. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +1 -1
  34. flwr/cli/pull.py +10 -5
  35. flwr/cli/run/run.py +77 -30
  36. flwr/cli/run_utils.py +130 -0
  37. flwr/cli/stop.py +25 -7
  38. flwr/cli/supernode/ls.py +16 -8
  39. flwr/cli/supernode/register.py +9 -4
  40. flwr/cli/supernode/unregister.py +5 -3
  41. flwr/cli/utils.py +376 -16
  42. flwr/client/__init__.py +1 -1
  43. flwr/client/dpfedavg_numpy_client.py +4 -1
  44. flwr/client/grpc_adapter_client/connection.py +6 -7
  45. flwr/client/grpc_rere_client/connection.py +10 -11
  46. flwr/client/grpc_rere_client/grpc_adapter.py +6 -2
  47. flwr/client/grpc_rere_client/node_auth_client_interceptor.py +2 -1
  48. flwr/client/message_handler/message_handler.py +2 -2
  49. flwr/client/mod/secure_aggregation/secaggplus_mod.py +3 -3
  50. flwr/client/numpy_client.py +1 -1
  51. flwr/client/rest_client/connection.py +12 -14
  52. flwr/client/run_info_store.py +4 -5
  53. flwr/client/typing.py +1 -1
  54. flwr/clientapp/client_app.py +9 -10
  55. flwr/clientapp/mod/centraldp_mods.py +16 -17
  56. flwr/clientapp/mod/localdp_mod.py +8 -9
  57. flwr/clientapp/typing.py +1 -1
  58. flwr/clientapp/utils.py +3 -3
  59. flwr/common/address.py +1 -2
  60. flwr/common/args.py +3 -4
  61. flwr/common/config.py +13 -16
  62. flwr/common/constant.py +5 -2
  63. flwr/common/differential_privacy.py +3 -4
  64. flwr/common/event_log_plugin/event_log_plugin.py +3 -4
  65. flwr/common/exit/exit.py +15 -2
  66. flwr/common/exit/exit_code.py +19 -0
  67. flwr/common/exit/exit_handler.py +6 -2
  68. flwr/common/exit/signal_handler.py +5 -5
  69. flwr/common/grpc.py +6 -6
  70. flwr/common/inflatable_protobuf_utils.py +1 -1
  71. flwr/common/inflatable_utils.py +38 -21
  72. flwr/common/logger.py +19 -19
  73. flwr/common/message.py +4 -4
  74. flwr/common/object_ref.py +7 -7
  75. flwr/common/record/array.py +3 -3
  76. flwr/common/record/arrayrecord.py +18 -30
  77. flwr/common/record/configrecord.py +3 -3
  78. flwr/common/record/recorddict.py +5 -5
  79. flwr/common/record/typeddict.py +9 -2
  80. flwr/common/recorddict_compat.py +7 -10
  81. flwr/common/retry_invoker.py +20 -20
  82. flwr/common/secure_aggregation/ndarrays_arithmetic.py +3 -3
  83. flwr/common/serde.py +5 -4
  84. flwr/common/serde_utils.py +2 -2
  85. flwr/common/telemetry.py +9 -5
  86. flwr/common/typing.py +52 -37
  87. flwr/compat/client/app.py +38 -37
  88. flwr/compat/client/grpc_client/connection.py +11 -11
  89. flwr/compat/server/app.py +5 -6
  90. flwr/proto/appio_pb2.py +13 -3
  91. flwr/proto/appio_pb2.pyi +134 -65
  92. flwr/proto/appio_pb2_grpc.py +20 -0
  93. flwr/proto/appio_pb2_grpc.pyi +27 -0
  94. flwr/proto/clientappio_pb2.py +17 -7
  95. flwr/proto/clientappio_pb2.pyi +15 -0
  96. flwr/proto/clientappio_pb2_grpc.py +206 -40
  97. flwr/proto/clientappio_pb2_grpc.pyi +168 -53
  98. flwr/proto/control_pb2.py +71 -52
  99. flwr/proto/control_pb2.pyi +277 -111
  100. flwr/proto/control_pb2_grpc.py +249 -40
  101. flwr/proto/control_pb2_grpc.pyi +185 -52
  102. flwr/proto/error_pb2.py +13 -3
  103. flwr/proto/error_pb2.pyi +24 -6
  104. flwr/proto/error_pb2_grpc.py +20 -0
  105. flwr/proto/error_pb2_grpc.pyi +27 -0
  106. flwr/proto/fab_pb2.py +14 -4
  107. flwr/proto/fab_pb2.pyi +59 -31
  108. flwr/proto/fab_pb2_grpc.py +20 -0
  109. flwr/proto/fab_pb2_grpc.pyi +27 -0
  110. flwr/proto/federation_pb2.py +38 -0
  111. flwr/proto/federation_pb2.pyi +56 -0
  112. flwr/proto/federation_pb2_grpc.py +24 -0
  113. flwr/proto/federation_pb2_grpc.pyi +31 -0
  114. flwr/proto/fleet_pb2.py +14 -4
  115. flwr/proto/fleet_pb2.pyi +137 -61
  116. flwr/proto/fleet_pb2_grpc.py +189 -48
  117. flwr/proto/fleet_pb2_grpc.pyi +175 -61
  118. flwr/proto/grpcadapter_pb2.py +14 -4
  119. flwr/proto/grpcadapter_pb2.pyi +38 -16
  120. flwr/proto/grpcadapter_pb2_grpc.py +35 -4
  121. flwr/proto/grpcadapter_pb2_grpc.pyi +38 -7
  122. flwr/proto/heartbeat_pb2.py +17 -7
  123. flwr/proto/heartbeat_pb2.pyi +51 -22
  124. flwr/proto/heartbeat_pb2_grpc.py +20 -0
  125. flwr/proto/heartbeat_pb2_grpc.pyi +27 -0
  126. flwr/proto/log_pb2.py +13 -3
  127. flwr/proto/log_pb2.pyi +34 -11
  128. flwr/proto/log_pb2_grpc.py +20 -0
  129. flwr/proto/log_pb2_grpc.pyi +27 -0
  130. flwr/proto/message_pb2.py +15 -5
  131. flwr/proto/message_pb2.pyi +154 -86
  132. flwr/proto/message_pb2_grpc.py +20 -0
  133. flwr/proto/message_pb2_grpc.pyi +27 -0
  134. flwr/proto/node_pb2.py +15 -5
  135. flwr/proto/node_pb2.pyi +50 -25
  136. flwr/proto/node_pb2_grpc.py +20 -0
  137. flwr/proto/node_pb2_grpc.pyi +27 -0
  138. flwr/proto/recorddict_pb2.py +13 -3
  139. flwr/proto/recorddict_pb2.pyi +184 -107
  140. flwr/proto/recorddict_pb2_grpc.py +20 -0
  141. flwr/proto/recorddict_pb2_grpc.pyi +27 -0
  142. flwr/proto/run_pb2.py +40 -31
  143. flwr/proto/run_pb2.pyi +149 -84
  144. flwr/proto/run_pb2_grpc.py +20 -0
  145. flwr/proto/run_pb2_grpc.pyi +27 -0
  146. flwr/proto/serverappio_pb2.py +13 -3
  147. flwr/proto/serverappio_pb2.pyi +32 -8
  148. flwr/proto/serverappio_pb2_grpc.py +246 -65
  149. flwr/proto/serverappio_pb2_grpc.pyi +221 -85
  150. flwr/proto/simulationio_pb2.py +16 -8
  151. flwr/proto/simulationio_pb2.pyi +15 -0
  152. flwr/proto/simulationio_pb2_grpc.py +162 -41
  153. flwr/proto/simulationio_pb2_grpc.pyi +149 -55
  154. flwr/proto/transport_pb2.py +20 -10
  155. flwr/proto/transport_pb2.pyi +249 -160
  156. flwr/proto/transport_pb2_grpc.py +35 -4
  157. flwr/proto/transport_pb2_grpc.pyi +38 -8
  158. flwr/server/app.py +38 -17
  159. flwr/server/client_manager.py +4 -5
  160. flwr/server/client_proxy.py +10 -11
  161. flwr/server/compat/app.py +4 -5
  162. flwr/server/compat/app_utils.py +2 -1
  163. flwr/server/compat/grid_client_proxy.py +10 -12
  164. flwr/server/compat/legacy_context.py +3 -4
  165. flwr/server/fleet_event_log_interceptor.py +2 -1
  166. flwr/server/grid/grid.py +2 -3
  167. flwr/server/grid/grpc_grid.py +10 -8
  168. flwr/server/grid/inmemory_grid.py +4 -4
  169. flwr/server/run_serverapp.py +2 -3
  170. flwr/server/server.py +34 -39
  171. flwr/server/server_app.py +7 -8
  172. flwr/server/server_config.py +1 -2
  173. flwr/server/serverapp/app.py +34 -28
  174. flwr/server/serverapp_components.py +4 -5
  175. flwr/server/strategy/aggregate.py +9 -8
  176. flwr/server/strategy/bulyan.py +13 -11
  177. flwr/server/strategy/dp_adaptive_clipping.py +16 -20
  178. flwr/server/strategy/dp_fixed_clipping.py +12 -17
  179. flwr/server/strategy/dpfedavg_adaptive.py +3 -4
  180. flwr/server/strategy/dpfedavg_fixed.py +6 -10
  181. flwr/server/strategy/fault_tolerant_fedavg.py +14 -13
  182. flwr/server/strategy/fedadagrad.py +18 -14
  183. flwr/server/strategy/fedadam.py +16 -14
  184. flwr/server/strategy/fedavg.py +16 -17
  185. flwr/server/strategy/fedavg_android.py +15 -15
  186. flwr/server/strategy/fedavgm.py +21 -18
  187. flwr/server/strategy/fedmedian.py +2 -3
  188. flwr/server/strategy/fedopt.py +11 -10
  189. flwr/server/strategy/fedprox.py +10 -9
  190. flwr/server/strategy/fedtrimmedavg.py +12 -11
  191. flwr/server/strategy/fedxgb_bagging.py +13 -11
  192. flwr/server/strategy/fedxgb_cyclic.py +6 -6
  193. flwr/server/strategy/fedxgb_nn_avg.py +4 -4
  194. flwr/server/strategy/fedyogi.py +16 -14
  195. flwr/server/strategy/krum.py +12 -11
  196. flwr/server/strategy/qfedavg.py +16 -15
  197. flwr/server/strategy/strategy.py +6 -9
  198. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +2 -1
  199. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -2
  200. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +3 -4
  201. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +10 -12
  202. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +1 -3
  203. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +4 -4
  204. flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +3 -2
  205. flwr/server/superlink/fleet/message_handler/message_handler.py +34 -28
  206. flwr/server/superlink/fleet/rest_rere/rest_api.py +2 -2
  207. flwr/server/superlink/fleet/vce/backend/backend.py +1 -1
  208. flwr/server/superlink/fleet/vce/backend/raybackend.py +5 -5
  209. flwr/server/superlink/fleet/vce/vce_api.py +15 -9
  210. flwr/server/superlink/linkstate/in_memory_linkstate.py +115 -150
  211. flwr/server/superlink/linkstate/linkstate.py +59 -43
  212. flwr/server/superlink/linkstate/linkstate_factory.py +22 -5
  213. flwr/server/superlink/linkstate/sqlite_linkstate.py +447 -438
  214. flwr/server/superlink/linkstate/utils.py +6 -6
  215. flwr/server/superlink/serverappio/serverappio_grpc.py +1 -2
  216. flwr/server/superlink/serverappio/serverappio_servicer.py +26 -21
  217. flwr/server/superlink/simulation/simulationio_grpc.py +1 -2
  218. flwr/server/superlink/simulation/simulationio_servicer.py +18 -13
  219. flwr/server/superlink/utils.py +4 -6
  220. flwr/server/typing.py +1 -1
  221. flwr/server/utils/tensorboard.py +15 -8
  222. flwr/server/workflow/default_workflows.py +5 -5
  223. flwr/server/workflow/secure_aggregation/secagg_workflow.py +2 -4
  224. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +8 -8
  225. flwr/serverapp/strategy/bulyan.py +16 -15
  226. flwr/serverapp/strategy/dp_adaptive_clipping.py +12 -11
  227. flwr/serverapp/strategy/dp_fixed_clipping.py +11 -14
  228. flwr/serverapp/strategy/fedadagrad.py +10 -11
  229. flwr/serverapp/strategy/fedadam.py +10 -11
  230. flwr/serverapp/strategy/fedavg.py +9 -10
  231. flwr/serverapp/strategy/fedavgm.py +17 -16
  232. flwr/serverapp/strategy/fedmedian.py +2 -2
  233. flwr/serverapp/strategy/fedopt.py +10 -11
  234. flwr/serverapp/strategy/fedprox.py +7 -8
  235. flwr/serverapp/strategy/fedtrimmedavg.py +9 -9
  236. flwr/serverapp/strategy/fedxgb_bagging.py +3 -3
  237. flwr/serverapp/strategy/fedxgb_cyclic.py +9 -9
  238. flwr/serverapp/strategy/fedyogi.py +9 -11
  239. flwr/serverapp/strategy/krum.py +7 -7
  240. flwr/serverapp/strategy/multikrum.py +9 -9
  241. flwr/serverapp/strategy/qfedavg.py +17 -16
  242. flwr/serverapp/strategy/strategy.py +6 -9
  243. flwr/serverapp/strategy/strategy_utils.py +7 -8
  244. flwr/simulation/app.py +46 -42
  245. flwr/simulation/legacy_app.py +12 -12
  246. flwr/simulation/ray_transport/ray_actor.py +10 -11
  247. flwr/simulation/ray_transport/ray_client_proxy.py +11 -12
  248. flwr/simulation/run_simulation.py +43 -43
  249. flwr/simulation/simulationio_connection.py +4 -4
  250. flwr/supercore/cli/flower_superexec.py +3 -4
  251. flwr/supercore/constant.py +31 -1
  252. flwr/supercore/corestate/corestate.py +24 -3
  253. flwr/supercore/corestate/in_memory_corestate.py +138 -0
  254. flwr/supercore/corestate/sqlite_corestate.py +157 -0
  255. flwr/supercore/ffs/disk_ffs.py +1 -2
  256. flwr/supercore/ffs/ffs.py +1 -2
  257. flwr/supercore/ffs/ffs_factory.py +1 -2
  258. flwr/{common → supercore}/heartbeat.py +20 -25
  259. flwr/supercore/object_store/in_memory_object_store.py +1 -2
  260. flwr/supercore/object_store/object_store.py +1 -2
  261. flwr/supercore/object_store/object_store_factory.py +1 -2
  262. flwr/supercore/object_store/sqlite_object_store.py +8 -7
  263. flwr/supercore/primitives/asymmetric.py +1 -1
  264. flwr/supercore/primitives/asymmetric_ed25519.py +11 -1
  265. flwr/supercore/sqlite_mixin.py +37 -34
  266. flwr/supercore/superexec/plugin/base_exec_plugin.py +1 -2
  267. flwr/supercore/superexec/plugin/exec_plugin.py +3 -3
  268. flwr/supercore/superexec/run_superexec.py +9 -13
  269. flwr/superlink/artifact_provider/artifact_provider.py +1 -2
  270. flwr/superlink/auth_plugin/auth_plugin.py +6 -9
  271. flwr/superlink/auth_plugin/noop_auth_plugin.py +6 -9
  272. flwr/superlink/federation/__init__.py +24 -0
  273. flwr/superlink/federation/federation_manager.py +64 -0
  274. flwr/superlink/federation/noop_federation_manager.py +71 -0
  275. flwr/superlink/servicer/control/control_account_auth_interceptor.py +22 -13
  276. flwr/superlink/servicer/control/control_event_log_interceptor.py +7 -7
  277. flwr/superlink/servicer/control/control_grpc.py +5 -6
  278. flwr/superlink/servicer/control/control_license_interceptor.py +3 -3
  279. flwr/superlink/servicer/control/control_servicer.py +102 -18
  280. flwr/supernode/cli/flower_supernode.py +58 -3
  281. flwr/supernode/nodestate/in_memory_nodestate.py +60 -49
  282. flwr/supernode/nodestate/nodestate.py +7 -8
  283. flwr/supernode/nodestate/nodestate_factory.py +7 -4
  284. flwr/supernode/runtime/run_clientapp.py +41 -22
  285. flwr/supernode/servicer/clientappio/clientappio_servicer.py +40 -10
  286. flwr/supernode/start_client_internal.py +158 -42
  287. {flwr-1.23.0.dist-info → flwr-1.24.0.dist-info}/METADATA +8 -8
  288. flwr-1.24.0.dist-info/RECORD +454 -0
  289. flwr/supercore/object_store/utils.py +0 -43
  290. flwr-1.23.0.dist-info/RECORD +0 -439
  291. {flwr-1.23.0.dist-info → flwr-1.24.0.dist-info}/WHEEL +0 -0
  292. {flwr-1.23.0.dist-info → flwr-1.24.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,18 @@ 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
46
43
 
44
+ from .run_utils import RunRow, format_runs
47
45
  from .utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
48
46
 
49
- _RunListType = tuple[int, str, str, str, str, str, str, str, str]
50
-
51
47
 
52
48
  def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
53
49
  ctx: typer.Context,
@@ -56,11 +52,11 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
56
52
  typer.Argument(help="Path of the Flower project"),
57
53
  ] = Path("."),
58
54
  federation: Annotated[
59
- Optional[str],
55
+ str | None,
60
56
  typer.Argument(help="Name of the federation"),
61
57
  ] = None,
62
58
  federation_config_overrides: Annotated[
63
- Optional[list[str]],
59
+ list[str] | None,
64
60
  typer.Option(
65
61
  "--federation-config",
66
62
  help=FEDERATION_CONFIG_HELP_MESSAGE,
@@ -74,7 +70,7 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
74
70
  ),
75
71
  ] = False,
76
72
  run_id: Annotated[
77
- Optional[int],
73
+ int | None,
78
74
  typer.Option(
79
75
  "--run-id",
80
76
  help="Specific run ID to display",
@@ -94,12 +90,11 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
94
90
  The following details are displayed:
95
91
 
96
92
  - **Run ID:** Unique identifier for the run.
97
- - **FAB:** Name of the FAB associated with the run (``{FAB_ID} (v{FAB_VERSION})``).
93
+ - **Federation:** The federation to which the run belongs.
94
+ - **App:** The App associated with the run (``<APP_ID>==<APP_VERSION>``).
98
95
  - **Status:** Current status of the run (pending, starting, running, finished).
99
96
  - **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.
97
+ - **Status Changed @:** Timestamp of the most recent status change.
103
98
 
104
99
  All timestamps follow ISO 8601, UTC and are formatted as ``YYYY-MM-DD HH:MM:SSZ``.
105
100
  """
@@ -115,7 +110,7 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
115
110
  typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
116
111
 
117
112
  pyproject_path = app / FAB_CONFIG_FILE if app else None
118
- config, errors, warnings = load_and_validate(path=pyproject_path)
113
+ config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
119
114
  config = process_loaded_project_config(config, errors, warnings)
120
115
  federation, federation_config = validate_federation_in_project_config(
121
116
  federation, config, federation_config_overrides
@@ -143,7 +138,10 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
143
138
  if output_format == CliOutputFormat.JSON:
144
139
  Console().print_json(_to_json(formatted_runs))
145
140
  else:
146
- Console().print(_to_table(formatted_runs))
141
+ if run_id is not None:
142
+ Console().print(_to_detail_table(formatted_runs[0]))
143
+ else:
144
+ Console().print(_to_table(formatted_runs))
147
145
  finally:
148
146
  if channel:
149
147
  channel.close()
@@ -157,6 +155,7 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
157
155
  f"{err}",
158
156
  fg=typer.colors.RED,
159
157
  bold=True,
158
+ err=True,
160
159
  )
161
160
  finally:
162
161
  if suppress_output:
@@ -164,156 +163,197 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
164
163
  captured_output.close()
165
164
 
166
165
 
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
- )
191
-
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
166
+ def _get_status_style(status_text: str) -> str:
167
+ """Determine the display style/color for a status.
215
168
 
169
+ Parameters
170
+ ----------
171
+ status_text : str
172
+ The status text to determine color for.
216
173
 
217
- def _to_table(run_list: list[_RunListType]) -> Table:
218
- """Format the provided run list to a rich Table."""
174
+ Returns
175
+ -------
176
+ str
177
+ Color name for rich console styling (e.g., 'green', 'red', 'blue').
178
+ """
179
+ status = status_text.lower()
180
+ sub_status = status_text.rsplit(":", maxsplit=1)[-1]
181
+
182
+ if sub_status == SubStatus.COMPLETED: # finished:completed
183
+ return "green"
184
+ if sub_status == SubStatus.FAILED: # finished:failed
185
+ return "red"
186
+ if sub_status == SubStatus.STOPPED: # finished:stopped
187
+ return "yellow"
188
+ if status in (Status.STARTING, Status.RUNNING): # starting, running
189
+ return "blue"
190
+ return "bright_black" # pending
191
+
192
+
193
+ def _to_table(run_list: list[RunRow]) -> Table:
194
+ """Format the provided run list to a rich Table.
195
+
196
+ Parameters
197
+ ----------
198
+ run_list : list[RunRow]
199
+ List of run information to display.
200
+
201
+ Returns
202
+ -------
203
+ Table
204
+ Rich Table object with formatted run information.
205
+ """
219
206
  table = Table(header_style="bold cyan", show_lines=True)
220
207
 
221
208
  # 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")
209
+ table.add_column(Text("Run ID", justify="center"), no_wrap=True)
210
+ table.add_column(Text("Federation", justify="center"))
211
+ table.add_column(Text("App", justify="center"))
226
212
  table.add_column(Text("Status", justify="center"))
227
213
  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")
214
+ table.add_column(Text("Status Changed @", justify="center"))
231
215
 
232
216
  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"
217
+ status_style = _get_status_style(row.status_text)
218
+
219
+ # Use the most recent timestamp
220
+ if row.finished_at != "N/A":
221
+ status_changed_at = row.finished_at
222
+ elif row.running_at != "N/A":
223
+ status_changed_at = row.running_at
224
+ elif row.starting_at != "N/A":
225
+ status_changed_at = row.starting_at
250
226
  else:
251
- status_style = "yellow"
227
+ status_changed_at = row.pending_at
252
228
 
253
229
  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,
230
+ f"[bold]{row.run_id}[/bold]",
231
+ row.federation,
232
+ f"@{row.fab_id}=={row.fab_version}",
233
+ f"[{status_style}]{row.status_text}[/{status_style}]",
234
+ row.elapsed,
235
+ status_changed_at,
261
236
  )
262
237
  table.add_row(*formatted_row)
263
238
 
264
239
  return table
265
240
 
266
241
 
267
- def _to_json(run_list: list[_RunListType]) -> str:
268
- """Format run status list to a JSON formatted string."""
242
+ def _to_detail_table(run: RunRow) -> Table:
243
+ """Format a single run's details in a vertical table layout.
244
+
245
+ Parameters
246
+ ----------
247
+ run : RunRow
248
+ The run information to display.
249
+
250
+ Returns
251
+ -------
252
+ Table
253
+ Rich Table object with detailed run information in vertical format.
254
+ """
255
+ status_style = _get_status_style(run.status_text)
256
+
257
+ # Create vertical table with field names on the left
258
+ table = Table(show_header=False, show_lines=False)
259
+ table.add_column("Field", style="bold cyan", no_wrap=True)
260
+ table.add_column("Value")
261
+
262
+ # Add rows with all details
263
+ table.add_row("Run ID", f"[bold]{run.run_id}[/bold]")
264
+ table.add_row("Federation", run.federation)
265
+ table.add_row("App", f"@{run.fab_id}=={run.fab_version}")
266
+ table.add_row("FAB Hash", f"{run.fab_hash[:8]}...{run.fab_hash[-8:]}")
267
+ table.add_row("Status", f"[{status_style}]{run.status_text}[/{status_style}]")
268
+ table.add_row("Elapsed", f"[blue]{run.elapsed}[/blue]")
269
+ table.add_row("Pending At", run.pending_at)
270
+ table.add_row("Starting At", run.starting_at)
271
+ table.add_row("Running At", run.running_at)
272
+ table.add_row("Finished At", run.finished_at)
273
+
274
+ return table
275
+
276
+
277
+ def _to_json(run_list: list[RunRow]) -> str:
278
+ """Format run status list to a JSON formatted string.
279
+
280
+ Parameters
281
+ ----------
282
+ run_list : list[RunRow]
283
+ List of run information to serialize.
284
+
285
+ Returns
286
+ -------
287
+ str
288
+ JSON string containing formatted run information.
289
+ """
269
290
  runs_list = []
270
291
  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
292
  runs_list.append(
283
293
  {
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,
294
+ "run-id": row.run_id,
295
+ "federation": row.federation,
296
+ "fab-id": row.fab_id,
297
+ "fab-name": row.fab_id.split("/")[-1],
298
+ "fab-version": row.fab_version,
299
+ "fab-hash": row.fab_hash[:8],
300
+ "status": row.status_text,
301
+ "elapsed": row.elapsed,
302
+ "pending-at": row.pending_at,
303
+ "starting-at": row.starting_at,
304
+ "running-at": row.running_at,
305
+ "finished-at": row.finished_at,
294
306
  }
295
307
  )
296
308
 
297
309
  return json.dumps({"success": True, "runs": runs_list})
298
310
 
299
311
 
300
- def _list_runs(stub: ControlStub) -> list[_RunListType]:
301
- """List all runs."""
312
+ def _list_runs(stub: ControlStub) -> list[RunRow]:
313
+ """List all runs.
314
+
315
+ Parameters
316
+ ----------
317
+ stub : ControlStub
318
+ The gRPC stub for Control API communication.
319
+
320
+ Returns
321
+ -------
322
+ list[RunRow]
323
+ List of formatted run information for all runs.
324
+ """
302
325
  with flwr_cli_grpc_exc_handler():
303
326
  res: ListRunsResponse = stub.ListRuns(ListRunsRequest())
304
- run_dict = {run_id: run_from_proto(proto) for run_id, proto in res.run_dict.items()}
327
+ runs = [run_from_proto(proto) for proto in res.run_dict.values()]
328
+
329
+ return format_runs(runs, res.now)
330
+
305
331
 
306
- return _format_runs(run_dict, res.now)
332
+ def _display_one_run(stub: ControlStub, run_id: int) -> list[RunRow]:
333
+ """Display information about a specific run.
307
334
 
335
+ Parameters
336
+ ----------
337
+ stub : ControlStub
338
+ The gRPC stub for Control API communication.
339
+ run_id : int
340
+ The unique identifier of the run to display.
308
341
 
309
- def _display_one_run(stub: ControlStub, run_id: int) -> list[_RunListType]:
310
- """Display information about a specific run."""
342
+ Returns
343
+ -------
344
+ list[RunRow]
345
+ List containing the formatted run information (single item).
346
+
347
+ Raises
348
+ ------
349
+ ValueError
350
+ If the run_id is not found.
351
+ """
311
352
  with flwr_cli_grpc_exc_handler():
312
353
  res: ListRunsResponse = stub.ListRuns(ListRunsRequest(run_id=run_id))
313
354
  if not res.run_dict:
314
355
  # This won't be reached as an gRPC error is raised if run_id is invalid
315
356
  raise ValueError(f"Run ID {run_id} not found")
316
357
 
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)
358
+ runs = [run_from_proto(proto) for proto in res.run_dict.values()]
359
+ return format_runs(runs, res.now)