1
0

Final rewrite of CLI arguments form. Now program has single entry point that serves everything. README to be updated.

This commit is contained in:
Alexander Andreev 2020-06-07 04:47:15 +04:00
parent ab6647ceb3
commit f4f026f8b2
10 changed files with 193 additions and 306 deletions

View File

@ -1,9 +1,9 @@
__all__ = ["__date__", "__version__", "__author__", "__author_email__", __all__ = ["__date__", "__version__", "__author__", "__author_email__",
"__copyright__", "__license__", "PIGGYBANK_FILE_EXTENSION", "__copyright__", "__license__", "PIGGYBANK_FILE_EXTENSION",
"print_program_version"] "VERSION"]
__date__ = "6 June 2020" __date__ = "7 June 2020"
__version__ = "1.0.0" __version__ = "1.0.0"
__author__ = "Alexander \"Arav\" Andreev" __author__ = "Alexander \"Arav\" Andreev"
__author_email__ = "me@arav.top" __author_email__ = "me@arav.top"
@ -17,10 +17,5 @@ http://www.wtfpl.net/ for more details."""
PIGGYBANK_FILE_EXTENSION = ".pb" PIGGYBANK_FILE_EXTENSION = ".pb"
VERSION = f"PiggyBank ver. {__version__} ({__date__})\n\n{__copyright__}\n" \
f"\n{__license__}"
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__}")

View File

@ -1,74 +1,12 @@
from re import search from typing import List
from typing import List, Callable
from sys import argv
from piggybank import print_program_version
from piggybank.configuration import Configuration
from piggybank.currencies import CURRENCIES from piggybank.currencies import CURRENCIES
__all__ = ["handle_default_arguments", "complement_list_of_coins", __all__ = ["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<help>-h|--help)|(?P<version>-v|--version)" \
r"|(?P<list_currencies>-L|--list-currencies)" \
r"|(?=--set-default-currency (?P<default_currency>\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"])
def complement_list_of_coins(coins: List[int], currency: str, def complement_list_of_coins(coins: List[int], currency: str,
_reversed: bool = False) -> List[int]: _reversed: bool = False) -> List[int]:
"""Complements list of coins up to the count of currency's coins.""" """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 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))

178
piggybank/cli/main.py Normal file
View File

@ -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<help>-h|--help)|(?P<version>-v|--version)" \
r"|(?P<list_currencies>-L|--list-currencies)" \
r"|(?=--set-default-currency (?P<default_currency>\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<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:
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)

View File

@ -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<reversed>-r|--reversed)? ?(?P<coins>[\d ,]+) in (?P<file>\S+)(?= of (?P<currency>\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)

View File

@ -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<file>\S+)(?= with (?P<transactions>t|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)

View File

@ -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<reversed>-r|--reversed)? ?(?P<coins>[\d ,]+) from (?P<file>\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)

View File

@ -59,6 +59,10 @@ class Configuration:
for key, value in self._configuration.items(): for key, value in self._configuration.items():
cf.write(f"{key} = {value}\n") cf.write(f"{key} = {value}\n")
@property
def items(self) -> dict:
return self._configuration
def __getitem__(self, key: str) -> Union[int, str, bool]: def __getitem__(self, key: str) -> Union[int, str, bool]:
return self._configuration[key] return self._configuration[key]

View File

@ -128,7 +128,7 @@ CURRENCIES: Dict[str, Currency] = {
[1, 5, 10, 50, 1_00, 2_00, 5_00, 10_00]), [1, 5, 10, 50, 1_00, 2_00, 5_00, 10_00]),
"SRUB": Currency("SRUB", "Russian ruble (shortened)", "SRUB": Currency("SRUB", "Russian ruble (shortened)",
"Russian Federation. Excluding coins of 1 and 5 kopek.", "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]), [10, 50, 1_00, 2_00, 5_00, 10_00]),
"BYN": Currency("BYN", "Belarusian ruble", "Belarus", 8, "BYN": Currency("BYN", "Belarusian ruble", "Belarus", 8,
["1к.", "2к.", "5к.", "10к.", "20к.", "50к.", "1р.", "2р."], ["1к.", "2к.", "5к.", "10к.", "20к.", "50к.", "1р.", "2р."],

View File

@ -23,10 +23,10 @@ class PiggyBank:
def transact(self, coins: List[int], direction: str = TYPE_INCOME) -> None: def transact(self, coins: List[int], direction: str = TYPE_INCOME) -> None:
"""Make a transaction.""" """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 " \ raise ValueError("Length of passed coins list doesn't match the " \
f"currency's coins count. ({len(coins)} " \ f"currency's coins count. ({len(coins)} " \
f"!= {CURRENCIES[self._currency]['count']})") f"!= {len(CURRENCIES[self._currency])})")
self._last_transaction = Transaction(coins, direction) self._last_transaction = Transaction(coins, direction)
self._transactions.append(self._last_transaction) self._transactions.append(self._last_transaction)
for coin_count in sum_transactions(self._transactions): for coin_count in sum_transactions(self._transactions):

View File

@ -48,6 +48,4 @@ formats = gztar
[options.entry_points] [options.entry_points]
console_scripts = console_scripts =
piggybank-put = piggybank.cli.put:main piggybank = piggybank.cli.main:main
piggybank-show = piggybank.cli.show:main
piggybank-take = piggybank.cli.take:main