commit aa8fb55e879e782268c663f81e73384673d56847 Author: Monsyne Dragon 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 "" % (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 "" % 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 "" % (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'])