blob: 4cd211e3224ac87feb4e3cb21b556dfd48b2a336 [file] [log] [blame]
Michael Walshced4eb02017-09-19 16:49:13 -05001#!/usr/bin/env python
2
3r"""
4Define variable manipulation functions.
5"""
6
7import os
Michael Walsh05c68d92017-09-20 16:36:37 -05008import re
Michael Walshced4eb02017-09-19 16:49:13 -05009
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
Michael Walsh074b7652019-05-22 16:25:38 -050019import func_args as fa
Michael Walshced4eb02017-09-19 16:49:13 -050020
21
22def create_var_dict(*args):
Michael Walshced4eb02017-09-19 16:49:13 -050023 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):
Michael Walshced4eb02017-09-19 16:49:13 -050067 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)
Michael Walshc2762f62019-05-17 15:21:35 -050082 gp.print_var(str1)
Michael Walshced4eb02017-09-19 16:49:13 -050083
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
Gunnar Mills096cd562018-03-26 10:19:12 -050094 dict.items()])
Michael Walshced4eb02017-09-19 16:49:13 -050095
96
97def split_to_dict(string,
98 record_delim=default_record_delim,
99 key_val_delim=default_key_val_delim):
Michael Walshced4eb02017-09-19 16:49:13 -0500100 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=""):
Michael Walshced4eb02017-09-19 16:49:13 -0500149 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):
Michael Walshced4eb02017-09-19 16:49:13 -0500166 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:
Michael Walshc2762f62019-05-17 15:21:35 -0500176 gp.print_var(boot_results_file_path)
Michael Walshced4eb02017-09-19 16:49:13 -0500177 file_path_data = parse_file_path(boot_results_file_path)
Michael Walshc2762f62019-05-17 15:21:35 -0500178 gp.print_var(file_path_data)
Michael Walshced4eb02017-09-19 16:49:13 -0500179
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
Michael Walsh05c68d92017-09-20 16:36:37 -0500206
207
208def parse_key_value(string,
209 delim=":",
210 strip=" ",
211 to_lower=1,
212 underscores=1):
Michael Walsh05c68d92017-09-20 16:36:37 -0500213 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:
MICHAEL J. WALSH9509a0f2018-02-08 11:08:52 -0600262 value = delim.join(pair[1:]).strip(strip)
Michael Walsh05c68d92017-09-20 16:36:37 -0500263
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,
Michael Walshcad07132018-02-19 17:28:01 -0600273 process_indent=0,
Michael Walsh05c68d92017-09-20 16:36:37 -0500274 **args):
Michael Walsh05c68d92017-09-20 16:36:37 -0500275 r"""
Michael Walsh1db86872019-04-16 11:48:25 -0500276 Convert a list containing key/value strings or tuples to a dictionary and
277 return it.
Michael Walsh05c68d92017-09-20 16:36:37 -0500278
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
Michael Walsh1db86872019-04-16 11:48:25 -0500305 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
Michael Walshcad07132018-02-19 17:28:01 -0600325 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
Michael Walsh05c68d92017-09-20 16:36:37 -0500349 Description of argument(s):
350 list A list of key/value strings. (See
351 docstring of parse_key_value function for
352 details).
Michael Walshcad07132018-02-19 17:28:01 -0600353 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
Michael Walsh1db86872019-04-16 11:48:25 -0500358 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).
Michael Walsh05c68d92017-09-20 16:36:37 -0500362 **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
Michael Walshcad07132018-02-19 17:28:01 -0600372 if not process_indent:
373 for entry in list:
Michael Walsh1db86872019-04-16 11:48:25 -0500374 if type(entry) is tuple:
375 key, value = entry
376 else:
377 key, value = parse_key_value(entry, **args)
Michael Walshcad07132018-02-19 17:28:01 -0600378 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 = []
Michael Walsh05c68d92017-09-20 16:36:37 -0500386 for entry in list:
Michael Walshc1dfc782017-09-26 16:08:51 -0500387 key, value = parse_key_value(entry, **args)
Michael Walshcad07132018-02-19 17:28:01 -0600388
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
Michael Walsh05c68d92017-09-20 16:36:37 -0500410 result_dict[key] = value
411
Michael Walshcad07132018-02-19 17:28:01 -0600412 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
Michael Walsh05c68d92017-09-20 16:36:37 -0500425 return result_dict
426
427
428def key_value_outbuf_to_dict(out_buf,
429 **args):
Michael Walsh05c68d92017-09-20 16:36:37 -0500430 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")))
Michael Walshc1dfc782017-09-26 16:08:51 -0500472 return key_value_list_to_dict(key_var_list, **args)
Michael Walshdb560d42017-11-20 16:42:49 -0600473
474
Michael Walshdc978822018-07-12 15:34:13 -0500475def 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
Michael Walshdb560d42017-11-20 16:42:49 -0600540def list_to_report(report_list,
Michael Walsh64043d52018-09-21 16:40:44 -0500541 to_lower=1,
542 field_delim=None):
Michael Walshdb560d42017-11-20 16:42:49 -0600543 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
Michael Walshdc978822018-07-12 15:34:13 -0500586 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
Michael Walshdb560d42017-11-20 16:42:49 -0600597 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.
Michael Walsh64043d52018-09-21 16:40:44 -0500605 field_delim Indicates that there are field delimiters
606 in report_list entries (which should be
607 removed).
Michael Walshdb560d42017-11-20 16:42:49 -0600608 """
609
Michael Walshdc978822018-07-12 15:34:13 -0500610 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
Michael Walsh64043d52018-09-21 16:40:44 -0500615 if field_delim is not None:
616 report_list = [re.sub("\\|", "", line) for line in report_list]
617
Michael Walshdb560d42017-11-20 16:42:49 -0600618 header_line = report_list[0]
619 if to_lower:
620 header_line = header_line.lower()
Michael Walshdc978822018-07-12 15:34:13 -0500621
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
Michael Walsh0a124e82019-10-21 15:38:44 -0500638 columns = list(map(str.strip,
639 re.findall(field_desc_regex, header_line)[0]))
Michael Walshdb560d42017-11-20 16:42:49 -0600640
641 report_obj = []
642 for report_line in report_list[1:]:
Michael Walshdc978822018-07-12 15:34:13 -0500643 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
Michael Walsh0a124e82019-10-21 15:38:44 -0500649 line = list(map(str.strip,
650 re.findall(field_desc_regex, report_line)[0]))
Michael Walshdb560d42017-11-20 16:42:49 -0600651 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):
Michael Walshdb560d42017-11-20 16:42:49 -0600662 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
Michael Walshdc978822018-07-12 15:34:13 -0500672 Filesystem 1K-blocks Used Available Use% Mounted
673 on
674 dev 247120 0 247120 0% /dev
675 tmpfs 248408 79792 168616 32% /run
Michael Walshdb560d42017-11-20 16:42:49 -0600676
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
Michael Walshdc978822018-07-12 15:34:13 -0500698 column names)
Michael Walshdb560d42017-11-20 16:42:49 -0600699
700 Description of argument(s):
Michael Walshdc978822018-07-12 15:34:13 -0500701 out_buf A text report. The first line must be a
Michael Walshdb560d42017-11-20 16:42:49 -0600702 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
Michael Walsh255181c2018-08-07 15:06:23 -0500709 report_list = list(filter(None, out_buf.split("\n")))
Michael Walshdb560d42017-11-20 16:42:49 -0600710 return list_to_report(report_list, **args)
Michael Walsh7822b9e2019-03-12 16:34:38 -0500711
712
Michael Walsh46ef0a22019-06-11 15:44:49 -0500713def nested_get(key_name, structure):
Michael Walsh7822b9e2019-03-12 16:34:38 -0500714 r"""
Michael Walsh46ef0a22019-06-11 15:44:49 -0500715 Return a list of all values from the nested structure that have the given
716 key name.
Michael Walsh7822b9e2019-03-12 16:34:38 -0500717
718 Example:
719
Michael Walsh46ef0a22019-06-11 15:44:49 -0500720 Given a dictionary structure named "personnel" with the following contents:
Michael Walsh7822b9e2019-03-12 16:34:38 -0500721
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
Michael Walsh46ef0a22019-06-11 15:44:49 -0500735 Would result in the following data returned:
Michael Walsh7822b9e2019-03-12 16:34:38 -0500736
737 last_names:
738 last_names[0]: Doe
739 last_names[1]: Smith
740
741 Description of argument(s):
Michael Walsh46ef0a22019-06-11 15:44:49 -0500742 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
George Keishinge0c5ec32019-06-13 09:45:29 -0500748 the structure and include its value in the
Michael Walsh46ef0a22019-06-11 15:44:49 -0500749 returned list.
Michael Walsh7822b9e2019-03-12 16:34:38 -0500750 """
751
752 result = []
Michael Walshd882cdc2019-04-24 16:46:34 -0500753 if type(structure) is list:
754 for entry in structure:
Michael Walsh46ef0a22019-06-11 15:44:49 -0500755 result += nested_get(key_name, entry)
Michael Walshd882cdc2019-04-24 16:46:34 -0500756 return result
Michael Walsh46ef0a22019-06-11 15:44:49 -0500757 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)
Michael Walsh7822b9e2019-03-12 16:34:38 -0500762
763 return result
Michael Walsh074b7652019-05-22 16:25:38 -0500764
765
Michael Walsh46ef0a22019-06-11 15:44:49 -0500766def 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
Michael Walsh399df5a2019-06-21 11:23:54 -0500829def filter_struct(structure, filter_dict, regex=False, invert=False):
Michael Walsh074b7652019-05-22 16:25:38 -0500830 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
Michael Walsh46ef0a22019-06-11 15:44:49 -0500834 The selection process is directed only at the first-level entries of the
835 structure.
836
Michael Walsh074b7652019-05-22 16:25:38 -0500837 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):
Michael Walsh46ef0a22019-06-11 15:44:49 -0500879 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.
Michael Walsh399df5a2019-06-21 11:23:54 -0500893 invert Invert the results. Instead of including
894 only matching entries in the results,
895 include only NON-matching entries in the
896 results.
Michael Walsh074b7652019-05-22 16:25:38 -0500897 """
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
Michael Walsh46ef0a22019-06-11 15:44:49 -0500903 # 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.
Michael Walsh074b7652019-05-22 16:25:38 -0500906 if type(structure) is list:
907 result = []
Michael Walsh46ef0a22019-06-11 15:44:49 -0500908 for element in structure:
Michael Walsh399df5a2019-06-21 11:23:54 -0500909 if match_struct(element, filter_dict, regex) != invert:
Michael Walsh46ef0a22019-06-11 15:44:49 -0500910 result.append(element)
Michael Walsh074b7652019-05-22 16:25:38 -0500911 else:
Michael Walsh074b7652019-05-22 16:25:38 -0500912 try:
913 result = collections.OrderedDict()
914 except AttributeError:
915 result = DotDict()
916 for struct_key, struct_value in structure.items():
Michael Walsh399df5a2019-06-21 11:23:54 -0500917 if match_struct(struct_value, filter_dict, regex) != invert:
Michael Walsh074b7652019-05-22 16:25:38 -0500918 result[struct_key] = struct_value
919
920 return result