- Gene
+ Stable ID
{{ response.stable_id }}
{% if (response.code == 404 or response.code == 500) and response.message %}
diff --git a/app/api/utils/commons.py b/app/api/utils/commons.py
index 951bc59..cd0f230 100644
--- a/app/api/utils/commons.py
+++ b/app/api/utils/commons.py
@@ -13,10 +13,14 @@ def build_stable_id_resolver_content(metadata_results) -> list[StableIdResolverC
content = StableIdResolverContent(
feature_explorer_url=build_feature_explorer_url(
- genome_id, metadata["unversioned_stable_id"]
+ genome_id,
+ metadata["unversioned_stable_id"],
+ metadata.get("stable_id_type", "gene"),
),
genome_browser_url=build_genome_browser_url(
- genome_id, metadata["unversioned_stable_id"]
+ genome_id,
+ metadata["unversioned_stable_id"],
+ metadata.get("stable_id_type", "gene"),
),
release_type=metadata.get("release", {}).get("type", ""),
release_name=metadata.get("release", {}).get("name", ""),
@@ -27,12 +31,19 @@ def build_stable_id_resolver_content(metadata_results) -> list[StableIdResolverC
return results
-def build_feature_explorer_url(genome_id: str, stable_id: str) -> str:
- return f"{ENSEMBL_URL}/feature-explorer/{genome_id}/gene:{stable_id}"
+def build_feature_explorer_url(
+ genome_id: str, stable_id: str, stable_id_type: str = "gene"
+) -> str:
+ return f"{ENSEMBL_URL}/feature-explorer/{genome_id}/{stable_id_type}:{stable_id}"
-def build_genome_browser_url(genome_id: str, stable_id: str) -> str:
- return f"{ENSEMBL_URL}/genome-browser/{genome_id}?focus=gene:{stable_id}"
+def build_genome_browser_url(
+ genome_id: str, stable_id: str, stable_id_type: str = "gene"
+) -> str:
+ return (
+ f"{ENSEMBL_URL}/genome-browser/{genome_id}"
+ f"?focus={stable_id_type}:{stable_id}"
+ )
def is_json_request(request) -> bool:
diff --git a/app/api/utils/metadata.py b/app/api/utils/metadata.py
index 0053993..3081bf9 100644
--- a/app/api/utils/metadata.py
+++ b/app/api/utils/metadata.py
@@ -25,6 +25,11 @@ def get_metadata(matches: List[SearchMatch] = []):
metadata_results[genome_id]["unversioned_stable_id"] = match.get(
"unversioned_stable_id"
)
+ # Fast-match supplies this field. Keep the historical gene
+ # default for search-hub responses that do not yet include it.
+ metadata_results[genome_id]["stable_id_type"] = (
+ match.get("type") or match.get("doc_type") or "gene"
+ )
except Exception as e:
raise Exception(
f"Failed to fetch metadata for genome '{genome_id}': {e}"
diff --git a/app/api/utils/search.py b/app/api/utils/search.py
index d2b6510..d60b453 100644
--- a/app/api/utils/search.py
+++ b/app/api/utils/search.py
@@ -71,7 +71,7 @@ def get_fast_match_results(params: SearchPayload):
# version suffixes (for example, ``ENSG00000127720.3``) are removed.
unversioned_stable_id = re.sub(r"\.\d+$", "", params.stable_id)
matches = []
- seen_genome_ids = set()
+ seen_matches = set()
for raw_match in raw_matches.split("+"):
try:
@@ -89,14 +89,19 @@ def get_fast_match_results(params: SearchPayload):
if params.type and doc_type != params.type:
continue
- if genome_id not in seen_genome_ids:
+ # A stable ID normally identifies one feature type, but retain the type
+ # in both the result and deduplication key when no filter was supplied.
+ # The resolver needs it to construct a gene or transcript destination.
+ match_key = (genome_id, doc_type)
+ if match_key not in seen_matches:
matches.append(
{
"genome_id": genome_id,
"unversioned_stable_id": unversioned_stable_id,
+ "type": doc_type,
}
)
- seen_genome_ids.add(genome_id)
+ seen_matches.add(match_key)
logger.info(
f"Fast-match stable ID lookup result: {len(matches)} matches "
diff --git a/app/static/APISpecification.yaml b/app/static/APISpecification.yaml
index 8f83a11..c68e067 100644
--- a/app/static/APISpecification.yaml
+++ b/app/static/APISpecification.yaml
@@ -42,7 +42,7 @@ paths:
parameters:
- name: stable_id
in: path
- description: Gene stable id used to represent a gene in a genome.
+ description: Gene, transcript, or protein stable ID.
required: true
explode: true
schema:
@@ -50,11 +50,11 @@ paths:
example: ENSG00000221914.11
- name: type
in: query
- description: Defines a type - gene/transcript/variant etc
+ description: Optional stable-ID type filter. If omitted, all supported types are searched.
required: false
schema:
type: string
- enum: [ gene, transcript, variant ]
+ enum: [ gene, transcript, protein ]
example:
gene
- name: app
diff --git a/stable_ids_sample.csv.redb b/stable_ids_sample.csv.redb
new file mode 100644
index 0000000..a01f642
Binary files /dev/null and b/stable_ids_sample.csv.redb differ
diff --git a/tests/test_fast_match_search.py b/tests/test_fast_match_search.py
index 6047a39..06bab16 100644
--- a/tests/test_fast_match_search.py
+++ b/tests/test_fast_match_search.py
@@ -51,10 +51,12 @@ def test_returns_matches_with_unversioned_stable_id(self, mock_fm_py):
{
"genome_id": "genome-1",
"unversioned_stable_id": "ENSG00000127720",
+ "type": "gene",
},
{
"genome_id": "genome-2",
"unversioned_stable_id": "ENSG00000127720",
+ "type": "gene",
},
]
},
@@ -80,11 +82,39 @@ def test_filters_other_document_types_and_duplicate_genomes(self, mock_fm_py):
{
"genome_id": "genome-2",
"unversioned_stable_id": "ENSG00000127720",
+ "type": "gene",
}
]
},
)
+ @patch("app.api.utils.search.fm_py")
+ @patch("app.api.utils.search.FAST_MATCH_ENABLED", True)
+ @patch("app.api.utils.search.FAST_MATCH_DB_PATH", "/data/stable-ids.redb")
+ def test_returns_supported_types_when_no_type_filter_is_supplied(self, mock_fm_py):
+ mock_fm_py.find_key.return_value = "genome-1|gene+genome-1|transcript"
+ params = SearchPayload(stable_id="ENSX000001", type=None, per_page=10)
+
+ result = get_search_results(params)
+
+ self.assertEqual(
+ result,
+ {
+ "matches": [
+ {
+ "genome_id": "genome-1",
+ "unversioned_stable_id": "ENSX000001",
+ "type": "gene",
+ },
+ {
+ "genome_id": "genome-1",
+ "unversioned_stable_id": "ENSX000001",
+ "type": "transcript",
+ },
+ ]
+ },
+ )
+
@patch("app.api.utils.search.fm_py")
@patch("app.api.utils.search.FAST_MATCH_ENABLED", True)
@patch("app.api.utils.search.FAST_MATCH_DB_PATH", "/data/stable-ids.redb")
diff --git a/tests/test_resolver.py b/tests/test_resolver.py
index 4e7db36..88321da 100644
--- a/tests/test_resolver.py
+++ b/tests/test_resolver.py
@@ -106,6 +106,39 @@ def test_resolve_success_with_redirect(
response.headers["location"], self.mock_resolved_url["genome1"]
)
+ @patch("app.api.resources.resolver_view.get_search_results")
+ @patch("app.api.resources.resolver_view.get_metadata")
+ def test_resolves_transcript_using_the_matched_type(
+ self, mock_get_metadata, mock_get_search_results
+ ):
+ transcript_id = "ENST00000357654"
+ mock_get_search_results.return_value = {
+ "matches": [
+ {
+ "genome_id": "genome1",
+ "unversioned_stable_id": transcript_id,
+ "type": "transcript",
+ }
+ ]
+ }
+ mock_get_metadata.return_value = {
+ "genome1": {
+ **self.mock_single_metadata_results_success["genome1"],
+ "unversioned_stable_id": transcript_id,
+ "stable_id_type": "transcript",
+ }
+ }
+
+ response = self.client.get(
+ f"/id/{transcript_id}", params={"app": "genome-browser"}, follow_redirects=False
+ )
+
+ self.assertEqual(response.status_code, 307)
+ self.assertEqual(
+ response.headers["location"],
+ f"{ENSEMBL_URL}/genome-browser/genome1?focus=transcript:{transcript_id}",
+ )
+
@patch("app.api.resources.resolver_view.get_search_results")
@patch("app.api.resources.resolver_view.get_metadata")
def test_resolve_success_with_html_response(