1from importlib import import_module
2from funcy import memoize, merge
3
4from django.conf import settings as base_settings
5from django.core.exceptions import ImproperlyConfigured
6from django.core.signals import setting_changed
7
8
9ALL_OPS = {'get', 'fetch', 'count', 'aggregate', 'exists'}
10
11
12class Defaults:
13    CACHEOPS_ENABLED = True
14    CACHEOPS_REDIS = {}
15    CACHEOPS_DEFAULTS = {}
16    CACHEOPS = {}
17    CACHEOPS_PREFIX = lambda query: ''
18    CACHEOPS_LRU = False
19    CACHEOPS_CLIENT_CLASS = None
20    CACHEOPS_DEGRADE_ON_FAILURE = False
21    CACHEOPS_SENTINEL = {}
22    # NOTE: we don't use this fields in invalidator conditions since their values could be very long
23    #       and one should not filter by their equality anyway.
24    CACHEOPS_SKIP_FIELDS = "FileField", "TextField", "BinaryField", "JSONField"
25    CACHEOPS_LONG_DISJUNCTION = 8
26    CACHEOPS_SERIALIZER = 'pickle'
27
28    FILE_CACHE_DIR = '/tmp/cacheops_file_cache'
29    FILE_CACHE_TIMEOUT = 60*60*24*30
30
31
32class Settings(object):
33    def __getattr__(self, name):
34        res = getattr(base_settings, name, getattr(Defaults, name))
35        if name in ['CACHEOPS_PREFIX', 'CACHEOPS_SERIALIZER']:
36            res = import_string(res) if isinstance(res, str) else res
37
38        # Convert old list of classes to list of strings
39        if name == 'CACHEOPS_SKIP_FIELDS':
40            res = [f if isinstance(f, str) else f.get_internal_type(res) for f in res]
41
42        # Save to dict to speed up next access, __getattr__ won't be called
43        self.__dict__[name] = res
44        return res
45
46settings = Settings()
47setting_changed.connect(lambda setting, **kw: settings.__dict__.pop(setting, None), weak=False)
48
49
50def import_string(path):
51    if "." in path:
52        module, attr = path.rsplit(".", 1)
53        return getattr(import_module(module), attr)
54    else:
55        return import_module(path)
56
57
58@memoize
59def prepare_profiles():
60    """
61    Prepares a dict 'app.model' -> profile, for use in model_profile()
62    """
63    profile_defaults = {
64        'ops': (),
65        'local_get': False,
66        'db_agnostic': True,
67        'lock': False,
68    }
69    profile_defaults.update(settings.CACHEOPS_DEFAULTS)
70
71    model_profiles = {}
72    for app_model, profile in settings.CACHEOPS.items():
73        if profile is None:
74            model_profiles[app_model.lower()] = None
75            continue
76
77        model_profiles[app_model.lower()] = mp = merge(profile_defaults, profile)
78        if mp['ops'] == 'all':
79            mp['ops'] = ALL_OPS
80        # People will do that anyway :)
81        if isinstance(mp['ops'], str):
82            mp['ops'] = {mp['ops']}
83        mp['ops'] = set(mp['ops'])
84
85        if 'timeout' not in mp:
86            raise ImproperlyConfigured(
87                'You must specify "timeout" option in "%s" CACHEOPS profile' % app_model)
88        if not isinstance(mp['timeout'], int):
89            raise ImproperlyConfigured(
90                '"timeout" option in "%s" CACHEOPS profile should be an integer' % app_model)
91
92    return model_profiles
93
94
95def model_profile(model):
96    """
97    Returns cacheops profile for a model
98    """
99    # Django migrations these fake models, we don't want to cache them
100    if model.__module__ == '__fake__':
101        return None
102
103    model_profiles = prepare_profiles()
104
105    app = model._meta.app_label.lower()
106    model_name = model._meta.model_name
107    for guess in ('%s.%s' % (app, model_name), '%s.*' % app, '*.*'):
108        if guess in model_profiles:
109            return model_profiles[guess]
110    else:
111        return None
112