1#!/usr/bin/env python
2
3r"""
4Define variable manipulation functions.
5"""
6
7import os
8import re
9
10try:
11    from robot.utils import DotDict
12except ImportError:
13    pass
14
15import collections
16
17import gen_print as gp
18import gen_misc as gm
19import func_args as fa
20
21
22def create_var_dict(*args):
23    r"""
24    Create a dictionary whose keys/values are the arg names/arg values passed
25    to it and return it to the caller.
26
27    Note: The resulting dictionary will be ordered.
28
29    Description of argument(s):
30    *args  An unlimited number of arguments to be processed.
31
32    Example use:
33
34    first_name = 'Steve'
35    last_name = 'Smith'
36    var_dict = create_var_dict(first_name, last_name)
37
38    gp.print_var(var_dict)
39
40    The print-out of the resulting var dictionary is:
41    var_dict:
42      var_dict[first_name]:                           Steve
43      var_dict[last_name]:                            Smith
44    """
45
46    try:
47        result_dict = collections.OrderedDict()
48    except AttributeError:
49        result_dict = DotDict()
50
51    arg_num = 1
52    for arg in args:
53        arg_name = gp.get_arg_name(None, arg_num, stack_frame_ix=2)
54        result_dict[arg_name] = arg
55        arg_num += 1
56
57    return result_dict
58
59
60default_record_delim = ':'
61default_key_val_delim = '.'
62
63
64def join_dict(dict,
65              record_delim=default_record_delim,
66              key_val_delim=default_key_val_delim):
67    r"""
68    Join a dictionary's keys and values into a string and return the string.
69
70    Description of argument(s):
71    dict                            The dictionary whose keys and values are
72                                    to be joined.
73    record_delim                    The delimiter to be used to separate
74                                    dictionary pairs in the resulting string.
75    key_val_delim                   The delimiter to be used to separate keys
76                                    from values in the resulting string.
77
78    Example use:
79
80    gp.print_var(var_dict)
81    str1 = join_dict(var_dict)
82    gp.print_var(str1)
83
84    Program output.
85    var_dict:
86      var_dict[first_name]:                           Steve
87      var_dict[last_name]:                            Smith
88    str1:
89    first_name.Steve:last_name.Smith
90    """
91
92    format_str = '%s' + key_val_delim + '%s'
93    return record_delim.join([format_str % (key, value) for (key, value) in
94                              dict.items()])
95
96
97def split_to_dict(string,
98                  record_delim=default_record_delim,
99                  key_val_delim=default_key_val_delim):
100    r"""
101    Split a string into a dictionary and return it.
102
103    This function is the complement to join_dict.
104
105    Description of argument(s):
106    string                          The string to be split into a dictionary.
107                                    The string must have the proper delimiters
108                                    in it.  A string created by join_dict
109                                    would qualify.
110    record_delim                    The delimiter to be used to separate
111                                    dictionary pairs in the input string.
112    key_val_delim                   The delimiter to be used to separate
113                                    keys/values in the input string.
114
115    Example use:
116
117    gp.print_var(str1)
118    new_dict = split_to_dict(str1)
119    gp.print_var(new_dict)
120
121
122    Program output.
123    str1:
124    first_name.Steve:last_name.Smith
125    new_dict:
126      new_dict[first_name]:                           Steve
127      new_dict[last_name]:                            Smith
128    """
129
130    try:
131        result_dict = collections.OrderedDict()
132    except AttributeError:
133        result_dict = DotDict()
134
135    raw_keys_values = string.split(record_delim)
136    for key_value in raw_keys_values:
137        key_value_list = key_value.split(key_val_delim)
138        try:
139            result_dict[key_value_list[0]] = key_value_list[1]
140        except IndexError:
141            result_dict[key_value_list[0]] = ""
142
143    return result_dict
144
145
146def create_file_path(file_name_dict,
147                     dir_path="/tmp/",
148                     file_suffix=""):
149    r"""
150    Create a file path using the given parameters and return it.
151
152    Description of argument(s):
153    file_name_dict                  A dictionary with keys/values which are to
154                                    appear as part of the file name.
155    dir_path                        The dir_path that is to appear as part of
156                                    the file name.
157    file_suffix                     A suffix to be included as part of the
158                                    file name.
159    """
160
161    dir_path = gm.add_trailing_slash(dir_path)
162    return dir_path + join_dict(file_name_dict) + file_suffix
163
164
165def parse_file_path(file_path):
166    r"""
167    Parse a file path created by create_file_path and return the result as a
168    dictionary.
169
170    This function is the complement to create_file_path.
171
172    Description of argument(s):
173    file_path                       The file_path.
174
175    Example use:
176    gp.print_var(boot_results_file_path)
177    file_path_data = parse_file_path(boot_results_file_path)
178    gp.print_var(file_path_data)
179
180    Program output.
181
182    boot_results_file_path:
183    /tmp/pgm_name.obmc_boot_test:openbmc_nickname.beye6:master_pid.2039:boot_re
184    sults
185    file_path_data:
186      file_path_data[dir_path]:                       /tmp/
187      file_path_data[pgm_name]:                       obmc_boot_test
188      file_path_data[openbmc_nickname]:               beye6
189      file_path_data[master_pid]:                     2039
190      file_path_data[boot_results]:
191    """
192
193    try:
194        result_dict = collections.OrderedDict()
195    except AttributeError:
196        result_dict = DotDict()
197
198    dir_path = os.path.dirname(file_path) + os.sep
199    file_path = os.path.basename(file_path)
200
201    result_dict['dir_path'] = dir_path
202
203    result_dict.update(split_to_dict(file_path))
204
205    return result_dict
206
207
208def parse_key_value(string,
209                    delim=":",
210                    strip=" ",
211                    to_lower=1,
212                    underscores=1):
213    r"""
214    Parse a key/value string and return as a key/value tuple.
215
216    This function is useful for parsing a line of program output or data that
217    is in the following form:
218    <key or variable name><delimiter><value>
219
220    An example of a key/value string would be as follows:
221
222    Current Limit State: No Active Power Limit
223
224    In the example shown, the delimiter is ":".  The resulting key would be as
225    follows:
226    Current Limit State
227
228    Note: If one were to take the default values of to_lower=1 and
229    underscores=1, the resulting key would be as follows:
230    current_limit_state
231
232    The to_lower and underscores arguments are provided for those who wish to
233    have their key names have the look and feel of python variable names.
234
235    The resulting value for the example above would be as follows:
236    No Active Power Limit
237
238    Another example:
239    name=Mike
240
241    In this case, the delim would be "=", the key is "name" and the value is
242    "Mike".
243
244    Description of argument(s):
245    string                          The string to be parsed.
246    delim                           The delimiter which separates the key from
247                                    the value.
248    strip                           The characters (if any) to strip from the
249                                    beginning and end of both the key and the
250                                    value.
251    to_lower                        Change the key name to lower case.
252    underscores                     Change any blanks found in the key name to
253                                    underscores.
254    """
255
256    pair = string.split(delim)
257
258    key = pair[0].strip(strip)
259    if len(pair) == 0:
260        value = ""
261    else:
262        value = delim.join(pair[1:]).strip(strip)
263
264    if to_lower:
265        key = key.lower()
266    if underscores:
267        key = re.sub(r" ", "_", key)
268
269    return key, value
270
271
272def key_value_list_to_dict(list,
273                           process_indent=0,
274                           **args):
275    r"""
276    Convert a list containing key/value strings or tuples to a dictionary and
277    return it.
278
279    See docstring of parse_key_value function for details on key/value strings.
280
281    Example usage:
282
283    For the following value of list:
284
285    list:
286      list[0]:          Current Limit State: No Active Power Limit
287      list[1]:          Exception actions:   Hard Power Off & Log Event to SEL
288      list[2]:          Power Limit:         0 Watts
289      list[3]:          Correction time:     0 milliseconds
290      list[4]:          Sampling period:     0 seconds
291
292    And the following call in python:
293
294    power_limit = key_value_outbuf_to_dict(list)
295
296    The resulting power_limit directory would look like this:
297
298    power_limit:
299      [current_limit_state]:        No Active Power Limit
300      [exception_actions]:          Hard Power Off & Log Event to SEL
301      [power_limit]:                0 Watts
302      [correction_time]:            0 milliseconds
303      [sampling_period]:            0 seconds
304
305    For the following list:
306
307    headers:
308      headers[0]:
309        headers[0][0]:           content-length
310        headers[0][1]:           559
311      headers[1]:
312        headers[1][0]:           x-xss-protection
313        headers[1][1]:           1; mode=block
314
315    And the following call in python:
316
317    headers_dict = key_value_list_to_dict(headers)
318
319    The resulting headers_dict would look like this:
320
321    headers_dict:
322      [content-length]:          559
323      [x-xss-protection]:        1; mode=block
324
325    Another example containing a sub-list (see process_indent description
326    below):
327
328    Provides Device SDRs      : yes
329    Additional Device Support :
330        Sensor Device
331        SEL Device
332        FRU Inventory Device
333        Chassis Device
334
335    Note that the 2 qualifications for containing a sub-list are met: 1)
336    'Additional Device Support' has no value and 2) The entries below it are
337    indented.  In this case those entries contain no delimiters (":") so they
338    will be processed as a list rather than as a dictionary.  The result would
339    be as follows:
340
341    mc_info:
342      mc_info[provides_device_sdrs]:            yes
343      mc_info[additional_device_support]:
344        mc_info[additional_device_support][0]:  Sensor Device
345        mc_info[additional_device_support][1]:  SEL Device
346        mc_info[additional_device_support][2]:  FRU Inventory Device
347        mc_info[additional_device_support][3]:  Chassis Device
348
349    Description of argument(s):
350    list                            A list of key/value strings.  (See
351                                    docstring of parse_key_value function for
352                                    details).
353    process_indent                  This indicates that indented
354                                    sub-dictionaries and sub-lists are to be
355                                    processed as such.  An entry may have a
356                                    sub-dict or sub-list if 1) It has no value
357                                    other than blank 2) There are entries
358                                    below it that are indented.  Note that
359                                    process_indent is not allowed for a list
360                                    of tuples (vs. a list of key/value
361                                    strings).
362    **args                          Arguments to be interpreted by
363                                    parse_key_value.  (See docstring of
364                                    parse_key_value function for details).
365    """
366
367    try:
368        result_dict = collections.OrderedDict()
369    except AttributeError:
370        result_dict = DotDict()
371
372    if not process_indent:
373        for entry in list:
374            if type(entry) is tuple:
375                key, value = entry
376            else:
377                key, value = parse_key_value(entry, **args)
378            result_dict[key] = value
379        return result_dict
380
381    # Process list while paying heed to indentation.
382    delim = args.get("delim", ":")
383    # Initialize "parent_" indentation level variables.
384    parent_indent = len(list[0]) - len(list[0].lstrip())
385    sub_list = []
386    for entry in list:
387        key, value = parse_key_value(entry, **args)
388
389        indent = len(entry) - len(entry.lstrip())
390
391        if indent > parent_indent and parent_value == "":
392            # This line is indented compared to the parent entry and the
393            # parent entry has no value.
394            # Append the entry to sub_list for later processing.
395            sub_list.append(str(entry))
396            continue
397
398        # Process any outstanding sub_list and add it to
399        # result_dict[parent_key].
400        if len(sub_list) > 0:
401            if any(delim in word for word in sub_list):
402                # If delim is found anywhere in the sub_list, we'll process
403                # as a sub-dictionary.
404                result_dict[parent_key] = key_value_list_to_dict(sub_list,
405                                                                 **args)
406            else:
407                result_dict[parent_key] = map(str.strip, sub_list)
408            del sub_list[:]
409
410        result_dict[key] = value
411
412        parent_key = key
413        parent_value = value
414        parent_indent = indent
415
416    # Any outstanding sub_list to be processed?
417    if len(sub_list) > 0:
418        if any(delim in word for word in sub_list):
419            # If delim is found anywhere in the sub_list, we'll process as a
420            # sub-dictionary.
421            result_dict[parent_key] = key_value_list_to_dict(sub_list, **args)
422        else:
423            result_dict[parent_key] = map(str.strip, sub_list)
424
425    return result_dict
426
427
428def key_value_outbuf_to_dict(out_buf,
429                             **args):
430    r"""
431    Convert a buffer with a key/value string on each line to a dictionary and
432    return it.
433
434    Each line in the out_buf should end with a \n.
435
436    See docstring of parse_key_value function for details on key/value strings.
437
438    Example usage:
439
440    For the following value of out_buf:
441
442    Current Limit State: No Active Power Limit
443    Exception actions:   Hard Power Off & Log Event to SEL
444    Power Limit:         0 Watts
445    Correction time:     0 milliseconds
446    Sampling period:     0 seconds
447
448    And the following call in python:
449
450    power_limit = key_value_outbuf_to_dict(out_buf)
451
452    The resulting power_limit directory would look like this:
453
454    power_limit:
455      [current_limit_state]:        No Active Power Limit
456      [exception_actions]:          Hard Power Off & Log Event to SEL
457      [power_limit]:                0 Watts
458      [correction_time]:            0 milliseconds
459      [sampling_period]:            0 seconds
460
461    Description of argument(s):
462    out_buf                         A buffer with a key/value string on each
463                                    line. (See docstring of parse_key_value
464                                    function for details).
465    **args                          Arguments to be interpreted by
466                                    parse_key_value.  (See docstring of
467                                    parse_key_value function for details).
468    """
469
470    # Create key_var_list and remove null entries.
471    key_var_list = list(filter(None, out_buf.split("\n")))
472    return key_value_list_to_dict(key_var_list, **args)
473
474
475def create_field_desc_regex(line):
476
477    r"""
478    Create a field descriptor regular expression based on the input line and
479    return it.
480
481    This function is designed for use by the list_to_report function (defined
482    below).
483
484    Example:
485
486    Given the following input line:
487
488    --------   ------------ ------------------ ------------------------
489
490    This function will return this regular expression:
491
492    (.{8})   (.{12}) (.{18}) (.{24})
493
494    This means that other report lines interpreted using the regular
495    expression are expected to have:
496    - An 8 character field
497    - 3 spaces
498    - A 12 character field
499    - One space
500    - An 18 character field
501    - One space
502    - A 24 character field
503
504    Description of argument(s):
505    line                            A line consisting of dashes to represent
506                                    fields and spaces to delimit fields.
507    """
508
509    # Split the line into a descriptors list.  Example:
510    # descriptors:
511    #  descriptors[0]:            --------
512    #  descriptors[1]:
513    #  descriptors[2]:
514    #  descriptors[3]:            ------------
515    #  descriptors[4]:            ------------------
516    #  descriptors[5]:            ------------------------
517    descriptors = line.split(" ")
518
519    # Create regexes list.  Example:
520    # regexes:
521    #  regexes[0]:                (.{8})
522    #  regexes[1]:
523    #  regexes[2]:
524    #  regexes[3]:                (.{12})
525    #  regexes[4]:                (.{18})
526    #  regexes[5]:                (.{24})
527    regexes = []
528    for descriptor in descriptors:
529        if descriptor == "":
530            regexes.append("")
531        else:
532            regexes.append("(.{" + str(len(descriptor)) + "})")
533
534    # Join the regexes list into a regex string.
535    field_desc_regex = ' '.join(regexes)
536
537    return field_desc_regex
538
539
540def list_to_report(report_list,
541                   to_lower=1,
542                   field_delim=None):
543    r"""
544    Convert a list containing report text lines to a report "object" and
545    return it.
546
547    The first entry in report_list must be a header line consisting of column
548    names delimited by white space.  No column name may contain white space.
549    The remaining report_list entries should contain tabular data which
550    corresponds to the column names.
551
552    A report object is a list where each entry is a dictionary whose keys are
553    the field names from the first entry in report_list.
554
555    Example:
556    Given the following report_list as input:
557
558    rl:
559      rl[0]: Filesystem           1K-blocks      Used Available Use% Mounted on
560      rl[1]: dev                     247120         0    247120   0% /dev
561      rl[2]: tmpfs                   248408     79792    168616  32% /run
562
563    This function will return a list of dictionaries as shown below:
564
565    df_report:
566      df_report[0]:
567        [filesystem]:                  dev
568        [1k-blocks]:                   247120
569        [used]:                        0
570        [available]:                   247120
571        [use%]:                        0%
572        [mounted]:                     /dev
573      df_report[1]:
574        [filesystem]:                  dev
575        [1k-blocks]:                   247120
576        [used]:                        0
577        [available]:                   247120
578        [use%]:                        0%
579        [mounted]:                     /dev
580
581    Notice that because "Mounted on" contains a space, "on" would be
582    considered the 7th field.  In this case, there is never any data in field
583    7 so things work out nicely.  A caller could do some pre-processing if
584    desired (e.g. change "Mounted on" to "Mounted_on").
585
586    Example 2:
587
588    If the 2nd line of report data is a series of dashes and spaces as in the
589    following example, that line will serve to delineate columns.
590
591    The 2nd line of data is like this:
592    ID                              status       size
593                                    tool,clientid,userid
594    -------- ------------ ------------------ ------------------------
595    20000001 in progress  0x7D0              ,,
596
597    Description of argument(s):
598    report_list                     A list where each entry is one line of
599                                    output from a report.  The first entry
600                                    must be a header line which contains
601                                    column names.  Column names may not
602                                    contain spaces.
603    to_lower                        Change the resulting key names to lower
604                                    case.
605    field_delim                     Indicates that there are field delimiters
606                                    in report_list entries (which should be
607                                    removed).
608    """
609
610    if len(report_list) <= 1:
611        # If we don't have at least a descriptor line and one line of data,
612        # return an empty array.
613        return []
614
615    if field_delim is not None:
616        report_list = [re.sub("\\|", "", line) for line in report_list]
617
618    header_line = report_list[0]
619    if to_lower:
620        header_line = header_line.lower()
621
622    field_desc_regex = ""
623    if re.match(r"^-[ -]*$", report_list[1]):
624        # We have a field descriptor line (as shown in example 2 above).
625        field_desc_regex = create_field_desc_regex(report_list[1])
626        field_desc_len = len(report_list[1])
627        pad_format_string = "%-" + str(field_desc_len) + "s"
628        # The field descriptor line has served its purpose.  Deleting it.
629        del report_list[1]
630
631    # Process the header line by creating a list of column names.
632    if field_desc_regex == "":
633        columns = header_line.split()
634    else:
635        # Pad the line with spaces on the right to facilitate processing with
636        # field_desc_regex.
637        header_line = pad_format_string % header_line
638        columns = list(map(str.strip,
639                           re.findall(field_desc_regex, header_line)[0]))
640
641    report_obj = []
642    for report_line in report_list[1:]:
643        if field_desc_regex == "":
644            line = report_line.split()
645        else:
646            # Pad the line with spaces on the right to facilitate processing
647            # with field_desc_regex.
648            report_line = pad_format_string % report_line
649            line = list(map(str.strip,
650                            re.findall(field_desc_regex, report_line)[0]))
651        try:
652            line_dict = collections.OrderedDict(zip(columns, line))
653        except AttributeError:
654            line_dict = DotDict(zip(columns, line))
655        report_obj.append(line_dict)
656
657    return report_obj
658
659
660def outbuf_to_report(out_buf,
661                     **args):
662    r"""
663    Convert a text buffer containing report lines to a report "object" and
664    return it.
665
666    Refer to list_to_report (above) for more details.
667
668    Example:
669
670    Given the following out_buf:
671
672    Filesystem                      1K-blocks      Used Available Use% Mounted
673                                    on
674    dev                             247120         0    247120   0% /dev
675    tmpfs                           248408     79792    168616  32% /run
676
677    This function will return a list of dictionaries as shown below:
678
679    df_report:
680      df_report[0]:
681        [filesystem]:                  dev
682        [1k-blocks]:                   247120
683        [used]:                        0
684        [available]:                   247120
685        [use%]:                        0%
686        [mounted]:                     /dev
687      df_report[1]:
688        [filesystem]:                  dev
689        [1k-blocks]:                   247120
690        [used]:                        0
691        [available]:                   247120
692        [use%]:                        0%
693        [mounted]:                     /dev
694
695    Other possible uses:
696    - Process the output of a ps command.
697    - Process the output of an ls command (the caller would need to supply
698      column names)
699
700    Description of argument(s):
701    out_buf                         A text report.  The first line must be a
702                                    header line which contains column names.
703                                    Column names may not contain spaces.
704    **args                          Arguments to be interpreted by
705                                    list_to_report.  (See docstring of
706                                    list_to_report function for details).
707    """
708
709    report_list = list(filter(None, out_buf.split("\n")))
710    return list_to_report(report_list, **args)
711
712
713def nested_get(key_name, structure):
714    r"""
715    Return a list of all values from the nested structure that have the given
716    key name.
717
718    Example:
719
720    Given a dictionary structure named "personnel" with the following contents:
721
722    personnel:
723      [manager]:
724        [last_name]:             Doe
725        [first_name]:            John
726      [accountant]:
727        [last_name]:             Smith
728        [first_name]:            Will
729
730    The following code...
731
732    last_names = nested_get('last_name', personnel)
733    print_var(last_names)
734
735    Would result in the following data returned:
736
737    last_names:
738      last_names[0]:             Doe
739      last_names[1]:             Smith
740
741    Description of argument(s):
742    key_name                        The key name (e.g. 'last_name').
743    structure                       Any nested combination of lists or
744                                    dictionaries (e.g. a dictionary, a
745                                    dictionary of dictionaries, a list of
746                                    dictionaries, etc.).  This function will
747                                    locate the given key at any level within
748                                    the structure and include its value in the
749                                    returned list.
750    """
751
752    result = []
753    if type(structure) is list:
754        for entry in structure:
755            result += nested_get(key_name, entry)
756        return result
757    elif gp.is_dict(structure):
758        for key, value in structure.items():
759            result += nested_get(key_name, value)
760            if key == key_name:
761                result.append(value)
762
763    return result
764
765
766def match_struct(structure, match_dict, regex=False):
767    r"""
768    Return True or False to indicate whether the structure matches the match
769    dictionary.
770
771    Example:
772
773    Given a dictionary structure named "personnel" with the following contents:
774
775    personnel:
776      [manager]:
777        [last_name]:             Doe
778        [first_name]:            John
779      [accountant]:
780        [last_name]:             Smith
781        [first_name]:            Will
782
783    The following call would return True.
784
785    match_struct(personnel, {'last_name': '^Doe$'}, regex=True)
786
787    Whereas the following call would return False.
788
789    match_struct(personnel, {'last_name': 'Johnson'}, regex=True)
790
791    Description of argument(s):
792    structure                       Any nested combination of lists or
793                                    dictionaries.  See the prolog of
794                                    get_nested() for details.
795    match_dict                      Each key/value pair in match_dict must
796                                    exist somewhere in the structure for the
797                                    structure to be considered a match.  A
798                                    match value of None is considered a
799                                    special case where the structure would be
800                                    considered a match only if the key in
801                                    question is found nowhere in the structure.
802    regex                           Indicates whether the values in the
803                                    match_dict should be interpreted as
804                                    regular expressions.
805    """
806
807    # The structure must match for each match_dict entry to be considered a
808    # match.  Therefore, any failure to match is grounds for returning False.
809    for match_key, match_value in match_dict.items():
810        struct_key_values = nested_get(match_key, structure)
811        if match_value is None:
812            # Handle this as special case.
813            if len(struct_key_values) != 0:
814                return False
815        else:
816            if len(struct_key_values) == 0:
817                return False
818            if regex:
819                matches = [x for x in struct_key_values
820                           if re.search(match_value, str(x))]
821                if not matches:
822                    return False
823            elif match_value not in struct_key_values:
824                return False
825
826    return True
827
828
829def filter_struct(structure, filter_dict, regex=False, invert=False):
830    r"""
831    Filter the structure by removing any entries that do NOT contain the
832    keys/values specified in filter_dict and return the result.
833
834    The selection process is directed only at the first-level entries of the
835    structure.
836
837    Example:
838
839    Given a dictionary named "properties" that has the following structure:
840
841    properties:
842      [/redfish/v1/Systems/system/Processors]:
843        [Members]:
844          [0]:
845            [@odata.id]:
846            /redfish/v1/Systems/system/Processors/cpu0
847          [1]:
848            [@odata.id]:
849            /redfish/v1/Systems/system/Processors/cpu1
850      [/redfish/v1/Systems/system/Processors/cpu0]:
851        [Status]:
852          [State]:                                    Enabled
853          [Health]:                                   OK
854      [/redfish/v1/Systems/system/Processors/cpu1]:
855        [Status]:
856          [State]:                                    Enabled
857          [Health]:                                   Bad
858
859    The following call:
860
861    properties = filter_struct(properties, "[('Health', 'OK')]")
862
863    Would return a new properties dictionary that looks like this:
864
865    properties:
866      [/redfish/v1/Systems/system/Processors/cpu0]:
867        [Status]:
868          [State]:                                    Enabled
869          [Health]:                                   OK
870
871    Note that the first item in the original properties directory had no key
872    anywhere in the structure named "Health".  Therefore, that item failed to
873    make the cut.  The next item did have a key named "Health" whose value was
874    "OK" so it was included in the new structure.  The third item had a key
875    named "Health" but its value was not "OK" so it also failed to make the
876    cut.
877
878    Description of argument(s):
879    structure                       Any nested combination of lists or
880                                    dictionaries.  See the prolog of
881                                    get_nested() for details.
882    filter_dict                     For each key/value pair in filter_dict,
883                                    each entry in structure must contain the
884                                    same key/value pair at some level.  A
885                                    filter_dict value of None is treated as a
886                                    special case.  Taking the example shown
887                                    above, [('State', None)] would mean that
888                                    the result should only contain records
889                                    that have no State key at all.
890    regex                           Indicates whether the values in the
891                                    filter_dict should be interpreted as
892                                    regular expressions.
893    invert                          Invert the results.  Instead of including
894                                    only matching entries in the results,
895                                    include only NON-matching entries in the
896                                    results.
897    """
898
899    # Convert filter_dict from a string containing a python object definition
900    # to an actual python object (if warranted).
901    filter_dict = fa.source_to_object(filter_dict)
902
903    # Determine whether structure is a list or a dictionary and process
904    # accordingly.  The result returned will be of the same type as the
905    # structure.
906    if type(structure) is list:
907        result = []
908        for element in structure:
909            if match_struct(element, filter_dict, regex) != invert:
910                result.append(element)
911    else:
912        try:
913            result = collections.OrderedDict()
914        except AttributeError:
915            result = DotDict()
916        for struct_key, struct_value in structure.items():
917            if match_struct(struct_value, filter_dict, regex) != invert:
918                result[struct_key] = struct_value
919
920    return result
921