From 4f81ac2e62d6986b9da688693043f77dbd1628a9 Mon Sep 17 00:00:00 2001 From: Pablo Martin Date: Fri, 7 Jun 2024 16:10:35 +0200 Subject: [PATCH] quite a bit of development around get rates input handling --- poetry.lock | 13 ++- pyproject.toml | 1 + .../test_xe_get_rates.py | 12 --- tests/tests_unit/test_input_handling.py | 94 +++++++++++++++++++ xexe/cli.py | 2 +- xexe/constants.py | 4 + xexe/inputs_handling.py | 51 ++++++++-- xexe/utils.py | 42 +++++++++ 8 files changed, 199 insertions(+), 20 deletions(-) rename tests/{tests_integration => tests_cli}/test_xe_get_rates.py (78%) create mode 100644 tests/tests_unit/test_input_handling.py create mode 100644 xexe/utils.py diff --git a/poetry.lock b/poetry.lock index d81df30..1f9df51 100644 --- a/poetry.lock +++ b/poetry.lock @@ -135,6 +135,17 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "currencies" +version = "2020.12.12" +description = "Display money format and its filthy currencies, for all money lovers out there." +optional = false +python-versions = "<4" +files = [ + {file = "currencies-2020.12.12-py3-none-any.whl", hash = "sha256:33b017bd11b0a70707ffa917e80cd8c4e07a1a6b412239ee19c10f145ed5f031"}, + {file = "currencies-2020.12.12.tar.gz", hash = "sha256:400cf313b8f6f33a59dcc9c9723dbe458eb8ff18a74ad2b79eba8a295a44b556"}, +] + [[package]] name = "idna" version = "3.7" @@ -226,4 +237,4 @@ requests = ">=2.19.1" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "fc10bcffee4d61f71d09d8ee3ecd62c907cf50c34e802bf747bc0e17770e7646" +content-hash = "2fea7228b5d6197e7c654a55dfc2415d44e9b6f98abd859a7067910a4fc89571" diff --git a/pyproject.toml b/pyproject.toml index 2a64513..399af5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ click = "^8.1.7" python-dotenv = "^1.0.1" pyfiglet = "^1.0.2" xecd-rates-client = "^1.0.0" +currencies = "^2020.12.12" [build-system] requires = ["poetry-core"] diff --git a/tests/tests_integration/test_xe_get_rates.py b/tests/tests_cli/test_xe_get_rates.py similarity index 78% rename from tests/tests_integration/test_xe_get_rates.py rename to tests/tests_cli/test_xe_get_rates.py index 7f1aed3..b723dda 100644 --- a/tests/tests_integration/test_xe_get_rates.py +++ b/tests/tests_cli/test_xe_get_rates.py @@ -35,15 +35,3 @@ def test_get_rates_breaks_without_output(): runner = CliRunner() result = runner.invoke(get_rates) assert result.exit_code == 2 - - -def test_get_rates_replaces_future_dates_properly(): - assert False - - -def test_get_rates_rejects_start_date_after_end_date(): - assert False - - -def test_get_rates_rejects_invalid_currency_codes(): - assert False diff --git a/tests/tests_unit/test_input_handling.py b/tests/tests_unit/test_input_handling.py new file mode 100644 index 0000000..de8170c --- /dev/null +++ b/tests/tests_unit/test_input_handling.py @@ -0,0 +1,94 @@ +import datetime + +import pytest +from currencies import Currency +from currencies.exceptions import CurrencyDoesNotExist + +from xexe.inputs_handling import handle_get_rates_inputs +from xexe.utils import DateRange + + +def test_handle_input_rates_works_with_full_correct_inputs(): + handled_inputs = handle_get_rates_inputs( + start_date=datetime.datetime.now(), + end_date=datetime.datetime.now() + datetime.timedelta(days=7), + currencies="USD,EUR,GBP", + dry_run=False, + output="test_output.csv", + ) + expected_result = { + "date_range": DateRange( + start_date=datetime.datetime.now().date(), + end_date=(datetime.datetime.now() + datetime.timedelta(days=7)).date(), + ), + "currencies": {Currency("USD"), Currency("EUR"), Currency("GBP")}, + "dry_run": False, + "output": pathlib.Path("test_output.csv"), + } + assert handled_inputs == expected_result + + +def test_handle_input_rates_raises_with_bad_currency_code(): + + with pytest.raises(CurrencyDoesNotExist): + handle_get_rates_inputs( + start_date=datetime.datetime.now(), + end_date=datetime.datetime.now() + datetime.timedelta(days=7), + currencies="not_a_currency,USD,not_this_either", + dry_run=False, + output="test_output.csv", + ) + + +def test_handle_input_rates_raises_with_start_date_after_end_date(): + with pytest.raises(ValueError): + handle_get_rates_inputs( + start_date=datetime.datetime.now(), + end_date=datetime.datetime.now() - datetime.timedelta(days=7), + currencies="GBP,USD", + dry_run=False, + output="test_output.csv", + ) + + +def test_handle_input_rates_raises_with_output_different_than_csv(): + with pytest.raises(ValueError): + handle_get_rates_inputs( + start_date=datetime.datetime.now(), + end_date=datetime.datetime.now() + datetime.timedelta(days=7), + currencies="GBP,USD", + dry_run=False, + output="test_output.xlsx", + ) + + +def test_handle_input_rates_brings_future_end_date_to_today(): + handled_inputs = handle_get_rates_inputs( + start_date=datetime.datetime.now() - datetime.timedelta(days=7), + end_date=datetime.datetime.now() + datetime.timedelta(days=7), + currencies="USD,EUR,GBP", + dry_run=False, + output="test_output.csv", + ) + + assert handled_inputs["date_range"].end_date == datetime.datetime.now().date() + + +def test_handle_input_rates_start_and_end_date_equal_works_fine(): + handled_inputs = handle_get_rates_inputs( + start_date=datetime.datetime.now(), + end_date=datetime.datetime.now(), + currencies="USD,EUR,GBP", + dry_run=False, + output="test_output.csv", + ) + expected_result = { + "date_range": DateRange( + start_date=datetime.datetime.now().date(), + end_date=(datetime.datetime.now() + datetime.timedelta(days=7)).date(), + ), + "currencies": {Currency("USD"), Currency("EUR"), Currency("GBP")}, + "dry_run": False, + "output": "test_output.csv", + } + assert handled_inputs == expected_result diff --git a/xexe/cli.py b/xexe/cli.py index b1d60c5..17e0f21 100644 --- a/xexe/cli.py +++ b/xexe/cli.py @@ -74,7 +74,7 @@ def get_rates( dry_run: bool, output: pathlib.Path, ): - handle_get_rates_inputs( + inputs = handle_get_rates_inputs( start_date=start_date, end_date=end_date, currencies=currencies, diff --git a/xexe/constants.py b/xexe/constants.py index 9c73e49..c68206f 100644 --- a/xexe/constants.py +++ b/xexe/constants.py @@ -1,6 +1,10 @@ import pathlib from dataclasses import dataclass +from currencies import Currency + +DEFAULT_CURRENCIES = {Currency("EUR"), Currency("GBP"), Currency("USD")} + @dataclass class PATHS: diff --git a/xexe/inputs_handling.py b/xexe/inputs_handling.py index a2acceb..dc6731b 100644 --- a/xexe/inputs_handling.py +++ b/xexe/inputs_handling.py @@ -1,12 +1,51 @@ +import datetime import logging +import pathlib +from typing import Union + +from currencies import Currency + +from xexe.constants import DEFAULT_CURRENCIES +from xexe.utils import DateRange logger = logging.getLogger() -def handle_get_rates_inputs(start_date, end_date, currencies, dry_run, output): +def handle_get_rates_inputs( + start_date: Union[datetime.datetime, datetime.date], + end_date: Union[datetime.datetime, datetime.date], + currencies: Union[None, str], + dry_run: bool, + output: Union[str, pathlib.Path], +): logger.info("Handling inputs.") - logger.debug(f"Received start_date: {start_date}") - logger.debug(f"Received end_date: {end_date}") - logger.debug(f"Received currencies: {currencies}") - logger.debug(f"dry_run state: {dry_run}") - logger.debug(f"Received output: {output}") + + date_range = DateRange(start_date=start_date.date(), end_date=end_date.date()) + + if date_range.end_date > datetime.datetime.today().date(): + date_range.end_date = datetime.datetime.today().date() + + if currencies: + # CLI input comes as string of comma-separated currency codes + currencies = [currency_code.strip() for currency_code in currencies.split(",")] + tmp = [Currency(currency_code) for currency_code in currencies] + currencies = tmp + + if currencies is None: + currencies = DEFAULT_CURRENCIES + + # The Path constructor is idempotent, so this works equally fine if output + # is a string or an actual Path object. + output = pathlib.Path(output) + if output.suffix != ".csv": + raise ValueError("Output must be a .csv file.") + + prepared_inputs = { + "date_range": date_range, + "currencies": currencies, + "dry_run": dry_run, + "output": output, + } + + logger.debug(prepared_inputs) + return prepared_inputs diff --git a/xexe/utils.py b/xexe/utils.py new file mode 100644 index 0000000..ebbef53 --- /dev/null +++ b/xexe/utils.py @@ -0,0 +1,42 @@ +import datetime + + +class DateRange: + + def __init__(self, start_date: datetime.date, end_date: datetime.date): + if type(start_date) != datetime.date or type(end_date) != datetime.date: + raise TypeError("start_date and end_date must be date objects.") + + if start_date > end_date: + raise ValueError("start_date can't be after end_date.") + + self._start_date = start_date + self._end_date = end_date + + @property + def start_date(self): + return self._start_date + + @start_date.setter + def start_date(self, value: datetime.date): + if type(value) != datetime.date: + raise TypeError("start_date must be a date object.") + + if value > self._end_date: + raise ValueError("start_date can't be after end_date.") + + self._start_date = value + + @property + def end_date(self): + return self._end_date + + @end_date.setter + def end_date(self, value: datetime.date): + if type(value) != datetime.date: + raise TypeError("end_date must be a date object.") + + if value < self._start_date: + raise ValueError("end_date can't be before start_date.") + + self._end_date = value