diff --git a/README.md b/README.md index 8b9d236..3878d1d 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,16 @@ The resolver service generates new Ensembl website urls for different features b `$ mv sample-env .env` `$ docker-compose -f docker-compose.yml up` - + ### Deploy the app and run docker-compose: Some urls that are available after deployment on your local machine: http://localhost:8001/id/ENSG00000127720 http://localhost:8001/id/ENSG00000127720.3 + ### Run unit tests: +``` +cd app +python -m unittest tests.test_rapid +``` diff --git a/app/api/models/resolver.py b/app/api/models/resolver.py index c126277..3a64a99 100644 --- a/app/api/models/resolver.py +++ b/app/api/models/resolver.py @@ -36,3 +36,6 @@ class MetadataResult(BaseModel): class ResolvedPayload(MetadataResult): resolved_url: str + +class ResolvedURLResponse(BaseModel): + resolved_url: str diff --git a/app/api/resources/rapid_view.py b/app/api/resources/rapid_view.py index 6703d33..028168c 100644 --- a/app/api/resources/rapid_view.py +++ b/app/api/resources/rapid_view.py @@ -2,6 +2,7 @@ from fastapi import APIRouter, Request, HTTPException, Query from fastapi.responses import RedirectResponse import logging +from api.models.resolver import ResolvedURLResponse from core.logging import InterceptHandler from core.config import ENSEMBL_URL from api.utils.metadata import get_genome_id_from_assembly_accession_id @@ -12,12 +13,24 @@ router = APIRouter() +@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) + + +@router.get("/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) + + # Resolve rapid urls -@router.get("/{species_url_name}/", name="Rapid Species Resources") +@router.get("/{species_url_name}", name="Rapid Species Resources") @router.get("/{species_url_name}/{subpath:path}", name="Rapid Species Resources") async def resolve_species( request: Request, species_url_name: str, subpath: str = "", r: str = Query(None) -) -> RedirectResponse: +): assembly_accession_id = format_assembly_accession(species_url_name) if assembly_accession_id is None: @@ -35,7 +48,7 @@ async def resolve_species( query_params = parse_qs(query_string, separator=";") url = construct_url(genome_id, subpath, query_params) - return RedirectResponse(url) + return resolved_response(url, request) else: raise HTTPException(status_code=404, detail="Genome not found") @@ -52,4 +65,10 @@ async def resolve_species( @router.get("/", name="Rapid Home") async def resolve_home(request: Request): - return RedirectResponse(ENSEMBL_URL) + return resolved_response(ENSEMBL_URL, request) + + +def resolved_response(url: str, request: Request): + if "application/json" in request.headers.get("accept"): + return ResolvedURLResponse(resolved_url=url) + return RedirectResponse(url=url, status_code=301) diff --git a/app/static/APISpecification.yaml b/app/static/APISpecification.yaml index 638da11..b9619c0 100644 --- a/app/static/APISpecification.yaml +++ b/app/static/APISpecification.yaml @@ -8,13 +8,15 @@ info: servers: - url: https://resolver.ensembl.org tags: - - name: resolver + - name: Resolver description: Resolver API resolves external urls to Ensembl + - name: Rapid Resolver + description: Resolver API to resolve rapid site urls to Ensembl paths: /id/{stable_id}: get: tags: - - resolver + - Resolver summary: Resolve stable ID with optional query params description: Resolves to a beta url when a stable id wih optional query params provided parameters: @@ -100,6 +102,235 @@ paths: $ref: '#/components/responses/404' '500': $ref: '#/components/responses/500' + /rapid: + get: + summary: Resolve rapid site url + description: Resolves to Ensembl site url (home page). + tags: + - Rapid Resolver + responses: + '301': + description: Redirect to resolved URL + headers: + Location: + schema: + type: string + description: Ensembl URL + content: {} + '200': + description: OK + headers: + Location: + schema: + type: string + description: Ensembl URL + content: + application/json: + schema: + type: object + properties: + resolved_url: + type: string + description: Resolved url to Ensembl site + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + '500': + $ref: '#/components/responses/500' + /rapid/Blast: + get: + summary: Resolve rapid site blast url + description: Resolves to Ensembl site blast url. + tags: + - Rapid Resolver + responses: + '301': + description: Redirect to Ensembl blast page + headers: + Location: + schema: + type: string + description: Ensembl blast URL + content: {} + '200': + description: OK + headers: + Location: + schema: + type: string + description: Ensembl blast URL + content: + application/json: + schema: + type: object + properties: + resolved_url: + type: string + description: Resolved url to Ensembl site help page + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + '500': + $ref: '#/components/responses/500' + /rapid/{species_url_name}: + get: + summary: Resolve rapid site url with species url name (with assembly accession) + description: Resolves to Ensembl site url with the species url name provided. + 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: + '301': + description: Redirect to resolved URL + headers: + Location: + schema: + type: string + description: Ensembl URL + content: {} + '200': + description: OK + headers: + Location: + schema: + type: string + description: Ensembl URL + content: + application/json: + schema: + type: object + properties: + resolved_url: + type: string + description: Resolved url to Ensembl site + '422': + description: Unprocessable Content + content: + application/json: + schema: + example: '{"status_code": 422, "detail": "Unable to process input accession ID"}' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + '500': + $ref: '#/components/responses/500' + /rapid/{species_url_name}/{subpath}: + get: + summary: Resolve rapid site url with species url name and subpath + description: Resolves to Ensembl site url with the species url name and subpath provided. + 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 + - name: subpath + in: path + description: Additional path under species_url_name + required: true + schema: + type: string + example: Location/View + - name: r + in: query + required: false + description: Region string + schema: + type: string + example: 2:361680-384534 + responses: + '301': + description: Redirect to resolved URL + headers: + Location: + schema: + type: string + description: Ensembl URL + content: {} + '200': + description: OK + headers: + Location: + schema: + type: string + description: Ensembl URL + content: + application/json: + schema: + type: object + properties: + resolved_url: + type: string + description: Resolved url to Ensembl site + '422': + description: Unprocessable Content + content: + application/json: + schema: + example: '{"status_code": 422, "detail": "Unable to process input accession ID"}' + '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 + description: Resolves to Ensembl site help page url. + tags: + - Rapid Resolver + parameters: + - name: subpath + in: path + required: true + schema: + type: string + example: index.html + responses: + '301': + description: Redirect to Ensembl help page + headers: + Location: + schema: + type: string + description: Ensembl help page URL + content: {} + '200': + description: OK + headers: + Location: + schema: + type: string + description: Ensembl help page URL + content: + application/json: + schema: + type: object + properties: + resolved_url: + type: string + description: Resolved url to Ensembl site help page + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + '500': + $ref: '#/components/responses/500' components: responses: 400: diff --git a/app/tests/test_rapid.py b/app/tests/test_rapid.py index 89da025..762e2fa 100644 --- a/app/tests/test_rapid.py +++ b/app/tests/test_rapid.py @@ -1,6 +1,7 @@ import unittest from unittest.mock import patch from fastapi.testclient import TestClient +from api.models.resolver import ResolvedURLResponse from core.config import ENSEMBL_URL from main import app @@ -31,6 +32,74 @@ def setUp(self): "genome1": f"{ENSEMBL_URL}/species/genome_uuid1", "genome2": f"{ENSEMBL_URL}/species/xyz", } + + # 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}/", + headers={"accept": "application/json"}, + follow_redirects=False, + ) + self.assertEqual(response.status_code, 200) # OK + self.assertEqual( + response.json(), + ResolvedURLResponse(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", + headers={"accept": "application/json"}, + follow_redirects=False, + ) + self.assertEqual(response.status_code, 200) # OK + self.assertEqual( + response.json(), + ResolvedURLResponse(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 + ) + self.assertEqual(response.status_code, 301) + self.assertIn("location", response.headers) + self.assertEqual( + response.headers["location"], f"{ENSEMBL_URL}/blast" + ) + + # test with accept header for JSON response + response = self.client.get( + f"{self.mock_rapid_api_url}/Blast", + headers={"accept": "application/json"}, + follow_redirects=False, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + ResolvedURLResponse(resolved_url=f"{ENSEMBL_URL}/blast").model_dump(mode='json') + ) + # Test species home page @patch("api.resources.rapid_view.get_genome_id_from_assembly_accession_id") @@ -48,7 +117,7 @@ def test_rapid_species_home_success( follow_redirects=False, ) - self.assertEqual(response.status_code, 307) # Temporary Redirect + self.assertEqual(response.status_code, 301) # Redirect self.assertIn("location", response.headers) self.assertEqual( response.headers["location"], self.mock_resolved_url["genome1"] @@ -64,12 +133,30 @@ def test_rapid_species_home_success( follow_redirects=False, ) - self.assertEqual(response.status_code, 307) # Temporary Redirect + 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 + ) + + response = self.client.get( + f"{self.mock_rapid_api_url}/{self.species_url_name}/", + headers={"accept": "application/json"}, + follow_redirects=False, + ) + + self.assertEqual(response.status_code, 200) # OK + self.assertEqual( + response.json(), + ResolvedURLResponse(resolved_url = self.mock_resolved_url["genome1"]).model_dump(mode='json') + ) + + # Test Region in detail page @patch("api.resources.rapid_view.get_genome_id_from_assembly_accession_id") def test_rapid_species_location_success( @@ -87,12 +174,30 @@ def test_rapid_species_location_success( follow_redirects=False, ) - self.assertEqual(response.status_code, 307) # Redirect + 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 + ) + + response = self.client.get( + f"{self.mock_rapid_api_url}/{self.species_url_name}/Location/View", + params={"r": "1:1000-2000"}, + headers={"accept": "application/json"}, + follow_redirects=False, + ) + + 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') + ) + # Test Gene pages @patch("api.resources.rapid_view.get_genome_id_from_assembly_accession_id") def test_rapid_species_gene_compara_homolog( @@ -108,12 +213,32 @@ def test_rapid_species_gene_compara_homolog( follow_redirects=False, ) - self.assertEqual(response.status_code, 307) # Redirect + 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 + ) + + response = self.client.get( + f"{self.mock_rapid_api_url}/{self.species_url_name}/Gene/Compara_Homolog", + params={"g": "GENE123"}, + follow_redirects=False, + headers={"accept": "application/json"}, + ) + + self.assertEqual(response.status_code, 200) # OK + self.assertEqual( + response.json(), + ResolvedURLResponse( + resolved_url=f"{ENSEMBL_URL}/entity-viewer/genome_uuid1/gene:GENE123?view=homology" + ).model_dump(mode='json'), + ) + # Test 404 @patch("api.resources.rapid_view.get_genome_id_from_assembly_accession_id") def test_rapid_species_404_not_found(