VCF-RDFizer can now connect its graph to external resources through three tiers
of linker plug-in. The output is a separate N-Triples linkset: enabling linking
does not change the aggregate, its compression inputs, or the core validation
queries. This is an initial implementation of
datalinking-design.md, with the remaining work listed
in §8 below.
The examples and the runner are installed with the Python package. Linking runs on the host; only conversion still needs Docker. RDFLib is now a runtime dependency for parsing actual Turtle manifests and unordered N-Triples, rather than approximating either syntax with regular expressions.
| Tier | Installed ID | Join and result | Network |
|---|---|---|---|
| 1: declarative | rsid-dbsnp |
ID rsID tokens → dbSNP identifier IRIs |
None |
| 2: reference bundle | gene-demo |
Explicit allele REF spans → synthetic GFF3 gene intervals | None with the shipped local bundle |
| 3: live resolver | rsid-ensembl |
Deduplicated rsID batches → service-confirmed Ensembl variation links | Ensembl HTTPS API, or cached responses |
The canonical example directories are under
vcf_rdfizer_data/linkers/. Each has its own
README and linker.ttl; only Tier 3 has resolver.py.
gene-demo is deliberately synthetic. Its three genes use example.org
identifiers and invented intervals. Its GRCh38 declaration exercises the
assembly guard; it does not turn the fixture into a biological annotation.
Use a real, digest-pinned reference before interpreting overlaps biologically.
From a checkout:
python -m pip install -e .
vcf-rdfizer-link list
vcf-rdfizer-link keys
# Two local tiers, no Docker or network needed.
vcf-rdfizer-link run -i examples/linking/example.vcf \
--link rsid-dbsnp,gene-demo --offline \
--links-cache ./example-cache -o ./example.links.ntThis writes seven links: three dbSNP links and four gene overlaps, plus
provenance triples. Record 2 overlaps both demo genes A and B. Record 3 has no
rsID and falls beyond the gene interval; it produces no link. The JSON report
is example.links.json. Existing graph or report paths are refused.
The companion run -i command derives subjects with the converter's default
IRI templates; it does not create or verify a base graph. To link an actual
conversion, use --rdf or one of the two entry points below.
Full mode inserts the stage after the direct RDF emitters and before compression:
vcf-rdfizer --mode full -i examples/linking/example.vcf \
--rdf-storage-mode plain --representations none --rdf-compression none \
--link rsid-dbsnp,gene-demo --offline -o ./resultsThe aggregate is results/example/example.nt; the side-graph is
results/example/example.links.nt. Space-optimized .nt.gz aggregates work as
well. If linking fails, that input fails at data-linking and compression is
not started for it. The completed base aggregate remains available.
To enrich an existing aggregate without conversion or Docker:
vcf-rdfizer --mode link --rdf ./results/example/example.nt \
--link rsid-dbsnp,gene-demo --offline -o ./relinkedThe result is relinked/example.links.nt, with a normal run_metrics/ tree.
vcf-rdfizer-link run --rdf <path> --link <ids> -o <file.links.nt> uses the
same runner but writes a single companion JSON report instead.
Both RDF entry points read the graph's hasRecord and hasCall edges. They
retain the actual subjects, including custom IRI templates, and reject a call
whose subject is absent. They require the VCF-RDFizer predicates for those
edges and the selected join fields. Arbitrary custom vocabularies cannot be
inferred. N-Triples may be unordered; the input reader stores only relevant
fields in a temporary SQLite index and discards genotype triples after parsing.
rsid-dbsnp/linker.ttl
implements the proposal's token example unchanged in substance:
vcfl:TokenJoinreadsID, or a named INFO value usingINFO/<key>.vcfl:splitOnsplits literal delimiters, defaulting to;for ID and,for INFO. Flags, missing keys, empty tokens, and.do not create keys.vcfl:acceptis a case-sensitive Python regular expression matched against the whole token. Duplicate tokens do not duplicate a triple.vcfl:objectTemplatecontains exactly one{TOKEN}placeholder. The runner percent-encodes the value before substitution, then validates the IRI.vcfl:subjectselectsvcfl:VariantCallorvcfl:VCFRecord; the manifest declares the predicate. The runner owns subject identity and serialization.
This example asserts a weak identifier connection with vcfl:sameVariantAs.
It does not check whether dbSNP still recognises the rsID, resolve retired IDs,
or assert owl:sameAs.
gene-demo/linker.ttl
declares vcfl:IntervalJoin, a GFF3 reference, its exact SHA-256, and its
assembly. The interval index is the implementation; no plug-in Python is needed.
The join uses 1-based closed intervals:
[POS, POS + len(REF) - 1]. A position at either boundary overlaps. Nested
and overlapping features are retained. vcfl:featureType defaults to gene;
vcfl:idAttribute defaults to ID. GFF3 percent escapes are decoded and the
selected identifier feeds the {ID} object template.
Limits are explicit: chromosome names must match exactly (1 and chr1 are
different); there is no contig aliasing or liftover. Symbolic alleles,
breakends, and spanning-deletion * records are skipped and counted in
skipped_records. The runner does not interpret INFO/END or confidence ranges.
It uses explicit DNA REF spans, including their anchor base, rather than an
inferred biological affected region.
References may be HTTPS URLs, local file: URLs, or relative IRI references
such as <genes.gff3> resolved beside linker.ttl. An absolute URL string is
also accepted, as in the original proposal. Fetched/local bundles are cached
under references/<sha256>; the digest is checked on every cache use. For
gzip references the digest covers the compressed bytes. The GFF3 index is
rebuilt in memory each invocation; the bundle bytes are cached, not the index.
Before resolution, the runner compares the input reference against the bundle.
It recognises GRCh37, GRCh38, and the hg19/hg38 aliases within reference
paths, or an exact declared assembly string. Missing/unrecognised metadata
requires --assembly after the user checks the input. That option cannot
override a recognised mismatch. No reference is fetched until the assembly
checks for all selected linkers have passed.
To adapt the example:
vcf-rdfizer-link init --example gene-demo -o my-gene-linker
# Edit id, reference URL, digest, assembly, attribute, and object template.
vcf-rdfizer-link check my-gene-linker --links-cache ./reference-cache
vcf-rdfizer-link dry-run my-gene-linker -i your-small.vcf --limit 100check parses the manifest, verifies/acquires the reference and parses its
features. Without an input VCF it cannot certify input assembly compatibility.
The shipped resolver calls Ensembl's batch variation endpoint. It submits one POST per batch (up to 100 rsIDs in this example) and emits links only for returned variation objects with a name. Missing IDs produce no link; malformed response objects abort the linker. No clinical assertions or frequencies are inferred from the response.
from vcf_rdfizer_linking import Link, LinkKey, LinkerContext
def resolve(batch, ctx):
response = ctx.session.post(
"https://rest.ensembl.org/variation/homo_sapiens",
json={"ids": [key.token for key in batch]},
)
response.raise_for_status()
# Inspect response.json(), then yield Link(key, absolute_object_iri).LinkKey is immutable (token, chrom, start, end). Link contains only
key and object. A resolver cannot supply a subject or change the manifest's
predicate through this protocol, and returning an undispatched key is an error.
The session offers get(url, params=...) and post(url, json=...); responses
have status_code, headers, content, json(), and raise_for_status().
Preview first, then supply your actual contact address for a live run:
vcf-rdfizer-link init --example rsid-ensembl -o ensembl-example
vcf-rdfizer-link dry-run ensembl-example -i examples/linking/example.vcf
vcf-rdfizer-link run -i examples/linking/example.vcf --link rsid-ensembl \
--links-contact-email you@your-institution.org \
--links-cache ./ensembl-cache -o ./ensembl.links.nt
# Reuse identical request batches without contacting the service.
vcf-rdfizer-link run -i examples/linking/example.vcf --link rsid-ensembl \
--offline --links-cache ./ensembl-cache -o ./ensembl-replay.links.ntThe built-in manifest's placeholder contact blocks a cache-miss live request;
an offline replay can still use its cache. Set vcfl:contactEmail in a copied
manifest or use --links-contact-email. Selecting a live linker sends its join
keys to the declared service; no sample/genotype payload is passed to this
example's resolver.
The session implements:
- A declared HTTPS endpoint origin and path. Redirects are refused, including reference-download redirects, so hidden requests cannot change hosts.
- A mandatory disk cache under
responses/<id>/<version>/<request-hash>.json. The hash includes method, URL and canonical JSON body; stored bodies have digests. Only successful responses are cached. Corrupt entries fail closed. - Full key deduplication per input aggregate, before any request. Sorted keys make batching deterministic. SQLite stores key-to-subject associations and resolved triples so those do not need to fit in memory.
- A per-host token bucket with capacity one, shared by selected linkers, using the strictest declared rate. At most one request is in flight per run. Per-linker request ceilings are shared across inputs in a directory run.
- Up to four HTTP attempts for 429/5xx, with exponential delay and numeric or
HTTP-date
Retry-Afterhonoured. Retries count toward the run ceiling. This follows Ensembl's documented rate-limit protocol. - A 30-second request timeout and 16 MiB response limit. Other HTTP failures, connection failures, and exhausted budgets abort the linker.
--offline/--links-cache-only: no network, with clear cache-miss errors. Verified local GFF3 references remain usable on a cold cache.
The limiter is shared within one invocation, not across separate processes. Directory inputs are deduplicated individually; identical request batches in later inputs use the response cache. Different batch membership produces a different cache key even if some identifiers overlap. Cache entries have no automatic expiry: use a fresh cache directory or a new plug-in version to refresh service answers deliberately.
Python plug-ins are trusted code. The session constrains traffic made
through its methods. Importing a third-party resolver is not a security sandbox
and cannot prevent that code from opening its own socket. The shipped resolver
uses only ctx.session. list, check, and dry-run do not import resolver
modules; Python entry-point discovery does execute the installed provider.
Tier 3 remains suitable for filtered variant sets. Genome-wide linking should use a suitable local reference bundle. The network budget is a hard ceiling, not a promise that a large run will finish.
vcf-rdfizer-link init -o my-linker copies the annotated Tier 1 example;
--example selects any of the three tiers. It refuses an existing destination.
Give copies a new vcfl:id before adding them to a search path. Discovery reads
packaged examples, repeatable --linker-path directories, the platform-separated
VCF_RDFIZER_LINKER_PATH, and vcf_rdfizer.linkers Python entry points.
A path may be a linker directory or a parent containing linker directories. Duplicate IDs at different locations are rejected rather than shadowed. Entry points export a zero-argument callable returning an installed directory:
[project.entry-points."vcf_rdfizer.linkers"]
my-linker = "my_package:linker_directory"from pathlib import Path
def linker_directory():
return Path(__file__).parent / "my-linker"Package that directory's manifest, resolver and optional local data. list
prints versions, tiers, references, licensing and terms; list --json provides
the parsed fields. check --json is available for automation. IDs and versions
must be safe path components. Unsupported joins, template variables, unknown
vcfl: properties, invalid regexes, bad digests and invalid budgets are errors.
dry-run <directory> -i <vcf> --limit N reads the first N records (default 100),
writes no persistent graph/report/cache, and never makes a network request. It
shows up to 20 concrete triples for local tiers, total local link counts, and
Tier 3 unique-key/batch counts and the declared request ceiling. A remote
Tier 2 bundle must already be cached. For Tier 3 it reports a plan, not
imagined API results; a general resolver's response-dependent request count
cannot be predicted without executing it. The shipped resolver makes one
request per batch before retries.
The runner stages all resolutions before serializing into a temporary file
with the wrapper's _append_rdf_atomically contract. It publishes the completed
side-graph atomically and refuses existing outputs. Failures preserve the base
aggregate and publish no partial linkset. Successful response/reference caches
from a failed run are retained for retry.
Each source/linker pair has a vcfl:Linkset node, even with zero matches. It
records producer ID/version, source, link count, generation time, and manifest
digest; Tier 2 adds reference digest and assembly; Tier 3 adds resolver and used
response digests. linkCount counts distinct links emitted by that linker for
that source and excludes provenance. If two linkers emit the same triple, it
appears once in the side-graph but counts in each linker's own provenance.
Full and post-hoc wrapper runs record linking results in run.json and
stages/linking/<name>.links.json, with RunTracker logging and the normal
progress sidecar protocol (--quiet/--no-progress apply). Reports include
unique keys, skipped records, links, requests, cache hits, transferred response
bytes, elapsed time, final HTTP status and cached-response timestamps/digests.
Failure reports carry the error and per-linker status. Main-aggregate triple
counts continue to exclude side-graph triples. --mode link summary counts are
the new side-graph's triples, including provenance.
An offline replay reproduces links from the same cached requests. Its
generatedAtTime and run timings are new; the entire side-graph is therefore
not promised byte-identical.
The implemented tiers are examples of the extension contract, not completion of every feature in the proposal:
- No allele join or allele normalization. Tier 3 uses token keys. No ClinVar/gnomAD/CADD matching is implied by this implementation.
- No production gene bundle is shipped. The synthetic fixture demonstrates digest checking, indexing and assembly refusal. Users supply real references.
- No
--merge-links. Keeping side-graphs separate leaves core validation intact; the wrapper's custom-mapping report-only forwarding gap is still open. - No plug-in SPARQL/mutation auto-discovery. Optional
queries/andmutations.pyfrom the proposal are not executed. The new unit tests cover linker invariants and failures but do not extend the published core mutation score. No standalone linkset validator is claimed. - No HDT/COTTAS input reader for linking. Supply
.ntor.nt.gz. - No persisted interval index or global cross-process quota. Reference features are indexed in memory; temporary SQLite storage still requires disk proportional to join fields and associations. This implementation has not been benchmarked on genome-scale cohorts.
- The manifest vocabulary is provisional. Third-party authors should pin the tool version while the remaining joins and validation contract settle.
test/test_linking_unit.py exercises known-answer
token links, INFO splitting/escaping, interval boundaries and nested features,
assembly refusal, digest corruption, unordered and gzip RDF, custom subjects,
empty inputs, full-mode integration, discovery, authoring commands, API batching,
cache replay, host pacing, retries, ceilings and failure atomicity.
The shipped Ensembl resolver runs in these tests against a fake HTTP transport; tests never contact Ensembl or require Docker. That verifies its response handling and the session policy, not the current availability or completeness of a public service.
python -m unittest test.test_linking_unit -v
python -m unittest discover -s test -p 'test_*_unit.py'- Original design — rationale and remaining ambitions
- Custom mappings — subject and vocabulary compatibility
- Output and metrics — the shared run report layout
- Validation methodology — the separate core suite