many changes
This commit is contained in:
parent
126ede37a4
commit
661941a65c
7 changed files with 102 additions and 21 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
import csv
|
||||||
import datetime
|
import datetime
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
|
@ -11,6 +12,18 @@ def test_get_rates_for_hardcoded_case_returns_expected_output():
|
||||||
"""
|
"""
|
||||||
Calling the CLI get-rates command to get the rates between USD, EUR and GBP
|
Calling the CLI get-rates command to get the rates between USD, EUR and GBP
|
||||||
for 2024-01-01 to 2024-01-03 returns the expected records in a CSV.
|
for 2024-01-01 to 2024-01-03 returns the expected records in a CSV.
|
||||||
|
|
||||||
|
ATTENTION!
|
||||||
|
|
||||||
|
This is a production-grade test. It will run against xe.com and requires you
|
||||||
|
to have a proper setup. It is not intended to be an automated or frequent
|
||||||
|
test, but rather be a helper.
|
||||||
|
|
||||||
|
Because of this, you can see that the test is mostly commented out. If you
|
||||||
|
want to run it, uncomment and use it only for what you need.
|
||||||
|
|
||||||
|
When commented, the test should always pass to not bother automated test
|
||||||
|
runs.
|
||||||
"""
|
"""
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
||||||
|
|
@ -50,7 +63,12 @@ def test_get_rates_dry_run_always_returns_42_as_rates():
|
||||||
day=random.choice(range(1, 29)),
|
day=random.choice(range(1, 29)),
|
||||||
).date()
|
).date()
|
||||||
|
|
||||||
some_random_currencies = random.choices(population=list(Currency), k=3)
|
some_random_currencies = [
|
||||||
|
# Get the last 3 right characters, which are the actual
|
||||||
|
# currency code
|
||||||
|
str(some_currency)[-3:]
|
||||||
|
for some_currency in random.sample(population=list(Currency), k=3)
|
||||||
|
]
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
||||||
|
|
@ -63,14 +81,7 @@ def test_get_rates_dry_run_always_returns_42_as_rates():
|
||||||
"--end-date",
|
"--end-date",
|
||||||
(some_random_date + datetime.timedelta(days=3)).strftime("%Y-%m-%d"),
|
(some_random_date + datetime.timedelta(days=3)).strftime("%Y-%m-%d"),
|
||||||
"--currencies",
|
"--currencies",
|
||||||
",".join(
|
",".join(some_random_currencies),
|
||||||
[
|
|
||||||
# Get the last 3 right characters, which are the actual
|
|
||||||
# currency code
|
|
||||||
str(some_currency)[-3:]
|
|
||||||
for some_currency in some_random_currencies
|
|
||||||
]
|
|
||||||
),
|
|
||||||
"--output",
|
"--output",
|
||||||
"test_output.csv",
|
"test_output.csv",
|
||||||
"--dry-run",
|
"--dry-run",
|
||||||
|
|
@ -79,6 +90,29 @@ def test_get_rates_dry_run_always_returns_42_as_rates():
|
||||||
|
|
||||||
assert run_result.exit_code == 0
|
assert run_result.exit_code == 0
|
||||||
|
|
||||||
# Write code here to read output file and compare it against expected
|
with open("test_output.csv", newline="") as csv_file:
|
||||||
# output
|
reader = csv.DictReader(csv_file)
|
||||||
assert False
|
rows = list(reader)
|
||||||
|
|
||||||
|
# Ensure that the output contains the correct number of rows
|
||||||
|
expected_num_rows = (3 + 1) * len(
|
||||||
|
some_random_currencies
|
||||||
|
) # 3 days + 1 day = 4 dates for each currency
|
||||||
|
assert (
|
||||||
|
len(rows) == expected_num_rows
|
||||||
|
), f"Expected {expected_num_rows} rows, but got {len(rows)}"
|
||||||
|
|
||||||
|
# Check that all rows have the expected rate of 42 and the correct dates
|
||||||
|
for row in rows:
|
||||||
|
assert row["rate"] == "42", f"Expected rate to be 42, but got {row['rate']}"
|
||||||
|
assert row["rate_date"] in [
|
||||||
|
(some_random_date + datetime.timedelta(days=i)).strftime("%Y-%m-%d")
|
||||||
|
for i in range(4)
|
||||||
|
], f"Unexpected rate_date {row['rate_date']}"
|
||||||
|
|
||||||
|
assert (
|
||||||
|
row["from_currency"] in some_random_currencies
|
||||||
|
), f"Unexpected from_currency {row['from_currency']}"
|
||||||
|
assert (
|
||||||
|
row["to_currency"] in some_random_currencies
|
||||||
|
), f"Unexpected to_currency {row['to_currency']}"
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ def test_handle_input_rates_works_with_full_correct_inputs():
|
||||||
end_date=datetime.datetime.now() - datetime.timedelta(days=1),
|
end_date=datetime.datetime.now() - datetime.timedelta(days=1),
|
||||||
currencies="USD,EUR,GBP",
|
currencies="USD,EUR,GBP",
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
|
ignore_warnings=True,
|
||||||
output="test_output.csv",
|
output="test_output.csv",
|
||||||
)
|
)
|
||||||
expected_result = {
|
expected_result = {
|
||||||
|
|
@ -23,6 +24,7 @@ def test_handle_input_rates_works_with_full_correct_inputs():
|
||||||
),
|
),
|
||||||
"currencies": {Currency("USD"), Currency("EUR"), Currency("GBP")},
|
"currencies": {Currency("USD"), Currency("EUR"), Currency("GBP")},
|
||||||
"dry_run": False,
|
"dry_run": False,
|
||||||
|
"ignore_warnings": True,
|
||||||
"output": pathlib.Path("test_output.csv"),
|
"output": pathlib.Path("test_output.csv"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,6 +40,7 @@ def test_handle_input_rates_raises_with_bad_currency_code():
|
||||||
end_date=datetime.datetime.now() + datetime.timedelta(days=7),
|
end_date=datetime.datetime.now() + datetime.timedelta(days=7),
|
||||||
currencies="not_a_currency,USD,not_this_either",
|
currencies="not_a_currency,USD,not_this_either",
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
|
ignore_warnings=True,
|
||||||
output="test_output.csv",
|
output="test_output.csv",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -49,6 +52,7 @@ def test_handle_input_rates_raises_with_start_date_after_end_date():
|
||||||
end_date=datetime.datetime.now() - datetime.timedelta(days=7),
|
end_date=datetime.datetime.now() - datetime.timedelta(days=7),
|
||||||
currencies="GBP,USD",
|
currencies="GBP,USD",
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
|
ignore_warnings=True,
|
||||||
output="test_output.csv",
|
output="test_output.csv",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -60,6 +64,7 @@ def test_handle_input_rates_raises_with_output_different_than_csv():
|
||||||
end_date=datetime.datetime.now() + datetime.timedelta(days=7),
|
end_date=datetime.datetime.now() + datetime.timedelta(days=7),
|
||||||
currencies="GBP,USD",
|
currencies="GBP,USD",
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
|
ignore_warnings=True,
|
||||||
output="test_output.xlsx",
|
output="test_output.xlsx",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -70,6 +75,7 @@ def test_handle_input_rates_brings_future_end_date_to_today():
|
||||||
end_date=datetime.datetime.now() + datetime.timedelta(days=7),
|
end_date=datetime.datetime.now() + datetime.timedelta(days=7),
|
||||||
currencies="USD,EUR,GBP",
|
currencies="USD,EUR,GBP",
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
|
ignore_warnings=True,
|
||||||
output="test_output.csv",
|
output="test_output.csv",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -82,6 +88,7 @@ def test_handle_input_rates_start_and_end_date_equal_works_fine():
|
||||||
end_date=datetime.datetime.now(),
|
end_date=datetime.datetime.now(),
|
||||||
currencies="USD,EUR,GBP",
|
currencies="USD,EUR,GBP",
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
|
ignore_warnings=True,
|
||||||
output="test_output.csv",
|
output="test_output.csv",
|
||||||
)
|
)
|
||||||
expected_result = {
|
expected_result = {
|
||||||
|
|
@ -91,6 +98,7 @@ def test_handle_input_rates_start_and_end_date_equal_works_fine():
|
||||||
),
|
),
|
||||||
"currencies": {Currency("USD"), Currency("EUR"), Currency("GBP")},
|
"currencies": {Currency("USD"), Currency("EUR"), Currency("GBP")},
|
||||||
"dry_run": False,
|
"dry_run": False,
|
||||||
|
"ignore_warnings": True,
|
||||||
"output": pathlib.Path("test_output.csv"),
|
"output": pathlib.Path("test_output.csv"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,12 +66,14 @@ def xe_healthcheck():
|
||||||
"--currencies", default=",".join([]), show_default=True, type=click.STRING
|
"--currencies", default=",".join([]), show_default=True, type=click.STRING
|
||||||
)
|
)
|
||||||
@click.option("--dry-run", is_flag=True)
|
@click.option("--dry-run", is_flag=True)
|
||||||
|
@click.option("--ignore-warnings", is_flag=True)
|
||||||
@click.option("--output", type=click.STRING, required=True)
|
@click.option("--output", type=click.STRING, required=True)
|
||||||
def get_rates(
|
def get_rates(
|
||||||
start_date: Union[str, datetime.datetime, datetime.date],
|
start_date: Union[str, datetime.datetime, datetime.date],
|
||||||
end_date: Union[str, datetime.datetime, datetime.date],
|
end_date: Union[str, datetime.datetime, datetime.date],
|
||||||
currencies: Union[None, str],
|
currencies: Union[None, str],
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
|
ignore_warnings: bool,
|
||||||
output: pathlib.Path,
|
output: pathlib.Path,
|
||||||
):
|
):
|
||||||
get_rates_inputs = handle_get_rates_inputs(
|
get_rates_inputs = handle_get_rates_inputs(
|
||||||
|
|
@ -79,6 +81,7 @@ def get_rates(
|
||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
currencies=currencies,
|
currencies=currencies,
|
||||||
dry_run=dry_run,
|
dry_run=dry_run,
|
||||||
|
ignore_warnings=ignore_warnings,
|
||||||
output=output,
|
output=output,
|
||||||
)
|
)
|
||||||
logger.info("Starting get-rates process.")
|
logger.info("Starting get-rates process.")
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ def handle_get_rates_inputs(
|
||||||
end_date: Union[datetime.datetime, datetime.date],
|
end_date: Union[datetime.datetime, datetime.date],
|
||||||
currencies: Union[None, str],
|
currencies: Union[None, str],
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
|
ignore_warnings: bool,
|
||||||
output: Union[str, pathlib.Path],
|
output: Union[str, pathlib.Path],
|
||||||
):
|
):
|
||||||
logger.info("Handling inputs.")
|
logger.info("Handling inputs.")
|
||||||
|
|
@ -31,7 +32,8 @@ def handle_get_rates_inputs(
|
||||||
tmp = {Currency(currency_code) for currency_code in currencies}
|
tmp = {Currency(currency_code) for currency_code in currencies}
|
||||||
currencies = tmp
|
currencies = tmp
|
||||||
|
|
||||||
if currencies is None:
|
if currencies is None or currencies == "":
|
||||||
|
logger.info("No currency list passed. Running for default currencies.")
|
||||||
currencies = DEFAULT_CURRENCIES
|
currencies = DEFAULT_CURRENCIES
|
||||||
|
|
||||||
# The Path constructor is idempotent, so this works equally fine if output
|
# The Path constructor is idempotent, so this works equally fine if output
|
||||||
|
|
@ -44,6 +46,7 @@ def handle_get_rates_inputs(
|
||||||
"date_range": date_range,
|
"date_range": date_range,
|
||||||
"currencies": currencies,
|
"currencies": currencies,
|
||||||
"dry_run": dry_run,
|
"dry_run": dry_run,
|
||||||
|
"ignore_warnings": ignore_warnings,
|
||||||
"output": output,
|
"output": output,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,16 +56,23 @@ def run_get_rates(
|
||||||
date_range: DateRange,
|
date_range: DateRange,
|
||||||
currencies: List[Currency],
|
currencies: List[Currency],
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
|
ignore_warnings: bool,
|
||||||
output: pathlib.Path,
|
output: pathlib.Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
logger.info("Getting rates")
|
logger.info("Getting rates")
|
||||||
|
|
||||||
process_state = GetRatesProcessState(output=output, dry_run=dry_run)
|
process_state = GetRatesProcessState(
|
||||||
|
output=output, dry_run=dry_run, ignore_warnings=ignore_warnings
|
||||||
|
)
|
||||||
|
|
||||||
rates = obtain_rates_from_source(
|
rates = obtain_rates_from_source(
|
||||||
process_state, date_range=date_range, currencies=currencies
|
process_state,
|
||||||
|
date_range=date_range,
|
||||||
|
currencies=currencies,
|
||||||
)
|
)
|
||||||
|
logger.info("Rates obtained.")
|
||||||
write_rates_to_output(process_state, rates)
|
write_rates_to_output(process_state, rates)
|
||||||
|
logger.info("Rates written to output.")
|
||||||
|
|
||||||
|
|
||||||
def obtain_rates_from_source(
|
def obtain_rates_from_source(
|
||||||
|
|
@ -77,6 +84,21 @@ def obtain_rates_from_source(
|
||||||
date_range=date_range, currencies=currencies
|
date_range=date_range, currencies=currencies
|
||||||
)
|
)
|
||||||
|
|
||||||
|
large_api_call_planned = (
|
||||||
|
rates_fetcher.is_production_grade and len(currency_and_date_combinations) > 100
|
||||||
|
)
|
||||||
|
if large_api_call_planned and not process_state.ignore_warnings:
|
||||||
|
user_confirmation_string = "i understand"
|
||||||
|
user_response = input(
|
||||||
|
f"WARNING: you are about to execute a large call {len(currency_and_date_combinations)} to a metered API. Type '{user_confirmation_string}' to move forward."
|
||||||
|
)
|
||||||
|
if user_response != user_confirmation_string:
|
||||||
|
raise Exception("Execution aborted.")
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"We are looking for the following rate combinations: {currency_and_date_combinations}"
|
||||||
|
)
|
||||||
|
|
||||||
rates = ExchangeRates()
|
rates = ExchangeRates()
|
||||||
for combination in currency_and_date_combinations:
|
for combination in currency_and_date_combinations:
|
||||||
try:
|
try:
|
||||||
|
|
@ -97,13 +119,15 @@ def obtain_rates_from_source(
|
||||||
|
|
||||||
def write_rates_to_output(process_state, rates):
|
def write_rates_to_output(process_state, rates):
|
||||||
rates_writer = process_state.get_writer()
|
rates_writer = process_state.get_writer()
|
||||||
|
logger.info("Attempting writing rates to output.")
|
||||||
rates_writer.write_rates(rates)
|
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, ignore_warnings: bool) -> None:
|
||||||
self.writer = self._select_writer(output)
|
self.writer = self._select_writer(output)
|
||||||
self.fetcher = self._select_fetcher(dry_run)
|
self.fetcher = self._select_fetcher(dry_run)
|
||||||
|
self.ignore_warnings = ignore_warnings
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _select_writer(output: str) -> CSVRateWriter:
|
def _select_writer(output: str) -> CSVRateWriter:
|
||||||
|
|
@ -115,12 +139,14 @@ class GetRatesProcessState:
|
||||||
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 _select_fetcher(dry_run: bool) -> str:
|
def _select_fetcher(dry_run: bool) -> RateFetcher:
|
||||||
if dry_run:
|
if dry_run:
|
||||||
return MockRateFetcher
|
logger.info("Dry-run activated. Running against MockRateFetcher.")
|
||||||
|
return MockRateFetcher()
|
||||||
|
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
return XERateFetcher
|
logger.info("Real run active. Running against XE.com's API.")
|
||||||
|
return XERateFetcher()
|
||||||
|
|
||||||
def get_fetcher(self) -> RateFetcher:
|
def get_fetcher(self) -> RateFetcher:
|
||||||
return self.fetcher
|
return self.fetcher
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ from xexe.exchange_rates import ExchangeRate
|
||||||
|
|
||||||
|
|
||||||
class RateFetcher(ABC):
|
class RateFetcher(ABC):
|
||||||
|
|
||||||
|
is_production_grade = False
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def fetch_rate(
|
def fetch_rate(
|
||||||
self, from_currency: Currency, to_currency: Currency, rate_date: datetime.date
|
self, from_currency: Currency, to_currency: Currency, rate_date: datetime.date
|
||||||
|
|
@ -19,6 +22,8 @@ class RateFetcher(ABC):
|
||||||
|
|
||||||
class MockRateFetcher(RateFetcher):
|
class MockRateFetcher(RateFetcher):
|
||||||
|
|
||||||
|
is_production_grade = False
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
|
@ -36,6 +41,8 @@ class MockRateFetcher(RateFetcher):
|
||||||
|
|
||||||
class XERateFetcher(RateFetcher):
|
class XERateFetcher(RateFetcher):
|
||||||
|
|
||||||
|
is_production_grade = True
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.xe_client = XecdClient(
|
self.xe_client = XecdClient(
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ class CSVRateWriter(RateWriter):
|
||||||
self.output_file_path = output_file_path
|
self.output_file_path = output_file_path
|
||||||
|
|
||||||
def write_rates(self, rates: ExchangeRates) -> None:
|
def write_rates(self, rates: ExchangeRates) -> None:
|
||||||
with open(self.output_file_path, mode="w", newline="") as csvfile:
|
with open(self.output_file_path, mode="w") as csv_file:
|
||||||
csv_writer = csv.writer(csvfile)
|
csv_writer = csv.writer(csv_file)
|
||||||
csv_writer.writerow(["from_currency", "to_currency", "rate", "rate_date"])
|
csv_writer.writerow(["from_currency", "to_currency", "rate", "rate_date"])
|
||||||
|
|
||||||
# Write the exchange rate data
|
# Write the exchange rate data
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue