flwr 1.25.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 (140) 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 +18 -69
  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 +28 -58
  25. flwr/cli/new/new.py +9 -29
  26. flwr/cli/pull.py +19 -37
  27. flwr/cli/run/run.py +85 -93
  28. flwr/cli/run_utils.py +1 -1
  29. flwr/cli/stop.py +32 -73
  30. flwr/cli/supernode/ls.py +25 -57
  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 -275
  35. flwr/client/grpc_rere_client/connection.py +3 -3
  36. flwr/client/grpc_rere_client/grpc_adapter.py +1 -1
  37. flwr/client/message_handler/message_handler.py +2 -1
  38. flwr/client/mod/centraldp_mods.py +1 -1
  39. flwr/client/mod/localdp_mod.py +1 -1
  40. flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
  41. flwr/client/run_info_store.py +2 -1
  42. flwr/clientapp/client_app.py +2 -1
  43. flwr/common/__init__.py +3 -2
  44. flwr/common/args.py +5 -5
  45. flwr/common/config.py +12 -17
  46. flwr/common/constant.py +3 -16
  47. flwr/common/context.py +2 -1
  48. flwr/common/exit/exit.py +4 -4
  49. flwr/common/exit/exit_code.py +6 -0
  50. flwr/common/grpc.py +2 -1
  51. flwr/common/logger.py +1 -1
  52. flwr/common/message.py +1 -1
  53. flwr/common/retry_invoker.py +13 -5
  54. flwr/common/secure_aggregation/ndarrays_arithmetic.py +5 -2
  55. flwr/common/serde.py +7 -5
  56. flwr/common/telemetry.py +1 -1
  57. flwr/common/typing.py +4 -3
  58. flwr/compat/client/app.py +6 -9
  59. flwr/compat/client/grpc_client/connection.py +2 -1
  60. flwr/compat/common/constant.py +29 -0
  61. flwr/compat/server/app.py +1 -1
  62. flwr/proto/clientappio_pb2.py +2 -2
  63. flwr/proto/clientappio_pb2_grpc.py +104 -88
  64. flwr/proto/clientappio_pb2_grpc.pyi +140 -80
  65. flwr/proto/federation_pb2.py +5 -3
  66. flwr/proto/federation_pb2.pyi +32 -2
  67. flwr/proto/run_pb2.py +5 -13
  68. flwr/proto/run_pb2.pyi +0 -57
  69. flwr/proto/serverappio_pb2.py +2 -2
  70. flwr/proto/serverappio_pb2_grpc.py +138 -207
  71. flwr/proto/serverappio_pb2_grpc.pyi +189 -155
  72. flwr/proto/simulationio_pb2.py +2 -2
  73. flwr/proto/simulationio_pb2_grpc.py +62 -90
  74. flwr/proto/simulationio_pb2_grpc.pyi +95 -55
  75. flwr/server/app.py +6 -13
  76. flwr/server/compat/grid_client_proxy.py +2 -1
  77. flwr/server/grid/grpc_grid.py +5 -5
  78. flwr/server/serverapp/app.py +11 -4
  79. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -1
  80. flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +13 -12
  81. flwr/server/superlink/fleet/message_handler/message_handler.py +6 -5
  82. flwr/server/superlink/linkstate/__init__.py +2 -2
  83. flwr/server/superlink/linkstate/in_memory_linkstate.py +2 -10
  84. flwr/server/superlink/linkstate/linkstate.py +2 -21
  85. flwr/server/superlink/linkstate/linkstate_factory.py +16 -8
  86. flwr/server/superlink/linkstate/{sqlite_linkstate.py → sql_linkstate.py} +432 -534
  87. flwr/server/superlink/linkstate/utils.py +49 -2
  88. flwr/server/superlink/serverappio/serverappio_servicer.py +1 -33
  89. flwr/server/superlink/simulation/simulationio_servicer.py +0 -19
  90. flwr/server/utils/validator.py +1 -1
  91. flwr/server/workflow/default_workflows.py +2 -1
  92. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
  93. flwr/serverapp/strategy/bulyan.py +7 -1
  94. flwr/serverapp/strategy/dp_fixed_clipping.py +9 -1
  95. flwr/serverapp/strategy/fedavg.py +1 -1
  96. flwr/serverapp/strategy/fedxgb_cyclic.py +1 -1
  97. flwr/simulation/ray_transport/ray_client_proxy.py +2 -6
  98. flwr/simulation/run_simulation.py +3 -12
  99. flwr/simulation/simulationio_connection.py +3 -3
  100. flwr/{common → supercore}/address.py +7 -33
  101. flwr/supercore/app_utils.py +2 -1
  102. flwr/supercore/constant.py +24 -2
  103. flwr/supercore/corestate/{sqlite_corestate.py → sql_corestate.py} +19 -23
  104. flwr/supercore/credential_store/__init__.py +33 -0
  105. flwr/supercore/credential_store/credential_store.py +34 -0
  106. flwr/supercore/credential_store/file_credential_store.py +76 -0
  107. flwr/{common → supercore}/date.py +0 -11
  108. flwr/supercore/ffs/disk_ffs.py +1 -1
  109. flwr/supercore/object_store/object_store_factory.py +14 -6
  110. flwr/supercore/object_store/{sqlite_object_store.py → sql_object_store.py} +115 -117
  111. flwr/supercore/sql_mixin.py +315 -0
  112. flwr/supercore/state/__init__.py +15 -0
  113. flwr/supercore/state/alembic/__init__.py +15 -0
  114. flwr/supercore/state/alembic/env.py +103 -0
  115. flwr/supercore/state/alembic/script.py.mako +43 -0
  116. flwr/supercore/state/alembic/utils.py +239 -0
  117. flwr/supercore/state/alembic/versions/__init__.py +15 -0
  118. flwr/supercore/state/alembic/versions/rev_2026_01_28_initialize_migration_of_state_tables.py +200 -0
  119. flwr/supercore/state/schema/README.md +121 -0
  120. flwr/supercore/state/schema/__init__.py +15 -0
  121. flwr/supercore/state/schema/corestate_tables.py +36 -0
  122. flwr/supercore/state/schema/linkstate_tables.py +152 -0
  123. flwr/supercore/state/schema/objectstore_tables.py +90 -0
  124. flwr/supercore/superexec/run_superexec.py +2 -2
  125. flwr/supercore/utils.py +36 -1
  126. flwr/superlink/federation/federation_manager.py +2 -2
  127. flwr/superlink/federation/noop_federation_manager.py +8 -6
  128. flwr/superlink/servicer/control/control_servicer.py +19 -17
  129. flwr/supernode/cli/flower_supernode.py +2 -1
  130. flwr/supernode/runtime/run_clientapp.py +14 -14
  131. flwr/supernode/servicer/clientappio/clientappio_servicer.py +10 -8
  132. flwr/supernode/start_client_internal.py +10 -6
  133. {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/METADATA +7 -5
  134. {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/RECORD +137 -116
  135. flwr/cli/federation/show.py +0 -318
  136. flwr/common/pyproject.py +0 -42
  137. flwr/supercore/sqlite_mixin.py +0 -159
  138. /flwr/{common → supercore}/version.py +0 -0
  139. {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/WHEEL +0 -0
  140. {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/entry_points.txt +0 -0
@@ -15,26 +15,19 @@
15
15
  """Flower command line interface `supernode register` command."""
16
16
 
17
17
 
18
- import io
19
- import json
20
18
  from pathlib import Path
21
19
  from typing import Annotated
22
20
 
21
+ import click
23
22
  import typer
24
23
  from cryptography.exceptions import UnsupportedAlgorithm
25
24
  from cryptography.hazmat.primitives import serialization
26
25
  from cryptography.hazmat.primitives.asymmetric import ec
27
- from rich.console import Console
28
26
 
29
- from flwr.cli.config_utils import (
30
- exit_if_no_address,
31
- load_and_validate,
32
- process_loaded_project_config,
33
- validate_federation_in_project_config,
34
- )
35
- from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
27
+ from flwr.cli.config_migration import migrate
28
+ from flwr.cli.flower_config import read_superlink_connection
29
+ from flwr.common.constant import CliOutputFormat
36
30
  from flwr.common.exit import ExitCode, flwr_exit
37
- from flwr.common.logger import print_json_error, redirect_output, restore_output
38
31
  from flwr.proto.control_pb2 import ( # pylint: disable=E0611
39
32
  RegisterNodeRequest,
40
33
  RegisterNodeResponse,
@@ -42,23 +35,25 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
42
35
  from flwr.proto.control_pb2_grpc import ControlStub
43
36
  from flwr.supercore.primitives.asymmetric import public_key_to_bytes, uses_nist_ec_curve
44
37
 
45
- from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
38
+ from ..utils import (
39
+ cli_output_handler,
40
+ flwr_cli_grpc_exc_handler,
41
+ init_channel_from_connection,
42
+ print_json_to_stdout,
43
+ )
46
44
 
47
45
 
48
46
  def register( # pylint: disable=R0914
47
+ ctx: typer.Context,
49
48
  public_key: Annotated[
50
49
  Path,
51
50
  typer.Argument(
52
51
  help="Path to a P-384 (or any other NIST EC curve) public key file.",
53
52
  ),
54
53
  ],
55
- app: Annotated[
56
- Path,
57
- typer.Argument(help="Path of the Flower project"),
58
- ] = Path("."),
59
- federation: Annotated[
54
+ superlink: Annotated[
60
55
  str | None,
61
- typer.Argument(help="Name of the federation"),
56
+ typer.Argument(help="Name of the SuperLink connection."),
62
57
  ] = None,
63
58
  output_format: Annotated[
64
59
  str,
@@ -70,69 +65,33 @@ def register( # pylint: disable=R0914
70
65
  ] = CliOutputFormat.DEFAULT,
71
66
  ) -> None:
72
67
  """Add a SuperNode to the federation."""
73
- suppress_output = output_format == CliOutputFormat.JSON
74
- captured_output = io.StringIO()
75
-
76
68
  # Load public key
77
- public_key_path = Path(public_key)
78
- public_key_bytes = try_load_public_key(public_key_path)
69
+ public_key_bytes = try_load_public_key(public_key.expanduser())
79
70
 
80
- try:
81
- if suppress_output:
82
- redirect_output(captured_output)
83
-
84
- # Load and validate federation config
85
- typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
86
-
87
- pyproject_path = app / FAB_CONFIG_FILE if app else None
88
- config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
89
- config = process_loaded_project_config(config, errors, warnings)
90
- federation, federation_config = validate_federation_in_project_config(
91
- federation, config
92
- )
93
- exit_if_no_address(federation_config, "supernode register")
71
+ with cli_output_handler(output_format=output_format) as is_json:
72
+ # Migrate legacy usage if any
73
+ migrate(superlink, args=ctx.args)
94
74
 
75
+ # Read superlink connection configuration
76
+ superlink_connection = read_superlink_connection(superlink)
95
77
  channel = None
78
+
96
79
  try:
97
- auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
98
- channel = init_channel(app, federation_config, auth_plugin)
99
- stub = ControlStub(channel) # pylint: disable=unused-variable # noqa: F841
80
+ channel = init_channel_from_connection(superlink_connection)
81
+ stub = ControlStub(channel)
100
82
 
101
83
  _register_node(
102
- stub=stub, public_key=public_key_bytes, output_format=output_format
84
+ stub=stub,
85
+ public_key=public_key_bytes,
86
+ is_json=is_json,
103
87
  )
104
88
 
105
- except ValueError as err:
106
- typer.secho(
107
- f"❌ {err}",
108
- fg=typer.colors.RED,
109
- bold=True,
110
- err=True,
111
- )
112
- raise typer.Exit(code=1) from err
113
89
  finally:
114
90
  if channel:
115
91
  channel.close()
116
92
 
117
- except (typer.Exit, Exception) as err: # pylint: disable=broad-except
118
- if suppress_output:
119
- restore_output()
120
- e_message = captured_output.getvalue()
121
- print_json_error(e_message, err)
122
- else:
123
- typer.secho(
124
- f"{err}",
125
- fg=typer.colors.RED,
126
- bold=True,
127
- err=True,
128
- )
129
- finally:
130
- if suppress_output:
131
- restore_output()
132
- captured_output.close()
133
-
134
93
 
135
- def _register_node(stub: ControlStub, public_key: bytes, output_format: str) -> None:
94
+ def _register_node(stub: ControlStub, public_key: bytes, is_json: bool) -> None:
136
95
  """Register a node."""
137
96
  with flwr_cli_grpc_exc_handler():
138
97
  response: RegisterNodeResponse = stub.RegisterNode(
@@ -143,31 +102,23 @@ def _register_node(stub: ControlStub, public_key: bytes, output_format: str) ->
143
102
  f"✅ SuperNode {response.node_id} registered successfully.",
144
103
  fg=typer.colors.GREEN,
145
104
  )
146
- if output_format == CliOutputFormat.JSON:
147
- run_output = json.dumps(
105
+ if is_json:
106
+ print_json_to_stdout(
148
107
  {
149
108
  "success": True,
150
109
  "node-id": response.node_id,
151
110
  }
152
111
  )
153
- restore_output()
154
- Console().print_json(run_output)
155
112
  else:
156
- typer.secho(
157
- "❌ SuperNode couldn't be registered.", fg=typer.colors.RED, err=True
158
- )
113
+ raise click.ClickException("SuperNode couldn't be registered.")
159
114
 
160
115
 
161
116
  def try_load_public_key(public_key_path: Path) -> bytes:
162
117
  """Try to load a public key from a file."""
163
118
  if not public_key_path.exists():
164
- typer.secho(
165
- f"Public key file '{public_key_path}' does not exist.",
166
- fg=typer.colors.RED,
167
- bold=True,
168
- err=True,
119
+ raise click.ClickException(
120
+ f"Public key file '{public_key_path}' does not exist."
169
121
  )
170
- raise typer.Exit(code=1)
171
122
 
172
123
  with open(public_key_path, "rb") as key_file:
173
124
  try:
@@ -15,42 +15,35 @@
15
15
  """Flower command line interface `supernode unregister` command."""
16
16
 
17
17
 
18
- import io
19
- import json
20
- from pathlib import Path
21
18
  from typing import Annotated
22
19
 
23
20
  import typer
24
- from rich.console import Console
25
21
 
26
- from flwr.cli.config_utils import (
27
- exit_if_no_address,
28
- load_and_validate,
29
- process_loaded_project_config,
30
- validate_federation_in_project_config,
31
- )
32
- from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
33
- from flwr.common.logger import print_json_error, redirect_output, restore_output
22
+ from flwr.cli.config_migration import migrate
23
+ from flwr.cli.flower_config import read_superlink_connection
24
+ from flwr.common.constant import CliOutputFormat
34
25
  from flwr.proto.control_pb2 import UnregisterNodeRequest # pylint: disable=E0611
35
26
  from flwr.proto.control_pb2_grpc import ControlStub
36
27
 
37
- from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
28
+ from ..utils import (
29
+ cli_output_handler,
30
+ flwr_cli_grpc_exc_handler,
31
+ init_channel_from_connection,
32
+ print_json_to_stdout,
33
+ )
38
34
 
39
35
 
40
36
  def unregister( # pylint: disable=R0914
37
+ ctx: typer.Context,
41
38
  node_id: Annotated[
42
39
  int,
43
40
  typer.Argument(
44
41
  help="ID of the SuperNode to remove.",
45
42
  ),
46
43
  ],
47
- app: Annotated[
48
- Path,
49
- typer.Argument(help="Path of the Flower project"),
50
- ] = Path("."),
51
- federation: Annotated[
44
+ superlink: Annotated[
52
45
  str | None,
53
- typer.Argument(help="Name of the federation"),
46
+ typer.Argument(help="Name of the SuperLink connection."),
54
47
  ] = None,
55
48
  output_format: Annotated[
56
49
  str,
@@ -62,66 +55,29 @@ def unregister( # pylint: disable=R0914
62
55
  ] = CliOutputFormat.DEFAULT,
63
56
  ) -> None:
64
57
  """Unregister a SuperNode from the federation."""
65
- suppress_output = output_format == CliOutputFormat.JSON
66
- captured_output = io.StringIO()
67
-
68
- try:
69
- if suppress_output:
70
- redirect_output(captured_output)
71
-
72
- # Load and validate federation config
73
- typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
74
-
75
- pyproject_path = app / FAB_CONFIG_FILE if app else None
76
- config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
77
- config = process_loaded_project_config(config, errors, warnings)
78
- federation, federation_config = validate_federation_in_project_config(
79
- federation, config
80
- )
81
- exit_if_no_address(federation_config, "supernode unregister")
58
+ with cli_output_handler(output_format=output_format) as is_json:
59
+ # Migrate legacy usage if any
60
+ migrate(superlink, args=ctx.args)
82
61
 
62
+ # Read superlink connection configuration
63
+ superlink_connection = read_superlink_connection(superlink)
83
64
  channel = None
65
+
84
66
  try:
85
- auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
86
- channel = init_channel(app, federation_config, auth_plugin)
87
- stub = ControlStub(channel) # pylint: disable=unused-variable # noqa: F841
67
+ channel = init_channel_from_connection(superlink_connection)
68
+ stub = ControlStub(channel)
88
69
 
89
- _unregister_node(stub=stub, node_id=node_id, output_format=output_format)
70
+ _unregister_node(stub=stub, node_id=node_id, is_json=is_json)
90
71
 
91
- except ValueError as err:
92
- typer.secho(
93
- f"❌ {err}",
94
- fg=typer.colors.RED,
95
- bold=True,
96
- err=True,
97
- )
98
- raise typer.Exit(code=1) from err
99
72
  finally:
100
73
  if channel:
101
74
  channel.close()
102
75
 
103
- except (typer.Exit, Exception) as err: # pylint: disable=broad-except
104
- if suppress_output:
105
- restore_output()
106
- e_message = captured_output.getvalue()
107
- print_json_error(e_message, err)
108
- else:
109
- typer.secho(
110
- f"{err}",
111
- fg=typer.colors.RED,
112
- bold=True,
113
- err=True,
114
- )
115
- finally:
116
- if suppress_output:
117
- restore_output()
118
- captured_output.close()
119
-
120
76
 
121
77
  def _unregister_node(
122
78
  stub: ControlStub,
123
79
  node_id: int,
124
- output_format: str,
80
+ is_json: bool,
125
81
  ) -> None:
126
82
  """Unregister a SuperNode from the federation."""
127
83
  with flwr_cli_grpc_exc_handler():
@@ -129,12 +85,10 @@ def _unregister_node(
129
85
  typer.secho(
130
86
  f"✅ SuperNode {node_id} unregistered successfully.", fg=typer.colors.GREEN
131
87
  )
132
- if output_format == CliOutputFormat.JSON:
133
- run_output = json.dumps(
88
+ if is_json:
89
+ print_json_to_stdout(
134
90
  {
135
91
  "success": True,
136
92
  "node-id": node_id,
137
93
  }
138
94
  )
139
- restore_output()
140
- Console().print_json(run_output)
flwr/cli/typing.py ADDED
@@ -0,0 +1,200 @@
1
+ # Copyright 2026 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Flower command line interface type definitions."""
16
+
17
+
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+
21
+ from flwr.cli.constant import (
22
+ DEFAULT_SIMULATION_BACKEND_NAME,
23
+ SuperLinkConnectionTomlKey,
24
+ )
25
+ from flwr.supercore.utils import check_federation_format
26
+
27
+ _ERROR_MSG_FMT = "SuperLinkConnection.%s is None"
28
+
29
+
30
+ @dataclass
31
+ class SimulationClientResources:
32
+ """Resource configuration for the ClientApp."""
33
+
34
+ num_cpus: float | None = None
35
+ num_gpus: float | None = None
36
+
37
+ def __post_init__(self) -> None:
38
+ """Validate client resources configuration."""
39
+ if self.num_cpus is not None and not isinstance(self.num_cpus, (int, float)):
40
+ raise ValueError("client-resources.num-cpus must be a number (int/float).")
41
+ if self.num_gpus is not None and not isinstance(self.num_gpus, (int, float)):
42
+ raise ValueError("client-resources.num-gpus must be a number (int/float).")
43
+
44
+
45
+ @dataclass
46
+ class SimulationInitArgs:
47
+ """Initialization arguments for the simulation."""
48
+
49
+ num_cpus: int | None = None
50
+ num_gpus: int | None = None
51
+ logging_level: str | None = None
52
+ log_to_drive: bool | None = None
53
+
54
+ def __post_init__(self) -> None:
55
+ """Validate initialization arguments."""
56
+ if self.num_cpus is not None and not isinstance(self.num_cpus, int):
57
+ raise ValueError("init-args.num-cpus must be an integer.")
58
+ if self.num_gpus is not None and not isinstance(self.num_gpus, int):
59
+ raise ValueError("init-args.num-gpus must be an integer.")
60
+ if self.logging_level is not None and not isinstance(self.logging_level, str):
61
+ raise ValueError("init-args.logging-level must be a string.")
62
+ if self.log_to_drive is not None and not isinstance(self.log_to_drive, bool):
63
+ raise ValueError("init-args.log-to-drive must be a boolean.")
64
+
65
+
66
+ @dataclass
67
+ class SimulationBackendConfig:
68
+ """Backend configuration for the simulation."""
69
+
70
+ client_resources: SimulationClientResources | None = None
71
+ init_args: SimulationInitArgs | None = None
72
+ name: str = DEFAULT_SIMULATION_BACKEND_NAME
73
+
74
+ def __post_init__(self) -> None:
75
+ """Validate backend configuration."""
76
+ if not isinstance(self.name, str):
77
+ raise ValueError("backend.name must be a string.")
78
+
79
+
80
+ @dataclass
81
+ class SuperLinkSimulationOptions:
82
+ """Options for local simulation."""
83
+
84
+ num_supernodes: int
85
+ backend: SimulationBackendConfig | None = None
86
+ verbose: bool | None = None
87
+
88
+ def __post_init__(self) -> None:
89
+ """Validate simulation options."""
90
+ if not isinstance(self.num_supernodes, int):
91
+ raise ValueError(
92
+ "Invalid simulation options: num-supernodes must be an integer."
93
+ )
94
+ if self.verbose is not None and not isinstance(self.verbose, bool):
95
+ raise ValueError("Invalid simulation options: verbose must be a boolean.")
96
+
97
+
98
+ @dataclass
99
+ class SuperLinkConnection:
100
+ """SuperLink connection configuration for CLI commands.
101
+
102
+ Attributes
103
+ ----------
104
+ name : str
105
+ The name of the connection configuration.
106
+ address : str
107
+ The address of the SuperLink (Control API).
108
+ root_certificates : str
109
+ The absolute path to the root CA certificate file.
110
+ insecure : bool (default: False)
111
+ Whether to use an insecure channel. If True, the
112
+ connection will not use TLS encryption.
113
+ federation : str
114
+ The name of the federation to interface with.
115
+ options : SuperLinkSimulationOptions
116
+ Configuration options for the simulation runtime.
117
+ """
118
+
119
+ name: str
120
+ address: str | None = None
121
+ root_certificates: str | None = None
122
+ _insecure: bool | None = None
123
+ federation: str | None = None
124
+ options: SuperLinkSimulationOptions | None = None
125
+
126
+ # pylint: disable=too-many-arguments,too-many-positional-arguments
127
+ def __init__(
128
+ self,
129
+ name: str,
130
+ address: str | None = None,
131
+ root_certificates: str | None = None,
132
+ insecure: bool | None = None,
133
+ federation: str | None = None,
134
+ options: SuperLinkSimulationOptions | None = None,
135
+ ) -> None:
136
+ self.name = name
137
+ self.address = address
138
+ self.root_certificates = root_certificates
139
+ self._insecure = insecure
140
+ self.federation = federation
141
+ self.options = options
142
+
143
+ self.__post_init__()
144
+
145
+ @property
146
+ def insecure(self) -> bool:
147
+ """Return the insecure flag or its default (False) if unset."""
148
+ if self._insecure is None:
149
+ return False
150
+ return self._insecure
151
+
152
+ def __post_init__(self) -> None:
153
+ """Validate SuperLink connection configuration."""
154
+ err_prefix = f"Invalid value for key '%s' in connection '{self.name}': "
155
+ if self.address is not None and not isinstance(self.address, str):
156
+ raise ValueError(
157
+ err_prefix % SuperLinkConnectionTomlKey.ADDRESS
158
+ + f"expected str, but got {type(self.address).__name__}."
159
+ )
160
+ if self.root_certificates is not None and not isinstance(
161
+ self.root_certificates, str
162
+ ):
163
+ raise ValueError(
164
+ err_prefix % SuperLinkConnectionTomlKey.ROOT_CERTIFICATES
165
+ + f"expected str, but got {type(self.root_certificates).__name__}."
166
+ )
167
+
168
+ # Ensure root certificates path is absolute
169
+ if self.root_certificates is not None:
170
+ if not Path(self.root_certificates).is_absolute():
171
+ raise ValueError(
172
+ err_prefix % SuperLinkConnectionTomlKey.ROOT_CERTIFICATES
173
+ + "expected absolute path, but got relative path "
174
+ f"'{self.root_certificates}'."
175
+ )
176
+
177
+ if self._insecure is not None and not isinstance(self._insecure, bool):
178
+ raise ValueError(
179
+ err_prefix % SuperLinkConnectionTomlKey.INSECURE
180
+ + f"expected bool, but got {type(self._insecure).__name__}."
181
+ )
182
+
183
+ if self.federation is not None:
184
+ if not isinstance(self.federation, str):
185
+ raise ValueError(
186
+ err_prefix % SuperLinkConnectionTomlKey.FEDERATION
187
+ + f"expected str, but got {type(self.federation).__name__}."
188
+ )
189
+
190
+ # Check if the federation string is valid
191
+ check_federation_format(self.federation)
192
+
193
+ # The connection needs to have either an address or options (or both).
194
+ if self.address is None and self.options is None:
195
+ raise ValueError(
196
+ "Invalid SuperLink connection format: "
197
+ f"'{SuperLinkConnectionTomlKey.ADDRESS}' and/or "
198
+ f"'{SuperLinkConnectionTomlKey.OPTIONS}' key "
199
+ "need to be specified."
200
+ )