flwr 1.16.0__py3-none-any.whl → 1.17.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/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
- flwr/cli/run/run.py +5 -9
- flwr/client/app.py +6 -4
- flwr/client/client_app.py +162 -99
- flwr/client/clientapp/app.py +2 -2
- flwr/client/grpc_client/connection.py +24 -21
- flwr/client/message_handler/message_handler.py +27 -27
- flwr/client/mod/__init__.py +2 -2
- flwr/client/mod/centraldp_mods.py +7 -7
- flwr/client/mod/comms_mods.py +16 -22
- flwr/client/mod/localdp_mod.py +4 -4
- flwr/client/mod/secure_aggregation/secaggplus_mod.py +31 -31
- flwr/client/run_info_store.py +2 -2
- flwr/common/__init__.py +12 -4
- flwr/common/config.py +4 -4
- flwr/common/constant.py +6 -6
- flwr/common/context.py +4 -4
- flwr/common/event_log_plugin/event_log_plugin.py +3 -3
- flwr/common/logger.py +2 -2
- flwr/common/message.py +327 -102
- flwr/common/record/__init__.py +8 -4
- flwr/common/record/arrayrecord.py +626 -0
- flwr/common/record/{configsrecord.py → configrecord.py} +75 -29
- flwr/common/record/conversion_utils.py +1 -1
- flwr/common/record/{metricsrecord.py → metricrecord.py} +78 -32
- flwr/common/record/recorddict.py +288 -0
- flwr/common/recorddict_compat.py +410 -0
- flwr/common/secure_aggregation/secaggplus_constants.py +1 -1
- flwr/common/serde.py +66 -71
- flwr/common/typing.py +8 -8
- flwr/proto/exec_pb2.py +3 -3
- flwr/proto/exec_pb2.pyi +3 -3
- flwr/proto/message_pb2.py +12 -12
- flwr/proto/message_pb2.pyi +9 -9
- flwr/proto/recorddict_pb2.py +70 -0
- flwr/proto/{recordset_pb2.pyi → recorddict_pb2.pyi} +35 -35
- flwr/proto/run_pb2.py +31 -31
- flwr/proto/run_pb2.pyi +3 -3
- flwr/server/__init__.py +3 -1
- flwr/server/app.py +56 -1
- flwr/server/compat/__init__.py +2 -2
- flwr/server/compat/app.py +11 -11
- flwr/server/compat/app_utils.py +16 -16
- flwr/server/compat/{driver_client_proxy.py → grid_client_proxy.py} +39 -39
- flwr/server/fleet_event_log_interceptor.py +94 -0
- flwr/server/{driver → grid}/__init__.py +8 -7
- flwr/server/{driver/driver.py → grid/grid.py} +47 -18
- flwr/server/{driver/grpc_driver.py → grid/grpc_grid.py} +87 -64
- flwr/server/{driver/inmemory_driver.py → grid/inmemory_grid.py} +24 -34
- flwr/server/run_serverapp.py +4 -4
- flwr/server/server_app.py +38 -18
- flwr/server/serverapp/app.py +10 -10
- flwr/server/superlink/fleet/vce/backend/backend.py +2 -2
- flwr/server/superlink/fleet/vce/backend/raybackend.py +2 -2
- flwr/server/superlink/fleet/vce/vce_api.py +1 -3
- flwr/server/superlink/linkstate/in_memory_linkstate.py +33 -8
- flwr/server/superlink/linkstate/linkstate.py +4 -4
- flwr/server/superlink/linkstate/sqlite_linkstate.py +61 -27
- flwr/server/superlink/linkstate/utils.py +93 -27
- flwr/server/superlink/{driver → serverappio}/__init__.py +1 -1
- flwr/server/superlink/{driver → serverappio}/serverappio_grpc.py +1 -1
- flwr/server/superlink/{driver → serverappio}/serverappio_servicer.py +4 -4
- flwr/server/superlink/simulation/simulationio_servicer.py +2 -2
- flwr/server/typing.py +3 -3
- flwr/server/utils/validator.py +4 -4
- flwr/server/workflow/default_workflows.py +48 -57
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +48 -50
- flwr/simulation/app.py +2 -2
- flwr/simulation/ray_transport/ray_actor.py +4 -2
- flwr/simulation/ray_transport/ray_client_proxy.py +34 -32
- flwr/simulation/run_simulation.py +15 -15
- flwr/superexec/deployment.py +4 -4
- flwr/superexec/exec_event_log_interceptor.py +135 -0
- flwr/superexec/exec_grpc.py +10 -4
- flwr/superexec/exec_servicer.py +2 -2
- flwr/superexec/exec_user_auth_interceptor.py +18 -2
- flwr/superexec/executor.py +3 -3
- flwr/superexec/simulation.py +3 -3
- {flwr-1.16.0.dist-info → flwr-1.17.0.dist-info}/METADATA +2 -2
- {flwr-1.16.0.dist-info → flwr-1.17.0.dist-info}/RECORD +94 -92
- flwr/common/record/parametersrecord.py +0 -339
- flwr/common/record/recordset.py +0 -209
- flwr/common/recordset_compat.py +0 -418
- flwr/proto/recordset_pb2.py +0 -70
- /flwr/proto/{recordset_pb2_grpc.py → recorddict_pb2_grpc.py} +0 -0
- /flwr/proto/{recordset_pb2_grpc.pyi → recorddict_pb2_grpc.pyi} +0 -0
- {flwr-1.16.0.dist-info → flwr-1.17.0.dist-info}/LICENSE +0 -0
- {flwr-1.16.0.dist-info → flwr-1.17.0.dist-info}/WHEEL +0 -0
- {flwr-1.16.0.dist-info → flwr-1.17.0.dist-info}/entry_points.txt +0 -0
|
@@ -35,7 +35,7 @@ warnings.filterwarnings("ignore", category=UserWarning)
|
|
|
35
35
|
# pylint: disable=too-many-arguments
|
|
36
36
|
# pylint: disable=too-many-instance-attributes
|
|
37
37
|
class FlowerClient(NumPyClient):
|
|
38
|
-
"""
|
|
38
|
+
"""Flower client for LLM fine-tuning."""
|
|
39
39
|
|
|
40
40
|
def __init__(
|
|
41
41
|
self,
|
flwr/cli/run/run.py
CHANGED
|
@@ -35,15 +35,11 @@ from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
|
|
|
35
35
|
from flwr.common.config import (
|
|
36
36
|
flatten_dict,
|
|
37
37
|
parse_config_args,
|
|
38
|
-
|
|
38
|
+
user_config_to_configrecord,
|
|
39
39
|
)
|
|
40
40
|
from flwr.common.constant import CliOutputFormat
|
|
41
41
|
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
|
42
|
-
from flwr.common.serde import
|
|
43
|
-
configs_record_to_proto,
|
|
44
|
-
fab_to_proto,
|
|
45
|
-
user_config_to_proto,
|
|
46
|
-
)
|
|
42
|
+
from flwr.common.serde import config_record_to_proto, fab_to_proto, user_config_to_proto
|
|
47
43
|
from flwr.common.typing import Fab
|
|
48
44
|
from flwr.proto.exec_pb2 import StartRunRequest # pylint: disable=E0611
|
|
49
45
|
from flwr.proto.exec_pb2_grpc import ExecStub
|
|
@@ -171,14 +167,14 @@ def _run_with_exec_api(
|
|
|
171
167
|
|
|
172
168
|
fab = Fab(fab_hash, content)
|
|
173
169
|
|
|
174
|
-
# Construct a `
|
|
170
|
+
# Construct a `ConfigRecord` out of a flattened `UserConfig`
|
|
175
171
|
fed_conf = flatten_dict(federation_config.get("options", {}))
|
|
176
|
-
c_record =
|
|
172
|
+
c_record = user_config_to_configrecord(fed_conf)
|
|
177
173
|
|
|
178
174
|
req = StartRunRequest(
|
|
179
175
|
fab=fab_to_proto(fab),
|
|
180
176
|
override_config=user_config_to_proto(parse_config_args(config_overrides)),
|
|
181
|
-
federation_options=
|
|
177
|
+
federation_options=config_record_to_proto(c_record),
|
|
182
178
|
)
|
|
183
179
|
with unauthenticated_exc_handler():
|
|
184
180
|
res = stub.StartRun(req)
|
flwr/client/app.py
CHANGED
|
@@ -495,8 +495,9 @@ def start_client_internal(
|
|
|
495
495
|
context = run_info_store.retrieve_context(run_id=run_id)
|
|
496
496
|
# Create an error reply message that will never be used to prevent
|
|
497
497
|
# the used-before-assignment linting error
|
|
498
|
-
reply_message =
|
|
499
|
-
|
|
498
|
+
reply_message = Message(
|
|
499
|
+
Error(code=ErrorCode.UNKNOWN, reason="Unknown"),
|
|
500
|
+
reply_to=message,
|
|
500
501
|
)
|
|
501
502
|
|
|
502
503
|
# Handle app loading and task message
|
|
@@ -593,8 +594,9 @@ def start_client_internal(
|
|
|
593
594
|
log(ERROR, "%s raised an exception", exc_entity, exc_info=ex)
|
|
594
595
|
|
|
595
596
|
# Create error message
|
|
596
|
-
reply_message =
|
|
597
|
-
|
|
597
|
+
reply_message = Message(
|
|
598
|
+
Error(code=e_code, reason=reason),
|
|
599
|
+
reply_to=message,
|
|
598
600
|
)
|
|
599
601
|
else:
|
|
600
602
|
# No exception, update node state
|
flwr/client/client_app.py
CHANGED
|
@@ -27,10 +27,13 @@ from flwr.client.message_handler.message_handler import (
|
|
|
27
27
|
from flwr.client.mod.utils import make_ffn
|
|
28
28
|
from flwr.client.typing import ClientFnExt, Mod
|
|
29
29
|
from flwr.common import Context, Message, MessageType
|
|
30
|
-
from flwr.common.logger import warn_deprecated_feature
|
|
30
|
+
from flwr.common.logger import warn_deprecated_feature
|
|
31
|
+
from flwr.common.message import validate_message_type
|
|
31
32
|
|
|
32
33
|
from .typing import ClientAppCallable
|
|
33
34
|
|
|
35
|
+
DEFAULT_ACTION = "default"
|
|
36
|
+
|
|
34
37
|
|
|
35
38
|
def _alert_erroneous_client_fn() -> None:
|
|
36
39
|
raise ValueError(
|
|
@@ -110,6 +113,7 @@ class ClientApp:
|
|
|
110
113
|
mods: Optional[list[Mod]] = None,
|
|
111
114
|
) -> None:
|
|
112
115
|
self._mods: list[Mod] = mods if mods is not None else []
|
|
116
|
+
self._registered_funcs: dict[str, ClientAppCallable] = {}
|
|
113
117
|
|
|
114
118
|
# Create wrapper function for `handle`
|
|
115
119
|
self._call: Optional[ClientAppCallable] = None
|
|
@@ -129,10 +133,7 @@ class ClientApp:
|
|
|
129
133
|
# Wrap mods around the wrapped handle function
|
|
130
134
|
self._call = make_ffn(ffn, mods if mods is not None else [])
|
|
131
135
|
|
|
132
|
-
#
|
|
133
|
-
self._train: Optional[ClientAppCallable] = None
|
|
134
|
-
self._evaluate: Optional[ClientAppCallable] = None
|
|
135
|
-
self._query: Optional[ClientAppCallable] = None
|
|
136
|
+
# Lifespan function
|
|
136
137
|
self._lifespan = _empty_lifespan
|
|
137
138
|
|
|
138
139
|
def __call__(self, message: Message, context: Context) -> Message:
|
|
@@ -142,27 +143,41 @@ class ClientApp:
|
|
|
142
143
|
if self._call:
|
|
143
144
|
return self._call(message, context)
|
|
144
145
|
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
146
|
+
# Get the category and the action
|
|
147
|
+
# A valid message type is of the form "<category>" or "<category>.<action>",
|
|
148
|
+
# where <category> must be "train"/"evaluate"/"query", and <action> is a
|
|
149
|
+
# valid Python identifier
|
|
150
|
+
if not validate_message_type(message.metadata.message_type):
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"Invalid message type: {message.metadata.message_type}"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
category, action = message.metadata.message_type, DEFAULT_ACTION
|
|
156
|
+
if "." in category:
|
|
157
|
+
category, action = category.split(".")
|
|
158
|
+
|
|
159
|
+
# Check if the function is registered
|
|
160
|
+
if (full_name := f"{category}.{action}") in self._registered_funcs:
|
|
161
|
+
return self._registered_funcs[full_name](message, context)
|
|
162
|
+
|
|
163
|
+
raise ValueError(f"No {category} function registered with name '{action}'")
|
|
161
164
|
|
|
162
165
|
def train(
|
|
163
|
-
self, mods: Optional[list[Mod]] = None
|
|
166
|
+
self, action: str = DEFAULT_ACTION, *, mods: Optional[list[Mod]] = None
|
|
164
167
|
) -> Callable[[ClientAppCallable], ClientAppCallable]:
|
|
165
|
-
"""
|
|
168
|
+
"""Register a train function with the ``ClientApp``.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
action : str (default: "default")
|
|
173
|
+
The action name used to route messages. Defaults to "default".
|
|
174
|
+
mods : Optional[list[Mod]] (default: None)
|
|
175
|
+
A list of function-specific modifiers.
|
|
176
|
+
|
|
177
|
+
Returns
|
|
178
|
+
-------
|
|
179
|
+
Callable[[ClientAppCallable], ClientAppCallable]
|
|
180
|
+
A decorator that registers a train function with the ``ClientApp``.
|
|
166
181
|
|
|
167
182
|
Examples
|
|
168
183
|
--------
|
|
@@ -172,42 +187,52 @@ class ClientApp:
|
|
|
172
187
|
>>>
|
|
173
188
|
>>> @app.train()
|
|
174
189
|
>>> def train(message: Message, context: Context) -> Message:
|
|
175
|
-
>>>
|
|
176
|
-
>>>
|
|
177
|
-
>>>
|
|
190
|
+
>>> print("Executing default train function")
|
|
191
|
+
>>> # Create and return an echo reply message
|
|
192
|
+
>>> return Message(message.content, reply_to=message)
|
|
178
193
|
|
|
179
|
-
Registering a train function with a
|
|
194
|
+
Registering a train function with a custom action name:
|
|
195
|
+
|
|
196
|
+
>>> app = ClientApp()
|
|
197
|
+
>>>
|
|
198
|
+
>>> # Messages with `message_type="train.custom_action"` will be
|
|
199
|
+
>>> # routed to this function.
|
|
200
|
+
>>> @app.train("custom_action")
|
|
201
|
+
>>> def custom_action(message: Message, context: Context) -> Message:
|
|
202
|
+
>>> print("Executing train function for custom action")
|
|
203
|
+
>>> return Message(message.content, reply_to=message)
|
|
204
|
+
|
|
205
|
+
Registering a train function with a function-specific Flower Mod:
|
|
180
206
|
|
|
181
207
|
>>> from flwr.client.mod import message_size_mod
|
|
182
208
|
>>>
|
|
183
209
|
>>> app = ClientApp()
|
|
184
210
|
>>>
|
|
211
|
+
>>> # Using the `mods` argument to apply a function-specific mod.
|
|
185
212
|
>>> @app.train(mods=[message_size_mod])
|
|
186
213
|
>>> def train(message: Message, context: Context) -> Message:
|
|
187
|
-
>>>
|
|
188
|
-
>>>
|
|
214
|
+
>>> print("Executing train function with message size mod")
|
|
215
|
+
>>> # Create and return an echo reply message
|
|
216
|
+
>>> return Message(message.content, reply_to=message)
|
|
189
217
|
"""
|
|
190
|
-
|
|
191
|
-
def train_decorator(train_fn: ClientAppCallable) -> ClientAppCallable:
|
|
192
|
-
"""Register the train fn with the ServerApp object."""
|
|
193
|
-
if self._call:
|
|
194
|
-
raise _registration_error(MessageType.TRAIN)
|
|
195
|
-
|
|
196
|
-
warn_preview_feature("ClientApp-register-train-function")
|
|
197
|
-
|
|
198
|
-
# Register provided function with the ClientApp object
|
|
199
|
-
# Wrap mods around the wrapped step function
|
|
200
|
-
self._train = make_ffn(train_fn, self._mods + (mods or []))
|
|
201
|
-
|
|
202
|
-
# Return provided function unmodified
|
|
203
|
-
return train_fn
|
|
204
|
-
|
|
205
|
-
return train_decorator
|
|
218
|
+
return _get_decorator(self, MessageType.TRAIN, action, mods)
|
|
206
219
|
|
|
207
220
|
def evaluate(
|
|
208
|
-
self, mods: Optional[list[Mod]] = None
|
|
221
|
+
self, action: str = DEFAULT_ACTION, *, mods: Optional[list[Mod]] = None
|
|
209
222
|
) -> Callable[[ClientAppCallable], ClientAppCallable]:
|
|
210
|
-
"""
|
|
223
|
+
"""Register an evaluate function with the ``ClientApp``.
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
action : str (default: "default")
|
|
228
|
+
The action name used to route messages. Defaults to "default".
|
|
229
|
+
mods : Optional[list[Mod]] (default: None)
|
|
230
|
+
A list of function-specific modifiers.
|
|
231
|
+
|
|
232
|
+
Returns
|
|
233
|
+
-------
|
|
234
|
+
Callable[[ClientAppCallable], ClientAppCallable]
|
|
235
|
+
A decorator that registers an evaluate function with the ``ClientApp``.
|
|
211
236
|
|
|
212
237
|
Examples
|
|
213
238
|
--------
|
|
@@ -217,43 +242,52 @@ class ClientApp:
|
|
|
217
242
|
>>>
|
|
218
243
|
>>> @app.evaluate()
|
|
219
244
|
>>> def evaluate(message: Message, context: Context) -> Message:
|
|
220
|
-
>>>
|
|
221
|
-
>>>
|
|
222
|
-
>>>
|
|
245
|
+
>>> print("Executing default evaluate function")
|
|
246
|
+
>>> # Create and return an echo reply message
|
|
247
|
+
>>> return Message(message.content, reply_to=message)
|
|
223
248
|
|
|
224
|
-
Registering an evaluate function with a
|
|
249
|
+
Registering an evaluate function with a custom action name:
|
|
250
|
+
|
|
251
|
+
>>> app = ClientApp()
|
|
252
|
+
>>>
|
|
253
|
+
>>> # Messages with `message_type="evaluate.custom_action"` will be
|
|
254
|
+
>>> # routed to this function.
|
|
255
|
+
>>> @app.evaluate("custom_action")
|
|
256
|
+
>>> def custom_action(message: Message, context: Context) -> Message:
|
|
257
|
+
>>> print("Executing evaluate function for custom action")
|
|
258
|
+
>>> return Message(message.content, reply_to=message)
|
|
259
|
+
|
|
260
|
+
Registering an evaluate function with a function-specific Flower Mod:
|
|
225
261
|
|
|
226
262
|
>>> from flwr.client.mod import message_size_mod
|
|
227
263
|
>>>
|
|
228
264
|
>>> app = ClientApp()
|
|
229
265
|
>>>
|
|
266
|
+
>>> # Using the `mods` argument to apply a function-specific mod.
|
|
230
267
|
>>> @app.evaluate(mods=[message_size_mod])
|
|
231
268
|
>>> def evaluate(message: Message, context: Context) -> Message:
|
|
232
|
-
>>>
|
|
233
|
-
>>>
|
|
234
|
-
>>>
|
|
269
|
+
>>> print("Executing evaluate function with message size mod")
|
|
270
|
+
>>> # Create and return an echo reply message
|
|
271
|
+
>>> return Message(message.content, reply_to=message)
|
|
235
272
|
"""
|
|
236
|
-
|
|
237
|
-
def evaluate_decorator(evaluate_fn: ClientAppCallable) -> ClientAppCallable:
|
|
238
|
-
"""Register the evaluate fn with the ServerApp object."""
|
|
239
|
-
if self._call:
|
|
240
|
-
raise _registration_error(MessageType.EVALUATE)
|
|
241
|
-
|
|
242
|
-
warn_preview_feature("ClientApp-register-evaluate-function")
|
|
243
|
-
|
|
244
|
-
# Register provided function with the ClientApp object
|
|
245
|
-
# Wrap mods around the wrapped step function
|
|
246
|
-
self._evaluate = make_ffn(evaluate_fn, self._mods + (mods or []))
|
|
247
|
-
|
|
248
|
-
# Return provided function unmodified
|
|
249
|
-
return evaluate_fn
|
|
250
|
-
|
|
251
|
-
return evaluate_decorator
|
|
273
|
+
return _get_decorator(self, MessageType.EVALUATE, action, mods)
|
|
252
274
|
|
|
253
275
|
def query(
|
|
254
|
-
self, mods: Optional[list[Mod]] = None
|
|
276
|
+
self, action: str = DEFAULT_ACTION, *, mods: Optional[list[Mod]] = None
|
|
255
277
|
) -> Callable[[ClientAppCallable], ClientAppCallable]:
|
|
256
|
-
"""
|
|
278
|
+
"""Register a query function with the ``ClientApp``.
|
|
279
|
+
|
|
280
|
+
Parameters
|
|
281
|
+
----------
|
|
282
|
+
action : str (default: "default")
|
|
283
|
+
The action name used to route messages. Defaults to "default".
|
|
284
|
+
mods : Optional[list[Mod]] (default: None)
|
|
285
|
+
A list of function-specific modifiers.
|
|
286
|
+
|
|
287
|
+
Returns
|
|
288
|
+
-------
|
|
289
|
+
Callable[[ClientAppCallable], ClientAppCallable]
|
|
290
|
+
A decorator that registers a query function with the ``ClientApp``.
|
|
257
291
|
|
|
258
292
|
Examples
|
|
259
293
|
--------
|
|
@@ -263,38 +297,35 @@ class ClientApp:
|
|
|
263
297
|
>>>
|
|
264
298
|
>>> @app.query()
|
|
265
299
|
>>> def query(message: Message, context: Context) -> Message:
|
|
266
|
-
>>>
|
|
267
|
-
>>>
|
|
268
|
-
>>>
|
|
300
|
+
>>> print("Executing default query function")
|
|
301
|
+
>>> # Create and return an echo reply message
|
|
302
|
+
>>> return Message(message.content, reply_to=message)
|
|
303
|
+
|
|
304
|
+
Registering a query function with a custom action name:
|
|
269
305
|
|
|
270
|
-
|
|
306
|
+
>>> app = ClientApp()
|
|
307
|
+
>>>
|
|
308
|
+
>>> # Messages with `message_type="query.custom_action"` will be
|
|
309
|
+
>>> # routed to this function.
|
|
310
|
+
>>> @app.query("custom_action")
|
|
311
|
+
>>> def custom_action(message: Message, context: Context) -> Message:
|
|
312
|
+
>>> print("Executing query function for custom action")
|
|
313
|
+
>>> return Message(message.content, reply_to=message)
|
|
314
|
+
|
|
315
|
+
Registering a query function with a function-specific Flower Mod:
|
|
271
316
|
|
|
272
317
|
>>> from flwr.client.mod import message_size_mod
|
|
273
318
|
>>>
|
|
274
319
|
>>> app = ClientApp()
|
|
275
320
|
>>>
|
|
321
|
+
>>> # Using the `mods` argument to apply a function-specific mod.
|
|
276
322
|
>>> @app.query(mods=[message_size_mod])
|
|
277
323
|
>>> def query(message: Message, context: Context) -> Message:
|
|
278
|
-
>>>
|
|
279
|
-
>>>
|
|
280
|
-
>>>
|
|
324
|
+
>>> print("Executing query function with message size mod")
|
|
325
|
+
>>> # Create and return an echo reply message
|
|
326
|
+
>>> return Message(message.content, reply_to=message)
|
|
281
327
|
"""
|
|
282
|
-
|
|
283
|
-
def query_decorator(query_fn: ClientAppCallable) -> ClientAppCallable:
|
|
284
|
-
"""Register the query fn with the ServerApp object."""
|
|
285
|
-
if self._call:
|
|
286
|
-
raise _registration_error(MessageType.QUERY)
|
|
287
|
-
|
|
288
|
-
warn_preview_feature("ClientApp-register-query-function")
|
|
289
|
-
|
|
290
|
-
# Register provided function with the ClientApp object
|
|
291
|
-
# Wrap mods around the wrapped step function
|
|
292
|
-
self._query = make_ffn(query_fn, self._mods + (mods or []))
|
|
293
|
-
|
|
294
|
-
# Return provided function unmodified
|
|
295
|
-
return query_fn
|
|
296
|
-
|
|
297
|
-
return query_decorator
|
|
328
|
+
return _get_decorator(self, MessageType.QUERY, action, mods)
|
|
298
329
|
|
|
299
330
|
def lifespan(
|
|
300
331
|
self,
|
|
@@ -325,7 +356,6 @@ class ClientApp:
|
|
|
325
356
|
lifespan_fn: Callable[[Context], Iterator[None]]
|
|
326
357
|
) -> Callable[[Context], Iterator[None]]:
|
|
327
358
|
"""Register the lifespan fn with the ServerApp object."""
|
|
328
|
-
warn_preview_feature("ClientApp-register-lifespan-function")
|
|
329
359
|
|
|
330
360
|
@contextmanager
|
|
331
361
|
def decorated_lifespan(context: Context) -> Iterator[None]:
|
|
@@ -365,6 +395,41 @@ class LoadClientAppError(Exception):
|
|
|
365
395
|
"""Error when trying to load `ClientApp`."""
|
|
366
396
|
|
|
367
397
|
|
|
398
|
+
def _get_decorator(
|
|
399
|
+
app: ClientApp, category: str, action: str, mods: Optional[list[Mod]]
|
|
400
|
+
) -> Callable[[ClientAppCallable], ClientAppCallable]:
|
|
401
|
+
"""Get the decorator for the given category and action."""
|
|
402
|
+
# pylint: disable=protected-access
|
|
403
|
+
if app._call:
|
|
404
|
+
raise _registration_error(category)
|
|
405
|
+
|
|
406
|
+
def decorator(fn: ClientAppCallable) -> ClientAppCallable:
|
|
407
|
+
|
|
408
|
+
# Check if the name is a valid Python identifier
|
|
409
|
+
if not action.isidentifier():
|
|
410
|
+
raise ValueError(
|
|
411
|
+
f"Cannot register {category} function with name '{action}'. "
|
|
412
|
+
"The name must follow Python's function naming rules."
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Check if the name is already registered
|
|
416
|
+
full_name = f"{category}.{action}" # Full name of the message type
|
|
417
|
+
if full_name in app._registered_funcs:
|
|
418
|
+
raise ValueError(
|
|
419
|
+
f"Cannot register {category} function with name '{action}'. "
|
|
420
|
+
f"A {category} function with the name '{action}' is already registered."
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# Register provided function with the ClientApp object
|
|
424
|
+
app._registered_funcs[full_name] = make_ffn(fn, app._mods + (mods or []))
|
|
425
|
+
|
|
426
|
+
# Return provided function unmodified
|
|
427
|
+
return fn
|
|
428
|
+
|
|
429
|
+
# pylint: enable=protected-access
|
|
430
|
+
return decorator
|
|
431
|
+
|
|
432
|
+
|
|
368
433
|
def _registration_error(fn_name: str) -> ValueError:
|
|
369
434
|
return ValueError(
|
|
370
435
|
f"""Use either `@app.{fn_name}()` or `client_fn`, but not both.
|
|
@@ -389,8 +454,6 @@ def _registration_error(fn_name: str) -> ValueError:
|
|
|
389
454
|
>>> def {fn_name}(message: Message, context: Context) -> Message:
|
|
390
455
|
>>> print("ClientApp {fn_name} running")
|
|
391
456
|
>>> # Create and return an echo reply message
|
|
392
|
-
>>> return message.
|
|
393
|
-
>>> content=message.content()
|
|
394
|
-
>>> )
|
|
457
|
+
>>> return Message(message.content, reply_to=message)
|
|
395
458
|
""",
|
|
396
459
|
)
|
flwr/client/clientapp/app.py
CHANGED
|
@@ -152,8 +152,8 @@ def run_clientapp( # pylint: disable=R0914
|
|
|
152
152
|
log(ERROR, "%s raised an exception", exc_entity, exc_info=ex)
|
|
153
153
|
|
|
154
154
|
# Create error message
|
|
155
|
-
reply_message =
|
|
156
|
-
|
|
155
|
+
reply_message = Message(
|
|
156
|
+
Error(code=e_code, reason=reason), reply_to=message
|
|
157
157
|
)
|
|
158
158
|
|
|
159
159
|
# Push Message and Context to SuperNode
|