Newer
Older
"""
Currency module.
This module contains the currency module, which is used to convert currencies.
Uses the ECB (European Central Bank) as the source of currency conversion rates.
To update the currency conversion rates, run the following command:
$ flask update-currency-rates
"""
from decimal import Decimal
from pathlib import Path
from zipfile import ZipFile
import urllib.request
import click
from currency_converter import (
SINGLE_DAY_ECB_URL,
from flask_babel import (
get_locale,
format_currency,
)
from babel.numbers import get_territory_currencies, parse_decimal
from flask import (
Flask,
current_app,
from markupsafe import Markup
from .auth import current_user
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
REF_CURRENCY = 'EUR'
"Reference currency for the currency converter."
logger = logging.getLogger(__name__)
class CurrencyProxy:
"""
Proxy for the currency converter.
This class is used to proxy the currency converter instance. This is to
ensure that the currency converter is only initialized when it is actually
used, and the used conversion list is the most up-to-date.
"""
def __init__(self, app: Flask):
self._converter = None
self._app = app
self._converter_updated = 0
self._dataset_updated = 0
def get_currency_converter(self) -> CurrencyConverter:
"""
Get a currency converter instance.
Automatically updates the currency converter if the dataset has been
updated.
Exceptions:
RuntimeError: If the currency file is not configured.
FileNotFoundError: If the currency file does not exist.
:return: A currency converter instance.
"""
if not (conversion_file := self._app.config.get('CURRENCY_FILE')):
raise RuntimeError('Currency file not configured.')
# Initialize the currency converter if it has not been initialized yet,
# or if the dataset has been updated.
self._dataset_updated = Path(conversion_file).stat().st_mtime
if self._converter is None or self._dataset_updated > self._converter_updated:
logger.info("Initializing currency converter with file %s.", conversion_file)
self._converter = CurrencyConverter(
currency_file=conversion_file,
ref_currency=REF_CURRENCY,
)
self._converter_updated = self._dataset_updated
return self._converter
def __getattr__(self, name):
"""
Proxy all other attributes to the currency converter.
"""
return getattr(self.get_currency_converter(), name)
def init_currency(app: Flask):
"""
Initialize the currency module.
This function initializes the currency module, and registers the currency
converter as an extension.
:param app: The Flask application.
:return: None
"""
# Set default currency file path
app.config.setdefault('CURRENCY_FILE', app.instance_path + '/currency.csv')
# Register the currency converter as an extension
app.extensions['currency_converter'] = CurrencyProxy(app)
# Register the currency converter as a template filter
app.add_template_filter(format_converted_currency, name='localcurrency')
app.cli.add_command(update_currency_rates)
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
def format_converted_currency(value, currency=None, **kwargs):
"""
Render a currency value in the preferred currency.
This function renders a currency value in the preferred currency for the
current locale. If the preferred currency is not the reference currency,
the value is converted to the preferred currency.
"""
if currency is None:
currency = get_preferred_currency()
# Convert the value to the preferred currency
local_value = convert_currency(value, currency)
# Format the value
html = render_template("money-tag.html",
base_amount=format_currency(value, currency=REF_CURRENCY, format_type='name', **kwargs),
local_amount=format_currency(local_value, currency=currency, **kwargs))
return Markup(html)
def convert_currency(value, currency=None, from_currency=REF_CURRENCY):
"""
Convert a currency value to the preferred currency.
This function converts a currency value to the preferred currency for the
current locale. If the preferred currency is not the reference currency,
the value is converted to the preferred currency.
"""
if currency != REF_CURRENCY:
return current_app.extensions['currency_converter'].convert(value, from_currency, currency)
return value
def convert_from_currency(value, currency) -> Decimal:
"""
Parses the localized currency value and converts it to the reference currency.
"""
locale = get_locale()
amount = parse_decimal(value, locale=locale)
if currency != REF_CURRENCY:
amount = Decimal(current_app.extensions['currency_converter'].convert(amount, currency, REF_CURRENCY))
return amount
def get_currencies():
"""
Get the list of supported currencies.
"""
return current_app.extensions['currency_converter'].currencies
def get_preferred_currency():
"""
Get the preferred currency.
This function returns the preferred currency for the current locale.
:return: The preferred currency.
"""
if current_user.is_authenticated and current_user.currency:
return str(current_user.currency)
# Fall back to the default currency for the locale
if territory := get_locale().territory:
currency = get_territory_currencies(territory)[0]
if currency in get_currencies():
return currency
else:
logger.warning("Default currency %s is not supported, falling back to %s.", currency, REF_CURRENCY)
return REF_CURRENCY
@click.command()
def update_currency_rates():
"""
Update currency file from the European Central Bank.
This command is meant to be run from the command line, and is not meant to be
used in the application:
$ flask update-currency-rates
:return: None
"""
click.echo('Updating currency file from the European Central Bank...')
fetch_currency_file()
click.echo('Done.')
def fetch_currency_file():
"""
Fetch the currency file from the European Central Bank.
This function fetches the currency file from the European Central Bank, and
stores it in the configured currency file path.
"""
from tempfile import NamedTemporaryFile # pylint: disable=import-outside-toplevel
fd, _ = urllib.request.urlretrieve(SINGLE_DAY_ECB_URL)
with ZipFile(fd) as zf:
file_name = zf.namelist().pop()
# Create a temporary file to store the currency file, to avoid corrupting
# the existing file if the download fails, or while writing the file.
file_path = os.path.dirname(current_app.config['CURRENCY_FILE'])
if not os.path.exists(file_path):
os.makedirs(file_path)
with NamedTemporaryFile(dir=file_path, delete=False) as f:
# Move the temporary file to the configured currency file path
os.rename(f.name, current_app.config['CURRENCY_FILE'])