Created
May 25, 2009 19:32
-
-
Save bavardage/117680 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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