From f4f026f8b2bd418ad8c612b03bc474feb559cb86 Mon Sep 17 00:00:00 2001 From: "Alexander \"Arav\" Andreev" Date: Sun, 7 Jun 2020 04:47:15 +0400 Subject: [PATCH] Final rewrite of CLI arguments form. Now program has single entry point that serves everything. README to be updated. --- piggybank/__init__.py | 13 +-- piggybank/cli/__init__.py | 68 +------------- piggybank/cli/main.py | 178 +++++++++++++++++++++++++++++++++++++ piggybank/cli/put.py | 61 ------------- piggybank/cli/show.py | 108 ---------------------- piggybank/cli/take.py | 57 ------------ piggybank/configuration.py | 4 + piggybank/currencies.py | 2 +- piggybank/piggybank.py | 4 +- setup.cfg | 4 +- 10 files changed, 193 insertions(+), 306 deletions(-) create mode 100644 piggybank/cli/main.py delete mode 100644 piggybank/cli/put.py delete mode 100644 piggybank/cli/show.py delete mode 100644 piggybank/cli/take.py diff --git a/piggybank/__init__.py b/piggybank/__init__.py index 3f47e0c..d6d8a04 100644 --- a/piggybank/__init__.py +++ b/piggybank/__init__.py @@ -1,9 +1,9 @@ __all__ = ["__date__", "__version__", "__author__", "__author_email__", "__copyright__", "__license__", "PIGGYBANK_FILE_EXTENSION", - "print_program_version"] + "VERSION"] -__date__ = "6 June 2020" +__date__ = "7 June 2020" __version__ = "1.0.0" __author__ = "Alexander \"Arav\" Andreev" __author_email__ = "me@arav.top" @@ -17,10 +17,5 @@ http://www.wtfpl.net/ for more details.""" PIGGYBANK_FILE_EXTENSION = ".pb" - - -def print_program_version() -> None: - """Print information about program. Includes name and version; copyright - notice and license.""" - print(f"PiggyBank ver. {__version__} ({__date__})\n\n{__copyright__}\n" - f"\n{__license__}") +VERSION = f"PiggyBank ver. {__version__} ({__date__})\n\n{__copyright__}\n" \ + f"\n{__license__}" diff --git a/piggybank/cli/__init__.py b/piggybank/cli/__init__.py index d9a9c05..92813c2 100644 --- a/piggybank/cli/__init__.py +++ b/piggybank/cli/__init__.py @@ -1,74 +1,12 @@ -from re import search -from typing import List, Callable -from sys import argv +from typing import List -from piggybank import print_program_version -from piggybank.configuration import Configuration from piggybank.currencies import CURRENCIES -__all__ = ["handle_default_arguments", "complement_list_of_coins", - "print_supported_currencies", "decimal_to_float"] - - -USAGE_COMMON: str = "Usage: piggybank-* " \ - "[(-h|--help)|(-v|--version)|(-L|--list-currencies)]\n" \ - "A set of common flags. Only one could be specified.\n" \ - "-h, --help -- print this help;\n" \ - "-v, --version -- print program version;\n" \ - "-L, --list-currencies -- print supported currencies;\n" \ - "--set-default-currency CURRENCY -- set default currency.\n" - - -def parse_common_arguments(args: str): - r = r"(?P-h|--help)|(?P-v|--version)" \ - r"|(?P-L|--list-currencies)" \ - r"|(?=--set-default-currency (?P\w+))" - 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, - "default-currency": argd["default_currency"] } - return None - - -def handle_default_arguments(args: dict, help_func: Callable) -> None: - cargs = parse_common_arguments(' '.join(argv)) - if not cargs is None: - if cargs["help"]: - help_func() - print(USAGE_COMMON) - exit() - elif cargs["version"]: - print_program_version() - exit() - elif cargs["list-currencies"]: - print_supported_currencies() - exit() - elif not cargs["default-currency"] is None: - conf = Configuration() - conf["default-currency"] = cargs["default-currency"] - conf.save() - exit() - - -def print_supported_currencies() -> None: - """Print a list of supported currencies.""" - print("Supported currencies are:") - for cur in CURRENCIES: - print(f" {cur:^4} ┃ {CURRENCIES[cur]['name']:^31}" - f" ┃ {CURRENCIES[cur]['description']}") - print("Default currency is", Configuration()["default-currency"]) +__all__ = ["complement_list_of_coins"] 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] * (CURRENCIES[currency]["count"] - len(coins)) + offset_array = [0] * (len(CURRENCIES[currency]) - len(coins)) return offset_array + coins if not _reversed else coins + offset_array - -def decimal_to_float(lst: List[int]) -> List[float]: - """Converts decimal style of storing money amounts to float.""" - return list(map(lambda x: x / 100, lst)) diff --git a/piggybank/cli/main.py b/piggybank/cli/main.py new file mode 100644 index 0000000..c9b5144 --- /dev/null +++ b/piggybank/cli/main.py @@ -0,0 +1,178 @@ +"""CLI: main""" + +from re import search, split +from sys import argv, stderr + +from piggybank import VERSION +from piggybank.cli import complement_list_of_coins +from piggybank.configuration import Configuration +from piggybank.currencies import Currency, BaseCurrencyError, CURRENCIES +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--set-default-currency -- set currency that'll be used as a default; +\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 piggybank; +\tof CURRENCY -- set a partcular currency for a piggybank; +\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)" \ + r"|(?=--set-default-currency (?P\w+))" + 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, + "default-currency": argd["default_currency"] } + 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: + cur = CURRENCIES[pb.currency] + def print_separator(l: str, m: str, r: str): + print(f"{l}{'━'*21}{m}{'━'*5}{m}{m.join(['━'*12]*cur.count)}{r}") + + print_separator("┏", "┳", "┓") + print(f"┃{'Timestamp':^21}┃ I/O ┃" \ + f"{'┃'.join(f'{n:^12}' for n in cur.coin_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: + cur = CURRENCIES[pb.currency] + def print_separator(l: str, lm: str, rm: str, r:str): + print(f"{l}{'━'*27}{lm}{rm.join(['━'*12]*cur.count)}{r}") + + print_separator("┏", "┳", "┳", "┓") + print(f"┃{cur.name:^27}┃{'┃'.join(f'{n:^12}' for n in cur.coin_names)}┃") + print_separator("┣", "╋", "╋", "┫") + s = sum_transactions(pb.transactions) + print(f"┃{'Counts':^27}┃{'┃'.join(f'{c:^12}' for c in s)}┃") + print_separator("┣", "╋", "╋", "┫") + s = cur.multiply(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:^{12*cur.count+cur.count-1}}┃") + 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_user_defined_currencies() -> None: + if not "currency" in Configuration().items: + return + for iso, cur in Configuration()["currency"].items(): + CURRENCIES[iso] = Currency.from_string(cur) + + +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"]: + print_supported_currencies() + exit() + elif not cargs["default-currency"] is None: + conf = Configuration() + conf["default-currency"] = cargs["default-currency"] + conf.save() + exit() + + args = parse_arguments(' '.join(argv)) + if not args: + print(USAGE) + exit() + + try: + load_user_defined_currencies() + try: + pb = PiggyBank.from_file(args["file"]) + except FileNotFoundError: + if args["action"] == "in": + currency = Configuration()["default-currency"] \ + if args["currency"] is None 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) diff --git a/piggybank/cli/put.py b/piggybank/cli/put.py deleted file mode 100644 index b3c92e2..0000000 --- a/piggybank/cli/put.py +++ /dev/null @@ -1,61 +0,0 @@ -"""CLI: Put a set of coins into a piggy bank.""" - -from re import split, search -from sys import argv, stderr - -from piggybank.configuration import Configuration -from piggybank.cli import complement_list_of_coins, handle_default_arguments -from piggybank.currencies import CURRENCIES, BaseCurrencyError -from piggybank.piggybank import PiggyBank - -__all__ = ["main"] - - -USAGE_PUT = "Usage: piggybank-put [-r|--reversed] COINS in FILE of CURRENCY\n" \ - "Put a set of coins in a piggybank. Set a currency of a new one.\n\n" \ - "-r, --reversed -- use reversed order of COINS (from greater to least);\n" \ - "COINS -- array of comma or whitespace separated coins;\n" \ - "in FILE -- .pb file name of your piggybank;\n" \ - "of CURRENCY -- set a currency for a new piggybank.\n" - - -def parse_put_arguments(args): - r = r"(?P-r|--reversed)? ?(?P[\d ,]+) in (?P\S+)(?= of (?P\S+))?" - argd = search(r, args) - if not argd is None: - argd = argd.groupdict() - return { - "coins": list(map(int, split(r"\D",argd["coins"]))), - "file": argd["file"], - "currency": Configuration()["default-currency"] \ - if argd["currency"] is None else argd["currency"], - "reversed": not argd["reversed"] is None } - return None - - -def main() -> None: - handle_default_arguments(' '.join(argv[1:]), lambda: print(USAGE_PUT)) - args = parse_put_arguments(' '.join(argv[1:])) - - if args is None: - print(USAGE_PUT) - exit() - - try: - try: - piggybank = PiggyBank.from_file(args["file"]) - except FileNotFoundError: - currency = Configuration()["default-currency"] \ - if args["currency"] is None else args["currency"] - piggybank = PiggyBank(currency) - coins = complement_list_of_coins(args["coins"], piggybank.currency, - args["reversed"]) - piggybank.transact(coins) - piggybank.save(args["file"]) - except BaseCurrencyError 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) diff --git a/piggybank/cli/show.py b/piggybank/cli/show.py deleted file mode 100644 index 64ebf33..0000000 --- a/piggybank/cli/show.py +++ /dev/null @@ -1,108 +0,0 @@ -"""CLI: Show summarised information on a piggybank.""" - -from re import match -from sys import argv, exit, stderr - -from piggybank.cli import handle_default_arguments -from piggybank.currencies import CURRENCIES, BaseCurrencyError -from piggybank.piggybank import PiggyBank -from piggybank.transaction import sum_transactions, multiply_transactions - -__all__ = ["main"] - - -USAGE_SHOW: str = "Usage: piggybank-show FILE [with t,transactions]\n\n" \ - "Show statistics about a piggybank.\n" \ - "FILE -- .pb file name of your piggybank;\n" \ - "with (t|transaction) -- also list all transactions.\n" - -DEFAULT_COIN_CENTERING: int = 10 - - -def parse_show_arguments(args): - r = r"^(?P\S+)(?= with (?Pt|transactions))?" - argd = match(r, args) - if not argd is None: - argd = argd.groupdict() - return { - "file": argd["file"], - "transactions": not argd["transactions"] is None } - return None - - -def print_summary(piggybank: PiggyBank, - centering: int = DEFAULT_COIN_CENTERING) -> None: - """Print summarised information on a piggy bank. - Prints a table with totals of how much coins of which face value are in a - piggy bank; A total sum converted to its currency for each face value and - overall total sum in a currency of a piggy bank.""" - def print_separator(left="┣", lmiddle="╋", rmiddle="╋", right="┫"): - line = rmiddle.join('━' * centering - for _ in - range(CURRENCIES[piggybank.currency]['count'])) - print(f"{left}{'━'*27}{lmiddle}{line}{right}") - - cc = sum_transactions(piggybank.transactions) - cs = multiply_transactions(piggybank.transactions, piggybank.currency) - ct = sum(cc) - - nline = "┃".join([f'{l:^{centering}}' - for l in CURRENCIES[piggybank.currency]["names"]]) - nline_len = len(nline) - - print_separator(left="┏", lmiddle="┳", rmiddle="━", right="┓") - print(f"┃{'currency':^27}┃" - f"{CURRENCIES[piggybank.currency]['name']:^{nline_len}}┃") - print_separator(rmiddle="┳") - print(f"┃{'face values':^27}┃{nline}┃") - print_separator() - print(f"┃{'amount':^27}┃{'┃'.join([f'{c:^{centering}}' for c in cc])}┃") - print_separator() - print(f"┃{'sum':^27}┃" - f"{'┃'.join(['{:^{}.2f}'.format(c / 100, centering) for c in cs])}┃") - print_separator(rmiddle="┻") - print(f"┃{'total':^27}┃{'{:^{}.2f}'.format(ct / 100, nline_len)}┃") - print_separator(left="┗", lmiddle="┻", rmiddle="━", right="┛") - - -def print_transactions(piggybank, centering=DEFAULT_COIN_CENTERING): - """Print a list of all transactions stored in a piggy bank.""" - def print_separator(left="┏", middle="┳", right="┓"): - line = middle.join('━' * centering - for _ in - range(CURRENCIES[piggybank.currency]['count'])) - print(f"{left}━━━━━━━━━━━━━━━━━━━━━{middle}━━━━━{middle}{line}{right}") - - nline = "┃".join([f'{l:^{centering}}' - for l in CURRENCIES[piggybank.currency]["names"]]) - - print_separator() - print(f"┃{'Timestamp':^21}┃ I/O ┃{nline}┃") - print_separator("┣", "╋", "┫") - for tr in piggybank.transactions: - coin_line = "┃".join([f'{c:^{centering}}' for c in tr.coins]) - ts = tr.timestamp.replace("T", " ") - print(f"┃ {ts} ┃{tr.direction:^5}┃{coin_line}┃") - print_separator("┗", "┻", "┛") - - -def main() -> None: - handle_default_arguments(' '.join(argv[1:]), lambda: print(USAGE_SHOW)) - args = parse_show_arguments(' '.join(argv[1:])) - - if args is None: - print(USAGE_SHOW) - exit() - - try: - piggybank = PiggyBank.from_file(args["file"]) - print_summary(piggybank) - if args["transactions"]: - print_transactions(piggybank) - except BaseCurrencyError as err: - print(f"{type(err).__name__}:", err, file=stderr) - except FileNotFoundError as err: - print(f"{type(err).__name__}:", f"{err} doesn't exist.", file=stderr) - except Exception as err: - print(f"Something went exceptionally wrong. Error:", - f"{type(err).__name__}:", err, file=stderr) diff --git a/piggybank/cli/take.py b/piggybank/cli/take.py deleted file mode 100644 index 65905e3..0000000 --- a/piggybank/cli/take.py +++ /dev/null @@ -1,57 +0,0 @@ -"""CLI: Take a set of coins from a coin box.""" - -from re import match, split -from sys import argv, exit, stderr - -from piggybank import print_program_version -from piggybank.configuration import Configuration -from piggybank.cli import complement_list_of_coins, handle_default_arguments -from piggybank.currencies import CURRENCIES, BaseCurrencyError -from piggybank.piggybank import PiggyBank -from piggybank.transaction import TYPE_OUTCOME - -__all__ = ["main"] - - -USAGE_TAKE: str = "Usage: piggybank-take [-r|--reversed] COINS from FILE\n\n" \ - "Take a set of coins from a piggybank.\n" \ - "-r, --reversed -- use reversed order of COINS (from greater to least);\n" \ - "COINS -- array of comma or whitespace separated coins;\n" \ - "from FILE -- .pb filename.\n" - - -def parse_take_arguments(args): - r = r"^(?P-r|--reversed)? ?(?P[\d ,]+) from (?P\S+)" - argd = match(r, args) - if not argd is None: - argd = argd.groupdict() - return { - "coins": list(map(int, split(r"\D", argd["coins"]))), - "file": argd["file"], - "reversed": not argd["reversed"] is None } - return None - - -def main(): - handle_default_arguments(' '.join(argv[1:]), lambda: print(USAGE_TAKE)) - args = parse_take_arguments(' '.join(argv[1:])) - - if args is None: - print(USAGE_TAKE) - exit() - - try: - piggybank = PiggyBank.from_file(args["file"]) - coins = complement_list_of_coins(args["coins"], piggybank.currency, - args["reversed"]) - piggybank.transact(coins, TYPE_OUTCOME) - piggybank.save(args["file"]) - except BaseCurrencyError as err: - print(f"{type(err).__name__}:", err, file=stderr) - except ValueError as err: - print(f"{type(err).__name__}:", err, file=stderr) - except FileNotFoundError as err: - print(f"{type(err).__name__}:", f"{err} doesn't exist.", file=stderr) - except Exception as err: - print(f"Something went exceptionally wrong. Error:", - f"{type(err).__name__}:", err, file=stderr) diff --git a/piggybank/configuration.py b/piggybank/configuration.py index b66d1e2..6ea65ff 100644 --- a/piggybank/configuration.py +++ b/piggybank/configuration.py @@ -59,6 +59,10 @@ class Configuration: for key, value in self._configuration.items(): cf.write(f"{key} = {value}\n") + @property + def items(self) -> dict: + return self._configuration + def __getitem__(self, key: str) -> Union[int, str, bool]: return self._configuration[key] diff --git a/piggybank/currencies.py b/piggybank/currencies.py index 1d166cd..5734214 100644 --- a/piggybank/currencies.py +++ b/piggybank/currencies.py @@ -128,7 +128,7 @@ CURRENCIES: Dict[str, Currency] = { [1, 5, 10, 50, 1_00, 2_00, 5_00, 10_00]), "SRUB": Currency("SRUB", "Russian ruble (shortened)", "Russian Federation. Excluding coins of 1 and 5 kopek.", - 8, ["10к.", "50к.", "1₽", "2₽", "5₽", "10₽"], + 6, ["10к.", "50к.", "1₽", "2₽", "5₽", "10₽"], [10, 50, 1_00, 2_00, 5_00, 10_00]), "BYN": Currency("BYN", "Belarusian ruble", "Belarus", 8, ["1к.", "2к.", "5к.", "10к.", "20к.", "50к.", "1р.", "2р."], diff --git a/piggybank/piggybank.py b/piggybank/piggybank.py index 3f1ee88..b501314 100644 --- a/piggybank/piggybank.py +++ b/piggybank/piggybank.py @@ -23,10 +23,10 @@ class PiggyBank: def transact(self, coins: List[int], direction: str = TYPE_INCOME) -> None: """Make a transaction.""" - if len(coins) != CURRENCIES[self._currency]["count"]: + if len(coins) != len(CURRENCIES[self._currency]): raise ValueError("Length of passed coins list doesn't match the " \ f"currency's coins count. ({len(coins)} " \ - f"!= {CURRENCIES[self._currency]['count']})") + f"!= {len(CURRENCIES[self._currency])})") self._last_transaction = Transaction(coins, direction) self._transactions.append(self._last_transaction) for coin_count in sum_transactions(self._transactions): diff --git a/setup.cfg b/setup.cfg index 157fcdf..54d6451 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,6 +48,4 @@ formats = gztar [options.entry_points] console_scripts = - piggybank-put = piggybank.cli.put:main - piggybank-show = piggybank.cli.show:main - piggybank-take = piggybank.cli.take:main + piggybank = piggybank.cli.main:main \ No newline at end of file