1
0

First part of a massive refactoring. Also Configuration class that'll be used to store a default currency in a config file was added.

This commit is contained in:
Alexander Andreev 2020-06-04 03:12:31 +04:00
parent d27f42644d
commit f99ce3f6cc
4 changed files with 181 additions and 101 deletions

View File

@ -1,8 +1,8 @@
__date__ = "29 December 2019" __date__ = "4 June 2020"
__version__ = "1.0.0" __version__ = "1.0.0"
__author__ = "Alexander \"Arav\" Andreev" __author__ = "Alexander \"Arav\" Andreev"
__email__ = "me@aravs.ru" __email__ = "me@arav.top"
__copyright__ = f"Copyright (c) 2019 {__author__} <{__email__}>" __copyright__ = f"Copyright (c) 2020 {__author__} <{__email__}>"
__license__ = \ __license__ = \
"""This program is free software. It comes without any warranty, to """This program is free software. It comes without any warranty, to
the extent permitted by applicable law. You can redistribute it the extent permitted by applicable law. You can redistribute it
@ -12,7 +12,6 @@ http://www.wtfpl.net/ for more details."""
PIGGYBANK_FILE_EXTENSION = ".pb" PIGGYBANK_FILE_EXTENSION = ".pb"
PIGGYBANK_CONFIGURATION_FILE = "piggybank.conf"
def print_program_version() -> None: def print_program_version() -> None:

View File

@ -0,0 +1,50 @@
"""An implementation of a simple key=value configuration."""
from os import getenv
from os.path import exists, join
from platform import system
from typing import Union
__all__ = ["Configuration", "get_configuration_path"]
DEFAULT_CONFIGURATION = {
"default-currency": "SRUB"
}
DEFAULT_CONFIGURATION_FILE = join(get_configuration_path(), "piggybank.conf")
def get_configuration_path():
if system() == "Linux":
return getenv("XDG_CONFIG_HOME") or f"{getenv('HOME')}/.config"
elif system() == "Windows":
return getenv("APPDATA")
class Configuration:
def __init__(self, configuration_file: str = DEFAULT_CONFIGURATION_FILE,
default_configuration: dict = DEFAULT_CONFIGURATION) -> None:
self._configuration_file = configuration_file
self._configuration = dict()
if exists(self._configuration_file):
self.load()
elif not default_configuration is None:
self._configuration = default_configuration
self.save()
def load(self) -> None:
for line in open(self._configuration_file, 'r'):
key, value = line.split(" = ")
self._configuration[key] = value
def save(self) -> None:
with open(self._configuration_file, 'w') as cf:
for key, value in self._configuration.items():
cf.write(f"{key} = {value}\n")
def __getitem__(self, key: str) -> Union[int, str, bool]:
return self._configuration[key]
def __setitem__(self, key: str, value: Union[int, str, bool]) -> None:
self._configuration[key] = value

View File

@ -5,105 +5,52 @@ from os.path import exists
from typing import List from typing import List
from piggybank import PIGGYBANK_FILE_EXTENSION from piggybank import PIGGYBANK_FILE_EXTENSION
from piggybank.currencies import CURRENCIES, DEFAULT_CURRENCY, \ from piggybank.currencies import CURRENCIES, \
CurrencyIsNotSupportedError, CurrenciesCoinCountMismatchError, \ CurrencyIsNotSupportedError, CurrenciesCoinCountMismatchError, \
CurrencyMismatchError CurrencyMismatchError
from piggybank.transaction import Transaction, TYPE_INCOME from piggybank.transaction import Transaction, sum_transactions, TYPE_INCOME
__all__ = ["PiggyBank"] __all__ = ["PiggyBank"]
class PiggyBank: class PiggyBank:
"""This class stores array of transactions and perform some actions on it.""" """This class stores array of transactions and perform some actions on it."""
def __init__(self, currency: str = DEFAULT_CURRENCY) -> None: def __init__(self, currency: str = None) -> None:
if currency.upper() in CURRENCIES: if not currency is None:
self._currency = currency.upper() self.currency = currency
else:
raise CurrencyIsNotSupportedError
self._transactions = [] self._transactions = []
self._last_transaction = None
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) != CURRENCIES[self._currency]["count"]:
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"!= {CURRENCIES[self._currency]['count']})")
self._last_transaction = Transaction(coins, direction)
self._transactions.append(Transaction(coins, direction)) self._transactions.append(self._last_transaction)
for coin_count in sum_transactions(self._transactions):
for coin_count in self.count:
if coin_count < 0: if coin_count < 0:
del self._transactions[-1] del self._transactions[-1]
self._last_transaction = None
raise ValueError( raise ValueError(
"You can't take out more than you have.") "You can't take out more than you have.")
@property
def count(self) -> List[int]:
"""Returns a list of counts for each face value in total."""
count = [0] * CURRENCIES[self.currency]["count"]
for tr in self.transactions:
count = [x + y if tr.direction == TYPE_INCOME
else x - y for x, y in zip(count, tr.coins)]
return count
@property
def sum(self) -> List[int]:
"""Returns a list of sums for each face value multiplied by its
currency's multipilers."""
return [x * y for x, y
in zip(self.count, CURRENCIES[self.currency]["multipliers"])]
@property
def total(self) -> int:
"""Returns a total amount of money stored in a PiggyBank."""
return sum(self.sum)
@staticmethod
def from_file(filename: str) -> PiggyBank:
"""Returns a PiggyBank object loaded from a file with name
`filename`."""
piggybank = PiggyBank()
piggybank.load(filename)
return piggybank
def save(self, filename: str) -> None:
"""Writes a PiggyBank object to a file with name `filename`."""
if not filename.endswith(PIGGYBANK_FILE_EXTENSION):
filename += PIGGYBANK_FILE_EXTENSION
with open(filename, 'w') as _file:
_file.write(f"{self.currency}\n")
for transaction in self.transactions:
_file.write(f"{str(transaction)}\n")
def load(self, filename: str) -> None:
"""Loads a PiggyBank from a file with name `filename`."""
if not filename.endswith(PIGGYBANK_FILE_EXTENSION):
filename += PIGGYBANK_FILE_EXTENSION
if not exists(filename):
raise FileNotFoundError(filename)
with open(filename, 'r') as _file:
currency = _file.readline()[:-1]
if currency not in CURRENCIES:
raise CurrencyIsNotSupportedError
else:
self.currency = currency
for transaction in _file:
self._transactions.append(Transaction.from_string(transaction))
@property @property
def currency(self) -> str: def currency(self) -> str:
"""Returns a currency of a PiggyBank.""" """Returns a currency of a PiggyBank."""
return self._currency return self._currency
@currency.setter @currency.setter
def currency(self, currency: str) -> None: def currency(self, currency: str = None) -> None:
"""Sets a currency of a PiggyBank with check for support. And if count """Sets a currency of a PiggyBank with check for support. And if count
of coins doesn't match the old currency it won't set a new one.""" of coins doesn't match the old currency it won't set a new one."""
currency = currency.upper() currency = currency.upper()
if currency not in CURRENCIES: if not currency in CURRENCIES:
raise CurrencyIsNotSupportedError raise CurrencyIsNotSupportedError
if CURRENCIES[currency]["count"] != CURRENCIES[self.currency]["count"]: if not self._currency is None and \
CURRENCIES[currency]["count"] \
!= CURRENCIES[self._currency]["count"]:
raise CurrenciesCoinCountMismatchError raise CurrenciesCoinCountMismatchError
self._currency = currency self._currency = currency
@ -112,13 +59,49 @@ class PiggyBank:
"""Returns a list of transactions.""" """Returns a list of transactions."""
return self._transactions return self._transactions
@staticmethod
def from_file(filename: str) -> PiggyBank:
"""Returns a PiggyBank object loaded from a file."""
piggybank = PiggyBank()
piggybank.load(filename)
return piggybank
def save(self, filename: str) -> None:
"""Appends last successful transaction to a file. File will be created
if not exist yet."""
if self._last_transaction is None:
raise ValueError("No successful transaction found.")
if not filename.endswith(PIGGYBANK_FILE_EXTENSION):
filename += PIGGYBANK_FILE_EXTENSION
if not exists(filename):
with open(filename, 'w') as pbf:
pbf.write(f"{self.currency}\n{self._last_transaction}\n")
else:
with open(filename, 'a') as pbf:
pbf.write(f"{self._last_transaction}\n")
def load(self, filename: str) -> None:
"""Loads a PiggyBank from a file."""
if not filename.endswith(PIGGYBANK_FILE_EXTENSION):
filename += PIGGYBANK_FILE_EXTENSION
if not exists(filename):
raise FileNotFoundError(filename)
with open(filename, 'r') as pbf:
currency = pbf.readline()[:-1]
if currency not in CURRENCIES:
raise CurrencyIsNotSupportedError
else:
self._currency = currency
for line in pbf:
self._transactions.append(Transaction.from_string(line))
def __eq__(self, piggybank: PiggyBank) -> str: def __eq__(self, piggybank: PiggyBank) -> str:
"""It compares only currency.""" """Compares only currency."""
return self.currency == piggybank._currency return self._currency == piggybank.currency
def __add__(self, piggybank: PiggyBank) -> PiggyBank: def __add__(self, piggybank: PiggyBank) -> PiggyBank:
if self != piggybank: if self != piggybank:
raise CurrencyMismatchError raise CurrencyMismatchError
new = PiggyBank(self.currency) new = PiggyBank(self._currency)
new._transactions = self.transactions + piggybank._transactions new._transactions = self._transactions + piggybank._transactions
return new return new

View File

@ -1,18 +1,44 @@
"""Transaction class implementation.""" """Transaction class implementation."""
from __future__ import annotations from __future__ import annotations
from operator import add, sub, mul
from time import strftime, strptime, gmtime from time import strftime, strptime, gmtime
from typing import Optional, List from typing import Optional, List, Union
__all__ = ["Transaction", "TYPE_INCOME", "TYPE_OUTCOME", "TIME_FORMAT"] from piggybank.currencies import CURRENCIES
TYPE_INCOME = "in" __all__ = ["Transaction", "sum_transactions", "multiply_transactions",
TYPE_OUTCOME = "out" "TYPE_INCOME", "TYPE_OUTCOME", "TIME_FORMAT"]
TYPE_INCOME = "i"
TYPE_OUTCOME = "o"
TIME_FORMAT = "%Y-%m-%dT%H:%M:%S" TIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
def sum_transactions(transactions: List[Transaction]) -> List[int]:
"""Sums all the coins and returns resulting list. All transactions must have
same length of coins' list."""
coins = [0] * len(transactions[0].coins)
for transaction in transactions:
if transaction.direction == TYPE_INCOME:
coins = list(map(add, transaction.coins, coins))
else:
coins = list(map(sub, transaction.coins, coins))
return coins
def multiply_transactions(transactions: Union[List[Transaction], Transaction],
currency: str) -> List[int]:
"""Multiplies coins' counts by currency."""
if type(transactions) is Transaction:
return list(map(mul, transactions.coins, \
CURRENCIES[currency]["multipliers"]))
else:
coins = sum_transactions(transactions)
return list(map(mul, coins, CURRENCIES[currency]["multipliers"]))
class Transaction: class Transaction:
"""An object that holds a transaction. """An object that holds a single transaction.
Arguments: Arguments:
- coins -- a list of numbers represent count for each face value; - coins -- a list of numbers represent count for each face value;
@ -22,33 +48,55 @@ class Transaction:
def __init__(self, coins: List[int], direction: str = TYPE_INCOME, def __init__(self, coins: List[int], direction: str = TYPE_INCOME,
timestamp: Optional[str] = None) -> None: timestamp: Optional[str] = None) -> None:
self.coins = coins self.coins = coins
if direction in [TYPE_INCOME, TYPE_OUTCOME]:
self.direction = direction self.direction = direction
self.timestamp = timestamp
@property
def coins(self) -> List[int]:
return self._coins
@coins.setter
def coins(self, coins: List[int]) -> None:
if coins is list:
self._coins = coins
else: else:
raise ValueError(f"Direction may only be" raise TypeError("Coins must be of type 'list'.")
f"'{TYPE_INCOME}' or '{TYPE_OUTCOME}'")
@property
def direction(self) -> str:
return self._direction
@direction.setter
def direction(self, direction: str = TYPE_INCOME) -> None:
if direction in [TYPE_INCOME, TYPE_OUTCOME]:
self._direction = direction
else:
raise ValueError("Direction may only be of TYPE_INCOME(\"i\") " \
"or TYPE_OUTCOME(\"o\").")
@property
def timestamp(self) -> str:
return self._timestamp
@timestamp.setter
def timestamp(self, timestamp: str = None) -> None:
if timestamp is None: if timestamp is None:
self.timestamp = strftime(TIME_FORMAT, gmtime()) self._timestamp = strftime(TIME_FORMAT, gmtime())
else: else:
try: try:
strptime(timestamp, TIME_FORMAT) strptime(timestamp, TIME_FORMAT)
except ValueError: except ValueError:
raise ValueError(f"Timestamp {timestamp} has wrong format. " raise ValueError(f"Timestamp {timestamp} has wrong format. " \
f"The right one is '{TIME_FORMAT}''") f"The right one is \"{TIME_FORMAT}\".")
self.timestamp = timestamp self._timestamp = timestamp
@staticmethod @staticmethod
def from_string(transaction: str) -> Transaction: def from_string(transaction: str) -> Transaction:
"""Makes a Transaction object from its string output.""" """Makes a Transaction object from its string output."""
timestamp, direction, coins = transaction.split() timestamp, direction, coins = transaction.split()
coins = list(map(int, coins.split(","))) coins = list(map(int, coins.split(",")))
return Transaction(coins, direction, timestamp) return Transaction(coins, direction, timestamp)
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.timestamp} {self.direction} " \ return f"{self.timestamp} {self.direction} " \
f"{','.join(str(c) for c in self.coins)}" f"{','.join(map(str, self.coins))}"
def __repr__(self) -> str:
return f"Transaction(coins={self.coins!r}," \
f"direction={self.direction!r}, timestamp={self.timestamp!r})"