diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md
index b112fd0f2..f075c5ea7 100644
--- a/docs/src/SUMMARY.md
+++ b/docs/src/SUMMARY.md
@@ -20,6 +20,7 @@
* [gget cosmic](en/cosmic.md)
* [gget diamond](en/diamond.md)
* [gget elm](en/elm.md)
+* [gget encode](en/encode.md)
* [gget enrichr](en/enrichr.md)
* [gget g2p](en/g2p.md)
* [gget gpt](en/gpt.md)
diff --git a/docs/src/en/encode.md b/docs/src/en/encode.md
new file mode 100644
index 000000000..f82aa5bac
--- /dev/null
+++ b/docs/src/en/encode.md
@@ -0,0 +1,97 @@
+[ View page source on GitHub ](https://github.com/scverse/gget/blob/main/docs/src/en/encode.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 encode 🧫
+Query and download data from the [ENCODE project](https://www.encodeproject.org/).
+`gget encode` has two modes, chosen automatically from the input:
+- If the input is an ENCODE **accession** (e.g. `ENCSR000AKS` for an experiment or `ENCFF000BXK` for a file), the matching object is fetched and its file(s) are returned (with download URLs). Use `--download` to fetch the files.
+- Otherwise, the input is used as a **free-text search** against the ENCODE search endpoint and the matching objects of the given `--type` are returned.
+
+Return format: JSON (command-line) or data frame/CSV (Python).
+
+**Positional argument**
+`search_term`
+ENCODE accession (e.g. `ENCSR000AKS` or `ENCFF000BXK`) or a free-text search term.
+
+**Optional arguments**
+`-t` `--type`
+ENCODE object type for free-text searches, e.g. `Experiment`, `File`, `Biosample`. Default: `Experiment`.
+
+`-l` `--limit`
+Maximum number of results for free-text searches. Default: 10.
+
+`-a` `--assembly`
+Only return files for this genome assembly, e.g. `GRCh38`. Default: None.
+
+`-ff` `--file_format`
+Only return files of this format, e.g. `bam`, `fastq`, `bigWig`. Default: None.
+
+`-ot` `--output_type`
+Only return files of this output type, e.g. `alignments`. Default: None.
+
+`-od` `--out_dir`
+Directory to download files into (used with `--download`). Default: current directory.
+
+`-o` `--out`
+Path to the file the results table 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**
+`-d` `--download`
+Download the returned files into the directory given by `--out_dir`. Only supported for ENCODE accessions (experiments/files).
+
+`-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 experiments:**
+```bash
+gget encode "CTCF K562"
+```
+```python
+# Python
+gget.encode("CTCF K562")
+```
+→ Returns a table of ENCODE experiments matching the search term.
+
+| accession | assay_title | biosample_summary | target | description | status | lab |
+| --- | --- | --- | --- | --- | --- | --- |
+| ENCSR... | TF ChIP-seq | Homo sapiens K562 | CTCF | ... | released | ... |
+
+
+**List the files of an experiment (filtered to GRCh38 BAMs):**
+```bash
+gget encode ENCSR000AKS --assembly GRCh38 --file_format bam
+```
+```python
+# Python
+gget.encode("ENCSR000AKS", assembly="GRCh38", file_format="bam")
+```
+→ Returns a table of files (with download URLs) for the experiment.
+
+| file_accession | file_format | output_type | assembly | file_size | status | url |
+| --- | --- | --- | --- | --- | --- | --- |
+| ENCFF... | bam | alignments | GRCh38 | 123456 | released | https://www.encodeproject.org/files/ENCFF.../@@download/ENCFF....bam |
+
+
+**Download the files into a directory:**
+```bash
+gget encode ENCSR000AKS --file_format bam --download --out_dir ./encode_data
+```
+```python
+# Python
+gget.encode("ENCSR000AKS", file_format="bam", download=True, out_dir="./encode_data")
+```
+→ Downloads the matching files into `./encode_data`.
+
+# References
+If you use `gget encode` 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)
+
+- ENCODE Project Consortium. (2012). An integrated encyclopedia of DNA elements in the human genome. Nature. [https://doi.org/10.1038/nature11247](https://doi.org/10.1038/nature11247)
diff --git a/docs/src/en/updates.md b/docs/src/en/updates.md
index 89be22281..85e8079e7 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 encode`](encode.md): **New module** to query and download data from the [ENCODE project](https://www.encodeproject.org/). Pass an ENCODE accession (e.g. `ENCSR000AKS` experiment or `ENCFF000BXK` file) to list its file(s) with download URLs — optionally filtered by `assembly`, `file_format`, and `output_type`, and downloaded with `download=True` — or pass a free-text term to search ENCODE objects of a given `type`. Available in the Python API and on the command line. Resolves [issue 151](https://github.com/scverse/gget/issues/151).
**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..956e9756f 100644
--- a/gget/__init__.py
+++ b/gget/__init__.py
@@ -14,6 +14,7 @@
from .gget_cosmic import cosmic
from .gget_diamond import diamond
from .gget_elm import elm
+from .gget_encode import encode
from .gget_enrichr import enrichr
from .gget_g2p import g2p
from .gget_gpt import gpt
diff --git a/gget/constants.py b/gget/constants.py
index 38f90119f..51f95c0c6 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)
+# ENCODE project REST API for gget encode
+ENCODE_URL = "https://www.encodeproject.org"
+
# 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_encode.py b/gget/gget_encode.py
new file mode 100644
index 000000000..bc8bfddce
--- /dev/null
+++ b/gget/gget_encode.py
@@ -0,0 +1,265 @@
+from __future__ import annotations
+
+import json as json_package
+import os
+import re
+from typing import Any, Literal, overload
+
+import pandas as pd
+import requests
+
+from .constants import DEFAULT_REQUESTS_TIMEOUT, ENCODE_URL
+from .utils import set_up_logger
+
+logger = set_up_logger()
+
+# Columns returned for ENCODE file listings (experiment/file accessions)
+_FILE_COLUMNS = [
+ "file_accession",
+ "file_format",
+ "output_type",
+ "assembly",
+ "file_size",
+ "status",
+ "url",
+]
+# Columns returned for free-text searches
+_SEARCH_COLUMNS = [
+ "accession",
+ "assay_title",
+ "biosample_summary",
+ "target",
+ "description",
+ "status",
+ "lab",
+]
+
+# ENCODE accessions look like ENCSR000AKS (experiment), ENCFF000BXK (file), etc.
+_ACCESSION_RE = re.compile(r"^ENC[A-Z]{2}[0-9A-Za-z]+$")
+
+
+def _is_encode_accession(term: str) -> bool:
+ """Return True if 'term' looks like an ENCODE accession (e.g. ENCSR000AKS)."""
+ return bool(_ACCESSION_RE.match(term.strip()))
+
+
+def _encode_get(path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
+ """GET a JSON object from the ENCODE REST API."""
+ url = f"{ENCODE_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 ENCODE server request failed: {exc}") from exc
+
+ if response.status_code == 404:
+ raise ValueError(f"ENCODE returned 404 (not found) for '{path}'. Please double-check the accession/term.")
+ if not response.ok:
+ raise RuntimeError(
+ f"The ENCODE server returned error status code {response.status_code}. Please try again later."
+ )
+
+ return response.json()
+
+
+def _files_to_df(
+ files: list[dict[str, Any]],
+ assembly: str | None = None,
+ file_format: str | None = None,
+ output_type: str | None = None,
+) -> pd.DataFrame:
+ """Build a data frame of ENCODE files, applying optional filters."""
+ rows = []
+ for f in files:
+ if assembly is not None and f.get("assembly") != assembly:
+ continue
+ if file_format is not None and f.get("file_format") != file_format:
+ continue
+ if output_type is not None and f.get("output_type") != output_type:
+ continue
+ href = f.get("href")
+ rows.append(
+ {
+ "file_accession": f.get("accession"),
+ "file_format": f.get("file_format"),
+ "output_type": f.get("output_type"),
+ "assembly": f.get("assembly"),
+ "file_size": f.get("file_size"),
+ "status": f.get("status"),
+ "url": f"{ENCODE_URL}{href}" if href else None,
+ }
+ )
+ return pd.DataFrame(rows, columns=_FILE_COLUMNS)
+
+
+def _search_row(g: dict[str, Any]) -> dict[str, Any]:
+ """Flatten one ENCODE search result into a row of scalar values."""
+ target = g.get("target")
+ target_label = target.get("label") if isinstance(target, dict) else target
+ lab = g.get("lab")
+ lab_title = lab.get("title") if isinstance(lab, dict) else lab
+ return {
+ "accession": g.get("accession"),
+ "assay_title": g.get("assay_title"),
+ "biosample_summary": g.get("biosample_summary"),
+ "target": target_label,
+ "description": g.get("description"),
+ "status": g.get("status"),
+ "lab": lab_title,
+ }
+
+
+def _download_files(df: pd.DataFrame, out_dir: str, verbose: bool = True) -> None:
+ """Download every file in the 'url' column of 'df' into out_dir."""
+ urls = [u for u in df.get("url", []) if u]
+ if len(urls) == 0:
+ logger.warning("No downloadable files found for the given query.")
+ return
+
+ os.makedirs(out_dir, exist_ok=True)
+ for url in urls:
+ filename = url.split("/")[-1]
+ dest = os.path.join(out_dir, filename)
+ if verbose:
+ logger.info(f"Downloading {filename}...")
+ try:
+ with requests.get(url, stream=True, timeout=DEFAULT_REQUESTS_TIMEOUT) as r:
+ r.raise_for_status()
+ with open(dest, "wb") as fh:
+ for chunk in r.iter_content(chunk_size=8192):
+ fh.write(chunk)
+ except requests.exceptions.RequestException as exc:
+ logger.error(f"Failed to download {filename}: {exc}")
+
+
+@overload
+def encode(
+ search_term: str,
+ type: str = "Experiment",
+ limit: int = 10,
+ assembly: str | None = None,
+ file_format: str | None = None,
+ output_type: str | None = None,
+ download: bool = False,
+ out_dir: str = ".",
+ save: bool = False,
+ verbose: bool = True,
+ *,
+ json: Literal[True],
+) -> list[dict[str, Any]] | None: ...
+
+
+@overload
+def encode(
+ search_term: str,
+ type: str = "Experiment",
+ limit: int = 10,
+ assembly: str | None = None,
+ file_format: str | None = None,
+ output_type: str | None = None,
+ download: bool = False,
+ out_dir: str = ".",
+ save: bool = False,
+ verbose: bool = True,
+ json: Literal[False] = False,
+) -> pd.DataFrame | None: ...
+
+
+def encode(
+ search_term: str,
+ type: str = "Experiment",
+ limit: int = 10,
+ assembly: str | None = None,
+ file_format: str | None = None,
+ output_type: str | None = None,
+ download: bool = False,
+ out_dir: str = ".",
+ save: bool = False,
+ verbose: bool = True,
+ json: bool = False,
+) -> pd.DataFrame | list[dict[str, Any]] | None:
+ """Query and download data from the ENCODE project (https://www.encodeproject.org/).
+
+ Two modes, chosen automatically from 'search_term':
+ - If 'search_term' is an ENCODE accession (e.g. "ENCSR000AKS" for an
+ experiment or "ENCFF000BXK" for a file), the matching object is fetched
+ and its file(s) are returned (with download URLs).
+ - Otherwise, 'search_term' is used as a free-text query against the ENCODE
+ search endpoint and the matching objects of the given 'type' are returned.
+
+ Args:
+ - search_term ENCODE accession (experiment/file/...) or free-text search term.
+ - type ENCODE object type for free-text searches, e.g. "Experiment",
+ "File", "Biosample". Default: "Experiment".
+ - limit Maximum number of results for free-text searches. Default: 10.
+ - assembly Only return files for this genome assembly, e.g. "GRCh38". Default: None.
+ - file_format Only return files of this format, e.g. "bam", "fastq", "bigWig". Default: None.
+ - output_type Only return files of this output type, e.g. "alignments". Default: None.
+ - download If True, download the returned files into 'out_dir'. Default: False.
+ - out_dir Directory to download files into. Default: "." (current directory).
+ - 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 files 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 search term or ENCODE accession in 'search_term'.")
+
+ term = str(search_term).strip()
+ source = "encode_search"
+
+ if _is_encode_accession(term):
+ if verbose:
+ logger.info(f"Fetching ENCODE object {term}...")
+ obj = _encode_get(f"/{term}/", params={"format": "json"})
+ obj_types = obj.get("@type", [])
+
+ if "File" in obj_types:
+ source = "encode_files"
+ results_df = _files_to_df([obj], assembly, file_format, output_type)
+ elif "files" in obj:
+ source = "encode_files"
+ results_df = _files_to_df(obj.get("files", []), assembly, file_format, output_type)
+ else:
+ # Generic object: return its top-level scalar metadata as a single row
+ source = "encode_metadata"
+ scalar = {k: v for k, v in obj.items() if isinstance(v, (str, int, float, bool)) or v is None}
+ results_df = pd.DataFrame([scalar])
+
+ if download:
+ _download_files(results_df, out_dir, verbose)
+ else:
+ if verbose:
+ logger.info(f"Searching ENCODE for '{term}' (type={type})...")
+ data = _encode_get(
+ "/search/",
+ params={"type": type, "searchTerm": term, "limit": limit, "format": "json"},
+ )
+ rows = [_search_row(g) for g in data.get("@graph", [])]
+ results_df = pd.DataFrame(rows, columns=_SEARCH_COLUMNS)
+ if download:
+ logger.warning(
+ "'download' is only supported for ENCODE accessions (experiments/files), not free-text searches."
+ )
+
+ if len(results_df) == 0:
+ logger.warning(f"No ENCODE 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..c7d0e6f12 100644
--- a/gget/main.py
+++ b/gget/main.py
@@ -29,6 +29,7 @@
from .gget_cosmic import cosmic # noqa: E402
from .gget_diamond import diamond # noqa: E402
from .gget_elm import elm # noqa: E402
+from .gget_encode import encode # noqa: E402
from .gget_enrichr import enrichr # noqa: E402
from .gget_g2p import g2p # noqa: E402
from .gget_gpt import gpt # noqa: E402
@@ -1002,6 +1003,104 @@ def main() -> None:
help="DEPRECATED - json is now the default output format (convert to csv using flag [--csv]).",
)
+ ## gget encode subparser
+ encode_desc = "Query and download data from the ENCODE project (https://www.encodeproject.org/)."
+ parser_encode = parent_subparsers.add_parser(
+ "encode",
+ parents=[parent],
+ description=encode_desc,
+ help=encode_desc,
+ add_help=True,
+ formatter_class=CustomHelpFormatter,
+ )
+ parser_encode.add_argument(
+ "search_term",
+ type=str,
+ help="ENCODE accession (e.g. ENCSR000AKS or ENCFF000BXK) or a free-text search term.",
+ )
+ parser_encode.add_argument(
+ "-t",
+ "--type",
+ type=str,
+ default="Experiment",
+ required=False,
+ help="ENCODE object type for free-text searches, e.g. 'Experiment', 'File', 'Biosample'. Default: 'Experiment'.",
+ )
+ parser_encode.add_argument(
+ "-l",
+ "--limit",
+ type=int,
+ default=10,
+ required=False,
+ help="Maximum number of results for free-text searches. Default: 10.",
+ )
+ parser_encode.add_argument(
+ "-a",
+ "--assembly",
+ type=str,
+ default=None,
+ required=False,
+ help="Only return files for this genome assembly, e.g. 'GRCh38'. Default: None.",
+ )
+ parser_encode.add_argument(
+ "-ff",
+ "--file_format",
+ type=str,
+ default=None,
+ required=False,
+ help="Only return files of this format, e.g. 'bam', 'fastq', 'bigWig'. Default: None.",
+ )
+ parser_encode.add_argument(
+ "-ot",
+ "--output_type",
+ type=str,
+ default=None,
+ required=False,
+ help="Only return files of this output type, e.g. 'alignments'. Default: None.",
+ )
+ parser_encode.add_argument(
+ "-d",
+ "--download",
+ default=False,
+ action="store_true",
+ required=False,
+ help="Download the returned files into the directory given by --out_dir.",
+ )
+ parser_encode.add_argument(
+ "-od",
+ "--out_dir",
+ type=str,
+ default=".",
+ required=False,
+ help="Directory to download files into. Default: current directory.",
+ )
+ parser_encode.add_argument(
+ "-csv",
+ "--csv",
+ default=True,
+ action="store_false",
+ required=False,
+ help="Returns results in csv format instead of json.",
+ )
+ parser_encode.add_argument(
+ "-o",
+ "--out",
+ type=str,
+ required=False,
+ help=(
+ "Path to the file the results table will be saved in, e.g. path/to/directory/results.csv (or .json).\n"
+ "Default: Standard out."
+ ),
+ )
+ parser_encode.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 +3055,7 @@ def main() -> None:
"muscle": parser_muscle,
"blast": parser_blast,
"blat": parser_blat,
+ "encode": parser_encode,
"enrichr": parser_enrichr,
"archs4": parser_archs4,
"setup": parser_setup,
@@ -3502,6 +3602,43 @@ def main() -> None:
if not args.out and args.csv:
print(json.dumps(gget_results, ensure_ascii=False, indent=4))
+ ## encode return
+ if args.command == "encode":
+ encode_results = encode(
+ search_term=args.search_term,
+ type=args.type,
+ limit=args.limit,
+ assembly=args.assembly,
+ file_format=args.file_format,
+ output_type=args.output_type,
+ download=args.download,
+ out_dir=args.out_dir,
+ json=args.csv,
+ verbose=args.quiet,
+ )
+
+ # Check if the function returned something
+ if encode_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)
+ encode_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(encode_results, f, ensure_ascii=False, indent=4)
+
+ # Print results if no directory specified
+ if not args.out and not args.csv:
+ encode_results.to_csv(sys.stdout, index=False)
+ if not args.out and args.csv:
+ print(json.dumps(encode_results, ensure_ascii=False, indent=4))
+
## enrichr return
if args.command == "enrichr":
# Handle deprecated flags for backwards compatibility
diff --git a/tests/fixtures/test_encode.json b/tests/fixtures/test_encode.json
new file mode 100644
index 000000000..f06250121
--- /dev/null
+++ b/tests/fixtures/test_encode.json
@@ -0,0 +1,10 @@
+{
+ "test_encode_no_term": {
+ "type": "error",
+ "args": {
+ "search_term": ""
+ },
+ "expected_result": "ValueError",
+ "expected_msg": "Please provide a search term or ENCODE accession in 'search_term'."
+ }
+}
diff --git a/tests/test_encode.py b/tests/test_encode.py
new file mode 100644
index 000000000..354408655
--- /dev/null
+++ b/tests/test_encode.py
@@ -0,0 +1,234 @@
+import json
+import os
+import tempfile
+import unittest
+from unittest.mock import MagicMock, patch
+
+import gget.gget_encode as gget_encode
+import pandas as pd
+import requests
+from gget.gget_encode import _files_to_df, _is_encode_accession, _search_row, encode
+
+from .from_json import from_json
+
+with open("./tests/fixtures/test_encode.json") as json_file:
+ encode_dict = json.load(json_file)
+
+
+class TestEncode(unittest.TestCase, metaclass=from_json(encode_dict, encode)):
+ pass # tests loaded from json
+
+
+class _FakeResponse:
+ """Minimal stand-in for a requests.Response used to test ENCODE 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
+
+
+_EXPERIMENT_PAYLOAD = {
+ "@type": ["Experiment", "Dataset", "Item"],
+ "accession": "ENCSR000AKS",
+ "files": [
+ {
+ "accession": "ENCFF000BXK",
+ "file_format": "bam",
+ "output_type": "alignments",
+ "assembly": "GRCh38",
+ "file_size": 123,
+ "status": "released",
+ "href": "/files/ENCFF000BXK/@@download/ENCFF000BXK.bam",
+ },
+ {
+ "accession": "ENCFF000BXM",
+ "file_format": "bigBed",
+ "output_type": "peaks",
+ "assembly": "hg19",
+ "file_size": 456,
+ "status": "released",
+ "href": "/files/ENCFF000BXM/@@download/ENCFF000BXM.bigBed",
+ },
+ ],
+}
+
+_FILE_PAYLOAD = {
+ "@type": ["File", "Item"],
+ "accession": "ENCFF000BXK",
+ "file_format": "bam",
+ "output_type": "alignments",
+ "assembly": "GRCh38",
+ "file_size": 123,
+ "status": "released",
+ "href": "/files/ENCFF000BXK/@@download/ENCFF000BXK.bam",
+}
+
+_SEARCH_PAYLOAD = {
+ "@graph": [
+ {
+ "accession": "ENCSR111AAA",
+ "assay_title": "TF ChIP-seq",
+ "biosample_summary": "Homo sapiens K562",
+ "target": {"label": "CTCF"},
+ "description": "desc",
+ "status": "released",
+ "lab": {"title": "Some Lab"},
+ }
+ ]
+}
+
+
+class TestEncodeHelpers(unittest.TestCase):
+ """Network-free tests of the ENCODE helpers (issue #151)."""
+
+ def test_is_accession(self):
+ self.assertTrue(_is_encode_accession("ENCSR000AKS"))
+ self.assertTrue(_is_encode_accession("ENCFF000BXK"))
+ self.assertFalse(_is_encode_accession("CTCF K562"))
+ self.assertFalse(_is_encode_accession("chip-seq"))
+
+ def test_files_to_df_and_filter(self):
+ df = _files_to_df(_EXPERIMENT_PAYLOAD["files"])
+ self.assertEqual(list(df.columns), gget_encode._FILE_COLUMNS)
+ self.assertEqual(df.shape[0], 2)
+ self.assertEqual(
+ df.iloc[0]["url"], "https://www.encodeproject.org/files/ENCFF000BXK/@@download/ENCFF000BXK.bam"
+ )
+ # Filter by assembly
+ df2 = _files_to_df(_EXPERIMENT_PAYLOAD["files"], assembly="GRCh38")
+ self.assertEqual(df2.shape[0], 1)
+ self.assertEqual(df2.iloc[0]["file_accession"], "ENCFF000BXK")
+ # Filter by file_format
+ df3 = _files_to_df(_EXPERIMENT_PAYLOAD["files"], file_format="bigBed")
+ self.assertEqual(df3.shape[0], 1)
+ self.assertEqual(df3.iloc[0]["file_format"], "bigBed")
+
+ def test_search_row(self):
+ row = _search_row(_SEARCH_PAYLOAD["@graph"][0])
+ self.assertEqual(row["accession"], "ENCSR111AAA")
+ self.assertEqual(row["target"], "CTCF")
+ self.assertEqual(row["lab"], "Some Lab")
+
+ @patch.object(gget_encode.requests, "get")
+ def test_experiment_accession(self, mock_get):
+ mock_get.return_value = _FakeResponse(_EXPERIMENT_PAYLOAD)
+ df = encode("ENCSR000AKS", verbose=False)
+ self.assertEqual(list(df.columns), gget_encode._FILE_COLUMNS)
+ self.assertEqual(df.shape[0], 2)
+
+ @patch.object(gget_encode.requests, "get")
+ def test_file_accession(self, mock_get):
+ mock_get.return_value = _FakeResponse(_FILE_PAYLOAD)
+ df = encode("ENCFF000BXK", verbose=False)
+ self.assertEqual(df.shape[0], 1)
+ self.assertEqual(df.iloc[0]["file_accession"], "ENCFF000BXK")
+
+ @patch.object(gget_encode.requests, "get")
+ def test_search_mode_json(self, mock_get):
+ mock_get.return_value = _FakeResponse(_SEARCH_PAYLOAD)
+ result = encode("CTCF K562", json=True, verbose=False)
+ self.assertIsInstance(result, list)
+ self.assertEqual(result[0]["accession"], "ENCSR111AAA")
+ self.assertEqual(result[0]["target"], "CTCF")
+
+ @patch.object(gget_encode.requests, "get")
+ def test_no_results_returns_none(self, mock_get):
+ mock_get.return_value = _FakeResponse({"@graph": []})
+ self.assertIsNone(encode("nonexistent term xyz", verbose=False))
+
+ @patch.object(gget_encode.requests, "get")
+ def test_http_error_raises(self, mock_get):
+ mock_get.return_value = _FakeResponse({}, ok=False, status_code=500)
+ with self.assertRaises(RuntimeError):
+ encode("ENCSR000AKS", verbose=False)
+
+ def test_empty_search_term_raises(self):
+ # Covers the empty/None search_term ValueError branch.
+ with self.assertRaises(ValueError):
+ encode(" ", verbose=False)
+
+ def test_files_to_df_output_type_filter(self):
+ # Covers the output_type filter branch.
+ df = _files_to_df(_EXPERIMENT_PAYLOAD["files"], output_type="peaks")
+ self.assertEqual(df.shape[0], 1)
+ self.assertEqual(df.iloc[0]["output_type"], "peaks")
+
+ @patch.object(gget_encode.requests, "get")
+ def test_encode_get_request_exception(self, mock_get):
+ # Covers the requests.RequestException -> RuntimeError branch in _encode_get.
+ mock_get.side_effect = requests.exceptions.ConnectionError("no network")
+ with self.assertRaises(RuntimeError):
+ encode("ENCSR000AKS", verbose=False)
+
+ @patch.object(gget_encode.requests, "get")
+ def test_encode_get_404(self, mock_get):
+ # Covers the 404 -> ValueError branch in _encode_get.
+ mock_get.return_value = _FakeResponse({}, ok=False, status_code=404)
+ with self.assertRaises(ValueError):
+ encode("ENCSR000AKS", verbose=False)
+
+ @patch.object(gget_encode.requests, "get")
+ def test_generic_object_metadata(self, mock_get):
+ # Covers the generic-object (non-File, no 'files') metadata branch.
+ mock_get.return_value = _FakeResponse(
+ {"@type": ["Biosample", "Item"], "accession": "ENCBS000AAA", "status": "released", "nested": {"x": 1}}
+ )
+ df = encode("ENCBS000AAA", verbose=False)
+ self.assertEqual(df.iloc[0]["accession"], "ENCBS000AAA")
+
+ @patch.object(gget_encode, "_download_files")
+ @patch.object(gget_encode.requests, "get")
+ def test_accession_download_and_verbose(self, mock_get, mock_dl):
+ # Covers the verbose-logging line and the download branch (accession path).
+ mock_get.return_value = _FakeResponse(_EXPERIMENT_PAYLOAD)
+ encode("ENCSR000AKS", download=True, verbose=True)
+ self.assertTrue(mock_dl.called)
+
+ @patch.object(gget_encode.requests, "get")
+ def test_search_verbose_and_download_warning(self, mock_get):
+ # Covers the search-path verbose line and the download-not-supported warning.
+ mock_get.return_value = _FakeResponse(_SEARCH_PAYLOAD)
+ result = encode("CTCF K562", download=True, verbose=True)
+ self.assertEqual(result.iloc[0]["accession"], "ENCSR111AAA")
+
+ @patch.object(gget_encode.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(_EXPERIMENT_PAYLOAD)
+ with tempfile.TemporaryDirectory() as tmp:
+ cwd = os.getcwd()
+ os.chdir(tmp)
+ try:
+ encode("ENCSR000AKS", save=True, verbose=False)
+ self.assertTrue(any(f.endswith(".csv") for f in os.listdir(".")))
+ encode("ENCSR000AKS", save=True, json=True, verbose=False)
+ self.assertTrue(any(f.endswith(".json") for f in os.listdir(".")))
+ finally:
+ os.chdir(cwd)
+
+ def test_download_files_no_urls(self):
+ # Covers the "no downloadable files" early-return branch.
+ gget_encode._download_files(pd.DataFrame({"url": []}), "unused_dir")
+
+ @patch.object(gget_encode.requests, "get")
+ def test_download_files_writes_and_handles_error(self, mock_get):
+ # Covers the streaming-download body and the per-file error branch.
+ resp = MagicMock()
+ resp.raise_for_status.return_value = None
+ resp.iter_content.return_value = [b"chunk1", b"chunk2"]
+ mock_get.return_value.__enter__.return_value = resp
+ df = pd.DataFrame({"url": ["https://www.encodeproject.org/files/ENCFF000BXK/@@download/ENCFF000BXK.bam"]})
+ with tempfile.TemporaryDirectory() as tmp:
+ gget_encode._download_files(df, tmp, verbose=True)
+ self.assertTrue(os.path.exists(os.path.join(tmp, "ENCFF000BXK.bam")))
+ # Error branch: requests.get raises -> logged, not raised
+ mock_get.side_effect = requests.exceptions.ConnectionError("boom")
+ gget_encode._download_files(df, tmp, verbose=False)
+
+
+if __name__ == "__main__":
+ unittest.main()