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
20 changes: 20 additions & 0 deletions docs/src/en/blast.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,26 @@ 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.

The following arguments expose the NCBI web BLAST ["Algorithm parameters"](https://blast.ncbi.nlm.nih.gov/Blast.cgi) so `gget blast` more fully matches the web app. Each defaults to `None`, in which case the NCBI server default is used.

`-ws` `--word_size`
Length of the seed words used for the search (`WORD_SIZE`). Default: server default.

`-gc` `--gapcosts`
Gap costs as `"open extend"`, e.g. `"11 1"` (`GAPCOSTS`). Default: server default.

`-mx` `--matrix`
Protein scoring matrix (`MATRIX`): one of PAM30, PAM70, PAM250, BLOSUM80, BLOSUM62, BLOSUM50, BLOSUM45, BLOSUM90. Default: server default.

`-nr` `--nucl_reward`
Reward for a nucleotide match (blastn only) (`NUCL_REWARD`). Default: server default.

`-np` `--nucl_penalty`
Penalty for a nucleotide mismatch (blastn only) (`NUCL_PENALTY`). Default: server default.

`-pi` `--perc_identity`
Percent identity cutoff between 0 and 100 (`PERC_IDENT`). Default: server default.

`-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
1 change: 1 addition & 0 deletions docs/src/en/updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#### *gget* officially became part of [*scverse*](https://scverse.org/) on June 9, 2026. 🥳🥳🥳

**Version ≥ 0.30.9** (XXX XX, 2026):
- [`gget blast`](blast.md): Exposed the NCBI web BLAST "Algorithm parameters" so `gget blast` more fully matches the web app (resolves [issue 58](https://github.com/scverse/gget/issues/58)). New arguments `word_size`, `gapcosts`, `matrix`, `nucl_reward`, `nucl_penalty`, and `perc_identity` (CLI: `-ws`/`--word_size`, `-gc`/`--gapcosts`, `-mx`/`--matrix`, `-nr`/`--nucl_reward`, `-np`/`--nucl_penalty`, `-pi`/`--perc_identity`) are validated and forwarded to the BLAST URL API (`WORD_SIZE`, `GAPCOSTS`, `MATRIX`, `NUCL_REWARD`, `NUCL_PENALTY`, `PERC_IDENT`). All default to `None` (server default), so existing behavior is unchanged.

**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
118 changes: 118 additions & 0 deletions gget/gget_blast.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,83 @@
BLAST_URL,
)

# Protein scoring matrices supported by the NCBI web BLAST app
BLAST_MATRICES = [
"PAM30",
"PAM70",
"PAM250",
"BLOSUM80",
"BLOSUM62",
"BLOSUM50",
"BLOSUM45",
"BLOSUM90",
]


def _build_algorithm_params(
word_size: int | None = None,
gapcosts: str | None = None,
matrix: str | None = None,
nucl_reward: int | None = None,
nucl_penalty: int | None = None,
perc_identity: float | None = None,
) -> list[tuple[str, Any]]:
"""Validate and assemble extra NCBI BLAST "Algorithm parameters" (issue #58).

Mirrors the algorithm-parameter panel of the NCBI web BLAST app. Returns a
list of (KEY, value) tuples for the BLAST URL API, omitting any unset
(None) parameter so default server behavior is preserved.

Args:
- word_size Length of the seed words (WORD_SIZE). Positive integer.
- gapcosts Gap costs as "open extend", e.g. "11 1" (GAPCOSTS).
- matrix Protein scoring matrix, e.g. "BLOSUM62" (MATRIX).
- nucl_reward blastn match reward, positive integer (NUCL_REWARD).
- nucl_penalty blastn mismatch penalty, negative integer (NUCL_PENALTY).
- perc_identity Percent identity cutoff, 0-100 (PERC_IDENT).

Raises ValueError on invalid values.
"""
params: list[tuple[str, Any]] = []

if word_size is not None:
if not isinstance(word_size, int) or isinstance(word_size, bool) or word_size < 2:
raise ValueError(f"Invalid word_size {word_size!r}. Expected an integer >= 2.")
params.append(("WORD_SIZE", word_size))

if gapcosts is not None:
parts = str(gapcosts).split()
if len(parts) != 2 or not all(p.lstrip("-").isdigit() for p in parts):
raise ValueError(f"Invalid gapcosts {gapcosts!r}. Expected two integers as 'open extend', e.g. '11 1'.")
params.append(("GAPCOSTS", f"{parts[0]} {parts[1]}"))

if matrix is not None:
matrix_upper = str(matrix).upper()
if matrix_upper not in BLAST_MATRICES:
raise ValueError(f"Invalid matrix {matrix!r}. Expected one of: {', '.join(BLAST_MATRICES)}")
params.append(("MATRIX", matrix_upper))

if nucl_reward is not None:
if not isinstance(nucl_reward, int) or isinstance(nucl_reward, bool):
raise ValueError(f"Invalid nucl_reward {nucl_reward!r}. Expected an integer.")
params.append(("NUCL_REWARD", nucl_reward))

if nucl_penalty is not None:
if not isinstance(nucl_penalty, int) or isinstance(nucl_penalty, bool):
raise ValueError(f"Invalid nucl_penalty {nucl_penalty!r}. Expected an integer.")
params.append(("NUCL_PENALTY", nucl_penalty))

if perc_identity is not None:
if (
not isinstance(perc_identity, (int, float))
or isinstance(perc_identity, bool)
or not (0 <= perc_identity <= 100)
):
raise ValueError(f"Invalid perc_identity {perc_identity!r}. Expected a number between 0 and 100.")
params.append(("PERC_IDENT", perc_identity))

return params


@overload
def blast(
Expand All @@ -39,6 +116,12 @@ def blast(
*,
json: Literal[True],
save: bool = False,
word_size: int | None = None,
gapcosts: str | None = None,
matrix: str | None = None,
nucl_reward: int | None = None,
nucl_penalty: int | None = None,
perc_identity: float | None = None,
) -> list[dict[str, Any]] | None: ...


Expand All @@ -55,6 +138,12 @@ def blast(
wrap_text: bool = False,
json: Literal[False] = False,
save: bool = False,
word_size: int | None = None,
gapcosts: str | None = None,
matrix: str | None = None,
nucl_reward: int | None = None,
nucl_penalty: int | None = None,
perc_identity: float | None = None,
) -> pd.DataFrame | None: ...


Expand All @@ -70,6 +159,12 @@ def blast(
wrap_text: bool = False,
json: bool = False,
save: bool = False,
word_size: int | None = None,
gapcosts: str | None = None,
matrix: str | None = None,
nucl_reward: int | None = None,
nucl_penalty: int | None = None,
perc_identity: float | None = None,
) -> pd.DataFrame | list[dict[str, Any]] | None:
"""BLAST a nucleotide or amino acid sequence against any BLAST DB.

Expand All @@ -89,8 +184,20 @@ 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).
- word_size int or None. Length of the seed words used for the search (WORD_SIZE).
Mirrors the "Word size" option of the NCBI web BLAST app. Default: None (server default).
- gapcosts str or None. Gap costs as "open extend" (e.g. "11 1") (GAPCOSTS). Default: None.
- matrix str or None. Protein scoring matrix (e.g. "BLOSUM62") (MATRIX).
One of: PAM30, PAM70, PAM250, BLOSUM80, BLOSUM62, BLOSUM50, BLOSUM45, BLOSUM90. Default: None.
- nucl_reward int or None. Reward for a nucleotide match (blastn only) (NUCL_REWARD). Default: None.
- nucl_penalty int or None. Penalty for a nucleotide mismatch (blastn only) (NUCL_PENALTY). Default: None.
- perc_identity float or None. Percent identity cutoff between 0 and 100 (PERC_IDENT). Default: None.
- verbose True/False whether to print progress information. Default True.

The word_size, gapcosts, matrix, nucl_reward, nucl_penalty, and perc_identity
arguments expose the NCBI web BLAST "Algorithm parameters" so gget blast more
fully matches the web app (issue #58).

Returns a data frame with the BLAST results.

NCBI server rule:
Expand Down Expand Up @@ -230,6 +337,16 @@ def blast(
else:
megablast = "on"

## Validate and assemble extra NCBI web BLAST "Algorithm parameters" (issue #58)
algorithm_params = _build_algorithm_params(
word_size=word_size,
gapcosts=gapcosts,
matrix=matrix,
nucl_reward=nucl_reward,
nucl_penalty=nucl_penalty,
perc_identity=perc_identity,
)

## 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 +364,7 @@ def blast(
("EXPECT", expect),
("FILTER", low_comp_filt),
("MEGABLAST", megablast),
*algorithm_params,
("CMD", "Put"),
]

Expand Down
55 changes: 55 additions & 0 deletions gget/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,55 @@ def main() -> None:
required=False,
help="float or None. An expect value cutoff. Default 10.0.",
)
parser_blast.add_argument(
"-ws",
"--word_size",
type=int,
default=None,
required=False,
help="Length of the seed words for the search (WORD_SIZE in the NCBI web BLAST app). Default: server default.",
)
parser_blast.add_argument(
"-gc",
"--gapcosts",
type=str,
default=None,
required=False,
help="Gap costs as 'open extend', e.g. '11 1' (GAPCOSTS). Default: server default.",
)
parser_blast.add_argument(
"-mx",
"--matrix",
type=str,
default=None,
required=False,
choices=["PAM30", "PAM70", "PAM250", "BLOSUM80", "BLOSUM62", "BLOSUM50", "BLOSUM45", "BLOSUM90"],
help="Protein scoring matrix (MATRIX), e.g. BLOSUM62. Default: server default.",
)
parser_blast.add_argument(
"-nr",
"--nucl_reward",
type=int,
default=None,
required=False,
help="Reward for a nucleotide match (blastn only) (NUCL_REWARD). Default: server default.",
)
parser_blast.add_argument(
"-np",
"--nucl_penalty",
type=int,
default=None,
required=False,
help="Penalty for a nucleotide mismatch (blastn only) (NUCL_PENALTY). Default: server default.",
)
parser_blast.add_argument(
"-pi",
"--perc_identity",
type=float,
default=None,
required=False,
help="Percent identity cutoff between 0 and 100 (PERC_IDENT). Default: server default.",
)
parser_blast.add_argument(
"-lcf",
"--low_comp_filt",
Expand Down Expand Up @@ -3111,6 +3160,12 @@ def main() -> None:
megablast=args.megablast_off,
verbose=args.quiet,
json=args.csv,
word_size=args.word_size,
gapcosts=args.gapcosts,
matrix=args.matrix,
nucl_reward=args.nucl_reward,
nucl_penalty=args.nucl_penalty,
perc_identity=args.perc_identity,
)

# Check if the function returned something
Expand Down
20 changes: 20 additions & 0 deletions tests/fixtures/test_blast.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,25 @@
"database": "banana"
},
"expected_result": "ValueError"
},
"test_blast_bad_matrix": {
"type": "error",
"args": {
"sequence": "MSKGEELFTGVVPILVELDGDVNGQKFSVSGEGEGDATYGKL",
"limit": 1,
"matrix": "banana"
},
"expected_result": "ValueError",
"expected_msg": "Invalid matrix 'banana'. Expected one of: PAM30, PAM70, PAM250, BLOSUM80, BLOSUM62, BLOSUM50, BLOSUM45, BLOSUM90"
},
"test_blast_bad_word_size": {
"type": "error",
"args": {
"sequence": "MSKGEELFTGVVPILVELDGDVNGQKFSVSGEGEGDATYGKL",
"limit": 1,
"word_size": 1
},
"expected_result": "ValueError",
"expected_msg": "Invalid word_size 1. Expected an integer >= 2."
}
}
65 changes: 64 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_algorithm_params, blast

from .from_json import from_json

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

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


class TestBuildAlgorithmParams(unittest.TestCase):
"""Network-free tests for the web-BLAST algorithm parameters (issue #58)."""

def test_empty(self):
self.assertEqual(_build_algorithm_params(), [])

def test_all_params(self):
params = _build_algorithm_params(
word_size=11,
gapcosts="11 1",
matrix="blosum62",
nucl_reward=1,
nucl_penalty=-2,
perc_identity=90.0,
)
self.assertEqual(
params,
[
("WORD_SIZE", 11),
("GAPCOSTS", "11 1"),
("MATRIX", "BLOSUM62"),
("NUCL_REWARD", 1),
("NUCL_PENALTY", -2),
("PERC_IDENT", 90.0),
],
)

def test_matrix_uppercased(self):
self.assertEqual(_build_algorithm_params(matrix="pam30"), [("MATRIX", "PAM30")])

def test_invalid_word_size(self):
with self.assertRaises(ValueError):
_build_algorithm_params(word_size=1)
with self.assertRaises(ValueError):
_build_algorithm_params(word_size="big")

def test_invalid_gapcosts(self):
with self.assertRaises(ValueError):
_build_algorithm_params(gapcosts="11")
with self.assertRaises(ValueError):
_build_algorithm_params(gapcosts="a b")

def test_invalid_matrix(self):
with self.assertRaises(ValueError):
_build_algorithm_params(matrix="banana")

def test_invalid_perc_identity(self):
with self.assertRaises(ValueError):
_build_algorithm_params(perc_identity=150)

def test_invalid_nucl_reward(self):
with self.assertRaises(ValueError):
_build_algorithm_params(nucl_reward="two")
with self.assertRaises(ValueError):
_build_algorithm_params(nucl_reward=True)

def test_invalid_nucl_penalty(self):
with self.assertRaises(ValueError):
_build_algorithm_params(nucl_penalty="minus three")
with self.assertRaises(ValueError):
_build_algorithm_params(nucl_penalty=False)
Loading