# Copyright 2024 Flower Labs GmbH. All Rights Reserved.## Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at## http://www.apache.org/licenses/LICENSE-2.0## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License.# =============================================================================="""Flower ClientApp."""importinspectfromtypingimportCallable,Optionalfromflwr.client.clientimportClientfromflwr.client.message_handler.message_handlerimport(handle_legacy_message_from_msgtype,)fromflwr.client.mod.utilsimportmake_ffnfromflwr.client.typingimportClientFnExt,Modfromflwr.commonimportContext,Message,MessageTypefromflwr.common.loggerimportwarn_deprecated_feature,warn_preview_featurefrom.typingimportClientAppCallabledef_alert_erroneous_client_fn()->None:raiseValueError("A `ClientApp` cannot make use of a `client_fn` that does ""not have a signature in the form: `def client_fn(context: ""Context)`. You can import the `Context` like this: ""`from flwr.common import Context`")def_inspect_maybe_adapt_client_fn_signature(client_fn:ClientFnExt)->ClientFnExt:client_fn_args=inspect.signature(client_fn).parametersiflen(client_fn_args)!=1:_alert_erroneous_client_fn()first_arg=list(client_fn_args.keys())[0]first_arg_type=client_fn_args[first_arg].annotationiffirst_arg_typeisstrorfirst_arg=="cid":# Warn previous signature for `client_fn` seems to be usedwarn_deprecated_feature("`client_fn` now expects a signature `def client_fn(context: Context)`.""The provided `client_fn` has signature: "f"{dict(client_fn_args.items())}. You can import the `Context` like this:"" `from flwr.common import Context`")# Wrap depcreated client_fn inside a function with the expected signaturedefadaptor_fn(context:Context,)->Client:# pylint: disable=unused-argument# if patition-id is defined, pass it. Else pass node_id that should# always be defined during Context init.cid=context.node_config.get("partition-id",context.node_id)returnclient_fn(str(cid))# type: ignorereturnadaptor_fnreturnclient_fnclassClientAppException(Exception):"""Exception raised when an exception is raised while executing a ClientApp."""def__init__(self,message:str):ex_name=self.__class__.__name__self.message=f"\nException {ex_name} occurred. Message: "+messagesuper().__init__(self.message)
[docs]classClientApp:"""Flower ClientApp. Examples -------- Assuming a typical `Client` implementation named `FlowerClient`, you can wrap it in a `ClientApp` as follows: >>> class FlowerClient(NumPyClient): >>> # ... >>> >>> def client_fn(context: Context): >>> return FlowerClient().to_client() >>> >>> app = ClientApp(client_fn) If the above code is in a Python module called `client`, it can be started as follows: >>> flower-client-app client:app --insecure In this `client:app` example, `client` refers to the Python module `client.py` in which the previous code lives in and `app` refers to the global attribute `app` that points to an object of type `ClientApp`. """def__init__(self,client_fn:Optional[ClientFnExt]=None,# Only for backward compatibilitymods:Optional[list[Mod]]=None,)->None:self._mods:list[Mod]=modsifmodsisnotNoneelse[]# Create wrapper function for `handle`self._call:Optional[ClientAppCallable]=Noneifclient_fnisnotNone:client_fn=_inspect_maybe_adapt_client_fn_signature(client_fn)defffn(message:Message,context:Context,)->Message:# pylint: disable=invalid-nameout_message=handle_legacy_message_from_msgtype(client_fn=client_fn,message=message,context=context)returnout_message# Wrap mods around the wrapped handle functionself._call=make_ffn(ffn,modsifmodsisnotNoneelse[])# Step functionsself._train:Optional[ClientAppCallable]=Noneself._evaluate:Optional[ClientAppCallable]=Noneself._query:Optional[ClientAppCallable]=Nonedef__call__(self,message:Message,context:Context)->Message:"""Execute `ClientApp`."""# Execute message using `client_fn`ifself._call:returnself._call(message,context)# Execute message using a newifmessage.metadata.message_type==MessageType.TRAIN:ifself._train:returnself._train(message,context)raiseValueError("No `train` function registered")ifmessage.metadata.message_type==MessageType.EVALUATE:ifself._evaluate:returnself._evaluate(message,context)raiseValueError("No `evaluate` function registered")ifmessage.metadata.message_type==MessageType.QUERY:ifself._query:returnself._query(message,context)raiseValueError("No `query` function registered")# Message type did not match one of the known message types abvoeraiseValueError(f"Unknown message_type: {message.metadata.message_type}")
[docs]deftrain(self)->Callable[[ClientAppCallable],ClientAppCallable]:"""Return a decorator that registers the train fn with the client app. Examples -------- >>> app = ClientApp() >>> >>> @app.train() >>> def train(message: Message, context: Context) -> Message: >>> print("ClientApp training running") >>> # Create and return an echo reply message >>> return message.create_reply(content=message.content()) """deftrain_decorator(train_fn:ClientAppCallable)->ClientAppCallable:"""Register the train fn with the ServerApp object."""ifself._call:raise_registration_error(MessageType.TRAIN)warn_preview_feature("ClientApp-register-train-function")# Register provided function with the ClientApp object# Wrap mods around the wrapped step functionself._train=make_ffn(train_fn,self._mods)# Return provided function unmodifiedreturntrain_fnreturntrain_decorator
[docs]defevaluate(self)->Callable[[ClientAppCallable],ClientAppCallable]:"""Return a decorator that registers the evaluate fn with the client app. Examples -------- >>> app = ClientApp() >>> >>> @app.evaluate() >>> def evaluate(message: Message, context: Context) -> Message: >>> print("ClientApp evaluation running") >>> # Create and return an echo reply message >>> return message.create_reply(content=message.content()) """defevaluate_decorator(evaluate_fn:ClientAppCallable)->ClientAppCallable:"""Register the evaluate fn with the ServerApp object."""ifself._call:raise_registration_error(MessageType.EVALUATE)warn_preview_feature("ClientApp-register-evaluate-function")# Register provided function with the ClientApp object# Wrap mods around the wrapped step functionself._evaluate=make_ffn(evaluate_fn,self._mods)# Return provided function unmodifiedreturnevaluate_fnreturnevaluate_decorator
[docs]defquery(self)->Callable[[ClientAppCallable],ClientAppCallable]:"""Return a decorator that registers the query fn with the client app. Examples -------- >>> app = ClientApp() >>> >>> @app.query() >>> def query(message: Message, context: Context) -> Message: >>> print("ClientApp query running") >>> # Create and return an echo reply message >>> return message.create_reply(content=message.content()) """defquery_decorator(query_fn:ClientAppCallable)->ClientAppCallable:"""Register the query fn with the ServerApp object."""ifself._call:raise_registration_error(MessageType.QUERY)warn_preview_feature("ClientApp-register-query-function")# Register provided function with the ClientApp object# Wrap mods around the wrapped step functionself._query=make_ffn(query_fn,self._mods)# Return provided function unmodifiedreturnquery_fnreturnquery_decorator
classLoadClientAppError(Exception):"""Error when trying to load `ClientApp`."""def_registration_error(fn_name:str)->ValueError:returnValueError(f"""Use either `@app.{fn_name}()` or `client_fn`, but not both. Use the `ClientApp` with an existing `client_fn`: >>> class FlowerClient(NumPyClient): >>> # ... >>> >>> def client_fn(context: Context): >>> return FlowerClient().to_client() >>> >>> app = ClientApp( >>> client_fn=client_fn, >>> ) Use the `ClientApp` with a custom {fn_name} function: >>> app = ClientApp() >>> >>> @app.{fn_name}() >>> def {fn_name}(message: Message, context: Context) -> Message: >>> print("ClientApp {fn_name} running") >>> # Create and return an echo reply message >>> return message.create_reply( >>> content=message.content() >>> ) """,)