From ed626eedaee0ed155c8628d18a5384af5a032aa9 Mon Sep 17 00:00:00 2001 From: Brian Walsh Date: Wed, 16 Oct 2024 14:47:43 -0700 Subject: [PATCH 1/3] improve docker, quay --- .github/workflows/build.yaml | 2 +- Dockerfile | 23 ++++++++++++++++------- pyproject.toml | 2 +- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 906a403..6e8822d 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -27,7 +27,7 @@ jobs: # Login to Quay.io and build image docker login quay.io - docker build -t $REPO:$BRANCH . + docker build --build-arg GITHUB_SHA=$GITHUB_SHA -t $REPO:$BRANCH . # Add 'latest' tag to 'main' image if [[ $BRANCH == 'main' ]]; then diff --git a/Dockerfile b/Dockerfile index 23a3ff6..fab88a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,21 @@ FROM python:3.12 -WORKDIR /app +ARG GITHUB_SHA +ENV GITHUB_SHA=$GITHUB_SHA -ADD "https://api.github.com/repos/ACED-IDP/image_viewer/commits?per_page=1" latest_commit - -RUN git clone https://github.com/ACED-IDP/image_viewer WORKDIR /app/image_viewer -RUN git checkout development -RUN pip install --no-cache-dir . -RUN git log --oneline +# Copy the project files into the container +COPY pyproject.toml ./ + +RUN pip install . + +# write git commit hash to a file +RUN echo $GITHUB_SHA > git_commit_hash.txt + +# Copy the rest of the project files into the container +COPY . . + +# Expose the port your app listens on +EXPOSE 8000 + CMD ["uvicorn", "image_viewer.app:app", "--reload"] diff --git a/pyproject.toml b/pyproject.toml index 455d719..c5fef2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "image_viewer" version = "0.1.0" description = "A FastAPI project for viewing images" authors = [ - { name = "Your Name", email = "your.email@example.com" } + { name = "Brian Walsh", email = "walsbr@ohsu.edu" } ] dependencies = [ "fastapi", From 6b52d52118d1fed1c6d8417df2039ad821193aa5 Mon Sep 17 00:00:00 2001 From: Brian Walsh Date: Wed, 23 Oct 2024 15:48:16 -0700 Subject: [PATCH 2/3] first draft vcf --- image_viewer/app.py | 4 +- image_viewer/indexd_searcher.py | 100 +++++++++++++++----- tests/fixtures/vcf/vcfExample.vcf.gz | Bin 0 -> 44389 bytes tests/fixtures/vcf/vcfExample.vcf.gz.tbi | Bin 0 -> 155 bytes tests/fixtures/vcf/vcfExampleTwo.vcf.gz | Bin 0 -> 10431 bytes tests/fixtures/vcf/vcfExampleTwo.vcf.gz.tbi | Bin 0 -> 157 bytes tests/unit/app/conftest.py | 6 +- tests/unit/app/test_file_type.py | 32 +++++++ 8 files changed, 115 insertions(+), 27 deletions(-) create mode 100644 tests/fixtures/vcf/vcfExample.vcf.gz create mode 100644 tests/fixtures/vcf/vcfExample.vcf.gz.tbi create mode 100644 tests/fixtures/vcf/vcfExampleTwo.vcf.gz create mode 100644 tests/fixtures/vcf/vcfExampleTwo.vcf.gz.tbi create mode 100644 tests/unit/app/test_file_type.py diff --git a/image_viewer/app.py b/image_viewer/app.py index 4f05940..aec9840 100644 --- a/image_viewer/app.py +++ b/image_viewer/app.py @@ -8,7 +8,7 @@ from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict -from image_viewer.indexd_searcher import aviator_url +from image_viewer.indexd_searcher import redirection_url #AVIVATOR_URL = "https://avivator.gehlenborglab.org/?image_url=" AVIVATOR_URL = "/aviator/?image_url=" @@ -62,7 +62,7 @@ async def view_object(object_id: str, authorization: str = Header(None), access_ try: logger.error(f"in view object {object_id} {settings.base_url}") - redirect_url = aviator_url(object_id, token, settings.base_url) + redirect_url = redirection_url(object_id, token, settings.base_url) logger.error(f"in view object {redirect_url}") return RedirectResponse(url=redirect_url) diff --git a/image_viewer/indexd_searcher.py b/image_viewer/indexd_searcher.py index 1fcdd2b..439f842 100644 --- a/image_viewer/indexd_searcher.py +++ b/image_viewer/indexd_searcher.py @@ -6,13 +6,76 @@ from gen3.index import Gen3Index from image_viewer.object_signer import get_signed_url import logging +import re +from dataclasses import dataclass logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -def aviator_url(object_id: str, access_token: str, base_url: str) -> str: - """Return the URL for the Aviator image viewer. +@dataclass +class RegexEqual(str): + string: str + match: re.Match = None + + def __eq__(self, pattern): + self.match = re.search(pattern, self.string) + return self.match is not None + + +def aviator_url(source_record, base_url, file_service, index_service): + """Return the URL for the Aviator image viewer.""" + source_file_name = source_record["file_name"] + + offset_file_name = source_file_name.replace("ome.tiff", "offsets.json") + offset_file_name = offset_file_name.replace("ome.tif", "offsets.json") + offsets_records = index_service.query_urls(offset_file_name) + if not isinstance(offsets_records, list) or len(offsets_records) != 1: + raise HTTPException(status_code=404, + detail=f"Could not find object with file_name {offset_file_name} {offsets_records}") + offsets_record = offsets_records[0] + if "did" not in offsets_record: + raise HTTPException(status_code=404, detail=f"Could not find did within {offsets_record}") + offsets_object_id = offsets_record["did"] + + # get the signed url for the source object + object_id = source_record["did"] + source_signed_url = get_signed_url(object_id, file_service) + offsets_signed_url = get_signed_url(offsets_object_id, file_service) + + # Use the configurable base_url from settings + # we encode the signed url because it will contain special characters + redirect_url = f"{base_url}{urllib.parse.quote_plus(source_signed_url)}&offsets_url={urllib.parse.quote_plus(offsets_signed_url)}" + return redirect_url + + +def genome_browser_url(source_record, base_url, file_service, index_service): + """Return the URL for the genome browser.""" + vcf_file_name = source_record["file_name"] + + tbi_file_name = vcf_file_name + ".tbi" + tbi_records = index_service.query_urls(tbi_file_name) + if not isinstance(tbi_records, list) or len(tbi_records) != 1: + raise HTTPException(status_code=404, + detail=f"Could not find object with file_name {tbi_file_name} {tbi_records}") + tbi_record = tbi_records[0] + if "did" not in tbi_record: + raise HTTPException(status_code=404, detail=f"Could not find did within {tbi_record}") + tbi_object_id = tbi_record["did"] + + # get the signed url for the source object + object_id = source_record["did"] + source_signed_url = get_signed_url(object_id, file_service) + offsets_signed_url = get_signed_url(tbi_object_id, file_service) + + # Use the configurable base_url from settings + # we encode the signed url because it will contain special characters + redirect_url = f"{base_url}{urllib.parse.quote_plus(source_signed_url)}&offsets_url={urllib.parse.quote_plus(offsets_signed_url)}" + return redirect_url + + +def redirection_url(object_id: str, access_token: str, base_url: str) -> str: + """Return the URL for the object. object_id: str The object ID of an ome.tif file to view access_token: str The access token to use for authentication base_url: str The base URL for the Aviator image viewer @@ -30,32 +93,25 @@ def aviator_url(object_id: str, access_token: str, base_url: str) -> str: file_service = Gen3File(auth) index_service = Gen3Index(auth) + logger.error(f"in redirection_url") + source_record = index_service.get(object_id) if not isinstance(source_record, dict): raise HTTPException(status_code=500, detail=f"Could not find object with id {object_id} {source_record}") - logger.error(f"aviator_url source_record {source_record}") + + logger.error(f"redirection_url source_record {source_record}") + if "file_name" not in source_record: raise HTTPException(status_code=500, detail=f"Could not find file_name within {source_record}") - if "ome.tif" not in source_record["file_name"]: - raise HTTPException(status_code=500, detail=f"Expected file_name to contain 'ome.tif' {source_record}") - source_file_name = source_record["file_name"] - offset_file_name = source_file_name.replace("ome.tiff", "offsets.json") - offset_file_name = offset_file_name.replace("ome.tif", "offsets.json") - offsets_records = index_service.query_urls(offset_file_name) - if not isinstance(offsets_records, list) or len(offsets_records) != 1: - raise HTTPException(status_code=500, - detail=f"Could not find object with file_name {offset_file_name} {offsets_records}") - offsets_record = offsets_records[0] - if "did" not in offsets_record: - raise HTTPException(status_code=500, detail=f"Could not find did within {offsets_record}") - offsets_object_id = offsets_record["did"] + redirect_url = None + match RegexEqual(source_record['file_name']): + case "\\.ome.tif?": + redirect_url = aviator_url(source_record, base_url, file_service, index_service) + case "\\.vcf": + redirect_url = genome_browser_url(source_record, base_url, file_service, index_service) - # get the signed url for the source object - source_signed_url = get_signed_url(object_id, file_service) - offsets_signed_url = get_signed_url(offsets_object_id, file_service) + if not redirect_url: + raise HTTPException(status_code=500, detail=f"Could not match a viewer for {source_record['file_name']}") - # Use the configurable base_url from settings - # we encode the signed url because it will contain special characters - redirect_url = f"{base_url}{urllib.parse.quote_plus(source_signed_url)}&offsets_url={urllib.parse.quote_plus(offsets_signed_url)}" return redirect_url diff --git a/tests/fixtures/vcf/vcfExample.vcf.gz b/tests/fixtures/vcf/vcfExample.vcf.gz new file mode 100644 index 0000000000000000000000000000000000000000..625a0a70d89b7cfe415feefdb197c54ad7504b6f GIT binary patch literal 44389 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 {_}") + print(f"Mocked redirection_url: {object_id} {access_token} {base_url} -> {_}") return _ - monkeypatch.setattr(image_viewer.indexd_searcher, "aviator_url", mock_aviator_url) - print("Monkey patched aviator_url") + monkeypatch.setattr(image_viewer.indexd_searcher, "redirection_url", mock_aviator_url) + print("Monkey patched redirection_url") def monkey_patch_signed_url(monkeypatch): diff --git a/tests/unit/app/test_file_type.py b/tests/unit/app/test_file_type.py new file mode 100644 index 0000000..697fe7a --- /dev/null +++ b/tests/unit/app/test_file_type.py @@ -0,0 +1,32 @@ +from image_viewer.indexd_searcher import RegexEqual + + +def test_simple_match(): + match RegexEqual("Something to match"): + case "^...match": + print("Nope...") + case "^S.*ing$": + print("Closer...") + case "^S.*match$": + print("Yep!") + case _: + assert False, "Should not match anything else" + + +def test_extension_match(): + + match RegexEqual("/a/b/c/d.txt"): + case "\\.txt": + print("ok") + case _: + assert False, "Should not match anything else" + + for file_name in ["/a/b/c/d.ome.tif", "/a/b/c/d.ome.tiff", "/a/b/c/d.vcf.gz", "/a/b/c/d.vcf"]: + match RegexEqual(file_name): + case "\\.ome.tif?": + continue + case "\\.vcf": + continue + case _: + assert False, "Should not match anything else" + assert False, "Should not match anything else" From 9f9d0df59fe3ce19e57da57f981ec757eefd2b44 Mon Sep 17 00:00:00 2001 From: Brian Walsh Date: Thu, 24 Oct 2024 14:56:48 -0700 Subject: [PATCH 3/3] adds /ucsc --- image_viewer/__init__.py | 4 +++ image_viewer/app.py | 25 ++++++++++++++++--- image_viewer/indexd_searcher.py | 24 ++++++++++++------ pyproject.toml | 3 ++- tests/unit/app/conftest.py | 6 +++-- .../unit/app/test_auth_needed_env_variable.py | 2 ++ 6 files changed, 51 insertions(+), 13 deletions(-) diff --git a/image_viewer/__init__.py b/image_viewer/__init__.py index e69de29..be59a5b 100644 --- a/image_viewer/__init__.py +++ b/image_viewer/__init__.py @@ -0,0 +1,4 @@ +from cacheout import Cache + +cache = Cache(ttl=60) # 1 second + diff --git a/image_viewer/app.py b/image_viewer/app.py index aec9840..7e176e0 100644 --- a/image_viewer/app.py +++ b/image_viewer/app.py @@ -3,11 +3,12 @@ import threading import uvicorn -from fastapi import FastAPI, HTTPException, Header, Cookie +from fastapi import FastAPI, HTTPException, Header, Cookie, Request from fastapi.responses import RedirectResponse from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict +from image_viewer import cache from image_viewer.indexd_searcher import redirection_url #AVIVATOR_URL = "https://avivator.gehlenborglab.org/?image_url=" @@ -44,7 +45,8 @@ async def health_check(): summary="View Object", description="Redirects to a URL for the object.", responses={307: {"description": "Temporary Redirect"}}) -async def view_object(object_id: str, authorization: str = Header(None), access_token: str = Cookie(None)): +async def view_object(object_id: str, request: Request, authorization: str = Header(None), access_token: str = Cookie(None)): + """Create a view for the object, render a redirect.""" token = None @@ -62,7 +64,7 @@ async def view_object(object_id: str, authorization: str = Header(None), access_ try: logger.error(f"in view object {object_id} {settings.base_url}") - redirect_url = redirection_url(object_id, token, settings.base_url) + redirect_url = redirection_url(object_id, token, settings.base_url, request) logger.error(f"in view object {redirect_url}") return RedirectResponse(url=redirect_url) @@ -71,6 +73,23 @@ async def view_object(object_id: str, authorization: str = Header(None), access_ raise HTTPException(status_code=e.status_code, detail=str(e)) +@app.get("/ucsc/{token_hash}/{object_id}", + summary="UCSC Genome Browser Track definition", + description="Redirects to a URL for the object.", + responses={200: {"description": "Track config https://genome.ucsc.edu/goldenpath/help/trackDb/trackDbHub.html"}}) +async def ucsc_track(token_hash: str, object_id: str, authorization: str = Header(None), access_token: str = Cookie(None)): + urls: dict = cache.get(f"{object_id}_{token_hash}") + if not urls: + raise HTTPException(status_code=404, detail="No signed URL found") + source = urls.get("source") + tbi = urls.get("tbi") + if not source or not tbi: + raise HTTPException(status_code=404, detail="No signed URL found") + return f""" + track type=vcf name="vcf" description="vcf" visibility=full bigDataUrl="{source}" bigDataIndex="{tbi}" + """ + + # Make the application multi-threaded def run_server(): uvicorn.run(app, host="0.0.0.0", port=8000, workers=4) # workers=4 makes the app multi-threaded. diff --git a/image_viewer/indexd_searcher.py b/image_viewer/indexd_searcher.py index 439f842..02c0f78 100644 --- a/image_viewer/indexd_searcher.py +++ b/image_viewer/indexd_searcher.py @@ -1,9 +1,12 @@ +import hashlib import urllib -from fastapi import HTTPException +from fastapi import HTTPException, Request from gen3.auth import Gen3Auth from gen3.file import Gen3File from gen3.index import Gen3Index + +from image_viewer import cache from image_viewer.object_signer import get_signed_url import logging import re @@ -49,7 +52,7 @@ def aviator_url(source_record, base_url, file_service, index_service): return redirect_url -def genome_browser_url(source_record, base_url, file_service, index_service): +def genome_browser_url(source_record, base_url, access_token, file_service, index_service): """Return the URL for the genome browser.""" vcf_file_name = source_record["file_name"] @@ -66,15 +69,22 @@ def genome_browser_url(source_record, base_url, file_service, index_service): # get the signed url for the source object object_id = source_record["did"] source_signed_url = get_signed_url(object_id, file_service) - offsets_signed_url = get_signed_url(tbi_object_id, file_service) + tbi_signed_url = get_signed_url(tbi_object_id, file_service) # Use the configurable base_url from settings # we encode the signed url because it will contain special characters - redirect_url = f"{base_url}{urllib.parse.quote_plus(source_signed_url)}&offsets_url={urllib.parse.quote_plus(offsets_signed_url)}" + access_token_hash = hashlib.md5(access_token.encode()).hexdigest() + cache.set(f"{object_id}_{access_token_hash}", { + "source": source_signed_url, + "tbi": tbi_signed_url + }) + # coordinate + hub_url = f"{base_url}/ucsc/{access_token_hash}/{object_id}" + redirect_url = f"http://genome.ucsc.edu/cgi-bin/hgTracks?hubUrl={urllib.parse.quote_plus(hub_url)}" return redirect_url -def redirection_url(object_id: str, access_token: str, base_url: str) -> str: +def redirection_url(object_id: str, access_token: str, base_url: str, request: Request) -> str: """Return the URL for the object. object_id: str The object ID of an ome.tif file to view access_token: str The access token to use for authentication @@ -97,7 +107,7 @@ def redirection_url(object_id: str, access_token: str, base_url: str) -> str: source_record = index_service.get(object_id) if not isinstance(source_record, dict): - raise HTTPException(status_code=500, detail=f"Could not find object with id {object_id} {source_record}") + raise HTTPException(status_code=404, detail=f"Could not find object with id {object_id} {source_record}") logger.error(f"redirection_url source_record {source_record}") @@ -109,7 +119,7 @@ def redirection_url(object_id: str, access_token: str, base_url: str) -> str: case "\\.ome.tif?": redirect_url = aviator_url(source_record, base_url, file_service, index_service) case "\\.vcf": - redirect_url = genome_browser_url(source_record, base_url, file_service, index_service) + redirect_url = genome_browser_url(source_record, request.base_url, access_token, file_service, index_service) if not redirect_url: raise HTTPException(status_code=500, detail=f"Could not match a viewer for {source_record['file_name']}") diff --git a/pyproject.toml b/pyproject.toml index c5fef2d..3e249c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,8 @@ dependencies = [ "requests", "pydantic-settings", "python-dotenv", - "gen3" + "gen3", + "cacheout" ] [project.optional-dependencies] diff --git a/tests/unit/app/conftest.py b/tests/unit/app/conftest.py index a225323..43dd012 100644 --- a/tests/unit/app/conftest.py +++ b/tests/unit/app/conftest.py @@ -1,6 +1,6 @@ import urllib.parse from importlib import reload - +from fastapi import Request from fastapi.testclient import TestClient import pytest import image_viewer.app @@ -41,8 +41,9 @@ def monkey_patch_aviator(monkeypatch): """Monkey patch the Aviator URL response.""" import image_viewer.indexd_searcher - def mock_aviator_url(object_id, access_token, base_url): + def mock_aviator_url(object_id, access_token, base_url, request: Request): """Mock the Aviator URL response""" + print("In mock_aviator_url") image_url = urllib.parse.quote_plus(f'https://image-{object_id}') offsets_url = urllib.parse.quote_plus(f'https://offsets-{object_id}') parms = f'image_url={image_url}&offsets_url={offsets_url}' @@ -52,6 +53,7 @@ def mock_aviator_url(object_id, access_token, base_url): monkeypatch.setattr(image_viewer.indexd_searcher, "redirection_url", mock_aviator_url) print("Monkey patched redirection_url") + # print(image_viewer.indexd_searcher.redirection_url("123", "456", "789", None)) def monkey_patch_signed_url(monkeypatch): diff --git a/tests/unit/app/test_auth_needed_env_variable.py b/tests/unit/app/test_auth_needed_env_variable.py index 51b300a..c17f352 100644 --- a/tests/unit/app/test_auth_needed_env_variable.py +++ b/tests/unit/app/test_auth_needed_env_variable.py @@ -16,11 +16,13 @@ def client_with_cookie_base_url(monkeypatch, base_url, valid_token): client_ = TestClient(image_viewer.app.app) client_.cookies.update({"access_token": valid_token}) + print(client_) yield client_ # Test setting base_url via environment variable def test_base_url_from_env_variable(monkeypatch, client_with_cookie_base_url, base_url): + print(client_with_cookie_base_url) object_id = "123" response = client_with_cookie_base_url.get(f"/view/{object_id}", follow_redirects=False)