mash-core

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.

A basic example

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" .

Language tags

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.

Making data easier to parse with ReverseRelations

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: