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"
__author__ = "Alexander \"Arav\" Andreev"
__email__ = "me@aravs.ru"
__copyright__ = f"Copyright (c) 2019 {__author__} <{__email__}>"
__email__ = "me@arav.top"
__copyright__ = f"Copyright (c) 2020 {__author__} <{__email__}>"
__license__ = \
"""This program is free software. It comes without any warranty, to
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_CONFIGURATION_FILE = "piggybank.conf"
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 piggybank import PIGGYBANK_FILE_EXTENSION
from piggybank.currencies import CURRENCIES, DEFAULT_CURRENCY, \
from piggybank.currencies import CURRENCIES, \
CurrencyIsNotSupportedError, CurrenciesCoinCountMismatchError, \
CurrencyMismatchError
from piggybank.transaction import Transaction, TYPE_INCOME
from piggybank.transaction import Transaction, sum_transactions, TYPE_INCOME
__all__ = ["PiggyBank"]
class PiggyBank:
"""This class stores array of transactions and perform some actions on it."""
def __init__(self, currency: str = DEFAULT_CURRENCY) -> None:
if currency.upper() in CURRENCIES:
self._currency = currency.upper()
else:
raise CurrencyIsNotSupportedError
def __init__(self, currency: str = None) -> None:
if not currency is None:
self.currency = currency
self._transactions = []
self._last_transaction = None
def transact(self, coins: List[int], direction: str = TYPE_INCOME) -> None:
"""Make a transaction."""
if len(coins) != CURRENCIES[self.currency]["count"]:
raise ValueError("Length of passed coins list doesn't match the "
f"currency's coins count. ({len(coins)} "
f"!= {CURRENCIES[self.currency]['count']})")
self._transactions.append(Transaction(coins, direction))
for coin_count in self.count:
if len(coins) != CURRENCIES[self._currency]["count"]:
raise ValueError("Length of passed coins list doesn't match the " \
f"currency's coins count. ({len(coins)} " \
f"!= {CURRENCIES[self._currency]['count']})")
self._last_transaction = Transaction(coins, direction)
self._transactions.append(self._last_transaction)
for coin_count in sum_transactions(self._transactions):
if coin_count < 0:
del self._transactions[-1]
self._last_transaction = None
raise ValueError(
"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
def currency(self) -> str:
"""Returns a currency of a PiggyBank."""
return self._currency
@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
of coins doesn't match the old currency it won't set a new one."""
currency = currency.upper()
if currency not in CURRENCIES:
if not currency in CURRENCIES:
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
self._currency = currency
@ -112,13 +59,49 @@ class PiggyBank:
"""Returns a list of 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:
"""It compares only currency."""
return self.currency == piggybank._currency
"""Compares only currency."""
return self._currency == piggybank.currency
def __add__(self, piggybank: PiggyBank) -> PiggyBank:
if self != piggybank:
raise CurrencyMismatchError
new = PiggyBank(self.currency)
new._transactions = self.transactions + piggybank._transactions
new = PiggyBank(self._currency)
new._transactions = self._transactions + piggybank._transactions
return new

View File

@ -1,54 +1,102 @@
"""Transaction class implementation."""
from __future__ import annotations
from operator import add, sub, mul
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"
TYPE_OUTCOME = "out"
__all__ = ["Transaction", "sum_transactions", "multiply_transactions",
"TYPE_INCOME", "TYPE_OUTCOME", "TIME_FORMAT"]
TYPE_INCOME = "i"
TYPE_OUTCOME = "o"
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:
"""An object that holds a transaction.
"""An object that holds a single transaction.
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
or TYPE_OUTCOME;
- timestamp -- date and time formated accordingly to TIME_FORMAT."""
def __init__(self, coins: List[int], direction: str = TYPE_INCOME,
timestamp: Optional[str] = None) -> None:
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:
raise ValueError(f"Direction may only be"
f"'{TYPE_INCOME}' or '{TYPE_OUTCOME}'")
raise TypeError("Coins must be of type 'list'.")
@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:
self.timestamp = strftime(TIME_FORMAT, gmtime())
self._timestamp = strftime(TIME_FORMAT, gmtime())
else:
try:
strptime(timestamp, TIME_FORMAT)
except ValueError:
raise ValueError(f"Timestamp {timestamp} has wrong format. "
f"The right one is '{TIME_FORMAT}''")
self.timestamp = timestamp
raise ValueError(f"Timestamp {timestamp} has wrong format. " \
f"The right one is \"{TIME_FORMAT}\".")
self._timestamp = timestamp
@staticmethod
def from_string(transaction: str) -> Transaction:
"""Makes a Transaction object from its string output."""
timestamp, direction, coins = transaction.split()
coins = list(map(int, coins.split(",")))
return Transaction(coins, direction, timestamp)
def __str__(self) -> str:
return f"{self.timestamp} {self.direction} " \
f"{','.join(str(c) for c in self.coins)}"
def __repr__(self) -> str:
return f"Transaction(coins={self.coins!r}," \
f"direction={self.direction!r}, timestamp={self.timestamp!r})"
f"{','.join(map(str, self.coins))}"