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.