Skip to content

Commit

Permalink
add currency exchange rates
Browse files Browse the repository at this point in the history
  • Loading branch information
Changaco committed Oct 27, 2017
1 parent cc1e5ee commit f851948
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 3 deletions.
18 changes: 17 additions & 1 deletion liberapay/cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
logger = logging.getLogger('liberapay.cron')


Daily = namedtuple('Daily', 'hour')
Weekly = namedtuple('Weekly', 'weekday hour')


Expand All @@ -20,7 +21,7 @@ def __init__(self, website):
self.exclusive_jobs = []

def __call__(self, period, func, exclusive=False):
if isinstance(period, int) and period <= 0:
if not self.website.env.run_cron_jobs:
return
if exclusive and not self.has_lock:
self.exclusive_jobs.append((period, func))
Expand All @@ -45,6 +46,21 @@ def f():
except Exception as e:
self.website.tell_sentry(e, {})
sleep(86400 * 6)
elif isinstance(period, Daily):
while True:
now = datetime.utcnow()
then = now.replace(hour=period.hour, minute=0, second=0)
seconds = (then - now).total_seconds()
if seconds > 0:
# later today
sleep(seconds)
elif seconds < -3600:
# tomorrow
sleep(3600 * 24 + seconds)
try:
func()
except Exception as e:
self.website.tell_sentry(e, {})
else:
while True:
try:
Expand Down
6 changes: 4 additions & 2 deletions liberapay/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@

from liberapay import utils, wireup
from liberapay.billing.payday import Payday, create_payday_issue
from liberapay.cron import Cron, Weekly
from liberapay.cron import Cron, Daily, Weekly
from liberapay.models.community import Community
from liberapay.models.participant import Participant
from liberapay.models.repository import refetch_repos
from liberapay.security import authentication, csrf, set_default_security_headers
from liberapay.utils import b64decode_s, b64encode_s, erase_cookie, http_caching, i18n, set_cookie
from liberapay.utils.currencies import fetch_currency_exchange_rates
from liberapay.utils.state_chain import (
attach_environ_to_request, create_response_object, canonize, insert_constants,
_dispatch_path_to_filesystem, merge_exception_into_response, return_500_for_exception,
Expand Down Expand Up @@ -94,7 +95,7 @@ def _assert(x):
# =============

conf = website.app_conf
if env.run_cron_jobs and conf:
if conf:
cron = Cron(website)
cron(conf.check_db_every, website.db.self_check, True)
cron(conf.dequeue_emails_every, Participant.dequeue_emails, True)
Expand All @@ -103,6 +104,7 @@ def _assert(x):
cron(Weekly(weekday=3, hour=2), create_payday_issue, True)
cron(conf.clean_up_counters_every, website.db.clean_up_counters, True)
cron(conf.update_cached_amounts_every, Payday.update_cached_amounts, True)
cron(Daily(hour=16), lambda: fetch_currency_exchange_rates(website.db), True)


# Website Algorithm
Expand Down
46 changes: 46 additions & 0 deletions liberapay/utils/currencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from __future__ import absolute_import, division, print_function, unicode_literals

from decimal import Decimal, ROUND_UP

from mangopay.utils import Money
import requests
import xmltodict

from liberapay.constants import D_CENT, D_ZERO
from liberapay.website import website


def _convert(self, c):
if self.currency == c:
return self
amount = self.amount * website.currency_exchange_rates[(self.currency, c)]
return Money(amount.quantize(D_CENT), c)


Money.convert = _convert


def fetch_currency_exchange_rates(db):
currencies = set(db.one("SELECT array_to_json(enum_range(NULL::currency))"))
r = requests.get('https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml')
rates = xmltodict.parse(r.text)['gesmes:Envelope']['Cube']['Cube']['Cube']
for fx in rates:
currency = fx['@currency']
if currency not in currencies:
continue
db.run("""
INSERT INTO currency_exchange_rates
(source_currency, target_currency, rate)
VALUES ('EUR', %(target)s, %(rate)s)
, (%(target)s, 'EUR', 1 / %(rate)s)
ON CONFLICT (source_currency, target_currency) DO UPDATE
SET rate = excluded.rate
""", dict(target=currency, rate=Decimal(fx['@rate'])))


def get_currency_exchange_rates(db):
r = {(r[0], r[1]): r[2] for r in db.all("SELECT * FROM currency_exchange_rates")}
if r:
return r
fetch_currency_exchange_rates(db)
return get_currency_exchange_rates(db)
6 changes: 6 additions & 0 deletions liberapay/wireup.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from liberapay.models import DB
from liberapay.security.authentication import ANON
from liberapay.utils import find_files, markdown, mkdir_p
from liberapay.utils.currencies import get_currency_exchange_rates
from liberapay.utils.emails import compile_email_spt
from liberapay.utils.http_caching import asset_etag
from liberapay.utils.i18n import (
Expand Down Expand Up @@ -636,6 +637,10 @@ def s3(env):
return {'s3': s3}


def currency_exchange_rates(db):
return {'currency_exchange_rates': get_currency_exchange_rates(db)}


minimal_algorithm = Algorithm(
env,
make_sentry_teller,
Expand All @@ -657,6 +662,7 @@ def s3(env):
load_scss_variables,
s3,
trusted_proxies,
currency_exchange_rates,
)


Expand Down
4 changes: 4 additions & 0 deletions sql/app-conf-tests.sql
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ END;
$$;

DROP FUNCTION update_app_conf(text, jsonb);

INSERT INTO currency_exchange_rates
VALUES ('EUR', 'USD', 1.2)
, ('USD', 'EUR', 1 / 1.2);
30 changes: 30 additions & 0 deletions sql/currencies.sql
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,33 @@ CREATE OPERATOR <= (
commutator = >=,
negator = >
);


-- Exchange rates

CREATE TABLE currency_exchange_rates
( source_currency currency NOT NULL
, target_currency currency NOT NULL
, rate numeric NOT NULL
, UNIQUE (source_currency, target_currency)
);


-- Currency conversion function

CREATE FUNCTION convert(currency_amount, currency) RETURNS currency_amount AS $$
DECLARE
rate numeric;
BEGIN
IF ($1.currency = $2) THEN RETURN $1; END IF;
rate := (
SELECT r.rate
FROM currency_exchange_rates r
WHERE r.source_currency = $1.currency
);
IF (rate IS NULL) THEN
RAISE 'missing exchange rate %->%', $1.currency, $2;
END IF;
RETURN ($1.amount / rate, $2);
END;
$$ LANGUAGE plpgsql STRICT;
1 change: 1 addition & 0 deletions tests/test.env
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ CACHE_STATIC=yes
CLEAN_ASSETS=yes
OVERRIDE_QUERY_CACHE=yes
ASPEN_CHANGES_RELOAD=no
RUN_CRON_JOBS=no

0 comments on commit f851948

Please sign in to comment.