Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6e9ed11
adding pseudonym to wador views
chinacat567 May 16, 2025
cd86735
using pydicomweb-client
chinacat567 May 16, 2025
fd6c6b5
Merge branch 'main' into adit-pseudonym
chinacat567 May 16, 2025
32a21bb
lint fix, lockfile update
chinacat567 May 16, 2025
3ec96a7
fixing formatting
chinacat567 May 18, 2025
0524510
fixing merge conflicts
chinacat567 May 18, 2025
1c24d03
fixing formatting errors
chinacat567 May 18, 2025
1932e62
more formatting fixes
chinacat567 May 18, 2025
0cf5bff
parsing pseudonym
chinacat567 May 19, 2025
325470d
Merge branch 'main' into adit-pseudonym
chinacat567 May 19, 2025
83e29ea
adding char limit, wildcard validation and tests
chinacat567 May 20, 2025
3caa6ef
validator and test refac
chinacat567 May 21, 2025
c3ad5a4
shared pseudnymizer, test changes
chinacat567 May 23, 2025
aaf8340
bug fix
chinacat567 May 23, 2025
0e219c3
adit client deadcode removal
chinacat567 May 23, 2025
4daa0b5
pseudonymizer changes
chinacat567 May 23, 2025
7f2136e
refac validate_pseudonym -> _get_pseudonym
chinacat567 May 24, 2025
ba3e6ee
passing pseudonym through adit client and changing acceptance tests t…
chinacat567 May 24, 2025
0928602
reorg validator directory
chinacat567 May 25, 2025
8f4456d
adding metadata functions to adit_client
chinacat567 May 25, 2025
b40a44a
unit test fix
chinacat567 May 25, 2025
5bca9eb
Merge branch 'main' into adit-pseudonym
chinacat567 May 25, 2025
22a9d74
pydicomweb-client => dicomweb-client
chinacat567 May 25, 2025
9bd49c3
<broken> : pseduonymizer changes, pytest changes
chinacat567 May 27, 2025
2631705
bug fix
chinacat567 May 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 7 additions & 61 deletions adit-client/adit_client/client.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,8 @@
import importlib.metadata
from typing import Iterator

from dicognito.anonymizer import Anonymizer
from dicognito.value_keeper import ValueKeeper
from dicomweb_client import DICOMwebClient, session_utils
from pydicom import Dataset

DEFAULT_SKIP_ELEMENTS_ANONYMIZATION = [
"AcquisitionDate",
"AcquisitionDateTime",
"AcquisitionTime",
"ContentDate",
"ContentTime",
"SeriesDate",
"SeriesTime",
"StudyDate",
"StudyTime",
]
from pydicomweb_client import DICOMwebClient, session_utils


class AditClient:
Expand All @@ -27,7 +13,6 @@ def __init__(
verify: str | bool = True,
trial_protocol_id: str | None = None,
trial_protocol_name: str | None = None,
skip_elements_anonymization: list[str] | None = None,
) -> None:
self.server_url = server_url
self.auth_token = auth_token
Expand All @@ -36,11 +21,6 @@ def __init__(
self.trial_protocol_name = trial_protocol_name
self.__version__ = importlib.metadata.version("adit-client")

if skip_elements_anonymization is None:
self.skip_elements_anonymization = DEFAULT_SKIP_ELEMENTS_ANONYMIZATION
else:
self.skip_elements_anonymization = skip_elements_anonymization

def search_for_studies(
self, ae_title: str, query: dict[str, str] | None = None
) -> list[Dataset]:
Expand Down Expand Up @@ -76,24 +56,16 @@ def retrieve_study(
"""Retrieve all instances of a study."""
images = self._create_dicom_web_client(ae_title).retrieve_study(study_uid)

anonymizer: Anonymizer | None = None
if pseudonym is not None:
anonymizer = self._setup_anonymizer()

return [self._handle_dataset(image, anonymizer, pseudonym) for image in images]
return [self._handle_dataset(image, pseudonym) for image in images]

def iter_study(
self, ae_title: str, study_uid: str, pseudonym: str | None = None
) -> Iterator[Dataset]:
"""Iterate over all instances of a study."""
images = self._create_dicom_web_client(ae_title).iter_study(study_uid)

anonymizer: Anonymizer | None = None
if pseudonym is not None:
anonymizer = self._setup_anonymizer()

for image in images:
yield self._handle_dataset(image, anonymizer, pseudonym)
yield self._handle_dataset(image, pseudonym)

def retrieve_series(
self,
Expand All @@ -107,11 +79,7 @@ def retrieve_series(
study_uid, series_instance_uid=series_uid
)

anonymizer: Anonymizer | None = None
if pseudonym is not None:
anonymizer = self._setup_anonymizer()

return [self._handle_dataset(image, anonymizer, pseudonym) for image in images]
return [self._handle_dataset(image, pseudonym) for image in images]

def iter_series(
self,
Expand All @@ -125,12 +93,8 @@ def iter_series(
study_uid, series_instance_uid=series_uid
)

anonymizer: Anonymizer | None = None
if pseudonym is not None:
anonymizer = self._setup_anonymizer()

for image in images:
yield self._handle_dataset(image, anonymizer, pseudonym)
yield self._handle_dataset(image, pseudonym)

def retrieve_image(
self,
Expand All @@ -145,11 +109,7 @@ def retrieve_image(
study_uid, series_uid, image_uid
)

anonymizer: Anonymizer | None = None
if pseudonym is not None:
anonymizer = self._setup_anonymizer()

return self._handle_dataset(image, anonymizer, pseudonym)
return self._handle_dataset(image, pseudonym)

def store_images(self, ae_title: str, images: list[Dataset]) -> Dataset:
"""Store images."""
Expand All @@ -175,15 +135,7 @@ def _create_dicom_web_client(self, ae_title: str) -> DICOMwebClient:
},
)

def _setup_anonymizer(self) -> Anonymizer:
anonymizer = Anonymizer()
for element in self.skip_elements_anonymization:
anonymizer.add_element_handler(ValueKeeper(element))
return anonymizer

def _handle_dataset(
self, ds: Dataset, anonymizer: Anonymizer | None, pseudonym: str | None
) -> Dataset:
def _handle_dataset(self, ds: Dataset, pseudonym: str | None) -> Dataset:
# Similar to what ADIT does in core/processors.py

if self.trial_protocol_id is not None:
Expand All @@ -192,12 +144,6 @@ def _handle_dataset(
if self.trial_protocol_name is not None:
ds.ClinicalTrialProtocolName = self.trial_protocol_name

if pseudonym is not None:
assert anonymizer is not None
anonymizer.anonymize(ds)
ds.PatientID = pseudonym
ds.PatientName = pseudonym

if pseudonym and self.trial_protocol_id:
session_id = f"{ds.StudyDate}-{ds.StudyTime}"
ds.PatientComments = (
Expand Down
25 changes: 7 additions & 18 deletions adit/core/processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
from pathlib import Path
from typing import Callable

from dicognito.anonymizer import Anonymizer
from dicognito.value_keeper import ValueKeeper
from django.conf import settings
from pydicom import Dataset

from adit.core.utils.pseudonymizer import Pseudonymizer

from .errors import DicomError
from .models import DicomAppSettings, DicomNode, DicomTask, TransferTask
from .types import DicomLogEntry, ProcessingResult
Expand Down Expand Up @@ -186,12 +186,11 @@ def _download_to_folder(
study_folder = patient_folder / f"{prefix}-{modalities}"
os.makedirs(study_folder, exist_ok=True)

anonymizer = self._setup_anonymizer()
pseudonymizer = Pseudonymizer(pseudonym)

modifier = partial(
self._modify_dataset,
anonymizer,
pseudonym,
pseudonymizer,
)

if series_uids:
Expand Down Expand Up @@ -281,12 +280,6 @@ def _find_series_list(self, series_uid: str) -> list[ResultDataset]:

return results

def _setup_anonymizer(self) -> Anonymizer:
anonymizer = Anonymizer()
for element in settings.SKIP_ELEMENTS_ANONYMIZATION:
anonymizer.add_element_handler(ValueKeeper(element))
return anonymizer

def _download_study(
self,
patient_id: str,
Expand Down Expand Up @@ -379,17 +372,12 @@ def callback(ds: Dataset | None) -> None:

def _modify_dataset(
self,
anonymizer: Anonymizer,
pseudonym: str | None,
pseudonymizer: Pseudonymizer,
ds: Dataset,
) -> None:
"""Optionally pseudonymize an incoming dataset with the given pseudonym
and add the trial ID and name to the DICOM header if specified."""
if pseudonym:
anonymizer.anonymize(ds)

ds.PatientID = pseudonym
ds.PatientName = pseudonym
pseudonymizer.pseudonymize(ds)

trial_protocol_id = self.transfer_task.job.trial_protocol_id
trial_protocol_name = self.transfer_task.job.trial_protocol_name
Expand All @@ -400,6 +388,7 @@ def _modify_dataset(
if trial_protocol_name:
ds.ClinicalTrialProtocolName = trial_protocol_name

pseudonym = pseudonymizer.pseudonym
if pseudonym and trial_protocol_id:
session_id = f"{ds.StudyDate}-{ds.StudyTime}"
ds.PatientComments = (
Expand Down
2 changes: 1 addition & 1 deletion adit/core/utils/dicom_web_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
from pathlib import Path
from typing import Callable, Iterator, NoReturn

from dicomweb_client import DICOMwebClient
from pydicom import Dataset
from pydicom.errors import InvalidDicomError
from pydicomweb_client import DICOMwebClient
from requests import HTTPError

from ..errors import DicomError, RetriableDicomError
Expand Down
45 changes: 45 additions & 0 deletions adit/core/utils/pseudonymizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from dicognito.anonymizer import Anonymizer
from dicognito.value_keeper import ValueKeeper
from django.conf import settings
from pydicom import Dataset


class Pseudonymizer:
"""
A utility class for pseudonymizing (or anonymizing) DICOM data.
"""

def __init__(self, pseudonym: str | None = None) -> None:
"""
Initialize the Pseudonymizer with an optional pseudonym.
:param pseudonym: The pseudonym to be used for the DICOM data.
"""
self.anonymizer = self._setup_anonymizer()
self.pseudonym = pseudonym

def _setup_anonymizer(self) -> Anonymizer:
"""
Set up the anonymizer instance and configure it to skip specific elements.
:return: An instance of the Anonymizer class.
"""
anonymizer = Anonymizer()
for element in settings.SKIP_ELEMENTS_ANONYMIZATION:
anonymizer.add_element_handler(ValueKeeper(element))
return anonymizer

def pseudonymize(
self,
ds: Dataset,
) -> None:
"""
Pseudonymize the given DICOM dataset using the anonymizer and optional pseudonym.
:param ds: The DICOM dataset to be pseudonymized.
"""
if self.pseudonym:
# Replace PatientID and PatientName with the provided pseudonym.
self.anonymizer.anonymize(ds) # Apply anonymization to the dataset.
ds.PatientID = self.pseudonym
ds.PatientName = self.pseudonym
2 changes: 1 addition & 1 deletion adit/core/utils/testing_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
import pandas as pd
from adit_radis_shared.accounts.factories import GroupFactory
from adit_radis_shared.common.utils.testing_helpers import add_permission
from dicomweb_client import DICOMwebClient
from django.conf import settings
from django.core.management import call_command
from playwright.sync_api import FilePayload
from pydicom import Dataset
from pydicomweb_client import DICOMwebClient
from pynetdicom.association import Association
from pynetdicom.status import Status

Expand Down
8 changes: 8 additions & 0 deletions adit/core/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,11 @@ def validate_series_number(value: str) -> None:
raise ValueError()
except ValueError:
raise ValidationError(f"Invalid series number: {value}")


def validate_pseudonym(pseudonym: str) -> None:
no_backslash_char_validator(pseudonym)
no_control_chars_validator(pseudonym)
no_wildcard_chars_validator(pseudonym)
if len(pseudonym) > 64:
raise ValidationError("Pseudonym string too long (max 64 characters).")
Loading