#!/usr/bin/env python3 """ PR Data Meta Model - instance migration v1.0 -> v1.1 ===================================================== Rewrites instance files for the T-Box v1.1 vocabulary. Text-level rewriting on purpose: it preserves comments, ordering and formatting, which a parse-and- reserialise round trip through rdflib would destroy -- and those comments carry the arbitration notes. USAGE python3 scripts/migrate_instances_v1_1.py # dry run, report only python3 scripts/migrate_instances_v1_1.py --apply # rewrite in place, .bak kept python3 scripts/migrate_instances_v1_1.py --apply f.ttl # a single file WHAT IT DOES AUTOMATICALLY namespace /metamodel/0.9/ -> /metamodel/ (unversioned terms) belongsToDomain, belongsToSubDomain -> belongsTo implements -> represents hasSteward -> monitoredBy usesDataObject -> packages DataAsset -> DataInterface hasGrainDimension, hasGranularity -> flagged, not converted WHAT IT REFUSES TO DO composedOf -> aboutConcept / usesConcept is NOT automated. Splitting it requires deciding which concept is the SUBJECT of each Business Object, and guessing that is exactly how v0.6 ended up with Calendar declared as being composed of Sell Out. The script lists the choices to be made and leaves composedOf in place until a human answers. hasBusinessDefinition on a Business Object is likewise reported, not moved: the text must go to the Concept named by aboutConcept, which does not exist yet at the time this runs. """ import glob import os import re import shutil import sys REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) INSTANCES = os.path.join(REPO, "instances") W = 78 # (pattern, replacement, label) -- applied in order, word-bounded RENAMES = [ (r"https://ontology\.pernod-ricard\.com/metamodel/0\.9/", "https://ontology.pernod-ricard.com/metamodel/", "namespace 0.9 -> unversioned"), (r"\bpr:belongsToDomain\b", "pr:belongsTo", "belongsToDomain -> belongsTo"), (r"\bpr:belongsToSubDomain\b", "pr:belongsTo", "belongsToSubDomain -> belongsTo"), (r"\bpr:implements\b", "pr:represents", "implements -> represents"), (r"\bpr:hasSteward\b", "pr:monitoredBy", "hasSteward -> monitoredBy"), (r"\bpr:usesDataObject\b", "pr:packages", "usesDataObject -> packages"), (r"\bpr:DataAsset\b", "pr:DataInterface", "DataAsset -> DataInterface"), ] # things a human must decide, reported per file MANUAL = [ (r"\bpr:composedOf\b", "composedOf", "Split into aboutConcept (exactly one -- the SUBJECT) and usesConcept " "(the rest). Not automated: guessing the subject is how v0.6 ended up " "with Calendar composed of Sell Out."), (r"\bpr:isComponentOf\b", "isComponentOf", "Inverse of the above. Re-express as usesConcept from the Business Object."), (r"\bpr:hasGranularity\b", "hasGranularity", "Grain moved off the Metric. Re-express as hasGrainElement on the Data " "Object, pointing at the Data Elements that make a row unique."), (r"\bpr:hasGrainDimension\b", "hasGrainDimension", "Same: hasGrainElement on the Data Object, targeting Data Elements."), ] def subject_blocks(text): """Yield (subject, block) for each top-level statement.""" for block in re.split(r"\.\s*\n(?=\S)", text): m = re.match(r"\s*(\S+)", block) if m: yield m.group(1), block def business_objects_with_definition(text): """Business Objects still carrying hasBusinessDefinition -- must move to the Concept.""" out = [] for subj, block in subject_blocks(text): if "pr:BusinessObject" in block and "pr:hasBusinessDefinition" in block: out.append(subj) return out def composed_of_map(text): """Business Object -> list of concepts, so the subject can be chosen.""" out = {} for subj, block in subject_blocks(text): if "pr:composedOf" not in block: continue m = re.search(r"pr:composedOf\s+([^;.]+)", block) if m: concepts = [c.strip() for c in m.group(1).split(",") if c.strip()] out[subj] = concepts return out def process(path, apply_changes): original = open(path, encoding="utf-8").read() text = original applied = [] for pattern, repl, label in RENAMES: text, n = re.subn(pattern, repl, text) if n: applied.append((label, n)) pending = [] for pattern, label, advice in MANUAL: n = len(re.findall(pattern, original)) if n: pending.append((label, n, advice)) bo_defs = business_objects_with_definition(original) comp = composed_of_map(original) print("=" * W) print(os.path.relpath(path, REPO)) print("=" * W) if applied: print(" AUTOMATIC") for label, n in applied: print(" %-42s %4d occurrence(s)" % (label, n)) else: print(" AUTOMATIC nothing to rewrite") if pending: print(" MANUAL") for label, n, advice in pending: print(" %-42s %4d occurrence(s)" % (label, n)) for line in _wrap(advice, 68): print(" " + line) if bo_defs: print(" BUSINESS DEFINITIONS TO MOVE") print(" %d Business Object(s) still carry hasBusinessDefinition." % len(bo_defs)) print(" Meaning belongs to the Concept. Move each text to the Concept") print(" named by that object's aboutConcept, then delete it here.") for s in bo_defs[:12]: print(" - %s" % s) if len(bo_defs) > 12: print(" ... and %d more" % (len(bo_defs) - 12)) if comp: print(" SUBJECT TO CHOOSE (composedOf -> aboutConcept + usesConcept)") for subj, concepts in list(comp.items())[:12]: print(" %s" % subj) print(" candidates: %s" % ", ".join(concepts)) if len(comp) > 12: print(" ... and %d more" % (len(comp) - 12)) changed = text != original if changed and apply_changes: shutil.copy2(path, path + ".bak") open(path, "w", encoding="utf-8").write(text) print(" WRITTEN (backup at %s.bak)" % os.path.basename(path)) elif changed: print(" DRY RUN -- re-run with --apply to write") print() return changed, len(pending) + len(bo_defs) + len(comp) def _wrap(text, width): words, line, out = text.split(), "", [] for w in words: if len(line) + len(w) + 1 > width: out.append(line) line = w else: line = (line + " " + w).strip() if line: out.append(line) return out def main(): apply_changes = "--apply" in sys.argv args = [a for a in sys.argv[1:] if not a.startswith("-")] files = ([a if os.path.isabs(a) else os.path.join(REPO, a) for a in args] or sorted(glob.glob(os.path.join(INSTANCES, "*.ttl")))) files = [f for f in files if os.path.exists(f)] if not files: print("No instance file found.") sys.exit(2) print() print("PR META MODEL - INSTANCE MIGRATION v1.0 -> v1.1 %s" % ("APPLY" if apply_changes else "DRY RUN")) print() total_changed = total_manual = 0 for f in files: changed, manual = process(f, apply_changes) total_changed += int(changed) total_manual += manual print("=" * W) print("%d file(s) rewritten, %d decision(s) left to a human." % (total_changed, total_manual)) if total_manual: print("Run the validator after the manual work: the XOR-of-meaning shape") print("on Data Elements will not pass until the concepts are wired.") print("=" * W) if __name__ == "__main__": main()