flwr 1.20.0__py3-none-any.whl → 1.22.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 (182) hide show
  1. flwr/__init__.py +4 -1
  2. flwr/app/__init__.py +28 -0
  3. flwr/app/exception.py +31 -0
  4. flwr/cli/app.py +2 -0
  5. flwr/cli/auth_plugin/oidc_cli_plugin.py +4 -4
  6. flwr/cli/cli_user_auth_interceptor.py +1 -1
  7. flwr/cli/config_utils.py +3 -3
  8. flwr/cli/constant.py +25 -8
  9. flwr/cli/log.py +9 -9
  10. flwr/cli/login/login.py +3 -3
  11. flwr/cli/ls.py +5 -5
  12. flwr/cli/new/new.py +15 -2
  13. flwr/cli/new/templates/app/README.flowertune.md.tpl +1 -1
  14. flwr/cli/new/templates/app/code/__init__.pytorch_legacy_api.py.tpl +1 -0
  15. flwr/cli/new/templates/app/code/client.baseline.py.tpl +64 -47
  16. flwr/cli/new/templates/app/code/client.huggingface.py.tpl +68 -30
  17. flwr/cli/new/templates/app/code/client.jax.py.tpl +63 -42
  18. flwr/cli/new/templates/app/code/client.mlx.py.tpl +80 -51
  19. flwr/cli/new/templates/app/code/client.numpy.py.tpl +36 -13
  20. flwr/cli/new/templates/app/code/client.pytorch.py.tpl +71 -46
  21. flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +55 -0
  22. flwr/cli/new/templates/app/code/client.sklearn.py.tpl +75 -30
  23. flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +69 -44
  24. flwr/cli/new/templates/app/code/client.xgboost.py.tpl +110 -0
  25. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +56 -90
  26. flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +1 -23
  27. flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +37 -58
  28. flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +39 -44
  29. flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -14
  30. flwr/cli/new/templates/app/code/server.baseline.py.tpl +27 -29
  31. flwr/cli/new/templates/app/code/server.huggingface.py.tpl +23 -19
  32. flwr/cli/new/templates/app/code/server.jax.py.tpl +27 -14
  33. flwr/cli/new/templates/app/code/server.mlx.py.tpl +29 -19
  34. flwr/cli/new/templates/app/code/server.numpy.py.tpl +30 -17
  35. flwr/cli/new/templates/app/code/server.pytorch.py.tpl +36 -26
  36. flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +31 -0
  37. flwr/cli/new/templates/app/code/server.sklearn.py.tpl +29 -21
  38. flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +28 -19
  39. flwr/cli/new/templates/app/code/server.xgboost.py.tpl +56 -0
  40. flwr/cli/new/templates/app/code/task.huggingface.py.tpl +16 -20
  41. flwr/cli/new/templates/app/code/task.jax.py.tpl +1 -1
  42. flwr/cli/new/templates/app/code/task.numpy.py.tpl +1 -1
  43. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +14 -27
  44. flwr/cli/new/templates/app/code/task.pytorch_legacy_api.py.tpl +111 -0
  45. flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +1 -2
  46. flwr/cli/new/templates/app/code/task.xgboost.py.tpl +67 -0
  47. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +4 -4
  48. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +2 -2
  49. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +4 -4
  50. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  51. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +2 -2
  52. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  53. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
  54. flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +53 -0
  55. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  56. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
  57. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +61 -0
  58. flwr/cli/pull.py +100 -0
  59. flwr/cli/run/run.py +9 -13
  60. flwr/cli/stop.py +7 -4
  61. flwr/cli/utils.py +36 -8
  62. flwr/client/grpc_rere_client/connection.py +1 -12
  63. flwr/client/rest_client/connection.py +3 -0
  64. flwr/clientapp/__init__.py +10 -0
  65. flwr/clientapp/mod/__init__.py +29 -0
  66. flwr/clientapp/mod/centraldp_mods.py +248 -0
  67. flwr/clientapp/mod/localdp_mod.py +169 -0
  68. flwr/clientapp/typing.py +22 -0
  69. flwr/common/args.py +20 -6
  70. flwr/common/auth_plugin/__init__.py +4 -4
  71. flwr/common/auth_plugin/auth_plugin.py +7 -7
  72. flwr/common/constant.py +26 -4
  73. flwr/common/event_log_plugin/event_log_plugin.py +1 -1
  74. flwr/common/exit/__init__.py +4 -0
  75. flwr/common/exit/exit.py +8 -1
  76. flwr/common/exit/exit_code.py +30 -7
  77. flwr/common/exit/exit_handler.py +62 -0
  78. flwr/common/{exit_handlers.py → exit/signal_handler.py} +20 -37
  79. flwr/common/grpc.py +0 -11
  80. flwr/common/inflatable_utils.py +1 -1
  81. flwr/common/logger.py +1 -1
  82. flwr/common/record/typeddict.py +12 -0
  83. flwr/common/retry_invoker.py +30 -11
  84. flwr/common/telemetry.py +4 -0
  85. flwr/compat/server/app.py +2 -2
  86. flwr/proto/appio_pb2.py +25 -17
  87. flwr/proto/appio_pb2.pyi +46 -2
  88. flwr/proto/clientappio_pb2.py +3 -11
  89. flwr/proto/clientappio_pb2.pyi +0 -47
  90. flwr/proto/clientappio_pb2_grpc.py +19 -20
  91. flwr/proto/clientappio_pb2_grpc.pyi +10 -11
  92. flwr/proto/control_pb2.py +66 -0
  93. flwr/proto/{exec_pb2.pyi → control_pb2.pyi} +24 -0
  94. flwr/proto/{exec_pb2_grpc.py → control_pb2_grpc.py} +88 -54
  95. flwr/proto/control_pb2_grpc.pyi +106 -0
  96. flwr/proto/serverappio_pb2.py +2 -2
  97. flwr/proto/serverappio_pb2_grpc.py +68 -0
  98. flwr/proto/serverappio_pb2_grpc.pyi +26 -0
  99. flwr/proto/simulationio_pb2.py +4 -11
  100. flwr/proto/simulationio_pb2.pyi +0 -58
  101. flwr/proto/simulationio_pb2_grpc.py +129 -27
  102. flwr/proto/simulationio_pb2_grpc.pyi +52 -13
  103. flwr/server/app.py +142 -152
  104. flwr/server/grid/grpc_grid.py +3 -0
  105. flwr/server/grid/inmemory_grid.py +1 -0
  106. flwr/server/serverapp/app.py +157 -146
  107. flwr/server/superlink/fleet/vce/backend/raybackend.py +3 -1
  108. flwr/server/superlink/fleet/vce/vce_api.py +6 -6
  109. flwr/server/superlink/linkstate/in_memory_linkstate.py +34 -0
  110. flwr/server/superlink/linkstate/linkstate.py +2 -1
  111. flwr/server/superlink/linkstate/sqlite_linkstate.py +45 -0
  112. flwr/server/superlink/serverappio/serverappio_grpc.py +1 -1
  113. flwr/server/superlink/serverappio/serverappio_servicer.py +61 -6
  114. flwr/server/superlink/simulation/simulationio_servicer.py +97 -21
  115. flwr/serverapp/__init__.py +12 -0
  116. flwr/serverapp/exception.py +38 -0
  117. flwr/serverapp/strategy/__init__.py +64 -0
  118. flwr/serverapp/strategy/bulyan.py +238 -0
  119. flwr/serverapp/strategy/dp_adaptive_clipping.py +335 -0
  120. flwr/serverapp/strategy/dp_fixed_clipping.py +374 -0
  121. flwr/serverapp/strategy/fedadagrad.py +159 -0
  122. flwr/serverapp/strategy/fedadam.py +178 -0
  123. flwr/serverapp/strategy/fedavg.py +320 -0
  124. flwr/serverapp/strategy/fedavgm.py +198 -0
  125. flwr/serverapp/strategy/fedmedian.py +105 -0
  126. flwr/serverapp/strategy/fedopt.py +218 -0
  127. flwr/serverapp/strategy/fedprox.py +174 -0
  128. flwr/serverapp/strategy/fedtrimmedavg.py +176 -0
  129. flwr/serverapp/strategy/fedxgb_bagging.py +117 -0
  130. flwr/serverapp/strategy/fedxgb_cyclic.py +220 -0
  131. flwr/serverapp/strategy/fedyogi.py +170 -0
  132. flwr/serverapp/strategy/krum.py +112 -0
  133. flwr/serverapp/strategy/multikrum.py +247 -0
  134. flwr/serverapp/strategy/qfedavg.py +252 -0
  135. flwr/serverapp/strategy/result.py +105 -0
  136. flwr/serverapp/strategy/strategy.py +285 -0
  137. flwr/serverapp/strategy/strategy_utils.py +299 -0
  138. flwr/simulation/app.py +161 -164
  139. flwr/simulation/run_simulation.py +25 -30
  140. flwr/supercore/app_utils.py +58 -0
  141. flwr/{supernode/scheduler → supercore/cli}/__init__.py +3 -3
  142. flwr/supercore/cli/flower_superexec.py +166 -0
  143. flwr/supercore/constant.py +19 -0
  144. flwr/supercore/{scheduler → corestate}/__init__.py +3 -3
  145. flwr/supercore/corestate/corestate.py +81 -0
  146. flwr/supercore/grpc_health/__init__.py +3 -0
  147. flwr/supercore/grpc_health/health_server.py +53 -0
  148. flwr/supercore/grpc_health/simple_health_servicer.py +2 -2
  149. flwr/{superexec → supercore/superexec}/__init__.py +1 -1
  150. flwr/supercore/superexec/plugin/__init__.py +28 -0
  151. flwr/{supernode/scheduler/simple_clientapp_scheduler_plugin.py → supercore/superexec/plugin/base_exec_plugin.py} +10 -6
  152. flwr/supercore/superexec/plugin/clientapp_exec_plugin.py +28 -0
  153. flwr/supercore/{scheduler/plugin.py → superexec/plugin/exec_plugin.py} +15 -5
  154. flwr/supercore/superexec/plugin/serverapp_exec_plugin.py +28 -0
  155. flwr/supercore/superexec/plugin/simulation_exec_plugin.py +28 -0
  156. flwr/supercore/superexec/run_superexec.py +199 -0
  157. flwr/superlink/artifact_provider/__init__.py +22 -0
  158. flwr/superlink/artifact_provider/artifact_provider.py +37 -0
  159. flwr/superlink/servicer/__init__.py +15 -0
  160. flwr/superlink/servicer/control/__init__.py +22 -0
  161. flwr/{superexec/exec_event_log_interceptor.py → superlink/servicer/control/control_event_log_interceptor.py} +7 -7
  162. flwr/{superexec/exec_grpc.py → superlink/servicer/control/control_grpc.py} +27 -29
  163. flwr/{superexec/exec_license_interceptor.py → superlink/servicer/control/control_license_interceptor.py} +6 -6
  164. flwr/{superexec/exec_servicer.py → superlink/servicer/control/control_servicer.py} +127 -31
  165. flwr/{superexec/exec_user_auth_interceptor.py → superlink/servicer/control/control_user_auth_interceptor.py} +10 -10
  166. flwr/supernode/cli/flower_supernode.py +3 -0
  167. flwr/supernode/cli/flwr_clientapp.py +18 -21
  168. flwr/supernode/nodestate/in_memory_nodestate.py +2 -2
  169. flwr/supernode/nodestate/nodestate.py +3 -59
  170. flwr/supernode/runtime/run_clientapp.py +39 -102
  171. flwr/supernode/servicer/clientappio/clientappio_servicer.py +10 -17
  172. flwr/supernode/start_client_internal.py +35 -76
  173. {flwr-1.20.0.dist-info → flwr-1.22.0.dist-info}/METADATA +9 -18
  174. {flwr-1.20.0.dist-info → flwr-1.22.0.dist-info}/RECORD +176 -128
  175. {flwr-1.20.0.dist-info → flwr-1.22.0.dist-info}/entry_points.txt +1 -0
  176. flwr/proto/exec_pb2.py +0 -62
  177. flwr/proto/exec_pb2_grpc.pyi +0 -93
  178. flwr/superexec/app.py +0 -45
  179. flwr/superexec/deployment.py +0 -191
  180. flwr/superexec/executor.py +0 -100
  181. flwr/superexec/simulation.py +0 -129
  182. {flwr-1.20.0.dist-info → flwr-1.22.0.dist-info}/WHEEL +0 -0
flwr/cli/pull.py ADDED
@@ -0,0 +1,100 @@
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 command line interface `pull` command."""
16
+
17
+
18
+ from pathlib import Path
19
+ from typing import Annotated, Optional
20
+
21
+ import typer
22
+
23
+ from flwr.cli.config_utils import (
24
+ exit_if_no_address,
25
+ load_and_validate,
26
+ process_loaded_project_config,
27
+ validate_federation_in_project_config,
28
+ )
29
+ from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
30
+ from flwr.common.constant import FAB_CONFIG_FILE
31
+ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
32
+ PullArtifactsRequest,
33
+ PullArtifactsResponse,
34
+ )
35
+ from flwr.proto.control_pb2_grpc import ControlStub
36
+
37
+ from .utils import flwr_cli_grpc_exc_handler, init_channel, try_obtain_cli_auth_plugin
38
+
39
+
40
+ def pull( # pylint: disable=R0914
41
+ run_id: Annotated[
42
+ int,
43
+ typer.Option(
44
+ "--run-id",
45
+ help="Run ID to pull artifacts from.",
46
+ ),
47
+ ],
48
+ app: Annotated[
49
+ Path,
50
+ typer.Argument(help="Path of the Flower App to run."),
51
+ ] = Path("."),
52
+ federation: Annotated[
53
+ Optional[str],
54
+ typer.Argument(help="Name of the federation."),
55
+ ] = None,
56
+ federation_config_overrides: Annotated[
57
+ Optional[list[str]],
58
+ typer.Option(
59
+ "--federation-config",
60
+ help=FEDERATION_CONFIG_HELP_MESSAGE,
61
+ ),
62
+ ] = None,
63
+ ) -> None:
64
+ """Pull artifacts from a Flower run."""
65
+ typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
66
+
67
+ pyproject_path = app / FAB_CONFIG_FILE if app else None
68
+ config, errors, warnings = load_and_validate(path=pyproject_path)
69
+ config = process_loaded_project_config(config, errors, warnings)
70
+ federation, federation_config = validate_federation_in_project_config(
71
+ federation, config, federation_config_overrides
72
+ )
73
+ exit_if_no_address(federation_config, "pull")
74
+ channel = None
75
+ try:
76
+
77
+ auth_plugin = try_obtain_cli_auth_plugin(app, federation, federation_config)
78
+ channel = init_channel(app, federation_config, auth_plugin)
79
+ stub = ControlStub(channel)
80
+ with flwr_cli_grpc_exc_handler():
81
+ res: PullArtifactsResponse = stub.PullArtifacts(
82
+ PullArtifactsRequest(run_id=run_id)
83
+ )
84
+
85
+ if not res.url:
86
+ typer.secho(
87
+ f"❌ A download URL for artifacts from run {run_id} couldn't be "
88
+ "obtained.",
89
+ fg=typer.colors.RED,
90
+ bold=True,
91
+ )
92
+ raise typer.Exit(code=1)
93
+
94
+ typer.secho(
95
+ f"✅ Artifacts for run {run_id} can be downloaded from: {res.url}",
96
+ fg=typer.colors.GREEN,
97
+ )
98
+ finally:
99
+ if channel:
100
+ channel.close()
flwr/cli/run/run.py CHANGED
@@ -30,7 +30,7 @@ from flwr.cli.config_utils import (
30
30
  process_loaded_project_config,
31
31
  validate_federation_in_project_config,
32
32
  )
33
- from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
33
+ from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE, RUN_CONFIG_HELP_MESSAGE
34
34
  from flwr.common.config import (
35
35
  flatten_dict,
36
36
  get_metadata_from_config,
@@ -41,8 +41,8 @@ from flwr.common.constant import CliOutputFormat
41
41
  from flwr.common.logger import print_json_error, redirect_output, restore_output
42
42
  from flwr.common.serde import config_record_to_proto, fab_to_proto, user_config_to_proto
43
43
  from flwr.common.typing import Fab
44
- from flwr.proto.exec_pb2 import StartRunRequest # pylint: disable=E0611
45
- from flwr.proto.exec_pb2_grpc import ExecStub
44
+ from flwr.proto.control_pb2 import StartRunRequest # pylint: disable=E0611
45
+ from flwr.proto.control_pb2_grpc import ControlStub
46
46
 
47
47
  from ..log import start_stream
48
48
  from ..utils import flwr_cli_grpc_exc_handler, init_channel, try_obtain_cli_auth_plugin
@@ -65,11 +65,7 @@ def run(
65
65
  typer.Option(
66
66
  "--run-config",
67
67
  "-c",
68
- help="Override run configuration values in the format:\n\n"
69
- "`--run-config 'key1=value1 key2=value2' --run-config 'key3=value3'`\n\n"
70
- "Values can be of any type supported in TOML, such as bool, int, "
71
- "float, or string. Ensure that the keys (`key1`, `key2`, `key3` "
72
- "in this example) exist in `pyproject.toml` for proper overriding.",
68
+ help=RUN_CONFIG_HELP_MESSAGE,
73
69
  ),
74
70
  ] = None,
75
71
  federation_config_overrides: Annotated[
@@ -112,7 +108,7 @@ def run(
112
108
  )
113
109
 
114
110
  if "address" in federation_config:
115
- _run_with_exec_api(
111
+ _run_with_control_api(
116
112
  app,
117
113
  federation,
118
114
  federation_config,
@@ -121,7 +117,7 @@ def run(
121
117
  output_format,
122
118
  )
123
119
  else:
124
- _run_without_exec_api(
120
+ _run_without_control_api(
125
121
  app, federation_config, run_config_overrides, federation
126
122
  )
127
123
  except (typer.Exit, Exception) as err: # pylint: disable=broad-except
@@ -142,7 +138,7 @@ def run(
142
138
 
143
139
 
144
140
  # pylint: disable-next=R0913, R0914, R0917
145
- def _run_with_exec_api(
141
+ def _run_with_control_api(
146
142
  app: Path,
147
143
  federation: str,
148
144
  federation_config: dict[str, Any],
@@ -154,7 +150,7 @@ def _run_with_exec_api(
154
150
  try:
155
151
  auth_plugin = try_obtain_cli_auth_plugin(app, federation, federation_config)
156
152
  channel = init_channel(app, federation_config, auth_plugin)
157
- stub = ExecStub(channel)
153
+ stub = ControlStub(channel)
158
154
 
159
155
  fab_bytes, fab_hash, config = build_fab(app)
160
156
  fab_id, fab_version = get_metadata_from_config(config)
@@ -203,7 +199,7 @@ def _run_with_exec_api(
203
199
  channel.close()
204
200
 
205
201
 
206
- def _run_without_exec_api(
202
+ def _run_without_control_api(
207
203
  app: Optional[Path],
208
204
  federation_config: dict[str, Any],
209
205
  config_overrides: Optional[list[str]],
flwr/cli/stop.py CHANGED
@@ -32,8 +32,11 @@ from flwr.cli.config_utils import (
32
32
  from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
33
33
  from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
34
34
  from flwr.common.logger import print_json_error, redirect_output, restore_output
35
- from flwr.proto.exec_pb2 import StopRunRequest, StopRunResponse # pylint: disable=E0611
36
- from flwr.proto.exec_pb2_grpc import ExecStub
35
+ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
36
+ StopRunRequest,
37
+ StopRunResponse,
38
+ )
39
+ from flwr.proto.control_pb2_grpc import ControlStub
37
40
 
38
41
  from .utils import flwr_cli_grpc_exc_handler, init_channel, try_obtain_cli_auth_plugin
39
42
 
@@ -88,7 +91,7 @@ def stop( # pylint: disable=R0914
88
91
  try:
89
92
  auth_plugin = try_obtain_cli_auth_plugin(app, federation, federation_config)
90
93
  channel = init_channel(app, federation_config, auth_plugin)
91
- stub = ExecStub(channel) # pylint: disable=unused-variable # noqa: F841
94
+ stub = ControlStub(channel) # pylint: disable=unused-variable # noqa: F841
92
95
 
93
96
  typer.secho(f"✋ Stopping run ID {run_id}...", fg=typer.colors.GREEN)
94
97
  _stop_run(stub=stub, run_id=run_id, output_format=output_format)
@@ -120,7 +123,7 @@ def stop( # pylint: disable=R0914
120
123
  captured_output.close()
121
124
 
122
125
 
123
- def _stop_run(stub: ExecStub, run_id: int, output_format: str) -> None:
126
+ def _stop_run(stub: ControlStub, run_id: int, output_format: str) -> None:
124
127
  """Stop a run."""
125
128
  with flwr_cli_grpc_exc_handler():
126
129
  response: StopRunResponse = stub.StopRun(request=StopRunRequest(run_id=run_id))
flwr/cli/utils.py CHANGED
@@ -32,6 +32,9 @@ from flwr.common.constant import (
32
32
  AUTH_TYPE_JSON_KEY,
33
33
  CREDENTIALS_DIR,
34
34
  FLWR_DIR,
35
+ NO_ARTIFACT_PROVIDER_MESSAGE,
36
+ NO_USER_AUTH_MESSAGE,
37
+ PULL_UNFINISHED_RUN_MESSAGE,
35
38
  RUN_ID_NOT_FOUND_MESSAGE,
36
39
  )
37
40
  from flwr.common.grpc import (
@@ -259,7 +262,7 @@ def try_obtain_cli_auth_plugin(
259
262
  def init_channel(
260
263
  app: Path, federation_config: dict[str, Any], auth_plugin: Optional[CliAuthPlugin]
261
264
  ) -> grpc.Channel:
262
- """Initialize gRPC channel to the Exec API."""
265
+ """Initialize gRPC channel to the Control API."""
263
266
  insecure, root_certificates_bytes = validate_certificate_in_federation_config(
264
267
  app, federation_config
265
268
  )
@@ -312,11 +315,27 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]:
312
315
  )
313
316
  raise typer.Exit(code=1) from None
314
317
  if e.code() == grpc.StatusCode.UNIMPLEMENTED:
315
- typer.secho(
316
- "❌ User authentication is not enabled on this SuperLink.",
317
- fg=typer.colors.RED,
318
- bold=True,
319
- )
318
+ if e.details() == NO_USER_AUTH_MESSAGE: # pylint: disable=E1101
319
+ typer.secho(
320
+ "❌ User authentication is not enabled on this SuperLink.",
321
+ fg=typer.colors.RED,
322
+ bold=True,
323
+ )
324
+ elif e.details() == NO_ARTIFACT_PROVIDER_MESSAGE: # pylint: disable=E1101
325
+ typer.secho(
326
+ "❌ The SuperLink does not support `flwr pull` command.",
327
+ fg=typer.colors.RED,
328
+ bold=True,
329
+ )
330
+ else:
331
+ typer.secho(
332
+ "❌ The SuperLink cannot process this request. Please verify that "
333
+ "you set the address to its Control API endpoint correctly in your "
334
+ "`pyproject.toml`, and ensure that the Flower versions used by "
335
+ "the CLI and SuperLink are compatible.",
336
+ fg=typer.colors.RED,
337
+ bold=True,
338
+ )
320
339
  raise typer.Exit(code=1) from None
321
340
  if e.code() == grpc.StatusCode.PERMISSION_DENIED:
322
341
  typer.secho(
@@ -324,7 +343,7 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]:
324
343
  fg=typer.colors.RED,
325
344
  bold=True,
326
345
  )
327
- # pylint: disable=E1101
346
+ # pylint: disable-next=E1101
328
347
  typer.secho(e.details(), fg=typer.colors.RED, bold=True)
329
348
  raise typer.Exit(code=1) from None
330
349
  if e.code() == grpc.StatusCode.UNAVAILABLE:
@@ -337,7 +356,7 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]:
337
356
  raise typer.Exit(code=1) from None
338
357
  if (
339
358
  e.code() == grpc.StatusCode.NOT_FOUND
340
- and e.details() == RUN_ID_NOT_FOUND_MESSAGE
359
+ and e.details() == RUN_ID_NOT_FOUND_MESSAGE # pylint: disable=E1101
341
360
  ):
342
361
  typer.secho(
343
362
  "❌ Run ID not found.",
@@ -345,4 +364,13 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]:
345
364
  bold=True,
346
365
  )
347
366
  raise typer.Exit(code=1) from None
367
+ if e.code() == grpc.StatusCode.FAILED_PRECONDITION:
368
+ if e.details() == PULL_UNFINISHED_RUN_MESSAGE: # pylint: disable=E1101
369
+ typer.secho(
370
+ "❌ Run is not finished yet. Artifacts can only be pulled after "
371
+ "the run is finished. You can check the run status with `flwr ls`.",
372
+ fg=typer.colors.RED,
373
+ bold=True,
374
+ )
375
+ raise typer.Exit(code=1) from None
348
376
  raise
@@ -40,7 +40,7 @@ from flwr.common.secure_aggregation.crypto.symmetric_encryption import (
40
40
  generate_key_pairs,
41
41
  )
42
42
  from flwr.common.serde import message_from_proto, message_to_proto, run_from_proto
43
- from flwr.common.typing import Fab, Run, RunNotRunningException
43
+ from flwr.common.typing import Fab, Run
44
44
  from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
45
45
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
46
46
  CreateNodeRequest,
@@ -157,17 +157,6 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
157
157
  stub = adapter_cls(channel)
158
158
  node: Optional[Node] = None
159
159
 
160
- def _should_giveup_fn(e: Exception) -> bool:
161
- if e.code() == grpc.StatusCode.PERMISSION_DENIED: # type: ignore
162
- raise RunNotRunningException
163
- if e.code() == grpc.StatusCode.UNAVAILABLE: # type: ignore
164
- return False
165
- return True
166
-
167
- # Restrict retries to cases where the status code is UNAVAILABLE
168
- # If the status code is PERMISSION_DENIED, additionally raise RunNotRunningException
169
- retry_invoker.should_giveup = _should_giveup_fn
170
-
171
160
  # Wrap stub
172
161
  _wrap_stub(stub, retry_invoker)
173
162
  ###########################################################################
@@ -176,6 +176,9 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
176
176
  # Shared variables for inner functions
177
177
  node: Optional[Node] = None
178
178
 
179
+ # Remove should_giveup from RetryInvoker as REST does not support gRPC status codes
180
+ retry_invoker.should_giveup = None
181
+
179
182
  ###########################################################################
180
183
  # heartbeat/create_node/delete_node/receive/send/get_run functions
181
184
  ###########################################################################
@@ -13,3 +13,13 @@
13
13
  # limitations under the License.
14
14
  # ==============================================================================
15
15
  """Public Flower ClientApp APIs."""
16
+
17
+
18
+ from flwr.client.client_app import ClientApp
19
+
20
+ from . import mod
21
+
22
+ __all__ = [
23
+ "ClientApp",
24
+ "mod",
25
+ ]
@@ -0,0 +1,29 @@
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 Built-in Mods."""
16
+
17
+
18
+ from flwr.client.mod.comms_mods import arrays_size_mod, message_size_mod
19
+
20
+ from .centraldp_mods import adaptiveclipping_mod, fixedclipping_mod
21
+ from .localdp_mod import LocalDpMod
22
+
23
+ __all__ = [
24
+ "LocalDpMod",
25
+ "adaptiveclipping_mod",
26
+ "arrays_size_mod",
27
+ "fixedclipping_mod",
28
+ "message_size_mod",
29
+ ]
@@ -0,0 +1,248 @@
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
+ """Clipping modifiers for central DP with client-side clipping."""
16
+
17
+
18
+ from collections import OrderedDict
19
+ from logging import ERROR, INFO
20
+ from typing import cast
21
+
22
+ from flwr.app import Error
23
+ from flwr.clientapp.typing import ClientAppCallable
24
+ from flwr.common import (
25
+ Array,
26
+ ArrayRecord,
27
+ ConfigRecord,
28
+ Context,
29
+ Message,
30
+ MetricRecord,
31
+ log,
32
+ )
33
+ from flwr.common.constant import ErrorCode
34
+ from flwr.common.differential_privacy import (
35
+ compute_adaptive_clip_model_update,
36
+ compute_clip_model_update,
37
+ )
38
+ from flwr.common.differential_privacy_constants import KEY_CLIPPING_NORM, KEY_NORM_BIT
39
+
40
+
41
+ # pylint: disable=too-many-return-statements
42
+ def fixedclipping_mod(
43
+ msg: Message, ctxt: Context, call_next: ClientAppCallable
44
+ ) -> Message:
45
+ """Client-side fixed clipping modifier.
46
+
47
+ This mod needs to be used with the `DifferentialPrivacyClientSideFixedClipping`
48
+ server-side strategy wrapper.
49
+
50
+ The wrapper sends the clipping_norm value to the client.
51
+
52
+ This mod clips the client model updates before sending them to the server.
53
+
54
+ It operates on messages of type `MessageType.TRAIN`.
55
+
56
+ Notes
57
+ -----
58
+ Consider the order of mods when using multiple.
59
+
60
+ Typically, fixedclipping_mod should be the last to operate on params.
61
+ """
62
+ if len(msg.content.array_records) != 1:
63
+ return _handle_multi_record_err("fixedclipping_mod", msg, ArrayRecord)
64
+ if len(msg.content.config_records) != 1:
65
+ return _handle_multi_record_err("fixedclipping_mod", msg, ConfigRecord)
66
+
67
+ # Get keys in the single ConfigRecord
68
+ keys_in_config = set(next(iter(msg.content.config_records.values())).keys())
69
+ if KEY_CLIPPING_NORM not in keys_in_config:
70
+ return _handle_no_key_err("fixedclipping_mod", msg)
71
+ # Record array record communicated to client and clipping norm
72
+ original_array_record = next(iter(msg.content.array_records.values()))
73
+ clipping_norm = cast(
74
+ float, next(iter(msg.content.config_records.values()))[KEY_CLIPPING_NORM]
75
+ )
76
+
77
+ # Call inner app
78
+ out_msg = call_next(msg, ctxt)
79
+
80
+ # Check if the msg has error
81
+ if out_msg.has_error():
82
+ return out_msg
83
+
84
+ # Ensure reply has a single ArrayRecord
85
+ if len(out_msg.content.array_records) != 1:
86
+ return _handle_multi_record_err("fixedclipping_mod", out_msg, ArrayRecord)
87
+
88
+ new_array_record_key, client_to_server_arrecord = next(
89
+ iter(out_msg.content.array_records.items())
90
+ )
91
+ # Ensure keys in returned ArrayRecord match those in the one sent from server
92
+ if list(original_array_record.keys()) != list(client_to_server_arrecord.keys()):
93
+ return _handle_array_key_mismatch_err("fixedclipping_mod", out_msg)
94
+
95
+ client_to_server_ndarrays = client_to_server_arrecord.to_numpy_ndarrays()
96
+ # Clip the client update
97
+ compute_clip_model_update(
98
+ param1=client_to_server_ndarrays,
99
+ param2=original_array_record.to_numpy_ndarrays(),
100
+ clipping_norm=clipping_norm,
101
+ )
102
+
103
+ log(
104
+ INFO, "fixedclipping_mod: parameters are clipped by value: %.4f.", clipping_norm
105
+ )
106
+ # Replace outgoing ArrayRecord's Array while preserving their keys
107
+ out_msg.content.array_records[new_array_record_key] = ArrayRecord(
108
+ OrderedDict(
109
+ {
110
+ k: Array(v)
111
+ for k, v in zip(
112
+ client_to_server_arrecord.keys(), client_to_server_ndarrays
113
+ )
114
+ }
115
+ )
116
+ )
117
+ return out_msg
118
+
119
+
120
+ def adaptiveclipping_mod(
121
+ msg: Message, ctxt: Context, call_next: ClientAppCallable
122
+ ) -> Message:
123
+ """Client-side adaptive clipping modifier.
124
+
125
+ This mod needs to be used with the DifferentialPrivacyClientSideAdaptiveClipping
126
+ server-side strategy wrapper.
127
+
128
+ The wrapper sends the clipping_norm value to the client.
129
+
130
+ This mod clips the client model updates before sending them to the server.
131
+
132
+ It also sends KEY_NORM_BIT to the server for computing the new clipping value.
133
+
134
+ It operates on messages of type `MessageType.TRAIN`.
135
+
136
+ Notes
137
+ -----
138
+ Consider the order of mods when using multiple.
139
+
140
+ Typically, adaptiveclipping_mod should be the last to operate on params.
141
+ """
142
+ if len(msg.content.array_records) != 1:
143
+ return _handle_multi_record_err("adaptiveclipping_mod", msg, ArrayRecord)
144
+ if len(msg.content.config_records) != 1:
145
+ return _handle_multi_record_err("adaptiveclipping_mod", msg, ConfigRecord)
146
+
147
+ # Get keys in the single ConfigRecord
148
+ keys_in_config = set(next(iter(msg.content.config_records.values())).keys())
149
+ if KEY_CLIPPING_NORM not in keys_in_config:
150
+ return _handle_no_key_err("adaptiveclipping_mod", msg)
151
+
152
+ # Record array record communicated to client and clipping norm
153
+ original_array_record = next(iter(msg.content.array_records.values()))
154
+ clipping_norm = cast(
155
+ float, next(iter(msg.content.config_records.values()))[KEY_CLIPPING_NORM]
156
+ )
157
+
158
+ # Call inner app
159
+ out_msg = call_next(msg, ctxt)
160
+
161
+ # Check if the msg has error
162
+ if out_msg.has_error():
163
+ return out_msg
164
+
165
+ # Ensure reply has a single ArrayRecord
166
+ if len(out_msg.content.array_records) != 1:
167
+ return _handle_multi_record_err("adaptiveclipping_mod", out_msg, ArrayRecord)
168
+
169
+ # Ensure reply has a single MetricRecord
170
+ if len(out_msg.content.metric_records) != 1:
171
+ return _handle_multi_record_err("adaptiveclipping_mod", out_msg, MetricRecord)
172
+
173
+ new_array_record_key, client_to_server_arrecord = next(
174
+ iter(out_msg.content.array_records.items())
175
+ )
176
+
177
+ # Ensure keys in returned ArrayRecord match those in the one sent from server
178
+ if list(original_array_record.keys()) != list(client_to_server_arrecord.keys()):
179
+ return _handle_array_key_mismatch_err("adaptiveclipping_mod", out_msg)
180
+
181
+ client_to_server_ndarrays = client_to_server_arrecord.to_numpy_ndarrays()
182
+ # Clip the client update
183
+ norm_bit = compute_adaptive_clip_model_update(
184
+ client_to_server_ndarrays,
185
+ original_array_record.to_numpy_ndarrays(),
186
+ clipping_norm,
187
+ )
188
+ log(
189
+ INFO,
190
+ "adaptiveclipping_mod: ndarrays are clipped by value: %.4f.",
191
+ clipping_norm,
192
+ )
193
+ # Replace outgoing ArrayRecord's Array while preserving their keys
194
+ out_msg.content.array_records[new_array_record_key] = ArrayRecord(
195
+ OrderedDict(
196
+ {
197
+ k: Array(v)
198
+ for k, v in zip(
199
+ client_to_server_arrecord.keys(), client_to_server_ndarrays
200
+ )
201
+ }
202
+ )
203
+ )
204
+ # Add to the MetricRecords the norm bit (recall reply messages only contain
205
+ # one MetricRecord)
206
+ metric_record_key = list(out_msg.content.metric_records.keys())[0]
207
+ # We cast it to `int` because MetricRecord does not support `bool` values
208
+ out_msg.content.metric_records[metric_record_key][KEY_NORM_BIT] = int(norm_bit)
209
+ return out_msg
210
+
211
+
212
+ def _handle_err(msg: Message, reason: str) -> Message:
213
+ """Log and return error message."""
214
+ log(ERROR, reason)
215
+ return Message(
216
+ Error(code=ErrorCode.MOD_FAILED_PRECONDITION, reason=reason),
217
+ reply_to=msg,
218
+ )
219
+
220
+
221
+ def _handle_multi_record_err(mod_name: str, msg: Message, record_type: type) -> Message:
222
+ """Log and return multi-record error."""
223
+ cnt = sum(isinstance(_, record_type) for _ in msg.content.values())
224
+ return _handle_err(
225
+ msg,
226
+ f"{mod_name} expects exactly one {record_type.__name__}, "
227
+ f"but found {cnt} {record_type.__name__}(s).",
228
+ )
229
+
230
+
231
+ def _handle_no_key_err(mod_name: str, msg: Message) -> Message:
232
+ """Log and return no-key error."""
233
+ return _handle_err(
234
+ msg,
235
+ f"{mod_name} requires the key '{KEY_CLIPPING_NORM}' to be present in the "
236
+ "ConfigRecord, but it was not found. "
237
+ "Please ensure the `DifferentialPrivacyClientSideFixedClipping` wrapper "
238
+ "is used in the ServerApp.",
239
+ )
240
+
241
+
242
+ def _handle_array_key_mismatch_err(mod_name: str, msg: Message) -> Message:
243
+ """Create array-key-mismatch error reasons."""
244
+ return _handle_err(
245
+ msg,
246
+ f"{mod_name} expects the keys in the ArrayRecord of the reply message to match "
247
+ "those from the ArrayRecord that the ClientApp received, but they do not.",
248
+ )