formatting: apply black formatter

This commit is contained in:
Cameron Yick
2021-08-14 21:03:11 -04:00
parent fc654d1e48
commit f4a9a04b2e
7 changed files with 320 additions and 263 deletions

View File

@@ -50,6 +50,10 @@ clean-test: ## remove test and coverage artifacts
lint: ## check style with flake8
flake8 tiingo tests
format: ## apply opinionated formatting
black tiingo/
test: ## run tests quickly with the default Python
py.test

View File

@@ -3,4 +3,4 @@ from tiingo.api import TiingoClient
from tiingo.wsclient import TiingoWebsocketClient
__author__ = """Cameron Yick"""
__email__ = 'cameron.yick@enigma.com'
__email__ = "cameron.yick@enigma.com"

View File

@@ -1,2 +1,2 @@
# -*- coding: utf-8 -*-
__version__ = '0.14.0'
__version__ = "0.14.0"

View File

@@ -16,10 +16,12 @@ from tiingo.exceptions import (
InstallPandasException,
APIColumnNameError,
InvalidFrequencyError,
MissingRequiredArgumentError)
MissingRequiredArgumentError,
)
try:
import pandas as pd
pandas_is_installed = True
except ImportError:
pandas_is_installed = False
@@ -40,11 +42,13 @@ def get_zipfile_from_response(response):
def get_buffer_from_zipfile(zipfile, filename):
if sys.version_info < (3, 0): # python 2
from StringIO import StringIO
return StringIO(zipfile.read(filename))
else: # python 3
# Source:
# https://stackoverflow.com/questions/5627954/py3k-how-do-you-read-a-file-inside-a-zip-file-as-text-not-bytes
from io import (TextIOWrapper, BytesIO)
from io import TextIOWrapper, BytesIO
return TextIOWrapper(BytesIO(zipfile.read(filename)))
@@ -52,9 +56,9 @@ def dict_to_object(item, object_name):
"""Converts a python dict to a namedtuple, saving memory."""
fields = item.keys()
values = item.values()
return json.loads(json.dumps(item),
object_hook=lambda d:
namedtuple(object_name, fields)(*values))
return json.loads(
json.dumps(item), object_hook=lambda d: namedtuple(object_name, fields)(*values)
)
class TiingoClient(RestClient):
@@ -69,28 +73,30 @@ class TiingoClient(RestClient):
self._base_url = "https://api.tiingo.com"
try:
api_key = self._config['api_key']
api_key = self._config["api_key"]
except KeyError:
api_key = os.environ.get('TIINGO_API_KEY')
api_key = os.environ.get("TIINGO_API_KEY")
self._api_key = api_key
if not(api_key):
raise RuntimeError("Tiingo API Key not provided. Please provide"
" via environment variable or config argument.")
if not (api_key):
raise RuntimeError(
"Tiingo API Key not provided. Please provide"
" via environment variable or config argument."
)
self._headers = {
'Authorization': "Token {}".format(api_key),
'Content-Type': 'application/json',
'User-Agent': 'tiingo-python-client {}'.format(VERSION)
"Authorization": "Token {}".format(api_key),
"Content-Type": "application/json",
"User-Agent": "tiingo-python-client {}".format(VERSION),
}
self._frequency_pattern = re.compile('^[0-9]+(min|hour)$', re.IGNORECASE)
self._frequency_pattern = re.compile("^[0-9]+(min|hour)$", re.IGNORECASE)
def __repr__(self):
return '<TiingoClient(url="{}")>'.format(self._base_url)
def _is_eod_frequency(self,frequency):
return frequency.lower() in ['daily', 'weekly', 'monthly', 'annually']
def _is_eod_frequency(self, frequency):
return frequency.lower() in ["daily", "weekly", "monthly", "annually"]
# TICKER PRICE ENDPOINTS
# https://api.tiingo.com/docs/tiingo/daily
@@ -102,29 +108,30 @@ class TiingoClient(RestClient):
Tickers for unrelated products are omitted.
https://apimedia.tiingo.com/docs/tiingo/daily/supported_tickers.zip
"""
listing_file_url = "https://apimedia.tiingo.com/docs/tiingo/daily/supported_tickers.zip"
listing_file_url = (
"https://apimedia.tiingo.com/docs/tiingo/daily/supported_tickers.zip"
)
response = requests.get(listing_file_url)
zipdata = get_zipfile_from_response(response)
raw_csv = get_buffer_from_zipfile(zipdata, 'supported_tickers.csv')
raw_csv = get_buffer_from_zipfile(zipdata, "supported_tickers.csv")
reader = csv.DictReader(raw_csv)
if not len(assetTypes):
return [row for row in reader]
assetTypesSet = set(assetTypes)
return [row for row in reader
if row.get('assetType') in assetTypesSet]
return [row for row in reader if row.get("assetType") in assetTypesSet]
def list_stock_tickers(self):
return self.list_tickers(['Stock'])
return self.list_tickers(["Stock"])
def list_etf_tickers(self):
return self.list_tickers(['ETF'])
return self.list_tickers(["ETF"])
def list_fund_tickers(self):
return self.list_tickers(['Mutual Fund'])
return self.list_tickers(["Mutual Fund"])
def get_ticker_metadata(self, ticker, fmt='json'):
def get_ticker_metadata(self, ticker, fmt="json"):
"""Return metadata for 1 ticker
Use TiingoClient.list_tickers() to get available options
@@ -132,11 +139,11 @@ class TiingoClient(RestClient):
ticker (str) : Unique identifier for stock
"""
url = "tiingo/daily/{}".format(ticker)
response = self._request('GET', url)
response = self._request("GET", url)
data = response.json()
if fmt == 'json':
if fmt == "json":
return data
elif fmt == 'object':
elif fmt == "object":
return dict_to_object(data, "Ticker")
def _invalid_frequency(self, frequency):
@@ -145,7 +152,9 @@ class TiingoClient(RestClient):
:param frequency (string): frequency string
:return (boolean):
"""
is_valid = self._is_eod_frequency(frequency) or re.match(self._frequency_pattern, frequency)
is_valid = self._is_eod_frequency(frequency) or re.match(
self._frequency_pattern, frequency
)
return not is_valid
def _get_url(self, ticker, frequency):
@@ -157,8 +166,10 @@ class TiingoClient(RestClient):
:return (string): url
"""
if self._invalid_frequency(frequency):
etext = ("Error: {} is an invalid frequency. Check Tiingo API documentation "
"for valid EOD or intraday frequency format.")
etext = (
"Error: {} is an invalid frequency. Check Tiingo API documentation "
"for valid EOD or intraday frequency format."
)
raise InvalidFrequencyError(etext.format(frequency))
else:
if self._is_eod_frequency(frequency):
@@ -179,19 +190,19 @@ class TiingoClient(RestClient):
all of the available data will be returned. In the event of a list of tickers,
this parameter is required.
"""
url = self._get_url(ticker, params['resampleFreq'])
response = self._request('GET', url, params=params)
if params['format'] == 'csv':
url = self._get_url(ticker, params["resampleFreq"])
response = self._request("GET", url, params=params)
if params["format"] == "csv":
if sys.version_info < (3, 0): # python 2
from StringIO import StringIO
else: # python 3
from io import StringIO
df = pd.read_csv(StringIO(response.content.decode('utf-8')))
df = pd.read_csv(StringIO(response.content.decode("utf-8")))
else:
df = pd.DataFrame(response.json())
df.set_index('date', inplace=True)
df.set_index("date", inplace=True)
if metric_name is not None:
prices = df[metric_name]
@@ -203,13 +214,13 @@ class TiingoClient(RestClient):
# Localize to UTC to ensure equivalence between data returned in json format and
# csv format. Tiingo daily data requested in csv format does not include a timezone.
if prices.index.tz is None:
prices.index = prices.index.tz_localize('UTC')
prices.index = prices.index.tz_localize("UTC")
return prices
def get_ticker_price(self, ticker,
startDate=None, endDate=None,
fmt='json', frequency='daily'):
def get_ticker_price(
self, ticker, startDate=None, endDate=None, fmt="json", frequency="daily"
):
"""By default, return latest EOD Composite Price for a stock ticker.
On average, each feed contains 3 data sources.
@@ -225,18 +236,18 @@ class TiingoClient(RestClient):
"""
url = self._get_url(ticker, frequency)
params = {
'format': fmt if fmt != "object" else 'json', # conversion local
'resampleFreq': frequency
"format": fmt if fmt != "object" else "json", # conversion local
"resampleFreq": frequency,
}
if startDate:
params['startDate'] = startDate
params["startDate"] = startDate
if endDate:
params['endDate'] = endDate
params["endDate"] = endDate
# TODO: evaluate whether to stream CSV to cache on disk, or
# load as array in memory, or just pass plain text
response = self._request('GET', url, params=params)
response = self._request("GET", url, params=params)
if fmt == "json":
return response.json()
elif fmt == "object":
@@ -245,11 +256,17 @@ class TiingoClient(RestClient):
else:
return response.content.decode("utf-8")
def get_dataframe(self, tickers,
startDate=None, endDate=None, metric_name=None,
frequency='daily', fmt='json'):
def get_dataframe(
self,
tickers,
startDate=None,
endDate=None,
metric_name=None,
frequency="daily",
fmt="json",
):
""" Return a pandas.DataFrame of historical prices for one or more ticker symbols.
"""Return a pandas.DataFrame of historical prices for one or more ticker symbols.
By default, return latest EOD Composite Price for a list of stock tickers.
On average, each feed contains 3 data sources.
@@ -270,53 +287,77 @@ class TiingoClient(RestClient):
fmt (string): 'csv' or 'json'
"""
valid_columns = {'open', 'high', 'low', 'close', 'volume', 'adjOpen', 'adjHigh', 'adjLow',
'adjClose', 'adjVolume', 'divCash', 'splitFactor'}
valid_columns = {
"open",
"high",
"low",
"close",
"volume",
"adjOpen",
"adjHigh",
"adjLow",
"adjClose",
"adjVolume",
"divCash",
"splitFactor",
}
if metric_name is not None and metric_name not in valid_columns:
raise APIColumnNameError('Valid data items are: ' + str(valid_columns))
raise APIColumnNameError("Valid data items are: " + str(valid_columns))
if metric_name is None and isinstance(tickers, list):
raise MissingRequiredArgumentError("""When tickers is provided as a list, metric_name is a required argument.
Please provide a metric_name, or call this method with one ticker at a time.""")
raise MissingRequiredArgumentError(
"""When tickers is provided as a list, metric_name is a required argument.
Please provide a metric_name, or call this method with one ticker at a time."""
)
params = {
'format': fmt,
'resampleFreq': frequency
}
params = {"format": fmt, "resampleFreq": frequency}
if startDate:
params['startDate'] = startDate
params["startDate"] = startDate
if endDate:
params['endDate'] = endDate
params["endDate"] = endDate
if pandas_is_installed:
if type(tickers) is str:
prices = self._request_pandas(
ticker=tickers, params=params, metric_name=metric_name)
ticker=tickers, params=params, metric_name=metric_name
)
else:
prices = pd.DataFrame()
for stock in tickers:
ticker_series = self._request_pandas(
ticker=stock, params=params, metric_name=metric_name)
ticker=stock, params=params, metric_name=metric_name
)
ticker_series = ticker_series.rename(stock)
prices = pd.concat([prices, ticker_series], axis=1, sort=True)
return prices
else:
error_message = ("Pandas is not installed, but .get_ticker_price() was "
error_message = (
"Pandas is not installed, but .get_ticker_price() was "
"called with fmt=pandas. In order to install tiingo with "
"pandas, reinstall with pandas as an optional dependency. \n"
"Install tiingo with pandas dependency: \'pip install tiingo[pandas]\'\n"
"Alternatively, just install pandas: pip install pandas.")
"Install tiingo with pandas dependency: 'pip install tiingo[pandas]'\n"
"Alternatively, just install pandas: pip install pandas."
)
raise InstallPandasException(error_message)
# NEWS FEEDS
# tiingo/news
def get_news(self, tickers=[], tags=[], sources=[], startDate=None,
endDate=None, limit=100, offset=0, sortBy="publishedDate",
def get_news(
self,
tickers=[],
tags=[],
sources=[],
startDate=None,
endDate=None,
limit=100,
offset=0,
sortBy="publishedDate",
onlyWithTickers=False,
fmt='json'):
fmt="json",
):
"""Return list of news articles matching given search terms
https://api.tiingo.com/docs/tiingo/news
@@ -334,24 +375,24 @@ class TiingoClient(RestClient):
"""
url = "tiingo/news"
params = {
'limit': limit,
'offset': offset,
'sortBy': sortBy,
'tickers': tickers,
'source': (",").join(sources) if sources else None,
'tags': tags,
'startDate': startDate,
'endDate': endDate,
'onlyWithTickers': onlyWithTickers
"limit": limit,
"offset": offset,
"sortBy": sortBy,
"tickers": tickers,
"source": (",").join(sources) if sources else None,
"tags": tags,
"startDate": startDate,
"endDate": endDate,
"onlyWithTickers": onlyWithTickers,
}
response = self._request('GET', url, params=params)
response = self._request("GET", url, params=params)
data = response.json()
if fmt == 'json':
if fmt == "json":
return data
elif fmt == 'object':
elif fmt == "object":
return [dict_to_object(item, "NewsArticle") for item in data]
def get_bulk_news(self, file_id=None, fmt='json'):
def get_bulk_news(self, file_id=None, fmt="json"):
"""Only available to institutional clients.
If ID is NOT provided, return array of available file_ids.
If ID is provided, provides URL which you can use to download your
@@ -362,76 +403,85 @@ class TiingoClient(RestClient):
else:
url = "tiingo/news/bulk_download"
response = self._request('GET', url)
response = self._request("GET", url)
data = response.json()
if fmt == 'json':
if fmt == "json":
return data
elif fmt == 'object':
elif fmt == "object":
return dict_to_object(data, "BulkNews")
# Crypto
# tiingo/crypto
def get_crypto_top_of_book(self, tickers=[], exchanges=[],
includeRawExchangeData=False, convertCurrency=None):
url = 'tiingo/crypto/top'
params = {
'tickers': ','.join(tickers)
}
def get_crypto_top_of_book(
self,
tickers=[],
exchanges=[],
includeRawExchangeData=False,
convertCurrency=None,
):
url = "tiingo/crypto/top"
params = {"tickers": ",".join(tickers)}
if len(exchanges):
params['exchanges'] = ','.join(exchanges)
params["exchanges"] = ",".join(exchanges)
if includeRawExchangeData is True:
params['includeRawExchangeData'] = True
params["includeRawExchangeData"] = True
if convertCurrency:
params['convertCurrency'] = convertCurrency
params["convertCurrency"] = convertCurrency
response = self._request('GET', url, params=params)
response = self._request("GET", url, params=params)
return response.json()
def get_crypto_price_history(self, tickers=[], baseCurrency=None,
startDate=None, endDate=None, exchanges=[],
consolidateBaseCurrency=False, includeRawExchangeData=False,
resampleFreq=None, convertCurrency=None):
url = 'tiingo/crypto/prices'
params = {
'tickers': ','.join(tickers)
}
def get_crypto_price_history(
self,
tickers=[],
baseCurrency=None,
startDate=None,
endDate=None,
exchanges=[],
consolidateBaseCurrency=False,
includeRawExchangeData=False,
resampleFreq=None,
convertCurrency=None,
):
url = "tiingo/crypto/prices"
params = {"tickers": ",".join(tickers)}
if startDate:
params['startDate'] = startDate
params["startDate"] = startDate
if endDate:
params['endDate'] = endDate
params["endDate"] = endDate
if len(exchanges):
params['exchanges'] = ','.join(exchanges)
params["exchanges"] = ",".join(exchanges)
if consolidateBaseCurrency is True:
params['consolidateBaseCurrency'] = ','.join(consolidateBaseCurrency)
params["consolidateBaseCurrency"] = ",".join(consolidateBaseCurrency)
if includeRawExchangeData is True:
params['includeRawExchangeData'] = includeRawExchangeData
params["includeRawExchangeData"] = includeRawExchangeData
if resampleFreq:
params['resampleFreq'] = resampleFreq
params["resampleFreq"] = resampleFreq
if convertCurrency:
params['convertCurrency'] = convertCurrency
params["convertCurrency"] = convertCurrency
response = self._request('GET', url, params=params)
response = self._request("GET", url, params=params)
return response.json()
def get_crypto_metadata(self, tickers=[], fmt='json'):
url = 'tiingo/crypto'
def get_crypto_metadata(self, tickers=[], fmt="json"):
url = "tiingo/crypto"
params = {
'tickers': ','.join(tickers),
'format': fmt,
"tickers": ",".join(tickers),
"format": fmt,
}
response = self._request('GET', url, params=params)
if fmt == 'csv':
response = self._request("GET", url, params=params)
if fmt == "csv":
return response.content.decode("utf-8")
else:
return response.json()
# FUNDAMENTAL DEFINITIONS
# tiingo/fundamentals/definitions
def get_fundamentals_definitions(self, tickers=[], fmt='json'):
def get_fundamentals_definitions(self, tickers=[], fmt="json"):
"""Return definitions for fundamentals for specified tickers
https://api.tiingo.com/documentation/fundamentals
@@ -440,20 +490,16 @@ class TiingoClient(RestClient):
fmt (string): 'csv' or 'json'
"""
url = "tiingo/fundamentals/definitions"
params = {
'tickers': tickers,
'format': fmt
}
response = self._request('GET', url, params=params)
if fmt == 'json':
params = {"tickers": tickers, "format": fmt}
response = self._request("GET", url, params=params)
if fmt == "json":
return response.json()
elif fmt == 'csv':
elif fmt == "csv":
return response.content.decode("utf-8")
# FUNDAMENTAL DAILY
# tiingo/fundamentals/<ticker>/daily
def get_fundamentals_daily(self, ticker, fmt='json',
startDate=None, endDate=None):
def get_fundamentals_daily(self, ticker, fmt="json", startDate=None, endDate=None):
"""Returns metrics which rely on daily price-updates
https://api.tiingo.com/documentation/fundamentals
@@ -464,22 +510,19 @@ class TiingoClient(RestClient):
startDate, endDate [date]: Boundaries of search window
fmt (string): 'csv' or 'json'
"""
url = 'tiingo/fundamentals/{}/daily'.format(ticker)
params = {
'startDate': startDate,
'endDate': endDate,
'format': fmt
}
response = self._request('GET', url, params=params)
if fmt == 'json':
url = "tiingo/fundamentals/{}/daily".format(ticker)
params = {"startDate": startDate, "endDate": endDate, "format": fmt}
response = self._request("GET", url, params=params)
if fmt == "json":
return response.json()
elif fmt == 'csv':
elif fmt == "csv":
return response.content.decode("utf-8")
# FUNDAMENTAL STATEMENTS
# tiingo/fundamentals/<ticker>/statements
def get_fundamentals_statements(self, ticker, asReported=False, fmt='json',
startDate=None, endDate=None):
def get_fundamentals_statements(
self, ticker, asReported=False, fmt="json", startDate=None, endDate=None
):
"""Returns data that is extracted from quarterly and annual statements.
https://api.tiingo.com/documentation/fundamentals
@@ -494,19 +537,19 @@ class TiingoClient(RestClient):
fmt (string): 'csv' or 'json'
"""
if asReported:
asReported = 'true'
asReported = "true"
else:
asReported = 'false'
asReported = "false"
url = 'tiingo/fundamentals/{}/statements'.format(ticker)
url = "tiingo/fundamentals/{}/statements".format(ticker)
params = {
'startDate': startDate,
'endDate': endDate,
'asReported': asReported,
'format': fmt
"startDate": startDate,
"endDate": endDate,
"asReported": asReported,
"format": fmt,
}
response = self._request('GET', url, params=params)
if fmt == 'json':
response = self._request("GET", url, params=params)
if fmt == "json":
return response.json()
elif fmt == 'csv':
elif fmt == "csv":
return response.content.decode("utf-8")

View File

@@ -10,5 +10,6 @@ class APIColumnNameError(Exception):
class InvalidFrequencyError(Exception):
pass
class MissingRequiredArgumentError(Exception):
pass

View File

@@ -12,7 +12,6 @@ class RestClientError(Exception):
class RestClient(object):
def __init__(self, config={}):
"""Base class for interacting with RESTful APIs
Child class MUST have a ._base_url property!
@@ -28,7 +27,7 @@ class RestClient(object):
self._headers = {}
self._base_url = ""
if config.get('session'):
if config.get("session"):
self._session = requests.Session()
else:
self._session = requests
@@ -44,10 +43,9 @@ class RestClient(object):
url (str): path appended to the base_url to create request
**kwargs: passed directly to a requests.request object
"""
resp = self._session.request(method,
'{}/{}'.format(self._base_url, url),
headers=self._headers,
**kwargs)
resp = self._session.request(
method, "{}/{}".format(self._base_url, url), headers=self._headers, **kwargs
)
try:
resp.raise_for_status()

View File

@@ -3,8 +3,9 @@ import websocket
import json
from tiingo.exceptions import MissingRequiredArgumentError
class TiingoWebsocketClient:
'''
"""
from tiingo import TiingoWebsocketClient
def cb_fn(msg):
@@ -36,56 +37,66 @@ class TiingoWebsocketClient:
# any logic should be implemented in the callback function
TiingoWebsocketClient(subscribe,endpoint="iex",on_msg_cb=cb_fn)
while True:pass
'''
"""
def __init__(self,config=None,endpoint=None,on_msg_cb=None):
def __init__(self, config=None, endpoint=None, on_msg_cb=None):
self._base_url = "wss://api.tiingo.com"
self.config = {} if config is None else config
try:
api_key = self.config['authorization']
api_key = self.config["authorization"]
except KeyError:
api_key = os.environ.get('TIINGO_API_KEY')
self.config.update({"authorization":api_key})
api_key = os.environ.get("TIINGO_API_KEY")
self.config.update({"authorization": api_key})
self._api_key = api_key
if not(api_key):
raise RuntimeError("Tiingo API Key not provided. Please provide"
if not (api_key):
raise RuntimeError(
"Tiingo API Key not provided. Please provide"
" via environment variable or config argument."
"Notice that this config dict takes the API Key as authorization ")
"Notice that this config dict takes the API Key as authorization "
)
self.endpoint = endpoint
if not (self.endpoint=="iex" or self.endpoint=="fx" or self.endpoint=="crypto"):
if not (
self.endpoint == "iex" or self.endpoint == "fx" or self.endpoint == "crypto"
):
raise AttributeError("Endpoint must be defined as either (iex,fx,crypto) ")
self.on_msg_cb = on_msg_cb
if not self.on_msg_cb:
raise MissingRequiredArgumentError("please define on_msg_cb It's a callback that gets called when new messages arrive "
raise MissingRequiredArgumentError(
"please define on_msg_cb It's a callback that gets called when new messages arrive "
"Example:"
"def cb_fn(msg):"
" print(msg)")
" print(msg)"
)
websocket.enableTrace(False)
ws = websocket.WebSocketApp("{0}/{1}".format(self._base_url,self.endpoint),
on_message = self.get_on_msg_cb(),
on_error = self.on_error,
on_close = self.on_close,
on_open = self.get_on_open(self.config))
ws = websocket.WebSocketApp(
"{0}/{1}".format(self._base_url, self.endpoint),
on_message=self.get_on_msg_cb(),
on_error=self.on_error,
on_close=self.on_close,
on_open=self.get_on_open(self.config),
)
ws.run_forever()
def get_on_open(self,config):
def get_on_open(self, config):
# the methods passed to websocketClient have to be unbounded if we want WebSocketApp to pass everything correctly
# see websocket-client/#471
def on_open(ws):
ws.send(json.dumps(config))
return on_open
def get_on_msg_cb(self):
def on_msg_cb_local(ws,msg):
def on_msg_cb_local(ws, msg):
self.on_msg_cb(msg)
return
return on_msg_cb_local
# since methods need to be unbound in order for websocketClient these methods don't have a self as their first parameter