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.
- techui_builder/__init__.py +16 -0
- techui_builder/__main__.py +180 -0
- techui_builder/_version.py +34 -0
- techui_builder/autofill.py +102 -0
- techui_builder/builder.py +486 -0
- techui_builder/generate.py +424 -0
- techui_builder/models.py +216 -0
- techui_builder/schema_generator.py +27 -0
- techui_builder/utils.py +32 -0
- techui_builder/validator.py +117 -0
- techui_builder-0.6.0a2.dist-info/METADATA +98 -0
- techui_builder-0.6.0a2.dist-info/RECORD +16 -0
- techui_builder-0.6.0a2.dist-info/WHEEL +5 -0
- techui_builder-0.6.0a2.dist-info/entry_points.txt +2 -0
- techui_builder-0.6.0a2.dist-info/licenses/LICENSE +201 -0
- techui_builder-0.6.0a2.dist-info/top_level.txt +1 -0
|
@@ -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
|