Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/src/en/blast.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ Limits number of hits to return. Default: 50.
`-e` `--expect`
Defines the [expect value](https://blast.ncbi.nlm.nih.gov/Blast.cgi?CMD=Web&PAGE_TYPE=BlastDocs&DOC_TYPE=FAQ#expect) cutoff. Default: 10.0.

`-tax` `--taxid`
One or more NCBI taxonomy IDs to restrict the search to, e.g. `9606` (human) or `txid9606`. On the command line, pass several space-separated IDs (`--taxid 9606 10090`); in Python, pass an `int`/`str` or a list (`taxid=[9606, 10090]`). Multiple IDs are combined with OR. This mirrors the "Organism" field of the [NCBI web BLAST](https://blast.ncbi.nlm.nih.gov/Blast.cgi) app. Default: None.

`-eq` `--entrez_query`
Raw [NCBI Entrez query](https://www.ncbi.nlm.nih.gov/books/NBK3837/) used to limit the search, e.g. `"Homo sapiens[ORGN] NOT predicted[Title]"`. Combined with `--taxid` using AND when both are provided. Default: None.

`-o` `--out`
Path to the file the results will be saved in, e.g. path/to/directory/results.csv (or .json). Default: Standard out.
Python: `save=True` will save the output in the current working directory.
Expand Down Expand Up @@ -73,6 +79,22 @@ gget.blast("fasta.fa")
```
→ Returns the BLAST results of the first sequence contained in the fasta.fa file.

<br/><br/>
**Restrict the search to one or more organisms (taxonomy filtering):**
```bash
# Limit to human (taxid 9606)
gget blast --taxid 9606 MKWMFKEDHSLEHRCVESAKIRAKYPDRVPVIVEKVSGSQIVDIDKRKYLVPSDITVAQFMWIIRKRIQLPSEKAIFLFVDKTVPQSR
```
```python
# Python: limit to human and mouse, excluding predicted records
gget.blast(
"MKWMFKEDHSLEHRCVESAKIRAKYPDRVPVIVEKVSGSQIVDIDKRKYLVPSDITVAQFMWIIRKRIQLPSEKAIFLFVDKTVPQSR",
taxid=[9606, 10090],
entrez_query="NOT predicted[Title]",
)
```
&rarr; Returns only BLAST hits matching the requested organism(s)/Entrez query, mirroring the "Organism" filter of the NCBI web BLAST app.

#### [More examples](https://github.com/pachterlab/gget_examples)

# References
Expand Down
3 changes: 3 additions & 0 deletions docs/src/en/updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
#### *gget* officially became part of [*scverse*](https://scverse.org/) on June 9, 2026. 🥳🥳🥳

**Version ≥ 0.30.9** (XXX XX, 2026):
- [`gget blast`](blast.md): Added taxonomy/organism filtering via the new `taxid` and `entrez_query` arguments (CLI: `--taxid`/`-tax` and `--entrez_query`/`-eq`), mirroring the "Organism" field of the NCBI web BLAST app (resolves [issue 71](https://github.com/scverse/gget/issues/71)).
- `taxid` accepts a single NCBI taxonomy ID or a list (e.g. `9606`, `"txid9606"`, or `[9606, 10090]`); multiple IDs are combined with OR and a `txid<ID>[ORGN]` Entrez term is built automatically.
- `entrez_query` passes a raw NCBI Entrez query through to BLAST (`ENTREZ_QUERY`); when both are given they are combined with AND. Backward compatible — both default to `None` (no filtering).

**Version ≥ 0.30.8** (Jun 28, 2026):
- [`gget g2p`](g2p.md): Either `gene` or `--uniprot_id` is now sufficient — whichever is missing is resolved via UniProt and cached. Gene→UniProt picks the canonical reviewed human Swiss-Prot entry; the resolution and its limitations are logged. The canonical pair is **always** prepended to the result as `gene_name` / `uniprot_id` columns (and stored on `df.attrs`), so the output schema is invariant regardless of input mode. Existing call sites continue to work.
Expand Down
60 changes: 60 additions & 0 deletions gget/gget_blast.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,47 @@
)


def _build_entrez_query(
taxid: int | str | list[int | str] | None = None,
entrez_query: str | None = None,
) -> str | None:
"""Build an NCBI Entrez query string to limit a BLAST search by taxonomy.

Args:
- taxid A single NCBI taxonomy ID or a list of taxonomy IDs to
restrict the search to (e.g. 9606 or "txid9606" for human).
Multiple IDs are combined with OR.
- entrez_query A raw NCBI Entrez query string passed through as-is
(e.g. "Homo sapiens[ORGN] NOT predicted[Title]").

When both are provided they are combined with AND. Returns None if neither
is provided. Raises ValueError if a taxid is not a valid (numeric) ID.
"""
entrez_terms = []

if taxid is not None:
# Accept a single taxid or an iterable of taxids
taxid_list = list(taxid) if isinstance(taxid, (list, tuple, set)) else [taxid]
taxid_terms = []
for tid in taxid_list:
tid_str = str(tid).strip().lower().removeprefix("txid")
if not tid_str.isdigit():
raise ValueError(
f"Invalid taxid {tid!r}. Expected a numeric NCBI taxonomy ID (e.g. 9606 or 'txid9606')."
)
taxid_terms.append(f"txid{tid_str}[ORGN]")

if len(taxid_terms) == 1:
entrez_terms.append(taxid_terms[0])
else:
entrez_terms.append("(" + " OR ".join(taxid_terms) + ")")

if entrez_query is not None and str(entrez_query).strip() != "":
entrez_terms.append(str(entrez_query).strip())

return " AND ".join(entrez_terms) if entrez_terms else None


@overload
def blast(
sequence: str,
Expand All @@ -39,6 +80,8 @@ def blast(
*,
json: Literal[True],
save: bool = False,
entrez_query: str | None = None,
taxid: int | str | list[int | str] | None = None,
) -> list[dict[str, Any]] | None: ...


Expand All @@ -55,6 +98,8 @@ def blast(
wrap_text: bool = False,
json: Literal[False] = False,
save: bool = False,
entrez_query: str | None = None,
taxid: int | str | list[int | str] | None = None,
) -> pd.DataFrame | None: ...


Expand All @@ -70,6 +115,8 @@ def blast(
wrap_text: bool = False,
json: bool = False,
save: bool = False,
entrez_query: str | None = None,
taxid: int | str | list[int | str] | None = None,
) -> pd.DataFrame | list[dict[str, Any]] | None:
"""BLAST a nucleotide or amino acid sequence against any BLAST DB.

Expand All @@ -89,6 +136,12 @@ def blast(
- wrap_text If True, displays data frame with wrapped text for easy reading. Default: False.
- json If True, returns results in json format instead of data frame. Default: False.
- save If True, the data frame is saved as a csv in the current directory (default: False).
- entrez_query Raw NCBI Entrez query string used to limit the BLAST search
(e.g. "Homo sapiens[ORGN] NOT predicted[Title]"). Default: None.
- taxid NCBI taxonomy ID, or list of taxonomy IDs, to restrict the search to
(e.g. 9606 or "txid9606" for human; ["9606", "10090"] for human and mouse).
Multiple IDs are combined with OR. Combined with 'entrez_query' using AND
when both are provided. Default: None.
- verbose True/False whether to print progress information. Default True.

Returns a data frame with the BLAST results.
Expand Down Expand Up @@ -230,6 +283,12 @@ def blast(
else:
megablast = "on"

## Build Entrez query for taxonomy / organism filtering (issue #71)
# Validates taxid(s) and combines with any raw entrez_query before submission.
entrez_query_final = _build_entrez_query(taxid, entrez_query)
if entrez_query_final is not None and verbose:
logger.info(f"Limiting BLAST search to Entrez query: {entrez_query_final}")

## Submit search
# The following code was partly adapted from the Biopython BLAST NCBIWWW project written
# by Jeffrey Chang (Copyright 1999), Brad Chapman, and Chris Wroe distributed under the
Expand All @@ -247,6 +306,7 @@ def blast(
("EXPECT", expect),
("FILTER", low_comp_filt),
("MEGABLAST", megablast),
("ENTREZ_QUERY", entrez_query_final),
("CMD", "Put"),
]

Expand Down
25 changes: 25 additions & 0 deletions gget/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,29 @@ def main() -> None:
required=False,
help="Returns results in csv format instead of json.",
)
parser_blast.add_argument(
"-tax",
"--taxid",
type=str,
nargs="+",
default=None,
required=False,
help=(
"One or more NCBI taxonomy IDs to restrict the search to, e.g. 9606 (human) "
"or 'txid9606'. Multiple IDs (space-separated) are combined with OR."
),
)
parser_blast.add_argument(
"-eq",
"--entrez_query",
type=str,
default=None,
required=False,
help=(
'Raw NCBI Entrez query to limit the search, e.g. "Homo sapiens[ORGN] NOT predicted[Title]". '
"Combined with --taxid using AND when both are provided."
),
)
parser_blast.add_argument(
"-o",
"--out",
Expand Down Expand Up @@ -3111,6 +3134,8 @@ def main() -> None:
megablast=args.megablast_off,
verbose=args.quiet,
json=args.csv,
entrez_query=args.entrez_query,
taxid=args.taxid,
)

# Check if the function returned something
Expand Down
10 changes: 10 additions & 0 deletions tests/fixtures/test_blast.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,15 @@
"database": "banana"
},
"expected_result": "ValueError"
},
"test_blast_bad_taxid": {
"type": "error",
"args": {
"sequence": "MSKGEELFTGVVPILVELDGDVNGQKFSVSGEGEGDATYGKL",
"limit": 1,
"taxid": "banana"
},
"expected_result": "ValueError",
"expected_msg": "Invalid taxid 'banana'. Expected a numeric NCBI taxonomy ID (e.g. 9606 or 'txid9606')."
}
}
43 changes: 42 additions & 1 deletion tests/test_blast.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
import unittest

from gget.gget_blast import blast
from gget.gget_blast import _build_entrez_query, blast

from .from_json import from_json

Expand All @@ -12,3 +12,44 @@

class TestBlast(unittest.TestCase, metaclass=from_json(blast_dict, blast)):
pass # all tests are loaded from json


class TestBuildEntrezQuery(unittest.TestCase):
"""Network-free tests for the taxonomy/Entrez query builder (issue #71)."""

def test_none(self):
self.assertIsNone(_build_entrez_query())
self.assertIsNone(_build_entrez_query(taxid=None, entrez_query=None))
self.assertIsNone(_build_entrez_query(entrez_query=" "))

def test_single_taxid_int(self):
self.assertEqual(_build_entrez_query(taxid=9606), "txid9606[ORGN]")

def test_single_taxid_str_and_prefix(self):
self.assertEqual(_build_entrez_query(taxid="9606"), "txid9606[ORGN]")
self.assertEqual(_build_entrez_query(taxid="txid9606"), "txid9606[ORGN]")
self.assertEqual(_build_entrez_query(taxid="TXID9606"), "txid9606[ORGN]")

def test_multiple_taxids_or(self):
self.assertEqual(
_build_entrez_query(taxid=[9606, "10090"]),
"(txid9606[ORGN] OR txid10090[ORGN])",
)

def test_entrez_query_only(self):
self.assertEqual(
_build_entrez_query(entrez_query="Homo sapiens[ORGN]"),
"Homo sapiens[ORGN]",
)

def test_taxid_and_entrez_query_combined(self):
self.assertEqual(
_build_entrez_query(taxid=9606, entrez_query="NOT predicted[Title]"),
"txid9606[ORGN] AND NOT predicted[Title]",
)

def test_invalid_taxid_raises(self):
with self.assertRaises(ValueError):
_build_entrez_query(taxid="banana")
with self.assertRaises(ValueError):
_build_entrez_query(taxid=[9606, "banana"])
Loading