Skip to content

Commit cadc8c7

Browse files
committed
Merge branch '#49_fix_suid_only_query_handling'
2 parents 075b529 + 4acd90d commit cadc8c7

File tree

3 files changed

+97
-12
lines changed

3 files changed

+97
-12
lines changed

dicomtrolley/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,7 @@ def init_from_query(
556556
f"parameters. You are probably converting between incompatible "
557557
f"query subtypes. Use a basic Query instance to avoid this error. "
558558
f"See ValidationError for details on which parameter is causing "
559-
f"the problem"
559+
f"the problem. Original error: {str(e)}"
560560
) from e
561561

562562
@staticmethod

dicomtrolley/qido_rs.py

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -122,17 +122,14 @@ def uri_search_params(self) -> Dict[str, Union[str, List[str]]]:
122122
if self.include_fields:
123123
search_params["includefield"] = self.include_fields
124124

125-
# now collect all other Query() fields that can be search params
125+
# now collect all other Query() fields that can be search params.
126+
# Exclude fields that should not be sent to server
126127
exclude_fields = {
127128
"min_study_date", # addressed above
128129
"max_study_date",
129-
"include_fields",
130-
"StudyInstanceUID", # potential part of url, not search params
131-
"SeriesInstanceUID",
132-
"SOPClassInstanceUID",
133-
"query_level",
130+
"include_fields", # addressed above
131+
"query_level", # encoded in url structure
134132
}
135-
136133
other_search_params = {
137134
key: val
138135
for key, val in self.dict().items()
@@ -235,6 +232,44 @@ def uri_base(self) -> str:
235232
f'Should be one of "{QueryLevels}"'
236233
)
237234

235+
def uri_search_params(self) -> Dict[str, Union[str, List[str]]]:
236+
"""The search parameter part of the URI as defined in
237+
DICOM PS3.18 section 10.6 table 10.6.1-2
238+
239+
Returns
240+
-------
241+
Dict[str, Union[str, List[str]]]
242+
Output that can be fed directly into a requests post request.
243+
format is param_name:param_value. If param_value is a list, the param
244+
will be included multiple times.
245+
See https://docs.python-requests.org/en/latest/user/quickstart/
246+
#passing-parameters-in-urls
247+
248+
Notes
249+
-----
250+
Will not output any parameters with Null or empty value (bool(value)==False).
251+
This does not affect query functionality but makes output cleaner in strings
252+
"""
253+
search_params: Dict[
254+
str, Union[str, List[str]]
255+
] = super().uri_search_params()
256+
257+
# Depending on query level, some UIDs in Hierarchical queries are part
258+
# of url and should not be part of parameter
259+
exclude_fields = set()
260+
if self.query_level == QueryLevels.INSTANCE:
261+
exclude_fields = {"StudyInstanceUID", "SeriesInstanceUID"}
262+
if self.query_level == QueryLevels.SERIES:
263+
exclude_fields = {"StudyInstanceUID"}
264+
if self.query_level == QueryLevels.STUDY:
265+
pass # for series level all uids are part of parameters. Don't exclude
266+
267+
return {
268+
key: val
269+
for key, val in search_params.items()
270+
if key not in exclude_fields
271+
}
272+
238273

239274
# used in RelationalQuery
240275
STUDY_VALUE_ERROR_TEXT: str = (
@@ -287,7 +322,7 @@ def uri_base(self) -> str:
287322
if self.query_level == QueryLevels.STUDY:
288323
raise ValueError(STUDY_VALUE_ERROR_TEXT)
289324
elif self.query_level == QueryLevels.SERIES:
290-
return "/studies"
325+
return "/series"
291326
elif self.query_level == QueryLevels.INSTANCE:
292327
if self.StudyInstanceUID:
293328
# all instances for this study
@@ -364,10 +399,11 @@ def ensure_query_type(cls, query: Query) -> QidoRSQueryBase:
364399
"to HierarchicalQuery"
365400
)
366401
return HierarchicalQuery.init_from_query(query)
367-
except UnSupportedParameterError:
402+
except UnSupportedParameterError as e:
368403
logger.debug(
369404
"Converting to HierarchicalQuery did not work. "
370-
"Trying slower but less stringent RelationalQuery"
405+
"Trying slower but less stringent RelationalQuery. Error was: "
406+
f"{str(e)}"
371407
)
372408
return RelationalQuery.init_from_query(query)
373409
else:

tests/test_qido_rs.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from dicomtrolley.core import QueryLevels
5+
from dicomtrolley.core import Query, QueryLevels
66
from dicomtrolley.qido_rs import HierarchicalQuery, QidoRS, RelationalQuery
77
from tests.conftest import set_mock_response
88
from tests.mock_responses import (
@@ -60,6 +60,47 @@ def test_hierarchical_query_uris():
6060
assert url == "/studies/123/series"
6161

6262

63+
@pytest.mark.parametrize(
64+
"query,expected_url",
65+
[
66+
# For this basic study-level query suid should be a parameter
67+
(
68+
HierarchicalQuery(
69+
StudyInstanceUID="123", query_level=QueryLevels.STUDY
70+
),
71+
"/qido/studies?StudyInstanceUID=123",
72+
),
73+
# But for series-level query suid becomes part of the path, not parameter
74+
(
75+
HierarchicalQuery(
76+
StudyInstanceUID="123", query_level=QueryLevels.SERIES
77+
),
78+
"/qido/studies/123/series",
79+
),
80+
],
81+
)
82+
def test_hierarchical_query_uri_suid_only(
83+
requests_mock, a_qido, query, expected_url
84+
):
85+
"""Exposes issue #49 - Query containing SeriesInstanceUID only is not
86+
translated properly. This test check more broadly how hierarchical queries
87+
generate they urls to make sure this is sane.
88+
"""
89+
# Return a valid response to avoid errors. We're only interested in the called url
90+
set_mock_response(requests_mock, QIDO_RS_STUDY_LEVEL)
91+
92+
# Simple query for a single StudyInstanceUID
93+
a_qido.find_studies(query)
94+
95+
# This should translate to a qido url call including the StudyInstanceUID
96+
hist = requests_mock.request_history
97+
assert len(hist) == 1 # sanity check
98+
called = hist[0].path
99+
if hist[0].query:
100+
called = called + "?" + hist[0].query
101+
assert called.lower() == expected_url.lower()
102+
103+
63104
@pytest.mark.parametrize(
64105
"query_params",
65106
[
@@ -117,3 +158,11 @@ def test_qido_searcher_204(requests_mock, a_qido):
117158
set_mock_response(requests_mock, QIDO_RS_204_NO_RESULTS)
118159
result = a_qido.find_studies(HierarchicalQuery())
119160
assert len(result) == 0
161+
162+
163+
def test_ensure_query_type(a_qido):
164+
"""Exposes bug where Series level Relational query calles a study url"""
165+
ensured = a_qido.ensure_query_type(
166+
Query(AccessionNumber="123", query_level=QueryLevels.SERIES)
167+
)
168+
assert ensured.uri_base() == "/series"

0 commit comments

Comments
 (0)