Containers

In order to regulate and template the information that gets entered into the Happi database, we use the concept of containers. Containers serve two primary roles:

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

  • Storing pertinent and structured metadata for the given instance.

Containers are created by instantiating the HappiItem class or a subclass of it. The metadata associated with the instance is broken up into fields or “entries” of type EntryInfo. This allows a developer to specify fields that are essential to every instance of a specific container type.

EntryInfo

These fields are specified using an instance of EntryInfo. This class 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: str | None = None, optional: bool | None = True, enforce: Any | None = None, default: Any | None = None, enforce_doc: str | None = None, include_default_as_kwarg: bool = True)

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, or function, 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, a compiled regex pattern i.e re.compile(...), or custom handling by passing a function that takes in one argument, the value. This function must do one of: return the value back as-is, return the value back converted to a corrected form, or raise a EnforceError.

  • 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.

  • enforce_doc (str, optional) – A human-readable explanation of the enforce field. Will be printed if the entered information does not follow the enforce type

  • include_default_as_kwarg (bool, optional) – Defaults to True. If a kwargs EntryInfo sets this to False, all kwargs will be compared to their corresponding Entries in the item and omitted from the kwargs dictionary if their value matches the Entry’s default. This can also be set on an individual Entry basis. The setting on an individual entry will only be taken into consideration if the kwarg EntryInfo has this set to True (default).

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 MyItem(HappiItem):
    my_field = EntryInfo('My generated field')
    number   = EntryInfo('Device number', enforce=int, default=0,
                         enforce_doc='This must be a number')
enforce_value(value)

Enforce the rules of the EntryInfo.

Accepts a value, verifies that it meets the criteria specified in the enforce attribute, and returns the same value, except that it will be converted to the correct type if enforce is a type.

Raises:

EnforceError – 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. happi.HappiItem should be used instead.

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]*$'),
   ...:                                enforce_doc = 'This item cannot have whitespace')
   ...: 

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 docstring 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

function

function(value)

If your enforce condition is complicated or obfuscated, you can add a docstring using the enforce_doc keyword that explains the rule. (This may be helpful for regex matches which are difficult for humans to read)

Fields that are important to the item can be marked as mandatory with optional=False and should have no default value.

When entering information you will not necessarily 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() are able to 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 value of 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=1729549771.113104)

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’s pyproject.toml. 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:

CXI
HXD
ICL
KFE
LFE
MEC
MFX
PBT
RIX
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.