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
items.
Happi is incredibly flexible, allowing us to put arbitrary key-value pair
information into the database. 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 Client
itself. To make this
intuitive, the Client deals primarily with Containers, which are objects
that hold and specify these rules.
Creating a New Entry
A new item must be a subclass of the basic HappiItem
container.
While you are free to use the initialized object wherever you see fit, the client
has a hook to create new items.
Before we can create our first client, we need to create a backend for our item 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, HappiItem
In [4]: client = Client(path='doc_test.json')
In [5]: item = client.create_item(
...: "HappiItem",
...: name="my_device",
...: device_class="types.SimpleNamespace",
...: args=[],
...: kwargs={},
...: position=345.5, # <- this is an extra field which happi allows
...: )
...:
In [6]: item
Out[6]: HappiItem (name=my_device)
In [7]: item.save()
For this example, we have added an “extraneous” field to the item called “position”. This is something that happi allows for. If you wish to make this a recognized field of an eforced type (e.g., don’t allow the user to make position a string value instead of a floating point value), please see the documentation on making your own container class.
Alternatively, you can create the item separately and add it explicitly using
HappiItem.save()
In [8]: item = HappiItem(
...: name="my_device2",
...: device_class="types.SimpleNamespace",
...: position=355.5, # <- this is an extra field which happi allows
...: )
...:
In [9]: item
Out[9]: HappiItem (name=my_device2)
In [10]: client.add_item(item)
Out[10]: 'my_device2'
The main advantage of the first method is that all of the container classes are already managed by the client 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 item before it can be loaded into the database.
Searching the Database
There are several ways to load information from the database
Client.find_item()
, Client.search()
, and dictionary-like access.
Client.find_item()
is intended to only load one item at at a time. Both
accept criteria in the from of keyword-value pairs to find the item or items
you desire.
You can quickly query the client by item name and get a SearchResult
that
can be used to introspect metadata or even instantiate the corresponding item
instance.
In [11]: result = client["my_device"]
The client acts as a Python mapping, so you may inspect it as you would a dictionary. For example:
# All of the item names:
In [12]: list(client.keys())
Out[12]: ['my_device', 'my_device2']
# All of the database entries as SearchResults:
In [13]: list(client.values())
Out[13]:
[SearchResult(client=<happi.client.Client object at 0x7f6d20be18b0>, metadata={'name': 'my_device', 'device_class': 'types.SimpleNamespace', 'args': [], 'kwargs': {}, 'active': True, 'documentation': None, '_id': 'my_device', 'creation': 'Fri Dec 20 19:42:10 2024', 'last_edit': 'Fri Dec 20 19:42:10 2024', 'position': 345.5, 'type': 'HappiItem'}),
SearchResult(client=<happi.client.Client object at 0x7f6d20be18b0>, metadata={'name': 'my_device2', 'device_class': 'types.SimpleNamespace', 'args': [], 'kwargs': {}, 'active': True, 'documentation': None, '_id': 'my_device2', 'creation': 'Fri Dec 20 19:42:10 2024', 'last_edit': 'Fri Dec 20 19:42:10 2024', 'position': 355.5, 'type': 'HappiItem'})]
# Pairs of (name, SearchResult):
In [14]: list(client.items())
Out[14]:
[('my_device',
SearchResult(client=<happi.client.Client object at 0x7f6d20be18b0>, metadata={'name': 'my_device', 'device_class': 'types.SimpleNamespace', 'args': [], 'kwargs': {}, 'active': True, 'documentation': None, '_id': 'my_device', 'creation': 'Fri Dec 20 19:42:10 2024', 'last_edit': 'Fri Dec 20 19:42:10 2024', 'position': 345.5, 'type': 'HappiItem'})),
('my_device2',
SearchResult(client=<happi.client.Client object at 0x7f6d20be18b0>, metadata={'name': 'my_device2', 'device_class': 'types.SimpleNamespace', 'args': [], 'kwargs': {}, 'active': True, 'documentation': None, '_id': 'my_device2', 'creation': 'Fri Dec 20 19:42:10 2024', 'last_edit': 'Fri Dec 20 19:42:10 2024', 'position': 355.5, 'type': 'HappiItem'}))]
You could, for example, grab the first key by name and access it using
__getitem__
:
In [15]: key_0 = list(client)[0]
In [16]: key_0
Out[16]: 'my_device'
In [17]: client[key_0]
Out[17]: SearchResult(client=<happi.client.Client object at 0x7f6d20be18b0>, metadata={'name': 'my_device', 'device_class': 'types.SimpleNamespace', 'args': [], 'kwargs': {}, 'active': True, 'documentation': None, '_id': 'my_device', 'creation': 'Fri Dec 20 19:42:10 2024', 'last_edit': 'Fri Dec 20 19:42:10 2024', 'position': 345.5, 'type': 'HappiItem'})
Or see how many entries are in the database:
In [18]: len(client)
Out[18]: 2
Here’s a search that gets all the items of type generic HappiItem
:
In [19]: results = client.search(type="HappiItem")
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 [20]: result = results[0]
In [21]: result['name']
Out[21]: 'my_device'
The HappiItem
can be readily retrieved:
In [22]: result.item
Out[22]: HappiItem (name=my_device)
In [23]: type(result.item)
Out[23]: happi.item.HappiItem
Or the object may be instantiated:
In [24]: result.get()
Out[24]: namespace(md=HappiItem (name=my_device))
See that SearchResult.get()
returns the class we expect, based on the
device_class
.
In [25]: result['device_class']
Out[25]: 'types.SimpleNamespace'
In [26]: type(result.get())
Out[26]: 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 [27]: client.search(type='HappiItem', beamline='MFX')
Out[27]: []
Searching a range
In this example, we have added an extraneous field position
that is not
present normally in the HappiItem
container.
We can search a range of values with any arbitrary key using
Client.search_range()
. For example:
In [28]: client.search_range("position", start=314.4, end=348.6)
Out[28]: [SearchResult(client=<happi.client.Client object at 0x7f6d20be18b0>, metadata={'name': 'my_device', 'device_class': 'types.SimpleNamespace', 'args': [], 'kwargs': {}, 'active': True, 'documentation': None, '_id': 'my_device', 'creation': 'Fri Dec 20 19:42:10 2024', 'last_edit': 'Fri Dec 20 19:42:10 2024', 'position': 345.5, 'type': 'HappiItem'})]
This would return all items between positions 314.4 and 348.6.
Any numeric key can be filtered in the same way, replacing 'position'
with
the key name.
Searching with regular expressions
Any key can use a regular expression for searching by using Client.search_regex()
In [29]: client.search_regex(name='my_device[2345]')
Out[29]: [SearchResult(client=<happi.client.Client object at 0x7f6d20be18b0>, metadata={'name': 'my_device2', 'device_class': 'types.SimpleNamespace', 'args': [], 'kwargs': {}, 'active': True, 'documentation': None, '_id': 'my_device2', 'creation': 'Fri Dec 20 19:42:10 2024', 'last_edit': 'Fri Dec 20 19:42:10 2024', 'position': 355.5, 'type': 'HappiItem'})]
Editing Item Information
The workflow for editing an item looks very similar to the code within
Creating a New Entry, but instead of instantiating the item you use either
Client.find_item()
or Client.search()
. When the item is retrieved
this way the class method HappiItem.save()
is overwritten, simply call
this when you are done editing.
In [30]: my_motor = client.find_item(name="my_device")
In [31]: my_motor.position = 425.4
In [32]: my_motor.save()
Out[32]: 'my_device'
Note
Because the database uses the name
key as an item’s identification you
can not edit this information in the same way. Instead you must explicitly
remove the item and then use Client.add_item()
to create a new
entry.
Finally, lets clean up our example objects by using
Client.remove_item()
to clean them from the database
In [33]: item_1 = client.find_item(name='my_device')
In [34]: item_2 = client.find_item(name='my_device2')
In [35]: for item in (item_1, item_2):
....: client.remove_item(item)
....:
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
.