# Copyright (C) 2015 Chintalagiri Shashank
#
# This file is part of Tendril.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Currency Types (:mod:`tendril.utils.types.currency`)
====================================================
The :mod:`tendril.utils.types.currency` contains classes which allow for easy
use and manipulation of currency values. The primary focus is on the primary
use cases of Currencies within tendril, i.e. :
- Handling foreign exchange conversions and exchange rates in application code
without too much fuss.
- Handling currency arithmetic and comparisons.
This module uses a specific `Base Currency`, defined by
:const:`tendril.utils.config.BASE_CURRENCY` and
:const:`tendril.utils.config.BASE_CURRENCY_SYMBOL` and available as this
module's :data:`native_currency_defn` module variable. In case this module is
to be used independent of Tendril, at least those configuration options
*must* be defined in :mod:`tendril.utils.config`.
.. rubric:: Module Contents
.. autosummary::
native_currency_defn
CurrencyDefinition
CurrencyValue
.. seealso:: :mod:`tendril.utils.types`, for an overview applicable to most
types defined in Tendril.
.. todo:: The core numbers in this module need to switched to
:class:`decimal.Decimal`.
"""
from cachecontrol.heuristics import ExpiresAfter
from tendril.utils.config import BASE_CURRENCY
from tendril.utils.config import BASE_CURRENCY_SYMBOL
from tendril.utils import www
# from six.moves.urllib.request import Request
# from six.moves.urllib.parse import urlencode
import json
import codecs
import numbers
from .unitbase import TypedComparisonMixin
# TODO Switch to a heuristic which uses a cached value only if a fresh
# one is not available.
requests_session = www.get_session(heuristic=ExpiresAfter(days=5))
[docs]class CurrencyDefinition(object):
"""
Instances of this class define a currency.
The minimal requirement to define a currency is a :attr:`code`, which
would usually be a standard internationally recognized currency code.
In addition to the :attr:`code`, a currency definition also includes
an optional :attr:`symbol`, which is used to create string representations
of currency values in that currency. In the absence of a :attr:`symbol`,
the :attr:`code` is used in it's place.
Unless otherwise specified during the instantiation of the class,
the exchange rate is obtained from internet services by the
:meth:`_get_exchval` method.
:param code: Standard currency code.
:param symbol: Symbol to use to represent the currency. Optional.
:param exchval: Exchange rate to use, if not automatic. Optional.
"""
def __init__(self, code, symbol=None, exchval=None):
self._code = code
self._symbol = symbol
if exchval is not None:
self._exchval = exchval
else:
self._exchval = self._get_exchval(self._code)
@property
def code(self):
"""
:return: The currency code.
:rtype: str
"""
return self._code
@property
def symbol(self):
"""
:return: The currency symbol, or code if no symbol.
:rtype: str
"""
if self._symbol is not None:
return self._symbol
else:
return self._code
@property
def exchval(self):
"""
:return: The exchange rate
:rtype: float
"""
return self._exchval
@property
def exch_rate(self):
"""
The exchange rate in a human-friendly string.
"""
return "{0}1/{1}{2:,.2f}".format(self.symbol, BASE_CURRENCY_SYMBOL, self.exchval)
@staticmethod
[docs] def _get_exchval(code):
"""
Obtains the exchange rate of the currency definition's :attr:`code`
using the `<http://fixer.io>`_ JSON API. The native currency is used
as the reference.
:param code: The currency code for which the exchange rate is needed.
:type code: str
:return: The exchange rate of currency specified by code vs the native
currency.
:rtype: float
"""
if BASE_CURRENCY == code:
return 1
apiurl = 'http://api.fixer.io/latest?'
params = {'base': BASE_CURRENCY,
'symbols': code}
r = requests_session.get(apiurl, params=params)
data = r.json()
try:
rate = 1 / float(data['rates'][code])
except KeyError:
raise KeyError(code)
return rate
[docs] def __eq__(self, other):
"""
Two instances of :class:`CurrencyDefinition` will evaluate to be equal
only when all three parameters of the instances are equal.
"""
if self.code != other.code:
return False
if self.symbol != other.symbol:
return False
if self.exchval != other.exchval:
return False
return True
def __repr__(self):
return "<CurrencyDefinition {0} {1} {2}>" \
"".format(self.code, self.symbol, self.exchval)
#: The native currency definition used by the module
#:
#: This definition uses the code contained in
#: :const:`tendril.utils.config.BASE_CURRENCY` and symbol
#: :const:`tendril.utils.config.BASE_CURRENCY_SYMBOL`. Application
#: code should import this definition instead of creating new currency
#: definitions whenever one is needed to represent a native currency value.
native_currency_defn = CurrencyDefinition(BASE_CURRENCY, BASE_CURRENCY_SYMBOL)
[docs]class CurrencyValue(TypedComparisonMixin):
"""
Instances of this class define a specific currency value, or a certain
sum of money.
The `currency_def` can either be a :class:`CurrencyDefinition` instance
(recommended), or a string containing the code for the currency.
:param val: The numerical value.
:param currency_def: The currency definition within which the value
is defined.
:type currency_def: :class:`CurrencyDefinition` or str
.. note:: Since the exchange rate is obtained at the instantiation of
the :class:`CurrencyDefinition`, using a string instead of a
predefined :class:`CurrencyDefinition` instance may result in
instances of the same currency, but with different exchange
rates.
:ivar _currency_def: The currency definition of the source value of the
instance.
:ivar _val: The numerical value in the source currency of the instance.
.. rubric:: Arithmetic Operations
.. autosummary::
__add__
__sub__
__mul__
__div__
_cmpkey
"""
def __init__(self, val, currency_def):
if isinstance(currency_def, CurrencyDefinition):
self._currency_def = currency_def
else:
self._currency_def = CurrencyDefinition(currency_def)
self._val = val
@property
def native_value(self):
"""
The numerical value of the currency value in the native currency,
i.e., that defined by :data:`native_currency_defn`.
:rtype: float
"""
return self._val * self._currency_def.exchval
@property
def native_string(self):
"""
The string representation of the currency value in the native
currency, i.e., that defined by :data:`native_currency_defn`.
:rtype: str
"""
return "{0} {1:,.2f}".format(BASE_CURRENCY_SYMBOL, self.native_value)
@property
def source_value(self):
"""
The numerical value of the currency value in the source currency,
i.e., that defined by :attr:`source_currency`.
:rtype: float
"""
return self._val
@property
def source_string(self):
"""
The string representation of the currency value in the source
currency, i.e., that defined by :attr:`source_currency`.
:rtype: str
"""
return "{0} {1:,.2f}".format(self._currency_def.symbol, self._val)
@property
def source_currency(self):
"""
The currency definition of the source currency, i.e, the instance
variable :data:`_currency_def`.
:rtype: :class:`CurrencyDefinition`
"""
return self._currency_def
@property
def exch_rate(self):
"""
The applicable exchange rate in a human-friendly string.
"""
return self._currency_def.exch_rate
@property
def is_foreign(self):
"""
Whether the source currency is Foreign (``True``) or is the native
currency (``False``).
"""
if self._currency_def.code != BASE_CURRENCY:
return True
else:
return False
def __repr__(self):
return self.native_string
def __float__(self):
return float(self.native_value)
[docs] def __add__(self, other):
"""
Addition of two :class:`CurrencyValue` instances returns a
:class:`CurrencyValue` instance with the sum of the two operands,
with currency conversion applied if necessary.
If the :attr:`source_currency` of the two operands are equal,
the result uses the the same :attr:`source_currency`. If not,
the result is uses the :data:`native_currency_defn` as it's
:attr:`source_currency`.
If the other operand is a numerical type and evaluates to 0, this
object is simply returned unchanged.
Addition with all other Types / Classes is not supported.
:rtype: :class:`CurrencyValue`
"""
if isinstance(other, numbers.Number) and other == 0:
return self
if not isinstance(other, CurrencyValue):
raise NotImplementedError
if self._currency_def.symbol == other.source_currency.symbol:
return CurrencyValue(
self.source_value + other.source_value, self.source_currency
)
else:
return CurrencyValue(
self.native_value + other.native_value, native_currency_defn
)
def __radd__(self, other):
if other == 0:
return self
else:
return self.__add__(other)
[docs] def __mul__(self, other):
"""
Multiplication of one :class:`CurrencyValue` instance with a
numerical type results in a :class:`CurrencyValue` instance,
whose value is is the currency type operand's value multiplied
by the numerical operand's value.
The :attr:`source_currency` of the returned :class:`CurrencyValue`
is the same as that of the currency type operand.
Multiplication with all other Types / Classes is not supported.
:rtype: :class:`CurrencyValue`
"""
if isinstance(other, numbers.Number):
return CurrencyValue(
self.source_value * other, self.source_currency
)
else:
raise NotImplementedError
[docs] def __div__(self, other):
"""
Division of one :class:`CurrencyValue` instance with a numerical type
results in a :class:`CurrencyValue` instance, whose value is is the
currency type operand's value divided by the numerical operand's
value.
The :attr:`source_currency` of the returned :class:`CurrencyValue`
is the same as that of the currency type operand. In this case, the
first operand must be a :class:`CurrencyValue`, and not the reverse.
Division of one :class:`CurrencyValue` instance by another returns a
numerical value, which is obtained by performing the division with the
operands' :attr:`native_value`.
Division with all other Types / Classes is not supported.
:rtype: :class:`CurrencyValue`
"""
if isinstance(other, numbers.Number):
return CurrencyValue(
self.source_value / other, self.source_currency
)
elif isinstance(other, CurrencyValue):
return self.native_value / other.native_value
else:
raise NotImplementedError
def __truediv__(self, other):
return self.__div__(other)
def __rmul__(self, other):
return self.__mul__(other)
[docs] def __sub__(self, other):
"""
Subtraction of two :class:`CurrencyValue` instances returns a
:class:`CurrencyValue` instance with the difference of the two
operands, with currency conversion applied if necessary.
If :attr:`source_currency` of the two operands are equal,
the result uses the the same :attr:`source_currency`. If not,
the result is in the :data:`native_currency_defn`.
If the other operand is a numerical type and evaluates to 0, this
object is simply returned unchanged.
Subtraction with all other Types / Classes is not supported.
:rtype: :class:`CurrencyValue`
"""
if isinstance(other, numbers.Number) and other == 0:
return self
elif not isinstance(other, CurrencyValue):
raise NotImplementedError
else:
return self.__add__(other.__mul__(-1))
[docs] def _cmpkey(self):
"""
The comparison of two :class:`CurrencyValue` instances behaves
identically to the comparison of the operands' :attr:`native_value`.
Comparison with all other Types / Classes is not supported.
"""
return self.native_value