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
@@ -0,0 +1,169 @@
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
+ """Local DP modifier."""
16
+
17
+
18
+ from collections import OrderedDict
19
+ from logging import INFO
20
+
21
+ import numpy as np
22
+
23
+ from flwr.clientapp.typing import ClientAppCallable
24
+ from flwr.common import Array, ArrayRecord
25
+ from flwr.common.context import Context
26
+ from flwr.common.differential_privacy import (
27
+ add_gaussian_noise_inplace,
28
+ compute_clip_model_update,
29
+ )
30
+ from flwr.common.logger import log
31
+ from flwr.common.message import Message
32
+
33
+ from .centraldp_mods import _handle_array_key_mismatch_err, _handle_multi_record_err
34
+
35
+
36
+ class LocalDpMod:
37
+ """Modifier for local differential privacy.
38
+
39
+ This mod clips the client model updates and
40
+ adds noise to the params before sending them to the server.
41
+
42
+ It operates on messages of type `MessageType.TRAIN`.
43
+
44
+ Parameters
45
+ ----------
46
+ clipping_norm : float
47
+ The value of the clipping norm.
48
+ sensitivity : float
49
+ The sensitivity of the client model.
50
+ epsilon : float
51
+ The privacy budget.
52
+ Smaller value of epsilon indicates a higher level of privacy protection.
53
+ delta : float
54
+ The failure probability.
55
+ The probability that the privacy mechanism
56
+ fails to provide the desired level of privacy.
57
+ A smaller value of delta indicates a stricter privacy guarantee.
58
+
59
+ Examples
60
+ --------
61
+ Create an instance of the local DP mod and add it to the client-side mods::
62
+
63
+ local_dp_mod = LocalDpMod( ... )
64
+ app = fl.client.ClientApp(mods=[local_dp_mod])
65
+ """
66
+
67
+ def __init__(
68
+ self, clipping_norm: float, sensitivity: float, epsilon: float, delta: float
69
+ ) -> None:
70
+ if clipping_norm <= 0:
71
+ raise ValueError("The clipping norm should be a positive value.")
72
+
73
+ if sensitivity < 0:
74
+ raise ValueError("The sensitivity should be a non-negative value.")
75
+
76
+ if epsilon < 0:
77
+ raise ValueError("Epsilon should be a non-negative value.")
78
+
79
+ if delta < 0:
80
+ raise ValueError("Delta should be a non-negative value.")
81
+
82
+ self.clipping_norm = clipping_norm
83
+ self.sensitivity = sensitivity
84
+ self.epsilon = epsilon
85
+ self.delta = delta
86
+
87
+ def __call__(
88
+ self, msg: Message, ctxt: Context, call_next: ClientAppCallable
89
+ ) -> Message:
90
+ """Perform local DP on the client model parameters.
91
+
92
+ Parameters
93
+ ----------
94
+ msg : Message
95
+ The message received from the ServerApp.
96
+ ctxt : Context
97
+ The context of the ClientApp.
98
+ call_next : ClientAppCallable
99
+ The callable to call the next mod (or the ClientApp) in the chain.
100
+
101
+ Returns
102
+ -------
103
+ Message
104
+ The modified message to be sent back to the server.
105
+ """
106
+ if len(msg.content.array_records) != 1:
107
+ return _handle_multi_record_err("LocalDpMod", msg, ArrayRecord)
108
+
109
+ # Record array record communicated to client and clipping norm
110
+ original_array_record = next(iter(msg.content.array_records.values()))
111
+
112
+ # Call inner app
113
+ out_msg = call_next(msg, ctxt)
114
+
115
+ # Check if the msg has error
116
+ if out_msg.has_error():
117
+ return out_msg
118
+
119
+ # Ensure reply has a single ArrayRecord
120
+ if len(out_msg.content.array_records) != 1:
121
+ return _handle_multi_record_err("LocalDpMod", out_msg, ArrayRecord)
122
+
123
+ new_array_record_key, client_to_server_arrecord = next(
124
+ iter(out_msg.content.array_records.items())
125
+ )
126
+
127
+ # Ensure keys in returned ArrayRecord match those in the one sent from server
128
+ if list(original_array_record.keys()) != list(client_to_server_arrecord.keys()):
129
+ return _handle_array_key_mismatch_err("LocalDpMod", out_msg)
130
+
131
+ client_to_server_ndarrays = client_to_server_arrecord.to_numpy_ndarrays()
132
+
133
+ # Clip the client update
134
+ compute_clip_model_update(
135
+ client_to_server_ndarrays,
136
+ original_array_record.to_numpy_ndarrays(),
137
+ self.clipping_norm,
138
+ )
139
+ log(
140
+ INFO,
141
+ "LocalDpMod: parameters are clipped by value: %.4f.",
142
+ self.clipping_norm,
143
+ )
144
+
145
+ std_dev = (
146
+ self.sensitivity * np.sqrt(2 * np.log(1.25 / self.delta)) / self.epsilon
147
+ )
148
+ add_gaussian_noise_inplace(
149
+ client_to_server_ndarrays,
150
+ std_dev,
151
+ )
152
+ log(
153
+ INFO,
154
+ "LocalDpMod: local DP noise with %.4f stddev added to parameters",
155
+ std_dev,
156
+ )
157
+
158
+ # Replace outgoing ArrayRecord's Array while preserving their keys
159
+ out_msg.content[new_array_record_key] = ArrayRecord(
160
+ OrderedDict(
161
+ {
162
+ k: Array(v)
163
+ for k, v in zip(
164
+ client_to_server_arrecord.keys(), client_to_server_ndarrays
165
+ )
166
+ }
167
+ )
168
+ )
169
+ return out_msg
@@ -0,0 +1,22 @@
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
+ """Custom types for Flower clients."""
16
+
17
+
18
+ from typing import Callable
19
+
20
+ from flwr.common import Context, Message
21
+
22
+ ClientAppCallable = Callable[[Message, Context], Message]
flwr/common/args.py CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  import argparse
19
19
  import sys
20
- from logging import DEBUG, ERROR, WARN
20
+ from logging import DEBUG, ERROR, INFO, WARN
21
21
  from os.path import isfile
22
22
  from pathlib import Path
23
23
  from typing import Optional, Union
@@ -28,6 +28,12 @@ from flwr.common.logger import log
28
28
 
29
29
  def add_args_flwr_app_common(parser: argparse.ArgumentParser) -> None:
30
30
  """Add common Flower arguments for flwr-*app to the provided parser."""
31
+ parser.add_argument(
32
+ "--token",
33
+ type=str,
34
+ required=False,
35
+ help="Unique token generated by AppIo API for each app execution",
36
+ )
31
37
  parser.add_argument(
32
38
  "--flwr-dir",
33
39
  default=None,
@@ -47,6 +53,18 @@ def add_args_flwr_app_common(parser: argparse.ArgumentParser) -> None:
47
53
  "is not encrypted. By default, the server runs with HTTPS enabled. "
48
54
  "Use this flag only if you understand the risks.",
49
55
  )
56
+ parser.add_argument(
57
+ "--parent-pid",
58
+ type=int,
59
+ default=None,
60
+ help="The PID of the parent process. When set, the process will terminate "
61
+ "when the parent process exits.",
62
+ )
63
+ parser.add_argument(
64
+ "--run-once",
65
+ action="store_true",
66
+ help="This flag is deprecated and will be removed in a future release.",
67
+ )
50
68
 
51
69
 
52
70
  def try_obtain_root_certificates(
@@ -72,11 +90,7 @@ def try_obtain_root_certificates(
72
90
  else:
73
91
  # Load the certificates if provided, or load the system certificates
74
92
  if root_cert_path is None:
75
- log(
76
- WARN,
77
- "Both `--insecure` and `--root-certificates` were not set. "
78
- "Using system certificates.",
79
- )
93
+ log(INFO, "Using system certificates")
80
94
  root_certificates = None
81
95
  elif not isfile(root_cert_path):
82
96
  log(ERROR, "Path argument `--root-certificates` does not point to a file.")
@@ -16,11 +16,11 @@
16
16
 
17
17
 
18
18
  from .auth_plugin import CliAuthPlugin as CliAuthPlugin
19
- from .auth_plugin import ExecAuthPlugin as ExecAuthPlugin
20
- from .auth_plugin import ExecAuthzPlugin as ExecAuthzPlugin
19
+ from .auth_plugin import ControlAuthPlugin as ControlAuthPlugin
20
+ from .auth_plugin import ControlAuthzPlugin as ControlAuthzPlugin
21
21
 
22
22
  __all__ = [
23
23
  "CliAuthPlugin",
24
- "ExecAuthPlugin",
25
- "ExecAuthzPlugin",
24
+ "ControlAuthPlugin",
25
+ "ControlAuthzPlugin",
26
26
  ]
@@ -21,13 +21,13 @@ from pathlib import Path
21
21
  from typing import Optional, Union
22
22
 
23
23
  from flwr.common.typing import AccountInfo
24
- from flwr.proto.exec_pb2_grpc import ExecStub
24
+ from flwr.proto.control_pb2_grpc import ControlStub
25
25
 
26
26
  from ..typing import UserAuthCredentials, UserAuthLoginDetails
27
27
 
28
28
 
29
- class ExecAuthPlugin(ABC):
30
- """Abstract Flower Auth Plugin class for ExecServicer.
29
+ class ControlAuthPlugin(ABC):
30
+ """Abstract Flower Auth Plugin class for ControlServicer.
31
31
 
32
32
  Parameters
33
33
  ----------
@@ -69,8 +69,8 @@ class ExecAuthPlugin(ABC):
69
69
  """Refresh authentication tokens in the provided metadata."""
70
70
 
71
71
 
72
- class ExecAuthzPlugin(ABC): # pylint: disable=too-few-public-methods
73
- """Abstract Flower Authorization Plugin class for ExecServicer.
72
+ class ControlAuthzPlugin(ABC): # pylint: disable=too-few-public-methods
73
+ """Abstract Flower Authorization Plugin class for ControlServicer.
74
74
 
75
75
  Parameters
76
76
  ----------
@@ -103,7 +103,7 @@ class CliAuthPlugin(ABC):
103
103
  @abstractmethod
104
104
  def login(
105
105
  login_details: UserAuthLoginDetails,
106
- exec_stub: ExecStub,
106
+ control_stub: ControlStub,
107
107
  ) -> UserAuthCredentials:
108
108
  """Authenticate the user and retrieve authentication credentials.
109
109
 
@@ -111,7 +111,7 @@ class CliAuthPlugin(ABC):
111
111
  ----------
112
112
  login_details : UserAuthLoginDetails
113
113
  An object containing the user's login details.
114
- exec_stub : ExecStub
114
+ control_stub : ControlStub
115
115
  A stub for executing RPC calls to the server.
116
116
 
117
117
  Returns
flwr/common/constant.py CHANGED
@@ -35,7 +35,7 @@ CLIENTAPPIO_PORT = "9094"
35
35
  SERVERAPPIO_PORT = "9091"
36
36
  FLEETAPI_GRPC_RERE_PORT = "9092"
37
37
  FLEETAPI_PORT = "9095"
38
- EXEC_API_PORT = "9093"
38
+ CONTROL_API_PORT = "9093"
39
39
  SIMULATIONIO_PORT = "9096"
40
40
  # Octets
41
41
  SERVER_OCTET = "0.0.0.0"
@@ -51,7 +51,7 @@ FLEET_API_GRPC_BIDI_DEFAULT_ADDRESS = (
51
51
  "[::]:8080" # IPv6 to keep start_server compatible
52
52
  )
53
53
  FLEET_API_REST_DEFAULT_ADDRESS = f"{SERVER_OCTET}:{FLEETAPI_PORT}"
54
- EXEC_API_DEFAULT_SERVER_ADDRESS = f"{SERVER_OCTET}:{EXEC_API_PORT}"
54
+ CONTROL_API_DEFAULT_SERVER_ADDRESS = f"{SERVER_OCTET}:{CONTROL_API_PORT}"
55
55
  SIMULATIONIO_API_DEFAULT_SERVER_ADDRESS = f"{SERVER_OCTET}:{SIMULATIONIO_PORT}"
56
56
  SIMULATIONIO_API_DEFAULT_CLIENT_ADDRESS = f"{CLIENT_OCTET}:{SIMULATIONIO_PORT}"
57
57
 
@@ -103,7 +103,7 @@ ISOLATION_MODE_PROCESS = "process"
103
103
  # Log streaming configurations
104
104
  CONN_REFRESH_PERIOD = 60 # Stream connection refresh period
105
105
  CONN_RECONNECT_INTERVAL = 0.5 # Reconnect interval between two stream connections
106
- LOG_STREAM_INTERVAL = 0.5 # Log stream interval for `ExecServicer.StreamLogs`
106
+ LOG_STREAM_INTERVAL = 0.5 # Log stream interval for `ControlServicer.StreamLogs`
107
107
  LOG_UPLOAD_INTERVAL = 0.2 # Minimum interval between two log uploads
108
108
 
109
109
  # Retry configurations
@@ -152,8 +152,11 @@ PULL_INITIAL_BACKOFF = 1 # Initial backoff time for pulling objects
152
152
  PULL_BACKOFF_CAP = 10 # Maximum backoff time for pulling objects
153
153
 
154
154
 
155
- # ExecServicer constants
155
+ # ControlServicer constants
156
156
  RUN_ID_NOT_FOUND_MESSAGE = "Run ID not found"
157
+ NO_USER_AUTH_MESSAGE = "ControlServicer initialized without user authentication"
158
+ NO_ARTIFACT_PROVIDER_MESSAGE = "ControlServicer initialized without artifact provider"
159
+ PULL_UNFINISHED_RUN_MESSAGE = "Cannot pull artifacts for an unfinished run"
157
160
 
158
161
 
159
162
  class MessageType:
@@ -199,6 +202,7 @@ class ErrorCode:
199
202
  MESSAGE_UNAVAILABLE = 3
200
203
  REPLY_MESSAGE_UNAVAILABLE = 4
201
204
  NODE_UNAVAILABLE = 5
205
+ MOD_FAILED_PRECONDITION = 6
202
206
 
203
207
  def __new__(cls) -> ErrorCode:
204
208
  """Prevent instantiation."""
@@ -259,3 +263,21 @@ class EventLogWriterType:
259
263
  def __new__(cls) -> EventLogWriterType:
260
264
  """Prevent instantiation."""
261
265
  raise TypeError(f"{cls.__name__} cannot be instantiated.")
266
+
267
+
268
+ class ExecPluginType:
269
+ """SuperExec plugin types."""
270
+
271
+ CLIENT_APP = "clientapp"
272
+ SERVER_APP = "serverapp"
273
+ SIMULATION = "simulation"
274
+
275
+ def __new__(cls) -> ExecPluginType:
276
+ """Prevent instantiation."""
277
+ raise TypeError(f"{cls.__name__} cannot be instantiated.")
278
+
279
+ @staticmethod
280
+ def all() -> list[str]:
281
+ """Return all SuperExec plugin types."""
282
+ # Filter all constants (uppercase) of the class
283
+ return [v for k, v in vars(ExecPluginType).items() if k.isupper()]
@@ -25,7 +25,7 @@ from flwr.common.typing import AccountInfo, LogEntry
25
25
 
26
26
 
27
27
  class EventLogWriterPlugin(ABC):
28
- """Abstract Flower Event Log Writer Plugin class for ExecServicer."""
28
+ """Abstract Flower Event Log Writer Plugin class for ControlServicer."""
29
29
 
30
30
  @abstractmethod
31
31
  def __init__(self) -> None:
@@ -17,8 +17,12 @@
17
17
 
18
18
  from .exit import flwr_exit
19
19
  from .exit_code import ExitCode
20
+ from .exit_handler import add_exit_handler
21
+ from .signal_handler import register_signal_handlers
20
22
 
21
23
  __all__ = [
22
24
  "ExitCode",
25
+ "add_exit_handler",
23
26
  "flwr_exit",
27
+ "register_signal_handlers",
24
28
  ]
flwr/common/exit/exit.py CHANGED
@@ -22,11 +22,15 @@ from logging import ERROR, INFO
22
22
  from typing import Any, NoReturn
23
23
 
24
24
  from flwr.common import EventType, event
25
+ from flwr.common.version import package_version
25
26
 
26
27
  from ..logger import log
27
28
  from .exit_code import EXIT_CODE_HELP
29
+ from .exit_handler import trigger_exit_handlers
28
30
 
29
- HELP_PAGE_URL = "https://flower.ai/docs/framework/ref-exit-codes/"
31
+ HELP_PAGE_URL = (
32
+ f"https://flower.ai/docs/framework/v{package_version}/en/ref-exit-codes/"
33
+ )
30
34
 
31
35
 
32
36
  def flwr_exit(
@@ -77,6 +81,9 @@ def flwr_exit(
77
81
  # Log the exit message
78
82
  log(log_level, exit_message)
79
83
 
84
+ # Trigger exit handlers
85
+ trigger_exit_handlers()
86
+
80
87
  # Exit
81
88
  sys.exit(sys_exit_code)
82
89
 
@@ -32,19 +32,22 @@ class ExitCode:
32
32
  SUPERLINK_LICENSE_INVALID = 101
33
33
  SUPERLINK_LICENSE_MISSING = 102
34
34
  SUPERLINK_LICENSE_URL_INVALID = 103
35
+ SUPERLINK_INVALID_ARGS = 104
35
36
 
36
37
  # ServerApp-specific exit codes (200-299)
38
+ SERVERAPP_STRATEGY_PRECONDITION_UNMET = 200
39
+ SERVERAPP_EXCEPTION = 201
40
+ SERVERAPP_STRATEGY_AGGREGATION_ERROR = 202
37
41
 
38
42
  # SuperNode-specific exit codes (300-399)
39
43
  SUPERNODE_REST_ADDRESS_INVALID = 300
40
44
  SUPERNODE_NODE_AUTH_KEYS_REQUIRED = 301
41
45
  SUPERNODE_NODE_AUTH_KEYS_INVALID = 302
42
46
 
43
- # ClientApp-specific exit codes (400-499)
47
+ # SuperExec-specific exit codes (400-499)
48
+ SUPEREXEC_INVALID_PLUGIN_CONFIG = 400
44
49
 
45
- # Simulation-specific exit codes (500-599)
46
-
47
- # Common exit codes (600-)
50
+ # Common exit codes (600-699)
48
51
  COMMON_ADDRESS_INVALID = 600
49
52
  COMMON_MISSING_EXTRA_REST = 601
50
53
  COMMON_TLS_NOT_SUPPORTED = 602
@@ -75,7 +78,25 @@ EXIT_CODE_HELP = {
75
78
  "The license URL is invalid. Please ensure that the `FLWR_LICENSE_URL` "
76
79
  "environment variable is set to a valid URL."
77
80
  ),
81
+ ExitCode.SUPERLINK_INVALID_ARGS: (
82
+ "Invalid arguments provided to SuperLink. Use `--help` check for the correct "
83
+ "usage. Alternatively, check the documentation."
84
+ ),
78
85
  # ServerApp-specific exit codes (200-299)
86
+ ExitCode.SERVERAPP_STRATEGY_PRECONDITION_UNMET: (
87
+ "The strategy received replies that cannot be aggregated. Please ensure all "
88
+ "replies returned by ClientApps have one `ArrayRecord` (none when replies are "
89
+ "from a round of federated evaluation, i.e. when message type is "
90
+ "`MessageType.EVALUATE`) and one `MetricRecord`. The records in all replies "
91
+ "must use identical keys. In addition, if the strategy expects a key to "
92
+ "perform weighted average (e.g. in FedAvg) please ensure the returned "
93
+ "MetricRecord from ClientApps do include this key."
94
+ ),
95
+ ExitCode.SERVERAPP_EXCEPTION: "An unhandled exception occurred in the ServerApp.",
96
+ ExitCode.SERVERAPP_STRATEGY_AGGREGATION_ERROR: (
97
+ "The strategy encountered an error during aggregation. Please check the logs "
98
+ "for more details."
99
+ ),
79
100
  # SuperNode-specific exit codes (300-399)
80
101
  ExitCode.SUPERNODE_REST_ADDRESS_INVALID: (
81
102
  "When using the REST API, please provide `https://` or "
@@ -91,9 +112,11 @@ EXIT_CODE_HELP = {
91
112
  "Please ensure that the file path points to a valid private/public key "
92
113
  "file and try again."
93
114
  ),
94
- # ClientApp-specific exit codes (400-499)
95
- # Simulation-specific exit codes (500-599)
96
- # Common exit codes (600-)
115
+ # SuperExec-specific exit codes (400-499)
116
+ ExitCode.SUPEREXEC_INVALID_PLUGIN_CONFIG: (
117
+ "The YAML configuration for the SuperExec plugin is invalid."
118
+ ),
119
+ # Common exit codes (600-699)
97
120
  ExitCode.COMMON_ADDRESS_INVALID: (
98
121
  "Please provide a valid URL, IPv4 or IPv6 address."
99
122
  ),
@@ -0,0 +1,62 @@
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
+ """Common function to register exit handlers."""
16
+
17
+
18
+ import signal
19
+ import threading
20
+ from typing import Callable
21
+
22
+ from .exit_code import ExitCode
23
+
24
+ SIGNAL_TO_EXIT_CODE: dict[int, int] = {
25
+ signal.SIGINT: ExitCode.GRACEFUL_EXIT_SIGINT,
26
+ signal.SIGTERM: ExitCode.GRACEFUL_EXIT_SIGTERM,
27
+ }
28
+ registered_exit_handlers: list[Callable[[], None]] = []
29
+ _lock_handlers = threading.Lock()
30
+
31
+ # SIGQUIT is not available on Windows
32
+ if hasattr(signal, "SIGQUIT"):
33
+ SIGNAL_TO_EXIT_CODE[signal.SIGQUIT] = ExitCode.GRACEFUL_EXIT_SIGQUIT
34
+
35
+
36
+ def add_exit_handler(exit_handler: Callable[[], None]) -> None:
37
+ """Add an exit handler to be called on graceful exit.
38
+
39
+ This function allows you to register additional exit handlers
40
+ that will be executed when `flwr_exit` is called.
41
+
42
+ Parameters
43
+ ----------
44
+ exit_handler : Callable[[], None]
45
+ A callable that takes no arguments and performs cleanup or
46
+ other actions before the application exits.
47
+
48
+ Notes
49
+ -----
50
+ The registered exit handlers will be called in LIFO order, i.e.,
51
+ the last registered handler will be the first to be called.
52
+ """
53
+ with _lock_handlers:
54
+ registered_exit_handlers.append(exit_handler)
55
+
56
+
57
+ def trigger_exit_handlers() -> None:
58
+ """Trigger all registered exit handlers in LIFO order."""
59
+ with _lock_handlers:
60
+ for handler in reversed(registered_exit_handlers):
61
+ handler()
62
+ registered_exit_handlers.clear()
@@ -12,7 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  # ==============================================================================
15
- """Common function to register exit handlers for server and client."""
15
+ """Common function to register signal handlers."""
16
16
 
17
17
 
18
18
  import signal
@@ -24,20 +24,21 @@ from grpc import Server
24
24
 
25
25
  from flwr.common.telemetry import EventType
26
26
 
27
- from .exit import ExitCode, flwr_exit
27
+ from .exit import flwr_exit
28
+ from .exit_code import ExitCode
29
+ from .exit_handler import add_exit_handler
28
30
 
29
31
  SIGNAL_TO_EXIT_CODE: dict[int, int] = {
30
32
  signal.SIGINT: ExitCode.GRACEFUL_EXIT_SIGINT,
31
33
  signal.SIGTERM: ExitCode.GRACEFUL_EXIT_SIGTERM,
32
34
  }
33
- registered_exit_handlers: list[Callable[[], None]] = []
34
35
 
35
36
  # SIGQUIT is not available on Windows
36
37
  if hasattr(signal, "SIGQUIT"):
37
38
  SIGNAL_TO_EXIT_CODE[signal.SIGQUIT] = ExitCode.GRACEFUL_EXIT_SIGQUIT
38
39
 
39
40
 
40
- def register_exit_handlers(
41
+ def register_signal_handlers(
41
42
  event_type: EventType,
42
43
  exit_message: Optional[str] = None,
43
44
  grpc_servers: Optional[list[Server]] = None,
@@ -63,7 +64,21 @@ def register_exit_handlers(
63
64
  Additional exit handlers can be added using `add_exit_handler`.
64
65
  """
65
66
  default_handlers: dict[int, Callable[[int, FrameType], None]] = {}
66
- registered_exit_handlers.extend(exit_handlers or [])
67
+
68
+ def _wait_to_stop() -> None:
69
+ if grpc_servers is not None:
70
+ for grpc_server in grpc_servers:
71
+ grpc_server.stop(grace=1)
72
+
73
+ if bckg_threads is not None:
74
+ for bckg_thread in bckg_threads:
75
+ bckg_thread.join()
76
+
77
+ # Ensure that `_wait_to_stop` is the last handler called on exit
78
+ add_exit_handler(_wait_to_stop)
79
+
80
+ for handler in exit_handlers or []:
81
+ add_exit_handler(handler)
67
82
 
68
83
  def graceful_exit_handler(signalnum: int, _frame: FrameType) -> None:
69
84
  """Exit handler to be registered with `signal.signal`.
@@ -74,17 +89,6 @@ def register_exit_handlers(
74
89
  # Reset to default handler
75
90
  signal.signal(signalnum, default_handlers[signalnum]) # type: ignore
76
91
 
77
- for handler in registered_exit_handlers:
78
- handler()
79
-
80
- if grpc_servers is not None:
81
- for grpc_server in grpc_servers:
82
- grpc_server.stop(grace=1)
83
-
84
- if bckg_threads is not None:
85
- for bckg_thread in bckg_threads:
86
- bckg_thread.join()
87
-
88
92
  # Setup things for graceful exit
89
93
  flwr_exit(
90
94
  code=SIGNAL_TO_EXIT_CODE[signalnum],
@@ -96,24 +100,3 @@ def register_exit_handlers(
96
100
  for sig in SIGNAL_TO_EXIT_CODE:
97
101
  default_handler = signal.signal(sig, graceful_exit_handler) # type: ignore
98
102
  default_handlers[sig] = default_handler # type: ignore
99
-
100
-
101
- def add_exit_handler(exit_handler: Callable[[], None]) -> None:
102
- """Add an exit handler to be called on graceful exit.
103
-
104
- This function allows you to register additional exit handlers
105
- that will be executed when the application exits gracefully,
106
- if `register_exit_handlers` was called.
107
-
108
- Parameters
109
- ----------
110
- exit_handler : Callable[[], None]
111
- A callable that takes no arguments and performs cleanup or
112
- other actions before the application exits.
113
-
114
- Notes
115
- -----
116
- This method is not thread-safe, and it allows you to add the
117
- same exit handler multiple times.
118
- """
119
- registered_exit_handlers.append(exit_handler)