
Full trigger logic now works. Added pipeline workers, and test handler. Added example configs Lots of unittests.
418 lines
14 KiB
Python
418 lines
14 KiB
Python
from datetime import datetime
|
|
from decimal import Decimal
|
|
import calendar
|
|
from enum import IntEnum
|
|
|
|
import timex
|
|
|
|
from sqlalchemy import event
|
|
from sqlalchemy import and_, or_
|
|
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 composite
|
|
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
|
|
timerange = 5
|
|
|
|
|
|
class StreamState(IntEnum):
|
|
active = 1
|
|
firing = 2
|
|
expiring = 3
|
|
error = 4
|
|
expire_error = 5
|
|
completed = 6
|
|
|
|
|
|
class DBException(Exception):
|
|
pass
|
|
|
|
|
|
class InvalidTraitType(DBException):
|
|
pass
|
|
|
|
|
|
def dt_to_decimal(dt):
|
|
t_sec = calendar.timegm(dt.utctimetuple()) + (dt.microsecond/1e6)
|
|
return Decimal("%.6f" % t_sec)
|
|
|
|
|
|
def decimal_to_dt(decimal_timestamp):
|
|
return datetime.utcfromtimestamp(float(decimal_timestamp))
|
|
|
|
|
|
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 dt_to_decimal(value)
|
|
return value
|
|
|
|
def process_result_value(self, value, dialect):
|
|
if value is None:
|
|
return value
|
|
elif dialect.name == 'mysql':
|
|
return decimal_to_dt(value)
|
|
return value
|
|
|
|
|
|
class DBTimeRange(object):
|
|
def __init__(self, begin, end):
|
|
self.begin = begin
|
|
self.end = end
|
|
|
|
def __composite_values__(self):
|
|
return self.begin, self.end
|
|
|
|
def __repr__(self):
|
|
return "DBTimeRange(begin=%r, end=%r)" % (self.begin, self.end)
|
|
|
|
def __eq__(self, other):
|
|
return isinstance(other, DBTimeRange) and \
|
|
other.begin == self.begin and \
|
|
other.end == self.end
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
|
|
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."""
|
|
|
|
ATTRIBUTE_MAP = {Datatype.none: None}
|
|
PY_TYPE_MAP = {unicode: Datatype.string,
|
|
int: Datatype.int,
|
|
float: Datatype.float,
|
|
datetime: Datatype.datetime,
|
|
DBTimeRange: Datatype.timerange}
|
|
|
|
def __init__(self, name, value=None):
|
|
self.name = name
|
|
self.value = value
|
|
|
|
@classmethod
|
|
def get_type_value(cls, value):
|
|
if value is None:
|
|
return Datatype.none, None
|
|
if isinstance(value, str):
|
|
value = value.decode('utf8', 'ignore')
|
|
if isinstance(value, timex.Timestamp):
|
|
value = value.timestamp
|
|
if isinstance(value, timex.TimeRange):
|
|
value = DBTimeRange(value.begin, value.end)
|
|
if type(value) in cls.PY_TYPE_MAP:
|
|
return cls.PY_TYPE_MAP[type(value)], value
|
|
return None, value
|
|
|
|
@hybrid_property
|
|
def value(self):
|
|
if self.type not in self.ATTRIBUTE_MAP:
|
|
raise InvalidTraitType("Invalid trait type in db for %s: %s" % (self.name, self.type))
|
|
attribute = self.ATTRIBUTE_MAP[self.type]
|
|
if attribute is None:
|
|
return None
|
|
if self.type == Datatype.timerange:
|
|
val = getattr(self, attribute)
|
|
return timex.TimeRange(val.begin, val.end)
|
|
else:
|
|
return getattr(self, attribute)
|
|
|
|
@value.setter
|
|
def value(self, value):
|
|
datatype, value = self.get_type_value(value)
|
|
if datatype not in self.ATTRIBUTE_MAP:
|
|
raise InvalidTraitType("Invalid trait type for %s: %s" % (self.name, datatype))
|
|
attribute = self.ATTRIBUTE_MAP[datatype]
|
|
self.type = int(datatype)
|
|
if attribute is not None:
|
|
setattr(self, attribute, value)
|
|
|
|
@value.deleter
|
|
def value(self):
|
|
self._set_value(None)
|
|
|
|
@value.comparator
|
|
class value(PropComparator):
|
|
"""A comparator for .value, builds a polymorphic comparison.
|
|
|
|
"""
|
|
def __init__(self, cls):
|
|
self.cls = cls
|
|
|
|
def __eq__(self, other):
|
|
dtype, value = self.cls.get_type_value(other)
|
|
if dtype is None:
|
|
dtype = Datatype.string
|
|
if dtype == Datatype.none:
|
|
return self.cls.type == int(Datatype.none)
|
|
attr = getattr(self.cls, self.cls.ATTRIBUTE_MAP[dtype])
|
|
return and_(attr == value, self.cls.type == int(dtype))
|
|
|
|
def __ne__(self, other):
|
|
dtype, value = self.cls.get_type_value(other)
|
|
if dtype is None:
|
|
dtype = Datatype.string
|
|
if dtype == Datatype.none:
|
|
return self.cls.type != int(Datatype.none)
|
|
attr = getattr(self.cls, self.cls.ATTRIBUTE_MAP[dtype])
|
|
return and_(attr != value, self.cls.type == int(dtype))
|
|
|
|
def __repr__(self):
|
|
return '<%s %r=%r>' % (self.__class__.__name__, self.name, self.value)
|
|
|
|
|
|
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)
|
|
|
|
ATTRIBUTE_MAP = {Datatype.none: None,
|
|
Datatype.string: 't_string',
|
|
Datatype.int: 't_int',
|
|
Datatype.float: 't_float',
|
|
Datatype.datetime: 't_datetime',}
|
|
|
|
t_string = Column(String(255), nullable=True, default=None)
|
|
t_float = Column(Float, nullable=True, default=None)
|
|
t_int = Column(Integer, nullable=True, default=None)
|
|
t_datetime = Column(PreciseTimestamp(),
|
|
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))
|
|
|
|
@property
|
|
def event_type_string(self):
|
|
return self.event_type.desc
|
|
|
|
@property
|
|
def as_dict(self):
|
|
d = dict(self._proxied)
|
|
d['message_id'] = self.message_id
|
|
d['event_type'] = self.event_type_string
|
|
d['timestamp'] = self.generated
|
|
return d
|
|
|
|
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)
|
|
|
|
|
|
stream_event_table = Table('streamevent', Base.metadata,
|
|
Column('stream_id', Integer, ForeignKey('stream.id'), primary_key=True),
|
|
Column('event_id', Integer,
|
|
ForeignKey('event.id'),
|
|
primary_key=True)
|
|
)
|
|
|
|
|
|
class Stream(ProxiedDictMixin, Base):
|
|
__tablename__ = 'stream'
|
|
|
|
__table_args__ = (
|
|
Index('ix_stream_name', 'name'),
|
|
Index('ix_stream_state', 'state'),
|
|
Index('ix_stream_expire_timestamp', 'expire_timestamp'),
|
|
Index('ix_stream_fire_timestamp', 'fire_timestamp')
|
|
)
|
|
id = Column(Integer, primary_key=True)
|
|
first_event = Column(PreciseTimestamp(), nullable=False)
|
|
last_event = Column(PreciseTimestamp(), nullable=False)
|
|
expire_timestamp = Column(PreciseTimestamp())
|
|
fire_timestamp = Column(PreciseTimestamp())
|
|
name = Column(String(255), nullable=False)
|
|
state = Column(Integer, default=StreamState.active, nullable=False)
|
|
state_serial_no = Column(Integer, default=0, nullable=False)
|
|
|
|
distinguished_by = relationship("DistinguishingTrait",
|
|
collection_class=attribute_mapped_collection('name'))
|
|
_proxied = association_proxy("distinguished_by", "value",
|
|
creator=lambda name, value: DistinguishingTrait(name=name, value=value))
|
|
|
|
events = relationship(Event, secondary=stream_event_table,
|
|
order_by=Event.generated)
|
|
|
|
@property
|
|
def distinguished_by_dict(self):
|
|
return dict(self._proxied)
|
|
|
|
def __init__(self, name, first_event, last_event=None, expire_timestamp=None,
|
|
fire_timestamp=None, state=None, state_serial_no=None):
|
|
self.name = name
|
|
self.first_event = first_event
|
|
if last_event is None:
|
|
last_event = first_event
|
|
self.last_event = last_event
|
|
self.expire_timestamp = expire_timestamp
|
|
self.fire_timestamp = fire_timestamp
|
|
if state is None:
|
|
state = StreamState.active
|
|
self.state = state
|
|
if state_serial_no is None:
|
|
state_serial_no = 0
|
|
self.state_serial_no = state_serial_no
|
|
|
|
|
|
class DistinguishingTrait(PolymorphicVerticalProperty, Base):
|
|
__tablename__ = 'dist_trait'
|
|
__table_args__ = (
|
|
Index('ix_dist_trait_dt_int', 'dt_int'),
|
|
Index('ix_dist_trait_dt_float', 'dt_float'),
|
|
Index('ix_dist_trait_dt_string', 'dt_string'),
|
|
Index('ix_dist_trait_dt_datetime', 'dt_datetime'),
|
|
Index('ix_dist_trait_dt_timerange_begin', 'dt_timerange_begin'),
|
|
Index('ix_dist_trait_dt_timerange_end', 'dt_timerange_end'),
|
|
)
|
|
stream_id = Column(Integer, ForeignKey('stream.id'), primary_key=True)
|
|
name = Column(String(100), primary_key=True)
|
|
type = Column(Integer)
|
|
|
|
|
|
ATTRIBUTE_MAP = {Datatype.none: None,
|
|
Datatype.string: 'dt_string',
|
|
Datatype.int: 'dt_int',
|
|
Datatype.float: 'dt_float',
|
|
Datatype.datetime: 'dt_datetime',
|
|
Datatype.timerange:'dt_timerange',
|
|
}
|
|
|
|
dt_string = Column(String(255), nullable=True, default=None)
|
|
dt_float = Column(Float, nullable=True, default=None)
|
|
dt_int = Column(Integer, nullable=True, default=None)
|
|
dt_datetime = Column(PreciseTimestamp(),
|
|
nullable=True, default=None)
|
|
dt_timerange_begin = Column(PreciseTimestamp(), nullable=True, default=None)
|
|
dt_timerange_end = Column(PreciseTimestamp(), nullable=True, default=None)
|
|
|
|
dt_timerange = composite(DBTimeRange, dt_timerange_begin, dt_timerange_end)
|
|
|
|
@property
|
|
def as_dict(self):
|
|
return {self.name: self.value}
|
|
|
|
def __repr__(self):
|
|
return "<DistinguishingTrait(%s) %s=%s/%s/%s/%s/(%s to %s) on %s>" % (self.name,
|
|
self.type,
|
|
self.dt_string,
|
|
self.dt_float,
|
|
self.dt_int,
|
|
self.dt_datetime,
|
|
self.dt_timerange_begin,
|
|
self.dt_timerange_end,
|
|
self.stream_id)
|