Passed
Push — develop ( 850286...2f2c0e )
by Dean
03:07 queued 32s
created

SyncProgress.remaining_seconds()   A

Complexity

Conditions 4

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 15.664
Metric Value
cc 4
dl 0
loc 16
ccs 1
cts 10
cp 0.1
crap 15.664
rs 9.2
1 1
from plugin.core.environment import Environment
2
3 1
from datetime import datetime
4 1
import inspect
5 1
import logging
6
7 1
log = logging.getLogger(__name__)
8
9
10 1
class SyncProgressBase(object):
11 1
    speed_smoothing = 0.5
12
13 1
    def __init__(self, tag):
14 1
        self.tag = tag
15
16 1
        self.current = 0
17 1
        self.maximum = 0
18
19 1
        self.started_at = None
20 1
        self.ended_at = None
21
22 1
    @property
23
    def elapsed(self):
24
        if self.started_at and self.ended_at:
25
            return (self.ended_at - self.started_at).total_seconds()
26
27
        if self.started_at:
28
            return (datetime.utcnow() - self.started_at).total_seconds()
29
30
        return None
31
32 1
    @property
33
    def percent(self):
34
        raise NotImplementedError
35
36 1
    @property
37
    def remaining_seconds(self):
38
        raise NotImplementedError
39
40 1
    def add(self, delta):
41
        if not delta:
42
            return
43
44
        # Update group maximum
45
        self.maximum += delta
46
47 1
    def start(self):
48
        self.started_at = datetime.utcnow()
49
        self.ended_at = None
50
51 1
    def step(self, delta=1):
52
        if not delta:
53
            return
54
55
        if not self.started_at:
56
            self.start()
57
58
        # Update group position
59
        self.current += delta
60
61 1
    def stop(self):
62
        self.ended_at = datetime.utcnow()
63
64 1
    def _ema(self, value, previous, smoothing):
65
        if smoothing is None:
66
            # Use default smoothing value
67
            smoothing = self.speed_smoothing
68
69
        # Calculate EMA
70
        return smoothing * value + (1 - smoothing) * previous
71
72
73 1
class SyncProgress(SyncProgressBase):
74 1
    def __init__(self, task):
75 1
        super(SyncProgress, self).__init__('root')
76
77 1
        self.task = task
78
79 1
        self.groups = None
80 1
        self.group_speeds = None
81
82 1
    @property
83
    def percent(self):
84
        if not self.groups:
85
            return 0.0
86
87
        samples = []
88
89
        for group in self.groups.itervalues():
0 ignored issues
show
Bug introduced by
The Instance of dict does not seem to have a member named itervalues.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
90
            samples.append(group.percent)
91
92
        return sum(samples) / len(samples)
93
94 1
    @property
95
    def remaining_seconds(self):
96
        if not self.groups:
97
            return 0.0
98
99
        samples = []
100
101
        for group in self.groups.itervalues():
0 ignored issues
show
Bug introduced by
The Instance of dict does not seem to have a member named itervalues.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
102
            remaining_seconds = group.remaining_seconds
103
104
            if remaining_seconds is None:
105
                continue
106
107
            samples.append(remaining_seconds)
108
109
        return sum(samples)
110
111 1
    def group(self, *tag):
112
        if self.groups is None:
113
            raise Exception("Progress tracking hasn't been started")
114
115
        # Resolve tag to string
116
        tag = self._resolve_tag(tag)
117
118
        # Return existing progress group (if available)
119
        if tag in self.groups:
120
            return self.groups[tag]
121
122
        # Construct new progress group
123
        group = self.groups[tag] = SyncProgressGroup(self, tag)
124
        return group
125
126 1
    def start(self):
127
        super(SyncProgress, self).start()
128
129
        # Reset active groups
130
        self.groups = {}
131
132
        # Retrieve group speeds
133
        self.group_speeds = Environment.dict['sync.progress.group_speeds'] or {}
134
135 1
    def stop(self):
136
        super(SyncProgress, self).stop()
137
138
        # Save progress statistics
139
        self.save()
140
141 1
    def save(self):
142
        # Update group speeds
143
        self.group_speeds = dict([
144
            (group.tag, self._group_speed(group))
145
            for group in self.groups.itervalues()
0 ignored issues
show
Bug introduced by
The Instance of dict does not seem to have a member named itervalues.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
146
        ])
147
148
        # Save to plugin dictionary
149
        Environment.dict['sync.progress.group_speeds'] = self.group_speeds
150
151 1
    def _group_speed(self, group):
152
        if not group.speed_min:
153
            # No group speed calculated yet
154
            return
155
156
        speed = self.group_speeds.get(group.tag)
157
158
        if not speed:
159
            # First sample
160
            return group.speed_min
161
162
        # Calculate EMA for group speed
163
        return self._ema(group.speed_min, speed, self.speed_smoothing)
164
165 1
    @staticmethod
166
    def _resolve_tag(tag):
167
        if isinstance(tag, (str, unicode)):
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'unicode'
Loading history...
168
            return tag
169
170
        # Resolve tag
171
        if inspect.isclass(tag[0]):
172
            cls = tag[0]
173
            path = '%s.%s' % (cls.__module__, cls.__name__)
174
175
            tag = [path] + list(tag[1:])
176
177
        # Convert tag to string
178
        return ':'.join(tag)
179
180
181 1
class SyncProgressGroup(SyncProgressBase):
182 1
    def __init__(self, root, tag):
183
        super(SyncProgressGroup, self).__init__(tag)
184
185
        self.root = root
186
187
        # Retrieve average group speed
188
        self.speed = self.root.group_speeds.get(self.tag)
189
        self.speed_min = None
190
191 1
    @property
192
    def percent(self):
193
        if self.maximum is None or self.current is None:
194
            return 0.0
195
196
        if self.maximum < 1:
197
            return 0.0
198
199
        value = (float(self.current) / self.maximum) * 100
200
201
        if value > 100:
202
            return 100.0
203
204
        return value
205
206 1
    @property
207
    def per_second(self):
208
        elapsed = self.elapsed
209
210
        if not elapsed:
211
            return None
212
213
        return float(self.current) / elapsed
214
215 1
    @property
216
    def remaining(self):
217
        if self.maximum is None or self.current is None:
218
            return None
219
220
        value = self.maximum - self.current
221
222
        if value < 0:
223
            return 0
224
225
        return value
226
227 1
    @property
228
    def remaining_seconds(self):
229
        remaining = self.remaining
230
231
        if remaining is None or self.speed is None:
232
            return None
233
234
        return float(remaining) / self.speed
235
236 1
    def add(self, delta):
237
        super(SyncProgressGroup, self).add(delta)
238
239
        # Update root maximum
240
        self.root.add(delta)
241
242 1
    def step(self, delta=1):
243
        super(SyncProgressGroup, self).step(delta)
244
245
        # Update average syncing speed
246
        self.update_speed()
247
248
        # Update root progress
249
        self.root.step(delta)
250
251 1
    def update_speed(self):
252
        if not self.per_second:
253
            # No steps emitted yet
254
            return
255
256
        if not self.speed:
257
            # First sample, set to current `per_second`
258
            self.speed = self.per_second
259
            return
260
261
        # Calculate average syncing speed (EMA)
262
        self.speed = self._ema(self.per_second, self.speed, self.speed_smoothing)
263
264
        # Update minimum speed
265
        if self.speed_min is None:
266
            # First sample
267
            self.speed_min = self.speed
268
        elif self.speed < self.speed_min:
269
            # New minimum speed reached
270
            self.speed_min = self.speed
271
272 1
    def __repr__(self):
273
        return '<SyncProgressGroup %s/%s - %.02f%% (remaining_seconds: %.02f, speed: %.02f, speed_min: %.02f)>' % (
274
            self.current,
275
            self.maximum,
276
            self.percent or 0,
277
278
            self.remaining_seconds or 0,
279
            self.speed or 0,
280
            self.speed_min or 0
281
        )
282