Python pipeline that converts the CeraTyOnt CSV lookup tables into a SKOS-compliant Turtle terminology and validates it against SHACL shapes (SkoHub SHACL as primary, plus CeraTyOnt-specific rules). Build + validation run in one script.
root/
├── data/ # input CSVs
│ ├── tbllookupformsgeneric.csv
│ ├── tbllookupformstradition.csv
│ ├── tbllookupformsservices.csv
│ ├── tbllookuppublisher.csv
│ └── v_ceratyont_potforms_distinct.csv
├── py/
│ ├── run.py # ← ONE script: builds + validates
│ ├── config.yaml # all paths, URIs, column mappings
│ └── shapes/
│ ├── skohub_shacl.ttl # primary: SkoHub SKOS SHACL shapes
│ └── ceratyont_shapes.ttl # secondary: CeraTyOnt-specific rules
├── output/
│ ├── ceratyont_skos.ttl # generated SKOS terminology
│ ├── validation_report.ttl # machine-readable SHACL report
│ └── validation_report.md # human-readable summary ← read this one
└── requirements.txt
Open a PowerShell terminal in the project root:
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -r requirements.txtIf PowerShell blocks Activate.ps1:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSignedIn VS Code: Ctrl+Shift+P → Python: Select Interpreter → pick .venv.
From the py/ folder (so relative paths in config.yaml resolve):
cd py
python run.pyThat's it — one command builds the Turtle and validates it.
| Flag | Effect |
|---|---|
-v, --verbose |
Debug logging |
--skip-build |
Only validate an existing ceratyont_skos.ttl |
--skip-validation |
Only build; don't run SHACL |
--strict |
Exit with code 1 if SHACL reports any violation (useful for CI) |
--config path.yaml |
Use a different config file |
The generated SKOS terminology in Turtle. Contents after a successful build:
- 1
skos:ConceptSchemewith full publication metadata:dct:title,dct:description,dct:creator(ORCID IRI),dct:contributor,dct:publisher,dct:license(IRI),dct:rights,dct:created,dct:issued,dct:modified(auto-updated to build date),dct:hasVersion,owl:versionInfo,dct:source,prov:wasDerivedFrom,rdfs:seeAlso(linking back to the CeraTyOnt OWL ontology), plusvann:preferredNamespaceUriandvann:preferredNamespacePrefix - The creator is modelled once as a
foaf:Personwith an ORCID IRI, and alldct:creatorlinks throughout the graph point to that same IRI — so tools can resolve the creator's name from a single dereferencable URL - 4 facet concepts as top-concepts of the scheme (
Generic Potforms,Traditions,Services,Publishers) — each acts as the root of its own branch and has askos:definition - 60 member
skos:Concepts attached to their facet viaskos:broader/skos:narrower(32 Generic + 3 Tradition + 10 Service + 15 Publisher), each with askos:definitionfrom a configurable template - 866 Potform
skos:Concepts, each attached to its Publisher, GenericPotform, and Tradition viaskos:broader(multi-parent hierarchy), giving four browseable axes: by publisher, by form, by tradition, by service - Every concept (facets, members, potforms) carries:
dct:creator— the ORCID IRI of Allard Meesdct:created— the last-modified timestamp of its source CSV (so the Potforms' created date is inherited fromv_ceratyont_potforms_distinct.csv's mtime, Publishers fromtbllookuppublisher.csv, etc.)skos:scopeNote— generic usage guidance from a configurable template (distinct fromskos:definition: the scopeNote explains when to use the concept, while the definition explains what it is)
- Every known-publisher Potform gets a
skos:notationof the form"<PublisherAbbrev> <FormLabel>"(e.g."Drag. 15"), plus — if that notation differs from the prefLabel — a matchingskos:altLabel. Abbreviations are managed inpy/abbreviations.py(not a CSV). - Cross-potform relations from the connections CSV:
skos:exactMatchfor "is same form as" pairs (symmetric)skos:relatedfor "has service member" pairs (symmetric)skos:related+lado:hasSame{Rim,Footring,Roulette,Groove,Flute}for feature-similarity relations (both directions)
- Auto-flip of suspicious Generic→Generic edges with
skos:historyNotedocumenting the change (controlled viabuild.auto_flip_*inconfig.yaml)
Approximate totals for the current source data: ~15000 triples, 2625 skos:broader,
390 skos:related, 182 skos:exactMatch, 930 skos:definition, 930 skos:scopeNote,
850 skos:notation, 120 skos:altLabel, 359 foaf:depiction.
The 16 potforms with publisher = NULL in the source data are still
included as concepts in the scheme but sit outside the facet hierarchy.
Note:
skos:Collectionobjects are intentionally omitted. SKOS viewers like SKOS-Play render Collections as separate branches parallel to top-concepts, which would duplicate the facet tree visually. The facet-concept hierarchy alone provides the same grouping without this redundancy.
Human-readable Markdown summary with:
- Data-graph statistics (triple counts, concept counts, etc.)
- Overall conforms/not-conforms flag
- Breakdown by severity (Violation / Warning / Info)
- Breakdown by SHACL constraint component (MinCount, Datatype, NodeKind…)
- Breakdown by message, each with a table of the first few offending focus nodes and their problem values
Example snippet from a report with issues:
Focus node Path Offending value ceratyont:broken_test_conceptskos:prefLabel— ceratyont:broken_test_concept_2skos:prefLabel"no language tag"
Full machine-readable SHACL sh:ValidationReport graph. Use this if you want
to process the report programmatically or in SHACL-aware tooling.
Every Potform has three skos:broader links (publisher, generic form,
tradition) — so it lives in three facet branches simultaneously. SKOS allows
this multi-parent hierarchy, and SKOS viewers will show the potform under
each parent.
ConceptScheme: ceratyont-terminology
│
├── [Top] Generic Potforms (facet)
│ ├── Bowl ─┬─ Bowl Decorated
│ │ └─ Bowl Flanged
│ ├── Cup ──── Cup Decorated ── (also broader: Decorated)
│ ├── Dish ─── Dish Rouletted ── (also broader: Rouletted)
│ ├── Varia ─┬─ Poinçon, Pyxis, Lid, Patera, …
│ │ └─ (sub-categories from the Generic→Generic edges)
│ └── … plus all 866 Potforms link here via skos:broader
│
├── [Top] Traditions (facet)
│ ├── italian ← potforms + Services I, II
│ ├── Gaulish-Germanic-Raetian ← potforms + Services A–F
│ └── African
│
├── [Top] Services (facet)
│ └── Service I, II, A, B, C, D, E, F, III, IV
│ (linked to potforms via skos:related)
│
└── [Top] Publishers (facet)
├── Dragendorff ─── potform_1 (Drag. 15), potform_2 (15/17), …
├── Conspectus, Curle, Déchelette, Hermet, Knorr, …
└── (15 publishers, 850 potforms attached)
Cross-references (non-hierarchical):
skos:exactMatch — "is same form as" (92 pairs)
skos:related — "has service member" + feature similarities
lado:hasSameRim / hasSameFootring / hasSameRoulette /
hasSameGroove / hasSameFlute — refined feature semantics
Two shape graphs are loaded and evaluated together (configured in
config.yaml under shapes:):
Generic SKOS structural constraints from https://github.com/skohub-io/skohub-shacl. Enforces, among other things:
skos:ConceptSchememust have a language-taggeddct:title,dct:description,dct:license(IRI),vann:preferredNamespaceUri(string), and at least oneskos:hasTopConcept.- Every
skos:Conceptmust have askos:prefLabelwith a unique language tag. - All label/note properties (
prefLabel,altLabel,definition,scopeNote,note,example…) must carry language tags. - All SKOS relational properties (
broader,narrower,related,inScheme,topConceptOf…) must point to the correct target class.
Project-specific additions:
foaf:depictionmust be an IRI, not a literal.skos:broader/skos:narrowertargets must beskos:Concepts.- A concept may not be its own
skos:broader(no reflexive hierarchy). skos:Collectionobjects (if any are ever added) must have at least one member. Currently unused because Collections are intentionally omitted from the model, but the constraint remains active as a safety net.
The upstream skohub_shacl.ttl had four small Turtle-syntax typos
(sh:message: / sh:severity: with a stray colon). These were corrected
in this repo's copy so pyshacl actually evaluates the affected constraints.
This CSV defines cross-class relations between concepts via an edgelabel
column. Each label maps to RDF as follows:
edgelabel value |
Modelled as |
|---|---|
has tradition |
skos:broader (Potform or Service → Tradition) |
has generic form |
skos:broader (Potform or Generic → Generic) |
has service member |
skos:related (Potform ↔ Service, symmetric) |
has publisher |
skipped — already modelled from potforms CSV |
is same form as |
skos:exactMatch (Potform ↔ Potform, symmetric) |
has same rim as |
skos:related + lado:hasSameRim |
has same footring as |
skos:related + lado:hasSameFootring |
has same roulette as |
skos:related + lado:hasSameRoulette |
has same groove as |
skos:related + lado:hasSameGroove |
has same flute as |
skos:related + lado:hasSameFlute |
The lado:* sub-properties preserve the specific feature semantics while
keeping skos:related present for SKOS-aware tools that don't know LADO.
LADO = Linked Archaeological Data Ontology — see http://www.w3id.org/lado/.
The edge mapping lives in config.yaml under edge_mapping: — you can add
or change labels there without touching Python code.
The run logs and the Markdown report flag:
- Unresolved rows —
id_fromlookupformorid_tolookupformthat don't match any entry in the lookup tables (data quality issue in the source DB) - Possibly reversed Generic→Generic edges — heuristic: if the from
label is a prefix of the to label (e.g.
Cup → Cup Rouletted), the edge may be inverted in the source CSV
These are informational — the build still succeeds, and SHACL validation runs regardless. Review the Markdown report to decide if the source data needs correcting.
Abbreviations used in skos:notation and skos:altLabel live in a Python
module, not a CSV — this keeps them versioned together with the code and
avoids an extra editable file per environment.
Each entry is tagged with a comment:
# CONFIRMED— the abbreviation matches the actual label prefix in the source data (e.g.Conspectus→Consp.because all 310 Conspectus labels in the source already start with"Consp.")# GUESSED— Claude's best guess from archaeological literature. These affect potforms whose labels are bare numbers. Review and correct in place.
To change an abbreviation: edit PUBLISHER_ABBREVIATIONS in
py/abbreviations.py and rebuild. Flipping # GUESSED → # CONFIRMED
when you verify a value is encouraged.
The build is smart about duplicates: if a potform's prefLabel already
starts with the abbreviation (or the full publisher name), no extra
skos:altLabel is emitted — just a skos:notation equal to the label.
Some edges in v_ceratyont_connections.csv of type "has generic form"
go between two generic concepts (e.g. Cup Decorated → Cup) to express a
sub-categorisation. A handful look reversed in the source — for instance
Cup → Cup Rouletted, where the from label (Cup) is more generic than
the to label (Cup Rouletted), suggesting the edge direction is wrong.
run.py detects these with a simple heuristic ("the from-label is a
prefix of the to-label") and either:
- flips them (default) — emitting
Cup Rouletted → skos:broader → Cupwith askos:historyNoteon the flipped concept documenting the change, or - flags them — if you set
build.auto_flip_suspicious_generic_edges: falseinconfig.yaml, they stay as-is and are listed in the Markdown report for manual review.
A safety limit (build.auto_flip_max, default 5) aborts the build if
the heuristic would suddenly match too many edges — protects you from
silent schema damage if the source data shape changes.
publisher = "NULL"inv_ceratyont_potforms_distinct.csv→ noskos:broader; askos:noterecords "publisher: unknown (NULL in source data)".image = "NULL"→ nofoaf:depictiontriple.- Unknown publisher names (referenced in potforms but missing from
tbllookuppublisher.csv) → warning in log, recorded asskos:note.
Everything tweakable lives in py/config.yaml — no Python edits needed:
- Base URI, scheme IRI, image base URL
- Language tag
- CSV column names (if schema evolves)
- IRI prefixes (e.g.
potform_→pf_) - Facet concept local names, labels, and definitions (under
facets:) concept_metadata:— creator IRI (ORCID) + name for every concept, and whether to derivedct:createdfrom CSV mtimesdefinition_templates:— per-kind placeholder templates forskos:definition(uses{label}as placeholder)scope_note_templates:— per-kind templates forskos:scopeNote, describing when to use each kind of concept (as opposed to what it is)build:— auto-flip behaviour, notation toggle, safety limits- List of SHACL shape files (merged into one graph before validation)
- Concept-scheme metadata (title, description, license, dates, version, preferred namespace URI + prefix, source ontology link)
pandas>=2.0
PyYAML>=6.0
rdflib>=7.0
pyshacl>=0.25
The remaining CeraTyOnt relations (hasSameRim, hasSameFootring,
partiallyCoincidentWith, generalisedAs → GenericPotform, hasType →
Tradition, …) will be added once the corresponding mapping CSVs are provided.