Compare commits

...

10 commits

Author SHA1 Message Date
Pablo Martin
a25f7d267b add AED to default deployment currencies 2025-05-27 13:03:05 +02:00
Pablo Martin
52adcc7538 Merged PR 5324: --pairs option for CLI
This PR implements an alternative option to `--currencies`, named `--pairs`. This options lets the user specify pairs of currencies instead of just a list of currencies (which assumes you want all pairs).

This is useful to run backfills for one new currency without having to also fetch from XE rates that are already present in the DWH.

Related work items: #29931
2025-05-27 09:44:24 +00:00
Pablo Martin
03134b08f7 . 2025-05-27 11:31:43 +02:00
Pablo Martin
d2e53e40be update version and changelog 2025-05-27 11:31:39 +02:00
Pablo Martin
9d6c93c89f always add pairs to handling output, if was pointless 2025-05-27 11:25:47 +02:00
Pablo Martin
d20c46d52d update readme 2025-05-27 11:18:34 +02:00
Pablo Martin
22bfc217f3 remove unused arg 2025-05-26 17:02:13 +02:00
Pablo Martin
7f8001ffca pulled up, fixed tests 2025-05-26 17:00:57 +02:00
Pablo Martin
a7a37d4614 remove unused code 2025-05-26 16:42:09 +02:00
Pablo Martin
b52af85987 it's all pairs 2025-05-26 16:41:49 +02:00
9 changed files with 57 additions and 81 deletions

View file

@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.1] - 2025-05-7
### Changed
- The default currencies present in the `run_xexe.sh` deployment script now include `AED`.
## [1.1.0] - 2025-05-07
### Added
- `get-rates` command now has a `--pairs` options that can be used instead of `--currencies`. `--pairs` allows the user to specify which currency pairs it wants to fetch rates for. This provides more control than `--currencies`, which assumes that the user wants ALL combinations of the listed currencies. `--pairs` will still automatically store reverse and equal rates for the passed pairs.
## [1.0.1] - 2024-06-27
### Changed

View file

@ -81,6 +81,15 @@ xexe get-rates --output my_rates.csv
xexe get-rates --currencies USD,EUR,GBP --output my_rates.csv
```
Using the `currencies` option will compute ALL combinations between different currencies. If you want a finer-grained control over which currency pairs you fetch, you can instead use the `pairs` options like this:
```bash
# Pairs must be concatenations of two valid ISO 4217 codes. Each pair must be
# comma-separated. No need to use reverse and equal pairs. As in, if you provide
# `USDEUR`, xexe will do that pair and also `EURUSD`, `EUREUR` and `USDUSD`.
xexe get-rates --pairs USDEUR,EURGBP --output my_rates.csv
```
The output file for `.csv` outputs will follow this schema:
- `date`
@ -89,7 +98,7 @@ The output file for `.csv` outputs will follow this schema:
- `exchange_rate`
- `exported_at`
The file will contain all the combinations of the different currencies and dates passed. This includes inverse and equal rates.
The file will contain all the combinations of the different currencies (or pairs) and dates passed. This includes inverse and equal rates.
This is better understood with an example. Find below a real call and its real CSV output:
@ -133,6 +142,7 @@ A few more details:
- Running `get-rates` with an `end-date` beyond the current date will ignore the future dates. The run will behave as if you had specified today as the `end-date`.
- Trying to place an `end-date` before a `start-date` will cause an exception.
- Running with the option `--dry-run` will run against a mock of the xe.com API. Format will be valid, but all rates will be fixed. This is for testing purposes.
- `xexe` will log details to `xexe.log`.
### Deploying for Superhog infra

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "xexe"
version = "1.0.1"
version = "1.1.1"
description = ""
authors = ["Pablo Martin <pablo.martin@superhog.com>"]
readme = "README.md"

View file

@ -50,7 +50,7 @@ if [ $? -ne 0 ]; then
fi
# Define some options here. You might want to change this some day.
currencies="CAD,EUR,NZD,ZAR,AUD,USD,PLN,GBP,CHF"
currencies="CAD,EUR,NZD,ZAR,AUD,USD,PLN,GBP,CHF,AED"
start_date=$(date -d "yesterday" +"%Y-%m-%d")
end_date=$(date -d "yesterday" +"%Y-%m-%d")

View file

@ -24,15 +24,23 @@ def test_handle_input_rates_works_with_full_correct_inputs():
start_date=(datetime.datetime.now() - datetime.timedelta(days=7)).date(),
end_date=(datetime.datetime.now() - datetime.timedelta(days=1)).date(),
),
"currencies": {Currency("USD"), Currency("EUR"), Currency("GBP")},
"pairs": {
CurrencyPair(Currency("USD"), Currency("EUR")),
CurrencyPair(Currency("GBP"), Currency("USD")),
CurrencyPair(Currency("GBP"), Currency("EUR")),
},
"dry_run": False,
"rates_source": "mock",
"ignore_warnings": True,
"output": pathlib.Path("test_output.csv"),
}
for key in expected_result.keys():
for key in {"date_range", "dry_run", "rates_source", "ignore_warnings", "output"}:
assert handled_inputs[key] == expected_result[key]
# We don't check for the currency pairs because the random ordering used
# by the currencies arg execution path does not guarantee the sorting,
# and CurrencyPair comparison needs proper sorting, and my head hurts
# and other tests are already catching for this correctness so.
def test_handle_input_rates_raises_with_bad_currency_code():
@ -106,7 +114,7 @@ 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",
pairs="USDEUR,EURUSD,GBPZAR",
dry_run=False,
rates_source="mock",
ignore_warnings=True,
@ -117,7 +125,11 @@ def test_handle_input_rates_start_and_end_date_equal_works_fine():
start_date=datetime.datetime.now().date(),
end_date=datetime.datetime.now().date(),
),
"currencies": {Currency("USD"), Currency("EUR"), Currency("GBP")},
"pairs": {
CurrencyPair(Currency("USD"), Currency("EUR")),
CurrencyPair(Currency("EUR"), Currency("USD")),
CurrencyPair(Currency("GBP"), Currency("ZAR")),
},
"dry_run": False,
"rates_source": "mock",
"ignore_warnings": True,

View file

@ -4,11 +4,7 @@ import pytest
from money.currency import Currency
from xexe.currency_pair import CurrencyPair
from xexe.utils import (
DateRange,
generate_currency_and_dates_combinations,
generate_pairs_and_dates_combinations,
)
from xexe.utils import DateRange, generate_pairs_and_dates_combinations
def test_date_range_breaks_with_reversed_dates():
@ -35,25 +31,6 @@ def test_date_range_generates_proper_dates_when_itered():
assert len(dates) == 3
def test_generate_currency_and_dates_combinations_outputs_correctly():
date_range = DateRange(
start_date=datetime.date(year=2024, month=1, day=1),
end_date=datetime.date(year=2024, month=1, day=3),
)
currencies = {Currency.USD, Currency.EUR, Currency.GBP}
combinations = generate_currency_and_dates_combinations(
currencies=currencies, date_range=date_range
)
assert len(combinations) == 9
assert len({combination["date"] for combination in combinations}) == 3
assert len({combination["from_currency"] for combination in combinations}) == 2
assert len({combination["to_currency"] for combination in combinations}) == 2
def test_generate_pair_and_dates_combinations_outputs_correctly():
date_range = DateRange(
start_date=datetime.date(year=2024, month=1, day=1),

View file

@ -1,6 +1,7 @@
import datetime
import logging
import pathlib
from itertools import combinations
from typing import Union
from money.currency import Currency
@ -29,6 +30,10 @@ def handle_get_rates_inputs(
if date_range.end_date > datetime.datetime.today().date():
date_range.end_date = datetime.datetime.today().date()
if (currencies is None or currencies == "") and not pairs:
logger.info("No currency list or pairs passed. Running for default currencies.")
currencies = DEFAULT_CURRENCIES
if pairs:
if currencies:
logger.error(f"Received both currencies and pairs.")
@ -47,12 +52,11 @@ def handle_get_rates_inputs(
if currencies:
# CLI input comes as a 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 or currencies == "" and not pairs:
logger.info("No currency list or pairs passed. Running for default currencies.")
currencies = DEFAULT_CURRENCIES
currencies = {Currency(currency_code) for currency_code in currencies}
pairs = list(combinations(currencies, 2))
pairs = {
CurrencyPair(from_currency=pair[0], to_currency=pair[1]) for pair in pairs
}
if rates_source not in RATES_SOURCES:
raise ValueError(f"--rates-source must be one of {RATES_SOURCES.keys()}.")
@ -67,16 +71,11 @@ def handle_get_rates_inputs(
prepared_inputs = {
"date_range": date_range,
"dry_run": dry_run,
"pairs": pairs,
"rates_source": rates_source,
"ignore_warnings": ignore_warnings,
"output": output,
}
if currencies:
prepared_inputs["currencies"] = currencies
if pairs:
prepared_inputs["pairs"] = pairs
logger.debug(prepared_inputs)
return prepared_inputs

View file

@ -11,11 +11,7 @@ from xexe.currency_pair import CurrencyPair
from xexe.exchange_rates import ExchangeRates, add_equal_rates, add_inverse_rates
from xexe.rate_fetching import build_rate_fetcher
from xexe.rate_writing import build_rate_writer
from xexe.utils import (
DateRange,
generate_currency_and_dates_combinations,
generate_pairs_and_dates_combinations,
)
from xexe.utils import DateRange, generate_pairs_and_dates_combinations
logger = logging.getLogger()
@ -76,26 +72,18 @@ def run_get_rates(
rates_source: str,
ignore_warnings: bool,
output: pathlib.Path,
currencies: Union[Set[Currency], None] = None,
pairs: Union[Set[CurrencyPair], None] = None,
) -> None:
logger.info("Getting rates")
process_state = GetRatesProcessState(ignore_warnings=ignore_warnings)
if currencies:
currency_and_date_combinations = generate_currency_and_dates_combinations(
date_range=date_range, currencies=currencies
)
if pairs:
currency_and_date_combinations = generate_pairs_and_dates_combinations(
date_range=date_range, pairs=pairs
)
currency_and_date_combinations = generate_pairs_and_dates_combinations(
date_range=date_range, pairs=pairs
)
rates = obtain_rates_from_source(
rates_source=rates_source,
date_range=date_range,
currency_and_date_combinations=currency_and_date_combinations,
ignore_warnings=ignore_warnings,
)
@ -110,7 +98,6 @@ def run_get_rates(
def obtain_rates_from_source(
rates_source: str,
date_range: DateRange,
ignore_warnings: bool,
currency_and_date_combinations,
) -> ExchangeRates:

View file

@ -69,27 +69,6 @@ class DateRange:
current_date += datetime.timedelta(days=1)
def generate_currency_and_dates_combinations(
date_range: DateRange, currencies: Set[Currency]
) -> Tuple[dict]:
currency_pairs = list(combinations(currencies, 2))
currency_date_combinations = []
for date in date_range:
for from_currency, to_currency in currency_pairs:
currency_date_combinations.append(
{
"from_currency": from_currency,
"to_currency": to_currency,
"date": date,
}
)
currency_date_combinations = tuple(currency_date_combinations)
return currency_date_combinations
def generate_pairs_and_dates_combinations(
date_range: DateRange, pairs: Set[CurrencyPair]
) -> Tuple[dict]: