diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md
index b112fd0f2..c2bb0d6b5 100644
--- a/docs/src/SUMMARY.md
+++ b/docs/src/SUMMARY.md
@@ -10,6 +10,7 @@
# Manual
* [gget 8cube](en/8cube.md)
+* [gget alliance](en/alliance.md)
* [gget alphafold](en/alphafold.md)
* [gget archs4](en/archs4.md)
* [gget bgee](en/bgee.md)
diff --git a/docs/src/en/alliance.md b/docs/src/en/alliance.md
new file mode 100644
index 000000000..841b7f51e
--- /dev/null
+++ b/docs/src/en/alliance.md
@@ -0,0 +1,72 @@
+[ View page source on GitHub ](https://github.com/scverse/gget/blob/main/docs/src/en/alliance.md)
+
+> 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 alliance 🧬
+Query the [Alliance of Genome Resources](https://www.alliancegenome.org/), which integrates data from the major model organism databases (human, mouse, rat, zebrafish, fly, worm, yeast, and more).
+`gget alliance` has two modes, chosen automatically from the input:
+- If the input is an Alliance **gene ID** (e.g. `HGNC:1101`, `MGI:109337`, `RGD:2219`, `ZFIN:...`, `FB:...`, `WB:...`, `SGD:...`), the gene's details are returned.
+- Otherwise, the input is used as a **free-text search** and the matching objects of the given `--category` are returned.
+
+Return format: JSON (command-line) or data frame/CSV (Python).
+
+**Positional argument**
+`search_term`
+Alliance gene ID (e.g. `HGNC:1101`) or a free-text search term.
+
+**Optional arguments**
+`-c` `--category`
+Category for free-text searches: one of `gene`, `allele`, `disease`, `go`, `variant`, `model`, or `all` for no filter. Default: `gene`.
+
+`-l` `--limit`
+Maximum number of results for free-text searches. Default: 10.
+
+`-o` `--out`
+Path to the file the results will be saved in, e.g. path/to/directory/results.csv (or .json). Default: Standard out.
+Python: `save=True` will save the output in the current working directory.
+
+**Flags**
+`-csv` `--csv`
+Command-line only. Returns results in CSV format.
+Python: Use `json=True` to return output in JSON format.
+
+`-q` `--quiet`
+Command-line only. Prevents progress information from being displayed.
+Python: Use `verbose=False` to prevent progress information from being displayed.
+
+### Examples
+**Search for genes across model organisms:**
+```bash
+gget alliance brca2
+```
+```python
+# Python
+gget.alliance("brca2")
+```
+→ Returns a table of genes matching the search term across Alliance member databases.
+
+| id | symbol | name | species | category | so_term_name |
+| --- | --- | --- | --- | --- | --- |
+| HGNC:1101 | BRCA2 | BRCA2 DNA repair associated | Homo sapiens | gene_search_result | protein_coding_gene |
+| MGI:109337 | Brca2 | breast cancer 2 | Mus musculus | gene_search_result | protein_coding_gene |
+
+
+**Fetch a single gene by its Alliance ID:**
+```bash
+gget alliance HGNC:1101
+```
+```python
+# Python
+gget.alliance("HGNC:1101")
+```
+→ Returns the gene's details.
+
+| id | symbol | name | species | taxon | gene_type | synonyms | data_provider |
+| --- | --- | --- | --- | --- | --- | --- | --- |
+| HGNC:1101 | BRCA2 | BRCA2 DNA repair associated | Homo sapiens | NCBITaxon:9606 | protein_coding_gene | ['FAD', ...] | RGD |
+
+# References
+If you use `gget alliance` in a publication, please cite the following articles:
+
+- Luebbert, L., & Pachter, L. (2023). Efficient querying of genomic reference databases with gget. Bioinformatics. [https://doi.org/10.1093/bioinformatics/btac836](https://doi.org/10.1093/bioinformatics/btac836)
+
+- Alliance of Genome Resources Consortium. (2024). Updates to the Alliance of Genome Resources central infrastructure. Genetics. [https://doi.org/10.1093/genetics/iyae049](https://doi.org/10.1093/genetics/iyae049)
diff --git a/docs/src/en/updates.md b/docs/src/en/updates.md
index 89be22281..6cddac2a3 100644
--- a/docs/src/en/updates.md
+++ b/docs/src/en/updates.md
@@ -5,6 +5,7 @@
#### *gget* officially became part of [*scverse*](https://scverse.org/) on June 9, 2026. 🥳🥳🥳
**Version ≥ 0.30.9** (XXX XX, 2026):
+- [`gget alliance`](alliance.md): **New module** to query the [Alliance of Genome Resources](https://www.alliancegenome.org/). Pass an Alliance gene ID (e.g. `HGNC:1101`, `MGI:109337`, `RGD:2219`) to fetch the gene's details, or a free-text term to search genes/alleles/diseases/GO/variants/models across the member model-organism databases (selectable with `category`). Available in the Python API and on the command line. Resolves [issue 162](https://github.com/scverse/gget/issues/162).
**Version ≥ 0.30.8** (Jun 28, 2026):
- [`gget g2p`](g2p.md): Either `gene` or `--uniprot_id` is now sufficient — whichever is missing is resolved via UniProt and cached. Gene→UniProt picks the canonical reviewed human Swiss-Prot entry; the resolution and its limitations are logged. The canonical pair is **always** prepended to the result as `gene_name` / `uniprot_id` columns (and stored on `df.attrs`), so the output schema is invariant regardless of input mode. Existing call sites continue to work.
diff --git a/gget/__init__.py b/gget/__init__.py
index f56cbcddc..5b336dc11 100644
--- a/gget/__init__.py
+++ b/gget/__init__.py
@@ -4,6 +4,7 @@
from importlib.metadata import PackageNotFoundError, version
from .gget_8cube import gene_expression, psi_block, specificity
+from .gget_alliance import alliance
from .gget_alphafold import alphafold
from .gget_archs4 import archs4
from .gget_bgee import bgee
diff --git a/gget/constants.py b/gget/constants.py
index 38f90119f..a0226f59f 100644
--- a/gget/constants.py
+++ b/gget/constants.py
@@ -7,6 +7,9 @@
# strategy avoid hanging indefinitely on slow upstreams.
DEFAULT_REQUESTS_TIMEOUT = (10, 60)
+# Alliance of Genome Resources REST API for gget alliance
+ALLIANCE_URL = "https://www.alliancegenome.org/api"
+
# Ensembl REST API server for gget seq and info
ENSEMBL_REST_API = "http://rest.ensembl.org/"
ENSEMBL_FTP_URL = "http://ftp.ensembl.org/pub/"
diff --git a/gget/gget_alliance.py b/gget/gget_alliance.py
new file mode 100644
index 000000000..9227371b1
--- /dev/null
+++ b/gget/gget_alliance.py
@@ -0,0 +1,230 @@
+from __future__ import annotations
+
+import json as json_package
+from typing import Any, Literal, overload
+
+import pandas as pd
+import requests
+
+from .constants import ALLIANCE_URL, DEFAULT_REQUESTS_TIMEOUT
+from .utils import set_up_logger
+
+logger = set_up_logger()
+
+# Columns returned when fetching a single gene by its Alliance ID
+_GENE_COLUMNS = [
+ "id",
+ "symbol",
+ "name",
+ "species",
+ "taxon",
+ "gene_type",
+ "synonyms",
+ "data_provider",
+]
+# Columns returned for a free-text search
+_SEARCH_COLUMNS = [
+ "id",
+ "symbol",
+ "name",
+ "species",
+ "category",
+ "so_term_name",
+]
+
+# Friendly category names -> Alliance search API category values
+_CATEGORY_MAP = {
+ "gene": "gene_search_result",
+ "allele": "allele_search_result",
+ "disease": "disease_search_result",
+ "go": "go_search_result",
+ "variant": "variant_search_result",
+ "model": "model",
+}
+
+# Recognized Alliance/model-organism-database gene ID (curie) prefixes
+_GENE_ID_PREFIXES = {
+ "HGNC",
+ "MGI",
+ "RGD",
+ "ZFIN",
+ "FB",
+ "WB",
+ "SGD",
+ "ENSEMBL",
+ "XENBASE",
+ "WORMBASE",
+ "FLYBASE",
+}
+
+
+def _is_gene_id(term: str) -> bool:
+ """Return True if 'term' looks like an Alliance gene ID (e.g. HGNC:1101)."""
+ if ":" not in term:
+ return False
+ prefix = term.split(":", 1)[0].upper()
+ return prefix in _GENE_ID_PREFIXES
+
+
+def _alliance_get(path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
+ """GET a JSON object from the Alliance of Genome Resources REST API."""
+ url = f"{ALLIANCE_URL}{path}"
+ try:
+ response = requests.get(
+ url,
+ params=params,
+ headers={"Accept": "application/json"},
+ timeout=DEFAULT_REQUESTS_TIMEOUT,
+ )
+ except requests.exceptions.RequestException as exc:
+ raise RuntimeError(f"The Alliance server request failed: {exc}") from exc
+
+ if response.status_code == 404:
+ raise ValueError(f"Alliance returned 404 (not found) for '{path}'. Please double-check the gene ID.")
+ if not response.ok:
+ raise RuntimeError(
+ f"The Alliance server returned error status code {response.status_code}. Please try again later."
+ )
+
+ return response.json()
+
+
+def _text(value: Any) -> Any:
+ """Extract display text from an Alliance {displayText/formatText} object."""
+ if isinstance(value, dict):
+ return value.get("displayText") or value.get("formatText")
+ return value
+
+
+def _gene_row(gene: dict[str, Any]) -> dict[str, Any]:
+ """Flatten an Alliance gene object into a row of scalar values."""
+ taxon = gene.get("taxon") or {}
+ species = taxon.get("name") if isinstance(taxon, dict) else None
+ taxon_curie = taxon.get("curie") if isinstance(taxon, dict) else None
+
+ gene_type = gene.get("geneType")
+ gene_type_name = gene_type.get("name") if isinstance(gene_type, dict) else gene_type
+
+ data_provider = gene.get("dataProvider")
+ provider = data_provider.get("abbreviation") if isinstance(data_provider, dict) else data_provider
+
+ synonyms = [_text(s) for s in (gene.get("geneSynonyms") or [])]
+
+ return {
+ "id": gene.get("primaryExternalId"),
+ "symbol": _text(gene.get("geneSymbol")),
+ "name": _text(gene.get("geneFullName")),
+ "species": species,
+ "taxon": taxon_curie,
+ "gene_type": gene_type_name,
+ "synonyms": synonyms,
+ "data_provider": provider,
+ }
+
+
+def _search_row(r: dict[str, Any]) -> dict[str, Any]:
+ """Flatten an Alliance search result into a row of scalar values."""
+ return {
+ "id": r.get("id"),
+ "symbol": r.get("symbol"),
+ "name": r.get("name"),
+ "species": r.get("species"),
+ "category": r.get("category"),
+ "so_term_name": r.get("soTermName"),
+ }
+
+
+@overload
+def alliance(
+ search_term: str,
+ category: str = "gene",
+ limit: int = 10,
+ save: bool = False,
+ verbose: bool = True,
+ *,
+ json: Literal[True],
+) -> list[dict[str, Any]] | None: ...
+
+
+@overload
+def alliance(
+ search_term: str,
+ category: str = "gene",
+ limit: int = 10,
+ save: bool = False,
+ verbose: bool = True,
+ json: Literal[False] = False,
+) -> pd.DataFrame | None: ...
+
+
+def alliance(
+ search_term: str,
+ category: str = "gene",
+ limit: int = 10,
+ save: bool = False,
+ verbose: bool = True,
+ json: bool = False,
+) -> pd.DataFrame | list[dict[str, Any]] | None:
+ """Query the Alliance of Genome Resources (https://www.alliancegenome.org/).
+
+ Two modes, chosen automatically from 'search_term':
+ - If 'search_term' is an Alliance gene ID (e.g. "HGNC:1101", "MGI:109337",
+ "RGD:2219", "ZFIN:...", "FB:...", "WB:...", "SGD:..."), the gene is fetched
+ and its details are returned.
+ - Otherwise, 'search_term' is used as a free-text query against the Alliance
+ search endpoint and the matching objects of the given 'category' are returned.
+
+ Args:
+ - search_term Alliance gene ID (curie) or a free-text search term.
+ - category Category for free-text searches: one of "gene", "allele",
+ "disease", "go", "variant", "model", or None/"all" for no filter.
+ Default: "gene".
+ - limit Maximum number of results for free-text searches. Default: 10.
+ - save If True, save the results table as csv/json in the working directory. Default: False.
+ - verbose True/False whether to print progress information. Default: True.
+ - json If True, returns results in json format instead of data frame. Default: False.
+
+ Returns a data frame (or list of dicts if json=True) of gene details or search
+ results. Returns None if no results are found.
+ """
+ if search_term is None or str(search_term).strip() == "":
+ raise ValueError("Please provide a gene ID or search term in 'search_term'.")
+
+ term = str(search_term).strip()
+ source = "alliance_search"
+
+ if _is_gene_id(term):
+ if verbose:
+ logger.info(f"Fetching Alliance gene {term}...")
+ obj = _alliance_get(f"/gene/{term}")
+ gene = obj.get("gene") if isinstance(obj, dict) else None
+ if not gene:
+ logger.warning(f"No Alliance gene found for '{term}'.")
+ return None
+ source = "alliance_gene"
+ results_df = pd.DataFrame([_gene_row(gene)], columns=_GENE_COLUMNS)
+ else:
+ params: dict[str, Any] = {"q": term, "limit": limit}
+ if category is not None and str(category).lower() not in ("all", ""):
+ params["category"] = _CATEGORY_MAP.get(str(category).lower(), category)
+ if verbose:
+ logger.info(f"Searching Alliance for '{term}' (category={category})...")
+ data = _alliance_get("/search", params=params)
+ rows = [_search_row(r) for r in data.get("results", [])]
+ results_df = pd.DataFrame(rows, columns=_SEARCH_COLUMNS)
+
+ if len(results_df) == 0:
+ logger.warning(f"No Alliance results found for '{term}'.")
+ return None
+
+ if json:
+ results_dict = json_package.loads(results_df.to_json(orient="records"))
+ if save:
+ with open(f"gget_{source}_results.json", "w", encoding="utf-8") as f:
+ json_package.dump(results_dict, f, ensure_ascii=False, indent=4)
+ return results_dict
+
+ if save:
+ results_df.to_csv(f"gget_{source}_results.csv", index=False)
+
+ return results_df
diff --git a/gget/main.py b/gget/main.py
index 7a2944b09..d8ff9f54a 100644
--- a/gget/main.py
+++ b/gget/main.py
@@ -19,6 +19,7 @@
logger = set_up_logger()
from .__init__ import __version__ # noqa: E402
+from .gget_alliance import alliance # noqa: E402
from .gget_alphafold import alphafold # noqa: E402
from .gget_archs4 import archs4 # noqa: E402
from .gget_bgee import bgee # noqa: E402
@@ -1002,6 +1003,67 @@ def main() -> None:
help="DEPRECATED - json is now the default output format (convert to csv using flag [--csv]).",
)
+ ## gget alliance subparser
+ alliance_desc = "Query the Alliance of Genome Resources (https://www.alliancegenome.org/)."
+ parser_alliance = parent_subparsers.add_parser(
+ "alliance",
+ parents=[parent],
+ description=alliance_desc,
+ help=alliance_desc,
+ add_help=True,
+ formatter_class=CustomHelpFormatter,
+ )
+ parser_alliance.add_argument(
+ "search_term",
+ type=str,
+ help="Alliance gene ID (e.g. HGNC:1101, MGI:109337, RGD:2219) or a free-text search term.",
+ )
+ parser_alliance.add_argument(
+ "-c",
+ "--category",
+ type=str,
+ default="gene",
+ required=False,
+ help=(
+ "Category for free-text searches: 'gene', 'allele', 'disease', 'go', 'variant', 'model', "
+ "or 'all' for no filter. Default: 'gene'."
+ ),
+ )
+ parser_alliance.add_argument(
+ "-l",
+ "--limit",
+ type=int,
+ default=10,
+ required=False,
+ help="Maximum number of results for free-text searches. Default: 10.",
+ )
+ parser_alliance.add_argument(
+ "-csv",
+ "--csv",
+ default=True,
+ action="store_false",
+ required=False,
+ help="Returns results in csv format instead of json.",
+ )
+ parser_alliance.add_argument(
+ "-o",
+ "--out",
+ type=str,
+ required=False,
+ help=(
+ "Path to the file the results will be saved in, e.g. path/to/directory/results.csv (or .json).\n"
+ "Default: Standard out."
+ ),
+ )
+ parser_alliance.add_argument(
+ "-q",
+ "--quiet",
+ default=True,
+ action="store_false",
+ required=False,
+ help="Does not print progress information.",
+ )
+
## gget enrichr subparser
enrichr_desc = "Perform an enrichment analysis on a list of genes using Enrichr."
parser_enrichr = parent_subparsers.add_parser(
@@ -2956,6 +3018,7 @@ def main() -> None:
"muscle": parser_muscle,
"blast": parser_blast,
"blat": parser_blat,
+ "alliance": parser_alliance,
"enrichr": parser_enrichr,
"archs4": parser_archs4,
"setup": parser_setup,
@@ -3502,6 +3565,38 @@ def main() -> None:
if not args.out and args.csv:
print(json.dumps(gget_results, ensure_ascii=False, indent=4))
+ ## alliance return
+ if args.command == "alliance":
+ alliance_results = alliance(
+ search_term=args.search_term,
+ category=args.category,
+ limit=args.limit,
+ json=args.csv,
+ verbose=args.quiet,
+ )
+
+ # Check if the function returned something
+ if alliance_results is not None:
+ # Save results if args.out specified
+ if args.out and not args.csv:
+ directory = "/".join(args.out.split("/")[:-1])
+ if directory != "":
+ os.makedirs(directory, exist_ok=True)
+ alliance_results.to_csv(args.out, index=False)
+
+ if args.out and args.csv:
+ directory = "/".join(args.out.split("/")[:-1])
+ if directory != "":
+ os.makedirs(directory, exist_ok=True)
+ with open(args.out, "w", encoding="utf-8") as f:
+ json.dump(alliance_results, f, ensure_ascii=False, indent=4)
+
+ # Print results if no directory specified
+ if not args.out and not args.csv:
+ alliance_results.to_csv(sys.stdout, index=False)
+ if not args.out and args.csv:
+ print(json.dumps(alliance_results, ensure_ascii=False, indent=4))
+
## enrichr return
if args.command == "enrichr":
# Handle deprecated flags for backwards compatibility
diff --git a/tests/fixtures/test_alliance.json b/tests/fixtures/test_alliance.json
new file mode 100644
index 000000000..ff2e93f5f
--- /dev/null
+++ b/tests/fixtures/test_alliance.json
@@ -0,0 +1,10 @@
+{
+ "test_alliance_no_term": {
+ "type": "error",
+ "args": {
+ "search_term": ""
+ },
+ "expected_result": "ValueError",
+ "expected_msg": "Please provide a gene ID or search term in 'search_term'."
+ }
+}
diff --git a/tests/test_alliance.py b/tests/test_alliance.py
new file mode 100644
index 000000000..3be458b0c
--- /dev/null
+++ b/tests/test_alliance.py
@@ -0,0 +1,184 @@
+import json
+import os
+import tempfile
+import unittest
+from unittest.mock import patch
+
+import gget.gget_alliance as gget_alliance
+import requests
+from gget.gget_alliance import _gene_row, _is_gene_id, _search_row, alliance
+
+from .from_json import from_json
+
+with open("./tests/fixtures/test_alliance.json") as json_file:
+ alliance_dict = json.load(json_file)
+
+
+class TestAlliance(unittest.TestCase, metaclass=from_json(alliance_dict, alliance)):
+ pass # tests loaded from json
+
+
+class _FakeResponse:
+ """Minimal stand-in for a requests.Response used to test parsing offline."""
+
+ def __init__(self, payload, ok=True, status_code=200):
+ self._payload = payload
+ self.ok = ok
+ self.status_code = status_code
+
+ def json(self):
+ return self._payload
+
+
+_GENE_PAYLOAD = {
+ "category": "gene_summary",
+ "gene": {
+ "primaryExternalId": "HGNC:1101",
+ "geneSymbol": {"displayText": "BRCA2", "formatText": "BRCA2"},
+ "geneFullName": {"displayText": "BRCA2 DNA repair associated"},
+ "geneType": {"name": "protein_coding_gene"},
+ "geneSynonyms": [{"displayText": "FAD"}, {"displayText": "FACD"}],
+ "taxon": {"curie": "NCBITaxon:9606", "name": "Homo sapiens"},
+ "dataProvider": {"abbreviation": "RGD"},
+ },
+}
+
+_SEARCH_PAYLOAD = {
+ "results": [
+ {
+ "id": "HGNC:1101",
+ "symbol": "BRCA2",
+ "name": "BRCA2 DNA repair associated",
+ "species": "Homo sapiens",
+ "category": "gene_search_result",
+ "soTermName": "protein_coding_gene",
+ },
+ {
+ "id": "MGI:109337",
+ "symbol": "Brca2",
+ "name": "breast cancer 2",
+ "species": "Mus musculus",
+ "category": "gene_search_result",
+ "soTermName": "protein_coding_gene",
+ },
+ ]
+}
+
+
+class TestAllianceHelpers(unittest.TestCase):
+ """Network-free tests of the Alliance helpers (issue #162)."""
+
+ def test_is_gene_id(self):
+ self.assertTrue(_is_gene_id("HGNC:1101"))
+ self.assertTrue(_is_gene_id("MGI:109337"))
+ self.assertTrue(_is_gene_id("rgd:2219"))
+ self.assertFalse(_is_gene_id("brca2"))
+ self.assertFalse(_is_gene_id("breast cancer"))
+
+ def test_gene_row(self):
+ row = _gene_row(_GENE_PAYLOAD["gene"])
+ self.assertEqual(row["id"], "HGNC:1101")
+ self.assertEqual(row["symbol"], "BRCA2")
+ self.assertEqual(row["name"], "BRCA2 DNA repair associated")
+ self.assertEqual(row["species"], "Homo sapiens")
+ self.assertEqual(row["taxon"], "NCBITaxon:9606")
+ self.assertEqual(row["gene_type"], "protein_coding_gene")
+ self.assertEqual(row["synonyms"], ["FAD", "FACD"])
+ self.assertEqual(row["data_provider"], "RGD")
+
+ def test_search_row(self):
+ row = _search_row(_SEARCH_PAYLOAD["results"][0])
+ self.assertEqual(row["id"], "HGNC:1101")
+ self.assertEqual(row["symbol"], "BRCA2")
+ self.assertEqual(row["species"], "Homo sapiens")
+
+ @patch.object(gget_alliance.requests, "get")
+ def test_gene_id_mode(self, mock_get):
+ mock_get.return_value = _FakeResponse(_GENE_PAYLOAD)
+ df = alliance("HGNC:1101", verbose=False)
+ self.assertEqual(list(df.columns), gget_alliance._GENE_COLUMNS)
+ self.assertEqual(df.shape[0], 1)
+ self.assertEqual(df.iloc[0]["symbol"], "BRCA2")
+
+ @patch.object(gget_alliance.requests, "get")
+ def test_search_mode_json(self, mock_get):
+ mock_get.return_value = _FakeResponse(_SEARCH_PAYLOAD)
+ result = alliance("brca2", json=True, verbose=False)
+ self.assertIsInstance(result, list)
+ self.assertEqual(len(result), 2)
+ self.assertEqual(result[0]["id"], "HGNC:1101")
+
+ @patch.object(gget_alliance.requests, "get")
+ def test_category_mapping(self, mock_get):
+ mock_get.return_value = _FakeResponse(_SEARCH_PAYLOAD)
+ alliance("brca2", category="gene", verbose=False)
+ # Confirm the friendly category was mapped to the API value
+ _, kwargs = mock_get.call_args
+ self.assertEqual(kwargs["params"]["category"], "gene_search_result")
+
+ @patch.object(gget_alliance.requests, "get")
+ def test_no_results_returns_none(self, mock_get):
+ mock_get.return_value = _FakeResponse({"results": []})
+ self.assertIsNone(alliance("nonexistent xyz", verbose=False))
+
+ @patch.object(gget_alliance.requests, "get")
+ def test_http_error_raises(self, mock_get):
+ mock_get.return_value = _FakeResponse({}, ok=False, status_code=500)
+ with self.assertRaises(RuntimeError):
+ alliance("brca2", verbose=False)
+
+ def test_empty_search_term_raises(self):
+ # Covers the empty/None search_term ValueError branch.
+ with self.assertRaises(ValueError):
+ alliance(" ", verbose=False)
+
+ def test_text_passthrough(self):
+ # Covers the non-dict passthrough branch of _text.
+ self.assertEqual(gget_alliance._text("plain"), "plain")
+ self.assertIsNone(gget_alliance._text(None))
+
+ @patch.object(gget_alliance.requests, "get")
+ def test_request_exception_raises(self, mock_get):
+ # Covers the requests.RequestException -> RuntimeError branch in _alliance_get.
+ mock_get.side_effect = requests.exceptions.ConnectionError("no network")
+ with self.assertRaises(RuntimeError):
+ alliance("brca2", verbose=False)
+
+ @patch.object(gget_alliance.requests, "get")
+ def test_404_raises(self, mock_get):
+ # Covers the 404 -> ValueError branch in _alliance_get.
+ mock_get.return_value = _FakeResponse({}, ok=False, status_code=404)
+ with self.assertRaises(ValueError):
+ alliance("HGNC:1101", verbose=False)
+
+ @patch.object(gget_alliance.requests, "get")
+ def test_gene_not_found_verbose(self, mock_get):
+ # Covers the gene-path verbose log and the "no gene found" branch.
+ mock_get.return_value = _FakeResponse({"gene": None})
+ self.assertIsNone(alliance("HGNC:9999", verbose=True))
+
+ @patch.object(gget_alliance.requests, "get")
+ def test_search_verbose(self, mock_get):
+ # Covers the search-path verbose log line.
+ mock_get.return_value = _FakeResponse(_SEARCH_PAYLOAD)
+ df = alliance("brca2", verbose=True)
+ self.assertEqual(df.shape[0], 2)
+
+ @patch.object(gget_alliance.requests, "get")
+ def test_save_csv_and_json(self, mock_get):
+ # Covers the save-to-CSV and json+save branches.
+ mock_get.return_value = _FakeResponse(_GENE_PAYLOAD)
+ with tempfile.TemporaryDirectory() as tmp:
+ cwd = os.getcwd()
+ os.chdir(tmp)
+ try:
+ alliance("HGNC:1101", save=True, verbose=False)
+ self.assertTrue(any(f.endswith(".csv") for f in os.listdir(".")))
+ alliance("HGNC:1101", save=True, json=True, verbose=False)
+ self.assertTrue(any(f.endswith(".json") for f in os.listdir(".")))
+ finally:
+ os.chdir(cwd)
+
+
+if __name__ == "__main__":
+ unittest.main()