Client tutorial
===============
If you haven't already read it, you should read the :doc:`../server/tutorial` first.

A simple client
---------------
A client is represented by the :class:`aiokatcp.Client` class. For more
advanced cases you may need to subclass it, but for simply making requests it
can be used as-is. Here is a simple function that connects to a server and
requests the help:

.. code:: python

   import asyncio
   import aiokatcp

   async def run_client():
       client = await aiokatcp.Client.connect('localhost', 4444)
       async with client:
           reply, informs = await client.request('help')
           for inform in informs:
               print(b' '.join(inform.arguments).decode('ascii'))

   asyncio.get_event_loop().run_until_complete(run_client())

Let's look at this one piece at a time. Firstly, we call
:meth:`.Client.connect`, which blocks until a connection is established. Note
that it will keep trying indefinitely, rather than raising an exception if a
connection could not immediately be established, so you may want to set a
timeout (for example, using `async_timeout`_). For more details, see
:ref:`autoreconnect`.

If you just want to create the :class:`.Client` object
without waiting for it to connect, you can just use the constructor, and later
used :meth:`.Client.wait_connected` to wait for a successful connection.

.. _async_timeout: https://github.com/aio-libs/async-timeout

Next, we use ``async with`` to ensure that the client is closed when we
exit the block, even if there was an exception. A request can raise exceptions
for two reasons:

1. If there is a problem with the connection, either at the time the request
   call is made or during the request, a :exc:`ConnectionError` is raised.
   Note that if the request has side-effects, they might or might not have
   taken effect.

2. If the server returns a response which starts with ``fail`` or ``invalid``,
   a :exc:`aiokatcp.FailReply` or :exc:`aiokatcp.InvalidReply` will be raised
   respectively. :exc:`aiokatcp.InvalidReply` is also used if the response was
   received, but did not have a valid response code i.e., ``ok``, ``fail`` or
   ``invalid``. If you prefer not to have these exceptions, use
   :meth:`.Client.request_raw`, which will return the reply as a
   :class:`.Message` rather than a list of arguments excluding the ``ok``.

We then print the informs, which for a ``help`` request consists of one per
request supported by the server. Note that the informs are instances of
:class:`.Message`, and the arguments are raw bytes. We thus have to decode the
values into text.

Asynchronous informs
--------------------
There are two ways to handle asynchronous informs. The first is to subclass
:class:`~.Client` and implement inform handlers. An inform handler is a message named
:samp:`inform_{name}` where *name* is the name of the inform. The type
annotations on the arguments are used to convert the arguments from raw bytes
to those types. For more details, see :ref:`type-conversions`.

Inform handlers are very similar to request handlers for servers. The
differences are that inform handlers are regular methods rather than
coroutines, and are not expected to return a value. They may raise
:exc:`~.InvalidReply` or :exc:`~.FailReply`, but this results only in a log
message.

As an example, here is how one might handle ``#sensor-status`` informs:

.. code:: python

    class MyClient(aiokatcp.Client):
        def inform_sensor_status(self, timestamp: aiokatcp.Timestamp,
                                 num_sensors: int, *args) -> None:
            if len(args) != 3 * num_sensors:
                raise FailReply('Wrong number of arguments')
            ...

It is also possible to overload :meth:`.Client.unhandled_inform` to handle
informs for which there is no specific handler.

Note that the base class already contains inform handlers for a number of
standard informs.

The second way to deal with asynchronous informs is to register callbacks. This
has the advantage that it does not require subclassing and so can be used on an
already-constructed :class:`~.Client`. Instead of a method inside the class,
write a function (with type annotations on the arguments, as before) and pass
it to :meth:`.Client.add_inform_callback`.
