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:
parent
d27f42644d
commit
f99ce3f6cc
@ -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:
|
||||||
|
50
piggybank/configuration.py
Normal file
50
piggybank/configuration.py
Normal 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
|
@ -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
|
||||||
|
@ -1,54 +1,102 @@
|
|||||||
"""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;
|
||||||
- direction -- is this income or outcome. Takes TYPE_INCOME
|
- direction -- is this income or outcome. Takes TYPE_INCOME
|
||||||
or TYPE_OUTCOME;
|
or TYPE_OUTCOME;
|
||||||
- timestamp -- date and time formated accordingly to TIME_FORMAT."""
|
- timestamp -- date and time formated accordingly to TIME_FORMAT."""
|
||||||
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})"
|
|
||||||
|
Loading…
Reference in New Issue
Block a user