pathology/dicom_proxy/dicom_proxy_blueprint.py (1,192 lines of code) (raw):

# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== """Flask blueprint for DICOM Store DICOM Proxy.""" import dataclasses import http import os import re from typing import List, Mapping, Optional import flask import flask_compress import requests_toolbelt from pathology.dicom_proxy import annotations_util from pathology.dicom_proxy import base_dicom_request_error from pathology.dicom_proxy import bulkdata_util from pathology.dicom_proxy import dicom_instance_request from pathology.dicom_proxy import dicom_proxy_flags from pathology.dicom_proxy import dicom_store_util from pathology.dicom_proxy import dicom_url_util from pathology.dicom_proxy import downsample_util from pathology.dicom_proxy import enum_types from pathology.dicom_proxy import execution_timer from pathology.dicom_proxy import flask_util from pathology.dicom_proxy import frame_caching_util from pathology.dicom_proxy import iccprofile_bulk_metadata_util from pathology.dicom_proxy import logging_util from pathology.dicom_proxy import metadata_augmentation from pathology.dicom_proxy import metadata_util from pathology.dicom_proxy import parameters_exceptions_and_return_types from pathology.dicom_proxy import proxy_const from pathology.dicom_proxy import render_frame_params from pathology.dicom_proxy import sparse_dicom_util from pathology.dicom_proxy import user_auth_util from pathology.shared_libs.build_version import build_version from pathology.shared_libs.flags import flag_utils from pathology.shared_libs.iap_auth_lib import auth from pathology.shared_libs.logging_lib import cloud_logging_client # Types _Compression = enum_types.Compression _DicomInstanceRequest = dicom_instance_request.DicomInstanceRequest _DicomInstanceWebRequest = ( parameters_exceptions_and_return_types.DicomInstanceWebRequest ) _DownsamplingFrameRequestError = ( parameters_exceptions_and_return_types.DownsamplingFrameRequestError ) _ICCProfile = enum_types.ICCProfile _Interpolation = enum_types.Interpolation _RenderFrameParams = render_frame_params.RenderFrameParams # Constants _DIMENSION_ORG_ERR = ( 'The DICOM proxy does not support returning frames from ' 'DICOM instances that have DimensionalOrganizationType ' 'tag (0020,9311) != TILED_FULL' ) _INSTANCE_IS_NOT_FULLY_TILED = 'Instance is not fully tiled.' # Threshold for initation of preemptive DICOM instance caching. # percentage of requested frames which were not in cache and had to be feteched # from dicom store. Calculated as total frames retrieved from store / total # frames requested. If this is greater than or equal to this threshold then # frame caching is initated. This threshold only has real value if frames are # requested in batches. Requests which get a single frame, will have # proportions which are either 0, the frame was was returned from cache or 1 # frame was fetched from store. _PREEMPTIVE_DICOM_INSTANCE_CACHING_THRESHOLD = 0.5 # Global dicom_proxy = flask.Blueprint( 'dicom_proxy', __name__, url_prefix=dicom_proxy_flags.PROXY_SERVER_URL_PATH_PREFIX if dicom_proxy_flags.PROXY_SERVER_URL_PATH_PREFIX else None, ) compress = flask_compress.Compress() _VALID_FRAME_CHARS = re.compile('[^0-9, ]') def _parse_interpolation(args: Mapping[str, str]) -> _Interpolation: """Parses interpolation algorithm from request args. Args: args: HTTP request args. Returns: _Interpolation Raises: ValueError: Unsupported interpolation algorithm """ arg = flask_util.get_parameter_value( args, enum_types.TileServerHttpParams.INTERPOLATION ) if arg == 'area': return _Interpolation.AREA # Recommended if arg == 'cubic': return _Interpolation.CUBIC if arg == 'lanczos4': return _Interpolation.LANCZOS4 if arg == 'linear': return _Interpolation.LINEAR if arg == 'nearest': return _Interpolation.NEAREST raise ValueError('Unsupported interpolation algorithm') def _parse_frame_list(frames: str) -> List[int]: """Parses frame string into list of indexs. Args: frames: String containing comma separated values. Returns: List of frame indexes. Raises: ValueError: list contains non-int values or invalid chars. IndexError: list contains invalid indexes. """ if _VALID_FRAME_CHARS.search(frames) is not None: raise ValueError(f'Frame string contains invalid chars Frames:{frames}') frame_indexes = [int(arg) for arg in frames.split(',')] if not frame_indexes: raise IndexError('No frame indexes.') if min(frame_indexes) <= 0: raise IndexError('Frame index <= 0 are invalid') return frame_indexes def _parse_compression_quality(args: Mapping[str, str]) -> int: """Returns request image compression quality. Args: args: HTTP Request Args. Raise: ValueError: Invalid quality value or cannot parse quality to int. """ quality = int( flask_util.get_parameter_value( args, enum_types.TileServerHttpParams.QUALITY ) ) if quality < 1 or quality > 100: raise ValueError('Invalid quality value') return quality def _parse_iccprofile(args: Mapping[str, str]) -> _ICCProfile: """Returns request image iccprofile color correction. https://dicom.nema.org/medical/dicom/2019a/output/chtml/part18/sect_6.5.8.html Args: args: HTTP Request Args. Raises: ValueError: request is not supported. """ if dicom_proxy_flags.DISABLE_ICC_PROFILE_CORRECTION_FLG.value: return _ICCProfile(proxy_const.ICCProfile.NO) return _ICCProfile( flask_util.get_parameter_value( args, enum_types.TileServerHttpParams.ICCPROFILE ).upper() ) def _get_request_compression( accept_header: Optional[str], instance_compression: _Compression ) -> _Compression: """Returns the image compression format specified in the request. Args: accept_header: Request http accept header value. instance_compression: Compression format of the DICOM instance. Returns: Requested compression format. Raises: ValueError: requested ccompression format is not supported. """ if accept_header is None: return _Compression.JPEG accept_header = accept_header.strip().lower() if not accept_header or 'image/jpeg' in accept_header: return _Compression.JPEG if 'image/png' in accept_header: return _Compression.PNG if 'image/webp' in accept_header: return _Compression.WEBP if 'image/gif' in accept_header: return _Compression.GIF if '*/*' in accept_header: return _Compression.JPEG if 'image/jxl' in accept_header: if instance_compression == _Compression.JPEG: return _Compression.JPEG_TRANSCODED_TO_JPEGXL else: return _Compression.JPEGXL raise ValueError('Unsupported compression format.') def _parse_embed_icc_profile(args: Mapping[str, str]) -> bool: """Returns False if icc_profile should not be embedded in returned images. Args: args: HTTP Request Args. Raises: Value error if parameter cannot be converted to bool. """ result = flag_utils.str_to_bool( flask_util.get_parameter_value( args, enum_types.TileServerHttpParams.EMBED_ICCPROFILE ) ) return result def _parse_request_params( args: Mapping[str, str], header: Mapping[str, str], instance_compression: _Compression, ) -> _RenderFrameParams: """Returns RenderedFrameParams initialized from HTTP request arguments. Args: args: Argument key/value mapping passed to url. Returns initialized RenderedFrameParams class. header: Passed to HTTP request. instance_compression: Compression format of the DICOM instance. Raises: ValueError: Error in parameter definition. """ downsample = flask_util.parse_downsample(args) # https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_8.3.5.html#sect_8.3.5.1.2 jpeg_quality = _parse_compression_quality(args) interpolation = _parse_interpolation(args) # https://dicom.nema.org/medical/dicom/2019a/output/chtml/part18/sect_6.5.8.html#sect_6.5.8.1.2.5 iccprofile = _parse_iccprofile(args) enable_caching = flask_util.parse_cache_enabled(args) embed_iccprofile = _parse_embed_icc_profile(args) result = _RenderFrameParams( downsample, interpolation, _get_request_compression( header.get(flask_util.ACCEPT_HEADER_KEY), instance_compression ), jpeg_quality, iccprofile, enable_caching, embed_iccprofile, ) return result @execution_timer.log_execution_time('_create_dicom_instance_web_request') def _create_dicom_instance_web_request( dicom_web_base_url: dicom_url_util.DicomWebBaseURL, study: str, series: str, instance: str, ) -> _DicomInstanceWebRequest: """Creates a DICOM instance web request from parsed url parameters. Args: dicom_web_base_url: Base DICOMweb URL for store. study: DICOM StudyInstanceUID. series: DICOM SeriesInstanceUID. instance: DICOM SOPInstanceUID. Returns: DicomInstanceWebRequest """ fl_request_args = flask_util.get_dicom_proxy_request_args() http_header = flask_util.norm_dict_keys( flask_util.get_headers(), [flask_util.ACCEPT_HEADER_KEY] ) return _DicomInstanceWebRequest( user_auth_util.AuthSession(flask_util.get_headers()), dicom_web_base_url, dicom_url_util.StudyInstanceUID(study), dicom_url_util.SeriesInstanceUID(series), dicom_url_util.SOPInstanceUID(instance), flask_util.parse_cache_enabled(fl_request_args), fl_request_args, http_header, ) def _get_rendered_frames( instance_request: _DicomInstanceRequest, frames: str, rendered_request: bool ) -> flask.Response: """Returns downsampled and/or transcoded frames from WSI DICOM instance. Args: instance_request: DicomInstanceRequest frames: String encoding list of frames to render. rendered_request: Is this a DICOM rendered frames request or frames request. Returns: Requested DICOM onstance frame images (flask.Response) """ try: dicom_frame_indexes = _parse_frame_list(frames) except (ValueError, IndexError) as exp: cloud_logging_client.error('Exception parsing frame list.', exp) return flask_util.exception_flask_response(exp) try: params = _parse_request_params( instance_request.url_args, instance_request.url_header, instance_request.metadata.image_compression, ) except ValueError as exp: cloud_logging_client.error('Exception parsing request params.', exp) return flask_util.exception_flask_response(exp) if ( not instance_request.metadata.is_tiled_full and instance_request.metadata.number_of_frames > 1 and params.downsample > 1.0 ): cloud_logging_client.error( _INSTANCE_IS_NOT_FULLY_TILED, dataclasses.asdict(instance_request.metadata), ) return flask_util.exception_flask_response(_DIMENSION_ORG_ERR) try: result = downsample_util.get_rendered_dicom_frames( instance_request, params, dicom_frame_indexes ) except _DownsamplingFrameRequestError as exp: cloud_logging_client.error('Error occurred getting rendered frame.', exp) return flask_util.exception_flask_response(exp) except base_dicom_request_error.BaseDicomRequestError as exp: cloud_logging_client.error('Error occurred getting rendered frame.', exp) return exp.flask_response() request_metric_dict = result.metrics.str_dict() execution_timer.log_message_to_execution_time_stack( f'Request metrics: {request_metric_dict}' ) frame_requests_from_store = float( result.metrics.number_of_frames_downloaded_from_store ) total_frame_requests = float(result.metrics.frame_requests) if ( frame_requests_from_store / total_frame_requests ) >= _PREEMPTIVE_DICOM_INSTANCE_CACHING_THRESHOLD: frame_caching_util.cache_instance_frames_in_background( instance_request, dicom_frame_indexes ) else: frame_caching_util.cleanup_preemptive_cache_loading_set() if rendered_request and len(result.images) == 1: return flask.Response( response=result.images[0], headers={'Content-Type': result.content_type}, direct_passthrough=True, ) fields = [] for index, frame_image_bytes in enumerate(result.images): frame_index = str(dicom_frame_indexes[index]) frame_content_type = result.multipart_content_type fields.append((frame_index, (None, frame_image_bytes, frame_content_type))) multipart_data = requests_toolbelt.MultipartEncoder(fields=fields) return flask.Response( response=multipart_data.read(), headers={ 'Content-Type': ( f'multipart/related; boundary={multipart_data.boundary_value}' ) }, direct_passthrough=True, ) def _missing_series_uid_redirect( url_base: dicom_url_util.DicomWebBaseURL, study_instance_uid: str, sop_instance_uid: str, prefix: str, ) -> flask.Response: """Redirects requests with study and sop instance uid and missing series uid. HTTP redirects were added to enable the proxy to correct non-standard DICOM rendered frame and rendered instance requests that are missing a series instance uid but have valid study instance uid and sop instance uid. Madcap generates malformed requests when a imaging is requested by study instance UID alone. The code here improves the behavior by enabling the the proxy to correctly handle these requests. Args: url_base: DicomWebBaseURL. study_instance_uid: DICOM instance study instance UID. sop_instance_uid: DICOM instance SOP instance UID. prefix: Prefix to append at end of redirect. Returns: Series Instance UID, and if series instance UID was discoverd then the DICOM web path to the instance, if the series instance uid Raises: metadata_util.GetSeriesInstanceUIDError: An error occured while searching for the Series Instance UID. """ headers = user_auth_util.AuthSession(flask_util.get_headers()) study_instance_uid = dicom_url_util.StudyInstanceUID(study_instance_uid) sop_instance_uid = dicom_url_util.SOPInstanceUID(sop_instance_uid) try: s_uid = metadata_util.get_series_instance_uid_for_study_and_instance_uid( headers, url_base, study_instance_uid, sop_instance_uid ) except metadata_util.GetSeriesInstanceUIDError as exp: cloud_logging_client.debug( 'Could not determine series instance uid; proxying request.', exp, ) return dicom_store_util.dicom_store_proxy() url = bulkdata_util.get_bulk_data_base_url(url_base) params = flask_util.get_parameters() return flask.redirect( f'{url}/{study_instance_uid}/series/{s_uid}/{sop_instance_uid}' f'{prefix}{params}', http.HTTPStatus.MOVED_PERMANENTLY, ) def _get_frames_instance_generic( dicom_web_base_url: dicom_url_util.DicomWebBaseURL, study_instance_uid: str, series_instance_uid: str, sop_instance_uid: str, framelist: str, is_rendered_request: bool, ) -> flask.Response: """Returns rendered (downsampled) frames from DICOM instance. Args: dicom_web_base_url: Base DICOMweb URL for store. study_instance_uid: DICOM study instance UID. series_instance_uid: DICOM series instance UID. sop_instance_uid: DICOM sop instance UID. framelist: Comma separated value string listing of frames to return. is_rendered_request: Is DICOMweb rendered request or frames request. Returns: flask.Response returning rendered frame request. """ instance_request = _create_dicom_instance_web_request( dicom_web_base_url, study_instance_uid, series_instance_uid, sop_instance_uid, ) try: if not instance_request.metadata.is_wsi_iod: cloud_logging_client.debug( 'Proxying rendered instance request instance is not VL Whole Slide' ' Microscopy Image SOPClassUID.', dataclasses.asdict(instance_request.metadata), ) return dicom_store_util.dicom_store_proxy() rendered_frame = _get_rendered_frames( instance_request, framelist, is_rendered_request ) # Test if single frame is being requested, if yes tell client to cache # response. if ',' in framelist: rendered_frame.headers['Cache-Control'] = ( f'max-age={dicom_proxy_flags.CACHE_CONTROL_TTL_RENDERED_FRAMES_FLG.value}' ) return rendered_frame except metadata_util.ReadDicomMetadataError as exp: cloud_logging_client.error('Reading DICOM metadata.', exp) return flask_util.exception_flask_response(exp) def _get_masked_flask_headers() -> Mapping[str, str]: return logging_util.mask_privileged_header_values(flask_util.get_headers()) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_INSTANCE_URL_DEFAULT_VERSION}/frames/<string:framelist>/rendered', methods=flask_util.GET_AND_POST_METHODS, endpoint='_get_frame_instance', defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION, 'is_rendered_request': True, }, strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_INSTANCE_URL_DEFAULT_VERSION}/frames/<string:framelist>', methods=flask_util.GET_AND_POST_METHODS, endpoint='_get_frame_instance', defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION, 'is_rendered_request': False, }, strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_INSTANCE_URL}/frames/<string:framelist>/rendered', methods=flask_util.GET_AND_POST_METHODS, endpoint='_get_frame_instance', defaults={'is_rendered_request': True}, strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_INSTANCE_URL}/frames/<string:framelist>', methods=flask_util.GET_AND_POST_METHODS, endpoint='_get_frame_instance', defaults={'is_rendered_request': False}, strict_slashes=False, ) @logging_util.log_exceptions @execution_timer.log_execution_time('_get_frame_instance') @auth.validate_iap @metadata_util.ClearLocalMetadata() def _get_frame_instance( store_api_version: str, projectid: str, location: str, datasetid: str, dicomstore: str, study_instance_uid: str, series_instance_uid: str, sop_instance_uid: str, framelist: str, is_rendered_request: bool, ) -> flask.Response: """Flask entry point for DICOMweb frame request that returns frame instances. Args: store_api_version: Healthcare API DICOM store version. projectid: GCP projectid. location: Location of healthcare api dataset. datasetid: Healthcare API dataset ID. dicomstore: DICOM store to connect to. study_instance_uid: DICOM study instance UID. series_instance_uid: DICOM series instance UID. sop_instance_uid: DICOM sop instance UID. framelist: Comma separated value string listing of frames to return. is_rendered_request: True if rendered frame request. Returns: Requested DICOM onstance frame images (flask.Response) """ return _get_frames_instance_generic( dicom_url_util.DicomWebBaseURL( store_api_version, projectid, location, datasetid, dicomstore ), study_instance_uid, series_instance_uid, sop_instance_uid, framelist, is_rendered_request, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_STUDY_URL_DEFAULT_VERSION}/instances/<string:sop_instance_uid>/frames/<string:framelist>/rendered', methods=flask_util.GET_AND_POST_METHODS, endpoint='_correct_frame_instance_missing_series_query', defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION, 'is_rendered_request': True, }, strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_STUDY_URL_DEFAULT_VERSION}/instances/<string:sop_instance_uid>/frames/<string:framelist>', methods=flask_util.GET_AND_POST_METHODS, endpoint='_correct_frame_instance_missing_series_query', defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION, 'is_rendered_request': False, }, strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_STUDY_URL}/instances/<string:sop_instance_uid>/frames/<string:framelist>/rendered', methods=flask_util.GET_AND_POST_METHODS, endpoint='_redirect_frame_instance_missing_series_query', defaults={'is_rendered_request': True}, strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_STUDY_URL}/instances/<string:sop_instance_uid>/frames/<string:framelist>', methods=flask_util.GET_AND_POST_METHODS, endpoint='_redirect_frame_instance_missing_series_query', defaults={'is_rendered_request': False}, strict_slashes=False, ) @logging_util.log_exceptions @execution_timer.log_execution_time( '_redirect_frame_instance_missing_series_query' ) @auth.validate_iap @metadata_util.ClearLocalMetadata() def _redirect_frame_instance_missing_series_query( store_api_version: str, projectid: str, location: str, datasetid: str, dicomstore: str, study_instance_uid: str, sop_instance_uid: str, framelist: str, is_rendered_request: bool, ) -> flask.Response: """Flask entry point for DICOMweb frame request that returns frame instances. Args: store_api_version: Healthcare API DICOM store version. projectid: GCP projectid. location: Location of healthcare api dataset. datasetid: Healthcare API dataset ID. dicomstore: DICOM store to connect to. study_instance_uid: DICOM study instance UID. sop_instance_uid: DICOM sop instance UID. framelist: Comma separated value string listing of frames to return. is_rendered_request: True if rendered frame request. Returns: Requested DICOM onstance frame images (flask.Response) """ prefix = f'/frames/{framelist}' if is_rendered_request: prefix = f'{prefix}/rendered' return _missing_series_uid_redirect( dicom_url_util.DicomWebBaseURL( store_api_version, projectid, location, datasetid, dicomstore ), study_instance_uid, sop_instance_uid, prefix, ) def _rendered_wsi_instance( instance_request: _DicomInstanceRequest, ) -> flask.Response: """Returns rendered WSI DICOM instance optionally downsampled. Args: instance_request: DicomInstanceRequest Returns: Requested DICOM onstance frame images (flask.Response) """ if instance_request.metadata.number_of_frames == 1: return _get_rendered_frames(instance_request, '1', True) try: params = _parse_request_params( instance_request.url_args, instance_request.url_header, instance_request.metadata.image_compression, ) except ValueError as exp: cloud_logging_client.error('Exception parsing request params.', exp) return flask_util.exception_flask_response(exp) try: result = downsample_util.downsample_dicom_web_instance( instance_request, params, batch_mode=True, decode_image_as_numpy=True, clip_image_dim=True, ) except _DownsamplingFrameRequestError as exp: cloud_logging_client.error('Error occurred getting rendered instance.', exp) return flask_util.exception_flask_response(exp) except base_dicom_request_error.BaseDicomRequestError as exp: cloud_logging_client.error('Error occurred getting rendered instance.', exp) return exp.flask_response() return flask.Response( response=result.images[0], headers={ 'Content-Type': result.content_type, 'Cache-Control': ( f'max-age={dicom_proxy_flags.CACHE_CONTROL_TTL_RENDERED_FRAMES_FLG.value}' ), }, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_INSTANCE_URL_DEFAULT_VERSION}/rendered', methods=flask_util.GET_AND_POST_METHODS, endpoint='_rendered_instance', defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION }, strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_INSTANCE_URL}/rendered', methods=flask_util.GET_AND_POST_METHODS, endpoint='_rendered_instance', strict_slashes=False, ) @logging_util.log_exceptions @execution_timer.log_execution_time('_rendered_instance') @auth.validate_iap @metadata_util.ClearLocalMetadata() def _rendered_instance( store_api_version: str, projectid: str, location: str, datasetid: str, dicomstore: str, study_instance_uid: str, series_instance_uid: str, sop_instance_uid: str, ) -> flask.Response: """Flask entry point for DICOMweb rendered instance request. Args: store_api_version: Healthcare API DICOM store version. projectid: GCP projectid. location: Location of healthcare api dataset. datasetid: Healthcare API dataset ID. dicomstore: DICOM store to connect to. study_instance_uid: DICOM study instance UID. series_instance_uid: DICOM series instance UID. sop_instance_uid: DICOM sop instance UID. Returns: Requested DICOM instance frame images (flask.Response) """ cloud_logging_client.info( 'External DICOM rendered instance entrypoint', { proxy_const.LogKeywords.HEALTHCARE_API_DICOM_VERSION: ( store_api_version ), proxy_const.LogKeywords.PROJECT_ID: projectid, proxy_const.LogKeywords.LOCATION: location, proxy_const.LogKeywords.DATASET_ID: datasetid, proxy_const.LogKeywords.DICOM_STORE: dicomstore, proxy_const.LogKeywords.STUDY_INSTANCE_UID: study_instance_uid, proxy_const.LogKeywords.SERIES_INSTANCE_UID: series_instance_uid, proxy_const.LogKeywords.SOP_INSTANCE_UID: sop_instance_uid, }, _get_masked_flask_headers(), ) try: instance_request = _create_dicom_instance_web_request( dicom_url_util.DicomWebBaseURL( store_api_version, projectid, location, datasetid, dicomstore ), study_instance_uid, series_instance_uid, sop_instance_uid, ) if not instance_request.metadata.is_wsi_iod: cloud_logging_client.debug( 'Proxying rendered instance request instance is not VL Whole Slide' ' Microscopy Image SOPClassUID.', dataclasses.asdict(instance_request.metadata), ) return dicom_store_util.dicom_store_proxy() if ( instance_request.metadata.is_tiled_sparse and ( instance_request.metadata.total_pixel_matrix_columns != instance_request.metadata.columns or instance_request.metadata.total_pixel_matrix_rows != instance_request.metadata.rows ) ) or ( not instance_request.metadata.is_tiled_full and instance_request.metadata.number_of_frames > 1 ): cloud_logging_client.warning( _INSTANCE_IS_NOT_FULLY_TILED, dataclasses.asdict(instance_request.metadata), ) return dicom_store_util.dicom_store_proxy() return _rendered_wsi_instance(instance_request) except metadata_util.ReadDicomMetadataError as exp: cloud_logging_client.error('Reading DICOM metadata.', exp) return flask_util.exception_flask_response(exp) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_STUDY_URL_DEFAULT_VERSION}/instances/<string:sop_instance_uid>/rendered', methods=flask_util.GET_AND_POST_METHODS, endpoint='_redirect_render_instance_missing_series_query', defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION, }, strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_STUDY_URL}/instances/<string:sop_instance_uid>/rendered', methods=flask_util.GET_AND_POST_METHODS, endpoint='_redirect_render_instance_missing_series_query', strict_slashes=False, ) @logging_util.log_exceptions @execution_timer.log_execution_time( '_redirect_render_instance_missing_series_query' ) @auth.validate_iap @metadata_util.ClearLocalMetadata() def _redirect_render_instance_missing_series_query( store_api_version: str, projectid: str, location: str, datasetid: str, dicomstore: str, study_instance_uid: str, sop_instance_uid: str, ) -> flask.Response: """Flask entry point for DICOMweb rendered instance request. Args: store_api_version: Healthcare API DICOM store version. projectid: GCP projectid. location: Location of healthcare api dataset. datasetid: Healthcare API dataset ID. dicomstore: DICOM store to connect to. study_instance_uid: DICOM study instance UID. sop_instance_uid: DICOM sop instance UID. Returns: Requested DICOM instance frame images (flask.Response) """ return _missing_series_uid_redirect( dicom_url_util.DicomWebBaseURL( store_api_version, projectid, location, datasetid, dicomstore ), study_instance_uid, sop_instance_uid, '/rendered', ) def _get_sop_instance_uid_param_value() -> str: return flask_util.get_key_value( flask_util.get_first_key_args(), 'SOPInstanceUID', '' ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_SERIES_URL_DEFAULT_VERSION}/instances', methods=flask_util.GET_AND_POST_METHODS, defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION, }, endpoint='_instances_search', strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_STUDY_URL_DEFAULT_VERSION}/instances', methods=flask_util.GET_AND_POST_METHODS, defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION, 'series_instance_uid': '', }, endpoint='_instances_search', strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_BASE_URL_DEFAULT_VERSION}/instances', methods=flask_util.GET_AND_POST_METHODS, defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION, 'study_instance_uid': '', 'series_instance_uid': '', }, endpoint='_instances_search', strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_SERIES_URL}/instances', methods=flask_util.GET_AND_POST_METHODS, endpoint='_instances_search', strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_STUDY_URL}/instances', methods=flask_util.GET_AND_POST_METHODS, defaults={ 'series_instance_uid': '', }, endpoint='_instances_search', strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_BASE_URL}/instances', methods=flask_util.GET_AND_POST_METHODS, defaults={ 'study_instance_uid': '', 'series_instance_uid': '', }, endpoint='_instances_search', strict_slashes=False, ) @logging_util.log_exceptions @execution_timer.log_execution_time('_instances_search') @auth.validate_iap @compress.compressed() @metadata_util.ClearLocalMetadata() def _instances_search( store_api_version: str, projectid: str, location: str, datasetid: str, dicomstore: str, study_instance_uid: str, series_instance_uid: str, ) -> flask.Response: """Flask entry point for DICOMweb instance metadata search request. Args: store_api_version: Healthcare API DICOM store version. projectid: GCP projectid. location: Location of healthcare api dataset. datasetid: Healthcare API dataset ID. dicomstore: DICOM store to connect to. study_instance_uid: DICOM Study Instance UID to return metadata. series_instance_uid: DICOM Series Instance UID to return metadata. Returns: Flask response containing dicom metadata (optionally downsampled). """ if study_instance_uid and series_instance_uid: sop_instance_uid = _get_sop_instance_uid_param_value() else: sop_instance_uid = '' cloud_logging_client.info( 'External DICOM instance search entrypoint', { proxy_const.LogKeywords.HEALTHCARE_API_DICOM_VERSION: ( store_api_version ), proxy_const.LogKeywords.PROJECT_ID: projectid, proxy_const.LogKeywords.LOCATION: location, proxy_const.LogKeywords.DATASET_ID: datasetid, proxy_const.LogKeywords.DICOM_STORE: dicomstore, proxy_const.LogKeywords.STUDY_INSTANCE_UID: study_instance_uid, proxy_const.LogKeywords.SERIES_INSTANCE_UID: series_instance_uid, proxy_const.LogKeywords.SOP_INSTANCE_UID: sop_instance_uid, }, _get_masked_flask_headers(), ) dicom_web_base_url = dicom_url_util.DicomWebBaseURL( store_api_version, projectid, location, datasetid, dicomstore ) # Sparse WSI DICOM encodes the position of each frame within the per-frame # functional group sequence. If this sequence is to large the Google DICOM # store (V1 API & V1Beta1 API) will not return the contents of the sequence. # The only way to retrieve the metadata is to retireve the whole instance. # Sparse DICOM is identified by encoding the value "TILED_SPARSE" in # the root DICOM tag keyword DimensionalOrganizationType. The proxy will # check and correct metadata responses for sparse WSI DICOM for missing per- # frame functional group sequence. To determine if a DICOM is sparse the # response must include the DimensionalOrganizationType in the response. # Metadata queries will always return this. Instance tag queres will not # unless the instance query is perfromed with the parameter includefield=all # or includefield=DimensionalOrganizationType or includefield=00209311. The # code here tests if an instance query is requesting the # PerFrameFunctionalGroupSequence and the DimensionalOrganizationType. Queries # requesting PerFrameFunctionalGroupSequence but not the # DimensionalOrganizationType are augmented to include a # DimensionalOrganizationType metadata request. if ( sparse_dicom_util.do_includefields_request_perframe_functional_group_seq() and not sparse_dicom_util.do_includefields_request_dimensional_organization_type() ): additional_parameters = [ f'includefield={sparse_dicom_util.DIMENSIONAL_ORGANIZATION_TYPE_DICOM_TAG}' ] else: additional_parameters = None return metadata_augmentation.augment_instance_metadata( dicom_web_base_url, dicom_url_util.StudyInstanceUID(study_instance_uid), dicom_url_util.SeriesInstanceUID(series_instance_uid), dicom_url_util.SOPInstanceUID(sop_instance_uid), dicom_store_util.dicom_store_proxy(params=additional_parameters), ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_STUDY_URL_DEFAULT_VERSION}/series', methods=flask_util.GET_AND_POST_METHODS, defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION, }, endpoint='_series_search', strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_BASE_URL_DEFAULT_VERSION}/series', methods=flask_util.GET_AND_POST_METHODS, defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION, 'study_instance_uid': '', }, endpoint='_series_search', strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_STUDY_URL}/series', methods=flask_util.GET_AND_POST_METHODS, endpoint='_series_search', strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_BASE_URL}/series', methods=flask_util.GET_AND_POST_METHODS, defaults={ 'study_instance_uid': '', }, endpoint='_series_search', strict_slashes=False, ) @logging_util.log_exceptions @execution_timer.log_execution_time('_studysearch') @auth.validate_iap @compress.compressed() @metadata_util.ClearLocalMetadata() def _series_search( store_api_version: str, projectid: str, location: str, datasetid: str, dicomstore: str, study_instance_uid: str, ) -> flask.Response: """Flask entry point for DICOMweb study metadata search request. Args: store_api_version: Healthcare API DICOM store version. projectid: GCP projectid. location: Location of healthcare api dataset. datasetid: Healthcare API dataset ID. dicomstore: DICOM store to connect to. study_instance_uid: DICOM Study Instance UID to return metadata. Returns: Flask response containing dicom metadata. """ cloud_logging_client.info( 'External DICOM study search entrypoint', { proxy_const.LogKeywords.HEALTHCARE_API_DICOM_VERSION: ( store_api_version ), proxy_const.LogKeywords.PROJECT_ID: projectid, proxy_const.LogKeywords.LOCATION: location, proxy_const.LogKeywords.DATASET_ID: datasetid, proxy_const.LogKeywords.DICOM_STORE: dicomstore, }, _get_masked_flask_headers(), ) dicom_web_base_url = dicom_url_util.DicomWebBaseURL( store_api_version, projectid, location, datasetid, dicomstore ) return metadata_augmentation.augment_series_response_metadata( dicom_web_base_url, dicom_url_util.StudyInstanceUID(study_instance_uid), dicom_store_util.dicom_store_proxy(), ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_BASE_URL_DEFAULT_VERSION}/studies', methods=flask_util.GET_AND_POST_METHODS, defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION, }, endpoint='_studies_search', strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_BASE_URL}/studies', methods=flask_util.GET_AND_POST_METHODS, endpoint='_studies_search', strict_slashes=False, ) @logging_util.log_exceptions @execution_timer.log_execution_time('_studysearch') @auth.validate_iap @compress.compressed() @metadata_util.ClearLocalMetadata() def _studies_search( store_api_version: str, projectid: str, location: str, datasetid: str, dicomstore: str, ) -> flask.Response: """Flask entry point for DICOMweb study metadata search request. Args: store_api_version: Healthcare API DICOM store version. projectid: GCP projectid. location: Location of healthcare api dataset. datasetid: Healthcare API dataset ID. dicomstore: DICOM store to connect to. Returns: Flask response containing dicom metadata. """ cloud_logging_client.info( 'External DICOM study search entrypoint', { proxy_const.LogKeywords.HEALTHCARE_API_DICOM_VERSION: ( store_api_version ), proxy_const.LogKeywords.PROJECT_ID: projectid, proxy_const.LogKeywords.LOCATION: location, proxy_const.LogKeywords.DATASET_ID: datasetid, proxy_const.LogKeywords.DICOM_STORE: dicomstore, }, _get_masked_flask_headers(), ) dicom_web_base_url = dicom_url_util.DicomWebBaseURL( store_api_version, projectid, location, datasetid, dicomstore ) return metadata_augmentation.augment_study_response_metadata( dicom_web_base_url, dicom_store_util.dicom_store_proxy(), ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_INSTANCE_URL_DEFAULT_VERSION}/metadata', methods=flask_util.GET_AND_POST_METHODS, endpoint='_metadata_search', defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION }, strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_SERIES_URL_DEFAULT_VERSION}/metadata', methods=flask_util.GET_AND_POST_METHODS, defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION, 'sop_instance_uid': '', }, endpoint='_metadata_search', strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_STUDY_URL_DEFAULT_VERSION}/metadata', methods=flask_util.GET_AND_POST_METHODS, defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION, 'series_instance_uid': '', 'sop_instance_uid': '', }, endpoint='_metadata_search', strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_BASE_URL_DEFAULT_VERSION}/metadata', methods=flask_util.GET_AND_POST_METHODS, endpoint='_metadata_search', defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION, 'study_instance_uid': '', 'series_instance_uid': '', 'sop_instance_uid': '', }, strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_INSTANCE_URL}/metadata', methods=flask_util.GET_AND_POST_METHODS, endpoint='_metadata_search', strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_SERIES_URL}/metadata', methods=flask_util.GET_AND_POST_METHODS, defaults={'sop_instance_uid': ''}, endpoint='_metadata_search', strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_STUDY_URL}/metadata', methods=flask_util.GET_AND_POST_METHODS, defaults={ 'series_instance_uid': '', 'sop_instance_uid': '', }, endpoint='_metadata_search', strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_BASE_URL}/metadata', methods=flask_util.GET_AND_POST_METHODS, endpoint='_metadata_search', defaults={ 'study_instance_uid': '', 'series_instance_uid': '', 'sop_instance_uid': '', }, strict_slashes=False, ) @logging_util.log_exceptions @execution_timer.log_execution_time('_metadata_search') @auth.validate_iap @compress.compressed() @metadata_util.ClearLocalMetadata() def _metadata_search( store_api_version: str, projectid: str, location: str, datasetid: str, dicomstore: str, study_instance_uid: str, series_instance_uid: str, sop_instance_uid: str, ) -> flask.Response: """Flask entry point for DICOMweb instance metadata search request. Args: store_api_version: Healthcare API DICOM store version. projectid: GCP projectid. location: Location of healthcare api dataset. datasetid: Healthcare API dataset ID. dicomstore: DICOM store to connect to. study_instance_uid: DICOM Study Instance UID to return metadata. series_instance_uid: DICOM Series Instance UID to return metadata. sop_instance_uid: DICOM SOP Instance UID to return metadata. Returns: Flask response containing dicom metadata (optionally downsampled). """ cloud_logging_client.info( 'External DICOM metadata search entrypoint', { proxy_const.LogKeywords.HEALTHCARE_API_DICOM_VERSION: ( store_api_version ), proxy_const.LogKeywords.PROJECT_ID: projectid, proxy_const.LogKeywords.LOCATION: location, proxy_const.LogKeywords.DATASET_ID: datasetid, proxy_const.LogKeywords.DICOM_STORE: dicomstore, proxy_const.LogKeywords.STUDY_INSTANCE_UID: study_instance_uid, proxy_const.LogKeywords.SERIES_INSTANCE_UID: series_instance_uid, proxy_const.LogKeywords.SOP_INSTANCE_UID: sop_instance_uid, }, _get_masked_flask_headers(), ) dicom_web_base_url = dicom_url_util.DicomWebBaseURL( store_api_version, projectid, location, datasetid, dicomstore ) return metadata_augmentation.augment_instance_metadata( dicom_web_base_url, dicom_url_util.StudyInstanceUID(study_instance_uid), dicom_url_util.SeriesInstanceUID(series_instance_uid), dicom_url_util.SOPInstanceUID(sop_instance_uid), dicom_store_util.dicom_store_proxy(), ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_BASE_URL_DEFAULT_VERSION}/<path:path>', methods=flask_util.GET_POST_AND_DELETE_METHODS, endpoint='_general_path', defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION }, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_BASE_URL}/<path:path>', methods=flask_util.GET_POST_AND_DELETE_METHODS, endpoint='_general_path', ) @logging_util.log_exceptions @execution_timer.log_execution_time('_general_path') @auth.validate_iap @metadata_util.ClearLocalMetadata() def _general_path( store_api_version: str, projectid: str, location: str, datasetid: str, dicomstore: str, path: str, ) -> flask.Response: """Flask entry point for DICOM web request to proxy. Args: store_api_version: Healthcare API DICOM store version. projectid: GCP projectid. location: Location of healthcare api dataset. datasetid: Healthcare API dataset ID. dicomstore: DICOM store to connect to. path: DICOM store web request. Returns: Requested DICOM onstance frame images (flask.Response) """ cloud_logging_client.debug( 'External DICOM proxy entrypoint', { proxy_const.LogKeywords.HEALTHCARE_API_DICOM_VERSION: ( store_api_version ), proxy_const.LogKeywords.PROJECT_ID: projectid, proxy_const.LogKeywords.LOCATION: location, proxy_const.LogKeywords.DATASET_ID: datasetid, proxy_const.LogKeywords.DICOM_STORE: dicomstore, proxy_const.LogKeywords.DICOMWEB_URL: path, }, _get_masked_flask_headers(), ) return dicom_store_util.dicom_store_proxy() def _dicom_instance_icc_profile_bulkdata( instance_request: _DicomInstanceRequest, ) -> flask.Response: """Return DICOM instance ICCProfile bulkdata. Args: instance_request: DICOM instance requesting ICCProfile bulkdata. Returns: ICCProfile bulkdata. """ try: # validate the user can read metadata for instance before returning. _ = instance_request.metadata icc_profile_bytes = instance_request.icc_profile() except metadata_util.ReadDicomMetadataError as exp: cloud_logging_client.error('Reading DICOM metadata.', exp) return flask_util.exception_flask_response(exp) if icc_profile_bytes is None: cloud_logging_client.error('ICCProfile is not found.') return flask_util.exception_flask_response('ICCProfile is not found.') multipart_data = requests_toolbelt.MultipartEncoder( fields=[( 'icc_profile.icc', ( None, icc_profile_bytes, 'application/octet-stream', ), )] ) return flask.Response( response=multipart_data.read(), headers={ 'Content-Type': ( f'multipart/related; boundary={multipart_data.boundary_value}' ) }, direct_passthrough=True, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_INSTANCE_URL_DEFAULT_VERSION}/{bulkdata_util.PROXY_BULK_DATA_URI}/<path:path>', methods=flask_util.GET_AND_POST_METHODS, endpoint='_get_bulkdata', defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION }, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_INSTANCE_URL}/{bulkdata_util.PROXY_BULK_DATA_URI}/<path:path>', methods=flask_util.GET_AND_POST_METHODS, endpoint='_get_bulkdata', ) @logging_util.log_exceptions @execution_timer.log_execution_time('_get_bulkdata') @auth.validate_iap @metadata_util.ClearLocalMetadata() def _get_bulkdata( store_api_version: str, projectid: str, location: str, datasetid: str, dicomstore: str, study_instance_uid: str, series_instance_uid: str, sop_instance_uid: str, path: str, ) -> flask.Response: """Flask entry point for ICCProfile bulkdata request. Args: store_api_version: Healthcare API DICOM store version. projectid: GCP projectid. location: Location of healthcare api dataset. datasetid: Healthcare API dataset ID. dicomstore: DICOM store to connect to. study_instance_uid: DICOM study instance UID. series_instance_uid: DICOM series instance UID. sop_instance_uid: DICOM sop instance UID. path: Path to instance bulk data. Returns: Proxy response or ICCProfile bulkdata. """ cloud_logging_client.info( 'External bulkdata retrieval entrypoint', { proxy_const.LogKeywords.HEALTHCARE_API_DICOM_VERSION: ( store_api_version ), proxy_const.LogKeywords.PROJECT_ID: projectid, proxy_const.LogKeywords.LOCATION: location, proxy_const.LogKeywords.DATASET_ID: datasetid, proxy_const.LogKeywords.DICOM_STORE: dicomstore, proxy_const.LogKeywords.STUDY_INSTANCE_UID: study_instance_uid, proxy_const.LogKeywords.SERIES_INSTANCE_UID: series_instance_uid, proxy_const.LogKeywords.SOP_INSTANCE_UID: sop_instance_uid, proxy_const.LogKeywords.PATH_TO_BULK_DATA: path, }, _get_masked_flask_headers(), ) dicom_web_base_url = dicom_url_util.DicomWebBaseURL( store_api_version, projectid, location, datasetid, dicomstore ) if bulkdata_util.does_dicom_store_support_bulkdata(dicom_web_base_url): cloud_logging_client.debug( 'DICOM store supports bulkdata proxying request.' ) return dicom_store_util.dicom_store_proxy() instance_request = _create_dicom_instance_web_request( dicom_web_base_url, study_instance_uid, series_instance_uid, sop_instance_uid, ) if path.endswith(iccprofile_bulk_metadata_util.ICCPROFILE_DICOM_TAG_KEYWORD): cloud_logging_client.debug('Performing ICC profile bulkdata retreval.') return _dicom_instance_icc_profile_bulkdata(instance_request) cloud_logging_client.debug('Performing bulkdata retrieval using proxy.') return dicom_store_util.dicom_store_proxy() @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_BASE_URL_DEFAULT_VERSION}/studies/<string:study_instance_uid>', methods=flask_util.POST_METHOD, endpoint='_store_annotation_instance', defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION }, strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_BASE_URL_DEFAULT_VERSION}/studies', methods=flask_util.POST_METHOD, endpoint='_store_annotation_instance', defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION, 'study_instance_uid': '', }, strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_BASE_URL}/studies/<string:study_instance_uid>', methods=flask_util.POST_METHOD, endpoint='_store_annotation_instance', strict_slashes=False, ) @dicom_proxy.route( f'{dicom_url_util.DICOM_WEB_BASE_URL}/studies', methods=flask_util.POST_METHOD, endpoint='_store_annotation_instance', defaults={'study_instance_uid': ''}, strict_slashes=False, ) @auth.validate_iap @logging_util.log_exceptions @metadata_util.ClearLocalMetadata() def _store_annotation_instance( store_api_version: str, projectid: str, location: str, datasetid: str, dicomstore: str, study_instance_uid: str = '', ) -> flask.Response: """Flask entry point for DICOMweb request to store an annotation instance. Args: store_api_version: Healthcare API DICOM store version. projectid: GCP projectid. location: Location of healthcare api dataset. datasetid: Healthcare API dataset ID. dicomstore: DICOM store to connect to. study_instance_uid: Optional study instance uid Returns: flask.Response """ cloud_logging_client.info( 'Externalstore DICOM instance entrypoint', { proxy_const.LogKeywords.HEALTHCARE_API_DICOM_VERSION: ( store_api_version ), proxy_const.LogKeywords.PROJECT_ID: projectid, proxy_const.LogKeywords.LOCATION: location, proxy_const.LogKeywords.DATASET_ID: datasetid, proxy_const.LogKeywords.DICOM_STORE: dicomstore, proxy_const.LogKeywords.STUDY_INSTANCE_UID: study_instance_uid, }, _get_masked_flask_headers(), ) if dicom_proxy_flags.ENABLE_ANNOTATIONS_ENDPOINT_FLG.value: return annotations_util.store_instance( dicom_url_util.DicomWebBaseURL( store_api_version, projectid, location, datasetid, dicomstore ), study_instance_uid, ) cloud_logging_client.debug('Performing instance store using proxy.') return dicom_store_util.dicom_store_proxy() @dicom_proxy.route( dicom_url_util.DICOM_WEB_INSTANCE_URL_DEFAULT_VERSION, methods=flask_util.DELETE_METHOD, endpoint='_delete_annotation_instance', defaults={ 'store_api_version': dicom_proxy_flags.DEFAULT_DICOM_STORE_API_VERSION }, strict_slashes=False, ) @dicom_proxy.route( dicom_url_util.DICOM_WEB_INSTANCE_URL, methods=flask_util.DELETE_METHOD, endpoint='_delete_annotation_instance', strict_slashes=False, ) @auth.validate_iap @logging_util.log_exceptions @metadata_util.ClearLocalMetadata() def _delete_annotation_instance( store_api_version: str, projectid: str, location: str, datasetid: str, dicomstore: str, study_instance_uid: str, series_instance_uid: str, sop_instance_uid: str, ) -> flask.Response: """Flask entry point for DICOMweb request to delete annotations. Deletes all annotations for a user on a slide. Args: store_api_version: Healthcare API DICOM store version. projectid: GCP projectid. location: Location of healthcare api dataset. datasetid: Healthcare API dataset ID. dicomstore: DICOM store to connect to. study_instance_uid: Study uid of instance to delete. series_instance_uid: Series uid of instance to delete. sop_instance_uid: Instance uid of instance to delete. Returns: flask.Response """ cloud_logging_client.info( 'External delete DICOM instance entrypoint.', { proxy_const.LogKeywords.HEALTHCARE_API_DICOM_VERSION: ( store_api_version ), proxy_const.LogKeywords.PROJECT_ID: projectid, proxy_const.LogKeywords.LOCATION: location, proxy_const.LogKeywords.DATASET_ID: datasetid, proxy_const.LogKeywords.DICOM_STORE: dicomstore, proxy_const.LogKeywords.STUDY_INSTANCE_UID: study_instance_uid, proxy_const.LogKeywords.SERIES_INSTANCE_UID: series_instance_uid, proxy_const.LogKeywords.SOP_INSTANCE_UID: sop_instance_uid, }, _get_masked_flask_headers(), ) if dicom_proxy_flags.ENABLE_ANNOTATIONS_ENDPOINT_FLG.value: return annotations_util.delete_instance( dicom_url_util.DicomWebBaseURL( store_api_version, projectid, location, datasetid, dicomstore ), study_instance_uid, series_instance_uid, sop_instance_uid, ) cloud_logging_client.debug('Performing instance delete using proxy.') return dicom_store_util.dicom_store_proxy() build_version.init_cloud_logging_build_version() # Required to initialize build version in logs of forked child processes. os.register_at_fork( after_in_child=build_version.init_cloud_logging_build_version )