Source code for mpu.units

#!/usr/bin/env python

"""Handle units - currently only currencies."""


# Core Library
import csv
import fractions
from functools import total_ordering
from typing import Tuple, Union

# Third party
import pkg_resources


[docs]class Currency: """Currency base class which contains information similar to ISO 4217.""" def __init__( self, name, code, numeric_code, symbol, exponent, entities, withdrawal_date, subunits, ): if not isinstance(name, str): raise ValueError( "A currencies name has to be of type str, but " "was: {}".format(type(name)) ) if not isinstance(code, str): raise ValueError( "A currencies code has to be of type str, but " "was: {}".format(type(code)) ) if not isinstance(exponent, (type(None), int)): raise ValueError( "A currencies exponent has to be of type None " "or int, but was: {}".format(type(code)) ) self.name = name self.code = code self.numeric_code = numeric_code self.symbol = symbol self.exponent = exponent self.entities = entities self.withdrawal_date = withdrawal_date self.subunits = subunits def __eq__(self, other): if isinstance(other, self.__class__): return self.numeric_code == other.numeric_code else: return False def __ne__(self, other): return not self.__eq__(other) def __str__(self): return self.name def __repr__(self): return ( "Currency(name={name}, code={code}, " "numeric_code={numeric_code})".format( name=self.name, code=self.code, numeric_code=self.numeric_code ) ) def __json__(self): """Return a JSON-serializable object.""" return { "name": self.name, "code": self.code, "numeric_code": self.numeric_code, "symbol": self.symbol, "exponent": self.exponent, "entities": self.entities, "withdrawal_date": self.withdrawal_date, "subunits": self.subunits, "__python__": "mpu.units:Currency.from_json", } for_json = __json__
[docs] @classmethod def from_json(cls, json): """Create a Currency object from a JSON dump.""" obj = cls( name=json["name"], code=json["code"], numeric_code=json["numeric_code"], symbol=json["symbol"], exponent=json["exponent"], entities=json["entities"], withdrawal_date=json["withdrawal_date"], subunits=json["subunits"], ) return obj
[docs]@total_ordering class Money: """ Unit of account. Parameters ---------- value : Union[str, fractions.Fraction, int, Tuple] currency : Currency or str Examples -------- >>> rent = Money(500, 'USD') >>> '{:.2f,shortcode}'.format(rent) 'USD 500.00' >>> '{:.2f,postshortcode}'.format(rent) '500.00 USD' >>> '{:.2f,symbol}'.format(rent) '$500.00' >>> '{:.2f,postsymbol}'.format(rent) '500.00$' >>> '{:.2f}'.format(rent) '500.00 USD' """ def __init__( self, value: Union[str, fractions.Fraction, int, Tuple], currency: Union[str, Currency], ): # Handle value if isinstance(value, tuple): if len(value) != 2: raise ValueError( ( "value was {}, but only tuples of length 2 " "str, int and decimal are allowed." ).format(value) ) self.value = fractions.Fraction(value[0], value[1]) elif isinstance(value, float): raise ValueError( "floats can be ambiguous. Please convert it to " "two integers (nominator and denominator) and " "pass a tuple to the constructor." ) else: self.value = fractions.Fraction(value) # convert to Decimal # Handle currency if isinstance(currency, Currency): self.currency = currency elif isinstance(currency, str): self.currency = get_currency(currency) elif currency is None: self.currency = Currency( name="", code="", numeric_code=None, symbol="", exponent=None, entities=None, withdrawal_date=None, subunits=None, ) else: raise ValueError( "currency is of type={}, but should be str or " "Currency".format(type(currency)) ) def __str__(self): exponent = 2 if self.currency.exponent is not None: exponent = self.currency.exponent if self.currency.numeric_code is None: return "{value:0.{exponent}f}".format( exponent=exponent, value=float(self.value) ) else: return "{value:0.{exponent}f} {currency}".format( exponent=exponent, value=float(self.value), currency=self.currency, ) def __repr__(self): return str(self) def __mul__(self, other): if isinstance(other, (int, fractions.Fraction)): return Money(self.value * other, self.currency) else: raise ValueError( ("Multiplication with type '{}' is not " "supported").format( type(other) ) ) def __format__(self, spec): if "," not in spec: if spec == "": exponent = 2 if self.currency.exponent is not None: exponent = self.currency.exponent spec = "0.{exponent:}f".format(exponent=exponent) value_formatter = spec symbol_formatter = "postshortcode" else: value_formatter, symbol_formatter = spec.split(",") value_str = ("{:" + value_formatter + "}").format(float(self.value)) if symbol_formatter == "symbol": sep = "" return self.currency.symbol + sep + value_str elif symbol_formatter == "postsymbol": sep = "" return value_str + sep + self.currency.symbol elif symbol_formatter == "shortcode": sep = " " if self.currency.numeric_code is None: sep = "" return self.currency.code + sep + value_str elif symbol_formatter == "postshortcode": sep = " " if self.currency.numeric_code is None: sep = "" return value_str + sep + self.currency.code else: raise NotImplementedError( "The formatter '{}' is not " "implemented for the Money class.".format(symbol_formatter) ) def __add__(self, other): if isinstance(other, Money): if self.currency == other.currency: return Money(self.value + other.value, self.currency) else: raise ValueError( ( "Addition of currency '{}' and '{}' is " "not supported. You need an exchange rate." ).format(self.currency, other.currency) ) else: raise ValueError( ("Addition with type '{}' is not " "supported").format(type(other)) ) def __sub__(self, other): if isinstance(other, Money): if self.currency == other.currency: return Money(self.value - other.value, self.currency) else: raise ValueError( ( "Subtraction of currency '{}' and '{}' " "is not supported. You need an exchange " "rate." ).format(self.currency, other.currency) ) else: raise ValueError( ("Subtraction with type '{}' is not " "supported").format(type(other)) ) def __truediv__(self, other): if isinstance(other, Money): if self.currency == other.currency: return self.value / other.value else: raise ValueError( ( "Division of currency '{}' and '{}' " "is not supported. You need an exchange " "rate." ).format(self.currency, other.currency) ) elif isinstance(other, (int, fractions.Fraction)): return Money(self.value / other, self.currency) else: raise ValueError( ("Division with type '{}' is not " "supported").format(type(other)) ) __div__ = __truediv__ def __eq__(self, other): if isinstance(other, self.__class__): same_currency = self.currency == other.currency return (self.value == other.value) and same_currency else: return False def __ne__(self, other): return not self.__eq__(other) __rmul__ = __mul__ def __gt__(self, other): if not isinstance(other, self.__class__): return False elif self.currency != other.currency: return False else: return self.value > other.value def __lt__(self, other): if not isinstance(other, self.__class__): return False elif self.currency != other.currency: return False else: return self.value < other.value def __neg__(self): return Money(-self.value, self.currency) def __pos__(self): return Money(self.value, self.currency) def __float__(self): return float(self.value) def __json__(self): """Return a JSON-serializable object.""" currency = str(self.currency) if self.currency.numeric_code is None: currency = None return { "value": "{}".format(self.value), "currency": currency, "__python__": "mpu.units:Money.from_json", } for_json = __json__
[docs] @classmethod def from_json(cls, json): """Create a Money object from a JSON dump.""" obj = cls(json["value"], json["currency"]) return obj
[docs]def get_currency(currency_str): """ Convert an identifier for a currency into a currency object. Parameters ---------- currency_str : str Returns ------- currency : Currency """ path = "units/currencies.csv" # always use slash in Python packages filepath = pkg_resources.resource_filename("mpu", path) with open(filepath) as fp: reader = csv.reader(fp, delimiter=",", quotechar='"') next(reader, None) # skip the headers for row in reader: is_currency = currency_str in [row[0], row[1], row[2]] if is_currency: entity = row[0] name = row[1] code = row[2] numeric_code = row[3] symbol = row[4] if len(row[5]) == 0: exponent = None else: exponent = int(row[5]) if len(row[6]) > 0: withdrawal_date = row[6] else: withdrawal_date = None subunits = row[7] return Currency( name=name, code=code, numeric_code=numeric_code, symbol=symbol, exponent=exponent, entities=[entity], withdrawal_date=withdrawal_date, subunits=subunits, ) raise ValueError("Could not find currency '{}'".format(currency_str))