CSIT-1041: Trending dashboard
[csit.git] / resources / tools / presentation / utils.py
1 # Copyright (c) 2017 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 """General purpose utilities.
15 """
16
17 import subprocess
18 import numpy as np
19 import pandas as pd
20 import logging
21
22 from os import walk, makedirs, environ
23 from os.path import join, isdir
24 from shutil import copy, Error
25 from math import sqrt
26
27 from errors import PresentationError
28
29
30 def mean(items):
31     """Calculate mean value from the items.
32
33     :param items: Mean value is calculated from these items.
34     :type items: list
35     :returns: MEan value.
36     :rtype: float
37     """
38
39     return float(sum(items)) / len(items)
40
41
42 def stdev(items):
43     """Calculate stdev from the items.
44
45     :param items: Stdev is calculated from these items.
46     :type items: list
47     :returns: Stdev.
48     :rtype: float
49     """
50
51     avg = mean(items)
52     variance = [(x - avg) ** 2 for x in items]
53     stddev = sqrt(mean(variance))
54     return stddev
55
56
57 def relative_change(nr1, nr2):
58     """Compute relative change of two values.
59
60     :param nr1: The first number.
61     :param nr2: The second number.
62     :type nr1: float
63     :type nr2: float
64     :returns: Relative change of nr1.
65     :rtype: float
66     """
67
68     return float(((nr2 - nr1) / nr1) * 100)
69
70
71 def remove_outliers(input_list, outlier_const=1.5, window=14):
72     """Return list with outliers removed, using split_outliers.
73
74     :param input_list: Data from which the outliers will be removed.
75     :param outlier_const: Outlier constant.
76     :param window: How many preceding values to take into account.
77     :type input_list: list of floats
78     :type outlier_const: float
79     :type window: int
80     :returns: The input list without outliers.
81     :rtype: list of floats
82     """
83
84     data = np.array(input_list)
85     upper_quartile = np.percentile(data, 75)
86     lower_quartile = np.percentile(data, 25)
87     iqr = (upper_quartile - lower_quartile) * outlier_const
88     quartile_set = (lower_quartile - iqr, upper_quartile + iqr)
89     result_lst = list()
90     for y in data.tolist():
91         if quartile_set[0] <= y <= quartile_set[1]:
92             result_lst.append(y)
93     return result_lst
94
95     # input_series = pd.Series()
96     # for index, value in enumerate(input_list):
97     #     item_pd = pd.Series([value, ], index=[index, ])
98     #     input_series.append(item_pd)
99     # output_series, _ = split_outliers(input_series, outlier_const=outlier_const,
100     #                                   window=window)
101     # output_list = [y for x, y in output_series.items() if not np.isnan(y)]
102     #
103     # return output_list
104
105
106 def split_outliers(input_series, outlier_const=1.5, window=14):
107     """Go through the input data and generate two pandas series:
108     - input data with outliers replaced by NAN
109     - outliers.
110     The function uses IQR to detect outliers.
111
112     :param input_series: Data to be examined for outliers.
113     :param outlier_const: Outlier constant.
114     :param window: How many preceding values to take into account.
115     :type input_series: pandas.Series
116     :type outlier_const: float
117     :type window: int
118     :returns: Input data with NAN outliers and Outliers.
119     :rtype: (pandas.Series, pandas.Series)
120     """
121
122     list_data = list(input_series.items())
123     head_size = min(window, len(list_data))
124     head_list = list_data[:head_size]
125     trimmed_data = pd.Series()
126     outliers = pd.Series()
127     for item_x, item_y in head_list:
128         item_pd = pd.Series([item_y, ], index=[item_x, ])
129         trimmed_data = trimmed_data.append(item_pd)
130     for index, (item_x, item_y) in list(enumerate(list_data))[head_size:]:
131         y_rolling_list = [y for (x, y) in list_data[index - head_size:index]]
132         y_rolling_array = np.array(y_rolling_list)
133         q1 = np.percentile(y_rolling_array, 25)
134         q3 = np.percentile(y_rolling_array, 75)
135         iqr = (q3 - q1) * outlier_const
136         low, high = q1 - iqr, q3 + iqr
137         item_pd = pd.Series([item_y, ], index=[item_x, ])
138         if low <= item_y <= high:
139             trimmed_data = trimmed_data.append(item_pd)
140         else:
141             outliers = outliers.append(item_pd)
142             nan_pd = pd.Series([np.nan, ], index=[item_x, ])
143             trimmed_data = trimmed_data.append(nan_pd)
144
145     return trimmed_data, outliers
146
147
148 def get_files(path, extension=None, full_path=True):
149     """Generates the list of files to process.
150
151     :param path: Path to files.
152     :param extension: Extension of files to process. If it is the empty string,
153     all files will be processed.
154     :param full_path: If True, the files with full path are generated.
155     :type path: str
156     :type extension: str
157     :type full_path: bool
158     :returns: List of files to process.
159     :rtype: list
160     """
161
162     file_list = list()
163     for root, _, files in walk(path):
164         for filename in files:
165             if extension:
166                 if filename.endswith(extension):
167                     if full_path:
168                         file_list.append(join(root, filename))
169                     else:
170                         file_list.append(filename)
171             else:
172                 file_list.append(join(root, filename))
173
174     return file_list
175
176
177 def get_rst_title_char(level):
178     """Return character used for the given title level in rst files.
179
180     :param level: Level of the title.
181     :type: int
182     :returns: Character used for the given title level in rst files.
183     :rtype: str
184     """
185     chars = ('=', '-', '`', "'", '.', '~', '*', '+', '^')
186     if level < len(chars):
187         return chars[level]
188     else:
189         return chars[-1]
190
191
192 def execute_command(cmd):
193     """Execute the command in a subprocess and log the stdout and stderr.
194
195     :param cmd: Command to execute.
196     :type cmd: str
197     :returns: Return code of the executed command.
198     :rtype: int
199     """
200
201     env = environ.copy()
202     proc = subprocess.Popen(
203         [cmd],
204         stdout=subprocess.PIPE,
205         stderr=subprocess.PIPE,
206         shell=True,
207         env=env)
208
209     stdout, stderr = proc.communicate()
210
211     logging.info(stdout)
212     logging.info(stderr)
213
214     if proc.returncode != 0:
215         logging.error("    Command execution failed.")
216     return proc.returncode, stdout, stderr
217
218
219 def get_last_successful_build_number(jenkins_url, job_name):
220     """Get the number of the last successful build of the given job.
221
222     :param jenkins_url: Jenkins URL.
223     :param job_name: Job name.
224     :type jenkins_url: str
225     :type job_name: str
226     :returns: The build number as a string.
227     :rtype: str
228     """
229
230     url = "{}/{}/lastSuccessfulBuild/buildNumber".format(jenkins_url, job_name)
231     cmd = "wget -qO- {url}".format(url=url)
232
233     return execute_command(cmd)
234
235
236 def get_last_completed_build_number(jenkins_url, job_name):
237     """Get the number of the last completed build of the given job.
238
239     :param jenkins_url: Jenkins URL.
240     :param job_name: Job name.
241     :type jenkins_url: str
242     :type job_name: str
243     :returns: The build number as a string.
244     :rtype: str
245     """
246
247     url = "{}/{}/lastCompletedBuild/buildNumber".format(jenkins_url, job_name)
248     cmd = "wget -qO- {url}".format(url=url)
249
250     return execute_command(cmd)
251
252
253 def archive_input_data(spec):
254     """Archive the report.
255
256     :param spec: Specification read from the specification file.
257     :type spec: Specification
258     :raises PresentationError: If it is not possible to archive the input data.
259     """
260
261     logging.info("    Archiving the input data files ...")
262
263     if spec.is_debug:
264         extension = spec.debug["input-format"]
265     else:
266         extension = spec.input["file-format"]
267     data_files = get_files(spec.environment["paths"]["DIR[WORKING,DATA]"],
268                            extension=extension)
269     dst = spec.environment["paths"]["DIR[STATIC,ARCH]"]
270     logging.info("      Destination: {0}".format(dst))
271
272     try:
273         if not isdir(dst):
274             makedirs(dst)
275
276         for data_file in data_files:
277             logging.info("      Copying the file: {0} ...".format(data_file))
278             copy(data_file, dst)
279
280     except (Error, OSError) as err:
281         raise PresentationError("Not possible to archive the input data.",
282                                 str(err))
283
284     logging.info("    Done.")