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.
- flwr/__init__.py +4 -1
- flwr/app/__init__.py +28 -0
- flwr/app/exception.py +31 -0
- flwr/cli/app.py +2 -0
- flwr/cli/auth_plugin/oidc_cli_plugin.py +4 -4
- flwr/cli/cli_user_auth_interceptor.py +1 -1
- flwr/cli/config_utils.py +3 -3
- flwr/cli/constant.py +25 -8
- flwr/cli/log.py +9 -9
- flwr/cli/login/login.py +3 -3
- flwr/cli/ls.py +5 -5
- flwr/cli/new/new.py +15 -2
- flwr/cli/new/templates/app/README.flowertune.md.tpl +1 -1
- flwr/cli/new/templates/app/code/__init__.pytorch_legacy_api.py.tpl +1 -0
- flwr/cli/new/templates/app/code/client.baseline.py.tpl +64 -47
- flwr/cli/new/templates/app/code/client.huggingface.py.tpl +68 -30
- flwr/cli/new/templates/app/code/client.jax.py.tpl +63 -42
- flwr/cli/new/templates/app/code/client.mlx.py.tpl +80 -51
- flwr/cli/new/templates/app/code/client.numpy.py.tpl +36 -13
- flwr/cli/new/templates/app/code/client.pytorch.py.tpl +71 -46
- flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +55 -0
- flwr/cli/new/templates/app/code/client.sklearn.py.tpl +75 -30
- flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +69 -44
- flwr/cli/new/templates/app/code/client.xgboost.py.tpl +110 -0
- flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +56 -90
- flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +1 -23
- flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +37 -58
- flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +39 -44
- flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -14
- flwr/cli/new/templates/app/code/server.baseline.py.tpl +27 -29
- flwr/cli/new/templates/app/code/server.huggingface.py.tpl +23 -19
- flwr/cli/new/templates/app/code/server.jax.py.tpl +27 -14
- flwr/cli/new/templates/app/code/server.mlx.py.tpl +29 -19
- flwr/cli/new/templates/app/code/server.numpy.py.tpl +30 -17
- flwr/cli/new/templates/app/code/server.pytorch.py.tpl +36 -26
- flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +31 -0
- flwr/cli/new/templates/app/code/server.sklearn.py.tpl +29 -21
- flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +28 -19
- flwr/cli/new/templates/app/code/server.xgboost.py.tpl +56 -0
- flwr/cli/new/templates/app/code/task.huggingface.py.tpl +16 -20
- flwr/cli/new/templates/app/code/task.jax.py.tpl +1 -1
- flwr/cli/new/templates/app/code/task.numpy.py.tpl +1 -1
- flwr/cli/new/templates/app/code/task.pytorch.py.tpl +14 -27
- flwr/cli/new/templates/app/code/task.pytorch_legacy_api.py.tpl +111 -0
- flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +1 -2
- flwr/cli/new/templates/app/code/task.xgboost.py.tpl +67 -0
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +4 -4
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +2 -2
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +4 -4
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +2 -2
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
- flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +53 -0
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +61 -0
- flwr/cli/pull.py +100 -0
- flwr/cli/run/run.py +9 -13
- flwr/cli/stop.py +7 -4
- flwr/cli/utils.py +36 -8
- flwr/client/grpc_rere_client/connection.py +1 -12
- flwr/client/rest_client/connection.py +3 -0
- flwr/clientapp/__init__.py +10 -0
- flwr/clientapp/mod/__init__.py +29 -0
- flwr/clientapp/mod/centraldp_mods.py +248 -0
- flwr/clientapp/mod/localdp_mod.py +169 -0
- flwr/clientapp/typing.py +22 -0
- flwr/common/args.py +20 -6
- flwr/common/auth_plugin/__init__.py +4 -4
- flwr/common/auth_plugin/auth_plugin.py +7 -7
- flwr/common/constant.py +26 -4
- flwr/common/event_log_plugin/event_log_plugin.py +1 -1
- flwr/common/exit/__init__.py +4 -0
- flwr/common/exit/exit.py +8 -1
- flwr/common/exit/exit_code.py +30 -7
- flwr/common/exit/exit_handler.py +62 -0
- flwr/common/{exit_handlers.py → exit/signal_handler.py} +20 -37
- flwr/common/grpc.py +0 -11
- flwr/common/inflatable_utils.py +1 -1
- flwr/common/logger.py +1 -1
- flwr/common/record/typeddict.py +12 -0
- flwr/common/retry_invoker.py +30 -11
- flwr/common/telemetry.py +4 -0
- flwr/compat/server/app.py +2 -2
- flwr/proto/appio_pb2.py +25 -17
- flwr/proto/appio_pb2.pyi +46 -2
- flwr/proto/clientappio_pb2.py +3 -11
- flwr/proto/clientappio_pb2.pyi +0 -47
- flwr/proto/clientappio_pb2_grpc.py +19 -20
- flwr/proto/clientappio_pb2_grpc.pyi +10 -11
- flwr/proto/control_pb2.py +66 -0
- flwr/proto/{exec_pb2.pyi → control_pb2.pyi} +24 -0
- flwr/proto/{exec_pb2_grpc.py → control_pb2_grpc.py} +88 -54
- flwr/proto/control_pb2_grpc.pyi +106 -0
- flwr/proto/serverappio_pb2.py +2 -2
- flwr/proto/serverappio_pb2_grpc.py +68 -0
- flwr/proto/serverappio_pb2_grpc.pyi +26 -0
- flwr/proto/simulationio_pb2.py +4 -11
- flwr/proto/simulationio_pb2.pyi +0 -58
- flwr/proto/simulationio_pb2_grpc.py +129 -27
- flwr/proto/simulationio_pb2_grpc.pyi +52 -13
- flwr/server/app.py +142 -152
- flwr/server/grid/grpc_grid.py +3 -0
- flwr/server/grid/inmemory_grid.py +1 -0
- flwr/server/serverapp/app.py +157 -146
- flwr/server/superlink/fleet/vce/backend/raybackend.py +3 -1
- flwr/server/superlink/fleet/vce/vce_api.py +6 -6
- flwr/server/superlink/linkstate/in_memory_linkstate.py +34 -0
- flwr/server/superlink/linkstate/linkstate.py +2 -1
- flwr/server/superlink/linkstate/sqlite_linkstate.py +45 -0
- flwr/server/superlink/serverappio/serverappio_grpc.py +1 -1
- flwr/server/superlink/serverappio/serverappio_servicer.py +61 -6
- flwr/server/superlink/simulation/simulationio_servicer.py +97 -21
- flwr/serverapp/__init__.py +12 -0
- flwr/serverapp/exception.py +38 -0
- flwr/serverapp/strategy/__init__.py +64 -0
- flwr/serverapp/strategy/bulyan.py +238 -0
- flwr/serverapp/strategy/dp_adaptive_clipping.py +335 -0
- flwr/serverapp/strategy/dp_fixed_clipping.py +374 -0
- flwr/serverapp/strategy/fedadagrad.py +159 -0
- flwr/serverapp/strategy/fedadam.py +178 -0
- flwr/serverapp/strategy/fedavg.py +320 -0
- flwr/serverapp/strategy/fedavgm.py +198 -0
- flwr/serverapp/strategy/fedmedian.py +105 -0
- flwr/serverapp/strategy/fedopt.py +218 -0
- flwr/serverapp/strategy/fedprox.py +174 -0
- flwr/serverapp/strategy/fedtrimmedavg.py +176 -0
- flwr/serverapp/strategy/fedxgb_bagging.py +117 -0
- flwr/serverapp/strategy/fedxgb_cyclic.py +220 -0
- flwr/serverapp/strategy/fedyogi.py +170 -0
- flwr/serverapp/strategy/krum.py +112 -0
- flwr/serverapp/strategy/multikrum.py +247 -0
- flwr/serverapp/strategy/qfedavg.py +252 -0
- flwr/serverapp/strategy/result.py +105 -0
- flwr/serverapp/strategy/strategy.py +285 -0
- flwr/serverapp/strategy/strategy_utils.py +299 -0
- flwr/simulation/app.py +161 -164
- flwr/simulation/run_simulation.py +25 -30
- flwr/supercore/app_utils.py +58 -0
- flwr/{supernode/scheduler → supercore/cli}/__init__.py +3 -3
- flwr/supercore/cli/flower_superexec.py +166 -0
- flwr/supercore/constant.py +19 -0
- flwr/supercore/{scheduler → corestate}/__init__.py +3 -3
- flwr/supercore/corestate/corestate.py +81 -0
- flwr/supercore/grpc_health/__init__.py +3 -0
- flwr/supercore/grpc_health/health_server.py +53 -0
- flwr/supercore/grpc_health/simple_health_servicer.py +2 -2
- flwr/{superexec → supercore/superexec}/__init__.py +1 -1
- flwr/supercore/superexec/plugin/__init__.py +28 -0
- flwr/{supernode/scheduler/simple_clientapp_scheduler_plugin.py → supercore/superexec/plugin/base_exec_plugin.py} +10 -6
- flwr/supercore/superexec/plugin/clientapp_exec_plugin.py +28 -0
- flwr/supercore/{scheduler/plugin.py → superexec/plugin/exec_plugin.py} +15 -5
- flwr/supercore/superexec/plugin/serverapp_exec_plugin.py +28 -0
- flwr/supercore/superexec/plugin/simulation_exec_plugin.py +28 -0
- flwr/supercore/superexec/run_superexec.py +199 -0
- flwr/superlink/artifact_provider/__init__.py +22 -0
- flwr/superlink/artifact_provider/artifact_provider.py +37 -0
- flwr/superlink/servicer/__init__.py +15 -0
- flwr/superlink/servicer/control/__init__.py +22 -0
- flwr/{superexec/exec_event_log_interceptor.py → superlink/servicer/control/control_event_log_interceptor.py} +7 -7
- flwr/{superexec/exec_grpc.py → superlink/servicer/control/control_grpc.py} +27 -29
- flwr/{superexec/exec_license_interceptor.py → superlink/servicer/control/control_license_interceptor.py} +6 -6
- flwr/{superexec/exec_servicer.py → superlink/servicer/control/control_servicer.py} +127 -31
- flwr/{superexec/exec_user_auth_interceptor.py → superlink/servicer/control/control_user_auth_interceptor.py} +10 -10
- flwr/supernode/cli/flower_supernode.py +3 -0
- flwr/supernode/cli/flwr_clientapp.py +18 -21
- flwr/supernode/nodestate/in_memory_nodestate.py +2 -2
- flwr/supernode/nodestate/nodestate.py +3 -59
- flwr/supernode/runtime/run_clientapp.py +39 -102
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +10 -17
- flwr/supernode/start_client_internal.py +35 -76
- {flwr-1.20.0.dist-info → flwr-1.22.0.dist-info}/METADATA +9 -18
- {flwr-1.20.0.dist-info → flwr-1.22.0.dist-info}/RECORD +176 -128
- {flwr-1.20.0.dist-info → flwr-1.22.0.dist-info}/entry_points.txt +1 -0
- flwr/proto/exec_pb2.py +0 -62
- flwr/proto/exec_pb2_grpc.pyi +0 -93
- flwr/superexec/app.py +0 -45
- flwr/superexec/deployment.py +0 -191
- flwr/superexec/executor.py +0 -100
- flwr/superexec/simulation.py +0 -129
- {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
|
flwr/clientapp/typing.py
ADDED
|
@@ -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
|
|
20
|
-
from .auth_plugin import
|
|
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
|
-
"
|
|
25
|
-
"
|
|
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.
|
|
24
|
+
from flwr.proto.control_pb2_grpc import ControlStub
|
|
25
25
|
|
|
26
26
|
from ..typing import UserAuthCredentials, UserAuthLoginDetails
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
class
|
|
30
|
-
"""Abstract Flower Auth Plugin class for
|
|
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
|
|
73
|
-
"""Abstract Flower Authorization Plugin class for
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
-
#
|
|
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
|
|
28
|
+
"""Abstract Flower Event Log Writer Plugin class for ControlServicer."""
|
|
29
29
|
|
|
30
30
|
@abstractmethod
|
|
31
31
|
def __init__(self) -> None:
|
flwr/common/exit/__init__.py
CHANGED
|
@@ -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 =
|
|
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
|
|
flwr/common/exit/exit_code.py
CHANGED
|
@@ -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
|
-
#
|
|
47
|
+
# SuperExec-specific exit codes (400-499)
|
|
48
|
+
SUPEREXEC_INVALID_PLUGIN_CONFIG = 400
|
|
44
49
|
|
|
45
|
-
#
|
|
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
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|