Skip to content

Commit 5fb964d

Browse files
committed
adding finish of function to derive groups (values, fields)
Signed-off-by: vsoch <[email protected]>
1 parent 6fc5e53 commit 5fb964d

File tree

9 files changed

+293
-23
lines changed

9 files changed

+293
-23
lines changed

deid/config/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def has_actions(self):
167167

168168
def listof(self, section):
169169
"""return a list of keys for a section"""
170-
listing = self._get_section(section)
170+
listing = self._get_section(section) or {}
171171
return list(listing.keys())
172172

173173
def ls_filters(self):

deid/config/utils.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
# pylint: skip-file
2929

3030
from deid.logger import bot
31-
from deid.utils import read_file
31+
from deid.utils import read_file, get_installdir
3232
from deid.data import data_base
3333
from deid.config.standards import (
3434
formats,
@@ -208,6 +208,9 @@ def find_deid(path=None):
208208
path: a path on the filesystem. If not provided, will assume PWD.
209209
210210
"""
211+
# A default deid will be loaded if all else fails
212+
default_deid = os.path.join(get_installdir(), "data", "deid.dicom")
213+
211214
if path is None:
212215
path = os.getcwd()
213216

@@ -218,7 +221,11 @@ def find_deid(path=None):
218221
]
219222

220223
if len(contenders) == 0:
221-
bot.exit("No deid settings files found in %s, exiting." % (path))
224+
bot.warning(
225+
"No deid settings files found in %s, will use default dicom.deid."
226+
% path
227+
)
228+
contenders.append(default_deid)
222229

223230
elif len(contenders) > 1:
224231
bot.warning("Multiple deid files found in %s, will use first." % (path))

deid/dicom/actions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def perform_action(dicom, action, item=None, fields=None, return_seen=False):
5151
"action" (eg, REPLACE) what to do with the field
5252
"value": if needed, the field from the response to replace with
5353
"""
54+
print(action)
5455
field = action.get("field") # e.g: PatientID, endswith:ID, values:name, fields:name
5556
value = action.get("value") # "suid" or "var:field"
5657
action = action.get("action") # "REPLACE"

deid/dicom/fields.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ def find_by_values(values, dicom):
9191
"""Given a list of values, find fields in the dicom that contain any
9292
of those values, as determined by a regular expression search.
9393
"""
94+
# Values must be strings
95+
values = [str(x) for x in values]
96+
print(values)
97+
9498
fields = []
9599
contenders = get_fields(dicom)
96100

deid/dicom/header.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,17 @@
2828
from deid.logger import bot
2929
from deid.utils import read_json
3030

31-
from .tags import remove_sequences
32-
from .groups import extract_values_list, extract_fields_list
33-
34-
from deid.dicom.tags import get_private
3531
from deid.config import DeidRecipe
3632

3733
from pydicom import read_file
3834

39-
from .utils import save_dicom
40-
from .actions import perform_action
41-
from pydicom.dataset import Dataset
35+
from deid.dicom.utils import save_dicom
36+
from deid.dicom.actions import perform_action
37+
from deid.dicom.tags import remove_sequences, get_private
38+
from deid.dicom.groups import extract_values_list, extract_fields_list
39+
from deid.dicom.fields import get_fields
4240

43-
from .fields import get_fields
41+
from pydicom.dataset import Dataset
4442

4543
import os
4644

@@ -137,8 +135,6 @@ def get_identifiers(
137135
skip_fields: if not None, added fields to skip
138136
139137
"""
140-
bot.debug("Extracting identifiers for %s dicom" % (len(dicom_files)))
141-
142138
if config is None:
143139
config = "%s/config.json" % here
144140

@@ -149,6 +145,7 @@ def get_identifiers(
149145
if not isinstance(dicom_files, list):
150146
dicom_files = [dicom_files]
151147

148+
bot.debug("Extracting identifiers for %s dicom" % len(dicom_files))
152149
ids = dict() # identifiers
153150

154151
# We will skip PixelData
@@ -159,7 +156,13 @@ def get_identifiers(
159156
skip = skip + skip_fields
160157

161158
for dicom_file in dicom_files:
162-
dicom = read_file(dicom_file, force=True)
159+
160+
# TODO: this should have shared reader class to hold dicom, ids, etc.
161+
if isinstance(dicom_file, Dataset):
162+
dicom = dicom_file
163+
dicom_file = dicom.filename
164+
else:
165+
dicom = read_file(dicom_file, force=force)
163166

164167
if dicom_file not in ids:
165168
ids[dicom_file] = dict()
@@ -253,7 +256,12 @@ def replace_identifiers(
253256
# Parse through dicom files, update headers, and save
254257
updated_files = []
255258
for _, dicom_file in enumerate(dicom_files):
256-
dicom = read_file(dicom_file, force=force)
259+
260+
if isinstance(dicom_file, Dataset):
261+
dicom = dicom_file
262+
dicom_file = dicom.filename
263+
else:
264+
dicom = read_file(dicom_file, force=force)
257265
dicom_name = os.path.basename(dicom_file)
258266
fields = dicom.dir()
259267

@@ -266,19 +274,19 @@ def replace_identifiers(
266274
if dicom_file in ids:
267275

268276
# Prepare additional lists of values and fields (updates item)
269-
if deid.has_values_lists():
270-
for group, actions in deid.get_values_lists().items():
277+
if recipe.has_values_lists():
278+
for group, actions in recipe.get_values_lists().items():
271279
ids[dicom_file][group] = extract_values_list(
272280
dicom=dicom, actions=actions
273281
)
274282

275-
if deid.has_fields_lists():
276-
for group, actions in deid.get_fields_lists().items():
283+
if recipe.has_fields_lists():
284+
for group, actions in recipe.get_fields_lists().items():
277285
ids[dicom_file][group] = extract_fields_list(
278286
dicom=dicom, actions=actions
279287
)
280288

281-
for action in deid.get_actions():
289+
for action in recipe.get_actions():
282290
dicom = perform_action(
283291
dicom=dicom, item=ids[dicom_file], action=action
284292
)

deid/tests/test_config.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,14 @@ def test_load_deid(self):
5656
config = load_deid(os.path.dirname(self.deid))
5757
self.assertTrue("format" in config)
5858

59-
print("Case 3: Testing error on non-existing load")
59+
print("Case 3: Testing error on non-existing load of file")
6060
with self.assertRaises(SystemExit) as cm:
61-
config = load_deid(self.tmpdir)
61+
config = load_deid(os.path.join(self.tmpdir, "deid.doesnt-exist"))
6262
self.assertEqual(cm.exception.code, 1)
6363

64+
print("Case 4: Testing load of default deid.")
65+
config = load_deid(self.tmpdir)
66+
6467
def test_find_deid(self):
6568

6669
print("Testing finding deid file, referencing directly.")

deid/tests/test_deid_recipe.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#!/usr/bin/env python
2+
3+
"""
4+
Test DeidRecipe class
5+
6+
Copyright (c) 2020 Vanessa Sochat
7+
8+
Permission is hereby granted, free of charge, to any person obtaining a copy
9+
of this software and associated documentation files (the "Software"), to deal
10+
in the Software without restriction, including without limitation the rights
11+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
copies of the Software, and to permit persons to whom the Software is
13+
furnished to do so, subject to the following conditions:
14+
15+
The above copyright notice and this permission notice shall be included in all
16+
copies or substantial portions of the Software.
17+
18+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24+
SOFTWARE.
25+
26+
"""
27+
28+
import unittest
29+
import tempfile
30+
import shutil
31+
import json
32+
import os
33+
34+
from deid.config import DeidRecipe
35+
from deid.utils import get_installdir
36+
37+
38+
class TestConfig(unittest.TestCase):
39+
def setUp(self):
40+
self.pwd = get_installdir()
41+
self.deid = os.path.abspath("%s/../examples/deid/deid.dicom" % self.pwd)
42+
self.tmpdir = tempfile.mkdtemp()
43+
print("\n######################START######################")
44+
45+
def tearDown(self):
46+
shutil.rmtree(self.tmpdir)
47+
print("\n######################END########################")
48+
49+
def test_load_recipe(self):
50+
51+
print("Case 1: Test loading default DeidRecipe")
52+
53+
recipe = DeidRecipe()
54+
55+
self.assertTrue(isinstance(recipe.deid, dict))
56+
57+
print("Checking basic sections are loaded")
58+
print(recipe.deid.keys())
59+
for section in ["header", "format", "filter"]:
60+
self.assertTrue(section in recipe.deid)
61+
62+
print("Case 2: Loading from file")
63+
recipe = DeidRecipe(self.deid)
64+
65+
def test_get_functions(self):
66+
67+
recipe = DeidRecipe(self.deid)
68+
69+
# Format
70+
self.assertEqual(recipe.get_format(), "dicom")
71+
72+
# Actions for header
73+
print("Testing get_actions")
74+
actions = recipe.get_actions()
75+
self.assertTrue(isinstance(actions, list))
76+
for key in ["action", "field", "value"]:
77+
self.assertTrue(key in actions[0])
78+
self.assertTrue(recipe.has_actions())
79+
80+
# Filters
81+
print("Testing get_filters")
82+
filters = recipe.get_filters()
83+
self.assertTrue(isinstance(filters, dict))
84+
85+
# whitelist, blacklist, graylist
86+
for key in recipe.ls_filters():
87+
self.assertTrue(key in filters)
88+
89+
recipe = DeidRecipe()
90+
filters = recipe.get_filters()
91+
self.assertTrue(isinstance(filters["whitelist"], list))
92+
93+
# Test that each filter has a set of filters, coords, name
94+
for key in ["filters", "coordinates", "name"]:
95+
self.assertTrue(key in filters["whitelist"][0])
96+
97+
# Each filter is a list of actions, name is string, coords are list
98+
self.assertTrue(isinstance(filters["whitelist"][0]["filters"], list))
99+
self.assertTrue(isinstance(filters["whitelist"][0]["name"], str))
100+
self.assertTrue(isinstance(filters["whitelist"][0]["coordinates"], list))
101+
102+
# Check content of the first filter
103+
for key in ["action", "field", "operator", "InnerOperators", "value"]:
104+
self.assertTrue(key in filters["whitelist"][0]["filters"][0])
105+
106+
# Fields and Values
107+
print("Testing get_fields_lists and get_values_lists")
108+
self.assertEqual(recipe.get_fields_lists(), None)
109+
self.assertEqual(recipe.get_values_lists(), None)
110+
self.assertEqual(recipe.ls_fieldlists(), [])
111+
self.assertEqual(recipe.ls_valuelists(), [])
112+
self.assertTrue(not recipe.has_fields_lists())
113+
self.assertTrue(not recipe.has_values_lists())
114+
115+
# Load in recipe with values and fields
116+
deid = os.path.abspath("%s/../examples/deid/deid.dicom-groups" % self.pwd)
117+
recipe = DeidRecipe(deid)
118+
119+
assert "values" in recipe.deid
120+
assert "fields" in recipe.deid
121+
self.assertTrue(isinstance(recipe.deid["values"], dict))
122+
self.assertTrue(isinstance(recipe.deid["fields"], dict))
123+
124+
self.assertTrue(recipe.get_fields_lists() is not None)
125+
self.assertTrue(recipe.get_values_lists() is not None)
126+
self.assertEqual(recipe.ls_fieldlists(), ["instance_fields"])
127+
self.assertEqual(recipe.ls_valuelists(), ["cookie_names", "operator_names"])
128+
self.assertTrue(recipe.has_fields_lists())
129+
self.assertTrue(recipe.has_values_lists())
130+
131+
132+
if __name__ == "__main__":
133+
unittest.main()

0 commit comments

Comments
 (0)