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, andREGISTRY;enums.py, when the ontology has a punned enumeration hierarchy;_shapes.ttl: the exact graph used for compilation, whichvalidate()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 |
|---|---|
|
Installed Python package that provides the base classes |
|
Export a node shape as a property schema |
|
Explicit Python name for a class or field |
|
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 |
|---|---|
|
class |
|
Python inheritance, so |
|
|
property with |
attribute on the domain class |
|
required keyword argument |
|
single-valued attribute; otherwise a collection |
|
type annotation and runtime check |
|
required value; for a collection, a required member |
|
replaceable default on new objects |
|
allowed choices |
|
inverse kept in sync |
|
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 asinletorfeed_water;an inlet, outlet, or bidirectional
sh:class;an exact
sh:hasValuemedium 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:qualifiedValueShapeIRI;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 |
|---|---|
|
accepts a Python number |
|
default value; otherwise required |
|
checked on construction |
|
unit for bare numbers |
|
allowed units for a |
|
fixed unit |
fixed |
preserved, and distinguishes fields with the same quantity kind |
|
shown by |
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_Unitstays as it is.Properties use the snake-case local name:
hasPointbecomeshas_point.A few S223 point predicates have shorter names:
hasMediumismedium,isConnectionPointOfisowner,connectsThroughisconnection, andpairedConnectionPointispaired_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:Pumpis WaTr’s, and S223’s isPump_2.Enumeration values are under
enums.valueswith flat names (Fluid-WaterbecomesFluid_Water), and under their branch for completion, such asenums.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.