Add range type step support
This commit is contained in:
@@ -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):
|
||||||
|
@@ -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')
|
||||||
|
Reference in New Issue
Block a user