From eb0753bc1ec11d18618f25d023b5dfad751b9e06 Mon Sep 17 00:00:00 2001 From: Mikko Nieminen Date: Tue, 10 Sep 2024 12:20:06 +0200 Subject: [PATCH] update samplesheets api versioning (#1936) --- docs_manual/source/api_samplesheets.rst | 24 ++-- samplesheets/tests/test_views_api.py | 7 ++ samplesheets/tests/test_views_api_taskflow.py | 28 +++-- samplesheets/views_api.py | 105 +++++++++++++++--- 4 files changed, 126 insertions(+), 38 deletions(-) diff --git a/docs_manual/source/api_samplesheets.rst b/docs_manual/source/api_samplesheets.rst index b97d8cbf..d2df63b7 100644 --- a/docs_manual/source/api_samplesheets.rst +++ b/docs_manual/source/api_samplesheets.rst @@ -6,6 +6,19 @@ Sample Sheets API The REST API for sample sheet operations is described in this document. +Versioning +========== + +Media Type + ``application/vnd.bihealth.sodar.samplesheets+json`` +Current Version + ``1.0`` +Accepted Versions + ``1.0`` +Header Example + ``Accept: application/vnd.bihealth.sodar.samplesheets+json; version=x.y`` + + API Views ========= @@ -58,14 +71,3 @@ iRODS Data Requests .. autoclass:: IrodsDataRequestAcceptAPIView .. autoclass:: IrodsDataRequestRejectAPIView - - -Versioning -========== - -For accept header versioning, the following header is expected in the current -SODAR version: - -.. code-block:: console - - Accept: application/vnd.bihealth.sodar+json; version=0.15.0 diff --git a/samplesheets/tests/test_views_api.py b/samplesheets/tests/test_views_api.py index 0fd345db..4ebf26d3 100644 --- a/samplesheets/tests/test_views_api.py +++ b/samplesheets/tests/test_views_api.py @@ -67,6 +67,10 @@ IrodsAccessTicketViewTestMixin, ) from samplesheets.views import SheetImportMixin +from samplesheets.views_api import ( + SAMPLESHEETS_API_MEDIA_TYPE, + SAMPLESHEETS_API_DEFAULT_VERSION, +) app_settings = AppSettingAPI() @@ -96,6 +100,9 @@ class SampleSheetAPIViewTestBase(SampleSheetIOMixin, APIViewTestBase): """Base view for samplesheets API views tests""" + media_type = SAMPLESHEETS_API_MEDIA_TYPE + api_version = SAMPLESHEETS_API_DEFAULT_VERSION + class IrodsAccessTicketAPITestBase( IrodsAccessTicketMixin, diff --git a/samplesheets/tests/test_views_api_taskflow.py b/samplesheets/tests/test_views_api_taskflow.py index 38e17b9d..841331aa 100644 --- a/samplesheets/tests/test_views_api_taskflow.py +++ b/samplesheets/tests/test_views_api_taskflow.py @@ -36,15 +36,6 @@ IRODS_REQUEST_STATUS_REJECTED, IRODS_REQUEST_ACTION_DELETE, ) -from samplesheets.views import ( - IRODS_REQUEST_EVENT_CREATE as CREATE_ALERT, - IRODS_REQUEST_EVENT_ACCEPT as ACCEPT_ALERT, - IRODS_REQUEST_EVENT_REJECT as REJECT_ALERT, -) -from samplesheets.views_api import ( - IRODS_QUERY_ERROR_MSG, -) - from samplesheets.tests.test_io import SampleSheetIOMixin, SHEET_DIR from samplesheets.tests.test_models import ( IrodsAccessTicketMixin, @@ -60,6 +51,16 @@ TICKET_STR, TICKET_LABEL, ) +from samplesheets.views import ( + IRODS_REQUEST_EVENT_CREATE as CREATE_ALERT, + IRODS_REQUEST_EVENT_ACCEPT as ACCEPT_ALERT, + IRODS_REQUEST_EVENT_REJECT as REJECT_ALERT, +) +from samplesheets.views_api import ( + IRODS_QUERY_ERROR_MSG, + SAMPLESHEETS_API_MEDIA_TYPE, + SAMPLESHEETS_API_DEFAULT_VERSION, +) # SODAR constants PROJECT_TYPE_PROJECT = SODAR_CONSTANTS['PROJECT_TYPE_PROJECT'] @@ -84,6 +85,9 @@ class SampleSheetAPITaskflowTestBase( ): """Base samplesheets API view test class with Taskflow enabled""" + media_type = SAMPLESHEETS_API_MEDIA_TYPE + api_version = SAMPLESHEETS_API_DEFAULT_VERSION + def setUp(self): super().setUp() # Make project with owner in Taskflow and Django @@ -109,6 +113,9 @@ class IrodsAccessTicketAPIViewTestBase( ): """Base samplesheets API view test class for iRODS access ticket requests""" + media_type = SAMPLESHEETS_API_MEDIA_TYPE + api_version = SAMPLESHEETS_API_DEFAULT_VERSION + def assert_alert_count(self, alert_name, user, count, project=None): """ Assert expected app alert count. If project is not specified, default to @@ -171,6 +178,9 @@ class TestIrodsDataRequestAPIViewBase( ): """Base samplesheets API view test class for iRODS delete requests""" + media_type = SAMPLESHEETS_API_MEDIA_TYPE + api_version = SAMPLESHEETS_API_DEFAULT_VERSION + # TODO: Retrieve this from a common base/helper class instead of redef def assert_alert_count(self, alert_name, user, count, project=None): """ diff --git a/samplesheets/views_api.py b/samplesheets/views_api.py index 7b50f8ef..58767857 100644 --- a/samplesheets/views_api.py +++ b/samplesheets/views_api.py @@ -25,7 +25,9 @@ UpdateAPIView, ) from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.renderers import JSONRenderer from rest_framework.response import Response +from rest_framework.versioning import AcceptHeaderVersioning from rest_framework.views import APIView # Projectroles dependency @@ -70,6 +72,10 @@ table_builder = SampleSheetTableBuilder() +# Local constants +SAMPLESHEETS_API_MEDIA_TYPE = 'application/vnd.bihealth.sodar.samplesheets+json' +SAMPLESHEETS_API_ALLOWED_VERSIONS = ['1.0'] +SAMPLESHEETS_API_DEFAULT_VERSION = '1.0' MD5_RE = re.compile(r'([a-fA-F\d]{32})') APP_NAME = 'samplesheets' IRODS_QUERY_ERROR_MSG = 'Exception querying iRODS objects' @@ -78,11 +84,31 @@ IRODS_TICKET_NO_UPDATE_FIELDS_MSG = 'No fields to update' +# Base Classes and Mixins ------------------------------------------------------ + + +class SamplesheetsAPIVersioningMixin: + """ + Samplesheets API view versioning mixin for overriding media type and + accepted versions. + """ + + class FilesfoldersAPIRenderer(JSONRenderer): + media_type = SAMPLESHEETS_API_MEDIA_TYPE + + class FilesfoldersAPIVersioning(AcceptHeaderVersioning): + allowed_versions = SAMPLESHEETS_API_ALLOWED_VERSIONS + default_version = SAMPLESHEETS_API_DEFAULT_VERSION + + renderer_classes = [FilesfoldersAPIRenderer] + versioning_class = FilesfoldersAPIVersioning + + # API Views -------------------------------------------------------------------- class InvestigationRetrieveAPIView( - SODARAPIGenericProjectMixin, RetrieveAPIView + SamplesheetsAPIVersioningMixin, SODARAPIGenericProjectMixin, RetrieveAPIView ): """ Retrieve metadata of an investigation with its studies and assays. @@ -116,7 +142,10 @@ class InvestigationRetrieveAPIView( class IrodsCollsCreateAPIView( - IrodsCollsCreateViewMixin, SODARAPIBaseProjectMixin, APIView + IrodsCollsCreateViewMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIBaseProjectMixin, + APIView, ): """ Create iRODS collections for a project. @@ -162,7 +191,10 @@ def post(self, request, *args, **kwargs): class SheetISAExportAPIView( - SheetISAExportMixin, SODARAPIBaseProjectMixin, APIView + SheetISAExportMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIBaseProjectMixin, + APIView, ): """ Export sample sheets as ISA-Tab TSV files, either packed in a zip archive or @@ -197,7 +229,12 @@ def get(self, request, *args, **kwargs): raise APIException('Unable to export ISA-Tab: {}'.format(ex)) -class SheetImportAPIView(SheetImportMixin, SODARAPIBaseProjectMixin, APIView): +class SheetImportAPIView( + SheetImportMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIBaseProjectMixin, + APIView, +): """ Upload sample sheet as separate ISA-Tab TSV files or a zip archive. Will replace existing sheets if valid. @@ -315,7 +352,7 @@ def post(self, request, *args, **kwargs): class IrodsAccessTicketRetrieveAPIView( - SODARAPIGenericProjectMixin, RetrieveAPIView + SamplesheetsAPIVersioningMixin, SODARAPIGenericProjectMixin, RetrieveAPIView ): """ Retrieve an iRODS access ticket for a project. @@ -345,7 +382,9 @@ class IrodsAccessTicketRetrieveAPIView( queryset_project_field = 'study__investigation__project' -class IrodsAccessTicketListAPIView(SODARAPIBaseProjectMixin, ListAPIView): +class IrodsAccessTicketListAPIView( + SamplesheetsAPIVersioningMixin, SODARAPIBaseProjectMixin, ListAPIView +): """ List iRODS access tickets for a project. @@ -376,7 +415,10 @@ def get_queryset(self): class IrodsAccessTicketCreateAPIView( - IrodsAccessTicketModifyMixin, SODARAPIGenericProjectMixin, CreateAPIView + IrodsAccessTicketModifyMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIGenericProjectMixin, + CreateAPIView, ): """ Create an iRODS access ticket for a project. @@ -429,7 +471,10 @@ def perform_create(self, serializer): class IrodsAccessTicketUpdateAPIView( - IrodsAccessTicketModifyMixin, SODARAPIGenericProjectMixin, UpdateAPIView + IrodsAccessTicketModifyMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIGenericProjectMixin, + UpdateAPIView, ): """ Update an iRODS access ticket for a project. @@ -463,7 +508,10 @@ def perform_update(self, serializer): class IrodsAccessTicketDestroyAPIView( - IrodsAccessTicketModifyMixin, SODARAPIGenericProjectMixin, DestroyAPIView + IrodsAccessTicketModifyMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIGenericProjectMixin, + DestroyAPIView, ): """ Delete an iRODS access ticket. @@ -497,7 +545,7 @@ def perform_destroy(self, instance): class IrodsDataRequestRetrieveAPIView( - SODARAPIGenericProjectMixin, RetrieveAPIView + SamplesheetsAPIVersioningMixin, SODARAPIGenericProjectMixin, RetrieveAPIView ): """ Retrieve a iRODS data request. @@ -526,7 +574,9 @@ class IrodsDataRequestRetrieveAPIView( serializer_class = IrodsDataRequestSerializer -class IrodsDataRequestListAPIView(SODARAPIBaseProjectMixin, ListAPIView): +class IrodsDataRequestListAPIView( + SamplesheetsAPIVersioningMixin, SODARAPIBaseProjectMixin, ListAPIView +): """ List the iRODS data requests for a project. @@ -561,7 +611,10 @@ def get_queryset(self): class IrodsDataRequestCreateAPIView( - IrodsDataRequestModifyMixin, SODARAPIGenericProjectMixin, CreateAPIView + IrodsDataRequestModifyMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIGenericProjectMixin, + CreateAPIView, ): """ Create an iRODS delete request for a project. @@ -592,7 +645,10 @@ def perform_create(self, serializer): class IrodsDataRequestUpdateAPIView( - IrodsDataRequestModifyMixin, SODARAPIGenericProjectMixin, UpdateAPIView + IrodsDataRequestModifyMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIGenericProjectMixin, + UpdateAPIView, ): """ Update an iRODS data request for a project. @@ -622,7 +678,10 @@ def perform_update(self, serializer): class IrodsDataRequestDestroyAPIView( - IrodsDataRequestModifyMixin, SODARAPIGenericProjectMixin, DestroyAPIView + IrodsDataRequestModifyMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIGenericProjectMixin, + DestroyAPIView, ): """ Delete an iRODS data request object. @@ -651,7 +710,10 @@ def perform_destroy(self, instance): class IrodsDataRequestAcceptAPIView( - IrodsDataRequestModifyMixin, SODARAPIBaseProjectMixin, APIView + IrodsDataRequestModifyMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIBaseProjectMixin, + APIView, ): """ Accept an iRODS data request for a project. @@ -696,7 +758,10 @@ def post(self, request, *args, **kwargs): class IrodsDataRequestRejectAPIView( - IrodsDataRequestModifyMixin, SODARAPIBaseProjectMixin, APIView + IrodsDataRequestModifyMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIBaseProjectMixin, + APIView, ): """ Reject an iRODS data request for a project. @@ -738,7 +803,9 @@ def post(self, request, *args, **kwargs): ) -class SampleDataFileExistsAPIView(SODARAPIBaseMixin, APIView): +class SampleDataFileExistsAPIView( + SamplesheetsAPIVersioningMixin, SODARAPIBaseMixin, APIView +): """ Return status of data object existing in SODAR iRODS by MD5 checksum. Includes all projects in search regardless of user permissions. @@ -812,7 +879,9 @@ def get(self, request, *args, **kwargs): return Response(ret, status=status.HTTP_200_OK) -class ProjectIrodsFileListAPIView(SODARAPIBaseProjectMixin, APIView): +class ProjectIrodsFileListAPIView( + SamplesheetsAPIVersioningMixin, SODARAPIBaseProjectMixin, APIView +): """ Return a list of files in the project sample data repository.