Skip to content

dicomtrolley.mint

Models the MINT DICOM exchange protocol See: https://code.google.com/archive/p/medical-imaging-network-transport/downloads

Mint

Bases: Searcher

A connection to a mint server

Source code in dicomtrolley/mint.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
class Mint(Searcher):
    """A connection to a mint server"""

    def __init__(self, session, url):
        """
        Parameters
        ----------
        session: requests.session
            A logged-in session over which MINT calls can be made
        url: str
            MINT endpoint, including protocol and port. Like https://server:8080/mint
        """

        self.session = session
        self.url = url

    def find_studies(self, query: Query) -> Sequence[MintStudy]:
        """Get all studies matching query

        Parameters
        ----------
        query: Query
            Search based on these parameters. See Query object

        Returns
        -------
        List[MintStudy]
            All studies matching query. Might be empty.
            If Query.QueryLevel is SERIES or INSTANCE, MintStudy objects might
            contain MintSeries and MintInstance instances.
        """

        logger.info(f"Running query {query.to_short_string()}")
        search_url = self.url + "/studies"
        response = self.session.get(
            search_url, params=MintQuery.init_from_query(query).as_parameters()
        )
        return parse_mint_studies_response(response)

__init__(session, url)

Parameters

session: requests.session A logged-in session over which MINT calls can be made url: str MINT endpoint, including protocol and port. Like https://server:8080/mint

Source code in dicomtrolley/mint.py
264
265
266
267
268
269
270
271
272
273
274
275
def __init__(self, session, url):
    """
    Parameters
    ----------
    session: requests.session
        A logged-in session over which MINT calls can be made
    url: str
        MINT endpoint, including protocol and port. Like https://server:8080/mint
    """

    self.session = session
    self.url = url

find_studies(query)

Get all studies matching query

Parameters

query: Query Search based on these parameters. See Query object

Returns

List[MintStudy] All studies matching query. Might be empty. If Query.QueryLevel is SERIES or INSTANCE, MintStudy objects might contain MintSeries and MintInstance instances.

Source code in dicomtrolley/mint.py
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
def find_studies(self, query: Query) -> Sequence[MintStudy]:
    """Get all studies matching query

    Parameters
    ----------
    query: Query
        Search based on these parameters. See Query object

    Returns
    -------
    List[MintStudy]
        All studies matching query. Might be empty.
        If Query.QueryLevel is SERIES or INSTANCE, MintStudy objects might
        contain MintSeries and MintInstance instances.
    """

    logger.info(f"Running query {query.to_short_string()}")
    search_url = self.url + "/studies"
    response = self.session.get(
        search_url, params=MintQuery.init_from_query(query).as_parameters()
    )
    return parse_mint_studies_response(response)

MintAttribute

A DICOM element like PatientID=001

Source code in dicomtrolley/mint.py
146
147
148
149
class MintAttribute:
    """A DICOM element like PatientID=001"""

    xml_element = "{http://medical.nema.org/mint}attr"

MintObject

Bases: DICOMObject

Python representation of MINT xml

Source code in dicomtrolley/mint.py
60
61
62
63
64
65
66
67
68
69
70
71
72
class MintObject(DICOMObject):
    """Python representation of MINT xml"""

    xml_element: ClassVar[str]

    def all_instances(self):
        """
        Returns
        -------
        List[MintInstance]
            All instances contained in this object
        """
        raise NotImplementedError()

all_instances()

Returns

List[MintInstance] All instances contained in this object

Source code in dicomtrolley/mint.py
65
66
67
68
69
70
71
72
def all_instances(self):
    """
    Returns
    -------
    List[MintInstance]
        All instances contained in this object
    """
    raise NotImplementedError()

MintQuery

Bases: ExtendedQuery

Things you can search for with the MINT find DICOM studies function

Notes

  • All string arguments support (*) as a wildcard
Source code in dicomtrolley/mint.py
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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
class MintQuery(ExtendedQuery):
    """Things you can search for with the MINT find DICOM studies function

    Notes
    -----
    * All string arguments support (*) as a wildcard
    """

    limit: int = 0  # how many results to return. 0 = all

    @model_validator(mode="after")
    def min_max_study_date_xor(self):  # noqa: B902, N805
        """Min and max should both be given or both be empty"""
        min_date = self.min_study_date
        max_date = self.max_study_date
        if min_date and not max_date:
            raise ValueError(
                f"min_study_date parameter was passed"
                f"({min_date}), "
                f"but max_study_date was not. Both need to be given"
            )
        elif max_date and not min_date:
            raise ValueError(
                f"max_study_date parameter was passed ({max_date}), "
                f"but min_study_date was not. Both need to be given"
            )
        return self

    @model_validator(mode="after")
    def include_fields_check(self):
        """Include fields should match query level"""
        if isinstance(self, list):
            # Interplay with base Query field_validator for include fields
            return self  # don't check
        else:
            include_fields = self.include_fields
        if not include_fields:
            return self  # May not exist if include_fields is invalid type

        query_level = self.query_level
        if query_level:  # May be None for child classes
            valid_fields = get_valid_fields(query_level=query_level)
            for field in include_fields:
                if field not in valid_fields:
                    raise ValueError(
                        f'"{field}" is not a valid include field for query '
                        f"level {query_level}. Valid fields: {valid_fields}"
                    )
        return self

    def __str__(self):
        return str(self.as_parameters())

    def as_parameters(self):
        """All non-empty query parameters. For use as url parameters"""
        parameters = {x: y for x, y in self.model_dump().items() if y}

        if "min_study_date" in parameters:
            parameters["min_study_date"] = parameters[
                "min_study_date"
            ].strftime("%Y%m%d")

        if "max_study_date" in parameters:
            parameters["max_study_date"] = parameters[
                "max_study_date"
            ].strftime("%Y%m%d")

        if "PatientBirthDate" in parameters:
            parameters["PatientBirthDate"] = parameters[
                "PatientBirthDate"
            ].strftime("%Y%m%d")

        if "query_level" in parameters:
            parameters["QueryLevel"] = MintQueryLevels.translate(
                parameters.pop("query_level")
            )

        if "include_fields" in parameters:
            parameters["IncludeFields"] = ",".join(
                parameters.pop("include_fields")
            )

        return parameters

as_parameters()

All non-empty query parameters. For use as url parameters

Source code in dicomtrolley/mint.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
def as_parameters(self):
    """All non-empty query parameters. For use as url parameters"""
    parameters = {x: y for x, y in self.model_dump().items() if y}

    if "min_study_date" in parameters:
        parameters["min_study_date"] = parameters[
            "min_study_date"
        ].strftime("%Y%m%d")

    if "max_study_date" in parameters:
        parameters["max_study_date"] = parameters[
            "max_study_date"
        ].strftime("%Y%m%d")

    if "PatientBirthDate" in parameters:
        parameters["PatientBirthDate"] = parameters[
            "PatientBirthDate"
        ].strftime("%Y%m%d")

    if "query_level" in parameters:
        parameters["QueryLevel"] = MintQueryLevels.translate(
            parameters.pop("query_level")
        )

    if "include_fields" in parameters:
        parameters["IncludeFields"] = ",".join(
            parameters.pop("include_fields")
        )

    return parameters

include_fields_check()

Include fields should match query level

Source code in dicomtrolley/mint.py
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
@model_validator(mode="after")
def include_fields_check(self):
    """Include fields should match query level"""
    if isinstance(self, list):
        # Interplay with base Query field_validator for include fields
        return self  # don't check
    else:
        include_fields = self.include_fields
    if not include_fields:
        return self  # May not exist if include_fields is invalid type

    query_level = self.query_level
    if query_level:  # May be None for child classes
        valid_fields = get_valid_fields(query_level=query_level)
        for field in include_fields:
            if field not in valid_fields:
                raise ValueError(
                    f'"{field}" is not a valid include field for query '
                    f"level {query_level}. Valid fields: {valid_fields}"
                )
    return self

min_max_study_date_xor()

Min and max should both be given or both be empty

Source code in dicomtrolley/mint.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
@model_validator(mode="after")
def min_max_study_date_xor(self):  # noqa: B902, N805
    """Min and max should both be given or both be empty"""
    min_date = self.min_study_date
    max_date = self.max_study_date
    if min_date and not max_date:
        raise ValueError(
            f"min_study_date parameter was passed"
            f"({min_date}), "
            f"but max_study_date was not. Both need to be given"
        )
    elif max_date and not min_date:
        raise ValueError(
            f"max_study_date parameter was passed ({max_date}), "
            f"but min_study_date was not. Both need to be given"
        )
    return self

MintQueryLevels

Source code in dicomtrolley/mint.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class MintQueryLevels:
    STUDY = "STUDY"
    SERIES = "SERIES"
    INSTANCE = "INSTANCE"

    ALL = {STUDY, SERIES, INSTANCE}

    @classmethod
    def translate(cls, value):
        """Translate from Query. For converting between queries

        Notes
        -----
        For MINT the query level values are identical to the generic query levels.
        Defining translation here anyway to be able to change generic values without
        side effects
        """
        translation = {
            QueryLevels.STUDY: cls.STUDY,
            QueryLevels.SERIES: cls.SERIES,
            QueryLevels.INSTANCE: cls.INSTANCE,
        }
        return translation[value]

translate(value) classmethod

Translate from Query. For converting between queries

Notes

For MINT the query level values are identical to the generic query levels. Defining translation here anyway to be able to change generic values without side effects

Source code in dicomtrolley/mint.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@classmethod
def translate(cls, value):
    """Translate from Query. For converting between queries

    Notes
    -----
    For MINT the query level values are identical to the generic query levels.
    Defining translation here anyway to be able to change generic values without
    side effects
    """
    translation = {
        QueryLevels.STUDY: cls.STUDY,
        QueryLevels.SERIES: cls.SERIES,
        QueryLevels.INSTANCE: cls.INSTANCE,
    }
    return translation[value]

MintStudy

Bases: Study, MintObject

Source code in dicomtrolley/mint.py
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
class MintStudy(Study, MintObject):
    xml_element: ClassVar = "{http://medical.nema.org/mint}study"

    mint_uuid: str  # non-DICOM MINT-only id for this series
    last_modified: str
    series: List[MintSeries]

    @classmethod
    def init_from_element(cls, element):
        data = parse_attribs(element)

        study = cls(
            data=data,
            uid=data.StudyInstanceUID,
            mint_uuid=element.attrib["studyUUID"],
            last_modified=element.attrib["lastModified"],
            series=[],
        )
        for x in element.findall(MintSeries.xml_element):
            study.series.append(MintSeries.init_from_element(x, parent=study))

        return study

    def dump_content(self) -> str:
        """Dump entire contents of this study and containing series, instances

        For quick inspection in scripts
        """

        output = [str(self.data)]
        for series in self.series:
            output.append(str(series.data))
            for instance in series.instances:
                output.append(str(instance.data))
        return "\n".join(output)

dump_content()

Dump entire contents of this study and containing series, instances

For quick inspection in scripts

Source code in dicomtrolley/mint.py
132
133
134
135
136
137
138
139
140
141
142
143
def dump_content(self) -> str:
    """Dump entire contents of this study and containing series, instances

    For quick inspection in scripts
    """

    output = [str(self.data)]
    for series in self.series:
        output.append(str(series.data))
        for instance in series.instances:
            output.append(str(instance.data))
    return "\n".join(output)

get_valid_fields(query_level)

All fields that can be returned at the given MINT query level

Source code in dicomtrolley/mint.py
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
def get_valid_fields(query_level) -> Set[str]:
    """All fields that can be returned at the given MINT query level"""
    if query_level == MintQueryLevels.INSTANCE:
        return (
            StudyLevel.fields
            | SeriesLevel.fields
            | SeriesLevelPromotable.fields
            | InstanceLevel.fields
        )
    elif query_level == MintQueryLevels.SERIES:
        return (
            StudyLevel.fields
            | SeriesLevel.fields
            | SeriesLevelPromotable.fields
        )
    elif query_level == MintQueryLevels.STUDY:
        return StudyLevel.fields
    else:
        raise ValueError(
            f'Unknown query level "{query_level}". Valid values '
            f"are {MintQueryLevels.ALL}"
        )

parse_attribs(element)

Parse xml attributes from a MINT find call to DICOM elements in a Dataset

Source code in dicomtrolley/mint.py
324
325
326
327
328
329
330
331
332
def parse_attribs(element):
    """Parse xml attributes from a MINT find call to DICOM elements in a Dataset"""
    dataset = Dataset()
    for child in element.findall(MintAttribute.xml_element):
        attr = child.attrib
        val = attr.get("val", "")  # treat missing val as empty
        dataset[attr["tag"]] = DataElement(attr["tag"], attr["vr"], val)

    return dataset

parse_mint_studies_response(response)

Parse the xml response to a MINT find DICOM studies call

Raises

DICOMTrolleyError If parsing fails

Source code in dicomtrolley/mint.py
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
def parse_mint_studies_response(response) -> List[MintStudy]:
    """Parse the xml response to a MINT find DICOM studies call

    Raises
    ------
    DICOMTrolleyError
        If parsing fails
    """
    xml_raw = response.text
    try:
        studies = ElementTree.fromstring(xml_raw).findall(
            MintStudy.xml_element
        )
    except ParseError as e:
        raise DICOMTrolleyError(
            f"Could not parse server response from {response.url} "
            f"({response.status_code}:{response.reason}) as MINT "
            f"studies. Response text was: {xml_raw}"
        ) from e
    logger.info(f"parsed {len(studies)} studies from mint response")
    return [MintStudy.init_from_element(x) for x in studies]