diff --git a/src/techui_builder/builder.py b/src/techui_builder/builder.py index 05f8c07..79c3f40 100644 --- a/src/techui_builder/builder.py +++ b/src/techui_builder/builder.py @@ -8,12 +8,13 @@ import yaml from epicsdbbuilder.recordbase import Record +from jinja2 import Template from lxml import etree, objectify from lxml.objectify import ObjectifiedElement from softioc.builder import records from techui_builder.generate import Generator -from techui_builder.models import Entity, TechUi +from techui_builder.models import Entity, SupportEntity, TechUi, TechUiSupport from techui_builder.validator import Validator logger_ = logging.getLogger(__name__) @@ -49,9 +50,10 @@ class Builder: default_factory=lambda: defaultdict(list), init=False ) status_pvs: dict[str, Record] = field(default_factory=dict, init=False) + + # These are global params for the class (not accessible by user) _services_dir: Path = field(init=False, repr=False) - _gui_map: dict = field(init=False, repr=False) - _write_directory: Path = field(default=Path("opis"), init=False, repr=False) + _write_directory: Path = field(init=False, repr=False) def __post_init__(self): # Populate beamline and components @@ -61,12 +63,30 @@ def __post_init__(self): def setup(self): """Run intial setup, e.g. extracting entries from service ioc.yaml.""" + # This needs to be before _read_map() + self.support_path = self._write_directory.joinpath("techui-support") + + self._read_map() + self._extract_services() - synoptic_dir = self._write_directory self.clean_files() - self.generator = Generator(synoptic_dir, self.conf.beamline.url) + self.generator = Generator( + self._write_directory, + self.conf.beamline.url, + self.support_path, + self.techui_support, + ) + + def _read_map(self): + """Read the techui-support.yaml file from techui-support.""" + support_yaml = self.support_path.joinpath("techui-support.yaml").absolute() + logger_.debug(f"techui-support.yaml location: {support_yaml}") + + self.techui_support = TechUiSupport.model_validate( + yaml.safe_load(support_yaml.read_text(encoding="utf-8")) + ) def clean_files(self): exclude = {"index.bob"} @@ -177,17 +197,28 @@ def _extract_entities(self, service_name: str, ioc_yaml: Path): with open(ioc_yaml) as ioc: ioc_conf: dict[str, list[dict[str, str]]] = yaml.safe_load(ioc) for entity in ioc_conf["entities"]: - if "P" in entity.keys(): + if entity["type"] in self.techui_support.support_modules: + support_mapping: SupportEntity = ( + self.techui_support.support_modules[entity["type"]] + ) + support_macros = support_mapping.macros + + macros = {k: v for k, v in entity.items() if k in support_macros} + + prefix_template = Template(support_mapping.prefix) + prefix: str = prefix_template.render(macros) + # Create Entity and append to entity list new_entity = Entity( service_name=service_name, type=entity["type"], desc=entity.get("desc", None), - P=entity["P"], - M=None if (val := entity.get("M")) is None else val, - R=None if (val := entity.get("R")) is None else val, + prefix=prefix, + macros=macros, ) - self.entities[new_entity.P].append(new_entity) + + pv_root = prefix.split(":", maxsplit=1)[0] + self.entities[pv_root].append(new_entity) def _generate_screen(self, screen_name: str): self.generator.build_screen(screen_name) diff --git a/src/techui_builder/generate.py b/src/techui_builder/generate.py index 80c986d..1cecd52 100644 --- a/src/techui_builder/generate.py +++ b/src/techui_builder/generate.py @@ -2,17 +2,16 @@ import os import re from collections import defaultdict -from collections.abc import Mapping, Sequence +from collections.abc import Mapping from dataclasses import dataclass, field from pathlib import Path -import yaml from lxml import objectify from phoebusgen import screen as pscreen from phoebusgen import widget as pwidget from phoebusgen.widget.widgets import ActionButton, EmbeddedDisplay, Group -from techui_builder.models import Component, Entity +from techui_builder.models import Component, Entity, TechUiSupport logger_ = logging.getLogger(__name__) @@ -23,12 +22,10 @@ class Generator: beamline_url: str = field(repr=False) # These are global params for the class (not accessible by user) - support_path: Path = field(init=False, repr=False) - techui_support: dict = field(init=False, repr=False) + support_path: Path = field(repr=False) + techui_support: TechUiSupport = field(repr=False) default_size: int = field(default=100, init=False, repr=False) - P: str = field(default="P", init=False, repr=False) - M: str = field(default="M", init=False, repr=False) - R: str = field(default="R", init=False, repr=False) + prefix: str = field(default="P", init=False, repr=False) widgets: list[ActionButton | EmbeddedDisplay] = field( default_factory=list[ActionButton | EmbeddedDisplay], init=False, repr=False ) @@ -42,20 +39,6 @@ class Generator: group_padding: int = field(default=50, init=False, repr=False) label_flag: bool = field(default=False, init=False, repr=False) - def __post_init__(self): - # This needs to be before _read_map() - self.support_path = self.synoptic_dir.joinpath("techui-support") - - self._read_map() - - def _read_map(self): - """Read the techui-support.yaml file from techui-support.""" - support_yaml = self.support_path.joinpath("techui-support.yaml").absolute() - logger_.debug(f"techui-support.yaml location: {support_yaml}") - - with open(support_yaml) as map: - self.techui_support = yaml.safe_load(map) - def _get_screen_dimensions(self, file: str) -> tuple[int, int]: """ Parses the bob files for information on the height @@ -161,33 +144,23 @@ def _get_group_dimensions(self, widget_list: list[EmbeddedDisplay | ActionButton ) def _initialise_name_suffix(self, component: Entity) -> tuple[str, str, str | None]: - if component.M is not None: - name: str = component.M - suffix: str = component.M - suffix_label: str | None = self.M - elif component.R is not None: - name = component.R - suffix = component.R - suffix_label = self.R - else: - name = component.P - suffix = "" - suffix_label = "" + try: + component_name = component.prefix.split(":", maxsplit=1)[1] + raw_name = component_name.removesuffix(":") + except IndexError: + component_name = "" + raw_name = "" + + suffix = component_name - name = name.removeprefix(":").removesuffix(":") # Try to get name from child labels if they exist, # if not, just use the name as it is. if component.child_labels is not None: - if name in component.child_labels.keys(): - name = component.child_labels[name] + if raw_name in component.child_labels.keys(): + component_name = component.child_labels[raw_name] self.label_flag = True - return (name, suffix, suffix_label) - - def _is_list_of_dicts(self, scrn_mapping: Mapping) -> bool: - return isinstance(scrn_mapping, Sequence) and all( - isinstance(scrn, Mapping) for scrn in scrn_mapping - ) + return (component_name, suffix, raw_name) def _allocate_widget( self, scrn_mapping: Mapping, component: Entity @@ -238,7 +211,7 @@ def _allocate_widget( height, ) # Add macros to the widgets - new_widget.macro(self.P, component.P) + new_widget.macro(self.prefix, component.prefix) if suffix_label != "": new_widget.macro(f"{suffix_label}", suffix) new_widget.macro("label", name.removeprefix(":").removesuffix(":")) @@ -266,7 +239,7 @@ def _allocate_widget( file=str(data_scrn_path), target="tab", macros={ - "P": component.P, + "P": component.prefix, f"{suffix_label}": suffix, }, ) @@ -275,7 +248,7 @@ def _allocate_widget( file=str(data_scrn_path), target="tab", macros={ - "P": component.P, + "P": component.prefix, }, ) @@ -294,7 +267,7 @@ def _create_widget( new_widget = [] try: - scrn_mapping = self.techui_support[component.type] + scrn_mapping = self.techui_support.support_modules[component.type].screens except KeyError: logger_.warning( f"No available widget for {component.type} in screen \ @@ -302,11 +275,8 @@ def _create_widget( ) return None - if self._is_list_of_dicts(scrn_mapping): - for value in scrn_mapping: - new_widget.append(self._allocate_widget(value, component)) - else: - new_widget = self._allocate_widget(scrn_mapping, component) + for value in scrn_mapping: + new_widget.append(self._allocate_widget(value, component)) return new_widget @@ -374,14 +344,14 @@ def layout_widgets(self, widgets: list[EmbeddedDisplay | ActionButton]): return sorted_widgets - def build_widgets(self, screen_name: str, screen_components: list[Entity]): + def build_widgets(self, screen_name: str, screen_entities: list[Entity]): # Empty widget buffer self.widgets = [] # order is an enumeration of the components, used to list them, # and serves as functionality in the math for formatting. - for component in screen_components: - new_widget = self._create_widget(name=screen_name, component=component) + for entity in screen_entities: + new_widget = self._create_widget(name=screen_name, component=entity) if new_widget is None: continue if isinstance(new_widget, list): diff --git a/src/techui_builder/models.py b/src/techui_builder/models.py index 61e6440..72133ae 100644 --- a/src/techui_builder/models.py +++ b/src/techui_builder/models.py @@ -1,6 +1,6 @@ import logging import re -from typing import Annotated, Literal +from typing import Annotated, Any, Literal from pydantic import ( BaseModel, @@ -272,7 +272,7 @@ class Entity(BaseModel): "ADAravis.aravisCamera" ), ] - P: Annotated[str, Field(description="PV Prefix for module entity")] + prefix: Annotated[str, Field(description="PV Prefix for module entity")] desc: Annotated[ str | None, Field(description="Optional description of module entity") ] = None @@ -280,7 +280,30 @@ class Entity(BaseModel): dict[str, str] | None, Field(description="Optional child labels for module entity"), ] = None - M: Annotated[str | None, Field(description="Optional PV suffix for a motor")] - R: Annotated[ - str | None, Field(description="Optional PV suffix for an ADAravis plugin") + macros: Annotated[ + dict[str, Any], + Field(description="Macros for the matching screen (can be empty)"), + ] + + +class SupportEntity(BaseModel): + """ + Table of variables from corresponding support module in techui-support.yaml file + """ + + prefix: Annotated[str, Field(description="Prefix for techui-support screen")] + macros: Annotated[ + list[str], + Field(description="Macros for the matching screen (can be empty)"), + ] + screens: Annotated[ + list[dict[str, str]], + Field(description="Dictionary of available screens for the support module"), + ] + + +class TechUiSupport(BaseModel): + support_modules: Annotated[ + dict[str, SupportEntity], + Field(description="The dictionary of techui-support.yaml entities"), ]