From 661941a65cd1259915a06ad8ff25e014463e8c25 Mon Sep 17 00:00:00 2001 From: Pablo Martin Date: Tue, 11 Jun 2024 21:10:07 +0200 Subject: [PATCH] many changes --- tests/tests_integration/test_get_rates.py | 58 ++++++++++++++++++----- tests/tests_unit/test_input_handling.py | 8 ++++ xexe/cli.py | 3 ++ xexe/inputs_handling.py | 5 +- xexe/processes.py | 38 ++++++++++++--- xexe/rate_fetching.py | 7 +++ xexe/rate_writing.py | 4 +- 7 files changed, 102 insertions(+), 21 deletions(-) diff --git a/tests/tests_integration/test_get_rates.py b/tests/tests_integration/test_get_rates.py index 42c9a9c..6920bf5 100644 --- a/tests/tests_integration/test_get_rates.py +++ b/tests/tests_integration/test_get_rates.py @@ -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']}" diff --git a/tests/tests_unit/test_input_handling.py b/tests/tests_unit/test_input_handling.py index 34309e6..03d9c26 100644 --- a/tests/tests_unit/test_input_handling.py +++ b/tests/tests_unit/test_input_handling.py @@ -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"), } diff --git a/xexe/cli.py b/xexe/cli.py index 6afb15b..dbc973a 100644 --- a/xexe/cli.py +++ b/xexe/cli.py @@ -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.") diff --git a/xexe/inputs_handling.py b/xexe/inputs_handling.py index f199733..67e3fbb 100644 --- a/xexe/inputs_handling.py +++ b/xexe/inputs_handling.py @@ -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, } diff --git a/xexe/processes.py b/xexe/processes.py index 4f91ed4..6777407 100644 --- a/xexe/processes.py +++ b/xexe/processes.py @@ -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 diff --git a/xexe/rate_fetching.py b/xexe/rate_fetching.py index b043aaf..be9f108 100644 --- a/xexe/rate_fetching.py +++ b/xexe/rate_fetching.py @@ -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( diff --git a/xexe/rate_writing.py b/xexe/rate_writing.py index bd29b63..6b661cd 100644 --- a/xexe/rate_writing.py +++ b/xexe/rate_writing.py @@ -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