Passed
Push — beta ( 72a57d...7d0ef0 )
by Dean
03:02
created

Table.map_items()   F

Complexity

Conditions 20

Size

Total Lines 79

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 394.9805

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 79
ccs 1
cts 47
cp 0.0213
rs 2.221
cc 20
crap 394.9805

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like Table.map_items() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1 1
from plugin.core.helpers.variable import try_convert
2 1
from plugin.core.backup import BackupManager
3 1
from plugin.core.constants import GUID_SERVICES
4 1
from plugin.core.database import Database
5 1
from plugin.core.exceptions import AccountAuthenticationError
6
7 1
from plex.objects.library import metadata as plex_objects
8 1
from stash import ApswArchive
9 1
from trakt import objects as trakt_objects
10 1
from trakt_sync.cache.backends import StashBackend
0 ignored issues
show
Bug introduced by
The name backends does not seem to exist in module trakt_sync.cache.
Loading history...
Configuration introduced by
Unable to import 'trakt_sync.cache.backends' (invalid syntax (<string>, line 11))

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
11 1
from trakt_sync.cache.main import Cache
12 1
import elapsed
13 1
import logging
14 1
import os
15
16 1
IGNORED_DATA = [
17
    Cache.Data.get(Cache.Data.Liked),
18
    Cache.Data.get(Cache.Data.Personal)
19
]
20
21 1
log = logging.getLogger(__name__)
22
23
24 1
class SyncStateTrakt(object):
25 1
    def __init__(self, state):
26 1
        self.state = state
27 1
        self.task = state.task
28
29 1
        self.cache = None
30
31 1
        self.changes = None
32 1
        self.table = Table(self.task)
33
34 1
    def load(self):
35
        # Construct cache
36
        self.cache = self._build_cache()
37
38
        # Load table handler
39
        self.table.load()
40
41 1
    def _build_cache(self):
42
        def storage(name):
43
            return StashBackend(
44
                ApswArchive(Database.cache('trakt'), name),
45
                'lru:///?capacity=500&compact_threshold=1500',
46
                'pickle:///?protocol=2'
47
            )
48
49
        cache = Cache(self.task.media, self.task.data, storage)
50
51
        # Bind to cache events
52
        cache.events.on([
53
            'refresh.sync.progress',
54
            'refresh.list.progress'
55
        ], self.on_refresh_progress)
56
57
        return cache
58
59 1
    def on_refresh_progress(self, source, current):
0 ignored issues
show
Unused Code introduced by
The argument current seems to be unused.
Loading history...
60
        # Step refresh progress for `source`
61
        self.task.progress.group(SyncStateTrakt, 'refresh:%s' % source).step()
62
63 1
    def __getitem__(self, key):
64
        collection = [
65
            self.task.account.trakt.username,
66
            Cache.Media.get(key[0]),
67
            Cache.Data.get(key[1])
68
        ]
69
70
        if len(key) > 2:
71
            # Include extra parameters (list id)
72
            collection.extend(key[2:])
73
74
        return self.cache[collection]
75
76 1
    def invalidate(self, *key):
77
        """Invalidate collection in trakt cache"""
78
        username = self.task.account.trakt.username
79
80
        # Invalidate collection
81
        self.cache.invalidate([username] + list(key))
82
83
        log.debug('Invalidated trakt cache %r for account: %r', key, username)
84
85 1
    @elapsed.clock
86
    def refresh(self):
87
        account = self.task.account
88
89
        if not account.trakt or not account.trakt.username:
90
            raise AccountAuthenticationError("Trakt account hasn't been authenticated")
91
92
        # Task checkpoint
93
        self.task.checkpoint()
94
95
        # Construct progress groups
96
        def setup_progress_group(source):
97
            # Retrieve steps from cache source
98
            steps = self.cache.source(source).steps()
99
100
            # Setup progress group with total steps
101
            self.task.progress.group(SyncStateTrakt, 'refresh:%s' % source).add(steps)
102
103
        setup_progress_group('list')
104
        setup_progress_group('sync')
105
106
        # Refresh cache for account, store changes
107
        self.changes = self.cache.refresh(account.trakt.username)
108
109
        # Resolve changes
110
        self.changes = list(self.changes)
111
112
        # Reset current table
113
        self.table.reset()
114
115 1
    @elapsed.clock
116
    def build_table(self):
117
        # Build table from cache
118
        self.table.build(self.cache)
119
120 1
    @elapsed.clock
121
    def flush(self):
122
        with elapsed.clock(SyncStateTrakt, 'flush:collections'):
123
            # Flush trakt collections to disk
124
            self.cache.collections.flush()
125
126
        with elapsed.clock(SyncStateTrakt, 'flush:stores'):
127
            # Flush trakt stores to disk
128
            for key, store in self.cache.stores.items():
129
                log.debug('[%-38s] Flushing collection...', '/'.join(key))
130
131
                store.flush()
132
133
        # Store backup of trakt data
134
        group = os.path.join('trakt', str(self.task.account.id))
135
136
        BackupManager.database.backup(group, Database.cache('trakt'), self.task.id, {
137
            'account': {
138
                'id': self.task.account.id,
139
                'name': self.task.account.name,
140
141
                'trakt': {
142
                    'username': self.task.account.trakt.username
143
                }
144
            }
145
        })
146
147
148 1
class Table(object):
149 1
    def __init__(self, task):
150 1
        self.task = task
151
152 1
        self.movies = None
153 1
        self.shows = None
154
155 1
        self.movie_keys = None
156 1
        self.show_keys = None
157 1
        self.episode_keys = None
158
159 1
        self._data = None
160 1
        self._media = None
161
162 1
    def load(self):
163
        # Parse data/media enums into lists
164
        self._data = [
165
            Cache.Data.get(d)
166
            for d in Cache.Data.parse(self.task.data)
167
        ]
168
169
        self._media = [
170
            Cache.Media.get(m)
171
            for m in Cache.Media.parse(self.task.media)
172
        ]
173
174 1
    def reset(self):
175
        self.movies = None
176
        self.shows = None
177
178
        self.movie_keys = None
179
        self.show_keys = None
180
        self.episode_keys = None
181
182 1
    def build(self, cache):
183
        # Map item `keys` into tables
184
        self.movies = {}
185
        self.shows = {}
186
187
        self.movie_keys = set()
188
        self.show_keys = set()
189
        self.episode_keys = {}
190
191
        log.debug('Building tables...')
192
193
        log.debug(' - Data: %s', ', '.join([
194
            '/'.join(x) if type(x) is tuple else x
195
            for x in self._data
196
        ]))
197
198
        log.debug(' - Media: %s', ', '.join([
199
            '/'.join(x) if type(x) is tuple else x
200
            for x in self._media
201
        ]))
202
203
        # Construct progress group
204
        self.task.progress.group(Table, 'build').add(len(cache.collections))
205
206
        # Map each item in cache collections
207
        for key in cache.collections:
208
            # Increment one step
209
            self.task.progress.group(Table, 'build').step()
210
211
            # Parse `key`
212
            if len(key) == 3:
213
                # Sync
214
                username, media, data = key
215
            elif len(key) == 4:
216
                # Lists
217
                username, media, data = tuple(key[0:3])
218
            else:
219
                log.warn('Unknown key: %r', key)
220
                continue
221
222
            if username != self.task.account.trakt.username:
223
                # Collection isn't for the current account
224
                continue
225
226
            if media and media not in self._media:
227
                log.debug('[%-38s] Media %r has not been enabled', '/'.join(key), media)
228
                continue
229
230
            if data not in self._data:
231
                log.debug('[%-38s] Data %r has not been enabled', '/'.join(key), data)
232
                continue
233
234
            # Map store items
235
            if data not in IGNORED_DATA:
236
                self.map_items(key, cache[key], media)
237
238
        log.debug(
239
            'Built tables with %d keys (movies: %d, shows: %d, episodes: %d)',
240
            len(self.movies) + len(self.shows),
241
            len(self.movie_keys),
242
            len(self.show_keys),
243
            len(self.episode_keys)
244
        )
245
246 1
    def map_items(self, key, store, media=None):
247
        # Retrieve key map
248
        if media is not None:
249
            keys = self.keys(media)
250
            table = self.table(media)
251
252
            if keys is None or table is None:
253
                log.debug('[%-38s] Collection has been ignored (unknown/unsupported media)', '/'.join(key))
254
                return
255
        else:
256
            keys = None
257
            table = None
258
259
        # Map each item in store
260
        log.debug('[%-38s] Building table from collection...', '/'.join(key))
261
262
        for pk, item in store.iteritems():
263
            # Trim `pk` season/episode values
264
            if len(pk) > 2:
265
                pk = tuple(pk[:2])
266
267
            if pk[0] not in GUID_SERVICES:
268
                log.info('Ignoring item %r with an unknown primary agent: %r', item, pk)
269
                continue
270
271
            # Detect media type from `item`
272
            if media is not None:
273
                i_media = media
274
                i_keys = keys
275
                i_table = table
276
            else:
277
                i_media = self.media(item)
278
                i_keys = self.keys(i_media)
279
                i_table = self.table(i_media)
280
281
            # Store `pk` in `keys
282
            if i_keys is not None:
283
                i_keys.add(pk)
284
285
            # Map `item.keys` -> `pk`
286
            for key in item.keys:
287
                # Expand `key`
288
                if type(key) is not tuple or len(key) != 2:
289
                    continue
290
291
                service, id = key
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in id.

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

Loading history...
292
293
                # Check if agent is supported
294
                if service not in GUID_SERVICES:
295
                    continue
296
297
                # Cast service id to integer
298
                if service in ['tvdb', 'tmdb', 'tvrage']:
299
                    id = try_convert(id, int, id)
300
301
                # Store key in table
302
                key = (service, id)
303
304
                if key in i_table:
305
                    continue
306
307
                i_table[key] = pk
308
309
            # Map episodes in show
310
            if i_media == 'episodes':
311
                if type(item) is trakt_objects.Show:
312
                    if pk not in self.episode_keys:
313
                        self.episode_keys[pk] = set()
314
315
                    for identifier, _ in item.episodes():
316
                        self.episode_keys[pk].add(identifier)
317
                elif type(item) is trakt_objects.Episode:
318
                    # TODO
319
                    pass
320
                else:
321
                    log.debug('Unknown episode item: %r', item)
322
323
        # Task checkpoint
324
        self.task.checkpoint()
325
326 1
    def keys(self, media):
327
        if type(media) is not str:
328
            media = self.media(media)
329
330
        if media == 'movies':
331
            return self.movie_keys
332
333
        if media in ['shows', 'seasons', 'episodes']:
334
            return self.show_keys
335
336
        log.warn('Unknown media: %r', media)
337
        return None
338
339 1
    @staticmethod
340
    def media(item):
341 1
        if type(item) is not type:
342 1
            i_type = type(item)
343
        else:
344
            i_type = item
345
346 1
        if issubclass(i_type, (trakt_objects.Movie, plex_objects.Movie)):
347 1
            return 'movies'
348
349 1
        if issubclass(i_type, (trakt_objects.Show, plex_objects.Show)):
350
            return 'shows'
351
352 1
        if issubclass(i_type, (trakt_objects.Season, plex_objects.Season)):
353
            return 'seasons'
354
355 1
        if issubclass(i_type, (trakt_objects.Episode, plex_objects.Episode)):
356 1
            return 'episodes'
357
358
        log.warn('Unknown item type: %r', i_type)
359
        return None
360
361 1
    def table(self, media):
362 1
        if type(media) is not str:
363 1
            media = self.media(media)
364
365 1
        if media == 'movies':
366 1
            return self.movies
367
368 1
        if media in ['shows', 'seasons', 'episodes']:
369 1
            return self.shows
370
371
        log.warn('Unknown media: %r', media)
372
        return None
373
374 1
    def __call__(self, media):
375
        return self.table(media)
376