tbox: v1.1 - belongsTo unifie, implements devient represents, aboutConcept/usesConcept remplacent composedOf, monitoredBy sur BO seul, grain deplace du Metric au DataObject, couche Delivery et DataInterface. shapes v0.2 scindees curated/observed avec XOR de sens sur DataElement. instances SODH migrees et arbitrees : 12 BO, 12 BC, 6 concepts empruntes en TO_ARBITRATE. Validation : 52 bloquants, dont 52 leves par la migration BR-013 restante
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user