Skip to content

Instantly share code, notes, and snippets.

@bavardage
Created May 25, 2009 19:32
Show Gist options
  • Save bavardage/117680 to your computer and use it in GitHub Desktop.
Save bavardage/117680 to your computer and use it in GitHub Desktop.
import logging
import pickle
import os
import shutil
import sys
import time
PROTOCOL = 0 #keep as ascii for now for debug funsies
logger = logging
logger.basicConfig(level=logging.DEBUG)
class EventLoop(object):
'''
This is a 'global object' style thing.
This provides the main event loop.
If some module or other wants something run, then they should tell this event loop about it by adding it to the functions set
Functions should ofc be non-blocking and fairly fast to execute.
To stop call the stop method
'''
functions = set()
stop = False
resolution = 0.05
@classmethod
def loop(cls):
while not cls.stop:
for f in cls.functions:
f()
time.sleep(cls.resolution)
@classmethod
def require_gtk(cls):
'''
Utility method to require gtk
'''
logger.info("adding gtk")
def do_gtk():
while gtk.events_pending():
gtk.main_iteration()
try:
import gtk
cls.functions.add(do_gtk)
except:
logger.error("gtk is not installed")
@classmethod
def require_qt(cls):
logger.info("adding qt")
def do_qt():
QtGui.qApp.processEvents()
try:
from PyQt4 import QtGui, QtCore
cls.functions.add(do_gtk)
except:
logger.error("qt is not installed")
from Viewers import * #import here because otherwise it seems I get some funky cyclic dependency magic
class Library(object):
'''
This class represents a collection of books.
It is deserialised from disk on program open and serialised on program close.
'''
pickle_filename = 'library.index'
next_directory_id = 0
def __init__(self, datapath):
logger.info("Creating an empty library")
self.datapath = datapath
self.books = []
@classmethod
def library(cls, datapath):
filename = os.path.join(datapath, cls.pickle_filename)
logger.info("Attempting to load library from: %s" % filename)
try:
lib = cls.load_from_filename(filename)
except Exception as e:
logger.info("Error - %s" % e)
lib = cls(datapath)
lib.datapath = datapath #probably not needed, but just to make sure?
return lib
def add_book(self, book):
self.books.append(book)
@classmethod
def load_from_file(cls, f):
lib = pickle.load(f)
if not isinstance(lib, cls):
logger.error("the pickled object is not a library")
raise Exception, "the pickled object is not a library"
else:
return lib
@classmethod
def load_from_filename(cls, fn):
with open(fn) as f:
lib = cls.load_from_file(f)
return lib
def write_out(self):
destination = os.path.join(self.datapath, self.pickle_filename)
try:
result = pickle.dumps(self, PROTOCOL)
except Exception as e:
logger.error("Error pickling library" + str(e))
else:
with file(destination, 'w') as f:
f.write(result)
f.flush()
class Book(object):
'''
This class represents a book.
A book can have multiple representations.
Books are held in libraries.
'''
directory_format = "%a/%t/" #should end in a slash
def __init__(self, library, title, author):
self.library = library
self.title = title
self.author = author
self.tags = set()
self.sessions = []
self.representations = []
self.directory = self._get_directory()
self.library.add_book(self)
def delete_session(self, session):
if session in self.sessions:
self.sessions.remove(session)
else:
logger.warning("session doesn't exist so didn't remove")
def _get_directory(self, append=0):
library_path = self.library.datapath
directory = self.directory_format.replace(
'%t', self.title).replace(
'%a', self.author)
if append:
directory += "%s" % append
path = os.path.abspath(os.path.join(library_path, directory))
try:
os.makedirs(path)
return path
except:
return self._get_directory(append+1)
def __getstate__(self):
odict = self.__dict__.copy()
for rep in self.representations:
rep.write_out()
return odict
class Representation(object):
'''
This class is a representation of a book in some format.
This really should have a representation on disk of some sort.
To create a new representation from data rather than a file,
the representation should define a new method.
'''
name = ''
filename = None
filename_format = None
do_not_pickle = []
def __init__(self, book, filename=None):
self.book = book
self.filename = filename or self.get_filename_for_book(book)
book.representations.append(self)
def show_in_viewer(self, Viewer):
session = Session(self)
session.view_with(Viewer)
def write_out(self):
'''
Write this representation out to disk
'''
if not self.filename_valid():
new_filename = self.get_filename()
shutil.move(self.filename, new_filename)
self.filename = new_filename
@classmethod
def new(cls, *args):
'''
Create a new instance of this representation from arguments,
not from disk.
This method should write the appropriate data to disk, then
create the object
'''
pass
def get_filename(self):
return self.get_filename_for_book(self.book)
@classmethod
def get_filename_for_book(cls, book):
'''
Returns a valid filename for this representation to be written to.
This may not be the actual filename that it currently is at
'''
filename = cls.filename_format.replace(
'%t', book.title).replace(
'%a', book.author)
location = os.path.join(book.directory, filename)
logger.info("the path is %s" % location)
if os.path.exists(location):
logger.info("path %s already exists" % location)
extra = 0
location += '0'
while os.path.exists(location):
location = location[:-1] + str(extra)
logger.info("try path %s" % location)
extra += 1
return location
def filename_valid(self):
'''
Returns true if the current filename for this representation
is in a valid location - i.e. in the subdirectory for this book.
'''
our_directory = os.path.dirname(self.filename)
logger.debug("our directory is %s from filename %s" % (our_directory,
self.filename))
book_directory = self.book.directory
logger.debug("comparing %s and %s" % (our_directory, book_directory))
return os.path.samefile(our_directory, book_directory)
@classmethod
def import_from_filename(cls, book, fn):
location = cls.get_filename_for_book(book)
shutil.copy(fn, location)
return cls(book, location)
def __getstate__(self):
odict = self.__dict__.copy()
for k in self.do_not_pickle:
if k in odict:
del odict[k]
return odict
@classmethod
def get_position_class(cls):
'''
return a class used to track a position in this representation
This class will be called with a default constructor to get
the starting position
'''
return int
@classmethod
def position_to_string(cls, position):
'''
Convert a valid position for this representation into a string
'''
return "%s" % position
@classmethod
def import_from_file(cls, book, f):
'''
add a representation of the given book by importing from the file f
'''
raise NotImplementedError
class Session(object):
'''
This class holds information about a current reading of a book
'''
def __init__(self, rep, position=None):
self.representation = rep
PositionClass = rep.get_position_class()
if isinstance(position, PositionClass):
self.position = position
else:
self.position = PositionClass()
rep.book.sessions.append(self)
@property
def book(self):
return self.representation.book
def delete(self):
self.book.delete_session(self)
del self #hmm, maybe work out a better deletion?!
def position_to_string(self):
return self.representation.position_to_string(self.position)
def view_with(self, Viewer):
viewer = Viewer()
viewer.view(self)
'''
Now define some basic formats - since this is fairly modular, more can be added easily
'''
UNICODE_FORMAT = 'utf-8'
UNICODE_ERRORS = 'replace'
import codecs
class Text(Representation):
name = 'Text'
filename_format = '%t.txt'
do_not_pickle = ['_text']
_text = None
def __init__(self, book, filename=None, encoding='utf-8'):
'''
Encoding is the encoding of the file to read in.
Internally data will be represented in UNICODE_FORMAT
'''
self.encoding = encoding
Representation.__init__(self, book, filename)
@classmethod
def new(cls, book, text, encoding='utf-8'):
'''
text should be in unicode format
'''
filename = cls.get_filename_for_book(book)
try:
with codecs.open(filename, 'w', encoding, errors=UNICODE_ERRORS) as f:
f.write(text)
f.flush()
except:
logger.error("Couldn't write to file: %s" % filename)
return None
else:
return cls(book, filename, encoding)
@property
def unicode_text(self):
'''
Return the text of this book as a unicode object
'''
if self._text: #TODO: test for file update on disk
return self._text
else:
with codecs.open(self.filename,
'r',
self.encoding,
errors=UNICODE_ERRORS) as f:
logger.info("reading data with encoding %s" % self.encoding)
data = f.read()
if isinstance(data, unicode):
self._text = data
else:
self._text = unicode(data, self.encoding, UNICODE_ERRORS)
return self._text
@property
def text(self):
'''
Return the text of the book encoded as utf-8
'''
return self.unicode_text.encode(UNICODE_FORMAT, UNICODE_ERRORS)
class Markdown(Text):
name = 'Markdown'
filename_format = '%t.markdown.txt'
class HTML(Text):
name = 'HTML'
filename_format = '%t.html'
class SaneHTML(Text):
name = 'SaneHTML'
filename_format = '%t.sanehtml'
@classmethod
def new(cls, book, text, contents):
sane_html = super(SaneHTML, cls).new(book, text, 'utf-8') #we are always utf-8
sane_html.contents = contents
return sane_html
class PDF(Representation):
name = 'PDF'
filename_format = '%t.pdf'
@classmethod
def position_to_string(cls, position):
return "page %s" % position
class MultipleFileRepresentation(Representation):
filenames = None
def __init__(self, book, directory = None):
self.book = book
if directory:
self.filename = directory
else:
self.filename = self.get_filename_for_book(book)
self.filenames = os.listdir(directory)
book.representations.append(self)
@property
def directory(self):
return self.filename
@classmethod
def import_from_filename(cls, book, fn):
logger.warning("Importing from a 'filename' when really importing from a directory")
cls.import_from_directory(book, fn)
@classmethod
def import_from_directory(cls, book, directory):
location = cls.get_filename_for_book(book)
shutil.copytree(directory, location)
return cls(book, location)
@classmethod
def position_to_string(cls, position):
'''
Convert a valid position for this representation into a string
'''
return "%s %s" % (position.fileindex, position.position)
class AudioPosition: #must make this public for serialisation funsies
def __init__(self):
self.track = 0
self.position = 0
class AudioBook(MultipleFileRepresentation):
name = 'Audio Book'
filename_format = '%t.d'
def __init__(self, book, directory=None):
MultipleFileRepresentation.__init__(self, book, directory)
self.filenames.sort()
@classmethod
def get_position_class(cls):
return AudioPosition
@classmethod
def position_to_string(cls, position):
minutes = position.position / 60
seconds = position.position % 60
return "Track %s %d:%02d" % (position.track,
minutes,
seconds)
from Converters import *
#TODO: unicode
#http://boodebr.org/main/python/all-about-python-and-unicode
#QT webkit
#http://www.ics.com/learning/icsnetwork/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment