From 947eaacd299453ca3ec22b5d77f52c956cb6439c Mon Sep 17 00:00:00 2001 From: Chris Berkhout Date: Mon, 27 Dec 2021 08:31:55 +1100 Subject: [PATCH] Support Bank of Canada daily exchange rates. --- README.md | 1 + poetry.lock | 53 ++-- src/pricehist/beanprice/bankofcanada.py | 4 + src/pricehist/sources/__init__.py | 2 + src/pricehist/sources/bankofcanada.py | 118 ++++++++ tests/live.sh | 12 + tests/pricehist/sources/test_bankofcanada.py | 246 ++++++++++++++++ .../test_bankofcanada/all-partial.json | 101 +++++++ .../sources/test_bankofcanada/recent.json | 41 +++ .../test_bankofcanada/series-partial.json | 272 ++++++++++++++++++ 10 files changed, 818 insertions(+), 32 deletions(-) create mode 100644 src/pricehist/beanprice/bankofcanada.py create mode 100644 src/pricehist/sources/bankofcanada.py create mode 100644 tests/pricehist/sources/test_bankofcanada.py create mode 100644 tests/pricehist/sources/test_bankofcanada/all-partial.json create mode 100644 tests/pricehist/sources/test_bankofcanada/recent.json create mode 100644 tests/pricehist/sources/test_bankofcanada/series-partial.json diff --git a/README.md b/README.md index 6db9850..cdb9bd9 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ pipx install pricehist ## Sources - **`alphavantage`**: [Alpha Vantage](https://www.alphavantage.co/) +- **`bankofcanada`**: [Bank of Canada daily exchange rates](https://www.bankofcanada.ca/valet/docs) - **`coinbasepro`**: [Coinbase Pro](https://pro.coinbase.com/) - **`coindesk`**: [CoinDesk Bitcoin Price Index](https://www.coindesk.com/coindesk-api) - **`coinmarketcap`**: [CoinMarketCap](https://coinmarketcap.com/) diff --git a/poetry.lock b/poetry.lock index 5632fbc..e71f93b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -415,11 +415,11 @@ testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "psutil (>=5.6.1)", "pytes [[package]] name = "typed-ast" -version = "1.4.3" +version = "1.5.1" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "typing-extensions" @@ -749,36 +749,25 @@ tox = [ {file = "tox-3.24.4.tar.gz", hash = "sha256:c30b57fa2477f1fb7c36aa1d83292d5c2336cd0018119e1b1c17340e2c2708ca"}, ] typed-ast = [ - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, - {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, - {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, - {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, - {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, - {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, - {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, - {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, - {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, - {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, + {file = "typed_ast-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d8314c92414ce7481eee7ad42b353943679cf6f30237b5ecbf7d835519e1212"}, + {file = "typed_ast-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b53ae5de5500529c76225d18eeb060efbcec90ad5e030713fe8dab0fb4531631"}, + {file = "typed_ast-1.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:24058827d8f5d633f97223f5148a7d22628099a3d2efe06654ce872f46f07cdb"}, + {file = "typed_ast-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a6d495c1ef572519a7bac9534dbf6d94c40e5b6a608ef41136133377bba4aa08"}, + {file = "typed_ast-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:de4ecae89c7d8b56169473e08f6bfd2df7f95015591f43126e4ea7865928677e"}, + {file = "typed_ast-1.5.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:256115a5bc7ea9e665c6314ed6671ee2c08ca380f9d5f130bd4d2c1f5848d695"}, + {file = "typed_ast-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:7c42707ab981b6cf4b73490c16e9d17fcd5227039720ca14abe415d39a173a30"}, + {file = "typed_ast-1.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:71dcda943a471d826ea930dd449ac7e76db7be778fcd722deb63642bab32ea3f"}, + {file = "typed_ast-1.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4f30a2bcd8e68adbb791ce1567fdb897357506f7ea6716f6bbdd3053ac4d9471"}, + {file = "typed_ast-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ca9e8300d8ba0b66d140820cf463438c8e7b4cdc6fd710c059bfcfb1531d03fb"}, + {file = "typed_ast-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9caaf2b440efb39ecbc45e2fabde809cbe56272719131a6318fd9bf08b58e2cb"}, + {file = "typed_ast-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c9bcad65d66d594bffab8575f39420fe0ee96f66e23c4d927ebb4e24354ec1af"}, + {file = "typed_ast-1.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:591bc04e507595887160ed7aa8d6785867fb86c5793911be79ccede61ae96f4d"}, + {file = "typed_ast-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:a80d84f535642420dd17e16ae25bb46c7f4c16ee231105e7f3eb43976a89670a"}, + {file = "typed_ast-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:38cf5c642fa808300bae1281460d4f9b7617cf864d4e383054a5ef336e344d32"}, + {file = "typed_ast-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b6ab14c56bc9c7e3c30228a0a0b54b915b1579613f6e463ba6f4eb1382e7fd4"}, + {file = "typed_ast-1.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2b8d7007f6280e36fa42652df47087ac7b0a7d7f09f9468f07792ba646aac2d"}, + {file = "typed_ast-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:b6d17f37f6edd879141e64a5db17b67488cfeffeedad8c5cec0392305e9bc775"}, + {file = "typed_ast-1.5.1.tar.gz", hash = "sha256:484137cab8ecf47e137260daa20bafbba5f4e3ec7fda1c1e69ab299b75fa81c5"}, ] typing-extensions = [ {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, diff --git a/src/pricehist/beanprice/bankofcanada.py b/src/pricehist/beanprice/bankofcanada.py new file mode 100644 index 0000000..7c09ba7 --- /dev/null +++ b/src/pricehist/beanprice/bankofcanada.py @@ -0,0 +1,4 @@ +from pricehist import beanprice +from pricehist.sources.bankofcanada import BankOfCanada + +Source = beanprice.source(BankOfCanada()) diff --git a/src/pricehist/sources/__init__.py b/src/pricehist/sources/__init__.py index 36bf676..dfde1be 100644 --- a/src/pricehist/sources/__init__.py +++ b/src/pricehist/sources/__init__.py @@ -1,4 +1,5 @@ from .alphavantage import AlphaVantage +from .bankofcanada import BankOfCanada from .coinbasepro import CoinbasePro from .coindesk import CoinDesk from .coinmarketcap import CoinMarketCap @@ -9,6 +10,7 @@ by_id = { source.id(): source for source in [ AlphaVantage(), + BankOfCanada(), CoinbasePro(), CoinDesk(), CoinMarketCap(), diff --git a/src/pricehist/sources/bankofcanada.py b/src/pricehist/sources/bankofcanada.py new file mode 100644 index 0000000..67b3b59 --- /dev/null +++ b/src/pricehist/sources/bankofcanada.py @@ -0,0 +1,118 @@ +import dataclasses +import json +from decimal import Decimal + +import requests + +from pricehist import exceptions +from pricehist.price import Price + +from .basesource import BaseSource + + +class BankOfCanada(BaseSource): + def id(self): + return "bankofcanada" + + def name(self): + return "Bank of Canada" + + def description(self): + return "Daily exchange rates of the Canadian dollar from the Bank of Canada" + + def source_url(self): + return "https://www.bankofcanada.ca/valet/docs" + + def start(self): + return "2017-01-03" + + def types(self): + return ["default"] + + def notes(self): + return ( + "Currently, only daily exchange rates are supported. They are " + "published once each business day by 16:30 ET. " + "All Bank of Canada exchange rates are indicative rates only.\n" + "To request support for other data provided by the " + "Bank of Canada Valet Web Services, please open an " + "issue in pricehist's Gitlab project. " + ) + + def symbols(self): + url = "https://www.bankofcanada.ca/valet/lists/series/json" + + try: + response = self.log_curl(requests.get(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: + data = json.loads(response.content) + series_names = data["series"].keys() + fx_series_names = [ + n for n in series_names if len(n) == 8 and n[0:2] == "FX" + ] + results = [ + (f"{n[2:5]}/{n[5:9]}", data["series"][n]["description"]) + for n in sorted(fx_series_names) + ] + + except Exception as e: + raise exceptions.ResponseParsingError(str(e)) from e + + if not results: + raise exceptions.ResponseParsingError("Expected data not found") + else: + return results + + def fetch(self, series): + if len(series.base) != 3 or len(series.quote) != 3: + raise exceptions.InvalidPair(series.base, series.quote, self) + + series_name = f"FX{series.base}{series.quote}" + data = self._data(series, series_name) + + prices = [] + for o in data.get("observations", []): + prices.append(Price(o["d"], Decimal(o[series_name]["v"]))) + + return dataclasses.replace(series, prices=prices) + + def _data(self, series, series_name): + url = f"https://www.bankofcanada.ca/valet/observations/{series_name}/json" + params = { + "start_date": series.start, + "end_date": series.end, + "order_dir": "asc", + } + + try: + response = self.log_curl(requests.get(url, params=params)) + except Exception as e: + raise exceptions.RequestError(str(e)) from e + + code = response.status_code + text = response.text + + try: + result = json.loads(response.content) + except Exception as e: + raise exceptions.ResponseParsingError(str(e)) from e + + if code == 404 and "not found" in text: + raise exceptions.InvalidPair(series.base, series.quote, self) + elif code == 400 and "End date must be greater than the Start date" in text: + raise exceptions.BadResponse(result["message"]) + else: + try: + response.raise_for_status() + except Exception as e: + raise exceptions.BadResponse(str(e)) from e + + return result diff --git a/tests/live.sh b/tests/live.sh index 93b9ba0..543d634 100755 --- a/tests/live.sh +++ b/tests/live.sh @@ -80,6 +80,18 @@ date,base,quote,amount,source,type END run_test "$name" "$cmd" "$expected" +name="Bank of Canada" +cmd="pricehist fetch bankofcanada CAD/USD -s 2021-01-04 -e 2021-01-08" +read -r -d '' expected < 0 + + assert isinstance(src.name(), str) + 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) + + +def test_symbols(src, series_list_response_ok): + syms = src.symbols() + assert ("CAD/USD", "Canadian dollar to US dollar daily exchange rate") in syms + assert len(syms) > 3 + + +def test_symbols_requests_logged(src, series_list_response_ok, caplog): + with caplog.at_level(logging.DEBUG): + src.symbols() + assert any( + ["DEBUG" == r.levelname and "curl " in r.message for r in caplog.records] + ) + + +def test_symbols_not_found(src, requests_mock, series_list_url): + requests_mock.add(responses.GET, series_list_url, body='{"series":{}}', status=200) + with pytest.raises(exceptions.ResponseParsingError) as e: + src.symbols() + assert "data not found" in str(e.value) + + +def test_symbols_network_issue(src, requests_mock, series_list_url): + requests_mock.add( + responses.GET, + series_list_url, + body=requests.exceptions.ConnectionError("Network issue"), + ) + with pytest.raises(exceptions.RequestError) as e: + src.symbols() + assert "Network issue" in str(e.value) + + +def test_symbols_bad_status(src, requests_mock, series_list_url): + requests_mock.add(responses.GET, series_list_url, status=500) + with pytest.raises(exceptions.BadResponse) as e: + src.symbols() + assert "Server Error" in str(e.value) + + +def test_symbols_parsing_error(src, requests_mock, series_list_url): + requests_mock.add(responses.GET, series_list_url, body="NOT JSON") + with pytest.raises(exceptions.ResponseParsingError) as e: + src.symbols() + assert "while parsing data" in str(e.value) + + +def test_fetch_known_pair(src, type, recent_response_ok): + series = src.fetch(Series("CAD", "USD", type, "2021-01-01", "2021-01-07")) + req = recent_response_ok.calls[0].request + assert req.params["order_dir"] == "asc" + assert req.params["start_date"] == "2021-01-01" + assert req.params["end_date"] == "2021-01-07" + assert series.prices[0] == Price("2021-01-04", Decimal("0.7843")) + assert series.prices[-1] == Price("2021-01-07", Decimal("0.7870")) + assert len(series.prices) == 4 + + +def test_fetch_requests_logged(src, type, recent_response_ok, caplog): + with caplog.at_level(logging.DEBUG): + src.fetch(Series("CAD", "USD", type, "2021-01-01", "2021-01-07")) + assert any( + ["DEBUG" == r.levelname and "curl " in r.message for r in caplog.records] + ) + + +def test_fetch_long_hist_from_start(src, type, all_response_ok): + series = src.fetch(Series("CAD", "USD", type, src.start(), "2021-01-07")) + assert series.prices[0] == Price("2017-01-03", Decimal("0.7443")) + assert series.prices[-1] == Price("2021-01-07", Decimal("0.7870")) + assert len(series.prices) > 13 + + +def test_fetch_from_before_start(src, type, requests_mock): + body = """{ "observations": [] }""" + requests_mock.add(responses.GET, fetch_url("FXCADUSD"), status=200, body=body) + series = src.fetch(Series("CAD", "USD", type, "2000-01-01", "2017-01-01")) + assert len(series.prices) == 0 + + +def test_fetch_to_future(src, type, all_response_ok): + series = src.fetch(Series("CAD", "USD", type, "2021-01-01", "2100-01-01")) + assert len(series.prices) > 0 + + +def test_wrong_dates_order(src, type, requests_mock): + body = """{ "message": "The End date must be greater than the Start date." }""" + requests_mock.add(responses.GET, fetch_url("FXCADUSD"), status=400, body=body) + with pytest.raises(exceptions.BadResponse) as e: + src.fetch(Series("CAD", "USD", type, "2021-01-07", "2021-01-01")) + assert "End date must be greater" in str(e.value) + + +def test_fetch_in_future(src, type, requests_mock): + body = """{ "observations": [] }""" + requests_mock.add(responses.GET, fetch_url("FXCADUSD"), status=200, body=body) + series = src.fetch(Series("CAD", "USD", type, "2030-01-01", "2030-01-07")) + assert len(series.prices) == 0 + + +def test_fetch_empty(src, type, requests_mock): + requests_mock.add( + responses.GET, fetch_url("FXCADUSD"), body="""{"observations":{}}""" + ) + series = src.fetch(Series("CAD", "USD", type, "2021-01-03", "2021-01-03")) + assert len(series.prices) == 0 + + +def test_fetch_no_quote(src, type): + with pytest.raises(exceptions.InvalidPair): + src.fetch(Series("CAD", "", type, "2021-01-01", "2021-01-07")) + + +def test_fetch_unknown_pair(src, type, requests_mock): + requests_mock.add( + responses.GET, + fetch_url("FXCADAFN"), + status=404, + body="""{ + "message": "Series FXCADAFN not found.", + "docs": "https://www.bankofcanada.ca/valet/docs" + }""", + ) + with pytest.raises(exceptions.InvalidPair): + src.fetch(Series("CAD", "AFN", type, "2021-01-01", "2021-01-07")) + + +def test_fetch_network_issue(src, type, requests_mock): + body = requests.exceptions.ConnectionError("Network issue") + requests_mock.add(responses.GET, fetch_url("FXCADUSD"), body=body) + with pytest.raises(exceptions.RequestError) as e: + src.fetch(Series("CAD", "USD", type, "2021-01-01", "2021-01-07")) + assert "Network issue" in str(e.value) + + +def test_fetch_bad_status(src, type, requests_mock): + requests_mock.add( + responses.GET, + fetch_url("FXCADUSD"), + status=500, + body="""{"message": "Some other reason"}""", + ) + with pytest.raises(exceptions.BadResponse) as e: + src.fetch(Series("CAD", "USD", type, "2021-01-01", "2021-01-07")) + assert "Internal Server Error" in str(e.value) + + +def test_fetch_parsing_error(src, type, requests_mock): + requests_mock.add(responses.GET, fetch_url("FXCADUSD"), body="NOT JSON") + with pytest.raises(exceptions.ResponseParsingError) as e: + src.fetch(Series("CAD", "USD", type, "2021-01-01", "2021-01-07")) + assert "while parsing data" in str(e.value) diff --git a/tests/pricehist/sources/test_bankofcanada/all-partial.json b/tests/pricehist/sources/test_bankofcanada/all-partial.json new file mode 100644 index 0000000..3604707 --- /dev/null +++ b/tests/pricehist/sources/test_bankofcanada/all-partial.json @@ -0,0 +1,101 @@ +{ + "terms": { + "url": "https://www.bankofcanada.ca/terms/" + }, + "seriesDetail": { + "FXCADUSD": { + "label": "CAD/USD", + "description": "Canadian dollar to US dollar daily exchange rate", + "dimension": { + "key": "d", + "name": "date" + } + } + }, + "observations": [ + { + "d": "2017-01-03", + "FXCADUSD": { + "v": "0.7443" + } + }, + { + "d": "2017-01-04", + "FXCADUSD": { + "v": "0.7510" + } + }, + { + "d": "2017-01-05", + "FXCADUSD": { + "v": "0.7551" + } + }, + { + "d": "2017-01-06", + "FXCADUSD": { + "v": "0.7568" + } + }, + { + "d": "2017-01-09", + "FXCADUSD": { + "v": "0.7553" + } + }, + { + "d": "2017-01-10", + "FXCADUSD": { + "v": "0.7568" + } + }, + { + "d": "2017-01-11", + "FXCADUSD": { + "v": "0.7547" + } + }, + { + "d": "2020-12-29", + "FXCADUSD": { + "v": "0.7809" + } + }, + { + "d": "2020-12-30", + "FXCADUSD": { + "v": "0.7831" + } + }, + { + "d": "2020-12-31", + "FXCADUSD": { + "v": "0.7854" + } + }, + { + "d": "2021-01-04", + "FXCADUSD": { + "v": "0.7843" + } + }, + { + "d": "2021-01-05", + "FXCADUSD": { + "v": "0.7870" + } + }, + { + "d": "2021-01-06", + "FXCADUSD": { + "v": "0.7883" + } + }, + { + "d": "2021-01-07", + "FXCADUSD": { + "v": "0.7870" + } + } + ] +} diff --git a/tests/pricehist/sources/test_bankofcanada/recent.json b/tests/pricehist/sources/test_bankofcanada/recent.json new file mode 100644 index 0000000..46ec248 --- /dev/null +++ b/tests/pricehist/sources/test_bankofcanada/recent.json @@ -0,0 +1,41 @@ +{ + "terms": { + "url": "https://www.bankofcanada.ca/terms/" + }, + "seriesDetail": { + "FXCADUSD": { + "label": "CAD/USD", + "description": "Canadian dollar to US dollar daily exchange rate", + "dimension": { + "key": "d", + "name": "date" + } + } + }, + "observations": [ + { + "d": "2021-01-04", + "FXCADUSD": { + "v": "0.7843" + } + }, + { + "d": "2021-01-05", + "FXCADUSD": { + "v": "0.7870" + } + }, + { + "d": "2021-01-06", + "FXCADUSD": { + "v": "0.7883" + } + }, + { + "d": "2021-01-07", + "FXCADUSD": { + "v": "0.7870" + } + } + ] +} diff --git a/tests/pricehist/sources/test_bankofcanada/series-partial.json b/tests/pricehist/sources/test_bankofcanada/series-partial.json new file mode 100644 index 0000000..68bfb4c --- /dev/null +++ b/tests/pricehist/sources/test_bankofcanada/series-partial.json @@ -0,0 +1,272 @@ +{ + "terms": { + "url": "https://www.bankofcanada.ca/terms/" + }, + "series": { + "FXAUDCAD": { + "label": "AUD/CAD", + "description": "Australian dollar to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXAUDCAD" + }, + "FXBRLCAD": { + "label": "BRL/CAD", + "description": "Brazilian real to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXBRLCAD" + }, + "FXCNYCAD": { + "label": "CNY/CAD", + "description": "Chinese renminbi to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCNYCAD" + }, + "FXEURCAD": { + "label": "EUR/CAD", + "description": "European euro to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXEURCAD" + }, + "FXHKDCAD": { + "label": "HKD/CAD", + "description": "Hong Kong dollar to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXHKDCAD" + }, + "FXINRCAD": { + "label": "INR/CAD", + "description": "Indian rupee to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXINRCAD" + }, + "FXIDRCAD": { + "label": "IDR/CAD", + "description": "Indonesian rupiah to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXIDRCAD" + }, + "FXJPYCAD": { + "label": "JPY/CAD", + "description": "Japanese yen to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXJPYCAD" + }, + "FXMYRCAD": { + "label": "MYR/CAD", + "description": "Malaysian ringgit to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXMYRCAD" + }, + "FXMXNCAD": { + "label": "MXN/CAD", + "description": "Mexican peso to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXMXNCAD" + }, + "FXNZDCAD": { + "label": "NZD/CAD", + "description": "New Zealand dollar to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXNZDCAD" + }, + "FXNOKCAD": { + "label": "NOK/CAD", + "description": "Norwegian krone to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXNOKCAD" + }, + "FXPENCAD": { + "label": "PEN/CAD", + "description": "Peruvian new sol to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXPENCAD" + }, + "FXRUBCAD": { + "label": "RUB/CAD", + "description": "Russian ruble to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXRUBCAD" + }, + "FXSARCAD": { + "label": "SAR/CAD", + "description": "Saudi riyal to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXSARCAD" + }, + "FXSGDCAD": { + "label": "SGD/CAD", + "description": "Singapore dollar to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXSGDCAD" + }, + "FXZARCAD": { + "label": "ZAR/CAD", + "description": "South African rand to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXZARCAD" + }, + "FXKRWCAD": { + "label": "KRW/CAD", + "description": "South Korean won to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXKRWCAD" + }, + "FXSEKCAD": { + "label": "SEK/CAD", + "description": "Swedish krona to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXSEKCAD" + }, + "FXCHFCAD": { + "label": "CHF/CAD", + "description": "Swiss franc to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCHFCAD" + }, + "FXTWDCAD": { + "label": "TWD/CAD", + "description": "Taiwanese dollar to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXTWDCAD" + }, + "FXTHBCAD": { + "label": "THB/CAD", + "description": "Thai baht to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXTHBCAD" + }, + "FXTRYCAD": { + "label": "TRY/CAD", + "description": "Turkish lira to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXTRYCAD" + }, + "FXGBPCAD": { + "label": "GBP/CAD", + "description": "UK pound sterling to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXGBPCAD" + }, + "FXUSDCAD": { + "label": "USD/CAD", + "description": "US dollar to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXUSDCAD" + }, + "FXVNDCAD": { + "label": "VND/CAD", + "description": "Vietnamese dong to Canadian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXVNDCAD" + }, + "FXCADAUD": { + "label": "CAD/AUD", + "description": "Canadian dollar to Australian dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADAUD" + }, + "FXCADBRL": { + "label": "CAD/BRL", + "description": "Canadian dollar to Brazilian real daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADBRL" + }, + "FXCADCNY": { + "label": "CAD/CNY", + "description": "Canadian dollar to Chinese renminbi daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADCNY" + }, + "FXCADEUR": { + "label": "CAD/EUR", + "description": "Canadian dollar to European euro daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADEUR" + }, + "FXCADHKD": { + "label": "CAD/HKD", + "description": "Canadian dollar to Hong Kong dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADHKD" + }, + "FXCADINR": { + "label": "CAD/INR", + "description": "Canadian dollar to Indian rupee daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADINR" + }, + "FXCADIDR": { + "label": "CAD/IDR", + "description": "Canadian dollar to Indonesian rupiah daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADIDR" + }, + "FXCADJPY": { + "label": "CAD/JPY", + "description": "Canadian dollar to Japanese yen daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADJPY" + }, + "FXCADMYR": { + "label": "CAD/MYR", + "description": "Canadian dollar to Malaysian ringgit daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADMYR" + }, + "FXCADMXN": { + "label": "CAD/MXN", + "description": "Canadian dollar to Mexican peso daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADMXN" + }, + "FXCADNZD": { + "label": "CAD/NZD", + "description": "Canadian dollar to New Zealand dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADNZD" + }, + "FXCADNOK": { + "label": "CAD/NOK", + "description": "Canadian dollar to Norwegian krone daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADNOK" + }, + "FXCADPEN": { + "label": "CAD/PEN", + "description": "Canadian dollar to Peruvian new sol daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADPEN" + }, + "FXCADRUB": { + "label": "CAD/RUB", + "description": "Canadian dollar to Russian ruble daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADRUB" + }, + "FXCADSAR": { + "label": "CAD/SAR", + "description": "Canadian dollar to Saudi riyal daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADSAR" + }, + "FXCADSGD": { + "label": "CAD/SGD", + "description": "Canadian dollar to Singapore dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADSGD" + }, + "FXCADZAR": { + "label": "CAD/ZAR", + "description": "Canadian dollar to South African rand daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADZAR" + }, + "FXCADKRW": { + "label": "CAD/KRW", + "description": "Canadian dollar to South Korean won daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADKRW" + }, + "FXCADSEK": { + "label": "CAD/SEK", + "description": "Canadian dollar to Swedish krona daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADSEK" + }, + "FXCADCHF": { + "label": "CAD/CHF", + "description": "Canadian dollar to Swiss franc daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADCHF" + }, + "FXCADTWD": { + "label": "CAD/TWD", + "description": "Canadian dollar to Taiwanese dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADTWD" + }, + "FXCADTHB": { + "label": "CAD/THB", + "description": "Canadian dollar to Thai baht daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADTHB" + }, + "FXCADTRY": { + "label": "CAD/TRY", + "description": "Canadian dollar to Turkish lira daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADTRY" + }, + "FXCADGBP": { + "label": "CAD/GBP", + "description": "Canadian dollar to UK pound sterling daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADGBP" + }, + "FXCADUSD": { + "label": "CAD/USD", + "description": "Canadian dollar to US dollar daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADUSD" + }, + "FXCADVND": { + "label": "CAD/VND", + "description": "Canadian dollar to Vietnamese dong daily exchange rate", + "link": "https://www.bankofcanada.ca/valet/series/FXCADVND" + }, + "INDINF_GRACE_Q": { + "label": "Foreign demand for Canadian non-commodity exports (GRACE) (2007=100)", + "description": "Foreign demand for Canadian non-commodity exports (GRACE) (2007=100)", + "link": "https://www.bankofcanada.ca/valet/series/INDINF_GRACE_Q" + } + } +}