Better logging of critical exceptions. Isolate tests and error handling for ECB.

This commit is contained in:
Chris Berkhout 2021-07-11 15:50:59 +02:00
parent 2aa4319dbb
commit c0c7e546a3
6 changed files with 604 additions and 66 deletions

View file

@ -1,6 +1,9 @@
import logging import logging
import sys
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from pricehist import exceptions
def fetch(series, source, output, invert: bool, quantize: int, fmt) -> str: def fetch(series, source, output, invert: bool, quantize: int, fmt) -> str:
if series.start < source.start(): if series.start < source.start():
@ -9,7 +12,12 @@ def fetch(series, source, output, invert: bool, quantize: int, fmt) -> str:
f"source start date of {source.start()}." f"source start date of {source.start()}."
) )
series = source.fetch(series) try:
series = source.fetch(series)
except exceptions.SourceError as e:
logging.debug("Critical exception encountered", exc_info=e)
logging.critical(str(e))
sys.exit(1)
if len(series.prices) == 0: if len(series.prices) == 0:
logging.warn(f"No data found for the interval [{series.start}--{series.end}].") logging.warn(f"No data found for the interval [{series.start}--{series.end}].")

View file

@ -4,11 +4,18 @@ import sys
class Formatter(logging.Formatter): class Formatter(logging.Formatter):
def format(self, record): def format(self, record):
message = record.msg % record.args if record.args else record.msg s = record.msg % record.args if record.args else record.msg
if record.levelno == logging.INFO:
return message if record.exc_info:
else: record.exc_text = self.formatException(record.exc_info)
return f"{record.levelname} {message}" if s[-1:] != "\n":
s = s + "\n"
s = s + "\n".join([f" {line}" for line in record.exc_text.splitlines()])
if record.levelno != logging.INFO:
s = "\n".join([f"{record.levelname} {line}" for line in s.splitlines()])
return s
def init(): def init():

View file

@ -1,13 +1,11 @@
import dataclasses import dataclasses
import logging
import sys
from datetime import datetime, timedelta from datetime import datetime, timedelta
from decimal import Decimal from decimal import Decimal
import requests import requests
from lxml import etree from lxml import etree
from pricehist import isocurrencies from pricehist import exceptions, isocurrencies
from pricehist.price import Price from pricehist.price import Price
from .basesource import BaseSource from .basesource import BaseSource
@ -36,19 +34,17 @@ class ECB(BaseSource):
return "" return ""
def symbols(self): def symbols(self):
root = self._data(more_than_90_days=True) quotes = self._quotes()
nodes = root.cssselect("[currency]")
currencies = sorted(set([n.attrib["currency"] for n in nodes]))
iso = isocurrencies.by_code() iso = isocurrencies.by_code()
return [(f"EUR/{c}", f"Euro against {iso[c].name}") for c in currencies] return [
(f"EUR/{c}", f"Euro against {iso[c].name if c in iso else c}")
for c in quotes
]
def fetch(self, series): def fetch(self, series):
if series.base != "EUR": # EUR is the only valid base. if series.base != "EUR" or not series.quote: # EUR is the only valid base.
logging.critical( raise exceptions.InvalidPair(series.base, series.quote, self)
f"Invalid pair '{'/'.join([series.base, series.quote])}'. "
f"Run 'pricehist source {self.id()} --symbols' to list valid pairs."
)
sys.exit(1)
almost_90_days_ago = (datetime.now().date() - timedelta(days=85)).isoformat() almost_90_days_ago = (datetime.now().date() - timedelta(days=85)).isoformat()
root = self._data(series.start < almost_90_days_ago) root = self._data(series.start < almost_90_days_ago)
@ -58,12 +54,24 @@ class ECB(BaseSource):
for row in day.cssselect(f"[currency='{series.quote}']"): for row in day.cssselect(f"[currency='{series.quote}']"):
rate = Decimal(row.attrib["rate"]) rate = Decimal(row.attrib["rate"])
all_rows.insert(0, (date, rate)) all_rows.insert(0, (date, rate))
if not all_rows and series.quote not in self._quotes():
raise exceptions.InvalidPair(series.base, series.quote, self)
selected = [ selected = [
Price(d, r) for d, r in all_rows if d >= series.start and d <= series.end Price(d, r) for d, r in all_rows if d >= series.start and d <= series.end
] ]
return dataclasses.replace(series, prices=selected) return dataclasses.replace(series, prices=selected)
def _quotes(self):
root = self._data(more_than_90_days=True)
nodes = root.cssselect("[currency]")
quotes = sorted(set([n.attrib["currency"] for n in nodes]))
if not quotes:
raise exceptions.ResponseParsingError("Expected data not found")
return quotes
def _data(self, more_than_90_days=False): def _data(self, more_than_90_days=False):
url_base = "https://www.ecb.europa.eu/stats/eurofxref" url_base = "https://www.ecb.europa.eu/stats/eurofxref"
if more_than_90_days: if more_than_90_days:
@ -71,6 +79,19 @@ class ECB(BaseSource):
else: else:
source_url = f"{url_base}/eurofxref-hist-90d.xml" # last 90 days source_url = f"{url_base}/eurofxref-hist-90d.xml" # last 90 days
response = self.log_curl(requests.get(source_url)) try:
root = etree.fromstring(response.content) response = self.log_curl(requests.get(source_url))
except Exception as e:
raise exceptions.RequestError(str(e)) from e
try:
response.raise_for_status()
except Exception as e:
raise exceptions.BadResponse(str(e)) from e
try:
root = etree.fromstring(response.content)
except Exception as e:
raise exceptions.ResponseParsingError(str(e)) from e
return root return root

View file

@ -1,14 +1,18 @@
import logging
import os
from datetime import datetime, timedelta
from decimal import Decimal from decimal import Decimal
from pathlib import Path
import pytest import pytest
import requests
import responses
from pricehist import exceptions, isocurrencies
from pricehist.price import Price from pricehist.price import Price
from pricehist.series import Series from pricehist.series import Series
from pricehist.sources.ecb import ECB from pricehist.sources.ecb import ECB
# import responses
# @responses.activate
def in_log(caplog, levelname, substr): def in_log(caplog, levelname, substr):
return any( return any(
@ -26,73 +30,204 @@ def type(src):
return src.types()[0] return src.types()[0]
@pytest.fixture
def xml():
dir = Path(os.path.splitext(__file__)[0])
return (dir / "eurofxref-hist-partial.xml").read_text()
@pytest.fixture
def empty_xml():
dir = Path(os.path.splitext(__file__)[0])
return (dir / "eurofxref-hist-empty.xml").read_text()
@pytest.fixture
def requests_mock():
with responses.RequestsMock() as mock:
yield mock
@pytest.fixture
def response_ok(requests_mock, xml):
requests_mock.add(
responses.GET,
"https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml",
body=xml,
status=200,
)
yield requests_mock
@pytest.fixture
def response_ok_90d(requests_mock, xml):
requests_mock.add(
responses.GET,
"https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml",
body=xml,
status=200,
)
yield requests_mock
@pytest.fixture
def response_empty_xml(requests_mock, empty_xml):
requests_mock.add(
responses.GET,
"https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml",
body=empty_xml,
status=200,
)
yield requests_mock
def test_normalizesymbol(src): def test_normalizesymbol(src):
assert src.normalizesymbol("eur") == "EUR" assert src.normalizesymbol("eur") == "EUR"
assert src.normalizesymbol("symbol") == "SYMBOL" assert src.normalizesymbol("symbol") == "SYMBOL"
@pytest.mark.live def test_metadata(src):
def test_known_pair(src, type): assert isinstance(src.id(), str)
series = src.fetch(Series("EUR", "AUD", type, "2021-01-11", "2021-01-22")) assert len(src.id()) > 0
assert series.prices[0] == Price("2021-01-11", Decimal("1.5783"))
assert series.prices[-1] == Price("2021-01-22", Decimal("1.577")) assert isinstance(src.name(), str)
assert len(series.prices) == 10 assert len(src.name()) > 0
assert isinstance(src.description(), str)
assert len(src.description()) > 0
assert isinstance(src.source_url(), str)
assert src.source_url().startswith("http")
assert datetime.strptime(src.start(), "%Y-%m-%d")
assert isinstance(src.types(), list)
assert len(src.types()) > 0
assert isinstance(src.types()[0], str)
assert len(src.types()[0]) > 0
assert isinstance(src.notes(), str)
@pytest.mark.live def test_symbols(src, response_ok):
def test_long_hist_from_start(src, type): syms = src.symbols()
series = src.fetch(Series("EUR", "AUD", type, src.start(), "2021-07-01")) assert ("EUR/AUD", "Euro against Australian Dollar") in syms
assert len(syms) > 40
def test_requests_logged_for_symbols(src, response_ok, caplog):
with caplog.at_level(logging.DEBUG):
src.symbols()
assert in_log(caplog, "DEBUG", " curl ")
def test_symbols_not_in_iso_data(src, response_ok, monkeypatch):
iso = isocurrencies.by_code()
del iso["AUD"]
monkeypatch.setattr(isocurrencies, "by_code", lambda: iso)
syms = src.symbols()
assert ("EUR/AUD", "Euro against AUD") in syms
def test_symbols_not_found(src, response_empty_xml):
with pytest.raises(exceptions.ResponseParsingError) as e:
src.symbols()
assert "data not found" in str(e.value)
def test_known_pair(src, type, response_ok):
series = src.fetch(Series("EUR", "AUD", type, "2021-01-04", "2021-01-08"))
assert series.prices[0] == Price("2021-01-04", Decimal("1.5928"))
assert series.prices[-1] == Price("2021-01-08", Decimal("1.5758"))
assert len(series.prices) == 5
def test_requests_logged_for_fetch(src, response_ok, caplog):
with caplog.at_level(logging.DEBUG):
src.fetch(Series("EUR", "AUD", type, "2021-01-04", "2021-01-08"))
assert in_log(caplog, "DEBUG", " curl ")
def test_recent_interval_uses_90d_data(src, type, response_ok_90d):
today = datetime.now().date()
start = (today - timedelta(days=80)).isoformat()
end = today.isoformat()
src.fetch(Series("EUR", "AUD", type, start, end))
assert len(response_ok_90d.calls) > 0
def test_long_hist_from_start(src, type, response_ok):
series = src.fetch(Series("EUR", "AUD", type, src.start(), "2021-01-08"))
assert series.prices[0] == Price("1999-01-04", Decimal("1.91")) assert series.prices[0] == Price("1999-01-04", Decimal("1.91"))
assert series.prices[-1] == Price("2021-07-01", Decimal("1.5836")) assert series.prices[-1] == Price("2021-01-08", Decimal("1.5758"))
assert len(series.prices) == 5759 assert len(series.prices) > 9
@pytest.mark.live def test_from_before_start(src, type, response_ok):
def test_from_before_start(src, type):
series = src.fetch(Series("EUR", "AUD", type, "1998-12-01", "1999-01-10")) series = src.fetch(Series("EUR", "AUD", type, "1998-12-01", "1999-01-10"))
assert series.prices[0] == Price("1999-01-04", Decimal("1.91")) assert series.prices[0] == Price("1999-01-04", Decimal("1.91"))
assert series.prices[-1] == Price("1999-01-08", Decimal("1.8406")) assert series.prices[-1] == Price("1999-01-08", Decimal("1.8406"))
assert len(series.prices) == 5 assert len(series.prices) == 5
@pytest.mark.live def test_to_future(src, type, response_ok):
def test_to_future(src, type): series = src.fetch(Series("EUR", "AUD", type, "2021-01-04", "2100-01-01"))
series = src.fetch(Series("EUR", "AUD", type, "2021-07-01", "2100-01-01"))
assert len(series.prices) > 0 assert len(series.prices) > 0
@pytest.mark.live def test_known_pair_no_data(src, type, response_ok):
def test_known_pair_no_data(src, type): series = src.fetch(Series("EUR", "ROL", type, "2021-01-04", "2021-02-08"))
series = src.fetch(Series("EUR", "ROL", type, "2020-01-01", "2021-01-01"))
assert len(series.prices) == 0 assert len(series.prices) == 0
def test_non_eur_base(src, type, caplog): def test_non_eur_base(src, type):
with pytest.raises(SystemExit) as e: with pytest.raises(exceptions.InvalidPair):
src.fetch(Series("USD", "AUD", type, "2021-01-01", "2021-02-01")) src.fetch(Series("USD", "AUD", type, "2021-01-04", "2021-01-08"))
assert e.value.code == 1
assert in_log(caplog, "CRITICAL", "Invalid pair")
@pytest.mark.xfail def test_unknown_quote(src, type, response_ok):
@pytest.mark.live with pytest.raises(exceptions.InvalidPair):
def test_unknown_quote(src, type, caplog): src.fetch(Series("EUR", "XZY", type, "2021-01-04", "2021-01-08"))
with pytest.raises(SystemExit) as e:
src.fetch(Series("EUR", "XZY", type, "2021-01-01", "2021-02-01"))
assert e.value.code == 1
assert in_log(caplog, "CRITICAL", "Invalid pair")
@pytest.mark.xfail def test_no_quote(src, type):
def test_no_quote(src, type, caplog): with pytest.raises(exceptions.InvalidPair):
with pytest.raises(SystemExit) as e: src.fetch(Series("EUR", "", type, "2021-01-04", "2021-01-08"))
src.fetch(Series("EUR", "", type, "2021-01-01", "2021-02-01"))
assert e.value.code == 1
assert in_log(caplog, "CRITICAL", "Invalid pair")
def test_unknown_pair(src, type, caplog): def test_unknown_pair(src, type):
with pytest.raises(SystemExit) as e: with pytest.raises(exceptions.InvalidPair):
src.fetch(Series("ABC", "XZY", type, "2021-01-01", "2021-02-01")) src.fetch(Series("ABC", "XZY", type, "2021-01-04", "2021-01-08"))
assert e.value.code == 1
assert in_log(caplog, "CRITICAL", "Invalid pair")
def test_network_issue(src, type, requests_mock):
requests_mock.add(
responses.GET,
"https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml",
body=requests.exceptions.ConnectionError("Network issue"),
)
with pytest.raises(exceptions.RequestError) as e:
src.fetch(Series("EUR", "AUD", type, "2021-01-04", "2021-01-08"))
assert "Network issue" in str(e.value)
def test_bad_status(src, type, requests_mock):
requests_mock.add(
responses.GET,
"https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml",
status=500,
)
with pytest.raises(exceptions.BadResponse) as e:
src.fetch(Series("EUR", "AUD", type, "2021-01-04", "2021-01-08"))
assert "Server Error" in str(e.value)
def test_parsing_error(src, type, requests_mock):
requests_mock.add(
responses.GET,
"https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml",
body="NOT XML",
)
with pytest.raises(exceptions.ResponseParsingError) as e:
src.fetch(Series("EUR", "AUD", type, "2021-01-04", "2021-01-08"))
assert "while parsing data" in str(e.value)

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<gesmes:Envelope xmlns:gesmes="http://www.gesmes.org/xml/2002-08-01" xmlns="http://www.ecb.int/vocabulary/2002-08-01/eurofxref">
<gesmes:subject>Reference rates</gesmes:subject>
<gesmes:Sender>
<gesmes:name>European Central Bank</gesmes:name>
</gesmes:Sender>
<Cube>
</Cube>
</gesmes:Envelope>

View file

@ -0,0 +1,358 @@
<?xml version="1.0" encoding="UTF-8"?>
<gesmes:Envelope xmlns:gesmes="http://www.gesmes.org/xml/2002-08-01" xmlns="http://www.ecb.int/vocabulary/2002-08-01/eurofxref">
<gesmes:subject>Reference rates</gesmes:subject>
<gesmes:Sender>
<gesmes:name>European Central Bank</gesmes:name>
</gesmes:Sender>
<Cube>
<Cube time="2021-01-08">
<Cube currency="USD" rate="1.225"/>
<Cube currency="JPY" rate="127.26"/>
<Cube currency="BGN" rate="1.9558"/>
<Cube currency="CZK" rate="26.163"/>
<Cube currency="DKK" rate="7.4369"/>
<Cube currency="GBP" rate="0.90128"/>
<Cube currency="HUF" rate="359.62"/>
<Cube currency="PLN" rate="4.5113"/>
<Cube currency="RON" rate="4.8708"/>
<Cube currency="SEK" rate="10.051"/>
<Cube currency="CHF" rate="1.0827"/>
<Cube currency="ISK" rate="155.5"/>
<Cube currency="NOK" rate="10.2863"/>
<Cube currency="HRK" rate="7.569"/>
<Cube currency="RUB" rate="90.8"/>
<Cube currency="TRY" rate="9.0146"/>
<Cube currency="AUD" rate="1.5758"/>
<Cube currency="BRL" rate="6.5748"/>
<Cube currency="CAD" rate="1.5543"/>
<Cube currency="CNY" rate="7.9184"/>
<Cube currency="HKD" rate="9.4982"/>
<Cube currency="IDR" rate="17247.33"/>
<Cube currency="ILS" rate="3.8981"/>
<Cube currency="INR" rate="89.7975"/>
<Cube currency="KRW" rate="1337.9"/>
<Cube currency="MXN" rate="24.4718"/>
<Cube currency="MYR" rate="4.9359"/>
<Cube currency="NZD" rate="1.6883"/>
<Cube currency="PHP" rate="58.947"/>
<Cube currency="SGD" rate="1.6228"/>
<Cube currency="THB" rate="36.848"/>
<Cube currency="ZAR" rate="18.7212"/>
</Cube>
<Cube time="2021-01-07">
<Cube currency="USD" rate="1.2276"/>
<Cube currency="JPY" rate="127.13"/>
<Cube currency="BGN" rate="1.9558"/>
<Cube currency="CZK" rate="26.147"/>
<Cube currency="DKK" rate="7.4392"/>
<Cube currency="GBP" rate="0.9019"/>
<Cube currency="HUF" rate="357.79"/>
<Cube currency="PLN" rate="4.4998"/>
<Cube currency="RON" rate="4.8712"/>
<Cube currency="SEK" rate="10.0575"/>
<Cube currency="CHF" rate="1.0833"/>
<Cube currency="ISK" rate="155.3"/>
<Cube currency="NOK" rate="10.3435"/>
<Cube currency="HRK" rate="7.566"/>
<Cube currency="RUB" rate="91.2"/>
<Cube currency="TRY" rate="8.9987"/>
<Cube currency="AUD" rate="1.5836"/>
<Cube currency="BRL" rate="6.5172"/>
<Cube currency="CAD" rate="1.5601"/>
<Cube currency="CNY" rate="7.9392"/>
<Cube currency="HKD" rate="9.5176"/>
<Cube currency="IDR" rate="17259.99"/>
<Cube currency="ILS" rate="3.9027"/>
<Cube currency="INR" rate="90.0455"/>
<Cube currency="KRW" rate="1342.29"/>
<Cube currency="MXN" rate="24.2552"/>
<Cube currency="MYR" rate="4.957"/>
<Cube currency="NZD" rate="1.6907"/>
<Cube currency="PHP" rate="59.043"/>
<Cube currency="SGD" rate="1.6253"/>
<Cube currency="THB" rate="36.859"/>
<Cube currency="ZAR" rate="18.7919"/>
</Cube>
<Cube time="2021-01-06">
<Cube currency="USD" rate="1.2338"/>
<Cube currency="JPY" rate="127.03"/>
<Cube currency="BGN" rate="1.9558"/>
<Cube currency="CZK" rate="26.145"/>
<Cube currency="DKK" rate="7.4393"/>
<Cube currency="GBP" rate="0.90635"/>
<Cube currency="HUF" rate="357.86"/>
<Cube currency="PLN" rate="4.516"/>
<Cube currency="RON" rate="4.872"/>
<Cube currency="SEK" rate="10.0653"/>
<Cube currency="CHF" rate="1.0821"/>
<Cube currency="ISK" rate="156.3"/>
<Cube currency="NOK" rate="10.381"/>
<Cube currency="HRK" rate="7.5595"/>
<Cube currency="RUB" rate="90.8175"/>
<Cube currency="TRY" rate="9.0554"/>
<Cube currency="AUD" rate="1.5824"/>
<Cube currency="BRL" rate="6.5119"/>
<Cube currency="CAD" rate="1.564"/>
<Cube currency="CNY" rate="7.9653"/>
<Cube currency="HKD" rate="9.5659"/>
<Cube currency="IDR" rate="17168.2"/>
<Cube currency="ILS" rate="3.9289"/>
<Cube currency="INR" rate="90.204"/>
<Cube currency="KRW" rate="1339.3"/>
<Cube currency="MXN" rate="24.3543"/>
<Cube currency="MYR" rate="4.9482"/>
<Cube currency="NZD" rate="1.6916"/>
<Cube currency="PHP" rate="59.296"/>
<Cube currency="SGD" rate="1.6246"/>
<Cube currency="THB" rate="36.921"/>
<Cube currency="ZAR" rate="18.5123"/>
</Cube>
<Cube time="2021-01-05">
<Cube currency="USD" rate="1.2271"/>
<Cube currency="JPY" rate="126.25"/>
<Cube currency="BGN" rate="1.9558"/>
<Cube currency="CZK" rate="26.227"/>
<Cube currency="DKK" rate="7.4387"/>
<Cube currency="GBP" rate="0.90333"/>
<Cube currency="HUF" rate="360.27"/>
<Cube currency="PLN" rate="4.5473"/>
<Cube currency="RON" rate="4.8721"/>
<Cube currency="SEK" rate="10.057"/>
<Cube currency="CHF" rate="1.0803"/>
<Cube currency="ISK" rate="156.1"/>
<Cube currency="NOK" rate="10.4713"/>
<Cube currency="HRK" rate="7.5588"/>
<Cube currency="RUB" rate="91.6715"/>
<Cube currency="TRY" rate="9.0694"/>
<Cube currency="AUD" rate="1.5927"/>
<Cube currency="BRL" rate="6.5517"/>
<Cube currency="CAD" rate="1.5651"/>
<Cube currency="CNY" rate="7.9315"/>
<Cube currency="HKD" rate="9.5136"/>
<Cube currency="IDR" rate="17075.1"/>
<Cube currency="ILS" rate="3.9277"/>
<Cube currency="INR" rate="89.867"/>
<Cube currency="KRW" rate="1335.85"/>
<Cube currency="MXN" rate="24.586"/>
<Cube currency="MYR" rate="4.9293"/>
<Cube currency="NZD" rate="1.7036"/>
<Cube currency="PHP" rate="59.02"/>
<Cube currency="SGD" rate="1.618"/>
<Cube currency="THB" rate="36.776"/>
<Cube currency="ZAR" rate="18.4194"/>
</Cube>
<Cube time="2021-01-04">
<Cube currency="USD" rate="1.2296"/>
<Cube currency="JPY" rate="126.62"/>
<Cube currency="BGN" rate="1.9558"/>
<Cube currency="CZK" rate="26.141"/>
<Cube currency="DKK" rate="7.4379"/>
<Cube currency="GBP" rate="0.9016"/>
<Cube currency="HUF" rate="361.32"/>
<Cube currency="PLN" rate="4.5475"/>
<Cube currency="RON" rate="4.8713"/>
<Cube currency="SEK" rate="10.0895"/>
<Cube currency="CHF" rate="1.0811"/>
<Cube currency="ISK" rate="156.1"/>
<Cube currency="NOK" rate="10.444"/>
<Cube currency="HRK" rate="7.5565"/>
<Cube currency="RUB" rate="90.342"/>
<Cube currency="TRY" rate="9.0579"/>
<Cube currency="AUD" rate="1.5928"/>
<Cube currency="BRL" rate="6.3241"/>
<Cube currency="CAD" rate="1.5621"/>
<Cube currency="CNY" rate="7.9484"/>
<Cube currency="HKD" rate="9.533"/>
<Cube currency="IDR" rate="17062.67"/>
<Cube currency="ILS" rate="3.943"/>
<Cube currency="INR" rate="89.789"/>
<Cube currency="KRW" rate="1332.03"/>
<Cube currency="MXN" rate="24.3031"/>
<Cube currency="MYR" rate="4.9264"/>
<Cube currency="NZD" rate="1.7065"/>
<Cube currency="PHP" rate="59.058"/>
<Cube currency="SGD" rate="1.6198"/>
<Cube currency="THB" rate="36.728"/>
<Cube currency="ZAR" rate="17.9214"/>
</Cube>
<Cube time="2021-06-25">
<Cube currency="USD" rate="1.195"/>
<Cube currency="JPY" rate="132.27"/>
<Cube currency="BGN" rate="1.9558"/>
<Cube currency="CZK" rate="25.487"/>
<Cube currency="DKK" rate="7.4363"/>
<Cube currency="GBP" rate="0.8595"/>
<Cube currency="HUF" rate="351.88"/>
<Cube currency="PLN" rate="4.5132"/>
<Cube currency="RON" rate="4.9263"/>
<Cube currency="SEK" rate="10.1103"/>
<Cube currency="CHF" rate="1.0956"/>
<Cube currency="ISK" rate="147.1"/>
<Cube currency="NOK" rate="10.136"/>
<Cube currency="HRK" rate="7.4975"/>
<Cube currency="RUB" rate="86.188"/>
<Cube currency="TRY" rate="10.3887"/>
<Cube currency="AUD" rate="1.5726"/>
<Cube currency="BRL" rate="5.8635"/>
<Cube currency="CAD" rate="1.4696"/>
<Cube currency="CNY" rate="7.7139"/>
<Cube currency="HKD" rate="9.2751"/>
<Cube currency="IDR" rate="17245.4"/>
<Cube currency="ILS" rate="3.8811"/>
<Cube currency="INR" rate="88.6824"/>
<Cube currency="KRW" rate="1346.35"/>
<Cube currency="MXN" rate="23.6766"/>
<Cube currency="MYR" rate="4.9664"/>
<Cube currency="NZD" rate="1.6881"/>
<Cube currency="PHP" rate="57.96"/>
<Cube currency="SGD" rate="1.6035"/>
<Cube currency="THB" rate="38.013"/>
<Cube currency="ZAR" rate="16.8359"/>
</Cube>
<Cube time="1999-01-08">
<Cube currency="USD" rate="1.1659"/>
<Cube currency="JPY" rate="130.09"/>
<Cube currency="CYP" rate="0.58187"/>
<Cube currency="CZK" rate="34.938"/>
<Cube currency="DKK" rate="7.4433"/>
<Cube currency="EEK" rate="15.6466"/>
<Cube currency="GBP" rate="0.7094"/>
<Cube currency="HUF" rate="250.15"/>
<Cube currency="LTL" rate="4.6643"/>
<Cube currency="LVL" rate="0.6654"/>
<Cube currency="MTL" rate="0.4419"/>
<Cube currency="PLN" rate="4.0363"/>
<Cube currency="ROL" rate="13143"/>
<Cube currency="SEK" rate="9.165"/>
<Cube currency="SIT" rate="188.84"/>
<Cube currency="SKK" rate="42.56"/>
<Cube currency="CHF" rate="1.6138"/>
<Cube currency="ISK" rate="80.99"/>
<Cube currency="NOK" rate="8.59"/>
<Cube currency="TRL" rate="371830"/>
<Cube currency="AUD" rate="1.8406"/>
<Cube currency="CAD" rate="1.7643"/>
<Cube currency="HKD" rate="9.0302"/>
<Cube currency="KRW" rate="1366.73"/>
<Cube currency="NZD" rate="2.1557"/>
<Cube currency="SGD" rate="1.9537"/>
<Cube currency="ZAR" rate="6.7855"/>
</Cube>
<Cube time="1999-01-07">
<Cube currency="USD" rate="1.1632"/>
<Cube currency="JPY" rate="129.43"/>
<Cube currency="CYP" rate="0.58187"/>
<Cube currency="CZK" rate="34.886"/>
<Cube currency="DKK" rate="7.4431"/>
<Cube currency="EEK" rate="15.6466"/>
<Cube currency="GBP" rate="0.70585"/>
<Cube currency="HUF" rate="250.09"/>
<Cube currency="LTL" rate="4.6548"/>
<Cube currency="LVL" rate="0.6627"/>
<Cube currency="MTL" rate="0.4413"/>
<Cube currency="PLN" rate="4.0165"/>
<Cube currency="ROL" rate="13092"/>
<Cube currency="SEK" rate="9.18"/>
<Cube currency="SIT" rate="188.8"/>
<Cube currency="SKK" rate="42.765"/>
<Cube currency="CHF" rate="1.6165"/>
<Cube currency="ISK" rate="81.06"/>
<Cube currency="NOK" rate="8.6295"/>
<Cube currency="TRL" rate="370147"/>
<Cube currency="AUD" rate="1.8474"/>
<Cube currency="CAD" rate="1.7602"/>
<Cube currency="HKD" rate="9.0131"/>
<Cube currency="KRW" rate="1337.16"/>
<Cube currency="NZD" rate="2.1531"/>
<Cube currency="SGD" rate="1.9436"/>
<Cube currency="ZAR" rate="6.8283"/>
</Cube>
<Cube time="1999-01-06">
<Cube currency="USD" rate="1.1743"/>
<Cube currency="JPY" rate="131.42"/>
<Cube currency="CYP" rate="0.582"/>
<Cube currency="CZK" rate="34.85"/>
<Cube currency="DKK" rate="7.4452"/>
<Cube currency="EEK" rate="15.6466"/>
<Cube currency="GBP" rate="0.7076"/>
<Cube currency="HUF" rate="250.67"/>
<Cube currency="LTL" rate="4.6994"/>
<Cube currency="LVL" rate="0.6649"/>
<Cube currency="MTL" rate="0.442"/>
<Cube currency="PLN" rate="4.0065"/>
<Cube currency="ROL" rate="13168"/>
<Cube currency="SEK" rate="9.305"/>
<Cube currency="SIT" rate="188.7"/>
<Cube currency="SKK" rate="42.778"/>
<Cube currency="CHF" rate="1.6116"/>
<Cube currency="ISK" rate="81.54"/>
<Cube currency="NOK" rate="8.7335"/>
<Cube currency="TRL" rate="372188"/>
<Cube currency="AUD" rate="1.882"/>
<Cube currency="CAD" rate="1.7711"/>
<Cube currency="HKD" rate="9.101"/>
<Cube currency="KRW" rate="1359.54"/>
<Cube currency="NZD" rate="2.189"/>
<Cube currency="SGD" rate="1.9699"/>
<Cube currency="ZAR" rate="6.7307"/>
</Cube>
<Cube time="1999-01-05">
<Cube currency="USD" rate="1.179"/>
<Cube currency="JPY" rate="130.96"/>
<Cube currency="CYP" rate="0.5823"/>
<Cube currency="CZK" rate="34.917"/>
<Cube currency="DKK" rate="7.4495"/>
<Cube currency="EEK" rate="15.6466"/>
<Cube currency="GBP" rate="0.7122"/>
<Cube currency="HUF" rate="250.8"/>
<Cube currency="LTL" rate="4.7174"/>
<Cube currency="LVL" rate="0.6657"/>
<Cube currency="MTL" rate="0.4432"/>
<Cube currency="PLN" rate="4.0245"/>
<Cube currency="ROL" rate="13168"/>
<Cube currency="SEK" rate="9.4025"/>
<Cube currency="SIT" rate="188.775"/>
<Cube currency="SKK" rate="42.848"/>
<Cube currency="CHF" rate="1.6123"/>
<Cube currency="ISK" rate="81.53"/>
<Cube currency="NOK" rate="8.7745"/>
<Cube currency="TRL" rate="372816"/>
<Cube currency="AUD" rate="1.8944"/>
<Cube currency="CAD" rate="1.7965"/>
<Cube currency="HKD" rate="9.1341"/>
<Cube currency="KRW" rate="1373.01"/>
<Cube currency="NZD" rate="2.2011"/>
<Cube currency="SGD" rate="1.9655"/>
<Cube currency="ZAR" rate="6.7975"/>
</Cube>
<Cube time="1999-01-04">
<Cube currency="USD" rate="1.1789"/>
<Cube currency="JPY" rate="133.73"/>
<Cube currency="CYP" rate="0.58231"/>
<Cube currency="CZK" rate="35.107"/>
<Cube currency="DKK" rate="7.4501"/>
<Cube currency="EEK" rate="15.6466"/>
<Cube currency="GBP" rate="0.7111"/>
<Cube currency="HUF" rate="251.48"/>
<Cube currency="LTL" rate="4.717"/>
<Cube currency="LVL" rate="0.6668"/>
<Cube currency="MTL" rate="0.4432"/>
<Cube currency="PLN" rate="4.0712"/>
<Cube currency="ROL" rate="13111"/>
<Cube currency="SEK" rate="9.4696"/>
<Cube currency="SIT" rate="189.045"/>
<Cube currency="SKK" rate="42.991"/>
<Cube currency="CHF" rate="1.6168"/>
<Cube currency="ISK" rate="81.48"/>
<Cube currency="NOK" rate="8.855"/>
<Cube currency="TRL" rate="372274"/>
<Cube currency="AUD" rate="1.91"/>
<Cube currency="CAD" rate="1.8004"/>
<Cube currency="HKD" rate="9.1332"/>
<Cube currency="KRW" rate="1398.59"/>
<Cube currency="NZD" rate="2.2229"/>
<Cube currency="SGD" rate="1.9554"/>
<Cube currency="ZAR" rate="6.9358"/>
</Cube>
</Cube>
</gesmes:Envelope>