# Custom Python Client Objects

To provide an "upgraded" and more finely-tuned user experience for certain
kinds of dataset, Tiled can be configured to use custom Python objects.
This is transparent and automatic from the point view of the user.

In the Python client, when a user accesses a given item, Tiled inspects the
item to decide what type of object to use to represent it.
In simple cases, this is just based on the `structure_family`: `"array"` goes
to `tiled.client.array.ArrayClient`;  `"table"` goes to
`tiled.client.dataframe.DataFrameClient`; `"container"` goes to
`tiled.clide.container.Container`. Those classes then manage further communication
with Tiled server to access their contents.

Each item always has exactly one `structure_family`, and it's always from a
fixed list. In addition, it may have a list of `specs`, labels which are meant
to communicate some more specific expectations about the data that may or may
not have meaning to a given client. If a client does not recognize some spec,
it can still access the metadata and data and performed Tiled's essential
functions. If it does recognize a spec, it can provide an upgraded user
experience.

## Example

Suppose data labeled with the `xdi` spec is guaranteed to have a metadata
dictionary containing the following two entries:

```py
x.metadata["XDI"]["Element"]["Symbol"]
x.metadata["XDI"]["Element"]["Edge"]
```

When the Tiled client encounters this type of data, we would like to hand
the user a custom Python object that includes the information in the string
representation displayed by the Python interpreter (or Jupyter notebook).

```py
import tiled.client.dataframe


class XDIDatasetClient(tiled.client.dataframe.DataFrameClient):
    "A custom DataFrame client for DataFrames that have XDI metadata."

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Optional sanity check to ensure this cannot be accidentally
        # registered for use with data that is not a table.
        assert self.item["attributes"]["structure_family"] == "table"

    def __repr__(self):
        md = self.metadata["XDI"]
        return f'<{x.item["id"]} {md["Element"]["Symbol"]} {md["Element"]["Edge"]}>'
```

```{note}

**An Aside of Python `__repr__`**

Python calls `__repr__` to obtain a string representation of it for interactive
display as in:

````py
>>> my_object
<...>
````

It is conventional to include angle brackets `<>` when the string is not valid
Python code, as opposed to

````py
>>> 1
1
````

where the string representation is the exactly the code you would run to
reproduce that object.

You may want to override `__str__` in addition, which determines what
is returned by `str(...)` and displayed by `print(...)`. If you override
`__str__` but not `__repr__`, then `__repr__` falls back to `__str__`.
```

Now we want to configure Tiled to this class whenever it encounters
data labeled with the `xdi` spec. We'll register it manually for development
and testing. Then we'll see how to configure it seamlessly for the user.

```py
from tiled.client.container import DEFAULT_STRUCTURE_CLIENT_DISPATCH
from tiled.client import from_uri

custom = dict(DEFAULT_STRUCTURE_CLIENT_DISPATCH["numpy"])
custom["xdi"] = XDIDatasetClient
client = from_uri("https://...", structure_clients=custom)
```

Test by accessing a dataset and checking the type:

```py
type(client[...])
```


Python
[entry points](https://packaging.python.org/en/latest/specifications/entry-points/)
allow Tiled to efficiently scan the software environment for third-party
packages that provide custom Tiled clients. (Crucially, for speed, it does _not_
need to import a package to discover what if any custom Tiled clients it
includes. The entry points declarations can be read statically.) To register a
custom Tiled client from a third party package, add this to the `setup.py`:

```py
# setup.py
setup(
    entry_points={
        "tiled.special_client": [
            "xdi = my_package.my_module:XDIDatasetClient",
        ],
    },
)
```

On the left side of the `=` is the spec name, and on the right is the import
path to the custom object we want Tiled to use. Beware the somewhat unusual
syntax, in particular the colon between the final module and the object.

Re-install the package after adding or editing the entry points. Notice that
even in "editable" installations (i.e. `pip install -e ...`) a re-install step
is needed to register the entry point.

Then, Tiled should be able to automatically discover the custom class with
no change in the user's process:

```py
from tiled.client import from_uri

client = from_uri("https://...")
```

When Tiled see an `xdi` spec, it will query the entry points for a client registered
with that spec. It will discover the custom Python package, import the relevant
class, and use it.

## Precedence

A given item may have multiple specs (or none). It always has exactly one structure family.
It's possible that clients have been registered for multiple specs in the in the list.
Tiled walks the spec list in order and uses the first one that it recognizes. If it
recognizes none of the specs, or if there are no specs, it falls back to using the
structure family. Specs should generally be sorted from most specific to least
specific, so that Tiled uses the most finely-tuned client object available.

## More Possibilities and Design Guidelines

There are many other useful things we could do with a custom client that is purpose-built
for a specific kinds of data and/or metadata. We can add convenience properties
to quickly access certain metadata.

```py
class CustomClient(...):
    @property
    def element(self):
        return self.metadata["XDI"]["Element"]["Symbol"]
```

We can add convenience methods that read certain sections of the data and perhaps do
light computation on the way out.

```py
class CustomClient(...):
    def energy(self):
        # Read energy column
        return self["energy"][:] * UNIT_CONVERSION
```

We offer two guidelines to help your custom clients compose well with Tiled and
with other scientific Python libraries.

1. In your subclass, _add_ methods, attributes, and properties, but do not
   _change_ the behavior of the existing methods, attributes, and properties in
   the base class. This is a well-known
   [principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle) in
   software design generally, and it is especially crucial here. If a user runs
   code in a software environment where the library with the custom objects
   happens to be missing, we want the user to immediate notice the missing
   methods, not get confusingly different results from the "standard" method
   and the customized one. In addition, it is helpful if the "vanilla" Tiled
   documentation and user knowledge transfers to the custom classes with
   _additions_ but no confusing _changes_.
2. If something custom will do I/O (i.e. download metadata or data from the
   server) make it method, not a property. Properties that do "surprise" I/O
   may block over a slow network and can be very confusing. The same guideline
   applies if the property performs more just very light computation.
