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
@@ -0,0 +1,429 @@
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 configuration utils."""
16
+
17
+
18
+ import re
19
+ from pathlib import Path
20
+ from typing import Any, cast
21
+
22
+ import click
23
+ import tomli
24
+ import tomli_w
25
+ import typer
26
+
27
+ from flwr.cli.constant import (
28
+ DEFAULT_FLOWER_CONFIG_TOML,
29
+ DEFAULT_SIMULATION_BACKEND_NAME,
30
+ FLOWER_CONFIG_FILE,
31
+ SimulationBackendConfigTomlKey,
32
+ SimulationClientResourcesTomlKey,
33
+ SimulationInitArgsTomlKey,
34
+ SuperLinkConnectionTomlKey,
35
+ SuperLinkSimulationOptionsTomlKey,
36
+ )
37
+ from flwr.cli.typing import (
38
+ SimulationBackendConfig,
39
+ SimulationClientResources,
40
+ SimulationInitArgs,
41
+ SuperLinkConnection,
42
+ SuperLinkSimulationOptions,
43
+ )
44
+ from flwr.common.config import flatten_dict
45
+ from flwr.supercore.utils import get_flwr_home
46
+
47
+
48
+ def _parse_simulation_options(options: dict[str, Any]) -> SuperLinkSimulationOptions:
49
+ """Parse simulation options from a dictionary in a SuperLink connection."""
50
+ num_supernodes = options.get(SuperLinkSimulationOptionsTomlKey.NUM_SUPERNODES)
51
+ verbose = options.get(SuperLinkSimulationOptionsTomlKey.VERBOSE)
52
+ # Validation handled in SuperLinkSimulationOptions.__post_init__
53
+
54
+ backend_dict = options.get(SuperLinkSimulationOptionsTomlKey.BACKEND)
55
+ simulation_backend: SimulationBackendConfig | None = None
56
+
57
+ if isinstance(backend_dict, dict):
58
+ # Parse client resources
59
+ client_resources_dict = backend_dict.get(
60
+ SimulationBackendConfigTomlKey.CLIENT_RESOURCES
61
+ )
62
+ client_resources: SimulationClientResources | None = None
63
+ if isinstance(client_resources_dict, dict):
64
+ client_resources = SimulationClientResources(
65
+ num_cpus=client_resources_dict.get(
66
+ SimulationClientResourcesTomlKey.NUM_CPUS
67
+ ),
68
+ num_gpus=client_resources_dict.get(
69
+ SimulationClientResourcesTomlKey.NUM_GPUS
70
+ ),
71
+ )
72
+
73
+ # Parse init args
74
+ init_args_dict = backend_dict.get(SimulationBackendConfigTomlKey.INIT_ARGS)
75
+ init_args: SimulationInitArgs | None = None
76
+ if isinstance(init_args_dict, dict):
77
+ init_args = SimulationInitArgs(
78
+ num_cpus=init_args_dict.get(SimulationInitArgsTomlKey.NUM_CPUS),
79
+ num_gpus=init_args_dict.get(SimulationInitArgsTomlKey.NUM_GPUS),
80
+ logging_level=init_args_dict.get(
81
+ SimulationInitArgsTomlKey.LOGGING_LEVEL
82
+ ),
83
+ log_to_drive=init_args_dict.get(SimulationInitArgsTomlKey.LOG_TO_DRIVE),
84
+ )
85
+
86
+ simulation_backend = SimulationBackendConfig(
87
+ name=backend_dict.get(
88
+ SimulationBackendConfigTomlKey.NAME, DEFAULT_SIMULATION_BACKEND_NAME
89
+ ),
90
+ client_resources=client_resources,
91
+ init_args=init_args,
92
+ )
93
+
94
+ # Note: validation happens in SuperLinkSimulationOptions.__post_init__
95
+ return SuperLinkSimulationOptions(
96
+ num_supernodes=cast(int, num_supernodes),
97
+ backend=simulation_backend,
98
+ verbose=verbose,
99
+ )
100
+
101
+
102
+ def _serialize_simulation_options(
103
+ options: SuperLinkSimulationOptions,
104
+ ) -> dict[str, Any]:
105
+ """Convert SuperLinkSimulationOptions to a dictionary for TOML serialization."""
106
+ options_dict: dict[str, Any] = {
107
+ SuperLinkSimulationOptionsTomlKey.NUM_SUPERNODES: options.num_supernodes
108
+ }
109
+
110
+ if options.verbose is not None:
111
+ options_dict[SuperLinkSimulationOptionsTomlKey.VERBOSE] = options.verbose
112
+
113
+ if options.backend is not None:
114
+ backend = options.backend
115
+
116
+ # Serialize client resources
117
+ c_res_dict: dict[str, Any] = {}
118
+ if backend.client_resources is not None:
119
+ client_res = backend.client_resources
120
+ c_res_dict = {
121
+ SimulationClientResourcesTomlKey.NUM_CPUS: client_res.num_cpus,
122
+ SimulationClientResourcesTomlKey.NUM_GPUS: client_res.num_gpus,
123
+ }
124
+ # Remove None values
125
+ c_res_dict = {k: v for k, v in c_res_dict.items() if v is not None}
126
+
127
+ # Serialize init args
128
+ init_args_dict: dict[str, Any] = {}
129
+ if backend.init_args is not None:
130
+ init_args = backend.init_args
131
+ init_args_dict = {
132
+ SimulationInitArgsTomlKey.NUM_CPUS: init_args.num_cpus,
133
+ SimulationInitArgsTomlKey.NUM_GPUS: init_args.num_gpus,
134
+ SimulationInitArgsTomlKey.LOGGING_LEVEL: init_args.logging_level,
135
+ SimulationInitArgsTomlKey.LOG_TO_DRIVE: init_args.log_to_drive,
136
+ }
137
+ # Remove None values
138
+ init_args_dict = {k: v for k, v in init_args_dict.items() if v is not None}
139
+
140
+ backend_dict = {
141
+ SimulationBackendConfigTomlKey.NAME: backend.name,
142
+ SimulationBackendConfigTomlKey.CLIENT_RESOURCES: c_res_dict,
143
+ SimulationBackendConfigTomlKey.INIT_ARGS: init_args_dict,
144
+ }
145
+ # Remove empty dicts
146
+ backend_dict = {k: v for k, v in backend_dict.items() if v}
147
+
148
+ options_dict[SuperLinkSimulationOptionsTomlKey.BACKEND] = backend_dict
149
+
150
+ return options_dict
151
+
152
+
153
+ def init_flwr_config() -> None:
154
+ """Initialize the Flower configuration file."""
155
+ config_path = get_flwr_home() / FLOWER_CONFIG_FILE
156
+
157
+ if not config_path.exists():
158
+ # Create parent directory if it doesn't exist
159
+ config_path.parent.mkdir(parents=True, exist_ok=True)
160
+ # Write Flower config file
161
+ config_path.write_text(DEFAULT_FLOWER_CONFIG_TOML, encoding="utf-8")
162
+
163
+ typer.secho(
164
+ f"\nFlower configuration not found. Created default configuration"
165
+ f" at {config_path}\n",
166
+ )
167
+
168
+
169
+ def parse_superlink_connection(
170
+ conn_dict: dict[str, Any], name: str
171
+ ) -> SuperLinkConnection:
172
+ """Parse SuperLink connection configuration from a TOML dictionary.
173
+
174
+ Parameters
175
+ ----------
176
+ conn_dict : dict[str, Any]
177
+ The TOML configuration dictionary for the connection.
178
+ name : str
179
+ The name of the connection.
180
+
181
+ Returns
182
+ -------
183
+ SuperLinkConnection
184
+ The parsed SuperLink connection configuration.
185
+ """
186
+ simulation_options: SuperLinkSimulationOptions | None = None
187
+ if SuperLinkConnectionTomlKey.OPTIONS in conn_dict:
188
+ options = conn_dict[SuperLinkConnectionTomlKey.OPTIONS]
189
+ if isinstance(options, dict):
190
+ simulation_options = _parse_simulation_options(options)
191
+ else:
192
+ raise ValueError(
193
+ f"Invalid value for key '{SuperLinkConnectionTomlKey.OPTIONS}': "
194
+ f"expected dict, but got {type(options).__name__}."
195
+ )
196
+
197
+ # Build and return SuperLinkConnection
198
+ return SuperLinkConnection(
199
+ name=name,
200
+ address=conn_dict.get(SuperLinkConnectionTomlKey.ADDRESS),
201
+ root_certificates=conn_dict.get(SuperLinkConnectionTomlKey.ROOT_CERTIFICATES),
202
+ insecure=conn_dict.get(SuperLinkConnectionTomlKey.INSECURE),
203
+ federation=conn_dict.get(SuperLinkConnectionTomlKey.FEDERATION),
204
+ options=simulation_options,
205
+ )
206
+
207
+
208
+ def serialize_superlink_connection(connection: SuperLinkConnection) -> dict[str, Any]:
209
+ """Convert SuperLinkConnection to a dictionary for TOML serialization.
210
+
211
+ Parameters
212
+ ----------
213
+ connection : SuperLinkConnection
214
+ The SuperLink connection to serialize.
215
+
216
+ Returns
217
+ -------
218
+ dict[str, Any]
219
+ Dictionary representation suitable for TOML serialization.
220
+ """
221
+ # pylint: disable=protected-access
222
+ conn_dict: dict[str, Any] = {
223
+ SuperLinkConnectionTomlKey.ADDRESS: connection.address,
224
+ SuperLinkConnectionTomlKey.ROOT_CERTIFICATES: connection.root_certificates,
225
+ SuperLinkConnectionTomlKey.INSECURE: connection._insecure,
226
+ SuperLinkConnectionTomlKey.FEDERATION: connection.federation,
227
+ }
228
+ # Remove None values
229
+ conn_dict = {k: v for k, v in conn_dict.items() if v is not None}
230
+
231
+ if connection.options is not None:
232
+ options_dict = _serialize_simulation_options(connection.options)
233
+ conn_dict[SuperLinkConnectionTomlKey.OPTIONS] = options_dict
234
+
235
+ return conn_dict
236
+
237
+
238
+ def read_superlink_connection(
239
+ connection_name: str | None = None,
240
+ ) -> SuperLinkConnection:
241
+ """Read a SuperLink connection from the Flower configuration file.
242
+
243
+ Parameters
244
+ ----------
245
+ connection_name : str | None
246
+ The name of the SuperLink connection to load. If None, the default connection
247
+ will be loaded.
248
+
249
+ Returns
250
+ -------
251
+ SuperLinkConnection
252
+ The SuperLink connection.
253
+
254
+ Raises
255
+ ------
256
+ click.ClickException
257
+ Raised if the configuration file is corrupted, or if the requested
258
+ connection (or default) cannot be found.
259
+ """
260
+ toml_dict, config_path = read_flower_config()
261
+
262
+ try:
263
+ superlink_config = toml_dict.get(SuperLinkConnectionTomlKey.SUPERLINK, {})
264
+
265
+ # Load the default SuperLink connection when not provided
266
+ if connection_name is None:
267
+ connection_name = superlink_config.get(SuperLinkConnectionTomlKey.DEFAULT)
268
+
269
+ # Exit when no connection name is available
270
+ if connection_name is None:
271
+ raise click.ClickException(
272
+ "No SuperLink connection set. A SuperLink connection needs to be "
273
+ "provided or one must be set as default in the Flower "
274
+ f"configuration file ({config_path}). Specify a default SuperLink "
275
+ "connection by adding: \n\n[superlink]\ndefault = 'connection_name'\n\n"
276
+ f"to the Flower configuration file ({config_path})."
277
+ )
278
+
279
+ # Try to find the connection with the given name
280
+ if connection_name not in superlink_config:
281
+ msg = (
282
+ f"SuperLink connection '{connection_name}' not found in the "
283
+ f"Flower configuration file ({config_path})."
284
+ )
285
+ # If default was used, show a specific error message
286
+ if connection_name == superlink_config.get(
287
+ SuperLinkConnectionTomlKey.DEFAULT
288
+ ):
289
+ msg += (
290
+ f"\nPlease check that the default connection '{connection_name}' "
291
+ "is defined in the [superlink] section."
292
+ )
293
+ raise click.ClickException(msg)
294
+
295
+ conn_dict = superlink_config[connection_name]
296
+ return parse_superlink_connection(conn_dict, connection_name)
297
+
298
+ except ValueError as err:
299
+ raise click.ClickException(
300
+ f"Failed to parse the Flower configuration file ({config_path}). {err}"
301
+ ) from err
302
+ except Exception as err:
303
+ raise click.ClickException(
304
+ f"An unexpected error occurred while reading the Flower configuration "
305
+ f"file ({config_path}). {err}"
306
+ ) from err
307
+
308
+
309
+ def write_superlink_connection(connection: SuperLinkConnection) -> None:
310
+ """Write a SuperLink connection to the Flower configuration file.
311
+
312
+ Parameters
313
+ ----------
314
+ connection : SuperLinkConnection
315
+ The SuperLink connection to write to the configuration file.
316
+
317
+ Raises
318
+ ------
319
+ click.ClickException
320
+ Raised if the configuration file cannot be read or written.
321
+ """
322
+ toml_dict, _ = read_flower_config()
323
+
324
+ # Ensure superlink section exists
325
+ if SuperLinkConnectionTomlKey.SUPERLINK not in toml_dict:
326
+ toml_dict[SuperLinkConnectionTomlKey.SUPERLINK] = {}
327
+
328
+ superlink_config = toml_dict[SuperLinkConnectionTomlKey.SUPERLINK]
329
+
330
+ # Serialize connection and flatten nested dicts using dotted keys
331
+ conn_dict = serialize_superlink_connection(connection)
332
+
333
+ # Add/update the connection
334
+ superlink_config[connection.name] = conn_dict
335
+
336
+ # Write back to file
337
+ write_flower_config(toml_dict)
338
+
339
+
340
+ def set_default_superlink_connection(connection_name: str) -> None:
341
+ """Set the default SuperLink connection."""
342
+ toml_dict, _ = read_flower_config()
343
+
344
+ # Get superlink section
345
+ superlink_config = toml_dict[SuperLinkConnectionTomlKey.SUPERLINK]
346
+
347
+ # Check if the connection exists
348
+ if connection_name not in superlink_config:
349
+ raise click.ClickException(
350
+ f"SuperLink connection '{connection_name}' not found in the Flower "
351
+ "configuration file. Cannot set as default."
352
+ )
353
+
354
+ # Set default connection
355
+ superlink_config[SuperLinkConnectionTomlKey.DEFAULT] = connection_name
356
+
357
+ # Write back to file
358
+ write_flower_config(toml_dict)
359
+
360
+
361
+ def read_flower_config() -> tuple[dict[str, Any], Path]:
362
+ """Read the Flower configuration file.
363
+
364
+ Returns
365
+ -------
366
+ tuple[dict[str, Any], Path]
367
+ A tuple containing the TOML configuration dictionary and the path to the
368
+ configuration file.
369
+
370
+ Raises
371
+ ------
372
+ click.ClickException
373
+ Raised if the configuration file is corrupted.
374
+ """
375
+ init_flwr_config()
376
+
377
+ config_path = get_flwr_home() / FLOWER_CONFIG_FILE
378
+
379
+ try:
380
+ with config_path.open("rb") as file:
381
+ return tomli.load(file), config_path
382
+
383
+ except tomli.TOMLDecodeError as err:
384
+ raise click.ClickException(
385
+ f"Failed to read the Flower configuration file ({config_path}). "
386
+ "Please ensure it is valid TOML."
387
+ ) from err
388
+
389
+
390
+ # This function may be subject to change once we introduce more configuration
391
+ def write_flower_config(toml_dict: dict[str, Any]) -> Path:
392
+ """Write the Flower configuration file.
393
+
394
+ Parameters
395
+ ----------
396
+ toml_dict : dict[str, Any]
397
+ The TOML configuration dictionary to write to the file.
398
+
399
+ Returns
400
+ -------
401
+ Path
402
+ The path to the configuration file.
403
+ """
404
+ config_path = get_flwr_home() / FLOWER_CONFIG_FILE
405
+
406
+ # Flatten SuperLink connections
407
+ superlink_config: dict[str, Any] = toml_dict[SuperLinkConnectionTomlKey.SUPERLINK]
408
+ for name in list(superlink_config.keys()):
409
+ if isinstance(superlink_config[name], dict):
410
+ superlink_config[name] = flatten_dict(superlink_config[name])
411
+
412
+ # Get the standard TOML text
413
+ toml_content = tomli_w.dumps(toml_dict)
414
+
415
+ # Remove double quotes around multi-dot keys
416
+ # All keys must be [A-Za-z0-9_-]+ except dots
417
+ lines = toml_content.splitlines(keepends=True)
418
+ pattern = re.compile(r'^"([^"]+\.[^"]+)"\s*=')
419
+ for i, line in enumerate(lines):
420
+ if match := pattern.match(line):
421
+ key = match.group(1)
422
+ lines[i] = line.replace(f'"{key}"', key)
423
+
424
+ toml_content = "".join(lines)
425
+
426
+ with config_path.open("w") as file:
427
+ file.write(toml_content)
428
+
429
+ return config_path
flwr/cli/install.py CHANGED
@@ -23,6 +23,7 @@ from io import BytesIO
23
23
  from pathlib import Path
24
24
  from typing import IO, Annotated
25
25
 
26
+ import click
26
27
  import typer
27
28
 
28
29
  from flwr.common.config import get_flwr_dir, get_metadata_from_config
@@ -62,26 +63,19 @@ def install(
62
63
  if source is None:
63
64
  source = Path(typer.prompt("Enter the source FAB file"))
64
65
 
65
- source = source.resolve()
66
+ source = source.expanduser().resolve()
66
67
  if not source.exists() or not source.is_file():
67
- typer.secho(
68
- f"The source {source} does not exist or is not a file.",
69
- fg=typer.colors.RED,
70
- bold=True,
71
- err=True,
68
+ raise click.ClickException(
69
+ f"The source {source} does not exist or is not a file."
72
70
  )
73
- raise typer.Exit(code=1)
74
71
 
75
72
  if source.suffix != ".fab":
76
- typer.secho(
77
- f"❌ The source {source} is not a `.fab` file.",
78
- fg=typer.colors.RED,
79
- bold=True,
80
- err=True,
81
- )
82
- raise typer.Exit(code=1)
73
+ raise click.ClickException(f"The source {source} is not a `.fab` file.")
83
74
 
84
- install_from_fab(source, flwr_dir)
75
+ try:
76
+ install_from_fab(source, flwr_dir)
77
+ except ValueError as e:
78
+ raise click.ClickException(str(e)) from None
85
79
 
86
80
 
87
81
  def install_from_fab(
@@ -107,7 +101,7 @@ def install_from_fab(
107
101
 
108
102
  Raises
109
103
  ------
110
- typer.Exit
104
+ click.ClickException
111
105
  If FAB format is invalid or hash verification fails.
112
106
  """
113
107
  fab_file_archive: Path | IO[bytes]
@@ -129,26 +123,14 @@ def install_from_fab(
129
123
  tmpdir_path = Path(tmpdir)
130
124
  info_dir = tmpdir_path / ".info"
131
125
  if not info_dir.exists():
132
- typer.secho(
133
- "❌ FAB file has incorrect format.",
134
- fg=typer.colors.RED,
135
- bold=True,
136
- err=True,
137
- )
138
- raise typer.Exit(code=1)
126
+ raise click.ClickException("FAB file has incorrect format.")
139
127
 
140
128
  content_file = info_dir / "CONTENT"
141
129
 
142
130
  if not content_file.exists() or not _verify_hashes(
143
131
  content_file.read_text(), tmpdir_path
144
132
  ):
145
- typer.secho(
146
- "❌ File hashes couldn't be verified.",
147
- fg=typer.colors.RED,
148
- bold=True,
149
- err=True,
150
- )
151
- raise typer.Exit(code=1)
133
+ raise click.ClickException("File hashes couldn't be verified.")
152
134
 
153
135
  shutil.rmtree(info_dir)
154
136
 
@@ -189,19 +171,10 @@ def validate_and_install(
189
171
 
190
172
  Raises
191
173
  ------
192
- typer.Exit
174
+ click.ClickException
193
175
  If configuration is invalid or metadata doesn't match.
194
176
  """
195
- config, _, _ = load_and_validate(project_dir / "pyproject.toml", check_module=False)
196
-
197
- if config is None:
198
- typer.secho(
199
- "❌ Invalid config inside FAB file.",
200
- fg=typer.colors.RED,
201
- bold=True,
202
- err=True,
203
- )
204
- raise typer.Exit(code=1)
177
+ config, _ = load_and_validate(project_dir / "pyproject.toml", check_module=False)
205
178
 
206
179
  fab_id, version = get_metadata_from_config(config)
207
180
  publisher, project_name = fab_id.split("/")
@@ -283,7 +256,7 @@ def _validate_fab_and_config_metadata(
283
256
 
284
257
  Raises
285
258
  ------
286
- typer.Exit
259
+ click.ClickException
287
260
  If filename format is incorrect or hash doesn't match.
288
261
  """
289
262
  publisher, project_name, version, fab_hash = config_metadata
@@ -299,33 +272,21 @@ def _validate_fab_and_config_metadata(
299
272
  != f"{publisher}.{project_name}.{version}"
300
273
  or len(fab_shorthash) != FAB_HASH_TRUNCATION # Verify hash length
301
274
  ):
302
- typer.secho(
303
- "FAB file has incorrect name. The file name must follow the format "
304
- "`<publisher>.<project_name>.<version>.<8hexchars>.fab`.",
305
- fg=typer.colors.RED,
306
- bold=True,
307
- err=True,
275
+ raise click.ClickException(
276
+ "FAB file has incorrect name. The file name must follow the format "
277
+ "`<publisher>.<project_name>.<version>.<8hexchars>.fab`."
308
278
  )
309
- raise typer.Exit(code=1)
310
279
 
311
280
  # Verify hash is a valid hexadecimal
312
281
  try:
313
282
  _ = int(fab_shorthash, 16)
314
283
  except Exception as e:
315
- typer.secho(
316
- f"FAB file has an invalid hexadecimal string `{fab_shorthash}`.",
317
- fg=typer.colors.RED,
318
- bold=True,
319
- err=True,
320
- )
321
- raise typer.Exit(code=1) from e
284
+ raise click.ClickException(
285
+ f"FAB file has an invalid hexadecimal string `{fab_shorthash}`."
286
+ ) from e
322
287
 
323
288
  # Verify shorthash matches
324
289
  if fab_shorthash != fab_hash[:FAB_HASH_TRUNCATION]:
325
- typer.secho(
326
- "The hash in the FAB file name does not match the hash of the FAB.",
327
- fg=typer.colors.RED,
328
- bold=True,
329
- err=True,
290
+ raise click.ClickException(
291
+ "The hash in the FAB file name does not match the hash of the FAB."
330
292
  )
331
- raise typer.Exit(code=1)
flwr/cli/log.py CHANGED
@@ -17,25 +17,22 @@
17
17
 
18
18
  import time
19
19
  from logging import DEBUG, ERROR, INFO
20
- from pathlib import Path
21
- from typing import Annotated, Any, cast
20
+ from typing import Annotated, cast
22
21
 
22
+ import click
23
23
  import grpc
24
24
  import typer
25
25
 
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
- )
26
+ from flwr.cli.config_migration import migrate, warn_if_federation_config_overrides
32
27
  from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
28
+ from flwr.cli.flower_config import read_superlink_connection
29
+ from flwr.cli.typing import SuperLinkConnection
33
30
  from flwr.common.constant import CONN_RECONNECT_INTERVAL, CONN_REFRESH_PERIOD
34
31
  from flwr.common.logger import log as logger
35
32
  from flwr.proto.control_pb2 import StreamLogsRequest # pylint: disable=E0611
36
33
  from flwr.proto.control_pb2_grpc import ControlStub
37
34
 
38
- from .utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
35
+ from .utils import flwr_cli_grpc_exc_handler, init_channel_from_connection
39
36
 
40
37
 
41
38
  class AllLogsRetrieved(BaseException):
@@ -158,23 +155,21 @@ def print_logs(run_id: int, channel: grpc.Channel, timeout: int) -> None:
158
155
 
159
156
 
160
157
  def log(
158
+ ctx: typer.Context,
161
159
  run_id: Annotated[
162
160
  int,
163
161
  typer.Argument(help="The Flower run ID to query"),
164
162
  ],
165
- app: Annotated[
166
- Path,
167
- typer.Argument(help="Path of the Flower project to run"),
168
- ] = Path("."),
169
- federation: Annotated[
163
+ superlink: Annotated[
170
164
  str | None,
171
- typer.Argument(help="Name of the federation to run the app on"),
165
+ typer.Argument(help="Name of the SuperLink connection."),
172
166
  ] = None,
173
167
  federation_config_overrides: Annotated[
174
168
  list[str] | None,
175
169
  typer.Option(
176
170
  "--federation-config",
177
171
  help=FEDERATION_CONFIG_HELP_MESSAGE,
172
+ hidden=True,
178
173
  ),
179
174
  ] = None,
180
175
  stream: Annotated[
@@ -190,27 +185,23 @@ def log(
190
185
  Retrieve and display logs from a Flower run. Logs can be streamed in real-time (with
191
186
  --stream) or printed once (with --show).
192
187
  """
193
- typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
188
+ # Warn `--federation-config` is ignored
189
+ warn_if_federation_config_overrides(federation_config_overrides)
194
190
 
195
- pyproject_path = app / "pyproject.toml" if app else None
196
- config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
197
- config = process_loaded_project_config(config, errors, warnings)
198
- federation, federation_config = validate_federation_in_project_config(
199
- federation, config, federation_config_overrides
200
- )
201
- exit_if_no_address(federation_config, "log")
191
+ # Migrate legacy usage if any
192
+ migrate(superlink, args=ctx.args)
193
+
194
+ # Read superlink connection configuration
195
+ superlink_connection = read_superlink_connection(superlink)
202
196
 
203
197
  try:
204
- _log_with_control_api(app, federation, federation_config, run_id, stream)
198
+ _log_with_control_api(superlink_connection, run_id, stream)
205
199
  except Exception as err: # pylint: disable=broad-except
206
- typer.secho(str(err), fg=typer.colors.RED, bold=True, err=True)
207
- raise typer.Exit(code=1) from None
200
+ raise click.ClickException(str(err)) from None
208
201
 
209
202
 
210
203
  def _log_with_control_api(
211
- app: Path,
212
- federation: str,
213
- federation_config: dict[str, Any],
204
+ superlink_connection: SuperLinkConnection,
214
205
  run_id: int,
215
206
  stream: bool,
216
207
  ) -> None:
@@ -218,19 +209,14 @@ def _log_with_control_api(
218
209
 
219
210
  Parameters
220
211
  ----------
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.
212
+ superlink_connection : SuperLinkConnection
213
+ Superlink connection configuration.
227
214
  run_id : int
228
215
  The unique identifier of the run to retrieve logs from.
229
216
  stream : bool
230
217
  If True, stream logs continuously; if False, print once.
231
218
  """
232
- auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
233
- channel = init_channel(app, federation_config, auth_plugin)
219
+ channel = init_channel_from_connection(superlink_connection)
234
220
 
235
221
  if stream:
236
222
  start_stream(run_id, channel, CONN_REFRESH_PERIOD)