PAL: Fix table data model
[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                     try:
532                         build_end = int(build_end)
533                     except ValueError:
534                         # defined as a range <start, build_type>
535                         build_end = self._get_build_number(job, build_end)
536                     builds = [x for x in range(builds[u"start"],
537                                                build_end + 1)
538                               if x not in builds.get(u"skip", list())]
539                     self.configuration[u"data-sets"][set_name][job] = builds
540                 elif isinstance(builds, list):
541                     for idx, item in enumerate(builds):
542                         try:
543                             builds[idx] = int(item)
544                         except ValueError:
545                             # defined as a range <build_type>
546                             builds[idx] = self._get_build_number(job, item)
547
548         # Data sets: add sub-sets to sets (only one level):
549         for set_name, data_set in self.configuration[u"data-sets"].items():
550             if isinstance(data_set, list):
551                 new_set = dict()
552                 for item in data_set:
553                     try:
554                         for key, val in self.configuration[u"data-sets"][item].\
555                                 items():
556                             new_set[key] = val
557                     except KeyError:
558                         raise PresentationError(
559                             f"Data set {item} is not defined in "
560                             f"the configuration section."
561                         )
562                 self.configuration[u"data-sets"][set_name] = new_set
563
564         # Mapping table:
565         self._load_mapping_table()
566
567         # Ignore list:
568         self._load_ignore_list()
569
570         logging.info(u"Done.")
571
572     def _parse_input(self):
573         """Parse input specification in the specification YAML file.
574
575         :raises: PresentationError if there are no data to process.
576         """
577
578         logging.info(u"Parsing specification file: input ...")
579
580         idx = self._get_type_index(u"input")
581         if idx is None:
582             raise PresentationError(u"No data to process.")
583
584         try:
585             for key, value in self._cfg_yaml[idx][u"general"].items():
586                 self._specification[u"input"][key] = value
587             self._specification[u"input"][u"builds"] = dict()
588
589             for job, builds in self._cfg_yaml[idx][u"builds"].items():
590                 if builds:
591                     if isinstance(builds, dict):
592                         build_end = builds.get(u"end", None)
593                         try:
594                             build_end = int(build_end)
595                         except ValueError:
596                             # defined as a range <start, build_type>
597                             build_end = self._get_build_number(job, build_end)
598                         builds = [x for x in range(builds[u"start"],
599                                                    build_end + 1)
600                                   if x not in builds.get(u"skip", list())]
601                     self._specification[u"input"][u"builds"][job] = list()
602                     for build in builds:
603                         self._specification[u"input"][u"builds"][job]. \
604                             append({u"build": build, u"status": None})
605
606                 else:
607                     logging.warning(
608                         f"No build is defined for the job {job}. Trying to "
609                         f"continue without it."
610                     )
611         except KeyError:
612             raise PresentationError(u"No data to process.")
613
614         logging.info(u"Done.")
615
616     def _parse_output(self):
617         """Parse output specification in the specification YAML file.
618
619         :raises: PresentationError if there is no output defined.
620         """
621
622         logging.info(u"Parsing specification file: output ...")
623
624         idx = self._get_type_index(u"output")
625         if idx is None:
626             raise PresentationError(u"No output defined.")
627
628         try:
629             self._specification[u"output"] = self._cfg_yaml[idx]
630         except (KeyError, IndexError):
631             raise PresentationError(u"No output defined.")
632
633         logging.info(u"Done.")
634
635     def _parse_static(self):
636         """Parse specification of the static content in the specification YAML
637         file.
638         """
639
640         logging.info(u"Parsing specification file: static content ...")
641
642         idx = self._get_type_index(u"static")
643         if idx is None:
644             logging.warning(u"No static content specified.")
645
646         for key, value in self._cfg_yaml[idx].items():
647             if isinstance(value, str):
648                 try:
649                     self._cfg_yaml[idx][key] = self._replace_tags(
650                         value, self._specification[u"environment"][u"paths"])
651                 except KeyError:
652                     pass
653
654         self._specification[u"static"] = self._cfg_yaml[idx]
655
656         logging.info(u"Done.")
657
658     def _parse_elements_tables(self, table):
659         """Parse tables from the specification YAML file.
660
661         :param table: Table to be parsed from the specification file.
662         :type table: dict
663         :raises PresentationError: If wrong data set is used.
664         """
665
666         try:
667             table[u"template"] = self._replace_tags(
668                 table[u"template"],
669                 self._specification[u"environment"][u"paths"])
670         except KeyError:
671             pass
672
673         # Add data sets
674         try:
675             for item in (u"reference", u"compare"):
676                 if table.get(item, None):
677                     data_set = table[item].get(u"data", None)
678                     if isinstance(data_set, str):
679                         table[item][u"data"] = \
680                             self.configuration[u"data-sets"][data_set]
681                     data_set = table[item].get(u"data-replacement", None)
682                     if isinstance(data_set, str):
683                         table[item][u"data-replacement"] = \
684                             self.configuration[u"data-sets"][data_set]
685
686             if table.get(u"history", None):
687                 for i in range(len(table[u"history"])):
688                     data_set = table[u"history"][i].get(u"data", None)
689                     if isinstance(data_set, str):
690                         table[u"history"][i][u"data"] = \
691                             self.configuration[u"data-sets"][data_set]
692                     data_set = table[u"history"][i].get(
693                         u"data-replacement", None)
694                     if isinstance(data_set, str):
695                         table[u"history"][i][u"data-replacement"] = \
696                             self.configuration[u"data-sets"][data_set]
697
698             if table.get(u"columns", None):
699                 for i in range(len(table[u"columns"])):
700                     data_set = table[u"columns"][i].get(u"data-set", None)
701                     if isinstance(data_set, str):
702                         table[u"columns"][i][u"data-set"] = \
703                             self.configuration[u"data-sets"][data_set]
704                     data_set = table[u"columns"][i].get(
705                         u"data-replacement", None)
706                     if isinstance(data_set, str):
707                         table[u"columns"][i][u"data-replacement"] = \
708                             self.configuration[u"data-sets"][data_set]
709
710         except KeyError:
711             raise PresentationError(
712                 f"Wrong data set used in {table.get(u'title', u'')}."
713             )
714
715         self._specification[u"tables"].append(table)
716
717     def _parse_elements_plots(self, plot):
718         """Parse plots from the specification YAML file.
719
720         :param plot: Plot to be parsed from the specification file.
721         :type plot: dict
722         :raises PresentationError: If plot layout is not defined.
723         """
724
725         # Add layout to the plots:
726         layout = plot[u"layout"].get(u"layout", None)
727         if layout is not None:
728             plot[u"layout"].pop(u"layout")
729             try:
730                 for key, val in (self.configuration[u"plot-layouts"]
731                                  [layout].items()):
732                     plot[u"layout"][key] = val
733             except KeyError:
734                 raise PresentationError(
735                     f"Layout {layout} is not defined in the "
736                     f"configuration section."
737                 )
738         self._specification[u"plots"].append(plot)
739
740     def _parse_elements_files(self, file):
741         """Parse files from the specification YAML file.
742
743         :param file: File to be parsed from the specification file.
744         :type file: dict
745         """
746
747         try:
748             file[u"dir-tables"] = self._replace_tags(
749                 file[u"dir-tables"],
750                 self._specification[u"environment"][u"paths"])
751         except KeyError:
752             pass
753         self._specification[u"files"].append(file)
754
755     def _parse_elements_cpta(self, cpta):
756         """Parse cpta from the specification YAML file.
757
758         :param cpta: cpta to be parsed from the specification file.
759         :type cpta: dict
760         :raises PresentationError: If wrong data set is used or if plot layout
761             is not defined.
762         """
763
764         for plot in cpta[u"plots"]:
765             # Add layout to the plots:
766             layout = plot.get(u"layout", None)
767             if layout is not None:
768                 try:
769                     plot[u"layout"] = \
770                         self.configuration[u"plot-layouts"][layout]
771                 except KeyError:
772                     raise PresentationError(
773                         f"Layout {layout} is not defined in the "
774                         f"configuration section."
775                     )
776             # Add data sets:
777             if isinstance(plot.get(u"data", None), str):
778                 data_set = plot[u"data"]
779                 try:
780                     plot[u"data"] = \
781                         self.configuration[u"data-sets"][data_set]
782                 except KeyError:
783                     raise PresentationError(
784                         f"Data set {data_set} is not defined in "
785                         f"the configuration section."
786                     )
787         self._specification[u"cpta"] = cpta
788
789     def _parse_elements(self):
790         """Parse elements (tables, plots, ..) specification in the specification
791         YAML file.
792         """
793
794         logging.info(u"Parsing specification file: elements ...")
795
796         count = 1
797         for element in self._cfg_yaml:
798
799             # Replace tags:
800             try:
801                 element[u"output-file"] = self._replace_tags(
802                     element[u"output-file"],
803                     self._specification[u"environment"][u"paths"])
804             except KeyError:
805                 pass
806
807             try:
808                 element[u"input-file"] = self._replace_tags(
809                     element[u"input-file"],
810                     self._specification[u"environment"][u"paths"])
811             except KeyError:
812                 pass
813
814             try:
815                 element[u"output-file-links"] = self._replace_tags(
816                     element[u"output-file-links"],
817                     self._specification[u"environment"][u"paths"])
818             except KeyError:
819                 pass
820
821             # Add data sets to the elements:
822             if isinstance(element.get(u"data", None), str):
823                 data_set = element[u"data"]
824                 try:
825                     element[u"data"] = \
826                         self.configuration[u"data-sets"][data_set]
827                 except KeyError:
828                     raise PresentationError(
829                         f"Data set {data_set} is not defined in the "
830                         f"configuration section."
831                     )
832             elif isinstance(element.get(u"data", None), list):
833                 new_list = list()
834                 for item in element[u"data"]:
835                     try:
836                         new_list.append(
837                             self.configuration[u"data-sets"][item]
838                         )
839                     except KeyError:
840                         raise PresentationError(
841                             f"Data set {item} is not defined in the "
842                             f"configuration section."
843                         )
844                 element[u"data"] = new_list
845
846             # Parse elements:
847             if element[u"type"] == u"table":
848
849                 logging.info(f"  {count:3d} Processing a table ...")
850                 self._parse_elements_tables(element)
851                 count += 1
852
853             elif element[u"type"] == u"plot":
854
855                 logging.info(f"  {count:3d} Processing a plot ...")
856                 self._parse_elements_plots(element)
857                 count += 1
858
859             elif element[u"type"] == u"file":
860
861                 logging.info(f"  {count:3d} Processing a file ...")
862                 self._parse_elements_files(element)
863                 count += 1
864
865             elif element[u"type"] == u"cpta":
866
867                 logging.info(
868                     f"  {count:3d} Processing Continuous Performance Trending "
869                     f"and Analysis ..."
870                 )
871                 self._parse_elements_cpta(element)
872                 count += 1
873
874         logging.info(u"Done.")
875
876     def read_specification(self):
877         """Parse specification in the specification YAML file.
878
879         :raises: PresentationError if an error occurred while parsing the
880             specification file.
881         """
882         try:
883             self._cfg_yaml = load(self._cfg_file, Loader=FullLoader)
884         except YAMLError as err:
885             raise PresentationError(msg=u"An error occurred while parsing the "
886                                         u"specification file.",
887                                     details=repr(err))
888
889         self._parse_env()
890         self._parse_configuration()
891         self._parse_input()
892         self._parse_output()
893         self._parse_static()
894         self._parse_elements()
895
896         logging.debug(f"Specification: \n{pformat(self._specification)}")