Merge pull request #135 from kvesteri/feature/currency-type
Feature/currency type
This commit is contained in:
@@ -38,6 +38,19 @@ CountryType
|
|||||||
|
|
||||||
.. autoclass:: CountryType
|
.. autoclass:: CountryType
|
||||||
|
|
||||||
|
|
||||||
|
CurrencyType
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. module:: sqlalchemy_utils.types.currency
|
||||||
|
|
||||||
|
.. autoclass:: CurrencyType
|
||||||
|
|
||||||
|
.. module:: sqlalchemy_utils.primitives.currency
|
||||||
|
|
||||||
|
.. autoclass:: Currency
|
||||||
|
|
||||||
|
|
||||||
EncryptedType
|
EncryptedType
|
||||||
^^^^^^^^^^^^^
|
^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
@@ -54,6 +54,7 @@ from .listeners import ( # noqa
|
|||||||
)
|
)
|
||||||
from .models import Timestamp # noqa
|
from .models import Timestamp # noqa
|
||||||
from .observer import observes # noqa
|
from .observer import observes # noqa
|
||||||
|
from .primitives import Currency, WeekDay, WeekDays # noqa
|
||||||
from .proxy_dict import proxy_dict, ProxyDict # noqa
|
from .proxy_dict import proxy_dict, ProxyDict # noqa
|
||||||
from .query_chain import QueryChain # noqa
|
from .query_chain import QueryChain # noqa
|
||||||
from .types import ( # noqa
|
from .types import ( # noqa
|
||||||
@@ -63,6 +64,7 @@ from .types import ( # noqa
|
|||||||
ColorType,
|
ColorType,
|
||||||
Country,
|
Country,
|
||||||
CountryType,
|
CountryType,
|
||||||
|
CurrencyType,
|
||||||
DateRangeType,
|
DateRangeType,
|
||||||
DateTimeRangeType,
|
DateTimeRangeType,
|
||||||
EmailType,
|
EmailType,
|
||||||
|
@@ -1,7 +1,3 @@
|
|||||||
from .weekday import WeekDay
|
from .currency import Currency # noqa
|
||||||
from .weekdays import WeekDays
|
from .weekday import WeekDay # noqa
|
||||||
|
from .weekdays import WeekDays # noqa
|
||||||
__all__ = (
|
|
||||||
WeekDay,
|
|
||||||
WeekDays
|
|
||||||
)
|
|
||||||
|
110
sqlalchemy_utils/primitives/currency.py
Normal file
110
sqlalchemy_utils/primitives/currency.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
babel = None
|
||||||
|
try:
|
||||||
|
import babel
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
import six
|
||||||
|
|
||||||
|
from sqlalchemy_utils import i18n, ImproperlyConfigured
|
||||||
|
from sqlalchemy_utils.utils import str_coercible
|
||||||
|
|
||||||
|
|
||||||
|
@str_coercible
|
||||||
|
class Currency(object):
|
||||||
|
"""
|
||||||
|
Currency class wraps a 3-letter currency code. It provides various
|
||||||
|
convenience properties and methods.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
from babel import Locale
|
||||||
|
from sqlalchemy_utils import Currency, i18n
|
||||||
|
|
||||||
|
|
||||||
|
# First lets add a locale getter for testing purposes
|
||||||
|
i18n.get_locale = lambda: Locale('en')
|
||||||
|
|
||||||
|
|
||||||
|
Currency('USD').name # US Dollar
|
||||||
|
Currency('USD').symbol # $
|
||||||
|
|
||||||
|
Currency(Currency('USD')).code # 'USD'
|
||||||
|
|
||||||
|
Currency always validates the given code.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
Currency(None) # raises TypeError
|
||||||
|
|
||||||
|
Currency('UnknownCode') # raises ValueError
|
||||||
|
|
||||||
|
|
||||||
|
Currency supports equality operators.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
Currency('USD') == Currency('USD')
|
||||||
|
Currency('USD') != Currency('EUR')
|
||||||
|
|
||||||
|
|
||||||
|
Currencies are hashable.
|
||||||
|
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
len(set([Currency('USD'), Currency('USD')])) # 1
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, code):
|
||||||
|
if babel is None:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"'babel' package is required in order to use Currency class."
|
||||||
|
)
|
||||||
|
if isinstance(code, Currency):
|
||||||
|
self.code = code
|
||||||
|
elif isinstance(code, six.string_types):
|
||||||
|
self.validate(code)
|
||||||
|
self.code = code
|
||||||
|
else:
|
||||||
|
raise TypeError(
|
||||||
|
'First argument given to Currency constructor should be '
|
||||||
|
'either an instance of Currency or valid three letter '
|
||||||
|
'currency code.'
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(self, code):
|
||||||
|
try:
|
||||||
|
i18n.get_locale().currencies[code]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError("{0}' is not valid currency code.")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def symbol(self):
|
||||||
|
return babel.numbers.get_currency_symbol(self.code, i18n.get_locale())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return i18n.get_locale().currencies[self.code]
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, Currency):
|
||||||
|
return self.code == other.code
|
||||||
|
elif isinstance(other, six.string_types):
|
||||||
|
return self.code == other
|
||||||
|
else:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not (self == other)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.code)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '%s(%r)' % (self.__class__.__name__, self.code)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.code
|
@@ -6,6 +6,7 @@ from .arrow import ArrowType
|
|||||||
from .choice import Choice, ChoiceType
|
from .choice import Choice, ChoiceType
|
||||||
from .color import ColorType
|
from .color import ColorType
|
||||||
from .country import Country, CountryType
|
from .country import Country, CountryType
|
||||||
|
from .currency import CurrencyType
|
||||||
from .email import EmailType
|
from .email import EmailType
|
||||||
from .encrypted import EncryptedType
|
from .encrypted import EncryptedType
|
||||||
from .ip_address import IPAddressType
|
from .ip_address import IPAddressType
|
||||||
@@ -33,6 +34,7 @@ __all__ = (
|
|||||||
ColorType,
|
ColorType,
|
||||||
Country,
|
Country,
|
||||||
CountryType,
|
CountryType,
|
||||||
|
CurrencyType,
|
||||||
DateRangeType,
|
DateRangeType,
|
||||||
DateTimeRangeType,
|
DateTimeRangeType,
|
||||||
EmailType,
|
EmailType,
|
||||||
|
80
sqlalchemy_utils/types/currency.py
Normal file
80
sqlalchemy_utils/types/currency.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
babel = None
|
||||||
|
try:
|
||||||
|
import babel
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
import six
|
||||||
|
from sqlalchemy import types
|
||||||
|
|
||||||
|
from sqlalchemy_utils import ImproperlyConfigured
|
||||||
|
from sqlalchemy_utils.primitives import Currency
|
||||||
|
|
||||||
|
from .scalar_coercible import ScalarCoercible
|
||||||
|
|
||||||
|
|
||||||
|
class CurrencyType(types.TypeDecorator, ScalarCoercible):
|
||||||
|
"""
|
||||||
|
Changes :class:`.Currency` objects to a string representation on the way in
|
||||||
|
and changes them back to :class:`.Currency` objects on the way out.
|
||||||
|
|
||||||
|
In order to use CurrencyType you need to install Babel_ first.
|
||||||
|
|
||||||
|
.. _Babel: http://babel.pocoo.org/
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
|
||||||
|
from sqlalchemy_utils import CurrencyType, Currency
|
||||||
|
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = 'user'
|
||||||
|
id = sa.Column(sa.Integer, autoincrement=True)
|
||||||
|
name = sa.Column(sa.Unicode(255))
|
||||||
|
currency = sa.Column(CurrencyType)
|
||||||
|
|
||||||
|
|
||||||
|
user = User()
|
||||||
|
user.currency = Currency('USD')
|
||||||
|
session.add(user)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
user.currency # Currency('USD')
|
||||||
|
user.currency.name # US Dollar
|
||||||
|
|
||||||
|
str(user.currency) # US Dollar
|
||||||
|
user.currency.symbol # $
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CurrencyType is scalar coercible::
|
||||||
|
|
||||||
|
|
||||||
|
user.currency = 'US'
|
||||||
|
user.currency # Currency('US')
|
||||||
|
"""
|
||||||
|
impl = types.String(3)
|
||||||
|
python_type = Currency
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if babel is None:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"'babel' package is required in order to use CurrencyType."
|
||||||
|
)
|
||||||
|
|
||||||
|
super(CurrencyType, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def process_bind_param(self, value, dialect):
|
||||||
|
if isinstance(value, Currency):
|
||||||
|
return value.code
|
||||||
|
elif isinstance(value, six.string_types):
|
||||||
|
return value
|
||||||
|
|
||||||
|
def process_result_value(self, value, dialect):
|
||||||
|
if value is not None:
|
||||||
|
return Currency(value)
|
||||||
|
|
||||||
|
def _coerce(self, value):
|
||||||
|
if value is not None and not isinstance(value, Currency):
|
||||||
|
return Currency(value)
|
||||||
|
return value
|
67
tests/primitives/test_currency.py
Normal file
67
tests/primitives/test_currency.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import six
|
||||||
|
from pytest import mark, raises
|
||||||
|
|
||||||
|
from sqlalchemy_utils import Currency, i18n
|
||||||
|
from sqlalchemy_utils.primitives.currency import babel # noqa
|
||||||
|
|
||||||
|
|
||||||
|
@mark.skipif('babel is None')
|
||||||
|
class TestCurrency(object):
|
||||||
|
def setup_method(self, method):
|
||||||
|
i18n.get_locale = lambda: babel.Locale('en')
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
assert Currency('USD') == Currency(Currency('USD'))
|
||||||
|
|
||||||
|
def test_hashability(self):
|
||||||
|
assert len(set([Currency('USD'), Currency('USD')])) == 1
|
||||||
|
|
||||||
|
def test_invalid_currency_code(self):
|
||||||
|
with raises(ValueError):
|
||||||
|
Currency('Unknown code')
|
||||||
|
|
||||||
|
def test_invalid_currency_code_type(self):
|
||||||
|
with raises(TypeError):
|
||||||
|
Currency(None)
|
||||||
|
|
||||||
|
@mark.parametrize(
|
||||||
|
('code', 'name'),
|
||||||
|
(
|
||||||
|
('USD', 'US Dollar'),
|
||||||
|
('EUR', 'Euro')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
def test_name_property(self, code, name):
|
||||||
|
assert Currency(code).name == name
|
||||||
|
|
||||||
|
@mark.parametrize(
|
||||||
|
('code', 'symbol'),
|
||||||
|
(
|
||||||
|
('USD', u'$'),
|
||||||
|
('EUR', u'€')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
def test_symbol_property(self, code, symbol):
|
||||||
|
assert Currency(code).symbol == symbol
|
||||||
|
|
||||||
|
def test_equality_operator(self):
|
||||||
|
assert Currency('USD') == 'USD'
|
||||||
|
assert 'USD' == Currency('USD')
|
||||||
|
assert Currency('USD') == Currency('USD')
|
||||||
|
|
||||||
|
def test_non_equality_operator(self):
|
||||||
|
assert Currency('USD') != 'EUR'
|
||||||
|
assert not (Currency('USD') != 'USD')
|
||||||
|
|
||||||
|
def test_unicode(self):
|
||||||
|
currency = Currency('USD')
|
||||||
|
assert six.text_type(currency) == u'USD'
|
||||||
|
|
||||||
|
def test_str(self):
|
||||||
|
currency = Currency('USD')
|
||||||
|
assert str(currency) == 'USD'
|
||||||
|
|
||||||
|
def test_representation(self):
|
||||||
|
currency = Currency('USD')
|
||||||
|
assert repr(currency) == "Currency('USD')"
|
40
tests/types/test_currency.py
Normal file
40
tests/types/test_currency.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from pytest import mark
|
||||||
|
|
||||||
|
from sqlalchemy_utils import Currency, CurrencyType, i18n
|
||||||
|
from sqlalchemy_utils.types.currency import babel
|
||||||
|
from tests import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
@mark.skipif('babel is None')
|
||||||
|
class TestCurrencyType(TestCase):
|
||||||
|
def setup_method(self, method):
|
||||||
|
TestCase.setup_method(self, method)
|
||||||
|
i18n.get_locale = lambda: babel.Locale('en')
|
||||||
|
|
||||||
|
def create_models(self):
|
||||||
|
class User(self.Base):
|
||||||
|
__tablename__ = 'user'
|
||||||
|
id = sa.Column(sa.Integer, primary_key=True)
|
||||||
|
currency = sa.Column(CurrencyType)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'User(%r)' % self.id
|
||||||
|
|
||||||
|
self.User = User
|
||||||
|
|
||||||
|
def test_parameter_processing(self):
|
||||||
|
user = self.User(
|
||||||
|
currency=Currency('USD')
|
||||||
|
)
|
||||||
|
|
||||||
|
self.session.add(user)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
user = self.session.query(self.User).first()
|
||||||
|
assert user.currency.name == u'US Dollar'
|
||||||
|
|
||||||
|
def test_scalar_attributes_get_coerced_to_objects(self):
|
||||||
|
user = self.User(currency='USD')
|
||||||
|
assert isinstance(user.currency, Currency)
|
Reference in New Issue
Block a user