import re import sys from functools import total_ordering from itertools import groupby from pathlib import Path from typing import List, Optional, MutableMapping from ee import EeException from ee.db import ObjDb from ee.logging import log from ee.part import Part, load_db __all__ = [ "load_bom", "check_bom", "BomLine", "generate_bom", "split_ref", "join_refs", ] @total_ordering class BomLine(object): def __init__(self, uri, part: Part): self.uri = uri self.part = part self.refs: List[str] = [] def add_ref(self, ref: str): self.refs.append(ref) def __lt__(self, other): return self.uri < other.uri class Bom(object): def __init__(self): self._lines: MutableMapping[str, BomLine] = {} def add_part(self, part_uri, supplier_part, reference): if part_uri not in self._lines: self._lines[part_uri] = line = BomLine(part_uri, supplier_part) else: line = self._lines[part_uri] line.add_ref(reference) @property def lines(self) -> List[BomLine]: return sorted(self._lines.values()) def load_bom(bom_path: Path, part_files: List[Path]) -> (ObjDb[Part](), ObjDb[Part]()): def uri_fn(part: Part): return part.uri def supplier_fn(part: Part): return part.supplier bom: ObjDb[Part] = ObjDb[Part]() for xml in load_db(bom_path).iterparts(): bom.add(Part(xml)) parts: ObjDb[Part] = ObjDb[Part]() parts.add_unique_index("uri", uri_fn) parts.add_index("supplier", supplier_fn) for part_file in part_files: for xml in load_db(part_file).iterparts(): parts.add(Part(xml)) return bom, parts def check_bom(bom: ObjDb[Part](), parts: ObjDb[Part]()): pass def generate_bom(allow_incomplete, bom_parts: ObjDb[Part], supplier_parts: ObjDb[Part]) -> Optional[Bom]: uri_idx = supplier_parts.index("uri") bom = Bom() for part in bom_parts.values: pr = part.get_only_part_reference() if allow_incomplete and pr is None: log.debug("Skipping part without part reference: {}".format(part.printable_reference)) continue elif pr is None: log.warn("Unresolved part: {}".format(part.printable_reference)) return part_uri = pr.part_uriProp supplier_part = uri_idx.get_single(part_uri) reference = part.get_exactly_one_schematic_reference().referenceProp bom.add_part(part_uri, supplier_part, reference) return bom def split_ref(ref): """Split "C12" into a tuple that's useful for sorting by component reference. For example: "C12" => ("C", 12, None). "Cfoo" => ("C", sys.maxsize, ""). """ m = split_ref.r.match(ref) if not m: return ref, sys.maxsize, "" groups = m.groups() ref = groups[0] val = groups[1] rest = groups[2] try: return ref, int(val), rest except ValueError: pass return ref, val, rest split_ref.r = re.compile("([A-Za-z]+)([0-9]+)(.*)") def join_refs(refs: List[str], max=None) -> str: refs = [ref.strip() for ref in refs] rs = sorted([split_ref(ref) for ref in refs]) for r in rs: if r[1] is None: raise EeException("Bad ref, must match '[A-Z]+[0-9]+'.") s = "" for kind, items in groupby(rs, lambda x: x[0]): items = list(items) groups = [] start = cur = items[0][1] for r in items[1:]: num = r[1] if cur + 1 != num: groups.append([start, cur]) start = cur = num else: cur = num groups.append([start, cur]) parts = [] for start, end in groups: if start == end: parts.append(str(start)) elif start + 1 == end: parts.append("{},{}".format(start, end)) else: parts.append("{}-{}".format(start, end)) s += kind + ",".join(parts) return s