diff --git a/.github/badges/tests.json b/.github/badges/tests.json
index 023be6a65..0f66c31ae 100644
--- a/.github/badges/tests.json
+++ b/.github/badges/tests.json
@@ -1 +1 @@
-{"schemaVersion": 1, "label": "tests", "message": "386/400 passing", "color": "yellow"}
+{"schemaVersion": 1, "label": "tests", "message": "419/422 passing", "color": "brightgreen"}
diff --git a/.mypy-baseline.txt b/.mypy-baseline.txt
new file mode 100644
index 000000000..f7944e2d9
--- /dev/null
+++ b/.mypy-baseline.txt
@@ -0,0 +1,725 @@
+gget/utils.py:0: error: Item "NavigableString" of "Tag | NavigableString | None" has no attribute "get" [union-attr]
+gget/utils.py:0: error: Item "None" of "Tag | NavigableString | None" has no attribute "get" [union-attr]
+gget/utils.py:0: error: Item "list[str]" of "str | list[str] | Any | None" has no attribute "split" [union-attr]
+gget/utils.py:0: error: Item "None" of "str | list[str] | Any | None" has no attribute "split" [union-attr]
+gget/utils.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "bool") [assignment]
+gget/utils.py:0: error: Incompatible types in assignment (expression has type "int", variable has type "bool") [assignment]
+gget/utils.py:0: error: Unsupported operand types for + ("bool" and "str") [operator]
+gget/utils.py:0: error: Argument 1 to "append" of "list" has incompatible type "bool"; expected "Literal[True]" [arg-type]
+gget/utils.py:0: error: Incompatible return value type (got "tuple[list[str], list[Literal[True]]]", expected "tuple[list[str], list[str]]") [return-value]
+gget/utils.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "Index[str]") [assignment]
+gget/utils.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "Index[str]") [assignment]
+gget/utils.py:0: error: Incompatible types in assignment (expression has type "ndarray[tuple[Any, ...], dtype[Any]]", variable has type "list[Any]") [assignment]
+gget/utils.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "list[Any]") [assignment]
+gget/utils.py:0: error: Incompatible types in assignment (expression has type "list[ndarray[tuple[int], dtype[Any]] | ExtensionArray]", target has type "str | bytes | date | datetime | timedelta | <7 more items> | complex | integer[Any] | floating[Any] | complexfloating[Any, Any] | NAType | NaTType | None | Sequence[str | bytes | date | datetime | timedelta | <7 more items> | complex | integer[Any] | floating[Any] | complexfloating[Any, Any] | NAType | NaTType | None] | Sequence[Sequence[str | bytes | date | datetime | timedelta | <7 more items> | complex | integer[Any] | floating[Any] | complexfloating[Any, Any] | NAType | NaTType | None]] | Mapping[Any, str | bytes | date | datetime | timedelta | <7 more items> | complex | integer[Any] | floating[Any] | complexfloating[Any, Any] | NAType | NaTType | None] | ExtensionArray | ndarray[tuple[Any, ...], dtype[Any]] | IndexOpsMixin[Any, Any] | DataFrame") [assignment]
+gget/utils.py:0: error: Incompatible types in assignment (expression has type "list[ndarray[tuple[int], dtype[Any]] | ExtensionArray]", target has type "str | bytes | date | datetime | timedelta | <7 more items> | complex | integer[Any] | floating[Any] | complexfloating[Any, Any] | NAType | NaTType | None | Sequence[str | bytes | date | datetime | timedelta | <7 more items> | complex | integer[Any] | floating[Any] | complexfloating[Any, Any] | NAType | NaTType | None] | Sequence[Sequence[str | bytes | date | datetime | timedelta | <7 more items> | complex | integer[Any] | floating[Any] | complexfloating[Any, Any] | NAType | NaTType | None]] | Mapping[Any, str | bytes | date | datetime | timedelta | <7 more items> | complex | integer[Any] | floating[Any] | complexfloating[Any, Any] | NAType | NaTType | None] | ExtensionArray | ndarray[tuple[Any, ...], dtype[Any]] | IndexOpsMixin[Any, Any] | DataFrame") [assignment]
+gget/utils.py:0: error: Item "None" of "Tag | None" has no attribute "findAll" [union-attr]
+gget/utils.py:0: error: Item "None" of "Tag | None" has no attribute "findAll" [union-attr]
+gget/utils.py:0: error: Missing return statement [return]
+gget/utils.py:0: error: Item "None" of "Tag | None" has no attribute "findAll" [union-attr]
+gget/utils.py:0: error: Item "None" of "Tag | None" has no attribute "findAll" [union-attr]
+gget/utils.py:0: error: Item "None" of "Tag | None" has no attribute "findAll" [union-attr]
+gget/utils.py:0: error: Argument 1 to "sorted" has incompatible type "list[list[Any]]"; expected "Iterable[str]" [arg-type]
+gget/gget_search.py:0: error: Return value expected [return-value]
+gget/gget_search.py:0: error: Incompatible types in assignment (expression has type "list[Never]", variable has type "str") [assignment]
+gget/gget_search.py:0: error: "str" has no attribute "append" [attr-defined]
+gget/gget_search.py:0: error: "Series[Any]" not callable [operator]
+gget/gget_search.py:0: error: "Series[Any]" not callable [operator]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "lower" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_ref.py:0: error: Item "None" of "str | None" has no attribute "split" [union-attr]
+gget/gget_opentargets.py:0: error: Incompatible types in assignment (expression has type "int", target has type "str") [assignment]
+gget/gget_mutate.py:0: error: Argument 1 to "read_fasta" has incompatible type "str | list[str]"; expected "str" [arg-type]
+gget/gget_mutate.py:0: error: Incompatible types in assignment (expression has type "DataFrame", variable has type "str | list[str]") [assignment]
+gget/gget_mutate.py:0: error: "str" has no attribute "columns" [attr-defined]
+gget/gget_mutate.py:0: error: Incompatible types in assignment (expression has type "DataFrame", variable has type "str | list[str]") [assignment]
+gget/gget_mutate.py:0: error: "str" has no attribute "columns" [attr-defined]
+gget/gget_mutate.py:0: error: Incompatible types in assignment (expression has type "DataFrame", variable has type "str | list[str]") [assignment]
+gget/gget_mutate.py:0: error: Incompatible types in assignment (expression has type "DataFrame", variable has type "str | list[str]") [assignment]
+gget/gget_mutate.py:0: error: Incompatible types in assignment (expression has type "DataFrame", variable has type "str | list[str]") [assignment]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "isna" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Item "str" of "str | list[str]" has no attribute "dropna" [union-attr]
+gget/gget_mutate.py:0: error: Item "list[str]" of "str | list[str]" has no attribute "dropna" [union-attr]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "Any" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "apply" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Incompatible types in assignment (expression has type "DataFrame", variable has type "str | list[str]") [assignment]
+gget/gget_mutate.py:0: error: Argument 1 to "add_mutation_type" has incompatible type "str | list[str]"; expected "DataFrame" [arg-type]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "Any" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "map" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "isin" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "values" [attr-defined]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Item "str" of "str | list[str]" has no attribute "dropna" [union-attr]
+gget/gget_mutate.py:0: error: Item "list[str]" of "str | list[str]" has no attribute "dropna" [union-attr]
+gget/gget_mutate.py:0: error: Item "str" of "str | list[str]" has no attribute "shape" [union-attr]
+gget/gget_mutate.py:0: error: Item "list[str]" of "str | list[str]" has no attribute "shape" [union-attr]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "str | Any" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "str" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "str" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "str" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "str" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "str" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | Any") [index]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "str" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "isna" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "dropna" [union-attr]
+gget/gget_mutate.py:0: error: Item "str" of "str | list[str]" has no attribute "empty" [union-attr]
+gget/gget_mutate.py:0: error: Item "list[str]" of "str | list[str]" has no attribute "empty" [union-attr]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "str" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "Any" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "Any" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "str | Any" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Item "str" of "str | list[str]" has no attribute "loc" [union-attr]
+gget/gget_mutate.py:0: error: Item "list[str]" of "str | list[str]" has no attribute "loc" [union-attr]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "isna" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "list[str]", "Any" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "list[str]" [call-overload]
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "astype" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "list[str]" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: Unsupported operand types for - (likely involving Union) [operator]
+gget/gget_mutate.py:0: note: Left operand is of type "str | Any"
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "int | Any" [call-overload]
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: Unsupported operand types for - (likely involving Union) [operator]
+gget/gget_mutate.py:0: note: Left operand is of type "str | Any"
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "int | Any" [call-overload]
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "Any" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "apply" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Item "bool" of "bool | Any" has no attribute "sum" [union-attr]
+gget/gget_mutate.py:0: error: "str" has no attribute "empty" [attr-defined]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported left operand type for - ("str") [operator]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported left operand type for - ("str") [operator]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "str" [attr-defined]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "isna" [attr-defined]
+gget/gget_mutate.py:0: error: Invalid index type "list[str]" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "empty" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: Unsupported operand types for - ("str" and "int") [operator]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "combine" [attr-defined]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported operand types for + ("str" and "int") [operator]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "min" [attr-defined]
+gget/gget_mutate.py:0: error: Invalid index type "list[str]" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Item "None" of "str | None" has no attribute "endswith" [union-attr]
+gget/gget_mutate.py:0: error: "str" has no attribute "columns" [attr-defined]
+gget/gget_mutate.py:0: error: Incompatible types in assignment (expression has type "DataFrame", variable has type "str | list[str]") [assignment]
+gget/gget_mutate.py:0: error: Argument 1 to "merge_gtf_transcript_locations_into_cosmic_csv" has incompatible type "str"; expected "DataFrame" [arg-type]
+gget/gget_mutate.py:0: error: Argument "gtf_transcript_id_column" to "merge_gtf_transcript_locations_into_cosmic_csv" has incompatible type "str | None"; expected "str" [arg-type]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: Unsupported operand types for - ("str" and "int") [operator]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "max" [attr-defined]
+gget/gget_mutate.py:0: error: Invalid index type "list[str]" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "min" [attr-defined]
+gget/gget_mutate.py:0: error: Invalid index type "list[str]" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "progress_apply" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "apply" [attr-defined]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "fillna" [attr-defined]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "fillna" [attr-defined]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: "bool" has no attribute "any" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "bool" has no attribute "any" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: "str" has no attribute "loc" [attr-defined]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "sum" [attr-defined]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported operand type for ~ ("str") [operator]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "apply" [attr-defined]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "max" [attr-defined]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported operand types for < ("str" and "int") [operator]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported operand types for >= ("str" and "int") [operator]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "str" [attr-defined]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported operand types for > ("str" and "int") [operator]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported operand types for <= ("str" and "int") [operator]
+gget/gget_mutate.py:0: error: "str" has no attribute "drop" [attr-defined]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Argument 1 to "cut" has incompatible type "str | Any"; expected "Series[Timestamp]" [arg-type]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Argument "bins" to "cut" has incompatible type "range"; expected "int | Sequence[datetime64[Any] | Timestamp] | IntervalIndex[Interval[Timestamp]] | DatetimeIndex | Series[Timestamp]" [arg-type]
+gget/gget_mutate.py:0: error: Item "str" of "str | list[str]" has no attribute "shape" [union-attr]
+gget/gget_mutate.py:0: error: Item "list[str]" of "str | list[str]" has no attribute "shape" [union-attr]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "Any" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "progress_apply" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Argument "end" to "translate_sequence" has incompatible type "Any | int | str"; expected "int" [arg-type]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "Any" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "apply" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Argument "end" to "translate_sequence" has incompatible type "Any | int | str"; expected "int" [arg-type]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "Any" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "progress_apply" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Argument "end" to "translate_sequence" has incompatible type "Any | int | str"; expected "int" [arg-type]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "Any" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "apply" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Argument "end" to "translate_sequence" has incompatible type "Any | int | str"; expected "int" [arg-type]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Item "str" of "str | list[str]" has no attribute "columns" [union-attr]
+gget/gget_mutate.py:0: error: Item "list[str]" of "str | list[str]" has no attribute "columns" [union-attr]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "int" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: Item "str" of "str | list[str]" has no attribute "columns" [union-attr]
+gget/gget_mutate.py:0: error: Item "list[str]" of "str | list[str]" has no attribute "columns" [union-attr]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "str | Any" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "Any" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "Any" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "list[str]" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Invalid index type "list[str]" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "groupby" [union-attr]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "groupby" [union-attr]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "Any" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "apply" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "Any" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "str" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "Any" [call-overload]
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported operand types for + (likely involving Union) [operator]
+gget/gget_mutate.py:0: note: Left operand is of type "str | Any"
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str | list[str]") [index]
+gget/gget_mutate.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "str", "str | Any" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __setitem__(self, SupportsIndex, str, /) -> None
+gget/gget_mutate.py:0: note: def __setitem__(self, slice, Iterable[str], /) -> None
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Argument 1 to "replace" of "str" has incompatible type "int"; expected "str" [arg-type]
+gget/gget_mutate.py:0: error: Argument 2 to "replace" of "str" has incompatible type "float"; expected "str" [arg-type]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Item "str" of "str | Any" has no attribute "sum" [union-attr]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Item "str" of "str | list[str]" has no attribute "drop" [union-attr]
+gget/gget_mutate.py:0: error: Item "list[str]" of "str | list[str]" has no attribute "drop" [union-attr]
+gget/gget_mutate.py:0: error: Item "bool" of "bool | Any" has no attribute "sum" [union-attr]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "str" [call-overload]
+gget/gget_mutate.py:0: note: Possible overload variants:
+gget/gget_mutate.py:0: note: def __getitem__(self, SupportsIndex, /) -> str
+gget/gget_mutate.py:0: note: def __getitem__(self, slice, /) -> list[str]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "str" [attr-defined]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "to_csv" [attr-defined]
+gget/gget_mutate.py:0: error: Unsupported target for indexed assignment ("str") [index]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "values" [attr-defined]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: Invalid index type "list[str]" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_mutate.py:0: error: "str" has no attribute "values" [attr-defined]
+gget/gget_mutate.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_info.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "Index[str]") [assignment]
+gget/gget_info.py:0: error: Item "None" of "Tag | NavigableString | None" has no attribute "text" [union-attr]
+gget/gget_info.py:0: error: Item "None" of "Tag | NavigableString | None" has no attribute "text" [union-attr]
+gget/gget_info.py:0: error: Item "NavigableString" of "Tag | NavigableString | None" has no attribute "get" [union-attr]
+gget/gget_info.py:0: error: Item "None" of "Tag | NavigableString | None" has no attribute "get" [union-attr]
+gget/gget_info.py:0: error: Incompatible types in assignment (expression has type "str | list[str] | Any | None", variable has type "float") [assignment]
+gget/gget_info.py:0: error: Item "None" of "Tag | NavigableString | None" has no attribute "find" [union-attr]
+gget/gget_info.py:0: error: Unexpected keyword argument "string" for "find" of "str" [call-arg]
+gget/gget_info.py:0: error: Item "None" of "Tag | NavigableString | int | Any | None" has no attribute "find_next_sibling" [union-attr]
+gget/gget_info.py:0: error: Item "int" of "Tag | NavigableString | int | Any | None" has no attribute "find_next_sibling" [union-attr]
+gget/gget_info.py:0: error: Item "None" of "Tag | NavigableString | Any | None" has no attribute "text" [union-attr]
+gget/gget_info.py:0: error: Incompatible types in assignment (expression has type "str | Any", variable has type "float") [assignment]
+gget/gget_info.py:0: error: Item "None" of "Tag | NavigableString | None" has no attribute "find" [union-attr]
+gget/gget_info.py:0: error: Unexpected keyword argument "string" for "find" of "str" [call-arg]
+gget/gget_info.py:0: error: Item "None" of "Tag | NavigableString | int | Any | None" has no attribute "find_next_sibling" [union-attr]
+gget/gget_info.py:0: error: Item "int" of "Tag | NavigableString | int | Any | None" has no attribute "find_next_sibling" [union-attr]
+gget/gget_info.py:0: error: Item "None" of "Tag | NavigableString | Any | None" has no attribute "text" [union-attr]
+gget/gget_info.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "Index[str]") [assignment]
+gget/gget_info.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "Index[str]") [assignment]
+gget/gget_info.py:0: error: Incompatible types in assignment (expression has type "str | Any", variable has type "list[Any]") [assignment]
+gget/gget_info.py:0: error: Argument 1 to "append" of "list" has incompatible type "float"; expected "str" [arg-type]
+gget/gget_info.py:0: error: Argument 1 to "append" of "list" has incompatible type "float"; expected "str" [arg-type]
+gget/gget_info.py:0: error: Argument 1 to "append" of "list" has incompatible type "float"; expected "str" [arg-type]
+gget/gget_gpt.py:0: error: Return value expected [return-value]
+gget/gget_g2p.py:0: error: Argument "params" to "get" has incompatible type "dict[str, object]"; expected "SupportsItems[str | bytes | int | float, str | bytes | int | float | Iterable[str | bytes | int | float] | None] | tuple[str | bytes | int | float, str | bytes | int | float | Iterable[str | bytes | int | float] | None] | Iterable[tuple[str | bytes | int | float, str | bytes | int | float | Iterable[str | bytes | int | float] | None]] | str | bytes | None" [arg-type]
+gget/gget_g2p.py:0: error: Unused "type: ignore" comment [unused-ignore]
+gget/gget_g2p.py:0: error: Unsupported operand types for + ("slice" and "int") [operator]
+gget/gget_g2p.py:0: note: Left operand is of type "int | slice | ndarray[tuple[int], dtype[numpy.bool[builtins.bool]]]"
+gget/gget_g2p.py:0: error: Argument 1 to "insert" of "DataFrame" has incompatible type "int | ndarray[tuple[Any, ...], dtype[signedinteger[Any]]]"; expected "int" [arg-type]
+gget/gget_cbio.py:0: error: Cannot assign to a method [method-assign]
+gget/gget_cbio.py:0: error: "type[DataFrame]" has no attribute "applymap" [attr-defined]
+gget/gget_cbio.py:0: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
+gget/gget_cbio.py:0: error: "Series[Any]" not callable [operator]
+gget/gget_cbio.py:0: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
+gget/gget_cbio.py:0: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
+gget/gget_cbio.py:0: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
+gget/gget_cbio.py:0: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
+gget/gget_cbio.py:0: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
+gget/gget_cbio.py:0: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
+gget/gget_blat.py:0: error: Return value expected [return-value]
+gget/gget_blat.py:0: error: Return value expected [return-value]
+gget/gget_blast.py:0: error: Missing return statement [return]
+gget/gget_blast.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "bool") [assignment]
+gget/gget_blast.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "bool") [assignment]
+gget/gget_blast.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "bool") [assignment]
+gget/gget_blast.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "bool") [assignment]
+gget/gget_blast.py:0: error: Return value expected [return-value]
+gget/gget_blast.py:0: error: Return value expected [return-value]
+gget/gget_blast.py:0: error: Return value expected [return-value]
+gget/gget_blast.py:0: error: Return value expected [return-value]
+gget/gget_bgee.py:0: error: Argument 1 to "_bgee_orthologs" has incompatible type "str | list[str]"; expected "str" [arg-type]
+gget/compile.py:0: error: Item "None" of "IO[bytes] | None" has no attribute "read" [union-attr]
+gget/compile.py:0: error: Item "None" of "IO[bytes] | None" has no attribute "read" [union-attr]
+gget/gget_setup.py:0: error: Item "None" of "IO[bytes] | None" has no attribute "read" [union-attr]
+gget/gget_setup.py:0: error: Item "None" of "IO[bytes] | None" has no attribute "read" [union-attr]
+gget/gget_setup.py:0: error: Item "None" of "IO[bytes] | None" has no attribute "read" [union-attr]
+gget/gget_setup.py:0: error: Item "None" of "IO[bytes] | None" has no attribute "read" [union-attr]
+gget/gget_seq.py:0: error: Return value expected [return-value]
+gget/gget_seq.py:0: error: Argument 2 to "rest_query" has incompatible type "dict[str, list[str]]"; expected "str" [arg-type]
+gget/gget_seq.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "dict[str, list[str]]") [assignment]
+gget/gget_seq.py:0: error: Argument 2 to "rest_query" has incompatible type "dict[str, list[str]]"; expected "str" [arg-type]
+gget/gget_muscle.py:0: error: Value of type variable "AnyStr" of "abspath" cannot be "str | list[str]" [type-var]
+gget/gget_muscle.py:0: error: Item "list[str]" of "str | list[str]" has no attribute "replace" [union-attr]
+gget/gget_muscle.py:0: error: Item "None" of "IO[bytes] | None" has no attribute "read" [union-attr]
+gget/gget_muscle.py:0: error: Argument 1 to "Popen" has incompatible type "list[str | list[str]]"; expected "str | bytes | PathLike[str] | PathLike[bytes] | Sequence[str | bytes | PathLike[str] | PathLike[bytes]]" [arg-type]
+gget/gget_muscle.py:0: error: Item "None" of "IO[bytes] | None" has no attribute "read" [union-attr]
+gget/gget_muscle.py:0: error: Argument 1 to "remove" has incompatible type "str | list[str]"; expected "str | bytes | PathLike[str] | PathLike[bytes]" [arg-type]
+gget/gget_enrichr.py:0: error: Item "None" of "DataFrame | None" has no attribute "index" [union-attr]
+gget/gget_enrichr.py:0: error: Item "None" of "DataFrame | None" has no attribute "loc" [union-attr]
+gget/gget_enrichr.py:0: error: Return value expected [return-value]
+gget/gget_enrichr.py:0: error: Return value expected [return-value]
+gget/gget_enrichr.py:0: error: Return value expected [return-value]
+gget/gget_enrichr.py:0: error: Return value expected [return-value]
+gget/gget_enrichr.py:0: error: Return value expected [return-value]
+gget/gget_diamond.py:0: error: Value of type variable "AnyStr" of "abspath" cannot be "str | list[str]" [type-var]
+gget/gget_diamond.py:0: error: Value of type variable "AnyStr" of "abspath" cannot be "str | list[str]" [type-var]
+gget/gget_diamond.py:0: error: Item "list[str]" of "str | list[str]" has no attribute "replace" [union-attr]
+gget/gget_diamond.py:0: error: Item "list[str]" of "str | list[str]" has no attribute "replace" [union-attr]
+gget/gget_diamond.py:0: error: Item "None" of "IO[bytes] | None" has no attribute "read" [union-attr]
+gget/gget_diamond.py:0: error: Item "None" of "IO[bytes] | None" has no attribute "read" [union-attr]
+gget/gget_diamond.py:0: error: Item "None" of "IO[bytes] | None" has no attribute "read" [union-attr]
+gget/gget_cosmic.py:0: error: Missing return statement [return]
+gget/gget_cosmic.py:0: error: Unsupported operand types for + ("Series[str]" and "str") [operator]
+gget/gget_cosmic.py:0: error: Unsupported operand types for + ("Series[bool]" and "Series[str]") [operator]
+gget/gget_cosmic.py:0: error: Argument 3 to "query_local_cosmic" has incompatible type "str | None"; expected "str" [arg-type]
+gget/gget_archs4.py:0: error: Missing return statement [return]
+gget/gget_archs4.py:0: error: Return value expected [return-value]
+gget/gget_archs4.py:0: error: Incompatible types in assignment (expression has type "Any | Series[Any]", variable has type "str") [assignment]
+gget/gget_archs4.py:0: error: Return value expected [return-value]
+gget/gget_archs4.py:0: error: Return value expected [return-value]
+gget/gget_archs4.py:0: error: Return value expected [return-value]
+gget/gget_elm.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "ndarray[tuple[int], dtype[Any]] | ExtensionArray") [assignment]
+gget/gget_elm.py:0: error: Argument "sequences" to "seq_workflow" has incompatible type "ndarray[tuple[int], dtype[Any]] | ExtensionArray"; expected "list[str]" [arg-type]
+gget/gget_alphafold.py:0: error: Argument 1 to "open" has incompatible type "str | list[str]"; expected "int | str | bytes | PathLike[str] | PathLike[bytes]" [arg-type]
+gget/gget_alphafold.py:0: error: Argument 1 to "open" has incompatible type "str | list[str]"; expected "int | str | bytes | PathLike[str] | PathLike[bytes]" [arg-type]
+gget/gget_alphafold.py:0: error: Incompatible types in assignment (expression has type "str | list[str]", variable has type "list[str]") [assignment]
+gget/gget_alphafold.py:0: error: List comprehension has incompatible type List[object]; expected List[bool] [misc]
+gget/gget_alphafold.py:0: error: Item "None" of "IO[bytes] | None" has no attribute "read" [union-attr]
+gget/gget_alphafold.py:0: error: Incompatible types in assignment (expression has type "TextIOWrapper[_WrappedBuffer]", variable has type "Future[str]") [assignment]
+gget/gget_alphafold.py:0: error: "Future[str]" has no attribute "write" [attr-defined]
+gget/gget_alphafold.py:0: error: Incompatible types in assignment (expression has type "TextIOWrapper[_WrappedBuffer]", variable has type "Future[str]") [assignment]
+gget/gget_alphafold.py:0: error: "Future[str]" has no attribute "write" [attr-defined]
+gget/gget_alphafold.py:0: error: Incompatible types in assignment (expression has type "TextIOWrapper[_WrappedBuffer]", variable has type "Future[str]") [assignment]
+gget/gget_alphafold.py:0: error: Argument 2 to "dump" has incompatible type "Future[str]"; expected "SupportsWrite[str]" [arg-type]
+gget/gget_virus.py:0: error: Library stubs not installed for "dateutil" [import-untyped]
+gget/gget_virus.py:0: note: Hint: "python3 -m pip install types-python-dateutil"
+gget/gget_virus.py:0: note: (or run "mypy --install-types" to install all missing stub packages)
+gget/gget_virus.py:0: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "float", target has type "bool | None") [assignment]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "float", target has type "bool | None") [assignment]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "float", target has type "bool | None") [assignment]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "float", target has type "bool | None") [assignment]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "float | Any", target has type "bool | None") [assignment]
+gget/gget_virus.py:0: error: Unsupported operand types for / ("bool" and "None") [operator]
+gget/gget_virus.py:0: error: Unsupported operand types for / ("None" and "bool") [operator]
+gget/gget_virus.py:0: error: Unsupported left operand type for / ("None") [operator]
+gget/gget_virus.py:0: note: Both left and right operands are unions
+gget/gget_virus.py:0: error: Incompatible return value type (got "list[Never]", expected "tuple[list[Any], dict[str, Any] | None]") [return-value]
+gget/gget_virus.py:0: error: Value of type "object" is not indexable [index]
+gget/gget_virus.py:0: error: Argument 1 to "makedirs" has incompatible type "str | None"; expected "str | bytes | PathLike[str] | PathLike[bytes]" [arg-type]
+gget/gget_virus.py:0: error: Argument 1 to "join" has incompatible type "str | None"; expected "str" [arg-type]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "IO[Any]", variable has type "TextIOWrapper[_WrappedBuffer] | None") [assignment]
+gget/gget_virus.py:0: error: Argument 1 to "open" has incompatible type "str | None"; expected "int | str | bytes | PathLike[str] | PathLike[bytes]" [arg-type]
+gget/gget_virus.py:0: error: Argument 1 to "join" has incompatible type "str | None"; expected "str" [arg-type]
+gget/gget_virus.py:0: error: Argument 1 to "_parse_accession_input" has incompatible type "str | None"; expected "str" [arg-type]
+gget/gget_virus.py:0: error: Argument 1 to "_parse_accession_input" has incompatible type "str | None"; expected "str" [arg-type]
+gget/gget_virus.py:0: error: Item "None" of "dict[str, Any] | None" has no attribute "get" [union-attr]
+gget/gget_virus.py:0: error: Value of type "dict[str, Any] | None" is not indexable [index]
+gget/gget_virus.py:0: error: Argument 4 to "_track_failed_operation" has incompatible type "dict[str, Any] | None"; expected "dict[str, Any]" [arg-type]
+gget/gget_virus.py:0: error: Value of type "dict[str, Any] | None" is not indexable [index]
+gget/gget_virus.py:0: error: Argument 4 to "_track_failed_operation" has incompatible type "dict[str, Any] | None"; expected "dict[str, Any]" [arg-type]
+gget/gget_virus.py:0: error: Argument 4 to "_track_failed_operation" has incompatible type "dict[str, Any] | None"; expected "dict[str, Any]" [arg-type]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "bool", target has type "str") [assignment]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "bool", target has type "str") [assignment]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "bool", target has type "str") [assignment]
+gget/gget_virus.py:0: error: Argument 1 to "_parse_date" has incompatible type "str | None"; expected "str" [arg-type]
+gget/gget_virus.py:0: error: Item "None" of "str | None" has no attribute "lower" [union-attr]
+gget/gget_virus.py:0: error: Item "None" of "str | None" has no attribute "lower" [union-attr]
+gget/gget_virus.py:0: error: Item "None" of "str | None" has no attribute "lower" [union-attr]
+gget/gget_virus.py:0: error: Value of type variable "SupportsRichComparisonT" of "min" cannot be "Any | None" [type-var]
+gget/gget_virus.py:0: error: Value of type variable "SupportsRichComparisonT" of "max" cannot be "Any | None" [type-var]
+gget/gget_virus.py:0: error: Argument 1 to "sum" has incompatible type "list[Any | None]"; expected "Iterable[bool]" [arg-type]
+gget/gget_virus.py:0: error: Argument "params" to "get" has incompatible type "dict[str, object]"; expected "SupportsItems[str | bytes | int | float, str | bytes | int | float | Iterable[str | bytes | int | float] | None] | tuple[str | bytes | int | float, str | bytes | int | float | Iterable[str | bytes | int | float] | None] | Iterable[tuple[str | bytes | int | float, str | bytes | int | float | Iterable[str | bytes | int | float] | None]] | str | bytes | None" [arg-type]
+gget/gget_virus.py:0: error: Argument 1 to "int" has incompatible type "str | None"; expected "str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc" [arg-type]
+gget/gget_virus.py:0: error: Item "None" of "Element | None" has no attribute "text" [union-attr]
+gget/gget_virus.py:0: error: Value of type "str | None" is not indexable [index]
+gget/gget_virus.py:0: error: Argument 1 to "len" has incompatible type "str | None"; expected "Sized" [arg-type]
+gget/gget_virus.py:0: error: Unexpected keyword argument "method_whitelist" for "Retry" [call-arg]
+gget/gget_virus.py:0: error: Argument "params" to "get" of "Session" has incompatible type "dict[str, object]"; expected "SupportsItems[str | bytes | int | float, str | bytes | int | float | Iterable[str | bytes | int | float] | None] | tuple[str | bytes | int | float, str | bytes | int | float | Iterable[str | bytes | int | float] | None] | Iterable[tuple[str | bytes | int | float, str | bytes | int | float | Iterable[str | bytes | int | float] | None]] | str | bytes | None" [arg-type]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "str | None", variable has type "str") [assignment]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "str | None", variable has type "str") [assignment]
+gget/gget_virus.py:0: error: Incompatible return value type (got "tuple[dict[str, Any], str | None]", expected "dict[str, Any]") [return-value]
+gget/gget_virus.py:0: error: Unexpected keyword argument "method_whitelist" for "Retry" [call-arg]
+gget/gget_virus.py:0: error: Argument 1 to "int" has incompatible type "str | None"; expected "str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc" [arg-type]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "bool", target has type "str | None") [assignment]
+gget/gget_virus.py:0: error: "dict[str | None, str | None]" has no attribute "append" [attr-defined]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "list[dict[str | None, str | None]]", target has type "dict[str | None, str | None]") [assignment]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "str | None", variable has type "str") [assignment]
+gget/gget_virus.py:0: error: Incompatible return value type (got "dict[str | None, dict[str, Any]]", expected "dict[str, Any]") [return-value]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "Literal[True]", target has type "str") [assignment]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "Literal[True]", target has type "str") [assignment]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "Literal[True]", target has type "str") [assignment]
+gget/gget_virus.py:0: error: Argument 1 to "_parse_date" has incompatible type "str | None"; expected "str" [arg-type]
+gget/gget_virus.py:0: error: Item "None" of "str | None" has no attribute "lower" [union-attr]
+gget/gget_virus.py:0: error: Item "None" of "str | None" has no attribute "lower" [union-attr]
+gget/gget_virus.py:0: error: Item "None" of "str | None" has no attribute "lower" [union-attr]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "datetime | None", variable has type "str | None") [assignment]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "datetime | None", variable has type "str | None") [assignment]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "datetime | None", variable has type "str | None") [assignment]
+gget/gget_virus.py:0: error: Unsupported operand types for < ("datetime" and "str") [operator]
+gget/gget_virus.py:0: error: Unsupported operand types for > ("datetime" and "str") [operator]
+gget/gget_virus.py:0: error: Unsupported operand types for > ("datetime" and "str") [operator]
+gget/gget_virus.py:0: error: Incompatible return value type (got "list[Never]", expected "tuple[list[str], dict[str, int]]") [return-value]
+gget/gget_virus.py:0: error: Argument 1 to "download_sars_cov2_optimized" has incompatible type "**dict[str, bool | str | None]"; expected "str | None" [arg-type]
+gget/gget_virus.py:0: error: Argument 1 to "download_sars_cov2_optimized" has incompatible type "**dict[str, bool | str | None]"; expected "bool | None" [arg-type]
+gget/gget_virus.py:0: error: Argument 1 to "download_sars_cov2_optimized" has incompatible type "**dict[str, bool | str | None]"; expected "bool" [arg-type]
+gget/gget_virus.py:0: error: Argument 1 to "download_alphainfluenza_optimized" has incompatible type "**dict[str, bool | str | None]"; expected "str | None" [arg-type]
+gget/gget_virus.py:0: error: Argument 1 to "download_alphainfluenza_optimized" has incompatible type "**dict[str, bool | str | None]"; expected "bool | None" [arg-type]
+gget/gget_virus.py:0: error: Argument 1 to "download_alphainfluenza_optimized" has incompatible type "**dict[str, bool | str | None]"; expected "bool" [arg-type]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "dict[str, Any]") [assignment]
+gget/gget_virus.py:0: error: Name "genbank_data_prefetch" already defined on line 0 [no-redef]
+gget/gget_virus.py:0: error: Argument "genbank_metadata" to "filter_genbank_metadata" has incompatible type "str"; expected "dict[str, Any]" [arg-type]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "dict[str, str]", variable has type "str | None") [assignment]
+gget/gget_virus.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/gget_virus.py:0: error: Incompatible types in assignment (expression has type "dict[Never, Never]", variable has type "str | None") [assignment]
+gget/gget_virus.py:0: error: Argument 4 to "filter_sequences" has incompatible type "**dict[str, int | None]"; expected "bool" [arg-type]
+gget/gget_virus.py:0: error: Argument "genbank_metadata" to "save_genbank_metadata_to_csv" has incompatible type "str"; expected "dict[str, Any]" [arg-type]
+gget/gget_virus.py:0: error: Argument "genbank_metadata" to "save_genbank_metadata_to_csv" has incompatible type "str"; expected "dict[str, Any]" [arg-type]
+gget/gget_virus.py:0: error: Argument "genbank_metadata" to "filter_genbank_metadata" has incompatible type "str"; expected "dict[str, Any]" [arg-type]
+gget/gget_virus.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice" [index]
+gget/main.py:0: error: Argument "type" to "add_argument" of "_ActionsContainer" has incompatible type "type[dict[Any, Any]]"; expected "Callable[[str], Any] | FileType | str" [arg-type]
+gget/main.py:0: error: Argument 1 to "write" of "IO" has incompatible type "str | None"; expected "str" [arg-type]
+gget/main.py:0: error: Incompatible types in assignment (expression has type "DataFrame | None", variable has type "DataFrame") [assignment]
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index ccff9d2d3..598e3b612 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -26,6 +26,8 @@ exclude: |
| gget/constants/
| tests/fixtures/
| tests/pytest_results.*\.txt
+ | \.github/badges/.*\.json
+ | \.mypy-baseline\.txt
)
repos:
- repo: https://github.com/biomejs/pre-commit
@@ -44,16 +46,32 @@ repos:
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
types_or: [python, pyi, jupyter]
- - repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.13.0
+ # mypy is run through mypy-baseline: the ~550 pre-existing type errors are
+ # grandfathered in .mypy-baseline.txt, so this hook only fails on NEW type
+ # errors introduced by a change. Works on pre-commit.ci (which runs
+ # --all-files) because the baseline filters by error identity, not by file.
+ # To refresh the baseline after fixing errors:
+ # mypy gget | mypy-baseline sync
+ # See https://github.com/scverse/gget/issues/216
+ - repo: local
hooks:
- - id: mypy
- files: ^gget/
+ - id: mypy-baseline
+ name: mypy (new errors only, baseline-filtered)
+ entry: bash -c 'mypy gget | mypy-baseline filter'
+ language: python
+ pass_filenames: false
+ always_run: true
+ # Pin everything that affects mypy's output so the baseline stays
+ # reproducible across machines and pre-commit.ci (unpinned stubs drift
+ # and surface "new" errors that aren't real). Bump deliberately and
+ # re-sync the baseline when you do.
additional_dependencies:
- - pandas-stubs
- - types-requests
- - types-tqdm
- - types-beautifulsoup4
+ - mypy==1.13.0
+ - mypy-baseline==0.7.4
+ - pandas-stubs==3.0.3.260530
+ - types-requests==2.33.0.20260518
+ - types-tqdm==4.68.0.20260608
+ - types-beautifulsoup4==4.12.0.20250516
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d193210b5..4b4e48fb3 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -38,37 +38,60 @@ Commit the changes once you are happy with them.
### ‼️ Self-review the following before creating a Pull Request ‼️
-1. Review the content for technical accuracy.
+1. Review the content for technical accuracy and biological relevance (who are your target users and is this useful to them?).
2. Copy-edit the changes/comments for grammar, spelling, and adherence to the general style of existing gget code.
-3. Format and lint your code with [pre-commit](https://pre-commit.com/) (powered by [ruff](https://docs.astral.sh/ruff/)). Install the hooks once with `prek install` (or `pre-commit install`) so they run automatically on every commit, or run them on demand with `prek run --all-files` (or `pre-commit run --all-files`).
-4. Make sure the unit tests pass:
- - The tested environments are defined in `pyproject.toml` under `[tool.hatch.envs.hatch-test]` (the single source of truth used by CI). Run the full matrix with `uvx hatch test`.
- - For a quick single-environment run, install the test dependencies with `uv sync --group test` and run `uv run pytest -ra -v --cov=gget --cov-report=term-missing tests`. To also exercise the `gget cellxgene` module, install its extra (`uv sync --group test --extra cellxgene`) on Python 3.12/3.13 — its dependency has no wheels for newer Python versions yet, and that test skips itself when the dependency is absent.
-5. Add new unit tests if applicable:
- - Arguments and expected results are stored in json files in ./tests/fixtures/
- - Unit tests can be added to ./tests/test_*.py and will be automatically detected
-6. Make sure the edits are compatible with both the Python and the command line interface
+3. **Command line interface:** Make sure edits keep the Python function and the command-line interface in sync. Any change to a function's arguments must be reflected in both the Python signature and the CLI argument parser.
- The command line interface and arguments are defined in ./gget/main.py
-8. Add new modules/arguments to the documentation if applicable:
+4. **Documentation:** Add new modules/arguments to the documentation if applicable:
- The manual for each module can be added/edited in `./docs/src/en/*.md` (the Spanish version of the docs in `./docs/src/es/*.md` is automatically generated/updated, and does not need to be edited manually)
- - List all edits in `./docs/src/en/updates.md`
+ - **Document all edits in `./docs/src/en/updates.md`.** Keep this brief and succinct.
+5. **Add unit tests:** Add new unit tests if applicable:
+ - Arguments and expected results are stored in json files in ./tests/fixtures/
+ - Unit tests can be added to ./tests/test_*.py and will be automatically detected
+6. **Run unit tests:** Make sure the unit tests pass:
+ - The tested environments are defined in `pyproject.toml` under `[tool.hatch.envs.hatch-test]` (the single source of truth used by CI). Run the full matrix with `uvx hatch test`.
+ - For a quick single-environment run, install the test dependencies with `uv sync --group test` and run `uv run pytest -ra -v --cov=gget --cov-report=term-missing tests`. To also exercise the `gget cellxgene` module, install its extra (`uv sync --group test --extra cellxgene`) on Python 3.12/3.13 — its dependency has no wheels for newer Python versions yet, and that test skips itself when the dependency is absent.
If you have any questions, feel free to start a [discussion](https://github.com/scverse/gget/discussions) or create an issue as described above.
+### Keep PRs small and focused
+
+Reviewers move faster on small, focused PRs. Whenever possible:
+
+- **Scope each PR to a single `gget` module or issue**. If you find yourself changing more than one module to address several distinct concerns, please open them as separate PRs — one per module. Bug fixes and small refactors inside one module can be combined; cross-module work should be split.
+- **Don't bundle unrelated changes** (e.g. a bug fix plus a new feature plus a refactor) in the same PR.
+
### Pull Request
When you're finished with the changes, [create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request), also known as a PR.
+‼️ Please DO NOT commit AI-generated code without careful review.
+
‼️ Please make all PRs against the `dev` branch of the gget repository.
- Don't forget to [link PR to issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one.
- Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge.
- If you run into any merge issues, checkout this [git tutorial](https://github.com/skills/resolve-merge-conflicts) to help you resolve merge conflicts and other issues.
-Once you submit your PR, a gget team member will review your proposal. We may ask questions or request additional information.
+Once you submit your PR, a `gget` team member will review your proposal. We may ask questions or request additional information.
+
+### What runs automatically on your PR
+
+When you open a PR, a few checks run automatically — you don't need to set anything up:
+
+- **Linting & formatting** ([pre-commit.ci](https://pre-commit.ci)): runs [ruff](https://docs.astral.sh/ruff/) (lint + format), [biome](https://biomejs.dev/), and basic hygiene hooks. It **auto-fixes** formatting issues by pushing a commit to your branch; it only fails the check for problems it can't fix automatically (e.g. lint errors).
+- **Type checking** (mypy, via pre-commit.ci): newly introduced type errors fail this check.
+- **Unit tests** (`CI - tests`): the complete test suite for all modules runs on Python 3.12, 3.13, and 3.14.
+- **Build check**: confirms the package still builds and is publishable.
+
+Optional: You can run these manually first (especially `uvx hatch test` and `prek run --all-files`) to catch most issues before creating the PR.
+
+### Failing tests for modules you didn't touch
+
+The `gget` test suite hits real upstream databases (Ensembl, UniProt, NCBI, ARCHS4, Open Targets, ELM, etc.). When those services change their data or schemas — which they do regularly — tests for the affected modules can start failing without anyone changing `gget` itself. **If automated CI tests fail in your PR for a module you did not touch, you can safely ignore those failures** when judging whether your PR is ready to merge. The maintainers track upstream-drift failures separately and do not expect contributors to fix unrelated breakages as a condition of merging.
-### Your PR is merged!
+### Your PR gets merged!
-Congratulations! 🎉 The gget team thanks you. ✨
+Congratulations! 🎉 The `gget` team thanks you. ✨
Once your PR is merged, your contributions will be publicly visible on the [gget repo](https://github.com/scverse/gget).
diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md
index 845d38f7f..b112fd0f2 100644
--- a/docs/src/SUMMARY.md
+++ b/docs/src/SUMMARY.md
@@ -21,7 +21,7 @@
* [gget diamond](en/diamond.md)
* [gget elm](en/elm.md)
* [gget enrichr](en/enrichr.md)
-* [gget g2p](en/g2p.md)
+* [gget g2p](en/g2p.md)
* [gget gpt](en/gpt.md)
* [gget info](en/info.md)
* [gget muscle](en/muscle.md)
@@ -64,7 +64,7 @@
* [gget diamond](es/diamond.md)
* [gget elm](es/elm.md)
* [gget enrichr](es/enrichr.md)
-* [gget g2p](es/g2p.md)
+* [gget g2p](es/g2p.md)
* [gget gpt](es/gpt.md)
* [gget info](es/info.md)
* [gget muscle](es/muscle.md)
diff --git a/docs/src/en/alphafold.md b/docs/src/en/alphafold.md
index c9d150f69..c161c361a 100644
--- a/docs/src/en/alphafold.md
+++ b/docs/src/en/alphafold.md
@@ -33,6 +33,9 @@ For higher accuracy, at the potential cost of longer inference times, set this t
`-o` `--out`
Path to folder to save prediction results in (str). Default: "./[date_time]_gget_alphafold_prediction".
+`-jhd` `--jackhmmer_savedir`
+Path to a parent directory in which to store the temporary jackhmmer files (str). By default, `gget alphafold` creates a "tmp" folder in your home directory (`~/tmp/jackhmmer/`), which can take up to ~2 GB of disk space. Use this argument to place these temporary files elsewhere, e.g. on a disk with more free space. Default: None.
+
**Flags**
`-mfm` `--multimer_for_monomer`
Use multimer model for a monomer.
diff --git a/docs/src/en/archs4.md b/docs/src/en/archs4.md
index e41b08644..32cf5b6f0 100644
--- a/docs/src/en/archs4.md
+++ b/docs/src/en/archs4.md
@@ -5,6 +5,8 @@
Find the most correlated genes to a gene of interest or find the gene's tissue expression atlas using [ARCHS4](https://maayanlab.cloud/archs4/).
Return format: JSON (command-line) or data frame/CSV (Python).
+> **Note on the data source:** `gget archs4` queries the legacy ARCHS4 HTTP endpoints at `maayanlab.cloud`, which are lightweight and still maintained. ARCHS4 has since launched a new site ([archs4.org](https://archs4.org)) with a larger, more up-to-date dataset, but its programmatic access is provided only via the [`archs4py`](https://github.com/MaayanLab/archs4py) package and multi-GB HDF5 downloads (there is currently no equivalent lightweight HTTP API to query). `gget archs4` therefore continues to use the legacy endpoints. A possible future migration is tracked in [issue 254](https://github.com/scverse/gget/issues/254).
+
**Positional argument**
`gene`
Short name (gene symbol) of gene of interest, e.g. STAT4.
diff --git a/docs/src/en/g2p.md b/docs/src/en/g2p.md
index d2896e4e0..e3466b0fb 100644
--- a/docs/src/en/g2p.md
+++ b/docs/src/en/g2p.md
@@ -2,34 +2,43 @@
> Python arguments are equivalent to long-option arguments (`--arg`), unless otherwise specified. Flags are True/False arguments in Python. The manual for any gget tool can be called from the command-line using the `-h` `--help` flag.
# gget g2p 🧬➜🧪
-Query the [Genomics 2 Proteins (G2P) portal](https://g2p.broadinstitute.org/) to link genes/proteins to residue-level structural and functional annotations (e.g. AlphaFold pLDDT, UniProt sites, predicted pockets, PTMs), the gene–transcript–protein–isoform–structure map, and isoform alignments.
+Query the [Genomics 2 Proteins (G2P) portal](https://g2p.broadinstitute.org/) to link genes/proteins to residue-level structural and functional annotations, the gene–transcript–protein–isoform–structure map, and isoform alignments.
-Returns: A data frame with the requested G2P information.
+The per-residue feature table is rich (~140 columns), including AlphaFold pLDDT, DSSP secondary structure, accessible surface area, UniProt sites (active/binding/domain/...), PhosphoSitePlus PTMs, fpocket / af2bind / p2rank pocket predictions, intra- and inter-chain hydrogen bonds, non-bonded interactions, disulfide bonds and salt bridges (from PDB and AlphaFold), the PFES (Protein Feature Enrichment Score) sub-scores used by G2P for missense variant interpretation, and per-residue MaveDB experimental functional scores. See the [`g2p-bis` documentation](https://github.com/broadinstitute/g2p-bis) for column descriptions.
+
+> Note: this module wraps the *public* G2P REST API. The variant overlays shown in the portal's web UI (gnomAD, ClinVar, HGMD) are not exposed by the public API and are therefore not available via `gget g2p` — use the portal directly for those.
+
+Returns: A data frame with the requested G2P information, or `None` if the query failed (network error, invalid arguments, or unknown gene/UniProt pair).
This module was written by [Elarwei](https://github.com/Elarwei001).
-**Positional argument**
-`gene`
+At least one of `gene` or `--uniprot_id` is required — the other is resolved automatically via the UniProt REST API and cached.
+
+**Arguments (at least one of `gene` / `--uniprot_id` required)**
+`gene` (positional)
Gene symbol, e.g. BRCA1.
+If omitted, resolved from `--uniprot_id`. In this case, note that gene → UniProt accession lookup is approximate (it picks the canonical reviewed human Swiss-Prot entry only); for non-human organisms, unreviewed entries, or a specific isoform, pass `--uniprot_id` explicitly.
-**Other required arguments**
`-u` `--uniprot_id`
-UniProt accession, e.g. P38398. For `--resource alignment` this is the canonical isoform (e.g. P01130-1).
-Tip: find a gene's UniProt ID with [`gget info`](info.md).
+UniProt accession, e.g. P38398. If omitted, resolved from `gene`. For `--resource alignment` this is the canonical isoform (e.g. P01130-1) and is **required** (gene→UniProt lookup returns the base accession and cannot disambiguate isoforms).
+Tip: find a gene's UniProt ID with [`gget search`](search.md) (to get a gene's Ensembl ID) and [`gget info`](info.md).
**Optional arguments**
`-r` `--resource`
Defines the type of information to return (default: 'features'):
-`features`: Per-residue protein feature table (AlphaFold pLDDT, UniProt sites, secondary structure, predicted pockets, PTMs, etc.).
-`map`: Gene → transcript → protein isoform → structure map (UniProt/Ensembl/RefSeq/PDB identifiers).
+`features`: Per-residue protein feature table (~140 columns: AlphaFold pLDDT, UniProt sites, secondary structure, predicted pockets, PTMs, PFES, MaveDB scores, ...).
+`map`: Gene → transcript → protein isoform → structure map (UniProt/Ensembl/RefSeq/PDB identifiers). The comma-joined `PDB Ids` column is augmented with a parsed `PDB Ids List` column (`list[str]`) for direct consumption (e.g. with [`gget pdb`](pdb.md)).
`alignment`: Residue-level sequence alignment between two isoforms (requires `--isoform`; `--uniprot_id` is the canonical isoform).
`-i` `--isoform`
Alternative isoform UniProt accession (e.g. P01130-2). Required when `--resource alignment`. Default: None.
+`--residues`
+Restrict the result to specific residue positions (applies to `--resource features` / `alignment` only). Command line: comma-separated list and/or inclusive ranges, e.g. `185,1775,1812` or `100-200` or `1-50,185`. Python: `int`, `list[int]`, `range`, or `set[int]`. Default: None (return all residues).
+
`-o` `--out`
Path to the file the results will be saved in, e.g. path/to/directory/results.json. Default: Standard out.
-Python: `save=True` will save the output in the current working directory.
+Python: `save=True` will save the output as a CSV in the current working directory; `out="path/to/file.csv"` writes to an explicit path and takes precedence over `save`.
**Flags**
`-csv` `--csv`
@@ -42,24 +51,62 @@ Python: Use `verbose=False` to prevent progress information from being displayed
### Examples
```bash
-# Per-residue protein features for BRCA1 (AlphaFold pLDDT, UniProt sites, ...)
+# Per-residue protein features for BRCA1 (AlphaFold pLDDT, UniProt sites, ...).
+# The gene symbol is optional — it can be resolved from the UniProt accession.
+gget g2p -u P38398
+```
+```python
+# Python
+gget.g2p(uniprot_id="P38398", resource="features")
+```
+→ Returns a data frame with one row per residue of the BRCA1 protein (UniProt P38398) and its structural/functional annotations.
+
+
+
+```bash
+# Same query, with the gene symbol passed explicitly
gget g2p BRCA1 -u P38398
```
```python
# Python
gget.g2p("BRCA1", uniprot_id="P38398", resource="features")
```
-→ Returns a data frame with one row per residue of the BRCA1 protein (UniProt P38398) and its structural/functional annotations.
+
+
+
+```bash
+# Symmetric: only the gene symbol — UniProt accession is resolved automatically
+# (canonical reviewed human Swiss-Prot entry).
+gget g2p BRCA1
+```
+```python
+# Python
+gget.g2p("BRCA1")
+```
+→ Same as above. The resolved `P38398` is logged and is visible in the leading `uniprot_id` column.
+
+
+
+```bash
+# Score only specific residue positions
+gget g2p BRCA1 -u P38398 --residues 185,1775,1812
+```
+```python
+# Python
+gget.g2p("BRCA1", uniprot_id="P38398", residues=[185, 1775, 1812])
+gget.g2p("BRCA1", uniprot_id="P38398", residues=range(100, 200))
+```
+→ Returns only the requested residues from the per-residue feature table.
```bash
# Gene -> transcript -> isoform -> structure map (CSV)
-gget g2p BRCA1 -u P38398 -r map --csv
+gget g2p -u P38398 -r map --csv
```
```python
# Python
-gget.g2p("BRCA1", uniprot_id="P38398", resource="map")
+gget.g2p(uniprot_id="P38398", resource="map")
```
→ Returns the mapping of BRCA1 to its UniProt isoforms, Ensembl/RefSeq identifiers, and PDB structures.
@@ -67,11 +114,11 @@ gget.g2p("BRCA1", uniprot_id="P38398", resource="map")
```bash
# Residue-level alignment between two LDLR isoforms
-gget g2p LDLR -u P01130-1 -r alignment -i P01130-2
+gget g2p -u P01130-1 -r alignment -i P01130-2
```
```python
# Python
-gget.g2p("LDLR", uniprot_id="P01130-1", resource="alignment", isoform="P01130-2")
+gget.g2p(uniprot_id="P01130-1", resource="alignment", isoform="P01130-2")
```
→ Returns the residue-level alignment between LDLR isoforms P01130-1 and P01130-2.
diff --git a/docs/src/en/opentargets.md b/docs/src/en/opentargets.md
index 7d74d56fd..b1966a5f3 100644
--- a/docs/src/en/opentargets.md
+++ b/docs/src/en/opentargets.md
@@ -7,6 +7,8 @@ Return format: JSON/CSV (command-line) or data frame (Python).
This module was written by [Sam Wagenaar](https://github.com/techno-sam).
+> ⚠️ **Change as of gget v0.30.8 — `resource="expression"` output is different.** OpenTargets retired the old `target.expressions` field, so `gget opentargets -r expression` now returns OpenTargets' `baselineExpression` data instead. **The output columns changed**: results are now per-biosample (tissue *and/or* cell type) summary statistics (`median`, `min`, `q1`, `q3`, `max`, `unit`) with `tissueBiosample.*` / `celltypeBiosample.*` identifiers and `datasourceId` / `datatypeId` — replacing the old per-tissue `tissue.*` / `rna.*` (z-score) columns, which no longer exist upstream. A gene can have thousands of biosamples; use `--filters` (e.g. `datasourceId`, `datatypeId`) or `--limit` to narrow the result. See the baseline expression example below.
+
**Positional argument**
`ens_id`
Ensembl gene ID, e.g ENSG00000169194.
@@ -18,13 +20,13 @@ Possible resources are:
| Resource | Return Value | Valid Filters | Sources |
|--------------------|-------------------------------------------------------------------|---------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `diseases` | Associated diseases | None | Various:
- [Open Targets](https://genetics.opentargets.org/)
- [ChEMBL](https://www.ebi.ac.uk/chembl/)
- [Europe PMC](http://europepmc.org/)
etc. |
-| `drugs` | Associated drugs | `disease_id` | [ChEMBL](https://www.ebi.ac.uk/chembl/) |
+| `diseases` | Associated diseases, phenotypes & traits (EFO-mapped) with the overall association `score` (0–1) | None | Various:- [Open Targets](https://genetics.opentargets.org/)
- [ChEMBL](https://www.ebi.ac.uk/chembl/)
- [Europe PMC](http://europepmc.org/)
etc. |
+| `drugs` | Associated drugs | `drug.drugType`
`drug.maximumClinicalStage` | [ChEMBL](https://www.ebi.ac.uk/chembl/) |
| `tractability` | Tractability data | None | [Open Targets](https://platform-docs.opentargets.org/target/tractability) |
-| `pharmacogenetics` | Pharmacogenetic responses | `drug_id` | [PharmGKB](https://www.pharmgkb.org/) |
-| `expression` | Gene expression data (by tissues, organs, and anatomical systems) | `tissue_id`
`anatomical_system`
`organ` | - [ExpressionAtlas](https://www.ebi.ac.uk/gxa/home)
- [HPA](https://www.proteinatlas.org/)
- [GTEx](https://www.gtexportal.org/home/)
|
-| `depmap` | DepMap gene→disease-effect data. | `tissue_id` | [DepMap Portal](https://depmap.org/portal/) |
-| `interactions` | Protein⇄protein interactions | `protein_a_id`
`protein_b_id`
`gene_b_id` | - [Open Targets](https://platform-docs.opentargets.org/target/molecular-interactions)
- [IntAct](https://platform-docs.opentargets.org/target/molecular-interactions#intact)
- [Signor](https://platform-docs.opentargets.org/target/molecular-interactions#signor)
- [Reactome](https://platform-docs.opentargets.org/target/molecular-interactions#reactome)
- [String](https://platform-docs.opentargets.org/target/molecular-interactions#string)
|
+| `pharmacogenetics` | Pharmacogenetic responses | `datasourceId`
`pgxCategory`
`evidenceLevel` | [PharmGKB](https://www.pharmgkb.org/) |
+| `expression` | Baseline expression per biosample (tissue/cell type) with summary statistics | `tissueBiosample.biosampleId`
`datasourceId`
`datatypeId` | - [GTEx](https://www.gtexportal.org/home/)
- [ExpressionAtlas](https://www.ebi.ac.uk/gxa/home)
- single-cell datasets
|
+| `depmap` | DepMap gene→disease-effect data. | `tissueId`
`diseaseFromSource` | [DepMap Portal](https://depmap.org/portal/) |
+| `interactions` | Protein⇄protein interactions | `sourceDatabase`
`targetB.id`
`targetB.approvedSymbol` | - [Open Targets](https://platform-docs.opentargets.org/target/molecular-interactions)
- [IntAct](https://platform-docs.opentargets.org/target/molecular-interactions#intact)
- [Signor](https://platform-docs.opentargets.org/target/molecular-interactions#signor)
- [Reactome](https://platform-docs.opentargets.org/target/molecular-interactions#reactome)
- [String](https://platform-docs.opentargets.org/target/molecular-interactions#string)
|
`-l` `--limit`
Limit the number of results, e.g 10. Default: No limit.
@@ -66,9 +68,13 @@ gget.opentargets('ENSG00000169194', resource='diseases', limit=1)
```
→ Returns the top disease associated with the gene ENSG00000169194.
-| id | name | description | score |
-|---------------|--------------------|-----------------------------------------------------------------------|------------------|
-| EFO_0000274 | atopic eczema | A chronic inflammatory genetically determined disease of the skin ... | 0.66364347241831 |
+| score | disease.id | disease.name | disease.description |
+|--------------------|----------------|--------------------|-------------------------------------------------------------------------|
+| 0.7279798021712002 | MONDO_0004980 | atopic eczema | A common chronic pruritic inflammatory skin disease with a strong ... |
+
+> **Understanding the `score` column and disease IDs** ([issue #168](https://github.com/scverse/gget/issues/168))
+> - `score` is OpenTargets' **overall target–disease association score** — a single value between 0 and 1 that aggregates the evidence across *all* data types and data sources. It is **not** a per-data-source score. The OpenTargets website shows, in addition, a per-data-type breakdown (genetic associations, somatic mutations, known drugs, animal models, etc.) in its "Associations" view; `gget opentargets` currently returns only the aggregated `score`, so a single gget row corresponds to one whole row of that web table.
+> - `disease.id` is an [EFO](https://www.ebi.ac.uk/efo/) ontology ID. OpenTargets maps every associated trait to EFO, which imports terms from several ontologies. As a result the returned IDs are **not exclusively diseases**: they can be MONDO disease terms (`MONDO_*`), Human Phenotype Ontology phenotypes (`HP_*`), Orphanet rare diseases (`Orphanet_*`), or EFO measurements/traits (`EFO_*`, e.g. biomarker or blood-measurement traits). `gget opentargets` returns the associations **exactly as OpenTargets reports them**, without dropping non-disease terms. To keep only MONDO disease terms, filter the returned data frame, e.g. `df[df["disease.id"].str.startswith("MONDO")]`.
@@ -84,12 +90,12 @@ gget.opentargets('ENSG00000169194', resource='drugs', limit=2)
→ Returns the top 2 drugs associated with the gene ENSG00000169194.
-| id | name | type | action_mechanism | description | synonyms | trade_names | disease_id | disease_name | trial_phase | trial_status | trial_ids | approved |
-|---------------|--------------|----------|-------------------------------------|--------------------------------------------------------------|----------------------------------------------------|-----------------------|-------------|-------------------------------|-------------|--------------|---------------|----------|
-| CHEMBL1743081 | TRALOKINUMAB | Antibody | Interleukin‑13 inhibitor | Antibody drug with a maximum clinical trial phase of IV ... | ['CAT-354', 'Tralokinumab'] | ['Adbry', 'Adtralza'] | EFO_0000274 | atopic eczema | 4 | | [] | True |
-| CHEMBL4297864 | CENDAKIMAB | Antibody | Interleukin‑13 inhibitor | Antibody drug with a maximum clinical trial phase of III ... | [ABT-308, Abt-308, CC-93538, Cendakimab, RPC-4046] | [] | EFO_0004232 | eosinophilic esophagitis | 3 | Recruiting | [NCT04991935] | False |
+| drug.id | drug.name | drug.drugType | drug.mechanismsOfAction.rows | drug.description | drug.synonyms | drug.tradeNames | drug.maximumClinicalStage | drug.indications.rows |
+|---------------|--------------|---------------|-------------------------------------|--------------------------------------------------------------|----------------------------------------|-----------------|---------------------------|---------------------------------------------------------------------------------|
+| CHEMBL1743035 | LEBRIKIZUMAB | Antibody | Interleukin‑13 inhibitor | Antibody drug with a maximum clinical stage of Approval ... | ['Lebrikizumab', 'MILR-1444A', ...] | Ebglyss | APPROVAL | [{'id': 'MONDO_0004980', 'name': 'atopic eczema'}, {'id': 'MONDO_0004979', 'name': 'asthma'}, ...] |
+| CHEMBL1742985 | ANRUKINZUMAB | Antibody | Interleukin‑13 inhibitor | Antibody drug with a maximum clinical stage of Phase 2 ... | ['Anrukinzumab', 'IMA-638'] | [] | PHASE2 | [{'id': 'MONDO_0005101', 'name': 'ulcerative colitis'}, ...] |
-*Note: Returned `trial_ids` are [ClinicalTrials.gov](https://clinicaltrials.gov) identifiers*
+*Note: associated diseases/indications are nested under `drug.indications.rows` (each a `{'id', 'name'}` dict); the `drug.id` values are [ChEMBL](https://www.ebi.ac.uk/chembl/) identifiers.*
@@ -105,13 +111,14 @@ gget.opentargets('ENSG00000169194', resource='tractability')
→ Returns tractability data for the gene ENSG00000169194.
-| label | modality |
-|-----------------------|----------------|
-| High-Quality Pocket | Small molecule |
-| Approved Drug | Antibody |
-| GO CC high conf | Antibody |
-| UniProt loc med conf | Antibody |
-| UniProt SigP or TMHMM | Antibody |
+| modality | label | value |
+|----------|---------------------|-------|
+| SM | Approved Drug | False |
+| SM | High-Quality Pocket | True |
+| AB | Approved Drug | True |
+| AB | Advanced Clinical | False |
+
+*Note: `modality` is `SM` (small molecule), `AB` (antibody), `PR` (PROTAC), `OC` (other clinical), etc.; `value` is a boolean indicating whether the target meets that tractability bucket.*
@@ -127,15 +134,15 @@ gget.opentargets('ENSG00000169194', resource='pharmacogenetics', limit=1)
→ Returns pharmacogenetic responses for the gene ENSG00000169194.
-| rs_id | genotype_id | genotype | variant_consequence_id | variant_consequence_label | drugs | phenotype | genotype_annotation | response_category | direct_target | evidence_level | source | literature |
-|-----------|-------------------|----------|------------------------|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------|-------------------|---------------|----------------|----------|------------|
-| rs1295686 | 5_132660151_T_T,T | TT | SO:0002073 | no_sequence_alteration | id name
0 None hepatitis vaccines | increased risk for non‑immune response to the hepatitis B vaccine | Patients with the TT genotype may be at increased risk for non-immune response to the hepatitis B vaccine... | efficacy | False | 3 | pharmgkb | [21111021] |
+| variantId | genotypeId | genotype | drugs | phenotypeText | genotypeAnnotationText | pgxCategory | isDirectTarget | evidenceLevel | datasourceId | literature | variantFunctionalConsequence.id | variantFunctionalConsequence.label |
+|-----------------|--------------------|----------|---------------------------------------------|--------------------------------------------|-------------------------------------------|-------------|----------------|---------------|--------------|------------|---------------------------------|------------------------------------|
+| 5_132657117_C_T | 5_132657117_C_C,T | CT | {'id': 'CHEMBL535', 'name': 'SUNITINIB'} | decreased severity of drug-induced toxicity | Patients with renal cell carcinoma and ... | toxicity | False | 3 | clinpgx | 26387812 | SO:0001631 | upstream_gene_variant |
-*Note: Returned `literature` ids are [Europe PMC](https://europepmc.org/article/med/) identifiers*
+*Note: `drugs` is a `{'id', 'name'}` dict (or list of them); `literature` ids are [Europe PMC](https://europepmc.org/article/med/) identifiers.*
-**Get tissues where a gene is most expressed:**
+**Get baseline expression of a gene across biosamples (tissues / cell types):**
```bash
gget opentargets ENSG00000169194 -r expression -l 2
```
@@ -145,12 +152,16 @@ import gget
gget.opentargets('ENSG00000169194', resource='expression', limit=2)
```
-→ Returns the top 2 tissues where the gene ENSG00000169194 is most expressed.
+→ Returns baseline expression summary statistics for the gene ENSG00000169194 per biosample.
+
+| median | min | q1 | q3 | max | unit | datasourceId | datatypeId | tissueBiosample.biosampleId | tissueBiosample.biosampleName | celltypeBiosample.biosampleId | celltypeBiosample.biosampleName |
+|----------|-----|----------|----------|---------|-----------------------------|----------------|---------------|-----------------------------|-------------------------------|-------------------------------|---------------------------------|
+| 0.066891 | 0 | 0.028268 | 0.142208 | 1.69407 | TPM | gtex | bulk rna-seq | UBERON_0000007 | pituitary gland | None | None |
+| 0.0 | 0 | 0.0 | 0.0 | 0.0 | CPM(pseudobulk sum) | tabula_sapiens | scrna-seq | UBERON_0000016 | endocrine pancreas | CL_0000115 | endothelial cell |
-| tissue_id | tissue_name | rna_zscore | rna_value | rna_unit | rna_level | anatomical_systems | organs |
-|----------------|---------------------------------------|------------|-----------|----------|-----------|----------------------------------------------------------------------|--------------------------------------------------------|
-| UBERON_0000473 | testis | 5 | 1026 | | 3 | [reproductive system] | [reproductive organ, reproductive structure] |
-| CL_0000542 | EBV‑transformed lymphocyte | 1 | 54 | | 2 | [hemolymphoid system, immune system, lymphoid system] | [immune organ] |
+*Note: bulk sources (e.g. GTEx) populate only `tissueBiosample.*` (`celltypeBiosample.*` is `None`); single-cell sources (e.g. Tabula Sapiens) populate both.*
+
+> **Note (OpenTargets API change):** OpenTargets retired the per-tissue `target.expressions` field (it now returns nothing) and moved baseline expression to the paginated `target.baselineExpression` field. `gget opentargets -r expression` now returns per-biosample expression summary statistics (`median`, `min`, `q1`, `q3`, `max`) from the current sources (e.g. GTEx bulk RNA-seq and single-cell datasets), with `tissueBiosample`/`celltypeBiosample` identifiers and `datasourceId`/`datatypeId` so results can be filtered. The returned columns therefore differ from earlier gget versions. A gene can have thousands of biosamples; OpenTargets returns at most 3000 per request, so for genes that exceed this a warning is logged and you should narrow the query with `--filters` (e.g. `datasourceId` or `datatypeId`). Use `--limit` to fetch fewer rows.
@@ -166,9 +177,9 @@ gget.opentargets('ENSG00000169194', resource='depmap')
→ Returns DepMap gene-disease effect data for the gene ENSG00000169194.
-| depmap_id | expression | effect | tissue_id | tissue_name | cell_line_name | disease_cell_line_id | disease_name | mutation |
-|------------------|------------|----------|----------------|-------------|----------------|----------------------|----------------------|----------|
-| ACH‑001532 | 0.176323 | 0.054950 | UBERON_0002113 | kidney | JMU-RTK-2 | None | Rhabdoid Cancer | None |
+| tissueId | tissueName | cellLineName | expression | diseaseFromSource | depmapId | geneEffect |
+|----------------|------------|--------------|-------------|-----------------------|-------------|-------------|
+| UBERON_0000977 | pleura | NCI-H2452 | 0.0223550275 | Pleural Mesothelioma | ACH-000092 | 0.0368397422 |
@@ -184,57 +195,30 @@ gget.opentargets('ENSG00000169194', resource='interactions', limit=2)
→ Returns the top 2 protein-protein interactions for the gene ENSG00000169194.
-| evidence_score | evidence_count | source_db | protein_a_id | gene_a_id | gene_a_symbol | role_a | taxon_a | protein_b_id | gene_b_id | gene_b_symbol | role_b | taxon_b |
-|----------------|----------------|-----------|-----------------|-----------------|---------------|-----------------------|---------|-----------------|-----------------|---------------|-----------------------|---------|
-| 0.999 | 3 | string | ENSP00000304915 | ENSG00000169194 | IL13 | unspecified role | 9606 | ENSP00000379111 | ENSG00000077238 | IL4R | unspecified role | 9606 |
-| 0.999 | 3 | string | ENSP00000304915 | ENSG00000169194 | IL13 | unspecified role | 9606 | ENSP00000360730 | ENSG00000131724 | IL13RA1 | unspecified role | 9606 |
+| score | count | sourceDatabase | intA | intABiologicalRole | intB | intBBiologicalRole | targetA.id | targetA.approvedSymbol | speciesA.taxonId | targetB.id | targetB.approvedSymbol | speciesB.taxonId |
+|-------|-------|----------------|-----------------|-----------------------|-----------------|-----------------------|-----------------|------------------------|------------------|-----------------|------------------------|------------------|
+| 0.999 | 3 | string | ENSP00000304915 | unspecified role | ENSP00000360730 | unspecified role | ENSG00000169194 | IL13 | 134 | ENSG00000131724 | IL13RA1 | 134 |
+| 0.999 | 3 | string | ENSP00000304915 | unspecified role | ENSP00000361004 | unspecified role | ENSG00000169194 | IL13 | 134 | ENSG00000123496 | IL13RA2 | 134 |
-**Get protein-protein interactions for a specific gene, filtering by protein and gene IDs:**
+**Get protein-protein interactions for a specific gene, filtered by column values:**
+
+Filters use the generic `--filter key=value` flag (CLI) / `filters={...}` argument (Python), where the keys are the exact returned column names. Multiple filters are combined with **AND** (exact equality). (The previous `filter_mode`/OR option and the `-fpa`/`--filter_gene_b` shortcuts were removed in v0.30.5.)
```bash
-gget opentargets ENSG00000169194 -r interactions -fpa P35225 --filter_gene_b ENSG00000077238
+gget opentargets ENSG00000169194 -r interactions --filter sourceDatabase=string --filter targetB.approvedSymbol=IL13RA1
```
```python
# Python
import gget
-gget.opentargets('ENSG00000169194', resource='interactions', filters={'protein_a_id': 'P35225', 'gene_b_id': 'ENSG00000077238'})
+gget.opentargets('ENSG00000169194', resource='interactions', filters={'sourceDatabase': 'string', 'targetB.approvedSymbol': 'IL13RA1'})
```
-→ Returns protein-protein interactions for the gene ENSG00000169194, where the first protein is P35225 **and** the second gene is ENSG00000077238.
-
-| evidence_score | evidence_count | source_db | protein_a_id | gene_a_id | gene_a_symbol | role_a | taxon_a | protein_b_id | gene_b_id | gene_b_symbol | role_b | taxon_b |
-|----------------|----------------|-----------|--------------|-----------------|---------------|-----------------------|---------|--------------|-----------------|---------------|-----------------------|---------|
-| None | 3 | reactome | P35225 | ENSG00000169194 | IL13 | unspecified role | 9606 | P24394 | ENSG00000077238 | IL4R | unspecified role | 9606 |
-| None | 2 | signor | P35225 | ENSG00000169194 | IL13 | regulator | 9606 | P24394 | ENSG00000077238 | IL4R | regulator target | 9606 |
+→ Returns the `string`-sourced interactions of IL13 where the partner is IL13RA1.
-
-
-**Get protein-protein interactions for a specific gene, filtering by protein or gene IDs:**
-```bash
-gget opentargets ENSG00000169194 -r interactions -fpa P35225 --filter_gene_b ENSG00000077238 ENSG00000111537 --or -l 5
-```
-```python
-# Python
-import gget
-gget.opentargets(
- 'ENSG00000169194',
- resource='interactions',
- filters={'protein_a_id': 'P35225', 'gene_b_id': ['ENSG00000077238', 'ENSG00000111537']},
- filter_mode='or',
- limit=5
-)
-```
-
-→ Returns protein-protein interactions for the gene ENSG00000169194, where the first protein is P35225 **or** the second gene is either ENSG00000077238 or ENSG00000111537.
-
-| evidence_score | evidence_count | source_db | protein_a_id | gene_a_id | gene_a_symbol | role_a | taxon_a | protein_b_id | gene_b_id | gene_b_symbol | role_b | taxon_b |
-|----------------|----------------|-----------|-----------------|-----------------|---------------|-----------------------|---------|-----------------|-----------------|---------------|-----------------------|---------|
-| 0.999 | 3 | string | ENSP00000304915 | ENSG00000169194 | IL13 | unspecified role | 9606 | ENSP00000379111 | ENSG00000077238 | IL4R | unspecified role | 9606 |
-| 0.961 | 2 | string | ENSP00000304915 | ENSG00000169194 | IL13 | unspecified role | 9606 | ENSP00000229135 | ENSG00000111537 | IFNG | unspecified role | 9606 |
-| 0.800 | 9 | intact | P35225 | ENSG00000169194 | IL13 | unspecified role | 9606 | Q14627 | ENSG00000123496 | IL13RA2 | unspecified role | 9606 |
-| 0.740 | 6 | intact | P35225 | ENSG00000169194 | IL13 | unspecified role | 9606 | P78552 | ENSG00000131724 | IL13RA1 | unspecified role | 9606 |
-| 0.400 | 1 | intact | P35225 | ENSG00000169194 | IL13 | unspecified role | 9606 | Q86XT9 | ENSG00000149932 | TMEM219 | stimulator | 9606 |
+| score | count | sourceDatabase | intA | intABiologicalRole | intB | intBBiologicalRole | targetA.id | targetA.approvedSymbol | speciesA.taxonId | targetB.id | targetB.approvedSymbol | speciesB.taxonId |
+|-------|-------|----------------|-----------------|-----------------------|-----------------|-----------------------|-----------------|------------------------|------------------|-----------------|------------------------|------------------|
+| 0.999 | 3 | string | ENSP00000304915 | unspecified role | ENSP00000360730 | unspecified role | ENSG00000169194 | IL13 | 134 | ENSG00000131724 | IL13RA1 | 134 |
diff --git a/docs/src/en/pdb.md b/docs/src/en/pdb.md
index b369bada3..84e0b24e0 100644
--- a/docs/src/en/pdb.md
+++ b/docs/src/en/pdb.md
@@ -3,7 +3,7 @@
> Python arguments are equivalent to long-option arguments (`--arg`), unless otherwise specified. Flags are True/False arguments in Python. The manual for any gget tool can be called from the command-line using the `-h` `--help` flag.
# gget pdb 🔮
Query [RCSB Protein Data Bank (PDB)](https://www.rcsb.org/) for the protein structure/metadata of a given PDB ID.
-Return format: Resource 'pdb' is returned in PDB format. All other resources are returned in JSON format.
+Return format: Resource 'pdb' is returned in legacy PDB format (or PDBx/mmCIF if the legacy PDB file is unavailable), 'mmcif' in PDBx/mmCIF format. All other resources are returned in JSON format.
**Positional argument**
`pdb_id`
@@ -12,7 +12,8 @@ PDB ID to be queried, e.g. '7S7U'.
**Optional arguments**
`-r` `--resource`
Defines type of information to be returned. One of the following:
- 'pdb': Returns the protein structure in PDB format (default).
+ 'pdb': Returns the protein structure in legacy PDB format (default). The legacy PDB format is being phased out by RCSB and is unavailable for large structures; in that case gget automatically falls back to the PDBx/mmCIF format.
+ 'mmcif': Returns the protein structure in PDBx/mmCIF format (.cif).
'entry': Information about PDB structures at the top level of PDB structure hierarchical data organization.
'pubmed': Get PubMed annotations (data integrated from PubMed) for a given entry's primary citation.
'assembly': Information about PDB structures at the quaternary structure level.
@@ -42,6 +43,17 @@ gget.pdb("7S7U", save=True)
```
→ Saves the structure of 7S7U in PDB format as '7S7U.pdb' in the current working directory.
+**Fetch a structure in PDBx/mmCIF format (recommended for large structures, which have no legacy PDB file):**
+```bash
+gget pdb 6Q38 -r mmcif -o 6Q38.cif
+```
+```python
+# Python
+gget.pdb("6Q38", resource="mmcif", save=True)
+```
+→ Saves the structure of 6Q38 in PDBx/mmCIF format as '6Q38.cif' in the current working directory.
+Note: `gget pdb 6Q38` (without `-r mmcif`) also works — since 6Q38 has no legacy PDB file, gget automatically falls back to PDBx/mmCIF and saves a '.cif' file.
+
**Find PDB crystal structures for a comparative analysis of protein structure:**
```bash
# Find PDB IDs associated with an Ensembl ID
diff --git a/docs/src/en/updates.md b/docs/src/en/updates.md
index f70e85465..0a6582640 100644
--- a/docs/src/en/updates.md
+++ b/docs/src/en/updates.md
@@ -4,6 +4,27 @@
#### *gget* officially became part of [*scverse*](https://scverse.org/) on June 9, 2026. 🥳🥳🥳
+**Version ≥ 0.30.8** (XXX XX, 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.
+ - New `residues=` filter (Python: int / list / range / set; CLI `--residues 100,200,300` or `100-200`) restricts `features` / `alignment` to specific positions client-side.
+ - `map` results gain a parsed `PDB Ids List` column (`list[str]`) alongside the comma-joined `PDB Ids` string, for direct chaining into [`gget pdb`](pdb.md).
+ - Fixed silent failure when the gene/UniProt pair was unknown: G2P returns HTTP 200 with a JSON `{"status":"failure",...}` body that was being parsed as a single TSV column. Now logged as an error and returns `None`.
+ - All failure modes now return `None` (was a mix of `None` and empty `DataFrame`).
+ - Added retries with exponential backoff on transient failures (5xx, connection errors, timeouts).
+ - URL-encoded path segments.
+ - New `out=` Python argument writes the result to an explicit CSV path (takes precedence over `save`).
+- [`gget alphafold`](alphafold.md): Added a new `jackhmmer_savedir` argument (`-jhd`/`--jackhmmer_savedir` on the command line) that lets you choose where the temporary jackhmmer files are stored. By default, `gget alphafold` still creates a `~/tmp/jackhmmer/` folder in your home directory (which can take up to ~2 GB of disk space); the new argument lets you redirect these files elsewhere, e.g. to a disk with more free space. Resolves [issue 49](https://github.com/scverse/gget/issues/49).
+- [`gget pdb`](pdb.md): Added support for the PDBx/mmCIF structure format (fixes [issue 178](https://github.com/scverse/gget/issues/178) and [issue 177](https://github.com/scverse/gget/issues/177)).
+ - New `resource="mmcif"` option downloads the structure in PDBx/mmCIF format (`.cif`).
+ - The default `resource="pdb"` now automatically falls back to PDBx/mmCIF when the legacy PDB file is unavailable (e.g. for large structures), since the legacy PDB format is being phased out by RCSB. A warning is logged and saved files use the correct extension (`.cif`).
+- [`gget opentargets`](opentargets.md): Adapted to several upstream Open Targets GraphQL API changes:
+ - **Fixed**: `gget opentargets resource="drugs"` was failing with `HTTP 400 — Field 'synonyms' of type '[DrugLabelAndSource!]!' must have a sub selection.` Open Targets changed `Drug.synonyms` and `Drug.tradeNames` from scalar lists to lists of structured `DrugLabelAndSource` objects. The internal GraphQL query was updated to request the `label` sub-field; the `drug.synonyms` / `drug.tradeNames` columns in the returned DataFrame remain `list[str]`, so existing user code is unaffected.
+ - **Fixed**: `gget opentargets resource="expression"` had started returning an empty DataFrame because Open Targets retired the `Target.expressions` field. The query now uses the current `Target.baselineExpression` field. **The returned columns have changed** because the upstream data model changed: results are now per-biosample (tissue/cell type) baseline expression summary statistics (`median`, `min`, `q1`, `q3`, `max`, `unit`) with `tissueBiosample`/`celltypeBiosample` identifiers and `datasourceId`/`datatypeId`, instead of the old per-tissue RNA `zscore`/`value`/`level` fields. A gene can have thousands of biosamples; the page size follows `--limit` (capped at the API's 3000-per-request maximum), and a warning is logged if results are truncated — narrow with `--filters` (e.g. `datasourceId`, `datatypeId`). Resolves [issue 247](https://github.com/scverse/gget/issues/247).
+ - **Docs**: Clarified the meaning of the `diseases` resource `score` column (OpenTargets' single overall target–disease association score, 0–1, aggregated across all data types/sources — not a per-data-source score) and that `disease.id` values are EFO-mapped traits that include not only MONDO diseases but also phenotypes (`HP_*`) and measurements (`EFO_*`), with an example of how to filter to MONDO disease terms only. Addresses [issue 168](https://github.com/scverse/gget/issues/168).
+- [`gget archs4`](archs4.md) (tissue mode): No longer crashes with `KeyError: ['color'] not found in axis` when ARCHS4 intermittently omits the optional `color` column from its CSV response. The column is now dropped only if present. Output also has a deterministic row order (sorted by `median` descending, with `id` as tiebreaker) so equal-median tissues no longer flip order between requests.
+- [`gget bgee`](bgee.md): Outbound Bgee API requests now send a `User-Agent: gget/` header so the Bgee service can attribute (and, if needed, allow-list) gget traffic, improving resilience against intermittent request blocking.
+
+
**Version ≥ 0.30.7** (Jun 21, 2026):
- [`gget cellxgene`](cellxgene.md): Added support for the three non-human primate species available in the CZ CELLxGENE Census LTS `2025-11-08`: rhesus macaque (`macaca_mulatta`), common marmoset (`callithrix_jacchus`), and chimpanzee (`pan_troglodytes`).
- The `species` argument (both Python and command line) now accepts all five supported organisms; the CLI `choices`, help text, and docstrings list them.
diff --git a/gget/gget_8cube.py b/gget/gget_8cube.py
index 16481e7cf..4e76c4dde 100644
--- a/gget/gget_8cube.py
+++ b/gget/gget_8cube.py
@@ -2,7 +2,7 @@
import io
import json as json_package
-from typing import Any
+from typing import Any, Literal, overload
import pandas as pd
import requests
@@ -54,12 +54,30 @@ def _normalize_gene_list(gene_list: list[str] | tuple[str, ...]) -> list[str]:
# --------------------------------------------------------------------
# 1. SPECIFICITY
# --------------------------------------------------------------------
+@overload
+def specificity(
+ gene_list: list[str] | tuple[str, ...],
+ json: Literal[False] = False,
+ save: bool = False,
+ verbose: bool = True,
+) -> pd.DataFrame: ...
+
+
+@overload
+def specificity(
+ gene_list: list[str] | tuple[str, ...],
+ json: Literal[True],
+ save: bool = False,
+ verbose: bool = True,
+) -> list[dict[str, Any]]: ...
+
+
def specificity(
gene_list: list[str] | tuple[str, ...],
json: bool = False,
save: bool = False,
verbose: bool = True,
-) -> Any:
+) -> pd.DataFrame | list[dict[str, Any]]:
"""Retrieve gene-level specificity statistics from the 8cubeDB.
(https://eightcubedb.onrender.com/).
@@ -126,6 +144,28 @@ def specificity(
# --------------------------------------------------------------------
# 2. PSI BLOCK
# --------------------------------------------------------------------
+@overload
+def psi_block(
+ gene_list: list[str] | tuple[str, ...],
+ analysis_level: str,
+ analysis_type: str,
+ json: Literal[False] = False,
+ save: bool = False,
+ verbose: bool = True,
+) -> pd.DataFrame: ...
+
+
+@overload
+def psi_block(
+ gene_list: list[str] | tuple[str, ...],
+ analysis_level: str,
+ analysis_type: str,
+ json: Literal[True],
+ save: bool = False,
+ verbose: bool = True,
+) -> list[dict[str, Any]]: ...
+
+
def psi_block(
gene_list: list[str] | tuple[str, ...],
analysis_level: str,
@@ -133,7 +173,7 @@ def psi_block(
json: bool = False,
save: bool = False,
verbose: bool = True,
-) -> Any:
+) -> pd.DataFrame | list[dict[str, Any]]:
"""Retrieve ψ_block (psi-block) specificity scores from the 8cubeDB.
ψ_block quantifies the specificity of a gene to a particular block
@@ -194,6 +234,28 @@ def psi_block(
# --------------------------------------------------------------------
# 3. GENE EXPRESSION
# --------------------------------------------------------------------
+@overload
+def gene_expression(
+ gene_list: list[str] | tuple[str, ...],
+ analysis_level: str,
+ analysis_type: str,
+ json: Literal[False] = False,
+ save: bool = False,
+ verbose: bool = True,
+) -> pd.DataFrame: ...
+
+
+@overload
+def gene_expression(
+ gene_list: list[str] | tuple[str, ...],
+ analysis_level: str,
+ analysis_type: str,
+ json: Literal[True],
+ save: bool = False,
+ verbose: bool = True,
+) -> list[dict[str, Any]]: ...
+
+
def gene_expression(
gene_list: list[str] | tuple[str, ...],
analysis_level: str,
@@ -201,7 +263,7 @@ def gene_expression(
json: bool = False,
save: bool = False,
verbose: bool = True,
-) -> Any:
+) -> pd.DataFrame | list[dict[str, Any]]:
"""Retrieve normalized gene expression values from 8cubeDB.
This endpoint returns mean and variance of normalized expression for the
diff --git a/gget/gget_alphafold.py b/gget/gget_alphafold.py
index db0ec7e64..9e578ed3b 100644
--- a/gget/gget_alphafold.py
+++ b/gget/gget_alphafold.py
@@ -140,8 +140,34 @@ def jackhmmer_chunk_callback(i: int) -> None:
return raw_msa_results
-def clean_up() -> None:
- """Function to clean up temporary files after running gget alphafold."""
+def get_jackhmmer_dir(jackhmmer_savedir: str | None = None) -> str:
+ """Return the directory used for jackhmmer's temporary FASTA and working files.
+
+ By default gget creates a "tmp" folder in the user's home directory
+ ("~/tmp/jackhmmer/"). These temporary files can take up to ~2 GB of disk space.
+ Passing a custom ``jackhmmer_savedir`` lets the user place this folder elsewhere
+ (e.g. on a disk with more free space), see https://github.com/scverse/gget/issues/49.
+
+ Args:
+ - jackhmmer_savedir Custom parent directory for the temporary jackhmmer folder (str).
+ If None (default), "~/tmp" is used.
+
+ Returns the absolute path to the temporary jackhmmer folder.
+ """
+ if jackhmmer_savedir is not None:
+ return os.path.abspath(os.path.join(jackhmmer_savedir, "jackhmmer", UUID))
+ return os.path.expanduser(os.path.join("~", "tmp", "jackhmmer", UUID))
+
+
+def clean_up(jackhmmer_dir: str | None = None) -> None:
+ """Function to clean up temporary files after running gget alphafold.
+
+ Args:
+ - jackhmmer_dir Path to the temporary jackhmmer folder to clean up (str).
+ If None (default), the default "~/tmp/jackhmmer/" is used.
+ """
+ if jackhmmer_dir is None:
+ jackhmmer_dir = get_jackhmmer_dir()
# # Remove fasta files with input sequences
# files = glob.glob("target_*.fasta")
# for f in files:
@@ -167,12 +193,13 @@ def clean_up() -> None:
# sys.stderr.write(stderr)
# Delete any fasta files left in temporary jackhmmer folder
- for file in glob.glob(os.path.expanduser(f"~/tmp/jackhmmer/{UUID}/*.fasta")):
+ for file in glob.glob(os.path.join(jackhmmer_dir, "*.fasta")):
if os.path.exists(file):
os.remove(file)
# Delete empty parent folders
- os.removedirs(os.path.expanduser(f"~/tmp/jackhmmer/{UUID}"))
+ if os.path.isdir(jackhmmer_dir):
+ os.removedirs(jackhmmer_dir)
def alphafold(
@@ -184,6 +211,7 @@ def alphafold(
plot: bool = True,
show_sidechains: bool = True,
verbose: bool = True,
+ jackhmmer_savedir: str | None = None,
) -> None:
"""Predicts the structure of a protein using a slightly simplified version of AlphaFold v2.3.0 (https://doi.org/10.1038/s41586-021-03819-2).
@@ -200,6 +228,10 @@ def alphafold(
- plot True/False whether to provide a graphical overview of the prediction (default: True).
- show_sidechains True/False whether to show side chains in the plot (default: True).
- verbose True/False whether to print progress information. Default True.
+ - jackhmmer_savedir Path to a parent directory in which to store the temporary jackhmmer
+ files (str). By default, gget creates a "tmp" folder in the home
+ directory ("~/tmp/jackhmmer/"), which can take up to ~2 GB of disk space.
+ Use this argument to place these temporary files elsewhere. Default: None.
Saves the predicted aligned error (json) and the prediction (PDB) in the defined 'out' folder.
@@ -500,16 +532,19 @@ class ModelType(enum.Enum):
TOTAL_JACKHMMER_CHUNKS = sum([cfg["num_streamed_chunks"] for cfg in MSA_DATABASES])
### Search against existing databases
+ # Resolve the temporary jackhmmer folder (optionally user-defined via jackhmmer_savedir)
+ jackhmmer_dir = get_jackhmmer_dir(jackhmmer_savedir)
+
# Get absolute path to output file and create output directory
if out is not None:
os.makedirs(out, exist_ok=True)
abs_out_path = os.path.abspath(out)
else:
# Use temporary jackhmmer folder which will later be deleted
- abs_out_path = os.path.expanduser(f"~/tmp/jackhmmer/{UUID}")
+ abs_out_path = jackhmmer_dir
# Create folder to save temporary jackhmmer database chunks in
- os.makedirs(os.path.expanduser(f"~/tmp/jackhmmer/{UUID}"), exist_ok=True)
+ os.makedirs(jackhmmer_dir, exist_ok=True)
features_for_chain = {}
raw_msa_results_for_sequence = {}
@@ -815,4 +850,4 @@ class ModelType(enum.Enum):
)
## Run clean_up function
- clean_up()
+ clean_up(jackhmmer_dir)
diff --git a/gget/gget_archs4.py b/gget/gget_archs4.py
index 36a46d4e0..e49a274bb 100644
--- a/gget/gget_archs4.py
+++ b/gget/gget_archs4.py
@@ -2,7 +2,7 @@
import io
import json as json_package
-from typing import Any
+from typing import Any, Literal, overload
import pandas as pd
import requests
@@ -17,6 +17,32 @@
from .gget_info import info # noqa: E402
+@overload
+def archs4(
+ gene: str,
+ ensembl: bool = False,
+ which: str = "correlation",
+ gene_count: int = 100,
+ species: str = "human",
+ json: Literal[True] = ...,
+ save: bool = False,
+ verbose: bool = True,
+) -> list[dict[str, Any]] | None: ...
+
+
+@overload
+def archs4(
+ gene: str,
+ ensembl: bool = False,
+ which: str = "correlation",
+ gene_count: int = 100,
+ species: str = "human",
+ json: Literal[False] = False,
+ save: bool = False,
+ verbose: bool = True,
+) -> pd.DataFrame | None: ...
+
+
def archs4(
gene: str,
ensembl: bool = False,
@@ -174,11 +200,15 @@ def archs4(
# Drop NaN rows
tissue_exp_df = tissue_exp_df.dropna()
- # Drop color columns
- tissue_exp_df = tissue_exp_df.drop(["color"], axis=1)
+ # Drop the "color" column if present (only used for plotting upstream, not by gget).
+ # ARCHS4 intermittently omits this column; use errors="ignore" so a missing
+ # "color" column does not raise a KeyError and crash the request.
+ tissue_exp_df = tissue_exp_df.drop(columns=["color"], errors="ignore")
- # Sort data frame by median expression
- tissue_exp_df = tissue_exp_df.sort_values("median", ascending=False)
+ # Sort data frame by median expression. Use "id" as a stable tiebreaker so the row
+ # order is deterministic when several tissues share the same median (ARCHS4 returns
+ # tied rows in a varying order between requests otherwise).
+ tissue_exp_df = tissue_exp_df.sort_values(["median", "id"], ascending=[False, True])
tissue_exp_df = tissue_exp_df.reset_index(drop=True)
if json:
diff --git a/gget/gget_bgee.py b/gget/gget_bgee.py
index 7788c3bed..d35e629b5 100644
--- a/gget/gget_bgee.py
+++ b/gget/gget_bgee.py
@@ -1,7 +1,8 @@
from __future__ import annotations
import json as json_
-from typing import TYPE_CHECKING, Any
+from importlib.metadata import PackageNotFoundError, version
+from typing import TYPE_CHECKING, Any, Literal, overload
from .utils import dig, http_json, json_list_to_df, set_up_logger
@@ -10,6 +11,13 @@
logger = set_up_logger()
+try:
+ _GGET_VERSION = version("gget")
+except PackageNotFoundError:
+ _GGET_VERSION = "unknown"
+
+_BGEE_HEADERS = {"User-Agent": f"gget/{_GGET_VERSION} (+https://github.com/scverse/gget)"}
+
def _bgee_species(gene_id: str, verbose: bool = True) -> int:
"""Get species ID from Bgee.
@@ -25,6 +33,7 @@ def _bgee_species(gene_id: str, verbose: bool = True) -> int:
"GET",
"https://bgee.org/api/",
context="Bgee API (species lookup)",
+ headers=_BGEE_HEADERS,
params={
"display_type": "json",
"page": "gene",
@@ -67,6 +76,7 @@ def _bgee_orthologs(gene_id: str, json: bool = False, verbose: bool = True) -> p
"GET",
"https://bgee.org/api/",
context="Bgee API (orthologs)",
+ headers=_BGEE_HEADERS,
params={
"display_type": "json",
"page": "gene",
@@ -130,6 +140,7 @@ def _bgee_expression(gene_id: str | list[str], json: bool = False, verbose: bool
"GET",
"https://bgee.org/api/",
context="Bgee API (expression)",
+ headers=_BGEE_HEADERS,
params={
"display_type": "json",
"page": "data",
@@ -169,13 +180,41 @@ def _bgee_expression(gene_id: str | list[str], json: bool = False, verbose: bool
return df
+@overload
+def bgee(
+ gene_id: str | list[str],
+ type: str,
+ json: Literal[True],
+ verbose: bool = ...,
+) -> list[dict[str, Any]]: ...
+
+
+@overload
+def bgee(
+ gene_id: str | list[str],
+ type: str = ...,
+ *,
+ json: Literal[True],
+ verbose: bool = ...,
+) -> list[dict[str, Any]]: ...
+
+
+@overload
+def bgee(
+ gene_id: str | list[str],
+ type: str = ...,
+ json: Literal[False] = False,
+ verbose: bool = ...,
+) -> pd.DataFrame: ...
+
+
# noinspection PyShadowingBuiltins
def bgee(
gene_id: str | list[str],
type: str = "orthologs",
json: bool = False,
verbose: bool = True,
-) -> pd.DataFrame | Any:
+) -> pd.DataFrame | list[dict[str, Any]]:
"""Get orthologs/expression data for a gene from Bgee (https://www.bgee.org/).
Args:
diff --git a/gget/gget_blast.py b/gget/gget_blast.py
index bb0f5b48c..c072820d3 100644
--- a/gget/gget_blast.py
+++ b/gget/gget_blast.py
@@ -3,7 +3,7 @@
import json as json_package
import time
from io import StringIO
-from typing import Any
+from typing import Any, Literal, overload
from urllib.parse import urlencode
# Using urllib instead of requests here because requests does not
@@ -25,6 +25,39 @@
)
+@overload
+def blast(
+ sequence: str,
+ program: str = "default",
+ database: str = "default",
+ limit: int = 50,
+ expect: float = 10.0,
+ low_comp_filt: bool = False,
+ megablast: bool = True,
+ verbose: bool = True,
+ wrap_text: bool = False,
+ *,
+ json: Literal[True],
+ save: bool = False,
+) -> list[dict[str, Any]] | None: ...
+
+
+@overload
+def blast(
+ sequence: str,
+ program: str = "default",
+ database: str = "default",
+ limit: int = 50,
+ expect: float = 10.0,
+ low_comp_filt: bool = False,
+ megablast: bool = True,
+ verbose: bool = True,
+ wrap_text: bool = False,
+ json: Literal[False] = False,
+ save: bool = False,
+) -> pd.DataFrame | None: ...
+
+
def blast(
sequence: str,
program: str = "default",
diff --git a/gget/gget_blat.py b/gget/gget_blat.py
index d1f88509f..0939eb0a0 100644
--- a/gget/gget_blat.py
+++ b/gget/gget_blat.py
@@ -3,7 +3,7 @@
import json as json_package
import time
from json.decoder import JSONDecodeError
-from typing import Any
+from typing import Any, Literal, overload
from urllib import request
from urllib.error import HTTPError, URLError
@@ -18,6 +18,28 @@
_BLAT_BACKOFF_BASE_SECONDS = 1.5
+@overload
+def blat(
+ sequence: str,
+ seqtype: str = ...,
+ assembly: str = ...,
+ json: Literal[True] = ...,
+ save: bool = ...,
+ verbose: bool = ...,
+) -> list[dict[str, Any]] | None: ...
+
+
+@overload
+def blat(
+ sequence: str,
+ seqtype: str = ...,
+ assembly: str = ...,
+ json: Literal[False] = ...,
+ save: bool = ...,
+ verbose: bool = ...,
+) -> pd.DataFrame | None: ...
+
+
def blat(
sequence: str,
seqtype: str = "default",
@@ -133,7 +155,7 @@ def blat(
## Build data frame to resemble BLAT web search results
# Define dataframe from dictionary
- df_dict = {}
+ df_dict: dict[str, list[Any]] = {}
for field in results["fields"]:
df_dict.update({field: []})
diff --git a/gget/gget_cosmic.py b/gget/gget_cosmic.py
index 020fdad58..594e9bf8e 100644
--- a/gget/gget_cosmic.py
+++ b/gget/gget_cosmic.py
@@ -9,7 +9,7 @@
import shutil
import subprocess
import tarfile
-from typing import Any
+from typing import Any, Literal, overload
import pandas as pd
@@ -315,6 +315,53 @@ def match_and_limit(mask, extract_fn):
return results
+@overload
+def cosmic(
+ searchterm: str | None,
+ cosmic_tsv_path: str | None = None,
+ limit: int = 100,
+ *,
+ json: Literal[True],
+ download_cosmic: bool = False,
+ cosmic_project: str | None = None,
+ cosmic_version: int | None = None,
+ grch_version: int = 37,
+ email: str | None = None,
+ password: str | None = None,
+ gget_mutate: bool = False,
+ keep_genome_info: bool = False,
+ remove_duplicates: bool = False,
+ seq_id_column: str = "seq_ID",
+ mutation_column: str = "mutation",
+ mut_id_column: str = "mutation_id",
+ out: str | None = None,
+ verbose: bool = True,
+) -> list[dict[str, Any]] | None: ...
+
+
+@overload
+def cosmic(
+ searchterm: str | None,
+ cosmic_tsv_path: str | None = None,
+ limit: int = 100,
+ json: Literal[False] = False,
+ download_cosmic: bool = False,
+ cosmic_project: str | None = None,
+ cosmic_version: int | None = None,
+ grch_version: int = 37,
+ email: str | None = None,
+ password: str | None = None,
+ gget_mutate: bool = False,
+ keep_genome_info: bool = False,
+ remove_duplicates: bool = False,
+ seq_id_column: str = "seq_ID",
+ mutation_column: str = "mutation",
+ mut_id_column: str = "mutation_id",
+ out: str | None = None,
+ verbose: bool = True,
+) -> pd.DataFrame | None: ...
+
+
def cosmic(
searchterm: str | None,
cosmic_tsv_path: str | None = None,
@@ -334,7 +381,7 @@ def cosmic(
mut_id_column: str = "mutation_id",
out: str | None = None,
verbose: bool = True,
-) -> Any:
+) -> pd.DataFrame | list[dict[str, Any]] | None:
"""Search for genes, mutations, etc associated with cancers using the COSMIC database.
(Catalogue Of Somatic Mutations In Cancer) database
diff --git a/gget/gget_diamond.py b/gget/gget_diamond.py
index f68bef39f..455b2bf7d 100644
--- a/gget/gget_diamond.py
+++ b/gget/gget_diamond.py
@@ -6,7 +6,9 @@
import subprocess
import sys
import uuid
-from typing import Any
+from typing import Any, Literal, overload
+
+import pandas as pd
from .compile import PACKAGE_PATH
from .utils import (
@@ -26,6 +28,37 @@
PRECOMPILED_DIAMOND_PATH = os.path.join(PACKAGE_PATH, f"bins/{platform.system()}/diamond")
+@overload
+def diamond(
+ query: str | list[str],
+ reference: str | list[str],
+ translated: bool = ...,
+ diamond_db: str | None = ...,
+ sensitivity: str = ...,
+ threads: int = ...,
+ diamond_binary: str | None = ...,
+ verbose: bool = ...,
+ *,
+ json: Literal[True],
+ out: str | None = ...,
+) -> list[dict[str, Any]]: ...
+
+
+@overload
+def diamond(
+ query: str | list[str],
+ reference: str | list[str],
+ translated: bool = ...,
+ diamond_db: str | None = ...,
+ sensitivity: str = ...,
+ threads: int = ...,
+ diamond_binary: str | None = ...,
+ verbose: bool = ...,
+ json: Literal[False] = False,
+ out: str | None = ...,
+) -> pd.DataFrame: ...
+
+
def diamond(
query: str | list[str],
reference: str | list[str],
@@ -37,7 +70,7 @@ def diamond(
verbose: bool = True,
json: bool = False,
out: str | None = None,
-) -> Any:
+) -> pd.DataFrame | list[dict[str, Any]]:
"""Align multiple protein or translated DNA sequences using DIAMOND (https://www.nature.com/articles/nmeth.3176).
Args:
diff --git a/gget/gget_elm.py b/gget/gget_elm.py
index f53e6f59e..f45680ae9 100644
--- a/gget/gget_elm.py
+++ b/gget/gget_elm.py
@@ -3,7 +3,7 @@
import json as json_package
import os
import re
-from typing import Any
+from typing import Any, Literal, overload
import numpy as np
import pandas as pd
@@ -223,6 +223,34 @@ def regex_match(sequence: str) -> pd.DataFrame:
return df_final
+@overload
+def elm(
+ sequence: str,
+ uniprot: bool = ...,
+ sensitivity: str = ...,
+ threads: int = ...,
+ diamond_binary: str | None = ...,
+ expand: bool = ...,
+ verbose: bool = ...,
+ json: Literal[True] = ...,
+ out: str | None = ...,
+) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: ...
+
+
+@overload
+def elm(
+ sequence: str,
+ uniprot: bool = ...,
+ sensitivity: str = ...,
+ threads: int = ...,
+ diamond_binary: str | None = ...,
+ expand: bool = ...,
+ verbose: bool = ...,
+ json: Literal[False] = ...,
+ out: str | None = ...,
+) -> tuple[pd.DataFrame, pd.DataFrame]: ...
+
+
def elm(
sequence: str,
uniprot: bool = False,
@@ -233,7 +261,7 @@ def elm(
verbose: bool = True,
json: bool = False,
out: str | None = None,
-) -> tuple[Any, Any]:
+) -> tuple[pd.DataFrame, pd.DataFrame] | tuple[list[dict[str, Any]], list[dict[str, Any]]]:
"""Locally predicts Eukaryotic Linear Motifs from an amino acid sequence or UniProt Acc using
data from the ELM database (http://elm.eu.org/).
diff --git a/gget/gget_enrichr.py b/gget/gget_enrichr.py
index 769316d6c..deb2c75b7 100644
--- a/gget/gget_enrichr.py
+++ b/gget/gget_enrichr.py
@@ -2,7 +2,7 @@
import json as json_package
import textwrap
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Literal, overload
# Plotting packages
import matplotlib.pyplot as plt
@@ -64,6 +64,46 @@ def clean_genes_list(genes_list: list[Any]) -> list[Any]:
return genes_clean
+@overload
+def enrichr(
+ genes: str | list[str],
+ database: str,
+ species: str = "human",
+ background_list: list[str] | None = None,
+ background: bool = False,
+ ensembl: bool = False,
+ ensembl_bkg: bool = False,
+ plot: bool = False,
+ figsize: tuple[float, float] = (10, 10),
+ ax: Axes | None = None,
+ kegg_out: str | None = None,
+ kegg_rank: int = 1,
+ json: Literal[True] = ...,
+ save: bool = False,
+ verbose: bool = True,
+) -> list[dict[str, Any]] | None: ...
+
+
+@overload
+def enrichr(
+ genes: str | list[str],
+ database: str,
+ species: str = "human",
+ background_list: list[str] | None = None,
+ background: bool = False,
+ ensembl: bool = False,
+ ensembl_bkg: bool = False,
+ plot: bool = False,
+ figsize: tuple[float, float] = (10, 10),
+ ax: Axes | None = None,
+ kegg_out: str | None = None,
+ kegg_rank: int = 1,
+ json: Literal[False] = False,
+ save: bool = False,
+ verbose: bool = True,
+) -> pd.DataFrame | None: ...
+
+
def enrichr(
genes: str | list[str],
database: str,
diff --git a/gget/gget_g2p.py b/gget/gget_g2p.py
index cc7bd77ea..9d060af6b 100644
--- a/gget/gget_g2p.py
+++ b/gget/gget_g2p.py
@@ -1,6 +1,10 @@
from __future__ import annotations
import io
+import json
+import time
+from functools import lru_cache
+from urllib.parse import quote
import pandas as pd
import requests
@@ -10,46 +14,257 @@
logger = set_up_logger()
+UNIPROT_ENTRY_URL = "https://rest.uniprot.org/uniprotkb/{accession}.json"
+UNIPROT_SEARCH_URL = "https://rest.uniprot.org/uniprotkb/search"
+
+
+@lru_cache(maxsize=256)
+def _resolve_gene_from_uniprot(uniprot_id: str) -> str | None:
+ """Look up the primary gene symbol for a UniProt accession via the UniProt REST entry endpoint.
+
+ Returns the gene symbol, or None if the accession is unknown or has no gene name.
+ Cached so repeated `gget g2p` calls for the same protein don't re-query UniProt.
+ """
+ url = UNIPROT_ENTRY_URL.format(accession=quote(uniprot_id, safe=""))
+ try:
+ r = requests.get(url, timeout=DEFAULT_REQUESTS_TIMEOUT)
+ except requests.exceptions.RequestException as e:
+ logger.error(f"UniProt lookup for '{uniprot_id}' failed: {e}")
+ return None
+ if not r.ok:
+ logger.error(
+ f"UniProt lookup for '{uniprot_id}' returned HTTP {r.status_code}. "
+ "Please check that this is a valid UniProt accession (e.g. 'P38398' or 'P01130-1')."
+ )
+ return None
+ try:
+ payload = r.json()
+ gene = payload["genes"][0]["geneName"]["value"]
+ except (json.JSONDecodeError, KeyError, IndexError, TypeError):
+ logger.error(f"UniProt returned an entry for '{uniprot_id}' but no primary gene symbol could be extracted.")
+ return None
+ return gene if isinstance(gene, str) and gene else None
+
+
+@lru_cache(maxsize=256)
+def _resolve_uniprot_from_gene(gene: str) -> tuple[str, int] | None:
+ """Look up the canonical reviewed human Swiss-Prot accession for a gene symbol.
+
+ Queries the UniProt REST search endpoint with
+ `gene_exact:{gene} AND organism_id:9606 AND reviewed:true` and returns
+ `(primary_accession, n_candidates)`. The candidate count lets the caller warn
+ when more than one reviewed entry matched. Returns None if no reviewed entry
+ was found.
+ """
+ params = {
+ "query": f"gene_exact:{gene} AND organism_id:9606 AND reviewed:true",
+ "format": "json",
+ "fields": "accession",
+ "size": 5,
+ }
+ try:
+ r = requests.get(UNIPROT_SEARCH_URL, params=params, timeout=DEFAULT_REQUESTS_TIMEOUT)
+ except requests.exceptions.RequestException as e:
+ logger.error(f"UniProt lookup for gene '{gene}' failed: {e}")
+ return None
+ if not r.ok:
+ logger.error(f"UniProt lookup for gene '{gene}' returned HTTP {r.status_code}.")
+ return None
+ try:
+ results = r.json().get("results", [])
+ except (json.JSONDecodeError, AttributeError):
+ logger.error(f"UniProt lookup for gene '{gene}' returned invalid JSON.")
+ return None
+ if not results:
+ return None
+ first = results[0] if isinstance(results[0], dict) else {}
+ accession = first.get("primaryAccession")
+ if not isinstance(accession, str) or not accession:
+ return None
+ return accession, len(results)
+
+
+def _normalize_residues(residues: object) -> set[int]:
+ """Coerce a `residues` argument (int / iterable of ints / range) to a set of ints."""
+ if isinstance(residues, bool):
+ raise ValueError("'residues' must be an int or an iterable of ints, not a bool.")
+ if isinstance(residues, int):
+ return {residues}
+ if isinstance(residues, (list, tuple, range, set, frozenset)):
+ try:
+ return {int(r) for r in residues}
+ except (TypeError, ValueError) as e:
+ raise ValueError(
+ "'residues' must be an int or an iterable of ints "
+ f"(e.g. [100, 200, 300] or range(100, 200)). Got: {residues!r}"
+ ) from e
+ raise ValueError(
+ f"'residues' must be an int or an iterable of ints (e.g. [100, 200, 300] or range(100, 200)). Got: {residues!r}"
+ )
+
+
+def _g2p_get_tsv(
+ url: str,
+ label: str,
+ retries: int = 3,
+ backoff: float = 1.0,
+) -> pd.DataFrame | None:
+ """GET a G2P endpoint expected to return TSV. Returns a parsed DataFrame or None on any failure.
+
+ Retries on transient failures (connection errors, timeouts, HTTP 5xx) with exponential
+ backoff. Detects JSON-shaped error bodies that the G2P portal sometimes returns over
+ the TSV channel with HTTP 200 (e.g. {"status":"failure","message":"No data for this gene."})
+ and surfaces them as logged errors instead of letting pandas parse them as a single column.
+ """
+ attempts = retries + 1
+ last_status: int | None = None
+ last_exc: Exception | None = None
+ for attempt in range(attempts):
+ try:
+ r = requests.get(url, timeout=DEFAULT_REQUESTS_TIMEOUT)
+ except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
+ last_exc = e
+ last_status = None
+ else:
+ if r.ok:
+ stripped = r.text.lstrip()
+ if not stripped:
+ logger.warning(f"{label}: the Genomics 2 Proteins portal returned an empty response.")
+ return None
+ if stripped[:1] in ("{", "["):
+ # JSON error body delivered over the TSV channel.
+ msg: str | None = None
+ try:
+ body = json.loads(stripped)
+ except json.JSONDecodeError:
+ body = None
+ if isinstance(body, dict):
+ m = body.get("message")
+ if isinstance(m, str):
+ msg = m
+ logger.error(
+ f"{label}: the Genomics 2 Proteins portal could not satisfy the request"
+ + (f": {msg}" if msg else ".")
+ + " Double-check that the gene symbol and UniProt accession match."
+ )
+ return None
+ return pd.read_csv(io.StringIO(r.text), sep="\t")
+ if r.status_code < 500:
+ logger.error(
+ f"{label}: the Genomics 2 Proteins portal returned HTTP {r.status_code}. "
+ "Double-check that the gene symbol and UniProt accession match."
+ )
+ return None
+ last_status = r.status_code
+ last_exc = None
+
+ if attempt < attempts - 1:
+ delay = backoff * (2**attempt)
+ reason = str(last_exc) if last_exc is not None else f"HTTP {last_status}"
+ logger.warning(
+ f"{label}: transient failure ({reason}); retrying in {delay:.1f}s (attempt {attempt + 2}/{attempts})."
+ )
+ time.sleep(delay)
+
+ if last_exc is not None:
+ logger.error(f"{label}: request failed after {attempts} attempts: {last_exc}")
+ else:
+ logger.error(f"{label}: returned HTTP {last_status} after {attempts} attempts.")
+ return None
+
def g2p(
- gene: str,
- uniprot_id: str,
+ gene: str | None = None,
+ uniprot_id: str | None = None,
resource: str = "features",
isoform: str | None = None,
+ residues: int | list[int] | tuple[int, ...] | range | set[int] | None = None,
save: bool = False,
+ out: str | None = None,
verbose: bool = True,
) -> pd.DataFrame | None:
- """Query the Genomics 2 Proteins (G2P) portal to link genes/proteins to per-residue structural and functional annotations.
+ """Query the Genomics 2 Proteins (G2P) portal for residue-level structural/functional annotations.
Portal: https://g2p.broadinstitute.org/
+ This module wraps the *public* G2P REST API (three endpoints: residue-level features,
+ isoform/structure mapping, and isoform alignment). The variant overlays the portal
+ shows in its web UI (gnomAD, ClinVar, HGMD) are not exposed by the public API and
+ therefore not available here — query the portal directly for those.
+
+ At least one of `gene` or `uniprot_id` is required. The other is resolved
+ automatically via the UniProt REST API and cached; when resolution happens it is
+ logged with the limitations.
+
+ The canonical pair used for the query is always prepended to the returned
+ DataFrame as `gene_name` and `uniprot_id` columns and stored as
+ `df.attrs["gene_name"]` / `df.attrs["uniprot_id"]`, so the output schema is
+ identical regardless of input mode and the chosen identifiers survive in saved
+ files.
+
Args:
- - gene Gene symbol, e.g. "BRCA1" (str).
- - uniprot_id UniProt accession, e.g. "P38398" (str). For resource="alignment" this is the
- canonical isoform accession (e.g. "P01130-1").
+ - gene Optional gene symbol, e.g. "BRCA1". If omitted, resolved from
+ `uniprot_id`. NOTE: gene-symbol → accession lookup is approximate
+ (gene symbols have synonyms and paralogues; the lookup picks the
+ *canonical reviewed human Swiss-Prot* entry — pass `uniprot_id`
+ explicitly for non-human organisms, unreviewed entries, or a
+ specific isoform).
+ - uniprot_id UniProt accession, e.g. "P38398". If omitted, resolved from `gene`.
+ For resource="alignment" this must be supplied explicitly as the
+ canonical isoform accession (e.g. "P01130-1"), because the
+ gene→UniProt path returns the base accession and cannot disambiguate
+ isoforms.
Tip: find a gene's UniProt ID with 'gget info'.
- resource Type of information to return (default: "features"):
- "features": per-residue feature table (AlphaFold pLDDT, UniProt sites,
- secondary structure, predicted pockets, PTMs, etc.).
+ "features": per-residue feature table (~140 columns). Includes:
+ AlphaFold pLDDT, DSSP secondary structure, accessible
+ surface area, UniProt sites (active/binding/domain/...),
+ PhosphoSitePlus PTMs, fpocket / af2bind / p2rank pocket
+ predictions, intra/inter-chain hydrogen bonds, non-bonded
+ interactions, disulfide bonds and salt bridges (from PDB
+ and AlphaFold), PFES (Protein Feature Enrichment Score)
+ and its sub-scores, and per-residue MaveDB experimental
+ functional scores. See https://github.com/broadinstitute/g2p-bis
+ for descriptions of each column.
"map": gene -> transcript -> protein isoform -> structure map
- (UniProt/Ensembl/RefSeq/PDB identifiers).
- "alignment": residue-level alignment between two isoforms (requires 'isoform';
- 'uniprot_id' is the canonical isoform).
+ (UniProt/Ensembl/RefSeq/PDB identifiers). The
+ comma-joined `PDB Ids` column is augmented with a
+ parsed `PDB Ids List` column (list[str]) for direct
+ consumption (e.g. with `gget pdb`).
+ "alignment": residue-level alignment between two isoforms (requires
+ 'isoform'; 'uniprot_id' is the canonical isoform).
- isoform Alternative isoform UniProt accession (e.g. "P01130-2"). Required when
resource="alignment" (default: None).
- - save If True, save the result as a CSV in the current working directory (default: False).
+ - residues Optional filter restricting the result to specific residue positions
+ (only applies to "features" and "alignment", which carry a
+ `residueId` column). Accepts an int, a list/tuple/set of ints, or a
+ range — e.g. `185`, `[185, 1775, 1812]`, `range(100, 200)`. Filtering
+ is performed client-side after fetching the full table.
+ - save If True, save the result as a CSV named
+ 'gget_g2p_{gene}_{uniprot_id}_{resource}.csv' in the current working
+ directory (default: False).
+ - out Optional explicit path to write the result as CSV. Takes precedence
+ over `save` (default: None).
- verbose True/False whether to print progress information (default: True).
- Returns a pandas DataFrame with the requested G2P information.
+ Returns a pandas DataFrame with the requested G2P information, or None if the query
+ failed (network error, invalid arguments, unknown gene/UniProt pair, or empty response).
"""
resources = ["features", "map", "alignment"]
if resource not in resources:
- raise ValueError(f"'resource' argument specified as {resource}. Expected one of: {', '.join(resources)}")
+ raise ValueError(f"'resource' argument specified as '{resource}'. Expected one of: {', '.join(resources)}")
- if not uniprot_id:
+ if not gene and not uniprot_id:
raise ValueError(
- "Please provide a UniProt accession as 'uniprot_id' (e.g. 'P38398'). "
- "You can find the UniProt ID for a gene using 'gget info'."
+ "Please provide at least one of 'gene' (e.g. 'BRCA1') or 'uniprot_id' "
+ "(e.g. 'P38398'). You can find both with 'gget info'."
+ )
+
+ if resource == "alignment" and not uniprot_id:
+ raise ValueError(
+ "resource='alignment' requires 'uniprot_id' to be specified explicitly as the "
+ "canonical isoform accession (e.g. 'P01130-1'). Gene→UniProt resolution returns "
+ "the base accession and cannot disambiguate isoforms."
)
if resource == "alignment" and not isoform:
@@ -58,46 +273,102 @@ def g2p(
"(e.g. 'P01130-2'). 'uniprot_id' is the canonical isoform (e.g. 'P01130-1')."
)
- # The G2P REST API returns tab-separated values
- base = f"{G2P_API}/gene/{gene}/protein/{uniprot_id}"
+ # Validate `residues` up-front (cheap, fails fast).
+ residue_set: set[int] | None = _normalize_residues(residues) if residues is not None else None
+ if residue_set is not None and resource == "map":
+ raise ValueError(
+ "The 'residues' filter does not apply to resource='map' (no per-residue table). "
+ "Use it with 'features' or 'alignment'."
+ )
+
+ if not uniprot_id:
+ if verbose:
+ logger.info(f"Resolving UniProt accession for gene '{gene}' via UniProt...")
+ lookup = _resolve_uniprot_from_gene(gene) # type: ignore[arg-type]
+ if not lookup:
+ raise ValueError(
+ f"No canonical reviewed human UniProt entry was found for gene '{gene}'. "
+ "Please pass the UniProt accession explicitly via 'uniprot_id' (find it with 'gget info')."
+ )
+ uniprot_id, n_candidates = lookup
+ if verbose:
+ extra = f" ({n_candidates} reviewed candidates matched; the first was chosen)" if n_candidates > 1 else ""
+ logger.info(
+ f"Resolved gene '{gene}' → UniProt '{uniprot_id}' "
+ f"(canonical reviewed human Swiss-Prot entry{extra}). "
+ "Gene-symbol lookup is approximate — it picks the canonical human "
+ "reviewed entry only. For non-human organisms, unreviewed entries, or "
+ "a specific isoform, pass 'uniprot_id' explicitly."
+ )
+
+ if not gene:
+ if verbose:
+ logger.info(f"Resolving gene symbol for UniProt accession '{uniprot_id}' via UniProt...")
+ gene = _resolve_gene_from_uniprot(uniprot_id)
+ if not gene:
+ raise ValueError(
+ f"Could not resolve a gene symbol for UniProt accession '{uniprot_id}'. "
+ "Please pass the gene symbol explicitly via the 'gene' argument."
+ )
+ if verbose:
+ logger.info(f"Resolved UniProt '{uniprot_id}' → gene '{gene}'.")
+
+ # G2P REST API path templates:
+ # /api/gene/{gene}/protein/{uniprotId}/protein-features
+ # /api/gene/{gene}/protein/{uniprotId}/gene-transcript-protein-isoform-structure-map
+ # /api/gene/{gene}/protein/{canonicalIsoform}/{alignmentIsoform}/alignment
+ gene_q = quote(gene, safe="")
+ uniprot_q = quote(uniprot_id, safe="")
+ base = f"{G2P_API}/gene/{gene_q}/protein/{uniprot_q}"
if resource == "features":
url = f"{base}/protein-features"
elif resource == "map":
url = f"{base}/gene-transcript-protein-isoform-structure-map"
else: # alignment
- url = f"{base}/{isoform}/alignment"
+ assert isoform is not None # validated above
+ url = f"{base}/{quote(isoform, safe='')}/alignment"
+ label = f"G2P {resource} ({gene} / {uniprot_id})"
if verbose:
- logger.info(f"Querying the Genomics 2 Proteins portal ('{resource}') for {gene} / {uniprot_id}...")
+ logger.info(f"Querying the Genomics 2 Proteins portal: {label}...")
- try:
- r = requests.get(
- url,
- timeout=DEFAULT_REQUESTS_TIMEOUT,
- headers={"Accept": "text/tab-separated-values"},
- )
- except requests.exceptions.RequestException as e:
- logger.error(f"Request to the Genomics 2 Proteins portal failed: {e}")
- return
+ df = _g2p_get_tsv(url, label=label)
+ if df is None:
+ return None
- if not r.ok:
- logger.error(
- f"G2P query (gene='{gene}', uniprot_id='{uniprot_id}', resource='{resource}') "
- f"returned status code {r.status_code}. Please double-check that the gene symbol "
- f"and UniProt accession match, then try again."
- )
- return
+ # Augment the `map` resource with a parsed list of PDB IDs alongside the
+ # comma-joined string column (so callers can pipe straight into `gget pdb`).
+ if resource == "map" and "PDB Ids" in df.columns:
+ pdb_lists = df["PDB Ids"].fillna("").map(lambda s: [p for p in str(s).split(",") if p])
+ df.insert(df.columns.get_loc("PDB Ids") + 1, "PDB Ids List", pdb_lists)
- if not r.text.strip():
- logger.warning("The Genomics 2 Proteins portal returned an empty result for this query.")
- return pd.DataFrame()
+ # Apply the residues filter (validated above; resource is features or alignment).
+ if residue_set is not None:
+ if "residueId" not in df.columns:
+ logger.warning(
+ f"{label}: 'residues' filter requested but the response has no 'residueId' "
+ "column; returning unfiltered result."
+ )
+ else:
+ df = df[df["residueId"].isin(residue_set)].reset_index(drop=True)
+ missing = sorted(residue_set - set(df["residueId"].tolist()))
+ if missing and verbose:
+ logger.warning(
+ f"{label}: {len(missing)} requested residue(s) not present in the response: "
+ f"{missing[:10]}{'...' if len(missing) > 10 else ''}"
+ )
- df = pd.read_csv(io.StringIO(r.text), sep="\t")
+ # Always prepend the canonical pair so the output schema is invariant across
+ # input modes (gene-only / uniprot-only / both) and survives `to_csv` / `to_json`.
+ df.attrs["gene_name"] = gene
+ df.attrs["uniprot_id"] = uniprot_id
+ df.insert(0, "uniprot_id", uniprot_id)
+ df.insert(0, "gene_name", gene)
- if save:
- out_name = f"gget_g2p_{gene}_{uniprot_id}_{resource}.csv"
- df.to_csv(out_name, index=False)
+ out_path: str | None = out if out else (f"gget_g2p_{gene}_{uniprot_id}_{resource}.csv" if save else None)
+ if out_path:
+ df.to_csv(out_path, index=False)
if verbose:
- logger.info(f"Results saved as {out_name}.")
+ logger.info(f"Results saved as {out_path}.")
return df
diff --git a/gget/gget_info.py b/gget/gget_info.py
index c38bd794f..a4005f612 100644
--- a/gget/gget_info.py
+++ b/gget/gget_info.py
@@ -1,7 +1,7 @@
from __future__ import annotations
import json as json_package
-from typing import Any
+from typing import Any, Literal, overload
import numpy as np
import pandas as pd
@@ -29,6 +29,37 @@
## gget info
+@overload
+def info(
+ ens_ids: str | list[str],
+ wrap_text: bool = ...,
+ ncbi: bool = ...,
+ uniprot: bool = ...,
+ pdb: bool = ...,
+ *,
+ json: Literal[True],
+ verbose: bool = ...,
+ save: bool = ...,
+ expand: bool = ...,
+ ensembl_only: bool = ...,
+) -> dict[str, Any] | None: ...
+
+
+@overload
+def info(
+ ens_ids: str | list[str],
+ wrap_text: bool = ...,
+ ncbi: bool = ...,
+ uniprot: bool = ...,
+ pdb: bool = ...,
+ json: Literal[False] = ...,
+ verbose: bool = ...,
+ save: bool = ...,
+ expand: bool = ...,
+ ensembl_only: bool = ...,
+) -> pd.DataFrame | None: ...
+
+
def info(
ens_ids: str | list[str],
wrap_text: bool = False,
@@ -393,7 +424,7 @@ def info(
ens_ids = []
# Dictionary to save clean info
- data = {
+ data: dict[str, list[Any]] = {
"all_transcripts": [],
"transcript_biotypes": [],
"transcript_names": [],
diff --git a/gget/gget_muscle.py b/gget/gget_muscle.py
index 69d0dcd82..a5053954d 100644
--- a/gget/gget_muscle.py
+++ b/gget/gget_muscle.py
@@ -122,7 +122,7 @@ def muscle(fasta: str | list[str], super5: bool = False, out: str | None = None,
# Get the titles and sequences from the generated .afa file
titles = []
seqs_master = []
- seqs = []
+ seqs: list[str] = []
with open(abs_out_path) as aln_file:
for i, line in enumerate(aln_file):
# Recognize title lines by the '>' character
diff --git a/gget/gget_opentargets.py b/gget/gget_opentargets.py
index edd90bc3f..8e75bed42 100644
--- a/gget/gget_opentargets.py
+++ b/gget/gget_opentargets.py
@@ -2,7 +2,7 @@
import json as json_
import textwrap
-from typing import Any
+from typing import Any, Literal, overload
import pandas as pd
@@ -43,8 +43,16 @@
}
}
description
- synonyms
- tradeNames
+ # synonyms and tradeNames are now [DrugLabelAndSource!]! (were scalar
+ # lists); query the `label` sub-field so _collapse_singletons flattens
+ # each {label: "X"} back to "X", preserving the list[str] shape users
+ # see in the resulting DataFrame.
+ synonyms {
+ label
+ }
+ tradeNames {
+ label
+ }
maximumClinicalStage
indications {
rows {
@@ -102,27 +110,44 @@
}
"""
+# OpenTargets retired the per-tissue `target.expressions` field (it now returns an
+# empty list for every gene). Baseline expression data moved to the paginated
+# `target.baselineExpression { count rows { ... } }` field, which provides per-biosample
+# (tissue and/or cell type) expression summary statistics. The page `size` is supplied
+# as a GraphQL variable so it can be driven by the caller's `limit` (and capped at the
+# API's documented max page size). `count` is the true total number of biosamples
+# upstream, used to warn the user when the result is truncated.
QUERY_STRING_EXPRESSION = """
-query target($ensemblId: String!) {
+query target($ensemblId: String!, $size: Int!) {
target(ensemblId: $ensemblId) {
- expressions {
- tissue {
- id
- label
- anatomicalSystems
- organs
- }
- rna {
- zscore
- value
+ baselineExpression(page: { index: 0, size: $size }) {
+ count
+ rows {
+ tissueBiosample {
+ biosampleId
+ biosampleName
+ }
+ celltypeBiosample {
+ biosampleId
+ biosampleName
+ }
+ median
+ min
+ q1
+ q3
+ max
unit
- level
+ datasourceId
+ datatypeId
}
}
}
}
"""
+# OpenTargets caps a single GraphQL page at 3000 rows.
+MAX_EXPRESSION_PAGE_SIZE = 3000
+
QUERY_STRING_DEPMAP = """
query target($ensemblId: String!) {
target(ensemblId: $ensemblId) {
@@ -255,6 +280,30 @@ def _unhash(x: Any) -> Any:
return x
+@overload
+def opentargets(
+ ensembl_id: str,
+ resource: str = "diseases",
+ limit: int | None = None,
+ verbose: bool = True,
+ wrap_text: bool = False,
+ filters: dict[str, Any] | None = None,
+ json: Literal[False] = False,
+) -> pd.DataFrame: ...
+
+
+@overload
+def opentargets(
+ ensembl_id: str,
+ resource: str = "diseases",
+ limit: int | None = None,
+ verbose: bool = True,
+ wrap_text: bool = False,
+ filters: dict[str, Any] | None = None,
+ json: Literal[True] = ...,
+) -> list[dict[str, Any]]: ...
+
+
def opentargets(
ensembl_id: str,
resource: str = "diseases",
@@ -263,7 +312,7 @@ def opentargets(
wrap_text: bool = False,
filters: dict[str, Any] | None = None,
json: bool = False,
-) -> Any:
+) -> pd.DataFrame | list[dict[str, Any]]:
"""Query OpenTargets for data associated with a given Ensembl gene ID.
Args:
@@ -274,7 +323,9 @@ def opentargets(
"drugs": Returns drugs associated with the gene.
"tractability": Returns tractability data for the gene.
"pharmacogenetics": Returns pharmacogenetics data for the gene.
- "expression": Returns gene expression data (by tissues, organs, and anatomical systems).
+ "expression": Returns baseline expression per biosample (tissue and/or cell type) as
+ summary statistics (median, min, q1, q3, max, unit) with tissueBiosample/
+ celltypeBiosample identifiers and datasourceId/datatypeId.
"depmap": Returns DepMap gene-disease effect data for the gene.
"interactions": Returns protein-protein interactions for the gene.
- limit Limit the number of results returned (default: No limit).
@@ -301,7 +352,11 @@ def opentargets(
rows_path = ["pharmacogenomics"]
elif resource == "expression":
query_string = QUERY_STRING_EXPRESSION
- rows_path = ["expressions"]
+ rows_path = ["baselineExpression", "rows"]
+ # Drive the page size from `limit` so a small limit fetches only what's needed
+ # and (crucially) a large/absent limit fetches everything up to the API max,
+ # rather than silently capping at a fixed page size.
+ expression_page_size = min(limit, MAX_EXPRESSION_PAGE_SIZE) if limit is not None else MAX_EXPRESSION_PAGE_SIZE
elif resource == "depmap":
query_string = QUERY_STRING_DEPMAP
rows_path = [
@@ -317,6 +372,8 @@ def opentargets(
)
variables = {"ensemblId": ensembl_id}
+ if resource == "expression":
+ variables["size"] = expression_page_size
if verbose:
logger.info(f"Querying OpenTargets for {resource} associated with {ensembl_id}...")
@@ -341,6 +398,20 @@ def opentargets(
api_target = dig(api_response, "data", "target", context="OpenTargets GraphQL")
+ # Warn when the gene has more baseline-expression biosamples upstream than a single
+ # API page can return (only possible when the user did not set an explicit limit;
+ # an explicit limit is treated as intentional). Without this, the result would be a
+ # silent partial slice of the data.
+ if resource == "expression" and limit is None:
+ total = (api_target.get("baselineExpression") or {}).get("count")
+ if isinstance(total, int) and total > MAX_EXPRESSION_PAGE_SIZE:
+ logger.warning(
+ f"{ensembl_id} has {total} baseline-expression biosamples, but the OpenTargets API "
+ f"returns at most {MAX_EXPRESSION_PAGE_SIZE} per request; the result is truncated to "
+ f"{MAX_EXPRESSION_PAGE_SIZE}. Use the 'filters' argument (e.g. datasourceId or datatypeId) "
+ "to narrow the query to the biosamples you need."
+ )
+
rows = api_target
for row_key in rows_path:
if not row_key.startswith("_FLATTEN_"):
diff --git a/gget/gget_pdb.py b/gget/gget_pdb.py
index d58a9a5c7..ddb8df91a 100644
--- a/gget/gget_pdb.py
+++ b/gget/gget_pdb.py
@@ -18,7 +18,11 @@ def pdb(pdb_id: str, resource: str = "pdb", identifier: str | int | None = None,
Args:
- pdb_id PDB ID to be queried (str), e.g. "7S7U".
- resource Defines type of information to be returned.
- "pdb": Returns the protein structure in PDB format (default).
+ "pdb": Returns the protein structure in legacy PDB format (default).
+ Note: the legacy PDB format is being phased out by RCSB and is
+ not available for large structures. When the legacy PDB file does
+ not exist, gget automatically falls back to the PDBx/mmCIF format.
+ "mmcif": Returns the protein structure in PDBx/mmCIF format (.cif).
"entry": Information about PDB structures at the top level of PDB structure hierarchical data organization.
"pubmed": Get PubMed annotations (data integrated from PubMed) for a given entry's primary citation.
"assembly": Information about PDB structures at the quaternary structure level.
@@ -33,11 +37,16 @@ def pdb(pdb_id: str, resource: str = "pdb", identifier: str | int | None = None,
Assembly/entity IDs are numbers (e.g. 1), and chain IDs are letters (e.g. "A").
- save True/False wether to save JSON/PDB with query results in the current working directory (default: False).
- Returns requested information in JSON format (except for resource="pdb" which returns protein structure in PDB format).
+ Returns requested information in JSON format (except for resource="pdb"/"mmcif" which
+ return the protein structure in PDB/PDBx-mmCIF format).
"""
+ # Resources that download a structure file (text), not JSON
+ structure_resources = ["pdb", "mmcif"]
+
# Check if resource argument is valid
resources = [
"pdb",
+ "mmcif",
"entry",
"pubmed",
"assembly",
@@ -73,26 +82,36 @@ def pdb(pdb_id: str, resource: str = "pdb", identifier: str | int | None = None,
if resource in need_chain_id and identifier is None:
raise ValueError("Please define chain ID (e.g. 'A') as 'identifier'.")
- # Define URLs for HTTP request
- if resource != "pdb":
- # URLs to request resources other than PDB file
+ # Define URLs for HTTP request.
+ # Each entry is (url, fetched_format) so we can track which structure format
+ # was actually returned ("pdb"/"mmcif"/None for JSON resources).
+ urls: list[tuple[str, str | None]]
+
+ if resource not in structure_resources:
+ # URLs to request resources other than a structure file
if identifier is not None:
url = f"{RCSB_PDB_API}{resource}/{pdb_id}/{identifier}"
else:
url = f"{RCSB_PDB_API}{resource}/{pdb_id}"
- urls = [url] # only one option for JSON resources
+ urls = [(url, None)] # only one option for JSON resources
+ elif resource == "mmcif":
+ # PDBx/mmCIF file
+ urls = [(f"https://files.rcsb.org/download/{pdb_id}.cif", "mmcif")]
else:
- # Try wwPDB first, then RCSB as fallback
+ # Legacy PDB format: try wwPDB first, then RCSB, then automatically
+ # fall back to PDBx/mmCIF (legacy PDB is unavailable for large structures).
urls = [
- f"https://files.wwpdb.org/download/{pdb_id}.pdb",
- f"https://files.rcsb.org/download/{pdb_id}.pdb",
+ (f"https://files.wwpdb.org/download/{pdb_id}.pdb", "pdb"),
+ (f"https://files.rcsb.org/download/{pdb_id}.pdb", "pdb"),
+ (f"https://files.rcsb.org/download/{pdb_id}.cif", "mmcif"),
]
# Submit URL request with fallback logic
r = None
code = None
- for url in urls:
+ fetched_format = None
+ for url, fmt in urls:
try:
r = urlopen(url)
@@ -102,6 +121,7 @@ def pdb(pdb_id: str, resource: str = "pdb", identifier: str | int | None = None,
code = r.getcode()
if code == 200:
+ fetched_format = fmt
break
except HTTPError:
continue
@@ -123,7 +143,7 @@ def pdb(pdb_id: str, resource: str = "pdb", identifier: str | int | None = None,
logger.error(f"{resource} for {pdb_id} was not found. Please double-check arguments and try again.")
return
- if resource != "pdb":
+ if resource not in structure_resources:
# Read json formatted results
results = json.load(r)
@@ -132,11 +152,19 @@ def pdb(pdb_id: str, resource: str = "pdb", identifier: str | int | None = None,
if ids is not None:
ids.sort()
else:
- # Read PDB file
+ # Read structure file (PDB or PDBx/mmCIF)
results = r.read().decode()
+ # Inform the user if a legacy PDB request was served as mmCIF instead
+ if resource == "pdb" and fetched_format == "mmcif":
+ logger.warning(
+ f"The legacy PDB format is not available for {pdb_id} (it is being phased out "
+ f"by RCSB). Returning the PDBx/mmCIF format instead. "
+ f"Use resource='mmcif' to request it directly and silence this warning."
+ )
+
if save:
- if resource != "pdb":
+ if resource not in structure_resources:
# Save the results in json format
if identifier is not None:
out_name = f"{pdb_id}_{identifier}_{resource}.json"
@@ -147,8 +175,9 @@ def pdb(pdb_id: str, resource: str = "pdb", identifier: str | int | None = None,
json.dump(results, f, ensure_ascii=False, indent=4)
else:
- # Save the PDB file
- with open(f"{pdb_id}.pdb", "w") as f:
+ # Save the structure file using the extension of the format actually fetched
+ extension = "cif" if fetched_format == "mmcif" else "pdb"
+ with open(f"{pdb_id}.{extension}", "w") as f:
f.write(results)
return results
diff --git a/gget/gget_ref.py b/gget/gget_ref.py
index f81b7fc4a..b16391c41 100644
--- a/gget/gget_ref.py
+++ b/gget/gget_ref.py
@@ -359,7 +359,7 @@ def ref(
## Return results
# If FTP=False, return dictionary/json of specified results
if ftp is False:
- ref_dict = {species: {}}
+ ref_dict: dict[str, dict[str, Any]] = {species: {}}
for return_val in which:
if return_val == "all":
ref_dict = {
diff --git a/gget/gget_search.py b/gget/gget_search.py
index a5ed75529..e8df84c85 100644
--- a/gget/gget_search.py
+++ b/gget/gget_search.py
@@ -3,7 +3,7 @@
import json as json_package
import time
import warnings
-from typing import Any
+from typing import Any, Literal, overload
import mysql.connector as sql
import numpy as np
@@ -37,6 +37,38 @@ def clean_cols(x: Any) -> Any:
return x
+@overload
+def search(
+ searchwords: str | list[str],
+ species: str,
+ release: int | None = None,
+ id_type: str = "gene",
+ seqtype: str | None = None,
+ andor: str = "or",
+ limit: int | None = None,
+ wrap_text: bool = False,
+ json: Literal[True] = ...,
+ save: bool = False,
+ verbose: bool = True,
+) -> list[dict[str, Any]] | None: ...
+
+
+@overload
+def search(
+ searchwords: str | list[str],
+ species: str,
+ release: int | None = None,
+ id_type: str = "gene",
+ seqtype: str | None = None,
+ andor: str = "or",
+ limit: int | None = None,
+ wrap_text: bool = False,
+ json: Literal[False] = False,
+ save: bool = False,
+ verbose: bool = True,
+) -> pd.DataFrame | None: ...
+
+
def search(
searchwords: str | list[str],
species: str,
@@ -49,7 +81,7 @@ def search(
json: bool = False,
save: bool = False,
verbose: bool = True,
-) -> Any:
+) -> pd.DataFrame | list[dict[str, Any]] | None:
"""Function to query Ensembl for genes based on species and free form search terms.
Automatically fetches results from latest Ensembl release, unless user specifies database (see 'species' argument)
diff --git a/gget/gget_virus.py b/gget/gget_virus.py
index 8a04dfdec..7742e6a91 100644
--- a/gget/gget_virus.py
+++ b/gget/gget_virus.py
@@ -1453,7 +1453,7 @@ def fetch_virus_metadata(
# e.g., "NC_045512.2%2CMN908947.3%2CMT020781.1"
url = f"{NCBI_API_BASE}/virus/accession/{virus}/dataset_report"
logger.debug("Using accession endpoint for virus: %s", virus)
- params = {}
+ params: dict[str, Any] = {}
else:
# For taxon names/IDs (e.g., 'Zika Virus', 'influenza'), use the taxon endpoint
url = f"{NCBI_API_BASE}/virus/taxon/{virus}/dataset_report"
@@ -4381,7 +4381,7 @@ def _load_metadata_dict_from_temp_jsonl(temp_file_path: str) -> dict[str, Any]:
Same format as load_metadata_from_api_reports().
"""
- metadata_dict = {}
+ metadata_dict: dict[str, Any] = {}
processed_count = 0
skipped_count = 0
@@ -4471,7 +4471,7 @@ def _load_cached_metadata_from_jsonl(jsonl_path: str) -> dict[str, Any]:
dict: Dictionary mapping accession numbers to metadata dictionaries.
"""
- metadata_dict = {}
+ metadata_dict: dict[str, Any] = {}
processed_count = 0
if not jsonl_path or not os.path.exists(jsonl_path):
@@ -4565,7 +4565,7 @@ def _stream_filter_cached_metadata_from_jsonl(
if "min_release_date" in filters_active:
min_release_date_parsed = _parse_date(min_release_date, filtername="min_release_date")
- metadata_dict = {}
+ metadata_dict: dict[str, Any] = {}
total_records = 0
filter_stats = {
"host": 0,
@@ -4967,7 +4967,7 @@ def filter_sequences(
# Initialize lists to store filtered results (metadata is small, kept in memory)
filtered_metadata = [] # Will store corresponding metadata dictionaries
- protein_headers = [] # Will store protein/segment information from FASTA headers
+ protein_headers: list[Any] = [] # Will store protein/segment information from FASTA headers
filtered_count = 0 # Count of sequences passing all filters
# Counters for logging filter statistics
@@ -5264,7 +5264,7 @@ def save_command_summary(
f.write(f"Average sequence length: {sum(lengths) / len(lengths):.0f} bp\n\n")
# Completeness breakdown
- completeness_counts = {}
+ completeness_counts: dict[str, int] = {}
for meta in filtered_metadata:
comp = meta.get("completeness", "unknown")
completeness_counts[comp] = completeness_counts.get(comp, 0) + 1
@@ -5274,7 +5274,7 @@ def save_command_summary(
f.write("\n")
# Source database breakdown
- source_counts = {}
+ source_counts: dict[str, int] = {}
for meta in filtered_metadata:
source = meta.get("sourceDatabase", "unknown")
source_counts[source] = source_counts.get(source, 0) + 1
@@ -6181,7 +6181,7 @@ def fetch_genbank_metadata(
_log_memory_usage("GenBank fetch start")
# Initialize tracking variables
- all_metadata = {} # Only used for final return; populated from temp JSONL at end
+ all_metadata: dict[str, Any] = {} # Only used for final return; populated from temp JSONL at end
failed_batches = []
total_metadata_written = 0 # Counter for metadata records streamed to disk
@@ -7053,7 +7053,7 @@ def _parse_genbank_xml(xml_content: str) -> dict[str, Any]:
logger.debug("Processing GenBank record: %s", accession)
# Initialize metadata dictionary for this record
- metadata = {
+ metadata: dict[str, Any] = {
"accession": accession,
"genbank_data": {}, # Store GenBank-specific fields
}
@@ -8741,14 +8741,14 @@ def virus(
refseq_only = False
# Initialize filter stats for each stage (populated by filter functions)
- metadata_filter_stats = {}
- genbank_filter_stats = {}
- sequence_filter_stats = {}
+ metadata_filter_stats: dict[str, int] = {}
+ genbank_filter_stats: dict[str, int] = {}
+ sequence_filter_stats: dict[str, int] = {}
total_after_genbank_filter = None
total_after_sequence_filter = None
# Initialize failed commands tracker for tracking all types of failures
- failed_commands = {
+ failed_commands: dict[str, Any] = {
"api_timeout": None,
"empty_response": None,
"sequence_batches": [],
@@ -9768,7 +9768,7 @@ def virus(
logger.warning("No sequences will pass the GenBank-based filters.")
filtered_accessions = []
filtered_metadata = []
- genbank_data_prefetch = {}
+ genbank_data_prefetch: dict[str, Any] = {}
save_command_summary(
outfolder=outfolder,
@@ -9985,7 +9985,7 @@ def virus(
# For large datasets (millions of sequences), this avoids OOM errors
total_final_sequences = _stream_copy_fasta(fna_file, output_fasta_file)
filtered_metadata_final = filtered_metadata # No change to metadata
- protein_headers = []
+ protein_headers: list[Any] = []
logger.info("All %d downloaded sequences were streamed to output", total_final_sequences)
_log_memory_usage("after streaming sequences to output")
else:
diff --git a/gget/main.py b/gget/main.py
index 49cfe7b4c..7a2944b09 100644
--- a/gget/main.py
+++ b/gget/main.py
@@ -1329,6 +1329,18 @@ def main() -> None:
"Default: ./[date_time]_gget_alphafold_prediction"
),
)
+ parser_alphafold.add_argument(
+ "-jhd",
+ "--jackhmmer_savedir",
+ type=str,
+ default=None,
+ required=False,
+ help=(
+ "Path to a parent directory in which to store the temporary jackhmmer files.\n"
+ "By default, gget creates a 'tmp' folder in the home directory ('~/tmp/jackhmmer/'),\n"
+ "which can take up to ~2 GB of disk space. Use this argument to place them elsewhere."
+ ),
+ )
parser_alphafold.add_argument(
"-q",
"--quiet",
@@ -1361,6 +1373,7 @@ def main() -> None:
type=str,
choices=[
"pdb",
+ "mmcif",
"entry",
"pubmed",
"assembly",
@@ -1375,7 +1388,10 @@ def main() -> None:
required=False,
help=(
"Defines type of information to be returned.\n\n"
- '"pdb": Returns the protein structure in PDB format.\n'
+ '"pdb": Returns the protein structure in legacy PDB format. Automatically falls\n'
+ " back to PDBx/mmCIF when the legacy PDB file is unavailable (e.g. for\n"
+ " large structures), since the legacy PDB format is being phased out by RCSB.\n"
+ '"mmcif": Returns the protein structure in PDBx/mmCIF format (.cif).\n'
'"entry": Information about PDB structures at the top level of PDB structure hierarchical data organization.\n'
'"pubmed": Get PubMed annotations (data integrated from PubMed) for a given entry\'s primary citation.\n'
'"assembly": Information about PDB structures at the quaternary structure level.\n'
@@ -1406,7 +1422,7 @@ def main() -> None:
required=False,
help=(
"Path to the file the results will be saved in, e.g. path/to/directory/7S7U.pdb or path/to/directory/7S7U_entry.json.\n"
- "Resource 'pdb' is returned in PDB format. All other resources are returned in JSON format.\n"
+ "Resource 'pdb' is returned in PDB format (or PDBx/mmCIF if the legacy PDB file is unavailable), 'mmcif' in PDBx/mmCIF format. All other resources are returned in JSON format.\n"
"Default: Standard out."
),
)
@@ -2339,14 +2355,26 @@ def main() -> None:
parser_g2p.add_argument(
"gene",
type=str,
- help="Gene symbol, e.g. BRCA1.",
+ nargs="?",
+ default=None,
+ help=(
+ "Optional gene symbol, e.g. BRCA1. If omitted, the gene is resolved "
+ "automatically from --uniprot_id via the UniProt REST API."
+ ),
)
parser_g2p.add_argument(
"-u",
"--uniprot_id",
type=str,
- required=True,
- help="UniProt accession, e.g. P38398. For '--resource alignment' this is the canonical isoform (e.g. P01130-1). Find it with `gget info`.",
+ required=False,
+ default=None,
+ help=(
+ "UniProt accession, e.g. P38398. Optional — if omitted, it is resolved from the "
+ "gene symbol via UniProt (canonical reviewed human Swiss-Prot entry only; pass "
+ "explicitly for non-human, unreviewed, or specific-isoform queries). "
+ "For '--resource alignment' this is the canonical isoform (e.g. P01130-1) and is "
+ "required. Find it with `gget info`."
+ ),
)
parser_g2p.add_argument(
"-r",
@@ -2370,6 +2398,17 @@ def main() -> None:
required=False,
help="Alternative isoform UniProt accession (e.g. P01130-2). Required for '--resource alignment'.",
)
+ parser_g2p.add_argument(
+ "--residues",
+ type=str,
+ default=None,
+ required=False,
+ help=(
+ "Restrict the result to specific residue positions (only applies to --resource "
+ "features / alignment). Comma-separated list and/or inclusive ranges, "
+ "e.g. '185,1775,1812' or '100-200' or '1-50,185,300-310'."
+ ),
+ )
parser_g2p.add_argument(
"-o",
"--out",
@@ -3676,6 +3715,7 @@ def main() -> None:
plot=False,
show_sidechains=False,
verbose=args.quiet,
+ jackhmmer_savedir=args.jackhmmer_savedir,
)
## pdb return
@@ -3687,7 +3727,7 @@ def main() -> None:
)
if pdb_results:
- if args.resource == "pdb":
+ if args.resource in ("pdb", "mmcif"):
if args.out:
# Create saving directory
directory = "/".join(args.out.split("/")[:-1])
@@ -3793,11 +3833,36 @@ def main() -> None:
## g2p return
if args.command == "g2p":
+ residues: list[int] | None = None
+ if args.residues:
+ residues = []
+ for part in args.residues.split(","):
+ part = part.strip()
+ if not part:
+ continue
+ if "-" in part:
+ try:
+ start_s, end_s = part.split("-", 1)
+ start, end = int(start_s), int(end_s)
+ except ValueError as e:
+ raise ValueError(
+ f"Could not parse '{part}' as a residue range. Expected 'start-end' (e.g. '100-200')."
+ ) from e
+ residues.extend(range(start, end + 1))
+ else:
+ try:
+ residues.append(int(part))
+ except ValueError as e:
+ raise ValueError(
+ f"Could not parse '{part}' as a residue position. Expected an int (e.g. '185')."
+ ) from e
+
g2p_results: pd.DataFrame = g2p(
args.gene,
uniprot_id=args.uniprot_id,
resource=args.resource,
isoform=args.isoform,
+ residues=residues,
verbose=args.quiet,
)
diff --git a/gget/utils.py b/gget/utils.py
index 2d86a564c..040730e39 100644
--- a/gget/utils.py
+++ b/gget/utils.py
@@ -103,7 +103,7 @@ def http_json(
url: str,
*,
context: str = "",
- timeout: float = DEFAULT_REQUESTS_TIMEOUT,
+ timeout: float | tuple[float, float] = DEFAULT_REQUESTS_TIMEOUT,
retries: int = 3,
backoff: float = 1.0,
**kwargs: Any,
@@ -1138,7 +1138,7 @@ def json_list_to_df(json_list: list[Any], columns: list[tuple[str, str]]) -> pd.
Returns data frame with columns as specified in keys.
"""
- tmp_columns = [[] for _ in range(len(columns))]
+ tmp_columns: list[list[Any]] = [[] for _ in range(len(columns))]
for json_obj in json_list:
for i, column_key in enumerate(columns):
@@ -1186,7 +1186,7 @@ def parse(filename: str, format: str | None = None) -> Iterator[FastaRecord]:
with open(filename, encoding="utf-8") as handle:
current_id = None
current_description = ""
- current_seq = []
+ current_seq: list[str] = []
for line in handle:
line = line.strip()
diff --git a/pyproject.toml b/pyproject.toml
index 2b7f7fb57..f5deb8f88 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ requires = [ "hatchling" ]
[project]
name = "gget"
-version = "0.30.7"
+version = "0.30.8"
description = "Efficient querying of genomic databases."
readme = "README.md"
keywords = [ "gget" ]
@@ -59,15 +59,16 @@ dev = [ "pre-commit" ]
test = [
"bravado==11.0.3",
"coverage>=7",
- "mypy>=1.13",
+ "mypy==1.13",
+ "mypy-baseline==0.7.4",
"openai<=0.28.1",
- "pandas-stubs",
+ "pandas-stubs==3.0.3.260530",
"parameterized==0.9",
"pytest>=7",
"pytest-cov>=6.2.1",
- "types-beautifulsoup4",
- "types-requests",
- "types-tqdm",
+ "types-beautifulsoup4==4.12.0.20250516",
+ "types-requests==2.33.0.20260518",
+ "types-tqdm==4.68.0.20260608",
]
[tool.hatch]
@@ -152,3 +153,9 @@ run.omit = [
"gget/main.py",
]
run.source = [ "gget" ]
+
+# mypy-baseline grandfathers the pre-existing type errors so the pre-commit
+# mypy hook only fails on NEW errors. Refresh after fixing some with:
+# mypy gget | mypy-baseline sync
+[tool.mypy-baseline]
+baseline_path = ".mypy-baseline.txt"
diff --git a/tests/fixtures/test_archs4.json b/tests/fixtures/test_archs4.json
index 5b060b564..73b24a471 100644
--- a/tests/fixtures/test_archs4.json
+++ b/tests/fixtures/test_archs4.json
@@ -598,14 +598,6 @@
7.62057,
8.60009
],
- [
- "System.Nervous System.CNS.THALAMUS",
- 3.29066,
- 5.995,
- 7.14836,
- 7.83426,
- 8.71165
- ],
[
"System.Nervous System.CNS.HYPOTHALAMUS",
3.44188,
@@ -614,6 +606,14 @@
7.99022,
8.81727
],
+ [
+ "System.Nervous System.CNS.THALAMUS",
+ 3.29066,
+ 5.995,
+ 7.14836,
+ 7.83426,
+ 8.71165
+ ],
[
"System.Immune System.Lymphoid.BLYMPHOCYTE",
5.57377,
@@ -678,14 +678,6 @@
7.42671,
8.89625
],
- [
- "System.Immune System.Lymphoid.TLYMPHOCYTE",
- 5.60813,
- 6.60849,
- 6.9754,
- 7.26685,
- 7.85409
- ],
[
"System.Connective Tissue.Bone marrow.CHONDROCYTE",
5.93985,
@@ -694,6 +686,14 @@
7.44552,
8.78176
],
+ [
+ "System.Immune System.Lymphoid.TLYMPHOCYTE",
+ 5.60813,
+ 6.60849,
+ 6.9754,
+ 7.26685,
+ 7.85409
+ ],
[
"System.Immune System.Myeloid.MACROPHAGE",
0.113644,
@@ -702,14 +702,6 @@
7.38198,
8.46912
],
- [
- "System.Respiratory System.Lung.LUNG",
- 0.113644,
- 5.88569,
- 6.95675,
- 7.7752,
- 9.18212
- ],
[
"System.Nervous System.CNS.ASTROCYTE",
1.20968,
@@ -718,6 +710,14 @@
7.42671,
8.33012
],
+ [
+ "System.Respiratory System.Lung.LUNG",
+ 0.113644,
+ 5.88569,
+ 6.95675,
+ 7.7752,
+ 9.18212
+ ],
[
"System.Connective Tissue.Adipose tissue.ADIPOSE",
4.26947,
@@ -766,14 +766,6 @@
7.98803,
9.42561
],
- [
- "System.Immune System.Myeloid.DENDRITIC CELL",
- 4.2942,
- 6.48802,
- 6.88087,
- 7.39734,
- 8.09957
- ],
[
"System.Connective Tissue.Adipose tissue.ADIPOCYTE",
4.86561,
@@ -782,6 +774,14 @@
7.13293,
7.71929
],
+ [
+ "System.Immune System.Myeloid.DENDRITIC CELL",
+ 4.2942,
+ 6.48802,
+ 6.88087,
+ 7.39734,
+ 8.09957
+ ],
[
"System.Immune System.Myeloid.MICROGLIA",
0.113644,
@@ -798,14 +798,6 @@
7.10033,
7.57266
],
- [
- "System.Integumentary System.Skin.FIBROBLAST",
- 0.113644,
- 6.35023,
- 6.8453,
- 7.43403,
- 9.31767
- ],
[
"System.Digestive System.Esophagus.ESOPHAGUS",
0.113644,
@@ -814,6 +806,14 @@
8.1174,
9.13015
],
+ [
+ "System.Integumentary System.Skin.FIBROBLAST",
+ 0.113644,
+ 6.35023,
+ 6.8453,
+ 7.43403,
+ 9.31767
+ ],
[
"System.Immune System.Myeloid.KUPFFER CELL",
6.16327,
@@ -1184,14 +1184,6 @@
"q3": 7.62057,
"max": 8.60009
},
- {
- "id": "System.Nervous System.CNS.THALAMUS",
- "min": 3.29066,
- "q1": 5.995,
- "median": 7.14836,
- "q3": 7.83426,
- "max": 8.71165
- },
{
"id": "System.Nervous System.CNS.HYPOTHALAMUS",
"min": 3.44188,
@@ -1200,6 +1192,14 @@
"q3": 7.99022,
"max": 8.81727
},
+ {
+ "id": "System.Nervous System.CNS.THALAMUS",
+ "min": 3.29066,
+ "q1": 5.995,
+ "median": 7.14836,
+ "q3": 7.83426,
+ "max": 8.71165
+ },
{
"id": "System.Immune System.Lymphoid.BLYMPHOCYTE",
"min": 5.57377,
@@ -1264,14 +1264,6 @@
"q3": 7.42671,
"max": 8.89625
},
- {
- "id": "System.Immune System.Lymphoid.TLYMPHOCYTE",
- "min": 5.60813,
- "q1": 6.60849,
- "median": 6.9754,
- "q3": 7.26685,
- "max": 7.85409
- },
{
"id": "System.Connective Tissue.Bone marrow.CHONDROCYTE",
"min": 5.93985,
@@ -1280,6 +1272,14 @@
"q3": 7.44552,
"max": 8.78176
},
+ {
+ "id": "System.Immune System.Lymphoid.TLYMPHOCYTE",
+ "min": 5.60813,
+ "q1": 6.60849,
+ "median": 6.9754,
+ "q3": 7.26685,
+ "max": 7.85409
+ },
{
"id": "System.Immune System.Myeloid.MACROPHAGE",
"min": 0.113644,
@@ -1288,14 +1288,6 @@
"q3": 7.38198,
"max": 8.46912
},
- {
- "id": "System.Respiratory System.Lung.LUNG",
- "min": 0.113644,
- "q1": 5.88569,
- "median": 6.95675,
- "q3": 7.7752,
- "max": 9.18212
- },
{
"id": "System.Nervous System.CNS.ASTROCYTE",
"min": 1.20968,
@@ -1304,6 +1296,14 @@
"q3": 7.42671,
"max": 8.33012
},
+ {
+ "id": "System.Respiratory System.Lung.LUNG",
+ "min": 0.113644,
+ "q1": 5.88569,
+ "median": 6.95675,
+ "q3": 7.7752,
+ "max": 9.18212
+ },
{
"id": "System.Connective Tissue.Adipose tissue.ADIPOSE",
"min": 4.26947,
@@ -1352,14 +1352,6 @@
"q3": 7.98803,
"max": 9.42561
},
- {
- "id": "System.Immune System.Myeloid.DENDRITIC CELL",
- "min": 4.2942,
- "q1": 6.48802,
- "median": 6.88087,
- "q3": 7.39734,
- "max": 8.09957
- },
{
"id": "System.Connective Tissue.Adipose tissue.ADIPOCYTE",
"min": 4.86561,
@@ -1368,6 +1360,14 @@
"q3": 7.13293,
"max": 7.71929
},
+ {
+ "id": "System.Immune System.Myeloid.DENDRITIC CELL",
+ "min": 4.2942,
+ "q1": 6.48802,
+ "median": 6.88087,
+ "q3": 7.39734,
+ "max": 8.09957
+ },
{
"id": "System.Immune System.Myeloid.MICROGLIA",
"min": 0.113644,
@@ -1384,14 +1384,6 @@
"q3": 7.10033,
"max": 7.57266
},
- {
- "id": "System.Integumentary System.Skin.FIBROBLAST",
- "min": 0.113644,
- "q1": 6.35023,
- "median": 6.8453,
- "q3": 7.43403,
- "max": 9.31767
- },
{
"id": "System.Digestive System.Esophagus.ESOPHAGUS",
"min": 0.113644,
@@ -1400,6 +1392,14 @@
"q3": 8.1174,
"max": 9.13015
},
+ {
+ "id": "System.Integumentary System.Skin.FIBROBLAST",
+ "min": 0.113644,
+ "q1": 6.35023,
+ "median": 6.8453,
+ "q3": 7.43403,
+ "max": 9.31767
+ },
{
"id": "System.Immune System.Myeloid.KUPFFER CELL",
"min": 6.16327,
@@ -2614,4 +2614,4 @@
},
"expected_result": "ValueError"
}
-}
\ No newline at end of file
+}
diff --git a/tests/fixtures/test_opentargets.json b/tests/fixtures/test_opentargets.json
index 34841d715..6ab3085a4 100644
--- a/tests/fixtures/test_opentargets.json
+++ b/tests/fixtures/test_opentargets.json
@@ -8,34 +8,26 @@
},
"expected_result": [
[
- 0.7297489019498119,
- "EFO_0000274",
+ 0.7279798021712002,
+ "MONDO_0004980",
"atopic eczema",
"A common chronic pruritic inflammatory skin disease with a strong genetic component. Onset typically occurs during the first 2 years of life."
],
[
- 0.6642728577751653,
+ 0.6952093612226559,
"MONDO_0004979",
"asthma",
"A bronchial disease that is characterized by chronic inflammation and narrowing of the airways, which is caused by a combination of environmental and genetic factors resulting in recurring periods of wheezing (a whistling sound while breathing), chest tightness, shortness of breath, mucus production and coughing. The symptoms appear due to a variety of triggers such as allergens, irritants, respiratory infections, weather changes, exercise, stress, reflux disease, medications, foods and emotional anxiety."
]
]
},
- "test_opentargets_expression_no_limit": {
- "type": "assert_equal_json_hash",
- "args": {
- "ensembl_id": "ENSG00000169194",
- "resource": "expression"
- },
- "expected_result": "7d32780ec48250553246c816d80b93ee"
- },
"test_opentargets_depmap": {
"type": "assert_equal_json_hash",
"args": {
"ensembl_id": "ENSG00000169194",
"resource": "depmap"
},
- "expected_result": "c335cc9c9b3167e8c5b3084e339c88a7"
+ "expected_result": "e81a60ff47c06bfecb3b421d28047718"
},
"test_opentargets_depmap_filter": {
"type": "assert_equal",
@@ -47,26 +39,7 @@
},
"limit": 2
},
- "expected_result": [
- [
- "UBERON_0002367",
- "prostate gland",
- "DU 145",
- 0.034343916922807693,
- "Prostate Adenocarcinoma",
- "ACH-000979",
- -0.14336788654327393
- ],
- [
- "UBERON_0002367",
- "prostate gland",
- "WPE1-NA22",
- 0.0291899424046278,
- "Non-Cancerous",
- "ACH-001422",
- 0.06934770196676254
- ]
- ]
+ "expected_result": []
},
"test_opentargets_interactions_no_limit": {
"type": "assert_equal_json_hash",
@@ -74,7 +47,7 @@
"ensembl_id": "ENSG00000169194",
"resource": "interactions"
},
- "expected_result": "fa95d278c2d31ded3731e154d65fcda5"
+ "expected_result": "8ed7f7380590628acdbaa5f9501365a6"
},
"test_opentargets_interactions_simple_filter": {
"type": "assert_equal",
@@ -163,13 +136,13 @@
},
"expected_result": [
{
- "score": 0.7297489019,
- "disease.id": "EFO_0000274",
+ "score": 0.7279798022,
+ "disease.id": "MONDO_0004980",
"disease.name": "atopic eczema",
"disease.description": "A common chronic pruritic inflammatory skin disease with a strong genetic component. Onset typically occurs during the first 2 years of life."
},
{
- "score": 0.6642728578,
+ "score": 0.6952093612,
"disease.id": "MONDO_0004979",
"disease.name": "asthma",
"disease.description": "A bronchial disease that is characterized by chronic inflammation and narrowing of the airways, which is caused by a combination of environmental and genetic factors resulting in recurring periods of wheezing (a whistling sound while breathing), chest tightness, shortness of breath, mucus production and coughing. The symptoms appear due to a variety of triggers such as allergens, irritants, respiratory infections, weather changes, exercise, stress, reflux disease, medications, foods and emotional anxiety."
@@ -190,7 +163,7 @@
"drug.name": "LEBRIKIZUMAB",
"drug.drugType": "Antibody",
"drug.mechanismsOfAction.rows": "Interleukin-13 inhibitor",
- "drug.description": "Antibody drug with a maximum clinical stage of Approval (across all indications), with an approval for atopic eczema and eczematoid dermatitis and 5 investigational indications.",
+ "drug.description": "Antibody drug with a maximum clinical stage of Approval (across all indications), with 3 approved and 7 investigational indications.",
"drug.synonyms": [
"Lebrikizumab",
"Lebrikizumab-lbkz",
@@ -200,19 +173,20 @@
"PRO-301444 RG-3637",
"PRO301444",
"RO-5490255",
- "Tnx-650"
+ "Tnx-650",
+ "ly3650150"
],
"drug.tradeNames": "Ebglyss",
"drug.maximumClinicalStage": "APPROVAL",
"drug.indications.rows": [
- {
- "id": "EFO_0000274",
- "name": "atopic eczema"
- },
{
"id": "EFO_0000768",
"name": "idiopathic pulmonary fibrosis"
},
+ {
+ "id": "EFO_1002029",
+ "name": "chronic rhinosinusitis with nasal polyps"
+ },
{
"id": "MONDO_0004979",
"name": "asthma"
@@ -222,16 +196,24 @@
"name": "Eczematoid dermatitis"
},
{
- "id": "EFO_0000341",
+ "id": "MONDO_0005002",
"name": "chronic obstructive pulmonary disease"
},
{
- "id": "EFO_0000183",
+ "id": "MONDO_0005083",
+ "name": "psoriasis"
+ },
+ {
+ "id": "MONDO_0004952",
"name": "Hodgkins lymphoma"
},
{
- "id": "MONDO_0004784",
- "name": "allergic asthma"
+ "id": "MONDO_0004980",
+ "name": "atopic eczema"
+ },
+ {
+ "id": "MONDO_0024332",
+ "name": "perennial allergic rhinitis"
}
]
},
@@ -249,56 +231,17 @@
"drug.maximumClinicalStage": "PHASE_2",
"drug.indications.rows": [
{
- "id": "MONDO_0004979",
- "name": "asthma"
+ "id": "MONDO_0005101",
+ "name": "ulcerative colitis"
},
{
- "id": "EFO_0000729",
- "name": "ulcerative colitis"
+ "id": "MONDO_0004979",
+ "name": "asthma"
}
]
}
]
},
- "test_opentargets_expression": {
- "function_call_to_reproduce": "output = opentargets(ensembl_id='ENSG00000169194', resource='expression', limit=2, json=True, verbose=False); print(json.dumps(output, indent=2))",
- "type": "assert_equal_json_with_keys",
- "args": {
- "ensembl_id": "ENSG00000169194",
- "resource": "expression",
- "limit": 2
- },
- "expected_result": [
- {
- "tissue.id": "UBERON_0002367",
- "tissue.label": "prostate gland",
- "tissue.anatomicalSystems": [
- "reproductive system"
- ],
- "tissue.organs": [
- "reproductive structure"
- ],
- "rna.zscore": -1,
- "rna.value": 4.0,
- "rna.unit": "",
- "rna.level": -1
- },
- {
- "tissue.id": "UBERON_0002113",
- "tissue.label": "kidney",
- "tissue.anatomicalSystems": [
- "renal system"
- ],
- "tissue.organs": [
- "kidney"
- ],
- "rna.zscore": -1,
- "rna.value": 0.0,
- "rna.unit": "",
- "rna.level": -1
- }
- ]
- },
"test_opentargets_interactions": {
"function_call_to_reproduce": "output = opentargets(ensembl_id='ENSG00000169194', resource='interactions', limit=2, json=True, verbose=False); print(json.dumps(output, indent=2))",
"type": "assert_equal_json_with_keys",
@@ -314,13 +257,13 @@
"sourceDatabase": "string",
"intA": "ENSP00000304915",
"intABiologicalRole": "unspecified role",
- "intB": "ENSP00000361004",
+ "intB": "ENSP00000360730",
"intBBiologicalRole": "unspecified role",
"targetA.id": "ENSG00000169194",
"targetA.approvedSymbol": "IL13",
"speciesA.taxonId": 134,
- "targetB.id": "ENSG00000123496",
- "targetB.approvedSymbol": "IL13RA2",
+ "targetB.id": "ENSG00000131724",
+ "targetB.approvedSymbol": "IL13RA1",
"speciesB.taxonId": 134
},
{
@@ -329,13 +272,13 @@
"sourceDatabase": "string",
"intA": "ENSP00000304915",
"intABiologicalRole": "unspecified role",
- "intB": "ENSP00000360730",
+ "intB": "ENSP00000361004",
"intBBiologicalRole": "unspecified role",
"targetA.id": "ENSG00000169194",
"targetA.approvedSymbol": "IL13",
"speciesA.taxonId": 134,
- "targetB.id": "ENSG00000131724",
- "targetB.approvedSymbol": "IL13RA1",
+ "targetB.id": "ENSG00000123496",
+ "targetB.approvedSymbol": "IL13RA2",
"speciesB.taxonId": 134
}
]
@@ -369,11 +312,11 @@
},
{
"variantId": "5_132660151_T_C",
- "genotypeId": "5_132660151_T_C,C",
- "genotype": "CC",
+ "genotypeId": "5_132660151_T_C,T",
+ "genotype": "CT",
"drugs": null,
- "phenotypeText": "decreased risk for non-immune response",
- "genotypeAnnotationText": "Patients with the CC genotype may be at decreased risk for non-immune response to the hepatitis B vaccine, as compared to patients with the TT genotype. Other genetic and clinical factors may also influence risk of non-immune response in patients receiving the hepatitis B vaccine.",
+ "phenotypeText": "decreased risk for non-immune response to the hepatitis B vaccine",
+ "genotypeAnnotationText": "Patients with the CT genotype may be at decreased risk for non-immune response to the hepatitis B vaccine, as compared to patients with the TT genotype, or at increased risk as compared to patients with the CC genotype. Other genetic and clinical factors may also influence risk of non-immune response in patients receiving the hepatitis B vaccine.",
"pgxCategory": "efficacy",
"isDirectTarget": false,
"evidenceLevel": "3",
diff --git a/tests/from_json.py b/tests/from_json.py
index 1b41cbcf6..82fcd68bf 100644
--- a/tests/from_json.py
+++ b/tests/from_json.py
@@ -279,6 +279,16 @@ def error(self: unittest.TestCase):
# _test_constructor = Callable[[str, dict[str, dict[str, ...]], Callable], Callable]
+def _skip(name, td, _func):
+ """Mark a JSON-defined test as skipped, with the `reason` from the fixture."""
+ reason = td[name].get("reason", "skipped")
+
+ def skip(self: unittest.TestCase):
+ self.skipTest(reason)
+
+ return skip
+
+
_TYPES = {
"assert_equal": _assert_equal,
"assert_equal_na": _assert_equal_na,
@@ -288,6 +298,7 @@ def error(self: unittest.TestCase):
"assert_equal_json_hash_nested": _assert_equal_json_hash_nested,
"assert_equal_json_with_keys": _assert_equal_json_with_keys,
"error": _error,
+ "skip": _skip,
}
diff --git a/tests/pytest_results.txt b/tests/pytest_results.txt
index 404bbba3a..7a27f5b17 100644
--- a/tests/pytest_results.txt
+++ b/tests/pytest_results.txt
@@ -1,1264 +1,446 @@
-Pytest results (Python 3.14) - 2026-06-26T04:50:58Z
+Pytest results (Python 3.14) - 2026-06-28T19:56:05Z
============================= test session starts ==============================
platform linux -- Python 3.14.6, pytest-9.1.1, pluggy-1.6.0 -- /home/runner/.local/share/hatch/env/virtual/gget/dY-b3nRT/hatch-test.py3.14/bin/python3
cachedir: .pytest_cache
-Using --randomly-seed=1601922275
+Using --randomly-seed=1578840813
rootdir: /home/runner/work/gget/gget
configfile: pyproject.toml
testpaths: tests
plugins: randomly-3.16.0, rerunfailures-14.0, mock-3.15.1, cov-7.1.0, xdist-3.8.0
-collecting ... collected 400 items
-
-tests/test_archs4.py::TestArchs4::test_archs4_tissue_ensembl PASSED [ 0%]
-tests/test_archs4.py::TestArchs4::test_archs4_defaults PASSED [ 0%]
-tests/test_archs4.py::TestArchs4::test_archs4_bad_which PASSED [ 0%]
-tests/test_archs4.py::TestArchs4::test_archs4_bad_species PASSED [ 1%]
-tests/test_archs4.py::TestArchs4::test_archs4_bad_ensembl PASSED [ 1%]
-tests/test_archs4.py::TestArchs4::test_archs4_tissue_mouse PASSED [ 1%]
-tests/test_archs4.py::TestArchs4::test_archs4_tissue_json PASSED [ 1%]
-tests/test_archs4.py::TestArchs4::test_archs4_bad_gene PASSED [ 2%]
-tests/test_archs4.py::TestArchs4::test_archs4_bad_gene_tissue PASSED [ 2%]
-tests/test_archs4.py::TestArchs4::test_archs4_mouse_json_ensembl PASSED [ 2%]
-tests/test_archs4.py::TestArchs4::test_archs4_tissue PASSED [ 2%]
-tests/test_search.py::TestSearch::test_search_plant PASSED [ 3%]
-tests/test_search.py::TestSearch::test_search_gene_bad_species PASSED [ 3%]
-tests/test_search.py::TestSearch::test_search_gene_one_sw_limit PASSED [ 3%]
-tests/test_search.py::TestSearch::test_search_gene_two_sw_and PASSED [ 3%]
-tests/test_search.py::TestSearch::test_search_transcript_bad_andor PASSED [ 4%]
-tests/test_search.py::TestSearch::test_search_gene_two_sw_limit PASSED [ 4%]
-tests/test_search.py::TestSearch::test_search_release PASSED [ 4%]
-tests/test_search.py::TestSearch::test_search_octopus PASSED [ 4%]
-tests/test_search.py::TestSearch::test_search_plant_db PASSED [ 5%]
-tests/test_search.py::TestSearch::test_search_transcript_two_sw_and PASSED [ 5%]
-tests/test_search.py::TestSearch::test_search_gene_one_sw_json PASSED [ 5%]
-tests/test_search.py::TestSearch::test_search_db PASSED [ 5%]
-tests/test_search.py::TestSearch::test_search_transcript_two_sw_or PASSED [ 6%]
-tests/test_search.py::TestSearch::test_search_transcript_bad_species PASSED [ 6%]
-tests/test_search.py::TestSearch::test_search_transcript_one_sw PASSED [ 6%]
-tests/test_search.py::TestSearch::test_search_gene_one_sw PASSED [ 6%]
-tests/test_search.py::TestSearch::test_search_gene_two_sw_or PASSED [ 7%]
-tests/test_search.py::TestSearch::test_search_gene_bad_andor PASSED [ 7%]
-tests/test_g2p.py::TestG2PValidation::test_missing_uniprot_raises PASSED [ 7%]
-tests/test_g2p.py::TestG2PValidation::test_invalid_resource_raises PASSED [ 7%]
-tests/test_g2p.py::TestG2PValidation::test_alignment_requires_isoform PASSED [ 8%]
-tests/test_g2p.py::TestG2P::test_g2p_alignment PASSED [ 8%]
-tests/test_g2p.py::TestG2P::test_g2p_features PASSED [ 8%]
-tests/test_g2p.py::TestG2P::test_g2p_map PASSED [ 8%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_kinase_interactions PASSED [ 9%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_json PASSED [ 9%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_bad_background PASSED [ 9%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_plot PASSED [ 9%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_background_ensembl PASSED [ 10%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_fish PASSED [ 10%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_bad_database_shortcut PASSED [ 10%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_diseases_drugs PASSED [ 10%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_ontology PASSED [ 11%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_bad_background_list PASSED [ 11%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_fly PASSED [ 11%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_worm PASSED [ 11%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_none PASSED [ 12%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_yeast PASSED [ 12%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_pathway PASSED [ 12%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_ensembl_ids PASSED [ 12%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_transcription PASSED [ 13%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_bad_gene PASSED [ 13%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_bad_species PASSED [ 13%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_celltypes PASSED [ 13%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_background PASSED [ 14%]
-tests/test_enrichr.py::TestEnrichr::test_enrichr_mouse_is_human PASSED [ 14%]
-tests/test_seq.py::TestSeq::test_seq_gene_transcript_iso PASSED [ 14%]
-tests/test_seq.py::TestSeq::test_seq_transcript_gene_iso PASSED [ 14%]
-tests/test_seq.py::TestSeq::test_seq_transcript_transcript_iso PASSED [ 15%]
-tests/test_seq.py::TestSeq::test_seq_transcript_transcript_WB PASSED [ 15%]
-tests/test_seq.py::TestSeq::test_seq_transcript PASSED [ 15%]
-tests/test_seq.py::TestSeq::test_seq_transcript_gene PASSED [ 15%]
-tests/test_seq.py::TestSeq::test_seq_missing_uniprot_gene_name PASSED [ 16%]
-tests/test_seq.py::TestSeq::test_seq_transcript_gene_WB PASSED [ 16%]
-tests/test_seq.py::TestSeq::test_seq_gene PASSED [ 16%]
-tests/test_seq.py::TestSeq::test_seq_gene_iso PASSED [ 16%]
-tests/test_blat.py::TestBlat::test_blat_bad_seqtype PASSED [ 17%]
-tests/test_blat.py::TestBlat::test_blat_nt_txt PASSED [ 17%]
-tests/test_blat.py::TestBlat::test_blat_nt_transDNA PASSED [ 17%]
-tests/test_blat.py::TestBlat::test_blat_bad_txt PASSED [ 17%]
-tests/test_blat.py::TestBlat::test_blat_nt_DNA PASSED [ 18%]
-tests/test_blat.py::TestBlat::test_blat_aa PASSED [ 18%]
-tests/test_blat.py::TestBlat::test_blat_nt_fasta PASSED [ 18%]
-tests/test_blat.py::TestBlat::test_blat_nt_protein PASSED [ 18%]
-tests/test_blat.py::TestBlat::test_blat_bad_assembly PASSED [ 19%]
-tests/test_blat.py::TestBlat::test_blat_bad_fasta PASSED [ 19%]
-tests/test_blat.py::TestBlat::test_blat_bad_fileformat PASSED [ 19%]
-tests/test_blat.py::TestBlat::test_blat_nt_json PASSED [ 19%]
-tests/test_blat.py::TestBlat::test_blat_nt PASSED [ 20%]
-tests/test_blat.py::TestBlat::test_blat_aa_RNA PASSED [ 20%]
-tests/test_blat.py::TestBlat::test_blat_nt_RNA PASSED [ 20%]
-tests/test_blat.py::TestBlat::test_blat_aa_protein PASSED [ 20%]
-tests/test_blat.py::TestBlat::test_blat_shortseq PASSED [ 21%]
-tests/test_cellxgene.py::TestCellxgene::test_cellxgene_metadata_macaca_mulatta SKIPPED [ 21%]
-tests/test_cellxgene.py::TestCellxgene::test_cellxgene_metadata SKIPPED [ 21%]
-tests/test_cellxgene.py::TestCellxgene::test_cellxgene_adata SKIPPED [ 21%]
-tests/test_cellxgene.py::TestCellxgeneValidation::test_typo_species_raises_valueerror PASSED [ 22%]
-tests/test_cellxgene.py::TestCellxgeneValidation::test_invalid_species_raises_valueerror PASSED [ 22%]
-tests/test_cellxgene.py::TestCellxgeneValidation::test_supported_species_includes_new_primates PASSED [ 22%]
-tests/test_info.py::TestInfo::test_info_mix PASSED [ 22%]
-tests/test_info.py::TestInfo::test_info_ncbitrue_uniprotfalse PASSED [ 23%]
-tests/test_info.py::TestInfo::test_info_WB_transcript PASSED [ 23%]
-tests/test_info.py::TestInfo::test_info_ncbifalse_uniprotfalse PASSED [ 23%]
-tests/test_info.py::TestInfo::test_info_ncbifalse_uniprottrue PASSED [ 23%]
-tests/test_info.py::TestInfo::test_info_exon PASSED [ 24%]
-tests/test_info.py::TestInfo::test_info_gene_list_non_model PASSED [ 24%]
-tests/test_info.py::TestInfo::test_info_transcript PASSED [ 24%]
-tests/test_info.py::TestInfo::test_info_gene PASSED [ 24%]
-tests/test_utils.py::TestUtils::test_search_iv_species_options PASSED [ 25%]
-tests/test_utils.py::TestUtils::test_ref_species_options_bad_type PASSED [ 25%]
-tests/test_utils.py::TestUtils::test_find_latest_ens_rel PASSED [ 25%]
-tests/test_utils.py::TestUtils::test_get_uniprot_info_gene PASSED [ 25%]
-tests/test_utils.py::TestUtils::test_aa_colors PASSED [ 26%]
-tests/test_utils.py::TestUtils::test_rest_query PASSED [ 26%]
-tests/test_utils.py::TestUtils::test_read_fasta PASSED [ 26%]
-tests/test_utils.py::TestUtils::test_search_species_options_bad_type PASSED [ 26%]
-tests/test_utils.py::TestUtils::test_get_uniprot_seqs PASSED [ 27%]
-tests/test_utils.py::TestUtils::test_ref_species_options PASSED [ 27%]
-tests/test_utils.py::TestUtils::test_n_colors PASSED [ 27%]
-tests/test_utils.py::TestUtils::test_get_uniprot_info_transcript PASSED [ 27%]
-tests/test_utils.py::TestUtils::test_ref_iv_species_options PASSED [ 28%]
-tests/test_utils.py::TestUtils::test_search_species_options PASSED [ 28%]
-tests/test_utils.py::TestUtils::test_rest_query_bad_type PASSED [ 28%]
-tests/test_cosmic.py::TestCosmicWorkflow::test_cosmic_defaults PASSED [ 28%]
-tests/test_mutate.py::TestMutate::test_intron_mutation_plus PASSED [ 29%]
-tests/test_mutate.py::TestMutate::test_multi_deletion_with_right_repeats PASSED [ 29%]
-tests/test_mutate.py::TestMutate::test_single_dup PASSED [ 29%]
-tests/test_mutate.py::TestMutate::test_single_substitution PASSED [ 29%]
-tests/test_mutate.py::TestMutate::test_multi_dup PASSED [ 30%]
-tests/test_mutate.py::TestMutate::test_multi_delins PASSED [ 30%]
-tests/test_mutate.py::TestMutate::test_inversion_with_overlaps PASSED [ 30%]
-tests/test_mutate.py::TestMutate::test_intron_mutation_minus PASSED [ 30%]
-tests/test_mutate.py::TestMutate::test_single_deletion_with_left_repeats PASSED [ 31%]
-tests/test_mutate.py::TestMutate::test_uncertain_mutation PASSED [ 31%]
-tests/test_mutate.py::TestMutate::test_single_deletion_with_right_repeats PASSED [ 31%]
-tests/test_mutate.py::TestMutate::test_posttranslational_mutation PASSED [ 31%]
-tests/test_mutate.py::TestMutate::test_multi_deletion PASSED [ 32%]
-tests/test_mutate.py::TestMutate::test_multi_insertion PASSED [ 32%]
-tests/test_mutate.py::TestMutate::test_multi_delins_with_psuedo_left_repeats PASSED [ 32%]
-tests/test_mutate.py::TestMutate::test_multi_insertion_with_left_repeats PASSED [ 32%]
-tests/test_mutate.py::TestMutate::test_single_deletion PASSED [ 33%]
-tests/test_mutate.py::TestMutate::test_single_substitution_near_left_end PASSED [ 33%]
-tests/test_mutate.py::TestMutate::test_large_k PASSED [ 33%]
-tests/test_mutate.py::TestMutate::test_index_error PASSED [ 33%]
-tests/test_mutate.py::TestMutate::test_single_insertion PASSED [ 34%]
-tests/test_mutate.py::TestMutate::test_multi_delins_with_true_left_repeats PASSED [ 34%]
-tests/test_mutate.py::TestMutate::test_single_substitution_near_right_end PASSED [ 34%]
-tests/test_mutate.py::TestMutate::test_ambiguous_mutation PASSED [ 34%]
-tests/test_mutate.py::TestMutate::test_single_delins PASSED [ 35%]
-tests/test_mutate.py::TestMutate::test_multi_delins_with_true_right_repeats PASSED [ 35%]
-tests/test_mutate.py::TestMutate::test_list_of_mutations PASSED [ 35%]
-tests/test_mutate.py::test_csv_of_mutations PASSED [ 35%]
-tests/test_mutate.py::test_mismatch_error PASSED [ 36%]
-tests/test_elm.py::TestELM::test_elm_uniprot_aminoacidseq PASSED [ 36%]
-tests/test_elm.py::TestELM::test_elm_uniprot_id_new PASSED [ 36%]
-tests/test_elm.py::TestELM::test_elm_uniprot_id_in_elm PASSED [ 36%]
-tests/test_muscle.py::TestMusclePrints::test_muscle_print_nt PASSED [ 37%]
-tests/test_muscle.py::TestMuscleAA::test_muscle_aa PASSED [ 37%]
-tests/test_muscle.py::TestMuscleAASuper::test_muscle_aa_super5 PASSED [ 37%]
-tests/test_muscle.py::TestMuscleSeqsInput::test_muscle_seqs_as_input PASSED [ 37%]
-tests/test_muscle.py::TestMuscleSuper::test_muscle_nt_super5 PASSED [ 38%]
-tests/test_muscle.py::TestMuscle::test_muscle_nt PASSED [ 38%]
-tests/test_muscle.py::TestMuscle::test_muscle_nt_txt PASSED [ 38%]
-tests/test_opentargets.py::TestOpenTargets::test_opentargets_diseases FAILED [ 38%]
-tests/test_opentargets.py::TestOpenTargets::test_error_opentargets_diseases_filter PASSED [ 39%]
-tests/test_opentargets.py::TestOpenTargets::test_opentargets_interactions_no_limit FAILED [ 39%]
-tests/test_opentargets.py::TestOpenTargets::test_error_opentargets_bad_resource PASSED [ 39%]
-tests/test_opentargets.py::TestOpenTargets::test_error_opentargets_drugs_invalid_filter FAILED [ 39%]
-tests/test_opentargets.py::TestOpenTargets::test_opentargets_depmap FAILED [ 40%]
-tests/test_opentargets.py::TestOpenTargets::test_opentargets FAILED [ 40%]
-tests/test_opentargets.py::TestOpenTargets::test_opentargets_tractability PASSED [ 40%]
-tests/test_opentargets.py::TestOpenTargets::test_opentargets_interactions_simple_filter PASSED [ 40%]
-tests/test_opentargets.py::TestOpenTargets::test_opentargets_interactions FAILED [ 41%]
-tests/test_opentargets.py::TestOpenTargets::test_opentargets_expression_no_limit FAILED [ 41%]
-tests/test_opentargets.py::TestOpenTargets::test_error_opentargets_nonexistent_id PASSED [ 41%]
-tests/test_opentargets.py::TestOpenTargets::test_opentargets_drugs FAILED [ 41%]
-tests/test_opentargets.py::TestOpenTargets::test_opentargets_depmap_filter FAILED [ 42%]
-tests/test_opentargets.py::TestOpenTargets::test_opentargets_expression FAILED [ 42%]
-tests/test_opentargets.py::TestOpenTargets::test_opentargets_pharmacogenetics FAILED [ 42%]
-tests/test_opentargets.py::TestOpenTargets::test_error_opentargets_bad_limit PASSED [ 42%]
-tests/test_cbio.py::TestCbio::test_cbio_download_partially_existing_files PASSED [ 43%]
-tests/test_cbio.py::TestCbio::test_cbio_download PASSED [ 43%]
-tests/test_cbio.py::TestCbio::test_error_cbio_download PASSED [ 43%]
-tests/test_cbio.py::TestCbioSearch::test_cbio_search PASSED [ 43%]
-tests/test_virus.py::TestVirus::test_parse_baseline_file_csv PASSED [ 44%]
-tests/test_virus.py::TestVirus::test_virus_completeness_filter_verification PASSED [ 44%]
-tests/test_virus.py::TestVirus::test_track_failed_operation_none_dict PASSED [ 44%]
-tests/test_virus.py::TestVirus::test_get_datasets_path_uses_bundled_binary PASSED [ 44%]
-tests/test_virus.py::TestVirus::test_check_min_max_equal PASSED [ 45%]
+collecting ... collected 422 items
+
+tests/test_cbio.py::TestCbio::test_cbio_download PASSED [ 0%]
+tests/test_cbio.py::TestCbio::test_cbio_download_partially_existing_files PASSED [ 0%]
+tests/test_cbio.py::TestCbio::test_error_cbio_download PASSED [ 0%]
+tests/test_cbio.py::TestCbioSearch::test_cbio_search PASSED [ 0%]
+tests/test_8cube.py::TestSpecificity::test_specificity_basic PASSED [ 1%]
+tests/test_8cube.py::TestGeneExpression::test_gene_expression_basic PASSED [ 1%]
+tests/test_8cube.py::TestPsiBlock::test_psi_block_basic PASSED [ 1%]
+tests/test_muscle.py::TestMusclePrints::test_muscle_print_nt PASSED [ 1%]
+tests/test_muscle.py::TestMuscleSeqsInput::test_muscle_seqs_as_input PASSED [ 2%]
+tests/test_muscle.py::TestMuscleAASuper::test_muscle_aa_super5 PASSED [ 2%]
+tests/test_muscle.py::TestMuscleSuper::test_muscle_nt_super5 PASSED [ 2%]
+tests/test_muscle.py::TestMuscleAA::test_muscle_aa PASSED [ 2%]
+tests/test_muscle.py::TestMuscle::test_muscle_nt PASSED [ 3%]
+tests/test_muscle.py::TestMuscle::test_muscle_nt_txt PASSED [ 3%]
+tests/test_pdb.py::TestPDB::test_pdb_legacy_fallback_to_mmcif PASSED [ 3%]
+tests/test_pdb.py::TestPDB::test_pdb_assembly PASSED [ 3%]
+tests/test_pdb.py::TestPDB::test_pdb_mmcif PASSED [ 4%]
+tests/test_pdb.py::TestPDB::test_pdb_pdb PASSED [ 4%]
+tests/test_cellxgene.py::TestCellxgene::test_cellxgene_adata SKIPPED [ 4%]
+tests/test_cellxgene.py::TestCellxgene::test_cellxgene_metadata SKIPPED [ 4%]
+tests/test_cellxgene.py::TestCellxgene::test_cellxgene_metadata_macaca_mulatta SKIPPED [ 4%]
+tests/test_cellxgene.py::TestCellxgeneValidation::test_invalid_species_raises_valueerror PASSED [ 5%]
+tests/test_cellxgene.py::TestCellxgeneValidation::test_supported_species_includes_new_primates PASSED [ 5%]
+tests/test_cellxgene.py::TestCellxgeneValidation::test_typo_species_raises_valueerror PASSED [ 5%]
+tests/test_alphafold.py::TestAlphafoldJackhmmerSavedir::test_clean_up_default_dir_when_none PASSED [ 5%]
+tests/test_alphafold.py::TestAlphafoldJackhmmerSavedir::test_cli_exposes_jackhmmer_savedir_flag PASSED [ 6%]
+tests/test_alphafold.py::TestAlphafoldJackhmmerSavedir::test_custom_dir PASSED [ 6%]
+tests/test_alphafold.py::TestAlphafoldJackhmmerSavedir::test_clean_up_missing_dir_is_safe PASSED [ 6%]
+tests/test_alphafold.py::TestAlphafoldJackhmmerSavedir::test_default_dir PASSED [ 6%]
+tests/test_alphafold.py::TestAlphafoldJackhmmerSavedir::test_clean_up_removes_custom_dir PASSED [ 7%]
+tests/test_virus.py::TestVirus::test_filter_genbank_metadata_empty PASSED [ 7%]
+tests/test_virus.py::TestVirus::test_virus_with_multiple_filters PASSED [ 7%]
+tests/test_virus.py::TestVirus::test_is_sars_cov2_query_common_names PASSED [ 7%]
+tests/test_virus.py::TestVirus::test_is_sars_cov2_query_negative PASSED [ 8%]
+tests/test_virus.py::TestVirus::test_merge_baseline_with_new_csv PASSED [ 8%]
+tests/test_virus.py::TestVirus::test_get_gget_version_returns_string PASSED [ 8%]
+tests/test_virus.py::TestVirus::test_unzip_file_valid PASSED [ 8%]
+tests/test_virus.py::TestVirus::test_parse_genbank_xml_empty_set PASSED [ 9%]
+tests/test_virus.py::TestVirus::test_parse_partial_date_year_month_min PASSED [ 9%]
+tests/test_virus.py::TestVirus::test_virus_min_greater_than_max_seq_length PASSED [ 9%]
+tests/test_virus.py::TestVirus::test_filter_genbank_metadata_gen_mol_type PASSED [ 9%]
+tests/test_virus.py::TestVirus::test_get_modified_virus_name_add_virus_suffix PASSED [ 9%]
+tests/test_virus.py::TestVirus::test_parse_baseline_file_nonexistent_raises PASSED [ 10%]
+tests/test_virus.py::TestVirus::test_save_partial_metadata_empty PASSED [ 10%]
+tests/test_virus.py::TestVirus::test_virus_invalid_genbank_batch_size_type PASSED [ 10%]
+tests/test_virus.py::TestVirus::test_filter_genbank_metadata_combined PASSED [ 10%]
+tests/test_virus.py::TestVirus::test_merge_metadata_csvs_fills_missing PASSED [ 11%]
+tests/test_virus.py::TestVirus::test_virus_invalid_genbank_batch_size_negative PASSED [ 11%]
+tests/test_virus.py::TestVirus::test_save_partial_metadata PASSED [ 11%]
+tests/test_virus.py::TestVirus::test_virus_with_annotated_filter PASSED [ 11%]
+tests/test_virus.py::TestVirus::test_filter_genbank_metadata_provirus_false PASSED [ 12%]
+tests/test_virus.py::TestVirus::test_get_datasets_path_returns_valid_path PASSED [ 12%]
+tests/test_virus.py::TestVirus::test_filter_cached_host_not_in_strategy PASSED [ 12%]
+tests/test_virus.py::TestVirus::test_local_name_with_namespace PASSED [ 12%]
+tests/test_virus.py::TestVirus::test_write_fasta_record_without_description PASSED [ 13%]
+tests/test_virus.py::TestVirus::test_virus_invalid_genbank_batch_size_zero PASSED [ 13%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_submitter_country PASSED [ 13%]
+tests/test_virus.py::TestVirus::test_validate_datasets_binary_valid PASSED [ 13%]
+tests/test_virus.py::TestVirus::test_save_genbank_metadata_to_csv_empty PASSED [ 13%]
+tests/test_virus.py::TestVirus::test_save_metadata_to_csv_basic PASSED [ 14%]
+tests/test_virus.py::TestVirus::test_retry_helper_non_retryable_exception PASSED [ 14%]
+tests/test_virus.py::TestVirus::test_filter_genbank_metadata_mature_peptide_count PASSED [ 14%]
+tests/test_virus.py::TestVirus::test_virus_relationship_check_counts_match PASSED [ 14%]
+tests/test_virus.py::TestVirus::test_filter_genbank_metadata_min_gene_count PASSED [ 15%]
+tests/test_virus.py::TestVirus::test_genbank_xml_to_csv_basic PASSED [ 15%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_collection_date_range PASSED [ 15%]
+tests/test_virus.py::TestVirus::test_get_modified_virus_name_add_space_before_virus PASSED [ 15%]
+tests/test_virus.py::TestVirus::test_filter_genbank_metadata_has_proteins PASSED [ 16%]
+tests/test_virus.py::TestVirus::test_virus_completeness_filter_verification PASSED [ 16%]
+tests/test_virus.py::TestVirus::test_virus_invalid_lab_passaged_type PASSED [ 16%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_max_seq_length PASSED [ 16%]
+tests/test_virus.py::TestVirus::test_merge_baseline_with_new_empty_new PASSED [ 17%]
+tests/test_virus.py::TestVirus::test_load_metadata_from_api_reports_multiple PASSED [ 17%]
+tests/test_virus.py::TestVirus::test_merge_baseline_with_new_deduplicates PASSED [ 17%]
+tests/test_virus.py::TestVirus::test_deduplicate_all_existing PASSED [ 17%]
+tests/test_virus.py::TestVirus::test_parse_baseline_file_json PASSED [ 18%]
+tests/test_virus.py::TestVirus::test_parse_baseline_file_none_raises PASSED [ 18%]
+tests/test_virus.py::TestVirus::test_get_modified_virus_name_empty_returns_none PASSED [ 18%]
+tests/test_virus.py::TestVirus::test_write_fasta_record_with_description PASSED [ 18%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_vaccine_strain_false PASSED [ 18%]
+tests/test_virus.py::TestVirus::test_filter_genbank_metadata_max_gene_count PASSED [ 19%]
+tests/test_virus.py::TestVirus::test_batch_accessions_for_url PASSED [ 19%]
+tests/test_virus.py::TestVirus::test_virus_multi_accession_file_input PASSED [ 19%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_lab_passaged_true PASSED [ 19%]
+tests/test_virus.py::TestVirus::test_virus_with_collection_date_filters PASSED [ 20%]
+tests/test_virus.py::TestVirus::test_clean_xml_declarations_no_declarations PASSED [ 20%]
+tests/test_virus.py::TestVirus::test_virus_with_protein_count_filters PASSED [ 20%]
+tests/test_virus.py::TestVirus::test_virus_with_lab_passaged_filter PASSED [ 20%]
+tests/test_virus.py::TestVirus::test_parse_baseline_file_text PASSED [ 21%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_isolation_source PASSED [ 21%]
+tests/test_virus.py::TestVirus::test_parse_date_full PASSED [ 21%]
+tests/test_virus.py::TestVirus::test_parse_accession_input_single PASSED [ 21%]
+tests/test_virus.py::TestVirus::test_filter_sequences_proteins_complete PASSED [ 22%]
+tests/test_virus.py::TestVirus::test_virus_with_length_filters PASSED [ 22%]
+tests/test_virus.py::TestVirus::test_track_failed_operation_appends PASSED [ 22%]
+tests/test_virus.py::TestVirus::test_check_min_max_date_valid PASSED [ 22%]
+tests/test_virus.py::TestVirus::test_force_garbage_collection_runs PASSED [ 22%]
+tests/test_virus.py::TestVirus::test_unzip_file_nonexistent_raises PASSED [ 23%]
+tests/test_virus.py::TestVirus::test_parse_partial_date_full_date PASSED [ 23%]
+tests/test_virus.py::TestVirus::test_clean_xml_declarations PASSED [ 23%]
+tests/test_virus.py::TestVirus::test_filter_sequences_max_ambiguous_chars PASSED [ 23%]
+tests/test_virus.py::TestVirus::test_parse_partial_date_empty_raises PASSED [ 24%]
+tests/test_virus.py::TestVirus::test_save_genbank_metadata_to_csv_basic PASSED [ 24%]
+tests/test_virus.py::TestVirus::test_check_min_max_none_values PASSED [ 24%]
+tests/test_virus.py::TestVirus::test_filter_cached_complete_only PASSED [ 24%]
+tests/test_virus.py::TestVirus::test_retry_helper_custom_retryable_exceptions PASSED [ 25%]
+tests/test_virus.py::TestVirus::test_check_min_max_valid PASSED [ 25%]
+tests/test_virus.py::TestVirus::test_calculate_max_accessions_per_batch PASSED [ 25%]
+tests/test_virus.py::TestVirus::test_filter_genbank_metadata_genotype PASSED [ 25%]
+tests/test_virus.py::TestVirus::test_local_name_without_namespace PASSED [ 26%]
+tests/test_virus.py::TestVirus::test_retry_helper_exponential_backoff_timing PASSED [ 26%]
+tests/test_virus.py::TestVirus::test_virus_invalid_keep_temp_type PASSED [ 26%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_segment PASSED [ 26%]
+tests/test_virus.py::TestVirus::test_parse_date_invalid_raises PASSED [ 27%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_geographic_location PASSED [ 27%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_submitter_name PASSED [ 27%]
+tests/test_virus.py::TestVirus::test_deduplicate_empty_baseline PASSED [ 27%]
+tests/test_virus.py::TestVirus::test_check_min_max_invalid_raises PASSED [ 27%]
+tests/test_virus.py::TestVirus::test_virus_multi_accession_space_separated PASSED [ 28%]
+tests/test_virus.py::TestVirus::test_virus_empty_virus_name PASSED [ 28%]
+tests/test_virus.py::TestVirus::test_virus_none_virus_name PASSED [ 28%]
+tests/test_virus.py::TestVirus::test_check_min_max_equal PASSED [ 28%]
+tests/test_virus.py::TestVirus::test_load_metadata_from_api_reports_basic PASSED [ 29%]
+tests/test_virus.py::TestVirus::test_load_metadata_from_api_reports_missing_accession PASSED [ 29%]
+tests/test_virus.py::TestVirus::test_virus_with_completeness_filter PASSED [ 29%]
+tests/test_virus.py::TestVirus::test_parse_accession_input_from_file PASSED [ 29%]
+tests/test_virus.py::TestVirus::test_virus_metadata_schema_validation PASSED [ 30%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_isolate PASSED [ 30%]
+tests/test_virus.py::TestVirus::test_filter_cached_geographic_location PASSED [ 30%]
+tests/test_virus.py::TestVirus::test_track_failed_operation_none_dict PASSED [ 30%]
+tests/test_virus.py::TestVirus::test_is_alphainfluenza_query_common_names PASSED [ 31%]
+tests/test_virus.py::TestVirus::test_parse_baseline_file_jsonl PASSED [ 31%]
+tests/test_virus.py::TestVirus::test_save_metadata_to_csv_empty_metadata PASSED [ 31%]
+tests/test_virus.py::TestVirus::test_virus_min_greater_than_max_gene_count PASSED [ 31%]
+tests/test_virus.py::TestVirus::test_filter_cached_no_filters PASSED [ 31%]
+tests/test_virus.py::TestVirus::test_get_datasets_path_uses_bundled_binary PASSED [ 32%]
+tests/test_virus.py::TestVirus::test_save_command_summary_creates_file PASSED [ 32%]
+tests/test_virus.py::TestVirus::test_parse_genbank_xml_basic PASSED [ 32%]
+tests/test_virus.py::TestVirus::test_parse_date_various_formats PASSED [ 32%]
+tests/test_virus.py::TestVirus::test_filter_genbank_metadata_no_filters PASSED [ 33%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_vaccine_strain_true PASSED [ 33%]
+tests/test_virus.py::TestVirus::test_retry_helper_success_after_retry PASSED [ 33%]
+tests/test_virus.py::TestVirus::test_merge_metadata_csvs_missing_standard_file PASSED [ 33%]
+tests/test_virus.py::TestVirus::test_parse_genbank_xml_invalid_raises PASSED [ 34%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_min_seq_length PASSED [ 34%]
+tests/test_virus.py::TestVirus::test_virus_with_max_ambiguous_chars_filter PASSED [ 34%]
+tests/test_virus.py::TestVirus::test_parse_accession_input_nonexistent_file_raises_error PASSED [ 34%]
+tests/test_virus.py::TestVirus::test_virus_integer_virus_id PASSED [ 35%]
+tests/test_virus.py::TestVirus::test_virus_specific_accession_file_creation PASSED [ 35%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_source_database PASSED [ 35%]
+tests/test_virus.py::TestVirus::test_parse_baseline_file_csv PASSED [ 35%]
+tests/test_virus.py::TestVirus::test_parse_partial_date_year_only_max_comparison PASSED [ 36%]
+tests/test_virus.py::TestVirus::test_check_min_max_date_invalid_raises PASSED [ 36%]
+tests/test_virus.py::TestVirus::test_validate_datasets_binary_empty_path PASSED [ 36%]
+tests/test_virus.py::TestVirus::test_virus_invalid_annotated_type PASSED [ 36%]
+tests/test_virus.py::TestVirus::test_filter_genbank_metadata_env_source PASSED [ 36%]
+tests/test_virus.py::TestVirus::test_virus_invalid_proteins_complete_type PASSED [ 37%]
+tests/test_virus.py::TestVirus::test_retry_helper_failed_commands_tracking PASSED [ 37%]
+tests/test_virus.py::TestVirus::test_filter_genbank_metadata_genotype_list PASSED [ 37%]
+tests/test_virus.py::TestVirus::test_virus_invalid_is_accession_type PASSED [ 37%]
+tests/test_virus.py::TestVirus::test_parse_accession_input_space_separated PASSED [ 38%]
+tests/test_virus.py::TestVirus::test_virus_multiple_filters_relationship_check PASSED [ 38%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_no_filters PASSED [ 38%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_host PASSED [ 38%]
+tests/test_virus.py::TestVirus::test_stream_copy_fasta_with_accession_filter PASSED [ 39%]
+tests/test_virus.py::TestVirus::test_virus_invalid_genbank_metadata_type PASSED [ 39%]
+tests/test_virus.py::TestVirus::test_write_fasta_record_long_sequence_wraps PASSED [ 39%]
+tests/test_virus.py::TestVirus::test_unzip_file_bad_zip_raises PASSED [ 39%]
+tests/test_virus.py::TestVirus::test_parse_partial_date_year_only_min_comparison PASSED [ 40%]
+tests/test_virus.py::TestVirus::test_virus_min_greater_than_max_protein_count PASSED [ 40%]
+tests/test_virus.py::TestVirus::test_get_memory_usage_returns_dict PASSED [ 40%]
+tests/test_virus.py::TestVirus::test_virus_host_filter_verification PASSED [ 40%]
+tests/test_virus.py::TestVirus::test_virus_with_has_proteins_filter PASSED [ 40%]
+tests/test_virus.py::TestVirus::test_virus_release_date_filter_verification PASSED [ 41%]
+tests/test_virus.py::TestVirus::test_get_modified_virus_name_remove_parentheses PASSED [ 41%]
+tests/test_virus.py::TestVirus::test_virus_with_host_filter PASSED [ 41%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_annotated_false PASSED [ 41%]
+tests/test_virus.py::TestVirus::test_retry_helper_successful_operation PASSED [ 42%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_max_release_date PASSED [ 42%]
+tests/test_virus.py::TestVirus::test_virus_min_greater_than_max_peptide_count PASSED [ 42%]
+tests/test_virus.py::TestVirus::test_parse_partial_date_year_month_max PASSED [ 42%]
+tests/test_virus.py::TestVirus::test_filter_cached_min_release_date PASSED [ 43%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_annotated_true PASSED [ 43%]
+tests/test_virus.py::TestVirus::test_filter_genbank_metadata_provirus_true PASSED [ 43%]
+tests/test_virus.py::TestVirus::test_merge_metadata_csvs_no_overwrite PASSED [ 43%]
+tests/test_virus.py::TestVirus::test_validate_datasets_binary_nonexistent PASSED [ 44%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_nuc_completeness_partial PASSED [ 44%]
+tests/test_virus.py::TestVirus::test_virus_with_genbank_metadata_retrieval PASSED [ 44%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_submitter_institution PASSED [ 44%]
+tests/test_virus.py::TestVirus::test_parse_accession_input_empty_file_raises_error PASSED [ 45%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_lab_passaged_false PASSED [ 45%]
tests/test_virus.py::TestVirus::test_filter_cached_refseq_only PASSED [ 45%]
-tests/test_virus.py::TestVirus::test_parse_date_various_formats PASSED [ 45%]
-tests/test_virus.py::TestVirus::test_filter_cached_min_release_date PASSED [ 45%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_source_database PASSED [ 46%]
-tests/test_virus.py::TestVirus::test_merge_metadata_csvs_fills_missing PASSED [ 46%]
-tests/test_virus.py::TestVirus::test_is_alphainfluenza_query_accession_mode PASSED [ 46%]
-tests/test_virus.py::TestVirus::test_parse_accession_input_from_file PASSED [ 46%]
-tests/test_virus.py::TestVirus::test_virus_with_multiple_filters PASSED [ 47%]
-tests/test_virus.py::TestVirus::test_filter_genbank_metadata_provirus_true PASSED [ 47%]
-tests/test_virus.py::TestVirus::test_virus_min_greater_than_max_release_date PASSED [ 47%]
-tests/test_virus.py::TestVirus::test_get_modified_virus_name_empty_returns_none PASSED [ 47%]
-tests/test_virus.py::TestVirus::test_load_metadata_from_api_reports_empty PASSED [ 48%]
-tests/test_virus.py::TestVirus::test_filter_genbank_metadata_max_gene_count PASSED [ 48%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_isolation_source PASSED [ 48%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_nuc_completeness_partial PASSED [ 48%]
-tests/test_virus.py::TestVirus::test_parse_baseline_file_none_raises PASSED [ 49%]
-tests/test_virus.py::TestVirus::test_parse_baseline_file_nonexistent_raises PASSED [ 49%]
-tests/test_virus.py::TestVirus::test_get_memory_usage_returns_dict PASSED [ 49%]
-tests/test_virus.py::TestVirus::test_filter_cached_geographic_location PASSED [ 49%]
-tests/test_virus.py::TestVirus::test_virus_min_greater_than_max_peptide_count PASSED [ 50%]
-tests/test_virus.py::TestVirus::test_parse_partial_date_empty_raises PASSED [ 50%]
-tests/test_virus.py::TestVirus::test_local_name_without_namespace PASSED [ 50%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_vaccine_strain_true PASSED [ 50%]
-tests/test_virus.py::TestVirus::test_virus_invalid_completeness PASSED [ 51%]
-tests/test_virus.py::TestVirus::test_virus_with_genbank_metadata_retrieval PASSED [ 51%]
-tests/test_virus.py::TestVirus::test_save_partial_metadata PASSED [ 51%]
-tests/test_virus.py::TestVirus::test_merge_baseline_with_new_csv PASSED [ 51%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_lab_passaged_false PASSED [ 52%]
-tests/test_virus.py::TestVirus::test_virus_invalid_annotated_type PASSED [ 52%]
-tests/test_virus.py::TestVirus::test_filter_cached_host_in_strategy_skipped PASSED [ 52%]
-tests/test_virus.py::TestVirus::test_filter_genbank_metadata_no_filters PASSED [ 52%]
-tests/test_virus.py::TestVirus::test_parse_accession_input_single PASSED [ 53%]
-tests/test_virus.py::TestVirus::test_virus_invalid_genbank_batch_size_type PASSED [ 53%]
-tests/test_virus.py::TestVirus::test_track_failed_operation_appends PASSED [ 53%]
-tests/test_virus.py::TestVirus::test_parse_partial_date_year_only_min_comparison PASSED [ 53%]
-tests/test_virus.py::TestVirus::test_genbank_xml_to_csv_basic PASSED [ 54%]
-tests/test_virus.py::TestVirus::test_virus_min_greater_than_max_protein_count PASSED [ 54%]
-tests/test_virus.py::TestVirus::test_filter_genbank_metadata_combined PASSED [ 54%]
-tests/test_virus.py::TestVirus::test_virus_with_has_proteins_filter PASSED [ 54%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_segment PASSED [ 55%]
-tests/test_virus.py::TestVirus::test_get_modified_virus_name_add_space_before_virus PASSED [ 55%]
-tests/test_virus.py::TestVirus::test_parse_genbank_xml_basic PASSED [ 55%]
-tests/test_virus.py::TestVirus::test_virus_specific_accession_file_creation PASSED [ 55%]
-tests/test_virus.py::TestVirus::test_parse_date_invalid_raises PASSED [ 56%]
-tests/test_virus.py::TestVirus::test_filter_genbank_metadata_min_gene_count PASSED [ 56%]
-tests/test_virus.py::TestVirus::test_local_name_with_namespace PASSED [ 56%]
-tests/test_virus.py::TestVirus::test_get_datasets_version_returns_string_or_none PASSED [ 56%]
-tests/test_virus.py::TestVirus::test_virus_none_virus_name PASSED [ 57%]
-tests/test_virus.py::TestVirus::test_track_failed_operation_creates_key PASSED [ 57%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_annotated_false PASSED [ 57%]
-tests/test_virus.py::TestVirus::test_load_metadata_from_api_reports_basic PASSED [ 57%]
-tests/test_virus.py::TestVirus::test_load_metadata_from_api_reports_multiple PASSED [ 58%]
-tests/test_virus.py::TestVirus::test_write_fasta_record_long_sequence_wraps PASSED [ 58%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_isolate PASSED [ 58%]
-tests/test_virus.py::TestVirus::test_retry_helper_custom_retryable_exceptions PASSED [ 58%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_collection_date_range PASSED [ 59%]
-tests/test_virus.py::TestVirus::test_virus_multi_accession_space_separated PASSED [ 59%]
-tests/test_virus.py::TestVirus::test_filter_cached_no_filters PASSED [ 59%]
-tests/test_virus.py::TestVirus::test_merge_metadata_csvs_no_overwrite PASSED [ 59%]
-tests/test_virus.py::TestVirus::test_get_modified_virus_name_already_correct_returns_none PASSED [ 60%]
-tests/test_virus.py::TestVirus::test_parse_partial_date_full_date PASSED [ 60%]
-tests/test_virus.py::TestVirus::test_write_fasta_record_with_description PASSED [ 60%]
-tests/test_virus.py::TestVirus::test_parse_partial_date_year_month_min PASSED [ 60%]
-tests/test_virus.py::TestVirus::test_get_datasets_path_returns_valid_path PASSED [ 61%]
-tests/test_virus.py::TestVirus::test_save_genbank_metadata_to_csv_basic PASSED [ 61%]
-tests/test_virus.py::TestVirus::test_filter_genbank_metadata_genotype PASSED [ 61%]
-tests/test_virus.py::TestVirus::test_write_fasta_record_without_description PASSED [ 61%]
-tests/test_virus.py::TestVirus::test_filter_cached_host_not_in_strategy PASSED [ 62%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_protein_count PASSED [ 62%]
-tests/test_virus.py::TestVirus::test_virus_host_filter_verification PASSED [ 62%]
-tests/test_virus.py::TestVirus::test_filter_cached_complete_only PASSED [ 62%]
-tests/test_virus.py::TestVirus::test_virus_with_completeness_filter PASSED [ 63%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_no_filters PASSED [ 63%]
-tests/test_virus.py::TestVirus::test_virus_empty_virus_name PASSED [ 63%]
-tests/test_virus.py::TestVirus::test_virus_invalid_genbank_metadata_type PASSED [ 63%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_submitter_institution PASSED [ 64%]
-tests/test_virus.py::TestVirus::test_virus_integer_virus_id PASSED [ 64%]
-tests/test_virus.py::TestVirus::test_filter_genbank_metadata_has_proteins PASSED [ 64%]
-tests/test_virus.py::TestVirus::test_parse_genbank_xml_invalid_raises PASSED [ 64%]
-tests/test_virus.py::TestVirus::test_load_metadata_from_api_reports_missing_accession PASSED [ 65%]
-tests/test_virus.py::TestVirus::test_validate_datasets_binary_nonexistent PASSED [ 65%]
-tests/test_virus.py::TestVirus::test_virus_with_max_ambiguous_chars_filter PASSED [ 65%]
-tests/test_virus.py::TestVirus::test_virus_with_protein_count_filters PASSED [ 65%]
-tests/test_virus.py::TestVirus::test_force_garbage_collection_runs PASSED [ 66%]
-tests/test_virus.py::TestVirus::test_merge_baseline_with_new_deduplicates PASSED [ 66%]
-tests/test_virus.py::TestVirus::test_parse_baseline_file_json PASSED [ 66%]
-tests/test_virus.py::TestVirus::test_clean_xml_declarations_no_declarations PASSED [ 66%]
-tests/test_virus.py::TestVirus::test_retry_helper_failed_commands_tracking PASSED [ 67%]
-tests/test_virus.py::TestVirus::test_virus_with_geographic_location_filter PASSED [ 67%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_annotated_true PASSED [ 67%]
-tests/test_virus.py::TestVirus::test_virus_with_collection_date_filters PASSED [ 67%]
-tests/test_virus.py::TestVirus::test_parse_partial_date_year_month_max PASSED [ 68%]
-tests/test_virus.py::TestVirus::test_filter_cached_annotated PASSED [ 68%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_geographic_location PASSED [ 68%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_min_seq_length PASSED [ 68%]
-tests/test_virus.py::TestVirus::test_merge_metadata_csvs_missing_standard_file PASSED [ 69%]
-tests/test_virus.py::TestVirus::test_parse_accession_input_nonexistent_file_raises_error PASSED [ 69%]
-tests/test_virus.py::TestVirus::test_save_metadata_to_csv_basic PASSED [ 69%]
-tests/test_virus.py::TestVirus::test_get_modified_virus_name_remove_parentheses PASSED [ 69%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_host PASSED [ 70%]
-tests/test_virus.py::TestVirus::test_clean_xml_declarations PASSED [ 70%]
-tests/test_virus.py::TestVirus::test_filter_genbank_metadata_genotype_list PASSED [ 70%]
-tests/test_virus.py::TestVirus::test_retry_helper_non_retryable_exception PASSED [ 70%]
-tests/test_virus.py::TestVirus::test_save_genbank_metadata_to_csv_empty PASSED [ 71%]
-tests/test_virus.py::TestVirus::test_save_command_summary_with_error PASSED [ 71%]
-tests/test_virus.py::TestVirus::test_save_metadata_to_csv_empty_metadata PASSED [ 71%]
-tests/test_virus.py::TestVirus::test_check_min_max_date_valid PASSED [ 71%]
-tests/test_virus.py::TestVirus::test_deduplicate_all_existing PASSED [ 72%]
-tests/test_virus.py::TestVirus::test_virus_invalid_genbank_batch_size_zero PASSED [ 72%]
-tests/test_virus.py::TestVirus::test_filter_genbank_metadata_provirus_false PASSED [ 72%]
-tests/test_virus.py::TestVirus::test_get_modified_virus_name_add_virus_suffix PASSED [ 72%]
-tests/test_virus.py::TestVirus::test_virus_with_host_filter PASSED [ 73%]
-tests/test_virus.py::TestVirus::test_check_min_max_none_values PASSED [ 73%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_submitter_country PASSED [ 73%]
-tests/test_virus.py::TestVirus::test_virus_invalid_lab_passaged_type PASSED [ 73%]
-tests/test_virus.py::TestVirus::test_virus_metadata_schema_validation PASSED [ 74%]
-tests/test_virus.py::TestVirus::test_validate_datasets_binary_empty_path PASSED [ 74%]
-tests/test_virus.py::TestVirus::test_get_modified_virus_name_invalid_attempt_returns_none PASSED [ 74%]
-tests/test_virus.py::TestVirus::test_filter_genbank_metadata_mature_peptide_count PASSED [ 74%]
-tests/test_virus.py::TestVirus::test_filter_genbank_metadata_gen_mol_type PASSED [ 75%]
-tests/test_virus.py::TestVirus::test_stream_copy_fasta_with_accession_filter PASSED [ 75%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_combined_filters PASSED [ 75%]
-tests/test_virus.py::TestVirus::test_parse_accession_input_space_separated PASSED [ 75%]
-tests/test_virus.py::TestVirus::test_unzip_file_nonexistent_raises PASSED [ 76%]
-tests/test_virus.py::TestVirus::test_save_partial_metadata_empty PASSED [ 76%]
-tests/test_virus.py::TestVirus::test_get_modified_virus_name_no_parentheses_returns_none PASSED [ 76%]
-tests/test_virus.py::TestVirus::test_retry_helper_exponential_backoff_timing PASSED [ 76%]
-tests/test_virus.py::TestVirus::test_virus_with_source_database_filter PASSED [ 77%]
-tests/test_virus.py::TestVirus::test_virus_with_length_filters PASSED [ 77%]
-tests/test_virus.py::TestVirus::test_filter_sequences_proteins_complete PASSED [ 77%]
-tests/test_virus.py::TestVirus::test_is_sars_cov2_query_negative PASSED [ 77%]
-tests/test_virus.py::TestVirus::test_virus_min_greater_than_max_seq_length PASSED [ 78%]
-tests/test_virus.py::TestVirus::test_retry_helper_success_after_retry PASSED [ 78%]
-tests/test_virus.py::TestVirus::test_virus_with_annotated_filter PASSED [ 78%]
-tests/test_virus.py::TestVirus::test_parse_baseline_file_text PASSED [ 78%]
-tests/test_virus.py::TestVirus::test_calculate_max_accessions_per_batch PASSED [ 79%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_max_seq_length PASSED [ 79%]
-tests/test_virus.py::TestVirus::test_unzip_file_bad_zip_raises PASSED [ 79%]
-tests/test_virus.py::TestVirus::test_parse_accession_input_empty_file_raises_error PASSED [ 79%]
-tests/test_virus.py::TestVirus::test_unzip_file_valid PASSED [ 80%]
-tests/test_virus.py::TestVirus::test_deduplicate_empty_baseline PASSED [ 80%]
-tests/test_virus.py::TestVirus::test_virus_multi_accession_file_input PASSED [ 80%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_submitter_name PASSED [ 80%]
-tests/test_virus.py::TestVirus::test_is_alphainfluenza_query_common_names PASSED [ 81%]
-tests/test_virus.py::TestVirus::test_virus_release_date_filter_verification PASSED [ 81%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_vaccine_strain_false PASSED [ 81%]
-tests/test_virus.py::TestVirus::test_virus_multiple_filters_relationship_check PASSED [ 81%]
-tests/test_virus.py::TestVirus::test_virus_invalid_is_accession_type PASSED [ 82%]
-tests/test_virus.py::TestVirus::test_virus_invalid_keep_temp_type PASSED [ 82%]
-tests/test_virus.py::TestVirus::test_is_sars_cov2_query_common_names PASSED [ 82%]
-tests/test_virus.py::TestVirus::test_get_gget_version_returns_string PASSED [ 82%]
-tests/test_virus.py::TestVirus::test_filter_sequences_max_ambiguous_chars PASSED [ 83%]
-tests/test_virus.py::TestVirus::test_validate_datasets_binary_valid PASSED [ 83%]
-tests/test_virus.py::TestVirus::test_parse_baseline_file_jsonl PASSED [ 83%]
-tests/test_virus.py::TestVirus::test_check_min_max_valid PASSED [ 83%]
-tests/test_virus.py::TestVirus::test_parse_genbank_xml_empty_set PASSED [ 84%]
-tests/test_virus.py::TestVirus::test_is_sars_cov2_query_accession_mode PASSED [ 84%]
-tests/test_virus.py::TestVirus::test_filter_genbank_metadata_env_source PASSED [ 84%]
-tests/test_virus.py::TestVirus::test_save_command_summary_creates_file PASSED [ 84%]
-tests/test_virus.py::TestVirus::test_check_min_max_date_invalid_raises PASSED [ 85%]
-tests/test_virus.py::TestVirus::test_filter_genbank_metadata_empty PASSED [ 85%]
-tests/test_virus.py::TestVirus::test_parse_baseline_file_empty_raises PASSED [ 85%]
-tests/test_virus.py::TestVirus::test_virus_min_greater_than_max_collection_date PASSED [ 85%]
-tests/test_virus.py::TestVirus::test_virus_invalid_proteins_complete_type PASSED [ 86%]
-tests/test_virus.py::TestVirus::test_filter_sequences_no_filters PASSED [ 86%]
-tests/test_virus.py::TestVirus::test_stream_copy_fasta_all_records PASSED [ 86%]
-tests/test_virus.py::TestVirus::test_virus_with_lab_passaged_filter PASSED [ 86%]
-tests/test_virus.py::TestVirus::test_parse_date_full PASSED [ 87%]
-tests/test_virus.py::TestVirus::test_is_alphainfluenza_query_negative PASSED [ 87%]
-tests/test_virus.py::TestVirus::test_virus_relationship_check_counts_match PASSED [ 87%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_lab_passaged_true PASSED [ 87%]
-tests/test_virus.py::TestVirus::test_virus_invalid_genbank_batch_size_negative PASSED [ 88%]
-tests/test_virus.py::TestVirus::test_check_min_max_invalid_raises PASSED [ 88%]
-tests/test_virus.py::TestVirus::test_batch_accessions_for_url PASSED [ 88%]
-tests/test_virus.py::TestVirus::test_deduplicate_metadata_against_baseline PASSED [ 88%]
-tests/test_virus.py::TestVirus::test_parse_partial_date_year_only_max_comparison PASSED [ 89%]
-tests/test_virus.py::TestVirus::test_datasets_cli_version_output PASSED [ 89%]
-tests/test_virus.py::TestVirus::test_virus_min_greater_than_max_gene_count PASSED [ 89%]
-tests/test_virus.py::TestVirus::test_retry_helper_successful_operation PASSED [ 89%]
-tests/test_virus.py::TestVirus::test_filter_metadata_only_max_release_date PASSED [ 90%]
-tests/test_virus.py::TestVirus::test_merge_baseline_with_new_empty_new PASSED [ 90%]
-tests/test_compile.py::TestCompilerWindows::test_compiler_windows PASSED [ 90%]
-tests/test_pdb.py::TestPDB::test_pdb_pdb PASSED [ 90%]
-tests/test_pdb.py::TestPDB::test_pdb_assembly PASSED [ 91%]
-tests/test_bgee.py::TestBgee::test_bgee_expression PASSED [ 91%]
-tests/test_bgee.py::TestBgee::test_error_bgee_unknown_type PASSED [ 91%]
-tests/test_bgee.py::TestBgee::test_bgee_expression_multiple PASSED [ 91%]
-tests/test_bgee.py::TestBgee::test_error_bgee_expression_no_exist PASSED [ 92%]
-tests/test_bgee.py::TestBgee::test_error_bgee_orthologs_no_exist PASSED [ 92%]
-tests/test_bgee.py::TestBgee::test_bgee_orthologs PASSED [ 92%]
-tests/test_diamond.py::TestDiamond::test_diamond_both_files PASSED [ 92%]
-tests/test_diamond.py::TestDiamond::test_diamond_seqs_multiple PASSED [ 93%]
-tests/test_diamond.py::TestDiamond::test_diamond_seqs_single PASSED [ 93%]
-tests/test_diamond.py::TestDiamond::test_diamond_ref_file PASSED [ 93%]
-tests/test_diamond.py::TestDiamond::test_diamond_JSON_out PASSED [ 93%]
-tests/test_diamond.py::TestDiamond::test_diamond_query_file PASSED [ 94%]
-tests/test_blast.py::TestBlast::test_blast_bad_db1 PASSED [ 94%]
-tests/test_blast.py::TestBlast::test_blast_bad_db2 PASSED [ 94%]
-tests/test_blast.py::TestBlast::test_blast_bad_fasta PASSED [ 94%]
-tests/test_blast.py::TestBlast::test_blast_db_missing PASSED [ 95%]
-tests/test_blast.py::TestBlast::test_blast_bad_seq PASSED [ 95%]
-tests/test_blast.py::TestBlast::test_blast_bad_program PASSED [ 95%]
-tests/test_blast.py::TestBlast::test_blast_nt PASSED [ 95%]
-tests/test_8cube.py::TestPsiBlock::test_psi_block_basic PASSED [ 96%]
-tests/test_8cube.py::TestGeneExpression::test_gene_expression_basic PASSED [ 96%]
-tests/test_8cube.py::TestSpecificity::test_specificity_basic PASSED [ 96%]
-tests/test_ref.py::TestRef::test_ref_bad_rel PASSED [ 96%]
-tests/test_ref.py::TestRef::test_ref_rel PASSED [ 97%]
-tests/test_ref.py::TestRef::test_ref_rel_ftp_octopus PASSED [ 97%]
-tests/test_ref.py::TestRef::test_ref_list PASSED [ 97%]
-tests/test_ref.py::TestRef::test_ref_which PASSED [ 97%]
-tests/test_ref.py::TestRef::test_ref_rel_ftp PASSED [ 98%]
-tests/test_ref.py::TestRef::test_ref_bad_which PASSED [ 98%]
-tests/test_ref.py::TestRef::test_ref PASSED [ 98%]
-tests/test_ref.py::TestRef::test_ref_ftp PASSED [ 98%]
-tests/test_ref.py::TestRef::test_ref_plant PASSED [ 99%]
-tests/test_ref.py::TestRef::test_ref_bad_species PASSED [ 99%]
-tests/test_ref.py::TestRef::test_ref_which_plant PASSED [ 99%]
-tests/test_ref.py::TestRef::test_ref_rel_protist PASSED [ 99%]
-tests/test_gpt.py::TestGpt::test_gpt PASSED [100%]
-
-=================================== FAILURES ===================================
-__________________ TestOpenTargets.test_opentargets_diseases ___________________
-
-self =
-
- def assert_equal_json_with_keys(self: unittest.TestCase):
- def normalize(x):
- if isinstance(x, tuple):
- x = [normalize(v) for v in x]
-
- if isinstance(x, list):
- x = [normalize(v) for v in x]
-
- # Detect list-of-pairs -> dict recursively.
- if all(isinstance(i, list) and len(i) == 2 for i in x):
- try:
- x = {i[0]: i[1] for i in x}
- except Exception: # noqa: BLE001
- try:
- x = {i[1]: i[0] for i in x}
- except Exception: # noqa: BLE001
- pass
-
- # Collapse singleton wrappers such as:
- # [{"drug": {...}}] -> {"drug": {...}}
- # ["26387812"] -> "26387812"
- if isinstance(x, list) and len(x) == 1:
- return normalize(x[0])
-
- if isinstance(x, dict):
- x = {k: normalize(v) for k, v in x.items()}
-
- # Collapse single-key wrapper dicts such as:
- # {"drug": {...}} -> {...}
- # {"drug": None} -> None
- if len(x) == 1:
- return normalize(next(iter(x.values())))
-
- return x
-
- return x
-
- test = name
-
- expected_result = td[test]["expected_result"]
- result_to_test = do_call(func, td[test]["args"])
- if isinstance(result_to_test, pd.DataFrame):
- result_to_test = json.loads(
- result_to_test.dropna(axis=1, how="all").to_json(orient="records", force_ascii=False)
- )
-
- result_to_test = normalize(result_to_test)
- expected_result = normalize(expected_result)
-
- # ✅ Infer keys from expected_result
- if not expected_result:
- raise ValueError(f"Test {test} has empty expected_result")
-
- keys = list(expected_result[0].keys())
-
- # Convert list-of-lists → list-of-dicts
- if isinstance(result_to_test, list) and len(result_to_test) > 0 and isinstance(result_to_test[0], list):
- result_to_test = [dict(zip(keys, row, strict=False)) for row in result_to_test]
-
- # Optional: float rounding
- def round_dict(d, ndigits=10):
- return {k: round(v, ndigits) if isinstance(v, float) else v for k, v in d.items()}
-
- result_to_test = [round_dict(r) for r in result_to_test]
- expected_result = [round_dict(r) for r in expected_result]
-
-> self.assertEqual(result_to_test, expected_result)
-E AssertionError: Lists differ: [{'score': 0.7279798022, 'disease.id': 'MONDO_0004980', [815 chars]y.'}] != [{'score': 0.7297489019, 'disease.id': 'EFO_0000274', 'd[813 chars]y.'}]
-E
-E First differing element 0:
-E {'score': 0.7279798022, 'disease.id': 'MONDO_0004980', [195 chars]fe.'}
-E {'score': 0.7297489019, 'disease.id': 'EFO_0000274', 'd[193 chars]fe.'}
-E
-E [{'disease.description': 'A common chronic pruritic inflammatory skin disease '
-E 'with a strong genetic component. Onset typically '
-E 'occurs during the first 2 years of life.',
-E - 'disease.id': 'MONDO_0004980',
-E ? ^^^^ ---
-E
-E + 'disease.id': 'EFO_0000274',
-E ? ^^ +++
-E
-E 'disease.name': 'atopic eczema',
-E - 'score': 0.7279798022},
-E ? - - ^^
-E
-E + 'score': 0.7297489019},
-E ? ++ ^^
-E
-E {'disease.description': 'A bronchial disease that is characterized by chronic '
-E 'inflammation and narrowing of the airways, which is '
-E 'caused by a combination of environmental and genetic '
-E 'factors resulting in recurring periods of wheezing '
-E '(a whistling sound while breathing), chest '
-E 'tightness, shortness of breath, mucus production and '
-E 'coughing. The symptoms appear due to a variety of '
-E 'triggers such as allergens, irritants, respiratory '
-E 'infections, weather changes, exercise, stress, '
-E 'reflux disease, medications, foods and emotional '
-E 'anxiety.',
-E 'disease.id': 'MONDO_0004979',
-E 'disease.name': 'asthma',
-E - 'score': 0.6952093612}]
-E + 'score': 0.6642728578}]
-
-tests/from_json.py:248: AssertionError
------------------------------- Captured log call -------------------------------
-INFO gget.utils:gget_opentargets.py:322 Querying OpenTargets for diseases associated with ENSG00000169194...
-____________ TestOpenTargets.test_opentargets_interactions_no_limit ____________
-
-self =
-
- def assert_equal_json_hash(self: unittest.TestCase):
- test = name
- expected_result = td[test]["expected_result"]
- result_to_test = do_call(func, td[test]["args"])
- # If result is a DataFrame, convert to list
- if isinstance(result_to_test, pd.DataFrame):
- result_to_test = result_to_test.dropna(axis=1).values.tolist()
-
- result_to_test = json.dumps(result_to_test)
- result_to_test = hashlib.md5(result_to_test.encode()).hexdigest()
-
-> self.assertEqual(result_to_test, expected_result)
-E AssertionError: '8ed7f7380590628acdbaa5f9501365a6' != 'fa95d278c2d31ded3731e154d65fcda5'
-E - 8ed7f7380590628acdbaa5f9501365a6
-E + fa95d278c2d31ded3731e154d65fcda5
-
-tests/from_json.py:145: AssertionError
------------------------------- Captured log call -------------------------------
-INFO gget.utils:gget_opentargets.py:322 Querying OpenTargets for interactions associated with ENSG00000169194...
-_________ TestOpenTargets.test_error_opentargets_drugs_invalid_filter __________
-
-self =
-
- def error(self: unittest.TestCase):
- test = name
- with self.assertRaises(Error) as cm:
-> do_call(func, td[test]["args"])
-
-tests/from_json.py:272:
-_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
-tests/from_json.py:60: in do_call
- return func(**args)
- ^^^^^^^^^^^^
-gget/gget_opentargets.py:325: in opentargets
- api_response = http_json(
-_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
-
- def http_json(
- method: str,
- url: str,
- *,
- context: str = "",
- timeout: float = DEFAULT_REQUESTS_TIMEOUT,
- retries: int = 3,
- backoff: float = 1.0,
- **kwargs: Any,
- ) -> Any:
- """Issue an HTTP request and return the parsed JSON body, raising a
-
- RuntimeError with consistent context if the request fails or the body
- is not valid JSON.
-
- `context` is a short human-readable label (e.g. "Bgee API") used in
- error messages so users can identify which upstream service failed.
- Transient failures (connection errors, read timeouts, HTTP 5xx) are
- retried up to `retries` additional times with exponential backoff
- (`backoff * 2**i` seconds between attempts). Set `retries=0` to disable.
- All other keyword arguments are forwarded to `requests.request`.
- """
- label = context or url
- last_exc = None
- last_status = None
- last_body = ""
- attempts = retries + 1
- for attempt in range(attempts):
- try:
- response = requests.request(method, url, timeout=timeout, **kwargs)
- except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
- last_exc = e
- last_status = None
- last_body = ""
- else:
- if response.ok:
- try:
- return response.json()
- except json.JSONDecodeError as e:
- body = response.text[:200] if response.text else ""
- raise RuntimeError(
- f"{label} returned non-JSON response (HTTP {response.status_code}): {body}"
- ) from e
- # Retry on transient server errors only; client errors are returned
- # to the caller without retry.
- if response.status_code < 500:
- body = response.text[:200] if response.text else ""
-> raise RuntimeError(f"{label} returned HTTP {response.status_code}. Body: {body}")
-E RuntimeError: OpenTargets GraphQL returned HTTP 400. Body: {"errors":[{"message":"Field 'synonyms' of type '[DrugLabelAndSource!]!' must have a sub selection. (line 16, column 11):\n synonyms\n ^","locations":[{"line":16,"column":11}]},{"mes
-
-gget/utils.py:148: RuntimeError
------------------------------- Captured log call -------------------------------
-INFO gget.utils:gget_opentargets.py:322 Querying OpenTargets for drugs associated with ENSG00000169194...
-___________________ TestOpenTargets.test_opentargets_depmap ____________________
-
-self =
-
- def assert_equal_json_hash(self: unittest.TestCase):
- test = name
- expected_result = td[test]["expected_result"]
- result_to_test = do_call(func, td[test]["args"])
- # If result is a DataFrame, convert to list
- if isinstance(result_to_test, pd.DataFrame):
- result_to_test = result_to_test.dropna(axis=1).values.tolist()
-
- result_to_test = json.dumps(result_to_test)
- result_to_test = hashlib.md5(result_to_test.encode()).hexdigest()
-
-> self.assertEqual(result_to_test, expected_result)
-E AssertionError: 'e81a60ff47c06bfecb3b421d28047718' != 'c335cc9c9b3167e8c5b3084e339c88a7'
-E - e81a60ff47c06bfecb3b421d28047718
-E + c335cc9c9b3167e8c5b3084e339c88a7
-
-tests/from_json.py:145: AssertionError
------------------------------- Captured log call -------------------------------
-INFO gget.utils:gget_opentargets.py:322 Querying OpenTargets for depmap associated with ENSG00000169194...
-_______________________ TestOpenTargets.test_opentargets _______________________
-
-self =
-
- def assert_equal(self: unittest.TestCase):
- test = name
- expected_result = td[test]["expected_result"]
- result_to_test = do_call(func, td[test]["args"])
- if test == "test_cosmic_defaults": # special case for cosmic
- import numpy as np
-
- expected_result = pd.DataFrame(expected_result[0])
- expected_result = expected_result.replace({None: np.nan})
- # result_to_test.equals(expected_result)
- pd.testing.assert_frame_equal(result_to_test, expected_result, check_dtype=False)
- return
-
- # If result is a DataFrame, convert to list
- if isinstance(result_to_test, pd.DataFrame):
- result_to_test = result_to_test.dropna(axis=1).values.tolist()
-
-> self.assertEqual(result_to_test, expected_result)
-E AssertionError: Lists differ: [[0.7279798021712002, 'MONDO_0004980', 'atopic [712 chars]y.']] != [[0.7297489019498119, 'EFO_0000274', 'atopic ec[710 chars]y.']]
-E
-E First differing element 0:
-E [0.7279798021712002, 'MONDO_0004980', 'atopic [148 chars]fe.']
-E [0.7297489019498119, 'EFO_0000274', 'atopic ec[146 chars]fe.']
-E
-E - [[0.7279798021712002,
-E - 'MONDO_0004980',
-E + [[0.7297489019498119,
-E + 'EFO_0000274',
-E 'atopic eczema',
-E 'A common chronic pruritic inflammatory skin disease with a strong genetic '
-E 'component. Onset typically occurs during the first 2 years of life.'],
-E - [0.6952093612226559,
-E + [0.6642728577751653,
-E 'MONDO_0004979',
-E 'asthma',
-E 'A bronchial disease that is characterized by chronic inflammation and '
-E 'narrowing of the airways, which is caused by a combination of environmental '
-E 'and genetic factors resulting in recurring periods of wheezing (a whistling '
-E 'sound while breathing), chest tightness, shortness of breath, mucus '
-E 'production and coughing. The symptoms appear due to a variety of triggers '
-E 'such as allergens, irritants, respiratory infections, weather changes, '
-E 'exercise, stress, reflux disease, medications, foods and emotional '
-E 'anxiety.']]
-
-tests/from_json.py:82: AssertionError
------------------------------- Captured log call -------------------------------
-INFO gget.utils:gget_opentargets.py:322 Querying OpenTargets for diseases associated with ENSG00000169194...
-________________ TestOpenTargets.test_opentargets_interactions _________________
-
-self =
-
- def assert_equal_json_with_keys(self: unittest.TestCase):
- def normalize(x):
- if isinstance(x, tuple):
- x = [normalize(v) for v in x]
-
- if isinstance(x, list):
- x = [normalize(v) for v in x]
-
- # Detect list-of-pairs -> dict recursively.
- if all(isinstance(i, list) and len(i) == 2 for i in x):
- try:
- x = {i[0]: i[1] for i in x}
- except Exception: # noqa: BLE001
- try:
- x = {i[1]: i[0] for i in x}
- except Exception: # noqa: BLE001
- pass
-
- # Collapse singleton wrappers such as:
- # [{"drug": {...}}] -> {"drug": {...}}
- # ["26387812"] -> "26387812"
- if isinstance(x, list) and len(x) == 1:
- return normalize(x[0])
-
- if isinstance(x, dict):
- x = {k: normalize(v) for k, v in x.items()}
-
- # Collapse single-key wrapper dicts such as:
- # {"drug": {...}} -> {...}
- # {"drug": None} -> None
- if len(x) == 1:
- return normalize(next(iter(x.values())))
-
- return x
-
- return x
-
- test = name
-
- expected_result = td[test]["expected_result"]
- result_to_test = do_call(func, td[test]["args"])
- if isinstance(result_to_test, pd.DataFrame):
- result_to_test = json.loads(
- result_to_test.dropna(axis=1, how="all").to_json(orient="records", force_ascii=False)
- )
-
- result_to_test = normalize(result_to_test)
- expected_result = normalize(expected_result)
-
- # ✅ Infer keys from expected_result
- if not expected_result:
- raise ValueError(f"Test {test} has empty expected_result")
-
- keys = list(expected_result[0].keys())
-
- # Convert list-of-lists → list-of-dicts
- if isinstance(result_to_test, list) and len(result_to_test) > 0 and isinstance(result_to_test[0], list):
- result_to_test = [dict(zip(keys, row, strict=False)) for row in result_to_test]
-
- # Optional: float rounding
- def round_dict(d, ndigits=10):
- return {k: round(v, ndigits) if isinstance(v, float) else v for k, v in d.items()}
-
- result_to_test = [round_dict(r) for r in result_to_test]
- expected_result = [round_dict(r) for r in expected_result]
-
-> self.assertEqual(result_to_test, expected_result)
-E AssertionError: Lists differ: [{'sc[137 chars]000360730', 'intBBiologicalRole': 'unspecified[573 chars]134}] != [{'sc[137 chars]000361004', 'intBBiologicalRole': 'unspecified[573 chars]134}]
-E
-E First differing element 0:
-E {'sco[136 chars]000360730', 'intBBiologicalRole': 'unspecified[189 chars] 134}
-E {'sco[136 chars]000361004', 'intBBiologicalRole': 'unspecified[189 chars] 134}
-E
-E [{'count': 3,
-E + 'intA': 'ENSP00000304915',
-E + 'intABiologicalRole': 'unspecified role',
-E + 'intB': 'ENSP00000361004',
-E + 'intBBiologicalRole': 'unspecified role',
-E + 'score': 0.999,
-E + 'sourceDatabase': 'string',
-E + 'speciesA.taxonId': 134,
-E + 'speciesB.taxonId': 134,
-E + 'targetA.approvedSymbol': 'IL13',
-E + 'targetA.id': 'ENSG00000169194',
-E + 'targetB.approvedSymbol': 'IL13RA2',
-E + 'targetB.id': 'ENSG00000123496'},
-E + {'count': 3,
-E 'intA': 'ENSP00000304915',
-E 'intABiologicalRole': 'unspecified role',
-E 'intB': 'ENSP00000360730',
-E 'intBBiologicalRole': 'unspecified role',
-E 'score': 0.999,
-E 'sourceDatabase': 'string',
-E 'speciesA.taxonId': 134,
-E 'speciesB.taxonId': 134,
-E 'targetA.approvedSymbol': 'IL13',
-E 'targetA.id': 'ENSG00000169194',
-E 'targetB.approvedSymbol': 'IL13RA1',
-E - 'targetB.id': 'ENSG00000131724'},
-E ? ^
-E
-E + 'targetB.id': 'ENSG00000131724'}]
-E ? ^
-E
-E - {'count': 3,
-E - 'intA': 'ENSP00000304915',
-E - 'intABiologicalRole': 'unspecified role',
-E - 'intB': 'ENSP00000361004',
-E - 'intBBiologicalRole': 'unspecified role',
-E - 'score': 0.999,
-E - 'sourceDatabase': 'string',
-E - 'speciesA.taxonId': 134,
-E - 'speciesB.taxonId': 134,
-E - 'targetA.approvedSymbol': 'IL13',
-E - 'targetA.id': 'ENSG00000169194',
-E - 'targetB.approvedSymbol': 'IL13RA2',
-E - 'targetB.id': 'ENSG00000123496'}]
-
-tests/from_json.py:248: AssertionError
------------------------------- Captured log call -------------------------------
-INFO gget.utils:gget_opentargets.py:322 Querying OpenTargets for interactions associated with ENSG00000169194...
-_____________ TestOpenTargets.test_opentargets_expression_no_limit _____________
-
-self =
-
- def assert_equal_json_hash(self: unittest.TestCase):
- test = name
- expected_result = td[test]["expected_result"]
- result_to_test = do_call(func, td[test]["args"])
- # If result is a DataFrame, convert to list
- if isinstance(result_to_test, pd.DataFrame):
- result_to_test = result_to_test.dropna(axis=1).values.tolist()
-
- result_to_test = json.dumps(result_to_test)
- result_to_test = hashlib.md5(result_to_test.encode()).hexdigest()
-
-> self.assertEqual(result_to_test, expected_result)
-E AssertionError: 'd751713988987e9331980363e24189ce' != '7d32780ec48250553246c816d80b93ee'
-E - d751713988987e9331980363e24189ce
-E + 7d32780ec48250553246c816d80b93ee
+tests/test_virus.py::TestVirus::test_get_modified_virus_name_no_parentheses_returns_none PASSED [ 45%]
+tests/test_virus.py::TestVirus::test_virus_min_greater_than_max_release_date PASSED [ 45%]
+tests/test_virus.py::TestVirus::test_track_failed_operation_creates_key PASSED [ 46%]
+tests/test_virus.py::TestVirus::test_get_modified_virus_name_already_correct_returns_none PASSED [ 46%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_protein_count PASSED [ 46%]
+tests/test_virus.py::TestVirus::test_is_sars_cov2_query_accession_mode PASSED [ 46%]
+tests/test_virus.py::TestVirus::test_virus_min_greater_than_max_collection_date PASSED [ 47%]
+tests/test_virus.py::TestVirus::test_filter_cached_annotated PASSED [ 47%]
+tests/test_virus.py::TestVirus::test_filter_sequences_no_filters PASSED [ 47%]
+tests/test_virus.py::TestVirus::test_load_metadata_from_api_reports_empty PASSED [ 47%]
+tests/test_virus.py::TestVirus::test_is_alphainfluenza_query_negative PASSED [ 48%]
+tests/test_virus.py::TestVirus::test_save_command_summary_with_error PASSED [ 48%]
+tests/test_virus.py::TestVirus::test_virus_with_source_database_filter PASSED [ 48%]
+tests/test_virus.py::TestVirus::test_deduplicate_metadata_against_baseline PASSED [ 48%]
+tests/test_virus.py::TestVirus::test_datasets_cli_version_output PASSED [ 49%]
+tests/test_virus.py::TestVirus::test_get_modified_virus_name_invalid_attempt_returns_none PASSED [ 49%]
+tests/test_virus.py::TestVirus::test_filter_metadata_only_combined_filters PASSED [ 49%]
+tests/test_virus.py::TestVirus::test_parse_baseline_file_empty_raises PASSED [ 49%]
+tests/test_virus.py::TestVirus::test_filter_cached_host_in_strategy_skipped PASSED [ 50%]
+tests/test_virus.py::TestVirus::test_virus_with_geographic_location_filter PASSED [ 50%]
+tests/test_virus.py::TestVirus::test_get_datasets_version_returns_string_or_none PASSED [ 50%]
+tests/test_virus.py::TestVirus::test_virus_invalid_completeness PASSED [ 50%]
+tests/test_virus.py::TestVirus::test_stream_copy_fasta_all_records PASSED [ 50%]
+tests/test_virus.py::TestVirus::test_is_alphainfluenza_query_accession_mode PASSED [ 51%]
+tests/test_archs4.py::TestArchs4MissingColor::test_tissue_missing_color_does_not_crash PASSED [ 51%]
+tests/test_archs4.py::TestArchs4::test_archs4_mouse_json_ensembl PASSED [ 51%]
+tests/test_archs4.py::TestArchs4::test_archs4_bad_species PASSED [ 51%]
+tests/test_archs4.py::TestArchs4::test_archs4_defaults PASSED [ 52%]
+tests/test_archs4.py::TestArchs4::test_archs4_bad_which PASSED [ 52%]
+tests/test_archs4.py::TestArchs4::test_archs4_tissue_ensembl PASSED [ 52%]
+tests/test_archs4.py::TestArchs4::test_archs4_bad_gene_tissue PASSED [ 52%]
+tests/test_archs4.py::TestArchs4::test_archs4_tissue_json PASSED [ 53%]
+tests/test_archs4.py::TestArchs4::test_archs4_tissue PASSED [ 53%]
+tests/test_archs4.py::TestArchs4::test_archs4_bad_gene PASSED [ 53%]
+tests/test_archs4.py::TestArchs4::test_archs4_tissue_mouse PASSED [ 53%]
+tests/test_archs4.py::TestArchs4::test_archs4_bad_ensembl PASSED [ 54%]
+tests/test_gpt.py::TestGpt::test_gpt PASSED [ 54%]
+tests/test_info.py::TestInfo::test_info_gene PASSED [ 54%]
+tests/test_info.py::TestInfo::test_info_exon PASSED [ 54%]
+tests/test_info.py::TestInfo::test_info_transcript PASSED [ 54%]
+tests/test_info.py::TestInfo::test_info_ncbifalse_uniprottrue PASSED [ 55%]
+tests/test_info.py::TestInfo::test_info_ncbitrue_uniprotfalse PASSED [ 55%]
+tests/test_info.py::TestInfo::test_info_gene_list_non_model PASSED [ 55%]
+tests/test_info.py::TestInfo::test_info_mix PASSED [ 55%]
+tests/test_info.py::TestInfo::test_info_ncbifalse_uniprotfalse PASSED [ 56%]
+tests/test_info.py::TestInfo::test_info_WB_transcript PASSED [ 56%]
+tests/test_blat.py::TestBlat::test_blat_bad_seqtype PASSED [ 56%]
+tests/test_blat.py::TestBlat::test_blat_nt_RNA PASSED [ 56%]
+tests/test_blat.py::TestBlat::test_blat_aa_RNA PASSED [ 57%]
+tests/test_blat.py::TestBlat::test_blat_nt_fasta PASSED [ 57%]
+tests/test_blat.py::TestBlat::test_blat_nt_DNA PASSED [ 57%]
+tests/test_blat.py::TestBlat::test_blat_nt_protein PASSED [ 57%]
+tests/test_blat.py::TestBlat::test_blat_aa PASSED [ 58%]
+tests/test_blat.py::TestBlat::test_blat_nt_txt PASSED [ 58%]
+tests/test_blat.py::TestBlat::test_blat_bad_fileformat PASSED [ 58%]
+tests/test_blat.py::TestBlat::test_blat_nt PASSED [ 58%]
+tests/test_blat.py::TestBlat::test_blat_bad_txt PASSED [ 59%]
+tests/test_blat.py::TestBlat::test_blat_nt_json PASSED [ 59%]
+tests/test_blat.py::TestBlat::test_blat_nt_transDNA PASSED [ 59%]
+tests/test_blat.py::TestBlat::test_blat_shortseq PASSED [ 59%]
+tests/test_blat.py::TestBlat::test_blat_aa_protein PASSED [ 59%]
+tests/test_blat.py::TestBlat::test_blat_bad_assembly PASSED [ 60%]
+tests/test_blat.py::TestBlat::test_blat_bad_fasta PASSED [ 60%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_transcription PASSED [ 60%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_json PASSED [ 60%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_yeast PASSED [ 61%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_bad_database_shortcut PASSED [ 61%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_fish PASSED [ 61%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_bad_gene PASSED [ 61%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_plot PASSED [ 62%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_none PASSED [ 62%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_ensembl_ids PASSED [ 62%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_bad_background_list PASSED [ 62%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_bad_species PASSED [ 63%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_diseases_drugs PASSED [ 63%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_bad_background PASSED [ 63%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_background_ensembl PASSED [ 63%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_background PASSED [ 63%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_kinase_interactions PASSED [ 64%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_mouse_is_human PASSED [ 64%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_pathway PASSED [ 64%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_celltypes PASSED [ 64%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_fly PASSED [ 65%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_ontology PASSED [ 65%]
+tests/test_enrichr.py::TestEnrichr::test_enrichr_worm PASSED [ 65%]
+tests/test_cosmic.py::TestCosmicWorkflow::test_cosmic_defaults PASSED [ 65%]
+tests/test_diamond.py::TestDiamond::test_diamond_both_files PASSED [ 66%]
+tests/test_diamond.py::TestDiamond::test_diamond_seqs_multiple PASSED [ 66%]
+tests/test_diamond.py::TestDiamond::test_diamond_query_file PASSED [ 66%]
+tests/test_diamond.py::TestDiamond::test_diamond_JSON_out PASSED [ 66%]
+tests/test_diamond.py::TestDiamond::test_diamond_seqs_single PASSED [ 67%]
+tests/test_diamond.py::TestDiamond::test_diamond_ref_file PASSED [ 67%]
+tests/test_mutate.py::test_csv_of_mutations PASSED [ 67%]
+tests/test_mutate.py::test_mismatch_error PASSED [ 67%]
+tests/test_mutate.py::TestMutate::test_single_dup PASSED [ 68%]
+tests/test_mutate.py::TestMutate::test_ambiguous_mutation PASSED [ 68%]
+tests/test_mutate.py::TestMutate::test_multi_dup PASSED [ 68%]
+tests/test_mutate.py::TestMutate::test_multi_deletion_with_right_repeats PASSED [ 68%]
+tests/test_mutate.py::TestMutate::test_single_delins PASSED [ 68%]
+tests/test_mutate.py::TestMutate::test_large_k PASSED [ 69%]
+tests/test_mutate.py::TestMutate::test_inversion_with_overlaps PASSED [ 69%]
+tests/test_mutate.py::TestMutate::test_list_of_mutations PASSED [ 69%]
+tests/test_mutate.py::TestMutate::test_multi_delins PASSED [ 69%]
+tests/test_mutate.py::TestMutate::test_multi_delins_with_true_right_repeats PASSED [ 70%]
+tests/test_mutate.py::TestMutate::test_single_deletion_with_right_repeats PASSED [ 70%]
+tests/test_mutate.py::TestMutate::test_posttranslational_mutation PASSED [ 70%]
+tests/test_mutate.py::TestMutate::test_single_deletion_with_left_repeats PASSED [ 70%]
+tests/test_mutate.py::TestMutate::test_single_substitution_near_right_end PASSED [ 71%]
+tests/test_mutate.py::TestMutate::test_index_error PASSED [ 71%]
+tests/test_mutate.py::TestMutate::test_multi_deletion PASSED [ 71%]
+tests/test_mutate.py::TestMutate::test_intron_mutation_plus PASSED [ 71%]
+tests/test_mutate.py::TestMutate::test_multi_delins_with_true_left_repeats PASSED [ 72%]
+tests/test_mutate.py::TestMutate::test_single_deletion PASSED [ 72%]
+tests/test_mutate.py::TestMutate::test_single_substitution PASSED [ 72%]
+tests/test_mutate.py::TestMutate::test_intron_mutation_minus PASSED [ 72%]
+tests/test_mutate.py::TestMutate::test_multi_insertion PASSED [ 72%]
+tests/test_mutate.py::TestMutate::test_multi_delins_with_psuedo_left_repeats PASSED [ 73%]
+tests/test_mutate.py::TestMutate::test_multi_insertion_with_left_repeats PASSED [ 73%]
+tests/test_mutate.py::TestMutate::test_single_substitution_near_left_end PASSED [ 73%]
+tests/test_mutate.py::TestMutate::test_single_insertion PASSED [ 73%]
+tests/test_mutate.py::TestMutate::test_uncertain_mutation PASSED [ 74%]
+tests/test_seq.py::TestSeq::test_seq_transcript_gene_iso PASSED [ 74%]
+tests/test_seq.py::TestSeq::test_seq_transcript_transcript_iso PASSED [ 74%]
+tests/test_seq.py::TestSeq::test_seq_gene_transcript_iso PASSED [ 74%]
+tests/test_seq.py::TestSeq::test_seq_gene PASSED [ 75%]
+tests/test_seq.py::TestSeq::test_seq_transcript_transcript_WB PASSED [ 75%]
+tests/test_seq.py::TestSeq::test_seq_transcript_gene_WB PASSED [ 75%]
+tests/test_seq.py::TestSeq::test_seq_gene_iso PASSED [ 75%]
+tests/test_seq.py::TestSeq::test_seq_transcript PASSED [ 76%]
+tests/test_seq.py::TestSeq::test_seq_transcript_gene PASSED [ 76%]
+tests/test_seq.py::TestSeq::test_seq_missing_uniprot_gene_name PASSED [ 76%]
+tests/test_search.py::TestSearch::test_search_db PASSED [ 76%]
+tests/test_search.py::TestSearch::test_search_transcript_two_sw_or PASSED [ 77%]
+tests/test_search.py::TestSearch::test_search_plant PASSED [ 77%]
+tests/test_search.py::TestSearch::test_search_gene_two_sw_and PASSED [ 77%]
+tests/test_search.py::TestSearch::test_search_transcript_one_sw PASSED [ 77%]
+tests/test_search.py::TestSearch::test_search_octopus PASSED [ 77%]
+tests/test_search.py::TestSearch::test_search_release PASSED [ 78%]
+tests/test_search.py::TestSearch::test_search_gene_bad_andor PASSED [ 78%]
+tests/test_search.py::TestSearch::test_search_gene_two_sw_or PASSED [ 78%]
+tests/test_search.py::TestSearch::test_search_transcript_bad_species PASSED [ 78%]
+tests/test_search.py::TestSearch::test_search_transcript_bad_andor PASSED [ 79%]
+tests/test_search.py::TestSearch::test_search_gene_one_sw PASSED [ 79%]
+tests/test_search.py::TestSearch::test_search_transcript_two_sw_and PASSED [ 79%]
+tests/test_search.py::TestSearch::test_search_gene_two_sw_limit PASSED [ 79%]
+tests/test_search.py::TestSearch::test_search_gene_one_sw_limit PASSED [ 80%]
+tests/test_search.py::TestSearch::test_search_plant_db PASSED [ 80%]
+tests/test_search.py::TestSearch::test_search_gene_bad_species PASSED [ 80%]
+tests/test_search.py::TestSearch::test_search_gene_one_sw_json PASSED [ 80%]
+tests/test_bgee.py::TestBgee::test_error_bgee_expression_no_exist PASSED [ 81%]
+tests/test_bgee.py::TestBgee::test_bgee_orthologs PASSED [ 81%]
+tests/test_bgee.py::TestBgee::test_error_bgee_unknown_type PASSED [ 81%]
+tests/test_bgee.py::TestBgee::test_bgee_expression PASSED [ 81%]
+tests/test_bgee.py::TestBgee::test_bgee_expression_multiple PASSED [ 81%]
+tests/test_bgee.py::TestBgee::test_error_bgee_orthologs_no_exist PASSED [ 82%]
+tests/test_g2p.py::TestG2PValidation::test_alignment_requires_explicit_uniprot PASSED [ 82%]
+tests/test_g2p.py::TestG2PValidation::test_residues_on_map_raises PASSED [ 82%]
+tests/test_g2p.py::TestG2PValidation::test_invalid_resource_raises PASSED [ 82%]
+tests/test_g2p.py::TestG2PValidation::test_residues_wrong_type_raises PASSED [ 83%]
+tests/test_g2p.py::TestG2PValidation::test_alignment_requires_isoform PASSED [ 83%]
+tests/test_g2p.py::TestG2PValidation::test_missing_both_raises PASSED [ 83%]
+tests/test_g2p.py::TestG2P::test_g2p_features PASSED [ 83%]
+tests/test_g2p.py::TestG2P::test_g2p_residues_filter PASSED [ 84%]
+tests/test_g2p.py::TestG2P::test_g2p_alignment PASSED [ 84%]
+tests/test_g2p.py::TestG2P::test_g2p_out_writes_to_explicit_path PASSED [ 84%]
+tests/test_g2p.py::TestG2P::test_g2p_map_has_pdb_list_column PASSED [ 84%]
+tests/test_g2p.py::TestG2P::test_g2p_residues_single_int PASSED [ 85%]
+tests/test_g2p.py::TestG2P::test_g2p_map PASSED [ 85%]
+tests/test_g2p.py::TestG2P::test_g2p_uniprot_auto_resolved PASSED [ 85%]
+tests/test_g2p.py::TestG2P::test_g2p_schema_invariant_across_input_modes PASSED [ 85%]
+tests/test_g2p.py::TestG2P::test_g2p_invalid_pair_returns_none PASSED [ 86%]
+tests/test_g2p.py::TestG2P::test_g2p_gene_auto_resolved PASSED [ 86%]
+tests/test_compile.py::TestCompilerWindows::test_compiler_windows PASSED [ 86%]
+tests/test_opentargets.py::TestOpenTargetsExpressionMocked::test_expression_empty_is_graceful PASSED [ 86%]
+tests/test_opentargets.py::TestOpenTargetsExpressionMocked::test_expression_json PASSED [ 86%]
+tests/test_opentargets.py::TestOpenTargetsExpressionMocked::test_expression_parses_baseline_rows PASSED [ 87%]
+tests/test_opentargets.py::TestOpenTargetsExpressionMocked::test_expression_limit PASSED [ 87%]
+tests/test_opentargets.py::TestOpenTargets::test_opentargets_drugs PASSED [ 87%]
+tests/test_opentargets.py::TestOpenTargets::test_opentargets PASSED [ 87%]
+tests/test_opentargets.py::TestOpenTargets::test_opentargets_interactions PASSED [ 88%]
+tests/test_opentargets.py::TestOpenTargets::test_error_opentargets_nonexistent_id PASSED [ 88%]
+tests/test_opentargets.py::TestOpenTargets::test_opentargets_tractability PASSED [ 88%]
+tests/test_opentargets.py::TestOpenTargets::test_error_opentargets_bad_resource PASSED [ 88%]
+tests/test_opentargets.py::TestOpenTargets::test_error_opentargets_drugs_invalid_filter PASSED [ 89%]
+tests/test_opentargets.py::TestOpenTargets::test_opentargets_pharmacogenetics PASSED [ 89%]
+tests/test_opentargets.py::TestOpenTargets::test_opentargets_interactions_simple_filter PASSED [ 89%]
+tests/test_opentargets.py::TestOpenTargets::test_opentargets_interactions_no_limit PASSED [ 89%]
+tests/test_opentargets.py::TestOpenTargets::test_opentargets_depmap PASSED [ 90%]
+tests/test_opentargets.py::TestOpenTargets::test_opentargets_diseases PASSED [ 90%]
+tests/test_opentargets.py::TestOpenTargets::test_opentargets_depmap_filter PASSED [ 90%]
+tests/test_opentargets.py::TestOpenTargets::test_error_opentargets_bad_limit PASSED [ 90%]
+tests/test_opentargets.py::TestOpenTargets::test_error_opentargets_diseases_filter PASSED [ 90%]
+tests/test_elm.py::TestELM::test_elm_uniprot_aminoacidseq PASSED [ 91%]
+tests/test_elm.py::TestELM::test_elm_uniprot_id_new PASSED [ 91%]
+tests/test_elm.py::TestELM::test_elm_uniprot_id_in_elm PASSED [ 91%]
+tests/test_blast.py::TestBlast::test_blast_nt PASSED [ 91%]
+tests/test_blast.py::TestBlast::test_blast_bad_seq PASSED [ 92%]
+tests/test_blast.py::TestBlast::test_blast_db_missing PASSED [ 92%]
+tests/test_blast.py::TestBlast::test_blast_bad_db1 PASSED [ 92%]
+tests/test_blast.py::TestBlast::test_blast_bad_program PASSED [ 92%]
+tests/test_blast.py::TestBlast::test_blast_bad_fasta PASSED [ 93%]
+tests/test_blast.py::TestBlast::test_blast_bad_db2 PASSED [ 93%]
+tests/test_ref.py::TestRef::test_ref_rel_protist PASSED [ 93%]
+tests/test_ref.py::TestRef::test_ref_which PASSED [ 93%]
+tests/test_ref.py::TestRef::test_ref_bad_which PASSED [ 94%]
+tests/test_ref.py::TestRef::test_ref_rel_ftp_octopus PASSED [ 94%]
+tests/test_ref.py::TestRef::test_ref PASSED [ 94%]
+tests/test_ref.py::TestRef::test_ref_ftp PASSED [ 94%]
+tests/test_ref.py::TestRef::test_ref_plant PASSED [ 95%]
+tests/test_ref.py::TestRef::test_ref_bad_species PASSED [ 95%]
+tests/test_ref.py::TestRef::test_ref_bad_rel PASSED [ 95%]
+tests/test_ref.py::TestRef::test_ref_rel_ftp PASSED [ 95%]
+tests/test_ref.py::TestRef::test_ref_rel PASSED [ 95%]
+tests/test_ref.py::TestRef::test_ref_which_plant PASSED [ 96%]
+tests/test_ref.py::TestRef::test_ref_list PASSED [ 96%]
+tests/test_utils.py::TestUtils::test_n_colors PASSED [ 96%]
+tests/test_utils.py::TestUtils::test_find_latest_ens_rel PASSED [ 96%]
+tests/test_utils.py::TestUtils::test_search_species_options_bad_type PASSED [ 97%]
+tests/test_utils.py::TestUtils::test_search_species_options PASSED [ 97%]
+tests/test_utils.py::TestUtils::test_ref_species_options PASSED [ 97%]
+tests/test_utils.py::TestUtils::test_read_fasta PASSED [ 97%]
+tests/test_utils.py::TestUtils::test_rest_query_bad_type PASSED [ 98%]
+tests/test_utils.py::TestUtils::test_get_uniprot_seqs PASSED [ 98%]
+tests/test_utils.py::TestUtils::test_get_uniprot_info_gene PASSED [ 98%]
+tests/test_utils.py::TestUtils::test_ref_species_options_bad_type PASSED [ 98%]
+tests/test_utils.py::TestUtils::test_search_iv_species_options PASSED [ 99%]
+tests/test_utils.py::TestUtils::test_ref_iv_species_options PASSED [ 99%]
+tests/test_utils.py::TestUtils::test_get_uniprot_info_transcript PASSED [ 99%]
+tests/test_utils.py::TestUtils::test_aa_colors PASSED [ 99%]
+tests/test_utils.py::TestUtils::test_rest_query PASSED [100%]
-tests/from_json.py:145: AssertionError
------------------------------- Captured log call -------------------------------
-INFO gget.utils:gget_opentargets.py:322 Querying OpenTargets for expression associated with ENSG00000169194...
-INFO gget.utils:gget_opentargets.py:361 No expression data found for ENSG00000169194.
-____________________ TestOpenTargets.test_opentargets_drugs ____________________
-
-self =
-
- def assert_equal_json_with_keys(self: unittest.TestCase):
- def normalize(x):
- if isinstance(x, tuple):
- x = [normalize(v) for v in x]
-
- if isinstance(x, list):
- x = [normalize(v) for v in x]
-
- # Detect list-of-pairs -> dict recursively.
- if all(isinstance(i, list) and len(i) == 2 for i in x):
- try:
- x = {i[0]: i[1] for i in x}
- except Exception: # noqa: BLE001
- try:
- x = {i[1]: i[0] for i in x}
- except Exception: # noqa: BLE001
- pass
-
- # Collapse singleton wrappers such as:
- # [{"drug": {...}}] -> {"drug": {...}}
- # ["26387812"] -> "26387812"
- if isinstance(x, list) and len(x) == 1:
- return normalize(x[0])
-
- if isinstance(x, dict):
- x = {k: normalize(v) for k, v in x.items()}
-
- # Collapse single-key wrapper dicts such as:
- # {"drug": {...}} -> {...}
- # {"drug": None} -> None
- if len(x) == 1:
- return normalize(next(iter(x.values())))
-
- return x
-
- return x
-
- test = name
-
- expected_result = td[test]["expected_result"]
-> result_to_test = do_call(func, td[test]["args"])
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-tests/from_json.py:222:
-_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
-tests/from_json.py:60: in do_call
- return func(**args)
- ^^^^^^^^^^^^
-gget/gget_opentargets.py:325: in opentargets
- api_response = http_json(
-_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
-
-method = 'POST', url = 'https://api.platform.opentargets.org/api/v4/graphql'
-context = 'OpenTargets GraphQL', timeout = (10, 60), retries = 3, backoff = 1.0
-kwargs = {'json': {'query': '\nquery target($ensemblId: String!) {\n target(ensemblId: $ensemblId) {\n drugAndClinicalCandi... }\n }\n }\n }\n }\n }\n }\n}\n', 'variables': {'ensemblId': 'ENSG00000169194'}}}
-
- def http_json(
- method: str,
- url: str,
- *,
- context: str = "",
- timeout: float = DEFAULT_REQUESTS_TIMEOUT,
- retries: int = 3,
- backoff: float = 1.0,
- **kwargs: Any,
- ) -> Any:
- """Issue an HTTP request and return the parsed JSON body, raising a
-
- RuntimeError with consistent context if the request fails or the body
- is not valid JSON.
-
- `context` is a short human-readable label (e.g. "Bgee API") used in
- error messages so users can identify which upstream service failed.
- Transient failures (connection errors, read timeouts, HTTP 5xx) are
- retried up to `retries` additional times with exponential backoff
- (`backoff * 2**i` seconds between attempts). Set `retries=0` to disable.
- All other keyword arguments are forwarded to `requests.request`.
- """
- label = context or url
- last_exc = None
- last_status = None
- last_body = ""
- attempts = retries + 1
- for attempt in range(attempts):
- try:
- response = requests.request(method, url, timeout=timeout, **kwargs)
- except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
- last_exc = e
- last_status = None
- last_body = ""
- else:
- if response.ok:
- try:
- return response.json()
- except json.JSONDecodeError as e:
- body = response.text[:200] if response.text else ""
- raise RuntimeError(
- f"{label} returned non-JSON response (HTTP {response.status_code}): {body}"
- ) from e
- # Retry on transient server errors only; client errors are returned
- # to the caller without retry.
- if response.status_code < 500:
- body = response.text[:200] if response.text else ""
-> raise RuntimeError(f"{label} returned HTTP {response.status_code}. Body: {body}")
-E RuntimeError: OpenTargets GraphQL returned HTTP 400. Body: {"errors":[{"message":"Field 'synonyms' of type '[DrugLabelAndSource!]!' must have a sub selection. (line 16, column 11):\n synonyms\n ^","locations":[{"line":16,"column":11}]},{"mes
-
-gget/utils.py:148: RuntimeError
------------------------------- Captured log call -------------------------------
-INFO gget.utils:gget_opentargets.py:322 Querying OpenTargets for drugs associated with ENSG00000169194...
-________________ TestOpenTargets.test_opentargets_depmap_filter ________________
-
-self =
-
- def assert_equal(self: unittest.TestCase):
- test = name
- expected_result = td[test]["expected_result"]
- result_to_test = do_call(func, td[test]["args"])
- if test == "test_cosmic_defaults": # special case for cosmic
- import numpy as np
-
- expected_result = pd.DataFrame(expected_result[0])
- expected_result = expected_result.replace({None: np.nan})
- # result_to_test.equals(expected_result)
- pd.testing.assert_frame_equal(result_to_test, expected_result, check_dtype=False)
- return
-
- # If result is a DataFrame, convert to list
- if isinstance(result_to_test, pd.DataFrame):
- result_to_test = result_to_test.dropna(axis=1).values.tolist()
-
-> self.assertEqual(result_to_test, expected_result)
-E AssertionError: Lists differ: [] != [['UBERON_0002367', 'prostate gland', 'DU [209 chars]254]]
-E
-E Second list contains 2 additional elements.
-E First extra element 0:
-E ['UBERON_0002367', 'prostate gland', 'DU 145', 0.034343916922807693, 'Prostate Adenocarcinoma', 'ACH-000979', -0.14336788654327393]
-E
-E - []
-E + [['UBERON_0002367',
-E + 'prostate gland',
-E + 'DU 145',
-E + 0.034343916922807693,
-E + 'Prostate Adenocarcinoma',
-E + 'ACH-000979',
-E + -0.14336788654327393],
-E + ['UBERON_0002367',
-E + 'prostate gland',
-E + 'WPE1-NA22',
-E + 0.0291899424046278,
-E + 'Non-Cancerous',
-E + 'ACH-001422',
-E + 0.06934770196676254]]
-
-tests/from_json.py:82: AssertionError
------------------------------- Captured log call -------------------------------
-INFO gget.utils:gget_opentargets.py:322 Querying OpenTargets for depmap associated with ENSG00000169194...
-_________________ TestOpenTargets.test_opentargets_expression __________________
-
-self =
-
- def assert_equal_json_with_keys(self: unittest.TestCase):
- def normalize(x):
- if isinstance(x, tuple):
- x = [normalize(v) for v in x]
-
- if isinstance(x, list):
- x = [normalize(v) for v in x]
-
- # Detect list-of-pairs -> dict recursively.
- if all(isinstance(i, list) and len(i) == 2 for i in x):
- try:
- x = {i[0]: i[1] for i in x}
- except Exception: # noqa: BLE001
- try:
- x = {i[1]: i[0] for i in x}
- except Exception: # noqa: BLE001
- pass
-
- # Collapse singleton wrappers such as:
- # [{"drug": {...}}] -> {"drug": {...}}
- # ["26387812"] -> "26387812"
- if isinstance(x, list) and len(x) == 1:
- return normalize(x[0])
-
- if isinstance(x, dict):
- x = {k: normalize(v) for k, v in x.items()}
-
- # Collapse single-key wrapper dicts such as:
- # {"drug": {...}} -> {...}
- # {"drug": None} -> None
- if len(x) == 1:
- return normalize(next(iter(x.values())))
-
- return x
-
- return x
-
- test = name
-
- expected_result = td[test]["expected_result"]
- result_to_test = do_call(func, td[test]["args"])
- if isinstance(result_to_test, pd.DataFrame):
- result_to_test = json.loads(
- result_to_test.dropna(axis=1, how="all").to_json(orient="records", force_ascii=False)
- )
-
- result_to_test = normalize(result_to_test)
- expected_result = normalize(expected_result)
-
- # ✅ Infer keys from expected_result
- if not expected_result:
- raise ValueError(f"Test {test} has empty expected_result")
-
- keys = list(expected_result[0].keys())
-
- # Convert list-of-lists → list-of-dicts
- if isinstance(result_to_test, list) and len(result_to_test) > 0 and isinstance(result_to_test[0], list):
- result_to_test = [dict(zip(keys, row, strict=False)) for row in result_to_test]
-
- # Optional: float rounding
- def round_dict(d, ndigits=10):
- return {k: round(v, ndigits) if isinstance(v, float) else v for k, v in d.items()}
-
- result_to_test = [round_dict(r) for r in result_to_test]
- expected_result = [round_dict(r) for r in expected_result]
-
-> self.assertEqual(result_to_test, expected_result)
-E AssertionError: Lists differ: [] != [{'tissue.id': 'UBERON_0002367', 'tissue.l[382 chars] -1}]
-E
-E Second list contains 2 additional elements.
-E First extra element 0:
-E {'tissue.id': 'UBERON_0002367', 'tissue.label': 'prostate gland', 'tissue.anatomicalSystems': 'reproductive system', 'tissue.organs': 'reproductive structure', 'rna.zscore': -1, 'rna.value': 4.0, 'rna.unit': '', 'rna.level': -1}
-E
-E - []
-E + [{'rna.level': -1,
-E + 'rna.unit': '',
-E + 'rna.value': 4.0,
-E + 'rna.zscore': -1,
-E + 'tissue.anatomicalSystems': 'reproductive system',
-E + 'tissue.id': 'UBERON_0002367',
-E + 'tissue.label': 'prostate gland',
-E + 'tissue.organs': 'reproductive structure'},
-E + {'rna.level': -1,
-E + 'rna.unit': '',
-E + 'rna.value': 0.0,
-E + 'rna.zscore': -1,
-E + 'tissue.anatomicalSystems': 'renal system',
-E + 'tissue.id': 'UBERON_0002113',
-E + 'tissue.label': 'kidney',
-E + 'tissue.organs': 'kidney'}]
-
-tests/from_json.py:248: AssertionError
------------------------------- Captured log call -------------------------------
-INFO gget.utils:gget_opentargets.py:322 Querying OpenTargets for expression associated with ENSG00000169194...
-INFO gget.utils:gget_opentargets.py:361 No expression data found for ENSG00000169194.
-______________ TestOpenTargets.test_opentargets_pharmacogenetics _______________
-
-self =
-
- def assert_equal_json_with_keys(self: unittest.TestCase):
- def normalize(x):
- if isinstance(x, tuple):
- x = [normalize(v) for v in x]
-
- if isinstance(x, list):
- x = [normalize(v) for v in x]
-
- # Detect list-of-pairs -> dict recursively.
- if all(isinstance(i, list) and len(i) == 2 for i in x):
- try:
- x = {i[0]: i[1] for i in x}
- except Exception: # noqa: BLE001
- try:
- x = {i[1]: i[0] for i in x}
- except Exception: # noqa: BLE001
- pass
-
- # Collapse singleton wrappers such as:
- # [{"drug": {...}}] -> {"drug": {...}}
- # ["26387812"] -> "26387812"
- if isinstance(x, list) and len(x) == 1:
- return normalize(x[0])
-
- if isinstance(x, dict):
- x = {k: normalize(v) for k, v in x.items()}
-
- # Collapse single-key wrapper dicts such as:
- # {"drug": {...}} -> {...}
- # {"drug": None} -> None
- if len(x) == 1:
- return normalize(next(iter(x.values())))
-
- return x
-
- return x
-
- test = name
-
- expected_result = td[test]["expected_result"]
- result_to_test = do_call(func, td[test]["args"])
- if isinstance(result_to_test, pd.DataFrame):
- result_to_test = json.loads(
- result_to_test.dropna(axis=1, how="all").to_json(orient="records", force_ascii=False)
- )
-
- result_to_test = normalize(result_to_test)
- expected_result = normalize(expected_result)
-
- # ✅ Infer keys from expected_result
- if not expected_result:
- raise ValueError(f"Test {test} has empty expected_result")
-
- keys = list(expected_result[0].keys())
-
- # Convert list-of-lists → list-of-dicts
- if isinstance(result_to_test, list) and len(result_to_test) > 0 and isinstance(result_to_test[0], list):
- result_to_test = [dict(zip(keys, row, strict=False)) for row in result_to_test]
-
- # Optional: float rounding
- def round_dict(d, ndigits=10):
- return {k: round(v, ndigits) if isinstance(v, float) else v for k, v in d.items()}
-
- result_to_test = [round_dict(r) for r in result_to_test]
- expected_result = [round_dict(r) for r in expected_result]
-
-> self.assertEqual(result_to_test, expected_result)
-E AssertionError: Lists differ: [{'va[867 chars]_T_C,T', 'genotype': 'CT', 'drugs': None, 'phe[687 chars]nt'}] != [{'va[867 chars]_T_C,C', 'genotype': 'CC', 'drugs': None, 'phe[593 chars]nt'}]
-E
-E First differing element 1:
-E {'var[54 chars]_T_C,T', 'genotype': 'CT', 'drugs': None, 'phe[686 chars]ant'}
-E {'var[54 chars]_T_C,C', 'genotype': 'CC', 'drugs': None, 'phe[592 chars]ant'}
-E
-E [{'datasourceId': 'clinpgx',
-E 'drugs': {'id': 'CHEMBL535', 'name': 'SUNITINIB'},
-E 'evidenceLevel': '3',
-E 'genotype': 'CT',
-E 'genotypeAnnotationText': 'Patients with renal cell carcinoma and the CT '
-E 'genotype may have a decreased severity of '
-E 'drug-induced toxicity when administered sunitinib '
-E 'as compared to patients with the TT genotype. '
-E 'Other clinical and genetic factors may also '
-E 'influence severity of drug-induced toxicity in '
-E 'patients with renal cell carcinoma who are '
-E 'administered sunitinib.',
-E 'genotypeId': '5_132657117_C_C,T',
-E 'isDirectTarget': False,
-E 'literature': '26387812',
-E 'pgxCategory': 'toxicity',
-E 'phenotypeText': 'decreased severity of drug-induced toxicity',
-E 'variantFunctionalConsequence.id': 'SO:0001631',
-E 'variantFunctionalConsequence.label': 'upstream_gene_variant',
-E 'variantId': '5_132657117_C_T'},
-E {'datasourceId': 'clinpgx',
-E 'drugs': None,
-E 'evidenceLevel': '3',
-E - 'genotype': 'CT',
-E ? ^
-E
-E + 'genotype': 'CC',
-E ? ^
-E
-E - 'genotypeAnnotationText': 'Patients with the CT genotype may be at decreased '
-E ? ^
-E
-E + 'genotypeAnnotationText': 'Patients with the CC genotype may be at decreased '
-E ? ^
-E
-E 'risk for non-immune response to the hepatitis B '
-E 'vaccine, as compared to patients with the TT '
-E + 'genotype. Other genetic and clinical factors may '
-E + 'also influence risk of non-immune response in '
-E - 'genotype, or at increased risk as compared to '
-E - 'patients with the CC genotype. Other genetic and '
-E - 'clinical factors may also influence risk of '
-E - 'non-immune response in patients receiving the '
-E - 'hepatitis B vaccine.',
-E + 'patients receiving the hepatitis B vaccine.',
-E ? +++++++++++++++++++++++
-E
-E - 'genotypeId': '5_132660151_T_C,T',
-E ? ^
-E
-E + 'genotypeId': '5_132660151_T_C,C',
-E ? ^
-E
-E 'isDirectTarget': False,
-E 'literature': '21111021',
-E 'pgxCategory': 'efficacy',
-E - 'phenotypeText': 'decreased risk for non-immune response to the hepatitis B '
-E ? --------------------
-E
-E + 'phenotypeText': 'decreased risk for non-immune response',
-E ? +
-E
-E - 'vaccine',
-E 'variantFunctionalConsequence.id': 'SO:0001627',
-E 'variantFunctionalConsequence.label': 'intron_variant',
-E 'variantId': '5_132660151_T_C'}]
-
-tests/from_json.py:248: AssertionError
------------------------------- Captured log call -------------------------------
-INFO gget.utils:gget_opentargets.py:322 Querying OpenTargets for pharmacogenetics associated with ENSG00000169194...
-=============================== warnings summary ===============================
-tests/test_search.py::TestSearch::test_search_plant
-tests/test_search.py::TestSearch::test_search_release
-tests/test_search.py::TestSearch::test_search_transcript_two_sw_or
tests/test_utils.py::TestUtils::test_search_species_options
/home/runner/work/gget/gget/gget/utils.py:899: DeprecationWarning: Call to deprecated method findAll. (Replaced by find_all) -- Deprecated since version 4.0.0.
for subsoup in soup.body.findAll("a"):
-tests/test_search.py::TestSearch::test_search_plant
-tests/test_search.py::TestSearch::test_search_plant
-tests/test_search.py::TestSearch::test_search_plant
-tests/test_search.py::TestSearch::test_search_plant
+tests/test_search.py::TestSearch::test_search_transcript_two_sw_or
+tests/test_search.py::TestSearch::test_search_transcript_two_sw_or
+tests/test_search.py::TestSearch::test_search_transcript_two_sw_or
+tests/test_search.py::TestSearch::test_search_transcript_two_sw_or
tests/test_utils.py::TestUtils::test_search_iv_species_options
tests/test_utils.py::TestUtils::test_search_iv_species_options
tests/test_utils.py::TestUtils::test_search_iv_species_options
@@ -1266,65 +448,27 @@ tests/test_utils.py::TestUtils::test_search_iv_species_options
/home/runner/work/gget/gget/gget/utils.py:883: DeprecationWarning: Call to deprecated method findAll. (Replaced by find_all) -- Deprecated since version 4.0.0.
for subsoup in soup.body.findAll("a"):
-tests/test_search.py: 10 warnings
- /home/runner/work/gget/gget/gget/gget_search.py:235: UserWarning: pandas only supports SQLAlchemy connectable (engine/connection) or database string URI or sqlite3 DBAPI2 connection. Other DBAPI2 objects are not tested. Please consider using SQLAlchemy.
- df_temp = pd.read_sql(query, con=db_connection)
-
-tests/test_search.py: 19 warnings
-tests/test_ref.py: 7 warnings
- /home/runner/work/gget/gget/gget/utils.py:922: DeprecationWarning: Call to deprecated method findAll. (Replaced by find_all) -- Deprecated since version 4.0.0.
- for subsoup in soup.body.findAll("a"):
-
-tests/test_search.py::TestSearch::test_search_gene_two_sw_limit
-tests/test_search.py::TestSearch::test_search_transcript_two_sw_and
tests/test_search.py::TestSearch::test_search_transcript_two_sw_or
tests/test_search.py::TestSearch::test_search_transcript_one_sw
- /home/runner/work/gget/gget/gget/gget_search.py:270: UserWarning: pandas only supports SQLAlchemy connectable (engine/connection) or database string URI or sqlite3 DBAPI2 connection. Other DBAPI2 objects are not tested. Please consider using SQLAlchemy.
+tests/test_search.py::TestSearch::test_search_transcript_two_sw_and
+tests/test_search.py::TestSearch::test_search_gene_two_sw_limit
+ /home/runner/work/gget/gget/gget/gget_search.py:302: UserWarning: pandas only supports SQLAlchemy connectable (engine/connection) or database string URI or sqlite3 DBAPI2 connection. Other DBAPI2 objects are not tested. Please consider using SQLAlchemy.
df_temp = pd.read_sql(query, con=db_connection)
-tests/test_utils.py: 1 warning
tests/test_ref.py: 11 warnings
+tests/test_utils.py: 1 warning
/home/runner/work/gget/gget/gget/utils.py:998: DeprecationWarning: Call to deprecated method findAll. (Replaced by find_all) -- Deprecated since version 4.0.0.
for subsoup in soup.body.findAll("a"):
-tests/test_utils.py: 4 warnings
tests/test_ref.py: 24 warnings
+tests/test_utils.py: 4 warnings
/home/runner/work/gget/gget/gget/utils.py:975: DeprecationWarning: Call to deprecated method findAll. (Replaced by find_all) -- Deprecated since version 4.0.0.
for subsoup in soup.body.findAll("a"):
-tests/test_mutate.py: 29 warnings
- /home/runner/work/gget/gget/gget/gget_mutate.py:667: UserWarning: This pattern is interpreted as a regular expression, and has match groups. To actually get the groups, use str.extract.
- mask = mutations[mut_column].str.contains(combined_pattern)
-
-tests/test_cbio.py::TestCbioSearch::test_cbio_search
-tests/test_cbio.py::TestCbioSearch::test_cbio_search
- /home/runner/.local/share/hatch/env/virtual/gget/dY-b3nRT/hatch-test.py3.14/lib/python3.14/site-packages/swagger_spec_validator/validator12.py:18: DeprecationWarning: jsonschema.RefResolver is deprecated as of v4.18.0, in favor of the https://github.com/python-jsonschema/referencing library, which provides more compliant referencing behavior as well as more flexible APIs for customization. A future release will remove RefResolver. Please file a feature request (on referencing) if you are missing an API for the kind of customization you need.
- from jsonschema import RefResolver
-
-tests/test_cbio.py::TestCbioSearch::test_cbio_search
- /home/runner/.local/share/hatch/env/virtual/gget/dY-b3nRT/hatch-test.py3.14/lib/python3.14/site-packages/swagger_spec_validator/ref_validators.py:14: DeprecationWarning: jsonschema.RefResolver is deprecated as of v4.18.0, in favor of the https://github.com/python-jsonschema/referencing library, which provides more compliant referencing behavior as well as more flexible APIs for customization. A future release will remove RefResolver. Please file a feature request (on referencing) if you are missing an API for the kind of customization you need.
- from jsonschema.validators import RefResolver
-
-tests/test_cbio.py::TestCbioSearch::test_cbio_search
- /home/runner/.local/share/hatch/env/virtual/gget/dY-b3nRT/hatch-test.py3.14/lib/python3.14/site-packages/swagger_spec_validator/validator20.py:18: DeprecationWarning: jsonschema.RefResolver is deprecated as of v4.18.0, in favor of the https://github.com/python-jsonschema/referencing library, which provides more compliant referencing behavior as well as more flexible APIs for customization. A future release will remove RefResolver. Please file a feature request (on referencing) if you are missing an API for the kind of customization you need.
- from jsonschema.validators import RefResolver
-
-tests/test_cbio.py::TestCbioSearch::test_cbio_search
-tests/test_cbio.py::TestCbioSearch::test_cbio_search
- /home/runner/.local/share/hatch/env/virtual/gget/dY-b3nRT/hatch-test.py3.14/lib/python3.14/site-packages/bravado_core/swagger20_validator.py:6: DeprecationWarning: jsonschema.RefResolver is deprecated as of v4.18.0, in favor of the https://github.com/python-jsonschema/referencing library, which provides more compliant referencing behavior as well as more flexible APIs for customization. A future release will remove RefResolver. Please file a feature request (on referencing) if you are missing an API for the kind of customization you need.
- from jsonschema import RefResolver
-
-tests/test_cbio.py::TestCbioSearch::test_cbio_search
- /home/runner/.local/share/hatch/env/virtual/gget/dY-b3nRT/hatch-test.py3.14/lib/python3.14/site-packages/bravado_core/spec.py:14: DeprecationWarning: jsonschema.RefResolver is deprecated as of v4.18.0, in favor of the https://github.com/python-jsonschema/referencing library, which provides more compliant referencing behavior as well as more flexible APIs for customization. A future release will remove RefResolver. Please file a feature request (on referencing) if you are missing an API for the kind of customization you need.
- from jsonschema.validators import RefResolver
-
-tests/test_cbio.py: 16 warnings
- /home/runner/.local/share/hatch/env/virtual/gget/dY-b3nRT/hatch-test.py3.14/lib/python3.14/site-packages/bravado_core/model.py:888: DeprecationWarning: jsonschema.RefResolver.in_scope is deprecated and will be removed in a future release.
- with spec.resolver.in_scope(additional_uri):
-
-tests/test_ref.py: 39 warnings
- /home/runner/work/gget/gget/gget/gget_ref.py:49: DeprecationWarning: Call to deprecated method findAll. (Replaced by find_all) -- Deprecated since version 4.0.0.
- links = [stuff.text.strip() for stuff in soup.findAll("td")]
+tests/test_utils.py: 1 warning
+tests/test_ref.py: 11 warnings
+ /home/runner/work/gget/gget/gget/utils.py:998: DeprecationWarning: Call to deprecated method findAll. (Replaced by find_all) -- Deprecated since version 4.0.0.
+ for subsoup in soup.body.findAll("a"):
tests/test_ref.py::TestRef::test_ref
tests/test_ref.py::TestRef::test_ref_ftp
@@ -1332,258 +476,3 @@ tests/test_ref.py::TestRef::test_ref_ftp
links = [stuff.text.strip() for stuff in soup.findAll("td")]
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
-============================= slowest 10 durations =============================
-231.41s call tests/test_virus.py::TestVirus::test_virus_integer_virus_id
-178.16s call tests/test_virus.py::TestVirus::test_virus_with_source_database_filter
-177.85s call tests/test_virus.py::TestVirus::test_virus_with_max_ambiguous_chars_filter
-132.20s call tests/test_virus.py::TestVirus::test_virus_with_lab_passaged_filter
-97.59s call tests/test_blast.py::TestBlast::test_blast_nt
-76.13s call tests/test_virus.py::TestVirus::test_virus_with_protein_count_filters
-72.80s call tests/test_virus.py::TestVirus::test_virus_with_has_proteins_filter
-32.42s call tests/test_search.py::TestSearch::test_search_gene_two_sw_limit
-27.97s call tests/test_virus.py::TestVirus::test_virus_with_annotated_filter
-22.40s call tests/test_virus.py::TestVirus::test_virus_with_length_filters
-=========================== short test summary info ============================
-SKIPPED [1] tests/test_cellxgene.py:65: cellxgene-census is not installed
-SKIPPED [1] tests/test_cellxgene.py:55: cellxgene-census is not installed
-SKIPPED [1] tests/test_cellxgene.py:45: cellxgene-census is not installed
-FAILED tests/test_opentargets.py::TestOpenTargets::test_opentargets_diseases - AssertionError: Lists differ: [{'score': 0.7279798022, 'disease.id': 'MONDO_0004980', [815 chars]y.'}] != [{'score': 0.7297489019, 'disease.id': 'EFO_0000274', 'd[813 chars]y.'}]
-
-First differing element 0:
-{'score': 0.7279798022, 'disease.id': 'MONDO_0004980', [195 chars]fe.'}
-{'score': 0.7297489019, 'disease.id': 'EFO_0000274', 'd[193 chars]fe.'}
-
- [{'disease.description': 'A common chronic pruritic inflammatory skin disease '
- 'with a strong genetic component. Onset typically '
- 'occurs during the first 2 years of life.',
-- 'disease.id': 'MONDO_0004980',
-? ^^^^ ---
-
-+ 'disease.id': 'EFO_0000274',
-? ^^ +++
-
- 'disease.name': 'atopic eczema',
-- 'score': 0.7279798022},
-? - - ^^
-
-+ 'score': 0.7297489019},
-? ++ ^^
-
- {'disease.description': 'A bronchial disease that is characterized by chronic '
- 'inflammation and narrowing of the airways, which is '
- 'caused by a combination of environmental and genetic '
- 'factors resulting in recurring periods of wheezing '
- '(a whistling sound while breathing), chest '
- 'tightness, shortness of breath, mucus production and '
- 'coughing. The symptoms appear due to a variety of '
- 'triggers such as allergens, irritants, respiratory '
- 'infections, weather changes, exercise, stress, '
- 'reflux disease, medications, foods and emotional '
- 'anxiety.',
- 'disease.id': 'MONDO_0004979',
- 'disease.name': 'asthma',
-- 'score': 0.6952093612}]
-+ 'score': 0.6642728578}]
-FAILED tests/test_opentargets.py::TestOpenTargets::test_opentargets_interactions_no_limit - AssertionError: '8ed7f7380590628acdbaa5f9501365a6' != 'fa95d278c2d31ded3731e154d65fcda5'
-- 8ed7f7380590628acdbaa5f9501365a6
-+ fa95d278c2d31ded3731e154d65fcda5
-FAILED tests/test_opentargets.py::TestOpenTargets::test_error_opentargets_drugs_invalid_filter - RuntimeError: OpenTargets GraphQL returned HTTP 400. Body: {"errors":[{"message":"Field 'synonyms' of type '[DrugLabelAndSource!]!' must have a sub selection. (line 16, column 11):\n synonyms\n ^","locations":[{"line":16,"column":11}]},{"mes
-FAILED tests/test_opentargets.py::TestOpenTargets::test_opentargets_depmap - AssertionError: 'e81a60ff47c06bfecb3b421d28047718' != 'c335cc9c9b3167e8c5b3084e339c88a7'
-- e81a60ff47c06bfecb3b421d28047718
-+ c335cc9c9b3167e8c5b3084e339c88a7
-FAILED tests/test_opentargets.py::TestOpenTargets::test_opentargets - AssertionError: Lists differ: [[0.7279798021712002, 'MONDO_0004980', 'atopic [712 chars]y.']] != [[0.7297489019498119, 'EFO_0000274', 'atopic ec[710 chars]y.']]
-
-First differing element 0:
-[0.7279798021712002, 'MONDO_0004980', 'atopic [148 chars]fe.']
-[0.7297489019498119, 'EFO_0000274', 'atopic ec[146 chars]fe.']
-
-- [[0.7279798021712002,
-- 'MONDO_0004980',
-+ [[0.7297489019498119,
-+ 'EFO_0000274',
- 'atopic eczema',
- 'A common chronic pruritic inflammatory skin disease with a strong genetic '
- 'component. Onset typically occurs during the first 2 years of life.'],
-- [0.6952093612226559,
-+ [0.6642728577751653,
- 'MONDO_0004979',
- 'asthma',
- 'A bronchial disease that is characterized by chronic inflammation and '
- 'narrowing of the airways, which is caused by a combination of environmental '
- 'and genetic factors resulting in recurring periods of wheezing (a whistling '
- 'sound while breathing), chest tightness, shortness of breath, mucus '
- 'production and coughing. The symptoms appear due to a variety of triggers '
- 'such as allergens, irritants, respiratory infections, weather changes, '
- 'exercise, stress, reflux disease, medications, foods and emotional '
- 'anxiety.']]
-FAILED tests/test_opentargets.py::TestOpenTargets::test_opentargets_interactions - AssertionError: Lists differ: [{'sc[137 chars]000360730', 'intBBiologicalRole': 'unspecified[573 chars]134}] != [{'sc[137 chars]000361004', 'intBBiologicalRole': 'unspecified[573 chars]134}]
-
-First differing element 0:
-{'sco[136 chars]000360730', 'intBBiologicalRole': 'unspecified[189 chars] 134}
-{'sco[136 chars]000361004', 'intBBiologicalRole': 'unspecified[189 chars] 134}
-
- [{'count': 3,
-+ 'intA': 'ENSP00000304915',
-+ 'intABiologicalRole': 'unspecified role',
-+ 'intB': 'ENSP00000361004',
-+ 'intBBiologicalRole': 'unspecified role',
-+ 'score': 0.999,
-+ 'sourceDatabase': 'string',
-+ 'speciesA.taxonId': 134,
-+ 'speciesB.taxonId': 134,
-+ 'targetA.approvedSymbol': 'IL13',
-+ 'targetA.id': 'ENSG00000169194',
-+ 'targetB.approvedSymbol': 'IL13RA2',
-+ 'targetB.id': 'ENSG00000123496'},
-+ {'count': 3,
- 'intA': 'ENSP00000304915',
- 'intABiologicalRole': 'unspecified role',
- 'intB': 'ENSP00000360730',
- 'intBBiologicalRole': 'unspecified role',
- 'score': 0.999,
- 'sourceDatabase': 'string',
- 'speciesA.taxonId': 134,
- 'speciesB.taxonId': 134,
- 'targetA.approvedSymbol': 'IL13',
- 'targetA.id': 'ENSG00000169194',
- 'targetB.approvedSymbol': 'IL13RA1',
-- 'targetB.id': 'ENSG00000131724'},
-? ^
-
-+ 'targetB.id': 'ENSG00000131724'}]
-? ^
-
-- {'count': 3,
-- 'intA': 'ENSP00000304915',
-- 'intABiologicalRole': 'unspecified role',
-- 'intB': 'ENSP00000361004',
-- 'intBBiologicalRole': 'unspecified role',
-- 'score': 0.999,
-- 'sourceDatabase': 'string',
-- 'speciesA.taxonId': 134,
-- 'speciesB.taxonId': 134,
-- 'targetA.approvedSymbol': 'IL13',
-- 'targetA.id': 'ENSG00000169194',
-- 'targetB.approvedSymbol': 'IL13RA2',
-- 'targetB.id': 'ENSG00000123496'}]
-FAILED tests/test_opentargets.py::TestOpenTargets::test_opentargets_expression_no_limit - AssertionError: 'd751713988987e9331980363e24189ce' != '7d32780ec48250553246c816d80b93ee'
-- d751713988987e9331980363e24189ce
-+ 7d32780ec48250553246c816d80b93ee
-FAILED tests/test_opentargets.py::TestOpenTargets::test_opentargets_drugs - RuntimeError: OpenTargets GraphQL returned HTTP 400. Body: {"errors":[{"message":"Field 'synonyms' of type '[DrugLabelAndSource!]!' must have a sub selection. (line 16, column 11):\n synonyms\n ^","locations":[{"line":16,"column":11}]},{"mes
-FAILED tests/test_opentargets.py::TestOpenTargets::test_opentargets_depmap_filter - AssertionError: Lists differ: [] != [['UBERON_0002367', 'prostate gland', 'DU [209 chars]254]]
-
-Second list contains 2 additional elements.
-First extra element 0:
-['UBERON_0002367', 'prostate gland', 'DU 145', 0.034343916922807693, 'Prostate Adenocarcinoma', 'ACH-000979', -0.14336788654327393]
-
-- []
-+ [['UBERON_0002367',
-+ 'prostate gland',
-+ 'DU 145',
-+ 0.034343916922807693,
-+ 'Prostate Adenocarcinoma',
-+ 'ACH-000979',
-+ -0.14336788654327393],
-+ ['UBERON_0002367',
-+ 'prostate gland',
-+ 'WPE1-NA22',
-+ 0.0291899424046278,
-+ 'Non-Cancerous',
-+ 'ACH-001422',
-+ 0.06934770196676254]]
-FAILED tests/test_opentargets.py::TestOpenTargets::test_opentargets_expression - AssertionError: Lists differ: [] != [{'tissue.id': 'UBERON_0002367', 'tissue.l[382 chars] -1}]
-
-Second list contains 2 additional elements.
-First extra element 0:
-{'tissue.id': 'UBERON_0002367', 'tissue.label': 'prostate gland', 'tissue.anatomicalSystems': 'reproductive system', 'tissue.organs': 'reproductive structure', 'rna.zscore': -1, 'rna.value': 4.0, 'rna.unit': '', 'rna.level': -1}
-
-- []
-+ [{'rna.level': -1,
-+ 'rna.unit': '',
-+ 'rna.value': 4.0,
-+ 'rna.zscore': -1,
-+ 'tissue.anatomicalSystems': 'reproductive system',
-+ 'tissue.id': 'UBERON_0002367',
-+ 'tissue.label': 'prostate gland',
-+ 'tissue.organs': 'reproductive structure'},
-+ {'rna.level': -1,
-+ 'rna.unit': '',
-+ 'rna.value': 0.0,
-+ 'rna.zscore': -1,
-+ 'tissue.anatomicalSystems': 'renal system',
-+ 'tissue.id': 'UBERON_0002113',
-+ 'tissue.label': 'kidney',
-+ 'tissue.organs': 'kidney'}]
-FAILED tests/test_opentargets.py::TestOpenTargets::test_opentargets_pharmacogenetics - AssertionError: Lists differ: [{'va[867 chars]_T_C,T', 'genotype': 'CT', 'drugs': None, 'phe[687 chars]nt'}] != [{'va[867 chars]_T_C,C', 'genotype': 'CC', 'drugs': None, 'phe[593 chars]nt'}]
-
-First differing element 1:
-{'var[54 chars]_T_C,T', 'genotype': 'CT', 'drugs': None, 'phe[686 chars]ant'}
-{'var[54 chars]_T_C,C', 'genotype': 'CC', 'drugs': None, 'phe[592 chars]ant'}
-
- [{'datasourceId': 'clinpgx',
- 'drugs': {'id': 'CHEMBL535', 'name': 'SUNITINIB'},
- 'evidenceLevel': '3',
- 'genotype': 'CT',
- 'genotypeAnnotationText': 'Patients with renal cell carcinoma and the CT '
- 'genotype may have a decreased severity of '
- 'drug-induced toxicity when administered sunitinib '
- 'as compared to patients with the TT genotype. '
- 'Other clinical and genetic factors may also '
- 'influence severity of drug-induced toxicity in '
- 'patients with renal cell carcinoma who are '
- 'administered sunitinib.',
- 'genotypeId': '5_132657117_C_C,T',
- 'isDirectTarget': False,
- 'literature': '26387812',
- 'pgxCategory': 'toxicity',
- 'phenotypeText': 'decreased severity of drug-induced toxicity',
- 'variantFunctionalConsequence.id': 'SO:0001631',
- 'variantFunctionalConsequence.label': 'upstream_gene_variant',
- 'variantId': '5_132657117_C_T'},
- {'datasourceId': 'clinpgx',
- 'drugs': None,
- 'evidenceLevel': '3',
-- 'genotype': 'CT',
-? ^
-
-+ 'genotype': 'CC',
-? ^
-
-- 'genotypeAnnotationText': 'Patients with the CT genotype may be at decreased '
-? ^
-
-+ 'genotypeAnnotationText': 'Patients with the CC genotype may be at decreased '
-? ^
-
- 'risk for non-immune response to the hepatitis B '
- 'vaccine, as compared to patients with the TT '
-+ 'genotype. Other genetic and clinical factors may '
-+ 'also influence risk of non-immune response in '
-- 'genotype, or at increased risk as compared to '
-- 'patients with the CC genotype. Other genetic and '
-- 'clinical factors may also influence risk of '
-- 'non-immune response in patients receiving the '
-- 'hepatitis B vaccine.',
-+ 'patients receiving the hepatitis B vaccine.',
-? +++++++++++++++++++++++
-
-- 'genotypeId': '5_132660151_T_C,T',
-? ^
-
-+ 'genotypeId': '5_132660151_T_C,C',
-? ^
-
- 'isDirectTarget': False,
- 'literature': '21111021',
- 'pgxCategory': 'efficacy',
-- 'phenotypeText': 'decreased risk for non-immune response to the hepatitis B '
-? --------------------
-
-+ 'phenotypeText': 'decreased risk for non-immune response',
-? +
-
-- 'vaccine',
- 'variantFunctionalConsequence.id': 'SO:0001627',
- 'variantFunctionalConsequence.label': 'intron_variant',
- 'variantId': '5_132660151_T_C'}]
-===== 11 failed, 386 passed, 3 skipped, 185 warnings in 1459.58s (0:24:19) =====
diff --git a/tests/test_alphafold.py b/tests/test_alphafold.py
new file mode 100644
index 000000000..2c05786ed
--- /dev/null
+++ b/tests/test_alphafold.py
@@ -0,0 +1,79 @@
+import os
+import subprocess
+import sys
+import unittest
+from unittest.mock import patch
+
+import gget.gget_alphafold as gget_alphafold
+from gget.gget_alphafold import clean_up, get_jackhmmer_dir
+from gget.gget_setup import UUID
+
+# AlphaFold requires heavy third-party dependencies (alphafold, openmm, jackhmmer, model
+# parameters) that are not available in the CI/test environment, so a full prediction run
+# cannot be exercised here. These tests validate the user-facing jackhmmer save-directory
+# option (https://github.com/scverse/gget/issues/49) at the argument/path-handling level.
+
+
+class TestAlphafoldJackhmmerSavedir(unittest.TestCase):
+ def test_default_dir(self):
+ """Without a custom save directory, the default ~/tmp/jackhmmer/ is used."""
+ expected = os.path.expanduser(os.path.join("~", "tmp", "jackhmmer", UUID))
+ self.assertEqual(get_jackhmmer_dir(), expected)
+ self.assertEqual(get_jackhmmer_dir(None), expected)
+
+ def test_custom_dir(self):
+ """A custom save directory is honored and returned as an absolute path."""
+ custom = os.path.join("some", "custom", "place")
+ result = get_jackhmmer_dir(custom)
+ expected = os.path.abspath(os.path.join(custom, "jackhmmer", UUID))
+ self.assertEqual(result, expected)
+ self.assertTrue(os.path.isabs(result))
+
+ def test_clean_up_removes_custom_dir(self):
+ """clean_up() deletes leftover FASTA files and the temporary folder it is given."""
+ import tempfile
+
+ with tempfile.TemporaryDirectory() as parent:
+ jackhmmer_dir = get_jackhmmer_dir(parent)
+ os.makedirs(jackhmmer_dir, exist_ok=True)
+ fasta_path = os.path.join(jackhmmer_dir, "target_1.fasta")
+ with open(fasta_path, "w") as f:
+ f.write(">query\nMKV")
+
+ clean_up(jackhmmer_dir)
+
+ self.assertFalse(os.path.exists(fasta_path))
+ self.assertFalse(os.path.isdir(jackhmmer_dir))
+
+ def test_clean_up_missing_dir_is_safe(self):
+ """clean_up() does not raise if the target directory does not exist."""
+ missing = get_jackhmmer_dir(os.path.join("definitely", "not", "there"))
+ # Should be a no-op rather than raising.
+ clean_up(missing)
+
+ def test_clean_up_default_dir_when_none(self):
+ """clean_up(None) resolves the default directory via get_jackhmmer_dir()."""
+ import tempfile
+
+ with tempfile.TemporaryDirectory() as parent:
+ default_dir = os.path.join(parent, "jackhmmer", "default")
+ os.makedirs(default_dir, exist_ok=True)
+ # Patch get_jackhmmer_dir so the None default resolves to a temp folder
+ # (never the real ~/tmp) for the duration of the call.
+ with patch.object(gget_alphafold, "get_jackhmmer_dir", return_value=default_dir) as mock_dir:
+ clean_up()
+ mock_dir.assert_called_once()
+ self.assertFalse(os.path.isdir(default_dir))
+
+ def test_cli_exposes_jackhmmer_savedir_flag(self):
+ """The command-line interface exposes the --jackhmmer_savedir option."""
+ result = subprocess.run(
+ [sys.executable, "-m", "gget", "alphafold", "--help"],
+ capture_output=True,
+ text=True,
+ )
+ self.assertIn("--jackhmmer_savedir", result.stdout)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_archs4.py b/tests/test_archs4.py
index c6336bc91..99440b054 100644
--- a/tests/test_archs4.py
+++ b/tests/test_archs4.py
@@ -1,5 +1,6 @@
import json
import unittest
+from unittest.mock import patch
from gget.gget_archs4 import archs4
@@ -12,3 +13,25 @@
class TestArchs4(unittest.TestCase, metaclass=from_json(archs4_dict, archs4)):
pass # all tests are loaded from json
+
+
+class _FakeResponse:
+ def __init__(self, text):
+ self.ok = True
+ self.content = text.encode("utf-8")
+
+
+class TestArchs4MissingColor(unittest.TestCase):
+ """Network-free regression tests: ARCHS4 intermittently omits the 'color' column from
+ the tissue-expression CSV. gget must not crash with a KeyError when it is absent
+ (the 'color' column is dropped and never used)."""
+
+ _CSV_NO_COLOR = "id,min,q1,median,q3,max\nTissueA,0,1,5,9,10\nTissueB,0,2,8,12,15\n"
+
+ def test_tissue_missing_color_does_not_crash(self):
+ with patch("gget.gget_archs4.requests.post", return_value=_FakeResponse(self._CSV_NO_COLOR)):
+ df = archs4("STAT4", which="tissue", verbose=False)
+ # Returns a valid, sorted data frame without a 'color' column (no KeyError).
+ self.assertEqual(len(df), 2)
+ self.assertNotIn("color", df.columns)
+ self.assertEqual(df.iloc[0]["id"], "TissueB") # sorted by median descending
diff --git a/tests/test_elm.py b/tests/test_elm.py
index de8f67c35..099cd0bec 100644
--- a/tests/test_elm.py
+++ b/tests/test_elm.py
@@ -1,4 +1,5 @@
import json
+import time
import unittest
from gget.gget_elm import elm
@@ -8,7 +9,14 @@
with open("./tests/fixtures/test_elm.json") as json_file:
elm_dict = json.load(json_file)
-gget_setup(module="elm")
+for attempt in range(3):
+ try:
+ gget_setup(module="elm")
+ break
+ except RuntimeError as exc:
+ if "ELM database files download failed" not in str(exc) or attempt == 2:
+ raise
+ time.sleep(30)
class TestELM(unittest.TestCase):
diff --git a/tests/test_g2p.py b/tests/test_g2p.py
index e3ecbc18b..3c20335fd 100644
--- a/tests/test_g2p.py
+++ b/tests/test_g2p.py
@@ -1,6 +1,8 @@
+import os
+import tempfile
import unittest
-from gget.gget_g2p import g2p
+from gget.gget_g2p import _resolve_gene_from_uniprot, _resolve_uniprot_from_gene, g2p
class TestG2P(unittest.TestCase):
@@ -17,6 +19,8 @@ def test_g2p_map(self):
self.assertListEqual(
list(df.columns),
[
+ "gene_name",
+ "uniprot_id",
"UniProtKB",
"UniProt Isoform",
"Ensembl Gene Id",
@@ -24,9 +28,13 @@ def test_g2p_map(self):
"Ensembl Transcript Id",
"RefSeq mRNA Id",
"PDB Ids",
+ "PDB Ids List",
],
)
self.assertGreater(len(df), 0)
+ # Identifier columns are populated with the query pair on every row.
+ self.assertTrue((df["gene_name"] == "BRCA1").all())
+ self.assertTrue((df["uniprot_id"] == "P38398").all())
# BRCA1's stable Ensembl gene ID should be present
self.assertTrue((df["Ensembl Gene Id"] == "ENSG00000012048").any())
@@ -48,6 +56,92 @@ def test_g2p_alignment(self):
self.assertIn("residueId", df.columns)
self.assertIn("AA", df.columns)
+ def test_g2p_gene_auto_resolved(self):
+ """When `gene` is omitted, it must be resolved from `uniprot_id` via UniProt."""
+ _resolve_gene_from_uniprot.cache_clear()
+ df = g2p(uniprot_id="P38398", resource="map", verbose=False)
+ self.assertIsNotNone(df)
+ self.assertGreater(len(df), 0)
+ self.assertTrue((df["Ensembl Gene Id"] == "ENSG00000012048").any())
+
+ def test_g2p_invalid_pair_returns_none(self):
+ """Regression: the G2P portal returns HTTP 200 with a JSON failure body when the
+ gene/UniProt pair is unknown. Previously this leaked through as a 0-row DataFrame
+ whose only column name was the JSON error string; now it must return None.
+ """
+ df = g2p("BRCA1", uniprot_id="P01130", resource="features", verbose=False)
+ self.assertIsNone(df)
+
+ def test_g2p_out_writes_to_explicit_path(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ out_path = os.path.join(tmp, "g2p_map.csv")
+ df = g2p(
+ "BRCA1",
+ uniprot_id="P38398",
+ resource="map",
+ out=out_path,
+ verbose=False,
+ )
+ self.assertIsNotNone(df)
+ self.assertTrue(os.path.exists(out_path))
+ self.assertGreater(os.path.getsize(out_path), 0)
+
+ def test_g2p_uniprot_auto_resolved(self):
+ """When only `gene` is supplied, UniProt accession must be resolved via UniProt."""
+ _resolve_uniprot_from_gene.cache_clear()
+ df = g2p("BRCA1", resource="map", verbose=False)
+ self.assertIsNotNone(df)
+ # The canonical pair travels with the data both as leading columns and df.attrs
+ self.assertIn("gene_name", df.columns)
+ self.assertIn("uniprot_id", df.columns)
+ self.assertTrue((df["gene_name"] == "BRCA1").all())
+ self.assertTrue((df["uniprot_id"] == "P38398").all())
+ self.assertEqual(df.attrs.get("gene_name"), "BRCA1")
+ self.assertEqual(df.attrs.get("uniprot_id"), "P38398")
+
+ def test_g2p_schema_invariant_across_input_modes(self):
+ """Output schema must be identical whether the user passes gene-only,
+ uniprot-only, or both — including the leading gene_name / uniprot_id columns."""
+ df_both = g2p("BRCA1", uniprot_id="P38398", resource="map", verbose=False)
+ df_gene_only = g2p("BRCA1", resource="map", verbose=False)
+ df_uniprot_only = g2p(uniprot_id="P38398", resource="map", verbose=False)
+ self.assertEqual(list(df_both.columns), list(df_gene_only.columns))
+ self.assertEqual(list(df_both.columns), list(df_uniprot_only.columns))
+ self.assertEqual(list(df_both.columns[:2]), ["gene_name", "uniprot_id"])
+
+ def test_g2p_residues_filter(self):
+ """`residues=` restricts the features table to the requested positions."""
+ df = g2p(
+ "BRCA1",
+ uniprot_id="P38398",
+ resource="features",
+ residues=[1, 100, 1000],
+ verbose=False,
+ )
+ self.assertIsNotNone(df)
+ self.assertEqual(sorted(df["residueId"].tolist()), [1, 100, 1000])
+
+ def test_g2p_residues_single_int(self):
+ df = g2p(
+ "BRCA1",
+ uniprot_id="P38398",
+ resource="features",
+ residues=42,
+ verbose=False,
+ )
+ self.assertIsNotNone(df)
+ self.assertEqual(df["residueId"].tolist(), [42])
+
+ def test_g2p_map_has_pdb_list_column(self):
+ """The `map` resource gets a parsed `PDB Ids List` column for direct consumption."""
+ df = g2p("BRCA1", uniprot_id="P38398", resource="map", verbose=False)
+ self.assertIn("PDB Ids List", df.columns)
+ # Each non-null entry should be a list of strings
+ for entry in df["PDB Ids List"].head(3):
+ self.assertIsInstance(entry, list)
+ for item in entry:
+ self.assertIsInstance(item, str)
+
class TestG2PValidation(unittest.TestCase):
"""Fast, network-free tests for argument validation."""
@@ -56,10 +150,23 @@ def test_invalid_resource_raises(self):
with self.assertRaises(ValueError):
g2p("BRCA1", uniprot_id="P38398", resource="not_a_resource")
- def test_missing_uniprot_raises(self):
+ def test_missing_both_raises(self):
with self.assertRaises(ValueError):
- g2p("BRCA1", uniprot_id=None)
+ g2p()
def test_alignment_requires_isoform(self):
with self.assertRaises(ValueError):
g2p("LDLR", uniprot_id="P01130-1", resource="alignment")
+
+ def test_alignment_requires_explicit_uniprot(self):
+ """Alignment cannot rely on gene→UniProt resolution (returns base accession, no isoform)."""
+ with self.assertRaises(ValueError):
+ g2p("LDLR", resource="alignment", isoform="P01130-2")
+
+ def test_residues_on_map_raises(self):
+ with self.assertRaises(ValueError):
+ g2p("BRCA1", uniprot_id="P38398", resource="map", residues=[1, 2, 3])
+
+ def test_residues_wrong_type_raises(self):
+ with self.assertRaises(ValueError):
+ g2p("BRCA1", uniprot_id="P38398", residues="not-ints") # type: ignore[arg-type]
diff --git a/tests/test_opentargets.py b/tests/test_opentargets.py
index e73ced1cc..f5908531b 100644
--- a/tests/test_opentargets.py
+++ b/tests/test_opentargets.py
@@ -1,6 +1,8 @@
import json
import unittest
+from unittest.mock import patch
+import pandas as pd
from gget.gget_opentargets import opentargets
from .from_json import from_json
@@ -12,3 +14,88 @@
class TestOpenTargets(unittest.TestCase, metaclass=from_json(ot_dict, opentargets)):
pass # all tests are loaded from json
+
+
+# Sample of the current OpenTargets `baselineExpression.rows` response shape.
+_BASELINE_EXPRESSION_ROWS = [
+ {
+ "tissueBiosample": {"biosampleId": "UBERON_0000007", "biosampleName": "pituitary gland"},
+ "celltypeBiosample": None,
+ "median": 0.066891,
+ "min": 0.0,
+ "q1": 0.028268,
+ "q3": 0.142208,
+ "max": 1.69407,
+ "unit": "TPM",
+ "datasourceId": "gtex",
+ "datatypeId": "bulk rna-seq",
+ },
+ {
+ "tissueBiosample": {"biosampleId": "UBERON_0002107", "biosampleName": "liver"},
+ "celltypeBiosample": None,
+ "median": 2.5,
+ "min": 0.1,
+ "q1": 1.0,
+ "q3": 3.0,
+ "max": 8.0,
+ "unit": "TPM",
+ "datasourceId": "gtex",
+ "datatypeId": "bulk rna-seq",
+ },
+]
+
+
+def _baseline_expression_response(rows):
+ return {"data": {"target": {"baselineExpression": {"rows": rows}}}}
+
+
+class TestOpenTargetsExpressionMocked(unittest.TestCase):
+ """Network-free tests for the expression resource after OpenTargets moved baseline
+ expression from the (now-empty) `expressions` field to `baselineExpression.rows`.
+
+ The previous live, exact-match fixtures `test_opentargets_expression` and
+ `test_opentargets_expression_no_limit` asserted the old per-tissue RNA shape
+ (`tissue.id`, `rna.zscore`, ...) which no longer exists upstream; they were
+ removed from tests/fixtures/test_opentargets.json and replaced by these
+ deterministic mocked tests."""
+
+ def test_expression_parses_baseline_rows(self):
+ with patch(
+ "gget.gget_opentargets.http_json",
+ return_value=_baseline_expression_response(_BASELINE_EXPRESSION_ROWS),
+ ):
+ df = opentargets("ENSG00000169194", resource="expression", verbose=False)
+
+ self.assertIsInstance(df, pd.DataFrame)
+ self.assertEqual(len(df), 2)
+ # Flattened biosample + summary-statistic columns are present
+ self.assertIn("tissueBiosample.biosampleId", df.columns)
+ self.assertIn("median", df.columns)
+ self.assertIn("unit", df.columns)
+ self.assertEqual(df.iloc[0]["tissueBiosample.biosampleId"], "UBERON_0000007")
+
+ def test_expression_limit(self):
+ with patch(
+ "gget.gget_opentargets.http_json",
+ return_value=_baseline_expression_response(_BASELINE_EXPRESSION_ROWS),
+ ):
+ df = opentargets("ENSG00000169194", resource="expression", limit=1, verbose=False)
+ self.assertEqual(len(df), 1)
+
+ def test_expression_json(self):
+ with patch(
+ "gget.gget_opentargets.http_json",
+ return_value=_baseline_expression_response(_BASELINE_EXPRESSION_ROWS),
+ ):
+ result = opentargets("ENSG00000169194", resource="expression", json=True, verbose=False)
+ self.assertIsInstance(result, list)
+ self.assertEqual(len(result), 2)
+
+ def test_expression_empty_is_graceful(self):
+ with patch(
+ "gget.gget_opentargets.http_json",
+ return_value=_baseline_expression_response([]),
+ ):
+ df = opentargets("ENSG00000169194", resource="expression", verbose=False)
+ self.assertIsInstance(df, pd.DataFrame)
+ self.assertEqual(len(df), 0)
diff --git a/tests/test_pdb.py b/tests/test_pdb.py
index a805e09a1..a0b02e7a5 100644
--- a/tests/test_pdb.py
+++ b/tests/test_pdb.py
@@ -78,10 +78,28 @@ def test_pdb_pdb(self):
"The reference and fetched PDB are not the same.",
)
+ def test_pdb_mmcif(self):
+ # Explicit PDBx/mmCIF download returns a CIF document (starts with "data_")
+ result = pdb("4ACQ", resource="mmcif")
+ self.assertTrue(
+ result.startswith("data_4ACQ"),
+ "resource='mmcif' did not return a PDBx/mmCIF document.",
+ )
+
+ def test_pdb_legacy_fallback_to_mmcif(self):
+ # Regression test for #177/#178: 6Q38 has no legacy PDB file, so a
+ # resource='pdb' request must transparently fall back to PDBx/mmCIF.
+ result = pdb("6Q38", resource="pdb")
+ self.assertTrue(
+ result.startswith("data_6Q38"),
+ "resource='pdb' did not fall back to PDBx/mmCIF when the legacy PDB file is missing.",
+ )
+
def tearDown(self):
super().tearDown()
- # Delete temporary result file
- try:
- os.remove("4ACQ.pdb")
- except OSError:
- pass
+ # Delete temporary result files
+ for fname in ("4ACQ.pdb", "4ACQ.cif", "6Q38.cif"):
+ try:
+ os.remove(fname)
+ except OSError:
+ pass