Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions app/api/models/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@


class SearchPayload(BaseModel):
stable_id: str = Field(default=None, title="Stable ID of a gene")
type: Literal["gene"] | None = Field(
default=None, title="Type of stable id, e.g. gene"
stable_id: str = Field(default=None, title="Stable ID")
type: Literal["gene", "transcript", "protein"] | None = Field(
default=None, title="Optional stable-ID type filter"
)
per_page: int = 1
app: Literal["genome-browser", "feature-explorer"] = Field(
Expand All @@ -15,8 +15,9 @@ class SearchPayload(BaseModel):


class SearchMatch(BaseModel):
genome: str
genome_id: str
unversioned_stable_id: str
type: Literal["gene", "transcript", "protein"] | None = None


class SearchResult(BaseModel):
Expand Down Expand Up @@ -89,6 +90,7 @@ class UrlResolverResponse(BaseModel):


class StableIdResolverContent(MetadataResult):
stable_id_type: Literal["gene", "transcript", "protein"] = "gene"
feature_explorer_url: str | None = None
genome_browser_url: str | None = None
release_type: str | None = None
Expand Down
2 changes: 1 addition & 1 deletion app/api/resources/resolver_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
async def resolve(
request: Request,
stable_id: str,
type: Optional[str] = "gene",
type: Optional[Literal["gene", "transcript", "protein"]] = None,
gca: Optional[str] = "",
app: Optional[Literal["genome-browser", "feature-explorer"]] = DEFAULT_APP,
):
Expand Down
2 changes: 1 addition & 1 deletion app/api/resources/templates/shared/id_content.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<div class="id-redirect-content">
<div class="redirect-info">
<div class="search-param">
<span class="search-param-label">Gene</span>
<span class="search-param-label">Stable ID</span>
<span class="search-param-value">{{ response.stable_id }}</span>
</div>
{% if (response.code == 404 or response.code == 500) and response.message %}
Expand Down
23 changes: 17 additions & 6 deletions app/api/utils/commons.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", ""),
Expand All @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions app/api/utils/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
11 changes: 8 additions & 3 deletions app/api/utils/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 "
Expand Down
6 changes: 3 additions & 3 deletions app/static/APISpecification.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,19 @@ 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:
type: string
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
Expand Down
Binary file added stable_ids_sample.csv.redb
Binary file not shown.
30 changes: 30 additions & 0 deletions tests/test_fast_match_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
]
},
Expand All @@ -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")
Expand Down
33 changes: 33 additions & 0 deletions tests/test_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading