Skip to content
ballesterosbrPublic

About

Turn a VCF file into a running GA4GH Beacon v2 REST API, with no database and no ingestion step

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

beaconize

Tests Python License

beaconize turns a VCF file into a running Beacon v2 REST API for variants only, in one line.

There is no database and no ingestion step. The beacon reads the VCF on every request, so a valid VCF sorted by position is served as it is, multiallelic records included. A bgzip and tabix index makes positional queries fast, but it is optional. Some answers still depend on what the file records, such as its assembly and its genotypes, as What a query counts explains.

It uses FastAPI as the web framework and Pydantic models generated from the official GA4GH Beacon v2 JSON schemas, release v2.2.0.

Intended use case. An organization that has completed a variant analysis and wants selected partners to check, before any regulatory or data access process begins, whether its dataset holds variants relevant to a possible collaboration. Each partner gets an API key and asks whether a variant is present, without any raw data leaving the holder. If the answers show a relevant overlap, the formal process can proceed. When the collaboration is settled, the beacon is shut down and leaves no store of variants or credentials behind.

Variants only, and not a production Beacon network node. beaconize answers the question of the first Beacon, whether a variant is present, through the Beacon v2 framework. It exposes genomic variants and a presence check on biosamples, not the individuals, phenotypes and other entities of the full Beacon v2 model. Each instance serves one VCF file, with no persistent database, no federation and no multi-tenant key management, for a few partners and a modest number of queries. For questions that combine a variant with a phenotype, or for a production node of a Beacon network, use a Beacon backed by richer data, such as the reference implementation or bycon.


Installation

Requires Python ≥ 3.11 and uv. Clone the repository and sync the environment:

git clone https://github.com/ballesterosbr/beaconize
cd beaconize
uv sync

This creates a .venv with all dependencies. Prefix commands with uv run (as below), or activate the environment with source .venv/bin/activate.


Try it in one minute

The package ships a small example VCF, the sample of the VCF 4.2 specification, so you can start a beacon without any data of your own.

Create a new Python file, beacon.py, with an import and a call. The whole beacon is the app object:

# beacon.py
from beaconize import example_beacon

app = example_beacon()

Serve that file:

uv run fastapi run beacon.py

fastapi run imports the module you name and serves its app, so the file can be called anything.

The beacon is now answering on http://localhost:8000. The example holds five variants in four records on chromosome 20, on build 36 of the human genome, so these queries return real answers:

# Is this exact allele present? VCF POS=14370 is interbase start=14369
curl "http://localhost:8000/g_variants?referenceName=20&start=14369&referenceBases=G&alternateBases=A&assemblyId=NCBI36"
# ... "responseSummary": {"exists": true}

# Any variant in this window?
curl "http://localhost:8000/g_variants?referenceName=20&start=1110000&end=1240000&assemblyId=NCBI36"
# ... "responseSummary": {"exists": true}

# Another assembly
curl "http://localhost:8000/g_variants?assemblyId=GRCh38"
# ... "responseSummary": {"exists": false}

# Beacon metadata
curl "http://localhost:8000/info"

Every response carries the Beacon v2 envelope, a meta block echoing the request and a responseSummary with the answer. At count granularity the summary also reports numTotalResults.

Interactive documentation for every endpoint is served at http://localhost:8000/docs.

The same thing from Python

client_example.py issues one query of every kind against a running beacon, matching and non-matching, and prints what came back. The beacon.py above is unprotected, so it takes no key:

uv run python client_example.py

Only when the beacon was started with AuthConfig, as in the controlled access example further down, pass one of its keys. The client then also exercises the access control responses, a 401 for an invalid key and a 429 once the key's quota is spent:

uv run python client_example.py --key "$(head -1 api_keys.txt)"

Both forms assume the beacon is on http://127.0.0.1:8000. Use --url to point the client elsewhere.


Serving your own VCF

Write the same kind of file, but calling from_vcf with the path to your VCF. Everything else is optional:

# beacon.py
from beaconize import from_vcf, ServiceConfig

app = from_vcf("variants.vcf", config=ServiceConfig(
    name="My Beacon",
    organization_name="My Org",
    contact_url="mailto:beacon@example.org",
))

Serve it the same way, with uv run fastapi run beacon.py. If the header of your VCF names no assembly, add assembly_id="GRCh38", or whichever it is, to ServiceConfig, so that queries naming an assembly can be checked.

A controlled access beacon, again the same file, reporting counts instead of a plain yes or no, exposing only PASS rows and requiring an API key:

# beacon.py
from beaconize import from_vcf, ServiceConfig, FilterConfig, AuthConfig

app = from_vcf("variants.vcf.gz",
    config=ServiceConfig(
        name="My Beacon",
        organization_name="My Org",
        contact_url="mailto:beacon@example.org",
        granularity="count",
    ),
    filter_config=FilterConfig(pass_only=True),
    auth_config=AuthConfig(api_keys_count=10, api_key_quota=100, api_keys_file="api_keys.txt"),
)

Generated API keys are printed to the console at startup and written to api_keys.txt. Clients send them in the X-API-Key header:

KEY=$(head -1 api_keys.txt)
curl -H "X-API-Key: $KEY" "http://localhost:8000/g_variants?referenceName=20&start=14369"

To reach partners on other networks, put the beacon behind a reverse proxy that serves HTTPS. experiments/deployment/README.md does it step by step, from the VCF to an authenticated query from another network and back to nothing running.


A whole chromosome, end to end

This example serves chromosome 2 of the 1000 Genomes Project phase 3 release, 7,081,600 records with 7,117,797 ALT alleles over 2,504 samples on GRCh37, and asks one query of each coordinate type. They are three of the worked queries of the accompanying article.

Two files are needed, both openly available from the 1000 Genomes phase 3 data release:

ALL.chr2.phase3_shapeit2_mvncall_integrated_v5b.20130502.genotypes.vcf.gz
ALL.chr2.phase3_shapeit2_mvncall_integrated_v5b.20130502.genotypes.vcf.gz.tbi

The second one is the tabix index the release already ships. Nothing has to be configured for it. The beacon looks for a file with the name of the VCF plus .tbi or .csi next to it, and uses it if it is there. Point from_vcf at the VCF alone and serve it at count granularity:

# beacon.py
from beaconize import from_vcf, ServiceConfig

app = from_vcf(
    "ALL.chr2.phase3_shapeit2_mvncall_integrated_v5b.20130502.genotypes.vcf.gz",
    config=ServiceConfig(name="1000G chr2", granularity="count"),
)
uv run fastapi run beacon.py

Startup reads the VCF header only, so it does not grow with the size of the file. Then, one query of each coordinate type:

# Sequence, one exact allele
curl "http://localhost:8000/g_variants?referenceName=2&assemblyId=GRCh37\
&start=75940816&referenceBases=C&alternateBases=T"
# ... "numTotalResults": 1

# Range, every variant overlapping a 100 bp window
curl "http://localhost:8000/g_variants?referenceName=2&assemblyId=GRCh37\
&start=18763813&end=18763913"
# ... "numTotalResults": 1

# Bracket, two values per bound, the start in [32865951, 32867291) and the end in [32865943, 32867301)
curl "http://localhost:8000/g_variants?referenceName=2&assemblyId=GRCh37\
&start=32865951&start=32867291&end=32865943&end=32867301"
# ... "numTotalResults": 46

Configuration

from_vcf accepts three independent, optional configuration objects.

ServiceConfig (identity and responses)

Nothing is left blank when a field is omitted, and no default reveals anything about the file being served, neither its name nor how many sequences or samples it holds. beacon_id is built from the organization name and base_url from the incoming request, while the rest carry a literal default. A field with no value at all is left out of the response rather than reported as null, as the Beacon v2 schemas expect.

Field Accepted values Default Description
name text "beaconize" Beacon display name
beacon_id text org.<organization_name>.beacon Identifier that tells this beacon apart from others in a network. A reversed domain string by convention
description text Ephemeral Beacon v2 API served from a VCF file Human-readable description
organization_name text "Beaconize" Organization name
organization_url URL beaconize GitHub URL Organization website
organization_description text None Organization description
contact_url URL or mailto: unset Who to contact about this beacon. Left unset, /service-info reports organization_url in its place and /info omits the field
base_url URL derived from request Override canonical URL (useful behind a proxy)
granularity "boolean" or "count" "boolean" "boolean" answers whether a variant exists, "count" adds how many
assembly_id assembly name None The file's assembly, when its header names none or one beaconize does not recognize. It overrides the header

FilterConfig (which variants are visible)

The three filters are combined with AND logic.

Field Accepted values Default Description
min_qual a number, or None for no threshold None Hide variants whose QUAL is below this value, and also those carrying no QUAL at all
pass_only True or False False With True, expose only variants whose FILTER field is PASS
carried_only True or False True Report an ALT allele only when some sample carries it, read from INFO/AC when it gives one count per ALT allele, otherwise from the genotypes. A file with neither reports every allele it lists, and startup says so

AuthConfig (API keys)

Field Accepted values Default Description
api_keys_count a whole number, 0 disables key protection 0 How many API keys to generate at startup
api_key_quota a whole number, or None for unlimited None Maximum calls to /g_variants per key
api_keys_file a path, or None to only print them None Where to write the generated keys, one per line

When api_keys_count > 0, every endpoint requires a valid X-API-Key header, and /configuration declares the security level CONTROLLED. Keys are random 32-byte URL-safe tokens generated in memory at startup, printed to the console, and optionally written to a file. They are ephemeral, so a server restart produces a fresh set.

Without keys the beacon answers anyone who can reach it, declares the security level PUBLIC, and warns at startup. That setting is meant for local testing.

The quota applies only to the genomic endpoint, counting both /g_variants and its legacy alias. Every other endpoint stays reachable for the lifetime of the key.

HTTP status Meaning
401 Unauthorized Missing or invalid API key
429 Too Many Requests The key's /g_variants quota is exhausted

Keys and quotas limit who can ask and how much within one run of the beacon. A restart issues new keys with fresh quotas, so they set no limit across runs, and whether to start a new instance for the same partner is the data holder's decision. The answers are only a boolean or a count, never records or samples. beaconize writes no record of the queries to disk. If an audit trail is needed, the access log of the proxy in front of it can provide one.


Endpoints

Informational

Endpoint Description
GET /info General Beacon metadata
GET /service-info GA4GH service registry metadata
GET /configuration Supported entry types, granularity, and security level
GET /entry_types Entry type definitions
GET /map Available endpoints (sitemap)
GET /filtering_terms Ontology filters, always an empty list, since a VCF carries no ontology terms

Genomic variants

GET /g_variants
POST /g_variants

All parameters are optional and are combined with AND logic. POST takes the same parameters in the query.requestParameters object of the JSON body.

Parameter VCF field Notes
referenceName CHROM 20, chr20, or a versioned RefSeq identifier such as refseq:NC_000020.10, which also implies its assembly
start POS 0-based interbase (see Query types below)
end POS + len(REF), or INFO/END for a symbolic allele 0-based interbase (see Query types below)
referenceBases REF As the VCF writes it, or trimmed (empty for an insertion). N matches any base
alternateBases ALT Each ALT allele separately, as the VCF writes it or trimmed (empty for a deletion). N alone matches any sequence allele
variantType derived See variantType below
assemblyId ##contig assembly= header Compared with the file's assembly, see The assembly check below
includeResultsetResponses not a filter ALL, HIT, MISS or NONE, echoed back in the response meta. Anything else is reported as NONE

What a query counts

The unit of every answer is the ALT allele, one genomic variation in the Beacon v2 model. A multiallelic record contributes one for each allele that matches, and an allele that two records at the same position both list counts once. By default an allele counts only when some sample carries it (carried_only). An ALT entry that marks no variant, ., *, <*> or <NON_REF>, never counts.

At boolean granularity the answer says whether at least one allele matches. At count granularity numTotalResults also gives how many. That number is not a count of carriers, which beaconize never reports.

Query types

  • Sequence, a single start with referenceBases or alternateBases. The exact allele at that position.
  • Range, a single start and a single end. Any variant that overlaps the window [start, end).
  • Bracket, two values for start and two for end, as in start=A&start=B. A variant, typically structural with imprecise breakpoints, whose start falls in [A, B) and whose end falls in the end interval. Both intervals are half-open, so B = A + 1 asks for one exact position.

geneId, HGVS (genomicAlleleShortForm) and aminoacidChange queries are not supported, because they require external annotation, such as maps from genes to coordinates and the normalization of variants, which the VCF alone does not provide.

Coordinates. VCF numbers bases from 1, and Beacon v2 uses 0-based interbase coordinates. A SNV at VCF POS=14370 is queried as start=14369, or end=14370. A symbolic allele such as <DEL> or <CN0> spans from its POS to its INFO/END.

variantType

Sequence alleles are typed once the bases REF and ALT share are trimmed, as SNV (also asked as SNP), MNV, INS or DEL, so AT>AC is an SNV. Breakends are BND. A symbolic allele answers to its tag and to each broader level of a subtype, so <INS:ME:ALU> also answers to INS:ME and INS. Copy number alleles also answer to the basic types and to the EFO terms that bycon uses:

Allele Also answers to
<DEL>, <CN0> DEL, CNV, EFO:0030067 (copy number loss), EFO:0030066 (copy number variation)
<DUP>, <CN2> and higher DUP, CNV, EFO:0030070 (copy number gain), EFO:0030066
<CNV> CNV, EFO:0030066

<CNn> is the copy number of a segment on one chromosome, as in the structural variants of the 1000 Genomes release, so <CN1> is the reference state and answers to its tag alone.

The assembly check

A query that names an assembly is compared with the assembly of the file, from its ##contig header or from ServiceConfig(assembly_id=...), which overrides it. If the two disagree, the answer is false and no variant is read. If neither names one, any value is accepted, and startup warns about it.

Both names are normalized before the comparison. beaconize recognizes

  • the common aliases of NCBI36, GRCh37, GRCh38 and T2T-CHM13, such as b36, b37, hg19, hs37d5, hg38 or chm13, written by hand in _ASSEMBLY_SYNONYMS in src/beaconize/core.py
  • the patch releases of GRCh37 and GRCh38, such as GRCh38.p13
  • the RefSeq and GenBank accessions of these assemblies, such as GCF_000001405.39, and the versioned RefSeq identifiers of the chromosomes of GRCh37, GRCh38 and T2T-CHM13, such as refseq:NC_000022.10, from tables generated from NCBI by tools/make_assembly_tables.py

Any other name matches only itself, and startup warns when the file names one. If it is another name for one of these assemblies, declare that assembly with ServiceConfig(assembly_id=...). A new alias goes into _ASSEMBLY_SYNONYMS, with a test.

Indexing

A bgzip and tabix index (.tbi or .csi) next to the file makes positional queries fast, since the beacon reads only the region asked for. Without one it scans the whole file, with the same results, only slower.

/g_variants is the canonical Beacon v2 path. /genomicVariants is kept as a legacy alias.

Biosamples

GET /biosamples
POST /biosamples

A fixed boolean response, true when the VCF has sample columns. A VCF holds no biosample metadata, so this is not a queryable entry type.

Beacon v2 does not require every entry type, only that /configuration and /entry_types declare the ones implemented. beaconize leaves out /individuals, /datasets, /cohorts, /analyses and /runs, because a VCF records the variant calls of one analysis and carries no metadata about individuals, studies or runs. Whether a file reflects a dataset or a cohort depends on the study design, not on anything the file states.


Tests

uv run pytest

The suite needs no network, no running server and no large files. It runs on the bundled example.vcf, on small VCFs that the tests write, and on a bgzipped and tabix-indexed slice of 1000 Genomes chromosome 22 in tests/fixtures/, so the index path is tested as well. GitHub Actions runs it on Linux and macOS with Python 3.11 to 3.14 for every push.


Regenerating Pydantic models

The models in src/beaconize/models/ are generated automatically from the official Beacon v2 JSON schemas. Do not edit them by hand.

To regenerate after a spec update:

bash generate.sh

The script fetches the schemas of release v2.2.0 from the ga4gh-beacon/beacon-v2 repository, at a fixed commit so that it always gives the same models, and writes Pydantic v2 models using datamodel-codegen.


Reproducing the article

experiments/README.md gives the command of every experiment of the accompanying article. results/ holds the data they produced, namely the query times, the memory footprint, the comparisons with the Beacon v2 reference implementation, the verifier reports, the use case queries and the deployment checks, described file by file in results/README.md.


License

MIT, see LICENSE.

About

Turn a VCF file into a running GA4GH Beacon v2 REST API, with no database and no ingestion step

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages