diff --git a/README.md b/README.md index a2e9353..48f9108 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # CoinGecko API wrapper +[![PyPi Version](https://img.shields.io/pypi/v/pycoingecko.svg)](https://pypi.python.org/pypi/pycoingecko/) Python3 wrapper around the [CoinGecko](https://www.coingecko.com/) API (V3) @@ -23,7 +24,9 @@ cg = CoinGeckoAPI() ### Examples The required parameters for each endpoint are defined as required (mandatory) parameters for the coresponding functions. -Optional parameters can be also passed using same names, as defined in CoinGecko API doc (https://www.coingecko.com/api/docs/v3) +**Any optional parameters** can be also passed using same names, as defined in CoinGecko API doc (https://www.coingecko.com/api/docs/v3) + +*Lists are also supported as input for multiple-valued comma-separated parameters (e.g. see /simple/price usage examples).* Usage examples: ```python @@ -31,9 +34,13 @@ Usage examples: >>> cg.get_price(ids='bitcoin', vs_currencies='usd') {'bitcoin': {'usd': 3462.04}} +>>> cg.get_price(ids='bitcoin,litecoin,ethereum', vs_currencies='usd') +# OR (lists can be used for multiple-valued arguments) >>> cg.get_price(ids=['bitcoin', 'litecoin', 'ethereum'], vs_currencies='usd') {'bitcoin': {'usd': 3461.27}, 'ethereum': {'usd': 106.92}, 'litecoin': {'usd': 32.72}} +>>> cg.get_price(ids='bitcoin,litecoin,ethereum', vs_currencies='usd,eur') +# OR (lists can be used for multiple-valued arguments) >>> cg.get_price(ids=['bitcoin', 'litecoin', 'ethereum'], vs_currencies=['usd', 'eur']) {'bitcoin': {'usd': 3459.39, 'eur': 3019.33}, 'ethereum': {'usd': 106.91, 'eur': 93.31}, 'litecoin': {'usd': 32.72, 'eur': 28.56}} diff --git a/pycoingecko/api.py b/pycoingecko/api.py index dfe642f..f603c29 100644 --- a/pycoingecko/api.py +++ b/pycoingecko/api.py @@ -4,8 +4,7 @@ from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry -from .utils import get_comma_separated_values - +from .utils import list_args_to_comma_separated class CoinGeckoAPI: @@ -24,10 +23,14 @@ def __request(self, url): #print(url) try: response = self.session.get(url, timeout = self.request_timeout) - response.raise_for_status() content = json.loads(response.content.decode('utf-8')) + response.raise_for_status() return content except Exception as e: + try: + raise ValueError(content) + except UnboundLocalError as e: + pass raise @@ -49,22 +52,29 @@ def ping(self): #---------- SIMPLE ----------# + @list_args_to_comma_separated def get_price(self, ids, vs_currencies, **kwargs): """Get the current price of any cryptocurrencies in any other supported currencies that you need""" - kwargs['ids'] = get_comma_separated_values(ids) - kwargs['vs_currencies'] = get_comma_separated_values(vs_currencies) + ids=ids.replace(' ','') + kwargs['ids'] = ids + vs_currencies=vs_currencies.replace(' ','') + kwargs['vs_currencies'] = vs_currencies api_url = '{0}simple/price'.format(self.api_base_url) api_url = self.__api_url_params(api_url, kwargs) return self.__request(api_url) + + @list_args_to_comma_separated def get_token_price(self, id, contract_addresses, vs_currencies, **kwargs): """Get the current price of any tokens on this coin (ETH only at this stage as per api docs) in any other supported currencies that you need""" - kwargs['contract_addresses'] = get_comma_separated_values(contract_addresses) - kwargs['vs_currencies'] = get_comma_separated_values(vs_currencies) + contract_addresses=contract_addresses.replace(' ','') + kwargs['contract_addresses'] = contract_addresses + vs_currencies=vs_currencies.replace(' ','') + kwargs['vs_currencies'] = vs_currencies api_url = '{0}simple/token_price/{1}'.format(self.api_base_url, id) api_url = self.__api_url_params(api_url, kwargs) @@ -79,6 +89,7 @@ def get_supported_vs_currencies(self): #---------- COINS ----------# + @list_args_to_comma_separated def get_coins(self, **kwargs): """List all coins with data (name, price, market, developer, community, etc)""" @@ -97,6 +108,7 @@ def get_coins_list(self): return self.__request(api_url) + @list_args_to_comma_separated def get_coins_markets(self, vs_currency, **kwargs): """List all supported coins price, market cap, volume, and market related data (no pagination required)""" @@ -108,6 +120,7 @@ def get_coins_markets(self, vs_currency, **kwargs): return self.__request(api_url) + @list_args_to_comma_separated def get_coin_by_id(self, id, **kwargs): """Get current data (name, price, market, ... including exchange tickers) for a coin""" @@ -117,6 +130,7 @@ def get_coin_by_id(self, id, **kwargs): return self.__request(api_url) + @list_args_to_comma_separated def get_coin_ticker_by_id(self, id, **kwargs): """Get coin tickers (paginated to 100 items)""" @@ -126,6 +140,7 @@ def get_coin_ticker_by_id(self, id, **kwargs): return self.__request(api_url) + @list_args_to_comma_separated def get_coin_history_by_id(self, id, date, **kwargs): """Get historical data (name, price, market, stats) at a given date for a coin""" @@ -137,6 +152,7 @@ def get_coin_history_by_id(self, id, date, **kwargs): return self.__request(api_url) + @list_args_to_comma_separated def get_coin_market_chart_by_id(self, id, vs_currency, days): """Get historical market data include price, market cap, and 24h volume (granularity auto)""" @@ -145,6 +161,7 @@ def get_coin_market_chart_by_id(self, id, vs_currency, days): return self.__request(api_url) + @list_args_to_comma_separated def get_coin_status_updates_by_id(self, id, **kwargs): """Get status updates for a given coin""" @@ -154,6 +171,7 @@ def get_coin_status_updates_by_id(self, id, **kwargs): return self.__request(api_url) + @list_args_to_comma_separated def get_coin_info_from_contract_address_by_id(self, id, contract_address): """Get coin info from contract address""" @@ -179,6 +197,7 @@ def get_exchanges_id_name_list(self): return self.__request(api_url) + @list_args_to_comma_separated def get_exchanges_by_id(self, id): """Get exchange volume in BTC and tickers""" @@ -187,6 +206,7 @@ def get_exchanges_by_id(self, id): return self.__request(api_url) + @list_args_to_comma_separated def get_exchanges_tickers_by_id(self, id, **kwargs): """Get exchange tickers (paginated)""" @@ -196,6 +216,7 @@ def get_exchanges_tickers_by_id(self, id, **kwargs): return self.__request(api_url) + @list_args_to_comma_separated def get_exchanges_status_updates_by_id(self, id, **kwargs): """Get status updates for a given exchange""" @@ -206,6 +227,7 @@ def get_exchanges_status_updates_by_id(self, id, **kwargs): #---------- STATUS UPDATES ----------# + @list_args_to_comma_separated def get_status_updates(self, **kwargs): """List all status_updates with data (description, category, created_at, user, user_title and pin)""" @@ -216,6 +238,7 @@ def get_status_updates(self, **kwargs): #---------- EVENTS ----------# + @list_args_to_comma_separated def get_events(self, **kwargs): """Get events, paginated by 100""" diff --git a/pycoingecko/utils.py b/pycoingecko/utils.py index babaaec..93cbd06 100644 --- a/pycoingecko/utils.py +++ b/pycoingecko/utils.py @@ -1,3 +1,15 @@ +def list_args_to_comma_separated(func): + """Return function that converts list input arguments to comma-separated strings""" + def input_args(*args,**kwargs): + for v in kwargs: + # check in **kwargs for lists and convert to comma-separated string + if isinstance(kwargs[v], list): kwargs[v]=','.join(kwargs[v]) + # check in *args for lists and convert to comma-separated string + args=[','.join(v) if isinstance(v, list) else v for v in args] + return func(*args, **kwargs) + return input_args + + def get_comma_separated_values(values): """Return the values as a comma-separated string""" @@ -5,3 +17,4 @@ def get_comma_separated_values(values): if not isinstance(values, list) and not isinstance(values, tuple): values = [values] return ','.join(values) + diff --git a/setup.py b/setup.py index dbc4194..7acf0fc 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setuptools.setup( name='pycoingecko', - version='0.2.0', + version='0.3.0', packages=['pycoingecko',], license='MIT', description = 'Python wrapper around the CoinGecko API', diff --git a/tests/test_api.py b/tests/test_api.py index ba4cac5..bba531d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -32,6 +32,118 @@ def test_ping(self): ## Assert assert response == ping_json + + #---------- SIMPLE ----------# + + #---------- /simple/price ----------# + @responses.activate + def test_get_price(self): + # Arrange + coins_json_sample = {"bitcoin": {"usd": 7984.89}} + + responses.add(responses.GET, 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd', + json = coins_json_sample, status = 200) + + # Act + response = CoinGeckoAPI().get_price('bitcoin', 'usd') + + ## Assert + assert response == coins_json_sample + + @responses.activate + def test_failed_get_price(self): + # Arrange + responses.add(responses.GET, 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd', + status = 404) + exception = HTTPError("HTTP Error") + + # Act Assert + with pytest.raises(HTTPError) as HE: + CoinGeckoAPI().get_price('bitcoin', 'usd') + + #---------- /simple/token_price/{id} ----------# + @responses.activate + def test_get_token_price(self): + # Arrange + coins_json_sample = {'0xB8c77482e45F1F44dE1745F52C74426C631bDD52': {'bnb': 1.0, 'bnb_market_cap': 144443301.0, 'bnb_24h_vol': 17983938.686249834, 'last_updated_at': 1558704332}} + + responses.add(responses.GET, 'https://api.coingecko.com/api/v3/simple/token_price/ethereum?include_market_cap=true&include_24hr_vol=true&include_last_updated_at=true&contract_addresses=0xB8c77482e45F1F44dE1745F52C74426C631bDD52&vs_currencies=bnb', + json = coins_json_sample, status = 200) + + # Act + response = CoinGeckoAPI().get_token_price('ethereum', '0xB8c77482e45F1F44dE1745F52C74426C631bDD52', 'bnb', include_market_cap='true', include_24hr_vol='true', include_last_updated_at='true') + + ## Assert + assert response == coins_json_sample + + @responses.activate + def test_failed_get_token_price(self): + # Arrange + responses.add(responses.GET, 'https://api.coingecko.com/api/v3/simple/token_price/ethereum?include_market_cap=true&include_24hr_vol=true&include_last_updated_at=true&contract_addresses=0xB8c77482e45F1F44dE1745F52C74426C631bDD52&vs_currencies=bnb', + status = 404) + exception = HTTPError("HTTP Error") + + # Act Assert + with pytest.raises(HTTPError) as HE: + CoinGeckoAPI().get_token_price('ethereum', '0xB8c77482e45F1F44dE1745F52C74426C631bDD52', 'bnb', include_market_cap='true', include_24hr_vol='true', include_last_updated_at='true') + + #---------- /simple/supported_vs_currencies ----------# + @responses.activate + def test_get_supported_vs_currencies(self): + # Arrange + coins_json_sample = ['btc', 'eth', 'ltc', 'bch', 'bnb', 'eos', 'xrp', 'xlm', 'usd', 'aed', 'ars', 'aud', 'bdt', 'bhd', 'bmd', 'brl', 'cad', 'chf', 'clp', 'cny', 'czk', 'dkk', 'eur', 'gbp', 'hkd', 'huf', 'idr', 'ils', 'inr', 'jpy', 'krw', 'kwd', 'lkr', 'mmk', 'mxn', 'myr', 'nok', 'nzd', 'php', 'pkr', 'pln', 'rub', 'sar', 'sek', 'sgd', 'thb', 'try', 'twd', 'uah', 'vef', 'vnd', 'zar', 'xdr', 'xag', 'xau'] + + responses.add(responses.GET, 'https://api.coingecko.com/api/v3/simple/supported_vs_currencies', + json = coins_json_sample, status = 200) + + # Act + response = CoinGeckoAPI().get_supported_vs_currencies() + + ## Assert + assert response == coins_json_sample + + @responses.activate + def test_failed_get_supported_vs_currencies(self): + # Arrange + responses.add(responses.GET, 'https://api.coingecko.com/api/v3/simple/supported_vs_currencies', + status = 404) + exception = HTTPError("HTTP Error") + + # Act Assert + with pytest.raises(HTTPError) as HE: + CoinGeckoAPI().get_supported_vs_currencies() + + + #---------- /simple/supported_vs_currencies ----------# + @responses.activate + def test_get_supported_vs_currencies(self): + # Arrange + coins_json_sample = ['btc', 'eth', 'ltc', 'bch', 'bnb', 'eos', 'xrp', 'xlm', 'usd', 'aed', 'ars', 'aud', 'bdt', 'bhd', 'bmd', 'brl', 'cad', 'chf', 'clp', 'cny', 'czk', 'dkk', 'eur', 'gbp', 'hkd', 'huf', 'idr', 'ils', 'inr', 'jpy', 'krw', 'kwd', 'lkr', 'mmk', 'mxn', 'myr', 'nok', 'nzd', 'php', 'pkr', 'pln', 'rub', 'sar', 'sek', 'sgd', 'thb', 'try', 'twd', 'uah', 'vef', 'vnd', 'zar', 'xdr', 'xag', 'xau'] + + responses.add(responses.GET, 'https://api.coingecko.com/api/v3/simple/supported_vs_currencies', + json = coins_json_sample, status = 200) + + # Act + response = CoinGeckoAPI().get_supported_vs_currencies() + + ## Assert + assert response == coins_json_sample + + @responses.activate + def test_failed_get_supported_vs_currencies(self): + # Arrange + responses.add(responses.GET, 'https://api.coingecko.com/api/v3/simple/supported_vs_currencies', + status = 404) + exception = HTTPError("HTTP Error") + + # Act Assert + with pytest.raises(HTTPError) as HE: + CoinGeckoAPI().get_supported_vs_currencies() + + + #---------- COINS ----------# + + #---------- /price/coins ----------# @responses.activate def test_failed_get_coins(self): # Arrange @@ -57,6 +169,8 @@ def test_get_coins(self): ## Assert assert response == coins_json_sample + + #---------- /price/coins/list ----------# @responses.activate def test_failed_get_coins_list(self): # Arrange @@ -82,6 +196,8 @@ def test_get_coins_list(self): ## Assert assert response == coins_json_sample + + #---------- /price/coins/markets ----------# @responses.activate def test_failed_get_coins_markets(self): # Arrange @@ -107,6 +223,8 @@ def test_get_coins_markets(self): ## Assert assert response == markets_json_sample + + #---------- /price/coins/{id} ----------# @responses.activate def test_failed_get_coin_by_id(self): # Arrange @@ -133,6 +251,36 @@ def test_get_coin_by_id(self): ## Assert assert response == bitcoin_json_sample + + #---------- /price/coins/{id}/tickers ----------# + @responses.activate + def test_failed_get_coin_ticker_by_id(self): + # Arrange + responses.add(responses.GET, 'https://api.coingecko.com/api/v3/coins/bitcoin/tickers', + status = 404) + exception = HTTPError("HTTP Error") + + # Act Assert + with pytest.raises(HTTPError) as HE: + CoinGeckoAPI().get_coin_ticker_by_id('bitcoin') + + + @responses.activate + def test_get_get_coin_ticker_by_id(self): + # Arrange + bitcoin_json_sample = {'name': 'Bitcoin', 'tickers': [{'base': 'BTC', 'target': 'USDT', 'market': {'name': 'BW.com', 'identifier': 'bw', 'has_trading_incentive': False}, 'last': 7963.0, ' volume': 93428.7568, 'converted_last': {'btc': 0.99993976, 'eth': 31.711347, 'usd': 7979.23}, 'converted_volume': {'btc': 93423, 'eth': 2962752, 'usd': 745489919}, ' bid_ask_spread_percentage': 0.111969, 'timestamp': '2019-05-24T11:20:14+00:00', 'is_anomaly': False, 'is_stale': False, 'trade_url': 'https://www.bw.com/trade/btc_us dt', 'coin_id': 'bitcoin'}]} + + responses.add(responses.GET, 'https://api.coingecko.com/api/v3/coins/bitcoin/tickers', + json = bitcoin_json_sample, status = 200) + + # Act + response = CoinGeckoAPI().get_coin_ticker_by_id('bitcoin') + + ## Assert + assert response == bitcoin_json_sample + + + #---------- /price/coins/{id}/history ----------# @responses.activate def test_failed_get_coin_history_by_id(self): # Arrange @@ -159,6 +307,8 @@ def test_get_coin_history_by_id(self): ## Assert assert response == history_json_sample + + #---------- /price/coins/{id}/market_chart ----------# @responses.activate def test_failed_get_coin_market_chart_by_id(self): # Arrange @@ -168,7 +318,7 @@ def test_failed_get_coin_market_chart_by_id(self): # Act Assert with pytest.raises(HTTPError) as HE: - CoinGeckoAPI().get_coin_market_chart_by_id('bitcoin', 'usd', 0) + CoinGeckoAPI().get_coin_market_chart_by_id('bitcoin', 'usd', 1) @responses.activate @@ -180,11 +330,72 @@ def test_get_coin_market_chart_by_id(self): json = json_response, status = 200) # Act - response = CoinGeckoAPI().get_coin_market_chart_by_id('bitcoin', 'usd', 0) + response = CoinGeckoAPI().get_coin_market_chart_by_id('bitcoin', 'usd', 1) ## Assert assert response == json_response + + #---------- /price/coins/{id}/status_updates ----------# + @responses.activate + def test_failed_get_coin_status_updates_by_id(self): + # Arrange + responses.add(responses.GET, 'https://api.coingecko.com/api/v3/coins/litecoin/status_updates', + status = 404) + exception = HTTPError("HTTP Error") + + # Act Assert + with pytest.raises(HTTPError) as HE: + CoinGeckoAPI().get_coin_status_updates_by_id('litecoin') + + + @responses.activate + def test_get_coin_status_updates_by_id(self): + # Arrange + json_response = [ {'description': 'Travala.com Partners with Litecoin Foundation to Champion Crypto Payments. \r\n#TravelWithLitecoin www.travala.com/litecoin\r\n\r\nRead the full announcement here: bit.ly/2LumY3b', 'category': 'general', 'created_at': '2019-05-14T13:56:43.282Z', 'user': 'Keith Yong', 'user_title': 'Operations Director', 'pin': False, 'project': {'type': 'Coin', 'id': 'litecoin', 'name': 'Litecoin', 'symbol': 'ltc', 'image': {'thumb': 'https://assets.coingecko.com/coins/images/2/thumb/litecoin.png?1547033580', 'small': 'https://assets.coingecko.com/coins/images/2/small/litecoin.png?1547033580', 'large': 'https://assets.coingecko.com/coins/images/2/large/litecoin.png?1547033580'}}} ] + + responses.add(responses.GET, 'https://api.coingecko.com/api/v3/coins/litecoin/status_updates', + json = json_response, status = 200) + + # Act + response = CoinGeckoAPI().get_coin_status_updates_by_id('litecoin') + + ## Assert + assert response == json_response + + + #---------- /price/coins/{id}/contract/{contract_address} ----------# + @responses.activate + def test_failed_get_coin_info_from_contract_address_by_id(self): + # Arrange + responses.add(responses.GET, 'https://api.coingecko.com/api/v3/coins/ethereum/contract/0xe41d2489571d322189246dafa5ebde1f4699f498', + status = 404) + exception = HTTPError("HTTP Error") + + # Act Assert + with pytest.raises(HTTPError) as HE: + CoinGeckoAPI().get_coin_info_from_contract_address_by_id(id='ethereum',contract_address='0xe41d2489571d322189246dafa5ebde1f4699f498') + + + @responses.activate + def test_get_coin_info_from_contract_address_by_id(self): + # Arrange + json_response = {'id': '0x', 'symbol': 'zrx', 'name': '0x', 'block_time_in_minutes': 0, 'categories': ['Protocol'], 'localization': {'en': '0x', 'es': '0x', 'de': '0x', 'nl': '0x', 'pt': '0x', 'fr': '0x', 'it': '0x', 'hu': '0x', 'ro': '0x', 'sv': '0x', 'pl': '0x', 'id': '0x', 'zh': '0x协议', 'zh-tw': '0x協議', 'ja': 'ロエックス', 'ko': '제로엑스', 'ru': '0x', 'ar': '0x', 'th': '0x', 'vi': '0x', 'tr': '0x'}} + + responses.add(responses.GET, 'https://api.coingecko.com/api/v3/coins/ethereum/contract/0xe41d2489571d322189246dafa5ebde1f4699f498', + json = json_response, status = 200) + + # Act + response = CoinGeckoAPI().get_coin_info_from_contract_address_by_id(id='ethereum',contract_address='0xe41d2489571d322189246dafa5ebde1f4699f498') + + ## Assert + assert response == json_response + + + #---------- EXCHANGES ----------# + + + #---------- /exchanges ----------# @responses.activate def test_failed_get_exchanges_list(self): # Arrange @@ -211,6 +422,37 @@ def test_get_exchanges_list(self): ## Assert assert response == json_response + + #---------- /exchanges/list ----------# + @responses.activate + def test_failed_get_exchanges_id_name_list(self): + # Arrange + responses.add(responses.GET, 'https://api.coingecko.com/api/v3/exchanges/list', + status = 404) + exception = HTTPError("HTTP Error") + + # Act Assert + with pytest.raises(HTTPError) as HE: + CoinGeckoAPI().get_exchanges_id_name_list() + + + @responses.activate + def test_get_exchanges_id_name_list(self): + # Arrange + json_response = [{'id': 'abcc', 'name': 'ABCC'}, {'id': 'acx', 'name': 'ACX'}, {'id': 'airswap', 'name': 'AirSwap'}] + + responses.add(responses.GET, 'https://api.coingecko.com/api/v3/exchanges/list', + json = json_response, status = 200) + + # Act + response = CoinGeckoAPI().get_exchanges_id_name_list() + + ## Assert + assert response == json_response + + + + #---------- /exchanges/{id} ----------# @responses.activate def test_failed_get_exchanges_by_id(self): # Arrange @@ -237,6 +479,10 @@ def test_get_exchanges_by_id(self): ## Assert assert response == json_response + + #---------- EXCHANGE RATES ----------# + + #---------- /exchange_rates ----------# @responses.activate def test_failed_get_exchange_rates(self): # Arrange @@ -263,6 +509,10 @@ def test_get_exchange_rates(self): ## Assert assert response == json_response + + #---------- GLOBAL ----------# + + #---------- /global ----------# @responses.activate def test_failed_get_global(self): # Arrange