flwr 1.13.1__py3-none-any.whl → 1.15.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 (158) hide show
  1. flwr/cli/app.py +5 -0
  2. flwr/cli/auth_plugin/__init__.py +31 -0
  3. flwr/cli/auth_plugin/oidc_cli_plugin.py +150 -0
  4. flwr/cli/build.py +1 -0
  5. flwr/cli/cli_user_auth_interceptor.py +90 -0
  6. flwr/cli/config_utils.py +43 -149
  7. flwr/cli/constant.py +27 -0
  8. flwr/cli/example.py +1 -0
  9. flwr/cli/install.py +2 -1
  10. flwr/cli/log.py +34 -37
  11. flwr/cli/login/__init__.py +22 -0
  12. flwr/cli/login/login.py +116 -0
  13. flwr/cli/ls.py +214 -106
  14. flwr/cli/new/__init__.py +1 -0
  15. flwr/cli/new/new.py +2 -1
  16. flwr/cli/new/templates/app/.gitignore.tpl +3 -0
  17. flwr/cli/new/templates/app/README.md.tpl +3 -2
  18. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +4 -4
  19. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +4 -4
  20. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +4 -4
  21. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +2 -2
  22. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +3 -4
  23. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +2 -2
  24. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +4 -4
  25. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +3 -3
  26. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +2 -2
  27. flwr/cli/run/__init__.py +1 -0
  28. flwr/cli/run/run.py +103 -43
  29. flwr/cli/stop.py +139 -0
  30. flwr/cli/utils.py +186 -8
  31. flwr/client/app.py +49 -50
  32. flwr/client/client.py +1 -32
  33. flwr/client/clientapp/app.py +23 -26
  34. flwr/client/clientapp/utils.py +2 -1
  35. flwr/client/grpc_adapter_client/connection.py +1 -1
  36. flwr/client/grpc_client/connection.py +2 -13
  37. flwr/client/grpc_rere_client/client_interceptor.py +19 -119
  38. flwr/client/grpc_rere_client/connection.py +59 -43
  39. flwr/client/grpc_rere_client/grpc_adapter.py +12 -12
  40. flwr/client/message_handler/message_handler.py +1 -2
  41. flwr/client/message_handler/task_handler.py +0 -17
  42. flwr/client/mod/comms_mods.py +1 -0
  43. flwr/client/mod/localdp_mod.py +1 -1
  44. flwr/client/nodestate/__init__.py +1 -0
  45. flwr/client/nodestate/nodestate.py +1 -0
  46. flwr/client/nodestate/nodestate_factory.py +1 -0
  47. flwr/client/numpy_client.py +0 -44
  48. flwr/client/rest_client/connection.py +37 -29
  49. flwr/client/supernode/app.py +20 -74
  50. flwr/common/address.py +1 -0
  51. flwr/common/args.py +26 -47
  52. flwr/common/auth_plugin/__init__.py +24 -0
  53. flwr/common/auth_plugin/auth_plugin.py +122 -0
  54. flwr/common/config.py +169 -17
  55. flwr/common/constant.py +38 -9
  56. flwr/common/differential_privacy.py +2 -1
  57. flwr/common/exit/__init__.py +24 -0
  58. flwr/common/exit/exit.py +99 -0
  59. flwr/common/exit/exit_code.py +93 -0
  60. flwr/common/exit_handlers.py +24 -10
  61. flwr/common/grpc.py +167 -4
  62. flwr/common/logger.py +66 -7
  63. flwr/common/message.py +1 -0
  64. flwr/common/object_ref.py +57 -54
  65. flwr/common/pyproject.py +1 -0
  66. flwr/common/record/__init__.py +1 -0
  67. flwr/common/record/parametersrecord.py +1 -0
  68. flwr/common/record/recordset.py +1 -1
  69. flwr/common/retry_invoker.py +77 -0
  70. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +45 -0
  71. flwr/common/secure_aggregation/secaggplus_utils.py +2 -2
  72. flwr/common/serde.py +6 -4
  73. flwr/common/telemetry.py +15 -4
  74. flwr/common/typing.py +32 -0
  75. flwr/common/version.py +1 -0
  76. flwr/proto/clientappio_pb2.py +1 -1
  77. flwr/proto/error_pb2.py +1 -1
  78. flwr/proto/exec_pb2.py +27 -15
  79. flwr/proto/exec_pb2.pyi +80 -2
  80. flwr/proto/exec_pb2_grpc.py +102 -0
  81. flwr/proto/exec_pb2_grpc.pyi +39 -0
  82. flwr/proto/fab_pb2.py +5 -5
  83. flwr/proto/fab_pb2.pyi +4 -1
  84. flwr/proto/fleet_pb2.py +31 -31
  85. flwr/proto/fleet_pb2.pyi +23 -23
  86. flwr/proto/fleet_pb2_grpc.py +30 -30
  87. flwr/proto/fleet_pb2_grpc.pyi +20 -20
  88. flwr/proto/grpcadapter_pb2.py +1 -1
  89. flwr/proto/log_pb2.py +1 -1
  90. flwr/proto/message_pb2.py +1 -1
  91. flwr/proto/node_pb2.py +3 -3
  92. flwr/proto/node_pb2.pyi +1 -4
  93. flwr/proto/recordset_pb2.py +1 -1
  94. flwr/proto/run_pb2.py +1 -1
  95. flwr/proto/serverappio_pb2.py +24 -25
  96. flwr/proto/serverappio_pb2.pyi +32 -32
  97. flwr/proto/serverappio_pb2_grpc.py +62 -28
  98. flwr/proto/serverappio_pb2_grpc.pyi +29 -16
  99. flwr/proto/simulationio_pb2.py +3 -3
  100. flwr/proto/simulationio_pb2_grpc.py +34 -0
  101. flwr/proto/simulationio_pb2_grpc.pyi +13 -0
  102. flwr/proto/task_pb2.py +1 -1
  103. flwr/proto/transport_pb2.py +1 -1
  104. flwr/server/app.py +152 -112
  105. flwr/server/compat/app_utils.py +7 -2
  106. flwr/server/compat/driver_client_proxy.py +1 -2
  107. flwr/server/driver/grpc_driver.py +38 -85
  108. flwr/server/driver/inmemory_driver.py +7 -2
  109. flwr/server/run_serverapp.py +8 -9
  110. flwr/server/serverapp/app.py +37 -13
  111. flwr/server/strategy/dpfedavg_fixed.py +1 -0
  112. flwr/server/superlink/driver/serverappio_grpc.py +2 -1
  113. flwr/server/superlink/driver/serverappio_servicer.py +148 -63
  114. flwr/server/superlink/ffs/disk_ffs.py +1 -0
  115. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +20 -87
  116. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -0
  117. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +2 -165
  118. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +56 -35
  119. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +99 -169
  120. flwr/server/superlink/fleet/message_handler/message_handler.py +69 -29
  121. flwr/server/superlink/fleet/rest_rere/rest_api.py +20 -19
  122. flwr/server/superlink/fleet/vce/__init__.py +1 -0
  123. flwr/server/superlink/fleet/vce/backend/__init__.py +1 -0
  124. flwr/server/superlink/fleet/vce/backend/raybackend.py +1 -0
  125. flwr/server/superlink/fleet/vce/vce_api.py +2 -2
  126. flwr/server/superlink/linkstate/in_memory_linkstate.py +60 -99
  127. flwr/server/superlink/linkstate/linkstate.py +30 -36
  128. flwr/server/superlink/linkstate/sqlite_linkstate.py +105 -188
  129. flwr/server/superlink/linkstate/utils.py +18 -8
  130. flwr/server/superlink/simulation/simulationio_grpc.py +1 -1
  131. flwr/server/superlink/simulation/simulationio_servicer.py +33 -0
  132. flwr/server/superlink/utils.py +65 -0
  133. flwr/server/utils/validator.py +9 -34
  134. flwr/simulation/app.py +20 -10
  135. flwr/simulation/legacy_app.py +4 -2
  136. flwr/simulation/ray_transport/ray_actor.py +1 -0
  137. flwr/simulation/ray_transport/utils.py +1 -0
  138. flwr/simulation/run_simulation.py +36 -22
  139. flwr/simulation/simulationio_connection.py +5 -1
  140. flwr/superexec/app.py +1 -0
  141. flwr/superexec/deployment.py +1 -0
  142. flwr/superexec/exec_grpc.py +20 -2
  143. flwr/superexec/exec_servicer.py +97 -2
  144. flwr/superexec/exec_user_auth_interceptor.py +101 -0
  145. flwr/superexec/executor.py +1 -0
  146. {flwr-1.13.1.dist-info → flwr-1.15.0.dist-info}/METADATA +14 -13
  147. {flwr-1.13.1.dist-info → flwr-1.15.0.dist-info}/RECORD +150 -144
  148. flwr/proto/common_pb2.py +0 -36
  149. flwr/proto/common_pb2.pyi +0 -121
  150. flwr/proto/common_pb2_grpc.py +0 -4
  151. flwr/proto/common_pb2_grpc.pyi +0 -4
  152. flwr/proto/control_pb2.py +0 -27
  153. flwr/proto/control_pb2.pyi +0 -7
  154. flwr/proto/control_pb2_grpc.py +0 -135
  155. flwr/proto/control_pb2_grpc.pyi +0 -53
  156. {flwr-1.13.1.dist-info → flwr-1.15.0.dist-info}/LICENSE +0 -0
  157. {flwr-1.13.1.dist-info → flwr-1.15.0.dist-info}/WHEEL +0 -0
  158. {flwr-1.13.1.dist-info → flwr-1.15.0.dist-info}/entry_points.txt +0 -0
flwr/cli/app.py CHANGED
@@ -14,15 +14,18 @@
14
14
  # ==============================================================================
15
15
  """Flower command line interface."""
16
16
 
17
+
17
18
  import typer
18
19
  from typer.main import get_command
19
20
 
20
21
  from .build import build
21
22
  from .install import install
22
23
  from .log import log
24
+ from .login import login
23
25
  from .ls import ls
24
26
  from .new import new
25
27
  from .run import run
28
+ from .stop import stop
26
29
 
27
30
  app = typer.Typer(
28
31
  help=typer.style(
@@ -39,6 +42,8 @@ app.command()(build)
39
42
  app.command()(install)
40
43
  app.command()(log)
41
44
  app.command()(ls)
45
+ app.command()(stop)
46
+ app.command()(login)
42
47
 
43
48
  typer_click_object = get_command(app)
44
49
 
@@ -0,0 +1,31 @@
1
+ # Copyright 2025 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 user auth plugins."""
16
+
17
+
18
+ from flwr.common.auth_plugin import CliAuthPlugin
19
+ from flwr.common.constant import AuthType
20
+
21
+ from .oidc_cli_plugin import OidcCliPlugin
22
+
23
+
24
+ def get_cli_auth_plugins() -> dict[str, type[CliAuthPlugin]]:
25
+ """Return all CLI authentication plugins."""
26
+ return {AuthType.OIDC: OidcCliPlugin}
27
+
28
+
29
+ __all__ = [
30
+ "get_cli_auth_plugins",
31
+ ]
@@ -0,0 +1,150 @@
1
+ # Copyright 2025 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 CLI user auth plugin for OIDC."""
16
+
17
+
18
+ import json
19
+ import time
20
+ from collections.abc import Sequence
21
+ from pathlib import Path
22
+ from typing import Any, Optional, Union
23
+
24
+ import typer
25
+
26
+ from flwr.common.auth_plugin import CliAuthPlugin
27
+ from flwr.common.constant import (
28
+ ACCESS_TOKEN_KEY,
29
+ AUTH_TYPE_KEY,
30
+ REFRESH_TOKEN_KEY,
31
+ AuthType,
32
+ )
33
+ from flwr.common.typing import UserAuthCredentials, UserAuthLoginDetails
34
+ from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
35
+ GetAuthTokensRequest,
36
+ GetAuthTokensResponse,
37
+ )
38
+ from flwr.proto.exec_pb2_grpc import ExecStub
39
+
40
+
41
+ class OidcCliPlugin(CliAuthPlugin):
42
+ """Flower OIDC auth plugin for CLI."""
43
+
44
+ def __init__(self, credentials_path: Path):
45
+ self.access_token: Optional[str] = None
46
+ self.refresh_token: Optional[str] = None
47
+ self.credentials_path = credentials_path
48
+
49
+ @staticmethod
50
+ def login(
51
+ login_details: UserAuthLoginDetails,
52
+ exec_stub: ExecStub,
53
+ ) -> UserAuthCredentials:
54
+ """Authenticate the user and retrieve authentication credentials."""
55
+ typer.secho(
56
+ "Please login with your user credentials here: "
57
+ f"{login_details.verification_uri_complete}",
58
+ fg=typer.colors.BLUE,
59
+ )
60
+ start_time = time.time()
61
+ time.sleep(login_details.interval)
62
+
63
+ while (time.time() - start_time) < login_details.expires_in:
64
+ res: GetAuthTokensResponse = exec_stub.GetAuthTokens(
65
+ GetAuthTokensRequest(device_code=login_details.device_code)
66
+ )
67
+
68
+ access_token = res.access_token
69
+ refresh_token = res.refresh_token
70
+
71
+ if access_token and refresh_token:
72
+ typer.secho(
73
+ "✅ Login successful.",
74
+ fg=typer.colors.GREEN,
75
+ bold=False,
76
+ )
77
+ return UserAuthCredentials(
78
+ access_token=access_token,
79
+ refresh_token=refresh_token,
80
+ )
81
+
82
+ time.sleep(login_details.interval)
83
+
84
+ typer.secho(
85
+ "❌ Timeout, failed to sign in.",
86
+ fg=typer.colors.RED,
87
+ bold=True,
88
+ )
89
+ raise typer.Exit(code=1)
90
+
91
+ def store_tokens(self, credentials: UserAuthCredentials) -> None:
92
+ """Store authentication tokens to the `credentials_path`.
93
+
94
+ The credentials, including tokens, will be saved as a JSON file
95
+ at `credentials_path`.
96
+ """
97
+ self.access_token = credentials.access_token
98
+ self.refresh_token = credentials.refresh_token
99
+ json_dict = {
100
+ AUTH_TYPE_KEY: AuthType.OIDC,
101
+ ACCESS_TOKEN_KEY: credentials.access_token,
102
+ REFRESH_TOKEN_KEY: credentials.refresh_token,
103
+ }
104
+
105
+ with open(self.credentials_path, "w", encoding="utf-8") as file:
106
+ json.dump(json_dict, file, indent=4)
107
+
108
+ def load_tokens(self) -> None:
109
+ """Load authentication tokens from the `credentials_path`."""
110
+ with open(self.credentials_path, encoding="utf-8") as file:
111
+ json_dict: dict[str, Any] = json.load(file)
112
+ access_token = json_dict.get(ACCESS_TOKEN_KEY)
113
+ refresh_token = json_dict.get(REFRESH_TOKEN_KEY)
114
+
115
+ if isinstance(access_token, str) and isinstance(refresh_token, str):
116
+ self.access_token = access_token
117
+ self.refresh_token = refresh_token
118
+
119
+ def write_tokens_to_metadata(
120
+ self, metadata: Sequence[tuple[str, Union[str, bytes]]]
121
+ ) -> Sequence[tuple[str, Union[str, bytes]]]:
122
+ """Write authentication tokens to the provided metadata."""
123
+ if self.access_token is None or self.refresh_token is None:
124
+ typer.secho(
125
+ "❌ Missing authentication tokens. Please login first.",
126
+ fg=typer.colors.RED,
127
+ bold=True,
128
+ )
129
+ raise typer.Exit(code=1)
130
+
131
+ return list(metadata) + [
132
+ (ACCESS_TOKEN_KEY, self.access_token),
133
+ (REFRESH_TOKEN_KEY, self.refresh_token),
134
+ ]
135
+
136
+ def read_tokens_from_metadata(
137
+ self, metadata: Sequence[tuple[str, Union[str, bytes]]]
138
+ ) -> Optional[UserAuthCredentials]:
139
+ """Read authentication tokens from the provided metadata."""
140
+ metadata_dict = dict(metadata)
141
+ access_token = metadata_dict.get(ACCESS_TOKEN_KEY)
142
+ refresh_token = metadata_dict.get(REFRESH_TOKEN_KEY)
143
+
144
+ if isinstance(access_token, str) and isinstance(refresh_token, str):
145
+ return UserAuthCredentials(
146
+ access_token=access_token,
147
+ refresh_token=refresh_token,
148
+ )
149
+
150
+ return None
flwr/cli/build.py CHANGED
@@ -14,6 +14,7 @@
14
14
  # ==============================================================================
15
15
  """Flower command line interface `build` command."""
16
16
 
17
+
17
18
  import hashlib
18
19
  import os
19
20
  import shutil
@@ -0,0 +1,90 @@
1
+ # Copyright 2024 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 run interceptor."""
16
+
17
+
18
+ from typing import Any, Callable, Union
19
+
20
+ import grpc
21
+
22
+ from flwr.common.auth_plugin import CliAuthPlugin
23
+ from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
24
+ StartRunRequest,
25
+ StreamLogsRequest,
26
+ )
27
+
28
+ Request = Union[
29
+ StartRunRequest,
30
+ StreamLogsRequest,
31
+ ]
32
+
33
+
34
+ class CliUserAuthInterceptor(
35
+ grpc.UnaryUnaryClientInterceptor, grpc.UnaryStreamClientInterceptor # type: ignore
36
+ ):
37
+ """CLI interceptor for user authentication."""
38
+
39
+ def __init__(self, auth_plugin: CliAuthPlugin):
40
+ self.auth_plugin = auth_plugin
41
+
42
+ def _authenticated_call(
43
+ self,
44
+ continuation: Callable[[Any, Any], Any],
45
+ client_call_details: grpc.ClientCallDetails,
46
+ request: Request,
47
+ ) -> grpc.Call:
48
+ """Send and receive tokens via metadata."""
49
+ new_metadata = self.auth_plugin.write_tokens_to_metadata(
50
+ client_call_details.metadata or []
51
+ )
52
+
53
+ details = client_call_details._replace(metadata=new_metadata)
54
+
55
+ response = continuation(details, request)
56
+ if response.initial_metadata():
57
+ credentials = self.auth_plugin.read_tokens_from_metadata(
58
+ response.initial_metadata()
59
+ )
60
+ # The metadata contains tokens only if they have been refreshed
61
+ if credentials is not None:
62
+ self.auth_plugin.store_tokens(credentials)
63
+
64
+ return response
65
+
66
+ def intercept_unary_unary(
67
+ self,
68
+ continuation: Callable[[Any, Any], Any],
69
+ client_call_details: grpc.ClientCallDetails,
70
+ request: Request,
71
+ ) -> grpc.Call:
72
+ """Intercept a unary-unary call for user authentication.
73
+
74
+ This method intercepts a unary-unary RPC call initiated from the CLI and adds
75
+ the required authentication tokens to the RPC metadata.
76
+ """
77
+ return self._authenticated_call(continuation, client_call_details, request)
78
+
79
+ def intercept_unary_stream(
80
+ self,
81
+ continuation: Callable[[Any, Any], Any],
82
+ client_call_details: grpc.ClientCallDetails,
83
+ request: Request,
84
+ ) -> grpc.Call:
85
+ """Intercept a unary-stream call for user authentication.
86
+
87
+ This method intercepts a unary-stream RPC call initiated from the CLI and adds
88
+ the required authentication tokens to the RPC metadata.
89
+ """
90
+ return self._authenticated_call(continuation, client_call_details, request)
flwr/cli/config_utils.py CHANGED
@@ -14,53 +14,20 @@
14
14
  # ==============================================================================
15
15
  """Utility to validate the `pyproject.toml` file."""
16
16
 
17
- import zipfile
18
- from io import BytesIO
17
+
19
18
  from pathlib import Path
20
- from typing import IO, Any, Optional, Union, get_args
19
+ from typing import Any, Optional, Union
21
20
 
22
21
  import tomli
23
22
  import typer
24
23
 
25
- from flwr.common import object_ref
26
- from flwr.common.typing import UserConfigValue
27
-
28
-
29
- def get_fab_config(fab_file: Union[Path, bytes]) -> dict[str, Any]:
30
- """Extract the config from a FAB file or path.
31
-
32
- Parameters
33
- ----------
34
- fab_file : Union[Path, bytes]
35
- The Flower App Bundle file to validate and extract the metadata from.
36
- It can either be a path to the file or the file itself as bytes.
37
-
38
- Returns
39
- -------
40
- Dict[str, Any]
41
- The `config` of the given Flower App Bundle.
42
- """
43
- fab_file_archive: Union[Path, IO[bytes]]
44
- if isinstance(fab_file, bytes):
45
- fab_file_archive = BytesIO(fab_file)
46
- elif isinstance(fab_file, Path):
47
- fab_file_archive = fab_file
48
- else:
49
- raise ValueError("fab_file must be either a Path or bytes")
50
-
51
- with zipfile.ZipFile(fab_file_archive, "r") as zipf:
52
- with zipf.open("pyproject.toml") as file:
53
- toml_content = file.read().decode("utf-8")
54
-
55
- conf = load_from_string(toml_content)
56
- if conf is None:
57
- raise ValueError("Invalid TOML content in pyproject.toml")
58
-
59
- is_valid, errors, _ = validate(conf, check_module=False)
60
- if not is_valid:
61
- raise ValueError(errors)
62
-
63
- return conf
24
+ from flwr.common.config import (
25
+ fuse_dicts,
26
+ get_fab_config,
27
+ get_metadata_from_config,
28
+ parse_config_args,
29
+ validate_config,
30
+ )
64
31
 
65
32
 
66
33
  def get_fab_metadata(fab_file: Union[Path, bytes]) -> tuple[str, str]:
@@ -77,12 +44,7 @@ def get_fab_metadata(fab_file: Union[Path, bytes]) -> tuple[str, str]:
77
44
  Tuple[str, str]
78
45
  The `fab_id` and `fab_version` of the given Flower App Bundle.
79
46
  """
80
- conf = get_fab_config(fab_file)
81
-
82
- return (
83
- f"{conf['tool']['flwr']['app']['publisher']}/{conf['project']['name']}",
84
- conf["project"]["version"],
85
- )
47
+ return get_metadata_from_config(get_fab_config(fab_file))
86
48
 
87
49
 
88
50
  def load_and_validate(
@@ -119,7 +81,7 @@ def load_and_validate(
119
81
  ]
120
82
  return (None, errors, [])
121
83
 
122
- is_valid, errors, warnings = validate(config, check_module, path.parent)
84
+ is_valid, errors, warnings = validate_config(config, check_module, path.parent)
123
85
 
124
86
  if not is_valid:
125
87
  return (None, errors, warnings)
@@ -132,108 +94,21 @@ def load(toml_path: Path) -> Optional[dict[str, Any]]:
132
94
  if not toml_path.is_file():
133
95
  return None
134
96
 
135
- with toml_path.open(encoding="utf-8") as toml_file:
136
- return load_from_string(toml_file.read())
137
-
138
-
139
- def _validate_run_config(config_dict: dict[str, Any], errors: list[str]) -> None:
140
- for key, value in config_dict.items():
141
- if isinstance(value, dict):
142
- _validate_run_config(config_dict[key], errors)
143
- elif not isinstance(value, get_args(UserConfigValue)):
144
- raise ValueError(
145
- f"The value for key {key} needs to be of type `int`, `float`, "
146
- "`bool, `str`, or a `dict` of those.",
147
- )
148
-
149
-
150
- # pylint: disable=too-many-branches
151
- def validate_fields(config: dict[str, Any]) -> tuple[bool, list[str], list[str]]:
152
- """Validate pyproject.toml fields."""
153
- errors = []
154
- warnings = []
155
-
156
- if "project" not in config:
157
- errors.append("Missing [project] section")
158
- else:
159
- if "name" not in config["project"]:
160
- errors.append('Property "name" missing in [project]')
161
- if "version" not in config["project"]:
162
- errors.append('Property "version" missing in [project]')
163
- if "description" not in config["project"]:
164
- warnings.append('Recommended property "description" missing in [project]')
165
- if "license" not in config["project"]:
166
- warnings.append('Recommended property "license" missing in [project]')
167
- if "authors" not in config["project"]:
168
- warnings.append('Recommended property "authors" missing in [project]')
169
-
170
- if (
171
- "tool" not in config
172
- or "flwr" not in config["tool"]
173
- or "app" not in config["tool"]["flwr"]
174
- ):
175
- errors.append("Missing [tool.flwr.app] section")
176
- else:
177
- if "publisher" not in config["tool"]["flwr"]["app"]:
178
- errors.append('Property "publisher" missing in [tool.flwr.app]')
179
- if "config" in config["tool"]["flwr"]["app"]:
180
- _validate_run_config(config["tool"]["flwr"]["app"]["config"], errors)
181
- if "components" not in config["tool"]["flwr"]["app"]:
182
- errors.append("Missing [tool.flwr.app.components] section")
183
- else:
184
- if "serverapp" not in config["tool"]["flwr"]["app"]["components"]:
185
- errors.append(
186
- 'Property "serverapp" missing in [tool.flwr.app.components]'
187
- )
188
- if "clientapp" not in config["tool"]["flwr"]["app"]["components"]:
189
- errors.append(
190
- 'Property "clientapp" missing in [tool.flwr.app.components]'
191
- )
192
-
193
- return len(errors) == 0, errors, warnings
194
-
195
-
196
- def validate(
197
- config: dict[str, Any],
198
- check_module: bool = True,
199
- project_dir: Optional[Union[str, Path]] = None,
200
- ) -> tuple[bool, list[str], list[str]]:
201
- """Validate pyproject.toml."""
202
- is_valid, errors, warnings = validate_fields(config)
203
-
204
- if not is_valid:
205
- return False, errors, warnings
206
-
207
- # Validate serverapp
208
- serverapp_ref = config["tool"]["flwr"]["app"]["components"]["serverapp"]
209
- is_valid, reason = object_ref.validate(serverapp_ref, check_module, project_dir)
97
+ with toml_path.open("rb") as toml_file:
98
+ try:
99
+ return tomli.load(toml_file)
100
+ except tomli.TOMLDecodeError:
101
+ return None
210
102
 
211
- if not is_valid and isinstance(reason, str):
212
- return False, [reason], []
213
-
214
- # Validate clientapp
215
- clientapp_ref = config["tool"]["flwr"]["app"]["components"]["clientapp"]
216
- is_valid, reason = object_ref.validate(clientapp_ref, check_module, project_dir)
217
-
218
- if not is_valid and isinstance(reason, str):
219
- return False, [reason], []
220
-
221
- return True, [], []
222
-
223
-
224
- def load_from_string(toml_content: str) -> Optional[dict[str, Any]]:
225
- """Load TOML content from a string and return as dict."""
226
- try:
227
- data = tomli.loads(toml_content)
228
- return data
229
- except tomli.TOMLDecodeError:
230
- return None
231
103
 
232
-
233
- def validate_project_config(
104
+ def process_loaded_project_config(
234
105
  config: Union[dict[str, Any], None], errors: list[str], warnings: list[str]
235
106
  ) -> dict[str, Any]:
236
- """Validate and return the Flower project configuration."""
107
+ """Process and return the loaded project configuration.
108
+
109
+ This function handles errors and warnings from the `load_and_validate` function,
110
+ exits on critical issues, and returns the validated configuration.
111
+ """
237
112
  if config is None:
238
113
  typer.secho(
239
114
  "Project configuration could not be loaded.\n"
@@ -258,7 +133,9 @@ def validate_project_config(
258
133
 
259
134
 
260
135
  def validate_federation_in_project_config(
261
- federation: Optional[str], config: dict[str, Any]
136
+ federation: Optional[str],
137
+ config: dict[str, Any],
138
+ overrides: Optional[list[str]] = None,
262
139
  ) -> tuple[str, dict[str, Any]]:
263
140
  """Validate the federation name in the Flower project configuration."""
264
141
  federation = federation or config["tool"]["flwr"]["federations"].get("default")
@@ -288,6 +165,11 @@ def validate_federation_in_project_config(
288
165
  )
289
166
  raise typer.Exit(code=1)
290
167
 
168
+ # Override the federation configuration if provided
169
+ if overrides:
170
+ overrides_dict = parse_config_args(overrides, flatten=False)
171
+ federation_config = fuse_dicts(federation_config, overrides_dict)
172
+
291
173
  return federation, federation_config
292
174
 
293
175
 
@@ -300,7 +182,7 @@ def validate_certificate_in_federation_config(
300
182
  root_certificates_bytes = (app / root_certificates).read_bytes()
301
183
  if insecure := bool(insecure_str):
302
184
  typer.secho(
303
- "❌ `root_certificates` were provided but the `insecure` parameter "
185
+ "❌ `root-certificates` were provided but the `insecure` parameter "
304
186
  "is set to `True`.",
305
187
  fg=typer.colors.RED,
306
188
  bold=True,
@@ -324,3 +206,15 @@ def validate_certificate_in_federation_config(
324
206
  raise typer.Exit(code=1)
325
207
 
326
208
  return insecure, root_certificates_bytes
209
+
210
+
211
+ def exit_if_no_address(federation_config: dict[str, Any], cmd: str) -> None:
212
+ """Exit if the provided federation_config has no "address" key."""
213
+ if "address" not in federation_config:
214
+ typer.secho(
215
+ f"❌ `flwr {cmd}` currently works with a SuperLink. Ensure that the correct"
216
+ "SuperLink (Exec API) address is provided in `pyproject.toml`.",
217
+ fg=typer.colors.RED,
218
+ bold=True,
219
+ )
220
+ raise typer.Exit(code=1)
flwr/cli/constant.py ADDED
@@ -0,0 +1,27 @@
1
+ # Copyright 2025 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
+ """Constants for CLI commands."""
16
+
17
+
18
+ # The help message for `--federation-config` option
19
+ FEDERATION_CONFIG_HELP_MESSAGE = (
20
+ "Override federation configuration values in the format:\n\n"
21
+ "`--federation-config 'key1=value1 key2=value2' --federation-config "
22
+ "'key3=value3'`\n\nValues can be of any type supported in TOML, such as "
23
+ "bool, int, float, or string. Ensure that the keys (`key1`, `key2`, `key3` "
24
+ "in this example) exist in the federation configuration under the "
25
+ "`[tool.flwr.federations.<YOUR_FEDERATION>]` table of the `pyproject.toml` "
26
+ "for proper overriding."
27
+ )
flwr/cli/example.py CHANGED
@@ -14,6 +14,7 @@
14
14
  # ==============================================================================
15
15
  """Flower command line interface `example` command."""
16
16
 
17
+
17
18
  import json
18
19
  import os
19
20
  import subprocess
flwr/cli/install.py CHANGED
@@ -14,6 +14,7 @@
14
14
  # ==============================================================================
15
15
  """Flower command line interface `install` command."""
16
16
 
17
+
17
18
  import hashlib
18
19
  import shutil
19
20
  import tempfile
@@ -153,7 +154,7 @@ def validate_and_install(
153
154
  )
154
155
  raise typer.Exit(code=1)
155
156
 
156
- version, fab_id = get_metadata_from_config(config)
157
+ fab_id, version = get_metadata_from_config(config)
157
158
  publisher, project_name = fab_id.split("/")
158
159
  config_metadata = (publisher, project_name, version, fab_hash)
159
160