| 1 |
# sAsync: |
|---|
| 2 |
# An enhancement to the SQLAlchemy package that provides persistent |
|---|
| 3 |
# dictionaries, text indexing and searching, and an access broker for |
|---|
| 4 |
# conveniently managing database access, table setup, and |
|---|
| 5 |
# transactions. Everything can be run in an asynchronous fashion using the |
|---|
| 6 |
# Twisted framework and its deferred processing capabilities. |
|---|
| 7 |
# |
|---|
| 8 |
# Copyright (C) 2006-2007 by Edwin A. Suominen, http://www.eepatents.com |
|---|
| 9 |
# |
|---|
| 10 |
# This program is free software; you can redistribute it and/or modify it under |
|---|
| 11 |
# the terms of the GNU General Public License as published by the Free Software |
|---|
| 12 |
# Foundation; either version 2 of the License, or (at your option) any later |
|---|
| 13 |
# version. |
|---|
| 14 |
# |
|---|
| 15 |
# This program is distributed in the hope that it will be useful, but WITHOUT |
|---|
| 16 |
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|---|
| 17 |
# FOR A PARTICULAR PURPOSE. See the file COPYING for more details. |
|---|
| 18 |
# |
|---|
| 19 |
# You should have received a copy of the GNU General Public License along with |
|---|
| 20 |
# this program; if not, write to the Free Software Foundation, Inc., 51 |
|---|
| 21 |
# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
|---|
| 22 |
|
|---|
| 23 |
""" |
|---|
| 24 |
Dictionary-like objects with behind-the-scenes database persistence |
|---|
| 25 |
|
|---|
| 26 |
""" |
|---|
| 27 |
|
|---|
| 28 |
# Imports |
|---|
| 29 |
from UserDict import DictMixin |
|---|
| 30 |
from twisted.internet import defer |
|---|
| 31 |
from misc import DeferredTracker |
|---|
| 32 |
|
|---|
| 33 |
import items |
|---|
| 34 |
|
|---|
| 35 |
|
|---|
| 36 |
class AsyncError(Exception): |
|---|
| 37 |
""" |
|---|
| 38 |
The requested action is incompatible with asynchronous operations. |
|---|
| 39 |
""" |
|---|
| 40 |
|
|---|
| 41 |
|
|---|
| 42 |
class PersistentDictBase(DictMixin, object): |
|---|
| 43 |
""" |
|---|
| 44 |
I am a base class for a database-persistent dictionary-like object uniquely |
|---|
| 45 |
identified by the hashable constructor argument I{ID}. |
|---|
| 46 |
|
|---|
| 47 |
Before you use any instance of me, you must specify the parameters for |
|---|
| 48 |
creating an SQLAlchemy database engine. A single argument is used, which |
|---|
| 49 |
specifies a connection to a database via an RFC-1738 url. In addition, the |
|---|
| 50 |
following keyword options can be employed, which are listed in the API docs |
|---|
| 51 |
for L{sasync} and L{sasync.database.AccessBroker}. |
|---|
| 52 |
|
|---|
| 53 |
You can set an engine globally, for all instances of me, via the |
|---|
| 54 |
L{sasync.engine} package-level function, or via the L{AccessBroker.engine} |
|---|
| 55 |
class method. Alternatively, you can specify an engine for one particular |
|---|
| 56 |
instance by supplying the parameters to my constructor. |
|---|
| 57 |
|
|---|
| 58 |
In my default mode of operation, both read and write item accesses occur |
|---|
| 59 |
asynchronously and return deferreds. However, you can put me into B{load} |
|---|
| 60 |
mode by calling my L{preload} method. At that point, all my items will be |
|---|
| 61 |
accessed synchronously as with any other dictionary. No other deferreds |
|---|
| 62 |
will be returned from any item access. Lazy writing will still be done, but |
|---|
| 63 |
behind the scenes and with no API access to write completions. |
|---|
| 64 |
|
|---|
| 65 |
B{IMPORTANT}: As with all sasync data store objects, make sure you call my |
|---|
| 66 |
L{shutdown} method for an instance of me that you're done with before |
|---|
| 67 |
allowing that instance to be deleted. |
|---|
| 68 |
|
|---|
| 69 |
@ivar isPreloadMode: Boolean flag that indicates if I am operating in |
|---|
| 70 |
preload mode. |
|---|
| 71 |
|
|---|
| 72 |
""" |
|---|
| 73 |
def __init__(self, ID, *url, **kw): |
|---|
| 74 |
""" |
|---|
| 75 |
Instantiates me with an item store keyed to the supplied hashable |
|---|
| 76 |
I{ID}. Ensures that I have access to a class-wide instance of a |
|---|
| 77 |
L{Search} object so that I can update the database's full-text index |
|---|
| 78 |
when writing values containing text content. |
|---|
| 79 |
|
|---|
| 80 |
In addition to any engine-specifying keywords supplied, the following |
|---|
| 81 |
are particular to this constructor: |
|---|
| 82 |
|
|---|
| 83 |
@param ID: A hashable object that is used as my unique identifier. |
|---|
| 84 |
|
|---|
| 85 |
@keyword search: Set C{True} if text indexing is to be updated when items |
|---|
| 86 |
are added, updated, or deleted. |
|---|
| 87 |
|
|---|
| 88 |
""" |
|---|
| 89 |
try: |
|---|
| 90 |
self.ID = hash(ID) |
|---|
| 91 |
except: |
|---|
| 92 |
raise TypeError("Item IDs must be hashable") |
|---|
| 93 |
# In-memory Caches |
|---|
| 94 |
self.data, self.keyCache = {}, {} |
|---|
| 95 |
# For tracking lazy writes |
|---|
| 96 |
self.writeTracker = DeferredTracker() |
|---|
| 97 |
# My very own persistent items store |
|---|
| 98 |
if url: |
|---|
| 99 |
self.i = items.Items(self.ID, url[0], **kw) |
|---|
| 100 |
else: |
|---|
| 101 |
self.i = items.Items(self.ID) |
|---|
| 102 |
self.isPreloadMode = False |
|---|
| 103 |
|
|---|
| 104 |
def preload(self): |
|---|
| 105 |
""" |
|---|
| 106 |
This method preloads all my items from the database (which may take a |
|---|
| 107 |
while), returning a C{Deferred} that fires when everything's ready and |
|---|
| 108 |
I've completed the transition into B{preload} mode. |
|---|
| 109 |
""" |
|---|
| 110 |
d = self.loadAll() |
|---|
| 111 |
d.addCallback(lambda _: setattr(self, 'isPreloadMode', True)) |
|---|
| 112 |
return d |
|---|
| 113 |
|
|---|
| 114 |
def shutdown(self, *null): |
|---|
| 115 |
""" |
|---|
| 116 |
Shuts down my database L{Transactor} and its synchronous task queue. |
|---|
| 117 |
""" |
|---|
| 118 |
d = self.writeTracker.deferToAll() |
|---|
| 119 |
d.addCallback(self.i.shutdown) |
|---|
| 120 |
return d |
|---|
| 121 |
|
|---|
| 122 |
def loadAll(self, *null): |
|---|
| 123 |
""" |
|---|
| 124 |
Loads all items from the database, setting my in-memory dict and key |
|---|
| 125 |
cache accordingly. |
|---|
| 126 |
""" |
|---|
| 127 |
def loaded(items): |
|---|
| 128 |
self.data.clear() |
|---|
| 129 |
self.data.update(items) |
|---|
| 130 |
self.keyCache = dict.fromkeys(items.keys(), True) |
|---|
| 131 |
return self.data |
|---|
| 132 |
|
|---|
| 133 |
return self.i.loadAll().addCallback(loaded) |
|---|
| 134 |
|
|---|
| 135 |
def deferToWrites(self, lastOnly=False): |
|---|
| 136 |
""" |
|---|
| 137 |
@see: L{DeferredTracker.deferToAll} |
|---|
| 138 |
|
|---|
| 139 |
""" |
|---|
| 140 |
if lastOnly: |
|---|
| 141 |
d = self.writeTracker.deferToLast() |
|---|
| 142 |
else: |
|---|
| 143 |
d = self.writeTracker.deferToAll() |
|---|
| 144 |
return d |
|---|
| 145 |
|
|---|
| 146 |
|
|---|
| 147 |
class PersistentDict(PersistentDictBase): |
|---|
| 148 |
""" |
|---|
| 149 |
I am a database-persistent dictionary-like object with memory caching of |
|---|
| 150 |
items and lazy writing. |
|---|
| 151 |
|
|---|
| 152 |
Getting, setting, or deleting my items returns C{Deferred} objects of the |
|---|
| 153 |
Twisted asynchronous framework that fire when the underlying database |
|---|
| 154 |
accesses are completed. Returning a deferred value avoids forcing the |
|---|
| 155 |
client code to block while the real value is being read from the |
|---|
| 156 |
database. |
|---|
| 157 |
|
|---|
| 158 |
@ivar data: The in-memory dictionary that each instance of me uses to cache |
|---|
| 159 |
values for a given ID. |
|---|
| 160 |
|
|---|
| 161 |
""" |
|---|
| 162 |
|
|---|
| 163 |
#--- Core dict operations ------------------------------------------------- |
|---|
| 164 |
|
|---|
| 165 |
def __getitem__(self, name): |
|---|
| 166 |
""" |
|---|
| 167 |
Returns a C{Deferred} to the value of item I{name} or the value itself |
|---|
| 168 |
if in preload mode. |
|---|
| 169 |
|
|---|
| 170 |
The value is only loaded from the database if it isn't already in the |
|---|
| 171 |
in-memory dictionary. |
|---|
| 172 |
""" |
|---|
| 173 |
def valueLoaded(value): |
|---|
| 174 |
if isinstance(value, items.Missing): |
|---|
| 175 |
raise KeyError( |
|---|
| 176 |
"No item '%s' in the database" % name) |
|---|
| 177 |
self.data[name] = value |
|---|
| 178 |
self.keyCache.setdefault(name, False) |
|---|
| 179 |
return value |
|---|
| 180 |
|
|---|
| 181 |
if name in self.data: |
|---|
| 182 |
value = self.data[name] |
|---|
| 183 |
if self.isPreloadMode: |
|---|
| 184 |
return value |
|---|
| 185 |
else: |
|---|
| 186 |
return defer.succeed(value) |
|---|
| 187 |
elif self.isPreloadMode: |
|---|
| 188 |
raise KeyError( |
|---|
| 189 |
"No item '%s' in the database" % name) |
|---|
| 190 |
else: |
|---|
| 191 |
return self.i.load(name).addCallback(valueLoaded) |
|---|
| 192 |
|
|---|
| 193 |
def __setitem__(self, name, value): |
|---|
| 194 |
""" |
|---|
| 195 |
Sets item I{name} to I{value}, saving it to the database if there |
|---|
| 196 |
isn't already an in-memory dictionary item with that exact value. |
|---|
| 197 |
""" |
|---|
| 198 |
def valueLoaded(loadedValue): |
|---|
| 199 |
if isinstance(loadedValue, items.Missing): |
|---|
| 200 |
# Item isn't in the database, so insert it |
|---|
| 201 |
return self.i.insert(name, value) |
|---|
| 202 |
else: |
|---|
| 203 |
# Update current value of item in the database |
|---|
| 204 |
return self.i.update(name, value) |
|---|
| 205 |
|
|---|
| 206 |
oldValue = self.data.get(name, None) |
|---|
| 207 |
self.data[name] = value |
|---|
| 208 |
self.keyCache.setdefault(name, False) |
|---|
| 209 |
# Everything from here on is just lazy writing |
|---|
| 210 |
if oldValue is None: |
|---|
| 211 |
# We're writing an item that hasn't been loaded from the database |
|---|
| 212 |
# yet |
|---|
| 213 |
if self.isPreloadMode: |
|---|
| 214 |
# If it hasn't been loaded yet, in preload mode, it ain't there |
|---|
| 215 |
d = self.i.insert(name, value) |
|---|
| 216 |
else: |
|---|
| 217 |
# Not in preload mode, so it may be in the database but not yet |
|---|
| 218 |
# loaded |
|---|
| 219 |
d = self.i.load(name) |
|---|
| 220 |
d.addCallback(valueLoaded) |
|---|
| 221 |
else: |
|---|
| 222 |
# There's already a value in the in-memory dictionary, update it |
|---|
| 223 |
d = self.i.update(name, value) |
|---|
| 224 |
self.writeTracker.put(d) |
|---|
| 225 |
|
|---|
| 226 |
def __delitem__(self, name): |
|---|
| 227 |
""" |
|---|
| 228 |
Deletes item I{name}, removing its entry from both the in-memory |
|---|
| 229 |
dictionary and the database |
|---|
| 230 |
""" |
|---|
| 231 |
if name in self.data: |
|---|
| 232 |
del self.data[name] |
|---|
| 233 |
self.keyCache.pop(name, None) |
|---|
| 234 |
d = self.i.delete(name) |
|---|
| 235 |
self.writeTracker.put(d) |
|---|
| 236 |
else: |
|---|
| 237 |
raise KeyError(name) |
|---|
| 238 |
|
|---|
| 239 |
def __contains__(self, key): |
|---|
| 240 |
""" |
|---|
| 241 |
Indicates if I contain item I{key}. |
|---|
| 242 |
|
|---|
| 243 |
In I{preload} mode, returns C{True} if the item is present in my |
|---|
| 244 |
in-memory dictionary and C{False} if not. |
|---|
| 245 |
|
|---|
| 246 |
In normal mode, returns an immediate C{Deferred} firing with C{True} |
|---|
| 247 |
without a transaction if the item is already present in my in-memory |
|---|
| 248 |
dictionary. If it isn't, tries to load the item (it will probably be |
|---|
| 249 |
requested soon anyhow) and returns a C{Deferred} that will ultimately |
|---|
| 250 |
fire with C{True} unless the load resulted in a L{Missing} object. In |
|---|
| 251 |
that case, deletes the loaded C{Missing} object from my in-memory |
|---|
| 252 |
dictionary and fires the deferred with C{False}. |
|---|
| 253 |
|
|---|
| 254 |
Using the C{<key> in <dict>} Python construct doesn't seem to work in |
|---|
| 255 |
normal mode. Use L{has_key} instead. |
|---|
| 256 |
""" |
|---|
| 257 |
if self.isPreloadMode: |
|---|
| 258 |
return self.data.__contains__(key) |
|---|
| 259 |
elif key in self.data or key in self.keyCache: |
|---|
| 260 |
return defer.succeed(True) |
|---|
| 261 |
else: |
|---|
| 262 |
d = self.i.load(key) |
|---|
| 263 |
d.addCallback(lambda value: not isinstance(value, items.Missing)) |
|---|
| 264 |
return d |
|---|
| 265 |
|
|---|
| 266 |
def keys(self): |
|---|
| 267 |
""" |
|---|
| 268 |
Returns an immediate or deferred list of the names of all my items in |
|---|
| 269 |
the database. |
|---|
| 270 |
""" |
|---|
| 271 |
def gotKeyList(keyList): |
|---|
| 272 |
self.keyCache = dict.fromkeys(keyList, True) |
|---|
| 273 |
return keyList |
|---|
| 274 |
|
|---|
| 275 |
if self.isPreloadMode: |
|---|
| 276 |
return self.data.keys() |
|---|
| 277 |
if True in self.keyCache.values(): |
|---|
| 278 |
# The key cache is valid as long as it has entries (=True) that were |
|---|
| 279 |
# retrieved from preloading or a previous call of this method. The |
|---|
| 280 |
# __setitem__ method will add new keys to the cache, but that |
|---|
| 281 |
# doesn't initialize it. |
|---|
| 282 |
keys = self.keyCache.keys() |
|---|
| 283 |
return defer.succeed(keys) |
|---|
| 284 |
# Empty or invalid key cache, load and cache a list of keys |
|---|
| 285 |
return self.i.names().addCallback(gotKeyList) |
|---|
| 286 |
|
|---|
| 287 |
#--- Replacement dict methods as needed ----------------------------------- |
|---|
| 288 |
|
|---|
| 289 |
def has_key(self, key): |
|---|
| 290 |
""" |
|---|
| 291 |
Returns an immediate or deferred Boolean indicating whether the key is |
|---|
| 292 |
present. |
|---|
| 293 |
""" |
|---|
| 294 |
return self.__contains__(key) |
|---|
| 295 |
|
|---|
| 296 |
def clear(self): |
|---|
| 297 |
""" |
|---|
| 298 |
Clears the in-memory dictionary of all items and deletes all their |
|---|
| 299 |
database entries. |
|---|
| 300 |
""" |
|---|
| 301 |
self.keyCache.clear() |
|---|
| 302 |
self.data.clear() |
|---|
| 303 |
d = self.writeTracker.deferToAll() |
|---|
| 304 |
d.addCallback(lambda _: self.i.names()) |
|---|
| 305 |
d.addCallback(lambda names: self.i.delete(*names)) |
|---|
| 306 |
self.writeTracker.put(d) |
|---|
| 307 |
return d |
|---|
| 308 |
|
|---|
| 309 |
def iteritems(self): |
|---|
| 310 |
""" |
|---|
| 311 |
B{Only for preload mode}: Iterate over all my items. |
|---|
| 312 |
""" |
|---|
| 313 |
if self.isPreloadMode: |
|---|
| 314 |
for item in self.data.iteritems(): |
|---|
| 315 |
yield item |
|---|
| 316 |
else: |
|---|
| 317 |
raise AsyncError("Can't iterate asynchronously") |
|---|
| 318 |
|
|---|
| 319 |
def iterkeys(self): |
|---|
| 320 |
""" |
|---|
| 321 |
B{Only for preload mode}: Iterate over all my keys. |
|---|
| 322 |
""" |
|---|
| 323 |
if self.isPreloadMode: |
|---|
| 324 |
for key in self.data.iterkeys(): |
|---|
| 325 |
yield key |
|---|
| 326 |
else: |
|---|
| 327 |
raise AsyncError("Can't iterate asynchronously") |
|---|
| 328 |
|
|---|
| 329 |
def itervalues(self): |
|---|
| 330 |
""" |
|---|
| 331 |
B{Only for preload mode}: Iterate over all my values. |
|---|
| 332 |
""" |
|---|
| 333 |
if self.isPreloadMode: |
|---|
| 334 |
for value in self.data.itervalues(): |
|---|
| 335 |
yield values |
|---|
| 336 |
else: |
|---|
| 337 |
raise AsyncError("Can't iterate asynchronously") |
|---|
| 338 |
|
|---|
| 339 |
def items(self): |
|---|
| 340 |
""" |
|---|
| 341 |
Returns an immediate or deferred sequence of (name, value) tuples |
|---|
| 342 |
representing all my items. |
|---|
| 343 |
""" |
|---|
| 344 |
if self.isPreloadMode: |
|---|
| 345 |
return self.data.items() |
|---|
| 346 |
else: |
|---|
| 347 |
return self.loadAll().addCallback(lambda x: x.items()) |
|---|
| 348 |
|
|---|
| 349 |
def values(self): |
|---|
| 350 |
""" |
|---|
| 351 |
Returns an immediate or deferred sequence of all my values. |
|---|
| 352 |
""" |
|---|
| 353 |
if self.isPreloadMode: |
|---|
| 354 |
return self.data.values() |
|---|
| 355 |
else: |
|---|
| 356 |
return self.loadAll().addCallback(lambda x: x.values()) |
|---|
| 357 |
|
|---|
| 358 |
def get(self, *args): |
|---|
| 359 |
""" |
|---|
| 360 |
Returns an immediate or deferred value of the value for the key |
|---|
| 361 |
specified as the first argument, or a default value if specified as an |
|---|
| 362 |
optional second argument. If the item is not present and no default |
|---|
| 363 |
value is supplied, raises the appropriate exception. |
|---|
| 364 |
""" |
|---|
| 365 |
def gotItem(loadedValue, key, defaultValue): |
|---|
| 366 |
if isinstance(loadedValue, items.Missing): |
|---|
| 367 |
return defaultValue |
|---|
| 368 |
else: |
|---|
| 369 |
self.data[key] = loadedValue |
|---|
| 370 |
return loadedValue |
|---|
| 371 |
|
|---|
| 372 |
key = args[0] |
|---|
| 373 |
if len(args) == 1: |
|---|
| 374 |
return self[key] |
|---|
| 375 |
defaultValue = args[1] |
|---|
| 376 |
if self.isPreloadMode: |
|---|
| 377 |
if self.has_key(key): |
|---|
| 378 |
return self[key] |
|---|
| 379 |
return defaultValue |
|---|
| 380 |
d = self.i.load(key) |
|---|
| 381 |
d.addCallback(gotItem, key, defaultValue) |
|---|
| 382 |
return d |
|---|
| 383 |
|
|---|
| 384 |
def setdefault(self, key, value): |
|---|
| 385 |
""" |
|---|
| 386 |
Sets my item specified by I{key} to I{value} if it doesn't exist |
|---|
| 387 |
already. Returns an immediate or deferred reference to the item's |
|---|
| 388 |
value after its new value (if any) is set. |
|---|
| 389 |
""" |
|---|
| 390 |
def gotItem(loadedValue): |
|---|
| 391 |
if isinstance(loadedValue, items.Missing): |
|---|
| 392 |
self.__setitem__(key, value) |
|---|
| 393 |
d = self.writeTracker.deferToLast() |
|---|
| 394 |
d.addCallback(lambda _: value) |
|---|
| 395 |
return d |
|---|
| 396 |
else: |
|---|
| 397 |
self.data[key] = loadedValue |
|---|
| 398 |
return loadedValue |
|---|
| 399 |
|
|---|
| 400 |
if self.isPreloadMode: |
|---|
| 401 |
if key in self.data: |
|---|
| 402 |
return self.data[key] |
|---|
| 403 |
else: |
|---|
| 404 |
self.__setitem__(key, value) |
|---|
| 405 |
return value |
|---|
| 406 |
elif key in self.data: |
|---|
| 407 |
return defer.succeed(self.data[key]) |
|---|
| 408 |
else: |
|---|
| 409 |
d = self.i.load(key) |
|---|
| 410 |
d.addCallback(gotItem) |
|---|
| 411 |
return d |
|---|
| 412 |
|
|---|
| 413 |
def copy(self): |
|---|
| 414 |
""" |
|---|
| 415 |
Returns an immediate or deferred copy of me that is a conventional |
|---|
| 416 |
(non-persisted) dictionary. |
|---|
| 417 |
""" |
|---|
| 418 |
if self.isPreloadMode: |
|---|
| 419 |
return self.data.copy() |
|---|
| 420 |
else: |
|---|
| 421 |
return self.loadAll() |
|---|
| 422 |
|
|---|
| 423 |
|
|---|
| 424 |
|
|---|
| 425 |
|
|---|
| 426 |
|
|---|