|
1 | | -import re |
2 | | -from typing import Pattern |
3 | | - |
4 | 1 | from pydantic import BaseModel, ConfigDict, model_validator |
5 | 2 |
|
6 | 3 | from pyaml import PyAMLException |
7 | | -from pyaml.configuration.catalog_entry import CatalogEntry, CatalogValue |
8 | | -from pyaml.configuration.catalog_entry import ConfigModel as CatalogEntryConfigModel |
| 4 | +from pyaml.configuration.catalog_entry import CatalogEntry, CatalogTarget |
| 5 | +from pyaml.control.catalog_view import CatalogView |
9 | 6 | from pyaml.control.deviceaccess import DeviceAccess |
10 | 7 |
|
11 | 8 | # Define the main class name for this module |
@@ -46,249 +43,153 @@ def _validate_unique_references(self) -> "ConfigModel": |
46 | 43 |
|
47 | 44 | class Catalog: |
48 | 45 | """ |
49 | | - A simple registry mapping reference keys to DeviceAccess objects. |
| 46 | + Shared configuration catalog. |
| 47 | +
|
| 48 | + This catalog stores *prototypes* (not attached) and can be shared across |
| 49 | + multiple ControlSystem instances. |
50 | 50 |
|
51 | | - The catalog is intentionally minimal: |
52 | | - - It resolves references to DeviceAccess or list[DeviceAccess] |
53 | | - - It does NOT expose any DeviceAccess-like interface (no get/set/readback/etc.) |
| 51 | + Key points |
| 52 | + ---------- |
| 53 | + - sr.get_catalog(name) returns this configuration object (as you described). |
| 54 | + - Each ControlSystem (identified by name) uses a per-CS runtime view: |
| 55 | + cfg_catalog.view(control_system) |
| 56 | + - Updates are propagated to all existing views eagerly (refresh occurs |
| 57 | + immediately), while actual device connections remain lazy inside |
| 58 | + DeviceAccess backends. |
| 59 | +
|
| 60 | + Update semantics |
| 61 | + --------------- |
| 62 | + update_proto(reference, proto) updates the shared prototype and triggers a |
| 63 | + refresh of that reference in every existing CatalogView. |
54 | 64 | """ |
55 | 65 |
|
56 | 66 | def __init__(self, cfg: ConfigModel): |
57 | 67 | self._cfg = cfg |
58 | | - self._entries: dict[str, CatalogValue] = {} |
| 68 | + self._protos: dict[str, CatalogTarget] = {} |
| 69 | + |
| 70 | + # Views indexed by ControlSystem name (instance identity rule). |
| 71 | + self._views: dict[str, CatalogView] = {} |
| 72 | + |
59 | 73 | for ref in cfg.refs: |
60 | | - self.add(ref.get_reference(), ref.get_value()) |
| 74 | + self.add_proto(ref.get_reference(), ref.get_value()) |
61 | 75 |
|
62 | 76 | # ------------------------------------------------------------------ |
63 | 77 |
|
64 | 78 | def get_name(self) -> str: |
| 79 | + """Return the catalog name.""" |
65 | 80 | return self._cfg.name |
66 | 81 |
|
67 | 82 | # ------------------------------------------------------------------ |
68 | 83 |
|
69 | | - def add(self, reference: str, value: CatalogValue): |
70 | | - """ |
71 | | - Register a reference in the catalog. |
| 84 | + def keys(self) -> list[str]: |
| 85 | + """Return all reference keys in this catalog.""" |
| 86 | + return list(self._protos.keys()) |
72 | 87 |
|
73 | | - Raises |
74 | | - ------ |
75 | | - PyAMLException |
76 | | - If the reference already exists. |
77 | | - """ |
78 | | - if reference in self._entries: |
79 | | - raise PyAMLException(f"Duplicate catalog reference: '{reference}'") |
80 | | - self._entries[reference] = value |
| 88 | + # ------------------------------------------------------------------ |
| 89 | + |
| 90 | + def has_reference(self, reference: str) -> bool: |
| 91 | + """Return True if a prototype exists for this reference.""" |
| 92 | + return reference in self._protos |
81 | 93 |
|
82 | 94 | # ------------------------------------------------------------------ |
83 | 95 |
|
84 | | - def get(self, reference: str) -> CatalogValue: |
| 96 | + def add_proto(self, reference: str, proto: CatalogTarget): |
85 | 97 | """ |
86 | | - Resolve a reference key. |
| 98 | + Add a new prototype entry. |
87 | 99 |
|
88 | | - Returns |
89 | | - ------- |
90 | | - DeviceAccess | list[DeviceAccess] |
| 100 | + Parameters |
| 101 | + ---------- |
| 102 | + reference : str |
| 103 | + Reference key. |
| 104 | + proto : DeviceAccess | list[DeviceAccess] |
| 105 | + Prototype device(s), not attached. |
91 | 106 |
|
92 | 107 | Raises |
93 | 108 | ------ |
94 | 109 | PyAMLException |
95 | | - If the reference does not exist. |
| 110 | + If the reference already exists. |
96 | 111 | """ |
97 | | - try: |
98 | | - return self._entries[reference] |
99 | | - except KeyError as exc: |
100 | | - raise PyAMLException(f"Catalog reference '{reference}' not found.") from exc |
| 112 | + if reference in self._protos: |
| 113 | + raise PyAMLException(f"Duplicate catalog reference: '{reference}'") |
| 114 | + self._protos[reference] = proto |
| 115 | + self._notify_update(reference) |
101 | 116 |
|
102 | 117 | # ------------------------------------------------------------------ |
103 | 118 |
|
104 | | - def get_one(self, reference: str) -> DeviceAccess: |
| 119 | + def update_proto(self, reference: str, proto: CatalogTarget): |
105 | 120 | """ |
106 | | - Resolve a reference and ensure it corresponds to a single DeviceAccess. |
| 121 | + Update an existing prototype entry. |
| 122 | +
|
| 123 | + Parameters |
| 124 | + ---------- |
| 125 | + reference : str |
| 126 | + Existing reference key. |
| 127 | + proto : DeviceAccess | list[DeviceAccess] |
| 128 | + New prototype device(s), not attached. |
107 | 129 |
|
108 | 130 | Raises |
109 | 131 | ------ |
110 | 132 | PyAMLException |
111 | | - If the reference does not exist or is multi-device. |
| 133 | + If the reference does not exist. |
112 | 134 | """ |
113 | | - value = self.get(reference) |
114 | | - |
115 | | - if isinstance(value, list): |
116 | | - raise PyAMLException( |
117 | | - f"Catalog reference '{reference}' is multi-device; use get_many()." |
118 | | - ) |
119 | | - |
120 | | - return value |
| 135 | + if reference not in self._protos: |
| 136 | + raise PyAMLException(f"Catalog reference '{reference}' not found.") |
| 137 | + self._protos[reference] = proto |
| 138 | + self._notify_update(reference) |
121 | 139 |
|
122 | 140 | # ------------------------------------------------------------------ |
123 | 141 |
|
124 | | - def get_many(self, reference: str) -> list[DeviceAccess]: |
| 142 | + def get_proto(self, reference: str) -> CatalogTarget: |
125 | 143 | """ |
126 | | - Resolve a reference and ensure it corresponds to multiple DeviceAccess. |
127 | | -
|
128 | | - Returns |
129 | | - ------- |
130 | | - list[DeviceAccess] |
| 144 | + Return the prototype for a given reference. |
131 | 145 |
|
132 | 146 | Raises |
133 | 147 | ------ |
134 | 148 | PyAMLException |
135 | | - If the reference does not exist or is single-device. |
136 | | - """ |
137 | | - value = self.get(reference) |
138 | | - |
139 | | - if not isinstance(value, list): |
140 | | - raise PyAMLException( |
141 | | - f"Catalog reference '{reference}' is single-device; use get_one()." |
142 | | - ) |
143 | | - |
144 | | - return value |
145 | | - |
146 | | - # ------------------------------------------------------------------ |
147 | | - |
148 | | - def find_by_prefix(self, prefix: str) -> dict[str, CatalogValue]: |
149 | | - """ |
150 | | - Return all catalog entries whose reference starts with |
151 | | - the given prefix. |
152 | | -
|
153 | | - Parameters |
154 | | - ---------- |
155 | | - prefix : str |
156 | | - Prefix to match at the beginning of reference keys. |
157 | | -
|
158 | | - Returns |
159 | | - ------- |
160 | | - dict[str, CatalogValue] |
161 | | - Mapping {reference -> DeviceAccess or list[DeviceAccess]}. |
162 | | -
|
163 | | - Notes |
164 | | - ----- |
165 | | - - The prefix is escaped using re.escape() to avoid |
166 | | - unintended regular expression behavior. |
167 | | - - This is a convenience wrapper around `find()`. |
168 | | - """ |
169 | | - return self.find(rf"^{re.escape(prefix)}") |
170 | | - |
171 | | - # ------------------------------------------------------------------ |
172 | | - |
173 | | - def find(self, pattern: str) -> dict[str, CatalogValue]: |
174 | | - """ |
175 | | - Resolve references matching a regular expression. |
176 | | -
|
177 | | - Parameters |
178 | | - ---------- |
179 | | - pattern : str |
180 | | - Regular expression applied to reference keys. |
181 | | -
|
182 | | - Returns |
183 | | - ------- |
184 | | - dict[str, DeviceAccess | list[DeviceAccess]] |
185 | | - Mapping {reference -> value}. |
186 | | - """ |
187 | | - regex: Pattern[str] = re.compile(pattern) |
188 | | - return {k: v for k, v in self._entries.items() if regex.search(k)} |
189 | | - |
190 | | - # ------------------------------------------------------------------ |
191 | | - |
192 | | - def get_sub_catalog_by_prefix(self, prefix: str) -> "Catalog": |
193 | | - """ |
194 | | - Create a new Catalog containing only the references |
195 | | - that start with the given prefix, and remove the prefix |
196 | | - from the keys in the returned catalog. |
197 | | -
|
198 | | - Parameters |
199 | | - ---------- |
200 | | - prefix : str |
201 | | - Prefix to match at the beginning of reference keys. |
202 | | -
|
203 | | - Returns |
204 | | - ------- |
205 | | - Catalog |
206 | | - A new Catalog instance containing only the matching |
207 | | - references, with the prefix removed from their keys. |
208 | | -
|
209 | | - Notes |
210 | | - ----- |
211 | | - - The prefix is matched literally (no regex behavior). |
212 | | - - The underlying DeviceAccess instances are NOT copied; |
213 | | - the same objects are reused. |
214 | | - - If no references match, an empty Catalog is returned. |
215 | | - - If removing the prefix results in duplicate keys, |
216 | | - a PyAMLException is raised. |
| 149 | + If the reference does not exist. |
217 | 150 | """ |
218 | | - sub_catalog = Catalog(ConfigModel(name=self.get_name() + "/" + prefix, refs=[])) |
219 | | - |
220 | | - for key, value in self._entries.items(): |
221 | | - if key.startswith(prefix): |
222 | | - # Remove prefix from key |
223 | | - new_key = key[len(prefix) :] |
224 | | - |
225 | | - if not new_key: |
226 | | - raise PyAMLException( |
227 | | - f"Removing prefix '{prefix}' from '{key}' " |
228 | | - "results in an empty reference." |
229 | | - ) |
230 | | - |
231 | | - sub_catalog.add(new_key, value) |
232 | | - |
233 | | - return sub_catalog |
| 151 | + try: |
| 152 | + return self._protos[reference] |
| 153 | + except KeyError as exc: |
| 154 | + raise PyAMLException(f"Catalog reference '{reference}' not found.") from exc |
234 | 155 |
|
235 | 156 | # ------------------------------------------------------------------ |
236 | 157 |
|
237 | | - def get_sub_catalog(self, pattern: str) -> "Catalog": |
| 158 | + def view(self, control_system: "ControlSystem") -> CatalogView: # noqa: F821 |
238 | 159 | """ |
239 | | - Create a new Catalog containing only the references |
240 | | - matching the given regular expression. |
| 160 | + Return a per-ControlSystem runtime view of this catalog. |
241 | 161 |
|
242 | 162 | Parameters |
243 | 163 | ---------- |
244 | | - pattern : str |
245 | | - Regular expression applied to reference keys. |
| 164 | + control_system : ControlSystem |
| 165 | + ControlSystem instance. Its name identifies the view. |
246 | 166 |
|
247 | 167 | Returns |
248 | 168 | ------- |
249 | | - Catalog |
250 | | - A new Catalog instance containing only the matching |
251 | | - references and their associated DeviceAccess objects. |
| 169 | + CatalogView |
| 170 | + Runtime view bound to the provided ControlSystem. |
252 | 171 |
|
253 | 172 | Notes |
254 | 173 | ----- |
255 | | - - The returned catalog is independent from the original one. |
256 | | - - The underlying DeviceAccess objects are not copied; the |
257 | | - same instances are reused. |
258 | | - - If no references match, an empty Catalog is returned. |
| 174 | + - A view is created once per ControlSystem name and cached. |
| 175 | + - The view is immediately refreshed (all entries attached in that CS context). |
259 | 176 | """ |
260 | | - data = self.find(pattern) |
261 | | - |
262 | | - # Create a new empty catalog with a derived name |
263 | | - sub_catalog = Catalog( |
264 | | - ConfigModel(name=self.get_name() + "/" + pattern, refs=[]) |
265 | | - ) |
266 | | - |
267 | | - # Re-register matching entries in the new catalog |
268 | | - for k, v in data.items(): |
269 | | - sub_catalog.add(k, v) |
270 | | - return sub_catalog |
271 | | - |
272 | | - # ------------------------------------------------------------------ |
273 | | - |
274 | | - def keys(self) -> list[str]: |
275 | | - """Return all catalog reference keys.""" |
276 | | - return list(self._entries.keys()) |
| 177 | + cs_name = control_system.name() |
| 178 | + view = self._views.get(cs_name) |
| 179 | + if view is None: |
| 180 | + view = CatalogView(config_catalog=self, control_system=control_system) |
| 181 | + self._views[cs_name] = view |
| 182 | + view.refresh_all() |
| 183 | + return view |
277 | 184 |
|
278 | 185 | # ------------------------------------------------------------------ |
279 | 186 |
|
280 | | - def has_reference(self, reference: str) -> bool: |
| 187 | + def _notify_update(self, reference: str): |
281 | 188 | """ |
282 | | - Return True if the reference exists in the catalog. |
| 189 | + Notify all existing views that a reference prototype has changed. |
283 | 190 |
|
284 | | - Parameters |
285 | | - ---------- |
286 | | - reference : str |
287 | | - Catalog reference key. |
288 | | -
|
289 | | - Returns |
290 | | - ------- |
291 | | - bool |
292 | | - True if the reference exists, False otherwise. |
| 191 | + We choose eager propagation: every view refreshes that reference immediately. |
| 192 | + DeviceAccess backends remain lazy, so this is typically cheap. |
293 | 193 | """ |
294 | | - return reference in self._entries |
| 194 | + for view in self._views.values(): |
| 195 | + view.refresh_reference(reference) |
0 commit comments