1
0
Fork 0
PiggyBank/piggybank/__main__.py

192 lines
7.3 KiB
Python

"""CLI: main"""
from re import search, split
from sys import argv, stderr
from os.path import exists
from typing import List
from piggybank import VERSION, CURRENCIES
from piggybank.configuration import Configuration, DEFAULT_CONFIGURATION_FILE
from piggybank.currency import Currency, BaseCurrencyError
from piggybank.piggybank import PiggyBank
from piggybank.transaction import sum_transactions, TYPE_INCOME, TYPE_OUTCOME
__all__ = ["main"]
USAGE = \
"""Usage: piggybank [OPTIONS] (COINS in FILE [of CURRENCY] | COINS from FILE
| show FILE [with t(ransactions)])
Options:
\t-h,--help -- print this help;
\t-v,--version -- print program version;
\t-L,--list-currencies -- list supported currencies and a default one;
\t-r,--reversed -- change COINS' complementation from left to right.
\t (e.g. 4 5 -> 0 0 0 0 4 5 by default; 4 5 0 0 0 0
\t with this flag. If currency has 6 coins)
There are three actions available: put, take and show.
Put: piggybank [-r|--reversed] COINS in FILE [of CURRENCY]
Take: piggybank [-r|--reversed] COINS from FILE
Show: piggybank show FILE [with t(ransactions)]
Arguments:
\tCOINS -- a set of comma or whitespace separated coin counts;
\tFILE -- a filename of a piggy bank;
\tof CURRENCY -- set a partcular currency for a piggy bank;
\twith t(ransactions) -- print a table of transactions as well.
"""
def parse_common_arguments(args: str) -> dict:
r = r"(?P<help>-h|--help)|(?P<version>-v|--version)" \
r"|(?P<list_currencies>-L|--list-currencies)"
argd = search(r, args)
if not argd is None:
argd = argd.groupdict()
return {
"help": not argd["help"] is None,
"version": not argd["version"] is None,
"list-currencies": not argd["list_currencies"] is None }
return None
def parse_arguments(args: str) -> dict:
r = r"((?P<reversed>-r|--reversed)? ?(?P<coins>[\d, ]+)?)" \
r" ?(?P<action>in|from|show) (?P<file>\S+)" \
r" ?(?=(?=of (?P<currency>\S+)" \
r"|(?=with (?P<transactions>t|transactions))))?"
argd = search(r, args)
if not argd is None:
argd = argd.groupdict()
return {
"reversed": not argd["reversed"] is None,
"coins": list(map(int, split(r"\D", argd["coins"].strip()))) \
if not argd["coins"] is None else None,
"action": argd["action"],
"file": argd["file"],
"currency": argd["currency"],
"show-transactions": argd["transactions"] }
return None
def print_transactions(pb: PiggyBank) -> None:
currency = CURRENCIES[pb.currency]
names = (currency.currency_symbol.format(n//100) if n >= 100 \
else currency.fraction_symbol.format(n) \
for n in currency.face_values)
def print_separator(l: str, m: str, r: str):
print(f"{l}{''*21}{m}{''*5}{m}{m.join([''*12]*len(currency))}{r}")
print_separator("", "", "")
print(f"{'Timestamp':^21}┃ I/O ┃" \
f"{''.join(f'{n:^12}' for n in names)}")
print_separator("", "", "")
for t in pb.transactions:
print(f"{t.timestamp.replace('T', ' ')} "
f"{t.direction:^5}"
f"{''.join(f'{c:^12}' for c in t.coins)}")
print_separator("", "", "")
def print_summary(pb: PiggyBank) -> None:
currency = CURRENCIES[pb.currency]
names = (currency.currency_symbol.format(n//100) if n >= 100 \
else currency.fraction_symbol.format(n) \
for n in currency.face_values)
def print_separator(l: str, lm: str, rm: str, r:str):
print(f"{l}{''*27}{lm}{rm.join([''*12]*len(currency))}{r}")
print_separator("", "", "", "")
print(f"{currency.name:^27}{''.join(f'{n:^12}' for n in names)}")
print_separator("", "", "", "")
s = sum_transactions(pb.transactions)
print(f"{'Counts':^27}{''.join(f'{c:^12}' for c in s)}")
print_separator("", "", "", "")
s = currency * s
print(f"{'Sums in currency':^27}{''.join(f'{c/100:^12.2f}' for c in s)}")
print_separator("", "", "", "")
s = sum(s)
print(f"{'Total in currency':^27}{s/100:^{12*len(currency)+len(currency)-1}.2f}")
print_separator("", "", "", "")
def print_supported_currencies() -> None:
"""Print a list of supported currencies."""
print("Supported currencies are:")
for cur in CURRENCIES.keys():
print(f" {cur:^4}{CURRENCIES[cur].name:^31}"
f"{CURRENCIES[cur].description}")
print("Default currency is", Configuration()["default-currency"])
def load_currencies() -> None:
"""Load currencies defined in a configuration file."""
if not "currency" in Configuration().items:
return
for iso, cur in Configuration()["currency"].items():
CURRENCIES[iso.upper()] = Currency.from_string(cur)
def complement_list_of_coins(coins: List[int], currency: str,
_reversed: bool = False) -> List[int]:
"""Complements list of coins up to the count of currency's coins."""
offset_array = [0] * (len(CURRENCIES[currency]) - len(coins))
return offset_array + coins if not _reversed else coins + offset_array
def main():
cargs = parse_common_arguments(' '.join(argv))
if not cargs is None:
if cargs["help"]:
print(USAGE)
exit()
elif cargs["version"]:
print(VERSION)
exit()
elif cargs["list-currencies"]:
load_currencies()
print_supported_currencies()
exit()
args = parse_arguments(' '.join(argv))
if not args:
print(USAGE)
exit()
try:
if not exists(DEFAULT_CONFIGURATION_FILE):
Configuration.write_default_to_file(DEFAULT_CONFIGURATION_FILE)
load_currencies()
try:
pb = PiggyBank.from_file(args["file"])
except FileNotFoundError:
if args["action"] == 'in':
currency = Configuration()["default-currency"] \
if not args["currency"] else args["currency"]
pb = PiggyBank(currency)
else:
raise FileNotFoundError(f"{args['file']} is missing.")
if args["action"] in ["in", "from"]:
coins = complement_list_of_coins(args["coins"], pb.currency,
args["reversed"])
pb.transact(coins, TYPE_INCOME if args["action"] == "in" \
else TYPE_OUTCOME)
pb.save(args["file"])
elif args["action"] == "show":
print_summary(pb)
if args["show-transactions"]:
print_transactions(pb)
except BaseCurrencyError as err:
print(f"{type(err).__name__}:", err, file=stderr)
except FileNotFoundError as err:
print(f"{type(err).__name__}:", err, file=stderr)
except ValueError as err:
print(f"{type(err).__name__}:", err, file=stderr)
except Exception as err:
print(f"Something went exceptionally wrong. Error:",
f"{type(err).__name__}:", err, file=stderr)
if __name__ == "__main__":
main()