Add range type step support

This commit is contained in:
Konsta Vesterinen
2014-03-04 16:09:23 +02:00
parent d2e01b354d
commit 4e54f1a05c
2 changed files with 116 additions and 10 deletions

View File

@@ -1,13 +1,15 @@
""" """
SQLAlchemy-Utils provides wide variety of range data types. All range data types return SQLAlchemy-Utils provides wide variety of range data types. All range data
Interval objects of intervals_ package. In order to use range data types you need to install intervals_ with: types return Interval objects of intervals_ package. In order to use range data
types you need to install intervals_ with:
:: ::
pip install intervals pip install intervals
Intervals package provides good chunk of additional interval operators that for example psycopg2 range objects do not support. Intervals package provides good chunk of additional interval operators that for
example psycopg2 range objects do not support.
@@ -16,10 +18,51 @@ Some good reading for practical interval implementations:
http://wiki.postgresql.org/images/f/f0/Range-types.pdf http://wiki.postgresql.org/images/f/f0/Range-types.pdf
Range type initialization
-------------------------
::
from sqlalchemy_utils import IntRangeType
class Event(Base):
__tablename__ = 'user'
id = sa.Column(sa.Integer, autoincrement=True)
name = sa.Column(sa.Unicode(255))
estimated_number_of_persons = sa.Column(IntRangeType)
You can also set a step parameter for range type. The values that are not
multipliers of given step will be rounded up to nearest step multiplier.
::
from sqlalchemy_utils import IntRangeType
class Event(Base):
__tablename__ = 'user'
id = sa.Column(sa.Integer, autoincrement=True)
name = sa.Column(sa.Unicode(255))
estimated_number_of_persons = sa.Column(IntRangeType(step=1000))
event = Event(estimated_number_of_persons=[100, 1200])
event.estimated_number_of_persons.lower # 0
event.estimated_number_of_persons.upper # 1000
Range type operators Range type operators
-------------------- --------------------
SQLAlchemy-Utils supports many range type operators. These operators follow the `intervals` package interval coercion rules. SQLAlchemy-Utils supports many range type operators. These operators follow the
`intervals` package interval coercion rules.
So for example when we make a query such as: So for example when we make a query such as:
@@ -50,6 +93,13 @@ All range types support all comparison operators (>, >=, ==, !=, <=, <).
Car.price_range > (300, 500) Car.price_range > (300, 500)
# Whether or not range is strictly left of another range
Car.price_range << [300, 500]
# Whether or not range is strictly right of another range
Car.price_range << [300, 500]
Membership operators Membership operators
^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
@@ -65,6 +115,7 @@ Membership operators
~ Car.price_range.in_([[300, 400], [700, 800]]) ~ Car.price_range.in_([[300, 400], [700, 800]])
.. _intervals: https://github.com/kvesteri/intervals .. _intervals: https://github.com/kvesteri/intervals
""" """
from collections import Iterable from collections import Iterable
@@ -178,6 +229,7 @@ class RangeType(types.TypeDecorator, ScalarCoercible):
raise ImproperlyConfigured( raise ImproperlyConfigured(
'RangeType needs intervals package installed.' 'RangeType needs intervals package installed.'
) )
self.step = kwargs.pop('step', None)
super(RangeType, self).__init__(*args, **kwargs) super(RangeType, self).__init__(*args, **kwargs)
def load_dialect_impl(self, dialect): def load_dialect_impl(self, dialect):
@@ -197,10 +249,10 @@ class RangeType(types.TypeDecorator, ScalarCoercible):
if value is not None: if value is not None:
if self.interval_class.step is not None: if self.interval_class.step is not None:
return self.canonicalize_result_value( return self.canonicalize_result_value(
self.interval_class(value) self.interval_class(value, step=self.step)
) )
else: else:
return self.interval_class(value) return self.interval_class(value, step=self.step)
return value return value
def canonicalize_result_value(self, value): def canonicalize_result_value(self, value):
@@ -209,7 +261,7 @@ class RangeType(types.TypeDecorator, ScalarCoercible):
def _coerce(self, value): def _coerce(self, value):
if value is None: if value is None:
return None return None
return self.interval_class(value) return self.interval_class(value, step=self.step)
class IntRangeType(RangeType): class IntRangeType(RangeType):
@@ -263,7 +315,6 @@ class IntRangeType(RangeType):
self.interval_class = intervals.IntInterval self.interval_class = intervals.IntInterval
class DateRangeType(RangeType): class DateRangeType(RangeType):
""" """
DateRangeType provides way for saving ranges of dates into database. On DateRangeType provides way for saving ranges of dates into database. On
@@ -290,6 +341,24 @@ class DateRangeType(RangeType):
class NumericRangeType(RangeType): class NumericRangeType(RangeType):
"""
NumericRangeType provides way for saving ranges of decimals into database.
On PostgreSQL this type maps to native NUMRANGE type while on other drivers
this maps to simple string column.
Example::
from sqlalchemy_utils import NumericRangeType
class Car(Base):
__tablename__ = 'car'
id = sa.Column(sa.Integer, autoincrement=True)
name = sa.Column(sa.Unicode(255)))
price_range = sa.Column(NumericRangeType)
"""
impl = NUMRANGE impl = NUMRANGE
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@@ -1,3 +1,6 @@
from decimal import Decimal
from pytest import mark from pytest import mark
import sqlalchemy as sa import sqlalchemy as sa
intervals = None intervals = None
@@ -30,8 +33,8 @@ class NumericRangeTestCase(TestCase):
return self.session.query(self.Car).first() return self.session.query(self.Car).first()
def test_nullify_range(self): def test_nullify_range(self):
building = self.create_car(None) car = self.create_car(None)
assert building.price_range == None assert car.price_range is None
@mark.parametrize( @mark.parametrize(
'number_range', 'number_range',
@@ -82,3 +85,37 @@ class NumericRangeTestCase(TestCase):
class TestNumericRangeOnPostgres(NumericRangeTestCase): class TestNumericRangeOnPostgres(NumericRangeTestCase):
dns = 'postgres://postgres@localhost/sqlalchemy_utils_test' dns = 'postgres://postgres@localhost/sqlalchemy_utils_test'
@mark.skipif('intervals is None')
class TestNumericRangeWithStep(TestCase):
def create_models(self):
class Car(self.Base):
__tablename__ = 'car'
id = sa.Column(sa.Integer, primary_key=True)
price_range = sa.Column(NumericRangeType(step=Decimal('0.5')))
self.Car = Car
def create_car(self, number_range):
car = self.Car(
price_range=number_range
)
self.session.add(car)
self.session.commit()
return self.session.query(self.Car).first()
def test_passes_step_argument_to_interval_object(self):
car = self.create_car([Decimal('0.2'), Decimal('0.8')])
assert car.price_range.lower == Decimal('0')
assert car.price_range.upper == Decimal('1')
assert car.price_range.step == Decimal('0.5')
def test_passes_step_fetched_objects(self):
self.create_car([Decimal('0.2'), Decimal('0.8')])
self.session.expunge_all()
car = self.session.query(self.Car).first()
assert car.price_range.lower == Decimal('0')
assert car.price_range.upper == Decimal('1')
assert car.price_range.step == Decimal('0.5')