diff --git a/app/api/models/resolver.py b/app/api/models/resolver.py index fb5d8e4..82e389c 100644 --- a/app/api/models/resolver.py +++ b/app/api/models/resolver.py @@ -1,4 +1,5 @@ -from typing import Optional, Literal, List, Dict +from enum import Enum +from typing import Optional, Literal, List, Dict, Annotated from pydantic import BaseModel, Field @@ -38,5 +39,24 @@ class MetadataResult(BaseModel): class ResolvedPayload(MetadataResult): resolved_url: str -class ResolvedURLResponse(BaseModel): + +class RapidResolverHtmlResponseType(str, Enum): + HOME = "HOME" + ERROR = "ERROR" + BLAST = "BLAST" + HELP = "HELP" + INFO = "INFO" + + # Exclude all fields except resolved_url in JSON response. +class RapidResolverResponse(BaseModel): resolved_url: str + response_type: Annotated[Optional[RapidResolverHtmlResponseType], Field(exclude=True)] = None + code: Annotated[Optional[int], Field(exclude=True)] = None + species_name: Annotated[Optional[str], Field(exclude=True)] = None + gene_id: Annotated[Optional[str], Field(exclude=True)] = None + location: Annotated[Optional[str], Field(exclude=True)] = None + message: Annotated[Optional[str], Field(exclude=True)] = None + rapid_archive_url: Annotated[Optional[str], Field(exclude=True)] = None + + class Config: + use_enum_values = True diff --git a/app/api/resources/rapid_view.py b/app/api/resources/rapid_view.py index 028168c..af3c846 100644 --- a/app/api/resources/rapid_view.py +++ b/app/api/resources/rapid_view.py @@ -1,12 +1,18 @@ +import os from urllib.parse import parse_qs -from fastapi import APIRouter, Request, HTTPException, Query -from fastapi.responses import RedirectResponse + +from dotenv import load_dotenv +from fastapi import APIRouter, Request, Query, HTTPException + +from jinja2 import Environment, FileSystemLoader +from starlette.responses import HTMLResponse + import logging -from api.models.resolver import ResolvedURLResponse +from api.models.resolver import RapidResolverResponse, RapidResolverHtmlResponseType from core.logging import InterceptHandler from core.config import ENSEMBL_URL from api.utils.metadata import get_genome_id_from_assembly_accession_id -from api.utils.rapid import construct_url, format_assembly_accession +from api.utils.rapid import construct_url, format_assembly_accession, construct_rapid_archive_url logging.getLogger().handlers = [InterceptHandler()] @@ -15,14 +21,22 @@ @router.get("/info/{subpath:path}", name="Resolve rapid help page") async def resolve_rapid_help(request: Request, subpath: str = ""): - help_page_url = f"{ENSEMBL_URL}/help" - return resolved_response(help_page_url, request) + response = RapidResolverResponse( + response_type=RapidResolverHtmlResponseType.HELP, + code=308, + resolved_url=f"{ENSEMBL_URL}/help", + ) + return resolved_response(response, request) -@router.get("/Blast", name="Resolve rapid blast page") +@router.get("/Multi/Tools/Blast", name="Resolve rapid blast page") async def resolve_rapid_blast(request: Request): - blast_page_url = f"{ENSEMBL_URL}/blast" - return resolved_response(blast_page_url, request) + response = RapidResolverResponse( + response_type=RapidResolverHtmlResponseType.BLAST, + code=308, + resolved_url=f"{ENSEMBL_URL}/blast", + ) + return resolved_response(response, request) # Resolve rapid urls @@ -31,12 +45,28 @@ async def resolve_rapid_blast(request: Request): async def resolve_species( request: Request, species_url_name: str, subpath: str = "", r: str = Query(None) ): + # Check if its blast redirect + if "tools/blast" in subpath.lower(): + response = RapidResolverResponse( + response_type=RapidResolverHtmlResponseType.BLAST, + code=308, + resolved_url=f"{ENSEMBL_URL}/blast", + species_name=species_url_name, + ) + return resolved_response(response, request) + assembly_accession_id = format_assembly_accession(species_url_name) if assembly_accession_id is None: - raise HTTPException( - status_code=422, detail="Unable to process input accession ID" + input_error_response = RapidResolverResponse( + response_type=RapidResolverHtmlResponseType.ERROR, + code=422, + resolved_url=f"{ENSEMBL_URL}/species-selector", + message="Invalid input accession ID", + species_name=species_url_name, ) + return resolved_response(input_error_response, request) + try: genome_object = get_genome_id_from_assembly_accession_id(assembly_accession_id) @@ -48,27 +78,72 @@ async def resolve_species( query_params = parse_qs(query_string, separator=";") url = construct_url(genome_id, subpath, query_params) - return resolved_response(url, request) + rapid_archive_url = construct_rapid_archive_url(species_url_name, subpath, query_params) + response = RapidResolverResponse( + response_type=RapidResolverHtmlResponseType.INFO, + code=308, + resolved_url=url, + species_name=species_url_name, + gene_id=query_params.get("g", [None])[0], + location=query_params.get("r", [None])[0], + rapid_archive_url=rapid_archive_url, + ) + return resolved_response(response, request) else: raise HTTPException(status_code=404, detail="Genome not found") - except HTTPException as e: logging.debug(e) - raise HTTPException( - status_code=e.status_code, detail="Unexpected error occured" + response = RapidResolverResponse( + response_type=RapidResolverHtmlResponseType.ERROR, + code=e.status_code, + resolved_url=f"{ENSEMBL_URL}/species-selector", + message=e.detail, + species_name=species_url_name, ) - + return resolved_response(response, request) except Exception as e: logging.debug(f"Unexpected error occurred: {e}") - raise HTTPException(status_code=500, detail="Unexpected error occurred") + response = RapidResolverResponse( + species_name=species_url_name, + response_type=RapidResolverHtmlResponseType.ERROR, + code=500, + resolved_url=f"{ENSEMBL_URL}/species-selector", + message=str(e), + ) + return resolved_response(response, request) @router.get("/", name="Rapid Home") async def resolve_home(request: Request): - return resolved_response(ENSEMBL_URL, request) + response = RapidResolverResponse( + response_type=RapidResolverHtmlResponseType.HOME, + code=308, + resolved_url=ENSEMBL_URL, + ) + return resolved_response(response, request) -def resolved_response(url: str, request: Request): +def resolved_response(response: RapidResolverResponse, request: Request): + # Return JSON response if requested if "application/json" in request.headers.get("accept"): - return ResolvedURLResponse(resolved_url=url) - return RedirectResponse(url=url, status_code=301) + # Handle error responses for JSON requests + if response.response_type == RapidResolverHtmlResponseType.ERROR: + raise HTTPException( + status_code=response.code, + detail=response.message or "An error occurred", + ) + # Doesn't raise redirect for JSON requests, just return the URL. Because swagger UI doesn't handle redirects well. + # So code is always 200 for successful JSON response. + return response + + # Default to HTML response + return HTMLResponse(generate_html_content(response)) + + +def generate_html_content(response): + load_dotenv() + CURR_DIR = os.path.dirname(os.path.abspath(__file__)) + env = Environment(loader=FileSystemLoader(os.path.join(CURR_DIR, "templates/rapid"))) + rapid_redirect_page_template = env.get_template("main.html") + rapid_redirect_page_html = rapid_redirect_page_template.render(response=response) + return rapid_redirect_page_html diff --git a/app/api/resources/templates/rapid/_home_icon.html b/app/api/resources/templates/rapid/_home_icon.html new file mode 100644 index 0000000..2ba4690 --- /dev/null +++ b/app/api/resources/templates/rapid/_home_icon.html @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/api/resources/templates/rapid/_logo.html b/app/api/resources/templates/rapid/_logo.html new file mode 100644 index 0000000..9b8dfae --- /dev/null +++ b/app/api/resources/templates/rapid/_logo.html @@ -0,0 +1,25 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/api/resources/templates/rapid/appbar.html b/app/api/resources/templates/rapid/appbar.html new file mode 100644 index 0000000..50f8336 --- /dev/null +++ b/app/api/resources/templates/rapid/appbar.html @@ -0,0 +1,25 @@ +
+
+ +
+ {% include "_logo.html" %} +
+
+
+ Beta +
+ +
+
+
+ Genome data & annotation +
+
\ No newline at end of file diff --git a/app/api/resources/templates/rapid/content.html b/app/api/resources/templates/rapid/content.html new file mode 100644 index 0000000..40ffdc0 --- /dev/null +++ b/app/api/resources/templates/rapid/content.html @@ -0,0 +1,44 @@ +{% import "url.html" as url_helper %} + +
+
+ {% if response.response_type == "BLAST" or response.response_type == "HELP" %} + {% if response.species_name %} +
+ Species {{ response.species_name }} +
+ {% endif %} + {{ url_helper.render_url(response.resolved_url) }} + {% elif response.response_type == "INFO" %} +
+ Species {{ response.species_name }} +
+ {% if response.gene_id %} +
+ Gene {{ response.gene_id }} +
+ {% endif %} + {% if response.location %} +
+ Location {{ response.location }} +
+ {% endif %} + {{ url_helper.render_url(response.resolved_url) }} + {% elif response.response_type == "ERROR" %} +
+ Species {{ response.species_name }} +
+
+ {{ response.message }} +
+ {{ url_helper.render_url(response.resolved_url) }} + {% else %} + {{ url_helper.render_url("https://beta.ensembl.org/") }} + {% endif %} +
+
+ + You will be redirected to the new Ensembl website, where you will find the latest genomic information + +
+
\ No newline at end of file diff --git a/app/api/resources/templates/rapid/footer.html b/app/api/resources/templates/rapid/footer.html new file mode 100644 index 0000000..3b14a93 --- /dev/null +++ b/app/api/resources/templates/rapid/footer.html @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/app/api/resources/templates/rapid/main.html b/app/api/resources/templates/rapid/main.html new file mode 100644 index 0000000..27128d7 --- /dev/null +++ b/app/api/resources/templates/rapid/main.html @@ -0,0 +1,26 @@ + + + + Ensembl Rapid Resolver + + + + + + + + + + + + + + + {% include "appbar.html" %} +
+ {% include "venn.html" %} + {% include "content.html" %} + {% include "footer.html" %} +
+ + diff --git a/app/api/resources/templates/rapid/url.html b/app/api/resources/templates/rapid/url.html new file mode 100644 index 0000000..f91f4fb --- /dev/null +++ b/app/api/resources/templates/rapid/url.html @@ -0,0 +1,8 @@ +{% macro render_url(url) %} +
+ Redirecting to + + {{ url }} + +
+{% endmacro %} diff --git a/app/api/resources/templates/rapid/venn.html b/app/api/resources/templates/rapid/venn.html new file mode 100644 index 0000000..0f46790 --- /dev/null +++ b/app/api/resources/templates/rapid/venn.html @@ -0,0 +1,14 @@ +
+ Venn of the redirect +
+
+
+ Your URL has changed or gone... +
+
+ ...we suggest this instead +
+
+ {{ response.code }} +
+
\ No newline at end of file diff --git a/app/api/utils/rapid.py b/app/api/utils/rapid.py index 950652b..56f0507 100644 --- a/app/api/utils/rapid.py +++ b/app/api/utils/rapid.py @@ -1,6 +1,6 @@ from loguru import logger import requests -from core.config import ENSEMBL_URL, NCBI_DATASETS_URL +from core.config import ENSEMBL_URL, NCBI_DATASETS_URL, RAPID_ARCHIVE_URL import re @@ -58,20 +58,54 @@ def construct_url(genome_id, subpath, query_params): location = query_params.get("r", [None])[0] gene_id = query_params.get("g", [None])[0] - if subpath == "" or re.search("Info/Index", subpath, re.IGNORECASE): + subpath = subpath or "" + subpath_lower = subpath.lower() + + if genome_id is None: + return ENSEMBL_URL + + if not subpath or "info/index" in subpath_lower: return f"{ENSEMBL_URL}/species/{genome_id}" - elif re.search("Location", subpath): + + if "location" in subpath_lower: + if "genome" in subpath_lower: + return f"{ENSEMBL_URL}/species/{genome_id}" + if location is None: + raise ValueError("Location paramater 'r' is missing for Location view") return f"{ENSEMBL_URL}/genome-browser/{genome_id}?focus=location:{location}" - elif re.search("Gene", subpath): - if re.search("Gene/Compara_Homolog", subpath): - return ( - f"{ENSEMBL_URL}/entity-viewer/{genome_id}/gene:{gene_id}?view=homology" - ) + + if "gene" in subpath_lower: + if not gene_id: + raise ValueError("Gene parameter 'g' is missing for Gene view") + if "compara_homolog" in subpath_lower: + return f"{ENSEMBL_URL}/entity-viewer/{genome_id}/gene:{gene_id}?view=homology" return f"{ENSEMBL_URL}/entity-viewer/{genome_id}/gene:{gene_id}" - elif re.search("Transcript", subpath): - if re.search("Domains|ProteinSummary", subpath): - return ( - f"{ENSEMBL_URL}/entity-viewer/{genome_id}/gene:{gene_id}?view=protein" - ) + + if "transcript" in subpath_lower: + if not gene_id: + raise ValueError("Gene parameter 'g' is missing for Transcript view") + if "domains" in subpath_lower or "proteinsummary" in subpath_lower: + return f"{ENSEMBL_URL}/entity-viewer/{genome_id}/gene:{gene_id}?view=protein" return f"{ENSEMBL_URL}/entity-viewer/{genome_id}/gene:{gene_id}" - return ENSEMBL_URL + + raise ValueError("Invalid path") + + +def construct_rapid_archive_url(species_url_name, subpath, query_params): + url = f"{RAPID_ARCHIVE_URL}/{species_url_name}" + query_params = query_params or {} + + params = [] + for key, values in query_params.items(): + value = values[0] if values else "" + if value: + params.append(f"{key}={value}") + + if subpath: + url += f"/{subpath}" + + if params: + url += "?" + ";".join(params) # ; separator for rapid + + return url + diff --git a/app/core/config.py b/app/core/config.py index 16ec69b..1899f2c 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -36,6 +36,7 @@ ) DEFAULT_APP = config("DEFAULT_APP", cast=str, default="entity-viewer") ENSEMBL_URL = config("ENSEMBL_URL", cast=str, default="https://beta.ensembl.org") +RAPID_ARCHIVE_URL = config("RAPID_ARCHIVE_URL", cast=str, default="https://rapid-archive.ensembl.org") NCBI_DATASETS_URL = config( "NCBI_DATASETS_URL", cast=str, default="https://api.ncbi.nlm.nih.gov/datasets/v2" ) diff --git a/app/static/APISpecification.yaml b/app/static/APISpecification.yaml index 6aaa319..ebe8c1d 100644 --- a/app/static/APISpecification.yaml +++ b/app/static/APISpecification.yaml @@ -112,8 +112,6 @@ paths: tags: - Rapid Resolver responses: - '301': - $ref: '#/components/responses/301' '200': $ref: '#/components/responses/200' '400': @@ -122,15 +120,13 @@ paths: $ref: '#/components/responses/404' '500': $ref: '#/components/responses/500' - /rapid/Blast: + /rapid/Multi/Tools/Blast: get: summary: Resolve rapid site blast url description: Resolves to Ensembl site blast url. tags: - Rapid Resolver responses: - '301': - $ref: '#/components/responses/301' '200': $ref: '#/components/responses/200' '400': @@ -154,8 +150,6 @@ paths: type: string example: Camarhynchus_parvulus_GCA_902806625.1 responses: - '301': - $ref: '#/components/responses/301' '200': $ref: '#/components/responses/200' '422': @@ -195,8 +189,6 @@ paths: type: string example: 2:361680-384534 responses: - '301': - $ref: '#/components/responses/301' '200': $ref: '#/components/responses/200' '422': @@ -207,6 +199,29 @@ paths: $ref: '#/components/responses/404' '500': $ref: '#/components/responses/500' + /rapid/{species_url_name}/Tools/Blast: + get: + summary: Resolve rapid site blast url + description: Resolve rapid site blast url + tags: + - Rapid Resolver + parameters: + - name: species_url_name + in: path + description: Species name with assembly accession + required: true + schema: + type: string + example: Camarhynchus_parvulus_GCA_902806625.1 + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + '500': + $ref: '#/components/responses/500' /rapid/info/{subpath}: get: summary: Resolve rapid site help and docs url @@ -221,8 +236,6 @@ paths: type: string example: index.html responses: - '301': - $ref: '#/components/responses/301' '200': $ref: '#/components/responses/200' '400': @@ -251,12 +264,6 @@ components: resolved_url: type: string description: Resolved URL to the Ensembl site - 301: - description: Redirect to resolved URL - headers: - Location: - $ref: '#/components/headers/Location' - content: { } 400: description: Bad Request. Missing stable id in the url content: diff --git a/app/static/css/rapid_page.css b/app/static/css/rapid_page.css new file mode 100644 index 0000000..452d1e9 --- /dev/null +++ b/app/static/css/rapid_page.css @@ -0,0 +1,184 @@ +:root { + --standard-gutter: 30px; + --global-padding-left: calc(var(--standard-gutter) * 4); + --color-light-blue: #33adff; + --font-weight-light: 300; + --color-medium-dark-grey: #9aa7b1; + --color-red: #d90000; + --color-medium-light-grey: #d4d9de; +} + +html, +body { + margin: 0; + padding: 0; + height: 100%; + font-family: Lato, 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.topbar { + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + align-items: center; + color: #fff; + height: 32px; + padding: 0 30px 0 22px; + background-color: #1b2c39; +} + +.topbar-left { + display: flex; + align-items: center; + line-height: 1; +} + +.logotype-wrapper { + margin-top: 3px; + margin-right: 15px; +} + +.topbar-left-text { + display: flex; + align-items: baseline; + margin-bottom: -3px; + column-gap: 30px; +} + +.topbar-right { + font-size: 13px; +} + +.release { + font-size: 11px; +} + +.copyright { + font-size: 12px; +} + +.copyright a { + color: var(--color-light-blue); + text-decoration: none; +} + +.light { + font-weight: var(--font-weight-light); +} + +.home-link { + font-size: 0; + margin-right: 16px; + cursor: pointer; +} + +.container { + display: flex; + flex-direction: column; + padding: 0 var(--global-padding-left); + align-items: center; + margin-top: 50px; +} + +.redirect-content { + margin-top: 30px; + text-align: center; + width: 350px; +} + +.redirect-text { + color: var(--color-medium-dark-grey); + font-size: 12px; + margin-top: 50px; +} + +.footer { + font-size: 12px; + color: var(--color-medium-dark-grey); + margin-top: 50px; + width: 300px; + text-align: center; +} + +.footer-link a { + font-size: 12px; + color: var(--color-light-blue); + text-decoration: none; +} + +.link { + color: var(--color-light-blue); + text-decoration: none; + font-size: 13px; +} + +.error-message { + color: var(--color-red); + font-size: 13px; + margin-bottom: 20px; +} + +.search-param { + font-size: 13px; + margin-bottom: 5px; +} + +.venn-header { + font-size: 14px; + margin-right: 50px; + margin-bottom: 20px; + margin-left: 50px; +} + +.venn { + position: relative; + width: 400px; + height: 200px; + margin-left: 50px; +} + +.circle { + position: absolute; + width: 200px; + height: 200px; + border-radius: 50%; + opacity: 0.6; + display: flex; + align-items: center; +} + +.circle.a { + left: 0; + background: var(--color-medium-light-grey); + justify-content: flex-start; + font-size: 12px; +} + +.circle.a span { + margin-left: 20px; + width: 60%; +} + +.circle.b span { + width: 60%; +} + +.circle.b { + left: 150px; + background: var(--color-medium-light-grey); + justify-content: flex-end; + font-size: 12px; +} + +.intersection { + position: absolute; + left: 44%; + top: 50%; + transform: translate(-50%, -50%); + font-size: 20px; +} + +.search-param-label { + font-size: 13px; + color: var(--color-medium-dark-grey); +} \ No newline at end of file diff --git a/app/static/icons/favicon-16x16.png b/app/static/icons/favicon-16x16.png new file mode 100644 index 0000000..77ee00c Binary files /dev/null and b/app/static/icons/favicon-16x16.png differ diff --git a/app/static/icons/favicon-32x32.png b/app/static/icons/favicon-32x32.png new file mode 100644 index 0000000..17be8f9 Binary files /dev/null and b/app/static/icons/favicon-32x32.png differ diff --git a/app/tests/test_rapid.py b/app/tests/test_rapid.py index 762e2fa..cf083ca 100644 --- a/app/tests/test_rapid.py +++ b/app/tests/test_rapid.py @@ -1,7 +1,7 @@ import unittest from unittest.mock import patch from fastapi.testclient import TestClient -from api.models.resolver import ResolvedURLResponse +from api.models.resolver import RapidResolverResponse from core.config import ENSEMBL_URL from main import app @@ -35,11 +35,6 @@ def setUp(self): # Test rapid home page def test_rapid_home_success(self): - response = self.client.get(f"{self.mock_rapid_api_url}/", follow_redirects=False) - self.assertEqual(response.status_code, 301) - self.assertIn("location", response.headers) - self.assertEqual(response.headers["location"], ENSEMBL_URL) - # test with accept header for JSON response response = self.client.get( f"{self.mock_rapid_api_url}/", @@ -49,21 +44,12 @@ def test_rapid_home_success(self): self.assertEqual(response.status_code, 200) # OK self.assertEqual( response.json(), - ResolvedURLResponse(resolved_url=ENSEMBL_URL).model_dump(mode='json') + RapidResolverResponse(resolved_url=ENSEMBL_URL).model_dump(mode='json') ) # Test rapid help page def test_rapid_help_success(self): - response = self.client.get( - f"{self.mock_rapid_api_url}/info/index.html", follow_redirects=False - ) - self.assertEqual(response.status_code, 301) - self.assertIn("location", response.headers) - self.assertEqual( - response.headers["location"], f"{ENSEMBL_URL}/help" - ) - # test with accept header for JSON response response = self.client.get( f"{self.mock_rapid_api_url}/info/index.html", @@ -73,31 +59,33 @@ def test_rapid_help_success(self): self.assertEqual(response.status_code, 200) # OK self.assertEqual( response.json(), - ResolvedURLResponse(resolved_url=f"{ENSEMBL_URL}/help").model_dump(mode='json') + RapidResolverResponse(resolved_url=f"{ENSEMBL_URL}/help").model_dump(mode='json') ) # Test rapid blast page def test_rapid_blast_success(self): - response = self.client.get( - f"{self.mock_rapid_api_url}/Blast", follow_redirects=False + # test with accept header for JSON response + response1 = self.client.get( + f"{self.mock_rapid_api_url}/Multi/Tools/Blast", + headers={"accept": "application/json"}, + follow_redirects=False, ) - self.assertEqual(response.status_code, 301) - self.assertIn("location", response.headers) + self.assertEqual(response1.status_code, 200) self.assertEqual( - response.headers["location"], f"{ENSEMBL_URL}/blast" + response1.json(), + RapidResolverResponse(resolved_url=f"{ENSEMBL_URL}/blast").model_dump(mode='json') ) - # test with accept header for JSON response - response = self.client.get( - f"{self.mock_rapid_api_url}/Blast", + response2 = self.client.get( + f"{self.mock_rapid_api_url}/{self.species_url_name}/Tools/Blast", headers={"accept": "application/json"}, follow_redirects=False, ) - self.assertEqual(response.status_code, 200) + self.assertEqual(response2.status_code, 200) self.assertEqual( - response.json(), - ResolvedURLResponse(resolved_url=f"{ENSEMBL_URL}/blast").model_dump(mode='json') + response2.json(), + RapidResolverResponse(resolved_url=f"{ENSEMBL_URL}/blast").model_dump(mode='json') ) @@ -106,39 +94,6 @@ def test_rapid_blast_success(self): def test_rapid_species_home_success( self, mock_get_genome_id_from_assembly_accession_id ): - - # test web-metadata-api response without genome_tag - mock_get_genome_id_from_assembly_accession_id.return_value = ( - self.mock_genome_id_response1 - ) - - response = self.client.get( - f"{self.mock_rapid_api_url}/{self.species_url_name}/", - follow_redirects=False, - ) - - self.assertEqual(response.status_code, 301) # Redirect - self.assertIn("location", response.headers) - self.assertEqual( - response.headers["location"], self.mock_resolved_url["genome1"] - ) - - # test web-metadata-api response with genome_tag - mock_get_genome_id_from_assembly_accession_id.return_value = ( - self.mock_genome_id_response2 - ) - - response = self.client.get( - f"{self.mock_rapid_api_url}/{self.species_url_name}/", - follow_redirects=False, - ) - - self.assertEqual(response.status_code, 301) # Redirect - self.assertIn("location", response.headers) - self.assertEqual( - response.headers["location"], self.mock_resolved_url["genome2"] - ) - # test with accept header for JSON response mock_get_genome_id_from_assembly_accession_id.return_value = ( self.mock_genome_id_response1 @@ -153,7 +108,7 @@ def test_rapid_species_home_success( self.assertEqual(response.status_code, 200) # OK self.assertEqual( response.json(), - ResolvedURLResponse(resolved_url = self.mock_resolved_url["genome1"]).model_dump(mode='json') + RapidResolverResponse(resolved_url = self.mock_resolved_url["genome1"]).model_dump(mode='json') ) @@ -162,24 +117,6 @@ def test_rapid_species_home_success( def test_rapid_species_location_success( self, mock_get_genome_id_from_assembly_accession_id ): - - # test web-metadata-api response without genome_tag - mock_get_genome_id_from_assembly_accession_id.return_value = ( - self.mock_genome_id_response1 - ) - - response = self.client.get( - f"{self.mock_rapid_api_url}/{self.species_url_name}/Location/View", - params={"r": "1:1000-2000"}, - follow_redirects=False, - ) - - self.assertEqual(response.status_code, 301) # Redirect - self.assertIn( - f"{ENSEMBL_URL}/genome-browser/genome_uuid1?focus=location:1:1000-2000", - response.headers["location"], - ) - # test with accept header for JSON response mock_get_genome_id_from_assembly_accession_id.return_value = ( self.mock_genome_id_response1 @@ -195,7 +132,7 @@ def test_rapid_species_location_success( self.assertEqual(response.status_code, 200) # OK self.assertEqual( response.json(), - ResolvedURLResponse(resolved_url = f"{ENSEMBL_URL}/genome-browser/genome_uuid1?focus=location:1:1000-2000").model_dump(mode='json') + RapidResolverResponse(resolved_url = f"{ENSEMBL_URL}/genome-browser/genome_uuid1?focus=location:1:1000-2000").model_dump(mode='json') ) # Test Gene pages @@ -203,22 +140,6 @@ def test_rapid_species_location_success( def test_rapid_species_gene_compara_homolog( self, mock_get_genome_id_from_assembly_accession_id ): - mock_get_genome_id_from_assembly_accession_id.return_value = ( - self.mock_genome_id_response1 - ) - - response = self.client.get( - f"{self.mock_rapid_api_url}/{self.species_url_name}/Gene/Compara_Homolog", - params={"g": "GENE123"}, - follow_redirects=False, - ) - - self.assertEqual(response.status_code, 301) # Redirect - self.assertIn( - f"{ENSEMBL_URL}/entity-viewer/genome_uuid1/gene:GENE123?view=homology", - response.headers["location"], - ) - # test with accept header for JSON response mock_get_genome_id_from_assembly_accession_id.return_value = ( self.mock_genome_id_response1 @@ -234,7 +155,7 @@ def test_rapid_species_gene_compara_homolog( self.assertEqual(response.status_code, 200) # OK self.assertEqual( response.json(), - ResolvedURLResponse( + RapidResolverResponse( resolved_url=f"{ENSEMBL_URL}/entity-viewer/genome_uuid1/gene:GENE123?view=homology" ).model_dump(mode='json'), ) @@ -247,19 +168,21 @@ def test_rapid_species_404_not_found( mock_get_genome_id_from_assembly_accession_id.return_value = {} response = self.client.get( - f"{self.mock_rapid_api_url}/{self.species_url_name}/Invalid_GCA" + f"{self.mock_rapid_api_url}/{self.species_url_name}/Invalid_GCA", + headers={"accept": "application/json"}, ) self.assertEqual(response.status_code, 404) # Test invalid url entity def test_rapid_species_422_unprocessable_entity(self): - response = self.client.get(f"{self.mock_rapid_api_url}/Invalid_Name/") + response = self.client.get(f"{self.mock_rapid_api_url}/Invalid_Name/", headers={"accept": "application/json"},) self.assertEqual(response.status_code, 422) # Test POST def test_rapid_species_post_method_not_allowed(self): response = self.client.post( - f"{self.mock_rapid_api_url}/{self.species_url_name}/" + f"{self.mock_rapid_api_url}/{self.species_url_name}/", + headers={"accept": "application/json"}, ) self.assertEqual(response.status_code, 405) @@ -277,6 +200,7 @@ def test_rapid_species_500_internal_server_error( ) response = self.client.get( - f"{self.mock_rapid_api_url}/{self.species_url_name}/" + f"{self.mock_rapid_api_url}/{self.species_url_name}/", + headers={"accept": "application/json"}, ) self.assertEqual(response.status_code, 500)