Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
21 changes: 20 additions & 1 deletion 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,23 @@ class MetadataResult(BaseModel):
class ResolvedPayload(MetadataResult):
resolved_url: str


class RapidRedirectResponseType(str, Enum):
HOME = "HOME"
ERROR = "ERROR"
BLAST = "BLAST"
HELP = "HELP"
INFO = "INFO"


class ResolvedURLResponse(BaseModel):
response_type: Annotated[Optional[RapidRedirectResponseType], Field(exclude=True)] = None
code: Annotated[Optional[int], Field(exclude=True)] = None
resolved_url: str
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

class Config:
use_enum_values = True
94 changes: 75 additions & 19 deletions app/api/resources/rapid_view.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
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 ResolvedURLResponse, RapidRedirectResponseType
from core.logging import InterceptHandler
from core.config import ENSEMBL_URL
from api.utils.metadata import get_genome_id_from_assembly_accession_id
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 = ResolvedURLResponse(
response_type=RapidRedirectResponseType.HELP,
code=308,
resolved_url=f"{ENSEMBL_URL}/help",
)
return resolved_response(response, request)


@router.get("/Blast", name="Resolve rapid blast page")
Copy link

Choose a reason for hiding this comment

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

we probably don't need this.

async def resolve_rapid_blast(request: Request):
blast_page_url = f"{ENSEMBL_URL}/blast"
return resolved_response(blast_page_url, request)
response = ResolvedURLResponse(
response_type=RapidRedirectResponseType.BLAST,
code=308,
resolved_url=f"{ENSEMBL_URL}/blast",
)
return resolved_response(response, request)


# Resolve rapid urls
Expand All @@ -34,9 +48,15 @@ async def resolve_species(
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 = ResolvedURLResponse(
response_type=RapidRedirectResponseType.ERROR,
code=422,
resolved_url=ENSEMBL_URL,
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 +68,63 @@ async def resolve_species(
query_params = parse_qs(query_string, separator=";")

url = construct_url(genome_id, subpath, query_params)
return resolved_response(url, request)
response = ResolvedURLResponse(
response_type=RapidRedirectResponseType.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],
)
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 = ResolvedURLResponse(
response_type=RapidRedirectResponseType.ERROR,
code=e.status_code,
resolved_url=ENSEMBL_URL,
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 = ResolvedURLResponse(
response_type=RapidRedirectResponseType.ERROR,
code=500,
resolved_url=ENSEMBL_URL,
message="Unexpected error occurred",
)
return resolved_response(response, request)


@router.get("/", name="Rapid Home")
async def resolve_home(request: Request):
return resolved_response(ENSEMBL_URL, request)
response = ResolvedURLResponse(
response_type=RapidRedirectResponseType.HOME,
code=308,
resolved_url=ENSEMBL_URL,
)
return resolved_response(response, request)


def resolved_response(url: str, request: Request):
def resolved_response(response: ResolvedURLResponse, request: Request):
if "application/json" in request.headers.get("accept"):
return ResolvedURLResponse(resolved_url=url)
return RedirectResponse(url=url, status_code=301)
if response.response_type == RapidRedirectResponseType.ERROR:
raise HTTPException(
status_code=response.status_code,
detail=response.message or "An error occurred",
)
return ResolvedURLResponse(resolved_url=response.resolved_url)
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
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="https://rapid-archive.ensembl.org" target="_blank" rel="noopener noreferrer">
rapid-archive.ensembl.org
</a>
</div>
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>
39 changes: 39 additions & 0 deletions app/api/resources/templates/rapid/content.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{% import "url.html" as url_helper %}

<div class="redirect-content">
<div class="redirect-info">
{% if response.response_type == "BLAST" or response.response_type == "HELP" %}
{{ 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("https://beta.ensembl.org/") }}
Copy link

@azangru azangru Aug 21, 2025

Choose a reason for hiding this comment

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

To which page on the beta site will the user be redirected?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure to which page we need to redirect in case of error

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.

For 404, the XD suggests to redirect to Species selector. For other errors, where (by definition) there is no succesfully resolved URL, we can use 404 template (with redirect to home page or species selector), or skip redirection altogether (only showing the error message).

{% 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>
27 changes: 27 additions & 0 deletions app/api/resources/templates/rapid/main.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!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:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
Copy link

@azangru azangru Aug 21, 2025

Choose a reason for hiding this comment

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

please check if you need all those font weights and font styles. Also, why roboto? And why is Lato italic both times?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't have dev version of XD. I'll update it after checking the XD

For now removed italics and Roboto and kept light, regular, bold weights.

<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
Loading