Skip to main content

PurRDF for Python

PyPI License: MIT OR Apache-2.0 Python versions

PurRDF is a from-scratch, dependency-light RDF 1.2 engine — parsers and serializers, SPARQL, SHACL, ShEx, RDFC-1.0 canonicalization, and the GTS graph-transport container — written in Rust and carried verbatim into Python, JavaScript, and C (the GTS container reaches Python and C; the JavaScript package does not expose it). The purrdf package is the Python surface of that one engine: the same byte-identical semantics in every language, including triple terms, reifiers, and base-direction literals that most incumbent libraries do not carry.

Install

pip install purrdf

Requires Python 3.13+. Wheels bundle the native extension; no Rust toolchain needed.

Parse RDF

import purrdf

quads = purrdf.parse(
    '<https://example.org/alice> <http://xmlns.com/foaf/0.1/name> "Alice" .',
    purrdf.RdfFormat.TURTLE,
)

purrdf.parse accepts Turtle, TriG, N-Triples, N-Quads, TriX, and HexTuples (purrdf.RdfFormat); JSON-LD and RDF/XML travel through the dedicated purrdf.from_json_ld / purrdf.to_json_ld and purrdf.from_rdf_xml / purrdf.to_rdf_xml converters. All codecs are first-party with byte-deterministic output.

Configured JSON-LD and YAML-LD use one strict versioned options document. Compile a reusable context when serializing several datasets:

import json
import purrdf

options = json.dumps({
    "version": 1,
    "mode": "context",
    "prefixes": {"ex": "https://example.org/", "schema": "https://schema.org/"},
})
context = purrdf.CompiledJsonLdContext(options)
jsonld = purrdf.serialize_jsonld(
    nquads,
    format=purrdf.RdfFormat.N_QUADS,
    output_format="jsonld",
    context=context,
)

expanded, context, and deterministic dataset-IRI derived modes are explicit. PurRDF never infers a caller vocabulary or fetches a remote context.

Project graph, tabular, and research-object carriers

purrdf.project and purrdf.lift are thin calls into the same Rust projection engine used by every other surface. Configuration is mandatory, strict JSON: PurRDF supplies no vocabulary, identity IRI, or resource-limit default.

import json
import purrdf

config = json.dumps({
    "profile": "lpg-csv",
    "config": {
        "rdf_type": "https://example.org/type",
        "scope": {"mode": "all"},
        "limits": {
            "max_artifacts": 16,
            "max_artifact_bytes": 1_000_000,
            "max_total_bytes": 4_000_000,
            "max_archive_bytes": 5_000_000,
            "max_term_depth": 16,
        },
        "execution_limits": {
            "max_input_records": 1_000,
            "max_model_records": 1_000,
            "max_nodes": 1_000,
            "max_edges": 1_000,
        },
    },
})
package = purrdf.project(
    "@prefix ex: <https://example.org/> . ex:alice ex:knows ex:bob .",
    format=purrdf.RdfFormat.TURTLE,
    profile="lpg-csv",
    config=config,
)
lifted = purrdf.lift(package.archive, profile="lpg-csv", config=config)
assert lifted.dataset.quad_count() == 1
print([(loss.code, loss.location) for loss in package.losses])

Project profiles are lpg-csv, neo4j-csv, open-cypher, graphml, csvw-exact, csvw-terms, okf-terms, obo-graphs, skos, croissant-1.1, ro-crate-1.3, datacite-4.6, dcat-3, dcat-rdf, void, and frictionless-data-package-1. Curated CSVW/OKF terms, OBO Graphs, SKOS, native DCAT RDF, and VoID are deliberately write-only, ledgered views. Returned archives are canonical deterministic USTAR bytes and every result carries its always-computed structured loss records. Research-object contexts, vocabularies, identities, and profiles are all mandatory caller configuration.

Native RDF dataset descriptions use the same call. The complete JSON names the output syntax and either a mapped/CONSTRUCT DCAT source or the VoID source graphs, role vocabulary, dataset-prefix registries, and resource bounds:

from pathlib import Path
import purrdf

source = Path("void-source.trig").read_text()
void_config = Path("void.json").read_text()
description = purrdf.project(
    source,
    format=purrdf.RdfFormat.TRIG,
    profile="void",
    config=void_config,
)
Path("void.tar").write_bytes(description.archive)

Portable void-source.trig, void.json, and dcat-rdf.json examples are in crates/rdf/tests/fixtures/dataset-description/.

Attached RO-Crate packaging uses the same call with assets= set to a canonical payload-only USTAR archive and configuration packaging: "attached". The result contains the exact payloads, deterministic metadata, and self-contained preview; missing, unowned, reserved, or size-inconsistent members raise ValueError. See the runnable projection_roundtrip.py file-producing example.

For large LPG carriers, purrdf.project_artifacts(...) invokes a transactional artifact callback with package/artifact begin, bounded chunk, artifact finish, commit, and abort events. An optional progress callback receives immutable ProjectionProgress snapshots; callback exceptions abort the package and are returned unchanged. This path retains the selected canonical LPG model but not complete artifact bodies or USTAR bytes. See the runnable atomic-directory projection_stream.py example.

Validate with SHACL

The SHACL engine lives at purrdf.shapes (mirroring the Rust crate; purrdf.shacl is a back-compat alias):

from purrdf import shapes

report = shapes.validate(shapes_ttl=my_shapes, data_nt=my_data)
print(report["conforms"])

Complete SHACL Core, SHACL-SPARQL constraints/targets, and SHACL-AF sh:rule entailment via shapes.entail(...). Reusable parsed shapes are available as shapes.Shapes(shapes_ttl).validate_nt(data_nt).

Validate with ShEx

from purrdf import shex

results = shex.validate(
    my_schema_shexc,
    my_data_ttl,
    [("https://example.org/alice", "https://example.org/PersonShape")],
)
print(all(entry["conformant"] for entry in results))

The ShEx 2.1 validator passes 1,105/1,105 attempted validation tests of the official shexTest suite (see the repo's docs/CONFORMANCE.md).

Entailment regimes

The SPARQL entailment regimes live at purrdf.entail (mirroring the purrdf-entail Rust crate). It closes a dataset under a regime's own specification rule table and takes no shapes at all — not to be confused with purrdf.shapes.entail(...), which applies the SHACL-AF sh:rules a shapes graph declares.

import purrdf
from purrdf import entail

dataset = purrdf.RdfDataset(my_turtle, purrdf.RdfFormat.TURTLE)
closure, report = entail.materialize(dataset, "rdfs", "")
print(closure.to_nquads())
print(report)

For callers holding a document rather than a parsed dataset, entail.materialize_nt(text, regime, program) takes N-Triples/N-Quads and returns (canonical_nquads, report). Both accept the regime as a plain string ("simple", "rdf", "rdfs", "owl-rl", "owl-direct", "rif", "d") or as entail.Regime.RDFS.

All seven regimes close; none is refused. The third argument is the regime's own rule document. Six regimes take none, so theirs is "" — and a non-empty one raises rather than being silently discarded. "rif" is the exception: it entails under the caller's rules, which PurRDF does not declare, so its program is a normative RIF-in-XML document:

closure, report = entail.materialize(dataset, "rif", my_rif_xml)

"owl-direct" takes no program either, and that is a statement rather than an omission: its extra input is a query's class expressions, and this surface closes a dataset rather than answering a query — so what it runs is the query-independent tableau augmentation (the classification, the realization, the entailed role assertions and the owl:sameAs identifications the tableau decides about the ontology's own named terms).

The report is the second return value and is never optional. It is a byte-stable rendering naming which rules fired and how often, which specification rules did not fire, which constructs the run left at a boundary, what it consumed of the evaluator's fixed ceilings, and the contract hash of the calculus that ran — so a cached closure minted under a different rule set can be refused rather than trusted.

The rule tables are readable directly, so coverage is something you measure rather than something you take on faith:

defined = entail.rules("owl-rl")             # 78 — OWL 2 Profiles §4.3 Tables 4–9
fired = entail.implemented_rules("owl-rl")   # 78
missing = [rule for rule in entail.rules("rdfs") if rule not in entail.implemented_rules("rdfs")]
# [] — RDFS fires 18 of its 18 rules; the gap is legitimately empty
added = entail.extensions("owl-rl")          # ['ext-eq-diff-sym']

extensions(regime) is a third, disjoint inventory: the rules this build fires that no specification table states. owl-rl has one — ext-eq-diff-sym, symmetry of owl:differentFrom, sound and shaped exactly like prp-symp — and every other regime has none. It appears in neither rules() nor implemented_rules() for any regime, so the 78 above is unaffected by it: those two are statements about the specification, and firing a sound rule the table omits does not change what the table says. Asking is a question in its own right rather than something you learn by materializing a dataset and reading the report's extension line — though the report says the same thing, and the two cannot drift apart.

rdfD1, rdfD1a, rdfs14 and rdfs14a are in that fired set and each concludes about a fresh blank node. The restricted chase mints one as a frontier-addressed Skolem witness and closes under it, so the rules genuinely run — but every conclusion mentioning a witness is withheld when the closure is materialized back, because a SPARQL entailment regime draws its answers from the scoping graph and a minted blank node is not in it. The report says so with a boundary surrogate line rather than with a missing rule, and completeness reads exact-within-boundaries rather than exact.

78 / 78 is rule-table coverage, and rule-table coverage is not entailment conformance. The two are measured separately and stating only the first is the overclaim the reasoning report exists to prevent: on this vendored W3C corpus of OWL 2 RL entailment tests this chase reaches 27 of 27 published positive entailments, and agrees with W3C on 23 of 23 negative ones — 3 of those 23 refuted, a decided non-entailment, and 20 admitted, the closure computed and observed not to contain the non-conclusion. Read the negative figure as "no unsoundness found", never as "23 non-entailments proved". Both numbers are true. The full scoreboard, the typed divergence ledger, and every other suite are in docs/CONFORMANCE.md.

ValueError is raised for an unknown regime spelling (the message names the accepted set), for a program that is wrong for the regime — a non-empty one for any regime but "rif", or one "rif" cannot parse as a normative RIF-in-XML document — and for an exhausted evaluation ceiling. An exhausted ceiling is a refusal, never a truncated closure handed back as a complete one. Being "owl-direct" or "rif" is not itself a refusal: both materialize.

Description-Logic reasoning services

Materialization is the chase. The OWL 2 Direct-Semantics reasoner is a second lane on the same module — a SHOIQ(D) hypertableau — and every one of its services is on purrdf.entail. Each takes an N-Triples (or N-Quads) document and returns (answer, certificate) as a tuple, so a caller unpacks the evidence rather than being able to not ask for it:

Service Call Answer
Consistency entail.consistency(data) consistency true / false / unknown — unknown means the tableau reached its step cap, and is never collapsed to false
Classification entail.classify(data) equivalent, subclass (transitively closed), direct (its reduction) and unsatisfiable lines
Realization entail.realize(data) type lines for the named individuals, then the most specific direct-type lines
Instance retrieval entail.instances(data, class_) instance <term> lines; class_ is ONE N-Triples term, angle brackets included
Axiom entailment entail.entails(data, axiom) entails true / false / unknown, then the axiom as it was read, so you can see which kind its predicate selected
Profile certification entail.profile(data) certified <profile> lines, most restrictive first (EL, QL, RL, DL, Full)
Module extraction entail.extract_module(data, signature, method) the locality module as canonical N-Quads; method is "bot", "top" or "star"
Justification entail.justify(data, axiom) a minimal subset of the ontology that still entails axiom, as canonical N-Quads
Proof entail.explain_conclusion(data, regime, conclusion) asserted, steps, and one rule line per rule the derivation cited

Proof terms: opt-in to produce, and a checker to consume

Every call above records nothing and returns a two-tuple. entail.prove is the opt-in: it records the tableau runs a service made — which costs the completion graph of each one — and returns a THREE-tuple whose third element is a purrdf-dl-proof 1 document. The answer and the certificate are byte-identical either way, because recording is an observation the reasoner makes of itself rather than a lever it reads.

Service Call Answer
Prove entail.prove(data, service, argument, step_cap, work_cap) (answer, certificate, proof); the proof is a purrdf-dl-proof 1 block whose header is derived from the term and whose body lines are the term's own canonical bytes as lowercase hex
Check a proof entail.check_proof(data, service, argument, answer, certificate, proof) the purrdf-dl-proof-check 1 report — the digest and input identity it checked, the runs it replayed, and the attested/trusted/unattested counts with the producer-shared components the check rests on
The service set entail.proof_services() the seven services a proof term can be about, so the set is measurable rather than written down here

service is one of those seven; argument is the question's own input in that service's grammar — "" for consistency/classify/realize (a non-empty one raises rather than being discarded), ONE N-Triples term for class-satisfiability/instances, ONE triple for entails, and a method <bot|top|star> line followed by one term per line for extract-module. entail.Reasoner(data, proofs=True) is the session-level opt-in, and session.prove(service, argument) is the same call over a document parsed once.

Nothing in check_proof trusts the producer: the ontology is parsed from data, the question is re-derived from service and argument, the claims are read back out of answer's own grammar, and the checking context comes from a reverse mapping the call performs itself. A proof for a different ontology, for a different question, or of a different answer is refused — and so is a document reading availability not-recorded, because an answer nobody asked to record must never be presented as a verified one.

The three services below are the chase lane's, not the tableau's, and their certificate is a purrdf-reasoning-report 4 block rather than a DL one. Note the collision and that both names are right: entail.entails asks the tableau about one axiom of the OWL 2 RDF mapping, while entail.graph_entails asks the regime's rule table whether a premise entails a conclusion graph.

Service Call Answer
Certain answers entail.certain_answers(regime, data, pattern, imports) mechanism, one var line per projected variable, one row per certain answer, and a limit line per reason the row set may not be exhaustive
Graph entailment entail.graph_entails(regime, premise, conclusion, imports) mechanism <name>, then entailment entailed / not-entailed / undecided — three verdicts, never two
Verified entailment entail.verify_entailment(regime, premise, conclusion, imports) the above plus warrant present/absent and verified true/false/not-applicable

pattern is N-Triples with ?name in any position, the predicate included; a blank node in it is a non-distinguished variable, constrained by the match and not projected, which is what SPARQL says a query blank node is. A variable inside an RDF 1.2 triple term is an ordinary variable — it binds and it is a column — and one name is one variable wherever it was written, so ?x <ex:p> <<( ?x <ex:q> <ex:r> )>> is the join it reads as. A row is a substitution the knowledge base entails the pattern under — true in every model, not merely present in one closure. The one slot that admits no variable is a literal's datatype: "5"^^?d asks for a binding in a position that holds an IRI rather than a term, and raises ValueError naming it.

A predicate variable is projected like any other, and under OWL_RL it also renders a limit: it ranges over the whole predicate vocabulary, so it ranges over the schema predicates no rule of the table concludes and over the constructs the mechanisms beyond the table decide, and the closure the rows are drawn from holds neither.

A pattern with no ?name in it is a conclusion graph, so certain_answers and graph_entails are asking one question and answer it through one fold: the mechanism is whichever one actually reached it, and the relation has no columns — a yes is one bare row line, a no is none. With something to project the five mechanisms beyond the rule table are not run, because a projected variable over what any of them decides is a different question; that one of them would have been needed arrives as a limit line naming the lane, never as an exhaustive empty answer.

mechanism says which of seven mechanisms reached the verdict. strict-table is the regime's own rule table, run once; refutation, freeze, comprehension, reflexivity and data-range each exist because that table decides no conclusion of that shape, and none of them adds a rule to it. composite is two or more of those folded over one conclusion — each discharging the triples it reads and handing the rest on — and it is spelled that way rather than by any one constituent's name, which would say that one mechanism sufficed.

entailment not-entailed is a proof — the procedure was complete for this premise, so the absence of a mapping is the absence of an entailment — while undecided is what an incomplete procedure is entitled to say instead. Reading the second as the first would turn a limitation of this library into a false statement about your ontology.

imports — the documents your premise says it is not all of

imports is an ordered sequence of (ontology_iri, document) pairs, where document is N-Quads (or N-Triples) text exactly like the premise. An ontology carrying an owl:imports states that its axioms are its own plus those of the documents it names, so answering over the premise alone would answer a different question — this is where those documents go, and your owl:imports triple stays exactly where you wrote it.

PurRDF fetches nothing. An ontology IRI you did not supply raises ValueError naming the document, never a network access and never a silently empty import. [] is the ordinary imports nothing case; the argument is required rather than defaulted, in the same position on all four hosts, so one call shape works from Python, from JavaScript, from C and from Rust. Resolution is transitive to a fixpoint, so a supplied document's own owl:imports is followed too.

from purrdf import entail

premise = (
    "<https://example.org/o>"
    " <http://www.w3.org/2002/07/owl#imports> <https://example.org/schema> .\n"
    "<https://example.org/socrates>"
    " <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://example.org/Man> .\n"
)
schema = (
    "<https://example.org/Man>"
    " <http://www.w3.org/2000/01/rdf-schema#subClassOf> <https://example.org/Mortal> .\n"
)
conclusion = (
    "<https://example.org/socrates>"
    " <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://example.org/Mortal> .\n"
)

answer, _ = entail.graph_entails(
    "owl-rl", premise, conclusion, [("https://example.org/schema", schema)]
)
assert "entailment entailed" in answer

# The same call with nothing supplied refuses BY NAME rather than reasoning over a
# premise that is missing the axioms it told you about.
try:
    entail.graph_entails("owl-rl", premise, conclusion, [])
except ValueError as refusal:
    assert "https://example.org/schema" in str(refusal)
from purrdf import entail

ontology = (
    "<https://example.org/Cat>"
    " <http://www.w3.org/2000/01/rdf-schema#subClassOf>"
    " <https://example.org/Animal> .\n"
    "<https://example.org/felix>"
    " <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>"
    " <https://example.org/Cat> .\n"
)

answer, certificate = entail.consistency(ontology)
assert answer.strip() == "consistency true"
assert certificate.startswith("purrdf-dl-certificate 1")
assert "completeness decided" in certificate

answer, _ = entail.instances(ontology, "<https://example.org/Animal>")
assert answer.strip() == "instance <https://example.org/felix>"

answer, _ = entail.profile(ontology)
assert answer.splitlines()[0] == "certified EL"

The certificate is the point, and there is a different one per kind of evidence. consistency, classify, realize, instances and entails render a purrdf-dl-certificate 1 block carrying the DL lane's own completeness — decided, decided-within-boundaries (an axiom that never became a DL clause, with each such construct named) or budget-exhausted. That is a different notion from the chase report's, which subtracts two rule tables, and it is rendered under a different banner so neither can be parsed as the other. profile reports no search at all — it is purely syntactic — so it renders a purrdf-owl-profile-certificate 1 block ending one-directional true: a certification proves membership, a violation does not prove non-membership. extract_module renders purrdf-module-extraction 1, whose conservative line says whether the module is minimal or a sound superset. justify renders purrdf-justification 1 and explain_conclusion renders purrdf-chase-proof 1; both re-check their own answers rather than restating them — sufficient and minimal are re-decided over the justification and over each one-axiom-smaller subset, and a proof's derived-* lines are what the checker re-derived from the proof term, not what the proof claims.

A tableau performs no derivation steps, so justify is a justification and deliberately not called a proof; explain_conclusion is the chase lane's genuinely derivational one. They are different kinds of thing rather than two spellings of one, which is why there is no single explain.

Beside completeness, a purrdf-dl-certificate 1 block carries eight search-cost counters — the numbers a caller needs to tell a decision that finished from one that merely stopped:

Line What it counts
steps rounds spent, against the per-decision round cap
budget the round cap the decision ran under (the knowledge base's own derived cap, or step_cap if that narrowed it)
work matcher, scan, closure and clone work spent, against the work cap
work-budget the work cap the decision ran under (derived, or work_cap if that narrowed it)
decisions how many sub-decisions the run made
peak-nodes the largest completion graph a decision built
disjunctions how many times the tableau's case-split rule fired
peak-depth how deep that rule's branch stack got

step_cap and work_cap (both 0 by default, meaning "no narrowing") appear on every DL service above and on Reasoner's constructor; each can only tighten the knowledge base's own derived cap, never loosen it, and a run narrowed into its cap answers unknown rather than a false it has not actually decided.

Nothing here re-implements the reasoner: every entry point routes through the same shared boundary the WebAssembly and C hosts call, checked against one committed golden-vector artifact, so the four hosts return byte-identical results for the same input.

SPARQL property functions

A property function is a host-supplied relation invoked from predicate position. Unlike an extension function it is a row source, so one call may emit zero, one, or many rows. Store.query / Store.query_governed / Store.update / Store.update_governed (and the same four on MutableDataset) take relations as data, registered for that call only:

import purrdf

EX = "http://example.org/"
store = purrdf.Store()

rows = store.query(
    f"SELECT ?person ?team WHERE {{ ?person <{EX}rel/memberOf> ?team }}",
    relations={
        f"{EX}rel/memberOf": (
            1,  # subject-side arity
            1,  # object-side arity
            [
                [purrdf.NamedNode(f"{EX}ada"), purrdf.NamedNode(f"{EX}alpha")],
                [purrdf.NamedNode(f"{EX}chen"), purrdf.NamedNode(f"{EX}beta")],
            ],
        )
    },
)

A table can also be written as RDF rather than as Python — an rdf:List of rdf:Lists in the store's own default graph, one inner list per row — and named by its head:

store.query(query, relations_from_graph={f"{EX}rel/memberOf": (purrdf.NamedNode(f"{EX}memberTable"), 1, 1)})

A registered IRI is recognized in predicate position exactly, so reaching one needs no namespace declaration. Passing property_fn_namespaces=[f"{EX}rel/"] asks for the stricter reading instead: every predicate under that prefix becomes a call, and an unregistered one is a hard error rather than a triple pattern that quietly matches nothing. A duplicate IRI, a ragged table, a torn list, or a head naming nothing raises ValueError where it is supplied.

The third spelling is not a table at all. path_relations registers a path-witness traversal over the store's own edges: a call reads ?start <iri> ( ?end ?pathId ?len ?step ?node ?edge ) and emits one row per hop, with ?edge bound to the traversed statement as an RDF 1.2 triple term, so GROUP BY ?pathId with ORDER BY ?step reassembles a whole walk inside the query. Every field of the specification is mandatory — PurRDF invents no relation IRI and no traversal envelope:

store.query(
    "SELECT ?end ?step ?node WHERE { <http://example.org/a> "
    "<http://example.org/pf#walk> ( ?end ?pathId ?len ?step ?node ?edge ) } "
    "ORDER BY ?len ?step",
    path_relations={
        "http://example.org/pf#walk": (
            [(purrdf.NamedNode("http://example.org/p"), "forward")],  # steps
            1, 4,          # min_hops, max_hops
            1024, 100000,  # max_paths_per_seed, max_expansions_per_invocation
            "walk",        # "walk" (every simple-prefix witness) | "shortest"
        )
    },
)

Registration is per call and carries no callable, so the whole evaluation still runs with the GIL released. The property functions that are arbitrary host closures on the Rust side — the full-text index, the GeoSPARQL relations, the embedding kNN relation — do not cross this boundary; only these three data-shaped registrations do.

Base IRIs

A document that spells a relative IRI needs a base. Every parse entry point takes an optional base= keyword (purrdf.parse(text, format, base=...), RdfDataset(text, format, base=...), the Store loaders, and the JSON-LD and RDF/XML converters), and shapes.validate / shapes.entail take shapes_base= for the shapes document. An in-document directive (@base, BASE, xml:base, @context.@base) wins over the keyword. With neither in scope a relative reference raises ValueError with the code iri-relative-no-base — PurRDF has no retrieval IRI for text handed to it as a string and will not invent one. N-Triples and N-Quads admit no relative reference by grammar, so they need no base.

rdflib compatibility layer

The package ships an rdflib-shaped API over the native engine:

from purrdf.compat.rdflib import Graph, URIRef

g = Graph()
g.parse(data=my_ntriples, format="nt")
print(len(g), g.serialize(format="turtle"))

For a literal, zero-change import rdflib, install the opt-in extra:

pip install purrdf[rdflib]

This pulls in the separate purrdf-rdflib distribution, whose top-level rdflib package re-exports the compat surface, so existing third-party code doing import rdflib / from rdflib.namespace import RDF transparently runs on purrdf. Caveat: that shadow claims the rdflib import name and must never be installed alongside the genuine rdflib — the two cannot co-inhabit one environment. It is a separate distribution (never bundled into the main purrdf wheel) precisely so environments that need the real rdflib simply omit it.

GTS graph transport and relational rows

GTS is PurRDF's single-file, content-addressed, append-only container for RDF 1.2 graphs. Build one from quads and read it back as in-memory relational rows:

import purrdf

gts_bytes = purrdf.gts_from_quads(my_nquads_bytes, format=purrdf.RdfFormat.N_QUADS)

rows = purrdf.gts_relational_rows_from_bytes(gts_bytes)
rows["terms"], rows["quads"], rows["reifiers"], rows["annotations"], rows["blobs"]

gts_relational_rows_from_bytes returns a GtsRelationalRows dict of five row lists. gts_to_sqlite(data, path), gts_to_duckdb(data, path) and gts_to_parquet(data, out_dir) write those five tables out — terms, quads, reifiers, annotations, blobs — in the projection's own row order, so exporting the same container twice produces the same content. gts_to_parquet writes one file per table and returns their paths in table order.

SQLite needs nothing beyond the standard library. The other two raise ModuleNotFoundError naming the extra when it is absent:

pip install 'purrdf[duckdb]'    # gts_to_duckdb
pip install 'purrdf[parquet]'   # gts_to_parquet

The same entry points are grouped under purrdf.gts for discoverability.

Learn more

Licensed under MIT OR Apache-2.0, at your option.

Release files for purrdf 1.1.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 purrdf 1.1.0
File Size Uploaded
purrdf-1.1.0.tar.gz 8.6 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for purrdf 1.1.0
File Interpreter ABI Platform
purrdf-1.1.0-cp313-abi3-manylinux_2_34_x86_64.whl CPython 3.13 abi3 Linux glibc 2.34+ x86-64 Details

Total release size: 18.2 MB

Release files / purrdf-1.1.0.tar.gz

Download URL purrdf-1.1.0.tar.gz
Size 8.6 MB
Tags Source
SHA-256 checksum
How to use checksums
390bbe1b49facc8ece37f19665a0d50843bc20c2172dc4e803c343ad758a8729
BLAKE2b-256 checksum
How to use checksums
7d4d24de667e8a0eefb97fa9e62c041a94332cc1aa07d39f4de4432aa18a2bdc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 4, 2026.

Transparency log

Release files / purrdf-1.1.0-cp313-abi3-manylinux_2_34_x86_64.whl

Download URL purrdf-1.1.0-cp313-abi3-manylinux_2_34_x86_64.whl
Size 9.6 MB
Tags CPython 3.13 Linux glibc 2.34+ x86-64 abi3
SHA-256 checksum
How to use checksums
00f63a79b33de9d8d8b597aaa9d70d306aff50c7845934f0f977eb0e9277197e
BLAKE2b-256 checksum
How to use checksums
b1cfc2fb812ef25e6f14990eedc66c409e2351ae9acff43fa33e0ab16e8d5b0b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 4, 2026.

Transparency log

Release history Release notifications | RSS feed

2.0.2

2 release files

2.0.1

2 release files

2.0.0

2 release files

This release

1.1.0 This release

2 release files

1.0.0

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.5

2 release files

0.8.3

2 release files

0.8.2

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.0

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.5

2 release files

0.1.3

2 release files

0.1.1

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