Loosen requirement for Alpha Vantage api key.
This commit is contained in:
parent
1468e1f64b
commit
89e8bc9964
2 changed files with 41 additions and 19 deletions
|
@ -8,7 +8,7 @@ from decimal import Decimal
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from pricehist import exceptions
|
from pricehist import __version__, exceptions
|
||||||
from pricehist.price import Price
|
from pricehist.price import Price
|
||||||
|
|
||||||
from .basesource import BaseSource
|
from .basesource import BaseSource
|
||||||
|
@ -16,6 +16,7 @@ from .basesource import BaseSource
|
||||||
|
|
||||||
class AlphaVantage(BaseSource):
|
class AlphaVantage(BaseSource):
|
||||||
QUERY_URL = "https://www.alphavantage.co/query"
|
QUERY_URL = "https://www.alphavantage.co/query"
|
||||||
|
API_KEY_NAME = "ALPHAVANTAGE_API_KEY"
|
||||||
|
|
||||||
def id(self):
|
def id(self):
|
||||||
return "alphavantage"
|
return "alphavantage"
|
||||||
|
@ -36,14 +37,14 @@ class AlphaVantage(BaseSource):
|
||||||
return ["close", "open", "high", "low", "adjclose", "mid"]
|
return ["close", "open", "high", "low", "adjclose", "mid"]
|
||||||
|
|
||||||
def notes(self):
|
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 (
|
return (
|
||||||
"Alpha Vantage has data on digital (crypto) currencies, physical "
|
"Alpha Vantage has data on digital (crypto) currencies, physical "
|
||||||
"(fiat) currencies and stocks.\n"
|
"(fiat) currencies and stocks.\n"
|
||||||
"An API key is required. One can be obtained for free from "
|
"You should obtain a free API key from "
|
||||||
"https://www.alphavantage.co/support/#api-key and should be made "
|
"https://www.alphavantage.co/support/#api-key and set it in "
|
||||||
"available in the ALPHAVANTAGE_API_KEY environment variable "
|
f"the {self.API_KEY_NAME} environment variable ({keystatus}), "
|
||||||
f"({keystatus}).\n"
|
"otherise, pricehist will attempt to use a generic key.\n"
|
||||||
"The PAIR for currencies should be in BASE/QUOTE form. The quote "
|
"The PAIR for currencies should be in BASE/QUOTE form. The quote "
|
||||||
"symbol must always be for a physical currency. The --symbols option "
|
"symbol must always be for a physical currency. The --symbols option "
|
||||||
"will list all digital and physical currency symbols.\n"
|
"will list all digital and physical currency symbols.\n"
|
||||||
|
@ -165,8 +166,7 @@ class AlphaVantage(BaseSource):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise exceptions.ResponseParsingError(str(e)) from e
|
raise exceptions.ResponseParsingError(str(e)) from e
|
||||||
|
|
||||||
if type(data) == dict and "Note" in data and "call frequency" in data["Note"]:
|
self._raise_for_generic_errors(data)
|
||||||
raise exceptions.RateLimit(data["Note"])
|
|
||||||
|
|
||||||
expected_keys = ["1. symbol", "2. name", "3. type", "4. region", "8. currency"]
|
expected_keys = ["1. symbol", "2. name", "3. type", "4. region", "8. currency"]
|
||||||
if (
|
if (
|
||||||
|
@ -204,8 +204,7 @@ class AlphaVantage(BaseSource):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise exceptions.ResponseParsingError(str(e)) from e
|
raise exceptions.ResponseParsingError(str(e)) from e
|
||||||
|
|
||||||
if type(data) == dict and "Note" in data and "call frequency" in data["Note"]:
|
self._raise_for_generic_errors(data)
|
||||||
raise exceptions.RateLimit(data["Note"])
|
|
||||||
|
|
||||||
if "Error Message" in data:
|
if "Error Message" in data:
|
||||||
if output_quote == "UNKNOWN":
|
if output_quote == "UNKNOWN":
|
||||||
|
@ -255,8 +254,7 @@ class AlphaVantage(BaseSource):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise exceptions.ResponseParsingError(str(e)) from e
|
raise exceptions.ResponseParsingError(str(e)) from e
|
||||||
|
|
||||||
if type(data) == dict and "Note" in data and "call frequency" in data["Note"]:
|
self._raise_for_generic_errors(data)
|
||||||
raise exceptions.RateLimit(data["Note"])
|
|
||||||
|
|
||||||
if type(data) != dict or "Time Series FX (Daily)" not in data:
|
if type(data) != dict or "Time Series FX (Daily)" not in data:
|
||||||
raise exceptions.ResponseParsingError("Unexpected content.")
|
raise exceptions.ResponseParsingError("Unexpected content.")
|
||||||
|
@ -297,8 +295,7 @@ class AlphaVantage(BaseSource):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise exceptions.ResponseParsingError(str(e)) from e
|
raise exceptions.ResponseParsingError(str(e)) from e
|
||||||
|
|
||||||
if type(data) == dict and "Note" in data and "call frequency" in data["Note"]:
|
self._raise_for_generic_errors(data)
|
||||||
raise exceptions.RateLimit(data["Note"])
|
|
||||||
|
|
||||||
if type(data) != dict or "Time Series (Digital Currency Daily)" not in data:
|
if type(data) != dict or "Time Series (Digital Currency Daily)" not in data:
|
||||||
raise exceptions.ResponseParsingError("Unexpected content.")
|
raise exceptions.ResponseParsingError("Unexpected content.")
|
||||||
|
@ -317,12 +314,23 @@ class AlphaVantage(BaseSource):
|
||||||
return normalized_data
|
return normalized_data
|
||||||
|
|
||||||
def _apikey(self, require=True):
|
def _apikey(self, require=True):
|
||||||
key_name = "ALPHAVANTAGE_API_KEY"
|
key = os.getenv(self.API_KEY_NAME)
|
||||||
key = os.getenv(key_name)
|
|
||||||
if require and not key:
|
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
|
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)]:
|
def _physical_symbols(self) -> list[(str, str)]:
|
||||||
url = "https://www.alphavantage.co/physical_currency_list/"
|
url = "https://www.alphavantage.co/physical_currency_list/"
|
||||||
return self._get_symbols(url, "Physical: ")
|
return self._get_symbols(url, "Physical: ")
|
||||||
|
|
|
@ -9,7 +9,7 @@ import pytest
|
||||||
import requests
|
import requests
|
||||||
import responses
|
import responses
|
||||||
|
|
||||||
from pricehist import exceptions
|
from pricehist import __version__, exceptions
|
||||||
from pricehist.price import Price
|
from pricehist.price import Price
|
||||||
from pricehist.series import Series
|
from pricehist.series import Series
|
||||||
from pricehist.sources.alphavantage import AlphaVantage
|
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)
|
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)
|
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:
|
with pytest.raises(exceptions.CredentialsError) as e:
|
||||||
src.fetch(Series("EUR", "AUD", type, "2021-01-04", "2021-01-08"))
|
src.fetch(Series("EUR", "AUD", type, "2021-01-04", "2021-01-08"))
|
||||||
assert "unavailable or invalid" in str(e.value)
|
assert "unavailable or invalid" in str(e.value)
|
||||||
|
|
Loading…
Add table
Reference in a new issue