Metadata-Version: 2.4
Name: agvd
Version: 0.1.3
Summary: Python client for the African Genome Variation Database (AGVD) GraphQL API
Author-email: WMudaki <wilson@mudaki.co.ke>
License-Expression: MIT
Project-URL: Homepage, https://github.com/wmudaki/agvdPy
Project-URL: Repository, https://github.com/wmudaki/agvdPy
Project-URL: Issues, https://github.com/wmudaki/agvdPy/issues
Project-URL: Changelog, https://github.com/wmudaki/agvdPy/blob/master/CHANGELOG.md
Keywords: agvd,genomics,variant,graphql,africa,bioinformatics,population-genetics
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28
Provides-Extra: cli
Requires-Dist: pandas>=1.5; extra == "cli"
Requires-Dist: pysam>=0.20; extra == "cli"
Requires-Dist: openpyxl>=3.0; extra == "cli"
Requires-Dist: tqdm>=4.60; extra == "cli"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-mock>=3.10; extra == "dev"
Requires-Dist: agvd[cli]; extra == "dev"
Dynamic: license-file

# agvd — Python client for the African Genome Variation Database

`agvd` is a Python client for the [African Genome Variation Database (AGVD)](https://agvd.afrigen-d.org) GraphQL API — a typed, paginated, retrying `AGVDClient` for programmatic use, plus the `agvd` command-line tool for bulk VCF/table annotation that ships on top of it.

---

## Installation

```bash
# The library only — AGVDClient, models, exceptions. Needs just `requests`.
pip install agvd

# + the bulk file-processing CLI (pandas, pysam, openpyxl, tqdm)
pip install "agvd[cli]"
```

Requires Python 3.10+.

---

## Authentication

Most queries are public. Four operations require a Bearer token: `search_variant`, the batch tools (`cli_variant_search`, `cli_vcf_filter`, `file_upload`, `cli_variant_peek`/`peek`), and `create_dataset` (which also needs the `admin` role). Contact [agvd@afrigen-d.org](mailto:agvd@afrigen-d.org) for a token.

```python
from agvd import AGVDClient

client = AGVDClient(token="YOUR_TOKEN")
```

Or set `AGVD_TOKEN` and omit `token=`:

```bash
export AGVD_TOKEN=YOUR_TOKEN
```

```python
client = AGVDClient()  # picks up AGVD_TOKEN automatically
```

Public queries work with no token at all — `AGVDClient()` with nothing configured raises only when you call something that actually needs one, and does it immediately (no network round trip for a 401).

---

## Quickstart

```python
from agvd import AGVDClient

client = AGVDClient(token="YOUR_TOKEN")

# Look up a specific variant (accepts any common id delimiter — see
# "Variant ID format" below).
conn = client.fetch_variant(variant_id=["1:2929187:C:T"])
variant = conn[0]
print(variant.variant_id, variant.chromosome, variant.annotation.gene_name)

# Every gene-name match, across every page, as a simple iterator:
for variant in client.find_by_gene_all(["BRCA1", "TP53"]):
    print(variant.variant_id, [c.name for c in variant.clusters])

# Variants in a region with a population cluster above a MAF threshold:
for variant in client.filter_pos_maf_all("17", start=43044000, end=43125000, maf=0.05, great=True):
    print(variant.variant_id)

client.close()  # or use `with AGVDClient(...) as client:`
```

### Pagination

Every query that can return many variants (`fetch_variant`, `find_by_gene`,
`find_by_position`, `search_variant`, `filter_pos_maf`, `filter_ids_maf`,
`filter_gene_maf`) returns one Relay page as a `Connection`:

```python
conn = client.find_by_gene(["BRCA1"], first=50)
for variant in conn:                 # this page's variants
    ...
conn.total_count                      # matches, ignoring pagination
conn.page_info.has_next_page
next_page = client.find_by_gene(["BRCA1"], first=50, after=conn.page_info.end_cursor)
```

Each of those methods has an `_all` counterpart (`find_by_gene_all`, ...)
that walks every page for you and yields `Variant` objects directly —
that's what the quickstart above uses. Use the plain method when you only
want one page, or just a count:

```python
conn = client.find_by_gene(["BRCA1"], count_only=True)
conn.total_count   # no variant records fetched at all
```

### Variant ID format

The database's internal `variantID` is stored **underscore-joined**:
`CHROMOSOME_POSITION_REFERENCE_ALTERNATE`, e.g. `17_43044295_G_A`. Every
id-based lookup does an *exact string match* against that field — no
normalisation happens server-side, so sending the wrong delimiter silently
matches nothing (no error).

Every method on `AGVDClient` that takes a `variant_id` accepts the id in
any common delimiter and normalises it for you — `"17:43044295:G:A"`,
`"17-43044295-G-A"`, and `"17_43044295_G_A"` are all equivalent. If you're
constructing ids yourself from parts, use `agvd.build_variant_id`:

```python
from agvd import build_variant_id

vid = build_variant_id("chr17", 43044295, "g", "a")  # -> "17_43044295_G_A"
```

> **Note on `cli_variant_peek`:** uniquely among these operations, the
> server echoes `variant_id` back **dash**-joined in the response (to
> match the website's URL convention) even though the input it matched
> against was underscore-joined. `AGVDClient` doesn't paper over this —
> the response model just carries whatever the server actually sent, with
> a docstring note on `cli_variant_peek` and `VariantAvailability`.

---

## API reference

| Method | Returns | Auth | Notes |
|---|---|---|---|
| `fetch_variant` / `_all` | `Connection[Variant]` | — | by `variant_id` and/or `rs_id` |
| `find_by_gene` / `_all` | `Connection[Variant]` | — | by gene name(s) |
| `find_by_position` / `_all` | `Connection[Variant]` | — | chromosome, optional exact position or range |
| `search_variant` / `_all` | `Connection[Variant]` | ✓ | free-text, variant id or gene name |
| `filter_pos_maf` / `_all` | `Connection[Variant]` | — | region + per-cluster MAF threshold |
| `filter_ids_maf` / `_all` | `Connection[Variant]` | — | id list + per-cluster MAF threshold |
| `filter_gene_maf` / `_all` | `Connection[Variant]` | — | gene(s) + per-cluster MAF threshold |
| `cli_variant_search` | `list[VariantThresholdResult]` | ✓ | ≤500 ids; **variantID only** — see note below |
| `cli_vcf_filter` | `list[VariantThresholdResult]` | ✓ | ≤500 ids; variantID or rsID |
| `file_upload` | `list[VariantThresholdResult]` | ✓ | ≤500 ids; `threshold` accepted but ignored server-side |
| `cli_variant_peek` | `list[VariantAvailability]` | ✓ | ≤500 ids; `dataset` required |
| `peek` | `list[VariantAvailability]` | ✓ | convenience: classifies a mixed id list and calls `cli_variant_peek` for each kind |
| `fetch_datasets` | `list[Dataset]` | — | |
| `create_dataset` | `Dataset` | ✓ (admin) | |
| `site_map_urls` | `list[SiteMapURL]` | — | |

**`cli_variant_search` only matches on `variant_id`** — despite the
underlying schema input type (`VCFQueryInput`) nominally accepting an
`rsID` list too, the server resolver never reads it. Passing only rsIDs
raises `AGVDUserInputError` before a network call. Use `cli_vcf_filter`
(which does resolve rsIDs server-side) if you need rsID-based threshold
lookups.

Every `*_maf` query's `great` argument means "keep clusters where
`maf >= maf`" when `True`, `maf <= maf` when `False`/omitted.

`dataset` on most methods switches on the non-SAALS genotype merge
(`AGVDClient.NON_SAALS_DATASET` / `"AGVD_24A_Main_Non_SAALS"`) when set to
that value — pass `dataset=` per call, or set a client-wide default:
`AGVDClient(token=..., dataset=agvd.NON_SAALS_DATASET)`.

---

## Error handling

Every failure raises a subclass of `agvd.AGVDError`, carrying the
server's (or a client-side) error code as `.code`:

```python
from agvd import AGVDClient, AGVDUserInputError, AGVDRateLimitError, AGVDError

try:
    client.find_by_position("99")  # not a real chromosome
except AGVDUserInputError as exc:
    print(exc.code, exc)            # BAD_USER_INPUT ...
except AGVDRateLimitError as exc:
    print("retry after", exc.retry_after, "seconds")
except AGVDError as exc:            # catches anything else, including
    print(exc.code, exc)            # error codes this version doesn't know about yet
```

| Exception | Code | Cause |
|---|---|---|
| `AGVDUserInputError` | `BAD_USER_INPUT` | Invalid argument (bad MAF/threshold, unknown chromosome, >500 ids, `first` out of 1–100, ...) |
| `AGVDAuthenticationError` | `UNAUTHENTICATED` | No/invalid/expired token |
| `AGVDForbiddenError` | `FORBIDDEN` | Valid token, wrong role (e.g. non-admin calling `create_dataset`) |
| `AGVDQueryTooComplexError` | `QUERY_TOO_COMPLEX` | Query exceeded the server's complexity budget |
| `AGVDRateLimitError` | `RATE_LIMITED` | HTTP 429 — 200 requests / 15 min per IP |
| `AGVDServerError` | `INTERNAL_SERVER_ERROR` | 5xx or a server-side DB error |
| `AGVDNetworkError` | `NETWORK_ERROR` / `TIMEOUT` | Never got a response |

5xx responses, network errors, and GraphQL `INTERNAL_SERVER_ERROR` are
retried automatically with exponential backoff (`max_retries=`, default
3); 429s wait out `Retry-After`; everything else (4xx, validation errors)
fails immediately — retrying a deterministic rejection just wastes quota.

---

## The `agvd` CLI

The CLI (`pip install "agvd[cli]"`) reads a VCF, CSV, TSV, or Excel file of variants, submits them in parallel batches to `cli_variant_search`, and writes an annotated copy plus a JSON summary. It's a thin wrapper over `AGVDClient` — see above if you'd rather call the API directly from Python.

```bash
agvd --KEY YOUR_TOKEN --INFILE variants.vcf --OUTPUT results.csv --THRESHOLD 0.05
```

### From a CSV/TSV/Excel file with a variant ID column

```bash
agvd --KEY YOUR_TOKEN --INFILE variants.csv --COLUMN variant_id --OUTPUT results.csv --THRESHOLD 0.05
```

### From a table with separate chromosome/position columns

```bash
agvd --KEY YOUR_TOKEN --INFILE variants.tsv --CHR chrom --POS position --REF ref --ALT alt --OUTPUT results.tsv --THRESHOLD 0.01
```

### Arguments

| Argument | Short | Description |
|---|---|---|
| `--KEY` | `-k` | API token (or set `AGVD_TOKEN`) |
| `--INFILE` | `-i` | Input file (`.vcf`, `.vcf.gz`, `.csv`, `.tsv`, `.xlsx`) |
| `--OUTPUT` | `-o` | Output file path (`.csv`, `.tsv`, or `.xlsx`) |
| `--THRESHOLD` | `-t` | MAF threshold for filtering (e.g. `0.05`) |
| `--DATASET` | `-d` | Dataset to query (default: `AGVD_24A_Main_Non_SAALS`) |
| `--BATCH` | `-b` | Variants per API request, 1–500 (default: `500`) |
| `--COLUMN` | `-c` | Column containing variant IDs (CSV/TSV/Excel) |
| `--CHR` / `--POS` / `--REF` / `--ALT` | | Separate chromosome/position/ref/alt column names |
| `--base-url` | | Override the GraphQL endpoint |
| `--threads` | | Parallel worker threads (default: `4`) |
| `--cache` | | Cache API responses in-process (LRU, up to 5000 batches) |
| `--dry-run` | | Parse and validate ids without making API calls |
| `--verbose` | | Debug-level logging |
| `--peek` | | Quick availability check — see below |
| `--version` | | Print the installed version and exit |

Only `variant_id`-style rows go to the batch MAF lookup (`cliVariantSearch`
doesn't support rsID — see the API reference above); rsID rows are marked
`UNSUPPORTED` in the output rather than silently dropped.

### Peek mode

Checks availability (and, for the website, a direct link) for each id, without a threshold comparison. Accepts a mix of rsIDs and variant ids:

```bash
agvd --KEY YOUR_TOKEN --peek rs334 17:43044295:G:A
# or from a file, one id per line:
agvd --KEY YOUR_TOKEN --peek --INFILE variants.txt
```

### Output

The output file keeps all original columns and adds:

| Column | Description |
|---|---|
| `AGVDCUTOFF` | `ABOVE`, `BELOW`, `NO MATCH`, `INVALID`, `UNSUPPORTED (rsID — ...)`, or `ERROR` |
| `African_MAF` | MAF value returned by the API |
| `THRESHOLD` | Threshold actually applied |
| `<ClusterName>_MAF` | Per-population cluster MAF (one column per cluster) |

A companion `<output>_summary.json` is written alongside it:

```json
{ "total": 1000, "successful": 998, "failed": 2, "success_rate": 0.998 }
```

---

## Development

```bash
pip install -e ".[dev]"
pytest
```

The package lives under `src/agvd/` (an installed-package / `src` layout,
not a flat set of top-level modules) — install it (`pip install -e .`)
rather than running from the repo root, or rely on `pythonpath = ["src"]`
in `pyproject.toml`, which `pytest` already picks up.

---

## License

MIT License — see [LICENSE](LICENSE) for details.

---

## Contact

For support or to request an AGVD API token: [agvd@afrigen-d.org](mailto:agvd@afrigen-d.org)
