Skip to main content

kapps-ogm

An object-graph mapper for RDF. It maps between instance data in a triplestore and pydantic models, in both directions.

What it does

The pydantic model's shape is derived from the ontology, not written by hand. You give it a class IRI; it reads that class's declared properties out of the store — the rdfs:range declarations and OWL restrictions attached to them — and builds the model from what it finds.

That is the whole point. The alternative is a pydantic model written in Python that mirrors an ontology, and mirrors it correctly only until somebody edits one of the two. Nothing detects the drift: the model keeps validating, against a shape the ontology no longer describes. Here there is one source of truth, and it is the store.

Installation

pip install kapps-ogm

Python 3.12 or later. You also need a reachable GraphDB triplestore — this library maps to and from one, it does not embed one. The client is kapps-triplestore-interface, which installs as a dependency; you do not need to install it yourself.

A round-trip example

Write an instance, read it back, and compare. Nothing here names a host: the connection comes from the environment.

import os

from kapps_triplestore_interface import IRI, GraphDB, GraphDBCredentials

from kapps_ogm.ogm import OGM
from kapps_ogm.utils.class_scope import ClassScope

ONTOLOGY = "https://example.org/ontology#"

credentials = GraphDBCredentials(
    base_url=os.environ["GRAPHDB_URL"],
    username=os.environ["GRAPHDB_USERNAME"],
    password=os.environ["GRAPHDB_PASSWORD"],
    repository=os.environ["GRAPHDB_REPOSITORY"],
)
ogm = OGM(db=GraphDB(credentials=credentials))

# How deep to go. An RDF graph has no natural boundary, so the caller states which
# paths to follow out from the root class; everything else is left in the store.
#
# Each chain runs from the root class down to a property you want, so a nested node
# needs its own properties named. `[[hasComponent]]` alone would put an empty scope
# under hasComponent, and a nested class with an empty scope is not hydrated at all --
# hasName and isActive would be unknown properties, dropped with a warning.
class_scope = ClassScope.from_property_chains(
    [
        [IRI(f"{ONTOLOGY}hasComponent"), IRI(f"{ONTOLOGY}hasName")],
        [IRI(f"{ONTOLOGY}hasComponent"), IRI(f"{ONTOLOGY}isActive")],
    ]
)

data = {
    f"{ONTOLOGY}hasComponent": [
        {
            f"{ONTOLOGY}hasName": ["Component A"],
            f"{ONTOLOGY}isActive": [True],
        },
    ],
}

node = ogm.create(
    class_iri=IRI(f"{ONTOLOGY}Assembly"),
    class_scope=class_scope,
    data=data,
    persist=True,
)

fetched = ogm.fetch(
    instance_iri=node.id,
    class_scope=class_scope,
    materialize=True,
)

assert fetched.to_json_ld() == node.to_json_ld()

create returns a Node — the mapped instance, carrying .id and .data. To get the pydantic model itself, call .materialize():

model = fetched.materialize()   # a pydantic BaseModel, validated against the derived shape
print(model.model_dump())

What the ontology has to declare

The mapper reads shapes out of the store, so a class whose properties are undeclared maps to an empty shape — it does not guess. Each property a model should carry needs an rdfs:range, an OWL restriction, or both.

Two rules are worth knowing before you write the TBox:

  • Several rdfs:range assertions on one property are read conjunctively — as their intersection, which is what RDFS entails. A value must satisfy all of them. Declaring two is not an error.
  • Ranges are inherited through rdfs:subPropertyOf. A range declared on a superproperty reaches every subproperty, and the chain is walked transitively. This is not something a reasoner materialises for you; the mapper resolves it explicitly.

Where two declarations genuinely conflict — incompatible datatypes, or a datatype target against a class target — that raises at specify time and names the property, rather than failing later inside pydantic where the ontology cannot be named.

Anonymous nodes

A nested node with no IRI of its own still needs an identity, or a write followed by a read returns a different node than the one written. Such nodes are given a Skolem IRI, minted under a /.well-known/genid/ path as RDF 1.1 Concepts §3.5 provides for, so that a third party reading the graph can tell the IRI stands in for a blank node.

The minting authority is an ontology-governance decision rather than this library's, so the namespace is configurable per instance and the default is a documented placeholder:

ogm = OGM(db=db, skolem_namespace="https://example.org/.well-known/genid/")

The /.well-known/genid/ path itself is not configurable. A namespace without it would mint addresses this library could no longer recognise as Skolem IRIs, which would silently disable the guard rather than fail, so it is rejected at construction.

Contributing

This repository is a publish target. It receives one commit per release, cut from a private development repository, so a pull request opened here has no shared history to merge into and cannot be merged. Please open an issue instead.

License

The package is licensed under the MIT license.

Acknowledgements

This package is developed as part of the INF subproject of the CRC 1574: Circular Factory for the Perpetual Product. This work is therefore supported by the Deutsche Forschungsgemeinschaft (DFG, German Research Foundation) [grant-number: SFB-1574-471687386]

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kapps_ogm-0.2.0.tar.gz (88.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

kapps_ogm-0.2.0-py3-none-any.whl (48.0 kB view details)

Uploaded Python 3

File details

Details for the file kapps_ogm-0.2.0.tar.gz.

File metadata

  • Download URL: kapps_ogm-0.2.0.tar.gz
  • Upload date:
  • Size: 88.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for kapps_ogm-0.2.0.tar.gz
Algorithm Hash digest
SHA256 d56aee045345d43bb9abadf7359c2af38f4f7c51eeabee1c575709765703854a
MD5 0fa7b3e37819fd0fe00955a3322f5f7a
BLAKE2b-256 3ffe7b61b4aa70e21f23d8c4213ab3bac144255ef5447973106bc529b0b59555

See more details on using hashes here.

Provenance

The following attestation bundles were made for kapps_ogm-0.2.0.tar.gz:

Publisher: publish.yml on circularfactory/kapps_ogm

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file kapps_ogm-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: kapps_ogm-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 48.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for kapps_ogm-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 232791f8a53f16f7d5ce03383fd9adb7dbc41a7df767633a8fc2c06de4b2f55a
MD5 a3c1756389dddfb1ee2311b984957f83
BLAKE2b-256 a48258c1fa6351c8b99abc8766e6cbdab530f7dd2f213a9739a81689e3550916

See more details on using hashes here.

Provenance

The following attestation bundles were made for kapps_ogm-0.2.0-py3-none-any.whl:

Publisher: publish.yml on circularfactory/kapps_ogm

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.2

2 files

0.1.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page