Abhilash Raj has proposed merging lp:~raj-abhilash1/mailman/sqlalchemy into 
lp:mailman.

Requested reviews:
  Mailman Coders (mailman-coders)

For more details, see:
https://code.launchpad.net/~raj-abhilash1/mailman/sqlalchemy/+merge/235329

Replace storm with sqlalchemy.
-- 
https://code.launchpad.net/~raj-abhilash1/mailman/sqlalchemy/+merge/235329
Your team Mailman Coders is requested to review the proposed merge of 
lp:~raj-abhilash1/mailman/sqlalchemy into lp:mailman.
=== modified file 'setup.py'
--- setup.py	2014-04-15 16:06:01 +0000
+++ setup.py	2014-09-19 19:26:31 +0000
@@ -104,7 +104,7 @@
         'nose2',
         'passlib',
         'restish',
-        'storm',
+        'sqlalchemy',
         'zope.component',
         'zope.configuration',
         'zope.event',

=== modified file 'src/mailman/app/subscriptions.py'
--- src/mailman/app/subscriptions.py	2014-04-15 14:03:39 +0000
+++ src/mailman/app/subscriptions.py	2014-09-19 19:26:31 +0000
@@ -28,7 +28,7 @@
 
 from operator import attrgetter
 from passlib.utils import generate_password as generate
-from storm.expr import And, Or
+from sqlalchemy import and_, or_
 from uuid import UUID
 from zope.component import getUtility
 from zope.interface import implementer
@@ -88,8 +88,7 @@
     @dbconnection
     def get_member(self, store, member_id):
         """See `ISubscriptionService`."""
-        members = store.find(
-            Member,
+        members = store.query(Member).filter(
             Member._member_id == member_id)
         if members.count() == 0:
             return None
@@ -117,7 +116,7 @@
                 # This probably could be made more efficient.
                 if address is None or user is None:
                     return []
-                query.append(Or(Member.address_id == address.id,
+                query.append(or_(Member.address_id == address.id,
                                 Member.user_id == user.id))
             else:
                 # subscriber is a user id.
@@ -126,15 +125,15 @@
                                    if address.id is not None)
                 if len(address_ids) == 0 or user is None:
                     return []
-                query.append(Or(Member.user_id == user.id,
-                                Member.address_id.is_in(address_ids)))
+                query.append(or_(Member.user_id == user.id,
+                                Member.address_id.in_(address_ids)))
         # Calculate the rest of the query expression, which will get And'd
         # with the Or clause above (if there is one).
         if list_id is not None:
             query.append(Member.list_id == list_id)
         if role is not None:
             query.append(Member.role == role)
-        results = store.find(Member, And(*query))
+        results = store.query(Member).filter(and_(*query))
         return sorted(results, key=_membership_sort_key)
 
     def __iter__(self):

=== modified file 'src/mailman/database/base.py'
--- src/mailman/database/base.py	2014-01-01 14:59:42 +0000
+++ src/mailman/database/base.py	2014-09-19 19:26:31 +0000
@@ -29,8 +29,9 @@
 
 from lazr.config import as_boolean
 from pkg_resources import resource_listdir, resource_string
-from storm.cache import GenerationalCache
-from storm.locals import create_database, Store
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm.session import Session
 from zope.interface import implementer
 
 from mailman.config import config
@@ -45,23 +46,24 @@
 
 
 @implementer(IDatabase)
-class StormBaseDatabase:
-    """The database base class for use with the Storm ORM.
+class SABaseDatabase:
+    """The database base class for use with SQLAlchemy.
 
-    Use this as a base class for your DB-specific derived classes.
+    Use this as a base class for your DB-Specific derived classes.
     """
-
     # Tag used to distinguish the database being used.  Override this in base
     # classes.
+
     TAG = ''
 
     def __init__(self):
         self.url = None
         self.store = None
+        self.transaction = None
 
     def begin(self):
         """See `IDatabase`."""
-        # Storm takes care of this for us.
+        # SA does this for us.
         pass
 
     def commit(self):
@@ -100,18 +102,9 @@
         """
         pass
 
-    def _prepare(self, url):
-        """Prepare the database for creation.
-
-        Some database backends need to do so me prep work before letting Storm
-        create the database.  For example, we have to touch the SQLite .db
-        file first so that it has the proper file modes.
-        """
-        pass
-
     def initialize(self, debug=None):
-        """See `IDatabase`."""
-        # Calculate the engine url.
+        """See `IDatabase`"""
+        # Calculate the engine url
         url = expand(config.database.url, config.paths)
         log.debug('Database url: %s', url)
         # XXX By design of SQLite, database file creation does not honor
@@ -129,13 +122,10 @@
         # engines, and yes, we could have chmod'd the file after the fact, but
         # half dozen and all...
         self.url = url
-        self._prepare(url)
-        database = create_database(url)
-        store = Store(database, GenerationalCache())
-        database.DEBUG = (as_boolean(config.database.debug)
-                          if debug is None else debug)
-        self.store = store
-        store.commit()
+        self.engine = create_engine(url)
+        session = sessionmaker(bind=self.engine)
+        self.store = session()
+        self.store.commit()
 
     def load_migrations(self, until=None):
         """Load schema migrations.
@@ -144,45 +134,8 @@
             With default value of None, load all migrations.
         :type until: string
         """
-        migrations_path = config.database.migrations_path
-        if '.' in migrations_path:
-            parent, dot, child = migrations_path.rpartition('.')
-        else:
-            parent = migrations_path
-            child = ''
-        # If the database does not yet exist, load the base schema.
-        filenames = sorted(resource_listdir(parent, child))
-        # Find out which schema migrations have already been loaded.
-        if self._database_exists(self.store):
-            versions = set(version.version for version in
-                           self.store.find(Version, component='schema'))
-        else:
-            versions = set()
-        for filename in filenames:
-            module_fn, extension = os.path.splitext(filename)
-            if extension != '.py':
-                continue
-            parts = module_fn.split('_')
-            if len(parts) < 2:
-                continue
-            version = parts[1].strip()
-            if len(version) == 0:
-                # Not a schema migration file.
-                continue
-            if version in versions:
-                log.debug('already migrated to %s', version)
-                continue
-            if until is not None and version > until:
-                # We're done.
-                break
-            module_path = migrations_path + '.' + module_fn
-            __import__(module_path)
-            upgrade = getattr(sys.modules[module_path], 'upgrade', None)
-            if upgrade is None:
-                continue
-            log.debug('migrating db to %s: %s', version, module_path)
-            upgrade(self, self.store, version, module_path)
-        self.commit()
+        from mailman.database.model import Model
+        Model.metadata.create_all(self.engine)
 
     def load_sql(self, store, sql):
         """Load the given SQL into the store.
@@ -200,29 +153,6 @@
             if statement.strip() != '':
                 store.execute(statement + ';')
 
-    def load_schema(self, store, version, filename, module_path):
-        """Load the schema from a file.
-
-        This is a helper method for migration classes to call.
-
-        :param store: The Storm store to load the schema into.
-        :type store: storm.locals.Store`
-        :param version: The schema version identifier of the form
-            YYYYMMDDHHMMSS.
-        :type version: string
-        :param filename: The file name containing the schema to load.  Pass
-            `None` if there is no schema file to load.
-        :type filename: string
-        :param module_path: The fully qualified Python module path to the
-            migration module being loaded.  This is used to record information
-            for use by the test suite.
-        :type module_path: string
-        """
-        if filename is not None:
-            contents = resource_string('mailman.database.schema', filename)
-            self.load_sql(store, contents)
-        # Add a marker that indicates the migration version being applied.
-        store.add(Version(component='schema', version=version))
 
     @staticmethod
     def _make_temporary():

=== modified file 'src/mailman/database/factory.py'
--- src/mailman/database/factory.py	2014-01-01 14:59:42 +0000
+++ src/mailman/database/factory.py	2014-09-19 19:26:31 +0000
@@ -62,10 +62,10 @@
 
 def _reset(self):
     """See `IDatabase`."""
-    from mailman.database.model import ModelMeta
+    from mailman.database.model import Model
     self.store.rollback()
     self._pre_reset(self.store)
-    ModelMeta._reset(self.store)
+    Model._reset(self)
     self._post_reset(self.store)
     self.store.commit()
 

=== modified file 'src/mailman/database/model.py'
--- src/mailman/database/model.py	2014-01-01 14:59:42 +0000
+++ src/mailman/database/model.py	2014-09-19 19:26:31 +0000
@@ -25,44 +25,24 @@
     ]
 
 
+import contextlib
 from operator import attrgetter
 
-from storm.properties import PropertyPublisherMeta
-
-
-
-class ModelMeta(PropertyPublisherMeta):
+from sqlalchemy.ext.declarative import declarative_base
+
+from mailman.config import config
+
+class ModelMeta(object):
     """Do more magic on table classes."""
 
-    _class_registry = set()
-
-    def __init__(self, name, bases, dict):
-        # Before we let the base class do it's thing, force an __storm_table__
-        # property to enforce our table naming convention.
-        self.__storm_table__ = name.lower()
-        super(ModelMeta, self).__init__(name, bases, dict)
-        # Register the model class so that it can be more easily cleared.
-        # This is required by the test framework so that the corresponding
-        # table can be reset between tests.
-        #
-        # The PRESERVE flag indicates whether the table should be reset or
-        # not.  We have to handle the actual Model base class explicitly
-        # because it does not correspond to a table in the database.
-        if not getattr(self, 'PRESERVE', False) and name != 'Model':
-            ModelMeta._class_registry.add(self)
-
     @staticmethod
-    def _reset(store):
-        from mailman.config import config
-        config.db._pre_reset(store)
-        # Make sure this is deterministic, by sorting on the storm table name.
-        classes = sorted(ModelMeta._class_registry,
-                         key=attrgetter('__storm_table__'))
-        for model_class in classes:
-            store.find(model_class).remove()
-
-
-
-class Model:
-    """Like Storm's `Storm` subclass, but with a bit extra."""
-    __metaclass__ = ModelMeta
+    def _reset(db):
+        meta = Model.metadata
+        engine = config.db.engine
+        with contextlib.closing(engine.connect()) as con:
+            trans = con.begin()
+            for table in reversed(meta.sorted_tables):
+                con.execute(table.delete())
+            trans.commit()
+
+Model = declarative_base(cls=ModelMeta)

=== modified file 'src/mailman/database/postgresql.py'
--- src/mailman/database/postgresql.py	2014-01-01 14:59:42 +0000
+++ src/mailman/database/postgresql.py	2014-09-19 19:26:31 +0000
@@ -32,12 +32,12 @@
 from operator import attrgetter
 from urlparse import urlsplit, urlunsplit
 
-from mailman.database.base import StormBaseDatabase
+from mailman.database.base import SABaseDatabase
 from mailman.testing.helpers import configuration
 
 
 
-class PostgreSQLDatabase(StormBaseDatabase):
+class PostgreSQLDatabase(SABaseDatabase):
     """Database class for PostgreSQL."""
 
     TAG = 'postgres'

=== modified file 'src/mailman/database/sqlite.py'
--- src/mailman/database/sqlite.py	2014-01-01 14:59:42 +0000
+++ src/mailman/database/sqlite.py	2014-09-19 19:26:31 +0000
@@ -34,12 +34,12 @@
 from functools import partial
 from urlparse import urlparse
 
-from mailman.database.base import StormBaseDatabase
+from mailman.database.base import SABaseDatabase
 from mailman.testing.helpers import configuration
 
 
 
-class SQLiteDatabase(StormBaseDatabase):
+class SQLiteDatabase(SABaseDatabase):
     """Database class for SQLite."""
 
     TAG = 'sqlite'
@@ -72,7 +72,7 @@
 def make_temporary(database):
     """Adapts by monkey patching an existing SQLite IDatabase."""
     tempdir = tempfile.mkdtemp()
-    url = 'sqlite:///' + os.path.join(tempdir, 'mailman.db')
+    url = 'sqlite:///'   + os.path.join(tempdir, 'mailman.db')
     with configuration('database', url=url):
         database.initialize()
     database._cleanup = types.MethodType(

=== modified file 'src/mailman/database/types.py'
--- src/mailman/database/types.py	2014-04-28 15:23:35 +0000
+++ src/mailman/database/types.py	2014-09-19 19:26:31 +0000
@@ -23,43 +23,108 @@
 __metaclass__ = type
 __all__ = [
     'Enum',
+    'UUID',
     ]
 
+import uuid
 
-from storm.properties import SimpleProperty
-from storm.variables import Variable
+from sqlalchemy import Integer
+from sqlalchemy.types import TypeDecorator, BINARY, CHAR
+from sqlalchemy.dialects import postgresql
 
 
 
-class _EnumVariable(Variable):
-    """Storm variable for supporting enum types.
-
-    To use this, make the database column a INTEGER.
-    """
-
-    def __init__(self, *args, **kws):
-        self._enum = kws.pop('enum')
-        super(_EnumVariable, self).__init__(*args, **kws)
-
-    def parse_set(self, value, from_db):
-        if value is None:
-            return None
-        if not from_db:
-            return value
-        return self._enum(value)
-
-    def parse_get(self, value, to_db):
-        if value is None:
-            return None
-        if not to_db:
-            return value
+class Enum(TypeDecorator):
+    """
+    Stores an integer-based Enum as an integer in the database, and converts it
+    on-the-fly.
+    """
+
+    impl = Integer
+
+    def __init__(self, *args, **kw):
+        self.enum = kw.pop("enum")
+        TypeDecorator.__init__(self, *args, **kw)
+
+    def process_bind_param(self, value, dialect):
+        if value is None:
+            return None
+
         return value.value
 
 
-class Enum(SimpleProperty):
-    """Custom type for Storm supporting enums."""
-
-    variable_class = _EnumVariable
-
-    def __init__(self, enum=None):
-        super(Enum, self).__init__(enum=enum)
+    def process_result_value(self, value, dialect):
+        if value is None:
+            return None
+        return self.enum(value)
+
+
+
+class UUID(TypeDecorator):
+    """
+    Stores a UUID in the database natively when it can and falls back to
+    a BINARY(16) or a CHAR(32) when it can't.
+
+    ::
+
+        from sqlalchemy_utils import UUIDType
+        import uuid
+
+        class User(Base):
+            __tablename__ = 'user'
+
+            # Pass `binary=False` to fallback to CHAR instead of BINARY
+            id = sa.Column(UUIDType(binary=False), primary_key=True)
+    """
+    impl = BINARY(16)
+
+    python_type = uuid.UUID
+
+    def __init__(self, binary=True, native=True):
+        """
+        :param binary: Whether to use a BINARY(16) or CHAR(32) fallback.
+        """
+        self.binary = binary
+        self.native = native
+
+    def load_dialect_impl(self, dialect):
+        if dialect.name == 'postgresql' and self.native:
+            # Use the native UUID type.
+            return dialect.type_descriptor(postgresql.UUID())
+
+        else:
+            # Fallback to either a BINARY or a CHAR.
+            kind = self.impl if self.binary else CHAR(32)
+            return dialect.type_descriptor(kind)
+
+    @staticmethod
+    def _coerce(value):
+        if value and not isinstance(value, uuid.UUID):
+            try:
+                value = uuid.UUID(value)
+
+            except (TypeError, ValueError):
+                value = uuid.UUID(bytes=value)
+
+        return value
+
+    def process_bind_param(self, value, dialect):
+        if value is None:
+            return value
+
+        if not isinstance(value, uuid.UUID):
+            value = self._coerce(value)
+
+        if self.native and dialect.name == 'postgresql':
+            return str(value)
+
+        return value.bytes if self.binary else value.hex
+
+    def process_result_value(self, value, dialect):
+        if value is None:
+            return value
+
+        if self.native and dialect.name == 'postgresql':
+            return uuid.UUID(value)
+
+        return uuid.UUID(bytes=value) if self.binary else uuid.UUID(value)

=== modified file 'src/mailman/interfaces/database.py'
--- src/mailman/interfaces/database.py	2014-01-01 14:59:42 +0000
+++ src/mailman/interfaces/database.py	2014-09-19 19:26:31 +0000
@@ -61,7 +61,7 @@
         """Abort the current transaction."""
 
     store = Attribute(
-        """The underlying Storm store on which you can do queries.""")
+        """The underlying SQLAlchemy store on which you can do queries.""")
 
 
 

=== modified file 'src/mailman/model/address.py'
--- src/mailman/model/address.py	2014-04-15 03:00:41 +0000
+++ src/mailman/model/address.py	2014-09-19 19:26:31 +0000
@@ -26,7 +26,9 @@
 
 
 from email.utils import formataddr
-from storm.locals import DateTime, Int, Reference, Unicode
+from sqlalchemy import (Column, Integer, String, Unicode,
+                        ForeignKey, DateTime)
+from sqlalchemy.orm import relationship, backref
 from zope.component import getUtility
 from zope.event import notify
 from zope.interface import implementer
@@ -42,20 +44,22 @@
 class Address(Model):
     """See `IAddress`."""
 
-    id = Int(primary=True)
-    email = Unicode()
-    _original = Unicode()
-    display_name = Unicode()
-    _verified_on = DateTime(name='verified_on')
-    registered_on = DateTime()
-
-    user_id = Int()
-    user = Reference(user_id, 'User.id')
-    preferences_id = Int()
-    preferences = Reference(preferences_id, 'Preferences.id')
+    __tablename__ = 'address'
+
+    id = Column(Integer, primary_key=True)
+    email = Column(Unicode)
+    _original = Column(Unicode)
+    display_name = Column(Unicode)
+    _verified_on = Column('verified_on', DateTime)
+    registered_on = Column(DateTime)
+
+    user_id = Column(Integer, ForeignKey('user.id'))
+
+    preferences_id = Column(Integer, ForeignKey('preferences.id'))
+    preferences = relationship('Preferences',
+                              backref=backref('Address', uselist=False))
 
     def __init__(self, email, display_name):
-        super(Address, self).__init__()
         getUtility(IEmailValidator).validate(email)
         lower_case = email.lower()
         self.email = lower_case

=== modified file 'src/mailman/model/autorespond.py'
--- src/mailman/model/autorespond.py	2014-01-01 14:59:42 +0000
+++ src/mailman/model/autorespond.py	2014-09-19 19:26:31 +0000
@@ -26,7 +26,10 @@
     ]
 
 
-from storm.locals import And, Date, Desc, Int, Reference
+from sqlalchemy import (Column, Integer, String, Unicode,
+                        ForeignKey, Date)
+from sqlalchemy import desc
+from sqlalchemy.orm import relationship
 from zope.interface import implementer
 
 from mailman.database.model import Model
@@ -42,16 +45,18 @@
 class AutoResponseRecord(Model):
     """See `IAutoResponseRecord`."""
 
-    id = Int(primary=True)
-
-    address_id = Int()
-    address = Reference(address_id, 'Address.id')
-
-    mailing_list_id = Int()
-    mailing_list = Reference(mailing_list_id, 'MailingList.id')
-
-    response_type = Enum(Response)
-    date_sent = Date()
+    __tablename__ = 'autorespondrecord'
+
+    id = Column(Integer, primary_key=True)
+
+    address_id = Column(Integer, ForeignKey('address.id'))
+    address = relationship('Address')
+
+    mailing_list_id = Column(Integer, ForeignKey('mailinglist.id'))
+    mailing_list = relationship('MailingList')
+
+    response_type = Column(Enum(enum=Response))
+    date_sent = Column(Date)
 
     def __init__(self, mailing_list, address, response_type):
         self.mailing_list = mailing_list
@@ -71,12 +76,11 @@
     @dbconnection
     def todays_count(self, store, address, response_type):
         """See `IAutoResponseSet`."""
-        return store.find(
-            AutoResponseRecord,
-            And(AutoResponseRecord.address == address,
-                AutoResponseRecord.mailing_list == self._mailing_list,
-                AutoResponseRecord.response_type == response_type,
-                AutoResponseRecord.date_sent == today())).count()
+        return store.query(AutoResponseRecord).filter_by(
+            address = address,
+            mailing_list = self._mailing_list,
+            response_type = response_type,
+            date_sent = today()).count()
 
     @dbconnection
     def response_sent(self, store, address, response_type):
@@ -88,10 +92,9 @@
     @dbconnection
     def last_response(self, store, address, response_type):
         """See `IAutoResponseSet`."""
-        results = store.find(
-            AutoResponseRecord,
-            And(AutoResponseRecord.address == address,
-                AutoResponseRecord.mailing_list == self._mailing_list,
-                AutoResponseRecord.response_type == response_type)
-            ).order_by(Desc(AutoResponseRecord.date_sent))
+        results = store.query(AutoResponseRecord).filter_by(
+            address = address,
+            mailing_list = self._mailing_list,
+            response_type = response_type
+            ).order_by(desc(AutoResponseRecord.date_sent))
         return (None if results.count() == 0 else results.first())

=== modified file 'src/mailman/model/bans.py'
--- src/mailman/model/bans.py	2014-01-01 14:59:42 +0000
+++ src/mailman/model/bans.py	2014-09-19 19:26:31 +0000
@@ -27,7 +27,7 @@
 
 import re
 
-from storm.locals import Int, Unicode
+from sqlalchemy import Column, Integer, Unicode
 from zope.interface import implementer
 
 from mailman.database.model import Model
@@ -40,9 +40,11 @@
 class Ban(Model):
     """See `IBan`."""
 
-    id = Int(primary=True)
-    email = Unicode()
-    list_id = Unicode()
+    __tablename__ = 'ban'
+
+    id = Column(Integer, primary_key=True)
+    email = Column(Unicode)
+    list_id = Column(Unicode)
 
     def __init__(self, email, list_id):
         super(Ban, self).__init__()
@@ -62,7 +64,7 @@
     @dbconnection
     def ban(self, store, email):
         """See `IBanManager`."""
-        bans = store.find(Ban, email=email, list_id=self._list_id)
+        bans = store.query(Ban).filter_by(email=email, list_id=self._list_id)
         if bans.count() == 0:
             ban = Ban(email, self._list_id)
             store.add(ban)
@@ -70,9 +72,10 @@
     @dbconnection
     def unban(self, store, email):
         """See `IBanManager`."""
-        ban = store.find(Ban, email=email, list_id=self._list_id).one()
+        ban = store.query(Ban).filter_by(email=email,
+                                         list_id=self._list_id).first()
         if ban is not None:
-            store.remove(ban)
+            store.delete(ban)
 
     @dbconnection
     def is_banned(self, store, email):
@@ -81,32 +84,32 @@
         if list_id is None:
             # The client is asking for global bans.  Look up bans on the
             # specific email address first.
-            bans = store.find(Ban, email=email, list_id=None)
+            bans = store.query(Ban).filter_by(email=email, list_id=None)
             if bans.count() > 0:
                 return True
             # And now look for global pattern bans.
-            bans = store.find(Ban, list_id=None)
+            bans = store.query(Ban).filter_by(list_id=None)
             for ban in bans:
                 if (ban.email.startswith('^') and
                     re.match(ban.email, email, re.IGNORECASE) is not None):
                     return True
         else:
             # This is a list-specific ban.
-            bans = store.find(Ban, email=email, list_id=list_id)
+            bans = store.query(Ban).filter_by(email=email, list_id=list_id)
             if bans.count() > 0:
                 return True
             # Try global bans next.
-            bans = store.find(Ban, email=email, list_id=None)
+            bans = store.query(Ban).filter_by(email=email, list_id=None)
             if bans.count() > 0:
                 return True
             # Now try specific mailing list bans, but with a pattern.
-            bans = store.find(Ban, list_id=list_id)
+            bans = store.query(Ban).filter_by(list_id=list_id)
             for ban in bans:
                 if (ban.email.startswith('^') and
                     re.match(ban.email, email, re.IGNORECASE) is not None):
                     return True
             # And now try global pattern bans.
-            bans = store.find(Ban, list_id=None)
+            bans = store.query(Ban).filter_by(list_id=None)
             for ban in bans:
                 if (ban.email.startswith('^') and
                     re.match(ban.email, email, re.IGNORECASE) is not None):

=== modified file 'src/mailman/model/bounce.py'
--- src/mailman/model/bounce.py	2014-01-01 14:59:42 +0000
+++ src/mailman/model/bounce.py	2014-09-19 19:26:31 +0000
@@ -26,7 +26,8 @@
     ]
 
 
-from storm.locals import Bool, Int, DateTime, Unicode
+
+from sqlalchemy import Column, Integer, Unicode, DateTime, Boolean
 from zope.interface import implementer
 
 from mailman.database.model import Model
@@ -42,13 +43,15 @@
 class BounceEvent(Model):
     """See `IBounceEvent`."""
 
-    id = Int(primary=True)
-    list_id = Unicode()
-    email = Unicode()
-    timestamp = DateTime()
-    message_id = Unicode()
-    context = Enum(BounceContext)
-    processed = Bool()
+    __tablename__ = 'bounceevent'
+
+    id = Column(Integer, primary_key=True)
+    list_id = Column(Unicode)
+    email = Column(Unicode)
+    timestamp = Column(DateTime)
+    message_id = Column(Unicode)
+    context = Column(Enum(enum=BounceContext))
+    processed = Column(Boolean)
 
     def __init__(self, list_id, email, msg, context=None):
         self.list_id = list_id
@@ -75,12 +78,12 @@
     @dbconnection
     def events(self, store):
         """See `IBounceProcessor`."""
-        for event in store.find(BounceEvent):
+        for event in store.query(BounceEvent).all():
             yield event
 
     @property
     @dbconnection
     def unprocessed(self, store):
         """See `IBounceProcessor`."""
-        for event in store.find(BounceEvent, BounceEvent.processed == False):
+        for event in store.query(BounceEvent).filter_by(processed = False):
             yield event

=== modified file 'src/mailman/model/digests.py'
--- src/mailman/model/digests.py	2014-01-01 14:59:42 +0000
+++ src/mailman/model/digests.py	2014-09-19 19:26:31 +0000
@@ -25,7 +25,8 @@
     ]
 
 
-from storm.locals import Int, Reference
+from sqlalchemy import Column, Integer, ForeignKey
+from sqlalchemy.orm import relationship
 from zope.interface import implementer
 
 from mailman.database.model import Model
@@ -39,15 +40,17 @@
 class OneLastDigest(Model):
     """See `IOneLastDigest`."""
 
-    id = Int(primary=True)
-
-    mailing_list_id = Int()
-    mailing_list = Reference(mailing_list_id, 'MailingList.id')
-
-    address_id = Int()
-    address = Reference(address_id, 'Address.id')
-
-    delivery_mode = Enum(DeliveryMode)
+    __tablename__ = 'onelastdigest'
+
+    id = Column(Integer, primary_key=True)
+
+    mailing_list_id = Column(Integer, ForeignKey('mailinglist.id'))
+    maling_list = relationship('MailingList')
+
+    address_id = Column(Integer, ForeignKey('address.id'))
+    address = relationship('Address')
+
+    delivery_mode = Column(Enum(enum=DeliveryMode))
 
     def __init__(self, mailing_list, address, delivery_mode):
         self.mailing_list = mailing_list

=== modified file 'src/mailman/model/domain.py'
--- src/mailman/model/domain.py	2014-01-01 14:59:42 +0000
+++ src/mailman/model/domain.py	2014-09-19 19:26:31 +0000
@@ -27,7 +27,7 @@
 
 
 from urlparse import urljoin, urlparse
-from storm.locals import Int, Unicode
+from sqlalchemy import Column, Unicode, Integer
 from zope.event import notify
 from zope.interface import implementer
 
@@ -44,12 +44,14 @@
 class Domain(Model):
     """Domains."""
 
-    id = Int(primary=True)
-
-    mail_host = Unicode()
-    base_url = Unicode()
-    description = Unicode()
-    contact_address = Unicode()
+    __tablename__ = 'domain'
+
+    id = Column(Integer, primary_key=True)
+
+    mail_host = Column(Unicode)
+    base_url = Column(Unicode)
+    description = Column(Unicode)
+    contact_address = Column(Unicode)
 
     def __init__(self, mail_host,
                  description=None,
@@ -92,8 +94,7 @@
     @dbconnection
     def mailing_lists(self, store):
         """See `IDomain`."""
-        mailing_lists = store.find(
-            MailingList,
+        mailing_lists = store.query(MailingList).filter(
             MailingList.mail_host == self.mail_host)
         for mlist in mailing_lists:
             yield mlist
@@ -140,14 +141,14 @@
     def remove(self, store, mail_host):
         domain = self[mail_host]
         notify(DomainDeletingEvent(domain))
-        store.remove(domain)
+        store.delete(domain)
         notify(DomainDeletedEvent(mail_host))
         return domain
 
     @dbconnection
     def get(self, store, mail_host, default=None):
         """See `IDomainManager`."""
-        domains = store.find(Domain, mail_host=mail_host)
+        domains = store.query(Domain).filter_by(mail_host=mail_host)
         if domains.count() < 1:
             return default
         assert domains.count() == 1, (
@@ -164,15 +165,15 @@
 
     @dbconnection
     def __len__(self, store):
-        return store.find(Domain).count()
+        return store.query(Domain).count()
 
     @dbconnection
     def __iter__(self, store):
         """See `IDomainManager`."""
-        for domain in store.find(Domain):
+        for domain in store.query(Domain).all():
             yield domain
 
     @dbconnection
     def __contains__(self, store, mail_host):
         """See `IDomainManager`."""
-        return store.find(Domain, mail_host=mail_host).count() > 0
+        return store.query(Domain).filter_by(mail_host=mail_host).count() > 0

=== modified file 'src/mailman/model/language.py'
--- src/mailman/model/language.py	2014-01-01 14:59:42 +0000
+++ src/mailman/model/language.py	2014-09-19 19:26:31 +0000
@@ -25,8 +25,8 @@
     ]
 
 
-from storm.locals import Int, Unicode
 from zope.interface import implementer
+from sqlalchemy import Column, Unicode, Integer
 
 from mailman.database import Model
 from mailman.interfaces import ILanguage
@@ -37,5 +37,7 @@
 class Language(Model):
     """See `ILanguage`."""
 
-    id = Int(primary=True)
-    code = Unicode()
+    __tablename__ = 'language'
+
+    id = Column(Integer, primary_key=True)
+    code = Column(Unicode)

=== modified file 'src/mailman/model/listmanager.py'
--- src/mailman/model/listmanager.py	2014-04-14 16:14:13 +0000
+++ src/mailman/model/listmanager.py	2014-09-19 19:26:31 +0000
@@ -52,9 +52,7 @@
             raise InvalidEmailAddressError(fqdn_listname)
         list_id = '{0}.{1}'.format(listname, hostname)
         notify(ListCreatingEvent(fqdn_listname))
-        mlist = store.find(
-            MailingList,
-            MailingList._list_id == list_id).one()
+        mlist = store.query(MailingList).filter_by(_list_id=list_id).first()
         if mlist:
             raise ListAlreadyExistsError(fqdn_listname)
         mlist = MailingList(fqdn_listname)
@@ -68,40 +66,40 @@
         """See `IListManager`."""
         listname, at, hostname = fqdn_listname.partition('@')
         list_id = '{0}.{1}'.format(listname, hostname)
-        return store.find(MailingList, MailingList._list_id == list_id).one()
+        return store.query(MailingList).filter_by(_list_id=list_id).first()
 
     @dbconnection
     def get_by_list_id(self, store, list_id):
         """See `IListManager`."""
-        return store.find(MailingList, MailingList._list_id == list_id).one()
+        return store.query(MailingList).filter_by(_list_id=list_id).first()
 
     @dbconnection
     def delete(self, store, mlist):
         """See `IListManager`."""
         fqdn_listname = mlist.fqdn_listname
         notify(ListDeletingEvent(mlist))
-        store.find(ContentFilter, ContentFilter.mailing_list == mlist).remove()
-        store.remove(mlist)
+        store.query(ContentFilter).filter_by(mailing_list=mlist).delete()
+        store.delete(mlist)
         notify(ListDeletedEvent(fqdn_listname))
 
     @property
     @dbconnection
     def mailing_lists(self, store):
         """See `IListManager`."""
-        for mlist in store.find(MailingList):
+        for mlist in store.query(MailingList).all():
             yield mlist
 
     @dbconnection
     def __iter__(self, store):
         """See `IListManager`."""
-        for mlist in store.find(MailingList):
+        for mlist in store.query(MailingList).all():
             yield mlist
 
     @property
     @dbconnection
     def names(self, store):
         """See `IListManager`."""
-        result_set = store.find(MailingList)
+        result_set = store.query(MailingList)
         for mail_host, list_name in result_set.values(MailingList.mail_host,
                                                       MailingList.list_name):
             yield '{0}@{1}'.format(list_name, mail_host)
@@ -110,7 +108,7 @@
     @dbconnection
     def list_ids(self, store):
         """See `IListManager`."""
-        result_set = store.find(MailingList)
+        result_set = store.query(MailingList)
         for list_id in result_set.values(MailingList._list_id):
             yield list_id
 
@@ -118,7 +116,7 @@
     @dbconnection
     def name_components(self, store):
         """See `IListManager`."""
-        result_set = store.find(MailingList)
+        result_set = store.query(MailingList)
         for mail_host, list_name in result_set.values(MailingList.mail_host,
                                                       MailingList.list_name):
             yield list_name, mail_host

=== modified file 'src/mailman/model/mailinglist.py'
--- src/mailman/model/mailinglist.py	2014-04-14 16:14:13 +0000
+++ src/mailman/model/mailinglist.py	2014-09-19 19:26:31 +0000
@@ -27,9 +27,10 @@
 
 import os
 
-from storm.locals import (
-    And, Bool, DateTime, Float, Int, Pickle, RawStr, Reference, Store,
-    TimeDelta, Unicode)
+from sqlalchemy import (Column, Boolean, DateTime, Float, Integer, Unicode,
+                        PickleType, Interval, ForeignKey, LargeBinary)
+from sqlalchemy import event
+from sqlalchemy.orm import relationship, sessionmaker
 from urlparse import urljoin
 from zope.component import getUtility
 from zope.event import notify
@@ -67,130 +68,132 @@
 SPACE = ' '
 UNDERSCORE = '_'
 
+Session = sessionmaker()
 
 
 @implementer(IMailingList)
 class MailingList(Model):
     """See `IMailingList`."""
 
-    id = Int(primary=True)
+    __tablename__ = 'mailinglist'
+
+    id = Column(Integer, primary_key=True)
 
     # XXX denotes attributes that should be part of the public interface but
     # are currently missing.
 
     # List identity
-    list_name = Unicode()
-    mail_host = Unicode()
-    _list_id = Unicode(name='list_id')
-    allow_list_posts = Bool()
-    include_rfc2369_headers = Bool()
-    advertised = Bool()
-    anonymous_list = Bool()
+    list_name = Column(Unicode)
+    mail_host = Column(Unicode)
+    _list_id = Column('list_id', Unicode)
+    allow_list_posts = Column(Boolean)
+    include_rfc2369_headers = Column(Boolean)
+    advertised = Column(Boolean)
+    anonymous_list = Column(Boolean)
     # Attributes not directly modifiable via the web u/i
-    created_at = DateTime()
+    created_at = Column(DateTime)
     # Attributes which are directly modifiable via the web u/i.  The more
     # complicated attributes are currently stored as pickles, though that
     # will change as the schema and implementation is developed.
-    next_request_id = Int()
-    next_digest_number = Int()
-    digest_last_sent_at = DateTime()
-    volume = Int()
-    last_post_at = DateTime()
+    next_request_id = Column(Integer)
+    next_digest_number = Column(Integer)
+    digest_last_sent_at = Column(DateTime)
+    volume = Column(Integer)
+    last_post_at = Column(DateTime)
     # Implicit destination.
-    acceptable_aliases_id = Int()
-    acceptable_alias = Reference(acceptable_aliases_id, 'AcceptableAlias.id')
+    # acceptable_aliases_id = Column(Integer, ForeignKey('acceptablealias.id'))
+    # acceptable_alias = relationship('AcceptableAlias', backref='mailing_list')
     # Attributes which are directly modifiable via the web u/i.  The more
     # complicated attributes are currently stored as pickles, though that
     # will change as the schema and implementation is developed.
-    accept_these_nonmembers = Pickle() # XXX
-    admin_immed_notify = Bool()
-    admin_notify_mchanges = Bool()
-    administrivia = Bool()
-    archive_policy = Enum(ArchivePolicy)
+    accept_these_nonmembers = Column(PickleType) # XXX
+    admin_immed_notify = Column(Boolean)
+    admin_notify_mchanges = Column(Boolean)
+    administrivia = Column(Boolean)
+    archive_policy = Column(Enum(enum=ArchivePolicy))
     # Automatic responses.
-    autoresponse_grace_period = TimeDelta()
-    autorespond_owner = Enum(ResponseAction)
-    autoresponse_owner_text = Unicode()
-    autorespond_postings = Enum(ResponseAction)
-    autoresponse_postings_text = Unicode()
-    autorespond_requests = Enum(ResponseAction)
-    autoresponse_request_text = Unicode()
+    autoresponse_grace_period = Column(Interval)
+    autorespond_owner = Column(Enum(enum=ResponseAction))
+    autoresponse_owner_text = Column(Unicode)
+    autorespond_postings = Column(Enum(enum=ResponseAction))
+    autoresponse_postings_text = Column(Unicode)
+    autorespond_requests = Column(Enum(enum=ResponseAction))
+    autoresponse_request_text = Column(Unicode)
     # Content filters.
-    filter_action = Enum(FilterAction)
-    filter_content = Bool()
-    collapse_alternatives = Bool()
-    convert_html_to_plaintext = Bool()
+    filter_action = Column(Enum(enum=FilterAction))
+    filter_content = Column(Boolean)
+    collapse_alternatives = Column(Boolean)
+    convert_html_to_plaintext = Column(Boolean)
     # Bounces.
-    bounce_info_stale_after = TimeDelta() # XXX
-    bounce_matching_headers = Unicode() # XXX
-    bounce_notify_owner_on_disable = Bool() # XXX
-    bounce_notify_owner_on_removal = Bool() # XXX
-    bounce_score_threshold = Int() # XXX
-    bounce_you_are_disabled_warnings = Int() # XXX
-    bounce_you_are_disabled_warnings_interval = TimeDelta() # XXX
-    forward_unrecognized_bounces_to = Enum(UnrecognizedBounceDisposition)
-    process_bounces = Bool()
+    bounce_info_stale_after = Column(Interval) # XXX
+    bounce_matching_headers = Column(Unicode) # XXX
+    bounce_notify_owner_on_disable = Column(Boolean) # XXX
+    bounce_notify_owner_on_removal = Column(Boolean) # XXX
+    bounce_score_threshold = Column(Integer) # XXX
+    bounce_you_are_disabled_warnings = Column(Integer) # XXX
+    bounce_you_are_disabled_warnings_interval = Column(Interval) # XXX
+    forward_unrecognized_bounces_to = Column(Enum(enum=UnrecognizedBounceDisposition))
+    process_bounces = Column(Boolean)
     # Miscellaneous
-    default_member_action = Enum(Action)
-    default_nonmember_action = Enum(Action)
-    description = Unicode()
-    digest_footer_uri = Unicode()
-    digest_header_uri = Unicode()
-    digest_is_default = Bool()
-    digest_send_periodic = Bool()
-    digest_size_threshold = Float()
-    digest_volume_frequency = Enum(DigestFrequency)
-    digestable = Bool()
-    discard_these_nonmembers = Pickle()
-    emergency = Bool()
-    encode_ascii_prefixes = Bool()
-    first_strip_reply_to = Bool()
-    footer_uri = Unicode()
-    forward_auto_discards = Bool()
-    gateway_to_mail = Bool()
-    gateway_to_news = Bool()
-    goodbye_message_uri = Unicode()
-    header_matches = Pickle()
-    header_uri = Unicode()
-    hold_these_nonmembers = Pickle()
-    info = Unicode()
-    linked_newsgroup = Unicode()
-    max_days_to_hold = Int()
-    max_message_size = Int()
-    max_num_recipients = Int()
-    member_moderation_notice = Unicode()
-    mime_is_default_digest = Bool()
+    default_member_action = Column(Enum(enum=Action))
+    default_nonmember_action = Column(Enum(enum=Action))
+    description = Column(Unicode)
+    digest_footer_uri = Column(Unicode)
+    digest_header_uri = Column(Unicode)
+    digest_is_default = Column(Boolean)
+    digest_send_periodic = Column(Boolean)
+    digest_size_threshold = Column(Float)
+    digest_volume_frequency = Column(Enum(enum=DigestFrequency))
+    digestable = Column(Boolean)
+    discard_these_nonmembers = Column(PickleType)
+    emergency = Column(Boolean)
+    encode_ascii_prefixes = Column(Boolean)
+    first_strip_reply_to = Column(Boolean)
+    footer_uri = Column(Unicode)
+    forward_auto_discards = Column(Boolean)
+    gateway_to_mail = Column(Boolean)
+    gateway_to_news = Column(Boolean)
+    goodbye_message_uri = Column(Unicode)
+    header_matches = Column(PickleType)
+    header_uri = Column(Unicode)
+    hold_these_nonmembers = Column(PickleType)
+    info = Column(Unicode)
+    linked_newsgroup = Column(Unicode)
+    max_days_to_hold = Column(Integer)
+    max_message_size = Column(Integer)
+    max_num_recipients = Column(Integer)
+    member_moderation_notice = Column(Unicode)
+    mime_is_default_digest = Column(Boolean)
     # FIXME: There should be no moderator_password
-    moderator_password = RawStr()
-    newsgroup_moderation = Enum(NewsgroupModeration)
-    nntp_prefix_subject_too = Bool()
-    nondigestable = Bool()
-    nonmember_rejection_notice = Unicode()
-    obscure_addresses = Bool()
-    owner_chain = Unicode()
-    owner_pipeline = Unicode()
-    personalize = Enum(Personalization)
-    post_id = Int()
-    posting_chain = Unicode()
-    posting_pipeline = Unicode()
-    _preferred_language = Unicode(name='preferred_language')
-    display_name = Unicode()
-    reject_these_nonmembers = Pickle()
-    reply_goes_to_list = Enum(ReplyToMunging)
-    reply_to_address = Unicode()
-    require_explicit_destination = Bool()
-    respond_to_post_requests = Bool()
-    scrub_nondigest = Bool()
-    send_goodbye_message = Bool()
-    send_welcome_message = Bool()
-    subject_prefix = Unicode()
-    topics = Pickle()
-    topics_bodylines_limit = Int()
-    topics_enabled = Bool()
-    welcome_message_uri = Unicode()
+    moderator_password = Column(LargeBinary) # TODO : was RawStr()
+    newsgroup_moderation = Column(Enum(enum=NewsgroupModeration))
+    nntp_prefix_subject_too = Column(Boolean)
+    nondigestable = Column(Boolean)
+    nonmember_rejection_notice = Column(Unicode)
+    obscure_addresses = Column(Boolean)
+    owner_chain = Column(Unicode)
+    owner_pipeline = Column(Unicode)
+    personalize = Column(Enum(enum=Personalization))
+    post_id = Column(Integer)
+    posting_chain = Column(Unicode)
+    posting_pipeline = Column(Unicode)
+    _preferred_language = Column('preferred_language', Unicode)
+    display_name = Column(Unicode)
+    reject_these_nonmembers = Column(PickleType)
+    reply_goes_to_list = Column(Enum(enum=ReplyToMunging))
+    reply_to_address = Column(Unicode)
+    require_explicit_destination = Column(Boolean)
+    respond_to_post_requests = Column(Boolean)
+    scrub_nondigest = Column(Boolean)
+    send_goodbye_message = Column(Boolean)
+    send_welcome_message = Column(Boolean)
+    subject_prefix = Column(Unicode)
+    topics = Column(PickleType)
+    topics_bodylines_limit = Column(Integer)
+    topics_enabled = Column(Boolean)
+    welcome_message_uri = Column(Unicode)
 
     def __init__(self, fqdn_listname):
-        super(MailingList, self).__init__()
         listname, at, hostname = fqdn_listname.partition('@')
         assert hostname, 'Bad list name: {0}'.format(fqdn_listname)
         self.list_name = listname
@@ -202,10 +205,11 @@
         # called when the MailingList object is loaded from the database, but
         # that's not the case when the constructor is called.  So, set up the
         # rosters explicitly.
-        self.__storm_loaded__()
+        self._post_load()
         makedirs(self.data_path)
 
-    def __storm_loaded__(self):
+
+    def _post_load(self, *args):
         self.owners = roster.OwnerRoster(self)
         self.moderators = roster.ModeratorRoster(self)
         self.administrators = roster.AdministratorRoster(self)
@@ -215,6 +219,10 @@
         self.subscribers = roster.Subscribers(self)
         self.nonmembers = roster.NonmemberRoster(self)
 
+    @classmethod
+    def __declare_last__(cls):
+        event.listen(cls, 'load', cls._post_load)
+
     def __repr__(self):
         return '<mailing list "{0}" at {1:#x}>'.format(
             self.fqdn_listname, id(self))
@@ -326,26 +334,24 @@
     def send_one_last_digest_to(self, address, delivery_mode):
         """See `IMailingList`."""
         digest = OneLastDigest(self, address, delivery_mode)
-        Store.of(self).add(digest)
+        Session.object_session(self).add(digest)
 
     @property
     def last_digest_recipients(self):
         """See `IMailingList`."""
-        results = Store.of(self).find(
-            OneLastDigest,
+        results = Session.object_session(self).query(OneLastDigest).filter(
             OneLastDigest.mailing_list == self)
         recipients = [(digest.address, digest.delivery_mode)
                       for digest in results]
-        results.remove()
+        results.delete()
         return recipients
 
     @property
     def filter_types(self):
         """See `IMailingList`."""
-        results = Store.of(self).find(
-            ContentFilter,
-            And(ContentFilter.mailing_list == self,
-                ContentFilter.filter_type == FilterType.filter_mime))
+        results = Session.object_session(self).query(ContentFilter).filter(
+            ContentFilter.mailing_list == self,
+            ContentFilter.filter_type == FilterType.filter_mime)
         for content_filter in results:
             yield content_filter.filter_pattern
 
@@ -353,12 +359,11 @@
     def filter_types(self, sequence):
         """See `IMailingList`."""
         # First, delete all existing MIME type filter patterns.
-        store = Store.of(self)
-        results = store.find(
-            ContentFilter,
-            And(ContentFilter.mailing_list == self,
-                ContentFilter.filter_type == FilterType.filter_mime))
-        results.remove()
+        store = Session.object_session(self)
+        results = store.query(ContentFilter).filter(
+            ContentFilter.mailing_list == self,
+            ContentFilter.filter_type == FilterType.filter_mime)
+        results.delete()
         # Now add all the new filter types.
         for mime_type in sequence:
             content_filter = ContentFilter(
@@ -368,10 +373,9 @@
     @property
     def pass_types(self):
         """See `IMailingList`."""
-        results = Store.of(self).find(
-            ContentFilter,
-            And(ContentFilter.mailing_list == self,
-                ContentFilter.filter_type == FilterType.pass_mime))
+        results = Session.object_session(self).query(ContentFilter).filter(
+            ContentFilter.mailing_list == self,
+            ContentFilter.filter_type == FilterType.pass_mime)
         for content_filter in results:
             yield content_filter.filter_pattern
 
@@ -379,12 +383,11 @@
     def pass_types(self, sequence):
         """See `IMailingList`."""
         # First, delete all existing MIME type pass patterns.
-        store = Store.of(self)
-        results = store.find(
-            ContentFilter,
-            And(ContentFilter.mailing_list == self,
-                ContentFilter.filter_type == FilterType.pass_mime))
-        results.remove()
+        store = Session.object_session(self)
+        results = store.query(ContentFilter).filter(
+            ContentFilter.mailing_list == self,
+            ContentFilter.filter_type == FilterType.pass_mime)
+        results.delete()
         # Now add all the new filter types.
         for mime_type in sequence:
             content_filter = ContentFilter(
@@ -394,10 +397,9 @@
     @property
     def filter_extensions(self):
         """See `IMailingList`."""
-        results = Store.of(self).find(
-            ContentFilter,
-            And(ContentFilter.mailing_list == self,
-                ContentFilter.filter_type == FilterType.filter_extension))
+        results = Session.object_session(self).query(ContentFilter).filter(
+            ContentFilter.mailing_list == self,
+            ContentFilter.filter_type == FilterType.filter_extension)
         for content_filter in results:
             yield content_filter.filter_pattern
 
@@ -405,12 +407,11 @@
     def filter_extensions(self, sequence):
         """See `IMailingList`."""
         # First, delete all existing file extensions filter patterns.
-        store = Store.of(self)
-        results = store.find(
-            ContentFilter,
-            And(ContentFilter.mailing_list == self,
-                ContentFilter.filter_type == FilterType.filter_extension))
-        results.remove()
+        store = Session.object_session(self)
+        results = store.query(ContentFilter).filter(
+            ContentFilter.mailing_list == self,
+            ContentFilter.filter_type == FilterType.filter_extension)
+        results.delete()
         # Now add all the new filter types.
         for mime_type in sequence:
             content_filter = ContentFilter(
@@ -420,10 +421,9 @@
     @property
     def pass_extensions(self):
         """See `IMailingList`."""
-        results = Store.of(self).find(
-            ContentFilter,
-            And(ContentFilter.mailing_list == self,
-                ContentFilter.filter_type == FilterType.pass_extension))
+        results = Session.object_session(self).query(ContentFilter).filter(
+            ContentFilter.mailing_list == self,
+            ContentFilter.filter_type == FilterType.pass_extension)
         for content_filter in results:
             yield content_filter.pass_pattern
 
@@ -431,12 +431,11 @@
     def pass_extensions(self, sequence):
         """See `IMailingList`."""
         # First, delete all existing file extensions pass patterns.
-        store = Store.of(self)
-        results = store.find(
-            ContentFilter,
-            And(ContentFilter.mailing_list == self,
-                ContentFilter.filter_type == FilterType.pass_extension))
-        results.remove()
+        store = Session.object_session(self)
+        results = store.query(ContentFilter).filter(
+            ContentFilter.mailing_list == self,
+            ContentFilter.filter_type == FilterType.pass_extension)
+        results.delete()
         # Now add all the new filter types.
         for mime_type in sequence:
             content_filter = ContentFilter(
@@ -457,24 +456,22 @@
 
     def subscribe(self, subscriber, role=MemberRole.member):
         """See `IMailingList`."""
-        store = Store.of(self)
+        store = Session.object_session(self)
         if IAddress.providedBy(subscriber):
-            member = store.find(
-                Member,
+            member = store.query(Member).filter(
                 Member.role == role,
                 Member.list_id == self._list_id,
-                Member._address == subscriber).one()
+                Member._address == subscriber).first()
             if member:
                 raise AlreadySubscribedError(
                     self.fqdn_listname, subscriber.email, role)
         elif IUser.providedBy(subscriber):
             if subscriber.preferred_address is None:
                 raise MissingPreferredAddressError(subscriber)
-            member = store.find(
-                Member,
+            member = store.query(Member).filter(
                 Member.role == role,
                 Member.list_id == self._list_id,
-                Member._user == subscriber).one()
+                Member._user == subscriber).first()
             if member:
                 raise AlreadySubscribedError(
                     self.fqdn_listname, subscriber, role)
@@ -494,12 +491,13 @@
 class AcceptableAlias(Model):
     """See `IAcceptableAlias`."""
 
-    id = Int(primary=True)
-
-    mailing_list_id = Int()
-    mailing_list = Reference(mailing_list_id, MailingList.id)
-
-    alias = Unicode()
+    __tablename__ = 'acceptablealias'
+
+    id = Column(Integer, primary_key=True)
+
+    mailing_list_id = Column(Integer, ForeignKey('mailinglist.id'))
+    mailing_list = relationship('MailingList', backref='acceptable_alias')
+    alias = Column(Unicode)
 
     def __init__(self, mailing_list, alias):
         self.mailing_list = mailing_list
@@ -516,27 +514,27 @@
 
     def clear(self):
         """See `IAcceptableAliasSet`."""
-        Store.of(self._mailing_list).find(
-            AcceptableAlias,
-            AcceptableAlias.mailing_list == self._mailing_list).remove()
+        Session.object_session(self._mailing_list).query(
+            AcceptableAlias).filter(
+                AcceptableAlias.mailing_list == self._mailing_list).delete()
 
     def add(self, alias):
         if not (alias.startswith('^') or '@' in alias):
             raise ValueError(alias)
         alias = AcceptableAlias(self._mailing_list, alias.lower())
-        Store.of(self._mailing_list).add(alias)
+        Session.object_session(self._mailing_list).add(alias)
 
     def remove(self, alias):
-        Store.of(self._mailing_list).find(
-            AcceptableAlias,
-            And(AcceptableAlias.mailing_list == self._mailing_list,
-                AcceptableAlias.alias == alias.lower())).remove()
+        Session.object_session(self._mailing_list).query(
+            AcceptableAlias).filter(
+                AcceptableAlias.mailing_list == self._mailing_list,
+                AcceptableAlias.alias == alias.lower()).delete()
 
     @property
     def aliases(self):
-        aliases = Store.of(self._mailing_list).find(
-            AcceptableAlias,
-            AcceptableAlias.mailing_list == self._mailing_list)
+        aliases = Session.object_session(self._mailing_list).query(
+            AcceptableAlias).filter(
+                AcceptableAlias.mailing_list_id == self._mailing_list.id)
         for alias in aliases:
             yield alias.alias
 
@@ -546,12 +544,14 @@
 class ListArchiver(Model):
     """See `IListArchiver`."""
 
-    id = Int(primary=True)
-
-    mailing_list_id = Int()
-    mailing_list = Reference(mailing_list_id, MailingList.id)
-    name = Unicode()
-    _is_enabled = Bool()
+    __tablename__ = 'listarchiver'
+
+    id = Column(Integer, primary_key=True)
+
+    mailing_list_id = Column(Integer, ForeignKey('mailinglist.id'))
+    mailing_list = relationship('MailingList')
+    name = Column(Unicode)
+    _is_enabled = Column(Boolean)
 
     def __init__(self, mailing_list, archiver_name, system_archiver):
         self.mailing_list = mailing_list
@@ -583,25 +583,24 @@
             system_archivers[archiver.name] = archiver
         # Add any system enabled archivers which aren't already associated
         # with the mailing list.
-        store = Store.of(self._mailing_list)
+        store = Session.object_session(self._mailing_list)
         for archiver_name in system_archivers:
-            exists = store.find(
-                ListArchiver,
-                And(ListArchiver.mailing_list == mailing_list,
-                    ListArchiver.name == archiver_name)).one()
+            exists = store.query(ListArchiver).filter(
+                ListArchiver.mailing_list == mailing_list,
+                ListArchiver.name == archiver_name).first()
             if exists is None:
                 store.add(ListArchiver(mailing_list, archiver_name,
                                        system_archivers[archiver_name]))
 
     @property
     def archivers(self):
-        entries = Store.of(self._mailing_list).find(
-            ListArchiver, ListArchiver.mailing_list == self._mailing_list)
+        entries = Session.object_session(self._mailing_list).query(
+            ListArchiver).filter(ListArchiver.mailing_list == self._mailing_list)
         for entry in entries:
             yield entry
 
     def get(self, archiver_name):
-        return Store.of(self._mailing_list).find(
-            ListArchiver,
-            And(ListArchiver.mailing_list == self._mailing_list,
-                ListArchiver.name == archiver_name)).one()
+        return Session.object_session(self._mailing_list).query(
+            ListArchiver).filter(
+                ListArchiver.mailing_list == self._mailing_list,
+                ListArchiver.name == archiver_name).first()

=== modified file 'src/mailman/model/member.py'
--- src/mailman/model/member.py	2014-03-02 21:38:32 +0000
+++ src/mailman/model/member.py	2014-09-19 19:26:31 +0000
@@ -24,8 +24,8 @@
     'Member',
     ]
 
-from storm.locals import Int, Reference, Unicode
-from storm.properties import UUID
+from sqlalchemy import Integer, Unicode, ForeignKey, Column
+from sqlalchemy.orm import relationship
 from zope.component import getUtility
 from zope.event import notify
 from zope.interface import implementer
@@ -33,7 +33,7 @@
 from mailman.core.constants import system_preferences
 from mailman.database.model import Model
 from mailman.database.transaction import dbconnection
-from mailman.database.types import Enum
+from mailman.database.types import Enum, UUID
 from mailman.interfaces.action import Action
 from mailman.interfaces.address import IAddress
 from mailman.interfaces.listmanager import IListManager
@@ -52,18 +52,20 @@
 class Member(Model):
     """See `IMember`."""
 
-    id = Int(primary=True)
-    _member_id = UUID()
-    role = Enum(MemberRole)
-    list_id = Unicode()
-    moderation_action = Enum(Action)
-
-    address_id = Int()
-    _address = Reference(address_id, 'Address.id')
-    preferences_id = Int()
-    preferences = Reference(preferences_id, 'Preferences.id')
-    user_id = Int()
-    _user = Reference(user_id, 'User.id')
+    __tablename__ = 'member'
+
+    id = Column(Integer, primary_key=True)
+    _member_id = Column(UUID)
+    role = Column(Enum(enum=MemberRole))
+    list_id = Column(Unicode)
+    moderation_action = Column(Enum(enum=Action))
+
+    address_id = Column(Integer, ForeignKey('address.id'))
+    _address = relationship('Address')
+    preferences_id = Column(Integer, ForeignKey('preferences.id'))
+    preferences = relationship('Preferences')
+    user_id = Column(Integer, ForeignKey('user.id'))
+    _user = relationship('User')
 
     def __init__(self, role, list_id, subscriber):
         self._member_id = uid_factory.new_uid()
@@ -198,5 +200,5 @@
         """See `IMember`."""
         # Yes, this must get triggered before self is deleted.
         notify(UnsubscriptionEvent(self.mailing_list, self))
-        store.remove(self.preferences)
-        store.remove(self)
+        store.delete(self.preferences)
+        store.delete(self)

=== modified file 'src/mailman/model/message.py'
--- src/mailman/model/message.py	2014-01-01 14:59:42 +0000
+++ src/mailman/model/message.py	2014-09-19 19:26:31 +0000
@@ -24,7 +24,7 @@
     'Message',
     ]
 
-from storm.locals import AutoReload, Int, RawStr, Unicode
+from sqlalchemy import Column, Integer, Unicode, LargeBinary
 from zope.interface import implementer
 
 from mailman.database.model import Model
@@ -37,15 +37,16 @@
 class Message(Model):
     """A message in the message store."""
 
-    id = Int(primary=True, default=AutoReload)
-    message_id = Unicode()
-    message_id_hash = RawStr()
-    path = RawStr()
+    __tablename__ = 'message'
+
+    id = Column(Integer, primary_key=True)
+    message_id = Column(Unicode)
+    message_id_hash = Column(Unicode)
+    path = Column(LargeBinary) # TODO : was RawStr()
     # This is a Messge-ID field representation, not a database row id.
 
     @dbconnection
     def __init__(self, store, message_id, message_id_hash, path):
-        super(Message, self).__init__()
         self.message_id = message_id
         self.message_id_hash = message_id_hash
         self.path = path

=== modified file 'src/mailman/model/messagestore.py'
--- src/mailman/model/messagestore.py	2014-01-01 14:59:42 +0000
+++ src/mailman/model/messagestore.py	2014-09-19 19:26:31 +0000
@@ -59,7 +59,7 @@
         # Calculate and insert the X-Message-ID-Hash.
         message_id = message_ids[0]
         # Complain if the Message-ID already exists in the storage.
-        existing = store.find(Message, Message.message_id == message_id).one()
+        existing = store.query(Message).filter(Message.message_id == message_id).first()
         if existing is not None:
             raise ValueError(
                 'Message ID already exists in message store: {0}'.format(
@@ -107,7 +107,7 @@
 
     @dbconnection
     def get_message_by_id(self, store, message_id):
-        row = store.find(Message, message_id=message_id).one()
+        row = store.query(Message).filter_by(message_id=message_id).first()
         if row is None:
             return None
         return self._get_message(row)
@@ -120,7 +120,7 @@
         # US-ASCII.
         if isinstance(message_id_hash, unicode):
             message_id_hash = message_id_hash.encode('ascii')
-        row = store.find(Message, message_id_hash=message_id_hash).one()
+        row = store.query(Message).filter_by(message_id_hash=message_id_hash).first()
         if row is None:
             return None
         return self._get_message(row)
@@ -128,14 +128,14 @@
     @property
     @dbconnection
     def messages(self, store):
-        for row in store.find(Message):
+        for row in store.query(Message).all():
             yield self._get_message(row)
 
     @dbconnection
     def delete_message(self, store, message_id):
-        row = store.find(Message, message_id=message_id).one()
+        row = store.query(Message).filter_by(message_id=message_id).first()
         if row is None:
             raise LookupError(message_id)
         path = os.path.join(config.MESSAGES_DIR, row.path)
         os.remove(path)
-        store.remove(row)
+        store.delete(row)

=== modified file 'src/mailman/model/mime.py'
--- src/mailman/model/mime.py	2014-01-01 14:59:42 +0000
+++ src/mailman/model/mime.py	2014-09-19 19:26:31 +0000
@@ -25,7 +25,8 @@
     ]
 
 
-from storm.locals import Int, Reference, Unicode
+from sqlalchemy import Column, Integer, Unicode, ForeignKey
+from sqlalchemy.orm import relationship
 from zope.interface import implementer
 
 from mailman.database.model import Model
@@ -38,13 +39,15 @@
 class ContentFilter(Model):
     """A single filter criteria."""
 
-    id = Int(primary=True)
-
-    mailing_list_id = Int()
-    mailing_list = Reference(mailing_list_id, 'MailingList.id')
-
-    filter_type = Enum(FilterType)
-    filter_pattern = Unicode()
+    __tablename__ = 'contentfilter'
+
+    id = Column(Integer, primary_key=True)
+
+    mailing_list_id = Column(Integer, ForeignKey('mailinglist.id'))
+    mailing_list = relationship('MailingList')
+
+    filter_type = Column(Enum(enum=FilterType))
+    filter_pattern = Column(Unicode)
 
     def __init__(self, mailing_list, filter_pattern, filter_type):
         self.mailing_list = mailing_list

=== modified file 'src/mailman/model/pending.py'
--- src/mailman/model/pending.py	2014-01-01 14:59:42 +0000
+++ src/mailman/model/pending.py	2014-09-19 19:26:31 +0000
@@ -31,7 +31,9 @@
 import hashlib
 
 from lazr.config import as_timedelta
-from storm.locals import DateTime, Int, RawStr, ReferenceSet, Unicode
+from sqlalchemy import (
+    Column, Integer, Unicode, ForeignKey, DateTime, LargeBinary)
+from sqlalchemy.orm import relationship
 from zope.interface import implementer
 from zope.interface.verify import verifyObject
 
@@ -49,14 +51,16 @@
 class PendedKeyValue(Model):
     """A pended key/value pair, tied to a token."""
 
+    __tablename__ = 'pendedkeyvalue'
+
     def __init__(self, key, value):
         self.key = key
         self.value = value
 
-    id = Int(primary=True)
-    key = Unicode()
-    value = Unicode()
-    pended_id = Int()
+    id = Column(Integer, primary_key=True)
+    key = Column(Unicode)
+    value = Column(Unicode)
+    pended_id = Column(Integer, ForeignKey('pended.id'))
 
 
 
@@ -64,16 +68,16 @@
 class Pended(Model):
     """A pended event, tied to a token."""
 
+    __tablename__ = 'pended'
+
     def __init__(self, token, expiration_date):
-        super(Pended, self).__init__()
         self.token = token
         self.expiration_date = expiration_date
 
-    id = Int(primary=True)
-    token = RawStr()
-    expiration_date = DateTime()
-    key_values = ReferenceSet(id, PendedKeyValue.pended_id)
-
+    id = Column(Integer, primary_key=True)
+    token = Column(LargeBinary) # TODO : was RawStr()
+    expiration_date = Column(DateTime)
+    key_values = relationship('PendedKeyValue')
 
 
 @implementer(IPendable)
@@ -105,7 +109,7 @@
             token = hashlib.sha1(repr(x)).hexdigest()
             # In practice, we'll never get a duplicate, but we'll be anal
             # about checking anyway.
-            if store.find(Pended, token=token).count() == 0:
+            if store.query(Pended).filter_by(token=token).count() == 0:
                 break
         else:
             raise AssertionError('Could not find a valid pendings token')
@@ -129,7 +133,7 @@
                 value = ('mailman.model.pending.unpack_list\1' +
                          '\2'.join(value))
             keyval = PendedKeyValue(key=key, value=value)
-            pending.key_values.add(keyval)
+            pending.key_values.append(keyval)
         store.add(pending)
         return token
 
@@ -137,7 +141,7 @@
     def confirm(self, store, token, expunge=True):
         # Token can come in as a unicode, but it's stored in the database as
         # bytes.  They must be ascii.
-        pendings = store.find(Pended, token=str(token))
+        pendings = store.query(Pended).filter_by(token=str(token))
         if pendings.count() == 0:
             return None
         assert pendings.count() == 1, (
@@ -146,7 +150,7 @@
         pendable = UnpendedPendable()
         # Find all PendedKeyValue entries that are associated with the pending
         # object's ID.  Watch out for type conversions.
-        for keyvalue in store.find(PendedKeyValue,
+        for keyvalue in store.query(PendedKeyValue).filter(
                                    PendedKeyValue.pended_id == pending.id):
             if keyvalue.value is not None and '\1' in keyvalue.value:
                 type_name, value = keyvalue.value.split('\1', 1)
@@ -154,23 +158,23 @@
             else:
                 pendable[keyvalue.key] = keyvalue.value
             if expunge:
-                store.remove(keyvalue)
+                store.delete(keyvalue)
         if expunge:
-            store.remove(pending)
+            store.delete(pending)
         return pendable
 
     @dbconnection
     def evict(self, store):
         right_now = now()
-        for pending in store.find(Pended):
+        for pending in store.query(Pended).all():
             if pending.expiration_date < right_now:
                 # Find all PendedKeyValue entries that are associated with the
                 # pending object's ID.
-                q = store.find(PendedKeyValue,
+                q = store.query(PendedKeyValue).filter(
                                PendedKeyValue.pended_id == pending.id)
                 for keyvalue in q:
-                    store.remove(keyvalue)
-                store.remove(pending)
+                    store.delete(keyvalue)
+                store.delete(pending)
 
 
 

=== modified file 'src/mailman/model/preferences.py'
--- src/mailman/model/preferences.py	2014-01-01 14:59:42 +0000
+++ src/mailman/model/preferences.py	2014-09-19 19:26:31 +0000
@@ -25,7 +25,7 @@
     ]
 
 
-from storm.locals import Bool, Int, Unicode
+from sqlalchemy import Column, Integer, Unicode,  Boolean
 from zope.component import getUtility
 from zope.interface import implementer
 
@@ -41,14 +41,16 @@
 class Preferences(Model):
     """See `IPreferences`."""
 
-    id = Int(primary=True)
-    acknowledge_posts = Bool()
-    hide_address = Bool()
-    _preferred_language = Unicode(name='preferred_language')
-    receive_list_copy = Bool()
-    receive_own_postings = Bool()
-    delivery_mode = Enum(DeliveryMode)
-    delivery_status = Enum(DeliveryStatus)
+    __tablename__ = 'preferences'
+
+    id = Column(Integer, primary_key=True)
+    acknowledge_posts = Column(Boolean)
+    hide_address = Column(Boolean)
+    _preferred_language = Column('preferred_language', Unicode)
+    receive_list_copy = Column(Boolean)
+    receive_own_postings = Column(Boolean)
+    delivery_mode = Column(Enum(enum=DeliveryMode))
+    delivery_status = Column(Enum(enum=DeliveryStatus))
 
     def __repr__(self):
         return '<Preferences object at {0:#x}>'.format(id(self))

=== modified file 'src/mailman/model/requests.py'
--- src/mailman/model/requests.py	2014-01-01 14:59:42 +0000
+++ src/mailman/model/requests.py	2014-09-19 19:26:31 +0000
@@ -26,7 +26,8 @@
 
 from cPickle import dumps, loads
 from datetime import timedelta
-from storm.locals import AutoReload, Int, RawStr, Reference, Unicode
+from sqlalchemy import Column, Unicode, Integer, ForeignKey, LargeBinary
+from sqlalchemy.orm import relationship
 from zope.component import getUtility
 from zope.interface import implementer
 
@@ -68,25 +69,23 @@
     @property
     @dbconnection
     def count(self, store):
-        return store.find(_Request, mailing_list=self.mailing_list).count()
+        return store.query(_Request).filter_by(mailing_list=self.mailing_list).count()
 
     @dbconnection
     def count_of(self, store, request_type):
-        return store.find(
-            _Request,
+        return store.query(_Request).filter_by(
             mailing_list=self.mailing_list, request_type=request_type).count()
 
     @property
     @dbconnection
     def held_requests(self, store):
-        results = store.find(_Request, mailing_list=self.mailing_list)
+        results = store.query(_Request).filter_by(mailing_list=self.mailing_list)
         for request in results:
             yield request
 
     @dbconnection
     def of_type(self, store, request_type):
-        results = store.find(
-            _Request,
+        results = store.query(_Request).filter_by(
             mailing_list=self.mailing_list, request_type=request_type)
         for request in results:
             yield request
@@ -104,11 +103,12 @@
             data_hash = token
         request = _Request(key, request_type, self.mailing_list, data_hash)
         store.add(request)
+        store.flush()
         return request.id
 
     @dbconnection
     def get_request(self, store, request_id, request_type=None):
-        result = store.get(_Request, request_id)
+        result = store.query(_Request).get(request_id)
         if result is None:
             return None
         if request_type is not None and result.request_type != request_type:
@@ -130,28 +130,29 @@
 
     @dbconnection
     def delete_request(self, store, request_id):
-        request = store.get(_Request, request_id)
+        request = store.query(_Request).get(request_id)
         if request is None:
             raise KeyError(request_id)
         # Throw away the pended data.
         getUtility(IPendings).confirm(request.data_hash)
-        store.remove(request)
+        store.delete(request)
 
 
 
 class _Request(Model):
     """Table for mailing list hold requests."""
 
-    id = Int(primary=True, default=AutoReload)
-    key = Unicode()
-    request_type = Enum(RequestType)
-    data_hash = RawStr()
-
-    mailing_list_id = Int()
-    mailing_list = Reference(mailing_list_id, 'MailingList.id')
+    __tablename__ = 'request'
+
+    id = Column(Integer, primary_key=True)# TODO: ???, default=AutoReload)
+    key = Column(Unicode)
+    request_type = Column(Enum(enum=RequestType))
+    data_hash = Column(LargeBinary)
+
+    mailing_list_id = Column(Integer, ForeignKey('mailinglist.id'))
+    mailing_list = relationship('MailingList')
 
     def __init__(self, key, request_type, mailing_list, data_hash):
-        super(_Request, self).__init__()
         self.key = key
         self.request_type = request_type
         self.mailing_list = mailing_list

=== modified file 'src/mailman/model/roster.py'
--- src/mailman/model/roster.py	2014-01-01 14:59:42 +0000
+++ src/mailman/model/roster.py	2014-09-19 19:26:31 +0000
@@ -37,7 +37,7 @@
     ]
 
 
-from storm.expr import And, Or
+from sqlalchemy import and_, or_
 from zope.interface import implementer
 
 from mailman.database.transaction import dbconnection
@@ -65,8 +65,7 @@
 
     @dbconnection
     def _query(self, store):
-        return store.find(
-            Member,
+        return store.query(Member).filter(
             Member.list_id == self._mlist.list_id,
             Member.role == self.role)
 
@@ -104,8 +103,7 @@
     @dbconnection
     def get_member(self, store, address):
         """See `IRoster`."""
-        results = store.find(
-            Member,
+        results = store.query(Member).filter(
             Member.list_id == self._mlist.list_id,
             Member.role == self.role,
             Address.email == address,
@@ -160,19 +158,17 @@
 
     @dbconnection
     def _query(self, store):
-        return store.find(
-            Member,
+        return store.query(Member).filter(
             Member.list_id == self._mlist.list_id,
-            Or(Member.role == MemberRole.owner,
+            or_(Member.role == MemberRole.owner,
                Member.role == MemberRole.moderator))
 
     @dbconnection
     def get_member(self, store, address):
         """See `IRoster`."""
-        results = store.find(
-                Member,
+        results = store.query(Member).filter(
                 Member.list_id == self._mlist.list_id,
-                Or(Member.role == MemberRole.moderator,
+                or_(Member.role == MemberRole.moderator,
                    Member.role == MemberRole.owner),
                 Address.email == address,
                 Member.address_id == Address.id)
@@ -206,10 +202,9 @@
         :return: A generator of members.
         :rtype: generator
         """
-        results = store.find(
-            Member,
-            And(Member.list_id == self._mlist.list_id,
-                Member.role == MemberRole.member))
+        results = store.query(Member).filter_by(
+            list_id = self._mlist.list_id,
+            role = MemberRole.member)
         for member in results:
             if member.delivery_mode in delivery_modes:
                 yield member
@@ -250,7 +245,7 @@
 
     @dbconnection
     def _query(self, store):
-        return store.find(Member, Member.list_id == self._mlist.list_id)
+        return store.query(Member).filter_by(list_id = self._mlist.list_id)
 
 
 
@@ -265,12 +260,11 @@
 
     @dbconnection
     def _query(self, store):
-        results = store.find(
-            Member,
-            Or(Member.user_id == self._user.id,
-               And(Address.user_id == self._user.id,
-                   Member.address_id == Address.id)))
-        return results.config(distinct=True)
+        results = store.query(Member).filter(
+            or_(Member.user_id == self._user.id,
+            and_(Address.user_id == self._user.id,
+                 Member.address_id == Address.id)))
+        return results.distinct()
 
     @property
     def member_count(self):
@@ -297,8 +291,7 @@
     @dbconnection
     def get_member(self, store, address):
         """See `IRoster`."""
-        results = store.find(
-            Member,
+        results = store.query(Member).filter(
             Member.address_id == Address.id,
             Address.user_id == self._user.id)
         if results.count() == 0:

=== modified file 'src/mailman/model/tests/test_listmanager.py'
--- src/mailman/model/tests/test_listmanager.py	2014-04-14 16:14:13 +0000
+++ src/mailman/model/tests/test_listmanager.py	2014-09-19 19:26:31 +0000
@@ -29,7 +29,7 @@
 
 import unittest
 
-from storm.locals import Store
+from sqlalchemy.orm import sessionmaker
 from zope.component import getUtility
 
 from mailman.app.lifecycle import create_list
@@ -139,9 +139,9 @@
         for name in filter_names:
             setattr(self._ant, name, ['test-filter-1', 'test-filter-2'])
         getUtility(IListManager).delete(self._ant)
-        store = Store.of(self._ant)
-        filters = store.find(ContentFilter,
-                             ContentFilter.mailing_list == self._ant)
+        Session = sessionmaker()
+        store = Session.object_session(self._ant)
+        filters = store.query(ContentFilter).filter_by(mailing_list = self._ant)
         self.assertEqual(filters.count(), 0)
 
 

=== modified file 'src/mailman/model/uid.py'
--- src/mailman/model/uid.py	2014-01-01 14:59:42 +0000
+++ src/mailman/model/uid.py	2014-09-19 19:26:31 +0000
@@ -25,10 +25,11 @@
     ]
 
 
-from storm.locals import Int
-from storm.properties import UUID
+
+from sqlalchemy import Column, Integer
 
 from mailman.database.model import Model
+from mailman.database.types import UUID
 from mailman.database.transaction import dbconnection
 
 
@@ -45,12 +46,14 @@
     There is no interface for this class, because it's purely an internal
     implementation detail.
     """
-    id = Int(primary=True)
-    uid = UUID()
+
+    __tablename__ = 'uid'
+
+    id = Column(Integer, primary_key=True)
+    uid = Column(UUID)
 
     @dbconnection
     def __init__(self, store, uid):
-        super(UID, self).__init__()
         self.uid = uid
         store.add(self)
 
@@ -70,7 +73,7 @@
         :type uid: unicode
         :raises ValueError: if the id is not unique.
         """
-        existing = store.find(UID, uid=uid)
+        existing = store.query(UID).filter_by(uid=uid)
         if existing.count() != 0:
             raise ValueError(uid)
         return UID(uid)

=== modified file 'src/mailman/model/user.py'
--- src/mailman/model/user.py	2014-01-01 14:59:42 +0000
+++ src/mailman/model/user.py	2014-09-19 19:26:31 +0000
@@ -24,14 +24,15 @@
     'User',
     ]
 
-from storm.locals import (
-    DateTime, Int, RawStr, Reference, ReferenceSet, Unicode)
-from storm.properties import UUID
+from sqlalchemy import (
+    Column, Unicode, Integer, DateTime, ForeignKey, LargeBinary)
+from sqlalchemy.orm import relationship, backref
 from zope.event import notify
 from zope.interface import implementer
 
 from mailman.database.model import Model
 from mailman.database.transaction import dbconnection
+from mailman.database.types import UUID
 from mailman.interfaces.address import (
     AddressAlreadyLinkedError, AddressNotLinkedError)
 from mailman.interfaces.user import (
@@ -51,24 +52,36 @@
 class User(Model):
     """Mailman users."""
 
-    id = Int(primary=True)
-    display_name = Unicode()
-    _password = RawStr(name='password')
-    _user_id = UUID()
-    _created_on = DateTime()
-
-    addresses = ReferenceSet(id, 'Address.user_id')
-    _preferred_address_id = Int()
-    _preferred_address = Reference(_preferred_address_id, 'Address.id')
-    preferences_id = Int()
-    preferences = Reference(preferences_id, 'Preferences.id')
+    __tablename__ = 'user'
+
+    id = Column(Integer, primary_key=True)
+    display_name = Column(Unicode)
+    _password = Column('password', LargeBinary) # TODO : was RawStr()
+    _user_id = Column(UUID)
+    _created_on = Column(DateTime)
+
+    addresses = relationship('Address',
+                             backref='user',
+                             primaryjoin=
+                                 id==Address.user_id)
+
+    _preferred_address_id = Column(Integer, ForeignKey('address.id',
+                                                       use_alter=True,
+                                                       name='_preferred_address'))
+    _preferred_address = relationship('Address',
+                                      primaryjoin=
+                                          _preferred_address_id==Address.id,
+                                      post_update=True)
+
+    preferences_id = Column(Integer, ForeignKey('preferences.id'))
+    preferences = relationship('Preferences',
+                               backref=backref('user', uselist=False))
 
     @dbconnection
     def __init__(self, store, display_name=None, preferences=None):
-        super(User, self).__init__()
         self._created_on = date_factory.now()
         user_id = uid_factory.new_uid()
-        assert store.find(User, _user_id=user_id).count() == 0, (
+        assert store.query(User).filter_by(_user_id=user_id).count() == 0, (
             'Duplicate user id {0}'.format(user_id))
         self._user_id = user_id
         self.display_name = ('' if display_name is None else display_name)
@@ -138,7 +151,7 @@
     @dbconnection
     def controls(self, store, email):
         """See `IUser`."""
-        found = store.find(Address, email=email)
+        found = store.query(Address).filter_by(email=email)
         if found.count() == 0:
             return False
         assert found.count() == 1, 'Unexpected count'
@@ -148,7 +161,7 @@
     def register(self, store, email, display_name=None):
         """See `IUser`."""
         # First, see if the address already exists
-        address = store.find(Address, email=email).one()
+        address = store.query(Address).filter_by(email=email).first()
         if address is None:
             if display_name is None:
                 display_name = ''

=== modified file 'src/mailman/model/usermanager.py'
--- src/mailman/model/usermanager.py	2014-01-01 14:59:42 +0000
+++ src/mailman/model/usermanager.py	2014-09-19 19:26:31 +0000
@@ -52,12 +52,12 @@
     @dbconnection
     def delete_user(self, store, user):
         """See `IUserManager`."""
-        store.remove(user)
+        store.delete(user)
 
     @dbconnection
     def get_user(self, store, email):
         """See `IUserManager`."""
-        addresses = store.find(Address, email=email.lower())
+        addresses = store.query(Address).filter_by(email=email.lower())
         if addresses.count() == 0:
             return None
         return addresses.one().user
@@ -65,7 +65,7 @@
     @dbconnection
     def get_user_by_id(self, store, user_id):
         """See `IUserManager`."""
-        users = store.find(User, _user_id=user_id)
+        users = store.query(User).filter_by(_user_id=user_id)
         if users.count() == 0:
             return None
         return users.one()
@@ -74,13 +74,13 @@
     @dbconnection
     def users(self, store):
         """See `IUserManager`."""
-        for user in store.find(User):
+        for user in store.query(User).all():
             yield user
 
     @dbconnection
     def create_address(self, store, email, display_name=None):
         """See `IUserManager`."""
-        addresses = store.find(Address, email=email.lower())
+        addresses = store.query(Address).filter(Address.email==email.lower())
         if addresses.count() == 1:
             found = addresses[0]
             raise ExistingAddressError(found.original_email)
@@ -101,12 +101,12 @@
         # unlinked before the address can be deleted.
         if address.user:
             address.user.unlink(address)
-        store.remove(address)
+        store.delete(address)
 
     @dbconnection
     def get_address(self, store, email):
         """See `IUserManager`."""
-        addresses = store.find(Address, email=email.lower())
+        addresses = store.query(Address).filter_by(email=email.lower())
         if addresses.count() == 0:
             return None
         return addresses.one()
@@ -115,12 +115,12 @@
     @dbconnection
     def addresses(self, store):
         """See `IUserManager`."""
-        for address in store.find(Address):
+        for address in store.query(Address).all():
             yield address
 
     @property
     @dbconnection
     def members(self, store):
         """See `IUserManager."""
-        for member in store.find(Member):
+        for member in store.query(Member).all():
                 yield member

=== modified file 'src/mailman/model/version.py'
--- src/mailman/model/version.py	2014-01-01 14:59:42 +0000
+++ src/mailman/model/version.py	2014-09-19 19:26:31 +0000
@@ -24,21 +24,24 @@
     'Version',
     ]
 
-from storm.locals import Int, Unicode
+from sqlalchemy import Column, Unicode, Integer
+
 from mailman.database.model import Model
 
 
 
 class Version(Model):
-    id = Int(primary=True)
-    component = Unicode()
-    version = Unicode()
+
+    __tablename__ = 'version'
+
+    id = Column(Integer, primary_key=True)
+    component = Column(Unicode)
+    version = Column(Unicode)
 
     # The testing machinery will generally reset all tables, however because
     # this table tracks schema migrations, we do not want to reset it.
     PRESERVE = True
 
     def __init__(self, component, version):
-        super(Version, self).__init__()
         self.component = component
         self.version = version

=== modified file 'src/mailman/utilities/importer.py'
--- src/mailman/utilities/importer.py	2014-04-14 16:14:13 +0000
+++ src/mailman/utilities/importer.py	2014-09-19 19:26:31 +0000
@@ -198,6 +198,14 @@
     send_welcome_msg='send_welcome_message',
     )
 
+# Datetime Fields that need a type conversion to python datetime
+# object for SQLite database.
+DATETIME_OBJECTS = [
+    'created_at',
+    'digest_last_sent_at',
+    'last_post_time',
+]
+
 EXCLUDES = set((
     'digest_members',
     'members',
@@ -217,6 +225,9 @@
         # Some attributes must not be directly imported.
         if key in EXCLUDES:
             continue
+        # Created at must not be set, it needs a type conversion
+        if key in DATETIME_OBJECTS:
+            continue
         # Some attributes from Mailman 2 were renamed in Mailman 3.
         key = NAME_MAPPINGS.get(key, key)
         # Handle the simple case where the key is an attribute of the
@@ -238,6 +249,15 @@
             except (TypeError, KeyError):
                 print('Type conversion error for key "{}": {}'.format(
                     key, value), file=sys.stderr)
+    for key in DATETIME_OBJECTS:
+        try:
+            value = datetime.datetime.utcfromtimestamp(config_dict[key])
+        except KeyError:
+            continue
+        if key == 'last_post_time':
+            setattr(mlist, 'last_post_at', value)
+            continue
+        setattr(mlist, key, value)
     # Handle the archiving policy.  In MM2.1 there were two boolean options
     # but only three of the four possible states were valid.  Now there's just
     # an enum.

_______________________________________________
Mailman-coders mailing list
Mailman-coders@python.org
https://mail.python.org/mailman/listinfo/mailman-coders

Reply via email to