PAL: Reverse download order for trending
[csit.git] / resources / tools / presentation / specification_parser.py
1 # Copyright (c) 2020 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Specification
15
16 Parsing of the specification YAML file.
17 """
18
19
20 import logging
21 from pprint import pformat
22
23 from yaml import load, FullLoader, YAMLError
24
25 from pal_errors import PresentationError
26 from pal_utils import (
27     get_last_successful_build_nr, get_last_completed_build_number
28 )
29
30
31 class Specification:
32     """Specification of Presentation and analytics layer.
33
34     - based on specification specified in the specification YAML file
35     - presentation and analytics layer is model driven
36     """
37
38     # Tags are used in specification YAML file and replaced while the file is
39     # parsed.
40     TAG_OPENER = u"{"
41     TAG_CLOSER = u"}"
42
43     def __init__(self, cfg_file):
44         """Initialization.
45
46         :param cfg_file: File handler for the specification YAML file.
47         :type cfg_file: BinaryIO
48         """
49         self._cfg_file = cfg_file
50         self._cfg_yaml = None
51
52         self._specification = {
53             u"environment": dict(),
54             u"configuration": dict(),
55             u"static": dict(),
56             u"input": dict(),
57             u"output": dict(),
58             u"tables": list(),
59             u"plots": list(),
60             u"files": list(),
61             u"cpta": dict()
62         }
63
64     @property
65     def specification(self):
66         """Getter - specification.
67
68         :returns: Specification.
69         :rtype: dict
70         """
71         return self._specification
72
73     @property
74     def environment(self):
75         """Getter - environment.
76
77         :returns: Environment specification.
78         :rtype: dict
79         """
80         return self._specification[u"environment"]
81
82     @property
83     def configuration(self):
84         """Getter - configuration.
85
86         :returns: Configuration of PAL.
87         :rtype: dict
88         """
89         return self._specification[u"configuration"]
90
91     @property
92     def static(self):
93         """Getter - static content.
94
95         :returns: Static content specification.
96         :rtype: dict
97         """
98         return self._specification[u"static"]
99
100     @property
101     def mapping(self):
102         """Getter - Mapping.
103
104         :returns: Mapping of the old names of test cases to the new (actual)
105             one.
106         :rtype: dict
107         """
108         return self._specification[u"configuration"][u"mapping"]
109
110     @property
111     def ignore(self):
112         """Getter - Ignore list.
113
114         :returns: List of ignored test cases.
115         :rtype: list
116         """
117         return self._specification[u"configuration"][u"ignore"]
118
119     @property
120     def alerting(self):
121         """Getter - Alerting.
122
123         :returns: Specification of alerts.
124         :rtype: dict
125         """
126         return self._specification[u"configuration"][u"alerting"]
127
128     @property
129     def input(self):
130         """Getter - specification - inputs.
131         - jobs and builds.
132
133         :returns: Inputs.
134         :rtype: dict
135         """
136         return self._specification[u"input"]
137
138     @input.setter
139     def input(self, new_value):
140         """Setter - specification - inputs.
141
142         :param new_value: New value to be set.
143         :type new_value: dict
144         """
145         self._specification[u"input"] = new_value
146
147     @property
148     def builds(self):
149         """Getter - builds defined in specification.
150
151         :returns: Builds defined in the specification.
152         :rtype: dict
153         """
154         return self.input[u"builds"]
155
156     @builds.setter
157     def builds(self, new_value):
158         """Setter - builds defined in specification.
159
160         :param new_value: New value to be set.
161         :type new_value: dict
162         """
163         self.input[u"builds"] = new_value
164
165     def add_build(self, job, build):
166         """Add a build to the specification.
167
168         :param job: The job which run the build.
169         :param build: The build to be added.
170         :type job: str
171         :type build: dict
172         """
173         if self._specification[u"input"][u"builds"].get(job, None) is None:
174             self._specification[u"input"][u"builds"][job] = list()
175         self._specification[u"input"][u"builds"][job].append(build)
176
177     @property
178     def output(self):
179         """Getter - specification - output formats and versions to be generated.
180         - formats: html, pdf
181         - versions: full, ...
182
183         :returns: Outputs to be generated.
184         :rtype: dict
185         """
186         return self._specification[u"output"]
187
188     @property
189     def tables(self):
190         """Getter - tables to be generated.
191
192         :returns: List of specifications of tables to be generated.
193         :rtype: list
194         """
195         return self._specification[u"tables"]
196
197     @property
198     def plots(self):
199         """Getter - plots to be generated.
200
201         :returns: List of specifications of plots to be generated.
202         :rtype: list
203         """
204         return self._specification[u"plots"]
205
206     @property
207     def files(self):
208         """Getter - files to be generated.
209
210         :returns: List of specifications of files to be generated.
211         :rtype: list
212         """
213         return self._specification[u"files"]
214
215     @property
216     def cpta(self):
217         """Getter - Continuous Performance Trending and Analysis to be
218         generated.
219
220         :returns: List of specifications of Continuous Performance Trending and
221             Analysis to be generated.
222         :rtype: list
223         """
224         return self._specification[u"cpta"]
225
226     def set_input_state(self, job, build_nr, state):
227         """Set the state of input
228
229         :param job: Job name.
230         :param build_nr: Build number.
231         :param state: The new input state.
232         :type job: str
233         :type build_nr: int
234         :type state: str
235         :raises: PresentationError if wrong job and/or build is provided.
236         """
237
238         try:
239             for build in self._specification[u"input"][u"builds"][job]:
240                 if build[u"build"] == build_nr:
241                     build[u"status"] = state
242                     break
243             else:
244                 raise PresentationError(
245                     f"Build {build_nr} is not defined for job {job} in "
246                     f"specification file."
247                 )
248         except KeyError:
249             raise PresentationError(
250                 f"Job {job} and build {build_nr} is not defined in "
251                 f"specification file."
252             )
253
254     def set_input_file_name(self, job, build_nr, file_name):
255         """Set the state of input
256
257         :param job: Job name.
258         :param build_nr: Build number.
259         :param file_name: The new file name.
260         :type job: str
261         :type build_nr: int
262         :type file_name: str
263         :raises: PresentationError if wrong job and/or build is provided.
264         """
265
266         try:
267             for build in self._specification[u"input"][u"builds"][job]:
268                 if build[u"build"] == build_nr:
269                     build[u"file-name"] = file_name
270                     break
271             else:
272                 raise PresentationError(
273                     f"Build {build_nr} is not defined for job {job} in "
274                     f"specification file."
275                 )
276         except KeyError:
277             raise PresentationError(
278                 f"Job {job} and build {build_nr} is not defined in "
279                 f"specification file."
280             )
281
282     def _get_build_number(self, job, build_type):
283         """Get the number of the job defined by its name:
284          - lastSuccessfulBuild
285          - lastCompletedBuild
286
287         :param job: Job name.
288         :param build_type: Build type:
289          - lastSuccessfulBuild
290          - lastCompletedBuild
291         :type job" str
292         :raises PresentationError: If it is not possible to get the build
293             number.
294         :returns: The build number.
295         :rtype: int
296         """
297
298         # defined as a range <start, end>
299         if build_type == u"lastSuccessfulBuild":
300             # defined as a range <start, lastSuccessfulBuild>
301             ret_code, build_nr, _ = get_last_successful_build_nr(
302                 self.environment[u"urls"][u"URL[JENKINS,CSIT]"], job)
303         elif build_type == u"lastCompletedBuild":
304             # defined as a range <start, lastCompletedBuild>
305             ret_code, build_nr, _ = get_last_completed_build_number(
306                 self.environment[u"urls"][u"URL[JENKINS,CSIT]"], job)
307         else:
308             raise PresentationError(f"Not supported build type: {build_type}")
309         if ret_code != 0:
310             raise PresentationError(u"Not possible to get the number of the "
311                                     u"build number.")
312         try:
313             build_nr = int(build_nr)
314             return build_nr
315         except ValueError as err:
316             raise PresentationError(
317                 f"Not possible to get the number of the build number. Reason:\n"
318                 f"{repr(err)}"
319             )
320
321     def _get_type_index(self, item_type):
322         """Get index of item type (environment, input, output, ...) in
323         specification YAML file.
324
325         :param item_type: Item type: Top level items in specification YAML file,
326             e.g.: environment, input, output.
327         :type item_type: str
328         :returns: Index of the given item type.
329         :rtype: int
330         """
331
332         index = 0
333         for item in self._cfg_yaml:
334             if item[u"type"] == item_type:
335                 return index
336             index += 1
337         return None
338
339     def _find_tag(self, text):
340         """Find the first tag in the given text. The tag is enclosed by the
341         TAG_OPENER and TAG_CLOSER.
342
343         :param text: Text to be searched.
344         :type text: str
345         :returns: The tag, or None if not found.
346         :rtype: str
347         """
348         try:
349             start = text.index(self.TAG_OPENER)
350             end = text.index(self.TAG_CLOSER, start + 1) + 1
351             return text[start:end]
352         except ValueError:
353             return None
354
355     def _replace_tags(self, data, src_data=None):
356         """Replace tag(s) in the data by their values.
357
358         :param data: The data where the tags will be replaced by their values.
359         :param src_data: Data where the tags are defined. It is dictionary where
360             the key is the tag and the value is the tag value. If not given,
361             'data' is used instead.
362         :type data: str, list or dict
363         :type src_data: dict
364         :returns: Data with the tags replaced.
365         :rtype: str, list or dict
366         :raises: PresentationError if it is not possible to replace the tag or
367             the data is not the supported data type (str, list or dict).
368         """
369
370         if src_data is None:
371             src_data = data
372
373         if isinstance(data, str):
374             tag = self._find_tag(data)
375             if tag is not None:
376                 data = data.replace(tag, src_data[tag[1:-1]])
377             return data
378
379         if isinstance(data, list):
380             new_list = list()
381             for item in data:
382                 new_list.append(self._replace_tags(item, src_data))
383             return new_list
384
385         if isinstance(data, dict):
386             counter = 0
387             for key, value in data.items():
388                 tag = self._find_tag(value)
389                 if tag is not None:
390                     try:
391                         data[key] = value.replace(tag, src_data[tag[1:-1]])
392                         counter += 1
393                     except KeyError:
394                         raise PresentationError(
395                             f"Not possible to replace the tag {tag}"
396                         )
397             if counter:
398                 self._replace_tags(data, src_data)
399             return data
400
401         raise PresentationError(u"Replace tags: Not supported data type.")
402
403     def _parse_env(self):
404         """Parse environment specification in the specification YAML file.
405         """
406
407         logging.info(u"Parsing specification file: environment ...")
408
409         idx = self._get_type_index(u"environment")
410         if idx is None:
411             return
412
413         try:
414             self._specification[u"environment"][u"configuration"] = \
415                 self._cfg_yaml[idx][u"configuration"]
416         except KeyError:
417             self._specification[u"environment"][u"configuration"] = None
418
419         try:
420             self._specification[u"environment"][u"paths"] = \
421                 self._replace_tags(self._cfg_yaml[idx][u"paths"])
422         except KeyError:
423             self._specification[u"environment"][u"paths"] = None
424
425         try:
426             self._specification[u"environment"][u"urls"] = \
427                 self._cfg_yaml[idx][u"urls"]
428         except KeyError:
429             self._specification[u"environment"][u"urls"] = None
430
431         try:
432             self._specification[u"environment"][u"make-dirs"] = \
433                 self._cfg_yaml[idx][u"make-dirs"]
434         except KeyError:
435             self._specification[u"environment"][u"make-dirs"] = None
436
437         try:
438             self._specification[u"environment"][u"remove-dirs"] = \
439                 self._cfg_yaml[idx][u"remove-dirs"]
440         except KeyError:
441             self._specification[u"environment"][u"remove-dirs"] = None
442
443         try:
444             self._specification[u"environment"][u"build-dirs"] = \
445                 self._cfg_yaml[idx][u"build-dirs"]
446         except KeyError:
447             self._specification[u"environment"][u"build-dirs"] = None
448
449         try:
450             self._specification[u"environment"][u"testbeds"] = \
451                 self._cfg_yaml[idx][u"testbeds"]
452         except KeyError:
453             self._specification[u"environment"][u"testbeds"] = None
454
455         logging.info(u"Done.")
456
457     def _load_mapping_table(self):
458         """Load a mapping table if it is specified. If not, use empty list.
459         """
460
461         mapping_file_name = self._specification[u"configuration"].\
462             get(u"mapping-file", None)
463         if mapping_file_name:
464             try:
465                 with open(mapping_file_name, u'r') as mfile:
466                     mapping = load(mfile, Loader=FullLoader)
467                     # Make sure everything is lowercase
468                     self._specification[u"configuration"][u"mapping"] = \
469                         {key.lower(): val.lower() for key, val in
470                          mapping.items()}
471                 logging.debug(f"Loaded mapping table:\n{mapping}")
472             except (YAMLError, IOError) as err:
473                 raise PresentationError(
474                     msg=f"An error occurred while parsing the mapping file "
475                         f"{mapping_file_name}",
476                     details=repr(err)
477                 )
478         else:
479             self._specification[u"configuration"][u"mapping"] = dict()
480
481     def _load_ignore_list(self):
482         """Load an ignore list if it is specified. If not, use empty list.
483         """
484
485         ignore_list_name = self._specification[u"configuration"].\
486             get(u"ignore-list", None)
487         if ignore_list_name:
488             try:
489                 with open(ignore_list_name, u'r') as ifile:
490                     ignore = load(ifile, Loader=FullLoader)
491                     # Make sure everything is lowercase
492                     self._specification[u"configuration"][u"ignore"] = \
493                         [item.lower() for item in ignore]
494                 logging.debug(f"Loaded ignore list:\n{ignore}")
495             except (YAMLError, IOError) as err:
496                 raise PresentationError(
497                     msg=f"An error occurred while parsing the ignore list file "
498                         f"{ignore_list_name}.",
499                     details=repr(err)
500                 )
501         else:
502             self._specification[u"configuration"][u"ignore"] = list()
503
504     def _parse_configuration(self):
505         """Parse configuration of PAL in the specification YAML file.
506         """
507
508         logging.info(u"Parsing specification file: configuration ...")
509
510         idx = self._get_type_index("configuration")
511         if idx is None:
512             logging.warning(
513                 u"No configuration information in the specification file."
514             )
515             return
516
517         try:
518             self._specification[u"configuration"] = self._cfg_yaml[idx]
519         except KeyError:
520             raise PresentationError(u"No configuration defined.")
521
522         # Data sets: Replace ranges by lists
523         for set_name, data_set in self.configuration[u"data-sets"].items():
524             if not isinstance(data_set, dict):
525                 continue
526             for job, builds in data_set.items():
527                 if not builds:
528                     continue
529                 if isinstance(builds, dict):
530                     build_end = builds.get(u"end", None)
531                     max_builds = builds.get(u"max-builds", None)
532                     try:
533                         build_end = int(build_end)
534                     except ValueError:
535                         # defined as a range <start, build_type>
536                         build_end = self._get_build_number(job, build_end)
537                     builds = [x for x in range(builds[u"start"], build_end + 1)]
538                     if max_builds and max_builds < len(builds):
539                         builds = builds[:max_builds]
540                     self.configuration[u"data-sets"][set_name][job] = builds
541                 elif isinstance(builds, list):
542                     for idx, item in enumerate(builds):
543                         try:
544                             builds[idx] = int(item)
545                         except ValueError:
546                             # defined as a range <build_type>
547                             builds[idx] = self._get_build_number(job, item)
548
549         # Data sets: add sub-sets to sets (only one level):
550         for set_name, data_set in self.configuration[u"data-sets"].items():
551             if isinstance(data_set, list):
552                 new_set = dict()
553                 for item in data_set:
554                     try:
555                         for key, val in self.configuration[u"data-sets"][item].\
556                                 items():
557                             new_set[key] = val
558                     except KeyError:
559                         raise PresentationError(
560                             f"Data set {item} is not defined in "
561                             f"the configuration section."
562                         )
563                 self.configuration[u"data-sets"][set_name] = new_set
564
565         # Mapping table:
566         self._load_mapping_table()
567
568         # Ignore list:
569         self._load_ignore_list()
570
571         logging.info(u"Done.")
572
573     def _parse_input(self):
574         """Parse input specification in the specification YAML file.
575
576         :raises: PresentationError if there are no data to process.
577         """
578
579         logging.info(u"Parsing specification file: input ...")
580
581         idx = self._get_type_index(u"input")
582         if idx is None:
583             raise PresentationError(u"No data to process.")
584
585         try:
586             for key, value in self._cfg_yaml[idx][u"general"].items():
587                 self._specification[u"input"][key] = value
588             self._specification[u"input"][u"builds"] = dict()
589
590             for job, builds in self._cfg_yaml[idx][u"builds"].items():
591                 if builds:
592                     if isinstance(builds, dict):
593                         build_end = builds.get(u"end", None)
594                         max_builds = builds.get(u"max-builds", None)
595                         reverse = bool(builds.get(u"reverse", False))
596                         try:
597                             build_end = int(build_end)
598                         except ValueError:
599                             # defined as a range <start, build_type>
600                             if build_end in (u"lastCompletedBuild",
601                                              u"lastSuccessfulBuild"):
602                                 reverse = True
603                             build_end = self._get_build_number(job, build_end)
604                         builds = [x for x in range(builds[u"start"],
605                                                    build_end + 1)
606                                   if x not in builds.get(u"skip", list())]
607                         if reverse:
608                             builds.reverse()
609                         if max_builds and max_builds < len(builds):
610                             builds = builds[:max_builds]
611                     self._specification[u"input"][u"builds"][job] = list()
612                     for build in builds:
613                         self._specification[u"input"][u"builds"][job]. \
614                             append({u"build": build, u"status": None})
615
616                 else:
617                     logging.warning(
618                         f"No build is defined for the job {job}. Trying to "
619                         f"continue without it."
620                     )
621
622         except KeyError:
623             raise PresentationError(u"No data to process.")
624
625         logging.info(u"Done.")
626
627     def _parse_output(self):
628         """Parse output specification in the specification YAML file.
629
630         :raises: PresentationError if there is no output defined.
631         """
632
633         logging.info(u"Parsing specification file: output ...")
634
635         idx = self._get_type_index(u"output")
636         if idx is None:
637             raise PresentationError(u"No output defined.")
638
639         try:
640             self._specification[u"output"] = self._cfg_yaml[idx]
641         except (KeyError, IndexError):
642             raise PresentationError(u"No output defined.")
643
644         logging.info(u"Done.")
645
646     def _parse_static(self):
647         """Parse specification of the static content in the specification YAML
648         file.
649         """
650
651         logging.info(u"Parsing specification file: static content ...")
652
653         idx = self._get_type_index(u"static")
654         if idx is None:
655             logging.warning(u"No static content specified.")
656
657         for key, value in self._cfg_yaml[idx].items():
658             if isinstance(value, str):
659                 try:
660                     self._cfg_yaml[idx][key] = self._replace_tags(
661                         value, self._specification[u"environment"][u"paths"])
662                 except KeyError:
663                     pass
664
665         self._specification[u"static"] = self._cfg_yaml[idx]
666
667         logging.info(u"Done.")
668
669     def _parse_elements_tables(self, table):
670         """Parse tables from the specification YAML file.
671
672         :param table: Table to be parsed from the specification file.
673         :type table: dict
674         :raises PresentationError: If wrong data set is used.
675         """
676
677         try:
678             table[u"template"] = self._replace_tags(
679                 table[u"template"],
680                 self._specification[u"environment"][u"paths"])
681         except KeyError:
682             pass
683
684         # Add data sets
685         try:
686             for item in (u"reference", u"compare"):
687                 if table.get(item, None):
688                     data_set = table[item].get(u"data", None)
689                     if isinstance(data_set, str):
690                         table[item][u"data"] = \
691                             self.configuration[u"data-sets"][data_set]
692                     data_set = table[item].get(u"data-replacement", None)
693                     if isinstance(data_set, str):
694                         table[item][u"data-replacement"] = \
695                             self.configuration[u"data-sets"][data_set]
696
697             if table.get(u"history", None):
698                 for i in range(len(table[u"history"])):
699                     data_set = table[u"history"][i].get(u"data", None)
700                     if isinstance(data_set, str):
701                         table[u"history"][i][u"data"] = \
702                             self.configuration[u"data-sets"][data_set]
703                     data_set = table[u"history"][i].get(
704                         u"data-replacement", None)
705                     if isinstance(data_set, str):
706                         table[u"history"][i][u"data-replacement"] = \
707                             self.configuration[u"data-sets"][data_set]
708
709             if table.get(u"columns", None):
710                 for i in range(len(table[u"columns"])):
711                     data_set = table[u"columns"][i].get(u"data-set", None)
712                     if isinstance(data_set, str):
713                         table[u"columns"][i][u"data-set"] = \
714                             self.configuration[u"data-sets"][data_set]
715                     data_set = table[u"columns"][i].get(
716                         u"data-replacement", None)
717                     if isinstance(data_set, str):
718                         table[u"columns"][i][u"data-replacement"] = \
719                             self.configuration[u"data-sets"][data_set]
720
721         except KeyError:
722             raise PresentationError(
723                 f"Wrong data set used in {table.get(u'title', u'')}."
724             )
725
726         self._specification[u"tables"].append(table)
727
728     def _parse_elements_plots(self, plot):
729         """Parse plots from the specification YAML file.
730
731         :param plot: Plot to be parsed from the specification file.
732         :type plot: dict
733         :raises PresentationError: If plot layout is not defined.
734         """
735
736         # Add layout to the plots:
737         layout = plot[u"layout"].get(u"layout", None)
738         if layout is not None:
739             plot[u"layout"].pop(u"layout")
740             try:
741                 for key, val in (self.configuration[u"plot-layouts"]
742                                  [layout].items()):
743                     plot[u"layout"][key] = val
744             except KeyError:
745                 raise PresentationError(
746                     f"Layout {layout} is not defined in the "
747                     f"configuration section."
748                 )
749         self._specification[u"plots"].append(plot)
750
751     def _parse_elements_files(self, file):
752         """Parse files from the specification YAML file.
753
754         :param file: File to be parsed from the specification file.
755         :type file: dict
756         """
757
758         try:
759             file[u"dir-tables"] = self._replace_tags(
760                 file[u"dir-tables"],
761                 self._specification[u"environment"][u"paths"])
762         except KeyError:
763             pass
764         self._specification[u"files"].append(file)
765
766     def _parse_elements_cpta(self, cpta):
767         """Parse cpta from the specification YAML file.
768
769         :param cpta: cpta to be parsed from the specification file.
770         :type cpta: dict
771         :raises PresentationError: If wrong data set is used or if plot layout
772             is not defined.
773         """
774
775         for plot in cpta[u"plots"]:
776             # Add layout to the plots:
777             layout = plot.get(u"layout", None)
778             if layout is not None:
779                 try:
780                     plot[u"layout"] = \
781                         self.configuration[u"plot-layouts"][layout]
782                 except KeyError:
783                     raise PresentationError(
784                         f"Layout {layout} is not defined in the "
785                         f"configuration section."
786                     )
787             # Add data sets:
788             if isinstance(plot.get(u"data", None), str):
789                 data_set = plot[u"data"]
790                 try:
791                     plot[u"data"] = \
792                         self.configuration[u"data-sets"][data_set]
793                 except KeyError:
794                     raise PresentationError(
795                         f"Data set {data_set} is not defined in "
796                         f"the configuration section."
797                     )
798         self._specification[u"cpta"] = cpta
799
800     def _parse_elements(self):
801         """Parse elements (tables, plots, ..) specification in the specification
802         YAML file.
803         """
804
805         logging.info(u"Parsing specification file: elements ...")
806
807         count = 1
808         for element in self._cfg_yaml:
809
810             # Replace tags:
811             try:
812                 element[u"output-file"] = self._replace_tags(
813                     element[u"output-file"],
814                     self._specification[u"environment"][u"paths"])
815             except KeyError:
816                 pass
817
818             try:
819                 element[u"input-file"] = self._replace_tags(
820                     element[u"input-file"],
821                     self._specification[u"environment"][u"paths"])
822             except KeyError:
823                 pass
824
825             try:
826                 element[u"output-file-links"] = self._replace_tags(
827                     element[u"output-file-links"],
828                     self._specification[u"environment"][u"paths"])
829             except KeyError:
830                 pass
831
832             # Add data sets to the elements:
833             if isinstance(element.get(u"data", None), str):
834                 data_set = element[u"data"]
835                 try:
836                     element[u"data"] = \
837                         self.configuration[u"data-sets"][data_set]
838                 except KeyError:
839                     raise PresentationError(
840                         f"Data set {data_set} is not defined in the "
841                         f"configuration section."
842                     )
843             elif isinstance(element.get(u"data", None), list):
844                 new_list = list()
845                 for item in element[u"data"]:
846                     try:
847                         new_list.append(
848                             self.configuration[u"data-sets"][item]
849                         )
850                     except KeyError:
851                         raise PresentationError(
852                             f"Data set {item} is not defined in the "
853                             f"configuration section."
854                         )
855                 element[u"data"] = new_list
856
857             # Parse elements:
858             if element[u"type"] == u"table":
859
860                 logging.info(f"  {count:3d} Processing a table ...")
861                 self._parse_elements_tables(element)
862                 count += 1
863
864             elif element[u"type"] == u"plot":
865
866                 logging.info(f"  {count:3d} Processing a plot ...")
867                 self._parse_elements_plots(element)
868                 count += 1
869
870             elif element[u"type"] == u"file":
871
872                 logging.info(f"  {count:3d} Processing a file ...")
873                 self._parse_elements_files(element)
874                 count += 1
875
876             elif element[u"type"] == u"cpta":
877
878                 logging.info(
879                     f"  {count:3d} Processing Continuous Performance Trending "
880                     f"and Analysis ..."
881                 )
882                 self._parse_elements_cpta(element)
883                 count += 1
884
885         logging.info(u"Done.")
886
887     def read_specification(self):
888         """Parse specification in the specification YAML file.
889
890         :raises: PresentationError if an error occurred while parsing the
891             specification file.
892         """
893         try:
894             self._cfg_yaml = load(self._cfg_file, Loader=FullLoader)
895         except YAMLError as err:
896             raise PresentationError(msg=u"An error occurred while parsing the "
897                                         u"specification file.",
898                                     details=repr(err))
899
900         self._parse_env()
901         self._parse_configuration()
902         self._parse_input()
903         self._parse_output()
904         self._parse_static()
905         self._parse_elements()
906
907         logging.debug(f"Specification: \n{pformat(self._specification)}")