1
0
PiggyBank/piggybank/__main__.py

191 lines
7.2 KiB
Python

"""CLI"""
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).
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':^27}{''.join(f'{c/100:^12.2f}' for c in s)}")
print_separator("", "", "", "")
s = sum(s)
print(f"{'Total':^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}")
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."""
complementary = [0] * (len(CURRENCIES[currency]) - len(coins))
return complementary + coins if not _reversed else coins + complementary
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()