From 412395f57a321ea3b67f74123f933cdc8f83f151 Mon Sep 17 00:00:00 2001 From: James Butler Date: Wed, 12 Nov 2025 21:04:08 -0500 Subject: [PATCH 1/5] ENH: Replace pydicom deprecated usage of get_frame_offsets --- src/dicomweb_client/file.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/dicomweb_client/file.py b/src/dicomweb_client/file.py index b1a421d..d03d6b2 100644 --- a/src/dicomweb_client/file.py +++ b/src/dicomweb_client/file.py @@ -32,7 +32,15 @@ from pydicom.datadict import dictionary_VR, keyword_for_tag, tag_for_keyword from pydicom.dataelem import DataElement from pydicom.dataset import Dataset, FileMetaDataset -from pydicom.encaps import encapsulate, get_frame_offsets +from pydicom.encaps import encapsulate +try: + # pydicom >= 3.0 - parse_basic_offsets replaces get_frame_offsets + from pydicom.encaps import parse_basic_offsets + _use_parse_basic_offsets = True +except ImportError: + # pydicom < 3.0 - use deprecated get_frame_offsets + from pydicom.encaps import get_frame_offsets + _use_parse_basic_offsets = False from pydicom.errors import InvalidDicomError from pydicom.filebase import DicomFileLike from pydicom.filereader import data_element_offset_to_value, dcmread @@ -350,7 +358,13 @@ def _read_bot(fp: DicomFileLike) -> np.ndarray: fp.is_implicit_VR, 'OB' ) fp.seek(pixel_data_element_value_offset - 4, 1) - is_empty, offsets = get_frame_offsets(fp) + + # Use parse_basic_offsets for pydicom >= 3.0, get_frame_offsets for < 3.0 + if _use_parse_basic_offsets: + offsets = parse_basic_offsets(fp) + else: + is_empty, offsets = get_frame_offsets(fp) + return np.array(offsets, dtype=np.uint32) From 0fb00308d0fd271159d2a88ab7fc8b11eadcbe69 Mon Sep 17 00:00:00 2001 From: James Butler Date: Thu, 13 Nov 2025 10:48:06 -0500 Subject: [PATCH 2/5] ENH: Support compatibility with pydicom 3 and newer --- pyproject.toml | 2 +- src/dicomweb_client/file.py | 18 ++---------------- tests/test_web.py | 16 ++++++++-------- 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dcabe36..9237700 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "requests>=2.18", "retrying>=1.3.3", "Pillow>=8.3", - "pydicom>=2.2", + "pydicom>=3.0.0", "typing-extensions>=4.0; python_version < '3.8.0'", ] diff --git a/src/dicomweb_client/file.py b/src/dicomweb_client/file.py index d03d6b2..854d703 100644 --- a/src/dicomweb_client/file.py +++ b/src/dicomweb_client/file.py @@ -32,15 +32,7 @@ from pydicom.datadict import dictionary_VR, keyword_for_tag, tag_for_keyword from pydicom.dataelem import DataElement from pydicom.dataset import Dataset, FileMetaDataset -from pydicom.encaps import encapsulate -try: - # pydicom >= 3.0 - parse_basic_offsets replaces get_frame_offsets - from pydicom.encaps import parse_basic_offsets - _use_parse_basic_offsets = True -except ImportError: - # pydicom < 3.0 - use deprecated get_frame_offsets - from pydicom.encaps import get_frame_offsets - _use_parse_basic_offsets = False +from pydicom.encaps import encapsulate, parse_basic_offsets from pydicom.errors import InvalidDicomError from pydicom.filebase import DicomFileLike from pydicom.filereader import data_element_offset_to_value, dcmread @@ -358,13 +350,7 @@ def _read_bot(fp: DicomFileLike) -> np.ndarray: fp.is_implicit_VR, 'OB' ) fp.seek(pixel_data_element_value_offset - 4, 1) - - # Use parse_basic_offsets for pydicom >= 3.0, get_frame_offsets for < 3.0 - if _use_parse_basic_offsets: - offsets = parse_basic_offsets(fp) - else: - is_empty, offsets = get_frame_offsets(fp) - + offsets = parse_basic_offsets(fp) return np.array(offsets, dtype=np.uint32) diff --git a/tests/test_web.py b/tests/test_web.py index ca91e63..5a631ba 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -1285,8 +1285,8 @@ def test_retrieve_instance_frames_rendered_png(httpserver, client, cache_dir): def test_store_instance_error_with_retries(httpserver, client, cache_dir): dataset = pydicom.Dataset.from_json({}) - dataset.is_little_endian = True - dataset.is_implicit_VR = True + dataset.file_meta = pydicom.dataset.FileMetaDataset() + dataset.file_meta.TransferSyntaxUID = pydicom.uid.ImplicitVRLittleEndian max_attempts = 2 client.set_http_retry_params( retry=True, @@ -1311,8 +1311,8 @@ def test_store_instance_error_with_retries_and_additional_params( httpserver, client, cache_dir ): dataset = pydicom.Dataset.from_json({}) - dataset.is_little_endian = True - dataset.is_implicit_VR = True + dataset.file_meta = pydicom.dataset.FileMetaDataset() + dataset.file_meta.TransferSyntaxUID = pydicom.uid.ImplicitVRLittleEndian max_attempts = 2 client.set_http_retry_params( retry=True, @@ -1339,8 +1339,8 @@ def test_store_instance_error_with_retries_and_additional_params( def test_store_instance_error_with_no_retries(httpserver, client, cache_dir): dataset = pydicom.Dataset.from_json({}) - dataset.is_little_endian = True - dataset.is_implicit_VR = True + dataset.file_meta = pydicom.dataset.FileMetaDataset() + dataset.file_meta.TransferSyntaxUID = pydicom.uid.ImplicitVRLittleEndian client.set_http_retry_params(retry=False) httpserver.serve_content( content='', @@ -1360,8 +1360,8 @@ def test_store_instance_error_with_no_retries_and_additional_params( httpserver, client, cache_dir ): dataset = pydicom.Dataset.from_json({}) - dataset.is_little_endian = True - dataset.is_implicit_VR = True + dataset.file_meta = pydicom.dataset.FileMetaDataset() + dataset.file_meta.TransferSyntaxUID = pydicom.uid.ImplicitVRLittleEndian client.set_http_retry_params(retry=False) httpserver.serve_content( content='', From b5540b78da668c76cd02e1cb90ebb6b6d74c988b Mon Sep 17 00:00:00 2001 From: James Butler Date: Sun, 16 Nov 2025 20:04:14 -0500 Subject: [PATCH 3/5] ENH: Avoid user warning about missing multipart response Observed warning: UserWarning: message sent by origin server in response to GET request of Retrieve Instance transaction was not compliant with the DICOM standard, message body shall have Content-Type 'multipart/related; type="application/dicom"' rather than "application/dicom" --- tests/test_web.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/test_web.py b/tests/test_web.py index 5a631ba..40ff73d 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -776,10 +776,20 @@ def test_retrieve_instance_singlepart(httpserver, client, cache_dir): cache_filename = str(cache_dir.joinpath('file.dcm')) with open(cache_filename, 'rb') as f: data = f.read() + media_type = 'application/dicom' + boundary = 'boundary' headers = { - 'content-type': 'application/dicom' + 'content-type': ( + 'multipart/related; ' + f'type="{media_type}"; ' + f'boundary="{boundary}"' + ), } - httpserver.serve_content(content=data, code=200, headers=headers) + message = DICOMwebClient._encode_multipart_message( + content=[data], + content_type=headers['content-type'] + ) + httpserver.serve_content(content=message, code=200, headers=headers) study_instance_uid = '1.2.3' series_instance_uid = '1.2.4' sop_instance_uid = '1.2.5' From da2328b6f68877f442d72cc2cba33f14075c3a42 Mon Sep 17 00:00:00 2001 From: James Butler Date: Sun, 16 Nov 2025 21:33:35 -0500 Subject: [PATCH 4/5] BUG: Fix XML loading to convert numeric VR values to proper Python types --- src/dicomweb_client/web.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/dicomweb_client/web.py b/src/dicomweb_client/web.py index f8e0d0e..8605276 100644 --- a/src/dicomweb_client/web.py +++ b/src/dicomweb_client/web.py @@ -29,6 +29,7 @@ ) import pydicom +from pydicom.valuerep import INT_VR, FLOAT_VR, VR import requests import retrying @@ -60,7 +61,7 @@ def _load_xml_dataset(dataset: Element) -> pydicom.dataset.Dataset: for element in dataset: keyword = element.attrib['keyword'] vr = element.attrib['vr'] - value: Optional[Union[List[Any], str]] + value: Optional[Union[List[Any], str, int, float]] if vr == 'SQ': value = [ _load_xml_dataset(item) @@ -74,6 +75,26 @@ def _load_xml_dataset(dataset: Element) -> pydicom.dataset.Dataset: value = [v.text.strip() for v in value] else: value = None + + # Convert string values to appropriate Python types for + # numeric VRs to satisfy pydicom 3.0+ stricter type validation + if value is not None: + try: + vr_enum = VR(vr) + if vr_enum in INT_VR: + if isinstance(value, list): + value = [int(v) for v in value] + else: + value = int(value) + elif vr_enum in FLOAT_VR: + if isinstance(value, list): + value = [float(v) for v in value] + else: + value = float(value) + except ValueError: + # VR not recognized, leave value as-is + pass + setattr(ds, keyword, value) return ds From 6d4d870cc651de791fe635ddd605b6ac607eff9c Mon Sep 17 00:00:00 2001 From: James Butler Date: Sun, 16 Nov 2025 22:04:42 -0500 Subject: [PATCH 5/5] BUG: Fix test_load_json_dataset_da use DICOM DA format (YYYYMMDD) instead of ISO format (YYYY-MM-DD) --- tests/test_web.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_web.py b/tests/test_web.py index 40ff73d..f058950 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -1537,7 +1537,7 @@ def test_delete_instance_error_with_additional_params( def test_load_json_dataset_da(httpserver, client, cache_dir): - value = ['2018-11-21'] + value = ['20181121'] # DA format must be YYYYMMDD dicom_json = { '00080020': { 'vr': 'DA',