commit aa8fb55e879e782268c663f81e73384673d56847
Author: Monsyne Dragon <mdragon@rackspace.com>
Date:   Thu Jun 26 01:55:26 2014 +0000

    Initial commit of DB schema.
    
    Initial commit of the event schema for the database.
    This includes models and alembic migration.

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a735f8b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,30 @@
+*.py[cod]
+
+# C extensions
+*.so
+
+# Packages
+*.egg
+*.egg-info
+dist
+build
+eggs
+parts
+var
+sdist
+develop-eggs
+.installed.cfg
+lib
+lib64
+__pycache__
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+nosetests.xml
+
+# Translations
+*.mo
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e06d208
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..8afbefe
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,2 @@
+include README.md
+include requirements.txt
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7df48b2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+winchester
+==========
+
+An OpenStack notification event processing library based on persistant streams.
+
diff --git a/alembic.ini b/alembic.ini
new file mode 100644
index 0000000..848a387
--- /dev/null
+++ b/alembic.ini
@@ -0,0 +1,50 @@
+# A generic, single database configuration.
+
+[alembic]
+# path to migration scripts
+script_location = alembic
+
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+sqlalchemy.url = driver://user:pass@localhost/dbname
+
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/alembic/env.py b/alembic/env.py
new file mode 100644
index 0000000..e82b51f
--- /dev/null
+++ b/alembic/env.py
@@ -0,0 +1,72 @@
+from __future__ import with_statement
+from alembic import context
+from sqlalchemy import engine_from_config, pool
+from logging.config import fileConfig
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+from winchester.models import Base
+target_metadata = Base.metadata
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+def run_migrations_offline():
+    """Run migrations in 'offline' mode.
+
+    This configures the context with just a URL
+    and not an Engine, though an Engine is acceptable
+    here as well.  By skipping the Engine creation
+    we don't even need a DBAPI to be available.
+
+    Calls to context.execute() here emit the given string to the
+    script output.
+
+    """
+    url = config.get_main_option("sqlalchemy.url")
+    context.configure(url=url)
+
+    with context.begin_transaction():
+        context.run_migrations()
+
+def run_migrations_online():
+    """Run migrations in 'online' mode.
+
+    In this scenario we need to create an Engine
+    and associate a connection with the context.
+
+    """
+    engine = engine_from_config(
+                config.get_section(config.config_ini_section),
+                prefix='sqlalchemy.',
+                poolclass=pool.NullPool)
+
+    connection = engine.connect()
+    context.configure(
+                connection=connection,
+                target_metadata=target_metadata
+                )
+
+    try:
+        with context.begin_transaction():
+            context.run_migrations()
+    finally:
+        connection.close()
+
+if context.is_offline_mode():
+    run_migrations_offline()
+else:
+    run_migrations_online()
+
diff --git a/alembic/script.py.mako b/alembic/script.py.mako
new file mode 100644
index 0000000..9570201
--- /dev/null
+++ b/alembic/script.py.mako
@@ -0,0 +1,22 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision}
+Create Date: ${create_date}
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+def upgrade():
+    ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+    ${downgrades if downgrades else "pass"}
diff --git a/alembic/versions/3ab6d7bf80cd_.py b/alembic/versions/3ab6d7bf80cd_.py
new file mode 100644
index 0000000..7571065
--- /dev/null
+++ b/alembic/versions/3ab6d7bf80cd_.py
@@ -0,0 +1,53 @@
+"""Initial Event schema.
+
+Revision ID: 3ab6d7bf80cd
+Revises: None
+Create Date: 2014-06-26 01:36:42.792353
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '3ab6d7bf80cd'
+down_revision = None
+
+from alembic import op
+import sqlalchemy as sa
+from winchester import models
+
+def upgrade():
+    ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('event_type',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('desc', sa.String(length=255), nullable=True),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('desc')
+    )
+    op.create_table('event',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('message_id', sa.String(length=50), nullable=True),
+    sa.Column('generated', models.PreciseTimestamp(), nullable=True),
+    sa.Column('event_type_id', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['event_type_id'], ['event_type.id'], ),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('message_id')
+    )
+    op.create_table('trait',
+    sa.Column('event_id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(length=100), nullable=False),
+    sa.Column('type', sa.Integer(), nullable=True),
+    sa.Column('t_string', sa.String(length=255), nullable=True),
+    sa.Column('t_float', sa.Float(), nullable=True),
+    sa.Column('t_int', sa.Integer(), nullable=True),
+    sa.Column('t_datetime', models.PreciseTimestamp(), nullable=True),
+    sa.ForeignKeyConstraint(['event_id'], ['event.id'], ),
+    sa.PrimaryKeyConstraint('event_id', 'name')
+    )
+    ### end Alembic commands ###
+
+
+def downgrade():
+    ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table('trait')
+    op.drop_table('event')
+    op.drop_table('event_type')
+    ### end Alembic commands ###
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..2bbf435
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+alembic>=0.4.1
+enum34>=1.0
+SQLAlchemy>=0.9.6
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..8c28267
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,3 @@
+[metadata]
+description-file = README.md
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..bb40033
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,36 @@
+import os
+from pip.req import parse_requirements
+from setuptools import setup, find_packages
+
+
+def read(fname):
+    return open(os.path.join(os.path.dirname(__file__), fname)).read()
+
+
+req_file = os.path.join(os.path.dirname(__file__), "requirements.txt")
+install_reqs = [str(r.req) for r in parse_requirements(req_file)]
+
+
+setup(
+    name='winchester',
+    version='0.10',
+    author='Monsyne Dragon',
+    author_email='mdragon@rackspace.com',
+    description=("An OpenStack notification event processing library."),
+    license='Apache License (2.0)',
+    keywords='OpenStack notifications events processing triggers',
+    packages=find_packages(exclude=['tests']),
+    classifiers=[
+        'Development Status :: 3 - Alpha',
+        'License :: OSI Approved :: Apache Software License',
+        'Operating System :: POSIX :: Linux',
+        'Programming Language :: Python :: 2.6',
+        'Programming Language :: Python :: 2.7',
+    ],
+    url='https://github.com/StackTach/winchester',
+    scripts=[],
+    long_description=read('README.md'),
+    install_requires=install_reqs,
+
+    zip_safe=False
+)
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 0000000..02e37d7
--- /dev/null
+++ b/test-requirements.txt
@@ -0,0 +1,3 @@
+mock>=1.0
+nose
+unittest2
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..9afa955
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,15 @@
+[tox]
+envlist = py26,py27
+
+[testenv]
+deps = 
+    -r{toxinidir}/requirements.txt
+    -r{toxinidir}/test-requirements.txt
+
+setenv = VIRTUAL_ENV={envdir}
+
+commands =
+   nosetests tests 
+
+sitepackages = False
+
diff --git a/winchester/__init__.py b/winchester/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/winchester/db.py b/winchester/db.py
new file mode 100644
index 0000000..fe1d7fa
--- /dev/null
+++ b/winchester/db.py
@@ -0,0 +1,70 @@
+import sqlalchemy
+from sqlalchemy.orm import sessionmaker
+
+from winchester import models
+
+
+ENGINES = dict()
+SESSIONMAKERS = dict()
+
+
+def sessioned(func):
+    def with_session(self, *args, **kw):
+        if 'session' in kw:
+            return func(self, *args, **kw)
+        else:
+            try:
+                session = self.get_session()
+                kw['session'] = session
+                retval = func(self, *args, **kw)
+                session.commit()
+                return retval
+            except:
+                session.rollback()
+                raise
+            finally:
+                session.close()
+    return with_session
+
+
+class DBInterface(object):
+
+    def __init__(self, config):
+        self.config = config
+        self.db_url = config['url']
+
+    @property
+    def engine(self):
+        global ENGINES
+        if self.db_url not in ENGINES:
+            engine = sqlalchemy.create_engine(self.db_url)
+            ENGINES[self.db_url] = engine
+        return ENGINES[self.db_url]
+
+    @property
+    def sessionmaker(self):
+        global SESSIONMAKERS
+        if self.db_url not in SESSIONMAKERS:
+            maker = sessionmaker(bind=self.engine)
+            SESSIONMAKERS[self.db_url] = maker
+        return SESSIONMAKERS[self.db_url]
+
+    def get_session(self):
+        return self.sessionmaker(expire_on_commit=False)
+
+    @sessioned
+    def get_event_type(self, description, session=None):
+        t = session.query(models.EventType).filter(models.EventType.desc == description).first()
+        if t is None:
+            t = models.EventType(description)
+            session.add(t)
+        return t
+
+    @sessioned
+    def create_event(self, message_id, event_type, generated, traits, session=None):
+        event_type = self.get_event_type(event_type, session=session)
+        e = models.Event(message_id, event_type, generated)
+        for name in traits:
+            e[name] = traits[name]
+        session.add(e)
+        return e
diff --git a/winchester/models.py b/winchester/models.py
new file mode 100644
index 0000000..d5b6b3f
--- /dev/null
+++ b/winchester/models.py
@@ -0,0 +1,237 @@
+from datetime import datetime
+
+from enum import IntEnum
+
+from sqlalchemy import event
+from sqlalchemy import literal_column
+from sqlalchemy import Column, Table, ForeignKey, Index, UniqueConstraint
+from sqlalchemy import Float, Boolean, Text, DateTime, Integer, String
+from sqlalchemy import cast, null, case
+from sqlalchemy.orm.interfaces import PropComparator
+from sqlalchemy.ext.hybrid import hybrid_property
+from sqlalchemy.dialects.mysql import DECIMAL
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.ext.associationproxy import association_proxy
+from sqlalchemy.orm import backref
+from sqlalchemy.orm import relationship
+from sqlalchemy.orm.collections import attribute_mapped_collection
+from sqlalchemy.types import TypeDecorator, DATETIME
+
+
+class Datatype(IntEnum):
+    none = 0
+    string = 1
+    int = 2
+    float = 3
+    datetime = 4
+
+
+class PreciseTimestamp(TypeDecorator):
+    """Represents a timestamp precise to the microsecond."""
+
+    impl = DATETIME
+
+    def load_dialect_impl(self, dialect):
+        if dialect.name == 'mysql':
+            return dialect.type_descriptor(DECIMAL(precision=20,
+                                                   scale=6,
+                                                   asdecimal=True))
+        return dialect.type_descriptor(DATETIME())
+
+    def process_bind_param(self, value, dialect):
+        if value is None:
+            return value
+        elif dialect.name == 'mysql':
+            return utils.dt_to_decimal(value)
+        return value
+
+    def process_result_value(self, value, dialect):
+        if value is None:
+            return value
+        elif dialect.name == 'mysql':
+            return utils.decimal_to_dt(value)
+        return value
+
+
+class ProxiedDictMixin(object):
+    """Adds obj[name] access to a mapped class.
+
+    This class basically proxies dictionary access to an attribute
+    called ``_proxied``.  The class which inherits this class
+    should have an attribute called ``_proxied`` which points to a dictionary.
+
+    """
+
+    def __len__(self):
+        return len(self._proxied)
+
+    def __iter__(self):
+        return iter(self._proxied)
+
+    def __getitem__(self, name):
+        return self._proxied[name]
+
+    def __contains__(self, name):
+        return name in self._proxied
+
+    def __setitem__(self, name, value):
+        self._proxied[name] = value
+
+    def __delitem__(self, name):
+        del self._proxied[name]
+
+
+class PolymorphicVerticalProperty(object):
+    """A name/value pair with polymorphic value storage."""
+
+    def __init__(self, name, value=None):
+        self.name = name
+        self.value = value
+
+    @hybrid_property
+    def value(self):
+        fieldname, discriminator = self.type_map[self.type]
+        if fieldname is None:
+            return None
+        else:
+            return getattr(self, fieldname)
+
+    @value.setter
+    def value(self, value):
+        py_type = type(value)
+        fieldname, discriminator = self.type_map[py_type]
+
+        self.type = discriminator
+        if fieldname is not None:
+            setattr(self, fieldname, value)
+
+    @value.deleter
+    def value(self):
+        self._set_value(None)
+
+    @value.comparator
+    class value(PropComparator):
+        """A comparator for .value, builds a polymorphic comparison via CASE.
+
+        """
+        def __init__(self, cls):
+            self.cls = cls
+
+        def _case(self):
+            pairs = set(self.cls.type_map.values())
+            whens = [
+                (
+                    literal_column("'%s'" % discriminator),
+                    cast(getattr(self.cls, attribute), String)
+                ) for attribute, discriminator in pairs
+                if attribute is not None
+            ]
+            return case(whens, self.cls.type, null())
+        def __eq__(self, other):
+            return self._case() == cast(other, String)
+        def __ne__(self, other):
+            return self._case() != cast(other, String)
+
+    def __repr__(self):
+        return '<%s %r=%r>' % (self.__class__.__name__, self.name, self.value)
+
+
+@event.listens_for(PolymorphicVerticalProperty, "mapper_configured", propagate=True)
+def on_new_class(mapper, cls_):
+    """Add type lookup info for polymorphic value columns.
+    """
+
+    info_dict = {}
+    info_dict[type(None)] = (None, Datatype.none)
+    info_dict[Datatype.none] = (None, Datatype.none)
+
+    for k in mapper.c.keys():
+        col = mapper.c[k]
+        if 'type' in col.info:
+            python_type, discriminator = col.info['type']
+            info_dict[python_type] = (k, discriminator)
+            info_dict[discriminator] = (k, discriminator)
+    cls_.type_map = info_dict
+
+
+Base = declarative_base()
+
+
+class Trait(PolymorphicVerticalProperty, Base):
+    __tablename__ = 'trait'
+    __table_args__ = (
+        Index('ix_trait_t_int', 't_int'),
+        Index('ix_trait_t_string', 't_string'),
+        Index('ix_trait_t_datetime', 't_datetime'),
+        Index('ix_trait_t_float', 't_float'),
+    )
+    event_id = Column(Integer, ForeignKey('event.id'), primary_key=True)
+    name = Column(String(100), primary_key=True)
+    type = Column(Integer)
+
+
+    t_string = Column(String(255), info=dict(type=(str, Datatype.string)),
+                      nullable=True, default=None)
+    t_float = Column(Float, info=dict(type=(float, Datatype.float)),
+                      nullable=True, default=None)
+    t_int = Column(Integer, info=dict(type=(int, Datatype.int)),
+                      nullable=True, default=None)
+    t_datetime = Column(PreciseTimestamp(), info=dict(type=(datetime, Datatype.datetime)),
+                      nullable=True, default=None)
+
+    def __repr__(self):
+        return "<Trait(%s) %s=%s/%s/%s/%s on %s>" % (self.name,
+                                                     self.type,
+                                                     self.t_string,
+                                                     self.t_float,
+                                                     self.t_int,
+                                                     self.t_datetime,
+                                                     self.event_id)
+
+
+class EventType(Base):
+    """Types of event records."""
+    __tablename__ = 'event_type'
+
+    id = Column(Integer, primary_key=True)
+    desc = Column(String(255), unique=True)
+
+    def __init__(self, event_type):
+        self.desc = event_type
+
+    def __repr__(self):
+        return "<EventType: %s>" % self.desc
+
+
+class Event(ProxiedDictMixin, Base):
+    __tablename__ = 'event'
+    __table_args__ = (
+        Index('ix_event_message_id', 'message_id'),
+        Index('ix_event_type_id', 'event_type_id'),
+        Index('ix_event_generated', 'generated')
+    )
+    id = Column(Integer, primary_key=True)
+    message_id = Column(String(50), unique=True)
+    generated = Column(PreciseTimestamp())
+
+    event_type_id = Column(Integer, ForeignKey('event_type.id'))
+    event_type = relationship("EventType", backref=backref('event_type'))
+
+    traits = relationship("Trait",
+                    collection_class=attribute_mapped_collection('name'))
+    _proxied = association_proxy("traits", "value",
+                            creator=lambda name, value: Trait(name=name, value=value))
+
+    def __init__(self, message_id, event_type, generated):
+
+        self.message_id = message_id
+        self.event_type = event_type
+        self.generated = generated
+
+    def __repr__(self):
+        return "<Event %s ('Event: %s %s, Generated: %s')>" % (self.id,
+                                                              self.message_id,
+                                                              self.event_type,
+                                                              self.generated)
+
+
diff --git a/winchester/trigger_manager.py b/winchester/trigger_manager.py
new file mode 100644
index 0000000..ebdbe16
--- /dev/null
+++ b/winchester/trigger_manager.py
@@ -0,0 +1,8 @@
+from winchester.db import DBInterface
+
+
+class TriggerManager(object):
+
+    def __init__(self, config):
+        self.config = config
+        self.db = DBInterface(config['database'])