2d55455120
to unlink it, the link info box popped up. Actually checking in cache changes that were supposed to be in previous release.
346 lines
9.4 KiB
Python
346 lines
9.4 KiB
Python
import re
|
|
import os
|
|
import sha
|
|
import psycopg2 as psycopg
|
|
from psycopg2.pool import PersistentConnectionPool
|
|
import random
|
|
from model.Persistent import Persistent
|
|
|
|
|
|
class Connection_wrapper( object ):
|
|
def __init__( self, connection ):
|
|
self.connection = connection
|
|
self.pending_saves = []
|
|
|
|
def __getattr__( self, name ):
|
|
return getattr( self.connection, name )
|
|
|
|
|
|
class Database( object ):
|
|
ID_BITS = 128 # number of bits within an id
|
|
ID_DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
|
|
def __init__( self, connection = None, cache = None ):
|
|
"""
|
|
Create a new database and return it.
|
|
|
|
@type connection: existing connection object with cursor()/close()/commit() methods, or NoneType
|
|
@param connection: database connection to use (optional, defaults to making a connection pool)
|
|
@type cache: cmemcache.Client or something with a similar API, or NoneType
|
|
@param cache: existing memory cache to use (optional, defaults to making a cache)
|
|
@rtype: Database
|
|
@return: newly constructed Database
|
|
"""
|
|
# This tells PostgreSQL to give us timestamps in UTC. I'd use "set timezone" instead, but that
|
|
# makes SQLite angry.
|
|
os.putenv( "PGTZ", "UTC" )
|
|
|
|
# forcibly replace psycopg's connect() function with another function that returns the psycopg
|
|
# connection wrapped in a class with a pending_saves member, used in save() and commit() below
|
|
original_connect = psycopg.connect
|
|
|
|
def connect( *args, **kwargs ):
|
|
return Connection_wrapper( original_connect( *args, **kwargs ) )
|
|
|
|
psycopg.connect = connect
|
|
|
|
if connection:
|
|
self.__connection = connection
|
|
self.__pool = None
|
|
else:
|
|
self.__connection = None
|
|
self.__pool = PersistentConnectionPool(
|
|
1, # minimum connections
|
|
50, # maximum connections
|
|
"dbname=luminotes user=luminotes password=%s" % os.getenv( "PGPASSWORD", "dev" ),
|
|
)
|
|
|
|
self.__cache = cache
|
|
|
|
try:
|
|
import cmemcache
|
|
print "using memcached"
|
|
except ImportError:
|
|
return None
|
|
|
|
def __get_connection( self ):
|
|
if self.__connection:
|
|
return self.__connection
|
|
else:
|
|
return self.__pool.getconn()
|
|
|
|
def __get_cache_connection( self ):
|
|
if self.__cache is not None:
|
|
return self.__cache
|
|
|
|
try:
|
|
import cmemcache
|
|
return cmemcache.Client( [ "127.0.0.1:11211" ], debug = 0 )
|
|
except ImportError:
|
|
return None
|
|
|
|
def save( self, obj, commit = True ):
|
|
"""
|
|
Save the given object to the database.
|
|
|
|
@type obj: Persistent
|
|
@param obj: object to save
|
|
@type commit: bool
|
|
@param commit: True to automatically commit after the save
|
|
"""
|
|
connection = self.__get_connection()
|
|
cursor = connection.cursor()
|
|
|
|
cursor.execute( obj.sql_exists() )
|
|
if cursor.fetchone():
|
|
cursor.execute( obj.sql_update() )
|
|
else:
|
|
cursor.execute( obj.sql_create() )
|
|
|
|
cache = self.__get_cache_connection()
|
|
|
|
if commit:
|
|
connection.commit()
|
|
if cache:
|
|
cache.set( obj.cache_key, obj )
|
|
elif cache:
|
|
# no commit yet, so don't touch the cache
|
|
connection.pending_saves.append( obj )
|
|
|
|
def commit( self ):
|
|
connection = self.__get_connection()
|
|
connection.commit()
|
|
|
|
# save any pending saves to the cache
|
|
cache = self.__get_cache_connection()
|
|
|
|
if cache:
|
|
for obj in connection.pending_saves:
|
|
cache.set( obj.cache_key, obj )
|
|
|
|
connection.pending_saves = []
|
|
|
|
def load( self, Object_type, object_id, revision = None ):
|
|
"""
|
|
Load the object corresponding to the given object id from the database and return it, or None if
|
|
the object_id is unknown. If a revision is provided, a specific revision of the object will be
|
|
loaded.
|
|
|
|
@type Object_type: type
|
|
@param Object_type: class of the object to load
|
|
@type object_id: unicode
|
|
@param object_id: id of the object to load
|
|
@type revision: int or NoneType
|
|
@param revision: revision of the object to load (optional)
|
|
@rtype: Object_type or NoneType
|
|
@return: loaded object, or None if no match
|
|
"""
|
|
if revision is None:
|
|
cache = self.__get_cache_connection()
|
|
else:
|
|
cache = None
|
|
|
|
if cache: # don't bother caching old revisions
|
|
obj = cache.get( Persistent.make_cache_key( Object_type, object_id ) )
|
|
if obj:
|
|
return obj
|
|
|
|
obj = self.select_one( Object_type, Object_type.sql_load( object_id, revision ) )
|
|
if obj and cache:
|
|
cache.set( obj.cache_key, obj )
|
|
|
|
return obj
|
|
|
|
def select_one( self, Object_type, sql_command, use_cache = False ):
|
|
"""
|
|
Execute the given sql_command and return its results in the form of an object of Object_type,
|
|
or None if there was no match.
|
|
|
|
@type Object_type: type
|
|
@param Object_type: class of the object to load
|
|
@type sql_command: unicode
|
|
@param sql_command: SQL command to execute
|
|
@type use_cache: bool
|
|
@param use_cache: whether to look for and store objects in the cache
|
|
@rtype: Object_type or NoneType
|
|
@return: loaded object, or None if no match
|
|
"""
|
|
if use_cache:
|
|
cache = self.__get_cache_connection()
|
|
else:
|
|
cache = None
|
|
|
|
if cache:
|
|
cache_key = sha.new( sql_command ).hexdigest()
|
|
obj = cache.get( cache_key )
|
|
if obj:
|
|
return obj
|
|
|
|
connection = self.__get_connection()
|
|
cursor = connection.cursor()
|
|
|
|
cursor.execute( sql_command )
|
|
|
|
row = self.__row_to_unicode( cursor.fetchone() )
|
|
if not row:
|
|
return None
|
|
|
|
if Object_type in ( tuple, list ):
|
|
obj = Object_type( row )
|
|
else:
|
|
obj = Object_type( *row )
|
|
|
|
if obj and cache:
|
|
cache.set( cache_key, obj )
|
|
|
|
return obj
|
|
|
|
def select_many( self, Object_type, sql_command ):
|
|
"""
|
|
Execute the given sql_command and return its results in the form of a list of objects of
|
|
Object_type.
|
|
|
|
@type Object_type: type
|
|
@param Object_type: class of the object to load
|
|
@type sql_command: unicode
|
|
@param sql_command: SQL command to execute
|
|
@rtype: list of Object_type
|
|
@return: loaded objects
|
|
"""
|
|
connection = self.__get_connection()
|
|
cursor = connection.cursor()
|
|
|
|
cursor.execute( sql_command )
|
|
|
|
objects = []
|
|
row = self.__row_to_unicode( cursor.fetchone() )
|
|
|
|
while row:
|
|
if Object_type in ( tuple, list ):
|
|
obj = Object_type( row )
|
|
else:
|
|
obj = Object_type( *row )
|
|
|
|
objects.append( obj )
|
|
row = self.__row_to_unicode( cursor.fetchone() )
|
|
|
|
return objects
|
|
|
|
def __row_to_unicode( self, row ):
|
|
if row is None:
|
|
return None
|
|
|
|
return [ isinstance( item, str ) and unicode( item, encoding = "utf8" ) or item for item in row ]
|
|
|
|
def execute( self, sql_command, commit = True ):
|
|
"""
|
|
Execute the given sql_command.
|
|
|
|
@type sql_command: unicode
|
|
@param sql_command: SQL command to execute
|
|
@type commit: bool
|
|
@param commit: True to automatically commit after the command
|
|
"""
|
|
connection = self.__get_connection()
|
|
cursor = connection.cursor()
|
|
|
|
cursor.execute( sql_command )
|
|
|
|
if commit:
|
|
connection.commit()
|
|
|
|
def uncache_command( self, sql_command ):
|
|
cache = self.__get_cache_connection()
|
|
if not cache: return
|
|
|
|
cache_key = sha.new( sql_command ).hexdigest()
|
|
cache.delete( cache_key )
|
|
|
|
@staticmethod
|
|
def generate_id():
|
|
int_id = random.getrandbits( Database.ID_BITS )
|
|
|
|
base = len( Database.ID_DIGITS )
|
|
digits = []
|
|
|
|
while True:
|
|
index = int_id % base
|
|
digits.insert( 0, Database.ID_DIGITS[ index ] )
|
|
int_id = int_id / base
|
|
if int_id == 0:
|
|
break
|
|
|
|
return "".join( digits )
|
|
|
|
def next_id( self, Object_type, commit = True ):
|
|
"""
|
|
Generate the next available object id and return it.
|
|
|
|
@type Object_type: type
|
|
@param Object_type: class of the object that the id is for
|
|
@type commit: bool
|
|
@param commit: True to automatically commit after storing the next id
|
|
"""
|
|
connection = self.__get_connection()
|
|
cursor = connection.cursor()
|
|
|
|
# generate a random id, but on the off-chance that it collides with something else already in
|
|
# the database, try again
|
|
next_id = Database.generate_id()
|
|
cursor.execute( Object_type.sql_id_exists( next_id ) )
|
|
|
|
while cursor.fetchone() is not None:
|
|
next_id = Database.generate_id()
|
|
cursor.execute( Object_type.sql_id_exists( next_id ) )
|
|
|
|
# save a new object with the next_id to the database
|
|
obj = Object_type( next_id )
|
|
cursor.execute( obj.sql_create() )
|
|
|
|
if commit:
|
|
connection.commit()
|
|
|
|
return next_id
|
|
|
|
def close( self ):
|
|
"""
|
|
Shutdown the database.
|
|
"""
|
|
if self.__connection:
|
|
self.__connection.close()
|
|
|
|
if self.__pool:
|
|
self.__pool.closeall()
|
|
|
|
|
|
class Valid_id( object ):
|
|
"""
|
|
Validator for an object id.
|
|
"""
|
|
ID_PATTERN = re.compile( "^[%s]+$" % Database.ID_DIGITS )
|
|
|
|
def __init__( self, none_okay = False ):
|
|
self.__none_okay = none_okay
|
|
|
|
def __call__( self, value ):
|
|
if self.__none_okay and value in ( None, "None", "" ): return None
|
|
if self.ID_PATTERN.search( value ): return str( value )
|
|
|
|
raise ValueError()
|
|
|
|
|
|
class Valid_revision( object ):
|
|
"""
|
|
Validator for an object revision timestamp.
|
|
"""
|
|
REVISION_PATTERN = re.compile( "^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d+[+-]\d\d(:)?\d\d$" )
|
|
|
|
def __init__( self, none_okay = False ):
|
|
self.__none_okay = none_okay
|
|
|
|
def __call__( self, value ):
|
|
if self.__none_okay and value in ( None, "None", "" ): return None
|
|
if self.REVISION_PATTERN.search( value ): return str( value )
|
|
|
|
raise ValueError()
|