Skip to main content

SPARQL ORM for Python — sessions, queries, and graph persistence on RDF stores

Project description

SPARQLModel

PyPI version Python Documentation License: MIT

The SQLModel of SPARQLPydantic 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.10 + pyoxigraph · Changelog (0.6.0)


Features

Area What you get
Models SPARQLModel, Field, Relationship, IRIPydantic 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)
Stores MemoryStore / AsyncMemoryStore; HttpStore / AsyncHttpStore for Fuseki/Jena ([http])
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

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_validateSPARQLModel 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 SPARQLModel classes for request/response bodies (see FastAPI below).
  • JSON-LDmodel_dump_jsonld() / model_validate_jsonld() on each model (serializers delegate to TripleModel; prefer TripleModel load_graph / dump_graph for file I/O).

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) (also accepts lists), multi-hop paths (Person.works_for.name), .limit(n), .use_inequality_for_ne(), .use_optional_for_comparisons().


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

from sparqlmodel.serializers import export_model

print(export_model(odos, format="turtle"))

Long term, file I/O moves to TripleModel parse / serialize; 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
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–1.3 milestones; SQLModel parity
Project plan Vision and release strategy
Ecosystem SparqlModel vs TripleModel boundaries

Known limitations (0.6.0)

  • Multi-valued predicates: first value per predicate on load; prefer put over add for upserts
  • HttpStore / AsyncHttpStore: mirror may lag behind the remote dataset for get / cascade; query().all() / .first() only hydrate IRIs present in the mirror (production mirror sync planned 1.0)
  • Query: limit only — offset / order_by / count planned 0.7–0.8 (roadmap)
  • session.graph is a triplemodel.Store (pyoxigraph), not an rdflib Graph — use TripleModel I/O for file round-trip
  • Default != uses NOT EXISTS (includes resources with no value); use .use_inequality_for_ne() for pre-0.5.2 inequality semantics
  • Multi-hop != still requires relationship hops (inner-join); missing works_for is not treated as “null name”
  • Sessions are not thread-safe; one session per request/task
  • Each model field must map to a unique RDF predicate; duplicate predicates raise ConfigurationError at class definition
  • Cyclic embedded models raise ConfigurationError on put / model_to_graph
  • Shared embedded resources referenced from multiple roots are preserved on put when another subject still links to them

License

MIT — see LICENSE.

Project details


Download files

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

Source Distribution

sparqlmodel-0.6.0.tar.gz (43.9 kB view details)

Uploaded Source

Built Distribution

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

sparqlmodel-0.6.0-py3-none-any.whl (51.9 kB view details)

Uploaded Python 3

File details

Details for the file sparqlmodel-0.6.0.tar.gz.

File metadata

  • Download URL: sparqlmodel-0.6.0.tar.gz
  • Upload date:
  • Size: 43.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for sparqlmodel-0.6.0.tar.gz
Algorithm Hash digest
SHA256 f0fcb02b02a8aebb805be2afa32aff8bddf6309923bb51579fa311a16b6bd187
MD5 a065a94989e221e532c73f9cc511fcd5
BLAKE2b-256 ad4e197b1b07d79404fcd3a073393e1be279023ec0d4b8662ce1d785f55b479c

See more details on using hashes here.

File details

Details for the file sparqlmodel-0.6.0-py3-none-any.whl.

File metadata

  • Download URL: sparqlmodel-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 51.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for sparqlmodel-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f31dcd9b2b399e597544574f8d99d0f831da82bf197af90e9c891f62197511b1
MD5 149480cbde3ce4f930f58a0d0e4d45f4
BLAKE2b-256 2202964f1a88545ef143b93b57d5ec7701c6774f58519105a69eed697ddc93a3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page