changeset 119:04a730f9d07d backend

move all sqlalchemy related classes to the respective sub-package. use a backend to abstract from access to the data
author Dirk Olmes <dirk@xanthippe.ping.de>
date Sun, 21 Aug 2011 03:55:16 +0200
parents 0e73adb2dec4
children e830fa1cc7a2
files Database.py DisplayModel.py Feed.py FeedEntry.py FeedList.py MainWindow.py Mapping.py Preference.py Preferences.py backend/__init__.py backend/sqlalchemy/Database.py backend/sqlalchemy/Feed.py backend/sqlalchemy/FeedEntry.py backend/sqlalchemy/FeedList.py backend/sqlalchemy/Mapping.py backend/sqlalchemy/Preference.py backend/sqlalchemy/Preferences.py backend/sqlalchemy/SqlAlchemyBackend.py backend/sqlalchemy/__init__.py backend/sqlalchemy/util.py feedworm-gui.py util.py
diffstat 20 files changed, 438 insertions(+), 392 deletions(-) [+]
line wrap: on
line diff
--- a/Database.py	Sun Aug 21 02:47:25 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-
-import Mapping
-import sqlalchemy
-import sqlalchemy.orm
-import sys
-import util
-
-# Keep the connection to the database only once. The feed updater and the GUI app will
-# operate on a single engine/session but this comes in handy for interactive use
-engine = None
-SessionMaker = None
-
-def createSession(databaseUrl=None):
-    if databaseUrl is None:
-        databaseUrl = _getDatabaseUrl()
-    initEngine(databaseUrl)
-    Mapping.createMapping(engine)
-    initSessionMaker()
-    return SessionMaker()
-
-def _getDatabaseUrl():
-    if len(sys.argv) < 2:
-        print("Usage: %s <database url>" % (sys.argv[0]))
-        sys.exit(1)
-    return sys.argv[1]
-
-def initEngine(databaseUrl):
-    global engine
-    if engine is None:
-        verbose = util.databaseLoggingEnabled()
-        engine = sqlalchemy.create_engine(databaseUrl, echo=verbose)
-
-def initSessionMaker():
-    global SessionMaker
-    if SessionMaker is None:
-        SessionMaker = sqlalchemy.orm.sessionmaker(bind=engine)
-    
\ No newline at end of file
--- a/DisplayModel.py	Sun Aug 21 02:47:25 2011 +0200
+++ b/DisplayModel.py	Sun Aug 21 03:55:16 2011 +0200
@@ -2,19 +2,25 @@
 from PyQt4.QtCore import QAbstractListModel, QModelIndex, QVariant, Qt
 
 class DisplayModel(QAbstractListModel):
-    def __init__(self, parent=None, list=None, displayFunction=None, **args):
+    def __init__(self, parent=None, list=None, displayAttribute=None, **args):
         QAbstractListModel.__init__(self, parent, *args)
         self.list = list
-        self.displayFunction = displayFunction
-                
+        self.displayAttribute = displayAttribute
+
     def rowCount(self, parent=QModelIndex()):
         return len(self.list)
-    
-    def data(self, index, role): 
+
+    def data(self, index, role):
         if index.isValid() and role == Qt.DisplayRole:
             row = index.row()
             object = self.list[row]
-            displayString = self.displayFunction(object)
+            displayString = self._stringToDisplay(object)
             return QVariant(displayString)
-        else: 
+        else:
             return QVariant()
+
+    def _stringToDisplay(self, object):
+        if hasattr(object, self.displayAttribute):
+            return getattr(object, self.displayAttribute)
+        else:
+            return "invalid display attribute: " + self.displayAttribute
--- a/Feed.py	Sun Aug 21 02:47:25 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,46 +0,0 @@
-
-from datetime import datetime, timedelta
-import FeedEntry
-
-class Feed(object):
-    @staticmethod
-    def all(session):
-        return session.query(Feed).order_by(Feed.title).all()
-
-    def __init__(self, title, rss_url):
-        self.title = title
-        self.rss_url = rss_url
-        # default: update every 60 minutes
-        self.update_interval = 60
-        self.incrementNextUpdateDate()
-        self.auto_load_entry_link = False
-        self.always_open_in_browser = False
-
-    def __repr__(self):
-        return "<Feed (%d) %s>" % (self.pk, self.title)
-
-    def userPresentableString(self):
-        return self.title
-
-    def entriesSortedByUpdateDate(self, hideReadEntries=False):
-        if hideReadEntries:
-            sortedEntries = self._unreadEntries()
-        else:
-            sortedEntries = list(self.entries)
-        sortedEntries.sort(FeedEntry.compareByUpdateDate)
-        return sortedEntries
-
-    def _unreadEntries(self):
-        retValue = []
-        for entry in self.entries:
-            if not entry.read:
-                retValue.append(entry)
-        return retValue
-
-    def incrementNextUpdateDate(self):
-        delta = timedelta(minutes=self.update_interval)
-        self.next_update = datetime.now() + delta
-
-    def markAllEntriesRead(self):
-        for entry in self.entries:
-            entry.markRead()
\ No newline at end of file
--- a/FeedEntry.py	Sun Aug 21 02:47:25 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-
-from datetime import datetime
-
-def compareByUpdateDate(first, second):
-    return cmp(first.updated, second.updated)
-
-class FeedEntry(object):
-    @staticmethod
-    def findById(id, session):
-        result = session.query(FeedEntry).filter(FeedEntry.id == id)
-        return result.first()
-
-    @staticmethod
-    def create(entry):
-        new = FeedEntry()
-        new.id = entry.id
-        new.link = entry.link
-        new.title = entry.title
-        new.updated = entry.updated_parsed
-        new.summary = entry.summary
-        return new
-
-    def __init__(self):
-        self.create_timestamp = datetime.now()
-        self.read = 0
-
-    def __repr__(self):
-        return "<FeedEntry (%d) %s>" % (self.pk, self.title)
-
-    def userPresentableString(self):
-        return self.title
-    
-    def toggleRead(self):
-        if self.read:
-            self.markUnread()
-        else:
-            self.markRead()
-            
-    def markRead(self):
-        self.read = 1
-
-    def markUnread(self):
-        self.read = 0
--- a/FeedList.py	Sun Aug 21 02:47:25 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-
-from Feed import Feed
-from FeedEntry import FeedEntry
-from Preferences import Preferences
-from sqlalchemy.orm import joinedload
-
-def getFeeds(session):
-    preferences = Preferences(session)
-    if preferences.showOnlyUnreadFeeds():
-        return _getUnreadFeeds(session)
-    else:
-        return Feed.all(session)
-
-def _getUnreadFeeds(session):
-    query = session.query(FeedEntry).filter(FeedEntry.read == 0)
-    queryWithOptions = query.options(joinedload("feed"))
-    result = queryWithOptions.all()
-    return _collectFeeds(result)
-
-def _collectFeeds(feedEntries):
-    feeds = [entry.feed for entry in feedEntries]
-    uniqueFeeds = set(feeds)
-    return list(uniqueFeeds)
--- a/MainWindow.py	Sun Aug 21 02:47:25 2011 +0200
+++ b/MainWindow.py	Sun Aug 21 03:55:16 2011 +0200
@@ -1,35 +1,32 @@
 
-from AddFeed import AddFeed
+#from AddFeed import AddFeed
 from DisplayModel import DisplayModel
-from Feed import Feed
 from FeedEntryItemDelegate import FeedEntryItemDelegate
 from FeedEntryTableModel import FeedEntryTableModel
 from FeedSettings import FeedSettings
-from Preferences import Preferences
-from PreferencesDialog import PreferencesDialog
+#from PreferencesDialog import PreferencesDialog
 from PyQt4.QtCore import QUrl
 from PyQt4.QtGui import QApplication
 from PyQt4.QtGui import QMainWindow
 from PyQt4.QtGui import QWidget
 from Ui_MainWindow import Ui_MainWindow
-import FeedList
 import subprocess
 
 STATUS_MESSAGE_DISPLAY_MILLIS = 20000
 
 class MainWindow(QMainWindow):
-    def __init__(self, session=None):
+    def __init__(self, backend=None):
         QWidget.__init__(self, None)
-        self.session = session
-        self.preferences = Preferences(session)
+        self.backend = backend
+        self.preferences = backend.preferences()
         self.ui = Ui_MainWindow()
         self.ui.setupUi(self)
         self.updateFeedList()
         self.initFeedEntryList()
 
     def updateFeedList(self):
-        self.allFeeds = FeedList.getFeeds(self.session)
-        feedModel = DisplayModel(self, self.allFeeds, Feed.userPresentableString)
+        self.allFeeds = self.backend.getFeeds()
+        feedModel = DisplayModel(self, self.allFeeds, "title")
         self.ui.feedList.setModel(feedModel)
         self.ui.feedList.update()
 
@@ -145,17 +142,18 @@
         self.ui.feedEntryList.doItemsLayout()
 
     def addFeed(self):
-        addFeed = AddFeed(self.session)
-        success = addFeed.exec_()
-        if not success:
-            return
-
-        if addFeed.exception is not None:
-            message = "Error while adding feed: " + str(addFeed.exception)
-            self._updateStatusBar(message)
-        else:
-            self.session.commit()
-            self.updateFeedList()
+        pass
+#        addFeed = AddFeed(self.session)
+#        success = addFeed.exec_()
+#        if not success:
+#            return
+#
+#        if addFeed.exception is not None:
+#            message = "Error while adding feed: " + str(addFeed.exception)
+#            self._updateStatusBar(message)
+#        else:
+#            self.session.commit()
+#            self.updateFeedList()
 
     def deleteFeed(self):
         try:
@@ -167,8 +165,9 @@
             self._updateStatusBar(message)
 
     def showPreferences(self):
-        preferences = PreferencesDialog(self.session)
-        preferences.exec_()
+        pass
+#        preferences = PreferencesDialog(self.session)
+#        preferences.exec_()
 
     def showFeedSettings(self):
         feedSettings = FeedSettings(self.session, self.selectedFeed)
@@ -178,8 +177,7 @@
         self.ui.statusbar.showMessage(message, STATUS_MESSAGE_DISPLAY_MILLIS)
 
     def close(self):
-        # save all uncommitted state, just in case
-        self.session.commit()
+        self.backend.dispose()
         QMainWindow.close(self)
 
     def copyArticleURLToClipboard(self):
--- a/Mapping.py	Sun Aug 21 02:47:25 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-
-from Feed import Feed
-from FeedEntry import FeedEntry
-from Preference import Preference
-from sqlalchemy import Boolean
-from sqlalchemy import Column
-from sqlalchemy import DateTime
-from sqlalchemy import ForeignKey
-from sqlalchemy import Integer
-from sqlalchemy import MetaData
-from sqlalchemy import String
-from sqlalchemy import Table
-from sqlalchemy import Text
-from sqlalchemy.orm import mapper
-from sqlalchemy.orm import relation
-
-mappingDefined = False
-feedEntryTable = None
-
-def createMapping(engine):
-    """ Make sure the mapping is defined only once. This is not really needed for the feed updater
-        or the GUI app but comes in handy when working interactively with the system. """
-    global mappingDefined
-    if not mappingDefined:
-        _createMapping(engine)
-        mappingDefined = True
-
-def _createMapping(engine):
-    metadata = MetaData(engine)
-    metadata.bind = engine
-
-    feedTable = Table("feed", metadata,
-        Column("pk", Integer, primary_key=True),
-        Column("title", String(255), nullable=False),
-        Column("rss_url", String(255), nullable=False),
-        # update interval is specified in minutes
-        Column("update_interval", Integer, nullable=False),
-        Column("next_update", DateTime, nullable=False),
-        # when displaying an entry of this feed, do not display the summary but rather load
-        # the link directly
-        Column("auto_load_entry_link", Boolean, nullable=False),
-        # this is actually a hack: when opening some sites in the QWebView it just crashes.
-        # This setting forces to open an entry's link in the external browser
-        Column("always_open_in_browser", Boolean, nullable=False)
-    )
-
-    global feedEntryTable
-    feedEntryTable = Table("feed_entry", metadata,
-        Column("pk", Integer, primary_key=True),
-        Column("create_timestamp", DateTime, nullable=False),
-        Column("read", Integer, nullable=False),
-
-        Column("id", String(512), nullable=False),
-        Column("link", String(512), nullable=False),
-        Column("title", Text, nullable=False),
-        Column("summary", Text, nullable=False),
-        Column("updated", DateTime),
-        Column("feed_id", Integer, ForeignKey("feed.pk"))
-    )
-
-    preferencesTable = Table("preference", metadata,
-        Column("pk", Integer, primary_key=True),
-        Column("key", String(255), nullable=False),
-        Column("value", String(255), nullable=False)
-    )
-
-    metadata.create_all()
-
-    mapper(FeedEntry, feedEntryTable)
-    mapper(Feed, feedTable,
-        properties = {
-            "entries" : relation(FeedEntry, backref="feed", lazy=True, cascade="delete, delete-orphan")
-        }
-    )
-    mapper(Preference, preferencesTable)
--- a/Preference.py	Sun Aug 21 02:47:25 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-
-class Preference(object):
-    @staticmethod
-    def forKey(key, session):
-        return session.query(Preference).filter(Preference.key == key).first()
-    
-    def __init__(self, key, value):
-        self.key = key
-        self.value = value
-    
-    def __repr__(self):
-        return "<Preference %s = %s>" % (self.key, self.value)
--- a/Preferences.py	Sun Aug 21 02:47:25 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,92 +0,0 @@
-
-from Preference import Preference
-import util
-
-DAYS_TO_KEEP_FEED_ENTRIES = "DAYS_TO_KEEP_FEED_ENTRIES"
-HIDE_READ_ENTRIES = "HIDE_READ_FEED_ENTRIES"
-PROXY_HOST = "PROXY_HOST"
-PROXY_PORT = "PROXY_PORT"
-SHOW_ONLY_UNREAD_FEEDS = "SHOW_ONLY_UNREAD_FEEDS"
-START_MAXIMIZED = "START_MAXIMIZED"
-
-class Preferences(object):
-    def __init__(self, session):
-        self.session = session
-        self.cache = {}
-
-    def _cachedPreference(self, key, defaultValue=None, addIfMissing=True):
-        if self.cache.has_key(key):
-            return self.cache[key]
-        else:
-            pref = Preference.forKey(key, self.session)
-            if pref is not None:
-                self.cache[key] = pref
-            elif pref is None and addIfMissing:
-                pref = Preference(key, str(defaultValue))
-                self.session.add(pref)
-                self.cache[key] = pref
-            return pref
-
-    def startMaximized(self):
-        pref = self._cachedPreference(START_MAXIMIZED, False)
-        return util.str2bool(pref.value)
-
-    def setStartMaximized(self, flag):
-        pref = self._cachedPreference(START_MAXIMIZED)
-        pref.value = util.bool2str(flag)
-
-    def hideReadFeedEntries(self):
-        pref = self._cachedPreference(HIDE_READ_ENTRIES, False)
-        return util.str2bool(pref.value)
-
-    def setHideReadFeedEntries(self, flag):
-        pref = self._cachedPreference(HIDE_READ_ENTRIES)
-        pref.value = util.bool2str(flag)
-
-    def showOnlyUnreadFeeds(self):
-        pref = self._cachedPreference(SHOW_ONLY_UNREAD_FEEDS, False)
-        return util.str2bool(pref.value)
-
-    def setShowOnlyUnreadFeeds(self, flag):
-        pref = self._cachedPreference(SHOW_ONLY_UNREAD_FEEDS)
-        pref.value = util.bool2str(flag)
-
-    def proxyHost(self):
-        pref = self._cachedPreference(PROXY_HOST)
-        return pref.value
-
-    def setProxyHost(self, hostname):
-        if hostname is None:
-            pref = self._cachedPreference(PROXY_HOST, addIfMissing=False)
-            if pref is not None:
-                self.session.delete(pref)
-                del(self.cache[PROXY_HOST])
-        else:
-            pref = self._cachedPreference(PROXY_HOST)
-            pref.value = str(hostname)
-
-    def proxyPort(self):
-        pref = self._cachedPreference(PROXY_PORT, 3128)
-        return int(pref.value)
-
-    def setProxyPort(self, port):
-        if port is None:
-            pref = self._cachedPreference(PROXY_PORT, addIfMissing=False)
-            if pref is not None:
-                self.session.delete(pref)
-                del(self.cache[PROXY_PORT])
-        else:
-            pref = self._cachedPreference(PROXY_PORT)
-            pref.value = str(port)
-
-    def isProxyConfigured(self):
-        pref = self._cachedPreference(PROXY_HOST, addIfMissing=False)
-        return pref is not None
-
-    def daysToKeepFeedEntries(self):
-        pref = self._cachedPreference(DAYS_TO_KEEP_FEED_ENTRIES, 90, addIfMissing=True)
-        return int(pref.value)
-
-    def setDaysToKeepFeedEntries(self, dayString):
-        pref = self._cachedPreference(DAYS_TO_KEEP_FEED_ENTRIES)
-        pref.value = dayString
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/backend/sqlalchemy/Database.py	Sun Aug 21 03:55:16 2011 +0200
@@ -0,0 +1,36 @@
+
+from sqlalchemy.engine import create_engine
+from sqlalchemy.orm import sessionmaker
+import Mapping
+import sys
+import util
+
+# Keep the connection to the database only once. The feed updater and the GUI app will
+# operate on a single engine/session but this comes in handy for interactive use
+engine = None
+SessionMaker = None
+
+def createSession(databaseUrl=None):
+    if databaseUrl is None:
+        databaseUrl = _getDatabaseUrl()
+    initEngine(databaseUrl)
+    Mapping.createMapping(engine)
+    initSessionMaker()
+    return SessionMaker()
+
+def _getDatabaseUrl():
+    if len(sys.argv) < 2:
+        print("Usage: %s <database url>" % (sys.argv[0]))
+        sys.exit(1)
+    return sys.argv[1]
+
+def initEngine(databaseUrl):
+    global engine
+    if engine is None:
+        verbose = util.databaseLoggingEnabled()
+        engine = create_engine(databaseUrl, echo=verbose)
+
+def initSessionMaker():
+    global SessionMaker
+    if SessionMaker is None:
+        SessionMaker = sessionmaker(bind=engine)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/backend/sqlalchemy/Feed.py	Sun Aug 21 03:55:16 2011 +0200
@@ -0,0 +1,46 @@
+
+from datetime import datetime, timedelta
+import FeedEntry
+
+class Feed(object):
+    @staticmethod
+    def all(session):
+        return session.query(Feed).order_by(Feed.title).all()
+
+    def __init__(self, title, rss_url):
+        self.title = title
+        self.rss_url = rss_url
+        # default: update every 60 minutes
+        self.update_interval = 60
+        self.incrementNextUpdateDate()
+        self.auto_load_entry_link = False
+        self.always_open_in_browser = False
+
+    def __repr__(self):
+        return "<Feed (%d) %s>" % (self.pk, self.title)
+
+    def userPresentableString(self):
+        return self.title
+
+    def entriesSortedByUpdateDate(self, hideReadEntries=False):
+        if hideReadEntries:
+            sortedEntries = self._unreadEntries()
+        else:
+            sortedEntries = list(self.entries)
+        sortedEntries.sort(FeedEntry.compareByUpdateDate)
+        return sortedEntries
+
+    def _unreadEntries(self):
+        retValue = []
+        for entry in self.entries:
+            if not entry.read:
+                retValue.append(entry)
+        return retValue
+
+    def incrementNextUpdateDate(self):
+        delta = timedelta(minutes=self.update_interval)
+        self.next_update = datetime.now() + delta
+
+    def markAllEntriesRead(self):
+        for entry in self.entries:
+            entry.markRead()
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/backend/sqlalchemy/FeedEntry.py	Sun Aug 21 03:55:16 2011 +0200
@@ -0,0 +1,43 @@
+
+from datetime import datetime
+
+def compareByUpdateDate(first, second):
+    return cmp(first.updated, second.updated)
+
+class FeedEntry(object):
+    @staticmethod
+    def findById(id, session):
+        result = session.query(FeedEntry).filter(FeedEntry.id == id)
+        return result.first()
+
+    @staticmethod
+    def create(entry):
+        new = FeedEntry()
+        new.id = entry.id
+        new.link = entry.link
+        new.title = entry.title
+        new.updated = entry.updated_parsed
+        new.summary = entry.summary
+        return new
+
+    def __init__(self):
+        self.create_timestamp = datetime.now()
+        self.read = 0
+
+    def __repr__(self):
+        return "<FeedEntry (%d) %s>" % (self.pk, self.title)
+
+    def userPresentableString(self):
+        return self.title
+    
+    def toggleRead(self):
+        if self.read:
+            self.markUnread()
+        else:
+            self.markRead()
+            
+    def markRead(self):
+        self.read = 1
+
+    def markUnread(self):
+        self.read = 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/backend/sqlalchemy/FeedList.py	Sun Aug 21 03:55:16 2011 +0200
@@ -0,0 +1,23 @@
+
+from Feed import Feed
+from FeedEntry import FeedEntry
+from Preferences import Preferences
+from sqlalchemy.orm import joinedload
+
+def getFeeds(session):
+    preferences = Preferences(session)
+    if preferences.showOnlyUnreadFeeds():
+        return _getUnreadFeeds(session)
+    else:
+        return Feed.all(session)
+
+def _getUnreadFeeds(session):
+    query = session.query(FeedEntry).filter(FeedEntry.read == 0)
+    queryWithOptions = query.options(joinedload("feed"))
+    result = queryWithOptions.all()
+    return _collectFeeds(result)
+
+def _collectFeeds(feedEntries):
+    feeds = [entry.feed for entry in feedEntries]
+    uniqueFeeds = set(feeds)
+    return list(uniqueFeeds)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/backend/sqlalchemy/Mapping.py	Sun Aug 21 03:55:16 2011 +0200
@@ -0,0 +1,75 @@
+
+from Feed import Feed
+from FeedEntry import FeedEntry
+from Preference import Preference
+from sqlalchemy import Boolean
+from sqlalchemy import Column
+from sqlalchemy import DateTime
+from sqlalchemy import ForeignKey
+from sqlalchemy import Integer
+from sqlalchemy import MetaData
+from sqlalchemy import String
+from sqlalchemy import Table
+from sqlalchemy import Text
+from sqlalchemy.orm import mapper
+from sqlalchemy.orm import relation
+
+mappingDefined = False
+feedEntryTable = None
+
+def createMapping(engine):
+    """ Make sure the mapping is defined only once. This is not really needed for the feed updater
+        or the GUI app but comes in handy when working interactively with the system. """
+    global mappingDefined
+    if not mappingDefined:
+        _createMapping(engine)
+        mappingDefined = True
+
+def _createMapping(engine):
+    metadata = MetaData(engine)
+    metadata.bind = engine
+
+    feedTable = Table("feed", metadata,
+        Column("pk", Integer, primary_key=True),
+        Column("title", String(255), nullable=False),
+        Column("rss_url", String(255), nullable=False),
+        # update interval is specified in minutes
+        Column("update_interval", Integer, nullable=False),
+        Column("next_update", DateTime, nullable=False),
+        # when displaying an entry of this feed, do not display the summary but rather load
+        # the link directly
+        Column("auto_load_entry_link", Boolean, nullable=False),
+        # this is actually a hack: when opening some sites in the QWebView it just crashes.
+        # This setting forces to open an entry's link in the external browser
+        Column("always_open_in_browser", Boolean, nullable=False)
+    )
+
+    global feedEntryTable
+    feedEntryTable = Table("feed_entry", metadata,
+        Column("pk", Integer, primary_key=True),
+        Column("create_timestamp", DateTime, nullable=False),
+        Column("read", Integer, nullable=False),
+
+        Column("id", String(512), nullable=False),
+        Column("link", String(512), nullable=False),
+        Column("title", Text, nullable=False),
+        Column("summary", Text, nullable=False),
+        Column("updated", DateTime),
+        Column("feed_id", Integer, ForeignKey("feed.pk"))
+    )
+
+    preferencesTable = Table("preference", metadata,
+        Column("pk", Integer, primary_key=True),
+        Column("key", String(255), nullable=False),
+        Column("value", String(255), nullable=False)
+    )
+
+    metadata.create_all()
+
+    mapper(FeedEntry, feedEntryTable)
+    mapper(Feed, feedTable,
+        properties = {
+            "entries" : relation(FeedEntry, backref="feed", lazy=True, cascade="delete, delete-orphan")
+        }
+    )
+    mapper(Preference, preferencesTable)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/backend/sqlalchemy/Preference.py	Sun Aug 21 03:55:16 2011 +0200
@@ -0,0 +1,12 @@
+
+class Preference(object):
+    @staticmethod
+    def forKey(key, session):
+        return session.query(Preference).filter(Preference.key == key).first()
+    
+    def __init__(self, key, value):
+        self.key = key
+        self.value = value
+    
+    def __repr__(self):
+        return "<Preference %s = %s>" % (self.key, self.value)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/backend/sqlalchemy/Preferences.py	Sun Aug 21 03:55:16 2011 +0200
@@ -0,0 +1,100 @@
+
+from Preference import Preference
+
+DAYS_TO_KEEP_FEED_ENTRIES = "DAYS_TO_KEEP_FEED_ENTRIES"
+HIDE_READ_ENTRIES = "HIDE_READ_FEED_ENTRIES"
+PROXY_HOST = "PROXY_HOST"
+PROXY_PORT = "PROXY_PORT"
+SHOW_ONLY_UNREAD_FEEDS = "SHOW_ONLY_UNREAD_FEEDS"
+START_MAXIMIZED = "START_MAXIMIZED"
+
+def str2bool(string):
+    return string.lower() in ["yes", "true", "t", "1"]
+
+def bool2str(bool):
+    if bool:
+        return "True"
+    else:
+        return "False"
+
+class Preferences(object):
+    def __init__(self, session):
+        self.session = session
+        self.cache = {}
+
+    def _cachedPreference(self, key, defaultValue=None, addIfMissing=True):
+        if self.cache.has_key(key):
+            return self.cache[key]
+        else:
+            pref = Preference.forKey(key, self.session)
+            if pref is not None:
+                self.cache[key] = pref
+            elif pref is None and addIfMissing:
+                pref = Preference(key, str(defaultValue))
+                self.session.add(pref)
+                self.cache[key] = pref
+            return pref
+
+    def startMaximized(self):
+        pref = self._cachedPreference(START_MAXIMIZED, False)
+        return str2bool(pref.value)
+
+    def setStartMaximized(self, flag):
+        pref = self._cachedPreference(START_MAXIMIZED)
+        pref.value = bool2str(flag)
+
+    def hideReadFeedEntries(self):
+        pref = self._cachedPreference(HIDE_READ_ENTRIES, False)
+        return str2bool(pref.value)
+
+    def setHideReadFeedEntries(self, flag):
+        pref = self._cachedPreference(HIDE_READ_ENTRIES)
+        pref.value = bool2str(flag)
+
+    def showOnlyUnreadFeeds(self):
+        pref = self._cachedPreference(SHOW_ONLY_UNREAD_FEEDS, False)
+        return str2bool(pref.value)
+
+    def setShowOnlyUnreadFeeds(self, flag):
+        pref = self._cachedPreference(SHOW_ONLY_UNREAD_FEEDS)
+        pref.value = bool2str(flag)
+
+    def proxyHost(self):
+        pref = self._cachedPreference(PROXY_HOST)
+        return pref.value
+
+    def setProxyHost(self, hostname):
+        if hostname is None:
+            pref = self._cachedPreference(PROXY_HOST, addIfMissing=False)
+            if pref is not None:
+                self.session.delete(pref)
+                del(self.cache[PROXY_HOST])
+        else:
+            pref = self._cachedPreference(PROXY_HOST)
+            pref.value = str(hostname)
+
+    def proxyPort(self):
+        pref = self._cachedPreference(PROXY_PORT, 3128)
+        return int(pref.value)
+
+    def setProxyPort(self, port):
+        if port is None:
+            pref = self._cachedPreference(PROXY_PORT, addIfMissing=False)
+            if pref is not None:
+                self.session.delete(pref)
+                del(self.cache[PROXY_PORT])
+        else:
+            pref = self._cachedPreference(PROXY_PORT)
+            pref.value = str(port)
+
+    def isProxyConfigured(self):
+        pref = self._cachedPreference(PROXY_HOST, addIfMissing=False)
+        return pref is not None
+
+    def daysToKeepFeedEntries(self):
+        pref = self._cachedPreference(DAYS_TO_KEEP_FEED_ENTRIES, 90, addIfMissing=True)
+        return int(pref.value)
+
+    def setDaysToKeepFeedEntries(self, dayString):
+        pref = self._cachedPreference(DAYS_TO_KEEP_FEED_ENTRIES)
+        pref.value = dayString
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/backend/sqlalchemy/SqlAlchemyBackend.py	Sun Aug 21 03:55:16 2011 +0200
@@ -0,0 +1,34 @@
+
+from Preferences import Preferences
+import Database
+import logging
+import util
+import FeedList
+
+class SqlAlchemyBackend(object):
+    '''
+    Backend that uses sqlalchemy for persistence
+    '''
+
+    def __init__(self):
+        self._initLogging()
+        self.session = Database.createSession()
+        self.prefs = Preferences(self.session)
+
+    def _initLogging(self):
+        logging.getLogger("sqlalchemy.orm").setLevel(logging.WARN)
+
+        sqlalchemyLogLevel = logging.ERROR
+        if util.databaseLoggingEnabled():
+            sqlalchemyLogLevel = logging.INFO
+        logging.getLogger("sqlalchemy").setLevel(sqlalchemyLogLevel)
+
+    def preferences(self):
+        return self.prefs
+
+    def getFeeds(self):
+        return FeedList.getFeeds(self.session)
+
+    def dispose(self):
+        # save all uncommitted state, just in case
+        self.session.commit()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/backend/sqlalchemy/util.py	Sun Aug 21 03:55:16 2011 +0200
@@ -0,0 +1,9 @@
+
+import sys
+
+def databaseLoggingEnabled():
+    loggingEnabled = False
+    for arg in sys.argv:
+        if arg == "--databaseLogging":
+            loggingEnabled = True
+    return loggingEnabled
--- a/feedworm-gui.py	Sun Aug 21 02:47:25 2011 +0200
+++ b/feedworm-gui.py	Sun Aug 21 03:55:16 2011 +0200
@@ -1,11 +1,10 @@
 
-import Database
 from MainWindow import MainWindow
-from Preferences import Preferences
 from PyQt4 import QtGui
 from PyQt4.QtNetwork import QNetworkProxy
+from backend.sqlalchemy.SqlAlchemyBackend import SqlAlchemyBackend
+import logging
 import sys
-import util
 
 def setupProxy(preferences):
     if preferences.isProxyConfigured():
@@ -15,14 +14,14 @@
         QNetworkProxy.setApplicationProxy(proxy)
 
 if __name__ == '__main__':
-    util.configureLogging()
-    session = Database.createSession()
-    preferences = Preferences(session)
+    logging.basicConfig(level=logging.DEBUG)
+    backend = SqlAlchemyBackend()
+    preferences = backend.preferences()
 
     setupProxy(preferences)
 
     app = QtGui.QApplication(sys.argv)
-    mainWindow = MainWindow(session)
+    mainWindow = MainWindow(backend)
 
     maximized = preferences.startMaximized()
     if maximized:
--- a/util.py	Sun Aug 21 02:47:25 2011 +0200
+++ b/util.py	Sun Aug 21 03:55:16 2011 +0200
@@ -1,27 +1,28 @@
 
-from datetime import datetime, timedelta
 from Feed import Feed
+from datetime import datetime, timedelta
 import logging
-import sys
 
 logger = logging.getLogger("database")
 
-def databaseLoggingEnabled():
-    loggingEnabled = False
-    for arg in sys.argv:
-        if arg == "--databaseLogging":
-            loggingEnabled = True
-    return loggingEnabled
+# TODO remove this
+#def databaseLoggingEnabled():
+#    loggingEnabled = False
+#    for arg in sys.argv:
+#        if arg == "--databaseLogging":
+#            loggingEnabled = True
+#    return loggingEnabled
 
-def configureLogging():
-    logging.basicConfig(level=logging.DEBUG)
-    
-    sqlalchemyLogLevel = logging.ERROR
-    if databaseLoggingEnabled():
-        sqlalchemyLogLevel = logging.INFO
-    logging.getLogger("sqlalchemy").setLevel(sqlalchemyLogLevel)
-    
-    logging.getLogger("sqlalchemy.orm").setLevel(logging.WARN)
+# TODO remove this
+#def configureLogging():
+#    logging.basicConfig(level=logging.DEBUG)
+#
+#    sqlalchemyLogLevel = logging.ERROR
+#    if databaseLoggingEnabled():
+#        sqlalchemyLogLevel = logging.INFO
+#    logging.getLogger("sqlalchemy").setLevel(sqlalchemyLogLevel)
+#
+#    logging.getLogger("sqlalchemy.orm").setLevel(logging.WARN)
 
 def loadFeeds(session=None, filename="feeds.txt"):
     file = open(filename)
@@ -39,11 +40,3 @@
         feed.next_update = datetime.now() - timedelta(minutes=1)
     session.commit()
 
-def str2bool(string):
-    return string.lower() in ["yes", "true", "t", "1"]
-
-def bool2str(bool):
-    if bool:
-        return "True"
-    else:
-        return "False"