From f99ce3f6cc1193ed0886d5ab3b9316c9554c2ac0 Mon Sep 17 00:00:00 2001 From: "Alexander \"Arav\" Andreev" Date: Thu, 4 Jun 2020 03:12:31 +0400 Subject: [PATCH] First part of a massive refactoring. Also Configuration class that'll be used to store a default currency in a config file was added. --- piggybank/__init__.py | 7 +- piggybank/configuration.py | 50 ++++++++++++++ piggybank/piggybank.py | 135 ++++++++++++++++--------------------- piggybank/transaction.py | 90 +++++++++++++++++++------ 4 files changed, 181 insertions(+), 101 deletions(-) create mode 100644 piggybank/configuration.py diff --git a/piggybank/__init__.py b/piggybank/__init__.py index 7e6244f..569e519 100644 --- a/piggybank/__init__.py +++ b/piggybank/__init__.py @@ -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: diff --git a/piggybank/configuration.py b/piggybank/configuration.py new file mode 100644 index 0000000..92fa7f6 --- /dev/null +++ b/piggybank/configuration.py @@ -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 diff --git a/piggybank/piggybank.py b/piggybank/piggybank.py index 1799815..517a678 100644 --- a/piggybank/piggybank.py +++ b/piggybank/piggybank.py @@ -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 diff --git a/piggybank/transaction.py b/piggybank/transaction.py index 0d1275b..b0aa180 100644 --- a/piggybank/transaction.py +++ b/piggybank/transaction.py @@ -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))}"