"""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-h|--help)|(?P-v|--version)" \ r"|(?P-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-r|--reversed)? ?(?P[\d, ]+)?)" \ r" ?(?Pin|from|show) (?P\S+)" \ r" ?(?=(?=of (?P\S+)" \ r"|(?=with (?Pt|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()