tbox: v2.2 - polymorphisme declare sur 19 proprietes, 17 domaines poses, provenance portee par les classes concretes, GovernanceLayerObject rattachee, affectations de gouvernance completes (12 completees, 10 SDO, 1 PO, 16 stewards par objet metier), Panel Coverage gouverne. Validation SHACL sans violation bloquante
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
# T-BOX AND A-BOX MIGRATION TO v2.2 — GOVERNANCE ASSIGNMENTS
|
||||
# =============================================================================
|
||||
# Data consumed by migrate_tbox_v2_2.py.
|
||||
#
|
||||
# An actor node is an ASSIGNMENT, not a person. The post is stable and carries
|
||||
# the canonical name, the identifier and the domain it answers to; the person
|
||||
# holding it is a value that changes. Modelled the other way round, a change of
|
||||
# incumbent would move nodes and edges rather than one literal, and the same
|
||||
# person covering two domains would exist twice.
|
||||
# =============================================================================
|
||||
|
||||
meta:
|
||||
from_version: "2.1"
|
||||
to_version: "2.2"
|
||||
namespace: "https://ontology.pernod-ricard.com/metamodel/"
|
||||
instance_namespace: "https://data.pernod-ricard.com/sodh/"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# T-BOX
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
declare_properties:
|
||||
- term: personName
|
||||
kind: DatatypeProperty
|
||||
domain: Actor
|
||||
range: "http://www.w3.org/2001/XMLSchema#string"
|
||||
comment: >
|
||||
The name of the person currently holding the post. Declared on Actor rather
|
||||
than on each role, since every role shares it and Actor is their smallest
|
||||
common ancestor below the layer root. Held as a value rather than a node
|
||||
because what changes is the person, not the assignment: the post keeps its
|
||||
identifier, its domain and every edge that points at it, and a change of
|
||||
holder rewrites one literal. Used on actors and nowhere else.
|
||||
|
||||
display_updates:
|
||||
- {term: DataSteward, acronym: DST}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# A-BOX — RENAME
|
||||
# ST is not the short form of Data Steward; DST is, and the rulebook now
|
||||
# declares it.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
instance_renames:
|
||||
- {from: ST_DD_04, to: DST_DD_04}
|
||||
- {from: ST_DD_05, to: DST_DD_05}
|
||||
- {from: ST_DD_06, to: DST_DD_06}
|
||||
- {from: ST_DD_10, to: DST_DD_10}
|
||||
- {from: ST_DD_16, to: DST_DD_16}
|
||||
- {from: ST_DD_21, to: DST_DD_21}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# A-BOX — COMPLETE THE EXISTING ASSIGNMENTS
|
||||
# The canonical name becomes the name of the POST. The person moves to heldBy.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
complete:
|
||||
- {node: DDO_DD_04, identifier: "DDO-04", name: "Data Domain Owner", held_by: "David Carpio", domain: DD_04}
|
||||
- {node: DDO_DD_05, identifier: "DDO-05", name: "Data Domain Owner", held_by: "David Carpio", domain: DD_05}
|
||||
- {node: DDO_DD_06, identifier: "DDO-06", name: "Data Domain Owner", held_by: "Ludovic Cottier", domain: DD_06}
|
||||
- {node: DDO_DD_10, identifier: "DDO-10", name: "Data Domain Owner", held_by: "Alberto Lupano", domain: DD_10}
|
||||
- {node: DDO_DD_16, identifier: "DDO-16", name: "Data Domain Owner", held_by: "Julien Soisson", domain: DD_16}
|
||||
- {node: DDO_DD_21, identifier: "DDO-21", name: "Data Domain Owner", held_by: "Vincent Meunier", domain: DD_21}
|
||||
- {node: DGL_DD_04, identifier: "DGL-04", name: "Data Governance Lead", held_by: "Marie Carabin", domain: DD_04}
|
||||
- {node: DGL_DD_05, identifier: "DGL-05", name: "Data Governance Lead", held_by: "Alberic Piot", domain: DD_05}
|
||||
- {node: DGL_DD_06, identifier: "DGL-06", name: "Data Governance Lead", held_by: "Bastien Gourdon", domain: DD_06}
|
||||
- {node: DGL_DD_10, identifier: "DGL-10", name: "Data Governance Lead", held_by: "Anas El Kesri", domain: DD_10}
|
||||
- {node: DGL_DD_16, identifier: "DGL-16", name: "Data Governance Lead", held_by: "Gaelle Seret", domain: DD_16}
|
||||
- {node: DGL_DD_21, identifier: "DGL-21", name: "Data Governance Lead", held_by: "Bastien Gourdon", domain: DD_21}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# A-BOX — SUB-DOMAIN OWNERS, 10
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
create_sub_domain_owners:
|
||||
- {node: SDO_SD_04_01, identifier: "SDO-04.01", held_by: "Etienne Coulon", sub_domain: SD_04_01, domain: DD_04}
|
||||
- {node: SDO_SD_04_02, identifier: "SDO-04.02", held_by: "Etienne Coulon", sub_domain: SD_04_02, domain: DD_04}
|
||||
- {node: SDO_SD_04_03, identifier: "SDO-04.03", held_by: "Etienne Coulon", sub_domain: SD_04_03, domain: DD_04}
|
||||
- {node: SDO_SD_05_01, identifier: "SDO-05.01", held_by: "David Carpio", sub_domain: SD_05_01, domain: DD_05}
|
||||
- {node: SDO_SD_06_01, identifier: "SDO-06.01", held_by: "Loic Berger", sub_domain: SD_06_01, domain: DD_06}
|
||||
- {node: SDO_SD_10_01, identifier: "SDO-10.01", held_by: "Alberto Lupano", sub_domain: SD_10_01, domain: DD_10}
|
||||
- {node: SDO_SD_16_01, identifier: "SDO-16.01", held_by: "Julien Soisson", sub_domain: SD_16_01, domain: DD_16}
|
||||
- {node: SDO_SD_16_02, identifier: "SDO-16.02", held_by: "Julien Soisson", sub_domain: SD_16_02, domain: DD_16}
|
||||
- {node: SDO_SD_16_03, identifier: "SDO-16.03", held_by: "Julien Soisson", sub_domain: SD_16_03, domain: DD_16}
|
||||
- {node: SDO_SD_21_01, identifier: "SDO-21.01", held_by: "Vincent Meunier", sub_domain: SD_21_01, domain: DD_21}
|
||||
|
||||
sub_domain_owner_name: Data Sub Domain Owner
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# A-BOX — PRODUCT OWNER, 1
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
create_product_owners:
|
||||
- {node: PO_SODH, identifier: "PO-06.01-001", held_by: "Guilherme Muller", product: DP_06_01_001, domain: DD_06}
|
||||
|
||||
product_owner_name: Data Product Owner
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# A-BOX — STEWARDS, ONE ASSIGNMENT PER BUSINESS OBJECT
|
||||
# Derived from the graph rather than listed: the business objects are the source
|
||||
# of truth for how many assignments exist and which domain each answers to.
|
||||
# The incumbent is read from this map by domain.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
steward_holders:
|
||||
DD_04: Marc Chambouleyron
|
||||
DD_05: Francois Vadrouille
|
||||
DD_06: Michael Flores
|
||||
DD_10: Magda Kugli
|
||||
DD_16: Clementine Sauries
|
||||
DD_21: Alberic Piot
|
||||
|
||||
steward_name: Data Steward
|
||||
|
||||
# The six per-domain steward nodes are replaced by one per business object.
|
||||
# monitoredBy is repointed from the old node to the new one, and the old node is
|
||||
# withdrawn once nothing names it (EV-003).
|
||||
retire_domain_stewards: true
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# A-BOX — GOVERN A TRANSVERSE CONCEPT
|
||||
# Panel Coverage is used by four business objects through usesConcept and is the
|
||||
# subject of none. A concept mobilised without an object that carries it has no
|
||||
# owner, no steward and no governance chain: it is used without being governed.
|
||||
# Creating the missing business object closes the chain BC -> BO -> SD -> DD,
|
||||
# and the steward assignment follows automatically since assignments are derived
|
||||
# from the business objects present in the graph.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
create_business_objects:
|
||||
- node: BO_06_01_006
|
||||
identifier: "BO-06.01-006"
|
||||
name: Panel Coverage
|
||||
is_about: BC_06_01_003
|
||||
sub_domain: SD_06_01
|
||||
domain: DD_06
|
||||
# -----------------------------------------------------------------------------
|
||||
# A-BOX — ownedBy AGAINST THE OLD DOMAIN-LEVEL STEWARDS
|
||||
# 53 subjects declared they belonged to the per-domain steward node. That node
|
||||
# disappears, so each reference is resolved rather than dropped in silence.
|
||||
#
|
||||
# business object -> removed. It already carries monitoredBy towards the
|
||||
# same assignment, and two properties holding one fact
|
||||
# are two answers to one question waiting to diverge.
|
||||
# sub-domain -> repointed to its Data Sub Domain Owner, whose function
|
||||
# this is; a steward had no business holding it.
|
||||
# everything else -> repointed to the steward assignment of the business
|
||||
# object it belongs to, followed through the graph.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
owned_by_resolution:
|
||||
remove_on: [BusinessObject]
|
||||
repoint_sub_domains: true
|
||||
repoint_to_business_object_assignment: true
|
||||
# Chains followed to reach the business object, in order. The first that
|
||||
# resolves wins; anything unresolved is reported and left untouched.
|
||||
chains:
|
||||
- {via: isAbout, direction: inbound} # a BO is about this concept
|
||||
- {via: represents, direction: outbound} # this object represents a BO
|
||||
- {via: measures, direction: outbound} # a metric measures a concept, then isAbout
|
||||
# -----------------------------------------------------------------------------
|
||||
# COMMON
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
status: PUBLISHED
|
||||
version: "1.0"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# SHAPES — NR-002 EXEMPTION
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
shape_notes:
|
||||
- rule: NR-002
|
||||
change: exempt descendants of Actor from the canonical-name uniqueness query
|
||||
reason: >
|
||||
An assignment is a post, not a catalogued object. Five steward posts in one
|
||||
domain are legitimately homonymous; what tells them apart is the identifier
|
||||
and the business object that names them. Applying a uniqueness rule written
|
||||
for catalogued objects would force fifteen invented names for one job.
|
||||
@@ -0,0 +1,166 @@
|
||||
# T-BOX MIGRATION TO v2.1 — DECLARATIONS
|
||||
# =============================================================================
|
||||
# Data consumed by migrate_tbox_v2_1.py. No renaming here: v2.1 declares what
|
||||
# v2.0 left silent.
|
||||
#
|
||||
# Three subjects, each closing a class of TN-025, TN-026 and TN-027 findings:
|
||||
# 1. polymorphism made explicit rather than inferred from prose
|
||||
# 2. domains stated wherever a single class of use exists
|
||||
# 3. provenance moved off the terms and onto the concrete classes
|
||||
# =============================================================================
|
||||
|
||||
meta:
|
||||
from_version: "2.0"
|
||||
to_version: "2.1"
|
||||
namespace: "https://ontology.pernod-ricard.com/metamodel/"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# THE ANNOTATION ITSELF (TN-025)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
declare_annotations:
|
||||
- term: polymorphic
|
||||
label: polymorphic
|
||||
comment: >
|
||||
Declares that a property deliberately carries no rdfs:domain because it
|
||||
serves several classes with no useful common ancestor. Its scope is
|
||||
controlled class by class in SHACL instead. Without this axiom a deliberate
|
||||
omission and a forgotten one look identical, and no check can tell them
|
||||
apart.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# POLYMORPHIC PROPERTIES — 18
|
||||
#
|
||||
# The test: a property takes as domain the smallest common ancestor of its
|
||||
# classes of use, PROVIDED that ancestor sits below a layer root. A layer root,
|
||||
# a provenance axis and MetaModelObject are all too high to say anything; a
|
||||
# property whose smallest common ancestor is one of those is polymorphic.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
polymorphic:
|
||||
- {term: identifier, reason: "borne by every object in the graph"}
|
||||
- {term: canonicalName, reason: "borne by every object in the graph"}
|
||||
- {term: status, reason: "borne by every object in the graph"}
|
||||
- {term: version, reason: "borne by every object in the graph"}
|
||||
- {term: creationDate, reason: "borne by every object in the graph"}
|
||||
- {term: lastReviewDate, reason: "borne by every object in the graph"}
|
||||
- {term: arbitrationStatus, reason: "any declared object may be under arbitration"}
|
||||
- {term: ownedByDomain, reason: "smallest common ancestor is a provenance axis, not a layer class"}
|
||||
- {term: harvestDate, reason: "smallest common ancestor is a provenance axis, not a layer class"}
|
||||
- {term: sourceIdentifier, reason: "smallest common ancestor is a provenance axis, not a layer class"}
|
||||
- {term: belongsTo, reason: "sub-domain to domain and business object to sub-domain: two couples with no useful ancestor"}
|
||||
- {term: storedIn, reason: "two granularities, data object to structure and data element to field"}
|
||||
- {term: represents, reason: "data object to business object and data element to business concept"}
|
||||
- {term: unit, reason: "data element and metric sit in different layers"}
|
||||
- {term: sourcedFrom, reason: "data object and data structure sit in different layers"}
|
||||
- {term: fullyQualifiedName, reason: "DATABASE.SCHEMA.RELATION[.COLUMN] names a structure and a column alike"}
|
||||
- {term: ownedBy, reason: "any object may have an owner; Actor is the range, the domain is free"}
|
||||
- {term: exampleValue, reason: "observed on a field, documented on a data element"}
|
||||
- {term: physicalName, reason: "a data object names its table as a data element names its column; BR-013 names one carrier where there are two"}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# DOMAINS TO DECLARE — 18
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
domains:
|
||||
- {term: formula, domain: Metric, basis: "shape; forbidden on BusinessConcept, which is what distinguishes a metric"}
|
||||
- {term: timeAggregation, domain: Metric, basis: "belongs to the calculation, alongside formula"}
|
||||
- {term: businessDefinition, domain: BusinessConcept, basis: "shape; forbidden on BusinessObject, whose meaning lives in its concept"}
|
||||
- {term: businessRule, domain: BusinessConcept, basis: "meaning belongs to the concept"}
|
||||
- {term: synonym, domain: BusinessConcept, basis: "meaning belongs to the concept"}
|
||||
- {term: technicalDefinition, domain: BusinessConcept, basis: "meaning belongs to the concept"}
|
||||
- {term: monitoredBy, domain: BusinessObject, basis: "shape; forbidden on DataElement and DataObject, where stewardship is inherited"}
|
||||
- {term: logicalFormat, domain: DataElement, basis: "a logical characteristic of the element"}
|
||||
- {term: isNullable, domain: Field, basis: "a property of the column"}
|
||||
- {term: ordinalPosition, domain: Field, basis: "a property of the column"}
|
||||
- {term: physicalDataType, domain: Field, basis: "a property of the column"}
|
||||
- {term: isInSchema, domain: DataStructure, basis: "shape"}
|
||||
- {term: viewDefinition, domain: View, basis: "a table has no SQL definition"}
|
||||
- {term: queryCount, domain: DataStructure, basis: "ACCESS_HISTORY traces tables, views and external tables alike"}
|
||||
- {term: lastQueryDate, domain: DataStructure, basis: "ACCESS_HISTORY traces tables, views and external tables alike"}
|
||||
- {term: definitionAddress, domain: Transformation, basis: "the Git URI of the code behind a transformation; a view uses viewDefinition"}
|
||||
- {term: expression, domain: BusinessIntelligenceField, basis: "the calculated-field formula, compared against the metric formula to detect drift"}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# NEW TERM (procedure C1)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
declare_properties:
|
||||
- term: serviceCommitment
|
||||
kind: DatatypeProperty
|
||||
domain: DataContract
|
||||
range: "http://www.w3.org/2001/XMLSchema#string"
|
||||
comment: >
|
||||
What the contract guarantees about the product it covers: refresh schedule,
|
||||
freshness, quality thresholds, support channel, notice period on a breaking
|
||||
change. Held as prose for now, which no shape can verify; the structured
|
||||
form, aligned on ODCS, is the next step. Recorded here because the
|
||||
commitment had been written into businessRule for want of anywhere else,
|
||||
where it sat next to genuine business rules and could not be told apart.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# RANGES MISSING ON POLYMORPHIC PROPERTIES
|
||||
# A polymorphic property may still have a single range: what it points AT can be
|
||||
# settled even when what it starts FROM cannot.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
ranges: []
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# PROVENANCE (TN-026)
|
||||
#
|
||||
# A vocabulary term is declared, always and by definition. What has a provenance
|
||||
# is the INSTANCES, and the concrete class is where the model states which one.
|
||||
# Nothing is inherited: each class declares its own mode, and the mode agrees
|
||||
# with the provenance axis the class descends from.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
provenance:
|
||||
strip_from_non_concrete: true # abstract classes, properties, vocabulary terms
|
||||
derive_from_axis: true # CapturedObject -> HARVESTED, DefinedObject -> ASSERTED
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ATTACHMENT (TN-027)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
attach:
|
||||
- term: GovernanceLayerObject
|
||||
parent: DefinedObject
|
||||
basis: >
|
||||
A layer root sits on a provenance axis like any other. Actors and governance
|
||||
objects are declared by governance, never harvested, so the governance layer
|
||||
descends from DefinedObject and its five roles inherit the axis.
|
||||
|
||||
# SystemType remains outside the layers: it is a controlled-vocabulary class,
|
||||
# the named and closed exception of TN-027. Recorded as an open arbitration
|
||||
# rather than silently resolved here.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# INSTANCE CORRECTIONS
|
||||
# Each one was surfaced by declaring a domain: an instance carrying a property
|
||||
# outside its domain is retyped by RDFS inference, and the shapes of the wrong
|
||||
# class then fire on it.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
instance_fixes:
|
||||
- action: remove
|
||||
property: businessDefinition
|
||||
subjects: [DI_06_01_001, DI_06_01_002]
|
||||
reason: >
|
||||
Meaning belongs to the Business Concept. An interface is a channel through
|
||||
which data is delivered; it defines nothing. Same rule the shapes already
|
||||
enforce on Business Object.
|
||||
- action: remove
|
||||
property: sourcedFrom
|
||||
subjects: [DI_06_01_001, DI_06_01_002]
|
||||
reason: >
|
||||
An interface has no system of truth: the data it exposes is sourced by the
|
||||
Data Objects behind it, and those name the system.
|
||||
- action: move
|
||||
property: businessRule
|
||||
to: serviceCommitment
|
||||
subjects: [DC_06_01_001]
|
||||
reason: >
|
||||
The text is a service commitment, not a business rule: refresh window,
|
||||
notice period, support channel. It was written into businessRule for want
|
||||
of a property to hold it.
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T12:21:45",
|
||||
"mode": "dry-run",
|
||||
"from_version": "2.0",
|
||||
"to_version": "2.1",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48"
|
||||
},
|
||||
"steps": {
|
||||
"annotations_declared": 1,
|
||||
"polymorphic_declared": 18,
|
||||
"domains_declared": 18,
|
||||
"attached": 1,
|
||||
"provenance_stripped": 5,
|
||||
"provenance_declared": 29,
|
||||
"provenance_no_axis": 1,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 74,
|
||||
"triples_before": 723,
|
||||
"triples_after": 786,
|
||||
"notes": [
|
||||
"annotations_declared: polymorphic",
|
||||
"attached: GovernanceLayerObject -> DefinedObject",
|
||||
"provenance_stripped: storedIn",
|
||||
"provenance_stripped: primarilyStoredIn",
|
||||
"provenance_stripped: DefinedObject",
|
||||
"provenance_stripped: exposesMetric",
|
||||
"provenance_stripped: CapturedObject",
|
||||
"provenance_no_axis: SystemType",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T12:22:48",
|
||||
"mode": "apply",
|
||||
"from_version": "2.0",
|
||||
"to_version": "2.1",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48"
|
||||
},
|
||||
"steps": {
|
||||
"annotations_declared": 1,
|
||||
"polymorphic_declared": 18,
|
||||
"domains_declared": 18,
|
||||
"attached": 1,
|
||||
"provenance_stripped": 5,
|
||||
"provenance_declared": 29,
|
||||
"provenance_no_axis": 1,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 74,
|
||||
"triples_before": 723,
|
||||
"triples_after": 786,
|
||||
"notes": [
|
||||
"annotations_declared: polymorphic",
|
||||
"attached: GovernanceLayerObject -> DefinedObject",
|
||||
"provenance_stripped: DefinedObject",
|
||||
"provenance_stripped: CapturedObject",
|
||||
"provenance_stripped: storedIn",
|
||||
"provenance_stripped: exposesMetric",
|
||||
"provenance_stripped: primarilyStoredIn",
|
||||
"provenance_no_axis: SystemType",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T12:22:50",
|
||||
"mode": "dry-run",
|
||||
"from_version": "2.0",
|
||||
"to_version": "2.1",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "04273379898d04b9952de68004de5768"
|
||||
},
|
||||
"steps": {
|
||||
"provenance_no_axis": 1
|
||||
},
|
||||
"total_changes": 1,
|
||||
"triples_before": 786,
|
||||
"triples_after": 786,
|
||||
"notes": [
|
||||
"provenance_no_axis: SystemType"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T14:34:17",
|
||||
"mode": "dry-run",
|
||||
"from_version": "2.0",
|
||||
"to_version": "2.1",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
|
||||
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395"
|
||||
},
|
||||
"steps": {
|
||||
"annotations_declared": 1,
|
||||
"properties_declared": 1,
|
||||
"polymorphic_declared": 19,
|
||||
"domains_declared": 17,
|
||||
"attached": 1,
|
||||
"provenance_stripped": 5,
|
||||
"provenance_declared": 29,
|
||||
"provenance_no_axis": 1,
|
||||
"instances_removed": 4,
|
||||
"instances_moved": 1,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 80,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 723,
|
||||
"instances/sodh.ttl": 2308
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"notes": [
|
||||
"annotations_declared: polymorphic",
|
||||
"properties_declared: serviceCommitment",
|
||||
"attached: GovernanceLayerObject -> DefinedObject",
|
||||
"provenance_stripped: CapturedObject",
|
||||
"provenance_stripped: storedIn",
|
||||
"provenance_stripped: primarilyStoredIn",
|
||||
"provenance_stripped: DefinedObject",
|
||||
"provenance_stripped: exposesMetric",
|
||||
"provenance_no_axis: SystemType",
|
||||
"instances_removed: DI_06_01_001 businessDefinition",
|
||||
"instances_removed: DI_06_01_002 businessDefinition",
|
||||
"instances_removed: DI_06_01_001 sourcedFrom",
|
||||
"instances_removed: DI_06_01_002 sourcedFrom",
|
||||
"instances_moved: DC_06_01_001 businessRule",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T14:35:03",
|
||||
"mode": "apply",
|
||||
"from_version": "2.0",
|
||||
"to_version": "2.1",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
|
||||
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395"
|
||||
},
|
||||
"steps": {
|
||||
"annotations_declared": 1,
|
||||
"properties_declared": 1,
|
||||
"polymorphic_declared": 19,
|
||||
"domains_declared": 17,
|
||||
"attached": 1,
|
||||
"provenance_stripped": 5,
|
||||
"provenance_declared": 29,
|
||||
"provenance_no_axis": 1,
|
||||
"instances_removed": 4,
|
||||
"instances_moved": 1,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 80,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 723,
|
||||
"instances/sodh.ttl": 2308
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"notes": [
|
||||
"annotations_declared: polymorphic",
|
||||
"properties_declared: serviceCommitment",
|
||||
"attached: GovernanceLayerObject -> DefinedObject",
|
||||
"provenance_stripped: storedIn",
|
||||
"provenance_stripped: primarilyStoredIn",
|
||||
"provenance_stripped: DefinedObject",
|
||||
"provenance_stripped: CapturedObject",
|
||||
"provenance_stripped: exposesMetric",
|
||||
"provenance_no_axis: SystemType",
|
||||
"instances_removed: DI_06_01_001 businessDefinition",
|
||||
"instances_removed: DI_06_01_002 businessDefinition",
|
||||
"instances_removed: DI_06_01_001 sourcedFrom",
|
||||
"instances_removed: DI_06_01_002 sourcedFrom",
|
||||
"instances_moved: DC_06_01_001 businessRule",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T14:35:07",
|
||||
"mode": "dry-run",
|
||||
"from_version": "2.0",
|
||||
"to_version": "2.1",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "d264f924c61faa2fe8caf7be8ec7490c",
|
||||
"instances/sodh.ttl": "135278dd413486cb1273fe193a5aa590"
|
||||
},
|
||||
"steps": {
|
||||
"provenance_no_axis": 1
|
||||
},
|
||||
"total_changes": 1,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"notes": [
|
||||
"provenance_no_axis: SystemType"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-07T10:27:29",
|
||||
"mode": "apply",
|
||||
"from_version": "2.0",
|
||||
"to_version": "2.1",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
|
||||
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395"
|
||||
},
|
||||
"steps": {
|
||||
"annotations_declared": 1,
|
||||
"properties_declared": 1,
|
||||
"polymorphic_declared": 19,
|
||||
"domains_declared": 17,
|
||||
"attached": 1,
|
||||
"provenance_stripped": 5,
|
||||
"provenance_declared": 29,
|
||||
"provenance_no_axis": 1,
|
||||
"instances_removed": 4,
|
||||
"instances_moved": 1,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 80,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 723,
|
||||
"instances/sodh.ttl": 2308
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"notes": [
|
||||
"annotations_declared: polymorphic",
|
||||
"properties_declared: serviceCommitment",
|
||||
"attached: GovernanceLayerObject -> DefinedObject",
|
||||
"provenance_stripped: storedIn",
|
||||
"provenance_stripped: exposesMetric",
|
||||
"provenance_stripped: DefinedObject",
|
||||
"provenance_stripped: CapturedObject",
|
||||
"provenance_stripped: primarilyStoredIn",
|
||||
"provenance_no_axis: SystemType",
|
||||
"instances_removed: DI_06_01_001 businessDefinition",
|
||||
"instances_removed: DI_06_01_002 businessDefinition",
|
||||
"instances_removed: DI_06_01_001 sourcedFrom",
|
||||
"instances_removed: DI_06_01_002 sourcedFrom",
|
||||
"instances_moved: DC_06_01_001 businessRule",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T15:17:04",
|
||||
"mode": "dry-run",
|
||||
"from_version": "2.1",
|
||||
"to_version": "2.2",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "d264f924c61faa2fe8caf7be8ec7490c",
|
||||
"instances/sodh.ttl": "135278dd413486cb1273fe193a5aa590"
|
||||
},
|
||||
"steps": {
|
||||
"properties_declared": 1,
|
||||
"display_updated": 1,
|
||||
"instances_renamed": 80,
|
||||
"assignments_completed": 12,
|
||||
"sub_domain_owners_created": 10,
|
||||
"product_owners_created": 1,
|
||||
"stewards_created": 15,
|
||||
"steward_still_referenced": 6,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 127,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 796,
|
||||
"instances/sodh.ttl": 2557
|
||||
},
|
||||
"notes": [
|
||||
"properties_declared: heldBy",
|
||||
"display_updated: DataSteward -> DST",
|
||||
"steward_still_referenced: DST_DD_04 cited by 8 subject(s)",
|
||||
"steward_still_referenced: DST_DD_05 cited by 3 subject(s)",
|
||||
"steward_still_referenced: DST_DD_06 cited by 23 subject(s)",
|
||||
"steward_still_referenced: DST_DD_10 cited by 4 subject(s)",
|
||||
"steward_still_referenced: DST_DD_16 cited by 11 subject(s)",
|
||||
"steward_still_referenced: DST_DD_21 cited by 4 subject(s)",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.2"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T15:23:27",
|
||||
"mode": "dry-run",
|
||||
"from_version": "2.1",
|
||||
"to_version": "2.2",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "d264f924c61faa2fe8caf7be8ec7490c",
|
||||
"instances/sodh.ttl": "135278dd413486cb1273fe193a5aa590"
|
||||
},
|
||||
"steps": {
|
||||
"properties_declared": 1,
|
||||
"display_updated": 1,
|
||||
"instances_renamed": 80,
|
||||
"assignments_completed": 12,
|
||||
"sub_domain_owners_created": 10,
|
||||
"product_owners_created": 1,
|
||||
"stewards_created": 15,
|
||||
"ownedby_removed": 15,
|
||||
"ownedby_repointed_to_sdo": 7,
|
||||
"ownedby_repointed_to_steward": 30,
|
||||
"ownedby_unresolved": 1,
|
||||
"domain_stewards_retired": 5,
|
||||
"steward_still_referenced": 1,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 180,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 796,
|
||||
"instances/sodh.ttl": 2532
|
||||
},
|
||||
"notes": [
|
||||
"properties_declared: heldBy",
|
||||
"display_updated: DataSteward -> DST",
|
||||
"ownedby_unresolved: BC_06_01_003 reaches no business object",
|
||||
"domain_stewards_retired: DST_DD_04",
|
||||
"domain_stewards_retired: DST_DD_05",
|
||||
"steward_still_referenced: DST_DD_06 cited by 1 subject(s)",
|
||||
"domain_stewards_retired: DST_DD_10",
|
||||
"domain_stewards_retired: DST_DD_16",
|
||||
"domain_stewards_retired: DST_DD_21",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.2"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T19:49:37",
|
||||
"mode": "dry-run",
|
||||
"from_version": "2.1",
|
||||
"to_version": "2.2",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "d264f924c61faa2fe8caf7be8ec7490c",
|
||||
"instances/sodh.ttl": "135278dd413486cb1273fe193a5aa590"
|
||||
},
|
||||
"steps": {
|
||||
"properties_declared": 1,
|
||||
"display_updated": 1,
|
||||
"instances_renamed": 80,
|
||||
"assignments_completed": 12,
|
||||
"sub_domain_owners_created": 10,
|
||||
"product_owners_created": 1,
|
||||
"business_objects_created": 1,
|
||||
"stewards_created": 16,
|
||||
"ownedby_repointed_to_steward": 31,
|
||||
"ownedby_removed": 15,
|
||||
"ownedby_repointed_to_sdo": 7,
|
||||
"domain_stewards_retired": 6,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 182,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 796,
|
||||
"instances/sodh.ttl": 2546
|
||||
},
|
||||
"notes": [
|
||||
"properties_declared: heldBy",
|
||||
"display_updated: DataSteward -> DST",
|
||||
"business_objects_created: BO_06_01_006",
|
||||
"domain_stewards_retired: DST_DD_04",
|
||||
"domain_stewards_retired: DST_DD_05",
|
||||
"domain_stewards_retired: DST_DD_06",
|
||||
"domain_stewards_retired: DST_DD_10",
|
||||
"domain_stewards_retired: DST_DD_16",
|
||||
"domain_stewards_retired: DST_DD_21",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.2"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T19:51:46",
|
||||
"mode": "apply",
|
||||
"from_version": "2.1",
|
||||
"to_version": "2.2",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "d264f924c61faa2fe8caf7be8ec7490c",
|
||||
"instances/sodh.ttl": "135278dd413486cb1273fe193a5aa590"
|
||||
},
|
||||
"steps": {
|
||||
"properties_declared": 1,
|
||||
"display_updated": 1,
|
||||
"instances_renamed": 80,
|
||||
"assignments_completed": 12,
|
||||
"sub_domain_owners_created": 10,
|
||||
"product_owners_created": 1,
|
||||
"business_objects_created": 1,
|
||||
"stewards_created": 16,
|
||||
"ownedby_repointed_to_steward": 31,
|
||||
"ownedby_removed": 15,
|
||||
"ownedby_repointed_to_sdo": 7,
|
||||
"domain_stewards_retired": 6,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 182,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 796,
|
||||
"instances/sodh.ttl": 2546
|
||||
},
|
||||
"notes": [
|
||||
"properties_declared: heldBy",
|
||||
"display_updated: DataSteward -> DST",
|
||||
"business_objects_created: BO_06_01_006",
|
||||
"domain_stewards_retired: DST_DD_04",
|
||||
"domain_stewards_retired: DST_DD_05",
|
||||
"domain_stewards_retired: DST_DD_06",
|
||||
"domain_stewards_retired: DST_DD_10",
|
||||
"domain_stewards_retired: DST_DD_16",
|
||||
"domain_stewards_retired: DST_DD_21",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.2"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T19:51:51",
|
||||
"mode": "dry-run",
|
||||
"from_version": "2.1",
|
||||
"to_version": "2.2",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "e705cde06956d746631e412719b6cc6d",
|
||||
"instances/sodh.ttl": "82ced2220c1e0525c1dcd5028afbc546"
|
||||
},
|
||||
"steps": {
|
||||
"instances_renamed": 0
|
||||
},
|
||||
"total_changes": 0,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 796,
|
||||
"instances/sodh.ttl": 2546
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 796,
|
||||
"instances/sodh.ttl": 2546
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-07T10:21:51",
|
||||
"mode": "apply",
|
||||
"from_version": "2.1",
|
||||
"to_version": "2.2",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
|
||||
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395"
|
||||
},
|
||||
"steps": {
|
||||
"properties_declared": 1,
|
||||
"display_updated": 1,
|
||||
"instances_renamed": 80,
|
||||
"assignments_completed": 12,
|
||||
"sub_domain_owners_created": 10,
|
||||
"product_owners_created": 1,
|
||||
"business_objects_created": 1,
|
||||
"stewards_created": 16,
|
||||
"ownedby_repointed_to_steward": 31,
|
||||
"ownedby_removed": 15,
|
||||
"ownedby_repointed_to_sdo": 7,
|
||||
"domain_stewards_retired": 6,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 182,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 723,
|
||||
"instances/sodh.ttl": 2308
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 729,
|
||||
"instances/sodh.ttl": 2550
|
||||
},
|
||||
"notes": [
|
||||
"properties_declared: personName",
|
||||
"display_updated: DataSteward -> DST",
|
||||
"business_objects_created: BO_06_01_006",
|
||||
"domain_stewards_retired: DST_DD_04",
|
||||
"domain_stewards_retired: DST_DD_05",
|
||||
"domain_stewards_retired: DST_DD_06",
|
||||
"domain_stewards_retired: DST_DD_10",
|
||||
"domain_stewards_retired: DST_DD_16",
|
||||
"domain_stewards_retired: DST_DD_21",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.2"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-07T10:21:59",
|
||||
"mode": "dry-run",
|
||||
"from_version": "2.1",
|
||||
"to_version": "2.2",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "6f2deb192272d0bceecac31fcb525ad1",
|
||||
"instances/sodh.ttl": "7c0d42f33590d2208b0af7b2068f559a"
|
||||
},
|
||||
"steps": {
|
||||
"instances_renamed": 0
|
||||
},
|
||||
"total_changes": 0,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 729,
|
||||
"instances/sodh.ttl": 2550
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 729,
|
||||
"instances/sodh.ttl": 2550
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-07T10:27:33",
|
||||
"mode": "apply",
|
||||
"from_version": "2.1",
|
||||
"to_version": "2.2",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "96687771a06f85759e7b894f9d248083",
|
||||
"instances/sodh.ttl": "135278dd413486cb1273fe193a5aa590"
|
||||
},
|
||||
"steps": {
|
||||
"properties_declared": 1,
|
||||
"display_updated": 1,
|
||||
"instances_renamed": 80,
|
||||
"assignments_completed": 12,
|
||||
"sub_domain_owners_created": 10,
|
||||
"product_owners_created": 1,
|
||||
"business_objects_created": 1,
|
||||
"stewards_created": 16,
|
||||
"ownedby_repointed_to_steward": 31,
|
||||
"ownedby_removed": 15,
|
||||
"ownedby_repointed_to_sdo": 7,
|
||||
"domain_stewards_retired": 6,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 182,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 797,
|
||||
"instances/sodh.ttl": 2546
|
||||
},
|
||||
"notes": [
|
||||
"properties_declared: personName",
|
||||
"display_updated: DataSteward -> DST",
|
||||
"business_objects_created: BO_06_01_006",
|
||||
"domain_stewards_retired: DST_DD_04",
|
||||
"domain_stewards_retired: DST_DD_05",
|
||||
"domain_stewards_retired: DST_DD_06",
|
||||
"domain_stewards_retired: DST_DD_10",
|
||||
"domain_stewards_retired: DST_DD_16",
|
||||
"domain_stewards_retired: DST_DD_21",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.2"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migrate the Pernod Ricard Data MetaModel from v2.0 to v2.1.
|
||||
|
||||
USAGE
|
||||
python3 migrate_tbox_v2_1.py # dry run, writes nothing
|
||||
python3 migrate_tbox_v2_1.py --apply
|
||||
|
||||
v2.1 renames nothing. It declares what v2.0 left silent: which properties are
|
||||
polymorphic on purpose, what domain the others take, and how the instances of
|
||||
each concrete class are produced.
|
||||
|
||||
DESIGN
|
||||
EV-004 dry run is the default
|
||||
EV-005 every edit goes through rdflib
|
||||
EV-006 guards test the target state, so a replay reports zero change
|
||||
EV-015 an execution log is written for every attempt
|
||||
"""
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
|
||||
import yaml
|
||||
from rdflib import Graph, Literal, Namespace, RDF, RDFS, OWL, URIRef
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SPEC = os.path.join(HERE, "declarations_v2_1.yaml")
|
||||
|
||||
|
||||
class Report(object):
|
||||
def __init__(self):
|
||||
self.steps = OrderedDict()
|
||||
self.notes = []
|
||||
|
||||
def add(self, step, n=1, detail=None):
|
||||
self.steps[step] = self.steps.get(step, 0) + n
|
||||
if detail:
|
||||
self.notes.append("%s: %s" % (step, detail))
|
||||
|
||||
@property
|
||||
def total(self):
|
||||
return sum(self.steps.values())
|
||||
|
||||
|
||||
def md5(path):
|
||||
h = hashlib.md5()
|
||||
with open(path, "rb") as fh:
|
||||
for chunk in iter(lambda: fh.read(65536), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def derive_label(name):
|
||||
"""TN-018 and TN-023: a label is derived from the local name, never typed."""
|
||||
spaced = re.sub(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", " ", name)
|
||||
return spaced[0].lower() + spaced[1:]
|
||||
|
||||
|
||||
def local(uri, ns):
|
||||
s = str(uri)
|
||||
return s[len(ns):] if s.startswith(ns) else None
|
||||
|
||||
|
||||
def ancestors(g, term):
|
||||
seen, stack = set(), [term]
|
||||
while stack:
|
||||
node = stack.pop()
|
||||
for parent in g.objects(node, RDFS.subClassOf):
|
||||
if isinstance(parent, URIRef) and parent not in seen:
|
||||
seen.add(parent)
|
||||
stack.append(parent)
|
||||
return seen
|
||||
|
||||
|
||||
def check_version(g, spec, path):
|
||||
"""EV-010 applied to a migration script: refuse to run against the wrong input.
|
||||
|
||||
A script announcing 2.1 -> 2.2 will happily work on a file still in 2.0 and
|
||||
report success, leaving a state that is neither version. The same silent
|
||||
failure the shapes are guarded against, one layer down.
|
||||
"""
|
||||
ns = spec["meta"]["namespace"]
|
||||
expected = URIRef(ns.rstrip("/") + "/" + spec["meta"]["from_version"])
|
||||
target = URIRef(ns.rstrip("/") + "/" + spec["meta"]["to_version"])
|
||||
found = set(g.objects(None, OWL.versionIRI))
|
||||
if target in found:
|
||||
print("ALREADY AT %s — nothing to do." % spec["meta"]["to_version"])
|
||||
sys.exit(0)
|
||||
if expected not in found:
|
||||
print("ABORT — %s declares %s; this script migrates from %s."
|
||||
% (os.path.basename(path),
|
||||
", ".join(sorted(str(f) for f in found)) or "no version",
|
||||
expected))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def migrate(g, instances, spec, report):
|
||||
ns = spec["meta"]["namespace"]
|
||||
pr = Namespace(ns)
|
||||
|
||||
# 1 — declare the annotation property itself
|
||||
for a in spec.get("declare_annotations") or []:
|
||||
term = pr[a["term"]]
|
||||
if (term, RDF.type, OWL.AnnotationProperty) not in g:
|
||||
g.add((term, RDF.type, OWL.AnnotationProperty))
|
||||
g.add((term, RDFS.label, Literal(a["label"])))
|
||||
g.add((term, RDFS.comment, Literal(" ".join(a["comment"].split()))))
|
||||
report.add("annotations_declared", 1, a["term"])
|
||||
|
||||
# 2 — declare the new properties (procedure C1)
|
||||
for d in spec.get("declare_properties") or []:
|
||||
term = pr[d["term"]]
|
||||
kind = getattr(OWL, d["kind"])
|
||||
if (term, RDF.type, kind) not in g:
|
||||
g.add((term, RDF.type, kind))
|
||||
g.add((term, RDFS.label, Literal(derive_label(d["term"]))))
|
||||
g.add((term, RDFS.domain, pr[d["domain"]]))
|
||||
g.add((term, RDFS.range, URIRef(d["range"])))
|
||||
g.add((term, RDFS.comment, Literal(" ".join(d["comment"].split()))))
|
||||
report.add("properties_declared", 1, d["term"])
|
||||
|
||||
# 3 — mark the deliberately polymorphic properties (TN-025)
|
||||
for p in spec.get("polymorphic") or []:
|
||||
term = pr[p["term"]]
|
||||
if (term, None, None) not in g:
|
||||
report.add("polymorphic_missing", 1, p["term"])
|
||||
continue
|
||||
if (term, pr.polymorphic, Literal(True)) not in g:
|
||||
g.add((term, pr.polymorphic, Literal(True)))
|
||||
report.add("polymorphic_declared", 1)
|
||||
if list(g.objects(term, RDFS.domain)):
|
||||
report.add("polymorphic_has_domain", 1,
|
||||
"%s is marked polymorphic and still declares a domain" % p["term"])
|
||||
|
||||
# 4 — declare the domains that were merely missing (TN-025)
|
||||
for d in spec.get("domains") or []:
|
||||
term, target = pr[d["term"]], pr[d["domain"]]
|
||||
if (term, None, None) not in g:
|
||||
report.add("domain_missing_term", 1, d["term"])
|
||||
continue
|
||||
if (target, RDF.type, OWL.Class) not in g:
|
||||
report.add("domain_missing_class", 1, "%s -> %s" % (d["term"], d["domain"]))
|
||||
continue
|
||||
if (term, RDFS.domain, target) not in g:
|
||||
g.remove((term, RDFS.domain, None))
|
||||
g.add((term, RDFS.domain, target))
|
||||
report.add("domains_declared", 1)
|
||||
|
||||
# 5 — ranges, where a polymorphic property still points at one class
|
||||
for r in spec.get("ranges") or []:
|
||||
term, target = pr[r["term"]], pr[r["range"]]
|
||||
if (term, RDFS.range, target) not in g:
|
||||
g.remove((term, RDFS.range, None))
|
||||
g.add((term, RDFS.range, target))
|
||||
report.add("ranges_declared", 1)
|
||||
|
||||
# 6 — attach a layer root to its provenance axis (TN-027)
|
||||
for a in spec.get("attach") or []:
|
||||
term, parent = pr[a["term"]], pr[a["parent"]]
|
||||
if (term, RDFS.subClassOf, parent) not in g:
|
||||
g.add((term, RDFS.subClassOf, parent))
|
||||
report.add("attached", 1, "%s -> %s" % (a["term"], a["parent"]))
|
||||
|
||||
# 7 — provenance belongs to the concrete classes, and to nothing else (TN-026)
|
||||
prov = spec.get("provenance") or {}
|
||||
concrete = {c for c in g.subjects(RDF.type, OWL.Class)
|
||||
if (c, pr.isAbstract, Literal(False)) in g}
|
||||
|
||||
if prov.get("strip_from_non_concrete"):
|
||||
for s in set(g.subjects(pr.authoringMode, None)):
|
||||
if s in concrete:
|
||||
continue
|
||||
g.remove((s, pr.authoringMode, None))
|
||||
g.remove((s, pr.harvestSource, None))
|
||||
report.add("provenance_stripped", 1, local(s, ns))
|
||||
|
||||
if prov.get("derive_from_axis"):
|
||||
for cls in sorted(concrete, key=str):
|
||||
up = ancestors(g, cls)
|
||||
if pr.CapturedObject in up:
|
||||
want = "HARVESTED"
|
||||
elif pr.DefinedObject in up:
|
||||
want = "ASSERTED"
|
||||
else:
|
||||
report.add("provenance_no_axis", 1, local(cls, ns))
|
||||
continue
|
||||
if (cls, pr.authoringMode, Literal(want)) in g:
|
||||
continue
|
||||
g.remove((cls, pr.authoringMode, None))
|
||||
g.add((cls, pr.authoringMode, Literal(want)))
|
||||
report.add("provenance_declared", 1)
|
||||
|
||||
# 8 — instance corrections, each one surfaced by declaring a domain
|
||||
ex = Namespace("https://data.pernod-ricard.com/sodh/")
|
||||
for fix in spec.get("instance_fixes") or []:
|
||||
prop = pr[fix["property"]]
|
||||
dest = pr[fix["to"]] if fix.get("to") else None
|
||||
for subject in fix["subjects"]:
|
||||
node = ex[subject]
|
||||
for gi in instances:
|
||||
for _, _, value in list(gi.triples((node, prop, None))):
|
||||
gi.remove((node, prop, value))
|
||||
if dest is not None:
|
||||
gi.add((node, dest, value))
|
||||
report.add("instances_%sd" % fix["action"], 1,
|
||||
"%s %s" % (subject, fix["property"]))
|
||||
|
||||
# 9 — bump the version (EV-014: the namespace itself never moves)
|
||||
target = URIRef(ns.rstrip("/") + "/" + spec["meta"]["to_version"])
|
||||
for onto in set(g.subjects(RDF.type, OWL.Ontology)):
|
||||
if (onto, OWL.versionIRI, target) not in g:
|
||||
g.remove((onto, OWL.versionIRI, None))
|
||||
g.add((onto, OWL.versionIRI, target))
|
||||
report.add("version_bumped", 1, str(target))
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--apply", action="store_true")
|
||||
ap.add_argument("--ontology", default=os.path.join(HERE, "..", "..", "ontology",
|
||||
"pr_metamodel.ttl"))
|
||||
ap.add_argument("--instances", action="append", default=[])
|
||||
ap.add_argument("--log-dir", default=os.path.join(HERE, "logs"))
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.exists(args.ontology):
|
||||
print("MISSING INPUT: %s" % args.ontology)
|
||||
sys.exit(1)
|
||||
|
||||
spec = yaml.safe_load(open(SPEC, encoding="utf-8"))
|
||||
checksums = OrderedDict((p, md5(p)) for p in [args.ontology] + args.instances)
|
||||
|
||||
g = Graph()
|
||||
g.parse(args.ontology, format="turtle")
|
||||
instances = []
|
||||
for path in args.instances:
|
||||
gi = Graph()
|
||||
gi.parse(path, format="turtle")
|
||||
instances.append(gi)
|
||||
check_version(g, spec, args.ontology)
|
||||
|
||||
before = [len(g)] + [len(x) for x in instances]
|
||||
|
||||
report = Report()
|
||||
migrate(g, instances, spec, report)
|
||||
after = [len(g)] + [len(x) for x in instances]
|
||||
|
||||
print("MIGRATION %s -> %s %s"
|
||||
% (spec["meta"]["from_version"], spec["meta"]["to_version"],
|
||||
"APPLY" if args.apply else "DRY RUN"))
|
||||
print()
|
||||
for step, n in report.steps.items():
|
||||
print(" %-28s %6d" % (step, n))
|
||||
print(" %-28s %6d" % ("total changes", report.total))
|
||||
print()
|
||||
for path, b, a in zip([args.ontology] + args.instances, before, after):
|
||||
print(" %-46s %6d -> %6d triples" % (os.path.basename(path), b, a))
|
||||
if report.notes:
|
||||
print()
|
||||
for n in report.notes:
|
||||
print(" note: " + n)
|
||||
|
||||
os.makedirs(args.log_dir, exist_ok=True)
|
||||
stamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S")
|
||||
log_path = os.path.join(args.log_dir, "migration_v2_1_%s.json" % stamp)
|
||||
json.dump({"attempt_timestamp": datetime.datetime.now().isoformat(timespec="seconds"),
|
||||
"mode": "apply" if args.apply else "dry-run",
|
||||
"from_version": spec["meta"]["from_version"],
|
||||
"to_version": spec["meta"]["to_version"],
|
||||
"input_checksums": checksums,
|
||||
"steps": report.steps, "total_changes": report.total,
|
||||
"triples_before": dict(zip([args.ontology] + args.instances, before)),
|
||||
"triples_after": dict(zip([args.ontology] + args.instances, after)),
|
||||
"notes": report.notes},
|
||||
open(log_path, "w", encoding="utf-8"), indent=2)
|
||||
print("\n log: %s" % log_path)
|
||||
|
||||
if not args.apply:
|
||||
print("\n DRY RUN — nothing written. Re-run with --apply when the counts "
|
||||
"above are what you expect.")
|
||||
return
|
||||
|
||||
g.serialize(destination=args.ontology, format="turtle")
|
||||
for path, gi in zip(args.instances, instances):
|
||||
gi.serialize(destination=path, format="turtle")
|
||||
print("\n written. Replay this script now: a second run must report zero "
|
||||
"changes (EV-006).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,367 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migrate the Pernod Ricard Data MetaModel and the SODH instances to v2.2.
|
||||
|
||||
USAGE
|
||||
python3 migrate_tbox_v2_2.py --ontology ... --instances ...
|
||||
python3 migrate_tbox_v2_2.py --ontology ... --instances ... --apply
|
||||
|
||||
v2.2 turns the actor nodes into governance ASSIGNMENTS: a stable post carrying
|
||||
an identifier, a domain and a name, plus the person holding it as a value. It
|
||||
completes the twelve that existed, creates the ten sub-domain owners, the
|
||||
product owner, and one steward assignment per business object.
|
||||
|
||||
DESIGN
|
||||
EV-004 dry run is the default
|
||||
EV-005 every edit goes through rdflib
|
||||
EV-006 guards test the target state, so a replay reports zero change
|
||||
EV-015 an execution log is written for every attempt
|
||||
"""
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
|
||||
import yaml
|
||||
from rdflib import Graph, Literal, Namespace, RDF, RDFS, OWL, URIRef
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SPEC = os.path.join(HERE, "assignments_v2_2.yaml")
|
||||
|
||||
|
||||
class Report(object):
|
||||
def __init__(self):
|
||||
self.steps = OrderedDict()
|
||||
self.notes = []
|
||||
|
||||
def add(self, step, n=1, detail=None):
|
||||
self.steps[step] = self.steps.get(step, 0) + n
|
||||
if detail:
|
||||
self.notes.append("%s: %s" % (step, detail))
|
||||
|
||||
@property
|
||||
def total(self):
|
||||
return sum(self.steps.values())
|
||||
|
||||
|
||||
def derive_label(name):
|
||||
"""TN-018 and TN-023: a label is derived from the local name, never typed."""
|
||||
spaced = re.sub(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", " ", name)
|
||||
return spaced[0].lower() + spaced[1:]
|
||||
|
||||
|
||||
def md5(path):
|
||||
h = hashlib.md5()
|
||||
with open(path, "rb") as fh:
|
||||
for chunk in iter(lambda: fh.read(65536), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def rename_node(g, old, new, report, step):
|
||||
"""EV-001 applied to an instance: rewrite every triple naming the node."""
|
||||
if (new, None, None) in g and (old, None, None) not in g:
|
||||
return 0
|
||||
n = 0
|
||||
for s, p, o in list(g):
|
||||
ns, no = (new if s == old else s), (new if o == old else o)
|
||||
if (ns, no) != (s, o):
|
||||
g.remove((s, p, o))
|
||||
g.add((ns, p, no))
|
||||
n += 1
|
||||
report.add(step, n)
|
||||
return n
|
||||
|
||||
|
||||
def assign(g, pr, node, role, identifier, name, held_by, domain, spec, report, step):
|
||||
"""Write one assignment. The guard tests the identifier, so a replay is a no-op."""
|
||||
if (node, pr.identifier, Literal(identifier)) in g:
|
||||
return False
|
||||
g.add((node, RDF.type, role))
|
||||
g.remove((node, pr.identifier, None))
|
||||
g.add((node, pr.identifier, Literal(identifier)))
|
||||
g.remove((node, pr.canonicalName, None))
|
||||
g.add((node, pr.canonicalName, Literal(name)))
|
||||
g.remove((node, pr.personName, None))
|
||||
if held_by:
|
||||
g.add((node, pr.personName, Literal(held_by)))
|
||||
g.remove((node, pr.status, None))
|
||||
g.add((node, pr.status, Literal(spec["status"])))
|
||||
g.remove((node, pr.version, None))
|
||||
g.add((node, pr.version, Literal(spec["version"])))
|
||||
if domain is not None:
|
||||
g.remove((node, pr.ownedByDomain, None))
|
||||
g.add((node, pr.ownedByDomain, domain))
|
||||
report.add(step, 1)
|
||||
return True
|
||||
|
||||
|
||||
def check_version(g, spec, path):
|
||||
"""EV-010 applied to a migration script: refuse to run against the wrong input.
|
||||
|
||||
A script announcing 2.1 -> 2.2 will happily work on a file still in 2.0 and
|
||||
report success, leaving a state that is neither version. The same silent
|
||||
failure the shapes are guarded against, one layer down.
|
||||
"""
|
||||
ns = spec["meta"]["namespace"]
|
||||
expected = URIRef(ns.rstrip("/") + "/" + spec["meta"]["from_version"])
|
||||
target = URIRef(ns.rstrip("/") + "/" + spec["meta"]["to_version"])
|
||||
found = set(g.objects(None, OWL.versionIRI))
|
||||
if target in found:
|
||||
print("ALREADY AT %s — nothing to do." % spec["meta"]["to_version"])
|
||||
sys.exit(0)
|
||||
if expected not in found:
|
||||
print("ABORT — %s declares %s; this script migrates from %s."
|
||||
% (os.path.basename(path),
|
||||
", ".join(sorted(str(f) for f in found)) or "no version",
|
||||
expected))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def migrate(onto, data, spec, report):
|
||||
ns, ins = spec["meta"]["namespace"], spec["meta"]["instance_namespace"]
|
||||
pr, ex = Namespace(ns), Namespace(ins)
|
||||
|
||||
# 1 — declare heldBy on Actor (procedure C1)
|
||||
for d in spec.get("declare_properties") or []:
|
||||
term = pr[d["term"]]
|
||||
if (term, RDF.type, getattr(OWL, d["kind"])) not in onto:
|
||||
onto.add((term, RDF.type, getattr(OWL, d["kind"])))
|
||||
onto.add((term, RDFS.label, Literal(derive_label(d["term"]))))
|
||||
onto.add((term, RDFS.domain, pr[d["domain"]]))
|
||||
onto.add((term, RDFS.range, URIRef(d["range"])))
|
||||
onto.add((term, RDFS.comment, Literal(" ".join(d["comment"].split()))))
|
||||
report.add("properties_declared", 1, d["term"])
|
||||
|
||||
# 2 — the short form of Data Steward is DST, and the rulebook now says so
|
||||
for u in spec.get("display_updates") or []:
|
||||
term = pr[u["term"]]
|
||||
if (term, pr.acronym, Literal(u["acronym"])) not in onto:
|
||||
onto.remove((term, pr.acronym, None))
|
||||
onto.add((term, pr.acronym, Literal(u["acronym"])))
|
||||
report.add("display_updated", 1, "%s -> %s" % (u["term"], u["acronym"]))
|
||||
|
||||
# 3 — rename the instance nodes that carried the wrong short form
|
||||
for r in spec.get("instance_renames") or []:
|
||||
rename_node(data, ex[r["from"]], ex[r["to"]], report, "instances_renamed")
|
||||
|
||||
# 4 — complete the assignments that already existed
|
||||
for c in spec.get("complete") or []:
|
||||
node = ex[c["node"]]
|
||||
if (node, None, None) not in data:
|
||||
report.add("complete_missing", 1, c["node"])
|
||||
continue
|
||||
role = next(iter(data.objects(node, RDF.type)), None)
|
||||
assign(data, pr, node, role, c["identifier"], c["name"],
|
||||
c["held_by"], ex[c["domain"]], spec, report, "assignments_completed")
|
||||
|
||||
# 5 — sub-domain owners
|
||||
sdo_of = {}
|
||||
for s in spec.get("create_sub_domain_owners") or []:
|
||||
node = ex[s["node"]]
|
||||
sdo_of[ex[s["sub_domain"]]] = node
|
||||
if assign(data, pr, node, pr.DataSubDomainOwner, s["identifier"],
|
||||
spec["sub_domain_owner_name"], s["held_by"], ex[s["domain"]],
|
||||
spec, report, "sub_domain_owners_created"):
|
||||
data.add((ex[s["sub_domain"]], pr.hasSubDomainOwner, node))
|
||||
|
||||
# 6 — product owners
|
||||
for p in spec.get("create_product_owners") or []:
|
||||
node = ex[p["node"]]
|
||||
product = ex[p["product"]]
|
||||
if (product, None, None) not in data:
|
||||
report.add("product_missing", 1, p["product"])
|
||||
continue
|
||||
if assign(data, pr, node, pr.DataProductOwner, p["identifier"],
|
||||
spec["product_owner_name"], p["held_by"], ex[p["domain"]],
|
||||
spec, report, "product_owners_created"):
|
||||
data.add((product, pr.hasProductOwner, node))
|
||||
|
||||
# 7 — create the business objects that close a governance chain
|
||||
for b in spec.get("create_business_objects") or []:
|
||||
node = ex[b["node"]]
|
||||
if (node, pr.identifier, Literal(b["identifier"])) in data:
|
||||
continue
|
||||
data.add((node, RDF.type, pr.BusinessObject))
|
||||
data.add((node, pr.identifier, Literal(b["identifier"])))
|
||||
data.add((node, pr.canonicalName, Literal(b["name"])))
|
||||
data.add((node, pr.isAbout, ex[b["is_about"]]))
|
||||
data.add((node, pr.belongsTo, ex[b["sub_domain"]]))
|
||||
data.add((node, pr.ownedByDomain, ex[b["domain"]]))
|
||||
data.add((node, pr.status, Literal(spec["status"])))
|
||||
data.add((node, pr.version, Literal(spec["version"])))
|
||||
report.add("business_objects_created", 1, b["node"])
|
||||
|
||||
# 8 — one steward assignment per business object, derived from the graph
|
||||
holders = spec.get("steward_holders") or {}
|
||||
steward_of = {}
|
||||
for bo in sorted(data.subjects(RDF.type, pr.BusinessObject), key=str):
|
||||
ident = next((str(v) for v in data.objects(bo, pr.identifier)), None)
|
||||
if not ident:
|
||||
report.add("steward_no_identifier", 1, str(bo).replace(ins, ""))
|
||||
continue
|
||||
suffix = ident.split("-", 1)[1] # 06.01-001
|
||||
domain_key = "DD_" + suffix.split(".", 1)[0] # DD_06
|
||||
holder = holders.get(domain_key)
|
||||
if holder is None:
|
||||
report.add("steward_no_holder", 1, domain_key)
|
||||
continue
|
||||
node = ex["DST_" + suffix.replace(".", "_").replace("-", "_")]
|
||||
steward_of[bo] = node
|
||||
if assign(data, pr, node, pr.DataSteward, "DST-" + suffix,
|
||||
spec["steward_name"], holder, ex[domain_key],
|
||||
spec, report, "stewards_created"):
|
||||
for _, _, old in list(data.triples((bo, pr.monitoredBy, None))):
|
||||
data.remove((bo, pr.monitoredBy, old))
|
||||
data.add((bo, pr.monitoredBy, node))
|
||||
|
||||
# 9 — resolve every ownedBy pointing at a per-domain steward node
|
||||
res = spec.get("owned_by_resolution") or {}
|
||||
old_stewards = {ex[r["from"]] for r in (spec.get("instance_renames") or [])} | \
|
||||
{ex[r["to"]] for r in (spec.get("instance_renames") or [])}
|
||||
|
||||
def business_object_of(subject):
|
||||
"""Follow the declared chains until a business object is reached."""
|
||||
for chain in res.get("chains") or []:
|
||||
prop = pr[chain["via"]]
|
||||
if chain["direction"] == "inbound":
|
||||
for candidate in data.subjects(prop, subject):
|
||||
if (candidate, RDF.type, pr.BusinessObject) in data:
|
||||
return candidate
|
||||
else:
|
||||
for target in data.objects(subject, prop):
|
||||
if (target, RDF.type, pr.BusinessObject) in data:
|
||||
return target
|
||||
for candidate in data.subjects(pr.isAbout, target):
|
||||
if (candidate, RDF.type, pr.BusinessObject) in data:
|
||||
return candidate
|
||||
return None
|
||||
|
||||
remove_on = {pr[c] for c in (res.get("remove_on") or [])}
|
||||
for subject, _, target in list(data.triples((None, pr.ownedBy, None))):
|
||||
if target not in old_stewards:
|
||||
continue
|
||||
types = set(data.objects(subject, RDF.type))
|
||||
if types & remove_on:
|
||||
data.remove((subject, pr.ownedBy, target))
|
||||
report.add("ownedby_removed", 1)
|
||||
continue
|
||||
if pr.DataSubDomain in types and res.get("repoint_sub_domains"):
|
||||
owner = sdo_of.get(subject)
|
||||
if owner is None:
|
||||
report.add("ownedby_unresolved", 1,
|
||||
"%s has no sub-domain owner" % str(subject).replace(ins, ""))
|
||||
continue
|
||||
data.remove((subject, pr.ownedBy, target))
|
||||
data.add((subject, pr.ownedBy, owner))
|
||||
report.add("ownedby_repointed_to_sdo", 1)
|
||||
continue
|
||||
bo = business_object_of(subject)
|
||||
owner = steward_of.get(bo) if bo is not None else None
|
||||
if owner is None:
|
||||
report.add("ownedby_unresolved", 1,
|
||||
"%s reaches no business object" % str(subject).replace(ins, ""))
|
||||
continue
|
||||
data.remove((subject, pr.ownedBy, target))
|
||||
data.add((subject, pr.ownedBy, owner))
|
||||
report.add("ownedby_repointed_to_steward", 1)
|
||||
|
||||
# 10 — withdraw the per-domain steward nodes, once nothing names them (EV-003)
|
||||
if spec.get("retire_domain_stewards"):
|
||||
for r in spec.get("instance_renames") or []:
|
||||
node = ex[r["to"]]
|
||||
referrers = {s for s in data.subjects(None, node)}
|
||||
if referrers:
|
||||
report.add("steward_still_referenced", 1,
|
||||
"%s cited by %d subject(s)" % (r["to"], len(referrers)))
|
||||
continue
|
||||
n = 0
|
||||
for t in list(data.triples((node, None, None))):
|
||||
data.remove(t)
|
||||
n += 1
|
||||
if n:
|
||||
report.add("domain_stewards_retired", 1, r["to"])
|
||||
|
||||
# 11 — bump the version
|
||||
target = URIRef(ns.rstrip("/") + "/" + spec["meta"]["to_version"])
|
||||
for o in set(onto.subjects(RDF.type, OWL.Ontology)):
|
||||
if (o, OWL.versionIRI, target) not in onto:
|
||||
onto.remove((o, OWL.versionIRI, None))
|
||||
onto.add((o, OWL.versionIRI, target))
|
||||
report.add("version_bumped", 1, str(target))
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--apply", action="store_true")
|
||||
ap.add_argument("--ontology", required=True)
|
||||
ap.add_argument("--instances", required=True)
|
||||
ap.add_argument("--log-dir", default=os.path.join(HERE, "logs"))
|
||||
args = ap.parse_args()
|
||||
|
||||
for path in (args.ontology, args.instances):
|
||||
if not os.path.exists(path):
|
||||
print("MISSING INPUT: %s" % path)
|
||||
sys.exit(1)
|
||||
|
||||
spec = yaml.safe_load(open(SPEC, encoding="utf-8"))
|
||||
checksums = OrderedDict((p, md5(p)) for p in (args.ontology, args.instances))
|
||||
|
||||
onto, data = Graph(), Graph()
|
||||
onto.parse(args.ontology, format="turtle")
|
||||
data.parse(args.instances, format="turtle")
|
||||
check_version(onto, spec, args.ontology)
|
||||
|
||||
before = (len(onto), len(data))
|
||||
|
||||
report = Report()
|
||||
migrate(onto, data, spec, report)
|
||||
after = (len(onto), len(data))
|
||||
|
||||
print("MIGRATION %s -> %s %s"
|
||||
% (spec["meta"]["from_version"], spec["meta"]["to_version"],
|
||||
"APPLY" if args.apply else "DRY RUN"))
|
||||
print()
|
||||
for step, n in report.steps.items():
|
||||
print(" %-28s %6d" % (step, n))
|
||||
print(" %-28s %6d" % ("total changes", report.total))
|
||||
print()
|
||||
for path, b, a in zip((args.ontology, args.instances), before, after):
|
||||
print(" %-46s %6d -> %6d triples" % (os.path.basename(path), b, a))
|
||||
if report.notes:
|
||||
print()
|
||||
for n in report.notes:
|
||||
print(" note: " + n)
|
||||
|
||||
os.makedirs(args.log_dir, exist_ok=True)
|
||||
stamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S")
|
||||
log_path = os.path.join(args.log_dir, "migration_v2_2_%s.json" % stamp)
|
||||
json.dump({"attempt_timestamp": datetime.datetime.now().isoformat(timespec="seconds"),
|
||||
"mode": "apply" if args.apply else "dry-run",
|
||||
"from_version": spec["meta"]["from_version"],
|
||||
"to_version": spec["meta"]["to_version"],
|
||||
"input_checksums": checksums,
|
||||
"steps": report.steps, "total_changes": report.total,
|
||||
"triples_before": dict(zip((args.ontology, args.instances), before)),
|
||||
"triples_after": dict(zip((args.ontology, args.instances), after)),
|
||||
"notes": report.notes},
|
||||
open(log_path, "w", encoding="utf-8"), indent=2)
|
||||
print("\n log: %s" % log_path)
|
||||
|
||||
if not args.apply:
|
||||
print("\n DRY RUN — nothing written. Re-run with --apply when the counts "
|
||||
"above are what you expect.")
|
||||
return
|
||||
|
||||
onto.serialize(destination=args.ontology, format="turtle")
|
||||
data.serialize(destination=args.instances, format="turtle")
|
||||
print("\n written. Replay this script now: a second run must report zero "
|
||||
"changes (EV-006).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user