flwr 1.24.0__py3-none-any.whl → 1.26.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 (204) hide show
  1. flwr/__init__.py +1 -1
  2. flwr/app/__init__.py +4 -1
  3. flwr/app/message_type.py +29 -0
  4. flwr/app/metadata.py +5 -2
  5. flwr/app/user_config.py +19 -0
  6. flwr/cli/app.py +37 -19
  7. flwr/cli/app_cmd/publish.py +25 -75
  8. flwr/cli/app_cmd/review.py +25 -66
  9. flwr/cli/auth_plugin/auth_plugin.py +5 -10
  10. flwr/cli/auth_plugin/noop_auth_plugin.py +1 -2
  11. flwr/cli/auth_plugin/oidc_cli_plugin.py +38 -38
  12. flwr/cli/build.py +15 -28
  13. flwr/cli/config/__init__.py +21 -0
  14. flwr/cli/config/ls.py +71 -0
  15. flwr/cli/config_migration.py +297 -0
  16. flwr/cli/config_utils.py +63 -156
  17. flwr/cli/constant.py +71 -0
  18. flwr/cli/federation/__init__.py +0 -2
  19. flwr/cli/federation/ls.py +256 -64
  20. flwr/cli/flower_config.py +429 -0
  21. flwr/cli/install.py +23 -62
  22. flwr/cli/log.py +23 -37
  23. flwr/cli/login/login.py +29 -63
  24. flwr/cli/ls.py +72 -61
  25. flwr/cli/new/new.py +98 -309
  26. flwr/cli/pull.py +19 -37
  27. flwr/cli/run/run.py +87 -100
  28. flwr/cli/run_utils.py +23 -5
  29. flwr/cli/stop.py +33 -74
  30. flwr/cli/supernode/ls.py +35 -62
  31. flwr/cli/supernode/register.py +31 -80
  32. flwr/cli/supernode/unregister.py +24 -70
  33. flwr/cli/typing.py +200 -0
  34. flwr/cli/utils.py +160 -412
  35. flwr/client/grpc_adapter_client/connection.py +2 -2
  36. flwr/client/grpc_rere_client/connection.py +9 -6
  37. flwr/client/grpc_rere_client/grpc_adapter.py +1 -1
  38. flwr/client/message_handler/message_handler.py +2 -1
  39. flwr/client/mod/centraldp_mods.py +1 -1
  40. flwr/client/mod/localdp_mod.py +1 -1
  41. flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
  42. flwr/client/rest_client/connection.py +6 -4
  43. flwr/client/run_info_store.py +2 -1
  44. flwr/clientapp/client_app.py +2 -1
  45. flwr/common/__init__.py +3 -2
  46. flwr/common/args.py +5 -5
  47. flwr/common/config.py +12 -17
  48. flwr/common/constant.py +3 -16
  49. flwr/common/context.py +2 -1
  50. flwr/common/exit/exit.py +4 -4
  51. flwr/common/exit/exit_code.py +6 -0
  52. flwr/common/grpc.py +2 -1
  53. flwr/common/logger.py +1 -1
  54. flwr/common/message.py +1 -1
  55. flwr/common/retry_invoker.py +13 -5
  56. flwr/common/secure_aggregation/ndarrays_arithmetic.py +5 -2
  57. flwr/common/serde.py +13 -5
  58. flwr/common/telemetry.py +1 -1
  59. flwr/common/typing.py +10 -3
  60. flwr/compat/client/app.py +6 -9
  61. flwr/compat/client/grpc_client/connection.py +2 -1
  62. flwr/compat/common/constant.py +29 -0
  63. flwr/compat/server/app.py +1 -1
  64. flwr/proto/clientappio_pb2.py +2 -2
  65. flwr/proto/clientappio_pb2_grpc.py +104 -88
  66. flwr/proto/clientappio_pb2_grpc.pyi +140 -80
  67. flwr/proto/federation_pb2.py +5 -3
  68. flwr/proto/federation_pb2.pyi +32 -2
  69. flwr/proto/fleet_pb2.py +10 -10
  70. flwr/proto/fleet_pb2.pyi +5 -1
  71. flwr/proto/run_pb2.py +18 -26
  72. flwr/proto/run_pb2.pyi +10 -58
  73. flwr/proto/serverappio_pb2.py +2 -2
  74. flwr/proto/serverappio_pb2_grpc.py +138 -207
  75. flwr/proto/serverappio_pb2_grpc.pyi +189 -155
  76. flwr/proto/simulationio_pb2.py +2 -2
  77. flwr/proto/simulationio_pb2_grpc.py +62 -90
  78. flwr/proto/simulationio_pb2_grpc.pyi +95 -55
  79. flwr/server/app.py +7 -13
  80. flwr/server/compat/grid_client_proxy.py +2 -1
  81. flwr/server/grid/grpc_grid.py +5 -5
  82. flwr/server/serverapp/app.py +11 -4
  83. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -1
  84. flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +13 -12
  85. flwr/server/superlink/fleet/message_handler/message_handler.py +42 -2
  86. flwr/server/superlink/linkstate/__init__.py +2 -2
  87. flwr/server/superlink/linkstate/in_memory_linkstate.py +36 -10
  88. flwr/server/superlink/linkstate/linkstate.py +34 -21
  89. flwr/server/superlink/linkstate/linkstate_factory.py +16 -8
  90. flwr/server/superlink/linkstate/{sqlite_linkstate.py → sql_linkstate.py} +471 -516
  91. flwr/server/superlink/linkstate/utils.py +49 -2
  92. flwr/server/superlink/serverappio/serverappio_servicer.py +1 -33
  93. flwr/server/superlink/simulation/simulationio_servicer.py +0 -19
  94. flwr/server/utils/validator.py +1 -1
  95. flwr/server/workflow/default_workflows.py +2 -1
  96. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
  97. flwr/serverapp/strategy/bulyan.py +7 -1
  98. flwr/serverapp/strategy/dp_fixed_clipping.py +9 -1
  99. flwr/serverapp/strategy/fedavg.py +1 -1
  100. flwr/serverapp/strategy/fedxgb_cyclic.py +1 -1
  101. flwr/simulation/ray_transport/ray_client_proxy.py +2 -6
  102. flwr/simulation/run_simulation.py +3 -12
  103. flwr/simulation/simulationio_connection.py +3 -3
  104. flwr/{common → supercore}/address.py +7 -33
  105. flwr/supercore/app_utils.py +2 -1
  106. flwr/supercore/constant.py +27 -2
  107. flwr/supercore/corestate/{sqlite_corestate.py → sql_corestate.py} +19 -23
  108. flwr/supercore/credential_store/__init__.py +33 -0
  109. flwr/supercore/credential_store/credential_store.py +34 -0
  110. flwr/supercore/credential_store/file_credential_store.py +76 -0
  111. flwr/{common → supercore}/date.py +0 -11
  112. flwr/supercore/ffs/disk_ffs.py +1 -1
  113. flwr/supercore/object_store/object_store_factory.py +14 -6
  114. flwr/supercore/object_store/{sqlite_object_store.py → sql_object_store.py} +115 -117
  115. flwr/supercore/sql_mixin.py +315 -0
  116. flwr/{cli/new/templates → supercore/state}/__init__.py +2 -2
  117. flwr/{cli/new/templates/app/code/flwr_tune → supercore/state/alembic}/__init__.py +2 -2
  118. flwr/supercore/state/alembic/env.py +103 -0
  119. flwr/supercore/state/alembic/script.py.mako +43 -0
  120. flwr/supercore/state/alembic/utils.py +239 -0
  121. flwr/{cli/new/templates/app → supercore/state/alembic/versions}/__init__.py +2 -2
  122. flwr/supercore/state/alembic/versions/rev_2026_01_28_initialize_migration_of_state_tables.py +200 -0
  123. flwr/supercore/state/schema/README.md +121 -0
  124. flwr/{cli/new/templates/app/code → supercore/state/schema}/__init__.py +2 -2
  125. flwr/supercore/state/schema/corestate_tables.py +36 -0
  126. flwr/supercore/state/schema/linkstate_tables.py +152 -0
  127. flwr/supercore/state/schema/objectstore_tables.py +90 -0
  128. flwr/supercore/superexec/run_superexec.py +2 -2
  129. flwr/supercore/utils.py +225 -0
  130. flwr/superlink/federation/federation_manager.py +2 -2
  131. flwr/superlink/federation/noop_federation_manager.py +8 -6
  132. flwr/superlink/servicer/control/control_grpc.py +2 -0
  133. flwr/superlink/servicer/control/control_servicer.py +106 -21
  134. flwr/supernode/cli/flower_supernode.py +2 -1
  135. flwr/supernode/nodestate/in_memory_nodestate.py +62 -1
  136. flwr/supernode/nodestate/nodestate.py +45 -0
  137. flwr/supernode/runtime/run_clientapp.py +14 -14
  138. flwr/supernode/servicer/clientappio/clientappio_servicer.py +13 -5
  139. flwr/supernode/start_client_internal.py +17 -10
  140. {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/METADATA +8 -8
  141. {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/RECORD +144 -184
  142. flwr/cli/federation/show.py +0 -317
  143. flwr/cli/new/templates/app/.gitignore.tpl +0 -163
  144. flwr/cli/new/templates/app/LICENSE.tpl +0 -202
  145. flwr/cli/new/templates/app/README.baseline.md.tpl +0 -127
  146. flwr/cli/new/templates/app/README.flowertune.md.tpl +0 -68
  147. flwr/cli/new/templates/app/README.md.tpl +0 -37
  148. flwr/cli/new/templates/app/code/__init__.baseline.py.tpl +0 -1
  149. flwr/cli/new/templates/app/code/__init__.py.tpl +0 -1
  150. flwr/cli/new/templates/app/code/__init__.pytorch_legacy_api.py.tpl +0 -1
  151. flwr/cli/new/templates/app/code/client.baseline.py.tpl +0 -75
  152. flwr/cli/new/templates/app/code/client.huggingface.py.tpl +0 -93
  153. flwr/cli/new/templates/app/code/client.jax.py.tpl +0 -71
  154. flwr/cli/new/templates/app/code/client.mlx.py.tpl +0 -102
  155. flwr/cli/new/templates/app/code/client.numpy.py.tpl +0 -46
  156. flwr/cli/new/templates/app/code/client.pytorch.py.tpl +0 -80
  157. flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +0 -55
  158. flwr/cli/new/templates/app/code/client.sklearn.py.tpl +0 -108
  159. flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +0 -82
  160. flwr/cli/new/templates/app/code/client.xgboost.py.tpl +0 -110
  161. flwr/cli/new/templates/app/code/dataset.baseline.py.tpl +0 -36
  162. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +0 -92
  163. flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +0 -87
  164. flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +0 -56
  165. flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +0 -73
  166. flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +0 -78
  167. flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -66
  168. flwr/cli/new/templates/app/code/server.baseline.py.tpl +0 -43
  169. flwr/cli/new/templates/app/code/server.huggingface.py.tpl +0 -42
  170. flwr/cli/new/templates/app/code/server.jax.py.tpl +0 -39
  171. flwr/cli/new/templates/app/code/server.mlx.py.tpl +0 -41
  172. flwr/cli/new/templates/app/code/server.numpy.py.tpl +0 -38
  173. flwr/cli/new/templates/app/code/server.pytorch.py.tpl +0 -41
  174. flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +0 -31
  175. flwr/cli/new/templates/app/code/server.sklearn.py.tpl +0 -44
  176. flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +0 -38
  177. flwr/cli/new/templates/app/code/server.xgboost.py.tpl +0 -56
  178. flwr/cli/new/templates/app/code/strategy.baseline.py.tpl +0 -1
  179. flwr/cli/new/templates/app/code/task.huggingface.py.tpl +0 -98
  180. flwr/cli/new/templates/app/code/task.jax.py.tpl +0 -57
  181. flwr/cli/new/templates/app/code/task.mlx.py.tpl +0 -102
  182. flwr/cli/new/templates/app/code/task.numpy.py.tpl +0 -7
  183. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +0 -99
  184. flwr/cli/new/templates/app/code/task.pytorch_legacy_api.py.tpl +0 -111
  185. flwr/cli/new/templates/app/code/task.sklearn.py.tpl +0 -67
  186. flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +0 -52
  187. flwr/cli/new/templates/app/code/task.xgboost.py.tpl +0 -67
  188. flwr/cli/new/templates/app/code/utils.baseline.py.tpl +0 -1
  189. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +0 -146
  190. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +0 -80
  191. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +0 -65
  192. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +0 -52
  193. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +0 -56
  194. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +0 -49
  195. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +0 -53
  196. flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +0 -53
  197. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +0 -52
  198. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +0 -53
  199. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +0 -61
  200. flwr/common/pyproject.py +0 -42
  201. flwr/supercore/sqlite_mixin.py +0 -159
  202. /flwr/{common → supercore}/version.py +0 -0
  203. {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/WHEEL +0 -0
  204. {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/entry_points.txt +0 -0
flwr/cli/login/login.py CHANGED
@@ -15,20 +15,14 @@
15
15
  """Flower command line interface `login` command."""
16
16
 
17
17
 
18
- from pathlib import Path
19
- from typing import Annotated
18
+ from typing import Annotated, cast
20
19
 
20
+ import click
21
21
  import typer
22
22
 
23
23
  from flwr.cli.auth_plugin import LoginError, NoOpCliAuthPlugin
24
- from flwr.cli.config_utils import (
25
- exit_if_no_address,
26
- get_insecure_flag,
27
- load_and_validate,
28
- process_loaded_project_config,
29
- validate_federation_in_project_config,
30
- )
31
24
  from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
25
+ from flwr.cli.utils import init_channel_from_connection
32
26
  from flwr.common.typing import AccountAuthLoginDetails
33
27
  from flwr.proto.control_pb2 import ( # pylint: disable=E0611
34
28
  GetLoginDetailsRequest,
@@ -36,68 +30,45 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
36
30
  )
37
31
  from flwr.proto.control_pb2_grpc import ControlStub
38
32
 
39
- from ..utils import (
40
- account_auth_enabled,
41
- flwr_cli_grpc_exc_handler,
42
- init_channel,
43
- load_cli_auth_plugin,
44
- )
33
+ from ..config_migration import migrate, warn_if_federation_config_overrides
34
+ from ..flower_config import read_superlink_connection
35
+ from ..utils import flwr_cli_grpc_exc_handler, load_cli_auth_plugin_from_connection
45
36
 
46
37
 
47
- def login( # pylint: disable=R0914
48
- app: Annotated[
49
- Path,
50
- typer.Argument(help="Path of the Flower App to run."),
51
- ] = Path("."),
52
- federation: Annotated[
38
+ def login(
39
+ ctx: typer.Context,
40
+ superlink: Annotated[
53
41
  str | None,
54
- typer.Argument(help="Name of the federation to login into."),
42
+ typer.Argument(help="Name of the SuperLink connection."),
55
43
  ] = None,
56
44
  federation_config_overrides: Annotated[
57
45
  list[str] | None,
58
46
  typer.Option(
59
47
  "--federation-config",
60
48
  help=FEDERATION_CONFIG_HELP_MESSAGE,
49
+ hidden=True,
61
50
  ),
62
51
  ] = None,
63
52
  ) -> None:
64
53
  """Login to Flower SuperLink."""
65
- typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
54
+ # Warn `--federation-config` is ignored
55
+ warn_if_federation_config_overrides(federation_config_overrides)
66
56
 
67
- pyproject_path = app / "pyproject.toml" if app else None
68
- config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
57
+ # Migrate legacy usage if any
58
+ migrate(superlink, args=ctx.args)
69
59
 
70
- config = process_loaded_project_config(config, errors, warnings)
71
- federation, federation_config = validate_federation_in_project_config(
72
- federation, config, federation_config_overrides
73
- )
74
- exit_if_no_address(federation_config, "login")
60
+ # Read superlink connection configuration
61
+ superlink_connection = read_superlink_connection(superlink)
62
+ superlink = superlink_connection.name
75
63
 
76
- # Check if `enable-account-auth` is set to `true`
77
-
78
- if not account_auth_enabled(federation_config):
79
- typer.secho(
80
- "❌ Account authentication is not enabled for the federation "
81
- f"'{federation}'. To enable it, set `enable-account-auth = true` "
82
- "in the federation configuration.",
83
- fg=typer.colors.RED,
84
- bold=True,
85
- err=True,
86
- )
87
- raise typer.Exit(code=1)
88
64
  # Check if insecure flag is set to `True`
89
- insecure = get_insecure_flag(federation_config)
90
- if insecure:
91
- typer.secho(
92
- "`flwr login` requires TLS to be enabled. `insecure` must NOT be set to "
93
- "`true` in the federation configuration.",
94
- fg=typer.colors.RED,
95
- bold=True,
96
- err=True,
65
+ if superlink_connection.insecure:
66
+ raise click.ClickException(
67
+ "`flwr login` requires TLS to be enabled. `insecure` must NOT be set to "
68
+ "`true` in the federation configuration."
97
69
  )
98
- raise typer.Exit(code=1)
99
70
 
100
- channel = init_channel(app, federation_config, NoOpCliAuthPlugin(Path()))
71
+ channel = init_channel_from_connection(superlink_connection, NoOpCliAuthPlugin())
101
72
  stub = ControlStub(channel)
102
73
 
103
74
  login_request = GetLoginDetailsRequest()
@@ -105,8 +76,9 @@ def login( # pylint: disable=R0914
105
76
  login_response: GetLoginDetailsResponse = stub.GetLoginDetails(login_request)
106
77
 
107
78
  # Get the auth plugin
108
- authn_type = login_response.authn_type
109
- auth_plugin = load_cli_auth_plugin(app, federation, federation_config, authn_type)
79
+ authn_plugin = load_cli_auth_plugin_from_connection(
80
+ cast(str, superlink_connection.address), login_response.authn_type
81
+ )
110
82
 
111
83
  # Login
112
84
  details = AccountAuthLoginDetails(
@@ -118,20 +90,14 @@ def login( # pylint: disable=R0914
118
90
  )
119
91
  try:
120
92
  with flwr_cli_grpc_exc_handler():
121
- credentials = auth_plugin.login(details, stub)
93
+ credentials = authn_plugin.login(details, stub)
122
94
  typer.secho(
123
95
  "✅ Login successful.",
124
96
  fg=typer.colors.GREEN,
125
97
  bold=False,
126
98
  )
127
99
  except LoginError as e:
128
- typer.secho(
129
- f"❌ Login failed: {e.message}",
130
- fg=typer.colors.RED,
131
- bold=True,
132
- err=True,
133
- )
134
- raise typer.Exit(code=1) from None
100
+ raise click.ClickException(f"Login failed: {e.message}") from None
135
101
 
136
102
  # Store the tokens
137
- auth_plugin.store_tokens(credentials)
103
+ authn_plugin.store_tokens(credentials)
flwr/cli/ls.py CHANGED
@@ -15,51 +15,47 @@
15
15
  """Flower command line interface `ls` command."""
16
16
 
17
17
 
18
- import io
19
18
  import json
20
- from pathlib import Path
21
- from typing import Annotated, cast
19
+ from typing import Annotated
22
20
 
23
21
  import typer
24
22
  from rich.console import Console
25
23
  from rich.table import Table
26
24
  from rich.text import Text
27
25
 
28
- from flwr.cli.config_utils import (
29
- exit_if_no_address,
30
- load_and_validate,
31
- process_loaded_project_config,
32
- validate_federation_in_project_config,
33
- )
26
+ from flwr.cli.config_migration import migrate, warn_if_federation_config_overrides
34
27
  from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
35
- from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat, Status, SubStatus
36
- from flwr.common.logger import print_json_error, redirect_output, restore_output
28
+ from flwr.cli.flower_config import read_superlink_connection
29
+ from flwr.common.constant import CliOutputFormat, Status, SubStatus
37
30
  from flwr.common.serde import run_from_proto
38
31
  from flwr.proto.control_pb2 import ( # pylint: disable=E0611
39
32
  ListRunsRequest,
40
33
  ListRunsResponse,
41
34
  )
42
35
  from flwr.proto.control_pb2_grpc import ControlStub
36
+ from flwr.supercore.utils import humanize_bytes, humanize_duration
43
37
 
44
38
  from .run_utils import RunRow, format_runs
45
- from .utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
39
+ from .utils import (
40
+ cli_output_handler,
41
+ flwr_cli_grpc_exc_handler,
42
+ init_channel_from_connection,
43
+ print_json_to_stdout,
44
+ )
46
45
 
47
46
 
48
47
  def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
49
48
  ctx: typer.Context,
50
- app: Annotated[
51
- Path,
52
- typer.Argument(help="Path of the Flower project"),
53
- ] = Path("."),
54
- federation: Annotated[
49
+ superlink: Annotated[
55
50
  str | None,
56
- typer.Argument(help="Name of the federation"),
51
+ typer.Argument(help="Name of the SuperLink connection."),
57
52
  ] = None,
58
53
  federation_config_overrides: Annotated[
59
54
  list[str] | None,
60
55
  typer.Option(
61
56
  "--federation-config",
62
57
  help=FEDERATION_CONFIG_HELP_MESSAGE,
58
+ hidden=True,
63
59
  ),
64
60
  ] = None,
65
61
  runs: Annotated[
@@ -85,7 +81,7 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
85
81
  ),
86
82
  ] = CliOutputFormat.DEFAULT,
87
83
  ) -> None:
88
- """List the details of one provided run ID or all runs in a Flower federation.
84
+ """List the details of one provided run ID or all runs (alias: ls).
89
85
 
90
86
  The following details are displayed:
91
87
 
@@ -98,32 +94,23 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
98
94
 
99
95
  All timestamps follow ISO 8601, UTC and are formatted as ``YYYY-MM-DD HH:MM:SSZ``.
100
96
  """
101
- # Resolve command used (list or ls)
102
- command_name = cast(str, ctx.command.name) if ctx.command else "list"
103
-
104
- suppress_output = output_format == CliOutputFormat.JSON
105
- captured_output = io.StringIO()
106
- try:
107
- if suppress_output:
108
- redirect_output(captured_output)
109
- # Load and validate federation config
110
- typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
111
-
112
- pyproject_path = app / FAB_CONFIG_FILE if app else None
113
- config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
114
- config = process_loaded_project_config(config, errors, warnings)
115
- federation, federation_config = validate_federation_in_project_config(
116
- federation, config, federation_config_overrides
117
- )
118
- exit_if_no_address(federation_config, command_name)
97
+ with cli_output_handler(output_format=output_format) as is_json:
98
+ # Warn `--federation-config` is ignored
99
+ warn_if_federation_config_overrides(federation_config_overrides)
100
+
101
+ # Migrate legacy usage if any
102
+ migrate(superlink, args=ctx.args)
103
+
104
+ # Read superlink connection configuration
105
+ superlink_connection = read_superlink_connection(superlink)
119
106
  channel = None
107
+
120
108
  try:
121
109
  if runs and run_id is not None:
122
110
  raise ValueError(
123
111
  "The options '--runs' and '--run-id' are mutually exclusive."
124
112
  )
125
- auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
126
- channel = init_channel(app, federation_config, auth_plugin)
113
+ channel = init_channel_from_connection(superlink_connection)
127
114
  stub = ControlStub(channel)
128
115
 
129
116
  # Display information about a specific run ID
@@ -134,9 +121,9 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
134
121
  else:
135
122
  typer.echo("📄 Listing all runs...")
136
123
  formatted_runs = _list_runs(stub)
137
- restore_output()
138
- if output_format == CliOutputFormat.JSON:
139
- Console().print_json(_to_json(formatted_runs))
124
+
125
+ if is_json:
126
+ print_json_to_stdout(_to_json(formatted_runs))
140
127
  else:
141
128
  if run_id is not None:
142
129
  Console().print(_to_detail_table(formatted_runs[0]))
@@ -145,22 +132,6 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
145
132
  finally:
146
133
  if channel:
147
134
  channel.close()
148
- except (typer.Exit, Exception) as err: # pylint: disable=broad-except
149
- if suppress_output:
150
- restore_output()
151
- e_message = captured_output.getvalue()
152
- print_json_error(e_message, err)
153
- else:
154
- typer.secho(
155
- f"{err}",
156
- fg=typer.colors.RED,
157
- bold=True,
158
- err=True,
159
- )
160
- finally:
161
- if suppress_output:
162
- restore_output()
163
- captured_output.close()
164
135
 
165
136
 
166
137
  def _get_status_style(status_text: str) -> str:
@@ -231,7 +202,7 @@ def _to_table(run_list: list[RunRow]) -> Table:
231
202
  row.federation,
232
203
  f"@{row.fab_id}=={row.fab_version}",
233
204
  f"[{status_style}]{row.status_text}[/{status_style}]",
234
- row.elapsed,
205
+ humanize_duration(row.elapsed),
235
206
  status_changed_at,
236
207
  )
237
208
  table.add_row(*formatted_row)
@@ -265,11 +236,39 @@ def _to_detail_table(run: RunRow) -> Table:
265
236
  table.add_row("App", f"@{run.fab_id}=={run.fab_version}")
266
237
  table.add_row("FAB Hash", f"{run.fab_hash[:8]}...{run.fab_hash[-8:]}")
267
238
  table.add_row("Status", f"[{status_style}]{run.status_text}[/{status_style}]")
268
- table.add_row("Elapsed", f"[blue]{run.elapsed}[/blue]")
239
+ table.add_row("Elapsed", f"[blue]{humanize_duration(run.elapsed)}[/blue]")
269
240
  table.add_row("Pending At", run.pending_at)
270
241
  table.add_row("Starting At", run.starting_at)
271
242
  table.add_row("Running At", run.running_at)
272
243
  table.add_row("Finished At", run.finished_at)
244
+ table.add_row(
245
+ "Network traffic (inbound)",
246
+ f"[blue]{humanize_bytes(run.network_traffic_inbound)}[/blue]",
247
+ )
248
+ table.add_row(
249
+ "Network traffic (outbound)",
250
+ f"[blue]{humanize_bytes(run.network_traffic_outbound)}[/blue]",
251
+ )
252
+ table.add_row(
253
+ "Network Traffic (total)",
254
+ "[blue]"
255
+ f"{humanize_bytes(run.network_traffic_inbound + run.network_traffic_outbound)}"
256
+ "[/blue]",
257
+ )
258
+ table.add_row(
259
+ "Compute Time (ServerApp)",
260
+ f"[blue]{humanize_duration(run.compute_time_serverapp)}[/blue]",
261
+ )
262
+ table.add_row(
263
+ "Compute Time (ClientApp)",
264
+ f"[blue]{humanize_duration(run.compute_time_clientapp)}[/blue]",
265
+ )
266
+ table.add_row(
267
+ "Compute Time (total)",
268
+ "[blue]"
269
+ f"{humanize_duration(run.compute_time_serverapp + run.compute_time_clientapp)}"
270
+ "[/blue]",
271
+ )
273
272
 
274
273
  return table
275
274
 
@@ -291,7 +290,7 @@ def _to_json(run_list: list[RunRow]) -> str:
291
290
  for row in run_list:
292
291
  runs_list.append(
293
292
  {
294
- "run-id": row.run_id,
293
+ "run-id": f"{row.run_id}",
295
294
  "federation": row.federation,
296
295
  "fab-id": row.fab_id,
297
296
  "fab-name": row.fab_id.split("/")[-1],
@@ -303,6 +302,18 @@ def _to_json(run_list: list[RunRow]) -> str:
303
302
  "starting-at": row.starting_at,
304
303
  "running-at": row.running_at,
305
304
  "finished-at": row.finished_at,
305
+ "network-traffic": {
306
+ "inbound-bytes": row.network_traffic_inbound,
307
+ "outbound-bytes": row.network_traffic_outbound,
308
+ "total-bytes": row.network_traffic_inbound
309
+ + row.network_traffic_outbound,
310
+ },
311
+ "compute-time": {
312
+ "serverapp-seconds": row.compute_time_serverapp,
313
+ "clientapp-seconds": row.compute_time_clientapp,
314
+ "total-seconds": row.compute_time_serverapp
315
+ + row.compute_time_clientapp,
316
+ },
306
317
  }
307
318
  )
308
319