SPARQL ORM for Python — sessions, queries, and graph persistence on RDF stores
Project description
SPARQLModel
The SQLModel of SPARQL — Pydantic v2 entity models mapped to RDF, a persistent session, and Python filters that compile to SPARQL.
Build knowledge-graph and metadata apps with typed SPARQLModel classes, with SPARQLSession() as session:, and ORM-style put, get, nested relationships, and a query builder — on in-memory graphs or remote SPARQL 1.1 endpoints. Same validation ergonomics as FastAPI and SQLModel: invalid data fails at construction and on load, before bad triples reach the store.
Requires Python 3.10+ · Built on TripleModel 0.12+ + pyoxigraph · Changelog (0.13.0)
Features
| Area | What you get |
|---|---|
| Models | SPARQLModel, Field, Relationship, IRI — Pydantic v2 validation (model_validate, constraints, extra="forbid") |
| RDF mapping | rdf_type, compact predicates, TripleModel sync_to_graph / from_graph under the hood |
| Session | add, put, delete, get, identity map, flush / pending queue (sync and async since 0.6) |
| Queries | session.query(Person).where(Person.name == "x") → SPARQL (&, |, in_, comparisons, multi-hop) |
| RDF modeling | Multi-valued fields, LangString / MultiLangString, ResourceRef, back_populates, polymorphic query, not_, VALUES, property paths (0.13) |
| Stores | MemoryStore / AsyncMemoryStore; HttpStore / AsyncHttpStore for Fuseki/Jena ([http]); GSP sync_mirror() (0.12) |
| FastAPI | SessionDep or AsyncSessionDep, lifespan helpers, Turtle/JSON-LD responses |
| Cascade | Composition on put/delete; Relationship(..., cascade=False) for references |
Install
pip install sparqlmodel
pip install "sparqlmodel[http]" # HttpStore + AsyncHttpStore (httpx)
pip install "sparqlmodel[fastapi]" # FastAPI session + RDF responses
pip install -e ".[dev,http,fastapi]" # development (includes pytest-asyncio for async tests)
For local development with uv, sync dev extras so async tests run: uv sync --extra dev.
Quickstart
from sparqlmodel import Field, IRI, Relationship, SPARQLModel, SPARQLSession
class Organization(SPARQLModel):
rdf_type = "schema:Organization"
__prefixes__ = {"schema": "https://schema.org/"}
id: IRI
name: str = Field("schema:name")
class Person(SPARQLModel):
rdf_type = "schema:Person"
__prefixes__ = {"schema": "https://schema.org/"}
id: IRI
name: str = Field("schema:name")
works_for: Organization | None = Relationship(
"schema:worksFor", model=Organization
)
acme = Organization(id=IRI("urn:org:acme"), name="Acme Corp")
odos = Person(id=IRI("urn:person:odos"), name="Odos", works_for=acme)
with SPARQLSession() as session:
session.put(odos)
found = session.query(Person).where(Person.name == "Odos").first()
team = session.query(Person).where(Person.works_for.name == "Acme Corp").all()
full = session.get(Person, odos.id, depth=1)
Pydantic models
SPARQLModel subclasses pydantic.BaseModel. You get the same advantages as in FastAPI or SQLModel: typed fields, IDE support, and validation on create and on load.
| When | What runs |
|---|---|
Person(...) / API body |
Pydantic validates types and Field constraints |
session.put(model) |
Validated instance → sync_to_graph (0.4+: same SPARQLModel instance subclasses TripleModel) |
session.get / query hydration |
Graph → model_validate → SPARQLModel instance |
# Field forwards pydantic.Field kwargs (min_length, ge, description, …)
class Person(SPARQLModel):
rdf_type = "schema:Person"
__prefixes__ = {"schema": "https://schema.org/"}
id: IRI
name: str = Field("schema:name", min_length=1)
extra="forbid"— unknown fields on a model raise at validation time (safer for APIs).- FastAPI — reuse the same
SPARQLModelclasses for request/response bodies (see FastAPI below). - JSON-LD —
model_dump_jsonld()/model_validate_jsonld()for API dicts (cascade-aware); files and HTTP bodies usemodel.serialize(format="json-ld")orPerson.parse(...).
Details: Models guide · ORM guide
Session
SPARQLSession is the unit of work. Use it as a context manager: flush pending writes on success, roll back the pending queue on error, close HTTP stores when done.
| Method | Purpose |
|---|---|
add(model) |
Append triples (no delete of existing subject data) |
put(model) |
Upsert with cascade and orphan cleanup |
delete(model) |
Remove owned triples for root + composition tree |
get(Model, iri, depth=0) |
Load one resource; depth 0–2 eager-loads relationships |
query(Model).where(...) |
Fluent query; filters compile to SPARQL |
execute(sparql) |
Raw SPARQL SELECT (auto-prefixes when configured) |
flush() / rollback_pending() |
Apply or discard put(..., flush=False) queue |
expire(Model, iri) |
Evict identity map and hydration cache |
Nested SPARQLModel values are composition (cascade on put/delete). Use Relationship(..., cascade=False) or an IRI when the target is owned elsewhere.
Query DSL
with SPARQLSession() as session:
session.query(Person).where(Person.name == "Odos").all()
session.query(Person).where(
(Person.name == "Odos") | (Person.name == "Ada")
).all()
session.query(Person).where(
Person.works_for.located_in.name == "Boston"
).all(depth=2)
session.query(Person).where(Person.name.in_(("Odos", "Ada"))).all()
session.query(Person).where(Person.name != "Other").all()
# pre-0.5.2 inequality (excludes unbound): .use_inequality_for_ne()
Operators: ==, !=, &, |, <, >, <=, >=, .in_(tuple) or .in_(list) (not a bare string — use ("x",) for one value), multi-hop paths (Person.works_for.name), .limit(n), .offset(n), .order_by(field, desc=False), .count(), .is_(None) / .is_not(None) on nullable relationships, .first() always LIMIT 1 (ignores prior .limit() and .offset()), .use_inequality_for_ne(), .use_optional_for_comparisons() (NE semantics toggle; real OPTIONAL blocks are automatic on nullable hops).
Stores
MemoryStore (default) — in-memory triplemodel.Store (pyoxigraph); tests and single-process apps:
with SPARQLSession() as session:
session.put(model)
HttpStore — SPARQL 1.1 over HTTP with a local mirror for get and cascade (sparqlmodel[http]):
from sparqlmodel import HttpStore, SPARQLSession
with SPARQLSession(
store=HttpStore(
"http://localhost:3030/ds/sparql",
graph_store_url="http://localhost:3030/ds/data",
)
) as session:
session.put(odos)
query / execute use the remote endpoint; get and cascade read the mirror updated by this store’s writes. See the production guide for mirror semantics and deployment notes.
FastAPI
Per-request sessions with a shared store — same pattern as SQLModel + SQLAlchemy:
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Request
from sparqlmodel import IRI
from sparqlmodel.fastapi import SessionDep, http_store_lifespan, negotiated_response
@asynccontextmanager
async def lifespan(app: FastAPI):
async with http_store_lifespan(app, "http://localhost:3030/ds/sparql"):
yield
app = FastAPI(lifespan=lifespan)
@app.get("/person/{iri}")
def person(iri: str, request: Request, session: SessionDep) -> object:
model = session.get(Person, IRI(iri))
if model is None:
raise HTTPException(status_code=404)
return negotiated_response(request, model)
Export
print(odos.serialize(format="turtle"))
# or backward-compatible wrappers:
from sparqlmodel.serializers import export_model
print(export_model(odos, format="turtle"))
File parse/serialize is implemented by TripleModel (parse, serialize, load_graph). See the roadmap.
Documentation
| Guide | Description |
|---|---|
| Read the Docs | Full site: install, guides, API reference, troubleshooting |
| Getting started | Quickstart and first session |
| Guides | Models (Pydantic), sessions, queries, FastAPI |
| Real-world examples | Nobel, DCAT, Wikidata, Schema.org (examples/realworld/) |
| ORM guide | Lifecycle, cascade, hydration, when to use SparqlModel vs TripleModel |
| Technical specification | Normative API; production checklist |
| Production guide | HttpStore, sessions, deployment |
| Roadmap | 0.5–0.15 milestones; SQLModel parity |
| Project plan | Vision and release strategy |
| Ecosystem | SparqlModel vs TripleModel boundaries |
Known limitations (0.13.0)
list[SPARQLModel]embed collections are not supported (TripleModel); useset[IRI]/set[ResourceRef]withRelationship(..., model=...)for multi-ref- Multi-valued scalars and refs (
set/list),LangString/MultiLangString, polymorphic query, property paths, andnot_()— see Models and Queries guides - Prefer
putoveraddfor upserts (stale literal cleanup) HttpStore/AsyncHttpStore: defaultmirror_mode="writer"pulls only when a subject is missing from the mirror; usemirror_mode="remote_authoritative"(0.10+),pull_subjects_into_mirror, orsync_mirror()(0.12+, requiresgraph_store_url) when reads must match remote updates. Retries and batched UPDATE: PRODUCTION; GSP mirror sync: PRODUCTION- Use
merge/refresh/expungefor explicit identity-map control (sessions guide) session.graphis atriplemodel.Store(pyoxigraph), not an rdflibGraph— use TripleModel I/O for file round-trip- Default
!=uses NOT EXISTS (includes resources with no value);.use_inequality_for_ne()on nullable hops also treats missing links as matching ==,<,>, andin_on optional paths still exclude unbound values (SPARQL-native)- Nullable relationship filters use
OPTIONALhops; required (non-nullable) hops still use inner-join semantics - Sessions are not thread-safe; one session per request/task
- Each model field must map to a unique RDF predicate; duplicate predicates raise
ConfigurationErrorat class definition - Cyclic embedded models raise
ConfigurationErroronput/model_to_graph - Shared embedded resources referenced from multiple roots are preserved on
putwhen another subject still links to them
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 sparqlmodel-0.13.0.tar.gz.
File metadata
- Download URL: sparqlmodel-0.13.0.tar.gz
- Upload date:
- Size: 63.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8c40b220fd88fbafdb657c3130184e382cd16fd4f26b8d03ed07a0c5aac6c477
|
|
| MD5 |
5ee9b95cbca56da6092e806c3554ffe0
|
|
| BLAKE2b-256 |
d7ca09e71efe674a1e8162ce22fabe3e04367c4e89038ababb2fedfced4d45ef
|
File details
Details for the file sparqlmodel-0.13.0-py3-none-any.whl.
File metadata
- Download URL: sparqlmodel-0.13.0-py3-none-any.whl
- Upload date:
- Size: 69.1 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 |
451f39bfff0853b2667e87699b5bea5890f36ca0605d86d97f1707b540d8ce2a
|
|
| MD5 |
8337087f5a83e8119ce02d6897071aff
|
|
| BLAKE2b-256 |
2bc085dba01f686a2b31bb2e44bc4219cc6a25c3f0e309fc9ead945b77ea0055
|