Skip to main content

SPARQL Endpoint Wrapper

PyPI CI

ghcr size

SPARQL Endpoint interface, inspired by SPARQLWrapper.

  • One entry point, typed results. execute() reads the query form and answers with SelectResult, AskResult, GraphResult or UpdateResult -- a union you match on, or narrow up front with expect=. Rows hold Uri, BlankNode, Literal or QuotedTriple, not nested dicts, and literals decode their own xsd datatype on request.
  • t-strings, parsed here. execute(t"...") renders each interpolated value as a complete RDF term and then proves it still occupies one token, so a value cannot end a literal early or start a comment. Queries are tokenized against the SPARQL 1.1 grammar before they are sent.
  • Typed errors. Every HTTP status the SPARQL protocol gives meaning to has its own exception, carrying the endpoint's own parser message.
  • Fast. One httpx connection pool per client, asyncio support, and no RDF parsing you did not ask for.
  • The query decides. rqw routes updates to the update endpoint on their own, picks GET or POST by URL length, and refuses a result format the query form cannot answer in before a single byte goes out.

Installation

# mise via github release
mise use -g github:eggplants/rqw

# mise via pipx
mise use -g pipx:rqw

# pipx
pipx install rqw

# pip
pip install rqw

Docker

docker pull ghcr.io/eggplants/rqw

docker run --rm ghcr.io/eggplants/rqw -Q 'SELECT * WHERE { ?s ?p ?o } LIMIT 1'

CLI

$ rqw -Q 'SELECT ?s WHERE { ?s a <http://dbpedia.org/ontology/Fish> } LIMIT 3' --table
s
--------------------------------------
http://dbpedia.org/resource/Actinopoda
http://dbpedia.org/resource/Alfonsino
http://dbpedia.org/resource/Amberjack

$ rqw -e https://query.wikidata.org/sparql -f query.rq -F csv

$ echo 'ASK { ?s ?p ?o }' | rqw -f -
{
  "head": {},
  "boolean": true
}

--help lists the rest: -F/--format, -m/--method, -a/--auth, -g/--default-graph, -H/--header, -P/--param, -t/--timeout.

Library

from rqw import SelectResult, SparqlClient

with SparqlClient("https://dbpedia.org/sparql") as sparql:
    match sparql.execute("""
        SELECT ?fish ?name WHERE {
          ?fish a <http://dbpedia.org/ontology/Fish> ; rdfs:label ?name .
          FILTER (lang(?name) = 'en')
        } LIMIT 5
    """):
        case SelectResult() as rows:
            for row in rows:
                print(row["fish"], row["name"])

execute() is the only way in. It reads the query form off the query itself and answers with the member of QueryResult that form calls for:

query form result
SELECT SelectResult a sequence of rows
ASK AskResult .value, and truthy on its own
CONSTRUCT, DESCRIBE GraphResult the serialized graph, .text or .data
INSERT, DELETE, DROP, ... UpdateResult .status, POSTed to the update endpoint
match sparql.execute(query):
    case SelectResult() as rows:
        print(len(rows))
    case AskResult(value=answer):
        print(answer)
    case GraphResult() as graph:
        print(graph.text)
    case UpdateResult(status=status):
        print(status)

Rows are mappings from variable name to term; a variable the query left unbound is simply absent, so "name" in row is the way to ask. Every term prints as itself, so str(row["fish"]) always works -- but a term is one of four things, and reaching past str means saying which:

from rqw import Literal, Uri

match row["name"]:
    case Literal(value=value, language="en"):
        ...
    case Uri(value=uri):
        ...

match row["count"]:
    case Literal() as count:
        count.as_python()  # 42, from an xsd:integer literal

t-strings

execute takes a t"..." template. Values interpolated into it are rendered as RDF terms -- quoted, escaped and typed -- never as query text:

from rqw import Uri

subject, name = Uri("http://example.org/a"), 'O"Brien'
sparql.execute(t"SELECT ?s WHERE {{ {subject} <http://e/name> {name} }}")
# sent as: SELECT ?s WHERE { <http://example.org/a> <http://e/name> "O\"Brien" }

SPARQL braces have to be doubled, because { is how a template marks an interpolation. That is the one piece of friction and there is no way around it: it is Python's own syntax.

Python SPARQL
Uri, BlankNode, Literal, QuotedTriple the term itself
str a quoted literal, "..."
bool true / false
int, float, Decimal a numeric literal
date, time, datetime "..."^^xsd:...
a list or tuple its items, space separated -- what VALUES wants
a nested t"..." spliced in, values and all

A str becomes a literal, never an IRI, because that is the safe way round. Ask for the other reading explicitly with a format spec: {value:iri} for an IRI, {value:var} for a variable.

The safety does not rest on the escaping alone. After rendering, rqw tokenizes the whole query and checks that every value it spliced in still lines up with a token boundary, so a value that tried to break out of a literal is refused rather than sent:

evil = '" } INSERT DATA { <a> <b> "pwned'
sparql.execute(t"SELECT ?s WHERE {{ ?s ?p {evil} }}")
# one literal, no second operation:
#   SELECT ?s WHERE { ?s ?p "\" } INSERT DATA { <a> <b> \"pwned" }

sparql.execute(t'SELECT ?s WHERE {{ ?s ?p "{evil}" }}')
# QuerySyntaxError: an interpolated value cannot appear inside a token

rqw.parse gives you the same Query on its own, which execute accepts directly when you want to send one query many times.

Narrowing with expect

expect names the form the query must be written in. The return type follows, and a query of any other form is refused before the request goes out:

rows = sparql.execute(query, expect=QueryForm.SELECT)   # -> SelectResult
answer = sparql.execute(query, expect=QueryForm.ASK)    # -> AskResult
graph = sparql.execute(query, expect=QueryForm.DESCRIBE, result_format=GraphFormat.NTRIPLES)

sparql.execute("ASK { ?s ?p ?o }", expect=QueryForm.SELECT)
# QueryFormError: expected a SELECT query, got ASK

What is checked before sending

The query is tokenized against the terminal productions of SPARQL 1.1 <https://www.w3.org/TR/sparql11-query/>, which catches an unterminated literal, a stray character, an unbalanced {}, () or [], a malformed BASE or PREFIX, and text that names no query form. Everything past that -- whether the WHERE clause makes sense, whether a function takes those arguments -- is left to the endpoint, which has to parse the query anyway.

Formats, and taking the body unparsed

CONSTRUCT and DESCRIBE are the only forms that leave the serialization open, since the rest are read out of the SPARQL JSON results format:

sparql.execute("DESCRIBE <http://dbpedia.org/resource/Tuna>", result_format=GraphFormat.NTRIPLES)

raw=True skips parsing entirely and hands back a RawResult -- one type, no union to narrow -- in whatever format you ask for:

csv = sparql.execute(query, result_format=SolutionFormat.CSV, raw=True)
csv.text          # 's,n\r\n...'
turtle = sparql.execute("CONSTRUCT { } WHERE { }", raw=True).data  # feed it to rdflib

Asking for a format a form cannot answer in is a QueryFormError, raised before the request goes out -- result_format=SolutionFormat.CSV without raw=True does not even type-check.

asyncio

AsyncSparqlClient has the same surface with execute awaitable. Queries sent through one client share its connection pool, so gather really does run them at once.

import asyncio

from rqw import AsyncSparqlClient

async def main() -> None:
    async with AsyncSparqlClient("https://dbpedia.org/sparql") as sparql:
        a, b = await asyncio.gather(
            sparql.execute("SELECT * WHERE { ?s ?p ?o } LIMIT 10"),
            sparql.execute("ASK { ?s ?p ?o }"),
        )

asyncio.run(main())

Errors

from rqw import BadQueryError, EndpointError, SparqlHttpError

try:
    sparql.execute("SELECT ?s WHERE {")
except BadQueryError as error:
    print(error.status, error.body)  # 400, the endpoint's own parser message

BadQueryError, UnauthorizedError, ForbiddenError, EndpointNotFoundError, UriTooLongError, UnsupportedMediaTypeError and EndpointError all derive from SparqlHttpError, and everything rqw raises derives from RqwError.

Endpoint settings

SparqlClient(
    "https://example.org/sparql",
    update_endpoint="https://example.org/update",
    default_graph=["http://example.org/g1"],
    method=HttpMethod.POST,          # default: GET while the URL is short enough
    encoding=RequestEncoding.DIRECT, # application/sparql-query, skips form encoding
    params={"timeout": "5000"},      # endpoint-specific knobs
    headers={"X-Trace": "1"},
    auth=("user", "password"),
    timeout=30.0,
)

Passing client=httpx.Client(...) swaps in your own transport, retries and proxies; rqw then leaves closing it to you.

License

MIT License

Release files for rqw 0.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for rqw 0.0.0
File Size Uploaded
rqw-0.0.0.tar.gz 24.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for rqw 0.0.0
File Interpreter ABI Platform
rqw-0.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 53.0 kB

Release files / rqw-0.0.0.tar.gz

Download URL rqw-0.0.0.tar.gz
Size 24.0 kB
Tags Source
SHA-256 checksum
How to use checksums
d817060f6d31756853b8659aca3d8cc6ce49c26b331151d6be732317aea2798f
BLAKE2b-256 checksum
How to use checksums
7e0974df649f4ee96396442a8ae44e04fb8839574b74e1b72b74f39e7674a611
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.8 {"installer":{"name":"uv","version":"0.12.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / rqw-0.0.0-py3-none-any.whl

Download URL rqw-0.0.0-py3-none-any.whl
Size 29.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
579be97e84b53bc966dd04cb635dcb197b9962539ae451b7f6918f4c60a15457
BLAKE2b-256 checksum
How to use checksums
749b1c248858bd0f288960e3f954826d10f96af08944650a0404dde1ed6a70a6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.8 {"installer":{"name":"uv","version":"0.12.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.0.0 This release

2 release 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