diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 3e1334f..e19bba2 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -30,8 +30,11 @@ jobs: run: | python -m pip install --upgrade pip python setup.py develop - python -m pip install flake8 pytest-cov codecov vcrpy + python -m pip install flake8 pytest-cov codecov vcrpy black tools/install_pandas.sh + - name: black + run: | + black --check tiingo - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names diff --git a/Makefile b/Makefile index 3448ba7..12a882f 100644 --- a/Makefile +++ b/Makefile @@ -50,9 +50,15 @@ clean-test: ## remove test and coverage artifacts lint: ## check style with flake8 flake8 tiingo tests +format: ## apply opinionated formatting + black tiingo/ + +format-check: ## check formatting for CI + black --check tiingo/ + test: ## run tests quickly with the default Python py.test - + test-all: ## run tests on every Python version with tox tox diff --git a/requirements_dev.txt b/requirements_dev.txt index 2ddb607..3ffd002 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -12,3 +12,4 @@ pytest==6.2.4 pytest-runner==5.3.1 vcrpy==2.1.1 twine==3.4.2 +black==21.7b0 diff --git a/tiingo/__init__.py b/tiingo/__init__.py index 2159444..560110a 100644 --- a/tiingo/__init__.py +++ b/tiingo/__init__.py @@ -3,4 +3,4 @@ from tiingo.wsclient import TiingoWebsocketClient __author__ = """Cameron Yick""" -__email__ = 'cameron.yick@enigma.com' +__email__ = "cameron.yick@enigma.com" diff --git a/tiingo/__version__.py b/tiingo/__version__.py index c25929f..2fbb3d1 100644 --- a/tiingo/__version__.py +++ b/tiingo/__version__.py @@ -1,2 +1,2 @@ # -*- coding: utf-8 -*- -__version__ = '0.14.0' +__version__ = "0.14.0" diff --git a/tiingo/api.py b/tiingo/api.py index 49702a4..9bd90b2 100644 --- a/tiingo/api.py +++ b/tiingo/api.py @@ -16,10 +16,12 @@ 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,16 +56,16 @@ 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): """Class for managing interactions with the Tiingo REST API - Supply API Key via Environment Variable TIINGO_API_KEY - or via the Config Object + Supply API Key via Environment Variable TIINGO_API_KEY + or via the Config Object """ def __init__(self, *args, **kwargs): @@ -69,74 +73,77 @@ def __init__(self, *args, **kwargs): 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 ''.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 def list_tickers(self, assetTypes=[]): """Return a list of dicts of metadata tickers for all supported tickers - of the specified asset type, as well as metadata about each ticker. - This includes supported date range, the exchange the ticker is traded - on, and the currency the stock is traded on. - 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" + of the specified asset type, as well as metadata about each ticker. + This includes supported date range, the exchange the ticker is traded + on, and the currency the stock is traded on. + 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" + ) 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 + Use TiingoClient.list_tickers() to get available options - Args: - ticker (str) : Unique identifier for stock + Args: + 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 @@ def _invalid_frequency(self, frequency): :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 @@ def _get_url(self, ticker, frequency): :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 @@ def _request_pandas(self, ticker, metric_name, params): 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,40 +214,40 @@ def _request_pandas(self, ticker, metric_name, params): # 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. + On average, each feed contains 3 data sources. - Supported tickers + Available Day Ranges are here: - https://apimedia.tiingo.com/docs/tiingo/daily/supported_tickers.zip + Supported tickers + Available Day Ranges are here: + https://apimedia.tiingo.com/docs/tiingo/daily/supported_tickers.zip - Args: - ticker (string): Unique identifier for stock ticker - startDate (string): Start of ticker range in YYYY-MM-DD format - endDate (string): End of ticker range in YYYY-MM-DD format - fmt (string): 'csv' or 'json' - frequency (string): Resample frequency + Args: + ticker (string): Unique identifier for stock ticker + startDate (string): Start of ticker range in YYYY-MM-DD format + endDate (string): End of ticker range in YYYY-MM-DD format + fmt (string): 'csv' or 'json' + frequency (string): Resample frequency """ 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,241 +256,273 @@ def get_ticker_price(self, ticker, else: return response.content.decode("utf-8") - 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. - - By default, return latest EOD Composite Price for a list of stock tickers. - On average, each feed contains 3 data sources. - - Supported tickers + Available Day Ranges are here: - https://apimedia.tiingo.com/docs/tiingo/daily/supported_tickers.zip - or from the TiingoClient.list_tickers() method. - - Args: - tickers (string/list): One or more unique identifiers for a stock ticker. - startDate (string): Start of ticker range in YYYY-MM-DD format. - endDate (string): End of ticker range in YYYY-MM-DD format. - metric_name (string): Optional parameter specifying metric to be returned for each - ticker. In the event of a single ticker, this is optional and if not specified - all of the available data will be returned. In the event of a list of tickers, - this parameter is required. - frequency (string): Resample frequency (defaults to daily). - fmt (string): 'csv' or '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. + + By default, return latest EOD Composite Price for a list of stock tickers. + On average, each feed contains 3 data sources. + + Supported tickers + Available Day Ranges are here: + https://apimedia.tiingo.com/docs/tiingo/daily/supported_tickers.zip + or from the TiingoClient.list_tickers() method. + + Args: + tickers (string/list): One or more unique identifiers for a stock ticker. + startDate (string): Start of ticker range in YYYY-MM-DD format. + endDate (string): End of ticker range in YYYY-MM-DD format. + metric_name (string): Optional parameter specifying metric to be returned for each + ticker. In the event of a single ticker, this is optional and if not specified + all of the available data will be returned. In the event of a list of tickers, + this parameter is required. + frequency (string): Resample frequency (defaults to daily). + 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 " - "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.") + 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." + ) 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", - onlyWithTickers=False, - fmt='json'): + def get_news( + self, + tickers=[], + tags=[], + sources=[], + startDate=None, + endDate=None, + limit=100, + offset=0, + sortBy="publishedDate", + onlyWithTickers=False, + fmt="json", + ): """Return list of news articles matching given search terms - https://api.tiingo.com/docs/tiingo/news - - # Dates are in YYYY-MM-DD Format. - - Args: - tickers [string] : List of unique Stock Tickers to search - tags [string] : List of topics tagged by Tiingo Algorithms - sources [string]: List of base urls to include as news sources - startDate, endDate [date]: Boundaries of news search window - limit (int): Max results returned. Default 100, max 1000 - offset (int): Search results offset, used for paginating - sortBy (string): "publishedDate" OR "crawlDate", descending - onlyWithTickers (bool): If true, only links with tagged tickers will return. + https://api.tiingo.com/docs/tiingo/news + + # Dates are in YYYY-MM-DD Format. + + Args: + tickers [string] : List of unique Stock Tickers to search + tags [string] : List of topics tagged by Tiingo Algorithms + sources [string]: List of base urls to include as news sources + startDate, endDate [date]: Boundaries of news search window + limit (int): Max results returned. Default 100, max 1000 + offset (int): Search results offset, used for paginating + sortBy (string): "publishedDate" OR "crawlDate", descending + onlyWithTickers (bool): If true, only links with tagged tickers will return. """ 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 - file, as well as some metadata about that file. + If ID is NOT provided, return array of available file_ids. + If ID is provided, provides URL which you can use to download your + file, as well as some metadata about that file. """ if file_id: url = "tiingo/news/bulk_download/{}".format(file_id) 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 + https://api.tiingo.com/documentation/fundamentals - Args: - tickers [string] : optional, either list or string - fmt (string): 'csv' or 'json' + Args: + tickers [string] : optional, either list or string + 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//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 + https://api.tiingo.com/documentation/fundamentals - # Dates are in YYYY-MM-DD Format. + # Dates are in YYYY-MM-DD Format. - Args: - tickers [string] : List of unique Stock Tickers to search - startDate, endDate [date]: Boundaries of search window - fmt (string): 'csv' or 'json' + Args: + tickers [string] : List of unique Stock Tickers to search + 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//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 @@ def get_fundamentals_statements(self, ticker, asReported=False, fmt='json', 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") diff --git a/tiingo/exceptions.py b/tiingo/exceptions.py index b510b75..81ff713 100644 --- a/tiingo/exceptions.py +++ b/tiingo/exceptions.py @@ -10,5 +10,6 @@ class APIColumnNameError(Exception): class InvalidFrequencyError(Exception): pass + class MissingRequiredArgumentError(Exception): pass diff --git a/tiingo/restclient.py b/tiingo/restclient.py index ee6a7aa..869baad 100644 --- a/tiingo/restclient.py +++ b/tiingo/restclient.py @@ -12,13 +12,12 @@ 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! + Child class MUST have a ._base_url property! - Args: - config (dict): Arbitrary options that child classes can access + Args: + config (dict): Arbitrary options that child classes can access """ self._config = config @@ -28,7 +27,7 @@ def __init__(self, config={}): self._headers = {} self._base_url = "" - if config.get('session'): + if config.get("session"): self._session = requests.Session() else: self._session = requests @@ -39,15 +38,14 @@ def __repr__(self): def _request(self, method, url, **kwargs): """Make HTTP request and return response object - Args: - method (str): GET, POST, PUT, DELETE - url (str): path appended to the base_url to create request - **kwargs: passed directly to a requests.request object + Args: + method (str): GET, POST, PUT, DELETE + 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() diff --git a/tiingo/wsclient.py b/tiingo/wsclient.py index ffeefe3..63a7ff1 100644 --- a/tiingo/wsclient.py +++ b/tiingo/wsclient.py @@ -3,23 +3,24 @@ import json from tiingo.exceptions import MissingRequiredArgumentError + class TiingoWebsocketClient: - ''' + """ from tiingo import TiingoWebsocketClient - + def cb_fn(msg): - # Example response + # Example response # msg = { - # "service":"iex" # An identifier telling you this is IEX data. + # "service":"iex" # An identifier telling you this is IEX data. # The value returned by this will correspond to the endpoint argument. - # + # # # Will always return "A" meaning new price quotes. There are also H type Heartbeat msgs used to keep the connection alive # "messageType":"A" # A value telling you what kind of data packet this is from our IEX feed. - # + # # # see https://api.tiingo.com/documentation/websockets/iex > Response for more info # "data":[] # an array containing trade information and a timestamp - # + # # } print(msg) @@ -28,68 +29,78 @@ def cb_fn(msg): 'eventName':'subscribe', 'authorization':'API_KEY_GOES_HERE', #see https://api.tiingo.com/documentation/websockets/iex > Request for more info - 'eventData': { + 'eventData': { 'thresholdLevel':5 } } # notice how the object isn't needed after using it - # any logic should be implemented in the callback function + # 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 - + 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" - " via environment variable or config argument." - "Notice that this config dict takes the API Key as authorization ") + 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 " + ) 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 " - "Example:" - "def cb_fn(msg):" - " print(msg)") + 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)" + ) 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): - # the methods passed to websocketClient have to be unbounded if we want WebSocketApp to pass everything correctly + + 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 + 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 - def on_error(ws, error): # lgtm[py/not-named-self] + def on_error(ws, error): # lgtm[py/not-named-self] print(error) def on_close(ws): # lgtm[py/not-named-self]