From 014b666a6e6716a666bc09c785c5d58d377de04b Mon Sep 17 00:00:00 2001 From: Cameron Yick Date: Fri, 25 Aug 2017 08:00:51 -0400 Subject: [PATCH] Cleanup Docstrings, Tests, and Todos --- setup.py | 2 +- tests/test_tiingo.py | 12 ++++++- tiingo/__init__.py | 7 ++-- tiingo/api.py | 84 +++++++++++++++++++++++++++++--------------- tiingo/restclient.py | 12 +++++-- 5 files changed, 78 insertions(+), 39 deletions(-) diff --git a/setup.py b/setup.py index 5c5d3d6..e024a64 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ requirements = [ setup_requirements = [ 'pytest-runner', - # TODO(hydrosquall): put setup requirements (distutils extensions, etc.) here + # TODO: put setup requirements (distutils extensions, etc.) here ] test_requirements = [ diff --git a/tests/test_tiingo.py b/tests/test_tiingo.py index 51cac85..0b88166 100644 --- a/tests/test_tiingo.py +++ b/tests/test_tiingo.py @@ -1,11 +1,21 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - """Tests for `tiingo` package.""" + import pytest from tiingo import TiingoClient +# TODO +# Add tests for +# Invalid API key +# Invalid ticker, etc +# Use unittest asserts rather than regular asserts +# Wrap server errors with client side descriptive errors +# Coerce startDate/endDate to string if they are passed in as datetime +# Use VCR.py to enable offline testing + +# Refactor fixtures into separate file @pytest.fixture def ticker_price_response(): """Test /tiingo//prices endpoint""" diff --git a/tiingo/__init__.py b/tiingo/__init__.py index 987f69c..d9f8da4 100644 --- a/tiingo/__init__.py +++ b/tiingo/__init__.py @@ -1,9 +1,6 @@ # -*- coding: utf-8 -*- - -"""Top-level package for Tiingo Python.""" +from tiingo.api import TiingoClient __author__ = """Cameron Yick""" __email__ = 'cameron.yick@enigma.com' -__version__ = '0.1.0' - -from tiingo.api import TiingoClient +__version__ = '0.1.1' diff --git a/tiingo/api.py b/tiingo/api.py index 77646c4..7b55990 100644 --- a/tiingo/api.py +++ b/tiingo/api.py @@ -1,14 +1,17 @@ # -*- coding: utf-8 -*- import os +import pkg_resources from tiingo.restclient import RestClient +VERSION = pkg_resources.get_distribution("tiingo").version + class TiingoClient(RestClient): - """Class for managing interactions with the Tiingo Platform + """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): @@ -24,7 +27,7 @@ class TiingoClient(RestClient): self._headers = { 'Authorization': "Token {}".format(api_key), 'Content-Type': 'application/json', - 'User-Agent': 'tiingo-python-client' + 'User-Agent': 'tiingo-python-client {}'.format(VERSION) } def __repr__(self): @@ -32,31 +35,40 @@ class TiingoClient(RestClient): # TICKER PRICE ENDPOINTS # https://api.tiingo.com/docs/tiingo/daily + def list_tickers(self): + """Return a list of all supported tickers. + https://apimedia.tiingo.com/docs/tiingo/daily/supported_tickers.zip + """ + raise NotImplementedError + def get_ticker_metadata(self, ticker): - """Return metadata for 1 ticker. + """Return metadata for 1 ticker + Use TiingoClient.list_tickers() to get available options + + Args: + ticker (str) : Unique identifier for stock """ url = "tiingo/daily/{}".format(ticker) response = self._request('GET', url) return response.json() - 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. - Each feed on average contains 3 data sources. + 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. Supported tickers + Available Day Ranges are here: - https://apimedia.tiingo.com/docs/tiingo/daily/supported_tickers.zip + 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 """ url = "tiingo/daily/{}/prices".format(ticker) - params = { 'format': fmt, 'frequency': frequency @@ -67,29 +79,38 @@ class TiingoClient(RestClient): if 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) - return response.json() + if fmt == "json": + return response.json() + else: + return response.content # FUND DATA (From over 26,000 mutual funds) # https://api.tiingo.com/docs/tiingo/funds # TODO: Validate the models returned by the fund def get_fund_metadata(self, fund): """Return metadata for 1 mutual fund / ETF + + Args: + fund (string): Unique identifier for fund/ETF """ url = "tiingo/funds/{}".format(fund) response = self._request('GET', url) return response.json() def get_fund_metrics(self, fund, startDate=None, endDate=None): - """Return metrics about a fund. By default, return latest metrics. + """Return metrics about a fund. If no date provided, + return latest metrics. Args: + fund (string): Unique identifier for fund/ETF startDate (string): Start of fund range in YYYY-MM-DD format endDate (string): End of fund range in YYYY-MM-DD format - fmt (string): 'csv' or 'json' - frequency (string): Resample frequency """ url = "tiingo/funds/{}/metrics".format(fund) params = {} + if startDate: params['startDate'] = startDate if endDate: @@ -99,32 +120,37 @@ class TiingoClient(RestClient): return response.json() # NEWS FEEDS + # tiingo/news def get_news(self, tickers=[], tags=[], sources=[], startDate=None, endDate=None, limit=100, offset=0, sortBy="publishedDate"): - """Return metrics about a fund. By default, return latest metrics. + """Return list of news articles matching given search terms + + https://api.tiingo.com/docs/tiingo/news + Args: - startDate (string): Start of fund range in YYYY-MM-DD format - endDate (string): End of fund range in YYYY-MM-DD format - fmt (string): 'csv' or 'json' - frequency (string): Resample frequency + 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 (#TODO: UPDATE THIS) """ - # Stub: - # https://api.tiingo.com/docs/tiingo/news - # "Finish later" + # params = {} + # if tickers: + # tickers = ",".join(tickers) raise NotImplementedError def get_bulk_news(self, file_id=None): """Only available to institutional clients. - If no ID is provided, return array of available ids. + 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. """ - # Stub: - # https://api.tiingo.com/docs/tiingo/news - # "Finish later" if file_id: url = "tiingo/news/bulk_download" else: url = "tiingo/news/bulk_download{}".format(file_id) + response = self._request('GET', url) return response.json() diff --git a/tiingo/restclient.py b/tiingo/restclient.py index fde4584..55c6d4b 100644 --- a/tiingo/restclient.py +++ b/tiingo/restclient.py @@ -3,6 +3,7 @@ import requests from requests.exceptions import HTTPError +# TODO: Possibly print HTTP json response if available? class RestClientError(Exception): "Wrapper around HTTP Errors" pass @@ -12,11 +13,10 @@ class RestClient(object): def __init__(self, config={}): """Base class for interacting with RESTful APIs - Child class MUST have a ._base_url property! Args: - config (dict): Arbitrary configuration options + config (dict): Arbitrary options that child classes can access """ self._config = config @@ -29,7 +29,13 @@ class RestClient(object): return ''.format(self._base_url) def _request(self, method, url, **kwargs): - """Make HTTP request and return response object""" + """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 + """ resp = self._session.request(method, '{}/{}'.format(self._base_url, url), headers=self._headers,