diff --git a/xexe/exchange_rates.py b/xexe/exchange_rates.py new file mode 100644 index 0000000..1a2145e --- /dev/null +++ b/xexe/exchange_rates.py @@ -0,0 +1,47 @@ +import datetime +from typing import Iterable, Union + +from money.currency import Currency +from money.money import Money + + +class ExchangeRate: + + def __init__( + self, + from_currency: Currency, + to_currency: Currency, + rate: Money, + rate_date: datetime.date, + ) -> None: + self.from_currency = from_currency + self.to_currency = to_currency + self.rate = rate + self.rate_date = rate_date + + @property + def descriptor(self) -> str: + return ( + str(self.from_currency.value) + + str(self.to_currency.value) + + str(self.rate_date.strformat("%Y-%m-%d")) + ) + + +class ExchangeRates: + + def __init__(self, rates: Union[Iterable[ExchangeRate], None] = None): + + self._rate_index = {} + + if rates is not None: + for rate in rates: + if not isinstance(rate, ExchangeRate): + raise TypeError("ExchangeRates can only hold Rates.") + self._rate_index[rate.descriptor] = rate + + def add_rate(self, new_rate: ExchangeRate) -> None: + self._rate_index[new_rate.descriptor] = new_rate + + def __iter__(self): + return iter(self._rate_index.values()) diff --git a/xexe/processes.py b/xexe/processes.py index 7593cbf..4f91ed4 100644 --- a/xexe/processes.py +++ b/xexe/processes.py @@ -6,7 +6,10 @@ from typing import List from money.currency import Currency from xecd_rates_client import XecdClient -from xexe.utils import DateRange +from xexe.exchange_rates import ExchangeRates +from xexe.rate_fetching import MockRateFetcher, RateFetcher, XERateFetcher +from xexe.rate_writing import CSVRateWriter, RateWriter +from xexe.utils import DateRange, generate_currency_and_dates_combinations logger = logging.getLogger() @@ -57,43 +60,70 @@ def run_get_rates( ) -> None: logger.info("Getting rates") - process_state = GetRatesProcessState(output=output) + process_state = GetRatesProcessState(output=output, dry_run=dry_run) - rates_fetcher = get_rates_fetcher(process_state.fetcher_type) + rates = obtain_rates_from_source( + process_state, date_range=date_range, currencies=currencies + ) + write_rates_to_output(process_state, rates) - try: - rates = rates_fetcher.fetch() - except Exception as e: - process_state.record_rate_fetching_error(e) - raise ConnectionError(f"Could not fetch rates. See logs.") - rates_writer = get_rates_writer(process_state.output_type) +def obtain_rates_from_source( + process_state, date_range: DateRange, currencies: List[Currency] +) -> ExchangeRates: + rates_fetcher = process_state.get_fetcher() - try: - rates_writer.write(rates) - except Exception as e: - process_state.record_rate_writing_error(e) - raise Exception(f"Could not write rates. See logs.") + currency_and_date_combinations = generate_currency_and_dates_combinations( + date_range=date_range, currencies=currencies + ) + + rates = ExchangeRates() + for combination in currency_and_date_combinations: + try: + rate = rates_fetcher.fetch_rate( + from_currency=combination["from_currency"], + to_currency=combination["to_currency"], + rate_date=combination["date"], + ) + except Exception as e: + logger.error(f"Error while fetching rates.") + logger.error(e, exc_info=True) + raise ConnectionError(f"Could not fetch rates. See logs.") + + rates.add_rate(rate) + + return rates + + +def write_rates_to_output(process_state, rates): + rates_writer = process_state.get_writer() + rates_writer.write_rates(rates) class GetRatesProcessState: def __init__(self, output: str, dry_run: bool) -> None: - self.writer_type = self._infer_writer_type(output) - self.fetcher_type = self._infer_fetcher_type(dry_run) + self.writer = self._select_writer(output) + self.fetcher = self._select_fetcher(dry_run) @staticmethod - def _infer_writer_type(output: str) -> str: + def _select_writer(output: str) -> CSVRateWriter: output_is_csv_file_path = bool(pathlib.Path(output).suffix == ".csv") if output_is_csv_file_path: - return "csv_file" + return CSVRateWriter(output_file_path=output) raise ValueError(f"Don't know how to handle passed output: {output}") @staticmethod - def _infer_fetcher_type(dry_run: bool) -> str: + def _select_fetcher(dry_run: bool) -> str: if dry_run: - return MockFetcher + return MockRateFetcher if not dry_run: - return XEFetcher + return XERateFetcher + + def get_fetcher(self) -> RateFetcher: + return self.fetcher + + def get_writer(self) -> RateWriter: + return self.writer diff --git a/xexe/rate_fetching.py b/xexe/rate_fetching.py index dc12f12..b043aaf 100644 --- a/xexe/rate_fetching.py +++ b/xexe/rate_fetching.py @@ -6,20 +6,7 @@ from money.currency import Currency from money.money import Money from xecd_rates_client import XecdClient - -class ExchangeRate: - - def __init__( - self, - from_currency: Currency, - to_currency: Currency, - rate: Money, - rate_date: datetime.date = None, - ) -> None: - self.from_currency - self.to_currency - self.rate - self.rate_date +from xexe.exchange_rates import ExchangeRate class RateFetcher(ABC): @@ -42,7 +29,7 @@ class MockRateFetcher(RateFetcher): return ExchangeRate( from_currency=from_currency, to_currency=to_currency, - rate=Money("42.0", to_currency), + rate=Money(42, to_currency), rate_date=rate_date, ) diff --git a/xexe/rate_writing.py b/xexe/rate_writing.py new file mode 100644 index 0000000..bd29b63 --- /dev/null +++ b/xexe/rate_writing.py @@ -0,0 +1,34 @@ +import csv +import pathlib +from abc import ABC, abstractmethod + +from xexe.exchange_rates import ExchangeRates + + +class RateWriter(ABC): + @abstractmethod + def write_rates(self, rates: ExchangeRates) -> None: + pass + + +class CSVRateWriter(RateWriter): + + def __init__(self, output_file_path: pathlib.Path) -> None: + super().__init__() + self.output_file_path = output_file_path + + def write_rates(self, rates: ExchangeRates) -> None: + with open(self.output_file_path, mode="w", newline="") as csvfile: + csv_writer = csv.writer(csvfile) + csv_writer.writerow(["from_currency", "to_currency", "rate", "rate_date"]) + + # Write the exchange rate data + for rate in rates._rate_index.values(): + csv_writer.writerow( + [ + rate.from_currency.value, + rate.to_currency.value, + rate.rate.amount, + rate.rate_date.strftime("%Y-%m-%d"), + ] + ) diff --git a/xexe/utils.py b/xexe/utils.py index 9db979e..dd3ecc8 100644 --- a/xexe/utils.py +++ b/xexe/utils.py @@ -1,6 +1,6 @@ import datetime from itertools import combinations -from typing import Set +from typing import Set, Tuple from money.currency import Currency @@ -69,13 +69,13 @@ class DateRange: def generate_currency_and_dates_combinations( date_range: DateRange, currencies: Set[Currency] -): +) -> Tuple[dict]: currency_pairs = list(combinations(currencies, 2)) - combinations = [] + currency_date_combinations = [] for date in date_range: for from_currency, to_currency in currency_pairs: - combinations.append( + currency_date_combinations.append( { "from_currency": from_currency, "to_currency": to_currency, @@ -83,7 +83,6 @@ def generate_currency_and_dates_combinations( } ) - combinations = tuple(combinations) + currency_date_combinations = tuple(currency_date_combinations) - # Convert the result to a tuple - return combinations + return currency_date_combinations