techui-builder 0.6.0a2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ """Top level API.
2
+
3
+ .. data:: __version__
4
+ :type: str
5
+
6
+ Version number as calculated by poetry-dynamic-versioning
7
+ """
8
+
9
+ from techui_builder.builder import Builder
10
+
11
+ from ._version import __version__
12
+
13
+ __all__ = [
14
+ "__version__",
15
+ "Builder",
16
+ ]
@@ -0,0 +1,180 @@
1
+ """Interface for ``python -m techui_builder``."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from rich.logging import RichHandler
9
+
10
+ from techui_builder import __version__
11
+ from techui_builder.autofill import Autofiller
12
+ from techui_builder.builder import Builder
13
+ from techui_builder.schema_generator import schema_generator
14
+
15
+ app = typer.Typer(
16
+ pretty_exceptions_show_locals=False,
17
+ help="""
18
+ A script for building Phoebus GUIs.
19
+
20
+ This is the required file structure:\n
21
+ \n
22
+ ixx-services\n
23
+ |-- services\n
24
+ | |-- blxxi-ea-device-01\n
25
+ | | `-- config\n
26
+ | | `-- ioc.yaml\n
27
+ | |-- ...\n
28
+ | `-- blxxi-va-device-01\n
29
+ | `-- config\n
30
+ | `-- ioc.yaml\n
31
+ `-- synoptic\n
32
+ . |-- techui-support/\n
33
+ | | `-- ...\n
34
+ . |-- techui.yaml\n
35
+ . `-- index.bob\n
36
+ """,
37
+ )
38
+
39
+ default_bobfile = "index.bob"
40
+
41
+
42
+ def version_callback(value: bool):
43
+ if value:
44
+ print(f"techui-builder version: {__version__}")
45
+ raise typer.Exit()
46
+
47
+
48
+ def schema_callback(value: bool):
49
+ if value:
50
+ schema_generator()
51
+ raise typer.Exit()
52
+
53
+
54
+ def log_level(level: str):
55
+ logging.basicConfig(
56
+ level=level,
57
+ format="%(message)s",
58
+ handlers=[RichHandler(omit_repeated_times=False, markup=True)],
59
+ )
60
+
61
+
62
+ # This is the default behaviour when no command provided
63
+ @app.callback(invoke_without_command=True)
64
+ def main(
65
+ filename: Annotated[Path, typer.Argument(help="The path to techui.yaml")],
66
+ bobfile: Annotated[
67
+ Path | None,
68
+ typer.Argument(help="Override for template bob file location."),
69
+ ] = None,
70
+ version: Annotated[
71
+ bool | None, typer.Option("--version", callback=version_callback)
72
+ ] = None,
73
+ loglevel: Annotated[
74
+ str,
75
+ typer.Option(
76
+ "--log-level",
77
+ help="Set log level to INFO, DEBUG, WARNING, ERROR or CRITICAL",
78
+ case_sensitive=False,
79
+ callback=log_level,
80
+ ),
81
+ ] = "INFO",
82
+ schema: Annotated[
83
+ bool | None,
84
+ typer.Option(
85
+ "--schema",
86
+ help="Generate schema for validating techui and ibek-mapping yaml files",
87
+ callback=schema_callback,
88
+ ),
89
+ ] = None,
90
+ ) -> None:
91
+ """Default function called from cmd line tool."""
92
+
93
+ logger_ = logging.getLogger(__name__)
94
+
95
+ bob_file = bobfile
96
+
97
+ gui = Builder(techui=filename)
98
+
99
+ # Get the relative path to the techui file from working dir
100
+ abs_path = filename.absolute()
101
+ logger_.debug(f"techui.yaml absolute path: {abs_path}")
102
+
103
+ # Get the current working dir
104
+ cwd = Path.cwd()
105
+ logger_.debug(f"Working directory: {cwd}")
106
+
107
+ # Get the relative path of ixx-services to techui.yaml
108
+ ixx_services_dir = next(
109
+ (
110
+ ixx_services.relative_to(cwd, walk_up=True)
111
+ for parent in abs_path.parents
112
+ for ixx_services in parent.glob(f"{gui.conf.beamline.short_dom}-services")
113
+ ),
114
+ None,
115
+ )
116
+ if ixx_services_dir is None:
117
+ logging.critical("ixx-services not found. Is you file structure correct?")
118
+ exit()
119
+ logger_.debug(f"ixx-services relative path: {ixx_services_dir}")
120
+
121
+ # Get the synoptic dir relative to the parent dir
122
+ synoptic_dir = ixx_services_dir.joinpath("synoptic")
123
+ logger_.debug(f"synoptic relative path: {synoptic_dir}")
124
+
125
+ if bob_file is None:
126
+ # Search default relative dir to techui filename
127
+ # There will only ever be one file, but if not return None
128
+ bob_file = next(
129
+ synoptic_dir.glob("index.bob"),
130
+ None,
131
+ )
132
+ if bob_file is None:
133
+ logging.critical(
134
+ f"Source bob file '{default_bobfile}' not found in \
135
+ {synoptic_dir}. Does it exist?"
136
+ )
137
+ exit()
138
+ elif not bob_file.exists():
139
+ logging.critical(f"Source bob file '{bob_file}' not found. Does it exist?")
140
+ exit()
141
+
142
+ logger_.debug(f"bob file: {bob_file}")
143
+
144
+ # # Overwrite after initialised to make sure this is picked up
145
+ gui._services_dir = ixx_services_dir.joinpath("services") # noqa: SLF001
146
+ gui._write_directory = synoptic_dir # noqa: SLF001
147
+
148
+ logger_.debug(
149
+ f"""
150
+
151
+ Builder created for {gui.conf.beamline.short_dom}.
152
+ Services directory: {gui._services_dir}
153
+ Write directory: {gui._write_directory}
154
+ """, # noqa: SLF001
155
+ )
156
+
157
+ gui.setup()
158
+ gui.create_screens()
159
+ gui.write_devsta_pvs()
160
+
161
+ logger_.info(f"Screens generated for {gui.conf.beamline.short_dom}.")
162
+
163
+ autofiller = Autofiller(bob_file)
164
+ autofiller.read_bob()
165
+ autofiller.autofill_bob(gui)
166
+
167
+ dest_bob = gui._write_directory.joinpath("index.bob") # noqa: SLF001
168
+
169
+ autofiller.write_bob(dest_bob)
170
+
171
+ logger_.info(f"Screens autofilled for {gui.conf.beamline.short_dom}.")
172
+
173
+ gui.write_json_map(synoptic=dest_bob, dest=gui._write_directory) # noqa: SLF001
174
+ logger_.info(
175
+ f"Json map generated for {gui.conf.beamline.short_dom} (from index.bob)"
176
+ )
177
+
178
+
179
+ if __name__ == "__main__":
180
+ app()
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.6.0a2'
32
+ __version_tuple__ = version_tuple = (0, 6, 0, 'a2')
33
+
34
+ __commit_id__ = commit_id = None
@@ -0,0 +1,102 @@
1
+ import logging
2
+ import os
3
+ from collections import defaultdict
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+
7
+ from lxml import objectify
8
+ from lxml.objectify import ObjectifiedElement
9
+
10
+ from techui_builder.builder import Builder, _get_action_group
11
+ from techui_builder.models import Component
12
+ from techui_builder.utils import read_bob
13
+
14
+ logger_ = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class Autofiller:
19
+ path: Path
20
+ macros: list[str] = field(default_factory=lambda: ["prefix", "desc", "file"])
21
+ widgets: dict[str, ObjectifiedElement] = field(
22
+ default_factory=defaultdict, init=False, repr=False
23
+ )
24
+
25
+ def read_bob(self) -> None:
26
+ self.tree, self.widgets = read_bob(self.path)
27
+
28
+ def autofill_bob(self, gui: "Builder"):
29
+ # Get names from component list
30
+
31
+ for symbol_name, child in self.widgets.items():
32
+ # If the name exists in the component list
33
+ if symbol_name in gui.conf.components.keys():
34
+ # Get first copy of component (should only be one)
35
+ comp = next(
36
+ (comp for comp in gui.conf.components if comp == symbol_name),
37
+ )
38
+
39
+ self.replace_content(
40
+ widget=child,
41
+ component_name=comp,
42
+ component=gui.conf.components[comp],
43
+ )
44
+
45
+ # Add option to allow left mouse click to run action
46
+ child["run_actions_on_mouse_click"] = "true"
47
+
48
+ def write_bob(self, filename: Path):
49
+ # Check if data/ dir exists and if not, make it
50
+ data_dir = filename.parent
51
+ if not data_dir.exists():
52
+ os.mkdir(data_dir)
53
+
54
+ # Remove any unnecessary xmlns:py and py:pytype metadata from tags
55
+ objectify.deannotate(self.tree, cleanup_namespaces=True)
56
+
57
+ self.tree.write(
58
+ filename,
59
+ pretty_print=True,
60
+ encoding="utf-8",
61
+ xml_declaration=True,
62
+ )
63
+ logger_.debug(f"Screen filled for {filename}")
64
+
65
+ def replace_content(
66
+ self,
67
+ widget: ObjectifiedElement,
68
+ component_name: str,
69
+ component: Component,
70
+ ):
71
+ for macro in self.macros:
72
+ # Get current component attribute
73
+ component_attr = getattr(component, macro)
74
+
75
+ # Fix to make sure widget is reverted back to widget that was passed in
76
+ current_widget = widget
77
+ match macro:
78
+ case "prefix":
79
+ tag_name = "pv_name"
80
+ component_attr += ":DEVSTA"
81
+ case "desc":
82
+ tag_name = "description"
83
+ current_widget = _get_action_group(widget)
84
+ if component_attr is None:
85
+ component_attr = component_name
86
+ case "file":
87
+ tag_name = "file"
88
+ current_widget = _get_action_group(widget)
89
+ if component_attr is None:
90
+ component_attr = f"{component_name}.bob"
91
+ case _:
92
+ raise ValueError("The provided macro type is not supported.")
93
+
94
+ if current_widget is None:
95
+ logger_.debug(
96
+ f"Skipping replace_content for {component_name} as no action\
97
+ group found"
98
+ )
99
+ continue
100
+
101
+ # Set component's tag text to the corresponding widget tag
102
+ current_widget[tag_name] = component_attr