28
28
# pylint: skip-file
29
29
30
30
from deid .logger import bot
31
- from deid .utils import read_file
31
+ from deid .utils import read_file , get_installdir
32
32
from deid .data import data_base
33
- from deid .config .standards import formats , actions , sections , filters
34
-
33
+ from deid .config .standards import (
34
+ formats ,
35
+ actions ,
36
+ sections ,
37
+ filters ,
38
+ groups ,
39
+ group_actions ,
40
+ )
35
41
from collections import OrderedDict
36
42
import os
37
43
import re
@@ -130,7 +136,7 @@ def load_deid(path=None):
130
136
config = OrderedDict ()
131
137
section = None
132
138
133
- while len ( spec ) > 0 :
139
+ while spec :
134
140
135
141
# Clean up white trailing/leading space
136
142
line = spec .pop (0 ).strip ()
@@ -139,15 +145,9 @@ def load_deid(path=None):
139
145
if line .startswith ("#" ):
140
146
continue
141
147
142
- # Starts with Format?
143
- elif bool (re .match ("format" , line , re .I )):
144
- fmt = re .sub ("FORMAT|(\s+)" , "" , line ).lower ()
145
- if fmt not in formats :
146
- bot .exit ("%s is not a valid format." % fmt )
147
-
148
- # Set format
149
- config ["format" ] = fmt
150
- bot .debug ("FORMAT set to %s" % fmt )
148
+ # Set format
149
+ elif bool (re .match ("^format" , line , re .I )):
150
+ config ["format" ] = parse_format (line )
151
151
152
152
# A new section?
153
153
elif line .startswith ("%" ):
@@ -168,24 +168,18 @@ def load_deid(path=None):
168
168
config = config , section = section , section_name = section_name
169
169
)
170
170
171
- # An action (replace, blank, remove, keep, jitter)
171
+ # A %fields action (only field allowed), %values allows split
172
+ elif line .upper ().startswith (group_actions ) and section in groups :
173
+ config = parse_group_action (
174
+ section = section , section_name = section_name , line = line , config = config
175
+ )
176
+
177
+ # An action (ADD, BLANK, JITTER, KEEP, REPLACE, REMOVE, LABEL)
172
178
elif line .upper ().startswith (actions ):
173
179
174
180
# Start of a filter group
175
181
if line .upper ().startswith ("LABEL" ) and section == "filter" :
176
- members = []
177
- keep_going = True
178
- while keep_going is True :
179
- next_line = spec [0 ]
180
- if next_line .upper ().strip ().startswith ("LABEL" ):
181
- keep_going = False
182
- elif next_line .upper ().strip ().startswith ("%" ):
183
- keep_going = False
184
- else :
185
- new_member = spec .pop (0 )
186
- members .append (new_member )
187
- if len (spec ) == 0 :
188
- keep_going = False
182
+ members = parse_filter_group (spec )
189
183
190
184
# Add the filter label to the config
191
185
config = parse_label (
@@ -201,7 +195,7 @@ def load_deid(path=None):
201
195
section = section , section_name = section_name , line = line , config = config
202
196
)
203
197
else :
204
- bot .debug ("%s not recognized to be in valid format, skipping." % line )
198
+ bot .warning ("%s not recognized to be in valid format, skipping." % line )
205
199
return config
206
200
207
201
@@ -214,6 +208,9 @@ def find_deid(path=None):
214
208
path: a path on the filesystem. If not provided, will assume PWD.
215
209
216
210
"""
211
+ # A default deid will be loaded if all else fails
212
+ default_deid = os .path .join (get_installdir (), "data" , "deid.dicom" )
213
+
217
214
if path is None :
218
215
path = os .getcwd ()
219
216
@@ -224,7 +221,11 @@ def find_deid(path=None):
224
221
]
225
222
226
223
if len (contenders ) == 0 :
227
- 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 )
228
229
229
230
elif len (contenders ) > 1 :
230
231
bot .warning ("Multiple deid files found in %s, will use first." % (path ))
@@ -238,6 +239,48 @@ def find_deid(path=None):
238
239
return path
239
240
240
241
242
+ def parse_format (line ):
243
+ """given a line that starts with FORMAT, parse the format of the
244
+ file and check that it is supported. If not, exit on error. If yes,
245
+ return the format.
246
+
247
+ Parameters
248
+ ==========
249
+ line: the line that starts with format.
250
+ """
251
+ fmt = re .sub ("FORMAT|(\s+)" , "" , line ).lower ()
252
+ if fmt not in formats :
253
+ bot .exit ("%s is not a valid format." % fmt )
254
+ bot .debug ("FORMAT set to %s" % fmt )
255
+ return fmt
256
+
257
+
258
+ def parse_filter_group (spec ):
259
+ """given the specification (a list of lines) continue parsing lines
260
+ until the filter group ends, as indicated by the start of a new LABEL,
261
+ (case 1), the start of a new section (case 2) or the end of the spec
262
+ file (case 3). Returns a list of members (lines) that belong to the
263
+ filter group. The list (by way of using pop) is updated in the calling
264
+ function.
265
+
266
+ Parameters
267
+ ==========
268
+ spec: unparsed lines of the deid recipe file
269
+ """
270
+ members = []
271
+ keep_going = True
272
+ while keep_going and spec :
273
+ next_line = spec [0 ]
274
+ if next_line .upper ().strip ().startswith ("LABEL" ):
275
+ keep_going = False
276
+ elif next_line .upper ().strip ().startswith ("%" ):
277
+ keep_going = False
278
+ else :
279
+ new_member = spec .pop (0 )
280
+ members .append (new_member )
281
+ return members
282
+
283
+
241
284
def parse_label (section , config , section_name , members , label = None ):
242
285
"""parse label will add a (optionally named) label to the filter
243
286
section, including one or more criteria
@@ -289,7 +332,10 @@ def parse_label(section, config, section_name, members, label=None):
289
332
290
333
291
334
def parse_member (members , operator = None ):
292
-
335
+ """a parsing function for a filter member. Will return a single member
336
+ with fields, values, and an operator. In the case of multiple and/or
337
+ statements that are chained, will instead return a list.
338
+ """
293
339
main_operator = operator
294
340
295
341
actions = []
@@ -382,7 +428,7 @@ def add_section(config, section, section_name=None):
382
428
if section is None :
383
429
bot .exit ("You must define a section (e.g. %header) before any action." )
384
430
385
- if section == "filter" and section_name is None :
431
+ if section in [ "filter" , "values" , "fields" ] and section_name is None :
386
432
bot .exit ("You must provide a name for a filter section." )
387
433
388
434
if section not in sections :
@@ -415,6 +461,54 @@ def _remove_comments(parts):
415
461
return value .split ("#" )[0 ] # remove comments
416
462
417
463
464
+ def parse_group_action (section , line , config , section_name ):
465
+ """parse a group action, either FIELD or SPLIT, which must belong to
466
+ either a fields or values section.
467
+
468
+ Parameters
469
+ =========
470
+ section: a valid section name from the deid config file
471
+ line: the line content to parse for the section/action
472
+ config: the growing/current config dictionary
473
+ section_name: optionally, a section name
474
+ """
475
+ if not line .upper ().startswith (group_actions ):
476
+ bot .exit ("%s is not a valid group action." % line )
477
+
478
+ if not line .upper ().startswith ("FIELD" ) and section == "fields" :
479
+ bot .exit ("%fields only supports FIELD actions." )
480
+
481
+ # We may have to deal with cases of spaces
482
+ bot .debug ("%s: adding %s" % (section , line ))
483
+ parts = line .split (" " )
484
+ action = parts .pop (0 ).replace (" " , "" )
485
+
486
+ # Both require some parts
487
+ if not parts :
488
+ bot .exit ("%s action %s requires additional arguments" % (section , action ))
489
+
490
+ # For both, the second is always a field or field expander
491
+ field = parts .pop (0 )
492
+
493
+ # Fields supports one or more fields with expanders (no third arguments)
494
+ if section == "fields" :
495
+ config [section ][section_name ].append ({"action" : action , "field" : field })
496
+
497
+ # Values supports FIELD or SPLIT
498
+ elif section == "values" :
499
+
500
+ # If we have a third set of arguments
501
+ if parts :
502
+ value = _remove_comments (parts )
503
+ config [section ][section_name ].append (
504
+ {"action" : action , "field" : field , "value" : value }
505
+ )
506
+ else :
507
+ config [section ][section_name ].append ({"action" : action , "field" : field })
508
+
509
+ return config
510
+
511
+
418
512
def parse_config_action (section , line , config , section_name = None ):
419
513
"""add action will take a line from a deid config file, a config (dictionary), and
420
514
an active section name (eg header) and add an entry to the config file to perform
@@ -428,7 +522,6 @@ def parse_config_action(section, line, config, section_name=None):
428
522
section_name: optionally, a section name
429
523
430
524
"""
431
-
432
525
if not line .upper ().startswith (actions ):
433
526
bot .exit ("%s is not a valid action line." % line )
434
527
0 commit comments