From 89e8bc9964b85434edd4e5ed38802f10604cf61e Mon Sep 17 00:00:00 2001 From: Chris Berkhout Date: Mon, 23 Aug 2021 20:59:45 +0200 Subject: [PATCH] Loosen requirement for Alpha Vantage api key. --- src/pricehist/sources/alphavantage.py | 42 ++++++++++++-------- tests/pricehist/sources/test_alphavantage.py | 18 ++++++++- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/pricehist/sources/alphavantage.py b/src/pricehist/sources/alphavantage.py index a74eeda..07aaae2 100644 --- a/src/pricehist/sources/alphavantage.py +++ b/src/pricehist/sources/alphavantage.py @@ -8,7 +8,7 @@ from decimal import Decimal import requests -from pricehist import exceptions +from pricehist import __version__, exceptions from pricehist.price import Price from .basesource import BaseSource @@ -16,6 +16,7 @@ from .basesource import BaseSource class AlphaVantage(BaseSource): QUERY_URL = "https://www.alphavantage.co/query" + API_KEY_NAME = "ALPHAVANTAGE_API_KEY" def id(self): return "alphavantage" @@ -36,14 +37,14 @@ class AlphaVantage(BaseSource): return ["close", "open", "high", "low", "adjclose", "mid"] def notes(self): - keystatus = "already set" if self._apikey(require=False) else "NOT YET set" + keystatus = "already set" if self._apikey(require=False) else "not yet set" return ( "Alpha Vantage has data on digital (crypto) currencies, physical " "(fiat) currencies and stocks.\n" - "An API key is required. One can be obtained for free from " - "https://www.alphavantage.co/support/#api-key and should be made " - "available in the ALPHAVANTAGE_API_KEY environment variable " - f"({keystatus}).\n" + "You should obtain a free API key from " + "https://www.alphavantage.co/support/#api-key and set it in " + f"the {self.API_KEY_NAME} environment variable ({keystatus}), " + "otherise, pricehist will attempt to use a generic key.\n" "The PAIR for currencies should be in BASE/QUOTE form. The quote " "symbol must always be for a physical currency. The --symbols option " "will list all digital and physical currency symbols.\n" @@ -165,8 +166,7 @@ class AlphaVantage(BaseSource): except Exception as e: raise exceptions.ResponseParsingError(str(e)) from e - if type(data) == dict and "Note" in data and "call frequency" in data["Note"]: - raise exceptions.RateLimit(data["Note"]) + self._raise_for_generic_errors(data) expected_keys = ["1. symbol", "2. name", "3. type", "4. region", "8. currency"] if ( @@ -204,8 +204,7 @@ class AlphaVantage(BaseSource): except Exception as e: raise exceptions.ResponseParsingError(str(e)) from e - if type(data) == dict and "Note" in data and "call frequency" in data["Note"]: - raise exceptions.RateLimit(data["Note"]) + self._raise_for_generic_errors(data) if "Error Message" in data: if output_quote == "UNKNOWN": @@ -255,8 +254,7 @@ class AlphaVantage(BaseSource): except Exception as e: raise exceptions.ResponseParsingError(str(e)) from e - if type(data) == dict and "Note" in data and "call frequency" in data["Note"]: - raise exceptions.RateLimit(data["Note"]) + self._raise_for_generic_errors(data) if type(data) != dict or "Time Series FX (Daily)" not in data: raise exceptions.ResponseParsingError("Unexpected content.") @@ -297,8 +295,7 @@ class AlphaVantage(BaseSource): except Exception as e: raise exceptions.ResponseParsingError(str(e)) from e - if type(data) == dict and "Note" in data and "call frequency" in data["Note"]: - raise exceptions.RateLimit(data["Note"]) + self._raise_for_generic_errors(data) if type(data) != dict or "Time Series (Digital Currency Daily)" not in data: raise exceptions.ResponseParsingError("Unexpected content.") @@ -317,12 +314,23 @@ class AlphaVantage(BaseSource): return normalized_data def _apikey(self, require=True): - key_name = "ALPHAVANTAGE_API_KEY" - key = os.getenv(key_name) + key = os.getenv(self.API_KEY_NAME) if require and not key: - raise exceptions.CredentialsError([key_name], self) + generic_key = f"pricehist_{__version__}" + logging.debug( + f"{self.API_KEY_NAME} not set. " + f"Defaulting to generic key '{generic_key}'." + ) + return generic_key return key + def _raise_for_generic_errors(self, data): + if type(data) == dict: + if "Note" in data and "call frequency" in data["Note"]: + raise exceptions.RateLimit(data["Note"]) + if "Error Message" in data and "apikey " in data["Error Message"]: + raise exceptions.CredentialsError([self.API_KEY_NAME], self) + def _physical_symbols(self) -> list[(str, str)]: url = "https://www.alphavantage.co/physical_currency_list/" return self._get_symbols(url, "Physical: ") diff --git a/tests/pricehist/sources/test_alphavantage.py b/tests/pricehist/sources/test_alphavantage.py index 237f8e9..334bbd4 100644 --- a/tests/pricehist/sources/test_alphavantage.py +++ b/tests/pricehist/sources/test_alphavantage.py @@ -9,7 +9,7 @@ import pytest import requests import responses -from pricehist import exceptions +from pricehist import __version__, exceptions from pricehist.price import Price from pricehist.series import Series from pricehist.sources.alphavantage import AlphaVantage @@ -625,8 +625,22 @@ def test_fetch_bad_pair_quote_non_physical(src, type, physical_list_ok): assert "quote must be a physical currency" in str(e.value) -def test_fetch_api_key_missing(src, type, physical_list_ok, monkeypatch): +def test_fetch_api_key_defaults_to_generic( + src, type, physical_list_ok, euraud_ok, monkeypatch +): monkeypatch.delenv(api_key_name) + src.fetch(Series("EUR", "AUD", type, "2021-01-04", "2021-01-08")) + req = euraud_ok.calls[-1].request + assert req.params["apikey"] == f"pricehist_{__version__}" + + +def test_fetch_api_key_invalid(src, type, physical_list_ok, requests_mock): + body = ( + '{ "Error Message": "the parameter apikey is invalid or missing. Please ' + "claim your free API key on (https://www.alphavantage.co/support/#api-key). " + 'It should take less than 20 seconds." }' + ) + requests_mock.add(responses.GET, physical_url, body=body) with pytest.raises(exceptions.CredentialsError) as e: src.fetch(Series("EUR", "AUD", type, "2021-01-04", "2021-01-08")) assert "unavailable or invalid" in str(e.value)