Skip to content

Commit

Permalink
Added DELETE methods to the api. (#32)
Browse files Browse the repository at this point in the history
Add non-standard delete methods for studies, series and instances
  • Loading branch information
codezizo committed May 13, 2020
1 parent 8736d14 commit e9667d6
Show file tree
Hide file tree
Showing 2 changed files with 220 additions and 0 deletions.
153 changes: 153 additions & 0 deletions src/dicomweb_client/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@ class DICOMwebClient(object):
URL path prefix for WADO-RS (not part of `base_url`)
stow_url_prefix: Union[str, None]
URL path prefix for STOW-RS (not part of `base_url`)
delete_url_prefix: Union[str, None]
URL path prefix for DELETE (not part of `base_url`)
'''

Expand Down Expand Up @@ -310,6 +312,7 @@ def __init__(
qido_url_prefix: Optional[str] = None,
wado_url_prefix: Optional[str] = None,
stow_url_prefix: Optional[str] = None,
delete_url_prefix: Optional[str] = None,
proxies: Optional[Dict[str, str]] = None,
headers: Optional[Dict[str, str]] = None,
callback: Optional[Callable] = None,
Expand All @@ -332,6 +335,8 @@ def __init__(
URL path prefix for WADO RESTful services
stow_url_prefix: str, optional
URL path prefix for STOW RESTful services
delete_url_prefix: str, optional
URL path prefix for DELETE RESTful services
proxies: Dict[str, str], optional
mapping of protocol or protocol + host to the URL of a proxy server
headers: Dict[str, str], optional
Expand All @@ -354,6 +359,7 @@ def __init__(
self.qido_url_prefix = qido_url_prefix
self.wado_url_prefix = wado_url_prefix
self.stow_url_prefix = stow_url_prefix
self.delete_url_prefix = delete_url_prefix

# This regular expression extracts the scheme and host name from the URL
# and optionally the port number and prefix:
Expand Down Expand Up @@ -491,6 +497,9 @@ def _get_service_url(self, service_name: str) -> str:
elif service_name == 'stow':
if self.stow_url_prefix is not None:
service_url += '/{}'.format(self.stow_url_prefix)
elif service_name == 'delete':
if self.delete_url_prefix is not None:
service_url += '/{}'.format(self.delete_url_prefix)
else:
raise ValueError(
'Unsupported DICOMweb service "{}".'.format(service_name)
Expand Down Expand Up @@ -1510,6 +1519,35 @@ def _http_post_multipart_application_dicom(
return _load_xml_dataset(tree)
return pydicom.Dataset()

def _http_delete(self, url: str):
'''Performs a HTTP DELETE request to the specified URL.
Parameters
----------
url: str
unique resource locator
Returns
-------
requests.models.Response
HTTP response message
'''
@retrying.retry(
retry_on_result=self._is_retriable_http_error,
wait_exponential_multiplier=self._wait_exponential_multiplier,
stop_max_attempt_number=self._max_attempts
)
def _invoke_delete_request(url: str) -> requests.models.Response:
return self._session.delete(url)

response = _invoke_delete_request(url)
if response.status_code == HTTPStatus.METHOD_NOT_ALLOWED:
logger.error(
'Resource could not be deleted. The origin server may not support'
'deletion or you may not have the necessary permissions.')
response.raise_for_status()
return response

def search_for_studies(
self,
fuzzymatching: Optional[bool] = None,
Expand Down Expand Up @@ -1745,6 +1783,35 @@ def retrieve_study_metadata(
url += '/metadata'
return self._http_get_application_json(url)

def delete_study(self, study_instance_uid: str) -> None:
'''Deletes specified study and its respective instances.
Parameters
----------
study_instance_uid: str
unique study identifier
Returns
-------
requests.models.Response
HTTP response object returned.
Note
----
The Delete Study resource is not part of the DICOM standard
and may not be supported by all origin servers.
WARNING
-------
This method performs a DELETE and should be used with caution.
'''
if study_instance_uid is None:
raise ValueError(
'Study Instance UID is required for deletion of a study.'
)
url = self._get_studies_url('delete', study_instance_uid)
return self._http_delete(url)

def _assert_uid_format(self, uid: str) -> None:
'''Checks whether a DICOM UID has the correct format.
Expand Down Expand Up @@ -1979,6 +2046,45 @@ def retrieve_series_rendered(
'retrieval of rendered series.'.format(common_media_type)
)

def delete_series(
self,
study_instance_uid: str,
series_instance_uid: str
) -> None:
'''Deletes specified series and its respective instances.
Parameters
----------
study_instance_uid: str
unique study identifier
series_instance_uid: str
unique series identifier
Note
----
The Delete Series resource is not part of the DICOM standard
and may not be supported by all origin servers.
Returns
-------
requests.models.Response
HTTP response object returned.
WARNING
-------
This method performs a DELETE and should be used with caution.
'''
if study_instance_uid is None:
raise ValueError(
'Study Instance UID is required for deletion of a series.'
)
if series_instance_uid is None:
raise ValueError(
'Series Instance UID is required for deletion of a series.'
)
url = self._get_series_url('delete', study_instance_uid,
series_instance_uid)
return self._http_delete(url)

def search_for_instances(
self,
study_instance_uid: Optional[str] = None,
Expand Down Expand Up @@ -2145,6 +2251,53 @@ def store_instances(
encoded_datasets
)

def delete_instance(
self,
study_instance_uid: str,
series_instance_uid: str,
sop_instance_uid: str
) -> None:
'''Deletes specified instance.
Parameters
----------
study_instance_uid: str
unique study identifier
series_instance_uid: str
unique series identifier
sop_instance_uid: str
unique instance identifier
Returns
-------
requests.models.Response
HTTP response object returned.
Note
----
The Delete Instance resource is not part of the DICOM standard
and may not be supported by all origin servers.
WARNING
-------
This method performs a DELETE and should be used with caution.
'''
if study_instance_uid is None:
raise ValueError(
'Study Instance UID is required for deletion of an instance.'
)
if series_instance_uid is None:
raise ValueError(
'Series Instance UID is required for deletion of an instance.'
)
if sop_instance_uid is None:
raise ValueError(
'SOP Instance UID is required for deletion of an instance.'
)
url = self._get_instances_url('delete', study_instance_uid,
series_instance_uid, sop_instance_uid)
return self._http_delete(url)

def retrieve_instance_metadata(
self,
study_instance_uid: str,
Expand Down
67 changes: 67 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,73 @@ def test_store_instance_error_with_no_retries(httpserver, client, cache_dir):
)


def test_delete_study_error(httpserver, client, cache_dir):
study_instance_uid = '1.2.3'
httpserver.serve_content(
content='',
code=HTTPStatus.METHOD_NOT_ALLOWED,
headers=''
)
with pytest.raises(HTTPError):
client.delete_study(study_instance_uid=study_instance_uid)
assert len(httpserver.requests) == 1
request = httpserver.requests[0]
expected_path = (
'/studies/{study_instance_uid}'.format(
study_instance_uid=study_instance_uid)
)
assert request.path == expected_path
assert request.method == 'DELETE'


def test_delete_series_error(httpserver, client, cache_dir):
study_instance_uid = '1.2.3'
series_instance_uid = '1.2.4'
httpserver.serve_content(
content='',
code=HTTPStatus.METHOD_NOT_ALLOWED,
headers=''
)
with pytest.raises(HTTPError):
client.delete_series(study_instance_uid=study_instance_uid,
series_instance_uid=series_instance_uid)
assert len(httpserver.requests) == 1
request = httpserver.requests[0]
expected_path = (
'/studies/{study_instance_uid}/series/{series_instance_uid}'.format(
study_instance_uid=study_instance_uid,
series_instance_uid=series_instance_uid)
)
assert request.path == expected_path
assert request.method == 'DELETE'


def test_delete_instance_error(httpserver, client, cache_dir):
study_instance_uid = '1.2.3'
series_instance_uid = '1.2.4'
sop_instance_uid = '1.2.5'
httpserver.serve_content(
content='',
code=HTTPStatus.METHOD_NOT_ALLOWED,
headers=''
)
with pytest.raises(HTTPError):
client.delete_instance(study_instance_uid=study_instance_uid,
series_instance_uid=series_instance_uid,
sop_instance_uid=sop_instance_uid)
assert len(httpserver.requests) == 1
request = httpserver.requests[0]
expected_path = (
'/studies/{study_instance_uid}/series/{series_instance_uid}/instances'
'/{sop_instance_uid}'.format(
study_instance_uid=study_instance_uid,
series_instance_uid=series_instance_uid,
sop_instance_uid=sop_instance_uid,)
)
assert request.path == expected_path
assert request.method == 'DELETE'


def test_load_json_dataset_da(httpserver, client, cache_dir):
value = ['2018-11-21']
dicom_json = {
Expand Down

0 comments on commit e9667d6

Please sign in to comment.