Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
241782 views
1
#################################################################################
2
#
3
# (c) Copyright 2010 William Stein
4
#
5
# This file is part of PSAGE
6
#
7
# PSAGE is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# PSAGE is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
#
20
#################################################################################
21
22
23
"""
24
This module implements a simple key:value store using SQLite3 and
25
cPickle, and other useful tools built on top of it.
26
"""
27
28
import cPickle, sqlite3, zlib
29
30
# A key:value store
31
32
class SQLiteKeyValueStore:
33
def __init__(self, file, compress=False):
34
"""
35
Create or open the SQLite3-based key:value database stored in the given file.
36
37
INPUTS:
38
- file -- string; the name of a file.
39
- compress -- bool (default: False); if True, by default compress all
40
pickled values using zlib
41
42
You do not have to be consistent with the compress option. The database will still
43
work if you switch back and forth between compress=True and compress=False.
44
"""
45
self._db = sqlite3.connect(file)
46
self._cursor = self._db.cursor()
47
self._file = file
48
self._compress = compress
49
try:
50
self._cursor.execute("select * from sqlite_master").next()
51
except StopIteration:
52
# This exception will occur if the database is brand new (has no tables yet)
53
try:
54
self._cursor.execute("CREATE TABLE cache (key BLOB, value BLOB, compressed INTEGER, UNIQUE(key))")
55
self._cursor.execute("CREATE INDEX cache_idx ON cache(key)")
56
self._db.commit()
57
except sqlite3.OperationalError:
58
pass # failure could happen if another process maybe created
59
# and initialized the database at the same time. That's fine.
60
61
def __del__(self):
62
"""Called when the database is freed to close the connection."""
63
self._db.close()
64
65
def __repr__(self):
66
"""String representation of the database."""
67
return "SQLite3-based key:value database stored in '%s'"%self._file
68
69
def has_key(self, key):
70
"""Returns True if database has the given key."""
71
return self._cursor.execute( "SELECT count(*) FROM cache WHERE key=?", (self._dumps(key),) ).next()[0] > 0
72
73
def __getitem__(self, key):
74
"""Return item in the database with given key, or raise KeyError."""
75
s = self._cursor.execute( "SELECT value,compressed FROM cache WHERE key=?", (self._dumps(key),) )
76
try:
77
v = s.next()
78
return self._loads(str(v[0]), bool(v[1]))
79
except StopIteration:
80
raise KeyError, str(key)
81
82
def __setitem__(self, key, value):
83
"""Sets an item in the database. Call commit to make this permanent."""
84
self._cursor.execute("INSERT OR REPLACE INTO cache VALUES(?, ?, ?)", (
85
self._dumps(key), self._dumps(value, self._compress), self._compress))
86
87
def __delitem__(self, key):
88
"""Removes an item from the database. Call commit to make this permanent."""
89
self._cursor.execute("DELETE FROM cache WHERE key=?", (self._dumps(key),) )
90
91
def _dumps(self, x, compress=False):
92
"""Converts a Python object to a binary string that can be stored in the database."""
93
s = cPickle.dumps(x,2)
94
if compress:
95
s = zlib.compress(s)
96
return sqlite3.Binary(s)
97
98
def _loads(self, x, compress=False):
99
"""Used internally to turn a pickled object in the database into a Python object."""
100
if compress:
101
x = zlib.decompress(x)
102
return cPickle.loads(x)
103
104
def keys(self):
105
"""Return list of keys in the database."""
106
return [self._loads(str(x[0])) for x in self._cursor.execute( "SELECT key FROM cache" )]
107
108
def commit(self):
109
"""Write assignments made to the database to disk."""
110
self._db.commit()
111
112
113
def test_sqlite_keyval_1():
114
"""A straightforward test."""
115
import tempfile
116
file = tempfile.mktemp()
117
try:
118
for compress in [False, True]:
119
db = SQLiteKeyValueStore(file, compress)
120
121
db[2] = 3
122
db[10] = {1:5, '17a':[2,5]}
123
assert db.keys() == [2,10]
124
assert db[10] == {1:5, '17a':[2,5]}
125
assert db[2] == 3
126
db.commit()
127
db[5] = 18 # does not get committed
128
129
db = SQLiteKeyValueStore(file, not compress)
130
assert db.keys() == [2,10]
131
assert db[10] == {1:5, '17a':[2,5]}
132
assert db[2] == 3
133
134
assert db.has_key(2)
135
assert not db.has_key(3)
136
del db
137
import os; os.unlink(file)
138
139
finally:
140
if os.path.exists(file):
141
import os; os.unlink(file)
142
143
144
# A SQLite cached function decorator
145
146
class sqlite_cached_function:
147
"""
148
Use this like so::
149
150
@sqlite_cached_function('/tmp/foo.sqlite', compress=True)
151
def f(n,k=5):
152
return n+k
153
154
Then whenever you call f, the values are cached in the sqlite
155
database /tmp/foo.sqlite. This will persist across different
156
sessions, of course. Moreover, f.db is the underlying
157
SQLiteKeyValueStore and f.keys() is a list of all keys computed
158
so far (normalized by ArgumentFixer).
159
"""
160
def __init__(self, file, compress=False):
161
self.db = SQLiteKeyValueStore(file, compress=compress)
162
163
def __call__(self, f):
164
"""Return decorated version of f."""
165
from sage.misc.function_mangling import ArgumentFixer
166
A = ArgumentFixer(f)
167
def g(*args, **kwds):
168
k = A.fix_to_named(*args, **kwds)
169
try:
170
return self.db[k]
171
except KeyError: pass
172
x = self.db[k] = f(*args, **kwds)
173
self.db.commit()
174
return x
175
def keys():
176
return self.db.keys()
177
g.keys = keys
178
g.db = self.db
179
return g
180
181
def test_sqlite_cached_function_1():
182
try:
183
import tempfile
184
file = tempfile.mktemp()
185
@sqlite_cached_function(file)
186
def f(a, b=10):
187
return a + b
188
assert f(2) == 12
189
assert f(2,4) == 6
190
assert f(2) == 12
191
assert f(2,4) == 6
192
finally:
193
import os; os.unlink(file)
194
195
def test_sqlite_cached_function_2():
196
try:
197
from sage.all import sleep, walltime
198
import tempfile
199
file = tempfile.mktemp()
200
201
@sqlite_cached_function(file, compress=True)
202
def f(a, b=10):
203
sleep(1)
204
return a + b
205
f(2)
206
f(2,b=4)
207
208
t = walltime()
209
assert f(2) == 12
210
assert f(b=4,a=2) == 6
211
assert walltime() - t < 1, "should be fast!"
212
213
# Make new cached function, which will now use the disk cache first.
214
@sqlite_cached_function(file, compress=True)
215
def f(a, b=10):
216
sleep(1)
217
218
t = walltime()
219
assert f(2) == 12
220
assert f(b=4,a=2) == 6
221
assert walltime() - t < 1, "should be fast!"
222
223
finally:
224
import os; os.unlink(file)
225
226
def test_sqlite_cached_function_3():
227
import tempfile
228
file = tempfile.mktemp()
229
230
try:
231
from sage.all import parallel, sleep
232
233
# This "nasty" test causes 10 processes to be spawned all at once,
234
# and simultaneously try to initialize and write to the database,
235
# repeatedly. This tests that we're dealing with concurrency robustly.
236
237
@parallel(10)
238
def f(a, b=10):
239
@sqlite_cached_function(file)
240
def g(a, b):
241
sleep(.5)
242
return a + b
243
return g(a, b)
244
245
for X in f(range(1,30)):
246
assert X[1] == X[0][0][0] + 10
247
248
finally:
249
import os; os.unlink(file)
250
251
252