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
24 changes: 22 additions & 2 deletions app/api/models/resolver.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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
117 changes: 96 additions & 21 deletions app/api/resources/rapid_view.py
Original file line number Diff line number Diff line change
@@ -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()]

Expand All @@ -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
Expand All @@ -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)

Expand All @@ -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,
Copy link

@veidenberg veidenberg Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ticket also mentions the case of multiple resolved URLs (code 300), which is not addressed here. In principle an assembly accession ID can resolve to multiple genome UUIDs (if it's part of both partial and integrated release, like the human reference), but it shouldn't apply to rapid species so it's fine (assuming any rapid assembly won't become part of a partial release in the future).

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
16 changes: 16 additions & 0 deletions app/api/resources/templates/rapid/_home_icon.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve" width="19px" height="19px" fill="#0099ff">
<path d="M26.4299088,18.9942322v7.4855804c0,0.6986561-0.4990387,1.2975006-1.2974987,1.2975006h-5.9884644
c-0.2994232,0-0.6986561-0.2994232-0.6986561-0.6986542v-5.7888489c0-0.2994232-0.2994213-0.6986561-0.6986542-0.6986561h-3.4932718
c-0.2994232,0-0.6986532,0.2994232-0.6986532,0.6986561v5.7888489c0,0.2994213-0.2994242,0.6986542-0.6986542,0.6986542H6.8675914
c-0.6986537,0-1.2975006-0.4990387-1.2975006-1.2975006v-7.4855804c0-0.1996155,0.0624285-0.3679199,0.1996155-0.4990387
l9.7811584-7.9846191c0.1996145-0.1996155,0.4990387-0.1996155,0.7984619,0l9.7811604,7.9846191
C26.3276825,18.6263123,26.3749332,18.7605743,26.4299088,18.9942322L26.4299088,18.9942322z"/>
<path d="M15.6065302,8.5148048c0.1996155-0.1996155,0.4990396-0.1996155,0.798461,0l12.0700836,9.9158611
c0.4120693,0.3384781,0.9688454,0.2207394,1.2509117-0.1226521l1.0718441-1.304884
c0.3384781-0.4120693,0.2207394-0.9688473-0.1226501-1.2509108L17.6026859,5.0215335
c-0.9980774-0.7984619-2.295579-0.7984619-3.1938486,0L1.3248215,15.7522116
c-0.343391,0.2820635-0.4611307,0.8388414-0.1226532,1.2509108l1.0718439,1.3048859
c0.2820647,0.3433914,0.8388419,0.4611301,1.2509112,0.122654L15.6065302,8.5148048z"/>
</svg>
25 changes: 25 additions & 0 deletions app/api/resources/templates/rapid/_logo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="e_x5F_logotype" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 236 32" style="enable-background:new 0 0 236 32;" xml:space="preserve" height="12px" fill="#ffffff">
<path d="M234.1,26.2v4.3h-24V1.4h4.5v24.8C214.6,26.2,234.1,26.2,234.1,26.2z"/>
<path d="M190.9,1.4c3.7,0,6.4,0.6,8.3,1.9c1.9,1.3,2.8,3.1,2.8,5.5c0,1.3-0.4,2.5-1.1,3.5c-0.8,1-1.9,1.8-3.4,2.4
c1.1,0.3,2.1,0.8,3,1.4c0.9,0.6,1.7,1.4,2.2,2.4c0.6,1,0.9,2.2,0.9,3.5c0,1.7-0.3,3.1-1,4.2s-1.6,2-2.7,2.6
c-1.1,0.6-2.4,1.1-3.7,1.4c-1.4,0.3-2.7,0.4-4.1,0.4h-17V1.4H190.9z M191.3,5.8h-11.8v7.5h12.3c1,0,1.9-0.2,2.8-0.5
c0.9-0.3,1.5-0.7,2.1-1.3c0.5-0.6,0.8-1.3,0.8-2.1c0-1.3-0.6-2.3-1.8-2.8C194.5,6,193,5.8,191.3,5.8z M192,17.6h-12.5v8.6h12.7
c2,0,3.6-0.4,4.9-1.1c1.3-0.7,1.9-1.8,1.9-3.3c0-1.1-0.4-2-1.1-2.6c-0.7-0.6-1.6-1-2.7-1.3C194.1,17.8,193.1,17.6,192,17.6z"/>
<path d="M138.4,1.4l11.5,15.5l11.5-15.5h4.4v29.2h-4.5V18.2c0-1.8,0-3.5,0.1-5.1s0.2-3.2,0.4-4.9l-10.5,14h-2.7L138,8.2
c0.2,1.6,0.3,3.2,0.4,4.9c0.1,1.6,0.1,3.3,0.1,5.1v12.4H134V1.4H138.4z"/>
<path d="M101.7,1.4h24.2v4.3h-19.7v7.7h17.4v4.3h-17.4v8.5h19.7v4.3h-24.2V1.4z"/>
<path d="M69.9,21.6c1.6,1.7,3.4,3,5.2,4c1.8,0.9,4,1.4,6.6,1.4c1.4,0,2.8-0.2,4.2-0.5c1.3-0.3,2.4-0.9,3.3-1.6s1.3-1.6,1.3-2.7
c0-0.9-0.4-1.7-1.1-2.1c-0.8-0.5-1.9-0.9-3.3-1.1c-1.4-0.3-3.2-0.5-5.2-0.8c-2.5-0.3-4.7-0.8-6.5-1.5c-1.8-0.7-3.2-1.6-4.2-2.7
s-1.5-2.6-1.5-4.3c0-1.8,0.6-3.4,1.7-4.7c1-1.3,2.6-2.3,4.5-3c2-0.7,4.1-1,6.6-1c3,0,5.5,0.4,7.7,1.3s3.9,2.2,5.1,3.8l-3,3.3
c-1.1-1.3-2.5-2.4-4.1-3.2C85.5,5.4,83.6,5,81.4,5c-1.5,0-2.8,0.2-4,0.5s-2.1,0.8-2.8,1.4c-0.7,0.6-1,1.3-1,2.2c0,1,0.4,1.8,1.1,2.4
c0.7,0.6,1.8,1.1,3.1,1.4c1.3,0.3,2.9,0.6,4.7,0.8c1.7,0.2,3.3,0.4,4.8,0.7c1.6,0.3,3,0.7,4.2,1.3c1.2,0.5,2.2,1.3,2.9,2.2
c0.7,0.9,1.1,2.1,1.1,3.6c0,1.9-0.6,3.5-1.7,5c-1.1,1.4-2.7,2.5-4.8,3.3c-2.1,0.8-4.5,1.2-7.2,1.2c-3,0-5.7-0.4-8.2-1.3
c-2.4-0.9-4.6-2.3-6.4-4.3L69.9,21.6z"/>
<path d="M56.2,1.4h4.5v29.2h-4.5L37.3,7.7c0.2,1.8,0.3,3.6,0.5,5.4s0.3,3.6,0.3,5.4v12.1h-4.5V1.4H38l18.9,23.5
c-0.2-1.4-0.3-2.8-0.4-4.5c-0.1-1.6-0.2-3.2-0.2-4.9c0-1.6-0.1-3.2-0.1-4.6V1.4z"/>
<path d="M25.5,1.4v4.3H5.8v7.7h17.4v4.3H5.8l0.1,4.1c0,1.4,0.7,2.6,1.9,3.3c1.3,0.7,2.9,1.1,4.9,1.1h12.8v4.3H12.9
c-1.4,0-2.7-0.1-4.1-0.4s-2.6-0.7-3.7-1.4c-1.1-0.6-2-1.5-2.7-2.6s-1-2.5-1-4.2v-1.3V1.4H25.5z"/>
</svg>
25 changes: 25 additions & 0 deletions app/api/resources/templates/rapid/appbar.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<div class="topbar">
<div class="topbar-left">
<div class="home-link">
<a href="https://beta.ensembl.org/">
{% include "_home_icon.html" %}
</a>
</div>
<div class="logotype-wrapper">
{% include "_logo.html" %}
</div>
<div class="topbar-left-text">
<div class="release">
<span class="light">Beta</span>
</div>
<div class="copyright">
<a href="https://www.ebi.ac.uk" target="_blank" rel="noopener noreferrer">
© EMBL-EBI
</a>
</div>
</div>
</div>
<div class="topbar-right">
<span>Genome data & annotation</span>
</div>
</div>
44 changes: 44 additions & 0 deletions app/api/resources/templates/rapid/content.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{% import "url.html" as url_helper %}

<div class="redirect-content">
<div class="redirect-info">
{% if response.response_type == "BLAST" or response.response_type == "HELP" %}
{% if response.species_name %}
<div class="search-param">
<span class="search-param-label">Species</span> {{ response.species_name }}
</div>
{% endif %}
{{ url_helper.render_url(response.resolved_url) }}
{% elif response.response_type == "INFO" %}
<div class="search-param">
<span class="search-param-label">Species</span> {{ response.species_name }}
</div>
{% if response.gene_id %}
<div class="search-param">
<span class="search-param-label">Gene</span> {{ response.gene_id }}
</div>
{% endif %}
{% if response.location %}
<div class="search-param">
<span class="search-param-label">Location</span> {{ response.location }}
</div>
{% endif %}
{{ url_helper.render_url(response.resolved_url) }}
{% elif response.response_type == "ERROR" %}
<div class="search-param">
<span class="search-param-label">Species</span> {{ response.species_name }}
</div>
<div class="error-message">
{{ response.message }}
</div>
{{ url_helper.render_url(response.resolved_url) }}
{% else %}
{{ url_helper.render_url("https://beta.ensembl.org/") }}
{% endif %}
</div>
<div class="redirect-text">
<span>
You will be redirected to the new Ensembl website, where you will find the latest genomic information
</span>
</div>
</div>
10 changes: 10 additions & 0 deletions app/api/resources/templates/rapid/footer.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<div class="footer">
<span>
If you require data in Ensembl Rapid release archive, or need links to the Rapid Release ftp, please visit
</span>
</div>
<div class="footer-link">
<a href="{{ response.rapid_archive_url }}" target="_blank" rel="noopener noreferrer">
rapid-archive.ensembl.org
</a>
</div>
26 changes: 26 additions & 0 deletions app/api/resources/templates/rapid/main.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<title>Ensembl Rapid Resolver</title>

<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Automatically refresh/redirects to the target url (resolved rapid url) after 10 seconds -->
<meta http-equiv="refresh" content="10;url={{ response.resolved_url }}" />

<link rel="icon" type="image/png" sizes="32x32" href="/static/icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/static/icons/favicon-16x16.png" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="/static/css/rapid_page.css">
</head>
<body>
{% include "appbar.html" %}
<div class="container">
{% include "venn.html" %}
{% include "content.html" %}
{% include "footer.html" %}
</div>
</body>
</html>
8 changes: 8 additions & 0 deletions app/api/resources/templates/rapid/url.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{% macro render_url(url) %}
<div>
<span class="search-param-label">Redirecting to </span>
<a class="link" href="{{ url }}">
{{ url }}
</a>
</div>
{% endmacro %}
14 changes: 14 additions & 0 deletions app/api/resources/templates/rapid/venn.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<div class="venn-header">
<span>Venn of the redirect</span>
</div>
<div class="venn">
<div class="circle a">
<span>Your URL has changed or gone...</span>
</div>
<div class="circle b">
<span>...we suggest this instead</span>
</div>
<div class="intersection">
<span>{{ response.code }}</span>
</div>
</div>
Loading