Source code for samplesheets.views_api

"""REST API views for the samplesheets app"""
from irods.exception import CAT_NO_ROWS_FOUND
from irods.models import DataObject
import logging
import re

from django.conf import settings
from django.urls import reverse

from rest_framework import status
from rest_framework.exceptions import (
    APIException,
    ParseError,
    ValidationError,
    NotFound,
)
from rest_framework.generics import RetrieveAPIView
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

# Projectroles dependency
from projectroles.app_settings import AppSettingAPI
from projectroles.models import RemoteSite
from projectroles.plugins import get_backend_api
from projectroles.views_api import (
    SODARAPIBaseMixin,
    SODARAPIBaseProjectMixin,
    SODARAPIGenericProjectMixin,
)

from samplesheets.io import SampleSheetIO
from samplesheets.models import Investigation, ISATab
from samplesheets.rendering import SampleSheetTableBuilder
from samplesheets.serializers import InvestigationSerializer
from samplesheets.views import (
    IrodsCollsCreateViewMixin,
    SheetImportMixin,
    SheetISAExportMixin,
    SITE_MODE_TARGET,
    REMOTE_LEVEL_READ_ROLES,
)


app_settings = AppSettingAPI()
logger = logging.getLogger(__name__)


MD5_RE = re.compile(r'([a-fA-F\d]{32})')
APP_NAME = 'samplesheets'


# API Views --------------------------------------------------------------------


[docs]class InvestigationRetrieveAPIView( SODARAPIGenericProjectMixin, RetrieveAPIView ): """ Retrieve metadata of an investigation with its studies and assays. This view can be used to e.g. retrieve assay UUIDs for landing zone operations. **URL:** ``/samplesheets/api/investigation/retrieve/{Project.sodar_uuid}`` **Methods:** ``GET`` **Returns:** - ``archive_name``: Original archive name if imported from a zip (string) - ``comments``: Investigation comments (JSON) - ``description``: Investigation description (string) - ``file_name``: Investigation file name (string) - ``identifier``: Locally unique investigation identifier (string) - ``irods_status``: Whether iRODS collections for the investigation have been created (boolean) - ``parser_version``: Version of altamISA used in importing (string) - ``project``: Project UUID (string) - ``sodar_uuid``: Investigation UUID (string) - ``studies``: Study and assay information (JSON, using study UUID as key) - ``title``: Investigation title (string) """ lookup_field = 'project__sodar_uuid' permission_required = 'samplesheets.view_sheet' serializer_class = InvestigationSerializer
[docs]class IrodsCollsCreateAPIView( IrodsCollsCreateViewMixin, SODARAPIBaseProjectMixin, APIView ): """ Create iRODS collections for a project. **URL:** ``/samplesheets/api/irods/collections/create/{Project.sodar_uuid}`` **Methods:** ``POST`` **Returns:** - ``path``: Full iRODS path to the root of created collections (string) """ http_method_names = ['post'] permission_required = 'samplesheets.create_colls' def post(self, request, *args, **kwargs): """POST request for creating iRODS collections""" irods_backend = get_backend_api('omics_irods', conn=False) ex_msg = 'Creating iRODS collections failed: ' investigation = Investigation.objects.filter( project__sodar_uuid=self.kwargs.get('project'), active=True ).first() if not investigation: raise ValidationError('{}Investigation not found'.format(ex_msg)) # TODO: TBD: Also allow updating? if investigation.irods_status: raise ValidationError( '{}iRODS collections already created'.format(ex_msg) ) try: self._create_colls(investigation) except Exception as ex: raise APIException('{}{}'.format(ex_msg, ex)) return Response( { 'detail': 'iRODS collections created', 'path': irods_backend.get_sample_path(investigation.project), }, status=status.HTTP_200_OK, )
[docs]class SheetISAExportAPIView( SheetISAExportMixin, SODARAPIBaseProjectMixin, APIView ): """ Export sample sheets as ISA-Tab TSV files, either packed in a zip archive or wrapped in a JSON structure. **URL for zip export:** ``/samplesheets/api/export/zip/{Project.sodar_uuid}`` **URL for JSON export:** ``/samplesheets/api/export/json/{Project.sodar_uuid}`` **Methods:** ``GET`` """ http_method_names = ['get'] permission_required = 'samplesheets.export_sheet' def get(self, request, *args, **kwargs): project = self.get_project() investigation = Investigation.objects.filter( project=project, active=True ).first() if not investigation: raise NotFound() export_format = 'json' if self.request.get_full_path() == reverse( 'samplesheets:api_export_zip', kwargs={'project': project.sodar_uuid}, ): export_format = 'zip' try: return self.get_isa_export(project, request, export_format) except Exception as ex: raise APIException('Unable to export ISA-Tab: {}'.format(ex))
[docs]class SheetImportAPIView(SheetImportMixin, SODARAPIBaseProjectMixin, APIView): """ Upload sample sheet as separate ISA-Tab TSV files or a zip archive. Will replace existing sheets if valid. The request should be in format of ``multipart/form-data``. Content type for each file must be provided. **URL:** ``/samplesheets/api/import/{Project.sodar_uuid}`` **Methods:** ``POST`` **Return:** - ``detail``: Detail of project success (string) - ``sodar_warnings``: SODAR import issue warnings (list of srings, optional) """ http_method_names = ['post'] permission_required = 'samplesheets.edit_sheet' def post(self, request, *args, **kwargs): """Handle POST request for submitting""" project = self.get_project() if app_settings.get_app_setting(APP_NAME, 'sheet_sync_enable', project): raise ValidationError( 'Sheet synchronization enabled in project: import not allowed' ) sheet_io = SampleSheetIO() old_inv = Investigation.objects.filter( project=project, active=True ).first() zip_file = None if len(request.FILES) == 0: raise ParseError('No files provided') # Zip file handling if len(request.FILES) == 1: file = request.FILES[next(iter(request.FILES))] try: zip_file = sheet_io.get_zip_file(file) except OSError as ex: raise ParseError('Failed to parse zip archive: {}'.format(ex)) isa_data = sheet_io.get_isa_from_zip(zip_file) # Multi-file handling else: try: isa_data = sheet_io.get_isa_from_files(request.FILES.values()) except Exception as ex: raise ParseError('Failed to parse TSV files: {}'.format(ex)) # Handle import action = 'replace' if old_inv else 'create' tl_event = self.create_timeline_event(project=project, action=action) try: investigation = sheet_io.import_isa( isa_data=isa_data, project=project, archive_name=zip_file.filename if zip_file else None, user=request.user, replace=True if old_inv else False, replace_uuid=old_inv.sodar_uuid if old_inv else None, ) except Exception as ex: self.handle_import_exception(ex, tl_event, ui_mode=False) raise APIException(str(ex)) # Handle replace if old_inv: try: investigation = self.handle_replace( investigation=investigation, old_inv=old_inv, tl_event=tl_event, ) ex_msg = None except Exception as ex: ex_msg = str(ex) if ex_msg or not investigation: raise ParseError( 'Sample sheet replacing failed: {}'.format( ex_msg if ex_msg else 'See SODAR error log' ) ) if tl_event: tl_event.add_object( obj=investigation, label='investigation', name=investigation.title, ) # Finalize import isa_version = ( ISATab.objects.filter(investigation_uuid=investigation.sodar_uuid) .order_by('-date_created') .first() ) self.finalize_import( investigation=investigation, action=action, tl_event=tl_event, isa_version=isa_version, ) ret_data = { 'detail': 'Sample sheets {}d for project "{}" ({})'.format( action, project.title, project.sodar_uuid ) } no_plugin_assays = self.get_assays_without_plugins(investigation) if no_plugin_assays: ret_data['sodar_warnings'] = [ self.get_assay_plugin_warning(a) for a in no_plugin_assays ] return Response(ret_data, status=status.HTTP_200_OK)
[docs]class SampleDataFileExistsAPIView(SODARAPIBaseMixin, APIView): """ Return status of data object existing in SODAR iRODS by MD5 checksum. Includes all projects in search regardless of user permissions. **URL:** ``/samplesheets/api/file/exists`` **Methods:** ``GET`` **Parameters:** - ``checksum``: MD5 checksum (string) **Returns:** - ``detail``: String - ``status``: Boolean """ http_method_names = ['get'] permission_classes = (IsAuthenticated,) def get(self, request, *args, **kwargs): if not settings.ENABLE_IRODS: raise APIException('iRODS not enabled') irods_backend = get_backend_api('omics_irods') if not irods_backend: raise APIException('iRODS backend not enabled') c = request.query_params.get('checksum') if not c or not re.match(MD5_RE, c): raise ParseError('Invalid MD5 checksum: "{}"'.format(c)) ret = {'detail': 'File does not exist', 'status': False} sql = ( 'SELECT DISTINCT ON (data_id) data_name ' 'FROM r_data_main JOIN r_coll_main USING (coll_id) ' 'WHERE (coll_name LIKE \'%/{coll}\' ' 'OR coll_name LIKE \'%/{coll}/%\') ' 'AND r_data_main.data_checksum = \'{sum}\''.format( coll=settings.IRODS_SAMPLE_COLL, sum=c ) ) # print('QUERY: {}'.format(sql)) # DEBUG columns = [DataObject.name] query = irods_backend.get_query(sql, columns) try: results = query.get_results() if sum(1 for _ in results) > 0: ret['detail'] = 'File exists' ret['status'] = True except CAT_NO_ROWS_FOUND: pass # No results, this is OK except Exception as ex: logger.error( '{} iRODS query exception: {}'.format( self.__class__.__name__, ex ) ) raise APIException( 'iRODS query exception, please contact an admin if issue ' 'persists' ) finally: query.remove() return Response(ret, status=status.HTTP_200_OK)
# TODO: Temporary HACK, should be replaced by proper API view class RemoteSheetGetAPIView(APIView): """Temporary API view for retrieving the sample sheet as JSON by a target site, either as rendered tables or the original ISA-Tab""" permission_classes = (AllowAny,) # We check the secret in get()/post() def get(self, request, **kwargs): secret = kwargs['secret'] isa = request.GET.get('isa') try: target_site = RemoteSite.objects.get( mode=SITE_MODE_TARGET, secret=secret ) except RemoteSite.DoesNotExist: return Response('Remote site not found, unauthorized', status=401) target_project = target_site.projects.filter( project_uuid=kwargs['project'] ).first() if ( not target_project or target_project.level != REMOTE_LEVEL_READ_ROLES ): return Response( 'No project access for remote site, unauthorized', status=401 ) try: investigation = Investigation.objects.get( project=target_project.get_project(), active=True ) except Investigation.DoesNotExist: return Response( 'No ISA investigation found for project', status=404 ) # All OK so far, return data # Rendered tables if not isa or int(isa) != 1: ret = {'studies': {}} tb = SampleSheetTableBuilder() # Build study tables for study in investigation.studies.all(): try: tables = tb.build_study_tables(study, ui=False) except Exception as ex: return Response(str(ex), status=500) ret['studies'][str(study.sodar_uuid)] = tables # Original ISA-Tab else: sheet_io = SampleSheetIO() try: ret = sheet_io.export_isa(investigation) except Exception as ex: return Response(str(ex), status=500) return Response(ret, status=200)