Compare commits

...

6 commits

Author SHA1 Message Date
gr4viton
359663f732
Merge 23d5b73ae1 into 61ea1f54d2 2024-12-23 21:26:17 +01:00
dependabot[bot]
61ea1f54d2 Update psycopg2-binary requirement
Some checks are pending
CI / lint (push) Waiting to run
CI / test (mariadb, minimal, 3.11) (push) Blocked by required conditions
CI / test (mariadb, normal, 3.11) (push) Blocked by required conditions
CI / test (mariadb, normal, 3.9) (push) Blocked by required conditions
CI / test (postgresql, minimal, 3.11) (push) Blocked by required conditions
CI / test (postgresql, normal, 3.11) (push) Blocked by required conditions
CI / test (postgresql, normal, 3.9) (push) Blocked by required conditions
CI / test (sqlite, minimal, 3.10) (push) Blocked by required conditions
CI / test (sqlite, minimal, 3.11) (push) Blocked by required conditions
CI / test (sqlite, minimal, 3.12) (push) Blocked by required conditions
CI / test (sqlite, minimal, 3.9) (push) Blocked by required conditions
CI / test (sqlite, normal, 3.10) (push) Blocked by required conditions
CI / test (sqlite, normal, 3.11) (push) Blocked by required conditions
CI / test (sqlite, normal, 3.12) (push) Blocked by required conditions
CI / test (sqlite, normal, 3.8) (push) Blocked by required conditions
CI / test (sqlite, normal, 3.9) (push) Blocked by required conditions
CI / docs (push) Waiting to run
Docker build / test (push) Waiting to run
Docker build / build_upload (push) Blocked by required conditions
Updates the requirements on [psycopg2-binary](https://github.com/psycopg/psycopg2) to permit the latest version.
- [Changelog](https://github.com/psycopg/psycopg2/blob/master/NEWS)
- [Commits](https://github.com/psycopg/psycopg2/compare/2.9.6...2.9.10)

---
updated-dependencies:
- dependency-name: psycopg2-binary
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 21:26:07 +01:00
dependabot[bot]
299c384908 Bump ruff from 0.6.8 to 0.8.4
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.8 to 0.8.4.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.6.8...0.8.4)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 21:24:04 +01:00
dependabot[bot]
4e9ff9b1ac Update qrcode requirement from <8,>=7.1 to >=7.1,<9
Updates the requirements on [qrcode](https://github.com/lincolnloop/python-qrcode) to permit the latest version.
- [Changelog](https://github.com/lincolnloop/python-qrcode/blob/main/CHANGES.rst)
- [Commits](https://github.com/lincolnloop/python-qrcode/compare/v7.1...v8.0)

---
updated-dependencies:
- dependency-name: qrcode
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 21:23:54 +01:00
dependabot[bot]
2aa410c68f Update cachetools requirement from <5,>=4.1 to >=4.1,<6
Updates the requirements on [cachetools](https://github.com/tkem/cachetools) to permit the latest version.
- [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tkem/cachetools/compare/v4.1.0...v5.5.0)

---
updated-dependencies:
- dependency-name: cachetools
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 21:23:33 +01:00
gr4viton
23d5b73ae1 refactor currency converter
to allow multiple exchange rate getters
defaulting to user defined csv file
or hard coded exchange rates
2024-10-28 22:59:10 +01:00
2 changed files with 89 additions and 192 deletions

View file

@ -1,10 +1,17 @@
import csv
import traceback import traceback
import warnings import warnings
from abc import ABC, abstractmethod
from decimal import Decimal
from typing import Dict, Optional
from cachetools import TTLCache, cached from cachetools import TTLCache, cached
import requests import requests
NO_CURRENCY = "XXX"
ExchangeRates = Dict[str, float]
class Singleton(type): class Singleton(type):
_instances = {} _instances = {}
@ -14,205 +21,93 @@ class Singleton(type):
return cls._instances[cls] return cls._instances[cls]
class CurrencyConverter(object, metaclass=Singleton): class ExchangeRateGetter(ABC):
# Get exchange rates
no_currency = "XXX" def get_rates(self) -> Optional[ExchangeRates]:
"""Method to retrieve a list of currency conversion rates.
Returns:
currencies: dict - key is a three-letter currency, value is float of conversion to base currency
"""
try:
return self._get_rates()
except Exception:
warnings.warn(
f"Exchange rate getter failed - {traceback.format_exc(limit=0).strip()}"
)
@abstractmethod
def _get_rates(self) -> Optional[ExchangeRates]:
"""Actual implementation of the exchange rate getter."""
raise NotImplementedError
class ApiExchangeRate(ExchangeRateGetter):
api_url = "https://api.exchangerate.host/latest?base=USD" api_url = "https://api.exchangerate.host/latest?base=USD"
def _get_rates(self) -> Optional[ExchangeRates]:
return requests.get(self.api_url).json()["rates"] # TODO not working currently probably
class UserExchangeRate(ExchangeRateGetter):
user_csv_file = "path/to/file.csv"
def _get_rates(self) -> Optional[ExchangeRates]:
"""Get rates from user defined csv.
The user_csv_file should contain the currency conversions to "USD" without 1 header row
Example:
```
currency_code,fx_rate_to_USD
CZK,25.0
...
```
TODO: make it work bi-directionally
TODO: document for the user where to place the file
"""
reader = csv.reader(self.user_csv_file)
rates = {}
for row in reader:
from_currency = row[0]
rate = float(row[1])
# TODO add validation and exception handling for typos
rates[from_currency] = rate
return rates
class HardCodedExchangeRate(ExchangeRateGetter):
def _get_rates(self) -> Optional[dict]:
return {"USD": 1.0} # TODO fill in more
class CurrencyConverter(object, metaclass=Singleton):
no_currency = NO_CURRENCY
def __init__(self): def __init__(self):
pass pass
@cached(cache=TTLCache(maxsize=1, ttl=86400)) @cached(cache=TTLCache(maxsize=1, ttl=86400))
def get_rates(self): def get_rates(self):
try: """Try to retrieve the exchange rate from various sources, defaulting to hard coded values."""
rates = requests.get(self.api_url).json()["rates"] for provider in [ApiExchangeRate, UserExchangeRate, HardCodedExchangeRate]:
except Exception: if rates:= provider.get_rates():
warnings.warn( break
f"Call to {self.api_url} failed: {traceback.format_exc(limit=0).strip()}" else:
)
# In case of any exception, let's have an empty value
rates = {} rates = {}
rates[self.no_currency] = 1.0 rates[NO_CURRENCY] = 1.0
return rates return rates
def get_currencies(self, with_no_currency=True): def get_currencies(self, with_no_currency: bool=True) -> list:
currencies = [ currencies = list(self.get_rates.keys())
"AED",
"AFN",
"ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BDT",
"BGN",
"BHD",
"BIF",
"BMD",
"BND",
"BOB",
"BRL",
"BSD",
"BTC",
"BTN",
"BWP",
"BYN",
"BZD",
"CAD",
"CDF",
"CHF",
"CLF",
"CLP",
"CNH",
"CNY",
"COP",
"CRC",
"CUC",
"CUP",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EGP",
"ERN",
"ETB",
"EUR",
"FJD",
"FKP",
"GBP",
"GEL",
"GGP",
"GHS",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"IMP",
"INR",
"IQD",
"IRR",
"ISK",
"JEP",
"JMD",
"JOD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KPW",
"KRW",
"KWD",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"LYD",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRU",
"MUR",
"MVR",
"MWK",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"OMR",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SDG",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"SSP",
"STD",
"STN",
"SVC",
"SYP",
"SZL",
"THB",
"TJS",
"TMT",
"TND",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VEF",
"VES",
"VND",
"VUV",
"WST",
"XAF",
"XAG",
"XAU",
"XCD",
"XDR",
"XOF",
"XPD",
"XPF",
"XPT",
"YER",
"ZAR",
"ZMW",
"ZWL",
]
if with_no_currency: if with_no_currency:
currencies.append(self.no_currency) currencies.append(self.no_currency)
return currencies return currencies
def exchange_currency(self, amount, source_currency, dest_currency): def exchange_currency(self, amount: float, source_currency: str, dest_currency: str) -> float:
"""Return the money amount converted from source_currency to dest_currency."""
if ( if (
source_currency == dest_currency source_currency == dest_currency
or source_currency == self.no_currency or source_currency == self.no_currency
@ -223,6 +118,8 @@ class CurrencyConverter(object, metaclass=Singleton):
rates = self.get_rates() rates = self.get_rates()
source_rate = rates[source_currency] source_rate = rates[source_currency]
dest_rate = rates[dest_currency] dest_rate = rates[dest_currency]
new_amount = (float(amount) / source_rate) * dest_rate # Using Decimal to not introduce floating-point operation absolute errors
# round to two digits because we are dealing with money new_amount = (Decimal(amount) / Decimal(source_rate)) * Decimal(dest_rate)
return round(new_amount, 2) # dealing with money - only round the shown amount before showing it to user
# - think about 10 * 0.0003 == 0?
return float(new_amount)

View file

@ -27,7 +27,7 @@ classifiers = [
dependencies = [ dependencies = [
"blinker>=1.4,<2", "blinker>=1.4,<2",
"cachetools>=4.1,<5", "cachetools>=4.1,<6",
"debts>=0.5,<1", "debts>=0.5,<1",
"email_validator>=1.0,<3", "email_validator>=1.0,<3",
"Flask>=2,<4", "Flask>=2,<4",
@ -43,7 +43,7 @@ dependencies = [
"itsdangerous>=2,<3", "itsdangerous>=2,<3",
"Jinja2>=3,<4", "Jinja2>=3,<4",
"python-dateutil", "python-dateutil",
"qrcode>=7.1,<8", "qrcode>=7.1,<9",
"requests>=2.25,<3", "requests>=2.25,<3",
"SQLAlchemy>=1.3.0,<1.5", "SQLAlchemy>=1.3.0,<1.5",
"SQLAlchemy-Continuum>=1.3.12,<2", # New 1.4 changes API, see #728 "SQLAlchemy-Continuum>=1.3.12,<2", # New 1.4 changes API, see #728
@ -53,11 +53,11 @@ dependencies = [
[project.optional-dependencies] [project.optional-dependencies]
database = [ database = [
# Python 3.11 support starts in 2.9.2 # Python 3.11 support starts in 2.9.2
"psycopg2-binary>=2.9.2,<2.9.9", "psycopg2-binary>=2.9.2,<2.9.11",
"PyMySQL>=0.9,<1.2", "PyMySQL>=0.9,<1.2",
] ]
dev = [ dev = [
"ruff==0.6.8", "ruff==0.8.4",
"flake8==5.0.4", "flake8==5.0.4",
"isort==5.11.5", "isort==5.11.5",
"vermin==1.6.0", "vermin==1.6.0",