DUNE-DAQ
DUNE Trigger and Data Acquisition software
Loading...
Searching...
No Matches
rename_duplicate_dals.py
Go to the documentation of this file.
1"""
2HW: Finds non-unique DAL objects and provides a simple CLI to rename them iteratively
3"""
4from typing import List, Dict, Any, Set
5from collections import defaultdict
6from pathlib import Path
7from itertools import combinations
8import re
9
10from rich.console import Console
11from rich.table import Table
12from rich.prompt import Prompt
13from rich.panel import Panel
14import click
15
16from conffwk import Configuration
17from conffwk.dal import DalBase
18
19console = Console()
20
21
22# ----- Errors -----
23
24class ConfigCommitError(Exception):
25 def __init__(self, config) -> None:
26 super().__init__(f"Cannot commit changes to {config}, make sure it isn't open in another process!")
27
28
29# ----- Relationship Cache -----
30
32 """Caches parent-child relationships between DAL objects to avoid repeated DB calls."""
33
34 def __init__(self, db: Configuration | str):
35 if isinstance(db, str):
36 db = Configuration("oksconflibs:" + db)
37 self.db = db
38 self._relations_cache: Dict[str, Any] = {}
39 self._parents_cache_parents_cache: Dict[Any, list] = {}
40 self.graph = self._build_graph()
41
42 def _relations(self, class_name: str):
43 if class_name not in self._relations_cache:
44 self._relations_cache[class_name] = self.db.relations(class_name, all=True)
45 return self._relations_cache[class_name]
46
47 def _build_graph(self) -> Dict[DalBase, List[DalBase]]:
48 graph = {}
49 for obj in self.db.get_all_dals().values():
50 children = []
51 for rel in self._relations(obj.className()):
52 related = getattr(obj, rel, None)
53 if related is None:
54 continue
55 children.extend(related if isinstance(related, list) else [related])
56 if children:
57 graph[obj] = children
58 return graph
59
60 def get_parents(self, obj: DalBase) -> List[DalBase]:
61 if obj not in self._parents_cache_parents_cache:
63 parent for parent, children in self.graph.items() if obj in children
64 ]
65 return self._parents_cache_parents_cache[obj]
66
67
68# ----- Extended DAL (wraps a DAL with its config + tree context) -----
69
71 """A DAL object enriched with its configuration and relationship tree."""
72
73 def __init__(self, dal: DalBase, config: Configuration, tree: RelationshipCache):
74 self.dal = dal
75 self.config = config
76 self.tree = tree
77 self._attributes: Dict[str, Any] | None = None
78 self._relations_relations: Dict[str, Set[str]] | None = None
79
80 @property
81 def id(self) -> str:
82 return self.dal.id
83
84 @property
85 def attributes(self) -> Dict[str, Any]:
86 if self._attributes is None:
87 self._attributes = {
88 a: getattr(self.dal, a, None)
89 for a in self.config.attributes(self.dal.className())
90 }
91 return self._attributes
92
93 @property
94 def relations(self) -> Dict[str, Set[str]]:
95 if self._relations_relations is None:
97 for rel in self.config.relations(self.dal.className(), all=True):
98 related = getattr(self.dal, rel, None)
99 if related is None:
100 related = []
101 elif not isinstance(related, list):
102 related = [related]
103 self._relations_relations[rel] = {repr(r) for r in related}
104 return self._relations_relations
105
106 def get_parents(self) -> List[DalBase]:
107 return self.tree.get_parents(self.dal)
108
109 def rename(self, name: str) -> None:
110 self.dal.rename(name)
111 self.config.update_dal(self.dal)
112
113 def __eq__(self, other: object) -> bool:
114 if not isinstance(other, ExtendedDal):
115 return False
116 return (
117 self.dal.className() == other.dal.className()
118 and self.idid == other.id
119 and self.attributesattributes == other.attributes
120 and self.relationsrelations == other.relations
121 )
122
123 def has_same_parents(self, other: "ExtendedDal") -> bool:
124 return self.get_parents() == other.get_parents()
125
126
127# ----- Consolidated group of duplicate DALs -----
128
130 """A deduplicated group of DAL objects that share the same repr (i.e. are duplicates)."""
131
132 def __init__(self, pairs: List[tuple], trees: Dict[Configuration, RelationshipCache]):
133 seen: List[ExtendedDal] = []
134 for dal, config in pairs:
135 ext = ExtendedDal(dal, config, trees[config])
136 if not any(ext == existing for existing in seen):
137 seen.append(ext)
138 self.members = seen
139
140 @property
141 def dals(self) -> List[DalBase]:
142 return [m.dal for m in self.members]
143
144 @property
145 def has_same_parents(self) -> bool:
146 return any(a.has_same_parents(b) for a, b in combinations(self.members, 2))
147
148 def __len__(self) -> int:
149 return len(self.members)
150
151 def __getitem__(self, idx: int) -> ExtendedDal:
152 return self.members[idx]
153
154 def __iter__(self):
155 return iter(self.members)
156
157
158# ----- Tree-based sorting -----
159
160def _natural_sort_key(s: str) -> list:
161 return [int(c) if c.isdigit() else c.lower() for c in re.split(r"(\d+)", s)]
162
163
165 groups: List[DalGroup],
166 trees: Dict[Configuration, RelationshipCache],
167) -> List[DalGroup]:
168 """Sort DalGroups by their depth in the configuration tree (BFS from Session root)."""
169 all_ids = {g.dals[0].id for g in groups}
170 tree_list = list(trees.values())
171 covered: Set[str] = set()
172 sorted_groups: List[DalGroup] = []
173 iteration = 0
174
175 while covered != all_ids:
176 iteration += 1
177
178 if iteration == 1:
179 sessions = tree_list[0].db.get_dals("Session")
180 if not sessions:
181 raise ValueError("Configuration must contain a Session object!")
182 root = sessions[0]
183 else:
184 remaining = all_ids - covered
185 root = next((g.dals[0] for g in groups if g.dals[0].id in remaining), None)
186 if root is None:
187 break
188
189 # BFS to build depth map
190 depth_map: Dict[str, int] = {}
191 queue = [(root, 0)]
192 visited: Set[int] = set()
193
194 while queue:
195 obj, depth = queue.pop(0)
196 if id(obj) in visited:
197 continue
198 visited.add(id(obj))
199 depth_map[obj.id] = depth
200 covered.add(obj.id)
201
202 seen_children: Set[int] = set()
203 for tree in tree_list:
204 for child in tree.graph.get(obj, []):
205 if id(child) not in visited and id(child) not in seen_children:
206 queue.append((child, depth + 1))
207 seen_children.add(id(child))
208
209 batch = [g for g in groups if g.dals[0].id in depth_map]
210 batch.sort(key=lambda g: (depth_map[g.dals[0].id], _natural_sort_key(g.dals[0].id)))
211 sorted_groups.extend(batch)
212
213 return sorted_groups
214
215
216# ----- DAL Collector -----
217
219 """Loads all configurations and finds duplicate DAL objects across them."""
220
221 def __init__(self, configs: List[Configuration]):
222 self.configs = configs
223
224 console.print("[blue]Building configuration trees…[/]")
225 self.trees = {c: RelationshipCache(c) for c in configs}
226
227 # Group all (dal, config) pairs by DAL repr
228 grouped: Dict[str, List[tuple]] = defaultdict(list)
229 for config in configs:
230 for dal in config.get_all_dals().values():
231 grouped[repr(dal)].append((dal, config))
232
233 # Keep only groups with more than one unique member
234 groups = [DalGroup(pairs, self.trees) for pairs in grouped.values()]
235 self.groups = [g for g in groups if len(g) > 1]
236
237 if self.groups:
238 self.groups = sort_groups_by_depth(self.groups, self.trees)
239
240 def commit(self):
241 for config in self.configs:
242 config.commit()
243
244 def __len__(self) -> int:
245 return len(self.groups)
246
247 def __getitem__(self, idx: int) -> DalGroup:
248 return self.groups[idx]
249
250
251# ----- CLI -----
252
254 def __init__(self, collector: DalCollector):
255 self.collector = collector
256 self.history: list = []
257 self._idx = 0
258
259 def run(self):
260 groups = self.collector.groups
261 if not groups:
262 console.print("[green]No duplicate DALs found.[/]")
263 return
264
265 last = "n"
266 # Rich treats [...] as markup tags, so use \[ to render literal square brackets
267 prompt = r"\[n] next, \[p] prev, \[c] commit, \[s] save & quit, \[q] quit, or a number to rename"
268
269 while self._idx <= len(groups):
270 # End-of-list: offer save/back/quit instead of silently exiting
271 if self._idx == len(groups):
272 console.print("\n[bold]Reached the end of all duplicate groups.[/]")
273 action = Prompt.ask(
274 r"\[s] save & quit, \[p] go back, \[q] quit without saving",
275 default="s",
276 ).lower().strip()
277 if action == "p":
278 self._idx -= 1
279 elif action == "s":
280 self.collector.commit()
281 console.print("[bold green]Saved and exiting.[/]")
282 return
283 else:
284 console.print("[bold red]Exiting without saving.[/]")
285 return
286 continue
287
288 group = groups[self._idx]
289 self._render(group)
290 console.print(f"[dim]({self._idx + 1}/{len(groups)})[/]")
291
292 action = Prompt.ask(prompt, default=last).lower().strip()
293
294 if action == "n":
295 self._idx += 1
296 last = action
297 elif action == "p":
298 if self._idx > 0:
299 self._idx -= 1
300 else:
301 console.print("[yellow]Already at the first item.[/]")
302 last = action
303 elif action == "c":
304 self.collector.commit()
305 console.print("[bold green]Changes committed.[/]")
306 elif action == "s":
307 self.collector.commit()
308 console.print("[bold green]Saved and exiting.[/]")
309 return
310 elif action == "q":
311 console.print("[bold red]Exiting without saving.[/]")
312 return
313 elif action.isdigit():
314 self._handle_rename(action, group)
315 last = action
316 else:
317 console.print("[red]Unknown command.[/]")
318
319 def _render(self, group: DalGroup):
320 console.clear()
321
322 if group.has_same_parents:
323 console.print(
324 Panel(
325 "[bold red]Renaming Disabled[/]\n\n"
326 "These duplicate DALs share the same parents.\n"
327 "Renaming would cause commit inconsistencies.",
328 title="⚠ WARNING",
329 border_style="red",
330 expand=False,
331 )
332 )
333 console.print()
334
335 table = Table(title="Duplicated DAL Objects")
336 for col, style in [("#", ""), ("DAL ID", "cyan"), ("Class", "magenta"),
337 ("Configuration", "purple"), ("Parents", "blue"),
338 ("Attributes", "green"), ("Relations", "yellow")]:
339 table.add_column(col, style=style, justify="right" if col == "#" else "left")
340
341 for i, ext in enumerate(group, 1):
342 attr_str = ", ".join(f"{k}={v}" for k, v in ext.attributes.items())
343 rel_str = ", ".join(f"{k}=[{', '.join(v)}]" for k, v in ext.relations.items())
344 table.add_row(
345 str(i), ext.id, ext.dal.className(),
346 str(ext.config), str(ext.get_parents()),
347 attr_str, rel_str,
348 )
349
350 console.print(table)
351
352 def _handle_rename(self, action: str, group: DalGroup):
353 if group.has_same_parents:
354 console.print("[bold red]Renaming disabled — these DALs share the same parents.[/]")
355 Prompt.ask("[dim]Press Enter to continue[/]", default="")
356 return
357
358 idx = int(action) - 1
359 if not (0 <= idx < len(group)):
360 console.print("[red]Invalid number.[/]")
361 Prompt.ask("[dim]Press Enter to continue[/]", default="")
362 return
363
364 ext = group[idx]
365 console.print(f"Renaming [cyan]{ext.id}[/] ({ext.dal.className()})")
366 new_name = Prompt.ask("Enter new name (or empty to cancel)").strip()
367 if not new_name:
368 console.print("[yellow]Rename cancelled.[/]")
369 return
370
371 old_name = ext.id
372 ext.rename(new_name)
373 self.history.append((ext, old_name))
374 console.print(f"[green]Renamed {old_name} → {new_name}[/]")
375
376
377@click.command()
378@click.option("--input-folder", "-i", required=True, type=click.Path())
379def rename_duplicate_dals(input_folder: str):
380 folder = Path(input_folder)
381 if not folder.exists() or not folder.is_dir():
382 raise FileNotFoundError(f"Cannot find {folder}")
383
384 configs = [Configuration(f"oksconflibs:{f}") for f in folder.glob("*.data.xml")]
385 if not configs:
386 raise FileNotFoundError(
387 "No configs found — ensure the folder contains .data.xml files."
388 )
389
390 collector = DalCollector(configs)
391 RenameDalCli(collector).run()
392
393
394if __name__ == "__main__":
__init__(self, List[Configuration] configs)
ExtendedDal __getitem__(self, int idx)
__init__(self, List[tuple] pairs, Dict[Configuration, RelationshipCache] trees)
bool has_same_parents(self, "ExtendedDal" other)
__init__(self, DalBase dal, Configuration config, RelationshipCache tree)
List[DalBase] get_parents(self, DalBase obj)
Dict[DalBase, List[DalBase]] _build_graph(self)
__init__(self, DalCollector collector)
_handle_rename(self, str action, DalGroup group)
static volatile sig_atomic_t run
List[DalGroup] sort_groups_by_depth(List[DalGroup] groups, Dict[Configuration, RelationshipCache] trees)