Extension compiler#

plumb gen compiles an ontology and its SHACL shapes into a Python package. For a walk-through, see Generate a domain API.

Command line#

# a local ontology file
plumb gen --file pumps.ttl --name Pumps --out pumps

# by IRI: ontoenv resolves and merges the owl:imports closure
plumb gen --iri https://example.org/ontology/pumps --name Pumps --out pumps

# merge extra graphs, such as shapes published separately
plumb gen --file core.ttl --extra shapes.ttl --name Pumps --out pumps

--source records where the ontology came from in the package metadata. The output is a package directory with:

  • __init__.py: the generated classes, property schemas, and REGISTRY;

  • enums.py, when the ontology has a punned enumeration hierarchy;

  • _shapes.ttl: the exact graph used for compilation, which validate() checks against.

Generation is deterministic. Compiling unchanged input gives byte-identical output, so ontology changes show up as reviewable diffs.

Extension annotations#

An extension names the installed package it builds on:

@prefix ex: <urn:example:pumps#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix codegen: <https://watermetadata.org/plumb/codegen#> .

ex: a owl:Ontology ;
    owl:imports <https://watermetadata.org/ontology/watr> ;
    codegen:baseModule "plumb.watr" .

Annotation

Purpose

codegen:baseModule

Installed Python package that provides the base classes

codegen:projection codegen:PropertySchema

Export a node shape as a property schema

codegen:pythonName

Explicit Python name for a class or field

codegen:localName

RDF local name for a property, independent of its Python keyword

Annotations choose names and projections. Requirements stay in SHACL.

Classes and properties#

Ontology

Generated Python

owl:Class / rdfs:Class

class

rdfs:subClassOf

Python inheritance, so isinstance follows subsumption

rdfs:label, skos:definition / rdfs:comment

meta and docstrings

property with rdfs:domain or schema:domainIncludes

attribute on the domain class

sh:minCount >= 1

required keyword argument

sh:maxCount 1

single-valued attribute; otherwise a collection

sh:class, sh:datatype, rdfs:range

type annotation and runtime check

sh:hasValue

required value; for a collection, a required member

sh:defaultValue

replaceable default on new objects

sh:in

allowed choices

owl:inverseOf

inverse kept in sync

owl:equivalentProperty

merged into the ontology’s primary namespace

An extension class has one constructive parent, which may be another class in the same extension. Contradictory bounds, defaults, or inherited choices are reported during compilation. Loading a graph preserves it as written: it does not fill in missing defaults or required values.

Constraints the compiler cannot turn into constructor checks, such as complex paths, still apply at validate() time, because validation uses the original shapes.

Named connection points#

A named point is a qualified shape over s223:hasConnectionPoint with:

  • an sh:name, which becomes the Python attribute, such as inlet or feed_water;

  • an inlet, outlet, or bidirectional sh:class;

  • an exact sh:hasValue medium and equal positive qualified minimum and maximum counts;

  • distinguishing facts, such as a fixed rdfs:label, when several points share a direction and medium.

dw:ClarificationStageShape a sh:NodeShape ;
    sh:targetClass dw:ClarificationStage ;
    sh:property [ sh:path s223:hasConnectionPoint ; sh:name "sludge" ;
        sh:qualifiedMinCount 1 ; sh:qualifiedMaxCount 1 ;
        sh:qualifiedValueShape dw:Sludge ] .
dw:Sludge sh:class s223:OutletConnectionPoint ;
    sh:property [ sh:path s223:hasMedium ; sh:hasValue watr:Fluid-Sludge ],
        [ sh:path rdfs:label ; sh:hasValue "sludge" ] .

One point becomes a PointRef; a group becomes a PointGroupRef. Pass them to connect(), and look up the concrete point with resolved.point(ref). When a graph is loaded, the point’s RDF facts identify it again; if they match more than one point, loading raises an error.

Nested sh:and, sh:or, and sh:xone shapes over connection points are kept as solver constraints. sh:or values on hasMedium become medium domains.

Property schemas#

Mark a node shape with codegen:projection codegen:PropertySchema. Each field is a property shape over s223:hasProperty with:

  • a stable sh:qualifiedValueShape IRI;

  • a qualified minimum and maximum count of one;

  • a readable sh:name, which becomes the field name.

The value shape supplies the property class, the numeric hasValue datatype, the QUDT quantity kind, the unit, and medium, substance, and aspect constraints. Shapes can be reused through sh:node and sh:and.

ex:PressureDesignShape a sh:NodeShape ;
    codegen:projection codegen:PropertySchema ;
    codegen:pythonName "PressureDesign" ;
    sh:property [ sh:path s223:hasProperty ; sh:name "pressure" ;
        sh:qualifiedMinCount 1 ; sh:qualifiedMaxCount 1 ;
        sh:qualifiedValueShape ex:Pressure ] .
ex:Pressure sh:class s223:QuantifiableProperty ;
    sh:property [ sh:path s223:hasValue ; sh:datatype xsd:double ;
                  sh:minCount 1 ; sh:maxCount 1 ; sh:minInclusive 0.0 ],
        [ sh:path qudt:hasQuantityKind ; sh:hasValue kind:Pressure ],
        [ sh:path qudt:hasUnit ; sh:defaultValue unit:PA ;
          sh:in (unit:PA unit:KiloPA unit:BAR) ],
        [ sh:path s223:hasAspect ; sh:hasValue s223:Aspect-Nominal ] .

Shape

Field behaviour

xsd:double, xsd:float, xsd:decimal

accepts a Python number

sh:defaultValue on hasValue

default value; otherwise required

sh:minInclusive, sh:maxInclusive, sh:in

checked on construction

sh:defaultValue on hasUnit

unit for bare numbers

sh:in on hasUnit

allowed units for a QuantityAmount

sh:hasValue on hasUnit with a single unit

fixed unit

fixed rdfs:label

preserved, and distinguishes fields with the same quantity kind

skos:notation

shown by describe(), such as S_NH4

Fields are scalar. Units are checked against the QUDT dimension vector when a value is created, and an unknown compatibility is an error rather than a guess. No unit conversion takes place.

Names#

  • Classes keep their IRI local name: Air_Handling_Unit stays as it is.

  • Properties use the snake-case local name: hasPoint becomes has_point.

  • A few S223 point predicates have shorter names: hasMedium is medium, isConnectionPointOf is owner, connectsThrough is connection, and pairedConnectionPoint is paired_with.

  • Names that collide with the object API (name, type, label, comment, meta, model, contains, ports, describe_ports) or with Python keywords get a trailing _.

  • Different IRIs with the same name get _2, _3, and so on, in sorted IRI order. A deprecated term is suffixed before a current one. When WaTr and S223 share a name, the WaTr class keeps it: Pump is WaTr’s, and S223’s is Pump_2.

  • Enumeration values are under enums.values with flat names (Fluid-Water becomes Fluid_Water), and under their branch for completion, such as enums.Substance.Medium.Mix.Fluid.Water.

What the compiler does not do#

The compiler projects the constructive part of SHACL into Python. It is not a general SHACL-SPARQL engine. Complex paths, disjunctive layouts outside connection points, and parents that the extension does not link produce diagnostics. Full validation always uses the original shapes, including the constraints the projection leaves out.