diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md
index b112fd0f2..a57874262 100644
--- a/docs/src/SUMMARY.md
+++ b/docs/src/SUMMARY.md
@@ -28,6 +28,7 @@
* [gget mutate](en/mutate.md)
* [gget opentargets](en/opentargets.md)
* [gget pdb](en/pdb.md)
+* [gget reactome](en/reactome.md)
* [gget ref](en/ref.md)
* [gget search](en/search.md)
* [gget setup](en/setup.md)
@@ -71,6 +72,7 @@
* [gget mutate](es/mutate.md)
* [gget opentargets](es/opentargets.md)
* [gget pdb](es/pdb.md)
+* [gget reactome](es/reactome.md)
* [gget ref](es/ref.md)
* [gget search](es/search.md)
* [gget setup](es/setup.md)
diff --git a/docs/src/en/reactome.md b/docs/src/en/reactome.md
new file mode 100644
index 000000000..5d24685c3
--- /dev/null
+++ b/docs/src/en/reactome.md
@@ -0,0 +1,158 @@
+[ View page source on GitHub ](https://github.com/scverse/gget/blob/main/docs/src/en/reactome.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 reactome 🔬
+Query the [Reactome](https://reactome.org/) pathway knowledgebase using its [ContentService REST API](https://reactome.org/dev/content-service).
+Return format: JSON/CSV (command-line) or data frame (Python).
+
+`gget reactome` supports several kinds of queries, selected with the `resource` argument:
+- `pathways` (default): return the Reactome pathways a given identifier (e.g. a UniProt accession) participates in.
+- `search`: full-text search of the Reactome knowledgebase (pathways, reactions, physical entities, ...).
+- `entity`: return details for a Reactome stable ID (e.g. `R-HSA-6804754`).
+- `interactors`: return the molecular interactors of an identifier (IntAct static interactors).
+- `orthology`: project a Reactome stable ID to its ortholog in another species (requires `--species`).
+- `event-hierarchy`: return the full pathway/reaction hierarchy for a species.
+
+**Positional argument**
+`query`
+Identifier, search term or species to query. Its meaning depends on `resource`:
+- `resource="pathways"`: an identifier, e.g. UniProt accession `P04637`.
+- `resource="search"`: a free-text search term, e.g. `TP53`.
+- `resource="entity"`: a Reactome stable ID, e.g. `R-HSA-6804754`.
+- `resource="interactors"`: a molecule accession, e.g. UniProt `P04637`.
+- `resource="orthology"`: a Reactome stable ID to project, e.g. `R-HSA-6804754`.
+- `resource="event-hierarchy"`: a species name or NCBI taxonomy ID, e.g. `Homo sapiens`.
+
+**Optional arguments**
+`-r` `--resource`
+Type of query to perform. Options: `pathways` (default), `search`, `entity`, `interactors`, `orthology`, `event-hierarchy`.
+
+`-s` `--source`
+Identifier resource/database for `resource="pathways"`, e.g. `UniProt` (default), `Ensembl`, `ChEBI`, `NCBI`.
+
+`-sp` `--species`
+Species, as a name (e.g. `Homo sapiens`) or NCBI taxonomy ID (e.g. `9606`). Filters `resource="pathways"` and `resource="search"`; is the **required target** for `resource="orthology"`. Default: None.
+
+`-t` `--types`
+Restrict `resource="search"` results to one or more entry types, e.g. `Pathway`, `Reaction`, `Protein`. Default: None (all types).
+
+`-o` `--out`
+Path to the file the results will be saved in, e.g. path/to/directory/results.json. Default: Standard out.
+
+**Flags**
+`-csv` `--csv`
+Command-line only. Returns the output in CSV format, instead of JSON 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
+
+**Get the Reactome pathways a protein participates in**
+
+```bash
+gget reactome P04637 --species "Homo sapiens"
+```
+
+```python
+import gget
+gget.reactome("P04637", species="Homo sapiens")
+```
+
+→ Returns the Reactome pathways the UniProt protein P04637 (human TP53) participates in.
+
+| stable_id | name | species | schema_class | in_disease |
+|---------------|------------------------------------------------------|--------------|--------------|------------|
+| R-HSA-111448 | Activation of NOXA and translocation to mitochondria | Homo sapiens | Pathway | False |
+| R-HSA-139915 | Activation of PUMA and translocation to mitochondria | Homo sapiens | Pathway | False |
+| ... | ... | ... | ... | ... |
+
+
+
+**Search the Reactome knowledgebase**
+
+```bash
+gget reactome TP53 -r search -t Pathway -sp "Homo sapiens"
+```
+```python
+import gget
+gget.reactome("TP53", resource="search", types="Pathway", species="Homo sapiens")
+```
+
+→ Returns Reactome pathways matching the search term "TP53".
+
+| stable_id | name | type | species | reactome_id |
+|---------------|-------------------------------|---------|--------------|---------------|
+| R-HSA-6804754 | Regulation of TP53 Expression | Pathway | Homo sapiens | R-HSA-6804754 |
+| R-HSA-5633007 | Regulation of TP53 Activity | Pathway | Homo sapiens | R-HSA-5633007 |
+| ... | ... | ... | ... | ... |
+
+
+
+**Get details for a Reactome entry by stable ID**
+
+```bash
+gget reactome R-HSA-6804754 -r entity
+```
+```python
+import gget
+gget.reactome("R-HSA-6804754", resource="entity")
+```
+
+→ Returns details for the Reactome entry R-HSA-6804754.
+
+| stable_id | name | schema_class | species | in_disease | summation |
+|---------------|-------------------------------|--------------|--------------|------------|------------------|
+| R-HSA-6804754 | Regulation of TP53 Expression | Pathway | Homo sapiens | False | Transcription... |
+
+
+
+**Get the molecular interactors of a protein**
+
+```bash
+gget reactome P04637 -r interactors
+```
+```python
+gget.reactome("P04637", resource="interactors")
+```
+
+→ Returns the IntAct interactors of P04637 (columns: `interactor_acc`, `interactor_name`, `score`, `evidences`), e.g. MDM2, with an interaction confidence score.
+
+
+
+**Project a pathway to another species (orthology)**
+
+```bash
+gget reactome R-HSA-6804754 -r orthology -sp "Mus musculus"
+```
+```python
+gget.reactome("R-HSA-6804754", resource="orthology", species="Mus musculus")
+```
+
+→ Returns the mouse ortholog of the human pathway (`R-MMU-6804754`, "Regulation of TP53 Expression", Mus musculus).
+
+
+
+**Get the full event (pathway/reaction) hierarchy for a species**
+
+```bash
+gget reactome "Homo sapiens" -r event-hierarchy
+```
+```python
+gget.reactome("Homo sapiens", resource="event-hierarchy")
+```
+
+→ Returns the entire human event hierarchy flattened to one row per event (columns: `stable_id`, `name`, `type`, `species`, `parent_id`, `level`). This is a large table — use `parent_id`/`level` to navigate the tree.
+
+> The returned DataFrame's `.attrs["reactome_release"]` records the Reactome release version (e.g. `97`) for reproducibility.
+
+
+# References
+If you use `gget reactome` in a publication, please cite the following articles:
+
+- Marc Gillespie, Bijay Jassal, Ralf Stephan, Marija Milacic, Karen Rothfels, Andrea Senff-Ribeiro, Johannes Griss, Cristoffer Sevilla, Lisa Matthews, Chuqiao Gong, Chuan Deng, Thawfeek Varusai, Eliot Ragueneau, Yusra Haider, Bruce May, Veronica Shamovsky, Joel Weiser, Timothy Brunson, Nasim Sanati, Liam Beckman, Xiang Shao, Antonio Fabregat, Konstantinos Sidiropoulos, Julieth Murillo, Guilherme Viteri, Justin Cook, Solomon Shorser, Gary Bader, Emek Demir, Chris Sander, Robin Haw, Guanming Wu, Lincoln Stein, Henning Hermjakob, Peter D'Eustachio (2022). The reactome pathway knowledgebase 2022. Nucleic Acids Research, Volume 50, Issue D1, 7 January 2022, Pages D687–D692, [https://doi.org/10.1093/nar/gkab1028](https://doi.org/10.1093/nar/gkab1028)
+
+- 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)
diff --git a/docs/src/en/updates.md b/docs/src/en/updates.md
index 89be22281..37a135d20 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 reactome`](reactome.md): **New module** to query the [Reactome](https://reactome.org/) pathway knowledgebase via its ContentService REST API. Query types are selected with the `resource` argument: `pathways` (the pathways an identifier such as a UniProt accession participates in), `search` (full-text search), `entity` (details for a Reactome stable ID), `interactors` (molecular interactors of an identifier), `orthology` (project a stable ID to its ortholog in another species), and `event-hierarchy` (the full pathway/reaction hierarchy for a species). Returned data frames carry the Reactome release version in `.attrs["reactome_release"]` for reproducibility.
**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/docs/src/es/reactome.md b/docs/src/es/reactome.md
new file mode 100644
index 000000000..c26837881
--- /dev/null
+++ b/docs/src/es/reactome.md
@@ -0,0 +1,158 @@
+[ Ver el codigo fuente de la pagina en GitHub ](https://github.com/scverse/gget/blob/main/docs/src/es/reactome.md)
+
+> Parámetros de Python són iguales a los parámetros largos (`--parámetro`) de Terminal, si no especificado de otra manera. Banderas son parámetros de verdadero o falso (True/False) en Python. El manuál para cualquier modulo de gget se puede llamar desde la Terminal con la bandera `-h` `--help`.
+# gget reactome 🔬
+Consulte la base de conocimientos de rutas [Reactome](https://reactome.org/) usando su [API REST ContentService](https://reactome.org/dev/content-service).
+Regresa: JSON/CSV (lÃnea de comandos) o data frame (Python).
+
+`gget reactome` admite varios tipos de consulta, seleccionados con el argumento `resource`:
+- `pathways` (por defecto): regresa las rutas de Reactome en las que participa un identificador dado (p. ej. un número de acceso UniProt).
+- `search`: búsqueda de texto completo en la base de conocimientos de Reactome (rutas, reacciones, entidades fÃsicas, ...).
+- `entity`: regresa los detalles de un ID estable de Reactome (p. ej. `R-HSA-6804754`).
+- `interactors`: regresa los interactores moleculares de un identificador (interactores estáticos de IntAct).
+- `orthology`: proyecta un ID estable de Reactome a su ortólogo en otra especie (requiere `--species`).
+- `event-hierarchy`: regresa la jerarquÃa completa de rutas/reacciones de una especie.
+
+**Parámetro posicional**
+`query`
+Identificador, término de búsqueda o especie a consultar. Su significado depende de `resource`:
+- `resource="pathways"`: un identificador, p. ej. el número de acceso UniProt `P04637`.
+- `resource="search"`: un término de búsqueda de texto libre, p. ej. `TP53`.
+- `resource="entity"`: un ID estable de Reactome, p. ej. `R-HSA-6804754`.
+- `resource="interactors"`: un número de acceso de molécula, p. ej. UniProt `P04637`.
+- `resource="orthology"`: un ID estable de Reactome a proyectar, p. ej. `R-HSA-6804754`.
+- `resource="event-hierarchy"`: un nombre de especie o ID de taxonomÃa de NCBI, p. ej. `Homo sapiens`.
+
+**Parámetros opcionales**
+`-r` `--resource`
+Tipo de consulta a realizar. Opciones: `pathways` (por defecto), `search`, `entity`, `interactors`, `orthology`, `event-hierarchy`.
+
+`-s` `--source`
+Recurso/base de datos del identificador para `resource="pathways"`, p. ej. `UniProt` (por defecto), `Ensembl`, `ChEBI`, `NCBI`.
+
+`-sp` `--species`
+Especie, como nombre (p. ej. `Homo sapiens`) o ID de taxonomÃa de NCBI (p. ej. `9606`). Filtra `resource="pathways"` y `resource="search"`; es el **objetivo requerido** para `resource="orthology"`. Por defecto: None.
+
+`-t` `--types`
+Restringe los resultados de `resource="search"` a uno o más tipos de entrada, p. ej. `Pathway`, `Reaction`, `Protein`. Por defecto: None (todos los tipos).
+
+`-o` `--out`
+Ruta al archivo en el que se guardarán los resultados, p. ej. ruta/al/directorio/resultados.json. Por defecto: salida estándar (STDOUT).
+
+**Banderas**
+`-csv` `--csv`
+Solo para la Terminal. Regresa los resultados en formato CSV, en lugar de formato JSON.
+Para Python: usa `json=True` para regresar los resultados en formato JSON.
+
+`-q` `--quiet`
+Solo para la Terminal. Impide que la información de progreso se muestre durante la corrida.
+Para Python: usa `verbose=False` para impedir que la información de progreso se muestre durante la corrida.
+
+
+### Ejemplos
+
+**Obtenga las rutas de Reactome en las que participa una proteÃna**
+
+```bash
+gget reactome P04637 --species "Homo sapiens"
+```
+
+```python
+import gget
+gget.reactome("P04637", species="Homo sapiens")
+```
+
+→ Regresa las rutas de Reactome en las que participa la proteÃna UniProt P04637 (TP53 humano).
+
+| stable_id | name | species | schema_class | in_disease |
+|---------------|------------------------------------------------------|--------------|--------------|------------|
+| R-HSA-111448 | Activation of NOXA and translocation to mitochondria | Homo sapiens | Pathway | False |
+| R-HSA-139915 | Activation of PUMA and translocation to mitochondria | Homo sapiens | Pathway | False |
+| ... | ... | ... | ... | ... |
+
+
+
+**Busque en la base de conocimientos de Reactome**
+
+```bash
+gget reactome TP53 -r search -t Pathway -sp "Homo sapiens"
+```
+```python
+import gget
+gget.reactome("TP53", resource="search", types="Pathway", species="Homo sapiens")
+```
+
+→ Regresa las rutas de Reactome que coinciden con el término de búsqueda "TP53".
+
+| stable_id | name | type | species | reactome_id |
+|---------------|-------------------------------|---------|--------------|---------------|
+| R-HSA-6804754 | Regulation of TP53 Expression | Pathway | Homo sapiens | R-HSA-6804754 |
+| R-HSA-5633007 | Regulation of TP53 Activity | Pathway | Homo sapiens | R-HSA-5633007 |
+| ... | ... | ... | ... | ... |
+
+
+
+**Obtenga los detalles de una entrada de Reactome por su ID estable**
+
+```bash
+gget reactome R-HSA-6804754 -r entity
+```
+```python
+import gget
+gget.reactome("R-HSA-6804754", resource="entity")
+```
+
+→ Regresa los detalles de la entrada de Reactome R-HSA-6804754.
+
+| stable_id | name | schema_class | species | in_disease | summation |
+|---------------|-------------------------------|--------------|--------------|------------|------------------|
+| R-HSA-6804754 | Regulation of TP53 Expression | Pathway | Homo sapiens | False | Transcription... |
+
+
+
+**Obtenga los interactores moleculares de una proteÃna**
+
+```bash
+gget reactome P04637 -r interactors
+```
+```python
+gget.reactome("P04637", resource="interactors")
+```
+
+→ Regresa los interactores de IntAct de P04637 (columnas: `interactor_acc`, `interactor_name`, `score`, `evidences`), p. ej. MDM2, con una puntuación de confianza de la interacción.
+
+
+
+**Proyecte una ruta a otra especie (ortologÃa)**
+
+```bash
+gget reactome R-HSA-6804754 -r orthology -sp "Mus musculus"
+```
+```python
+gget.reactome("R-HSA-6804754", resource="orthology", species="Mus musculus")
+```
+
+→ Regresa el ortólogo en ratón de la ruta humana (`R-MMU-6804754`, "Regulation of TP53 Expression", Mus musculus).
+
+
+
+**Obtenga la jerarquÃa completa de eventos (rutas/reacciones) de una especie**
+
+```bash
+gget reactome "Homo sapiens" -r event-hierarchy
+```
+```python
+gget.reactome("Homo sapiens", resource="event-hierarchy")
+```
+
+→ Regresa toda la jerarquÃa de eventos humana aplanada a una fila por evento (columnas: `stable_id`, `name`, `type`, `species`, `parent_id`, `level`). Es una tabla grande — use `parent_id`/`level` para navegar el árbol.
+
+> El DataFrame regresado guarda la versión de lanzamiento de Reactome en `.attrs["reactome_release"]` (p. ej. `97`) para reproducibilidad.
+
+
+# Citar
+Si utiliza `gget reactome` en una publicación, favor de citar los siguientes artÃculos:
+
+- Marc Gillespie, Bijay Jassal, Ralf Stephan, Marija Milacic, Karen Rothfels, Andrea Senff-Ribeiro, Johannes Griss, Cristoffer Sevilla, Lisa Matthews, Chuqiao Gong, Chuan Deng, Thawfeek Varusai, Eliot Ragueneau, Yusra Haider, Bruce May, Veronica Shamovsky, Joel Weiser, Timothy Brunson, Nasim Sanati, Liam Beckman, Xiang Shao, Antonio Fabregat, Konstantinos Sidiropoulos, Julieth Murillo, Guilherme Viteri, Justin Cook, Solomon Shorser, Gary Bader, Emek Demir, Chris Sander, Robin Haw, Guanming Wu, Lincoln Stein, Henning Hermjakob, Peter D'Eustachio (2022). The reactome pathway knowledgebase 2022. Nucleic Acids Research, Volume 50, Issue D1, 7 January 2022, Pages D687–D692, [https://doi.org/10.1093/nar/gkab1028](https://doi.org/10.1093/nar/gkab1028)
+
+- 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)
diff --git a/gget/__init__.py b/gget/__init__.py
index f56cbcddc..1ed240a05 100644
--- a/gget/__init__.py
+++ b/gget/__init__.py
@@ -22,6 +22,7 @@
from .gget_mutate import mutate
from .gget_opentargets import opentargets
from .gget_pdb import pdb
+from .gget_reactome import reactome
from .gget_ref import ref
from .gget_search import search
from .gget_seq import seq
diff --git a/gget/gget_reactome.py b/gget/gget_reactome.py
new file mode 100644
index 000000000..57f56bfdc
--- /dev/null
+++ b/gget/gget_reactome.py
@@ -0,0 +1,399 @@
+from __future__ import annotations
+
+import json as json_
+import re
+from importlib.metadata import PackageNotFoundError, version
+from typing import Any
+
+import pandas as pd
+
+from .utils import HTTPStatusError, http_json, set_up_logger
+
+logger = set_up_logger()
+
+try:
+ _GGET_VERSION = version("gget")
+except PackageNotFoundError:
+ _GGET_VERSION = "unknown"
+
+# Reactome ContentService REST API (https://reactome.org/dev/content-service)
+REACTOME_CONTENT_API = "https://reactome.org/ContentService"
+
+_REACTOME_HEADERS = {
+ "User-Agent": f"gget/{_GGET_VERSION} (+https://github.com/scverse/gget)",
+ "Accept": "application/json",
+}
+
+# Supported values for the 'resource' argument
+REACTOME_RESOURCES = ("pathways", "search", "entity", "interactors", "orthology", "event-hierarchy")
+
+# Regex to strip the ... tags Reactome wraps around
+# matched search terms.
+_HTML_TAG_RE = re.compile(r"<[^>]+>")
+
+
+def _strip_html(text: Any) -> Any:
+ """Remove HTML highlight tags returned by the Reactome search endpoint."""
+ if isinstance(text, str):
+ return _HTML_TAG_RE.sub("", text)
+ return text
+
+
+def _is_http_status(error: Exception, status: int) -> bool:
+ """True if the error raised by http_json corresponds to the given HTTP status."""
+ return isinstance(error, HTTPStatusError) and error.status_code == status
+
+
+def _reactome_release() -> str | None:
+ """Return the current Reactome release version (e.g. '97'), or None if unavailable."""
+ # This endpoint returns a plain-text integer; requesting application/json yields HTTP 406.
+ headers = {**_REACTOME_HEADERS, "Accept": "text/plain"}
+ try:
+ return str(http_json("GET", f"{REACTOME_CONTENT_API}/data/database/version", headers=headers)).strip()
+ except (RuntimeError, ValueError):
+ return None
+
+
+def _reactome_pathways(
+ query: str,
+ source: str = "UniProt",
+ species: str | None = None,
+ verbose: bool = True,
+) -> pd.DataFrame:
+ """Map an identifier to the Reactome pathways it participates in.
+
+ Uses the ContentService '/data/mapping/{source}/{identifier}/pathways' endpoint.
+ """
+ if verbose:
+ logger.info(f"Querying Reactome for pathways containing {source} identifier '{query}'.")
+
+ params = {}
+ if species is not None:
+ params["species"] = species
+
+ try:
+ payload = http_json(
+ "GET",
+ f"{REACTOME_CONTENT_API}/data/mapping/{source}/{query}/pathways",
+ context="Reactome ContentService (pathways)",
+ headers=_REACTOME_HEADERS,
+ params=params,
+ )
+ except RuntimeError as e:
+ # Reactome returns 404 when the identifier is not found / has no mapped pathways.
+ if _is_http_status(e, 404):
+ logger.warning(f"No Reactome pathways found for {source} identifier '{query}'.")
+ return pd.DataFrame(columns=["stable_id", "name", "species", "schema_class", "in_disease"])
+ raise
+
+ rows = [
+ {
+ "stable_id": entry.get("stId"),
+ "name": entry.get("displayName"),
+ "species": entry.get("speciesName"),
+ "schema_class": entry.get("schemaClass"),
+ "in_disease": entry.get("isInDisease"),
+ }
+ for entry in payload
+ ]
+ return pd.DataFrame(rows, columns=["stable_id", "name", "species", "schema_class", "in_disease"])
+
+
+def _reactome_search(
+ query: str,
+ species: str | None = None,
+ types: str | None = None,
+ verbose: bool = True,
+) -> pd.DataFrame:
+ """Full-text search of the Reactome knowledgebase.
+
+ Uses the ContentService '/search/query' endpoint and flattens the grouped results.
+ """
+ if verbose:
+ logger.info(f"Searching Reactome for '{query}'.")
+
+ params: dict[str, Any] = {"query": query, "cluster": "true"}
+ if species is not None:
+ params["species"] = species
+ if types is not None:
+ params["types"] = types
+
+ try:
+ payload = http_json(
+ "GET",
+ f"{REACTOME_CONTENT_API}/search/query",
+ context="Reactome ContentService (search)",
+ headers=_REACTOME_HEADERS,
+ params=params,
+ )
+ except RuntimeError as e:
+ # Reactome returns 404 when the search yields no results.
+ if _is_http_status(e, 404):
+ logger.warning(f"No Reactome search results found for '{query}'.")
+ return pd.DataFrame(columns=["stable_id", "name", "type", "species", "reactome_id"])
+ raise
+
+ rows = []
+ for group in payload.get("results", []):
+ for entry in group.get("entries", []):
+ entry_species = entry.get("species")
+ if isinstance(entry_species, list):
+ entry_species = entry_species[0] if entry_species else None
+ rows.append(
+ {
+ "stable_id": entry.get("stId"),
+ "name": _strip_html(entry.get("name")),
+ "type": entry.get("exactType") or entry.get("type"),
+ "species": entry_species,
+ "reactome_id": entry.get("id"),
+ }
+ )
+ return pd.DataFrame(rows, columns=["stable_id", "name", "type", "species", "reactome_id"])
+
+
+def _reactome_entity(query: str, verbose: bool = True) -> pd.DataFrame:
+ """Fetch details for a Reactome entry (pathway, reaction, physical entity, ...) by stable ID.
+
+ Uses the ContentService '/data/query/{id}' endpoint.
+ """
+ if verbose:
+ logger.info(f"Fetching Reactome entry '{query}'.")
+
+ try:
+ payload = http_json(
+ "GET",
+ f"{REACTOME_CONTENT_API}/data/query/{query}",
+ context="Reactome ContentService (entity)",
+ headers=_REACTOME_HEADERS,
+ )
+ except RuntimeError as e:
+ if _is_http_status(e, 404):
+ raise ValueError(
+ f"No Reactome entry found for identifier '{query}'. "
+ "Provide a valid Reactome stable ID (e.g. 'R-HSA-6804754') or database ID."
+ ) from e
+ raise
+
+ name = payload.get("displayName")
+ if name is None:
+ names = payload.get("name")
+ if isinstance(names, list) and names:
+ name = names[0]
+
+ row = {
+ "stable_id": payload.get("stId"),
+ "name": name,
+ "schema_class": payload.get("schemaClass"),
+ "species": payload.get("speciesName"),
+ "in_disease": payload.get("isInDisease"),
+ "summation": None,
+ }
+ summation = payload.get("summation")
+ if isinstance(summation, list) and summation:
+ row["summation"] = _strip_html(summation[0].get("text"))
+
+ return pd.DataFrame([row], columns=["stable_id", "name", "schema_class", "species", "in_disease", "summation"])
+
+
+def _reactome_species_dbid(species: str) -> int:
+ """Resolve a species name or NCBI taxonomy ID to its Reactome species dbId."""
+ payload = http_json("GET", f"{REACTOME_CONTENT_API}/data/species/main", headers=_REACTOME_HEADERS)
+ wanted = str(species).strip().lower()
+ for entry in payload:
+ if wanted in (str(entry.get("displayName", "")).lower(), str(entry.get("taxId", "")).lower()):
+ return int(entry["dbId"])
+ raise ValueError(
+ f"Species '{species}' was not found among Reactome's species. Provide a Reactome species "
+ "name (e.g. 'Mus musculus') or NCBI taxonomy ID (e.g. '10090')."
+ )
+
+
+def _reactome_interactors(query: str, verbose: bool = True) -> pd.DataFrame:
+ """Return the molecular interactors of an identifier (IntAct static interactors).
+
+ Uses the ContentService '/interactors/static/molecule/{acc}/details' endpoint.
+ """
+ if verbose:
+ logger.info(f"Querying Reactome interactors for '{query}'.")
+
+ columns = ["interactor_acc", "interactor_name", "score", "evidences"]
+ try:
+ payload = http_json(
+ "GET",
+ f"{REACTOME_CONTENT_API}/interactors/static/molecule/{query}/details",
+ context="Reactome ContentService (interactors)",
+ headers=_REACTOME_HEADERS,
+ )
+ except RuntimeError as e:
+ if _is_http_status(e, 404):
+ logger.warning(f"No Reactome interactors found for '{query}'.")
+ return pd.DataFrame(columns=columns)
+ raise
+
+ entities = payload.get("entities", [])
+ interactors = entities[0].get("interactors", []) if entities else []
+ rows = [
+ {
+ "interactor_acc": it.get("acc"),
+ "interactor_name": it.get("alias"),
+ "score": it.get("score"),
+ "evidences": it.get("evidences"),
+ }
+ for it in interactors
+ ]
+ return pd.DataFrame(rows, columns=columns)
+
+
+def _reactome_orthology(query: str, species: str | None, verbose: bool = True) -> pd.DataFrame:
+ """Project a Reactome event/entity to its ortholog in another species.
+
+ Uses the ContentService '/data/orthology/{id}/species/{speciesDbId}' endpoint.
+ """
+ if species is None:
+ raise ValueError("resource='orthology' requires a target 'species' (e.g. species='Mus musculus' or '10090').")
+
+ species_dbid = _reactome_species_dbid(species)
+ if verbose:
+ logger.info(f"Projecting Reactome entry '{query}' to species '{species}'.")
+
+ try:
+ payload = http_json(
+ "GET",
+ f"{REACTOME_CONTENT_API}/data/orthology/{query}/species/{species_dbid}",
+ context="Reactome ContentService (orthology)",
+ headers=_REACTOME_HEADERS,
+ )
+ except RuntimeError as e:
+ if _is_http_status(e, 404):
+ raise ValueError(
+ f"No Reactome ortholog found for '{query}' in species '{species}'. Provide a valid "
+ "Reactome stable ID (e.g. 'R-HSA-6804754') and a species Reactome covers."
+ ) from e
+ raise
+
+ row = {
+ "stable_id": payload.get("stId"),
+ "name": payload.get("displayName"),
+ "species": payload.get("speciesName"),
+ "schema_class": payload.get("schemaClass"),
+ }
+ return pd.DataFrame([row], columns=["stable_id", "name", "species", "schema_class"])
+
+
+def _reactome_event_hierarchy(query: str, verbose: bool = True) -> pd.DataFrame:
+ """Return the full event (pathway/reaction) hierarchy for a species as a flat adjacency table.
+
+ Uses the ContentService '/data/eventsHierarchy/{species}' endpoint; the nested tree is flattened
+ to one row per event with its parent_id and nesting level.
+ """
+ if verbose:
+ logger.info(f"Fetching the Reactome event hierarchy for species '{query}'.")
+
+ columns = ["stable_id", "name", "type", "species", "parent_id", "level"]
+ try:
+ payload = http_json(
+ "GET",
+ f"{REACTOME_CONTENT_API}/data/eventsHierarchy/{query}",
+ context="Reactome ContentService (event-hierarchy)",
+ headers=_REACTOME_HEADERS,
+ )
+ except RuntimeError as e:
+ if _is_http_status(e, 404):
+ raise ValueError(
+ f"No Reactome event hierarchy found for species '{query}'. Provide a Reactome species "
+ "name (e.g. 'Homo sapiens') or NCBI taxonomy ID (e.g. '9606')."
+ ) from e
+ raise
+
+ rows: list[dict[str, Any]] = []
+
+ def _walk(node: dict[str, Any], parent_id: str | None, level: int) -> None:
+ rows.append(
+ {
+ "stable_id": node.get("stId"),
+ "name": node.get("name"),
+ "type": node.get("type"),
+ "species": node.get("species"),
+ "parent_id": parent_id,
+ "level": level,
+ }
+ )
+ for child in node.get("children", []) or []:
+ _walk(child, node.get("stId"), level + 1)
+
+ for top in payload:
+ _walk(top, None, 0)
+
+ return pd.DataFrame(rows, columns=columns)
+
+
+def reactome(
+ query: str,
+ resource: str = "pathways",
+ source: str = "UniProt",
+ species: str | None = None,
+ types: str | None = None,
+ json: bool = False,
+ verbose: bool = True,
+) -> pd.DataFrame | list[dict[str, Any]]:
+ """Query the Reactome pathway knowledgebase (https://reactome.org/).
+
+ Args:
+ - query Identifier, search term or species to query (str). Its meaning depends on 'resource':
+ - resource="pathways": an identifier (e.g. UniProt accession 'P04637').
+ - resource="search": a free-text search term (e.g. 'TP53').
+ - resource="entity": a Reactome stable ID (e.g. 'R-HSA-6804754').
+ - resource="interactors": a molecule accession (e.g. UniProt 'P04637').
+ - resource="orthology": a Reactome stable ID to project to another species.
+ - resource="event-hierarchy": a species name or NCBI taxonomy ID.
+ - resource Type of query to perform (str). One of:
+ - "pathways" (default): pathways the identifier participates in.
+ - "search": full-text search of the Reactome knowledgebase.
+ - "entity": details for a Reactome stable ID.
+ - "interactors": molecular interactors of an identifier (IntAct static).
+ - "orthology": project a stable ID to its ortholog in another species
+ (requires 'species').
+ - "event-hierarchy": the full pathway/reaction hierarchy for a species.
+ - source Identifier resource/database for resource="pathways" (str), e.g. 'UniProt',
+ 'Ensembl', 'ChEBI', 'NCBI'. Default: "UniProt".
+ - species Species as a name (e.g. 'Homo sapiens') or NCBI taxonomy ID (e.g. '9606').
+ Filters resource="pathways"/"search"; is the required target for resource="orthology".
+ Default: None.
+ - types Restrict resource="search" results to one or more entry types (str), e.g.
+ 'Pathway', 'Reaction', 'Protein'. Default: None (all types).
+ - json If True, return the result as a JSON-serializable list of dicts instead of a
+ DataFrame (default: False).
+ - verbose True/False whether to print progress information. Default: True.
+
+ Returns the requested information as a DataFrame (or list of dicts if json=True). The DataFrame's
+ `.attrs["reactome_release"]` carries the Reactome release version, for reproducibility.
+ """
+ if resource not in REACTOME_RESOURCES:
+ raise ValueError(f"Argument 'resource' must be one of {REACTOME_RESOURCES}, not '{resource}'.")
+
+ if not isinstance(query, str) or not query.strip():
+ raise ValueError("Argument 'query' must be a non-empty string.")
+
+ if resource == "pathways":
+ df = _reactome_pathways(query, source=source, species=species, verbose=verbose)
+ elif resource == "search":
+ df = _reactome_search(query, species=species, types=types, verbose=verbose)
+ elif resource == "entity":
+ df = _reactome_entity(query, verbose=verbose)
+ elif resource == "interactors":
+ df = _reactome_interactors(query, verbose=verbose)
+ elif resource == "orthology":
+ df = _reactome_orthology(query, species=species, verbose=verbose)
+ else: # resource == "event-hierarchy"
+ df = _reactome_event_hierarchy(query, verbose=verbose)
+
+ # Record the Reactome release for reproducibility (best-effort; non-fatal).
+ release = _reactome_release()
+ if release is not None:
+ df.attrs["reactome_release"] = release
+ if verbose:
+ logger.info(f"Reactome release: {release}.")
+
+ if json:
+ return json_.loads(df.to_json(orient="records", force_ascii=False))
+ return df
diff --git a/gget/main.py b/gget/main.py
index 7a2944b09..10aa2a1e9 100644
--- a/gget/main.py
+++ b/gget/main.py
@@ -3,7 +3,7 @@
import argparse
import sys
from datetime import datetime
-from typing import Any
+from typing import Any, cast
import pandas as pd
@@ -37,6 +37,7 @@
from .gget_mutate import mutate # noqa: E402
from .gget_opentargets import OPENTARGETS_RESOURCES, opentargets # noqa: E402
from .gget_pdb import pdb # noqa: E402
+from .gget_reactome import reactome # noqa: E402
from .gget_ref import ref # noqa: E402
from .gget_search import search # noqa: E402
from .gget_seq import seq # noqa: E402
@@ -2342,6 +2343,94 @@ def main() -> None:
help="Does not print progress information.",
)
+ ## reactome parser arguments
+ reactome_desc = "Query the Reactome pathway database (https://reactome.org/)."
+ parser_reactome = parent_subparsers.add_parser(
+ "reactome",
+ parents=[parent],
+ description=reactome_desc,
+ help=reactome_desc,
+ add_help=True,
+ formatter_class=CustomHelpFormatter,
+ )
+ parser_reactome.add_argument(
+ "query",
+ type=str,
+ help=(
+ "Identifier or search term to query. Meaning depends on --resource:\n"
+ " - resource 'pathways': an identifier (e.g. UniProt accession 'P04637').\n"
+ " - resource 'search': a free-text search term (e.g. 'TP53').\n"
+ " - resource 'entity': a Reactome stable ID (e.g. 'R-HSA-6804754')."
+ ),
+ )
+ parser_reactome.add_argument(
+ "-r",
+ "--resource",
+ type=str,
+ choices=["pathways", "search", "entity", "interactors", "orthology", "event-hierarchy"],
+ default="pathways",
+ required=False,
+ help=(
+ "Type of query to perform:\n"
+ " - 'pathways' (default): pathways the identifier participates in.\n"
+ " - 'search': full-text search of the Reactome knowledgebase.\n"
+ " - 'entity': details for a Reactome stable ID.\n"
+ " - 'interactors': molecular interactors of an identifier.\n"
+ " - 'orthology': project a stable ID to another species (needs --species).\n"
+ " - 'event-hierarchy': the full pathway/reaction hierarchy for a species."
+ ),
+ )
+ parser_reactome.add_argument(
+ "-s",
+ "--source",
+ type=str,
+ default="UniProt",
+ required=False,
+ help="Identifier resource/database for resource 'pathways' (e.g. 'UniProt', 'Ensembl', 'ChEBI'). Default: UniProt.",
+ )
+ parser_reactome.add_argument(
+ "-sp",
+ "--species",
+ type=str,
+ default=None,
+ required=False,
+ help="Restrict results to a species, as a name (e.g. 'Homo sapiens') or NCBI taxonomy ID (e.g. '9606').",
+ )
+ parser_reactome.add_argument(
+ "-t",
+ "--types",
+ type=str,
+ default=None,
+ required=False,
+ help="Restrict resource 'search' results to one or more entry types (e.g. 'Pathway', 'Reaction', 'Protein').",
+ )
+ parser_reactome.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.json.\n"
+ "Default: Standard out."
+ ),
+ )
+ parser_reactome.add_argument(
+ "-csv",
+ "--csv",
+ default=False,
+ action="store_true",
+ required=False,
+ help="Returns results in csv format instead of json.",
+ )
+ parser_reactome.add_argument(
+ "-q",
+ "--quiet",
+ default=True,
+ action="store_false",
+ required=False,
+ help="Does not print progress information.",
+ )
+
## g2p parser arguments
g2p_desc = "Query the Genomics 2 Proteins (G2P) portal for residue-level protein structure/function annotations."
parser_g2p = parent_subparsers.add_parser(
@@ -2971,6 +3060,7 @@ def main() -> None:
"opentargets": parser_opentargets,
"cbio": parser_cbio,
"bgee": parser_bgee,
+ "reactome": parser_reactome,
"8cube": parser_8cube,
"virus": parser_virus,
}
@@ -3831,6 +3921,39 @@ def main() -> None:
else:
print(bgee_results.to_json(orient="records", force_ascii=False, indent=4))
+ ## reactome return
+ if args.command == "reactome":
+ # The CLI never passes json=True, so reactome() always returns a DataFrame here.
+ reactome_results = cast(
+ "pd.DataFrame",
+ reactome(
+ args.query,
+ resource=args.resource,
+ source=args.source,
+ species=args.species,
+ types=args.types,
+ verbose=args.quiet,
+ ),
+ )
+
+ if args.out is not None and args.out != "":
+ # Make saving directory
+ directory = os.path.dirname(args.out)
+ if directory != "":
+ os.makedirs(directory, exist_ok=True)
+
+ # Save results
+ with open(args.out, "w", encoding="utf-8") as f:
+ if args.csv:
+ reactome_results.to_csv(f, index=False)
+ else:
+ reactome_results.to_json(f, orient="records", force_ascii=False, indent=4)
+ else:
+ if args.csv:
+ reactome_results.to_csv(sys.stdout, index=False)
+ else:
+ print(reactome_results.to_json(orient="records", force_ascii=False, indent=4))
+
## g2p return
if args.command == "g2p":
residues: list[int] | None = None
diff --git a/gget/utils.py b/gget/utils.py
index 040730e39..c6171ccce 100644
--- a/gget/utils.py
+++ b/gget/utils.py
@@ -98,6 +98,19 @@ def parallel_map(fn: Callable[[Any], Any], items: Iterable[Any], *, max_workers:
return list(executor.map(fn, items))
+class HTTPStatusError(RuntimeError):
+ """RuntimeError raised by http_json for a non-2xx HTTP response.
+
+ Subclasses RuntimeError (so existing `except RuntimeError` handlers keep working) but
+ carries the numeric `status_code`, letting callers branch on it without parsing the
+ error message.
+ """
+
+ def __init__(self, message: str, status_code: int | None = None) -> None:
+ super().__init__(message)
+ self.status_code = status_code
+
+
def http_json(
method: str,
url: str,
@@ -145,7 +158,10 @@ def http_json(
# 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}")
+ raise HTTPStatusError(
+ f"{label} returned HTTP {response.status_code}. Body: {body}",
+ status_code=response.status_code,
+ )
last_exc = None
last_status = response.status_code
last_body = response.text[:200] if response.text else ""
@@ -163,7 +179,10 @@ def http_json(
if last_exc is not None:
raise RuntimeError(f"{label} request failed after {attempts} attempts: {last_exc}") from last_exc
- raise RuntimeError(f"{label} returned HTTP {last_status} after {attempts} attempts. Body: {last_body}")
+ raise HTTPStatusError(
+ f"{label} returned HTTP {last_status} after {attempts} attempts. Body: {last_body}",
+ status_code=last_status,
+ )
def dig(obj: Any, *path: str, context: str = "") -> Any:
diff --git a/tests/fixtures/test_reactome.json b/tests/fixtures/test_reactome.json
new file mode 100644
index 000000000..ffaf218da
--- /dev/null
+++ b/tests/fixtures/test_reactome.json
@@ -0,0 +1,19 @@
+{
+ "test_reactome_invalid_resource": {
+ "type": "error",
+ "args": {
+ "query": "P04637",
+ "resource": "bogus"
+ },
+ "expected_result": "ValueError",
+ "expected_msg": "Argument 'resource' must be one of ('pathways', 'search', 'entity', 'interactors', 'orthology', 'event-hierarchy'), not 'bogus'."
+ },
+ "test_reactome_empty_query": {
+ "type": "error",
+ "args": {
+ "query": " "
+ },
+ "expected_result": "ValueError",
+ "expected_msg": "Argument 'query' must be a non-empty string."
+ }
+}
diff --git a/tests/test_reactome.py b/tests/test_reactome.py
new file mode 100644
index 000000000..25b5ca693
--- /dev/null
+++ b/tests/test_reactome.py
@@ -0,0 +1,280 @@
+import json
+import unittest
+from unittest.mock import patch
+
+import gget.gget_reactome as gget_reactome
+import pandas as pd
+from gget.gget_reactome import reactome
+from gget.utils import HTTPStatusError
+
+from .from_json import from_json
+
+
+def _http_error(status: int) -> HTTPStatusError:
+ """Build the structured HTTP error that http_json raises for a given status."""
+ return HTTPStatusError(f"Reactome returned HTTP {status}.", status_code=status)
+
+
+# Load dictionary containing arguments and expected results for the offline (deterministic)
+# argument-validation tests.
+with open("./tests/fixtures/test_reactome.json") as json_file:
+ reactome_dict = json.load(json_file)
+
+# Reactome content is updated quarterly, so the network tests below assert structural
+# invariants (columns, identifier shape, presence of a stable well-known entry) rather than
+# exact pathway lists, and skip themselves if the Reactome service is unreachable.
+
+
+class TestReactome(unittest.TestCase, metaclass=from_json(reactome_dict, reactome)):
+ def _maybe_skip(self, func, **kwargs):
+ try:
+ return func(**kwargs)
+ except RuntimeError as e: # network/transient upstream failure
+ self.skipTest(f"Reactome service unreachable: {e}")
+
+ def test_reactome_pathways_network(self):
+ df = self._maybe_skip(reactome, query="P04637", resource="pathways", species="9606", verbose=False)
+ self.assertIsInstance(df, pd.DataFrame)
+ self.assertEqual(list(df.columns), ["stable_id", "name", "species", "schema_class", "in_disease"])
+ self.assertGreater(len(df), 0)
+ # All stable IDs are Reactome identifiers, and the species filter is honored.
+ self.assertTrue(df["stable_id"].str.startswith("R-").all())
+ self.assertTrue((df["species"] == "Homo sapiens").all())
+
+ def test_reactome_search_network(self):
+ df = self._maybe_skip(reactome, query="TP53", resource="search", types="Pathway", verbose=False)
+ self.assertEqual(list(df.columns), ["stable_id", "name", "type", "species", "reactome_id"])
+ self.assertGreater(len(df), 0)
+ # HTML highlight tags from the search endpoint must be stripped from names.
+ self.assertFalse(df["name"].str.contains("<", regex=False).any())
+
+ def test_reactome_entity_network(self):
+ df = self._maybe_skip(reactome, query="R-HSA-6804754", resource="entity", verbose=False)
+ self.assertEqual(len(df), 1)
+ self.assertEqual(df.iloc[0]["stable_id"], "R-HSA-6804754")
+ self.assertEqual(df.iloc[0]["schema_class"], "Pathway")
+
+ def test_reactome_json_output_network(self):
+ result = self._maybe_skip(
+ reactome, query="P04637", resource="pathways", species="9606", json=True, verbose=False
+ )
+ self.assertIsInstance(result, list)
+ self.assertGreater(len(result), 0)
+ self.assertIn("stable_id", result[0])
+
+ def test_reactome_pathways_no_results_network(self):
+ # An unmapped identifier yields an empty DataFrame (Reactome returns HTTP 404), not an error.
+ df = self._maybe_skip(reactome, query="NOTAREALID", resource="pathways", verbose=False)
+ self.assertIsInstance(df, pd.DataFrame)
+ self.assertEqual(len(df), 0)
+ self.assertEqual(list(df.columns), ["stable_id", "name", "species", "schema_class", "in_disease"])
+
+ def test_reactome_pathways_species_name_network(self):
+ # Species passed as a NAME (not a taxon ID) is honored just like the numeric ID.
+ df = self._maybe_skip(reactome, query="P04637", resource="pathways", species="Homo sapiens", verbose=False)
+ self.assertGreater(len(df), 0)
+ self.assertTrue((df["species"] == "Homo sapiens").all())
+
+ def test_reactome_interactors_network(self):
+ df = self._maybe_skip(reactome, query="P04637", resource="interactors", verbose=False)
+ self.assertEqual(list(df.columns), ["interactor_acc", "interactor_name", "score", "evidences"])
+ self.assertGreater(len(df), 0)
+
+ def test_reactome_orthology_network(self):
+ df = self._maybe_skip(
+ reactome, query="R-HSA-6804754", resource="orthology", species="Mus musculus", verbose=False
+ )
+ self.assertEqual(len(df), 1)
+ self.assertEqual(df.iloc[0]["species"], "Mus musculus")
+ self.assertTrue(df.iloc[0]["stable_id"].startswith("R-MMU-"))
+
+ def test_reactome_event_hierarchy_network(self):
+ df = self._maybe_skip(reactome, query="9606", resource="event-hierarchy", verbose=False)
+ self.assertEqual(list(df.columns), ["stable_id", "name", "type", "species", "parent_id", "level"])
+ self.assertGreater(len(df), 0)
+ # Top-level events (level 0) have no parent.
+ self.assertTrue(df[df["level"] == 0]["parent_id"].isna().all())
+
+
+class TestReactomeOffline(unittest.TestCase):
+ """Network-free tests of the Reactome parsing/branch logic (issue #114).
+
+ All HTTP access goes through gget_reactome.http_json, which is mocked here.
+ """
+
+ def test_strip_html_passthrough(self):
+ # Non-string values pass through unchanged.
+ self.assertIsNone(gget_reactome._strip_html(None))
+ self.assertEqual(gget_reactome._strip_html(123), 123)
+ self.assertEqual(gget_reactome._strip_html("TP53"), "TP53")
+
+ @patch.object(gget_reactome, "http_json")
+ def test_pathways_parsing_verbose(self, mock_http):
+ mock_http.return_value = [
+ {
+ "stId": "R-HSA-1",
+ "displayName": "Pathway A",
+ "speciesName": "Homo sapiens",
+ "schemaClass": "Pathway",
+ "isInDisease": False,
+ }
+ ]
+ df = reactome("P04637", resource="pathways", species="Homo sapiens", verbose=True)
+ self.assertEqual(df.iloc[0]["stable_id"], "R-HSA-1")
+ self.assertEqual(df.iloc[0]["species"], "Homo sapiens")
+
+ @patch.object(gget_reactome, "http_json")
+ def test_pathways_404_returns_empty(self, mock_http):
+ mock_http.side_effect = _http_error(404)
+ df = reactome("NOTREAL", resource="pathways", verbose=False)
+ self.assertEqual(len(df), 0)
+
+ @patch.object(gget_reactome, "http_json")
+ def test_pathways_other_error_raises(self, mock_http):
+ mock_http.side_effect = _http_error(500)
+ with self.assertRaises(RuntimeError):
+ reactome("P04637", resource="pathways", verbose=False)
+
+ @patch.object(gget_reactome, "http_json")
+ def test_search_parsing_species_types_verbose(self, mock_http):
+ mock_http.return_value = {
+ "results": [
+ {
+ "entries": [
+ {
+ "stId": "R-HSA-9",
+ "name": "TP53",
+ "exactType": "Protein",
+ "species": ["Homo sapiens"],
+ "id": 12345,
+ }
+ ]
+ }
+ ]
+ }
+ df = reactome("TP53", resource="search", species="9606", types="Protein", verbose=True)
+ self.assertEqual(df.iloc[0]["stable_id"], "R-HSA-9")
+ self.assertEqual(df.iloc[0]["name"], "TP53") # HTML stripped
+ self.assertEqual(df.iloc[0]["species"], "Homo sapiens") # list flattened
+
+ @patch.object(gget_reactome, "http_json")
+ def test_search_404_returns_empty(self, mock_http):
+ mock_http.side_effect = _http_error(404)
+ df = reactome("zzzz", resource="search", verbose=False)
+ self.assertEqual(len(df), 0)
+
+ @patch.object(gget_reactome, "http_json")
+ def test_search_other_error_raises(self, mock_http):
+ mock_http.side_effect = _http_error(503)
+ with self.assertRaises(RuntimeError):
+ reactome("TP53", resource="search", verbose=False)
+
+ @patch.object(gget_reactome, "http_json")
+ def test_entity_name_fallback_and_summation(self, mock_http):
+ # displayName missing -> fall back to first of name list; summation HTML stripped.
+ mock_http.return_value = {
+ "stId": "R-HSA-6804754",
+ "displayName": None,
+ "name": ["Regulation of TP53 Activity"],
+ "schemaClass": "Pathway",
+ "speciesName": "Homo sapiens",
+ "isInDisease": False,
+ "summation": [{"text": "
Some summary.
"}], + } + result = reactome("R-HSA-6804754", resource="entity", json=True, verbose=True) + self.assertEqual(result[0]["name"], "Regulation of TP53 Activity") + self.assertEqual(result[0]["summation"], "Some summary.") + + @patch.object(gget_reactome, "http_json") + def test_entity_404_raises_valueerror(self, mock_http): + mock_http.side_effect = _http_error(404) + with self.assertRaises(ValueError): + reactome("R-HSA-0", resource="entity", verbose=False) + + @patch.object(gget_reactome, "http_json") + def test_entity_other_error_raises(self, mock_http): + mock_http.side_effect = _http_error(500) + with self.assertRaises(RuntimeError): + reactome("R-HSA-6804754", resource="entity", verbose=False) + + def test_invalid_resource_and_empty_query(self): + with self.assertRaises(ValueError): + reactome("TP53", resource="banana", verbose=False) + with self.assertRaises(ValueError): + reactome(" ", resource="pathways", verbose=False) + + # --- interactors / orthology / event-hierarchy (issue #114 follow-up) --- + # _reactome_release is patched off so it doesn't consume an extra mocked http_json call. + + @patch.object(gget_reactome, "_reactome_release", return_value=None) + @patch.object(gget_reactome, "http_json") + def test_interactors_parsing_verbose(self, mock_http, _rel): + mock_http.return_value = { + "entities": [ + {"acc": "P04637", "interactors": [{"acc": "Q00987", "alias": "MDM2", "score": 0.99, "evidences": 122}]} + ] + } + df = reactome("P04637", resource="interactors", verbose=True) + self.assertEqual(list(df.columns), ["interactor_acc", "interactor_name", "score", "evidences"]) + self.assertEqual(df.iloc[0]["interactor_name"], "MDM2") + + @patch.object(gget_reactome, "_reactome_release", return_value=None) + @patch.object(gget_reactome, "http_json") + def test_interactors_404_returns_empty(self, mock_http, _rel): + mock_http.side_effect = _http_error(404) + df = reactome("NOTREAL", resource="interactors", verbose=False) + self.assertEqual(len(df), 0) + + def test_orthology_requires_species(self): + with self.assertRaises(ValueError): + reactome("R-HSA-6804754", resource="orthology", verbose=False) + + @patch.object(gget_reactome, "_reactome_release", return_value=None) + @patch.object(gget_reactome, "http_json") + def test_orthology_parsing(self, mock_http, _rel): + # First call resolves the species dbId; second returns the ortholog. + mock_http.side_effect = [ + [{"dbId": 48892, "displayName": "Mus musculus", "taxId": 10090}], + { + "stId": "R-MMU-6804754", + "displayName": "Regulation of TP53 Expression", + "speciesName": "Mus musculus", + "schemaClass": "Pathway", + }, + ] + df = reactome("R-HSA-6804754", resource="orthology", species="Mus musculus", verbose=True) + self.assertEqual(df.iloc[0]["stable_id"], "R-MMU-6804754") + self.assertEqual(df.iloc[0]["species"], "Mus musculus") + + @patch.object(gget_reactome, "_reactome_release", return_value=None) + @patch.object(gget_reactome, "http_json") + def test_orthology_unknown_species_raises(self, mock_http, _rel): + mock_http.return_value = [{"dbId": 48887, "displayName": "Homo sapiens", "taxId": 9606}] + with self.assertRaises(ValueError): + reactome("R-HSA-6804754", resource="orthology", species="Nonexistent sp", verbose=False) + + @patch.object(gget_reactome, "_reactome_release", return_value=None) + @patch.object(gget_reactome, "http_json") + def test_event_hierarchy_flattening(self, mock_http, _rel): + mock_http.return_value = [ + { + "stId": "R-HSA-1", + "name": "Top", + "type": "TopLevelPathway", + "species": "Homo sapiens", + "children": [ + {"stId": "R-HSA-2", "name": "Sub", "type": "Pathway", "species": "Homo sapiens", "children": []} + ], + } + ] + df = reactome("9606", resource="event-hierarchy", verbose=True) + self.assertEqual(list(df.columns), ["stable_id", "name", "type", "species", "parent_id", "level"]) + self.assertEqual(len(df), 2) + self.assertTrue(pd.isna(df.iloc[0]["parent_id"])) + self.assertEqual(df.iloc[0]["level"], 0) + self.assertEqual(df.iloc[1]["parent_id"], "R-HSA-1") + self.assertEqual(df.iloc[1]["level"], 1) + + +if __name__ == "__main__": + unittest.main()