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"
|
||||
__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:
|
||||
|
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 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
|
||||
|
@ -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))}"
|
||||
|
Loading…
Reference in New Issue
Block a user