Skip to content

Commit

Permalink
add api for bssh runs sample sheet
Browse files Browse the repository at this point in the history
  • Loading branch information
raylrui committed Feb 25, 2025
1 parent a77f46b commit 08a39bb
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from sequence_run_manager.viewsets.state import StateViewSet
from sequence_run_manager.viewsets.comment import CommentViewSet
from sequence_run_manager.viewsets.sequence_run_stats import SequenceStatsViewSet
from sequence_run_manager.viewsets.sequence_run_action import SequenceRunActionViewSet
from sequence_run_manager.settings.base import API_VERSION

api_namespace = "api"
Expand All @@ -15,11 +16,12 @@

router = OptionalSlashDefaultRouter()
router.register(r"sequence", SequenceViewSet, basename="sequence")

router.register(r"sequence", SequenceRunActionViewSet, basename="sequence-action")
router.register("sequence/(?P<orcabus_id>[^/]+)/comment", CommentViewSet, basename="sequence-comment")
router.register("sequence/(?P<orcabus_id>[^/]+)/state", StateViewSet, basename="sequence-states")
router.register(r"sequence/stats", SequenceStatsViewSet, basename="sequence-stats")


urlpatterns = [
path(f"{api_base}", include(router.urls)),
path('schema/openapi.json', SpectacularJSONAPIView.as_view(), name='schema'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from sequence_run_manager_proc.services.bssh_srv import BSSHService
from rest_framework.viewsets import ViewSet
from rest_framework.response import Response
from rest_framework.generics import get_object_or_404
from rest_framework.decorators import action
from rest_framework import status
from drf_spectacular.utils import extend_schema
from drf_spectacular.types import OpenApiTypes
from sequence_run_manager.models import Sequence
import logging
logger = logging.getLogger(__name__)

class SequenceRunActionViewSet(ViewSet):
"""
ViewSet for sequence run actions
"""
lookup_value_regex = "[^/]+"
queryset = Sequence.objects.all()

@extend_schema(
responses=OpenApiTypes.OBJECT,
description="Get the sample sheet for a sequence run"
)
@action(detail=True, methods=['get'], url_name='get_sample_sheet', url_path='get_sample_sheet')
def get_sample_sheet(self, request, *args, **kwargs):
"""
Get the sample sheet for a sequence run
"""
pk = self.kwargs.get('pk')
sequence_run = get_object_or_404(self.queryset, pk=pk)



logger.info(f'Sequence run: {sequence_run}')

logger.info(f'Sequence run: {sequence_run.api_url}')
logger.info(f'Sequence run: {sequence_run.sample_sheet_name}')

bssh_srv = BSSHService()
sample_sheet = bssh_srv.get_sample_sheet_from_bssh_run_files(sequence_run.api_url, sequence_run.sample_sheet_name)
if not sample_sheet:
return Response({"detail": "Sample sheet not found"}, status=status.HTTP_404_NOT_FOUND)
else:
response = {
"SampleSheetName": sequence_run.sample_sheet_name,
"SampleSheetContent": sample_sheet
}
return Response(response, status=status.HTTP_200_OK)
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import base64
import json
from libumccr.aws import libsm
import requests

logger = logging.getLogger(__name__)

Expand All @@ -15,7 +16,12 @@ class BSSHService:

def __init__(self):
assert os.environ.get("BASESPACE_ACCESS_TOKEN_SECRET_ID", None), "BASESPACE_ACCESS_TOKEN_SECRET_ID is not set"
BASESPACE_ACCESS_TOKEN = libsm.get_secret(os.environ.get("BASESPACE_ACCESS_TOKEN_SECRET_ID"))
try:
BASESPACE_ACCESS_TOKEN = libsm.get_secret(os.environ.get("BASESPACE_ACCESS_TOKEN_SECRET_ID"))
except Exception as e:
logger.error(f"Error retrieving BSSH token from the Secret Manager: {e}")
raise e

if not BASESPACE_ACCESS_TOKEN:
raise ValueError("BSSH_TOKEN is not set")
self.headers = {
Expand All @@ -24,6 +30,32 @@ def __init__(self):
}
self.base_url = "https://api.aps2.sh.basespace.illumina.com/v2/"


def handle_request_error(self, e: Exception, operation: str):
"""
Handles various request exceptions and returns appropriate error
Args:
e: The caught exception
operation: Description of the operation being performed
"""
if isinstance(e, requests.exceptions.HTTPError):
logger.error(f"HTTP error occurred: {e.response.status_code} - {e.response.reason}")
logger.error(f"Response text: {e.response.text}")
raise ValueError(f"Error {operation}: {str(e)}")

elif isinstance(e, (requests.exceptions.ConnectionError, requests.exceptions.Timeout)):
logger.error(f"Connection error occurred: {str(e)}")
raise ValueError(f"Error connecting to BSSH: {str(e)}")

elif isinstance(e, requests.exceptions.RequestException):
logger.error(f"Request error occurred: {str(e)}")
raise ValueError(f"Error making request to BSSH: {str(e)}")

else:
logger.error(f"Unexpected error: {str(e)}")
raise ValueError(f"Unexpected error {operation}: {str(e)}")

def get_run_details(self, api_url: str) -> Dict[str, Any]:
"""
Retrieve run details from ICA API
Expand Down Expand Up @@ -146,15 +178,20 @@ def get_run_details(self, api_url: str) -> Dict[str, Any]:
}
"""

response = urllib.request.Request(api_url, headers=self.headers)
with urllib.request.urlopen(response) as response:
if response.status < 200 or response.status >= 300:
raise ValueError(f'Non 20X status code returned')

logger.info(f'Bssh run details api call successfully.')
response_json = json.loads(response.read().decode())
return response_json

try:
response = requests.get(
api_url,
headers=self.headers
)

# Raise error for bad status codes
response.raise_for_status()

logger.info('BSSH run details API call successful.')
return response.json()

except Exception as e:
self.handle_request_error(e, "getting run details")

def get_libraries_from_run_details(self, run_details: Dict[str, Any]) -> List[str]:
"""
Expand All @@ -166,7 +203,7 @@ def get_libraries_from_run_details(self, run_details: Dict[str, Any]) -> List[st
libraries.extend([lib.get('Name') for lib in item.get('LibraryItems', [])])
return libraries

def get_sample_sheet(self, api_url: str, sample_sheet_name: str) -> Optional[Dict[str, Any]]:
def get_sample_sheet_from_bssh_run_files(self, api_url: str, sample_sheet_name: str) -> Optional[Dict[str, Any]]:
"""
Retrieve sample sheet from ICA project
Expand Down Expand Up @@ -217,6 +254,8 @@ def get_sample_sheet(self, api_url: str, sample_sheet_name: str) -> Optional[Dic
# Construct API URL for files in project
bssh_run_files_url = f"{api_url}/files"

logger.info(f'Bssh run files url: {bssh_run_files_url} , sample sheet name: {sample_sheet_name}')

try:
file_content_url = None
offset = 0
Expand All @@ -229,15 +268,17 @@ def get_sample_sheet(self, api_url: str, sample_sheet_name: str) -> Optional[Dic
'offset': offset,
'limit': limit
}
response = urllib.request.Request(bssh_run_files_url, headers=self.headers, params=params)
with urllib.request.urlopen(response) as response:
if response.status < 200 or response.status >= 300:
raise ValueError(f'Non 20X status code returned when getting files in bssh run')

response_json = json.loads(response.read().decode())

files = response_json.get('items', [])
# Use requests library for the API call
response = requests.get(
bssh_run_files_url,
params=params, # requests will handle query parameter encoding
headers=self.headers
)

# Raise error for bad status codes
response.raise_for_status()
response_json = response.json()
files = response_json.get('Items', [])
if not files:
break # No more items to process

Expand All @@ -259,27 +300,29 @@ def get_sample_sheet(self, api_url: str, sample_sheet_name: str) -> Optional[Dic
if not file_content_url:
raise ValueError("Sample sheet not found")

# Get file content
response = urllib.request.Request(file_content_url, headers=self.headers)
with urllib.request.urlopen(response) as response:
if response.status < 200 or response.status >= 300:
raise ValueError(f'Non 20X status code returned when getting file content')

logger.info(f'File content api call successfully.')
response_json = json.loads(response.read().decode())

# Get the raw content as bytes
content = response.read()
logger.info(f'File content url: {file_content_url}')

# Compress with gzip
compressed = gzip.compress(content)

# Encode to base64 and convert to string
b64gz_string = base64.b64encode(compressed).decode('utf-8')
try:
response = requests.get(
file_content_url,
headers=self.headers,
stream=True # Important for binary content
)
response.raise_for_status()
content = response.content

if not content:
raise ValueError('Empty content received from BSSH')

logger.info(f'Successfully retrieved file content, size: {len(content)} bytes')

return b64gz_string
# Convert to base64
b64gz_string = base64.b64encode(content).decode('utf-8')
return b64gz_string

except Exception as e:
self.handle_request_error(e, "getting sample sheet content")

except Exception as e:
raise ValueError(f"Error getting files in project: {e}")

self.handle_request_error(e, "getting sample sheet file")

Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

router = OptionalSlashDefaultRouter()

router.register(r"workflowrun/stats", WorkflowRunStatsViewSet, basename="workflowrun_list_all") # put it before workflowrun, as it will match the workflowrun/list_all/ url
router.register(r"workflowrun/stats", WorkflowRunStatsViewSet, basename="workflowrun_stats")
router.register(r"analysis", AnalysisViewSet, basename="analysis")
router.register(r"analysisrun", AnalysisRunViewSet, basename="analysisrun")
router.register(r"analysiscontext", AnalysisContextViewSet, basename="analysiscontext")
Expand Down

0 comments on commit 08a39bb

Please sign in to comment.