import argparse from itertools import groupby from pathlib import Path from typing import List import ee.tools from ee.db import ObjDb from ee.part import Part, load_db, common_fact_types class Messages(object): INFO = 1 WARNING = 2 ERROR = 3 def __init__(self, part): self.part = part self.messages = [] def error(self, msg): self.messages.append((self.part, Messages.ERROR, msg)) return self def warning(self, msg): self.messages.append((self.part, Messages.WARNING, msg)) return self def info(self, msg): self.messages.append((self.part, Messages.INFO, msg)) return self @property def errors(self): return [m[2] for m in self.messages if m[1] == Messages.ERROR] @property def warnings(self): return [m[2] for m in self.messages if m[1] == Messages.WARNING] @property def infos(self): return [m[2] for m in self.messages if m[1] == Messages.INFO] def __len__(self): return self.messages.__len__() def append(self, messages: "Messages"): self.messages.extend(messages.messages) class Context(object): def __init__(self, supplier_parts: ObjDb[Part]): self.supplier_parts = supplier_parts self.uri_idx = supplier_parts.index("uri") def get_supplier_part(self, part_uri): return self.uri_idx.get_single(part_uri) def check_has_part(ctx: Context, bom: Part, sch: Part): ms = Messages(sch) part_ref = bom.get_only_part_reference() if part_ref is None: ms.warning("Part has footprint in schematic but no BOM part selected.") return ms def check_has_footprint(ctx: Context, bom: Part, sch: Part): ms = Messages(sch) fp = sch.facts.get_value(common_fact_types.footprint) if fp is None: ms.warning("No footprint in schematic part") supplier_ref = bom.get_only_part_reference() if supplier_ref is None: return # Just return, other checks will tell about missing BOM part supplier_part = ctx.get_supplier_part(supplier_ref.part_uriProp) supplier_fp = supplier_part.facts.get_value(common_fact_types.footprint) if supplier_ref is not None and supplier_fp is None: ms.warning(f"The BOM part does not have a footprint.") if fp is not None and supplier_fp is not None and fp != supplier_fp: return ms.warning("Part has footprint in schematic and a BOM part but their footprints do not match. " f"Footprints: part: {fp}, BOM: {supplier_fp}.") if fp is None and supplier_fp is not None: return ms.warning(f"The schematic part's footprint should be compatible with the BOM part's footprint: " f"{supplier_fp}.") if len(ms.messages) == 0: ms.info("Part has footprint and BOM part. Their footprints matches.") return ms def validate(f, ctx: Context, bom_part: Part, sch_part: Part): validators = [ check_has_footprint, check_has_part, ] messages = Messages(sch_part) for validator in validators: m = validator(ctx, bom_part, sch_part) if m: messages.append(m) title = "Checking {}".format(sch_part.printable_reference) print(title, file=f) print("{}".format("=" * len(title)), file=f) print("", file=f) for msg in messages.errors: print("* ERROR: {}".format(msg), file=f) for msg in messages.warnings: print("* WARNING: {}".format(msg), file=f) for msg in messages.infos: print("* INFO: {}".format(msg), file=f) if len(messages) == 0: print("No issues found", file=f) print("", file=f) return messages def work(bom_path: Path, sch_path: Path, report_path: Path, part_dbs: List[Path]): bom_parts: ObjDb[Part] = ObjDb[Part]() bom_parts.add_unique_index("uri", lambda p: p.uri) [bom_parts.add(Part(xml)) for xml in load_db(bom_path).iterparts()] sch_parts: ObjDb[Part] = ObjDb[Part]() sch_parts.add_unique_index("uri", lambda p: p.uri) ref_idx = sch_parts.add_unique_index("ref", lambda p: p.get_exactly_one_schematic_reference().referenceProp) [sch_parts.add(Part(xml)) for xml in load_db(sch_path).iterparts()] supplier_parts = ObjDb[Part]() supplier_parts.add_unique_index("uri", lambda p: p.uri) for path in part_dbs: for xml in load_db(path).iterparts(): supplier_parts.add(Part(xml)) ctx = Context(supplier_parts) with report_path.open("w") as f: messages = Messages(None) for bom_part in bom_parts: sch_part = ref_idx.get_single(bom_part.get_exactly_one_schematic_reference().referenceProp) ms = validate(f, ctx, bom_part, sch_part) messages.append(ms) print("", file=f) print("Message index", file=f) print("=============", file=f) print("", file=f) for message, messages_ in groupby(sorted(messages.messages, key=lambda m: m[2]), lambda m: m[2]): messages = list(messages_) print(message, file=f) print("{}".format("-" * len(message)), file=f) for m in messages: print(" * {}".format(m[0].printable_reference), file=f) print("", file=f) parser = argparse.ArgumentParser() ee.tools.add_default_argparse_group(parser) parser.add_argument("--bom", required=True, metavar="PART DB") parser.add_argument("--sch", required=True, metavar="PART DB") parser.add_argument("--report", required=True, metavar="FILE") parser.add_argument("--part-db", dest="part_dbs", nargs="*", required=True, metavar="PART DB") args = parser.parse_args() ee.tools.process_default_argparse_group(args) part_dbs_paths = [Path(path) for path in args.part_dbs] work(Path(args.bom), Path(args.sch), Path(args.report), part_dbs_paths)