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