Skip to content

dicomtrolley.wado_uri

Models the WADO-uri protocol

[DICOM part18 chapter 9] (https://dicom.nema.org/medical/dicom/current/output/chtml/part18/chapter_9.html)

WadoURI

Bases: Downloader

A connection to a WADO-URI server

Source code in dicomtrolley/wado_uri.py
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
class WadoURI(Downloader):
    """A connection to a WADO-URI server"""

    def __init__(self, session, url, use_async=False, max_workers=None):
        """
        Parameters
        ----------
        session: requests.session
            A logged in session over which WADO calls can be made
        url: str
            WADO-URI endpoint, including protocol and port. Like
            https://server:8080/wado
        use_async: bool, optional
            If True, download will split instances into chunks and download each
            chunk in a separate thread. If False, use single thread Defaults to False
        max_workers, Optional[int]
            Only used of use_async=True. Number of workers to use for multi-threading.
            Defaults to None, meaning ThreadPoolExecutor default number is used.
        """

        self.session = session
        self.url = url
        self.use_async = use_async
        self.max_workers = max_workers

    @staticmethod
    def to_wado_parameters(instance):
        """WADO url parameters for to retrieve instance

        Returns
        -------
        Dict[str]
            All parameters for a standard WADO get request
        """
        return {
            "requestType": "WADO",
            "studyUID": instance.study_uid,
            "seriesUID": instance.series_uid,
            "objectUID": instance.instance_uid,
            "contentType": "application/dicom",
        }

    @staticmethod
    def parse_wado_response(response: Response) -> Dataset:
        """Create a Dataset out of http response from WADO server

        Raises
        ------
        DICOMTrolleyError
            If response is not as expected or if parsing fails

        Returns
        -------
        Dataset
        """
        if response.status_code != 200:

            raise DICOMTrolleyError(
                f"Calling {response.url} failed ({response.status_code} - "
                f"{response.reason})\n"
                f"response content was {str(response.content[:300])}"
            )
        raw = DicomBytesIO(response.content)
        try:
            return dcmread(raw)
        except InvalidDicomError as e:
            raise DICOMTrolleyError(
                f"Error parsing response as dicom: {e}."
                f" Response content (first 300 elements) was"
                f" {str(response.content[:300])}"
            ) from e

    def get_dataset(self, instance: InstanceReference):
        """Get DICOM dataset for the given instance (slice)

        Raises
        ------
        DICOMTrolleyError
            If getting does not work for some reason

        Returns
        -------
        Dataset
            A pydicom dataset
        """
        return self.parse_wado_response(
            self.session.get(
                self.url, params=self.to_wado_parameters(instance)
            )
        )

    def datasets(self, objects: Sequence[DICOMDownloadable]):
        """Retrieve each instance in objects

        Returns
        -------
        Iterator[Dataset, None, None]

        Raises
        ------
        NonInstanceParameterError
            If objects contain non-instance targets like a StudyInstanceUID.
            wado_uri can only download instances

        """
        if self.use_async:
            yield from self.datasets_async(
                objects, max_workers=self.max_workers
            )
        else:
            yield from self.datasets_single_thread(objects)

    def datasets_single_thread(self, objects: Sequence[DICOMDownloadable]):
        """Retrieve each instance in objects

        Returns
        -------
        Iterator[Dataset, None, None]

        Raises
        ------
        NonInstanceParameterError
            If objects contain non-instance targets like a StudyInstanceUID.
            wado_uri can only download instances

        """
        instances = to_instance_refs(objects)  # raise exception if needed
        for instance in instances:
            yield self.get_dataset(instance)

    def datasets_async(
        self, objects: Sequence[DICOMDownloadable], max_workers=None
    ):
        """Retrieve each instance via WADO

        Parameters
        ----------
        objects: Sequence[DICOMDownloadable]
            Retrieve dataset for each of these instances
        max_workers: int, optional
            Use this number of workers in ThreadPoolExecutor. Defaults to
            default for ThreadPoolExecutor

        Raises
        ------
        DICOMTrolleyError
            When a server response cannot be parsed as DICOM
        NonInstanceParameterError
            If objects contain non-instance targets like a StudyInstanceUID and
            download can only process Instance targets. See Exception docstring
            for rationale

        Returns
        -------
        Iterator[Dataset, None, None]
        """

        with FuturesSession(
            session=self.session,
            executor=ThreadPoolExecutor(max_workers=max_workers),
        ) as futures_session:
            futures = []
            for instance in objects:
                futures.append(
                    futures_session.get(
                        self.url, params=self.to_wado_parameters(instance)
                    )
                )

            for future in as_completed(futures):
                yield self.parse_wado_response(future.result())

__init__(session, url, use_async=False, max_workers=None)

Parameters

session: requests.session A logged in session over which WADO calls can be made url: str WADO-URI endpoint, including protocol and port. Like https://server:8080/wado use_async: bool, optional If True, download will split instances into chunks and download each chunk in a separate thread. If False, use single thread Defaults to False max_workers, Optional[int] Only used of use_async=True. Number of workers to use for multi-threading. Defaults to None, meaning ThreadPoolExecutor default number is used.

Source code in dicomtrolley/wado_uri.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def __init__(self, session, url, use_async=False, max_workers=None):
    """
    Parameters
    ----------
    session: requests.session
        A logged in session over which WADO calls can be made
    url: str
        WADO-URI endpoint, including protocol and port. Like
        https://server:8080/wado
    use_async: bool, optional
        If True, download will split instances into chunks and download each
        chunk in a separate thread. If False, use single thread Defaults to False
    max_workers, Optional[int]
        Only used of use_async=True. Number of workers to use for multi-threading.
        Defaults to None, meaning ThreadPoolExecutor default number is used.
    """

    self.session = session
    self.url = url
    self.use_async = use_async
    self.max_workers = max_workers

datasets(objects)

Retrieve each instance in objects

Returns

Iterator[Dataset, None, None]

Raises

NonInstanceParameterError If objects contain non-instance targets like a StudyInstanceUID. wado_uri can only download instances

Source code in dicomtrolley/wado_uri.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def datasets(self, objects: Sequence[DICOMDownloadable]):
    """Retrieve each instance in objects

    Returns
    -------
    Iterator[Dataset, None, None]

    Raises
    ------
    NonInstanceParameterError
        If objects contain non-instance targets like a StudyInstanceUID.
        wado_uri can only download instances

    """
    if self.use_async:
        yield from self.datasets_async(
            objects, max_workers=self.max_workers
        )
    else:
        yield from self.datasets_single_thread(objects)

datasets_async(objects, max_workers=None)

Retrieve each instance via WADO

Parameters

objects: Sequence[DICOMDownloadable] Retrieve dataset for each of these instances max_workers: int, optional Use this number of workers in ThreadPoolExecutor. Defaults to default for ThreadPoolExecutor

Raises

DICOMTrolleyError When a server response cannot be parsed as DICOM NonInstanceParameterError If objects contain non-instance targets like a StudyInstanceUID and download can only process Instance targets. See Exception docstring for rationale

Returns

Iterator[Dataset, None, None]

Source code in dicomtrolley/wado_uri.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def datasets_async(
    self, objects: Sequence[DICOMDownloadable], max_workers=None
):
    """Retrieve each instance via WADO

    Parameters
    ----------
    objects: Sequence[DICOMDownloadable]
        Retrieve dataset for each of these instances
    max_workers: int, optional
        Use this number of workers in ThreadPoolExecutor. Defaults to
        default for ThreadPoolExecutor

    Raises
    ------
    DICOMTrolleyError
        When a server response cannot be parsed as DICOM
    NonInstanceParameterError
        If objects contain non-instance targets like a StudyInstanceUID and
        download can only process Instance targets. See Exception docstring
        for rationale

    Returns
    -------
    Iterator[Dataset, None, None]
    """

    with FuturesSession(
        session=self.session,
        executor=ThreadPoolExecutor(max_workers=max_workers),
    ) as futures_session:
        futures = []
        for instance in objects:
            futures.append(
                futures_session.get(
                    self.url, params=self.to_wado_parameters(instance)
                )
            )

        for future in as_completed(futures):
            yield self.parse_wado_response(future.result())

datasets_single_thread(objects)

Retrieve each instance in objects

Returns

Iterator[Dataset, None, None]

Raises

NonInstanceParameterError If objects contain non-instance targets like a StudyInstanceUID. wado_uri can only download instances

Source code in dicomtrolley/wado_uri.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def datasets_single_thread(self, objects: Sequence[DICOMDownloadable]):
    """Retrieve each instance in objects

    Returns
    -------
    Iterator[Dataset, None, None]

    Raises
    ------
    NonInstanceParameterError
        If objects contain non-instance targets like a StudyInstanceUID.
        wado_uri can only download instances

    """
    instances = to_instance_refs(objects)  # raise exception if needed
    for instance in instances:
        yield self.get_dataset(instance)

get_dataset(instance)

Get DICOM dataset for the given instance (slice)

Raises

DICOMTrolleyError If getting does not work for some reason

Returns

Dataset A pydicom dataset

Source code in dicomtrolley/wado_uri.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def get_dataset(self, instance: InstanceReference):
    """Get DICOM dataset for the given instance (slice)

    Raises
    ------
    DICOMTrolleyError
        If getting does not work for some reason

    Returns
    -------
    Dataset
        A pydicom dataset
    """
    return self.parse_wado_response(
        self.session.get(
            self.url, params=self.to_wado_parameters(instance)
        )
    )

parse_wado_response(response) staticmethod

Create a Dataset out of http response from WADO server

Raises

DICOMTrolleyError If response is not as expected or if parsing fails

Returns

Dataset

Source code in dicomtrolley/wado_uri.py
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
@staticmethod
def parse_wado_response(response: Response) -> Dataset:
    """Create a Dataset out of http response from WADO server

    Raises
    ------
    DICOMTrolleyError
        If response is not as expected or if parsing fails

    Returns
    -------
    Dataset
    """
    if response.status_code != 200:

        raise DICOMTrolleyError(
            f"Calling {response.url} failed ({response.status_code} - "
            f"{response.reason})\n"
            f"response content was {str(response.content[:300])}"
        )
    raw = DicomBytesIO(response.content)
    try:
        return dcmread(raw)
    except InvalidDicomError as e:
        raise DICOMTrolleyError(
            f"Error parsing response as dicom: {e}."
            f" Response content (first 300 elements) was"
            f" {str(response.content[:300])}"
        ) from e

to_wado_parameters(instance) staticmethod

WADO url parameters for to retrieve instance

Returns

Dict[str] All parameters for a standard WADO get request

Source code in dicomtrolley/wado_uri.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@staticmethod
def to_wado_parameters(instance):
    """WADO url parameters for to retrieve instance

    Returns
    -------
    Dict[str]
        All parameters for a standard WADO get request
    """
    return {
        "requestType": "WADO",
        "studyUID": instance.study_uid,
        "seriesUID": instance.series_uid,
        "objectUID": instance.instance_uid,
        "contentType": "application/dicom",
    }