many changes

This commit is contained in:
Pablo Martin 2024-06-11 21:10:07 +02:00
parent 126ede37a4
commit 661941a65c
7 changed files with 102 additions and 21 deletions

View file

@ -1,3 +1,4 @@
import csv
import datetime
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
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()
@ -50,7 +63,12 @@ def test_get_rates_dry_run_always_returns_42_as_rates():
day=random.choice(range(1, 29)),
).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()
@ -63,14 +81,7 @@ def test_get_rates_dry_run_always_returns_42_as_rates():
"--end-date",
(some_random_date + datetime.timedelta(days=3)).strftime("%Y-%m-%d"),
"--currencies",
",".join(
[
# Get the last 3 right characters, which are the actual
# currency code
str(some_currency)[-3:]
for some_currency in some_random_currencies
]
),
",".join(some_random_currencies),
"--output",
"test_output.csv",
"--dry-run",
@ -79,6 +90,29 @@ def test_get_rates_dry_run_always_returns_42_as_rates():
assert run_result.exit_code == 0
# Write code here to read output file and compare it against expected
# output
assert False
with open("test_output.csv", newline="") as csv_file:
reader = csv.DictReader(csv_file)
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']}"

View file

@ -14,6 +14,7 @@ def test_handle_input_rates_works_with_full_correct_inputs():
end_date=datetime.datetime.now() - datetime.timedelta(days=1),
currencies="USD,EUR,GBP",
dry_run=False,
ignore_warnings=True,
output="test_output.csv",
)
expected_result = {
@ -23,6 +24,7 @@ def test_handle_input_rates_works_with_full_correct_inputs():
),
"currencies": {Currency("USD"), Currency("EUR"), Currency("GBP")},
"dry_run": False,
"ignore_warnings": True,
"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),
currencies="not_a_currency,USD,not_this_either",
dry_run=False,
ignore_warnings=True,
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),
currencies="GBP,USD",
dry_run=False,
ignore_warnings=True,
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),
currencies="GBP,USD",
dry_run=False,
ignore_warnings=True,
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),
currencies="USD,EUR,GBP",
dry_run=False,
ignore_warnings=True,
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(),
currencies="USD,EUR,GBP",
dry_run=False,
ignore_warnings=True,
output="test_output.csv",
)
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")},
"dry_run": False,
"ignore_warnings": True,
"output": pathlib.Path("test_output.csv"),
}

View file

@ -66,12 +66,14 @@ def xe_healthcheck():
"--currencies", default=",".join([]), show_default=True, type=click.STRING
)
@click.option("--dry-run", is_flag=True)
@click.option("--ignore-warnings", is_flag=True)
@click.option("--output", type=click.STRING, required=True)
def get_rates(
start_date: Union[str, datetime.datetime, datetime.date],
end_date: Union[str, datetime.datetime, datetime.date],
currencies: Union[None, str],
dry_run: bool,
ignore_warnings: bool,
output: pathlib.Path,
):
get_rates_inputs = handle_get_rates_inputs(
@ -79,6 +81,7 @@ def get_rates(
end_date=end_date,
currencies=currencies,
dry_run=dry_run,
ignore_warnings=ignore_warnings,
output=output,
)
logger.info("Starting get-rates process.")

View file

@ -16,6 +16,7 @@ def handle_get_rates_inputs(
end_date: Union[datetime.datetime, datetime.date],
currencies: Union[None, str],
dry_run: bool,
ignore_warnings: bool,
output: Union[str, pathlib.Path],
):
logger.info("Handling inputs.")
@ -31,7 +32,8 @@ def handle_get_rates_inputs(
tmp = {Currency(currency_code) for currency_code in currencies}
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
# 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,
"currencies": currencies,
"dry_run": dry_run,
"ignore_warnings": ignore_warnings,
"output": output,
}

View file

@ -56,16 +56,23 @@ def run_get_rates(
date_range: DateRange,
currencies: List[Currency],
dry_run: bool,
ignore_warnings: bool,
output: pathlib.Path,
) -> None:
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(
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)
logger.info("Rates written to output.")
def obtain_rates_from_source(
@ -77,6 +84,21 @@ def obtain_rates_from_source(
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()
for combination in currency_and_date_combinations:
try:
@ -97,13 +119,15 @@ def obtain_rates_from_source(
def write_rates_to_output(process_state, rates):
rates_writer = process_state.get_writer()
logger.info("Attempting writing rates to output.")
rates_writer.write_rates(rates)
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.fetcher = self._select_fetcher(dry_run)
self.ignore_warnings = ignore_warnings
@staticmethod
def _select_writer(output: str) -> CSVRateWriter:
@ -115,12 +139,14 @@ class GetRatesProcessState:
raise ValueError(f"Don't know how to handle passed output: {output}")
@staticmethod
def _select_fetcher(dry_run: bool) -> str:
def _select_fetcher(dry_run: bool) -> RateFetcher:
if dry_run:
return MockRateFetcher
logger.info("Dry-run activated. Running against MockRateFetcher.")
return MockRateFetcher()
if not dry_run:
return XERateFetcher
logger.info("Real run active. Running against XE.com's API.")
return XERateFetcher()
def get_fetcher(self) -> RateFetcher:
return self.fetcher

View file

@ -10,6 +10,9 @@ from xexe.exchange_rates import ExchangeRate
class RateFetcher(ABC):
is_production_grade = False
@abstractmethod
def fetch_rate(
self, from_currency: Currency, to_currency: Currency, rate_date: datetime.date
@ -19,6 +22,8 @@ class RateFetcher(ABC):
class MockRateFetcher(RateFetcher):
is_production_grade = False
def __init__(self) -> None:
super().__init__()
@ -36,6 +41,8 @@ class MockRateFetcher(RateFetcher):
class XERateFetcher(RateFetcher):
is_production_grade = True
def __init__(self) -> None:
super().__init__()
self.xe_client = XecdClient(

View file

@ -18,8 +18,8 @@ class CSVRateWriter(RateWriter):
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)
with open(self.output_file_path, mode="w") as csv_file:
csv_writer = csv.writer(csv_file)
csv_writer.writerow(["from_currency", "to_currency", "rate", "rate_date"])
# Write the exchange rate data