Skip to content
102 changes: 54 additions & 48 deletions src/techui_builder/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
from typing import Annotated

import typer
from rich.logging import RichHandler

from techui_builder import __version__
from techui_builder._logger import Logger
from techui_builder.autofill import Autofiller
from techui_builder.builder import Builder
from techui_builder.schema_generator import schema_generator

logger_ = logging.getLogger(__name__)

app = typer.Typer(
pretty_exceptions_show_locals=False,
help="""
Expand Down Expand Up @@ -52,52 +54,12 @@ def schema_callback(value: bool):


def log_level(level: str):
logging.basicConfig(
level=level,
format="%(message)s",
handlers=[RichHandler(omit_repeated_times=False, markup=True)],
)


# This is the default behaviour when no command provided
@app.callback(invoke_without_command=True)
def main(
filename: Annotated[Path, typer.Argument(help="The path to techui.yaml")],
bobfile: Annotated[
Path | None,
typer.Argument(help="Override for template bob file location."),
] = None,
version: Annotated[
bool | None, typer.Option("--version", callback=version_callback)
] = None,
loglevel: Annotated[
str,
typer.Option(
"--log-level",
help="Set log level to INFO, DEBUG, WARNING, ERROR or CRITICAL",
case_sensitive=False,
callback=log_level,
),
] = "INFO",
schema: Annotated[
bool | None,
typer.Option(
"--schema",
help="Generate schema for validating techui and ibek-mapping yaml files",
callback=schema_callback,
),
] = None,
) -> None:
"""Default function called from cmd line tool."""
Logger(level)

logger_ = logging.getLogger(__name__)

bob_file = bobfile

gui = Builder(techui=filename)

def find_dirs(file_path: Path, beamline: str) -> tuple:
# Get the relative path to the techui file from working dir
abs_path = filename.absolute()
abs_path = file_path.absolute()
logger_.debug(f"techui.yaml absolute path: {abs_path}")

# Get the current working dir
Expand All @@ -109,7 +71,7 @@ def main(
(
ixx_services.relative_to(cwd, walk_up=True)
for parent in abs_path.parents
for ixx_services in parent.glob(f"{gui.conf.beamline.location}-services")
for ixx_services in parent.glob(f"{beamline}-services")
),
None,
)
Expand All @@ -122,11 +84,15 @@ def main(
synoptic_dir = ixx_services_dir.joinpath("synoptic")
logger_.debug(f"synoptic relative path: {synoptic_dir}")

return ixx_services_dir, synoptic_dir


def find_bob(bob_file: Path | None, synoptic_dir: Path):
if bob_file is None:
# Search default relative dir to techui filename
# There will only ever be one file, but if not return None
bob_file = next(
synoptic_dir.glob("index.bob"),
synoptic_dir.glob(default_bobfile),
None,
)
if bob_file is None:
Expand All @@ -140,6 +106,46 @@ def main(
exit()

logger_.debug(f"bob file: {bob_file}")
return bob_file


# This is the default behaviour when no command provided
@app.callback(invoke_without_command=True)
def main(
filename: Annotated[Path, typer.Argument(help="The path to techui.yaml")],
bobfile: Annotated[
Path | None,
typer.Argument(help="Override for template bob file location."),
] = None,
version: Annotated[
bool | None, typer.Option("--version", callback=version_callback)
] = None,
loglevel: Annotated[
str,
typer.Option(
"--log-level",
"-l",
help="Set log level to INFO, DEBUG, WARNING, ERROR or CRITICAL",
case_sensitive=False,
callback=log_level,
),
] = "INFO",
schema: Annotated[
bool | None,
typer.Option(
"--schema",
help="Generate schema for validating techui and ibek-mapping yaml files",
callback=schema_callback,
),
] = None,
) -> None:
"""Default function called from cmd line tool."""

gui = Builder(techui=filename)

ixx_services_dir, synoptic_dir = find_dirs(filename, gui.conf.beamline.location)

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
Expand All @@ -160,9 +166,9 @@ def main(

logger_.info(f"Screens generated for {gui.conf.beamline.location}.")

autofiller = Autofiller(bob_file)
autofiller = Autofiller(bob_file, gui.conf.components)
autofiller.read_bob()
autofiller.autofill_bob(gui)
autofiller.autofill_bob()

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

Expand Down
18 changes: 18 additions & 0 deletions src/techui_builder/_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import logging
from dataclasses import dataclass, field

from rich.logging import RichHandler


@dataclass
class Logger:
level: str = field(default="INFO")

handler = RichHandler(omit_repeated_times=False, markup=True)

def __post_init__(self):
logging.basicConfig(
level=self.level.upper(),
format="%(message)s",
handlers=[self.handler],
)
11 changes: 6 additions & 5 deletions src/techui_builder/autofill.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from lxml.etree import Element, SubElement, tostring
from lxml.objectify import ObjectifiedElement, fromstring

from techui_builder.builder import Builder, _get_action_group
from techui_builder.builder import _get_action_group
from techui_builder.models import Component
from techui_builder.utils import read_bob

Expand All @@ -18,6 +18,7 @@
@dataclass
class Autofiller:
path: Path
gui_components: dict[str, Component]
macros: list[str] = field(
default_factory=lambda: ["prefix", "desc", "file", "macros"]
)
Expand All @@ -28,21 +29,21 @@ class Autofiller:
def read_bob(self) -> None:
self.tree, self.widgets = read_bob(self.path)

def autofill_bob(self, gui: "Builder"):
def autofill_bob(self):
# Get names from component list

for symbol_name, child in self.widgets.items():
# If the name exists in the component list
if symbol_name in gui.conf.components.keys():
if symbol_name in self.gui_components.keys():
# Get first copy of component (should only be one)
comp = next(
(comp for comp in gui.conf.components if comp == symbol_name),
(comp for comp in self.gui_components if comp == symbol_name),
)

self.replace_content(
widget=child,
component_name=comp,
component=gui.conf.components[comp],
component=self.gui_components[comp],
)

# Add option to allow left mouse click to run action
Expand Down
29 changes: 20 additions & 9 deletions src/techui_builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ def _extract_services(self):
self._extract_entities(ioc_yaml=service.joinpath("config/ioc.yaml"))
except OSError:
logger_.error(
f"No ioc.yaml file for service: [bold]{service.name}[/bold]. \
Does it exist?"
f"No ioc.yaml file for service: [bold]{service.name}[/bold]."
" Does it exist?"
)

def _extract_entities(self, ioc_yaml: Path):
Expand Down Expand Up @@ -199,7 +199,10 @@ def _validate_screen(self, screen_name: str):
def create_screens(self):
"""Create the screens for each component in techui.yaml"""
if len(self.entities) == 0:
logger_.critical("No ioc entities found, has setup() been run?")
logger_.critical(
"No ioc entities found. This [italic]normally[/italic]"
" suggests an issue with finding ixx-services."
)
exit()

# Loop over every component defined in techui.yaml and locate
Expand All @@ -218,8 +221,8 @@ def create_screens(self):
for extra_p in component.extras:
if extra_p not in self.entities.keys():
logger_.error(
f"Extra prefix {extra_p} for {component_name} does not \
exist."
f"Extra prefix {extra_p} for {component_name} does not"
" exist."
)
continue
screen_entities.extend(self.entities[extra_p])
Expand All @@ -238,9 +241,9 @@ def create_screens(self):

else:
logger_.warning(
f"{self.techui.name}: The prefix [bold]{component.prefix}[/bold]\
set in the component [bold]{component_name}[/bold] does not match any P field in the\
ioc.yaml files in services"
f"{self.techui.name}: The prefix [bold]{component.prefix}[/bold] "
f"set in the component [bold]{component_name}[/bold] does not match"
" any P field in the ioc.yaml files in services"
)

def _generate_json_map(self, screen_path: Path, dest_path: Path) -> JsonMap:
Expand Down Expand Up @@ -487,4 +490,12 @@ def _get_action_group(element: ObjectifiedElement) -> ObjectifiedElement | None:
return None
except AttributeError:
# TODO: Find better way of handling there being no "actions" group
logger_.error(f"Actions group not found in component: {element.text}")
# TODO: Do widgets always have a name attr, or _can_ it be empty??
name = element.name

parent_name = p.name if (p := element.getparent()) is not None else None

logger_.error(
f"Actions group not found in component [bold]{name}[/bold] on "
f"[bold]{parent_name}[/bold]"
)
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from techui_builder.autofill import Autofiller
from techui_builder.builder import Builder, JsonMap
from techui_builder.generate import Generator
from techui_builder.models import Component
from techui_builder.validator import Validator


Expand Down Expand Up @@ -120,7 +121,7 @@ def generator():
def autofiller():
index_bob = Path(__file__).parent.joinpath(Path("t01-services/synoptic/index.bob"))

a = Autofiller(index_bob)
a = Autofiller(index_bob, {"test_widget": MagicMock(spec=Component)})

return a

Expand Down
Loading