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 Containers, see
Containers in order to see more about how they are created.
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, location_group="Loc1", functional_group="Func1", device_class='types.SimpleNamespace', args=[])
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, location_group="Loc2", functional_group="Func2")
In [8]: client.add_device(device)
Out[8]: 'my_device2'
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.
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
:
In [9]: results = client.search(type='Device')
This returns a list of zero or more SearchResult
instances, which can
be used to introspect metadata or even instantiate the corresponding device
instance.
Working with the SearchResult¶
Representing a single search result from Client.search
and its variants, a
SearchResult
can be used in multiple ways.
This result can be keyed for metadata as in:
In [10]: result = results[0]
In [11]: result['name']
Out[11]: 'my_device'
The HappiItem
can be readily retrieved:
In [12]: result.item
Out[12]: Device (name=my_device, prefix=PV:BASE, z=345.5)
In [13]: type(result.item)
Out[13]: happi.device.Device
Or the object may be instantiated:
In [14]: result.get()
Out[14]:
namespace(name='my_device',
md=Device (name=my_device, prefix=PV:BASE, z=345.5))
See that SearchResult.get()
returns the class we expect, based on the
device_class.
In [15]: result['device_class']
Out[15]: 'types.SimpleNamespace'
In [16]: type(result.get())
Out[16]: types.SimpleNamespace
There are also some more advance methods to search specific areas of the beamline or use programmer-friendly regular expressions, described in the upcoming sections.
Searching for items on a beamline¶
To search for items on a beamline such as MFX, one would use the following:
In [17]: client.search(type='Device', beamline='MFX')
Out[17]: [SearchResult(client=<happi.client.Client object at 0x7fb8785c3160>, metadata={'name': 'my_device2', 'device_class': None, 'args': ['{{prefix}}'], 'kwargs': {'name': '{{name}}'}, 'active': True, 'documentation': None, 'prefix': 'PV:BASE2', 'beamline': 'MFX', 'location_group': 'Loc2', 'functional_group': 'Func2', 'z': 355.5, 'stand': None, 'detailed_screen': None, 'embedded_screen': None, 'engineering_screen': None, 'system': None, 'macros': None, 'lightpath': False, 'parent': None, '_id': 'my_device2', 'creation': 'Wed Jan 6 13:28:22 2021', 'last_edit': 'Wed Jan 6 13:28:22 2021', 'type': 'Device'})]
Searching a range¶
Searching a Z-range on the beamline, or a range with any arbitrary key is also
easy by way of Client.search_range()
. For example:
In [18]: client.search_range('z', start=314.4, end=348.6, type='Device')
Out[18]: [SearchResult(client=<happi.client.Client object at 0x7fb8785c3160>, metadata={'name': 'my_device', 'device_class': 'types.SimpleNamespace', 'args': [], 'kwargs': {'name': '{{name}}'}, 'active': True, 'documentation': None, 'prefix': 'PV:BASE', 'beamline': 'XRT', 'location_group': 'Loc1', 'functional_group': 'Func1', 'z': 345.5, 'stand': None, 'detailed_screen': None, 'embedded_screen': None, 'engineering_screen': None, 'system': None, 'macros': None, 'lightpath': False, 'parent': None, '_id': 'my_device', 'creation': 'Wed Jan 6 13:28:22 2021', 'last_edit': 'Wed Jan 6 13:28:22 2021', 'type': 'Device'})]
This would return all devices between Z=314.4 and Z=348.6.
Any numeric key can be filtered in the same way, replacing ‘z’ with the key name.
Searching with regular expressions¶
Any key can use a regular expression for searching by using Client.search_regex()
In [19]: client.search_regex(name='my_device[2345]')
Out[19]: [SearchResult(client=<happi.client.Client object at 0x7fb8785c3160>, metadata={'name': 'my_device2', 'device_class': None, 'args': ['{{prefix}}'], 'kwargs': {'name': '{{name}}'}, 'active': True, 'documentation': None, 'prefix': 'PV:BASE2', 'beamline': 'MFX', 'location_group': 'Loc2', 'functional_group': 'Func2', 'z': 355.5, 'stand': None, 'detailed_screen': None, 'embedded_screen': None, 'engineering_screen': None, 'system': None, 'macros': None, 'lightpath': False, 'parent': None, '_id': 'my_device2', 'creation': 'Wed Jan 6 13:28:22 2021', 'last_edit': 'Wed Jan 6 13:28:22 2021', 'type': 'Device'})]
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 [20]: my_motor = client.find_device(prefix='PV:BASE')
In [21]: my_motor.z = 425.4
In [22]: my_motor.save()
Out[22]: 'my_device'
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 [23]: device_1 = client.find_device(name='my_device')
In [24]: device_2 = client.find_device(name='my_device2')
In [25]: 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
.