diff --git a/docs/usage.rst b/docs/usage.rst index 959177f..e6a56c5 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -200,6 +200,12 @@ In this case, the method can be called repeatedly to request remaining results u studies.extend(subset) offset += len(subset) +The same can be achieved more conveniently using the ``get_remaining`` parameter. + +.. code-block:: python + + studies = client.search_for_studies(get_remaining=True) + .. _searchforseries: diff --git a/src/dicomweb_client/__init__.py b/src/dicomweb_client/__init__.py index d71a18b..519f8b2 100644 --- a/src/dicomweb_client/__init__.py +++ b/src/dicomweb_client/__init__.py @@ -1,4 +1,4 @@ -__version__ = '0.54.4' +__version__ = '0.55.0' from dicomweb_client.api import DICOMwebClient, DICOMfileClient from dicomweb_client.protocol import DICOMClient diff --git a/src/dicomweb_client/file.py b/src/dicomweb_client/file.py index e760f02..259e44c 100644 --- a/src/dicomweb_client/file.py +++ b/src/dicomweb_client/file.py @@ -1410,7 +1410,8 @@ def search_for_studies( limit: Optional[int] = None, offset: Optional[int] = None, fields: Optional[Sequence[str]] = None, - search_filters: Optional[Dict[str, Any]] = None + search_filters: Optional[Dict[str, Any]] = None, + get_remaining: bool = False ) -> List[Dict[str, dict]]: """Search for studies. @@ -1428,6 +1429,8 @@ def search_for_studies( Search filter criteria as key-value pairs, where *key* is a keyword or a tag of the attribute and *value* is the expected value that should match + get_remaining: bool, optional + Whether remaining results should be included Returns ------- @@ -1492,7 +1495,8 @@ def search_for_series( limit: Optional[int] = None, offset: Optional[int] = None, fields: Optional[Sequence[str]] = None, - search_filters: Optional[Dict[str, Any]] = None + search_filters: Optional[Dict[str, Any]] = None, + get_remaining: bool = False ) -> List[Dict[str, dict]]: """Search for series. @@ -1512,6 +1516,8 @@ def search_for_series( Search filter criteria as key-value pairs, where *key* is a keyword or a tag of the attribute and *value* is the expected value that should match + get_remaining: bool, optional + Whether remaining results should be included Returns ------- @@ -1639,7 +1645,8 @@ def search_for_instances( limit: Optional[int] = None, offset: Optional[int] = None, fields: Optional[Sequence[str]] = None, - search_filters: Optional[Dict[str, Any]] = None + search_filters: Optional[Dict[str, Any]] = None, + get_remaining: bool = False ) -> List[Dict[str, dict]]: """Search for instances. @@ -1661,6 +1668,8 @@ def search_for_instances( Search filter criteria as key-value pairs, where *key* is a keyword or a tag of the attribute and *value* is the expected value that should match + get_remaining: bool, optional + Whether remaining results should be included Returns ------- @@ -2416,16 +2425,18 @@ def iter_instance_frames( else m for m in media_types ])) - if transfer_syntax_uid.is_encapsulated: - are_media_types_valid = all( - m.startswith('image') - for m in acceptable_media_types + are_media_types_valid = all( + m.startswith('image') or + m.startswith('application/octet-stream') + for m in acceptable_media_types + ) + if not are_media_types_valid: + raise ValueError( + 'Instance frames can only be retrieved ' + 'using media type "image/{jpeg,jls,jp2,jpx,dicom-rle}" or ' + '"application/octet-stream".' ) - if not are_media_types_valid: - raise ValueError( - 'Compressed instance frames can only be ' - 'retrieved using media type "image".' - ) + if transfer_syntax_uid.is_encapsulated: if 'image/jp2k' in acceptable_media_types: if transfer_syntax_uid == '1.2.840.10008.1.2.4.90': image_type = None @@ -2445,16 +2456,6 @@ def iter_instance_frames( ) ) else: - are_media_types_valid = all( - m == 'application/octet-stream' - for m in acceptable_media_types - ) - if not are_media_types_valid: - raise ValueError( - 'Uncompressed instance frames can only be ' - 'retrieved using media type ' - '"application/octet-stream".' - ) image_type = None else: # Return as stored. diff --git a/src/dicomweb_client/protocol.py b/src/dicomweb_client/protocol.py index f53ce3d..1f5d473 100644 --- a/src/dicomweb_client/protocol.py +++ b/src/dicomweb_client/protocol.py @@ -28,7 +28,8 @@ def search_for_studies( limit: Optional[int] = None, offset: Optional[int] = None, fields: Optional[Sequence[str]] = None, - search_filters: Optional[Dict[str, Any]] = None + search_filters: Optional[Dict[str, Any]] = None, + get_remaining: bool = False ) -> List[Dict[str, dict]]: """Search for studies. @@ -46,6 +47,9 @@ def search_for_studies( Search filter criteria as key-value pairs, where *key* is a keyword or a tag of the attribute and *value* is the expected value that should match + get_remaining: bool, optional + Whether remaining results should be included (this may repeatedly + query the server for remaining results) Returns ------- @@ -228,7 +232,8 @@ def search_for_series( limit: Optional[int] = None, offset: Optional[int] = None, fields: Optional[Sequence[str]] = None, - search_filters: Optional[Dict[str, Any]] = None + search_filters: Optional[Dict[str, Any]] = None, + get_remaining: bool = False ) -> List[Dict[str, dict]]: """Search for series. @@ -248,6 +253,9 @@ def search_for_series( Search filter criteria as key-value pairs, where *key* is a keyword or a tag of the attribute and *value* is the expected value that should match + get_remaining: bool, optional + Whether remaining results should be included (this may repeatedly + query the server for remaining results) Returns ------- @@ -425,7 +433,8 @@ def search_for_instances( limit: Optional[int] = None, offset: Optional[int] = None, fields: Optional[Sequence[str]] = None, - search_filters: Optional[Dict[str, Any]] = None + search_filters: Optional[Dict[str, Any]] = None, + get_remaining: bool = False ) -> List[Dict[str, dict]]: """Search for instances. @@ -447,6 +456,9 @@ def search_for_instances( Search filter criteria as key-value pairs, where *key* is a keyword or a tag of the attribute and *value* is the expected value that should match + get_remaining: bool, optional + Whether remaining results should be included (this may repeatedly + query the server for remaining results) Returns ------- diff --git a/src/dicomweb_client/web.py b/src/dicomweb_client/web.py index 8253d93..8a50132 100644 --- a/src/dicomweb_client/web.py +++ b/src/dicomweb_client/web.py @@ -15,6 +15,7 @@ Dict, Iterator, List, + Mapping, Optional, Set, Sequence, @@ -630,7 +631,8 @@ def _http_get_application_json( self, url: str, params: Optional[Dict[str, Any]] = None, - stream: bool = False + stream: bool = False, + get_remaining: bool = False ) -> List[Dict[str, dict]]: """GET a resource with "applicaton/dicom+json" media type. @@ -643,6 +645,8 @@ def _http_get_application_json( stream: bool, optional Whether data should be streamed (i.e., requested using chunked transfer encoding) + get_remaining: bool, optional + Whether remaining data should also be requested Returns ------- @@ -650,22 +654,36 @@ def _http_get_application_json( Content of HTTP message body in DICOM JSON format """ - content_type = 'application/dicom+json, application/json' - response = self._http_get( - url, - params=params, - headers={'Accept': content_type}, - stream=stream - ) - if response.content: - decoded_response = response.json() - # All metadata resources are expected to be sent as a JSON array of - # DICOM data sets. However, some origin servers may incorrectly - # sent an individual data set. - if isinstance(decoded_response, dict): - return [decoded_response] - return decoded_response - return [] + + def get(url, params, stream): + response = self._http_get( + url, + params=params, + headers={'Accept': 'application/dicom+json, application/json'}, + stream=stream + ) + if response.content: + decoded_response = response.json() + # All metadata resources are expected to be sent as a JSON + # array of DICOM data sets. However, some origin servers may + # incorrectly sent an individual data set. + if isinstance(decoded_response, dict): + return [decoded_response] + return decoded_response + return [] + + if get_remaining: + results = [] + params['offset'] = params.get('offset', 0) + while True: + subset = get(url, params, stream) + if len(subset) == 0: + break + results.extend(subset) + params['offset'] += len(subset) + return results + else: + return get(url, params, stream) @classmethod def _extract_part_content(cls, part: bytes) -> Union[bytes, None]: @@ -1610,7 +1628,8 @@ def search_for_studies( limit: Optional[int] = None, offset: Optional[int] = None, fields: Optional[Sequence[str]] = None, - search_filters: Optional[Dict[str, Any]] = None + search_filters: Optional[Dict[str, Any]] = None, + get_remaining: bool = False ) -> List[Dict[str, dict]]: """Search for studies. @@ -1628,6 +1647,9 @@ def search_for_studies( Search filter criteria as key-value pairs, where *key* is a keyword or a tag of the attribute and *value* is the expected value that should match + get_remaining: bool, optional + Whether remaining results should be included (this may repeatedly + query the server for remaining results) Returns ------- @@ -1648,7 +1670,11 @@ def search_for_studies( params = self._parse_qido_query_parameters( fuzzymatching, limit, offset, fields, search_filters ) - return self._http_get_application_json(url, params) + return self._http_get_application_json( + url, + params=params, + get_remaining=get_remaining + ) @classmethod def _parse_media_type(cls, media_type: str) -> Tuple[str, str]: @@ -2057,7 +2083,8 @@ def search_for_series( limit: Optional[int] = None, offset: Optional[int] = None, fields: Optional[Sequence[str]] = None, - search_filters: Optional[Dict[str, Any]] = None + search_filters: Optional[Dict[str, Any]] = None, + get_remaining: bool = False ) -> List[Dict[str, dict]]: """Search for series. @@ -2077,6 +2104,9 @@ def search_for_series( Search filter criteria as key-value pairs, where *key* is a keyword or a tag of the attribute and *value* is the expected value that should match + get_remaining: bool, optional + Whether remaining results should be included (this may repeatedly + query the server for remaining results) Returns ------- @@ -2101,7 +2131,11 @@ def search_for_series( params = self._parse_qido_query_parameters( fuzzymatching, limit, offset, fields, search_filters ) - return self._http_get_application_json(url, params) + return self._http_get_application_json( + url, + params=params, + get_remaining=get_remaining + ) def _get_series( self, @@ -2417,7 +2451,8 @@ def search_for_instances( limit: Optional[int] = None, offset: Optional[int] = None, fields: Optional[Sequence[str]] = None, - search_filters: Optional[Dict[str, Any]] = None + search_filters: Optional[Dict[str, Any]] = None, + get_remaining: bool = False ) -> List[Dict[str, dict]]: """Search for instances. @@ -2439,6 +2474,9 @@ def search_for_instances( Search filter criteria as key-value pairs, where *key* is a keyword or a tag of the attribute and *value* is the expected value that should match + get_remaining: bool, optional + Whether remaining results should be included (this may repeatedly + query the server for remaining results) Returns ------- @@ -2469,7 +2507,11 @@ def search_for_instances( params = self._parse_qido_query_parameters( fuzzymatching, limit, offset, fields, search_filters ) - return self._http_get_application_json(url, params) + return self._http_get_application_json( + url, + params=params, + get_remaining=get_remaining + ) def retrieve_instance( self,