Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions example/t01-services/synoptic/techui.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# yaml-language-server: $schema=../../../schemas/techui.schema.json

# create_gui example for Phoebus GuiBuilder
beamline:
location: bl01t
Expand Down
25 changes: 15 additions & 10 deletions src/techui_builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@
class Builder:
"""
This class provides the functionality to process the required
techui.yaml file into screens mapped from ioc.yaml and
*-mapping.yaml files.
techui.yaml file into screens mapped from ioc.yaml and fastcs.yaml files.
It does this by leveraging *-support.yaml mapping files in submodules that
include the respective screens. For example, all generic screens are
situated in techui-support, with a corresponding techui-support.yaml
containing the mappings. It is possible to submodule custom *-support
repos with more bespoke screens as well.

By default it looks for a `techui.yaml` file in the same dir
of the script Guibuilder is called in. Optionally a custom path
of the script techui-builder is called in. Optionally a custom path
can be declared.

"""
Expand All @@ -44,11 +48,11 @@ def __post_init__(self):

def setup(self):
"""
Run intial setup, e.g. extracting entries
Run initial setup, e.g. extracting entries
from service ioc.yaml or fastcs.yaml.
"""
# This needs to be before _read_map()
self.support_path = self._write_directory.joinpath("techui-support")
self.support_path = self._write_directory / "techui-support"

self._read_map()

Expand All @@ -65,7 +69,7 @@ def setup(self):

def _read_map(self):
"""Read the techui-support.yaml file from techui-support."""
support_yaml = self.support_path.joinpath("techui-support.yaml").absolute()
support_yaml = self.support_path / "techui-support.yaml"
logger_.debug(f"techui-support.yaml location: {support_yaml}")

self.techui_support = TechUiSupport.model_validate(
Expand Down Expand Up @@ -101,15 +105,15 @@ def clean_files(self):
def _extract_services(self):
"""
Finds the services folders in the services directory
and extracts all entites
and extracts all entities.
"""

# Loop over every dir in services, ignoring anything that isn't a service
for service in self._services_dir.glob(f"{self.conf.beamline.location}-*-*-*"):
service_name = service.name
# If service doesn't exist, file open will fail throwing exception
try:
service_yaml_dir = service.joinpath("config")
service_yaml_dir = service / "config"

yaml_matches = [
p
Expand Down Expand Up @@ -140,13 +144,14 @@ def _extract_services(self):

def _extract_entities(self, service_name: str, service_yaml: Path):
"""
Extracts the entries in ioc.yaml matching the defined prefix
Extracts the entries in ioc.yaml or fastcs.yaml matching the defined prefix.
"""

with open(service_yaml) as ioc:
ioc_conf: dict[str, list[dict[str, str]]] = yaml.safe_load(ioc)

for key in ioc_conf.keys():
# ioc.yaml includes 'entities' and fastcs.yaml includes 'controllers'
_regex = re.compile(r"^(?:(entities)|(controllers))$")
match = _regex.match(key)
if match:
Expand Down Expand Up @@ -226,7 +231,7 @@ def create_screens(self):
screen_entities.extend(self.entities[extra_p])

# This is used by both generate and validate,
# so called beforehand for tidyness
# so called beforehand for tidiness
self.generator.build_widgets(component_name, screen_entities)
self.generator.build_groups(component_name, self.conf.components)

Expand Down
4 changes: 3 additions & 1 deletion src/techui_builder/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

@dataclass
class Generator:
"""Helper class containing functions to generate Component screens."""

synoptic_dir: Path = field(repr=False)
beamline_url: str = field(repr=False)

Expand Down Expand Up @@ -195,7 +197,7 @@ def _allocate_widget(
"Only related displays can have remote screens"
)
else:
screen_path = self.support_path.joinpath(f"bob/{file}")
screen_path = self.support_path / f"bob/{file}"
logger_.debug(f"Screen path: {screen_path}")

# Path of screen relative to synoptic/
Expand Down
21 changes: 15 additions & 6 deletions src/techui_builder/generate_jsonmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ def log_level(level: str):

@dataclass
class JsonMap:
"""Dataclass to handle the structure of a JsonMap element."""

file: str
display_name: str | None
exists: bool = True
Expand All @@ -54,6 +56,8 @@ class JsonMap:

@dataclass
class JsonMapGenerator:
"""Helper class containing functions to generate a JsonMap file."""

bob_path: Path = field(default=Path("index.bob"))
techui: Path = field(default=Path("techui.yaml"))
output: Path | None = field(default=None)
Expand Down Expand Up @@ -95,7 +99,7 @@ def generate_json_map(
def _get_display_name(
name_element: str | None, component_name: str | None, file_path: Path
):
# Validated screen names don't get renegerated
# Validated screen names don't get regenerated
name = name_element
display_name = self._get_component_label(
name_element,
Expand All @@ -114,7 +118,12 @@ def _child_file_crawl(
display_name: str | None,
macro_dictionary: dict[str, Any],
):
# TODO: misleading var name?
"""
Function to determine if the child node file exists, and if it
does, recursively generate a JsonMap element for it and it's children.

If it can't be found, a minimal JsonMap element is returned.
"""
child_file_path = destination_path / file_path_text

match = _PVI_FILE_RE.fullmatch(file_path_text)
Expand Down Expand Up @@ -331,7 +340,7 @@ def _get_component_label(
if name_elem in child_labels.values():
display_name = name_elem
# In the case of screens not regenerated, such as validated screens,
# the name text will not be updated to the childlabel,so we check
# the name text will not be updated to the child_label,so we check
# keys solely for generating the json_map from the top level .bob.
elif name_elem in child_labels:
display_name = child_labels[name_elem]
Expand Down Expand Up @@ -398,16 +407,16 @@ def write_json_map(
self,
):
"""
Maps the valid entries from the ioc.yaml file
to the required screen in *-mapping.yaml
Maps the valid entries from the ioc.yaml or fastcs.yaml file
to the required screen in *-support.yaml
"""
if not self.bob_path.exists():
raise FileNotFoundError(
f"Cannot generate json map for {self.bob_path}. Has it been generated?"
)

map = self.generate_json_map(self.bob_path, self._parent_path)
with open(self._write_directory.joinpath("JsonMap.json"), "w") as f:
with open(self._write_directory / "JsonMap.json", "w") as f:
f.write(
json.dumps(map, indent=4, default=lambda o: _serialise_json_map(o))
+ "\n"
Expand Down
8 changes: 4 additions & 4 deletions src/techui_builder/main_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def find_bob(bob_file: Path | None, synoptic_dir: Path):


# This is the 'build' behaviour
@app.command("build", help="Run techui-builder for a given techui.yaml")
@app.command("build", help="Run `techui-builder build` for a given techui.yaml")
def main(
filename: Annotated[Path, typer.Argument(help="The path to techui.yaml")],
bobfile: Annotated[
Expand All @@ -111,7 +111,7 @@ def main(
),
] = "INFO",
) -> None:
"""Default function called from cmd line tool."""
"""Function to run when `techui-builder build` is called."""

gui = Builder(techui=filename)

Expand All @@ -120,7 +120,7 @@ def main(
bob_file = find_bob(bobfile, synoptic_dir)

# # Overwrite after initialised to make sure this is picked up
gui._services_dir = ixx_services_dir.joinpath("services") # noqa: SLF001
gui._services_dir = ixx_services_dir / "services" # noqa: SLF001
gui._write_directory = synoptic_dir # noqa: SLF001

logger_.debug(
Expand All @@ -141,7 +141,7 @@ def main(
autofiller.read_bob()
autofiller.autofill_bob()

dest_bob = gui._write_directory.joinpath("index.bob") # noqa: SLF001
dest_bob = gui._write_directory / "index.bob" # noqa: SLF001

autofiller.write_bob(dest_bob)

Expand Down
100 changes: 61 additions & 39 deletions src/techui_builder/models.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import logging
import re
from typing import Annotated, Any, Literal
from typing import Annotated, Any

from pydantic import (
BaseModel,
ConfigDict,
Field,
RootModel,
StringConstraints,
computed_field,
field_validator,
model_validator,
Expand All @@ -21,8 +19,7 @@
# short: 'b23', 'ixx-1'


_DLS_PREFIX_RE = re.compile(
r"""
_DLS_PREFIX_PATTERN_VERBOSE = r"""
^ # start of string
(?= # lookahead to ensure the following pattern matches
[A-Za-z0-9-]{13,16} # match 13 to 16 alphanumeric characters or hyphens
Expand All @@ -42,9 +39,17 @@
(?:\.([a-zA-Z0-9_]+))? # match zero or one dot followed by one or more
# alphanumeric characters (capture group 3)
$ # end of string
""",
re.VERBOSE,
"""

_DLS_PREFIX_RE = re.compile(_DLS_PREFIX_PATTERN_VERBOSE, re.VERBOSE)

# JSON Schema (ECMA 262) has no concept of VERBOSE mode, so derive a
# compact equivalent once at import time for use in json_schema_extra.
# (i.e. This is to handle the inline comments)
_DLS_PREFIX_PATTERN_COMPACT = re.sub(
r"\s+", "", re.sub(r"#.*", "", _DLS_PREFIX_PATTERN_VERBOSE)
)

# Checks for database link flags, e.g. NPP MS, at the end
_DATABASE_FLAGS_RE = re.compile(
r"(?:PP|NPP)?" + r"(?:[ ]+(?:MS|NMS|MSS|MSI))?$",
Expand All @@ -58,10 +63,40 @@
class Beamline(BaseModel):
"""Global Beamline values read from `beamline:` table in techui.yaml"""

domain: Annotated[str, Field(description="Short BL location e.g. b23, ixx-1")]
location: Annotated[str, Field(description="Full BL domain e.g. bl23b")]
domain: Annotated[
str,
Field(
description="Short BL location e.g. b23, ixx-1",
# Make sure vscode is aware of schema validation
json_schema_extra={
"pattern": _DOMAIN_RE.pattern,
"type": "string",
},
),
]
location: Annotated[
str,
Field(
description="Full BL domain e.g. bl23b",
# Make sure vscode is aware of schema validation
json_schema_extra={
"pattern": _LOCATION_RE.pattern,
"type": "string",
},
),
]
desc: Annotated[str, Field(description="Description")]
url: Annotated[str, Field(description="URL of ixx-opis")]
url: Annotated[
str,
Field(
description="URL of ixx-opis",
# Make sure vscode is aware of schema validation
json_schema_extra={
"pattern": _OPIS_URL_RE.pattern,
"type": "string",
},
),
]
model_config = ConfigDict(extra="forbid")

@field_validator("domain")
Expand All @@ -73,7 +108,7 @@ def normalize_domain(cls, v: str) -> str:
# e.g. t01
return v

raise ValueError("Invalid beamline domain.")
raise ValueError("Invalid beamline domain. It needs to be in the form of t01.")

@field_validator("location")
@classmethod
Expand All @@ -83,7 +118,9 @@ def normalize_location(cls, v: str) -> str:
# already long: bl01t
return v

raise ValueError("Invalid beamline location.")
raise ValueError(
"Invalid beamline location. It needs to be in the form of bl01t."
)

@field_validator("url")
@classmethod
Expand All @@ -105,7 +142,17 @@ def check_url(cls, url: str) -> str:
class Component(BaseModel):
"""One UI Component from techui.yaml `components:` dictionary"""

prefix: Annotated[str, Field(description="Component PV Prefix")]
prefix: Annotated[
str,
Field(
description="Component PV Prefix",
# Make sure vscode is aware of schema validation
json_schema_extra={
"pattern": _DLS_PREFIX_PATTERN_COMPACT,
"type": "string",
},
),
]
label: Annotated[str | None, Field(description="Component label")] = None
child_labels: Annotated[
dict[str, str] | None, Field(description="Component Children Label")
Expand Down Expand Up @@ -235,34 +282,9 @@ class TechUi(BaseModel):
techui-support mapping models
"""

BobPath = Annotated[
str, StringConstraints(pattern=r"^(?:[A-Za-z0-9_.-]+/)*[A-Za-z0-9_.-]+\.bob$")
]
# Must contain at least one $(NAME) macro
MacroString = Annotated[
str,
StringConstraints(pattern=r"^[A-Za-z0-9_:\-./\s\$\(\)]+$"),
]
ScreenType = Literal["embedded", "related"]


class GuiComponentEntry(BaseModel):
file: BobPath
prefix: MacroString
suffix: MacroString | None = None
type: ScreenType
model_config = ConfigDict(extra="forbid")


GuiComponentUnion = list[GuiComponentEntry] | GuiComponentEntry


class GuiComponents(RootModel[dict[str, GuiComponentUnion]]):
pass


class Entity(BaseModel):
"""One table of IOC variables extracted from an ioc.yaml file"""
"""One table of IOC variables extracted from an ioc.yaml or fastcs.yaml file"""

service_name: Annotated[str, Field(description="Service name of the IOC")]
type: Annotated[
Expand Down
4 changes: 2 additions & 2 deletions src/techui_builder/schema_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import typer

from techui_builder.models import (
GuiComponents,
TechUi,
TechUiSupport,
)

SCHEMAS_DIR = Path("schemas")
Expand Down Expand Up @@ -40,5 +40,5 @@ def schema_generator() -> None:
write_json_schema("techui", tu)

# ibek_mapping
tu_support = GuiComponents.model_json_schema()
tu_support = TechUiSupport.model_json_schema()
write_json_schema("techui.support", tu_support)
Loading