Using the Client

Users will interact with the database by using the happi.Client, this will handle the authentication, and methods for adding, editing and removing devices.

Happi is incredibly flexible, allowing us to put arbitrary key-value pair information into the databse. While this will make adding functionality easy in the future, it also means that any rules on the structure of the data we allow will need to be performed by the happi.Client itself. To make this intuitive, the client deals primarily with objects we will call Device Containers, see Device Containers in order to see more about how the devices are created. However the basic use cases for the client can be demonstrated without much knowledge of how the Device container works.

Creating a New Entry

A new device must be a subclass of the basic container Device. While you are free to use the initialized object whereever you see fit, the client has a hook to create new devices.

Before we can create our first client, we need to create a backend for our device information to be stored.

In [1]: from happi.backends.json_db import JSONBackend

In [2]: db = JSONBackend(path='doc_test.json', initialize=True)

If you are connecting to an existing database you can pass the information directly into the Client itself at __init__`. See Selecting a Backend about how to configure your default backend choice

In [3]: from happi import Client, Device

In [4]: client = Client(path='doc_test.json')

In [5]: device = client.create_device("Device", name='my_device',prefix='PV:BASE', beamline='XRT', z=345.5)

In [6]: device.save()

Alternatively, you can create the device separately and add the device explicitly using Device.save()

In [7]: device = Device(name='my_device2',prefix='PV:BASE2', beamline='MFX', z=355.5)

In [8]: client.add_device(device)

The main advantage of the first method is that all of the container classes are already stored in the Client.device_types dictionary so they can be easily accessed with a string. Keep in mind, that either way, all of the mandatory information needs to be given to the device before it can be loaded into the database. For more information on device creation see Device Containers.

Searching the Database

There are two ways to load information from the database Client.find_device() and Client.search(). The former should only be used to load one device at at a time. Both accept criteria in the from of keyword-value pairs to find the device or device/s you desire. Here are some example searches to demonstrate the power of the Happi Client

First, lets look for all the devices of type generic Device, as first their corresponding objects or as a dictionary

In [9]: client.search(type='Device')
Out[9]: 
[Device my_device (prefix=PV:BASE, z=345.5),
 Device my_device2 (prefix=PV:BASE2, z=355.5)]

In [10]: client.search(type='Device', as_dict=True)
Out[10]: 
[{'_id': 'PV:BASE',
  'active': True,
  'args': ['{{prefix}}'],
  'beamline': 'XRT',
  'creation': 'Sun Feb 11 18:25:31 2018',
  'device_class': None,
  'kwargs': {'name': '{{name}}'},
  'last_edit': 'Sun Feb 11 18:25:31 2018',
  'macros': None,
  'name': 'my_device',
  'parent': None,
  'prefix': 'PV:BASE',
  'screen': None,
  'stand': None,
  'system': None,
  'type': 'Device',
  'z': 345.5},
 {'_id': 'PV:BASE2',
  'active': True,
  'args': ['{{prefix}}'],
  'beamline': 'MFX',
  'creation': 'Sun Feb 11 18:25:31 2018',
  'device_class': None,
  'kwargs': {'name': '{{name}}'},
  'last_edit': 'Sun Feb 11 18:25:31 2018',
  'macros': None,
  'name': 'my_device2',
  'parent': None,
  'prefix': 'PV:BASE2',
  'screen': None,
  'stand': None,
  'system': None,
  'type': 'Device',
  'z': 355.5}]

There are also some more advance methods to search specific areas of the beamline

In [11]: client.search(type='Device', beamline='MFX')
Out[11]: [Device my_device2 (prefix=PV:BASE2, z=355.5)]

In [12]: client.search(type='Device', start=314.4, end=348.6)
Out[12]: [Device my_device (prefix=PV:BASE, z=345.5)]

You can also explicitly load a single device. The advantage of this method is you won’t have to parse a list of returned devices. If nothing meets your given criteria, an SearchError will be raised

In [13]: device =  client.find_device(prefix='PV:BASE2')

In [14]: print(device.prefix, device.name)
PV:BASE2 my_device2

In [15]: try:
   ....:     client.find_device(name='non-existant')
   ....: except Exception as exc:
   ....:     print(exc)
   ....: 
No device information found that matches the search criteria

Editing Device Information

The workflow for editing a device looks very similar to the code within Creating a New Entry, but instead of instantiating the device you use either Client.find_device() or Client.search() to grab an existing device from the dataprefix. When the device is retreived this way the class method Device.save() is overwritten, simply call this when you are done editing the Device information.

In [16]: my_motor = client.find_device(prefix='PV:BASE')

In [17]: my_motor.z = 425.4

In [18]: my_motor.save()

Note

Because the database uses the prefix key as a device’s identification you can not edit this information in the same way. Instead you must explicitly remove the device and then use Client.add_device() to create a new entry.

Finally, lets clean up our example objects by using Client.remove_device() to clean them from the database

In [19]: device_1 = client.find_device(name='my_device')

In [20]: device_2 = client.find_device(name='my_device2')

In [21]: for device in (device_1, device_2):
   ....:     client.remove_device(device)
   ....: 

Selecting a Backend

Happi supports both JSON and MongoDB backends. You can always import your chosen backend directly, but in order to save time you can create an environment variable HAPPI_BACKEND and set this to "mongodb". This well tell the library to assume you want to use the MongoBackend. Otherwise, the library uses the JSONBackend.