Files
data-meta-model/scripts/migrate_instances_v1_1.py

215 lines
7.7 KiB
Python

#!/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()