.. index:: pair: quickstart (Python); calling services using the "wrapped" API pair: LN services; wrapped API for Python .. _quickstart/python/services-wrapped: .. default-domain:: py ####################################### Calling Services with the "wrapped" API ####################################### .. contents:: This document explains how to define a service provider and client which uses the "wrapped" LN client API. The purpose of that API is to reduce :term:`boilerplate code`, in situations where a lot of services need to be defined and used. It is a slightly adapted version of the python service code example in the :doc:`quickstart` section. Note that, while the code is shorter, it is not necessarily simpler, because it uses generated code. Defining a service provider in Python, using the "wrapped" API ============================================================== .. index:: single: LN services; provider example in python We show Python code which implements the service provider, giving it the name :file:`quickstart/python/services_wrapped/provider_wrapped.py`. Here is how it looks: .. literalinclude:: examples/quickstart/python/services_wrapped_api/provider_wrapped.py :language: python :linenos: :emphasize-lines: 9,14,15,18 To explain it very briefly: In line 9, **we define a class which is derived from** :py:class:`links_and_nodes.service_provider`, and register some method names along with message definitions which are used to call these methods. The input parameters in the message definitions become the input parameters of these methods, and the result parameter becomes the return value. The method call :py:meth:`links_and_nodes.service_provider.wrap_service_provider()` has the method name as the first parameter, and the used message definition as the second parameter. The name of the service itself is passed in :py:meth:`links_and_nodes.service_provider..__init__()` which is the inherited constructor of the parent class. The calls to the function :meth:`links_and_nodes.service_provider.do_register()` perform a registration of the service provider with the LN manager, and also assign the name of a :term:`service group` to the service handler. Here, it is set to a default name, which will be sufficient in almost all cases. Note that names of services and message definitions, which happen to be almost identical in our example, can be different. (For further reading, the section on :ref:`Message Definitions and Topics ` in the tutorial gives a more in-depth explanation of the relationships between message definitions, topics, and their names.) .. index:: pair: quickstart (Python); service group The function which actually handles service requests has the name :meth:`links_and_nodes.client.wait_and_handle_service_group_requests()`. Here, the optional first parameter is a string with the name of the group of service handlers which will be run to serve a request. In our example, the service handlers will in the main thread of the program, which keeps things nicely simple. Calling services from Python, using the wrapped API =================================================== .. index:: single: LN services; client example in python triple: service client; wrap_service; quickstart example In addition, we need, of course, again code which *calls* the computation. For this, we create a service client program :file:`examples/quickstart/python/services_wrapped_api/client_wrapped.py` like this: .. literalinclude:: examples/quickstart/python/services_wrapped_api/client_wrapped.py :language: python :linenos: :emphasize-lines: 22, 29-31 Here, the method call :py:meth:`services_wrapper.wrap_service()` has again two parameters, the first is the name of the service method that is provided (which here is equal to the the function name at the provider side), and the second is the used service message definition. With this, the call ``area_service_client.compute_circle_area()``, for example, becomes a service call which is forwarded to the service provider and implemented by ``area_provider.compute_circle_area()``, and the result of the call is returned by the wrapped method. Error Responses --------------- The "wrapped" API shown here has **one important difference to the "direct" API** that is shown in the introductory quickstart section: When a service call returns which has any non-empty string assigned to the field ``resp.error_message``, automatically a Python exception of type ``ln.ServiceErrorResponse`` will be created with that field as its only argument [#prior-to-v2.1]_ . Such exceptions need to be caught by the service client, as shown for example in lines 22 and 29 to 31. .. index:: triple: LN services; wrapped API for Python; error handling triple: LN services; wrapped API for Python; pre-processing of requests triple: LN services; wrapped API for Python; post-processing of requests triple: LN services; wrapped API for Python; error handling triple: LN services; wrapped API for Python; exceptions pair: Python API; preprocessor keyword argument to wrap_service pair: Python API; postprocessor keyword argument to wrap_service pair: Python API; postprocessors keyword argument to wrap_service pair: Python API; throw_on_these_error_indicators kwarg .. _python/service/wrapped-api/error-handling: Configuring the Response for Error Returns ------------------------------------------ In the "wrapped" API as of links and nodes up to version 2.0.x, there was no straight-forward way to transfer additional data alongside with an error message if a service call should return an error. This could make it harder to return extra information for more specific error handling. Beginning from LN 2.1.0, it is also possible to configure the service call to return further response data from the exception object, filter error responses with a post-processing function, or disable the automatic generation of exceptions. The resulting service client and service provider can be configured to be executed in the LN manager exactly as explained in the Python quickstart tutorial in :doc:`quickstart_python_services`. This can be done in two ways: 1. By defining extra response fields which indicate an error: If we change the initialization of the service handle as follows: .. sourcecode:: python self.wrap_service("request", "elevator/request/elevator_call", throw_on_these_error_indicators=["error_message", "error_code"] ) Then any response which has either a :term:`truthy` field ``error_message`` or ``error_code``, which means a non-empty error string, or a non-zero numerical code, will result in an exception being raised. The returned exception will have a "response" member which is a dictionary that contains both ``"error_message"`` and ``"error_code"`` as strings. So, we could re-write the above sample as follows: .. sourcecode:: python self.wrap_service("request", "elevator/request/elevator_call", throw_on_these_error_indicators=["error_message", "error_code"] ) # ... r = random.uniform(-5, 20) try: print("computing circle area with radius r = {:.2f}".format(r)) Result = c.compute_circle_area(r) # look up result from a call that returns multiple fields in a dict print("circle result: %5.2f" % Result["area"]) # responses with a non-empty error message are considered # as errors, and are turned into exceptions except Exception as e: print("Error, with message = "{}" and code = {} ".format( e.response["error_message"], e.response["errror_code"] )) print("continuing...") 2. In addition, the service call wrapper can configure a post-processor callback method which can inspect the returned message, like so: .. sourcecode:: python self.wrap_service("request", "elevator/request/elevator_call", postprocessor=self.check_elevator_call_response ) # ... def check_elevator_call_response(self, response): if response["error_code"] != 0: raise MyElevatorException(...) return response # return all fields to caller, or # drop all error-indicator-fields, as they should be of no interrest for the caller: response.pop("error_code") response.pop("error_message") return response Further response processing functions can be defined with the ``preprocessors`` keyword argument, which specifies a ``dict`` with the argument name of each message field as a key, and a pre-processing function as the value, and ``postprocessors`` (note the plural), which defines a corresponding dict of fields and functions for each response field. Note that while the API that is used is different with the goal to reduce boilerplate code, the transmitted service messages ath the level of LN services are exactly the same as with the direct API. This means that "wrapped" and "direct" clients and providers can be intermixed without any restrictions. .. seealso:: * :py:meth:`links_and_nodes.services_wrapper.wrap_service` * :py:class:`links_and_nodes.ServiceErrorResponse` .. rubric:: Footnotes .. [#prior-to-v2.1] For LN version prior to 2.1.0, the type of the exception is Python's ``Exception``. Type