Added some EAV helpers

This commit is contained in:
Konsta Vesterinen
2013-11-13 14:33:51 +02:00
parent 0233c6e0bf
commit 4b791686a9
4 changed files with 158 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
from .aggregates import aggregated from .aggregates import aggregated
from .decorators import generates from .decorators import generates
from .eav import MetaValue, MetaType
from .exceptions import ImproperlyConfigured from .exceptions import ImproperlyConfigured
from .functions import ( from .functions import (
batch_fetch, batch_fetch,
@@ -86,6 +87,8 @@ __all__ = (
IPAddressType, IPAddressType,
LocaleType, LocaleType,
Merger, Merger,
MetaType,
MetaValue,
NumberRange, NumberRange,
NumberRangeException, NumberRangeException,
NumberRangeRawType, NumberRangeRawType,

100
sqlalchemy_utils/eav.py Normal file
View File

@@ -0,0 +1,100 @@
from inspect import isclass
import sqlalchemy as sa
from sqlalchemy.ext.hybrid import hybrid_property
import six
class MetaType(sa.types.TypeDecorator):
impl = sa.types.UnicodeText
def __init__(self, data_types):
sa.types.TypeDecorator.__init__(self)
self.data_types = data_types
def type_key(self, data_type):
for key, type_ in six.iteritems(self.data_types):
if (
isinstance(data_type, type_) or
(isclass(data_type) and issubclass(data_type, type_))
):
return six.text_type(key)
def process_bind_param(self, value, dialect):
if value is None:
return
return self.type_key(value)
def process_result_value(self, value, dialect):
if value is not None:
return self.data_types[value]
class MetaValue(object):
def __init__(self, attr, type_column):
self.attr = attr
self.type_column = type_column
def coalesce(*args):
for arg in args:
if arg is not None:
return arg
return None
@sa.event.listens_for(sa.orm.mapper, 'mapper_configured')
def instrument_meta_values(mapper, class_):
operations = []
for key, attr_value in six.iteritems(class_.__dict__):
if isinstance(attr_value, MetaValue):
attr = attr_value.attr
type_column = attr_value.type_column
parent_class = getattr(class_, attr).mapper.class_
type_prop = getattr(parent_class, type_column).property
type_ = type_prop.columns[0].type
generated_keys = []
for type_key, data_type in six.iteritems(type_.data_types):
generated_key = key + '_' + type_key
operations.append((
class_,
generated_key,
sa.Column(generated_key, data_type)
))
generated_keys.append(generated_key)
def getter(self):
return coalesce(
*map(lambda key: getattr(self, key), generated_keys)
)
def setter(self, value):
setattr(
self,
key + '_' +
type_.type_key(
getattr(
getattr(self, attr),
type_column
)
),
value
)
def expression(self):
return sa.func.coalesce(
*map(lambda key: getattr(self, key), generated_keys)
)
operations.append((
class_,
key,
hybrid_property(
getter,
setter,
expr=expression
)
))
for operation in operations:
setattr(*operation)

View File

@@ -37,6 +37,7 @@ class TestCase(object):
Session = sessionmaker(bind=self.connection) Session = sessionmaker(bind=self.connection)
self.session = Session() self.session = Session()
sa.orm.configure_mappers()
def teardown_method(self, method): def teardown_method(self, method):
aggregates.manager.reset() aggregates.manager.reset()

54
tests/test_eav.py Normal file
View File

@@ -0,0 +1,54 @@
import sqlalchemy as sa
from sqlalchemy_utils import MetaType, MetaValue
from tests import TestCase
class TestMetaModel(TestCase):
def create_models(self):
class Question(self.Base):
__tablename__ = 'question'
id = sa.Column(sa.Integer, primary_key=True)
data_type = sa.Column(
MetaType({
'str': sa.String,
'unicode': sa.UnicodeText,
'int': sa.Integer,
'datetime': sa.DateTime
})
)
class Answer(self.Base):
__tablename__ = 'answer'
id = sa.Column(sa.Integer, primary_key=True)
value = MetaValue('question', 'data_type')
question_id = sa.Column(sa.Integer, sa.ForeignKey(Question.id))
question = sa.orm.relationship(Question)
self.Question = Question
self.Answer = Answer
def test_meta_type_conversion(self):
question = self.Question(data_type=sa.String(200))
self.session.add(question)
self.session.commit()
self.session.refresh(question)
assert question.data_type.__name__ == 'String'
def test_auto_generates_meta_value_columns(self):
assert hasattr(self.Answer, 'value_str')
assert hasattr(self.Answer, 'value_int')
assert hasattr(self.Answer, 'value_datetime')
def test_meta_value_setting(self):
question = self.Question(data_type=sa.String)
answer = self.Answer(question=question)
answer.value = 'some answer'
assert answer.value == answer.value_str
def test_meta_value_as_expression(self):
assert str(self.Answer.value) == (
'coalesce(answer.value_int, answer.value_unicode'
', answer.value_str, answer.value_datetime)'
)