Source code for mpu.units

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

# Core Library
import csv
import fractions
from functools import total_ordering
from typing import Any, Dict, List, Optional, Tuple, Union

# Third party
import pkg_resources


[docs]class Currency: """Currency base class which contains information similar to ISO 4217.""" def __init__( self, name: str, code: str, numeric_code: str, symbol: str, exponent: Optional[int], entities: Optional[List], withdrawal_date: Optional[str], subunits: Optional[str], ): if not isinstance(name, str): raise ValueError( f"A currencies name has to be of type str, but was: {type(name)}" ) if not isinstance(code, str): raise ValueError( f"A currencies code has to be of type str, but was: {type(code)}" ) if not isinstance(exponent, (type(None), int)): raise ValueError( "A currencies exponent has to be of type None " f"or int, but was: {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: Any) -> bool: if isinstance(other, self.__class__): return self.numeric_code == other.numeric_code else: return False def __ne__(self, other: Any) -> bool: return not self.__eq__(other) def __str__(self) -> str: return self.name def __repr__(self) -> str: return ( f"Currency(name={self.name}, code={self.code}, " f"numeric_code={self.numeric_code})" ) def __json__(self) -> Dict[str, Any]: """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: Dict) -> "Currency": """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( f"value was {value}, but only tuples of length 2 " "str, int and decimal are allowed." ) 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( f"currency is of type={type(currency)}, but should be str or " "Currency" ) def __str__(self) -> str: exponent = 2 if self.currency.exponent is not None: exponent = self.currency.exponent if self.currency.numeric_code is None: return f"{float(self.value):0.{exponent}f}" else: return f"{float(self.value):0.{exponent}f} {self.currency}" def __repr__(self) -> str: return str(self) def __mul__(self, other: Union[int, fractions.Fraction]) -> "Money": if isinstance(other, (int, fractions.Fraction)): return Money(self.value * other, self.currency) else: raise ValueError( f"Multiplication with type '{type(other)}' is not supported" ) def __format__(self, spec: str) -> str: if "," not in spec: if spec == "": exponent = 2 if self.currency.exponent is not None: exponent = self.currency.exponent spec = f"0.{exponent}f" 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( f"The formatter '{symbol_formatter}' is not " "implemented for the Money class." ) def __add__(self, other: "Money") -> "Money": if not isinstance(other, Money): raise ValueError(f"Addition with type '{type(other)}' is not supported") if not (self.currency == other.currency): raise ValueError( f"Addition of currency '{self.currency}' and '{other.currency}' " "is not supported. You need an exchange rate." ) return Money(self.value + other.value, self.currency) def __sub__(self, other: "Money") -> "Money": if not isinstance(other, Money): raise ValueError(f"Subtraction with type '{type(other)}' is not supported") if not (self.currency == other.currency): raise ValueError( f"Subtraction of currency '{self.currency}' and " f"'{other.currency}' is not supported. You need an exchange " "rate." ) return Money(self.value - other.value, self.currency) def __truediv__(self, other: "Money") -> Union[fractions.Fraction, "Money"]: if isinstance(other, Money): if self.currency == other.currency: return self.value / other.value else: raise ValueError( f"Division of currency '{self.currency}' and " f"'{other.currency}' is not supported. You need an " "exchange rate." ) elif isinstance(other, (int, fractions.Fraction)): return Money(self.value / other, self.currency) else: raise ValueError(f"Division with type '{type(other)}' is not supported") __div__ = __truediv__ def __eq__(self, other: Any) -> bool: if not isinstance(other, self.__class__): raise ValueError( "Only instances of Money can be compared to each " f"other. Other was of type {type(other)}" ) elif self.currency != other.currency: raise ValueError( f"Left has currency={self.currency}, right has " f"currency={other.currency}. You need to convert " "to the same currency first." ) else: return self.value == other.value def __ne__(self, other: Any) -> bool: return not self.__eq__(other) __rmul__ = __mul__ def __gt__(self, other: "Money") -> bool: if not isinstance(other, self.__class__): return False elif self.currency != other.currency: raise ValueError( f"Left has currency={self.currency}, right has " f"currency={other.currency}. You need to convert " "to the same currency first." ) else: return self.value > other.value def __lt__(self, other: "Money") -> bool: if not isinstance(other, self.__class__): return False elif self.currency != other.currency: raise ValueError( f"Left has currency={self.currency}, right has " f"currency={other.currency}. You need to convert " "to the same currency first." ) else: return self.value < other.value def __neg__(self) -> "Money": return Money(-self.value, self.currency) def __pos__(self) -> "Money": return Money(self.value, self.currency) def __float__(self) -> float: return float(self.value) def __json__(self) -> Dict[str, Any]: """Return a JSON-serializable object.""" currency = str(self.currency) if self.currency.numeric_code is None: currency = None return { "value": f"{self.value}", "currency": currency, "__python__": "mpu.units:Money.from_json", } for_json = __json__
[docs] @classmethod def from_json(cls, json: Dict[str, Any]) -> "Money": """Create a Money object from a JSON dump.""" obj = cls(json["value"], json["currency"]) return obj
[docs]def get_currency(currency_str: str) -> Currency: """ 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: Optional[str] = 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(f"Could not find currency '{currency_str}'")