tbox: v2.0 - 47 renommages de vocabulaire, alignement T-Box/A-Box sur les attributs transverses, GovernanceLayerObject inseree au-dessus d'Actor, isAbstract declare sur les 44 classes, ActivationStatus et Environment fermes en litteraux, labels derives de l'IRI, conformsTo pose sur les shapes, requetes SPARQL embarquees reecrites. Validation SHACL sans violation bloquante, rejeu a zero modification. Viewer T-Box regenere mais encore porteur d'IRI en dur, cf backlog
This commit is contained in:
@@ -31,6 +31,7 @@ from collections import OrderedDict
|
||||
|
||||
import yaml
|
||||
from rdflib import Graph, Literal, Namespace, RDF, RDFS, OWL, URIRef, XSD
|
||||
from rdflib.namespace import SH
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SPEC = os.path.join(HERE, "renames.yaml")
|
||||
@@ -133,7 +134,13 @@ def drop_subject(graph, subject, report, step):
|
||||
|
||||
|
||||
def count_instances(graphs, term):
|
||||
"""EV-002. The proof required before any permanent withdrawal."""
|
||||
"""EV-002. The proof required before any permanent withdrawal.
|
||||
|
||||
Counted on INSTANCE graphs only. A mention inside a shapes graph is not an
|
||||
instantiation: a shape carrying sh:maxCount 0 on a term is a guard forbidding
|
||||
its reappearance, which is the opposite of a use, and it keeps working after
|
||||
the term is gone since it checks an identifier rather than a declaration.
|
||||
"""
|
||||
n = 0
|
||||
for g in graphs:
|
||||
n += len(list(g.triples((None, RDF.type, term))))
|
||||
@@ -142,34 +149,84 @@ def count_instances(graphs, term):
|
||||
return n
|
||||
|
||||
|
||||
def migrate(onto, others, spec, rules, report):
|
||||
def rewrite_queries(graph, renames, report):
|
||||
"""Rename inside embedded SPARQL queries.
|
||||
|
||||
A sh:select or sh:ask body is a literal, so no triple-level rename reaches
|
||||
it: the query keeps naming a term that no longer exists and the constraint
|
||||
silently stops matching. This is not the line-by-line editing EV-005
|
||||
forbids: the graph is parsed, the literal is retrieved as a value, and what
|
||||
is rewritten is a structured query language where a prefixed name has clear
|
||||
token boundaries.
|
||||
"""
|
||||
n = 0
|
||||
for prop in (SH.select, SH.ask, SH.construct):
|
||||
for s, p, o in list(graph.triples((None, prop, None))):
|
||||
text = str(o)
|
||||
new = text
|
||||
for old, target in renames:
|
||||
new = re.sub(r"\bpr:%s\b" % re.escape(old), "pr:%s" % target, new)
|
||||
if new != text:
|
||||
graph.remove((s, p, o))
|
||||
graph.add((s, p, Literal(new)))
|
||||
n += 1
|
||||
report.add("queries_rewritten", n)
|
||||
return n
|
||||
|
||||
|
||||
def migrate(onto, instances, shapes, spec, rules, report):
|
||||
ns = spec["meta"]["namespace"]
|
||||
pr = Namespace(ns)
|
||||
others = instances + shapes
|
||||
all_graphs = [onto] + others
|
||||
|
||||
# 1 — deprecated terms are withdrawn outright (phase clause), proof first
|
||||
# 1 — deprecated terms are withdrawn outright (phase clause), proof first.
|
||||
# The proof is taken on instance graphs only: a mention in a shapes graph
|
||||
# is a guard against reappearance, not a use.
|
||||
# Reference cleaning likewise stops at the ontology and the instances.
|
||||
# EV-003 guards against phantom nodes rebuilt by inference in the model
|
||||
# graph; a mention inside a shape is a constraint on an identifier, in a
|
||||
# separate graph, taking part in no inference over the ontology. Removing
|
||||
# it would strip the sh:path from a guard and leave a property shape
|
||||
# carrying a maxCount and no path — an invalid shape, and the loss of the
|
||||
# very rule that forbids the term from coming back.
|
||||
if spec["structural"].get("drop_deprecated"):
|
||||
for subj in list(onto.subjects(OWL.deprecated, Literal(True))):
|
||||
name = local(subj, ns) or str(subj)
|
||||
used = sum(count_instances([g], subj) for g in others)
|
||||
used = count_instances(instances, subj)
|
||||
if used:
|
||||
report.add("deprecated_kept", 1, "%s still used %d times" % (name, used))
|
||||
report.add("deprecated_kept", 1, "%s instantiated %d times" % (name, used))
|
||||
continue
|
||||
for g in all_graphs:
|
||||
for g in [onto] + instances:
|
||||
drop_subject(g, subj, report, "deprecated_dropped")
|
||||
|
||||
# 2 — renames, in dependency order
|
||||
# 2 — vocabulary renames, in dependency order
|
||||
for order in (1, 2, 3):
|
||||
for r in [x for x in spec["renames"] if x["order"] == order]:
|
||||
for g in all_graphs:
|
||||
rename(g, pr, r["from"], r["to"], report, "rename_order_%d" % order)
|
||||
|
||||
# 3 — merges
|
||||
# 3 — instance-side alignment: forms the instances use that the vocabulary
|
||||
# never declared and that the TN-012 renames do not converge on.
|
||||
# Applied to the shapes too: they target these forms in sh:path.
|
||||
for r in spec.get("abox_renames") or []:
|
||||
for g in all_graphs:
|
||||
rename(g, pr, r["from"], r["to"], report, "abox_aligned")
|
||||
|
||||
# 4 — embedded SPARQL queries carry term names as text, invisible to a
|
||||
# triple-level rename. Rewritten with the same map, after it.
|
||||
pairs = [(r["from"], r["to"]) for r in spec["renames"]]
|
||||
pairs += [(r["from"], r["to"]) for r in (spec.get("abox_renames") or [])]
|
||||
pairs += [(m["from"], m["into"]) for m in (spec.get("merges") or [])]
|
||||
for g in all_graphs:
|
||||
rewrite_queries(g, pairs, report)
|
||||
|
||||
# 5 — merges
|
||||
for m in spec.get("merges") or []:
|
||||
for g in all_graphs:
|
||||
merge(g, pr, m["from"], m["into"], report)
|
||||
|
||||
# 4 — reclassify display properties (TN-003)
|
||||
# 6 — reclassify display properties (TN-003)
|
||||
for rc in spec["structural"]["reclassify"]:
|
||||
term = pr[rc["term"]]
|
||||
if (term, RDF.type, OWL.AnnotationProperty) not in onto:
|
||||
@@ -177,7 +234,7 @@ def migrate(onto, others, spec, rules, report):
|
||||
onto.add((term, RDF.type, OWL.AnnotationProperty))
|
||||
report.add("reclassified", 1, rc["term"])
|
||||
|
||||
# 5 — create the governance layer root (TN-027)
|
||||
# 7 — create the governance layer root (TN-027)
|
||||
for c in spec["structural"].get("create_classes") or []:
|
||||
term = pr[c["term"]]
|
||||
if (term, RDF.type, OWL.Class) not in onto:
|
||||
@@ -188,21 +245,21 @@ def migrate(onto, others, spec, rules, report):
|
||||
onto.add((term, pr.isAbstract, Literal(True)))
|
||||
report.add("classes_created", 1, c["term"])
|
||||
|
||||
# 6 — reparent (TN-027)
|
||||
# 8 — reparent (TN-027)
|
||||
for rp in spec["structural"].get("reparent") or []:
|
||||
term, parent = pr[rp["term"]], pr[rp["parent"]]
|
||||
if (term, RDFS.subClassOf, parent) not in onto:
|
||||
onto.add((term, RDFS.subClassOf, parent))
|
||||
report.add("reparented", 1, "%s -> %s" % (rp["term"], rp["parent"]))
|
||||
|
||||
# 7 — sub-properties (TN-028)
|
||||
# 9 — sub-properties (TN-028)
|
||||
for sp in spec["structural"].get("subproperties") or []:
|
||||
term, parent = pr[sp["term"]], pr[sp["parent"]]
|
||||
if (term, RDFS.subPropertyOf, parent) not in onto:
|
||||
onto.add((term, RDFS.subPropertyOf, parent))
|
||||
report.add("subproperties", 1, "%s -> %s" % (sp["term"], sp["parent"]))
|
||||
|
||||
# 8 — controlled values become literals (TN-016, TN-017)
|
||||
# 10 — controlled values become literals (TN-016, TN-017)
|
||||
for conv in spec["structural"].get("to_literal") or []:
|
||||
prop = pr[conv["property"]]
|
||||
if (prop, RDF.type, OWL.DatatypeProperty) not in onto:
|
||||
@@ -222,10 +279,10 @@ def migrate(onto, others, spec, rules, report):
|
||||
for ind in conv["drop_individuals"] + [conv["drop_class"]]:
|
||||
subj = pr[ind]
|
||||
if (subj, None, None) in onto:
|
||||
for g in all_graphs:
|
||||
for g in [onto] + instances: # shapes keep their guards
|
||||
drop_subject(g, subj, report, "to_literal_dropped")
|
||||
|
||||
# 9 — abstractness, from the rulebook annex (TN-006, TN-007)
|
||||
# 11 — abstractness, from the rulebook annex (TN-006, TN-007)
|
||||
for a in rules["abstractness"]:
|
||||
term = pr[a["term"]]
|
||||
if (term, RDF.type, OWL.Class) not in onto:
|
||||
@@ -237,7 +294,7 @@ def migrate(onto, others, spec, rules, report):
|
||||
onto.add((term, pr.isAbstract, want))
|
||||
report.add("isAbstract_declared", 1)
|
||||
|
||||
# 10 — display annotations, from the rulebook annex (TN-022)
|
||||
# 12 — display annotations, from the rulebook annex (TN-022)
|
||||
# The annex is authoritative AND exhaustive: a null value means any existing
|
||||
# annotation is removed. A short label identical to the label carries no
|
||||
# information and is one more thing to keep in step.
|
||||
@@ -255,7 +312,7 @@ def migrate(onto, others, spec, rules, report):
|
||||
onto.remove((term, prop, None))
|
||||
report.add("display_removed", len(current))
|
||||
|
||||
# 11 — regenerate every label by derivation (TN-018, TN-023)
|
||||
# 13 — regenerate every label by derivation (TN-018, TN-023)
|
||||
kinds = (OWL.Class, OWL.ObjectProperty, OWL.DatatypeProperty, OWL.AnnotationProperty)
|
||||
for kind in kinds:
|
||||
for term in set(onto.subjects(RDF.type, kind)):
|
||||
@@ -268,7 +325,7 @@ def migrate(onto, others, spec, rules, report):
|
||||
onto.add((term, RDFS.label, want))
|
||||
report.add("labels_regenerated", 1)
|
||||
|
||||
# 12 — bump the ontology version (EV-014: the namespace itself never moves)
|
||||
# 14 — bump the ontology version (EV-014: the namespace itself never moves)
|
||||
target = URIRef(ns.rstrip("/") + "/" + spec["meta"]["to_version"])
|
||||
for onto_iri in set(onto.subjects(RDF.type, OWL.Ontology)):
|
||||
if (onto_iri, OWL.versionIRI, target) not in onto:
|
||||
@@ -276,6 +333,19 @@ def migrate(onto, others, spec, rules, report):
|
||||
onto.add((onto_iri, OWL.versionIRI, target))
|
||||
report.add("version_bumped", 1, str(target))
|
||||
|
||||
# 15 — the shapes declare the ontology version they target (EV-010).
|
||||
# The shapes graph carries no ontology node, so it is created here.
|
||||
so = spec["structural"].get("shapes_ontology")
|
||||
if so:
|
||||
node = URIRef(so["iri"])
|
||||
for g in shapes:
|
||||
if (node, DCTERMS.conformsTo, target) not in g:
|
||||
g.remove((node, DCTERMS.conformsTo, None))
|
||||
g.add((node, RDF.type, OWL.Ontology))
|
||||
g.add((node, RDFS.label, Literal(so["label"])))
|
||||
g.add((node, DCTERMS.conformsTo, target))
|
||||
report.add("shapes_conformsTo", 1, str(target))
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------- main
|
||||
|
||||
@@ -303,15 +373,22 @@ def main():
|
||||
|
||||
onto = Graph()
|
||||
onto.parse(args.ontology, format="turtle")
|
||||
others, other_paths = [], args.instances + args.shapes
|
||||
for p in other_paths:
|
||||
instances, shapes = [], []
|
||||
for p in args.instances:
|
||||
g = Graph()
|
||||
g.parse(p, format="turtle")
|
||||
others.append(g)
|
||||
instances.append(g)
|
||||
for p in args.shapes:
|
||||
g = Graph()
|
||||
g.parse(p, format="turtle")
|
||||
g.bind("dcterms", DCTERMS)
|
||||
shapes.append(g)
|
||||
others = instances + shapes
|
||||
other_paths = args.instances + args.shapes
|
||||
|
||||
before = [len(onto)] + [len(g) for g in others]
|
||||
report = Report()
|
||||
migrate(onto, others, spec, rules, report)
|
||||
migrate(onto, instances, shapes, spec, rules, report)
|
||||
after = [len(onto)] + [len(g) for g in others]
|
||||
|
||||
print("MIGRATION %s -> %s %s"
|
||||
|
||||
Reference in New Issue
Block a user