Source code for dicom_dre.parameters

"""Per-patient de-identification parameters applied at ``apply()`` time.

``DeidParameters`` is the typed, immutable record of the per-patient values a
caller supplies when de-identifying a dataset. A profile is a build-once,
patient-invariant policy object; the per-patient identity values are supplied on
``DeidParameters`` and passed to :meth:`DeidProfile.apply`.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING


if TYPE_CHECKING:
    from collections.abc import Mapping


# Write-time defaults resolved by the parameterized tag actions. Kept here as a
# single source of truth for the placeholder identifiers and the default study id.
# When jitter is absent the date-shift is derived per study by stable_jitter.
IDENTIFIER_PLACEHOLDER = "[REDACTED]"  # PatientID/AccessionNumber fail-safe when neither supplied nor hashable
DEFAULT_STUDY_ID = "UNKNOWN"  # STUDY_ID (UID-hash salt / ClinicalTrialProtocolID) when absent

# Per-patient identity keys accepted by ``DeidParameters.from_mapping``. Build-time
# settings (UID root, allowlist CSV, hash salt) are not part of this set; they are
# held on ``ProfileSettings`` instead.
_IDENTITY_KEYS = frozenset(
    {
        "PATIENT_ID",
        "PATIENT_NAME",
        "ACCESSION_NUMBER",
        "STUDY_ID",
        "SERIES_DESCRIPTION",
        "STUDY_DESCRIPTION",
        "PROTOCOL_NAME",
        "JITTER",
    }
)


[docs] @dataclass(frozen=True, slots=True) class DeidParameters: """Immutable per-patient de-identification parameters. Each field records exactly what the caller supplied; ``None`` means "not supplied" and the parameterized tag actions resolve placeholder defaults at write time. The type is hashable and picklable so it can be shared read-only across threads and serialized to worker processes. """ patient_id: str | None = None patient_name: str | None = None accession_number: str | None = None study_id: str | None = None series_description: str | None = None study_description: str | None = None protocol_name: str | None = None jitter: int | None = None
[docs] @classmethod def from_mapping(cls, mapping: Mapping[str, str]) -> DeidParameters: """Build ``DeidParameters`` from an uppercase-keyed parameter mapping. Reads the per-patient identity keys ``PATIENT_ID``, ``PATIENT_NAME``, ``ACCESSION_NUMBER``, ``STUDY_ID``, ``SERIES_DESCRIPTION``, ``STUDY_DESCRIPTION``, ``PROTOCOL_NAME``, and ``JITTER``. Build-time settings (UID root, allowlist CSV, identifier-hash salt) are not accepted here; they belong to :class:`dicom_dre.profiles.config.ProfileSettings`. Args: mapping: Parameter mapping with uppercase keys. Returns: The parsed parameters. Raises: ValueError: If ``JITTER`` is present but not an integer, or if the mapping contains a key that is not a per-patient identity key. """ unknown = set(mapping) - _IDENTITY_KEYS if unknown: keys = ", ".join(sorted(unknown)) raise ValueError( f"Unknown de-identification parameter(s): {keys}. " f"Valid parameters: {', '.join(sorted(_IDENTITY_KEYS))}." ) jitter_value = mapping.get("JITTER") if jitter_value is None: jitter: int | None = None else: try: jitter = int(jitter_value) except (TypeError, ValueError) as exc: raise ValueError(f"JITTER parameter must be an integer, got {jitter_value!r}") from exc return cls( patient_id=mapping.get("PATIENT_ID"), patient_name=mapping.get("PATIENT_NAME"), accession_number=mapping.get("ACCESSION_NUMBER"), study_id=mapping.get("STUDY_ID"), series_description=mapping.get("SERIES_DESCRIPTION"), study_description=mapping.get("STUDY_DESCRIPTION"), protocol_name=mapping.get("PROTOCOL_NAME"), jitter=jitter, )