Containers

Containers serve two primary roles:

  • Identifying how to instantiate the object it represents (by way of class name, args, and keyword arguments).

  • Storing pertinent and structured metadata for the given instance.

Containers are created by subclassing from the built-in HappiItem class. The metadata associated with the instance is broken up into fields or “entries” of type EntryInfo.

EntryInfo

In order to regulate and template the information that gets entered into the Happi database, we use the concept of containers. This allows a developer to specify fields that are essential to every instance of a specific type.

These fields are specified using an instance of EntryInfo. .EntryInfo provides several primary features:

  • Mark a field as required (or optional)

  • Add default values when unspecified

  • Enforce - or validate - a certain format for the field

class happi.EntryInfo(doc=None, optional=True, enforce=None, default=None)

A piece of information related to a specific device

These are entered as class attributes for a given device container. They help control the information entered into a device

Parameters
  • doc (str) – A short string to document the device

  • optional (bool, optional) – By default all EntryInfo is optional, but in certain cases you may want to demand a particular piece of information upon initialization

  • enforce (type, list, compiled regex, optional) – Specify that all entered information is entered in a specific format. This can either by a Python type i.e. int, float e.t.c., a list of acceptable values, or a compiled regex pattern i.e re.compile(...)

  • default (optional) – A default value for the trait to have if the user does not specify. Keep in mind that this should be the same type as enforce if you are demanding a certain type.

Raises

ContainerError: – If there is an error with the way the enforced value interacts with its default value, or if the piece of information entered is unenforcable based on the the settings

Example

class MyDevice(Device):

    my_field = EntryInfo('My generated field')
    number   = EntryInfo('Device number', enforce=int, default=0)
enforce_value(value)

Enforce the rules of the EntryInfo

Parameters

value

Returns

Identical to the provided value except it may have been converted to the correct type

Return type

value

Raises

ValueError: – If the value is not the correct type, or does not match the pattern

HappiItem

In order to ensure that information is entered into the database in an organized fashion, the client will only accept classes that inherit from HappiItem. Each item will have the key information represented as class attributes, available to be manipulated like any other regular property

Editing the information for a container is a simple as:

In [1]: from happi import HappiItem

In [2]: item = HappiItem(name='my_device')

In [3]: item.name = 'new_name'

Note

happi.Device class is deprecated due to ambiguous name, conflicting with ophyd.Device.

Example Container

In order to show the flexibility of the EntryInfo class, we’ll put together a new example container. The class can be invoked in the same way you would usually handle class inheritance, the only difference is that you specify class attributes as EntryInfo objects:

In [4]: import re

In [5]: from happi import HappiItem, EntryInfo

In [6]: class MyItem(HappiItem):
   ...:     """My new item, with a known model number."""
   ...:     model_no      = EntryInfo('Model Number of Item', optional=False)
   ...:     count         = EntryInfo('Count of Item', enforce=int, default=0)
   ...:     choices       = EntryInfo('Choice Info', enforce=['a','b','c'])
   ...:     no_whitespace = EntryInfo('Enforce no whitespace',
   ...:                                enforce = re.compile(r'[\S]*$'))
   ...: 

By default, EntryInfo will create an optional init keyword argument with a default of None with the same name as the class attribute. A quick way to see how this information will be put into the the database is taking a look at dict(item):

In [7]: item = MyItem(name="my_item", model_no="QABC1234")

In [8]: dict(item)
Out[8]: 
{'name': 'my_item',
 'device_class': None,
 'args': [],
 'kwargs': {},
 'active': True,
 'documentation': None,
 'model_no': 'QABC1234',
 'count': 0,
 'choices': None,
 'no_whitespace': None}

As shown in the example above, using the EntryInfo keywords, you can put a short doc string to give a better explanation of the field, and also enforce that user enter a specific format of information.

While the user will always be able to enter None for the attribute, if a real value is given it will be checked against the specified enforce keyword, raising ValueError if invalid. Here is a table for how the EntryInfo check the type

Enforce

Method of enforcement

None

Any value will work

type

type(value)

list

list.index(value)

regex

regex.match(value) != None

Fields that are important to the item can be marked as mandatory (optional=False); these should have no default value.

When entering information you will not neccesarily see a difference between optional and mandatory EntryInfo, however the database client will reject the item if these fields do not have the requisite values set.

Loading your Object

A container’s primary role is containing the information necessary to load the Python representation of an object.

Internally, happi keeps track of all containers by way of its registry, the HappiRegistry.

This information is stored as a device_class, args and kwargs. The former stores a string that indicates the Python class of the item, the other two indicate the information that is needed to instantiate it. With this information both from_container() and load_device() will handle importing modules and instantiating your object.

Note

happi will attach the original metadata with a fixed attribute name .md to your object. You can use this to keep track of the container metadata used to instantiate your object.

This can be disabled by setting attach_md to False in from_container().

Often information contained in the args or kwargs will be duplicated in other parts of the container. For instance most ophyd objects will want a name and prefix on initialization. Instead of repeating that information you can just use a template and have the information automatically populated for you by the container itself. For instance, in the aforementioned example container.args = ["{{name}}"] would substitute the attribute listed as container.name in as an argument. If the template contains the substituted attribute alone the type will also be converted.

In [9]: from happi import from_container

In [10]: container = MyItem(name="my_item", model_no="QABC1234",
   ....:                    device_class='ophyd.sim.SynSignal',
   ....:                    kwargs={'name': '{{name}}'})
   ....: 

In [11]: obj = from_container(container, attach_md=True)

In [12]: obj
Out[12]: SynSignal(name='my_item', value=0, timestamp=1609968502.747005)

Integrating with your package

Happi provides some containers for your convenience, but intentionally does not support all use cases or control systems in a monolithic fashion.

The suggested method for supporting your new package would be to make your package have a dependency on happi, of course, and subclass .HappiItem to make a new container in your own package.

Then, add an entry point specified by the happi.containers keyword to your package setup.py. An example entry points can be found here.

HappiRegistry takes care of loading the entry points and making them available throughout the library.

Built-in Container Conventions

In order for the database to be as easy to parse as possible we need to establish conventions on how information is entered. Please read through this before entering any information into the database.

HappiItem Entries

These are fields that are common to all Happi items.

name

This is simply a short name we can use to refer to the item.

device_class

This is the full class name, which lets happi know how to instantiate your item.

The device_class name remains for backward-compatibility reasons. Thinking of it as class_name or creator_callable would be more apt.

Note

This may also be the name of a factory function - happi only cares that it’s callable.

args

Argument list to be passed on instantiation. May contain templated macros such as {{name}}.

kwargs

Keyword argument dictionary to be passed on instantiation. May contain templated macros such as {{name}}.

active

Is the object actively deployed?

documentation

A brief set of information about the object.

OphydItem entries

ophyd has first-class support in happi - but not much is required on top of the base HappiItem to support it.

prefix

This should be the prefix for all of the PVs contained within the device. It does not matter if this is an invalid record by itself.

LCLSItem entries

This class is now part of pcdsdevices. It remains documented here as PCDS is the original developer and primary user of happi as of the time of writing. If you intend to use the same metadata that we do, please copy and repurpose the LCLSItem class.

beamline

Beamline is required. While it is expected to be one of the following, it is not enforced:

KFE
LFE
MEC
MFX
PBT
TMO
TXI
XCS
XPP

z

Position of the device on the z-axis in the lcls coordinates.

location_group

The group of this device in terms of its location. This is primarily used for LUCID’s grid.

functional_group

The group of this device in terms of its function. This is primarily used for LUCID’s grid.

stand

Acronym for stand, must be three alphanumeric characters like an LCLSI stand (e.g. DG3) or follow the LCLSII stand naming convention (e.g. L0S04).

lightpath

If the device should be included in the LCLS Lightpath.

embedded_screen

Path to an embeddable PyDM control screen for this device.

detailed_screen

Path to a detailed PyDM control screen for this device.

engineering_screen

Path to a detailed engineering PyDM control screen for this device.