source: orange-bioinformatics/orangecontrib/bio/obiKEGG/caching.py @ 1875:6d87bc9aaf5a

Revision 1875:6d87bc9aaf5a, 7.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 6 months ago (diff)

Cache entry should be invalid if unpickling raises an error.

Line 
1"""
2Caching framework for cached kegg api calls.
3
4"""
5import os
6import UserDict
7import sqlite3
8import cPickle as pickle
9
10from contextlib import closing
11
12from datetime import datetime, date, timedelta
13from . import conf
14
15
16class Store(object):
17    def __init__(self):
18        self.timestamp = 0
19
20    def open(self):
21        raise NotImplementedError
22
23    def __enter__(self):
24        return self
25
26    def __exit__(self, *args):
27        pass
28
29
30class Sqlite3Store(Store, UserDict.DictMixin):
31    def __init__(self, filename):
32        self.filename = filename
33        self.con = sqlite3.connect(filename)
34        self.con.execute("""
35            CREATE TABLE IF NOT EXISTS cache
36                (key TEXT UNIQUE,
37                 value TEXT
38                )
39        """)
40        self.con.execute("""
41            CREATE INDEX IF NOT EXISTS cache_index
42            ON cache (key)
43        """)
44        self.con.commit()
45
46    def __getitem__(self, key):
47        cur = self.con.execute("""
48            SELECT value
49            FROM cache
50            WHERE key=?
51        """, (key,))
52        r = cur.fetchall()
53
54        if not r:
55            raise KeyError(key)
56        else:
57            pickle_str = str(r[0][0])
58            try:
59                return pickle.loads(pickle_str)
60            except Exception:
61                raise KeyError(key)
62
63    def __setitem__(self, key, value):
64        value = pickle.dumps(value)
65        self.con.execute("""
66            INSERT OR REPLACE INTO cache
67            VALUES (?, ?)
68        """, (key, value))
69        self.con.commit()
70
71    def __delitem__(self, key):
72        self.con.execute("""
73            DELETE FROM cache
74            WHERE key=?
75        """, (key,))
76        self.con.commit()
77
78    def keys(self):
79        cur = self.con.execute("""
80            SELECT key
81            FROM cache
82        """)
83        return [str(r[0]) for r in cur.fetchall()]
84
85    def close(self):
86        pass
87
88
89class DictStore(Store, UserDict.DictMixin):
90    def __init__(self):
91        Store.__init__(self)
92
93    def close(self):
94        pass
95
96
97class cache_entry(object):
98    def __init__(self, value, mtime=None, expires=None):
99        self.value = value
100        self.mtime = mtime
101        self.expires = expires
102
103_SESSION_START = datetime.now()
104
105
106class cached_wrapper(object):
107    """
108    TODO: needs documentation
109    """
110    def __init__(self, function, instance, class_, cache_store,
111                 last_modified=None):
112        self.function = function
113        self.instance = instance
114        self.class_ = class_
115        self.cache_store = cache_store
116        self.last_modified = last_modified
117
118    def has_key(self, key):
119        with closing(self.cache_store()) as store:
120            return key in store
121
122    def key_from_args(self, args, kwargs=None):
123        key = self.function.__name__ + repr(args)
124        if kwargs is not None:
125            raise NotImplementedError("kwargs are not supported yet")
126        return key
127
128    def invalidate_key(self, key):
129        with closing(self.cache_store()) as store:
130            del store[key]
131
132    def last_modified_from_args(self, args, kwargs=None):
133        if self.instance is not None:
134            return self.instance.last_modified(args)
135
136    def invalidate_args(self, args):
137        return self.invalidate_key(self.key_from_args(args))
138
139    def invalidate_all(self):
140        prefix = self.key_from_args(()).rstrip(",)")
141        with self.cache_store() as store:
142            for key in store.keys():
143                if key.startswith(prefix):
144                    del store[key]
145
146    def memoize(self, args, kwargs, value, timestamp=None):
147        key = self.key_from_args(args, kwargs)
148        if timestamp is None:
149            timestamp = datetime.now()
150
151        with closing(self.cache_store()) as store:
152            store[key] = cache_entry(value, mtime=timestamp)
153
154    def __call__(self, *args):
155        key = self.key_from_args(args)
156        with closing(self.cache_store()) as store:
157            if self.key_has_valid_cache(key, store):
158                rval = store[key].value
159            else:
160                rval = self.function(self.instance, *args)
161                store[key] = cache_entry(rval, datetime.now(), None)
162
163        return rval
164
165    def key_has_valid_cache(self, key, store):
166        if key not in store:
167            return False
168        else:
169            entry = store[key]
170            return self.is_entry_valid(entry, None)
171
172    def is_entry_valid(self, entry, args):
173        # For now always return True, the caching architecture needs to be
174        # completely reworked
175        return True
176
177        # Need to check datetime first (it subclasses date)
178        if isinstance(entry.mtime, datetime):
179            mtime = entry.mtime
180        elif isinstance(entry.mtime, date):
181            mtime = datetime(entry.mtime.year, entry.mtime.month,
182                             entry.mtime.day, 1, 1, 1)
183        else:
184            return False
185
186        last_modified = self.last_modified_from_args(args)
187
188        if isinstance(last_modified, date):
189            last_modified = datetime(last_modified.year, last_modified.month,
190                                     last_modified.day, 1, 1, 1)
191        elif isinstance(last_modified, basestring):
192            # Could have different format
193            mtime = mtime.strftime("%Y %m %d %H %M %S")
194
195        elif last_modified is None:
196            if conf.params["cache.invalidate"] == "always":
197                return False
198            elif conf.params["cache.invalidate"] == "session":
199                last_modified = _SESSION_START
200            elif conf.params["cache.invalidate"] == "daily":
201                last_modified = datetime.now().replace(hour=0, minute=0,
202                                                       second=0, microsecond=0)
203            elif conf.params["cache.invalidate"] == "weekly":
204                last_modified = datetime.now() - timedelta(7)
205            else:  # ???
206                pass
207        return last_modified <= mtime
208
209
210class cached_method(object):
211    def __init__(self, function):
212        self.function = function
213
214    def __get__(self, instance, owner):
215        if instance is not None:
216            return cached_wrapper(self.function, instance, owner,
217                                  self.get_cache_store(instance, owner))
218        return self
219
220    def get_cache_store(self, instance, owner):
221        if hasattr(instance, "cache_store"):
222            return instance.cache_store
223        elif not hasattr("_cached_method_cache"):
224            instance._cached_method_cache = DictStore()
225        return instance._cached_method_cache
226
227
228class bget_cached_method(cached_method):
229    def __get__(self, instance, owner):
230        if instance is not None:
231            return cached_wrapper(self.function, instance, owner,
232                                  self.get_cache_store(instance, owner),
233                                  self.get_last_modified(instance, owner))
234        return self
235
236    def get_last_modified(self, instance, owner):
237        if hasattr(instance, "last_modified"):
238            return instance.last_modified
239
240
241def touch_dir(path):
242    path = os.path.expanduser(path)
243    if not os.path.exists(path):
244        os.makedirs(path)
245
246
247def clear_cache():
248    """Clear all locally cached KEGG data.
249    """
250    import glob
251    path = conf.params["cache.path"]
252    if os.path.realpath(path) != os.path.realpath(conf.kegg_dir):
253        raise Exception("Non default cache path. Please remove the contents "
254                        "of %r manually." % path)
255
256    for cache_filename in glob.glob(os.path.join(path, "*.sqlite3")):
257        os.remove(cache_filename)
258
259    for ko_filename in glob.glob(os.path.join(path, "*.keg")):
260        os.remove(ko_filename)
261
262    for kgml_filename in glob.glob(os.path.join(path, "*.xml")):
263        os.remove(kgml_filename)
264
265    for png_filename in glob.glob(os.path.join(path, "*.png")):
266        os.remove(png_filename)
Note: See TracBrowser for help on using the repository browser.