from __future__ import annotations from dataclasses import dataclass from pathlib import Path from typing import Iterable SCALE_NATIVE = { "Z": 20_000_000, "A": 3_000_000, "B": 1_000_000, "C": 200_000, "D": 100_000, "E": 50_000, "F": 20_000, "G": 7_500, } @dataclass(frozen=True) class CellFile: path: Path relative_path: Path scale_code: str native_scale: int @dataclass(frozen=True) class ScanResult: root: Path cells: tuple[CellFile, ...] object_dictionary: Path | None attribute_dictionary: Path | None def _find_case_insensitive(root: Path, names: set[str]) -> Path | None: lowers = {n.lower() for n in names} for p in root.rglob("*"): if p.is_file() and p.name.lower() in lowers: return p return None def scan_cm93(root: Path) -> ScanResult: root = root.resolve() if not root.exists(): raise FileNotFoundError(root) cells = [] for p in root.rglob("*"): if not p.is_file(): continue ext = p.suffix.upper().lstrip(".") if ext in SCALE_NATIVE: cells.append( CellFile( path=p, relative_path=p.relative_to(root), scale_code=ext, native_scale=SCALE_NATIVE[ext], ) ) cells.sort(key=lambda c: (c.scale_code, str(c.relative_path).lower())) obj = _find_case_insensitive(root, {"CM93OBJ.DIC"}) attr = _find_case_insensitive(root, {"ATTRLUT.DIC", "CM93ATTR.DIC"}) return ScanResult(root, tuple(cells), obj, attr)