Passed
Push — develop ( b9b638...b3bfc7 )
by Dean
03:47
created

Database._connection()   A

Complexity

Conditions 3

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 8.2077
Metric Value
cc 3
dl 0
loc 9
ccs 1
cts 6
cp 0.1666
crap 8.2077
rs 9.6666
1 1
from plugin.core.environment import Environment
2
3 1
from datetime import datetime
4 1
from threading import RLock
5 1
from playhouse.apsw_ext import APSWDatabase
6 1
import apsw
0 ignored issues
show
Configuration introduced by
The import apsw could not be resolved.

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...
7 1
import logging
8 1
import os
9
10 1
log = logging.getLogger(__name__)
11
12 1
BACKUP_PATH = os.path.join(Environment.path.plugin_data, 'Backups')
13 1
BUSY_TIMEOUT = 3000
14
15
16 1
class Database(object):
17 1
    _cache = {
18
        'peewee': {},
19
        'raw': {}
20
    }
21 1
    _lock = RLock()
22
23 1
    @classmethod
24
    def main(cls):
25 1
        return cls._get(Environment.path.plugin_database, 'peewee')
26
27 1
    @classmethod
28
    def cache(cls, name):
29 1
        return cls._get(os.path.join(Environment.path.plugin_caches, '%s.db' % name), 'raw')
30
31 1
    @classmethod
32 1
    def backup(cls, group, database, tag=None):
33
        timestamp = datetime.now()
34
35
        # Build backup directory/name
36
        directory, name = cls._backup_path(group, tag, timestamp)
37
        path = os.path.join(directory, name)
38
39
        log.info('[%s] Backing up database to %r', group, path)
40
41
        # Ensure directory exists
42
        if not os.path.exists(directory):
43
            os.makedirs(directory)
44
45
        # Backup database
46
        destination = cls._connect(path, 'raw')
47
48
        # Get `database` connection
49
        source = cls._connection(database)
50
51
        # Backup `source` database to `destination`
52
        try:
53
            cls._backup(group, source, destination)
54
        finally:
55
            # Close `destination` database
56
            destination.close()
57
58
        # Ensure path exists
59
        if not os.path.exists(path):
60
            log.error('Backup failed (file doesn\'t exist)')
61
            return False
62
63
        return True
64
65 1
    @classmethod
66 1
    def reset(cls, group, database, tag=None):
67
        # Backup database
68
        if not cls.backup(group, database, tag):
69
            return False
70
71
        log.info('[%s] Resetting database objects...', group)
72
73
        # Get `database` connection
74
        conn = cls._connection(database)
75
76
        # Drop all objects (index, table, trigger)
77
        conn.cursor().execute(
78
            "PRAGMA writable_schema = 1; "
79
            "DELETE FROM sqlite_master WHERE type IN ('table', 'index', 'trigger'); "
80
            "PRAGMA writable_schema = 0;"
81
        )
82
83
        # Recover space
84
        conn.cursor().execute('VACUUM;')
85
86
        # Check database integrity
87
        integrity, = conn.cursor().execute('PRAGMA INTEGRITY_CHECK;').fetchall()[0]
88
89
        if integrity != 'ok':
90
            log.error('[%s] Database integrity check error: %r', group, integrity)
91
            return False
92
93
        log.info('[%s] Database reset', group)
94
        return True
95
96 1
    @classmethod
97
    def _backup(cls, group, source, destination):
98
        with destination.backup('main', source, 'main') as b:
99
            while not b.done:
100
                # Backup page step
101
                b.step(100)
102
103
                # Report progress
104
                progress = float(b.pagecount - b.remaining) / b.pagecount
105
106
                log.debug('[%s] Backup Progress: %3d%%', group, progress * 100)
107
108 1
    @classmethod
109
    def _backup_path(cls, group, tag, timestamp):
110
        # Build directory
111
        directory = os.path.join(
112
            BACKUP_PATH,
113
            group,
114
            str(timestamp.year),
115
            '%02d' % timestamp.month
116
        )
117
118
        # Build filename
119
        name = '%02d_%02d%02d%02d%s.db' % (
120
            timestamp.day,
121
            timestamp.hour,
122
            timestamp.minute,
123
            timestamp.second,
124
            ('_%s' % tag) if tag else ''
125
        )
126
127
        return directory, name
128
129 1
    @classmethod
130
    def _connect(cls, path, type):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in type.

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

Loading history...
131
        # Connect to new database
132 1
        if type == 'peewee':
133 1
            db = APSWDatabase(path, autorollback=True, journal_mode='WAL', timeout=BUSY_TIMEOUT)
134 1
        elif type == 'raw':
135 1
            db = apsw.Connection(path, flags=apsw.SQLITE_OPEN_READWRITE | apsw.SQLITE_OPEN_CREATE | apsw.SQLITE_OPEN_WAL)
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (121/120).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
136 1
            db.setbusytimeout(BUSY_TIMEOUT)
137
        else:
138
            raise ValueError('Unknown database type: %r' % type)
139
140 1
        log.debug('Connected to database at %r', path)
141 1
        return db
142
143 1
    @classmethod
144
    def _connection(cls, database):
145
        if isinstance(database, APSWDatabase):
146
            return database.get_conn()
147
148
        if isinstance(database, apsw.Connection):
149
            return database
150
151
        raise ValueError('Unknown "database" parameter provided')
152
153 1
    @classmethod
154
    def _get(cls, path, type):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in type.

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

Loading history...
155 1
        path = os.path.abspath(path)
156 1
        cache = cls._cache[type]
157
158 1
        with cls._lock:
159 1
            if path not in cache:
160 1
                cache[path] = cls._connect(path, type)
161
162
            # Return cached connection
163 1
            return cache[path]
164
165