Skip to content

Commit a1c60a6

Browse files
feat: use uri's _validate_uid method in web. Skip uid lenght validation in permissive mode.
1 parent c1732d1 commit a1c60a6

File tree

4 files changed

+38
-51
lines changed

4 files changed

+38
-51
lines changed

src/dicomweb_client/uri.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -629,12 +629,13 @@ def _validate_resource_identifiers_and_suffix(
629629

630630
def _validate_uid(uid: str, permissive: bool) -> None:
631631
"""Validates a DICOM UID."""
632-
if len(uid) > _MAX_UID_LENGTH:
633-
raise ValueError('UID cannot have more than 64 chars. '
634-
f'Actual count in {uid!r}: {len(uid)}')
635-
if not permissive and _REGEX_UID.fullmatch(uid) is None:
636-
raise ValueError(f'UID {uid!r} must match regex {_REGEX_UID!r} in '
637-
'conformance with the DICOM Standard.')
632+
if not permissive:
633+
if len(uid) > _MAX_UID_LENGTH:
634+
raise ValueError('UID cannot have more than 64 chars. '
635+
f'Actual count in {uid!r}: {len(uid)}')
636+
if _REGEX_UID.fullmatch(uid) is None:
637+
raise ValueError(f'UID {uid!r} must match regex {_REGEX_UID!r} in '
638+
'conformance with the DICOM Standard.')
638639
elif permissive and _REGEX_PERMISSIVE_UID.fullmatch(uid) is None:
639640
raise ValueError(f'Permissive mode is enabled. UID {uid!r} must match '
640641
f'regex {_REGEX_PERMISSIVE_UID!r}.')

src/dicomweb_client/web.py

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,11 @@
3232
import requests
3333
import retrying
3434

35-
from dicomweb_client.uri import build_query_string, parse_query_parameters
35+
from dicomweb_client.uri import build_query_string, parse_query_parameters, _validate_uid
3636

3737

3838
logger = logging.getLogger(__name__)
3939

40-
# For DICOM Standard spec validation of UID components in `DICOMwebClient`.
41-
_REGEX_UID = re.compile(r'[0-9]+([.][0-9]+)*')
42-
_REGEX_PERMISSIVE_UID = re.compile(r'[^/@]+')
43-
4440

4541
def _load_xml_dataset(dataset: Element) -> pydicom.dataset.Dataset:
4642
"""Load DICOM Data Set in DICOM XML format.
@@ -205,7 +201,7 @@ def __init__(
205201
headers: Optional[Dict[str, str]] = None,
206202
callback: Optional[Callable] = None,
207203
chunk_size: int = 10**6,
208-
permissive: bool = False
204+
permissive_uid: bool = False
209205
) -> None:
210206
"""Instatiate client.
211207
@@ -327,7 +323,7 @@ def __init__(
327323
if callback is not None:
328324
self._session.hooks = {'response': [callback, ]}
329325
self._chunk_size = chunk_size
330-
self._permissive = permissive
326+
self._permissive_uid = permissive_uid
331327
self.set_http_retry_params()
332328

333329
def _get_transaction_url(self, transaction: _Transaction) -> str:
@@ -2177,31 +2173,6 @@ def delete_study(
21772173
url += f'?{additional_params_query_string}'
21782174
self._http_delete(url)
21792175

2180-
def _assert_uid_format(self, uid: str) -> None:
2181-
"""Check whether a DICOM UID has the correct format.
2182-
2183-
Parameters
2184-
----------
2185-
uid: str
2186-
DICOM UID
2187-
2188-
Raises
2189-
------
2190-
TypeError
2191-
When `uid` is not a string
2192-
ValueError
2193-
When `uid` doesn't match the regular expression pattern for
2194-
DICOM UIDs (strict or permissive regex)
2195-
2196-
"""
2197-
if not isinstance(uid, str):
2198-
raise TypeError('DICOM UID must be a string.')
2199-
if not self._permissive and _REGEX_UID.fullmatch(uid) is None:
2200-
raise ValueError(f'UID {uid!r} must match regex {_REGEX_UID!r} in '
2201-
'conformance with the DICOM Standard.')
2202-
elif self._permissive and _REGEX_PERMISSIVE_UID.fullmatch(uid) is None:
2203-
raise ValueError(f'Permissive mode is enabled. UID {uid!r} must match '
2204-
f'regex {_REGEX_PERMISSIVE_UID!r}.')
22052176

22062177
def search_for_series(
22072178
self,
@@ -2255,7 +2226,7 @@ def search_for_series(
22552226
22562227
""" # noqa: E501
22572228
if study_instance_uid is not None:
2258-
self._assert_uid_format(study_instance_uid)
2229+
_validate_uid(study_instance_uid, self._permissive_uid)
22592230
logger.info(f'search for series of study "{study_instance_uid}"')
22602231
else:
22612232
logger.info('search for series')
@@ -2314,12 +2285,12 @@ def _get_series(
23142285
f'retrieve series "{series_instance_uid}" '
23152286
f'of study "{study_instance_uid}"'
23162287
)
2317-
self._assert_uid_format(study_instance_uid)
2288+
_validate_uid(study_instance_uid, self._permissive_uid)
23182289
if series_instance_uid is None:
23192290
raise ValueError(
23202291
'Series Instance UID is required for retrieval of series.'
23212292
)
2322-
self._assert_uid_format(series_instance_uid)
2293+
_validate_uid(series_instance_uid, self._permissive_uid)
23232294
url = self._get_series_url(
23242295
_Transaction.RETRIEVE,
23252296
study_instance_uid,
@@ -2474,7 +2445,7 @@ def retrieve_series_metadata(
24742445
'Study Instance UID is required for retrieval of '
24752446
'series metadata.'
24762447
)
2477-
self._assert_uid_format(study_instance_uid)
2448+
_validate_uid(study_instance_uid, self._permissive_uid)
24782449
if series_instance_uid is None:
24792450
raise ValueError(
24802451
'Series Instance UID is required for retrieval of '
@@ -2484,7 +2455,7 @@ def retrieve_series_metadata(
24842455
f'retrieve metadata of series "{series_instance_uid}" '
24852456
f'of study "{study_instance_uid}"'
24862457
)
2487-
self._assert_uid_format(series_instance_uid)
2458+
_validate_uid(series_instance_uid, self._permissive_uid)
24882459
url = self._get_series_url(
24892460
_Transaction.RETRIEVE,
24902461
study_instance_uid,
@@ -2677,7 +2648,7 @@ def search_for_instances(
26772648
if series_instance_uid is not None:
26782649
message += f' of series "{series_instance_uid}"'
26792650
if study_instance_uid is not None:
2680-
self._assert_uid_format(study_instance_uid)
2651+
_validate_uid(study_instance_uid, self._permissive_uid)
26812652
message += f' of study "{study_instance_uid}"'
26822653
logger.info(message)
26832654
url = self._get_instances_url(
@@ -2744,12 +2715,12 @@ def retrieve_instance(
27442715
raise ValueError(
27452716
'Study Instance UID is required for retrieval of instance.'
27462717
)
2747-
self._assert_uid_format(study_instance_uid)
2718+
_validate_uid(study_instance_uid, self._permissive_uid)
27482719
if series_instance_uid is None:
27492720
raise ValueError(
27502721
'Series Instance UID is required for retrieval of instance.'
27512722
)
2752-
self._assert_uid_format(series_instance_uid)
2723+
_validate_uid(series_instance_uid, self._permissive_uid)
27532724
if sop_instance_uid is None:
27542725
raise ValueError(
27552726
'SOP Instance UID is required for retrieval of instance.'
@@ -2759,7 +2730,7 @@ def retrieve_instance(
27592730
f'of series "{series_instance_uid}" '
27602731
f'of study "{study_instance_uid}"'
27612732
)
2762-
self._assert_uid_format(sop_instance_uid)
2733+
_validate_uid(sop_instance_uid, self._permissive_uid)
27632734
url = self._get_instances_url(
27642735
_Transaction.RETRIEVE,
27652736
study_instance_uid,

tests/test_uri.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,21 @@ def test_uid_permissive_invalid(uid):
7070
URI(_BASE_URL, uid, permissive=True)
7171

7272

73+
def test_uid_length_permissive():
74+
"""Tests that UIDs longer than 64 chars are allowed in permissive mode."""
75+
# Create a UID with 65 characters
76+
uid_65 = '1' * 65
77+
78+
# Should fail in strict mode
79+
with pytest.raises(ValueError, match='UID cannot have more than 64 chars'):
80+
URI(_BASE_URL, uid_65, permissive=False)
81+
82+
# Should succeed in permissive mode
83+
URI(_BASE_URL, uid_65, permissive=True)
84+
URI(_BASE_URL, '1.2.3', uid_65, permissive=True)
85+
URI(_BASE_URL, '1.2.3', '4.5.6', uid_65, permissive=True)
86+
87+
7388
def test_uid_missing_error():
7489
"""Checks *ValueError* is raised when an expected UID is missing."""
7590
with pytest.raises(ValueError, match='`study_instance_uid` missing with'):

tests/test_web.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1614,16 +1614,16 @@ def test_load_xml_response(httpserver, client, cache_dir):
16141614
'1.2/3.4', '[email protected]', '1.2.a.4', '1.2.A.4', '.23.4', '1.2..4', '1.2.', '.'
16151615
])
16161616
def test_uid_strict_validation_fail(httpserver, invalid_uid):
1617-
client = DICOMwebClient(httpserver.url, permissive=False)
1617+
client = DICOMwebClient(httpserver.url, permissive_uid=False)
16181618
with pytest.raises(ValueError,
16191619
match=f'UID {invalid_uid!r} must match regex'):
16201620
client.search_for_series(study_instance_uid=invalid_uid)
16211621

16221622

16231623
@pytest.mark.parametrize('uid', ['13-abc', 'hello', 'is it', "you're me"])
16241624
def test_uid_permissive_validation_pass(httpserver, uid):
1625-
client_strict = DICOMwebClient(httpserver.url, permissive=False)
1626-
client_permissive = DICOMwebClient(httpserver.url, permissive=True)
1625+
client_strict = DICOMwebClient(httpserver.url, permissive_uid=False)
1626+
client_permissive = DICOMwebClient(httpserver.url, permissive_uid=True)
16271627

16281628
# Should fail in strict mode
16291629
with pytest.raises(ValueError, match=f'UID {uid!r} must match regex'):
@@ -1636,7 +1636,7 @@ def test_uid_permissive_validation_pass(httpserver, uid):
16361636

16371637
@pytest.mark.parametrize('uid', ['1.23.5@', '1/23.4'])
16381638
def test_uid_permissive_validation_fail(httpserver, uid):
1639-
client = DICOMwebClient(httpserver.url, permissive=True)
1639+
client = DICOMwebClient(httpserver.url, permissive_uid=True)
16401640
with pytest.raises(ValueError,
16411641
match=f'UID {uid!r} must match regex'):
16421642
client.search_for_series(study_instance_uid=uid)

0 commit comments

Comments
 (0)