Completed
Push — master ( 889440...e965d2 )
by Dieter
01:16
created

buildtimetrend.Stage   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 91
Duplicated Lines 0 %
Metric Value
dl 0
loc 91
rs 10
wmc 20

12 Methods

Rating   Name   Duplication   Size   Complexity  
A Stage.set_timestamp() 0 16 4
A Stage.set_duration_nano() 0 6 2
A Stage.to_dict() 0 3 1
A Stage.__init__() 0 5 1
A Stage.set_name() 0 8 2
A Stage.set_command() 0 7 2
A Stage.set_started_at_nano() 0 3 1
A Stage.set_started_at() 0 3 1
A Stage.set_timestamp_nano() 0 9 1
A Stage.set_finished_at_nano() 0 3 1
A Stage.set_finished_at() 0 3 1
A Stage.set_duration() 0 10 3
1
# vim: set expandtab sw=4 ts=4:
2
"""
3
Build stage related classes.
4
5
Read timestamps.csv, calculates stage duration and saves the result
6
to an xml file.
7
8
Copyright (C) 2014-2016 Dieter Adriaenssens <[email protected]>
9
10
This file is part of buildtimetrend/python-lib
11
<https://github.com/buildtimetrend/python-lib/>
12
13
This program is free software: you can redistribute it and/or modify
14
it under the terms of the GNU Affero General Public License as published by
15
the Free Software Foundation, either version 3 of the License, or
16
any later version.
17
18
This program is distributed in the hope that it will be useful,
19
but WITHOUT ANY WARRANTY; without even the implied warranty of
20
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
GNU Affero General Public License for more details.
22
23
You should have received a copy of the GNU Affero General Public License
24
along with this program. If not, see <http://www.gnu.org/licenses/>.
25
"""
26
27
import csv
28
from lxml import etree
29
from buildtimetrend import logger
30
from buildtimetrend.tools import split_timestamp
31
from buildtimetrend.tools import check_file
32
from buildtimetrend.tools import nano2sec
33
34
35
class Stages(object):
36
37
    """
38
    Build stages class.
39
40
    It gathers timestamps from a csv file and calculates stage duration.
41
    Output stages in xml format.
42
    """
43
44
    def __init__(self):
45
        """Initialize instance."""
46
        self.stages = []
47
        self.started_at = None
48
        self.finished_at = None
49
        self.end_timestamp = 0
50
51
    def set_end_timestamp(self, timestamp):
52
        """
53
        Set end timestamp.
54
55
        Parameters:
56
        - timestamp : end timestamp
57
        """
58
        if isinstance(timestamp, (int, float)) and timestamp > 0:
59
            logger.info("Set end_timestamp : %f", timestamp)
60
            self.end_timestamp = timestamp
61
62
    def read_csv(self, csv_filename):
63
        """
64
        Gather timestamps from a csv file and calculate stage duration.
65
66
        Parameters :
67
        - csv_filename : csv filename containing timestamps
68
        Returns false if file doesn't exist, true if it was read successfully.
69
        """
70
        # load timestamps file
71
        if not check_file(csv_filename):
72
            return False
73
74
        # read timestamps, calculate stage duration
75
        with open(csv_filename, 'r') as csv_data:
76
            timestamps = csv.reader(csv_data, delimiter=',', quotechar='"')
77
            self.parse_timestamps(timestamps)
78
79
        return True
80
81
    def total_duration(self):
82
        """Calculate total duration of all stages."""
83
        total_duration = 0
84
        # calculate total duration
85
        for stage in self.stages:
86
            total_duration += stage["duration"]
87
88
        return total_duration
89
90
    def to_xml(self):
91
        """Return xml object from stages dictionary."""
92
        root = etree.Element("stages")
93
94
        for stage in self.stages:
95
            root.append(etree.Element(
96
                "stage", name=stage["name"],
97
                duration=str(stage["duration"])))
98
99
        return root
100
101
    def to_xml_string(self):
102
        """Return xml string from stages dictionary."""
103
        return etree.tostring(self.to_xml(), pretty_print=True)
104
105
    def parse_timestamps(self, timestamps):
106
        """
107
        Parse timestamps and calculate stage durations.
108
109
        The timestamp of each stage is used as both the start point of it's
110
        stage and the endpoint of the previous stage.
111
        On parsing each timestamp, the previous timestamp and previous
112
        event name are used to calculate the duration of the previous stage.
113
        For this reason, parsing the first timestamp line
114
        doesn't produce a stage duration.
115
        The parsing ends when an event with the name 'end' is encountered.
116
        """
117
        previous_timestamp = 0
118
        event_name = None
119
120
        # list of possible end tags
121
        end_tags = ['end', 'done', 'finished', 'completed']
122
123
        # iterate over all timestamps
124
        for row in timestamps:
125
            timestamp = float(row[1])
126
127
            # skip calculating the duration of the first stage,
128
            # the next timestamp is needed
129
            if event_name is not None:
130
                # finish parsing when an end timestamp is encountered
131
                if event_name.lower() in end_tags:
132
                    break
133
134
                self.create_stage(event_name, previous_timestamp, timestamp)
135
136
            # event name of the timestamp is used in the next iteration
137
            # the timestamp of the next stage is used as the ending timestamp
138
            # of this stage
139
            event_name = row[0]
140
            previous_timestamp = timestamp
141
142
        if self.end_timestamp > 0 and event_name.lower() not in end_tags:
143
            self.create_stage(event_name,
144
                              previous_timestamp,
145
                              self.end_timestamp)
146
147
    def add_stage(self, stage):
148
        """
149
        Add a stage.
150
151
        param stage Stage instance
152
        """
153
        if not isinstance(stage, Stage):
154
            raise TypeError(
155
                "param {0!s} should be a Stage instance".format(stage)
156
            )
157
158
        # add stage
159
        self.stages.append(stage.to_dict())
160
161
        # assign starting timestamp of first stage
162
        # to started_at of the build job
163
        if self.started_at is None and "started_at" in stage.data:
164
            self.started_at = stage.data["started_at"]
165
166
        # assign finished timestamp
167
        if "finished_at" in stage.data:
168
            self.finished_at = stage.data["finished_at"]
169
170
    def create_stage(self, name, start_time, end_time):
171
        """
172
        Create and add a stage.
173
174
        Parameters :
175
        - name : stage name
176
        - start_time : start of stage timestamp
177
        - end_time : end of stage timestamp
178
        """
179
        # timestamps should be integer or floating point numbers
180
        if not (isinstance(start_time, (int, float)) and
181
                isinstance(end_time, (int, float))):
182
            return None
183
184
        # calculate duration from start and end timestamp
185
        duration = end_time - start_time
186
        logger.info('Duration %s : %fs', name, duration)
187
188
        # create stage
189
        stage = Stage()
190
        stage.set_name(name)
191
        stage.set_started_at(start_time)
192
        stage.set_finished_at(end_time)
193
        stage.set_duration(duration)
194
195
        self.add_stage(stage)
196
        return stage
197
198
199
class Stage(object):
200
201
    """Build stage object."""
202
203
    def __init__(self):
204
        """Initialize instance."""
205
        self.data = {}
206
        self.set_name("")
207
        self.set_duration(0)
208
209
    def set_name(self, name):
210
        """Set stage name."""
211
        if name is None:
212
            return False
213
214
        self.data["name"] = str(name)
215
        logger.info("Set name : %s", name)
216
        return True
217
218
    def set_command(self, command):
219
        """Set stage command."""
220
        if command is None:
221
            return False
222
223
        self.data["command"] = str(command)
224
        return True
225
226
    def set_started_at(self, timestamp):
227
        """Set time when stage was started."""
228
        return self.set_timestamp("started_at", timestamp)
229
230
    def set_started_at_nano(self, timestamp):
231
        """Set time when stage was started in nanoseconds."""
232
        return self.set_timestamp_nano("started_at", timestamp)
233
234
    def set_finished_at(self, timestamp):
235
        """Set time when stage was finished."""
236
        return self.set_timestamp("finished_at", timestamp)
237
238
    def set_finished_at_nano(self, timestamp):
239
        """Set time when stage was finished in nanoseconds."""
240
        return self.set_timestamp_nano("finished_at", timestamp)
241
242
    def set_timestamp(self, name, timestamp):
243
        """
244
        Set timestamp.
245
246
        Parameters:
247
        - name timestamp name
248
        - timestamp seconds since epoch
249
        """
250
        if timestamp is not None and name is not None:
251
            try:
252
                self.data[name] = split_timestamp(timestamp)
253
                return True
254
            except TypeError:
255
                return False
256
257
        return False
258
259
    def set_timestamp_nano(self, name, timestamp):
260
        """
261
        Set timestamp in nanoseconds.
262
263
        Parameters:
264
        - name timestamp name
265
        - timestamp nanoseconds since epoch
266
        """
267
        return self.set_timestamp(name, nano2sec(timestamp))
268
269
    def set_duration(self, duration):
270
        """Set stage duration in seconds."""
271
        try:
272
            duration = float(duration)
273
            if duration >= 0:
274
                self.data["duration"] = duration
275
                return True
276
            return False
277
        except (ValueError, TypeError):
278
            return False
279
280
    def set_duration_nano(self, duration):
281
        """Set stage duration in nanoseconds."""
282
        try:
283
            return self.set_duration(nano2sec(duration))
284
        except (ValueError, TypeError):
285
            return False
286
287
    def to_dict(self):
288
        """Return stages data as dictionary."""
289
        return self.data
290