midom 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
midom/__init__.py ADDED
File without changes
midom/components.py ADDED
@@ -0,0 +1,168 @@
1
+ """Python definition of the parts"""
2
+ from typing import Dict, List
3
+
4
+ from dicomcriterion import Criterion
5
+ from dicomcriterion.exceptions import CriterionError
6
+ from pydantic import BaseModel, ConfigDict, field_validator
7
+ from pydantic.functional_serializers import field_serializer
8
+
9
+ from midom.constants import ActionCode, ActionCodes
10
+ from midom.identifiers import (
11
+ PrivateBlockTagIdentifier,
12
+ TagIdentifier,
13
+ tag_identifier_from_string,
14
+ )
15
+
16
+
17
+ class PixelArea(BaseModel):
18
+ area: tuple[int, int, int, int]
19
+
20
+
21
+ class TagAction(BaseModel):
22
+ """Describes the action to take for a single identifier (tag or tag group) and the
23
+ reason for this action.
24
+ """
25
+
26
+ model_config = ConfigDict(
27
+ arbitrary_types_allowed=True
28
+ ) # for TagIdentifier
29
+
30
+ identifier: TagIdentifier
31
+ action: ActionCode
32
+ justification: str
33
+
34
+ @field_serializer("identifier")
35
+ def serialize_identifier(self, value, _info):
36
+ return value.key()
37
+
38
+ @field_validator("identifier", mode="before")
39
+ @classmethod
40
+ def deserialize_identifier(cls, value):
41
+ if isinstance(value, TagIdentifier):
42
+ return value
43
+ elif isinstance(value, str):
44
+ return tag_identifier_from_string(value)
45
+ else:
46
+ raise ValueError(
47
+ f'Invalid input data for TagAction.identifier: "{value}"'
48
+ )
49
+
50
+ @field_serializer("action")
51
+ def serialize_action(self, value, _info):
52
+ return value.key
53
+
54
+ @field_validator("action", mode="before")
55
+ @classmethod
56
+ def deserialize_action(cls, value):
57
+ if isinstance(value, ActionCode):
58
+ return value
59
+ elif isinstance(value, str):
60
+ return ActionCodes.from_string(value)
61
+ else:
62
+ raise ValueError(
63
+ f'Invalid input data for TagAction.identifier: "{value}"'
64
+ )
65
+
66
+
67
+ class CriterionString(BaseModel):
68
+ model_config = ConfigDict(arbitrary_types_allowed=True)
69
+
70
+ content: Criterion
71
+
72
+ @field_serializer("content")
73
+ def serialize_identifier(self, value, _info):
74
+ return value._expression
75
+
76
+ @field_validator("content", mode="before")
77
+ @classmethod
78
+ def deserialize_tag(cls, value):
79
+ if isinstance(value, Criterion):
80
+ return value
81
+ elif isinstance(value, str):
82
+ try:
83
+ return Criterion(value)
84
+ except CriterionError as e:
85
+ raise ValueError(
86
+ f'Could not parse "{value}" as criterion. Original error: {e}'
87
+ ) from e
88
+ else:
89
+ raise ValueError(
90
+ f"Expected Criterion or str as input but "
91
+ f'found f"{type(value)}" for "{value}'
92
+ )
93
+
94
+
95
+ class Filter(BaseModel):
96
+ criterion: CriterionString
97
+ justification: str
98
+
99
+
100
+ class PixelOperation(BaseModel):
101
+ description: str
102
+ criterion: CriterionString
103
+ areas: List[PixelArea]
104
+
105
+
106
+ class PrivateAllowGroup(BaseModel):
107
+ # Allow arbitrary for PrivateBlockTagIdentifier
108
+ model_config = ConfigDict(arbitrary_types_allowed=True)
109
+
110
+ elements: List[PrivateBlockTagIdentifier]
111
+ justification: str
112
+
113
+ @field_serializer("elements")
114
+ def serialize_identifier(self, value, _info):
115
+ return [x.key() for x in value]
116
+
117
+ @field_validator("elements", mode="before")
118
+ @classmethod
119
+ def deserialize_tag(cls, values):
120
+ if not isinstance(values, list):
121
+ ValueError(f"elements should be a list, got {values}")
122
+ deserialized = []
123
+ for x in values:
124
+ if isinstance(x, TagIdentifier):
125
+ deserialized.append(x)
126
+ elif isinstance(x, str):
127
+ deserialized.append(tag_identifier_from_string(x))
128
+ else:
129
+ raise ValueError(
130
+ f'Invalid input data for TagAction.identifier: "{x}"'
131
+ )
132
+ return deserialized
133
+
134
+
135
+ class Protocol(BaseModel):
136
+ """Defines how to handle the deidentification of any incoming dataset. It does
137
+ not say anything about implementation, it only prescribes what should be done
138
+ to each part of a dataset and under which circumstances to reject it outright.
139
+
140
+ Overview of elements:
141
+ tags: Dict[str, List[TagAction]]
142
+ SOPInstanceUID that can contain wildcards: List of what to do with DICOM tags
143
+
144
+ filters: List[Filter]
145
+ If any of these filters matches, reject the DICOM dataset
146
+
147
+ pixel: List[PixelOperation]
148
+ What to do with pixel data. Where to put black boxes if a rule matches
149
+
150
+ private: List[PrivateAllowGroup]
151
+ Which private tags to allow and why
152
+ """
153
+
154
+ tags: Dict[str, List[TagAction]]
155
+ filters: List[Filter]
156
+ pixel: List[PixelOperation]
157
+ private: List[PrivateAllowGroup]
158
+
159
+ def sort_tags(self):
160
+ """Sort tag list for each SOPInstanceUID according to generality. The more
161
+ general, the lower in the list. This means you can take any DICOM tag and
162
+ try to match each element in the list until one hits.
163
+ """
164
+ for key, action_list in self.tags.items():
165
+ self.tags[key] = sorted(
166
+ action_list,
167
+ key=lambda x: x.identifier.number_of_matchable_tags(),
168
+ )
midom/constants.py ADDED
@@ -0,0 +1,77 @@
1
+ """Implementations of some of the MIDOM constants and objects"""
2
+ from pydantic.dataclasses import dataclass
3
+
4
+
5
+ class DeltaCodes:
6
+ """How has a DICOM element changed?"""
7
+
8
+ UNCHANGED = "UNCHANGED"
9
+ CHANGED = "CHANGED"
10
+ REMOVED = "REMOVED"
11
+ EMPTIED = "EMPTIED"
12
+ CREATED = "CREATED"
13
+
14
+ ALL = {UNCHANGED, CHANGED, REMOVED, EMPTIED, CREATED}
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class ActionCode:
19
+ key: str
20
+ var_name: str
21
+
22
+
23
+ class ActionCodes:
24
+ """NEMA specifications from table E1-1. Indicates what to do with each tag"""
25
+
26
+ DUMMY = ActionCode("D", "DUMMY") # replace with dummy
27
+ EMPTY = ActionCode("Z", "EMPTY") # replace with zero length
28
+ REMOVE = ActionCode("X", "REMOVE") # remove
29
+ KEEP = ActionCode("K", "KEEP") # keep
30
+ CLEAN = ActionCode("C", "CLEAN") # clean
31
+ UID = ActionCode("U", "UID") # replace with consistent UID
32
+ EMPTY_OR_DUMMY = ActionCode("Z/D", "EMPTY_OR_DUMMY")
33
+ REPLACE_OR_DUMMY = ActionCode("X/Z", "REPLACE_OR_DUMMY") # X unless Z is
34
+ # required for consistency
35
+ REMOVE_OR_EMPTY = ActionCode("X/Z", "REMOVE_OR_EMPTY") # X unless Z is
36
+ # required for consistency
37
+ REMOVE_OR_DUMMY = ActionCode("X/D", "REMOVE_OR_DUMMY") # X unless D is
38
+ # required for consistency
39
+ REMOVE_OR_EMPTY_OR_DUMMY = ActionCode(
40
+ "X/Z/D", "REMOVE_OR_EMPTY_OR_DUMMY"
41
+ ) # X unless Z or D is required
42
+ REMOVE_OR_EMPTY_OR_UID = ActionCode(
43
+ "X/Z/U*", "REMOVE_OR_EMPTY_OR_UID"
44
+ ) # X unless Z or U is required
45
+ UNDEFINED = ActionCode(
46
+ "?", "Undefined"
47
+ ) # not part of ALL below, special case
48
+
49
+ ALL = {
50
+ DUMMY,
51
+ EMPTY,
52
+ REMOVE,
53
+ KEEP,
54
+ CLEAN,
55
+ UID,
56
+ EMPTY_OR_DUMMY,
57
+ REMOVE_OR_EMPTY,
58
+ REMOVE_OR_DUMMY,
59
+ REMOVE_OR_EMPTY_OR_DUMMY,
60
+ REMOVE_OR_EMPTY_OR_UID,
61
+ }
62
+
63
+ PER_KEY = {x.key: x for x in ALL}
64
+ PER_VAR_NAME = {x.var_name: x for x in ALL}
65
+
66
+ @classmethod
67
+ def from_string(cls, key_or_var_name: str):
68
+ """I've got a string with either a full name or a key. Which action
69
+ code is this?
70
+ """
71
+ try:
72
+ return (cls.PER_KEY | cls.PER_VAR_NAME)[key_or_var_name]
73
+ except KeyError as e:
74
+ raise ValueError(
75
+ f"Unknown action code '{key_or_var_name}'. I "
76
+ f"know {','.join([str(x) for x in cls.ALL])}"
77
+ ) from e