blob: 35fb594e7e54554be1ab96d296950b83fd8f0313 [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
638 columns = map(str.strip, re.findall(field_desc_regex, header_line)[0])
Michael Walshdb560d42017-11-20 16:42:49 -0600639
640 report_obj = []
641 for report_line in report_list[1:]:
Michael Walshdc978822018-07-12 15:34:13 -0500642 if field_desc_regex == "":
643 line = report_line.split()
644 else:
645 # Pad the line with spaces on the right to facilitate processing
646 # with field_desc_regex.
647 report_line = pad_format_string % report_line
648 line = map(str.strip, re.findall(field_desc_regex, report_line)[0])
Michael Walshdb560d42017-11-20 16:42:49 -0600649 try:
650 line_dict = collections.OrderedDict(zip(columns, line))
651 except AttributeError:
652 line_dict = DotDict(zip(columns, line))
653 report_obj.append(line_dict)
654
655 return report_obj
656
657
658def outbuf_to_report(out_buf,
659 **args):
Michael Walshdb560d42017-11-20 16:42:49 -0600660 r"""
661 Convert a text buffer containing report lines to a report "object" and
662 return it.
663
664 Refer to list_to_report (above) for more details.
665
666 Example:
667
668 Given the following out_buf:
669
Michael Walshdc978822018-07-12 15:34:13 -0500670 Filesystem 1K-blocks Used Available Use% Mounted
671 on
672 dev 247120 0 247120 0% /dev
673 tmpfs 248408 79792 168616 32% /run
Michael Walshdb560d42017-11-20 16:42:49 -0600674
675 This function will return a list of dictionaries as shown below:
676
677 df_report:
678 df_report[0]:
679 [filesystem]: dev
680 [1k-blocks]: 247120
681 [used]: 0
682 [available]: 247120
683 [use%]: 0%
684 [mounted]: /dev
685 df_report[1]:
686 [filesystem]: dev
687 [1k-blocks]: 247120
688 [used]: 0
689 [available]: 247120
690 [use%]: 0%
691 [mounted]: /dev
692
693 Other possible uses:
694 - Process the output of a ps command.
695 - Process the output of an ls command (the caller would need to supply
Michael Walshdc978822018-07-12 15:34:13 -0500696 column names)
Michael Walshdb560d42017-11-20 16:42:49 -0600697
698 Description of argument(s):
Michael Walshdc978822018-07-12 15:34:13 -0500699 out_buf A text report. The first line must be a
Michael Walshdb560d42017-11-20 16:42:49 -0600700 header line which contains column names.
701 Column names may not contain spaces.
702 **args Arguments to be interpreted by
703 list_to_report. (See docstring of
704 list_to_report function for details).
705 """
706
Michael Walsh255181c2018-08-07 15:06:23 -0500707 report_list = list(filter(None, out_buf.split("\n")))
Michael Walshdb560d42017-11-20 16:42:49 -0600708 return list_to_report(report_list, **args)
Michael Walsh7822b9e2019-03-12 16:34:38 -0500709
710
Michael Walsh46ef0a22019-06-11 15:44:49 -0500711def nested_get(key_name, structure):
Michael Walsh7822b9e2019-03-12 16:34:38 -0500712 r"""
Michael Walsh46ef0a22019-06-11 15:44:49 -0500713 Return a list of all values from the nested structure that have the given
714 key name.
Michael Walsh7822b9e2019-03-12 16:34:38 -0500715
716 Example:
717
Michael Walsh46ef0a22019-06-11 15:44:49 -0500718 Given a dictionary structure named "personnel" with the following contents:
Michael Walsh7822b9e2019-03-12 16:34:38 -0500719
720 personnel:
721 [manager]:
722 [last_name]: Doe
723 [first_name]: John
724 [accountant]:
725 [last_name]: Smith
726 [first_name]: Will
727
728 The following code...
729
730 last_names = nested_get('last_name', personnel)
731 print_var(last_names)
732
Michael Walsh46ef0a22019-06-11 15:44:49 -0500733 Would result in the following data returned:
Michael Walsh7822b9e2019-03-12 16:34:38 -0500734
735 last_names:
736 last_names[0]: Doe
737 last_names[1]: Smith
738
739 Description of argument(s):
Michael Walsh46ef0a22019-06-11 15:44:49 -0500740 key_name The key name (e.g. 'last_name').
741 structure Any nested combination of lists or
742 dictionaries (e.g. a dictionary, a
743 dictionary of dictionaries, a list of
744 dictionaries, etc.). This function will
745 locate the given key at any level within
George Keishinge0c5ec32019-06-13 09:45:29 -0500746 the structure and include its value in the
Michael Walsh46ef0a22019-06-11 15:44:49 -0500747 returned list.
Michael Walsh7822b9e2019-03-12 16:34:38 -0500748 """
749
750 result = []
Michael Walshd882cdc2019-04-24 16:46:34 -0500751 if type(structure) is list:
752 for entry in structure:
Michael Walsh46ef0a22019-06-11 15:44:49 -0500753 result += nested_get(key_name, entry)
Michael Walshd882cdc2019-04-24 16:46:34 -0500754 return result
Michael Walsh46ef0a22019-06-11 15:44:49 -0500755 elif gp.is_dict(structure):
756 for key, value in structure.items():
757 result += nested_get(key_name, value)
758 if key == key_name:
759 result.append(value)
Michael Walsh7822b9e2019-03-12 16:34:38 -0500760
761 return result
Michael Walsh074b7652019-05-22 16:25:38 -0500762
763
Michael Walsh46ef0a22019-06-11 15:44:49 -0500764def match_struct(structure, match_dict, regex=False):
765 r"""
766 Return True or False to indicate whether the structure matches the match
767 dictionary.
768
769 Example:
770
771 Given a dictionary structure named "personnel" with the following contents:
772
773 personnel:
774 [manager]:
775 [last_name]: Doe
776 [first_name]: John
777 [accountant]:
778 [last_name]: Smith
779 [first_name]: Will
780
781 The following call would return True.
782
783 match_struct(personnel, {'last_name': '^Doe$'}, regex=True)
784
785 Whereas the following call would return False.
786
787 match_struct(personnel, {'last_name': 'Johnson'}, regex=True)
788
789 Description of argument(s):
790 structure Any nested combination of lists or
791 dictionaries. See the prolog of
792 get_nested() for details.
793 match_dict Each key/value pair in match_dict must
794 exist somewhere in the structure for the
795 structure to be considered a match. A
796 match value of None is considered a
797 special case where the structure would be
798 considered a match only if the key in
799 question is found nowhere in the structure.
800 regex Indicates whether the values in the
801 match_dict should be interpreted as
802 regular expressions.
803 """
804
805 # The structure must match for each match_dict entry to be considered a
806 # match. Therefore, any failure to match is grounds for returning False.
807 for match_key, match_value in match_dict.items():
808 struct_key_values = nested_get(match_key, structure)
809 if match_value is None:
810 # Handle this as special case.
811 if len(struct_key_values) != 0:
812 return False
813 else:
814 if len(struct_key_values) == 0:
815 return False
816 if regex:
817 matches = [x for x in struct_key_values
818 if re.search(match_value, str(x))]
819 if not matches:
820 return False
821 elif match_value not in struct_key_values:
822 return False
823
824 return True
825
826
Michael Walsh399df5a2019-06-21 11:23:54 -0500827def filter_struct(structure, filter_dict, regex=False, invert=False):
Michael Walsh074b7652019-05-22 16:25:38 -0500828 r"""
829 Filter the structure by removing any entries that do NOT contain the
830 keys/values specified in filter_dict and return the result.
831
Michael Walsh46ef0a22019-06-11 15:44:49 -0500832 The selection process is directed only at the first-level entries of the
833 structure.
834
Michael Walsh074b7652019-05-22 16:25:38 -0500835 Example:
836
837 Given a dictionary named "properties" that has the following structure:
838
839 properties:
840 [/redfish/v1/Systems/system/Processors]:
841 [Members]:
842 [0]:
843 [@odata.id]:
844 /redfish/v1/Systems/system/Processors/cpu0
845 [1]:
846 [@odata.id]:
847 /redfish/v1/Systems/system/Processors/cpu1
848 [/redfish/v1/Systems/system/Processors/cpu0]:
849 [Status]:
850 [State]: Enabled
851 [Health]: OK
852 [/redfish/v1/Systems/system/Processors/cpu1]:
853 [Status]:
854 [State]: Enabled
855 [Health]: Bad
856
857 The following call:
858
859 properties = filter_struct(properties, "[('Health', 'OK')]")
860
861 Would return a new properties dictionary that looks like this:
862
863 properties:
864 [/redfish/v1/Systems/system/Processors/cpu0]:
865 [Status]:
866 [State]: Enabled
867 [Health]: OK
868
869 Note that the first item in the original properties directory had no key
870 anywhere in the structure named "Health". Therefore, that item failed to
871 make the cut. The next item did have a key named "Health" whose value was
872 "OK" so it was included in the new structure. The third item had a key
873 named "Health" but its value was not "OK" so it also failed to make the
874 cut.
875
876 Description of argument(s):
Michael Walsh46ef0a22019-06-11 15:44:49 -0500877 structure Any nested combination of lists or
878 dictionaries. See the prolog of
879 get_nested() for details.
880 filter_dict For each key/value pair in filter_dict,
881 each entry in structure must contain the
882 same key/value pair at some level. A
883 filter_dict value of None is treated as a
884 special case. Taking the example shown
885 above, [('State', None)] would mean that
886 the result should only contain records
887 that have no State key at all.
888 regex Indicates whether the values in the
889 filter_dict should be interpreted as
890 regular expressions.
Michael Walsh399df5a2019-06-21 11:23:54 -0500891 invert Invert the results. Instead of including
892 only matching entries in the results,
893 include only NON-matching entries in the
894 results.
Michael Walsh074b7652019-05-22 16:25:38 -0500895 """
896
897 # Convert filter_dict from a string containing a python object definition
898 # to an actual python object (if warranted).
899 filter_dict = fa.source_to_object(filter_dict)
900
Michael Walsh46ef0a22019-06-11 15:44:49 -0500901 # Determine whether structure is a list or a dictionary and process
902 # accordingly. The result returned will be of the same type as the
903 # structure.
Michael Walsh074b7652019-05-22 16:25:38 -0500904 if type(structure) is list:
905 result = []
Michael Walsh46ef0a22019-06-11 15:44:49 -0500906 for element in structure:
Michael Walsh399df5a2019-06-21 11:23:54 -0500907 if match_struct(element, filter_dict, regex) != invert:
Michael Walsh46ef0a22019-06-11 15:44:49 -0500908 result.append(element)
Michael Walsh074b7652019-05-22 16:25:38 -0500909 else:
Michael Walsh074b7652019-05-22 16:25:38 -0500910 try:
911 result = collections.OrderedDict()
912 except AttributeError:
913 result = DotDict()
914 for struct_key, struct_value in structure.items():
Michael Walsh399df5a2019-06-21 11:23:54 -0500915 if match_struct(struct_value, filter_dict, regex) != invert:
Michael Walsh074b7652019-05-22 16:25:38 -0500916 result[struct_key] = struct_value
917
918 return result