Pydantic TripleModel classes ↔ RDF triples via pyoxigraph (beta).
Project description
TripleModel
Typed Pydantic models for RDF. Declare fields once, get correct triples in and out of pyoxigraph Store objects — no manual quad writes for every property.
| Install | pip install triplemodel |
| Import | from triplemodel import TripleModel, rdf_field |
| Docs | triplemodel.readthedocs.io |
Person(slug="alice", name="Alice") → (ex:alice, foaf:name, "Alice") → Person(...)
TripleModel is the mapping layer between Pydantic-shaped domain models and RDF triples: subject IRIs, XSD literals, nested resources, rdf:List, language tags, graph sync, and file parse/serialize. It is stateless (no ORM session); SparqlModel (sessions, SPARQL, ORM) builds on top — see the ecosystem guide.
0.12.0 is beta. Public API is frozen from 0.9 until 1.0 — see API stability, changelog, migration 0.11 (and 0.10 pyoxigraph). 0.12.0 is additive (no migration guide). See roadmap.
Install
pip install triplemodel
Requirements: Python 3.10+, Pydantic 2, pyoxigraph 0.5+.
Quick start
from triplemodel import TripleModel, rdf_field
FOAF = "http://xmlns.com/foaf/0.1/"
class Person(TripleModel):
class Rdf:
namespace = "http://example.org/people/"
type_uri = f"{FOAF}Person"
id_field = "slug"
slug: str
name: str = rdf_field(f"{FOAF}name")
age: int | None = rdf_field(f"{FOAF}age", default=None)
alice = Person(slug="alice", name="Alice", age=30)
graph = alice.to_graph()
assert Person.from_graph(graph, alice.subject_uri()) == alice
print(alice.subject_uri())
http://example.org/people/alice
Unmapped fields are ignored on export/import — useful for computed or application-only data.
Features
| Area | Capability |
|---|---|
| Mapping | Nested class Rdf + rdf_field() or Annotated[..., Predicate(...)]; predicate validation at class definition |
| Identity | Subject IRIs from namespace + id_field (percent-encoded ids); IriId for full-IRI ids |
| Scalars | str, int, float, bool, date, datetime; IRI-like str → URIRef; literal_datatype (e.g. xsd:gYear) |
| Collections | set[T] → multiple objects per predicate; list[T] → ordered rdf:List |
| Literals | LangString, Lang(), OpaqueLiteral, ResourceRef |
| Nesting | Child TripleModel with Rdf.embed "iri" or "bnode"; ref_field for URI foreign keys |
| Typing | Rdf.instance_of for Wikidata-style property classification (wdt:P31, etc.) |
| Graph writes | to_graph / sync_to_graph with add, replace, or patch |
| Namespaces | Rdf.prefixes, CURIE predicates ("foaf:name"), bind_namespaces |
| Named graphs | Rdf.graph_iri, to_dataset / from_dataset, TriG / N-Quads via Dataset |
| File I/O | parse / parse_file / parse_url, serialize, load_graph, load_dataset, load_models, load_models_from_graph, load_models_from_dataset, dump_model (Turtle, TriG, N-Triples, JSON-LD, …) |
| Dispatch | parse(..., dispatch=True), graph_to_model_dispatch, all_from_graph_dispatch by rdf:type |
| Inverse predicates | rdf_field(..., inverse=...) for import; forward predicate on export |
| Package typing | PEP 561 py.typed |
| SPARQL | ask, construct_models, select_models, load_sparql, apply_update, prepare_model_query |
| Graph algorithms | graphs_equal, graph_diff, model_diff, cbd_graph / cbd_model, hydrate_refs, model_join |
| RDFS | Subclass-aware dispatch, transitive_objects / transitive_subjects, VocabularyRegistry, Transitive import |
| Scale & stores | iter_graph_to_models, load_models_streaming, parse_into_store_graph, open_graph (memory / disk) |
| Strict import | Rdf.strict_import, Rdf.warn_unmapped_fields on graph_to_model |
| Plugins | triplemodel.plugins — register_predicate_resolver, literal/resource registration |
| Codegen | Experimental triplemodel-codegen CLI (OWL/RDFS → stub models) |
list vs set
| Annotation | RDF shape |
|---|---|
set[str] |
Multiple objects on one predicate (unordered) |
list[str] |
One ordered rdf:List (rdf:first / rdf:rest) |
Use set for tags or duplicate predicates; use list when the graph should contain a real RDF list.
Full example
Language tags, RDF lists, and nested blank-node embeds:
from typing import Annotated
from triplemodel import TripleModel, rdf_field, sync_to_graph
from triplemodel.terms.lang import Lang, LangString
from triplemodel.vocab import DC, FOAF
class Address(TripleModel):
class Rdf:
namespace = "http://example.org/address/"
type_uri = "http://example.org/Address"
id_field = "slug"
slug: str = "home"
street: str = rdf_field("http://example.org/street")
class Person(TripleModel):
class Rdf:
namespace = "http://example.org/people/"
type_uri = f"{FOAF}Person"
id_field = "slug"
embed = "bnode"
prefixes = {"foaf": str(FOAF), "dc": str(DC)}
slug: str
title: LangString = rdf_field(f"{DC}title")
nick: list[str] = rdf_field("foaf:nick", default_factory=list)
address: Address | None = rdf_field("http://example.org/home", default=None)
person = Person(
slug="alice",
title=LangString("Alice's profile", "en"),
nick=["Al", "Alice"],
address=Address(street="1 Main St"),
)
graph = person.to_graph()
person.nick = ["Alice"]
sync_to_graph(person, graph, mode="replace")
again = Person.from_graph(graph, person.subject_uri())
print(again.nick)
['Alice']
Runnable version: examples/exit_criteria_03.py (same models; see also examples/doc/snippets/).
How mapping works
class Rdf — resource metadata
| Attribute | Role |
|---|---|
namespace |
Base IRI; subject = namespace + encoded id_field value |
type_uri |
rdf:type on export; filter for all_from_graph() |
id_field |
Python field whose value becomes the subject id segment |
embed |
"iri" (default) or "bnode" for nested models |
prefixes |
CURIE map → Graph.bind on new graphs |
graph_mode |
Default to_graph mode when mode= is omitted |
blank_node_policy |
"fresh" or "stable" nested bnodes |
skolemize_export / skolemize_import |
Blank-node skolemization defaults |
base_uri |
Default base IRI for resolving relative IRIs on parse |
jsonld_context |
Reserved for API stability; not applied on pyoxigraph (issues UserWarning) |
Override the subject per call with uri= when the IRI still lives under namespace:
graph = alice.to_graph(uri="http://example.org/people/alice")
Helpers: subject_base(), id_from_subject_uri().
Fields → predicates
name: str = rdf_field("foaf:name") # with Rdf.prefixes
from typing import Annotated
from triplemodel import Predicate
title: Annotated[str, Predicate("http://purl.org/dc/terms/title")]
title_fr: Annotated[str, Predicate(f"{DC}title"), Lang("fr")]
Subclasses inherit a parent Rdf when the child does not define one. A child class Rdf: replaces the parent config entirely — never use an empty nested Rdf on a subclass.
Scalar → RDF (export)
| Python | RDF |
|---|---|
str (plain) |
xsd:string |
str with URI scheme (http:, urn:, …) |
URIRef |
int, float, bool, date, datetime |
XSD literal |
LangString, ResourceRef, OpaqueLiteral |
matching literal / IRI |
Register custom types with register_literal_type and LiteralRegistry.
Updating an existing graph
Default to_graph() uses mode="add" — it only appends. To remove triples when fields are cleared, use sync modes:
from triplemodel import sync_to_graph
sync_to_graph(person, graph, mode="replace") # replace all owned triples for this subject
sync_to_graph(person, graph, mode="patch") # per-predicate replace; lighter touch
replace clears nested IRI children and list heads before re-export; patch updates predicates present in the model and clears empty fields (including on nested resources). See the updating graphs guide.
API overview
Instance & class methods
| Method | Description |
|---|---|
subject_uri(uri=None) |
Subject IRI for this instance |
to_triples(uri=None) |
(subject, predicate, object) rows |
to_graph(graph=None, *, uri=None, mode="add", ...) |
Serialize into a Graph |
sync_to_graph(graph, *, uri=None, mode="replace", ...) |
Sync owned triples in-place on this instance |
from_graph(graph, uri, *, resolver=, registry=, ...) |
Load one resource |
all_from_graph(graph, *, type_uri=None, resolver=, registry=, de_skolemize=, ...) |
Load all resources of this type |
parse / parse_file / parse_url |
Parse RDF and return list[TripleModel] (class methods; optional dispatch=True) |
serialize |
Write instance triples to a file or string |
rdf_config() |
Resolved RdfConfig |
Common imports
from triplemodel import (
TripleModel,
rdf_field,
ref_field,
Predicate,
IriId,
GraphMode,
sync_to_graph,
models_to_graph,
load_graph,
load_models,
load_models_from_graph,
load_models_from_dataset,
load_dataset,
parse_into_dataset,
model_to_dataset,
models_to_dataset,
get_graph_context,
graph_to_model_dispatch,
all_from_graph_dispatch,
ask,
apply_update,
construct_models,
select_models,
load_sparql,
open_sparql_graph,
prepare_model_query,
run_sparql,
init_bindings_from_model,
init_ns_from_model,
graph_from_construct_result,
merge_graphs,
expand_curie,
bind_namespaces,
LangString,
Lang,
ResourceRef,
OpaqueLiteral,
RDF,
XSD,
)
Full API: Read the Docs API reference.
More examples
Batch export
from triplemodel import Store
from triplemodel import models_to_graph
graph = models_to_graph([alice, bob])
models_to_graph([alice, bob], Graph()) # merge into existing graph
Encoded subject ids
bob = Person(slug="bob jones", name="Bob")
print(bob.subject_uri())
http://example.org/people/bob%20jones
Multiple tags on one predicate — use set[str] = rdf_field("foaf:topic", default_factory=set).
Runnable scripts: examples/exit_criteria_03.py, examples/readme_examples.py, examples/realworld/ (Nobel, DCAT, Wikidata, Schema.org), and examples/doc/snippets/.
TripleModel vs SparqlModel
| You need | Package |
|---|---|
Pydantic ↔ triples on an in-memory Store |
triplemodel |
File I/O, datasets, SPARQL sessions, cascade put |
SparqlModel (planned TripleModel dependency) |
Known limitations
- Union queries —
from_datasetreads one named graph only; useDataset.queryfor union semantics (see the datasets guide). - BNode embed is experimental; prefer
embed="iri"for stable linking. - Collections —
list[T]/set[T]require scalarT;list[TripleModel]andset[TripleModel]are not supported. - Inverse predicates — import uses forward or inverse triples (forward wins on conflict);
sync_to_graphinadd,replace, orpatchclears incoming inverse triples before writing forward predicates (including reassignment and dropped nested IRI/bnode children). Export writes forward predicates only. - Discovery —
all_from_graph()and dispatch discovery match subjects via forward owned predicates (andrdf:type/instance_of); resources reachable only through inverse triples are not discovered. - Dispatch parse —
parse(..., dispatch=True)loads every registeredrdf:type(not only the class you call.parseon);type_uri=is ignored whendispatch=True. - Skolemize —
skolemize/de_skolemizeon import or export mutate the entire sharedStore, not only the resource being loaded or synced.sync_to_graph(..., mode="patch")runs skolemize after cleanup and export;replace/addskolemize viawrite_model_addbefore appending new triples. - Remote SPARQL graph —
open_sparql_graphandload_sparqlraiseNotImplementedErrorin 0.10.0; load remote data into a localStore(see SPARQL guide). - BNode embed + fresh policy — with default
blank_node_policy="fresh",replace/patchremove and re-export nested blank nodes on every sync even when the nested value is unchanged; useembed="iri"orblank_node_policy="stable"for stable identities. - BNode subjects are skipped by
all_from_graph()and byparse(..., dispatch=True)/all_from_graph_dispatch(). - Subclass dispatch —
parse(..., dispatch=True)andall_from_graph_dispatch()resolve subjects viaRdf.resolve_subclass(RDFS closure when enabled); unregistered types are omitted without error. - Default add mode does not remove stale triples — use
sync_to_graphormode="replace". sync_to_graph()defaults toreplacewhenRdf.graph_modeis"add"(unliketo_graph(), which defaults to add).
Details: user guides · RDF lists & lang.
Development
git clone https://github.com/eddiethedean/triplemodel.git && cd triplemodel
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev,docs]"
make ci # pytest, lint, docs (matches GitHub Actions)
make release-check # before tagging: examples + twine check
CI: Python 3.10–3.13. Release process: RELEASING.md.
Documentation
| Resource | Link |
|---|---|
| User guides | guides index |
| API reference | api |
| Changelog | CHANGELOG.md |
| Roadmap | docs/ROADMAP.md |
| Ecosystem | docs/ECOSYSTEM.md |
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file triplemodel-0.12.0.tar.gz.
File metadata
- Download URL: triplemodel-0.12.0.tar.gz
- Upload date:
- Size: 222.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4443fe48a45f09a7deae17f5a159ed325756516e686888413b6c456ef601610d
|
|
| MD5 |
de6564c5f6c384c04f5b23175ba8b2ba
|
|
| BLAKE2b-256 |
a3e8acbc46f6087ea622eff748cce4aef69ef2e47a2f5f392569dac98d69e996
|
File details
Details for the file triplemodel-0.12.0-py3-none-any.whl.
File metadata
- Download URL: triplemodel-0.12.0-py3-none-any.whl
- Upload date:
- Size: 113.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2a4a201e3bd361070c035fe33ba3257dd9bf99d8b398a1fc6865eb5c2273ae17
|
|
| MD5 |
e2c5795366c7347bc8b3eca14917113d
|
|
| BLAKE2b-256 |
dd502b878cf98dfb2bac9a24d2e9d13482d8166bbbad4023bb788bc96df8d905
|