Mash features a simple RDF ORM which allows you to extract data from RDF graphs into Pydantic BaseModel classes, and to serialize BaseModel classes into RDF graphs. Using the SemanticModel class and Predicate annotation you can define your own rich metadata classes - either based on existing standards or your own models.
from typing import Annotated, ClassVar
from mash import models, serialize, Predicate as P
class Person(models.SemanticModel):
_type: ClassVar = "foaf:Person"
name: Annotated[str, P("foaf:name")]
mail: Annotated[str | None, P("foaf:mbox")] = None
friends: Annotated[list["Person"], P("mash:friends")] = []
christopher_robin = Person(identifier="cr", name="Christopher Robin")
winnie = Person(identifier="wp", name="Winnie The Pooh", mail="Winnie@hundredacres.com", friends=[christopher_robin])
turtle_text = serialize(winnie)
print(turtle_text)
<urn:uuid:cr> a foaf:Person ;
foaf:name "Christopher Robin" .
<urn:uuid:wp> a foaf:Person ;
mash:friends <urn:uuid:cr> ;
foaf:mbox "Winnie@hundredacres.com" ;
foaf:name "Winnie The Pooh" .
RDF allows you to specify languages in multiple, mash supports this feature through the LangStr type.
from typing import Annotated, ClassVar
import mash
class Product(mash.SemanticModel):
_type: ClassVar = "mash:Product"
description: Annotated[mash.LangStr, mash.Predicate("mash:description")]
english_product = Product(identifier="urn:test", description="This is a string")
norwegian_product = Product(identifier="urn:test", description="Dette er en streng")
with mash.use_language("en"):
m = mash.serialize(english_product, as_model=True)
with mash.use_language("no"):
mash.serialize(norwegian_product, as_model=True, add_to=m)
with mash.use_language("en"):
result = mash.instantiate(m, model=Product)[0]
assert result.description == english_product.description
with mash.use_language("no"):
result = mash.instantiate(m, model=Product)[0]
assert result.description == norwegian_product.description
Note that one has to keep track of the models’ language oneself for now.
Normally in RDF some relations might go in directions that are not pracitical. If you want everything attached to one class rather than to point in all directions from different classes, you can achieve this with ReverseRelations.
Below you will find an example from the Norwegian power grid, where the Substation class has no references to VoltageLevel or SubstationPart, but where those classes contain references to Substation. In this situation it is way more pratical to put all the references on the Substation object.
from typing import Annotated, ClassVar
import mash
from mash import Predicate as P
from mash import ReverseRelation
from tests.grid import get_latest_nemo
from tests.grid.profiles.nemo import IdentifiedObject, NemoProfile, SubstationPart, VoltageLevel
class SubstationEmbed(IdentifiedObject):
_type: ClassVar = "cim:Substation"
substation_kind: Annotated[list[str], P("elb:Substation.substationKind")]
substation_parts: Annotated[list[SubstationPart], ReverseRelation("elb:SubstationPart.Substation")]
voltage_levels: Annotated[list[VoltageLevel], ReverseRelation("cim:VoltageLevel.Substation")]
latest_nemo = get_latest_nemo()
with NemoProfile:
m = mash.model_from_dataset(latest_nemo)
substations = mash.instantiate(m, model=SubstationEmbed)
print(substations[0:2]) # -> [SubstationEmbed(identifier='urn:uuid:00038402-ddee-....
Recommended next read: