fetching and writing rates
This commit is contained in:
parent
7012cbec97
commit
41eae45f68
5 changed files with 140 additions and 43 deletions
47
xexe/exchange_rates.py
Normal file
47
xexe/exchange_rates.py
Normal file
|
|
@ -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())
|
||||||
|
|
@ -6,7 +6,10 @@ from typing import List
|
||||||
from money.currency import Currency
|
from money.currency import Currency
|
||||||
from xecd_rates_client import XecdClient
|
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()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
|
@ -57,43 +60,70 @@ def run_get_rates(
|
||||||
) -> None:
|
) -> None:
|
||||||
logger.info("Getting rates")
|
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:
|
currency_and_date_combinations = generate_currency_and_dates_combinations(
|
||||||
rates_writer.write(rates)
|
date_range=date_range, currencies=currencies
|
||||||
except Exception as e:
|
)
|
||||||
process_state.record_rate_writing_error(e)
|
|
||||||
raise Exception(f"Could not write rates. See logs.")
|
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:
|
class GetRatesProcessState:
|
||||||
def __init__(self, output: str, dry_run: bool) -> None:
|
def __init__(self, output: str, dry_run: bool) -> None:
|
||||||
self.writer_type = self._infer_writer_type(output)
|
self.writer = self._select_writer(output)
|
||||||
self.fetcher_type = self._infer_fetcher_type(dry_run)
|
self.fetcher = self._select_fetcher(dry_run)
|
||||||
|
|
||||||
@staticmethod
|
@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")
|
output_is_csv_file_path = bool(pathlib.Path(output).suffix == ".csv")
|
||||||
|
|
||||||
if output_is_csv_file_path:
|
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}")
|
raise ValueError(f"Don't know how to handle passed output: {output}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _infer_fetcher_type(dry_run: bool) -> str:
|
def _select_fetcher(dry_run: bool) -> str:
|
||||||
if dry_run:
|
if dry_run:
|
||||||
return MockFetcher
|
return MockRateFetcher
|
||||||
|
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
return XEFetcher
|
return XERateFetcher
|
||||||
|
|
||||||
|
def get_fetcher(self) -> RateFetcher:
|
||||||
|
return self.fetcher
|
||||||
|
|
||||||
|
def get_writer(self) -> RateWriter:
|
||||||
|
return self.writer
|
||||||
|
|
|
||||||
|
|
@ -6,20 +6,7 @@ from money.currency import Currency
|
||||||
from money.money import Money
|
from money.money import Money
|
||||||
from xecd_rates_client import XecdClient
|
from xecd_rates_client import XecdClient
|
||||||
|
|
||||||
|
from xexe.exchange_rates import ExchangeRate
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class RateFetcher(ABC):
|
class RateFetcher(ABC):
|
||||||
|
|
@ -42,7 +29,7 @@ class MockRateFetcher(RateFetcher):
|
||||||
return ExchangeRate(
|
return ExchangeRate(
|
||||||
from_currency=from_currency,
|
from_currency=from_currency,
|
||||||
to_currency=to_currency,
|
to_currency=to_currency,
|
||||||
rate=Money("42.0", to_currency),
|
rate=Money(42, to_currency),
|
||||||
rate_date=rate_date,
|
rate_date=rate_date,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
34
xexe/rate_writing.py
Normal file
34
xexe/rate_writing.py
Normal file
|
|
@ -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"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import datetime
|
import datetime
|
||||||
from itertools import combinations
|
from itertools import combinations
|
||||||
from typing import Set
|
from typing import Set, Tuple
|
||||||
|
|
||||||
from money.currency import Currency
|
from money.currency import Currency
|
||||||
|
|
||||||
|
|
@ -69,13 +69,13 @@ class DateRange:
|
||||||
|
|
||||||
def generate_currency_and_dates_combinations(
|
def generate_currency_and_dates_combinations(
|
||||||
date_range: DateRange, currencies: Set[Currency]
|
date_range: DateRange, currencies: Set[Currency]
|
||||||
):
|
) -> Tuple[dict]:
|
||||||
currency_pairs = list(combinations(currencies, 2))
|
currency_pairs = list(combinations(currencies, 2))
|
||||||
|
|
||||||
combinations = []
|
currency_date_combinations = []
|
||||||
for date in date_range:
|
for date in date_range:
|
||||||
for from_currency, to_currency in currency_pairs:
|
for from_currency, to_currency in currency_pairs:
|
||||||
combinations.append(
|
currency_date_combinations.append(
|
||||||
{
|
{
|
||||||
"from_currency": from_currency,
|
"from_currency": from_currency,
|
||||||
"to_currency": to_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 currency_date_combinations
|
||||||
return combinations
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue