Graph algorithms and RDFS

TripleModel adds thin wrappers over pyoxigraph-backed graph operations for testing, subgraph extraction, RDFS-aware dispatch, and batch reference hydration. These helpers are not a reasoner or ORM — they complement from_graph / graph_to_model_dispatch.

When to use which helper

Goal

Helper

Assert two graphs match in tests

graphs_equal, graph_diff

Compare two model instances after sync

model_diff

Extract a resource’s bounded description

cbd_graph, cbd_model, Person.cbd(...)

Pick Agent vs Person when rdfs:subClassOf is in the graph

graph_to_model_dispatch (subclass-aware resolve_model_class)

Batch-load shared ref targets (e.g. countries)

hydrate_refs, model_join

Walk rdfs:subClassOf or transitive predicates

subclass_uris, transitive_objects, transitive_subjects

Central prefix + type registry

VocabularyRegistry

Subclass / inverse hints from OWL TTL or static maps

OntologyRegistry, apply_hints_to_model

Graph comparison

from triplemodel import graphs_equal, graph_diff, model_diff

assert graphs_equal(g1, g2)
diff = graph_diff(g1, g2)
assert not diff.only_in_a and not diff.only_in_b

changes = model_diff(alice, bob, graph=g)  # optional predicate-level diff

graphs_equal(..., normalize_bnodes=True) skolemizes blank nodes before isomorphism checks — useful when graphs were built separately. For graphs parsed in two parse() calls, blank-node identity will not match; see Working with graphs and Safe graph merge below.

Concise bounded description (CBD)

CBD returns the predicate closure around a subject. TripleModel exposes it for import:

from triplemodel import TripleModel, cbd_model

person = cbd_model(Person, graph, subject_uri)
# or
person = Person.cbd(graph, subject_uri)

CBD returns a subgraph, not a fully flattened tree. Nested TripleModel fields still hydrate via embed / ref_field when you call from_graph on the CBD graph. For a Python object graph, prefer Rdf.embed = "bnode" (see Nested models).

Run examples/exit_criteria_07.py for CBD + subclass dispatch.

RDFS subclass dispatch

Register both superclass and subclass models. When a subject is typed with a subclass IRI, dispatch picks the most specific registered class using rdfs:subClassOf in the graph:

from triplemodel import graph_to_model_dispatch, register_rdf_resource

register_rdf_resource(Person)
register_rdf_resource(Agent)

agent = graph_to_model_dispatch(graph, alice_uri)

Rdf.resolve_subclass (default True on RdfConfig) controls this behavior when resolve_model_class() is called without use_subclass=. Disable per class with class Rdf: resolve_subclass = False, or pass use_subclass=False to resolve_model_class_with_rdfs for exact rdf:type matching only.

Bulk loading via all_from_graph_dispatch() uses the same resolution rules as single-subject graph_to_model_dispatch().

Ontology hints (OntologyRegistry)

For SparqlModel-style subclass and inverse metadata without a full reasoner, load a small OWL/RDFS file or register hints statically:

from triplemodel import OntologyRegistry, apply_hints_to_model

reg = OntologyRegistry.from_ttl("ontology.ttl")
assert reg.subtypes_of("http://example.org/Animal")  # includes Dog, etc.
assert reg.inverse_of("http://example.org/hasPart") == "http://example.org/partOf"

reg.register_subclasses("http://example.org/Animal", ["http://example.org/Dog"])
reg.register_inverse("http://example.org/hasPart", "http://example.org/partOf")

apply_hints_to_model(MyModel, reg, mutate=True)  # sets inverse= on fields when unambiguous

API

Direction on rdfs:subClassOf

OntologyRegistry.subtypes_of(type_uri)

Descendants (subtypes / subclasses of type_uri)

subclass_uris(graph, type_uri)

Ancestors (superclasses of type_uri) in a data graph

Use graph-backed subclass_uris when the instance graph carries rdfs:subClassOf axioms; use OntologyRegistry when you ship a separate ontology file or maintain a static map.

Batch reference hydration

ref_field hydrates one nested model per instance per reference — correct but slow when many rows share the same country URI. After a lightweight import:

from triplemodel import hydrate_refs

cities = City.all_from_graph(graph)  # partial country refs
cities = hydrate_refs(cities, graph, "country")

For ResourceRef fields, pass spec={"country": Country}. model_join(cities, graph, {"country": Country}) is an alias.

See examples/realworld/wikidata_capitals.py for the Wikidata capitals pattern.

Transitive import (optional)

Mark a set field as transitive to expand multi-hop object URIs on import only (export still writes direct edges):

from triplemodel import rdf_field, Transitive
from typing import Annotated

parts: set[str] = rdf_field("ex:partOf", default_factory=set, transitive=True)
# or Annotated[set[str], Transitive()]

Vocabulary registry

from triplemodel import VocabularyRegistry

reg = VocabularyRegistry()
reg.register(Person)
reg.register(Organization)
reg.bind_vocab(graph)
cls = reg.model_for_subject(graph, subject)

This wraps register_rdf_resource and the internal type_uri index for documented multi-class setups.

Safe graph merge

merge_graphs combines triples as-is. Blank nodes from separate parse() calls are different nodes even when structurally identical. Prefer:

  • One parse() / one load_graph for combined data, or

  • Skolemization / stable blank-node policy via Rdf.blank_node_policy, or

  • graphs_equal(..., normalize_bnodes=True) only for testing.

Details: Working with graphs.

Catalog patterns (cookbook)

No new APIs — reuse existing examples/realworld/:

Pattern

Example

DCAT portal graphs

examples/realworld/dcat_data_catalog.py

Schema.org NGO registry

examples/realworld/schema_org_ngos.py

Nobel / biographical LOD

examples/realworld/nobel_laureates.py

See Real-world patterns (0.4.1) for load_models, ref_field, and Wikidata typing.

OWL/RDFS codegen (0.8, experimental)

Experimental codegen (OWL/RDFS → stub TripleModel classes) shipped in 0.8 — see Stores, scale, and strict import and Codegen (experimental).