Skip to content

Commit b557d1a

Browse files
First catalog version with first reference in a test.
1 parent 1913bfb commit b557d1a

File tree

5 files changed

+266
-0
lines changed

5 files changed

+266
-0
lines changed

pyaml/accelerator.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .arrays.array import ArrayConfig
1010
from .common.element import Element
1111
from .common.exception import PyAMLConfigException
12+
from .configuration.catalog import Catalog
1213
from .configuration.factory import Factory
1314
from .configuration.fileloader import load, set_root_folder
1415
from .control.controlsystem import ControlSystem
@@ -44,6 +45,8 @@ class ConfigModel(BaseModel):
4445
Acceleration description
4546
devices : list[Element]
4647
Element list
48+
control_system_catalog : Catalog
49+
catalog of DeviceAccess objects
4750
"""
4851

4952
model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
@@ -57,6 +60,7 @@ class ConfigModel(BaseModel):
5760
description: str | None = None
5861
arrays: list[ArrayConfig] = Field(default=None, repr=False)
5962
devices: list[Element] = Field(repr=False)
63+
control_system_catalog: Catalog = Field(repr=False)
6064

6165

6266
class Accelerator(object):
@@ -69,6 +73,7 @@ def __init__(self, cfg: ConfigModel):
6973

7074
if cfg.controls is not None:
7175
for c in cfg.controls:
76+
c.set_catalog(cfg.control_system_catalog)
7277
if c.name() == "live":
7378
self.__live = c
7479
else:

pyaml/configuration/catalog.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import re
2+
from typing import Pattern
3+
4+
from pydantic import BaseModel, ConfigDict, model_validator
5+
6+
from pyaml import PyAMLException
7+
from pyaml.configuration.catalog_entry import CatalogEntry, CatalogValue
8+
from pyaml.configuration.catalog_entry import ConfigModel as CatalogEntryConfigModel
9+
from pyaml.control.deviceaccess import DeviceAccess
10+
11+
# Define the main class name for this module
12+
PYAMLCLASS = "Catalog"
13+
14+
15+
class ConfigModel(BaseModel):
16+
"""
17+
Configuration model for a value catalog.
18+
19+
Attributes
20+
----------
21+
refs : list[CatalogEntryConfigModel]
22+
List of catalog entries.
23+
"""
24+
25+
model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
26+
27+
refs: list[CatalogEntry]
28+
29+
@model_validator(mode="after")
30+
def _validate_unique_references(self) -> "ConfigModel":
31+
"""
32+
Ensure that all references are unique within the catalog.
33+
"""
34+
seen: set[str] = set()
35+
for entry in self.refs:
36+
if entry.get_reference() in seen:
37+
raise ValueError(
38+
f"Duplicate catalog reference: '{entry.get_reference()}'."
39+
)
40+
seen.add(entry.get_reference())
41+
return self
42+
43+
44+
class Catalog:
45+
"""
46+
A simple registry mapping reference keys to DeviceAccess objects.
47+
48+
The catalog is intentionally minimal:
49+
- It resolves references to DeviceAccess or list[DeviceAccess]
50+
- It does NOT expose any DeviceAccess-like interface (no get/set/readback/etc.)
51+
"""
52+
53+
def __init__(self, cfg: ConfigModel):
54+
self._entries: dict[str, CatalogValue] = {}
55+
for ref in cfg.refs:
56+
self.add(ref.get_reference(), ref.get_value())
57+
58+
# ------------------------------------------------------------------
59+
60+
def add(self, reference: str, value: CatalogValue):
61+
"""
62+
Register a reference in the catalog.
63+
64+
Raises
65+
------
66+
PyAMLException
67+
If the reference already exists.
68+
"""
69+
if reference in self._entries:
70+
raise PyAMLException(f"Duplicate catalog reference: '{reference}'")
71+
self._entries[reference] = value
72+
73+
# ------------------------------------------------------------------
74+
75+
def get(self, reference: str) -> CatalogValue:
76+
"""
77+
Resolve a reference key.
78+
79+
Returns
80+
-------
81+
DeviceAccess | list[DeviceAccess]
82+
83+
Raises
84+
------
85+
PyAMLException
86+
If the reference does not exist.
87+
"""
88+
try:
89+
return self._entries[reference]
90+
except KeyError as exc:
91+
raise PyAMLException(f"Catalog reference '{reference}' not found.") from exc
92+
93+
# ------------------------------------------------------------------
94+
95+
def get_one(self, reference: str) -> DeviceAccess:
96+
"""
97+
Resolve a reference and ensure it corresponds to a single DeviceAccess.
98+
99+
Raises
100+
------
101+
PyAMLException
102+
If the reference does not exist or is multi-device.
103+
"""
104+
value = self.get(reference)
105+
106+
if isinstance(value, list):
107+
raise PyAMLException(
108+
f"Catalog reference '{reference}' is multi-device; use get_many()."
109+
)
110+
111+
return value
112+
113+
# ------------------------------------------------------------------
114+
115+
def get_many(self, reference: str) -> list[DeviceAccess]:
116+
"""
117+
Resolve a reference and ensure it corresponds to multiple DeviceAccess.
118+
119+
Returns
120+
-------
121+
list[DeviceAccess]
122+
123+
Raises
124+
------
125+
PyAMLException
126+
If the reference does not exist or is single-device.
127+
"""
128+
value = self.get(reference)
129+
130+
if not isinstance(value, list):
131+
raise PyAMLException(
132+
f"Catalog reference '{reference}' is single-device; use get_one()."
133+
)
134+
135+
return value
136+
137+
# ------------------------------------------------------------------
138+
139+
def find(self, pattern: str) -> dict[str, CatalogValue]:
140+
"""
141+
Resolve references matching a regular expression.
142+
143+
Returns
144+
-------
145+
dict[str, DeviceAccess | list[DeviceAccess]]
146+
Mapping {reference -> value}.
147+
"""
148+
regex: Pattern[str] = re.compile(pattern)
149+
return {k: v for k, v in self._entries.items() if regex.search(k)}
150+
151+
# ------------------------------------------------------------------
152+
153+
def keys(self) -> list[str]:
154+
"""Return all catalog reference keys."""
155+
return list(self._entries.keys())
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from typing import Union
2+
3+
from pydantic import BaseModel, ConfigDict, model_validator
4+
5+
from pyaml.control.deviceaccess import DeviceAccess
6+
7+
# Define the main class name for this module
8+
PYAMLCLASS = "CatalogEntry"
9+
10+
11+
class ConfigModel(BaseModel):
12+
"""
13+
Configuration model for a single catalog entry.
14+
15+
Exactly one of 'device' or 'devices' must be provided.
16+
17+
Attributes
18+
----------
19+
reference : str
20+
Unique key used to identify the value in the catalog.
21+
device : dict | None
22+
Factory configuration dict for a single DeviceAccess.
23+
devices : list[DeviceAccess] | None
24+
Factory configuration dicts for multiple DeviceAccess objects.
25+
"""
26+
27+
model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
28+
29+
reference: str
30+
device: DeviceAccess = None
31+
devices: list[DeviceAccess] = None
32+
33+
@model_validator(mode="after")
34+
def _validate_one_of(self) -> "ConfigModel":
35+
"""
36+
Ensure exactly one of (device, devices) is provided and properly shaped.
37+
"""
38+
has_device = self.device is not None
39+
has_devices = self.devices is not None and len(self.devices) > 0
40+
41+
# both True or both False -> invalid
42+
if has_device == has_devices:
43+
raise ValueError(
44+
"Catalog entry must define exactly one of 'device' or 'devices'."
45+
)
46+
47+
if has_device and not isinstance(self.device, DeviceAccess):
48+
raise ValueError("'device' must be a DeviceAccess.")
49+
50+
if has_devices:
51+
if not isinstance(self.devices, list) or len(self.devices) == 0:
52+
raise ValueError("'devices' must be a non-empty list.")
53+
for i, d in enumerate(self.devices):
54+
if not isinstance(d, DeviceAccess):
55+
raise ValueError(f"'devices[{i}]' must be a DeviceAccess.")
56+
57+
return self
58+
59+
60+
CatalogValue = Union[DeviceAccess, list[DeviceAccess]]
61+
62+
63+
class CatalogEntry:
64+
def __init__(self, cfg: ConfigModel):
65+
self._cfg: ConfigModel = cfg
66+
self._value: CatalogValue = (
67+
cfg.device if cfg.device is not None else cfg.devices
68+
)
69+
70+
def get_reference(self) -> str:
71+
return self._cfg.reference
72+
73+
def get_value(self) -> CatalogValue:
74+
return self._value

pyaml/control/controlsystem.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from ..common.element import Element
99
from ..common.element_holder import ElementHolder
1010
from ..common.exception import PyAMLException
11+
from ..configuration.catalog import Catalog
1112
from ..configuration.factory import Factory
1213
from ..control.abstract_impl import (
1314
CSBPMArrayMapper,
@@ -47,6 +48,10 @@ class ControlSystem(ElementHolder, metaclass=ABCMeta):
4748

4849
def __init__(self):
4950
ElementHolder.__init__(self)
51+
self._catalog: Catalog | None = None
52+
53+
def set_catalog(self, catalog: Catalog):
54+
self._catalog = catalog
5055

5156
@abstractmethod
5257
def attach(self, dev: list[DeviceAccess]) -> list[DeviceAccess]:

tests/config/bpms.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,30 @@ devices:
8585
- [A0, SH1A-C01-V]
8686
- [A1, SH1A-C01-SQ]
8787
model: sr/magnet_models/SH1AC01.yaml
88+
control_system_catalog:
89+
type: pyaml.configuration.catalog
90+
refs:
91+
- type: pyaml.configuration.catalog_entry
92+
reference: BPM_C01-02
93+
devices:
94+
- type: tango.pyaml.attribute_read_only
95+
attribute: srdiag/bpm/c01-02/SA_HPosition
96+
unit: mm
97+
- type: tango.pyaml.attribute_read_only
98+
attribute: srdiag/bpm/c01-02/SA_HPosition
99+
unit: mm
100+
- type: pyaml.configuration.catalog_entry
101+
reference: BPM_C01-03
102+
devices:
103+
- type: tango.pyaml.attribute_read_only
104+
attribute: srdiag/bpm/c01-03/SA_HPosition
105+
unit: mm
106+
- type: tango.pyaml.attribute_read_only
107+
attribute: srdiag/bpm/c01-03/SA_VPosition
108+
unit: mm
109+
- type: pyaml.configuration.catalog_entry
110+
reference: BPM_C01-04
111+
device:
112+
type: tango.pyaml.attribute_read_only
113+
attribute: srdiag/bpm/c01-04/Position
114+
unit: mm

0 commit comments

Comments
 (0)