Test Failed
Push — develop ( 572338...fb7300 )
by Dean
02:29
created

SyncProgressBase.add()   A

Complexity

Conditions 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 3.6875
Metric Value
cc 2
dl 0
loc 6
ccs 1
cts 4
cp 0.25
crap 3.6875
rs 9.4285
1 1
from plugin.core.environment import Environment
2 1
3
from datetime import datetime
4 1
import inspect
5
import logging
6
7 1
log = logging.getLogger(__name__)
8 1
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
        self.ended_at = None
21 1
22
    @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 1
32
    @property
33
    def percent(self):
34
        raise NotImplementedError
35
36
    @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
    def start(self):
48
        self.started_at = datetime.utcnow()
49
        self.ended_at = None
50 1
51
    def step(self, delta=1):
52
        if not delta:
53
            return
54
55
        if not self.started_at:
56
            self.start()
57 1
58
        # Update group position
59
        self.current += delta
60
61
    def stop(self):
62
        self.ended_at = datetime.utcnow()
63
64
    def _ema(self, value, previous, smoothing):
65
        if smoothing is None:
66 1
            # Use default smoothing value
67
            smoothing = self.speed_smoothing
68
69
        # Calculate EMA
70
        return smoothing * value + (1 - smoothing) * previous
71
72
73
class SyncProgress(SyncProgressBase):
74
    def __init__(self, task):
75 1
        super(SyncProgress, self).__init__('root')
76
77
        self.task = task
78
79
        self.groups = None
80
        self.group_speeds = None
81
82
    @property
83
    def percent(self):
84 1
        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 1
94
    @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
    def group(self, *tag):
112
        # Resolve tag to string
113
        tag = self._resolve_tag(tag)
114
115
        # Return existing progress group (if available)
116
        if tag in self.groups:
117
            return self.groups[tag]
118
119
        # Construct new progress group
120
        group = self.groups[tag] = SyncProgressGroup(self, tag)
121
        return group
122
123
    def start(self):
124
        super(SyncProgress, self).start()
125
126
        # Reset active groups
127
        self.groups = {}
128
129
        # Retrieve group speeds
130
        self.group_speeds = Environment.dict['sync.progress.group_speeds'] or {}
131
132
    def stop(self):
133
        super(SyncProgress, self).stop()
134
135
        # Save progress statistics
136
        self.save()
137
138
    def save(self):
139
        # Update group speeds
140
        self.group_speeds = dict([
141
            (group.tag, self._group_speed(group))
142
            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...
143
        ])
144
145
        # Save to plugin dictionary
146
        Environment.dict['sync.progress.group_speeds'] = self.group_speeds
147
148
    def _group_speed(self, group):
149
        speed = self.group_speeds.get(group.tag)
150
151
        if speed is None:
152
            # First sample
153
            return group.speed_min
154
155
        # Calculate EMA for group speed
156
        return self._ema(group.speed_min, speed, self.speed_smoothing)
157
158
    @staticmethod
159
    def _resolve_tag(tag):
160
        if isinstance(tag, (str, unicode)):
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'unicode'
Loading history...
161
            return tag
162
163
        # Resolve tag
164
        if inspect.isclass(tag[0]):
165
            cls = tag[0]
166
            path = '%s.%s' % (cls.__module__, cls.__name__)
167
168
            tag = [path] + list(tag[1:])
169
170
        # Convert tag to string
171
        return ':'.join(tag)
172
173
174
class SyncProgressGroup(SyncProgressBase):
175
    def __init__(self, root, tag):
176
        super(SyncProgressGroup, self).__init__(tag)
177
178
        self.root = root
179
180
        # Retrieve average group speed
181
        self.speed = self.root.group_speeds.get(self.tag)
182
        self.speed_min = None
183
184
    @property
185
    def percent(self):
186
        if self.maximum is None or self.current is None:
187
            return 0.0
188
189
        if self.maximum < 1:
190
            return 0.0
191
192
        value = (float(self.current) / self.maximum) * 100
193
194
        if value > 100:
195
            return 100.0
196
197
        return value
198
199
    @property
200
    def per_second(self):
201
        elapsed = self.elapsed
202
203
        if not elapsed:
204
            return None
205
206
        return float(self.current) / elapsed
207
208
    @property
209
    def remaining(self):
210
        if self.maximum is None or self.current is None:
211
            return None
212
213
        value = self.maximum - self.current
214
215
        if value < 0:
216
            return 0
217
218
        return value
219
220
    @property
221
    def remaining_seconds(self):
222
        remaining = self.remaining
223
224
        if remaining is None or self.speed is None:
225
            return None
226
227
        return float(remaining) / self.speed
228
229
    def add(self, delta):
230
        super(SyncProgressGroup, self).add(delta)
231
232
        # Update root maximum
233
        self.root.add(delta)
234
235
    def step(self, delta=1):
236
        super(SyncProgressGroup, self).step(delta)
237
238
        # Update average syncing speed
239
        self.update_speed()
240
241
        # Update root progress
242
        self.root.step(delta)
243
244
    def update_speed(self):
245
        if not self.per_second:
246
            # No steps emitted yet
247
            return
248
249
        if not self.speed:
250
            # First sample, set to current `per_second`
251
            self.speed = self.per_second
252
            return
253
254
        # Calculate average syncing speed (EMA)
255
        self.speed = self._ema(self.per_second, self.speed, self.speed_smoothing)
256
257
        # Update minimum speed
258
        if self.speed_min is None:
259
            # First sample
260
            self.speed_min = self.speed
261
        elif self.speed < self.speed_min:
262
            # New minimum speed reached
263
            self.speed_min = self.speed
264
265
    def __repr__(self):
266
        return '<SyncProgressGroup %s/%s - %.02f%% (remaining_seconds: %.02f, speed: %.02f, speed_min: %.02f)>' % (
267
            self.current,
268
            self.maximum,
269
            self.percent or 0,
270
271
            self.remaining_seconds or 0,
272
            self.speed or 0,
273
            self.speed_min or 0
274
        )
275