SyncTask.elapsed()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 3.6875

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 1
cts 4
cp 0.25
rs 9.4285
c 0
b 0
f 0
cc 2
crap 3.6875
1 1
from plugin.managers.exception import ExceptionManager
2 1
from plugin.models import *
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in Exception.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
Coding Style introduced by
The usage of wildcard imports like plugin.models should generally be avoided.
Loading history...
3 1
from plugin.sync.core.enums import SyncData, SyncMode
4 1
from plugin.sync.core.exceptions import SyncAbort, QueueError
5 1
from plugin.sync.core.task.artifacts import SyncArtifacts
6 1
from plugin.sync.core.task.configuration import SyncConfiguration
7 1
from plugin.sync.core.task.map import SyncMap
8 1
from plugin.sync.core.task.pending import SyncPending
9 1
from plugin.sync.core.task.progress import SyncProgress
10 1
from plugin.sync.core.task.profiler import SyncProfiler
11 1
from plugin.sync.core.task.state import SyncState
12
13 1
from datetime import datetime
14 1
from peewee import JOIN_LEFT_OUTER
15 1
import logging
16 1
import time
17
18 1
log = logging.getLogger(__name__)
19
20
21 1
class SyncTask(object):
22 1
    def __init__(self, account, mode, data, media, result, status, **kwargs):
23 1
        self.account = account
24
25
        # Sync options
26 1
        self.mode = mode
27 1
        self.data = data
28 1
        self.media = media
29
30
        # Extra arguments
31 1
        self.kwargs = kwargs
32
33
        # Handlers/Modes for task
34 1
        self.handlers = None
35 1
        self.modes = None
36
37
        # State/Result management
38 1
        self.result = result
39 1
        self.status = status
40
41 1
        self.exceptions = []
42
43 1
        self.finished = False
44 1
        self.started = False
45 1
        self.success = None
46
47 1
        self._abort = False
48
49
        # Construct children
50 1
        self.artifacts = SyncArtifacts(self)
51 1
        self.configuration = SyncConfiguration(self)
52 1
        self.map = SyncMap(self)
53 1
        self.pending = SyncPending(self)
54 1
        self.progress = SyncProgress(self)
55 1
        self.profiler = SyncProfiler(self)
56
57 1
        self.state = SyncState(self)
58
59 1
    @property
60
    def id(self):
61
        if self.result is None:
62
            return None
63
64
        return self.result.id
65
66 1
    @property
67
    def elapsed(self):
68
        if self.result is None:
69
            return None
70
71
        return (datetime.utcnow() - self.result.started_at).total_seconds()
72
73 1
    def construct(self, handlers, modes):
74
        log.debug('Constructing %d handlers...', len(handlers))
75
        self.handlers = dict(self._construct_modules(handlers, 'data'))
76
77
        log.debug('Constructing %d modes...', len(modes))
78
        self.modes = dict(self._construct_modules(modes, 'mode'))
79
80 1
    def load(self):
81
        log.debug('Task Arguments: %r', self.kwargs)
82
83
        # Load task configuration
84
        self.configuration.load(self.account)
85
86
        # Automatically determine enabled data types
87
        if self.data is None:
88
            self.data = self.get_enabled_data(self.configuration, self.mode)
89
90
        log.debug('Task Data: %r', self.data)
91
        log.debug('Task Media: %r', self.media)
92
93
        if self.data is None:
94
            raise QueueError('No collections enabled for sync')
95
96
        # Load children
97
        self.profiler.load()
98
        self.state.load()
99
100 1
    def abort(self, timeout=None):
101
        # Set `abort` flag, thread will abort on the next `checkpoint()`
102
        self._abort = True
103
104
        if timeout is None:
105
            return
106
107
        # Wait `timeout` seconds for task to finish
108
        for x in xrange(timeout):
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'xrange'
Loading history...
Unused Code introduced by
The variable x seems to be unused.
Loading history...
109
            if self.finished:
110
                return
111
112
            time.sleep(1)
113
114 1
    def checkpoint(self):
115
        # Check if an abort has been requested
116
        if not self._abort:
117
            return
118
119
        raise SyncAbort()
120
121 1
    def finish(self):
122
        # Update result in database
123
        self.result.ended_at = datetime.utcnow()
124
        self.result.success = self.success
125
        self.result.save()
126
127
        # Store exceptions in database
128
        for exc_info in self.exceptions:
129
            try:
130
                self.store_exception(self.result, exc_info)
131
            except Exception as ex:
132
                log.warn('Unable to store exception: %s', str(ex), exc_info=True)
133
134
        # Flush caches to archives
135
        self.state.flush()
136
137
        # Display profiler report
138
        self.profiler.log_report()
139
140
        # Mark finished
141
        self.finished = True
142
143 1
    @staticmethod
144
    def store_exception(result, exc_info):
145
        exception, error = ExceptionManager.create.from_exc_info(exc_info)
146
147
        # Link error to result
148
        SyncResultError.create(
149
            result=result,
150
            error=error
151
        )
152
153
        # Link exception to result
154
        SyncResultException.create(
155
            result=result,
156
            exception=exception
157
        )
158
159 1
    @classmethod
160
    def create(cls, account, mode, data, media, trigger, **kwargs):
161
        # Get account
162
        if type(account) is int:
163
            account = cls.get_account(account)
164
        elif type(account) is not Account:
165
            raise ValueError('Unexpected value provided for the "account" parameter')
166
167
        # Get/Create sync status
168
        status, created = SyncStatus.get_or_create(
0 ignored issues
show
Unused Code introduced by
The variable created seems to be unused.
Loading history...
169
            account=account,
170
            mode=mode,
171
            section=kwargs.get('section', None)
172
        )
173
174
        # Create sync result
175
        result = SyncResult.create(
176
            status=status,
177
            trigger=trigger,
178
179
            started_at=datetime.utcnow()
180
        )
181
182
        # Create sync task
183
        task = SyncTask(
184
            account, mode,
185
            data, media,
186
            result, status,
187
            **kwargs
188
        )
189
190
        # Load sync configuration/state
191
        task.load()
192
193
        return task
194
195 1
    @classmethod
196
    def get_account(cls, account_id):
197
        # TODO Move account retrieval/join to `Account` class
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
198
        return (
199
            Account.select(
200
                Account.id,
0 ignored issues
show
Bug introduced by
The Class Account does not seem to have a member named id.

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...
201
                Account.name,
202
203
                PlexAccount.id,
0 ignored issues
show
Bug introduced by
The Class PlexAccount does not seem to have a member named id.

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...
204
                PlexAccount.key,
205
                PlexAccount.username,
206
                PlexBasicCredential.token_plex,
207
                PlexBasicCredential.token_server,
208
209
                TraktAccount.username,
210
                TraktBasicCredential.token,
211
212
                TraktOAuthCredential.access_token,
213
                TraktOAuthCredential.refresh_token,
214
                TraktOAuthCredential.created_at,
215
                TraktOAuthCredential.expires_in
216
            )
217
            # Plex
218
            .join(
219
                PlexAccount, JOIN_LEFT_OUTER, on=(
220
                    PlexAccount.account == Account.id
0 ignored issues
show
Bug introduced by
The Class Account does not seem to have a member named id.

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...
221
                ).alias('plex')
222
            )
223
            .join(
224
                PlexBasicCredential, JOIN_LEFT_OUTER, on=(
225
                    PlexBasicCredential.account == PlexAccount.id
0 ignored issues
show
Bug introduced by
The Class PlexAccount does not seem to have a member named id.

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...
226
                ).alias('basic')
227
            )
228
            # Trakt
229
            .switch(Account)
230
            .join(
231
                TraktAccount, JOIN_LEFT_OUTER, on=(
232
                    TraktAccount.account == Account.id
0 ignored issues
show
Bug introduced by
The Class Account does not seem to have a member named id.

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...
233
                ).alias('trakt')
234
            )
235
            .join(
236
                TraktBasicCredential, JOIN_LEFT_OUTER, on=(
237
                    TraktBasicCredential.account == TraktAccount.id
0 ignored issues
show
Bug introduced by
The Class TraktAccount does not seem to have a member named id.

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...
238
                ).alias('basic')
239
            )
240
            .switch(TraktAccount)
241
            .join(
242
                TraktOAuthCredential, JOIN_LEFT_OUTER, on=(
243
                    TraktOAuthCredential.account == TraktAccount.id
0 ignored issues
show
Bug introduced by
The Class TraktAccount does not seem to have a member named id.

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...
244
                ).alias('oauth')
245
            )
246
            .where(Account.id == account_id)
0 ignored issues
show
Bug introduced by
The Class Account does not seem to have a member named id.

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...
247
            .get()
248
        )
249
250 1
    @classmethod
251
    def get_enabled_data(cls, config, mode):
252
        # Determine accepted modes
253
        modes = [SyncMode.Full]
254
255
        if mode == SyncMode.Full:
256
            modes.extend([
257
                SyncMode.FastPull,
258
                SyncMode.Pull,
259
                SyncMode.Push
260
            ])
261
        elif mode == SyncMode.FastPull:
262
            modes.extend([
263
                mode,
264
                SyncMode.Pull
265
            ])
266
        else:
267
            modes.append(mode)
268
269
        # Retrieve enabled data
270
        enabled = []
271
272
        if config['sync.watched.mode'] in modes:
273
            enabled.append(SyncData.Watched)
274
275
        if config['sync.ratings.mode'] in modes:
276
            enabled.append(SyncData.Ratings)
277
278
        if config['sync.playback.mode'] in modes:
279
            enabled.append(SyncData.Playback)
280
281
        if config['sync.collection.mode'] in modes:
282
            enabled.append(SyncData.Collection)
283
284
        # Lists
285
        if config['sync.lists.watchlist.mode'] in modes:
286
            enabled.append(SyncData.Watchlist)
287
288
        if config['sync.lists.liked.mode'] in modes:
289
            enabled.append(SyncData.Liked)
290
291
        if config['sync.lists.personal.mode'] in modes:
292
            enabled.append(SyncData.Personal)
293
294
        # Convert to enum value
295
        result = None
296
297
        for data in enabled:
298
            if result is None:
299
                result = data
300
                continue
301
302
            result |= data
303
304
        return result
305
306 1
    def _construct_modules(self, modules, attribute):
307
        for cls in modules:
308
            keys = getattr(cls, attribute, None)
309
310
            if keys is None:
311
                log.warn('Module %r is missing a valid %r attribute', cls, attribute)
312
                continue
313
314
            # Convert `keys` to list
315
            if type(keys) is not list:
316
                keys = [keys]
317
318
            # Construct module
319
            obj = cls(self)
320
321
            # Return module with keys
322
            for key in keys:
323
                yield key, obj
324