Python: build a .rete, step by step

This tutorial walks through preparing a .rete file from Python with the rete_graph.Builder — a lazy builder: every step just records configuration and returns the builder, so calls chain freely; nothing parses or builds until you say run(). You'll configure the sources, the embedded Dataset Card, the community pyramid, and the full-text index, then run the build, verify the result, and export the file.

uv pip install rete-graph        # rdflib optional: uv pip install rete-graph[rdflib]

The whole flow, before we take it apart:

import rete_graph as rete

builder = (
    rete.Builder()
    .add_file("people.ttl")                      # 1. sources
    .card(title="People", license="CC0-1.0")     # 2. dataset card
    .pyramid(algo="louvain")                     # 3. pyramid
    .text_index()                                # 4. full-text index
)
data = builder.run()                             # 5. build
builder.export("people.rete")                    # 6. ship

1. Start a builder and add sources

A Builder starts empty. add() queues RDF text ("nt", "nq", "ttl", "rdfxml"), add_file() queues a file (format inferred from the suffix), and either accepts a graph object from another RDF library — anything with a .serialize() method, e.g. an rdflib Graph or Dataset:

import rdflib
import rete_graph as rete

people = rdflib.Graph()
people.parse("people.ttl")

builder = (
    rete.Builder()
    .add(people)                                       # rdflib object
    .add_file("places.nt")                             # file, format inferred
    .add("<urn:a> <urn:knows> <urn:b> .")              # inline N-Triples
)

All sources merge into one graph. N-Quads sources keep their named graphs (they become a dataset; rdflib Dataset objects round-trip the same way). Nothing has been parsed yet — the builder only holds text and settings.

2. The Dataset Card

A .rete file can embed a Dataset Card — a data-catalog record living in the file's metadata section, so the data travels with its own documentation. card() sets the curated fields; repeat calls merge:

builder.card(
    title="People & places",                     # short display name
    description="Who knows whom, and where they lived (demo).",
    license="CC0-1.0",                           # SPDX id or a URL
    source="https://example.org/source-dump",    # where the data came from
    created="2026-07-16",                        # ISO date of this snapshot
    example_queries=[                            # starter SPARQL, shown by tools
        "SELECT ?s ?o WHERE { ?s <urn:knows> ?o } LIMIT 10",
    ],
)

Beyond the named keyword arguments, card() passes any extra field straight into the card JSON — which is how you set the curated identity and provenance fields the card schema defines (the same ones the CLI's --card-file takes):

builder.card(
    version="1.0.0",                             # Croissant requires one
    creators=[{"name": "Ada Lovelace",
               "orcid": "https://orcid.org/0000-0002-1825-0097"}],
    publisher={"name": "EPFL", "ror": "https://ror.org/02s376052"},
    canonical_url="https://data.example.org/people.rete",
    source_date="2026-07-01",                    # the SOURCE's own snapshot date
    derived_from=["https://example.org/source-dump"],
    doi="https://doi.org/10.5281/zenodo.0000000",
    cite_as="Lovelace, A. (2026). People & places.",
)

Field by field:

FieldMeaning
titleHuman name of the dataset
descriptionA paragraph of context — what, where from, caveats
licenseThe data license (SPDX identifier or URL)
sourceProvenance: the upstream dump/API this was built from
createdSnapshot date (ISO 8601 string)
example_queriesRunnable SPARQL strings for a newcomer's first click
versionDataset version (semver or a date)
creatorsPeople: {name, orcid} — the ORCID as an IRI, joinable against the published ORCID graph
publisherOrganisation: {name, ror} — the ROR as an IRI
canonical_urlWhere the authoritative copy of this file lives
sparql_endpointA public endpoint serving this dataset
source_dateThe source data's own snapshot date (distinct from created)
derived_fromWhat this file was derived from (dumps, endpoints, shards)
doiThe dataset's DOI, as an IRI (https://doi.org/…)
cite_asPreferred citation text

The statisticstriple_count, quad_count, named_graph_count, term_count, plus the format_version — are stamped automatically at build time; you never write them. The card is readable from every client: Graph.card() here (on a remote file it fetches only the metadata section's byte range), rete card / rete info in the CLI, and the playground's catalog view.

Beyond plain strings, example() embeds a rich example — a short title and the plain-language question alongside the SPARQL — into the card's starter-query library, where the CLI and playground surface it:

builder.example(
    "SELECT ?o WHERE { <urn:alice> <urn:knows> ?o }",
    title="Who does Alice know?",
    question="Which people does Alice know?",
)

After opening the built file (even remotely), g.examples() lists every embedded query, and each entry's ["sparql"] runs as-is — the dataset ships its own documentation and its own first queries.

One honest limit: the Python builder writes the card you supply — it cannot derive one. The CLI's rete build additionally derives an enriched profile (top predicates and classes, vocabularies, hubs, datatype/language histograms, a tiered starter-query library, an optional coherence verdict) and writes the adjacent build-info record (timestamp, builder, parameters, measured starter-query costs); the JSON-LD and Croissant projections (rete card --format jsonld|croissant) are also CLI-only. The Python builder embeds the curated fields + counts only — if you want the full auto-profile and build record, rebuild the exported data with the CLI.

One shape to watch: every example query in this tutorial matches the default graph. If your sources are N-Quads (or rdflib Datasets with named graphs), a bare pattern like { ?s <urn:knows> ?o } correctly returns nothing for statements living in named graphs — scope it with GRAPH ?g { … }, or use a surface that offers the opt-in union default graph mode (the playground's ⛁ All graphs toggle; not standard SPARQL, and not available in this client).

3. The pyramid

The community pyramid is the file's "zoom-out" structure: communities summarized level by level, powering the playground's overview, progressive rendering, and label prefix search.

builder.pyramid(algo="louvain")     # default: topological communities
builder.pyramid(algo="types")       # one community per rdf:type class
builder.pyramid(False)              # skip it entirely
  • louvain (default) clusters by graph structure — "what hangs together densely?". Deterministic, byte-identical across runs.
  • types partitions by rdf:type class — communities are self-naming, and the summary becomes the class→class relation graph. It falls back to Louvain when the graph carries no usable typing. If your data types entities with something other than rdf:type, force it:
builder.type_predicate("http://www.wikidata.org/prop/direct/P31")
  • pyramid(False) writes no pyramid section at all. SPARQL, SHACL, and reachability are unaffected — the file stays fully queryable and gets markedly smaller — but summary/progressive views and prefix_search are gone. Good for pure query workloads.

4. The full-text index

Opt-in, because it costs file size: a word index over literals that powers Graph.text_search() and fast CONTAINS filters (~39× a raw scan):

builder.text_index()

5. Run

run() executes the whole configuration — parse every source, build the dictionary, the triple indexes, the pyramid, the text index, embed the card — and returns the complete file image as bytes:

data = builder.run()
print(builder.stats)
# {'statements': 6, 'defaultTriples': 6, 'namedGraphs': 0,
#  'terms': 10, 'pyramidLevels': 1}

The result is cached: run() twice returns the same bytes without rebuilding, and export()/graph() reuse it. Changing any setting (another add, a card edit, a pyramid flag) invalidates the cache, and the next run() rebuilds.

6. Verify before shipping

The image opens like any other graph — check it holds what you meant:

g = builder.graph()                  # == rete.open(builder.run())

assert g.quads == builder.stats["statements"]
g.query("SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 5")
g.schema()                           # classes + relations profile
g.card()                             # the embedded card, counts included
g.text_search("alice")               # the index built in step 4
g.content_hash()                     # blake3-16 — cite it when publishing

7. Export

path = builder.export("people.rete")

The exported file is complete and immutable — build-once, read-many. Reopen it from disk (lazily, like a remote file) or verify it with the CLI:

g = rete.open("people.rete")
g.card()["title"]                    # ranged metadata read — a few KB
rete card people.rete                # the same card, rendered as a catalog
rete info people.rete

Host it on anything that serves HTTP Range (S3, R2, GitHub, any CDN — see Hosting your .rete) and it is queryable in place from Python, the CLI, and the browser playground, with no server.

Scaling up, and the CLI equivalents

In-memory builds comfortably handle test and small-to-medium graphs (up to a few million triples). Past that, switch to the streaming CLI — every builder step has a flag twin:

Python builderrete build flag
.add_file("x.nt")positional input files
.card(title=..., ...)--card-file card.json / --title
.pyramid(algo="types")--pyramid-algo types
.pyramid(False)--no-pyramid
.text_index()--text-index
.type_predicate(iri)--type-predicate <iri>
.export("out.rete")-o out.rete

The CLI also compresses sections with zstd at higher levels, streams input twice instead of holding it, and derives the enriched card profile. See the CLI reference and Tables, VKG & big builds.

The complete script

"""people.ttl (+ an rdflib graph) -> people.rete, with card + pyramid + text index."""
import rdflib
import rete_graph as rete

enrichment = rdflib.Graph()
enrichment.parse("extra-labels.ttl")

builder = (
    rete.Builder()
    .add_file("people.ttl")
    .add(enrichment)
    .card(
        title="People & places",
        description="Who knows whom, and where they lived (demo).",
        license="CC0-1.0",
        source="https://example.org/source-dump",
        created="2026-07-16",
        example_queries=["SELECT ?s ?o WHERE { ?s <urn:knows> ?o } LIMIT 10"],
    )
    .pyramid(algo="louvain")
    .text_index()
)

builder.run()
print("built:", builder.stats)

g = builder.graph()
assert g.card()["title"] == "People & places"

print("exported:", builder.export("people.rete"))
print("content hash:", g.content_hash())