def test_create_item(happi_client: Client, item_info: Dict[str, Any]): item = happi_client.create_item(OphydItem, **item_info) assert item.prefix == item_info['prefix'] assert item.name == item_info['name'] # Invalid Entry with pytest.raises(TypeError): happi_client.create_item(int)
def test_choices_for_field(happi_client: Client): name_choices = happi_client.choices_for_field('name') assert name_choices == {'alias'} prefix_choices = happi_client.choices_for_field('prefix') assert prefix_choices == {'BASE:PV'} with pytest.raises(SearchError): happi_client.choices_for_field('not_a_field')
def test_validate(happi_client: Client): # No bad items assert happi_client.validate() == list() # A single bad item happi_client.backend.save('_id', {happi_client._id_key: 'bad'}, insert=True) assert happi_client.validate() == ['bad']
def __init__(self): self._happi_db_dirs = [happi_site_dir, happi_user_dir] self._device_view = HappiClientTreeView() self._client_model = HappiClientModel() self._device_view.setModel(self._client_model) for db_dir in self._happi_db_dirs: for db_file in Path(db_dir).glob('*.json'): client = Client(path=str(db_file)) self._client_model.add_client(client) try: mongo_client = Client( MongoBackend(host='127.0.0.1', db='happi', collection='labview_static', user=USER_MONGO, pw=PW_MONGO, timeout=None)) self._client_model.add_client(mongo_client) except Exception as e: #TODO catch exception properly msg.logError(e) widget = QWidget() layout = QVBoxLayout() layout.addWidget(self._device_view) widget.setLayout(layout) icon = QIcon(str(static.path('icons/calibrate.png'))) name = "Devices" super(HappiSettingsPlugin, self).__init__(icon, name, widget) self._device_view.expandAll() self.restore()
def test_export(happi_client: Client, valve: OphydItem): # Setup client with both items happi_client.add_item(valve) fd, temp_path = tempfile.mkstemp() happi_client.export(open(temp_path, 'w+'), sep=',', attrs=['name', 'prefix']) exp = open(temp_path, 'r').read() assert "alias,BASE:PV" in exp assert "name,BASE:VGC:PV" in exp # Cleanup os.remove(temp_path) os.close(fd)
def test_find_document(happi_client: Client, item_info: Dict[str, Any]): doc = happi_client.find_document(**item_info) assert doc.pop('prefix') == item_info['prefix'] assert doc.pop('name') == item_info['name'] # Remove id and check prefix = item_info.pop('prefix') doc = happi_client.find_document(**item_info) assert doc.pop('prefix') == prefix assert doc.pop('name') == item_info['name'] # Not available with pytest.raises(SearchError): doc = happi_client.find_document(prefix='Does not Exist')
def test_change_item_name(happi_client: Client, item_info: Dict[str, Any]): item = happi_client.find_item(**item_info) assert item.name != 'new_name' item.name = 'new_name' item.save() # old entry should not exist anymore with pytest.raises(SearchError): happi_client.find_item(**item_info) # new entry with new name should be there new_item = happi_client.find_item(name=item.name) # prefix or other attributes should be the same assert new_item.prefix == item.prefix # we should only have one deivce now which is the new one assert happi_client.all_items == [new_item]
def mockjsonclient(): # Write underlying database with open('testing.json', 'w+') as handle: simplejson.dump({device_info()['prefix']: device_info()}, handle) # Return handle name db = JSONBackend('testing.json') return Client(database=db)
def test_find_item(happi_client: Client, item_info: Dict[str, Any]): item = happi_client.find_item(**item_info) assert isinstance(item, OphydItem) assert item.prefix == item_info['prefix'] assert item.name == item_info['name'] # Test edit and save item.stand = 'DG3' item.save() loaded_item = happi_client.find_item(**item_info) assert loaded_item.prefix == item_info['prefix'] assert loaded_item.name == item_info['name'] # Bad load bad = {'a': 'b'} happi_client.backend.save('a', bad, insert=True) with pytest.raises(EntryError): happi_client.find_item(**bad)
def test_beckoff_axis_device_class(mockqsbackend): c = Client(database=mockqsbackend) d = load_devices(*c.all_items).__dict__ vh_y = d.get('vh_y') sam_x = d.get('sam_x') assert vh_y.__class__.__name__ == 'BeckhoffAxis' assert sam_x.__class__.__name__ == 'IMS'
def test_qsbackend_with_acromag(mockqsbackend): c = Client(database=mockqsbackend) d = load_devices(*c.all_devices, pprint=False).__dict__ ai1 = d.get('ai_7') ao1 = d.get('ao_6') assert ai1.__class__.__name__ == 'EpicsSignalRO' assert ao1.__class__.__name__ == 'EpicsSignal'
def mockjsonclient(item_info: Dict[str, Any]): # Write underlying database with tempfile.NamedTemporaryFile(mode='w') as handle: simplejson.dump({item_info['name']: item_info}, handle) handle.flush() # flush buffer to write file # Return handle name db = JSONBackend(handle.name) yield Client(database=db)
def test_change_container_pass(happi_client: Client, item: str, target: str, request: pytest.FixtureRequest): i = request.getfixturevalue(item) t = request.getfixturevalue(target) kw = happi_client.change_container(i, t) for k in kw: assert i.post()[k] == kw[k]
def __init__(self, prefix='', *, name, client: happi.Client, devices: List[str]): super(CompoundDevice, self).__init__(prefix=prefix, name=name) self.devices = client.find_device()
def test_from_cfg(happi_cfg: str): # happi_cfg modifies environment variables to make config discoverable client = Client.from_config() # Internal db path should be constructed relative to the happi cfg dir expected_db = os.path.abspath( os.path.join(os.path.dirname(happi_cfg), 'db.json')) print(happi_cfg) assert isinstance(client.backend, JSONBackend) assert client.backend.path == expected_db
def test_qsbackend_with_client(mockqsbackend): c = Client(database=mockqsbackend) assert len(c.all_items) == 14 assert all( d.__class__.__name__ in {'Trigger', 'Motor', 'Acromag', 'LCLSItem'} for d in c.all_items) device_types = [device.__class__.__name__ for device in c.all_items] assert device_types.count('Motor') == 7 assert device_types.count('Trigger') == 2 # Acromag: six entries, but one erroneously has the same name assert device_types.count('Acromag') == 5
def mockmongoclient(item_info: Dict[str, Any]): with patch('happi.backends.mongo_db.MongoClient') as mock_mongo: mc = MongoClient() mc['test_db'].create_collection('test_collect') mock_mongo.return_value = mc # Client backend = MongoBackend(db='test_db', collection='test_collect') client = Client(database=backend) # Insert a single device client.backend._collection.insert_one(item_info) return client
def add_connection(self, channel): # If we haven't made a Client by the time we need the Plugin. Try # and load one from configuration file if not _client: register_client(Client.from_config()) try: super().add_connection(channel) except SearchError: logger.error("Unable to find device for %r in happi database.", channel) except Exception: logger.exception("Unable to load %r from happi", channel.address)
def test_qsbackend_with_client(mockqsbackend): from pcdsdevices.device_types import Motor, Trigger, Acromag c = Client(database=mockqsbackend) assert len(c.all_devices) == 14 assert all([ isinstance(d, Motor) or isinstance(d, Trigger) or isinstance(d, Acromag) for d in c.all_devices ]) device_types = [device.__class__ for device in c.all_devices] assert device_types.count(Motor) == 6 assert device_types.count(Trigger) == 2 assert device_types.count(Acromag) == 6
def make_tile_configs(tile, directory, dbpath): """ Write TILE IOC configs based on database entries. """ # Hack in our test device types registry._registry['pcdsdevices.Elliptec'] = Elliptec registry._reverse_registry[Elliptec] = 'pcdsdevices.Elliptec' registry._registry['pcdsdevices.Qmini'] = Qmini registry._reverse_registry[Qmini] = 'pcdsdevices.Qmini' registry._registry['pcdsdevices.SmarActMotor'] = SmarActMotor registry._reverse_registry[SmarActMotor] = 'pcdsdevices.SmarActMotor' registry._registry['pcdsdevices.SmarActTipTiltMotor'] = SmarActTipTiltMotor registry._reverse_registry[SmarActTipTiltMotor] = \ 'pcdsdevices.SmarActTipTiltMotor' registry._registry['pcdsdevices.LasBasler'] = LasBasler registry._reverse_registry[LasBasler] = 'pcdsdevices.LasBasler' registry._registry['pcdsdevices.ThorlabsWfs'] = ThorlabsWfs registry._reverse_registry[ThorlabsWfs] = 'pcdsdevices.ThorlabsWfs' registry._registry['pcdsdevices.ThorlabsPM101PowerMeter'] = \ ThorlabsPM101PowerMeter registry._reverse_registry[ThorlabsPM101PowerMeter] = \ 'pcdsdevices.ThorlabsPM101PowerMeter' registry._registry['pcdsdevices.EnvironmentalMontior'] = \ EnvironmentalMonitor registry._reverse_registry[EnvironmentalMonitor] = \ 'pcdsdevices.EnvironmentalMonitor' # Just use our test devices for now if tile not in ['lm1k4_com']: raise ValueError("Unrecognized TILE {}".format(tile)) client = Client(path=dbpath) results = client.search(location_group=tile) for dev in dev_map.keys(): # Aggregate devices based on container type devs = [result for result in results if result['type'] == dev] print("Found {} devices of type {}".format(len(devs), dev)) func = dev_map[devs[0]['type']] func(devs, directory)
def add_client(self, client: Client): self._clients.append(client) if isinstance(client.backend, JSONBackend): client_item = QStandardItem(client.backend.path) elif isinstance(client.backend, MongoBackend): client_item = QStandardItem( f"{client.backend._client.HOST}/{client.backend._collection.full_name}" ) self.appendRow(client_item) for result in client.search(): # add an OphydItem self.add_device(client_item, result.item) client_item.setData(client)
def test_add_item(happi_client: Client, valve: OphydItem): happi_client.add_item(valve) # No duplicates with pytest.raises(DuplicateError): happi_client.add_item(valve) # No incompletes d = OphydItem() with pytest.raises(EntryError): happi_client.add_item(d)
def test_remove_item(happi_client: Client, item: OphydItem, valve: OphydItem, item_info: Dict[str, Any]): happi_client.remove_item(item) assert list(happi_client.backend.find(item_info)) == [] # Invalid item with pytest.raises(ValueError): happi_client.remove_item(5) # Valve not in dictionary with pytest.raises(SearchError): happi_client.remove_item(valve)
def test_search_range(happi_client: Client, valve: OphydItem): happi_client.add_item(valve) # Search between two points res = happi_client.search_range('z', start=0, end=500) assert len(res) == 2 # Search between two points, nothing found res = happi_client.search_range('z', start=10000, end=500000) assert not res # Search without an endpoint res = happi_client.search_range('z', start=0) assert len(res) == 2 # Search invalid range with pytest.raises(ValueError): happi_client.search_range('z', start=1000, end=5)
def test_search(happi_client: Client, valve: OphydItem, item_info: Dict[str, Any]): happi_client.add_item(valve) res = happi_client.search(name=item_info['name']) # Single search return assert len(res) == 1 loaded_item = res[0].item assert loaded_item.prefix == item_info['prefix'] assert loaded_item.name == item_info['name'] # No results assert not happi_client.search(name='not') # Returned as dict res = happi_client.search(**item_info) loaded_item = res[0].item assert loaded_item['prefix'] == item_info['prefix'] assert loaded_item['name'] == item_info['name'] # Search off keyword res = happi_client.search(beamline='LCLS') assert len(res) == 2
def client(): client = Client(path=os.path.join(os.path.dirname(__file__), 'happi.json')) register_client(client) return client
def load_devices_from_happi(device_names, *, namespace, **kwargs): """ Load the devices from Happi based on the list of ``device_names``. The elements of the list may be strings (device name) or tuples of two strings (device name used for database search and the name with which the device is loaded in the script namespace). Two names may be needed in case the devices are stored in the database using compound names that contain beamline acronyms (e.g. ``abc_det`` and ``def_det`` for beamlines ABC and DEF) but they are expected to have the name ``det`` when loaded during ABC and DEF beamline startup respectively (see examples). The devices are loaded into a namespace referenced by ``namespace`` parameter. The function may be called multiple times in a row for the same namespace to populate it with results of multiple searches. The function also returns the list of names of loaded devices. Happi should be configured before the function could be used: Happi configuration file should be created and environment variable ``HAPPI_CFG`` with the path to the configuration file should be set. For example, if JSON Happi database is contained in the file ``path=/home/user/happi/database.json`` and the path to configuration file is ``path=/home/user/happi.ini``, then the environment variable should be set as .. code-block:: HAPPI_CFG=/home/user/happi.ini and configuration file ``happi.ini`` should contain .. code-block:: [DEFAULT] backend=json path=/home/user/happi/database.json Examples -------- .. code-block:: python # Load devices 'det1' and 'motor1'. load_devices_from_happi(["det1", "motor1"], namespace=locals()) # Works exactly the same as the previous example. If the second tuple element is # evaluated as boolean value of False, then it is ignored and the device is not renamed. load_devices_from_happi([("det1", ""), ("motor1", "")], namespace=locals()) # Load 'abc_det1' as 'det1' and 'abc_motor1' as 'motor1'. load_devices_from_happi([("abc_det1", "det1"), ("abc_motor1", "motor1")], namespace=locals()) # Get the dictionary of devices device_dict = {} load_devices_from_happi(["det1", "motor1"], namespace=device_dict) # 'device_dict': {"dev1": device1, "motor1": motor1} # Obtain the list of updated items (devices) item_list = load_devices_from_happi([("abc_det1", "det1"), ("abc_motor1", "motor1")], namespace=locals()) # 'item_list': ["det1", "motor1"] Parameters ---------- device_names : list(str) or list(tuple(str)) List of device names. Elements of the list could be strings or tuples (lists) of string with two elements. If an element is a tuple of two names, then the first name is used in database search and the second name is the name of the device after it is loaded into the namespace. Each element of the list is processed separately and may contain mix of strings and tuples. If the second name in the tuple is an empty string, then it is ignored and the device is imported with the same name as used in database. It is expected that search will result in one found device per device name, otherwise ``RuntimeError`` is raised. namespace : dict Reference to the namespace where the devices should be loaded. It can be the reference to ``global()`` or ``locals()`` of the startup script or reference to a dictionary. It is a required keyword argument. kwargs : dict Additional search parameters. Returns ------- list(str) the list of names of items in the ``namespace`` that were updated by the function. Raises ------ RuntimeError Search for one of the device names returns no devices or more than one device. """ kwargs = kwargs or {} if not isinstance(namespace, dict): raise TypeError( f"Parameter 'namespace' must be a dictionary: the value of type {type(namespace)} was passed instead" ) # Verify that 'device_names' has correct type if not isinstance(device_names, (tuple, list)): raise TypeError( "Parameter 'device_names' value must be a tuple or a list: " f"type(device_names) = {type(device_names)}") for n, name in enumerate(device_names): if not isinstance(name, (str, tuple, list)): raise TypeError( f"Parameter 'device_names': element #{n} must be str, tuple or list: " f"device_names[n] = {name}") if isinstance(name, (tuple, list)): if len(name) != 2 or not isinstance( name[0], str) or not isinstance(name[1], str): raise TypeError( f"Parameter 'device_names': element #{n} is expected to be in the form " f"('name_in_db', 'name_in_namespace'): device_names[n] = {name}" ) elif name[1] and not re.search(r"^[a-z][_a-z0-9]*$", name[1]): raise TypeError( f"The device '{name[0]}' can not be renamed: The new device name '{name[1]}' " "may consist of lowercase letters, numbers and '_' and must start from lowercase letter" ) from happi import Client, load_devices client = Client.from_config() results = [] for d_name in device_names: if isinstance(d_name, str): name_db, name_ns = d_name, None else: name_db, name_ns = d_name # Assemble search parameters search_params = dict(kwargs) search_params.update({"name": name_db}) res = client.search(**search_params) if not res: raise RuntimeError( f"No devices with name '{name_db}' were found in Happi database. " f"Search parameters {search_params}") elif len(res) > 1: raise RuntimeError( f"Multiple devices with name '{name_db}' were found in Happi database. " f"Search parameters {search_params}") else: r = res[0] # Modify the object name (if needed) if name_ns: # In order for the following conversion to work properly, the name of # the device should be specified only once (as `name` attribute), # and aliases `{{name}}` should be used if the name is used as any other # parameter. This is standard and recommended practice for instantiating # Happi items (devices). # # In search results, the reference to the device is `r._device`. # `r.metadata` contains expanded metadata (it is probably not used, but it # is a good idea to change it as well for consistency. We don't touch `_id`. # The modified data is not expected to be saved to the database. setattr(r._device, "name", name_ns) r.metadata["name"] = name_ns # Instantiate the object results.append(r) ns = load_devices(*[_.item for _ in results]) ns_dict = ns.__dict__ namespace.update(ns_dict) return list(ns_dict)
def test_from_cfg_abs(happi_cfg_abs: str): # happi_cfg modifies environment variables to make config discoverable client = Client.from_config() assert isinstance(client.backend, JSONBackend) # Ensure the json backend is using the db that we gave an absolute path to. assert client.backend.path == '/var/run/db.json'
def test_qsbackend_with_client(mockqsbackend): c = Client(database=mockqsbackend) assert len(c.all_devices) == 6 assert all([isinstance(d, Motor) for d in c.all_devices])
def test_find_cfg(happi_cfg: str): # Use our config directory assert happi_cfg == Client.find_config() # Set the path explicitly using HAPPI env variable os.environ['HAPPI_CFG'] = happi_cfg assert happi_cfg == Client.find_config()