Compare commits
10 commits
aba2920a70
...
a25f7d267b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a25f7d267b | ||
|
|
52adcc7538 | ||
|
|
03134b08f7 | ||
|
|
d2e53e40be | ||
|
|
9d6c93c89f | ||
|
|
d20c46d52d | ||
|
|
22bfc217f3 | ||
|
|
7f8001ffca | ||
|
|
a7a37d4614 | ||
|
|
b52af85987 |
9 changed files with 57 additions and 81 deletions
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
12
README.md
12
README.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue