#!/usr/bin/python
# 
# Copyright (C) 2000 Charles Cazabon <software@discworld.dyndns.org>
# Licensed under the GNU General Public License version 2.
# See the file COPYING for license details.
#
# This code currently uses my debugutil module, available from
# http://www.qcc.sk.ca/~charlesc/software/
#
'''
Simple persistent-store dictionary storage, with lockless, portable, and safe
pseudo-concurrent read/write access.
'''

import string
import cPickle
import time
from debugutil import *		# Get rid of all DEBUG() calls to eliminate this.


##############################
# Exceptions
SearchTextError = 'SearchTextError'
KeyAddError = 'KeyAddError'
InitError = 'InitError'


##############################
class Simpledb:
	'''Classful simple persistent database/data store with lockless (portable)
	and safe concurrency.
	'''
	#######################################
	def __init__ (self, filename, sleep = 1):
		'''Create an __instance of the class, based on filename supplied.
		'''
		DEBUG ()
		if sleep < 1:
			raise InitError, 'error:  sleep value %s < 1' % sleep
			
		self.__dict = None
		self.__changed = 0
		self.__isopen = 0
		self.sleeptime = sleep		# How long to sleep when file unavailable.
		self.__instance = '%s.%s' % (os.getpid (), time.time ())
		self.__fname = filename
		self.__tname = '%s.inuse.%s' % (self.__fname, self.__instance)
		self.bakext = 'old'			# Extension to place on backup files.
		return


	#######################################
	def dbopen (self):
		'''Open the database file in a lockless but concurrently-safe way.
		'''
		DEBUG ()
		
		while not self.__isopen:
			try:
				os.rename (self.__fname, self.__tname)
				self.__isopen = 1
			except os.error:
				# Another __instance has the file open
				DEBUG (TRACE, 'deferring database open')
				time.sleep (self.sleeptime)
				
		try:
			f = open (self.__tname, 'r')
			self.__dict = cPickle.load (f)
			f.close ()
			
		except EOFError:
			# Empty file, shouldn't be problem
			DEBUG (INFO, 'database file was empty')
			self.__dict = {}

		except IOError:
			# Bad
			try:
				os.rename (self.__tname, self.__fname)
			except os.error:
				pass

			DEBUG (ERROR, 'error opening database file')
			raise
		
		return
	# end dbopen
	
	
	#######################################
	def dbclose (self):
		'''Close the database file, writing out changes if necessary.
		'''
		DEBUG ()
	
		if self.__changed:
			# Write out the new file
			DEBUG (INFO, 'writing changed contents to file')
		
			# Backup the database before going further
			bakfilename = '%s.%s' % (self.__fname, self.bakext)
			try:
				bakfile = open (bakfilename, 'w')
				bakfile.truncate (0)
				bakfile.seek (0)
				f = open (self.__tname, 'r')
				bakfile.write (f.read ())
				bakfile.close ()
				f.close ()
				DEBUG (INFO, 'made backup of data file')

			except IOError:
				DEBUG (ERROR, 'failure creating backup datafile')
				raise
					
			try:
				DEBUG (INFO, 'opening datafile for write')
				f = open (self.__tname, 'w')
				DEBUG (INFO, 'truncating file')
				f.truncate (0)
				DEBUG (INFO, 'seeking file')
				f.seek (0)
				DEBUG (INFO, 'pickling dictionary')
				cPickle.dump (self.__dict, f)
				DEBUG (INFO, 'completed pickle dump')
				f.close ()
				DEBUG (INFO, 'closed datafile')
			
			except cPickle.PicklingError:
				# Bad
				DEBUG (ERROR, 'failure storing data')
				raise
				
			except IOError:
				# Barfed.  Bad file?  File is toast now anyways.
				DEBUG (ERROR, 'IOError - restoring data')
				# Restore from backup
				bakfile = open (bakfilename, 'r')
				bakfile.seek (0)
				f.truncate (0)
				f.seek (0)
				f.write (bakfile.read ())
				bakfile.close ()
				f.close ()
				DEBUG (TRACE, 'restored data')

		self.__changed = 0
		self.__dict = None
		
		# Then move the file back.
		try:
			os.rename (self.__tname, self.__fname)
			
		except os.error:
			# Hmmm, barfed.  Shouldn't happen.
			DEBUG (ERROR, 'failed renaming inuse file to standard file')
			raise
		
		self.__isopen = 0
		return
	# end dbclose
	
	
	#######################################
	def search (self, key):
		'''Search for an entry with a given key.
		'''
		DEBUG ()
	
		if not self.__dict.has_key (key):
			return None
	
		return self.__dict[key]
	# end search
	
	
	#######################################
	def delete (self, key):
		'''Delete a given key.
		'''
		DEBUG ()
	
		if self.__dict.has_key (key):
			del self.__dict[key]
			self.__changed = 1
				
		return
	# end close
	
	
	#######################################
	def add (self, key, value):
		'''Add a new key to the dictionary.  Fails if key already exists.
		'''
		DEBUG ()
	
		if self.__dict.has_key (key):
			raise KeyAddError, 'error:  key "%s" already exists' % key
	
		self.__dict[key] = value
		self.__changed = 1
		return
	# end add
	
	
	#######################################
	def update (self, key, value):
		'''Updates an existing key, or adds a new one if not found.
		'''
		DEBUG ()
	
		self.__dict[key] = value
		self.__changed = 1
	
		return
	# end add
	
	
	#######################################
	def sorted_list (self):
		'''Return a sorted list of the dictionary's contents.
		'''
		DEBUG ()
	
		list = []
		list = self.__dict.items ()
		list.sort ()
		
		return list
	# end sorted_list
	
	
	#######################################
	def find_text (self, text):
		'''Return all entries which contain a given text string.
		'''
		DEBUG ()
	
		text = string.lower (string.strip (text))
	
		if not text:
			raise SearchTextError, 'error:  no search text supplied'
	
		results = []
	
		for item in self.__dict.keys ():
			# Check each item in database
			record = self.__dict[item]
			for field in record:
				try:
					if (string.find (string.lower (field), text) != -1):
						# Record contains a match for this text
						results.append (self.__dict[item])
						break	# Skip immediately to next record
	
				except:
					# Field isn't a text field, just skip
					pass
	
		# Sort the results
		results.sort ()
	
		return results
	# end find_text

