Completed
Push — master ( f3cea8...224350 )
by Bertrand
01:02
created

cachalot._get_tables()   D

Complexity

Conditions 11

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 11
dl 0
loc 22
rs 4.0714

How to fix   Complexity   

Complexity

Complex classes like cachalot._get_tables() 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
# coding: utf-8
2
3
from __future__ import unicode_literals
4
import datetime
5
from decimal import Decimal
6
from hashlib import sha1
7
from time import time
8
from uuid import UUID
9
10
from django.db import connections
11
from django.db.models.sql import Query
12
from django.db.models.sql.where import ExtraWhere, SubqueryConstraint
13
from django.utils.module_loading import import_string
14
from django.utils.six import text_type, binary_type
15
16
from .settings import cachalot_settings
17
from .signals import post_invalidation
18
from .transaction import AtomicCache
19
20
21
class UncachableQuery(Exception):
22
    pass
23
24
25
CACHABLE_PARAM_TYPES = {
26
    bool, int, float, Decimal, binary_type, text_type, type(None),
27
    datetime.date, datetime.time, datetime.datetime, datetime.timedelta, UUID,
28
}
29
30
try:
31
    from psycopg2.extras import (
32
        NumericRange, DateRange, DateTimeRange, DateTimeTZRange, Inet)
33
except ImportError:
34
    pass
35
else:
36
    CACHABLE_PARAM_TYPES.update((
37
        NumericRange, DateRange, DateTimeRange, DateTimeTZRange, Inet))
38
39
40
def check_parameter_types(params):
41
    for p in params:
42
        cl = p.__class__
43
        if cl not in CACHABLE_PARAM_TYPES:
44
            if cl is list or cl is tuple:
45
                check_parameter_types(p)
46
            elif cl is dict:
47
                check_parameter_types(p.items())
48
            else:
49
                raise UncachableQuery
50
51
52
def get_query_cache_key(compiler):
53
    """
54
    Generates a cache key from a SQLCompiler.
55
56
    This cache key is specific to the SQL query and its context
57
    (which database is used).  The same query in the same context
58
    (= the same database) must generate the same cache key.
59
60
    :arg compiler: A SQLCompiler that will generate the SQL query
61
    :type compiler: django.db.models.sql.compiler.SQLCompiler
62
    :return: A cache key
63
    :rtype: str
64
    """
65
    sql, params = compiler.as_sql()
66
    check_parameter_types(params)
67
    cache_key = '%s:%s:%s' % (compiler.using, sql, params)
68
    return sha1(cache_key.encode('utf-8')).hexdigest()
69
70
71
def get_table_cache_key(db_alias, table):
72
    """
73
    Generates a cache key from a SQL table.
74
75
    :arg db_alias: Alias of the used database
76
    :type db_alias: str or unicode
77
    :arg table: Name of the SQL table
78
    :type table: str or unicode
79
    :return: A cache key
80
    :rtype: str
81
    """
82
    cache_key = '%s:%s' % (db_alias, table)
83
    return sha1(cache_key.encode('utf-8')).hexdigest()
84
85
86
def _get_query_cache_key(compiler):
87
    return import_string(cachalot_settings.CACHALOT_QUERY_KEYGEN)(compiler)
88
89
90
def _get_table_cache_key(db_alias, table):
91
    return import_string(cachalot_settings.CACHALOT_TABLE_KEYGEN)(db_alias, table)
92
93
94
def _get_tables_from_sql(connection, lowercased_sql):
95
    return [t for t in connection.introspection.django_table_names()
96
            if t in lowercased_sql]
97
98
99
def _find_subqueries(children):
100
    for child in children:
101
        if child.__class__ is SubqueryConstraint:
102
            if child.query_object.__class__ is Query:
103
                yield child.query_object
104
            else:
105
                yield child.query_object.query
106
        else:
107
            rhs = None
108
            if hasattr(child, 'rhs'):
109
                rhs = child.rhs
110
            elif child.__class__ is tuple:
111
                rhs = child[-1]
112
            if rhs.__class__ is Query:
113
                yield rhs
114
            elif hasattr(rhs, 'query'):
115
                yield rhs.query
116
        if hasattr(child, 'children'):
117
            for grand_child in _find_subqueries(child.children):
118
                yield grand_child
119
120
121
def _get_tables(query, db_alias):
122
    if '?' in query.order_by and not cachalot_settings.CACHALOT_CACHE_RANDOM:
123
        raise UncachableQuery
124
125
    tables = set(query.table_map)
126
    tables.add(query.get_meta().db_table)
127
    subquery_constraints = _find_subqueries(query.where.children
128
                                            + query.having.children)
129
    for subquery in subquery_constraints:
130
        tables.update(_get_tables(subquery, db_alias))
131
    if query.extra_select or hasattr(query, 'subquery') \
132
            or any(c.__class__ is ExtraWhere for c in query.where.children):
133
        sql = query.get_compiler(db_alias).as_sql()[0].lower()
134
        additional_tables = _get_tables_from_sql(connections[db_alias], sql)
135
        tables.update(additional_tables)
136
137
    whitelist = cachalot_settings.CACHALOT_ONLY_CACHABLE_TABLES
138
    blacklist = cachalot_settings.CACHALOT_UNCACHABLE_TABLES
139
    if (whitelist and not tables.issubset(whitelist)) \
140
            or not tables.isdisjoint(blacklist):
141
        raise UncachableQuery
142
    return tables
143
144
145
def _get_table_cache_keys(compiler):
146
    db_alias = compiler.using
147
    tables = _get_tables(compiler.query, db_alias)
148
    return [_get_table_cache_key(db_alias, t) for t in tables]
149
150
151
def _invalidate_tables(cache, db_alias, tables):
152
    now = time()
153
    d = {}
154
    for table in tables:
155
        d[_get_table_cache_key(db_alias, table)] = now
156
    cache.set_many(d, None)
157
158
    if isinstance(cache, AtomicCache):
159
        cache.to_be_invalidated.update(tables)
160
161
162
def _invalidate_table(cache, db_alias, table):
163
    cache.set(_get_table_cache_key(db_alias, table), time(), None)
164
165
    if isinstance(cache, AtomicCache):
166
        cache.to_be_invalidated.add(table)
167
    else:
168
        post_invalidation.send(table, db_alias=db_alias)
169