2HW: Finds non-unique DAL objects and provides a simple CLI to rename them iteratively
4from typing
import List, Dict, Any, Set
5from collections
import defaultdict
6from pathlib
import Path
7from itertools
import combinations
10from rich.console
import Console
11from rich.table
import Table
12from rich.prompt
import Prompt
13from rich.panel
import Panel
16from conffwk
import Configuration
17from conffwk.dal
import DalBase
26 super().
__init__(f
"Cannot commit changes to {config}, make sure it isn't open in another process!")
32 """Caches parent-child relationships between DAL objects to avoid repeated DB calls."""
35 if isinstance(db, str):
36 db = Configuration(
"oksconflibs:" + db)
49 for obj
in self.
db.get_all_dals().values():
52 related = getattr(obj, rel,
None)
55 children.extend(related
if isinstance(related, list)
else [related])
63 parent
for parent, children
in self.
graph.items()
if obj
in children
71 """A DAL object enriched with its configuration and relationship tree."""
73 def __init__(self, dal: DalBase, config: Configuration, tree: RelationshipCache):
88 a: getattr(self.
dal, a,
None)
98 related = getattr(self.
dal, rel,
None)
101 elif not isinstance(related, list):
114 if not isinstance(other, ExtendedDal):
117 self.
dal.className() == other.dal.className()
130 """A deduplicated group of DAL objects that share the same repr (i.e. are duplicates)."""
132 def __init__(self, pairs: List[tuple], trees: Dict[Configuration, RelationshipCache]):
133 seen: List[ExtendedDal] = []
134 for dal, config
in pairs:
136 if not any(ext == existing
for existing
in seen):
141 def dals(self) -> List[DalBase]:
142 return [m.dal
for m
in self.
members]
146 return any(a.has_same_parents(b)
for a, b
in combinations(self.
members, 2))
161 return [int(c)
if c.isdigit()
else c.lower()
for c
in re.split(
r"(\d+)", s)]
165 groups: List[DalGroup],
166 trees: Dict[Configuration, RelationshipCache],
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] = []
175 while covered != all_ids:
179 sessions = tree_list[0].db.get_dals(
"Session")
181 raise ValueError(
"Configuration must contain a Session object!")
184 remaining = all_ids - covered
185 root = next((g.dals[0]
for g
in groups
if g.dals[0].id
in remaining),
None)
190 depth_map: Dict[str, int] = {}
192 visited: Set[int] = set()
195 obj, depth = queue.pop(0)
196 if id(obj)
in visited:
199 depth_map[obj.id] = depth
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))
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)
219 """Loads all configurations and finds duplicate DAL objects across them."""
224 console.print(
"[blue]Building configuration trees…[/]")
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))
234 groups = [
DalGroup(pairs, self.
trees)
for pairs
in grouped.values()]
235 self.
groups = [g
for g
in groups
if len(g) > 1]
262 console.print(
"[green]No duplicate DALs found.[/]")
267 prompt =
r"\[n] next, \[p] prev, \[c] commit, \[s] save & quit, \[q] quit, or a number to rename"
269 while self.
_idx <= len(groups):
271 if self.
_idx == len(groups):
272 console.print(
"\n[bold]Reached the end of all duplicate groups.[/]")
274 r"\[s] save & quit, \[p] go back, \[q] quit without saving",
281 console.print(
"[bold green]Saved and exiting.[/]")
284 console.print(
"[bold red]Exiting without saving.[/]")
288 group = groups[self.
_idx]
290 console.print(f
"[dim]({self._idx + 1}/{len(groups)})[/]")
292 action = Prompt.ask(prompt, default=last).lower().strip()
301 console.print(
"[yellow]Already at the first item.[/]")
305 console.print(
"[bold green]Changes committed.[/]")
308 console.print(
"[bold green]Saved and exiting.[/]")
311 console.print(
"[bold red]Exiting without saving.[/]")
313 elif action.isdigit():
317 console.print(
"[red]Unknown command.[/]")
322 if group.has_same_parents:
325 "[bold red]Renaming Disabled[/]\n\n"
326 "These duplicate DALs share the same parents.\n"
327 "Renaming would cause commit inconsistencies.",
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")
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())
345 str(i), ext.id, ext.dal.className(),
346 str(ext.config), str(ext.get_parents()),
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=
"")
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=
"")
365 console.print(f
"Renaming [cyan]{ext.id}[/] ({ext.dal.className()})")
366 new_name = Prompt.ask(
"Enter new name (or empty to cancel)").strip()
368 console.print(
"[yellow]Rename cancelled.[/]")
373 self.
history.append((ext, old_name))
374 console.print(f
"[green]Renamed {old_name} → {new_name}[/]")
378@click.option("--input-folder", "-i", required=True, type=click.Path())
380 folder = Path(input_folder)
381 if not folder.exists()
or not folder.is_dir():
382 raise FileNotFoundError(f
"Cannot find {folder}")
384 configs = [Configuration(f
"oksconflibs:{f}")
for f
in folder.glob(
"*.data.xml")]
386 raise FileNotFoundError(
387 "No configs found — ensure the folder contains .data.xml files."
394if __name__ ==
"__main__":
None __init__(self, config)
DalGroup __getitem__(self, int idx)
__init__(self, List[Configuration] configs)
bool has_same_parents(self)
ExtendedDal __getitem__(self, int idx)
__init__(self, List[tuple] pairs, Dict[Configuration, RelationshipCache] trees)
Dict[str, Any] attributes(self)
Dict[str, Set[str]]|None _relations
List[DalBase] get_parents(self)
Dict[str, Set[str]] relations
bool __eq__(self, object other)
bool has_same_parents(self, "ExtendedDal" other)
None rename(self, str name)
Dict[str, Set[str]] relations(self)
__init__(self, DalBase dal, Configuration config, RelationshipCache tree)
Dict[str, Any] attributes
Dict[str, Any]|None _attributes
List[DalBase] get_parents(self, DalBase obj)
Dict[DalBase, List[DalBase]] _build_graph(self)
Dict[DalBase, List[DalBase]] graph
__init__(self, Configuration|str db)
_relations(self, str class_name)
_render(self, DalGroup group)
__init__(self, DalCollector collector)
_handle_rename(self, str action, DalGroup group)
static volatile sig_atomic_t run
list _natural_sort_key(str s)
List[DalGroup] sort_groups_by_depth(List[DalGroup] groups, Dict[Configuration, RelationshipCache] trees)