From 447564660263c9a3226819d78b6e23a6df2bed99 Mon Sep 17 00:00:00 2001 From: Vladislav Pavlov Date: Wed, 26 Feb 2025 18:24:38 +0700 Subject: [PATCH 1/2] feat(async): add asynchronous CoinGeckoAPI implementation - Updated func_args_preprocessing decorator in utils.py to support async methods. - Implemented AsyncCoinGeckoAPI class with asynchronous functionality mirroring CoinGeckoAPI. - Added mock tests for all AsyncCoinGeckoAPI methods. - Imported AsyncCoinGeckoAPI in __init__.py. --- pycoingecko/__init__.py | 1 + pycoingecko/api_async.py | 838 ++++++++++++++++ pycoingecko/utils.py | 42 +- tests/test_api_async.py | 2009 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 2871 insertions(+), 19 deletions(-) create mode 100644 pycoingecko/api_async.py create mode 100644 tests/test_api_async.py diff --git a/pycoingecko/__init__.py b/pycoingecko/__init__.py index ecce009..8594d84 100644 --- a/pycoingecko/__init__.py +++ b/pycoingecko/__init__.py @@ -1,2 +1,3 @@ from .api import CoinGeckoAPI +from .api_async import AsyncCoinGeckoAPI from .version import __version__ diff --git a/pycoingecko/api_async.py b/pycoingecko/api_async.py new file mode 100644 index 0000000..0efc50d --- /dev/null +++ b/pycoingecko/api_async.py @@ -0,0 +1,838 @@ +import aiohttp +import orjson +from aiohttp import ClientTimeout +from aiohttp_retry import ExponentialRetry, RetryClient + +from .utils import func_args_preprocessing + + +class AsyncCoinGeckoAPI: + __API_URL_BASE = "https://api.coingecko.com/api/v3/" + __PRO_API_URL_BASE = "https://pro-api.coingecko.com/api/v3/" + + def __init__( + self, + api_key: str = "", + retries: int = 5, + demo_api_key: str = "", + ) -> None: + self.retries: int = retries + self.extra_params: dict | None = None + self.api_base_url: str = self.__API_URL_BASE + + if api_key: + self.api_base_url = self.__PRO_API_URL_BASE + self.extra_params = {"x_cg_pro_api_key": api_key} + elif demo_api_key: + self.extra_params = {"x_cg_demo_api_key": demo_api_key} + + self.request_timeout = ClientTimeout(total=120) + self.session: RetryClient | None = None + + async def __aenter__(self) -> "AsyncCoinGeckoAPI": + if self.session is None: + retry_options = ExponentialRetry( + attempts=self.retries, + start_timeout=0.5, + statuses={502, 503, 504}, + ) + self.session = RetryClient( + raise_for_status=False, + timeout=self.request_timeout, + retry_options=retry_options, + ) + return self + + async def __aexit__(self, exc_type, exc, tb) -> None: + if self.session is not None: + await self.session.close() + self.session = None + + @classmethod + async def create( + cls, + api_key: str = "", + retries: int = 5, + demo_api_key: str = "", + ) -> "AsyncCoinGeckoAPI": + instance = cls(api_key, retries, demo_api_key) + retry_options = ExponentialRetry( + attempts=retries, + start_timeout=0.5, + statuses={502, 503, 504}, + ) + instance.session = RetryClient( + raise_for_status=False, + timeout=instance.request_timeout, + retry_options=retry_options, + ) + return instance + + async def close(self) -> None: + if self.session is not None: + await self.session.close() + + async def __request(self, url: str, params: dict) -> dict: + # if using pro or demo version of CoinGecko with api key, inject key in every call + if self.extra_params is not None: + params.update(self.extra_params) + + try: + async with self.session.get( + url, + params=params, + timeout=self.request_timeout, + ) as response: + response.raise_for_status() + + data = await response.read() + + try: + content = orjson.loads(data) + return content + except orjson.JSONDecodeError: + raise ValueError(f"Invalid JSON: {data}") + except aiohttp.ClientResponseError as e: + raise ValueError(f"Response error: {e.status} {e.message}") from e + except aiohttp.ClientError as e: + raise ValueError(f"Client error: {str(e)}") from e + + @staticmethod + def __build_params(required: dict, optional: dict, extra: dict) -> dict: + params = {} + params.update(required) + params.update({k: v for k, v in optional.items() if v is not None}) + params.update({k: v for k, v in extra.items() if v is not None}) + return params + + async def ping(self, **kwargs: dict) -> dict: + """Check API server status""" + url = f"{self.api_base_url}ping" + return await self.__request(url, params=kwargs) + + async def key(self, **kwargs: dict) -> dict: + """Monitor your account's API usage, including rate limits, monthly total credits, remaining credits, and more""" + url = f"{self.api_base_url}key" + return await self.__request(url, params=kwargs) + + # region Simple + @func_args_preprocessing + async def get_price( + self, + ids: str, + vs_currencies: str, + include_market_cap: bool = None, + include_24hr_vol: bool = None, + include_24hr_change: bool = None, + include_last_updated_at: bool = None, + precision: str = None, + **kwargs: dict, + ) -> dict: + """Get the current price of any cryptocurrencies in any other supported currencies that you need""" + url = f"{self.api_base_url}simple/price" + required = { + "ids": ids.replace(" ", ""), + "vs_currencies": vs_currencies.replace(" ", ""), + } + optional = { + "include_market_cap": include_market_cap, + "include_24hr_vol": include_24hr_vol, + "include_24hr_change": include_24hr_change, + "include_last_updated_at": include_last_updated_at, + "precision": precision, + } + params = self.__build_params(required, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_token_price( + self, + id: str, + contract_addresses: str, + vs_currencies: str, + include_market_cap: bool = None, + include_24hr_vol: bool = None, + include_24hr_change: bool = None, + include_last_updated_at: bool = None, + precision: str = None, + **kwargs: dict, + ) -> dict: + """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""" + url = f"{self.api_base_url}simple/token_price/{id}" + required = { + "contract_addresses": contract_addresses.replace(" ", ""), + "vs_currencies": vs_currencies.replace(" ", ""), + } + optional = { + "include_market_cap": include_market_cap, + "include_24hr_vol": include_24hr_vol, + "include_24hr_change": include_24hr_change, + "include_last_updated_at": include_last_updated_at, + "precision": precision, + } + params = self.__build_params(required, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_supported_vs_currencies(self, **kwargs: dict) -> dict: + """Get list of supported_vs_currencies""" + url = f"{self.api_base_url}simple/supported_vs_currencies" + return await self.__request(url, params=kwargs) + + # endregion + + # region Coins + @func_args_preprocessing + async def get_coins( + self, + include_platform: bool = None, + status: bool = None, + **kwargs: dict, + ) -> dict: + """List all coins with data (name, price, market, developer, community, etc)""" + url = f"{self.api_base_url}coins" + optional = { + "include_platform": include_platform, + "status": status, + } + params = self.__build_params({}, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_coin_top_gainers_losers( + self, + vs_currency: str, + duration: str = None, + top_coins: str = None, + **kwargs: dict, + ) -> dict: + """Get top gainers and losers""" + url = f"{self.api_base_url}coins/top_gainers_losers" + required = {"vs_currency": vs_currency} + optional = { + "duration": duration, + "top_coins": top_coins, + } + params = self.__build_params(required, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_coins_list_new(self, **kwargs: dict) -> dict: + """This endpoint allows you to query the latest 200 coins that recently listed on CoinGecko""" + url = f"{self.api_base_url}coins/list/new" + return await self.__request(url, params=kwargs) + + @func_args_preprocessing + async def get_coins_list(self, **kwargs: dict) -> dict: + """List all supported coins id, name and symbol (no pagination required)""" + url = f"{self.api_base_url}coins/list" + return await self.__request(url, params=kwargs) + + @func_args_preprocessing + async def get_coins_markets( + self, + vs_currency: str, + ids: str = None, + category: str = None, + order: str = None, + per_page: int = None, + page: int = None, + sparkline: bool = None, + price_change_percentage: str = None, + locale: str = None, + precision: str = None, + **kwargs: dict, + ) -> dict: + """List all supported coins price, market cap, volume, and market related data""" + url = f"{self.api_base_url}coins/markets" + required = {"vs_currency": vs_currency} + optional = { + "ids": ids, + "category": category, + "order": order, + "per_page": per_page, + "page": page, + "sparkline": sparkline, + "price_change_percentage": price_change_percentage, + "locale": locale, + "precision": precision, + } + params = self.__build_params(required, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_coin_by_id( + self, + id: str, + localization: bool = None, + tickers: bool = None, + market_data: bool = None, + community_data: bool = None, + developer_data: bool = None, + sparkline: bool = None, + **kwargs: dict, + ) -> dict: + """Get current data (name, price, market, ... including exchange tickers) for a coin""" + url = f"{self.api_base_url}coins/{id}" + optional = { + "localization": localization, + "tickers": tickers, + "market_data": market_data, + "community_data": community_data, + "developer_data": developer_data, + "sparkline": sparkline, + } + params = self.__build_params({}, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_coin_ticker_by_id( + self, + id: str, + exchange_ids: str = None, + include_exchange_logo: bool = None, + page: int = None, + order: str = None, + depth: bool = None, + **kwargs: dict, + ) -> dict: + """Get coin tickers (paginated to 100 items)""" + url = f"{self.api_base_url}coins/{id}/tickers" + optional = { + "exchange_ids": exchange_ids, + "include_exchange_logo": include_exchange_logo, + "page": page, + "order": order, + "depth": depth, + } + params = self.__build_params({}, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_coin_history_by_id( + self, + id: str, + date: str, + localization: bool = None, + **kwargs: dict, + ) -> dict: + """Get historical data (name, price, market, stats) at a given date for a coin""" + url = f"{self.api_base_url}coins/{id}/history" + required = {"date": date} + optional = {"localization": localization} + params = self.__build_params(required, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_coin_market_chart_by_id( + self, + id: str, + vs_currency: str, + days: int, + interval: str = None, + precision: str = None, + **kwargs: dict, + ) -> dict: + """Get historical market data include price, market cap, and 24h volume (granularity auto)""" + url = f"{self.api_base_url}coins/{id}/market_chart" + required = {"vs_currency": vs_currency, "days": days} + optional = {"interval": interval, "precision": precision} + params = self.__build_params(required, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_coin_market_chart_range_by_id( + self, + id: str, + vs_currency: str, + from_timestamp: int, + to_timestamp: int, + interval: str = None, + precision: str = None, + **kwargs: dict, + ) -> dict: + """Get historical market data include price, market cap, and 24h volume within a range of timestamp (granularity auto)""" + url = f"{self.api_base_url}coins/{id}/market_chart/range" + required = { + "vs_currency": vs_currency, + "from": from_timestamp, + "to": to_timestamp, + } + optional = {"interval": interval, "precision": precision} + params = self.__build_params(required, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_coin_ohlc_by_id( + self, + id: str, + vs_currency: str, + days: int, + interval: str = None, + precision: str = None, + **kwargs: dict, + ) -> dict: + """Get coin's OHLC""" + url = f"{self.api_base_url}coins/{id}/ohlc" + required = {"vs_currency": vs_currency, "days": days} + optional = {"interval": interval, "precision": precision} + params = self.__build_params(required, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_coin_ohlc_by_id_range( + self, + id: str, + vs_currency: str, + from_timestamp: int, + to_timestamp: int, + interval: str, + **kwargs: dict, + ) -> dict: + """Get coin's OHLC within a range of timestamp""" + url = f"{self.api_base_url}coins/{id}/ohlc/range" + required = { + "vs_currency": vs_currency, + "from": from_timestamp, + "to": to_timestamp, + "interval": interval, + } + params = self.__build_params(required, {}, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_coin_circulating_supply_chart( + self, + id: str, + days: int, + interval: str = None, + **kwargs: dict, + ) -> dict: + """Get coin's circulating supply chart""" + url = f"{self.api_base_url}coins/{id}/circulating_supply_chart" + required = {"days": days} + optional = {"interval": interval} + params = self.__build_params(required, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_coin_circulating_supply_chart_range( + self, + id: str, + from_timestamp: int, + to_timestamp: int, + **kwargs: dict, + ) -> dict: + """Get coin's circulating supply chart within a range of timestamp""" + url = f"{self.api_base_url}coins/{id}/circulating_supply_chart/range" + required = {"from": from_timestamp, "to": to_timestamp} + params = self.__build_params(required, {}, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_coin_total_supply_chart( + self, + id: str, + days: int, + interval: str = None, + **kwargs: dict, + ) -> dict: + """Get coin's total supply chart""" + url = f"{self.api_base_url}coins/{id}/total_supply_chart" + required = {"days": days} + optional = {"interval": interval} + params = self.__build_params(required, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_coin_total_supply_chart_range( + self, + id: str, + from_timestamp: int, + to_timestamp: int, + **kwargs: dict, + ) -> dict: + """Get coin's total supply chart within a range of timestamp""" + url = f"{self.api_base_url}coins/{id}/total_supply_chart/range" + required = {"from": from_timestamp, "to": to_timestamp} + params = self.__build_params(required, {}, kwargs) + return await self.__request(url, params=params) + + # endregion + + # region Contract + @func_args_preprocessing + async def get_coin_info_from_contract_address_by_id( + self, + id: str, + contract_address: str, + **kwargs: dict, + ) -> dict: + """Get coin info from contract address""" + url = f"{self.api_base_url}coins/{id}/contract/{contract_address}" + return await self.__request(url, kwargs) + + @func_args_preprocessing + async def get_coin_market_chart_from_contract_address_by_id( + self, + id: str, + contract_address: str, + vs_currency: str, + days: str, + interval: str = None, + precision: str = None, + **kwargs: dict, + ) -> dict: + """Get historical market data include price, market cap, and 24h volume (granularity auto) from a contract address""" + url = f"{self.api_base_url}coins/{id}/contract/{contract_address}/market_chart" + required = {"vs_currency": vs_currency, "days": days} + optional = {"interval": interval, "precision": precision} + params = self.__build_params(required, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_coin_market_chart_range_from_contract_address_by_id( + self, + id: str, + contract_address: str, + vs_currency: str, + from_timestamp: int, + to_timestamp: int, + interval: str = None, + precision: str = None, + **kwargs: dict, + ) -> dict: + """Get historical market data include price, market cap, and 24h volume within a range of timestamp (granularity auto) from a contract address""" + url = f"{self.api_base_url}coins/{id}/contract/{contract_address}/market_chart/range" + required = { + "vs_currency": vs_currency, + "from": from_timestamp, + "to": to_timestamp, + } + optional = {"interval": interval, "precision": precision} + params = self.__build_params(required, optional, kwargs) + return await self.__request(url, params=params) + + # endregion + + # region Asset Platforms + @func_args_preprocessing + async def get_asset_platforms(self, filter: str = None, **kwargs: dict) -> dict: + """List all asset platforms (Blockchain networks)""" + url = f"{self.api_base_url}asset_platforms" + optional = {"filter": filter} + params = self.__build_params({}, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_asset_platform_by_id( + self, + asset_platform_id: str, + **kwargs: dict, + ) -> dict: + """List all asset platforms (Blockchain networks) by platform id""" + url = f"{self.api_base_url}token_lists/{asset_platform_id}/all.json" + return await self.__request(url, kwargs) + + # endregion + + # region Categories + @func_args_preprocessing + async def get_coins_categories_list(self, **kwargs: dict) -> dict: + """List all categories""" + url = f"{self.api_base_url}coins/categories/list" + return await self.__request(url, kwargs) + + @func_args_preprocessing + async def get_coins_categories(self, order: str = None, **kwargs: dict) -> dict: + """List all categories with market data""" + url = f"{self.api_base_url}coins/categories" + optional = {"order": order} + params = self.__build_params({}, optional, kwargs) + return await self.__request(url, params=params) + + # endregion + + # region Exchanges + @func_args_preprocessing + async def get_exchanges_list( + self, + per_page: int = None, + page: int = None, + **kwargs: dict, + ) -> dict: + """List all exchanges""" + url = f"{self.api_base_url}exchanges" + optional = {"per_page": per_page, "page": page} + params = self.__build_params({}, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_exchanges_id_name_list( + self, + status: str = None, + **kwargs: dict, + ) -> dict: + """List all supported markets id and name (no pagination required)""" + url = f"{self.api_base_url}exchanges/list" + optional = {"status": status} + params = self.__build_params({}, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_exchanges_by_id(self, id: str, **kwargs: dict) -> dict: + """Get exchange volume in BTC and tickers""" + url = f"{self.api_base_url}exchanges/{id}" + return await self.__request(url, kwargs) + + @func_args_preprocessing + async def get_exchanges_tickers_by_id( + self, + id: str, + coin_ids: str = None, + include_exchange_logo: bool = None, + page: int = None, + depth: bool = None, + order: str = None, + **kwargs: dict, + ) -> dict: + """Get exchange tickers (paginated, 100 tickers per page)""" + url = f"{self.api_base_url}exchanges/{id}/tickers" + optional = { + "coin_ids": coin_ids, + "include_exchange_logo": include_exchange_logo, + "page": page, + "depth": depth, + "order": order, + } + params = self.__build_params({}, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_exchanges_volume_chart_by_id( + self, + id: str, + days: str, + **kwargs: dict, + ) -> dict: + """Get volume chart data for a given exchange""" + url = f"{self.api_base_url}exchanges/{id}/volume_chart" + required = {"days": days} + params = self.__build_params(required, {}, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_exchanges_volume_chart_by_id_within_time_range( + self, + id: str, + from_timestamp: int, + to_timestamp: int, + **kwargs: dict, + ) -> dict: + """Get volume chart data for a given exchange within a time range""" + url = f"{self.api_base_url}exchanges/{id}/volume_chart/range" + required = {"from": from_timestamp, "to": to_timestamp} + params = self.__build_params(required, {}, kwargs) + return await self.__request(url, params=params) + + # endregion + + # region Derivatives + @func_args_preprocessing + async def get_derivatives(self, **kwargs: dict) -> dict: + """List all derivative tickers""" + url = f"{self.api_base_url}derivatives" + return await self.__request(url, kwargs) + + @func_args_preprocessing + async def get_derivatives_exchanges( + self, + order: str = None, + per_page: int = None, + page: int = None, + **kwargs: dict, + ) -> dict: + """List all derivative tickers""" + url = f"{self.api_base_url}derivatives/exchanges" + optional = {"order": order, "per_page": per_page, "page": page} + params = self.__build_params({}, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_derivatives_exchanges_by_id( + self, + id: str, + include_tickers: str = None, + **kwargs: dict, + ) -> dict: + """List all derivative tickers""" + url = f"{self.api_base_url}derivatives/exchanges/{id}" + optional = {"include_tickers": include_tickers} + params = self.__build_params({}, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_derivatives_exchanges_list(self, **kwargs: dict) -> dict: + """List all derivative tickers""" + url = f"{self.api_base_url}derivatives/exchanges/list" + return await self.__request(url, kwargs) + + # endregion + + # region NFTS (BETA) + @func_args_preprocessing + async def get_nfts_list( + self, + order: str = None, + per_page: int = None, + page: int = None, + **kwargs: dict, + ) -> dict: + """List all supported NFT ids, paginated by 100 items per page, paginated to 100 items""" + url = f"{self.api_base_url}nfts/list" + optional = {"order": order, "per_page": per_page, "page": page} + params = self.__build_params({}, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_nfts_by_id(self, id: str, **kwargs: dict) -> dict: + """Get current data (name, price_floor, volume_24h ...) for an NFT collection. native_currency (string) is only a representative of the currency""" + url = f"{self.api_base_url}nfts/{id}" + return await self.__request(url, kwargs) + + @func_args_preprocessing + async def get_nfts_by_asset_platform_id_and_contract_address( + self, + asset_platform_id: str, + contract_address: str, + **kwargs: dict, + ) -> dict: + """Get current data (name, price_floor, volume_24h ...) for an NFT collection. native_currency (string) is only a representative of the currency""" + url = f"{self.api_base_url}nfts/{asset_platform_id}/contract/{contract_address}" + return await self.__request(url, kwargs) + + @func_args_preprocessing + async def get_nfts_markets( + self, + asset_platform_id: str = None, + order: str = None, + per_page: int = None, + page: int = None, + **kwargs: dict, + ) -> dict: + """This endpoint allows you to query all the supported NFT collections with floor price, market cap, volume and market related data on CoinGecko""" + url = f"{self.api_base_url}nfts/markets" + optional = { + "asset_platform_id": asset_platform_id, + "order": order, + "per_page": per_page, + "page": page, + } + params = self.__build_params({}, optional, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_nfts_market_chart_by_id( + self, + id: str, + days: str, + **kwargs: dict, + ) -> dict: + """This endpoint allows you query historical market data of a NFT collection, including floor price, market cap, and 24h volume, by number of days away from now""" + url = f"{self.api_base_url}nfts/{id}/market_chart" + required = {"days": days} + params = self.__build_params(required, {}, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_ntfs_market_chart_by_asset_platform_id_and_contract_address( + self, + asset_platform_id: str, + contract_address: str, + days: str, + **kwargs, + ) -> dict: + """This endpoint allows you query historical market data of a NFT collection, including floor price, market cap, and 24h volume, by number of days away from now based on the provided contract address""" + url = f"{self.api_base_url}nfts/{asset_platform_id}/contract/{contract_address}/market_chart" + required = {"days": days} + params = self.__build_params(required, {}, kwargs) + return await self.__request(url, params=params) + + @func_args_preprocessing + async def get_nfts_tickers_by_id(self, id: str, **kwargs: dict) -> dict: + """This endpoint allows you to query the latest floor price and 24h volume of a NFT collection, on each NFT marketplace, e.g. OpenSea and LooksRare""" + url = f"{self.api_base_url}nfts/{id}/tickers" + return await self.__request(url, kwargs) + + # endregion + + # region Exchange Rates + @func_args_preprocessing + async def get_exchange_rates(self, **kwargs: dict) -> dict: + """Get BTC-to-Currency exchange rates""" + url = f"{self.api_base_url}exchange_rates" + return await self.__request(url, kwargs) + + # endregion + + # region Search + @func_args_preprocessing + async def search(self, query: str, **kwargs: dict) -> dict: + """Search for coins, categories and markets on CoinGecko""" + url = f"{self.api_base_url}search" + required = {"query": query} + params = self.__build_params(required, {}, kwargs) + return await self.__request(url, params=params) + + # endregion + + # region Trending + @func_args_preprocessing + async def get_search_trending(self, **kwargs: dict) -> dict: + """Get top 7 trending coin searches""" + url = f"{self.api_base_url}search/trending" + return await self.__request(url, kwargs) + + # endregion + + # region Global + @func_args_preprocessing + async def get_global(self, **kwargs: dict) -> dict: + """Get cryptocurrency global data""" + url = f"{self.api_base_url}global" + return await self.__request(url, kwargs) + + @func_args_preprocessing + async def get_global_decentralized_finance_defi(self, **kwargs: dict) -> dict: + """Get cryptocurrency global decentralized finance(defi) data""" + url = f"{self.api_base_url}global/decentralized_finance_defi" + return await self.__request(url, kwargs) + + @func_args_preprocessing + async def get_global_market_cap_chart( + self, + days: str, + vs_currency: str = None, + **kwargs: dict, + ) -> dict: + """Get cryptocurrency global market cap chart data""" + url = f"{self.api_base_url}global/market_cap_chart" + required = {"days": days} + optional = {"vs_currency": vs_currency} + params = self.__build_params(required, optional, kwargs) + return await self.__request(url, params=params) + + # endregion + + # region Companies + @func_args_preprocessing + async def get_companies_public_treasury_by_coin_id( + self, + coin_id: str, + **kwargs: dict, + ) -> dict: + """Get public companies data""" + api_url = f"{self.api_base_url}companies/public_treasury/{coin_id}" + return await self.__request(api_url, kwargs) + + # endregion diff --git a/pycoingecko/utils.py b/pycoingecko/utils.py index 6a026ae..b12b1e8 100644 --- a/pycoingecko/utils.py +++ b/pycoingecko/utils.py @@ -1,34 +1,39 @@ +import asyncio from functools import wraps +from typing import Any, Callable, Union -def func_args_preprocessing(func): +def func_args_preprocessing(func: Callable) -> Callable: """Return function that converts list input arguments to comma-separated strings""" @wraps(func) - def input_args(*args, **kwargs): - - # check in **kwargs for lists and booleans - for v in kwargs: - kwargs[v] = arg_preprocessing(kwargs[v]) - # check in *args for lists and booleans - args = [arg_preprocessing(v) for v in args] + async def async_wrapper(*args, **kwargs): + args, kwargs = process_args(*args, **kwargs) + return await func(*args, **kwargs) + @wraps(func) + def sync_wrapper(*args, **kwargs): + args, kwargs = process_args(*args, **kwargs) return func(*args, **kwargs) - return input_args + return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper -def arg_preprocessing(arg_v): - """Return the values of an argument after preprocessing""" +def process_args(*args: tuple, **kwargs: dict) -> tuple: + processed_args = tuple(arg_preprocessing(arg) for arg in args) + processed_kwargs = {k: arg_preprocessing(v) for k, v in kwargs.items()} + return processed_args, processed_kwargs + +def arg_preprocessing(value: Any) -> Union[str, Any]: + """Return the values of an argument after preprocessing""" # check if arg is list and convert it to comma-separated string - if isinstance(arg_v, list): - arg_v = ','.join(arg_v) + if isinstance(value, list): + value = ",".join(map(str, value)) # check if arg is boolean and convert it to string - elif isinstance(arg_v, bool): - arg_v = str(arg_v).lower() - - return arg_v + elif isinstance(value, bool): + value = str(value).lower() + return value def get_comma_separated_values(values): @@ -38,5 +43,4 @@ def get_comma_separated_values(values): if not isinstance(values, list) and not isinstance(values, tuple): values = [values] - return ','.join(values) - + return ",".join(values) diff --git a/tests/test_api_async.py b/tests/test_api_async.py new file mode 100644 index 0000000..9acc8be --- /dev/null +++ b/tests/test_api_async.py @@ -0,0 +1,2009 @@ +import pytest # need: pytest-asyncio +from aioresponses import aioresponses + +from pycoingecko import AsyncCoinGeckoAPI + + +@pytest.fixture +def failed_request(): + async def _failed_request(url: str, api_method, *args, **kwargs): + # Use aioresponses for a failed response (e.g. 404 status) + with aioresponses() as m: + m.get(url, status=404) + async with AsyncCoinGeckoAPI() as api: + # Pass the api as the first argument of the method, since this is an instance method + with pytest.raises(Exception): + await api_method(api, *args, **kwargs) + # After leaving the context, the session should be closed (None) + assert api.session is None + + return _failed_request + + +@pytest.fixture +def success_request(): + async def _success_request( + url: str, + expected_payload: dict, + api_method, + *args, + **kwargs, + ): + # Use aioresponses for a successful response + with aioresponses() as m: + m.get(url, payload=expected_payload) + # Pass the api as the first argument of the method, since this is an instance method + async with AsyncCoinGeckoAPI() as api: + result = await api_method(api, *args, **kwargs) + assert result == expected_payload + return result + + return _success_request + + +@pytest.mark.asyncio +class TestAsyncCoinGeckoAPI: + async def test_create_instance(self): + # Check that the session is already initialized after creating an instance via create + api = await AsyncCoinGeckoAPI.create(api_key="dummy_key") + assert api.session is not None + await api.close() + + async def test_ping(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/ping", + "expected_payload": {"gecko_says": "(V3) To the Moon!"}, + "api_method": AsyncCoinGeckoAPI.ping, + } + await success_request(**data) + await failed_request(**data) + + async def test_key_success(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/key", + "expected_payload": {"gecko_says": "(V3) To the Moon!"}, + "api_method": AsyncCoinGeckoAPI.key, + } + await success_request(**data) + await failed_request(**data) + + # region Simple + # ---------- /simple/price ---------- # + async def test_get_price(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd", + "expected_payload": { + "bitcoin": {"usd": 7984.89}, + }, + "api_method": AsyncCoinGeckoAPI.get_price, + "ids": "bitcoin", + "vs_currencies": "usd", + } + await success_request(**data) + await failed_request(**data) + + # ---------- /simple/token_price/{id} ---------- # + async def test_get_token_price(self, success_request, failed_request): + data = { + "url": "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", + "expected_payload": { + "0xB8c77482e45F1F44dE1745F52C74426C631bDD52": { + "bnb": 1.0, + "bnb_market_cap": 144443301.0, + "bnb_24h_vol": 17983938.686249834, + "last_updated_at": 1558704332, + } + }, + "api_method": AsyncCoinGeckoAPI.get_token_price, + "id": "ethereum", + "contract_addresses": "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", + "vs_currencies": "bnb", + "include_market_cap": "true", + "include_24hr_vol": "true", + "include_last_updated_at": "true", + } + await success_request(**data) + await failed_request(**data) + + # ---------- /simple/supported_vs_currencies ---------- # + async def test_get_supported_vs_currencies(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/simple/supported_vs_currencies", + "expected_payload": ["btc", "eth", "ltc", "bch", "bnb", "eos", "xrp"], + "api_method": AsyncCoinGeckoAPI.get_supported_vs_currencies, + } + await success_request(**data) + await failed_request(**data) + + # endregion + + # region Coins + # ---------- /price/coins ---------- # + async def test_get_coins(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/coins", + "expected_payload": [ + { + "id": "bitcoin", + "symbol": "btc", + "name": "Bitcoin", + "localization": { + "en": "Bitcoin", + "es": "Bitcoin", + "de": "Bitcoin", + "nl": "Bitcoin", + "pt": "Bitcoin", + "fr": "Bitcoin", + "it": "Bitcoin", + "hu": "Bitcoin", + "ro": "Bitcoin", + "sv": "Bitcoin", + "pl": "Bitcoin", + "id": "Bitcoin", + "zh": "Bitcoin", + "zh-tw": "Bitcoin", + "ja": "Bitcoin", + "ko": "Bitcoin", + "ru": "Bitcoin", + "ar": "Bitcoin", + "th": "Bitcoin", + "vi": "Bitcoin", + "tr": "Bitcoin", + }, + "image": { + "thumb": "https://assets.coingecko.com/coins/images/1/thumb/bitcoin.png?1510040391", + "small": "https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1510040391", + "large": "https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1510040391", + }, + } + ], + "api_method": AsyncCoinGeckoAPI.get_coins, + } + await success_request(**data) + await failed_request(**data) + + # ---------- /coins/top_gainers_losers ---------- # + async def test_get_top_gainers_losers(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/coins/top_gainers_losers?vs_currency=usd", + "expected_payload": [ + { + "top_gainers": [ + { + "id": "bonk", + "symbol": "bonk", + "name": "Bonk", + "image": "https://assets.coingecko.com/coins/images/28600/original/bonk.jpg?1696527587", + "market_cap_rank": 75, + "usd": 0.000024645873833743, + "usd_24h_vol": 105344205.633894, + "usd_1y_change": 4244.15510979623, + }, + { + "id": "0x0-ai-ai-smart-contract", + "symbol": "0x0", + "name": "0x0.ai: AI Smart Contract", + "image": "https://assets.coingecko.com/coins/images/28880/original/0x0.png?1696527857", + "market_cap_rank": 235, + "usd": 0.388236182838391, + "usd_24h_vol": 1608196.56989005, + "usd_1y_change": 3688.24996780839, + }, + ] + } + ], + "api_method": AsyncCoinGeckoAPI.get_coin_top_gainers_losers, + "vs_currency": "usd", + } + await success_request(**data) + await failed_request(**data) + + # ---------- /coins/list/new ---------- # + async def test_get_new_coins(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/coins/list/new", + "expected_payload": [ + { + "id": "long-johnson", + "symbol": "olong", + "name": "Long Johnson", + "activated_at": 1712562430, + }, + { + "id": "dogita", + "symbol": "doga", + "name": "DOGITA", + "activated_at": 1712562282, + }, + { + "id": "bebe-on-base", + "symbol": "bebe", + "name": "Bebe on Base", + "activated_at": 1712561709, + }, + ], + "api_method": AsyncCoinGeckoAPI.get_coins_list_new, + } + await success_request(**data) + await failed_request(**data) + + # ---------- /coins/markets ---------- # + async def test_get_markets(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd", + "expected_payload": [ + { + "id": "bitcoin", + "symbol": "btc", + "name": "Bitcoin", + "image": "https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1696501400", + "current_price": 70187, + "market_cap": 1381651251183, + "market_cap_rank": 1, + "fully_diluted_valuation": 1474623675796, + "total_volume": 20154184933, + "high_24h": 70215, + "low_24h": 68060, + "price_change_24h": 2126.88, + "price_change_percentage_24h": 3.12502, + "market_cap_change_24h": 44287678051, + "market_cap_change_percentage_24h": 3.31157, + "circulating_supply": 19675987, + "total_supply": 21000000, + "max_supply": 21000000, + "ath": 73738, + "ath_change_percentage": -4.77063, + "ath_date": "2024-03-14T07:10:36.635Z", + "atl": 67.81, + "atl_change_percentage": 103455.83335, + "atl_date": "2013-07-06T00:00:00.000Z", + "roi": None, + "last_updated": "2024-04-07T16:49:31.736Z", + } + ], + "api_method": AsyncCoinGeckoAPI.get_coins_markets, + "vs_currency": "usd", + } + await success_request(**data) + await failed_request(**data) + + # ---------- /coins/{id} ---------- # + async def test_get_coin(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/coins/bitcoin", + "expected_payload": { + "id": "bitcoin", + "symbol": "btc", + "name": "Bitcoin", + "web_slug": "bitcoin", + "asset_platform_id": None, + "platforms": {"": ""}, + "detail_platforms": { + "": {"decimal_place": None, "contract_address": ""} + }, + "block_time_in_minutes": 10, + "hashing_algorithm": "SHA-256", + "categories": [ + "FTX Holdings", + "Cryptocurrency", + "Proof of Work (PoW)", + "Layer 1 (L1)", + ], + "preview_listing": False, + "public_notice": None, + "additional_notices": [], + "localization": {"en": "Bitcoin", "de": "Bitcoin"}, + "description": { + "en": "Bitcoin is the first successful internet money based on peer-to-peer technology....", + "de": "", + }, + }, + "api_method": AsyncCoinGeckoAPI.get_coin_by_id, + "id": "bitcoin", + } + await success_request(**data) + await failed_request(**data) + + # ---------- /coins/{id}/tickers ---------- # + async def test_get_coin_ticker_by_id(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/coins/bitcoin/tickers", + "expected_payload": { + "name": "Bitcoin", + "tickers": [ + { + "base": "BTC", + "target": "USDT", + "market": { + "name": "Binance", + "identifier": "binance", + "has_trading_incentive": False, + "logo": "https://assets.coingecko.com/markets/images/52/small/binance.jpg?1706864274", + }, + "last": 69476, + "volume": 20242.03975, + "cost_to_move_up_usd": 19320706.3958517, + "cost_to_move_down_usd": 16360235.3694131, + "converted_last": { + "btc": 1.000205, + "eth": 20.291404, + "usd": 69498, + }, + "converted_volume": { + "btc": 20249, + "eth": 410802, + "usd": 1406996874, + }, + "trust_score": "green", + "bid_ask_spread_percentage": 0.010014, + "timestamp": "2024-04-08T04:02:01+00:00", + "last_traded_at": "2024-04-08T04:02:01+00:00", + "last_fetch_at": "2024-04-08T04:03:00+00:00", + "is_anomaly": False, + "is_stale": False, + "trade_url": "https://www.binance.com/en/trade/BTC_USDT?ref=37754157", + "token_info_url": None, + "coin_id": "bitcoin", + "target_coin_id": "tether", + } + ], + }, + "api_method": AsyncCoinGeckoAPI.get_coin_ticker_by_id, + "id": "bitcoin", + } + await success_request(**data) + await failed_request(**data) + + # ---------- /coins/{id}/history ---------- # + async def test_get_coin_history_by_id(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/coins/bitcoin/history?date=30-12-2024", + "expected_payload": { + "id": "bitcoin", + "symbol": "btc", + "name": "Bitcoin", + "localization": { + "en": "Bitcoin", + "de": "Bitcoin", + "ru": "Биткоин", + "ja": "ビットコイン", + "zh": "比特币", + "zh-tw": "比特幣", + "ko": "비트코인", + "ar": "بيتكوين", + "th": "บิตคอยน์", + }, + "image": { + "thumb": "https://coin-images.coingecko.com/coins/images/1/thumb/bitcoin.png?1696501400", + "small": "https://coin-images.coingecko.com/coins/images/1/small/bitcoin.png?1696501400", + }, + "market_data": { + "current_price": { + "aed": 344025.187095508, + "sats": 100005313.304369, + }, + "market_cap": { + "aed": 6812934096770.4, + "sats": 1.98045112441611e15, + }, + "total_volume": { + "aed": 88391731699.4704, + "sats": 25694754784530.3, + }, + }, + "community_data": { + "facebook_likes": None, + "twitter_followers": None, + "reddit_average_posts_48h": 0, + "reddit_average_comments_48h": 0, + "reddit_subscribers": None, + "reddit_accounts_active_48h": 0, + }, + "developer_data": { + "forks": None, + "stars": None, + "subscribers": None, + "total_issues": None, + "closed_issues": None, + "pull_requests_merged": None, + "pull_request_contributors": None, + "code_additions_deletions_4_weeks": { + "additions": None, + "deletions": None, + }, + "commit_count_4_weeks": None, + }, + "public_interest_stats": {"alexa_rank": None, "bing_matches": None}, + }, + "api_method": AsyncCoinGeckoAPI.get_coin_history_by_id, + "id": "bitcoin", + "date": "30-12-2024", + } + await success_request(**data) + await failed_request(**data) + + # ---------- /coins/{id}/market_chart ---------- # + async def test_get_coin_market_chart_by_id(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart?vs_currency=usd&days=1", + "expected_payload": { + "prices": [ + [1740387682131, 95542.3087967139], + [1740388005959, 95716.6278476215], + [1740473761586, 89463.5815318958], + [1740473821000, 89463.2121291266], + ], + "market_caps": [ + [1740387682131, 1895161399620.51], + [1740388005959, 1896738269146.75], + [1740473761586, 1775110486653.52], + [1740473821000, 1774041210855.89], + ], + "total_volumes": [ + [1740387682131, 21192459268.1462], + [1740388005959, 21508808505.1849], + [1740473761586, 62805352653.1377], + [1740473821000, 62306902132.5355], + ], + }, + "api_method": AsyncCoinGeckoAPI.get_coin_market_chart_by_id, + "id": "bitcoin", + "vs_currency": "usd", + "days": 1, + } + await success_request(**data) + await failed_request(**data) + + # ---------- /coins/{id}/market_chart/range ---------- # + async def test_get_coin_market_chart_range_by_id( + self, success_request, failed_request + ): + data = { + "url": "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart/range?vs_currency=usd&from=1740472800&to=1740474890", + "expected_payload": { + "prices": [ + [1740472843952, 89872.88504560938], + [1740473189341, 89630.68888211073], + [1740473504372, 89526.33017478665], + [1740473761586, 89463.58153189576], + [1740474030896, 89396.73950666585], + [1740474333399, 89511.73260680553], + [1740474695714, 89393.31021356549], + ], + "market_caps": [ + [1740472843952, 1782885720126.8223], + [1740473189341, 1781968181147.7288], + [1740473504372, 1775110486653.5244], + [1740473761586, 1775110486653.5244], + [1740474030896, 1774041210855.8936], + [1740474333399, 1772339784127.5793], + [1740474695714, 1774155265278.5728], + ], + "total_volumes": [ + [1740472843952, 58156817087.720375], + [1740473189341, 61322511114.51894], + [1740473504372, 69350678582.10666], + [1740473761586, 62805352653.13765], + [1740474030896, 63921438401.71314], + [1740474333399, 65494105244.377045], + [1740474695714, 67371801211.92553], + ], + }, + "api_method": AsyncCoinGeckoAPI.get_coin_market_chart_range_by_id, + "id": "bitcoin", + "vs_currency": "usd", + "from_timestamp": 1740472800, + "to_timestamp": 1740474890, + } + await success_request(**data) + await failed_request(**data) + + # ---------- /coins/{id}/ohlc ---------- # + async def test_get_coin_ohlc_by_id(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/coins/bitcoin/ohlc?vs_currency=usd&days=1", + "expected_payload": [ + [ + [1740389400000, 95542.0, 95835.0, 95542.0, 95835.0], + [1740474000000, 89640.0, 89919.0, 89464.0, 89464.0], + ] + ], + "api_method": AsyncCoinGeckoAPI.get_coin_ohlc_by_id, + "id": "bitcoin", + "vs_currency": "usd", + "days": 1, + } + await success_request(**data) + await failed_request(**data) + + # ---------- coins/{id}/ohlc/range ---------- # + async def test_get_coin_ohlc_by_id_range(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/coins/bitcoin/ohlc/range?vs_currency=usd&from=1709251200&to=1709596800&interval=daily", + "expected_payload": [ + [1709395200000, 61942, 62211, 61721, 61845], + [1709409600000, 61828, 62139, 61726, 62139], + [1709424000000, 62171, 62210, 61821, 62068], + ], + "api_method": AsyncCoinGeckoAPI.get_coin_ohlc_by_id_range, + "id": "bitcoin", + "vs_currency": "usd", + "from_timestamp": 1709251200, + "to_timestamp": 1709596800, + "interval": "daily", + } + await success_request(**data) + await failed_request(**data) + + # ---------- coins/{id}/circulating_supply_chart ---------- # + async def test_get_coin_circulating_supply_chart( + self, success_request, failed_request + ): + data = { + "url": "https://api.coingecko.com/api/v3/coins/bitcoin/circulating_supply_chart?days=1", + "expected_payload": { + "circulating_supply": [ + [1712448000000, "19675268.0"], + [1712534400000, "19676337.0"], + [1712569704000, "19676725.0"], + ] + }, + "api_method": AsyncCoinGeckoAPI.get_coin_circulating_supply_chart, + "id": "bitcoin", + "days": "1", + } + await success_request(**data) + await failed_request(**data) + + # ---------- coins/{id}/circulating_supply_chart/range ---------- # + async def test_get_coin_circulating_supply_chart_range( + self, success_request, failed_request + ): + data = { + "url": "https://api.coingecko.com/api/v3/coins/bitcoin/circulating_supply_chart/range?from=1609459200&to=1640908800", + "expected_payload": { + "circulating_supply": [ + [1712448000000, "19675268.0"], + [1712534400000, "19676337.0"], + [1712569704000, "19676725.0"], + ] + }, + "api_method": AsyncCoinGeckoAPI.get_coin_circulating_supply_chart_range, + "id": "bitcoin", + "from_timestamp": 1609459200, + "to_timestamp": 1640908800, + } + await success_request(**data) + await failed_request(**data) + + # ---------- coins/{id}/total_supply_chart ---------- # + async def test_get_coin_total_supply_chart(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/coins/bitcoin/total_supply_chart?days=1", + "expected_payload": { + "total_supply": [ + [1712448000000, "21000000.0"], + [1712534400000, "21000000.0"], + [1712586776000, "21000000.0"], + ] + }, + "api_method": AsyncCoinGeckoAPI.get_coin_total_supply_chart, + "id": "bitcoin", + "days": "1", + } + await success_request(**data) + await failed_request(**data) + + # ---------- coins/{id}/total_supply_chart/range ---------- # + async def test_get_coin_total_supply_chart_range( + self, success_request, failed_request + ): + data = { + "url": "https://api.coingecko.com/api/v3/coins/bitcoin/total_supply_chart/range?from=1609459200&to=1640908800", + "expected_payload": { + "total_supply": [ + [1712448000000, "21000000.0"], + [1712534400000, "21000000.0"], + [1712586776000, "21000000.0"], + ] + }, + "api_method": AsyncCoinGeckoAPI.get_coin_total_supply_chart_range, + "id": "bitcoin", + "from_timestamp": 1609459200, + "to_timestamp": 1640908800, + } + await success_request(**data) + await failed_request(**data) + + # endregion + + # region Contract + # ---------- /coins/{id}/contract/{contract_address} ---------- # + async def test_get_coin_info_from_contract_address_by_id( + self, success_request, failed_request + ): + data = { + "url": "https://api.coingecko.com/api/v3/coins/ethereum/contract/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "expected_payload": { + "id": "usd-coin", + "symbol": "usdc", + "name": "USDC", + "web_slug": "usdc", + "asset_platform_id": "ethereum", + "platforms": { + "ethereum": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "polkadot": "1337", + "flow": "A.b19436aae4d94622.FiatToken", + "avalanche": "0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e", + "optimistic-ethereum": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "stellar": "USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", + "near-protocol": "17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1", + "hedera-hashgraph": "0.0.456858", + "zksync": "0x1d17cbcf0d6d143135ae902365d2e5e2a16538d4", + "tron": "TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8", + "celo": "0xceba9300f2b948710d2653dd7b07f33a8b32118c", + "arbitrum-one": "0xaf88d065e77c8cc2239327c5edb3a432268e5831", + "base": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + "polygon-pos": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "solana": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + }, + "detail_platforms": { + "ethereum": { + "decimal_place": 6, + "contract_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + }, + "solana": { + "decimal_place": 6, + "contract_address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + }, + }, + "block_time_in_minutes": 0, + "categories": ["Ronin Ecosystem", "ZkSync Ecosystem"], + "additional_notices": [], + "localization": {"en": "USDC", "ja": "USDコイン"}, + "description": { + "en": "USDC is a fully collateralized US dollar stablecoin...." + }, + "links": { + "homepage": ["https://www.circle.com/en/usdc"], + "whitepaper": "", + "blockchain_site": [ + "https://etherscan.io/token/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "https://explorer.energi.network/token/0xffd7510ca0a3279c7a5f50018a26c21d5bc1dbcf", + ], + "chat_url": ["https://discord.com/invite/buildoncircle"], + "announcement_url": [ + "https://medium.com/centre-blog", + "https://blog.circle.com/2018/09/26/introducing-usd-coin/", + ], + "twitter_screen_name": "circle", + "subreddit_url": "https://www.reddit.com", + "repos_url": { + "github": ["https://github.com/centrehq/centre-tokens"], + "bitbucket": [], + }, + }, + "image": { + "thumb": "https://assets.coingecko.com/coins/images/6319/thumb/usdc.png?1696506694", + "small": "https://assets.coingecko.com/coins/images/6319/small/usdc.png?1696506694", + "large": "https://assets.coingecko.com/coins/images/6319/large/usdc.png?1696506694", + }, + "country_origin": "US", + "contract_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "sentiment_votes_up_percentage": 33.33, + "sentiment_votes_down_percentage": 66.67, + "watchlist_portfolio_users": 126374, + "market_cap_rank": 7, + }, + "api_method": AsyncCoinGeckoAPI.get_coin_info_from_contract_address_by_id, + "id": "ethereum", + "contract_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + } + await success_request(**data) + await failed_request(**data) + + # ---------- /coins/{id}/contract/{contract_address}/market_chart ---------- # + async def test_get_coin_market_chart_from_contract_address_by_id( + self, success_request, failed_request + ): + data = { + "url": "https://api.coingecko.com/api/v3/coins/ethereum/contract/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48/market_chart?vs_currency=usd&days=1", + "expected_payload": { + "prices": [ + [1711843200000, 69702.3087473573], + [1711929600000, 71246.9514406015], + [1711983682000, 68887.7495158568], + ], + "market_caps": [ + [1711843200000, 1370247487960.09], + [1711929600000, 1401370211582.37], + [1711983682000, 1355701979725.16], + ], + "total_volumes": [ + [1711843200000, 16408802301.8374], + [1711929600000, 19723005998.215], + [1711983682000, 30137418199.6431], + ], + }, + "api_method": AsyncCoinGeckoAPI.get_coin_market_chart_from_contract_address_by_id, + "id": "ethereum", + "contract_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "vs_currency": "usd", + "days": "1", + } + await success_request(**data) + await failed_request(**data) + + # ---------- coins/{id}/contract/{contract_address}/market_chart/range ---------- # + async def test_get_coin_market_chart_range_from_contract_address_by_id( + self, success_request, failed_request + ): + data = { + "url": "https://api.coingecko.com/api/v3/coins/ethereum/contract/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48/market_chart/range?vs_currency=usd&from=1609459200&to=1640908800", + "expected_payload": { + "prices": [ + [1704067241331, 42261.0406175669], + [1704070847420, 42493.2764087546], + [1704074443652, 42654.0731066594], + ], + "market_caps": [ + [1704067241331, 827596236151.196], + [1704070847420, 831531023621.411], + [1704074443652, 835499399014.932], + ], + "total_volumes": [ + [1704067241331, 14305769170.9498], + [1704070847420, 14130205376.1709], + [1704074443652, 13697382902.2424], + ], + }, + "api_method": AsyncCoinGeckoAPI.get_coin_market_chart_range_from_contract_address_by_id, + "id": "ethereum", + "contract_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "vs_currency": "usd", + "from_timestamp": 1609459200, + "to_timestamp": 1640908800, + } + await success_request(**data) + await failed_request(**data) + + # endregion + + # region Asset Platforms + # ---------- asset_platforms ---------- # + async def test_get_asset_platforms(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/asset_platforms?filter=nft", + "expected_payload": [ + { + "id": "polygon-pos", + "chain_identifier": 137, + "name": "Polygon POS", + "shortname": "MATIC", + "native_coin_id": "matic-network", + "image": { + "thumb": "https://coin-images.coingecko.com/asset_platforms/images/15/thumb/polygon_pos.png?1706606645", + "small": "https://coin-images.coingecko.com/asset_platforms/images/15/small/polygon_pos.png?1706606645", + "large": "https://coin-images.coingecko.com/asset_platforms/images/15/large/polygon_pos.png?1706606645", + }, + } + ], + "api_method": AsyncCoinGeckoAPI.get_asset_platforms, + "filter": "nft", + } + await success_request(**data) + await failed_request(**data) + + # ---------- token_lists/{asset_platform_id}/all.json ---------- # + async def test_get_asset_platform_by_id(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/token_lists/ethereum/all.json", + "expected_payload": { + "name": "CoinGecko", + "logoURI": "https://static.coingecko.com/s/thumbnail-007177f3eca19695592f0b8b0eabbdae282b54154e1be912285c9034ea6cbaf2.png", + "keywords": ["defi"], + "timestamp": "2024-04-08T14:02:47.028+00:00", + "tokens": [ + { + "chainId": 1, + "address": "0xd2877702675e6ceb975b4a1dff9fb7baf4c91ea9", + "name": "Wrapped Terra Classic", + "symbol": "LUNC", + "decimals": 18, + "logoURI": "https://assets.coingecko.com/coins/images/13628/thumb/wluna.png?1696513376", + }, + { + "chainId": 1, + "address": "0x5bb29c33c4a3c29f56f8aca40b4db91d8a5fe2c5", + "name": "One Share", + "symbol": "ONS", + "decimals": 18, + "logoURI": "https://assets.coingecko.com/coins/images/13531/thumb/bss.a1671c75.png?1696513292", + }, + ], + }, + "api_method": AsyncCoinGeckoAPI.get_asset_platform_by_id, + "asset_platform_id": "ethereum", + } + await success_request(**data) + await failed_request(**data) + + # endregion + + # region Categories + # ---------- coins/categories/list ---------- # + async def test_get_coins_categories_list(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/coins/categories/list", + "expected_payload": [ + {"category_id": "aave-tokens", "name": "Aave Tokens"}, + {"category_id": "aaccount-abstraction", "name": "Account Abstraction"}, + ], + "api_method": AsyncCoinGeckoAPI.get_coins_categories_list, + } + await success_request(**data) + await failed_request(**data) + + # ---------- coins/categories ---------- # + async def test_get_coins_categories(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/coins/categories", + "expected_payload": [ + { + "id": "layer-1", + "name": "Layer 1 (L1)", + "market_cap": 2061406861196.14, + "market_cap_change_24h": -0.66091235190398, + "content": "", + "top_3_coins_id": ["bitcoin", "ethereum", "binancecoin"], + "top_3_coins": [ + "https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1696501400", + "https://assets.coingecko.com/coins/images/279/small/ethereum.png?1696501628", + "https://assets.coingecko.com/coins/images/825/small/bnb-icon2_2x.png?1696501970", + ], + "volume_24h": 61146432400.1739, + "updated_at": "2024-04-06T08:25:46.402Z", + } + ], + "api_method": AsyncCoinGeckoAPI.get_coins_categories, + } + await success_request(**data) + await failed_request(**data) + + # endregion + + # region Exchanges + # ---------- exchanges ---------- # + async def test_get_exchanges_list(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/exchanges", + "expected_payload": [ + { + "id": "bybit_spot", + "name": "Bybit", + "year_established": 2018, + "country": "British Virgin Islands", + "description": "Bybit is a cryptocurrency exchange that offers a professional platform featuring an ultra-fast matching engine, excellent customer service and multilingual community support for crypto traders of all levels...", + "url": "https://www.bybit.com", + "image": "https://assets.coingecko.com/markets/images/698/small/bybit_spot.png?1706864649", + "has_trading_incentive": False, + "trust_score": 10, + "trust_score_rank": 1, + "trade_volume_24h_btc": 51075.6271283852, + "trade_volume_24h_btc_normalized": 47765.5886637453, + }, + { + "id": "gdax", + "name": "Coinbase Exchange", + "year_established": 2012, + "country": "United States", + "description": "", + "url": "https://www.coinbase.com/", + "image": "https://assets.coingecko.com/markets/images/23/small/Coinbase_Coin_Primary.png?1706864258", + "has_trading_incentive": False, + "trust_score": 10, + "trust_score_rank": 2, + "trade_volume_24h_btc": 37443.7299607648, + "trade_volume_24h_btc_normalized": 37443.7299607648, + }, + ], + "api_method": AsyncCoinGeckoAPI.get_exchanges_list, + } + await success_request(**data) + await failed_request(**data) + + # ---------- exchanges/list ---------- # + async def test_get_exchanges_id_name_list(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/exchanges/list", + "expected_payload": [ + {"id": "10kswap-starknet-alpha", "name": "10KSwap"}, + {"id": "1bch", "name": "1BCH"}, + {"id": "3xcalibur", "name": "3xcalibur"}, + ], + "api_method": AsyncCoinGeckoAPI.get_exchanges_id_name_list, + } + await success_request(**data) + await failed_request(**data) + + # ---------- exchanges/{id} ---------- # + async def test_get_exchanges_by_id(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/exchanges/binance", + "expected_payload": { + "name": "Binance", + "year_established": 2017, + "country": "Cayman Islands", + "description": "", + "url": "https://www.binance.com/", + "image": "https://assets.coingecko.com/markets/images/52/small/binance.jpg?1706864274", + "facebook_url": "https://www.facebook.com/binanceexchange", + "reddit_url": "https://www.reddit.com/r/binance/", + "telegram_url": "", + "slack_url": "", + "other_url_1": "https://medium.com/binanceexchange", + "other_url_2": "https://steemit.com/@binanceexchange", + "twitter_handle": "binance", + "has_trading_incentive": False, + "centralized": True, + "public_notice": "", + "alert_notice": "", + "trust_score": 9, + "trust_score_rank": 6, + "trade_volume_24h_btc": 207319.133772613, + "trade_volume_24h_btc_normalized": 81673.2971244154, + "coins": 384, + "pairs": 1281, + "tickers": [ + { + "base": "BTC", + "target": "USDT", + "market": { + "name": "Binance", + "identifier": "binance", + "has_trading_incentive": False, + "logo": "https://assets.coingecko.com/markets/images/52/small/binance.jpg?1706864274", + }, + "last": 69476, + "volume": 20242.03975, + "cost_to_move_up_usd": 19320706.3958517, + "cost_to_move_down_usd": 16360235.3694131, + "converted_last": { + "btc": 1.000205, + "eth": 20.291404, + "usd": 69498, + }, + "converted_volume": { + "btc": 20249, + "eth": 410802, + "usd": 1406996874, + }, + "trust_score": "green", + "bid_ask_spread_percentage": 0.010014, + "timestamp": "2024-04-08T04:02:01+00:00", + "last_traded_at": "2024-04-08T04:02:01+00:00", + "last_fetch_at": "2024-04-08T04:03:00+00:00", + "is_anomaly": False, + "is_stale": False, + "trade_url": "https://www.binance.com/en/trade/BTC_USDT?ref=37754157", + "token_info_url": None, + "coin_id": "bitcoin", + "target_coin_id": "tether", + } + ], + }, + "api_method": AsyncCoinGeckoAPI.get_exchanges_by_id, + "id": "binance", + } + await success_request(**data) + await failed_request(**data) + + # ---------- exchanges/{id}/tickers ---------- # + async def test_get_exchanges_tickers_by_id(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/exchanges/binance/tickers", + "expected_payload": { + "name": "Bitcoin", + "tickers": [ + { + "base": "BTC", + "target": "USDT", + "market": { + "name": "Binance", + "identifier": "binance", + "has_trading_incentive": False, + "logo": "https://assets.coingecko.com/markets/images/52/small/binance.jpg?1706864274", + }, + "last": 69476, + "volume": 20242.03975, + "cost_to_move_up_usd": 19320706.3958517, + "cost_to_move_down_usd": 16360235.3694131, + "converted_last": { + "btc": 1.000205, + "eth": 20.291404, + "usd": 69498, + }, + "converted_volume": { + "btc": 20249, + "eth": 410802, + "usd": 1406996874, + }, + "trust_score": "green", + "bid_ask_spread_percentage": 0.010014, + "timestamp": "2024-04-08T04:02:01+00:00", + "last_traded_at": "2024-04-08T04:02:01+00:00", + "last_fetch_at": "2024-04-08T04:03:00+00:00", + "is_anomaly": False, + "is_stale": False, + "trade_url": "https://www.binance.com/en/trade/BTC_USDT?ref=37754157", + "token_info_url": None, + "coin_id": "bitcoin", + "target_coin_id": "tether", + } + ], + }, + "api_method": AsyncCoinGeckoAPI.get_exchanges_tickers_by_id, + "id": "binance", + } + await success_request(**data) + await failed_request(**data) + + # ---------- exchanges/{id}/volume_chart ---------- # + async def test_get_exchanges_volume_chart_by_id( + self, success_request, failed_request + ): + data = { + "url": "https://api.coingecko.com/api/v3/exchanges/binance/volume_chart?days=1", + "expected_payload": [ + [1711792200000, "306800.0517941023777005"], + [1711795800000, "302561.8185582217570913"], + [1711799400000, "298240.5127048246776691"], + ], + "api_method": AsyncCoinGeckoAPI.get_exchanges_volume_chart_by_id, + "id": "binance", + "days": "1", + } + await success_request(**data) + await failed_request(**data) + + # ---------- exchanges/{id}/volume_chart/range ---------- # + async def test_get_exchanges_volume_chart_by_id_within_time_range( + self, success_request, failed_request + ): + data = { + "url": "https://api.coingecko.com/api/v3/exchanges/binance/volume_chart/range?from=1672531200&to=1675123200", + "expected_payload": [ + [1711792200000, "306800.0517941023777005"], + [1711795800000, "302561.8185582217570913"], + [1711799400000, "298240.5127048246776691"], + ], + "api_method": AsyncCoinGeckoAPI.get_exchanges_volume_chart_by_id_within_time_range, + "id": "binance", + "from_timestamp": 1672531200, + "to_timestamp": 1675123200, + } + await success_request(**data) + await failed_request(**data) + + # endregion + + # region Derivatives + # ---------- derivatives ---------- # + async def test_get_derivatives(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/derivatives", + "expected_payload": [ + { + "market": "Deepcoin (Derivatives)", + "symbol": "ETHUSDT", + "index_id": "ETH", + "price": "3395.91", + "price_percentage_change_24h": 1.5274069068216, + "contract_type": "perpetual", + "index": 3393.5342, + "basis": -0.0523015571479482, + "spread": 0.01, + "funding_rate": -0.007182, + "open_interest": 9327998764.66, + "volume_24h": 392642535.232121, + "last_traded_at": 1712467658, + "expired_at": None, + }, + { + "market": "BYDFi (Futures)", + "symbol": "BTC-PERPUSDT", + "index_id": "BTC", + "price": "69434.1", + "price_percentage_change_24h": 2.04057930105749, + "contract_type": "perpetual", + "index": 69407.5, + "basis": -0.000576303273834822, + "spread": 0.01, + "funding_rate": 0.012, + "open_interest": 7690212057.6, + "volume_24h": 132888173.547, + "last_traded_at": 1712467920, + "expired_at": None, + }, + ], + "api_method": AsyncCoinGeckoAPI.get_derivatives, + } + await success_request(**data) + await failed_request(**data) + + # ---------- derivatives/exchange ---------- # + async def test_get_derivatives_exchanges(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/derivatives/exchanges", + "expected_payload": [ + { + "name": "Binance (Futures)", + "id": "binance_futures", + "open_interest_btc": 279958.61, + "trade_volume_24h_btc": "574366.94", + "number_of_perpetual_pairs": 330, + "number_of_futures_pairs": 44, + "image": "https://assets.coingecko.com/markets/images/466/small/binance_futures.jpg?1706864452", + "year_established": 2019, + "country": None, + "description": "", + "url": "https://www.binance.com/", + }, + { + "name": "Bitget Futures", + "id": "bitget_futures", + "open_interest_btc": 123267.93, + "trade_volume_24h_btc": "228027.47", + "number_of_perpetual_pairs": 254, + "number_of_futures_pairs": 0, + "image": "https://assets.coingecko.com/markets/images/591/small/2023-07-25_21.47.43.jpg?1706864543", + "year_established": None, + "country": None, + "description": "", + "url": "https://www.bitget.com/en/", + }, + ], + "api_method": AsyncCoinGeckoAPI.get_derivatives_exchanges, + } + await success_request(**data) + await failed_request(**data) + + # ---------- derivatives/exchanges/{id} ---------- # + async def test_get_derivatives_exchanges_by_id( + self, success_request, failed_request + ): + data = { + "url": "https://api.coingecko.com/api/v3/derivatives/exchanges/binance_futures", + "expected_payload": { + "name": "Binance (Futures)", + "open_interest_btc": 280210.26, + "trade_volume_24h_btc": "568502.31", + "number_of_perpetual_pairs": 330, + "number_of_futures_pairs": 44, + "image": "https://assets.coingecko.com/markets/images/466/small/binance_futures.jpg?1706864452", + "year_established": 2019, + "country": None, + "description": "", + "url": "https://www.binance.com/", + "tickers": { + "tickers": [ + { + "symbol": "1000BONKUSDT", + "base": "1000BONK", + "target": "USDT", + "trade_url": "https://www.binance.com/en/futuresng/1000BONKUSDT", + "contract_type": "perpetual", + "last": 0.023, + "h24_percentage_change": -0.811, + "index": 0.0229866, + "index_basis_percentage": -0.071, + "bid_ask_spread": 0.000217533173808922, + "funding_rate": 0.005, + "open_interest_usd": 28102263.9997715, + "h24_volume": 2679284723, + "converted_volume": { + "btc": "888.799603175094638929930629459045946", + "eth": "18029.8066338945133622149580216234476206402026327668", + "usd": "61648664.9602525617243462802989936852339753270611794", + }, + "converted_last": { + "btc": "0.000000331730179904099217651505502", + "eth": "0.0000067293358108303271067525726423602078742716", + "usd": "0.0230093742673322299700755918127159362875878", + }, + "last_traded": 1712550723, + "expired_at": None, + } + ] + }, + }, + "api_method": AsyncCoinGeckoAPI.get_derivatives_exchanges_by_id, + "id": "binance_futures", + } + await success_request(**data) + await failed_request(**data) + + # ---------- derivatives/exchanges/list ---------- # + async def test_get_derivatives_exchanges_list( + self, success_request, failed_request + ): + data = { + "url": "https://api.coingecko.com/api/v3/derivatives/exchanges/list", + "expected_payload": [ + {"id": "binance_futures", "name": "Binance (Futures)"}, + {"id": "bybit", "name": "Bybit (Futures)"}, + {"id": "deepcoin_derivatives", "name": "Deepcoin (Derivatives)"}, + ], + "api_method": AsyncCoinGeckoAPI.get_derivatives_exchanges_list, + } + await success_request(**data) + await failed_request(**data) + + # endregion + + # region NFTS (BETA) + # ---------- nfts/list ---------- # + async def test_get_nfts_list(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/nfts/list", + "expected_payload": [ + { + "id": "bored-ape-yacht-club", + "contract_address": "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", + "name": "Bored Ape Yacht Club", + "asset_platform_id": "ethereum", + "symbol": "BAYC", + }, + { + "id": "pudgy-penguins", + "contract_address": "0xBd3531dA5CF5857e7CfAA92426877b022e612cf8", + "name": "Pudgy Penguins", + "asset_platform_id": "ethereum", + "symbol": "PPG", + }, + ], + "api_method": AsyncCoinGeckoAPI.get_nfts_list, + } + await success_request(**data) + await failed_request(**data) + + # ---------- nfts/list ---------- # + async def test_get_nfts_by_id(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/nfts/pudgy-penguins", + "expected_payload": { + "id": "pudgy-penguins", + "contract_address": "0xBd3531dA5CF5857e7CfAA92426877b022e612cf8", + "asset_platform_id": "ethereum", + "name": "Pudgy Penguins", + "symbol": "PPG", + "image": { + "small": "https://coin-images.coingecko.com/nft_contracts/images/38/small/pudgy.jpg?1730778323", + "small_2x": "https://coin-images.coingecko.com/nft_contracts/images/38/small_2x/pudgy.jpg?1730778323", + }, + "banner_image": "https://coin-images.coingecko.com/nft_contracts/images/20/bored-ape-yacht-club-banner.png?1708416120", + "description": "Pudgy Penguins is a collection of 8,888 unique NFTs featuring cute cartoon penguins, which are generated from a collection of 150 different hand-drawn traits.", + "native_currency": "ethereum", + "native_currency_symbol": "ETH", + "market_cap_rank": 3, + "floor_price": {"native_currency": 12.5, "usd": 42317}, + "market_cap": {"native_currency": 111100, "usd": 376114941}, + "volume_24h": {"native_currency": 429.88, "usd": 1455314}, + "floor_price_in_usd_24h_percentage_change": 1.07067, + "floor_price_24h_percentage_change": { + "usd": 1.07067060717791, + "native_currency": 1.21457489878543, + }, + "market_cap_24h_percentage_change": { + "usd": 1.07067060717767, + "native_currency": -0.404858299595142, + }, + "volume_24h_percentage_change": { + "usd": -3.19833776698741, + "native_currency": -1.80185531390094, + }, + "number_of_unique_addresses": 4752, + "number_of_unique_addresses_24h_percentage_change": 0.08425, + "volume_in_usd_24h_percentage_change": -3.19834, + "total_supply": 8888, + "one_day_sales": 36, + "one_day_sales_24h_percentage_change": -2.7027027027027, + "one_day_average_sale_price": 11.9411943888889, + "one_day_average_sale_price_24h_percentage_change": 0.925870927379588, + "links": { + "homepage": "https://www.pudgypenguins.com/", + "twitter": "https://twitter.com/pudgypenguins", + "discord": "https://discord.gg/pudgypenguins", + }, + "floor_price_7d_percentage_change": { + "usd": -18.0014948262365, + "native_currency": -13.7931034482759, + }, + "floor_price_14d_percentage_change": { + "usd": -8.63235339431041, + "native_currency": -8.61905110022663, + }, + "floor_price_30d_percentage_change": { + "usd": -14.3765649314409, + "native_currency": -0.777901254167328, + }, + "floor_price_60d_percentage_change": { + "usd": 15.2779758703282, + "native_currency": -18.0327868852459, + }, + "floor_price_1y_percentage_change": { + "usd": 429.5685372855, + "native_currency": 196.208530805687, + }, + "explorers": [ + { + "name": "Etherscan", + "link": "https://etherscan.io/token/0xBd3531dA5CF5857e7CfAA92426877b022e612cf8", + }, + { + "name": "Ethplorer", + "link": "https://ethplorer.io/address/0xBd3531dA5CF5857e7CfAA92426877b022e612cf8", + }, + ], + "user_favorites_count": 3660, + "ath": {"native_currency": 22.9, "usd": 67535}, + "ath_change_percentage": { + "native_currency": -59.825327510917, + "usd": -64.3396788440525, + }, + "ath_date": { + "native_currency": "2024-02-17T09:25:05.056Z", + "usd": "2024-02-29T11:45:08.150Z", + }, + }, + "api_method": AsyncCoinGeckoAPI.get_nfts_by_id, + "id": "pudgy-penguins", + } + await success_request(**data) + await failed_request(**data) + + # ---------- nfts/{asset_platform_id}/contract/{contract_address} ---------- # + async def test_get_nfts_by_asset_platform_id_and_contract_address( + self, success_request, failed_request + ): + data = { + "url": "https://api.coingecko.com/api/v3/nfts/ethereum/contract/0xBd3531dA5CF5857e7CfAA92426877b022e612cf8", + "expected_payload": { + "id": "pudgy-penguins", + "contract_address": "0xBd3531dA5CF5857e7CfAA92426877b022e612cf8", + "asset_platform_id": "ethereum", + "name": "Pudgy Penguins", + "symbol": "PPG", + "image": { + "small": "https://coin-images.coingecko.com/nft_contracts/images/38/small/pudgy.jpg?1730778323", + "small_2x": "https://coin-images.coingecko.com/nft_contracts/images/38/small_2x/pudgy.jpg?1730778323", + }, + "banner_image": "https://coin-images.coingecko.com/nft_contracts/images/20/bored-ape-yacht-club-banner.png?1708416120", + "description": "Pudgy Penguins is a collection of 8,888 unique NFTs featuring cute cartoon penguins, which are generated from a collection of 150 different hand-drawn traits.", + "native_currency": "ethereum", + "native_currency_symbol": "ETH", + "market_cap_rank": 3, + "floor_price": {"native_currency": 12.5, "usd": 42317}, + "market_cap": {"native_currency": 111100, "usd": 376114941}, + "volume_24h": {"native_currency": 429.88, "usd": 1455314}, + "floor_price_in_usd_24h_percentage_change": 1.07067, + "floor_price_24h_percentage_change": { + "usd": 1.07067060717791, + "native_currency": 1.21457489878543, + }, + "market_cap_24h_percentage_change": { + "usd": 1.07067060717767, + "native_currency": -0.404858299595142, + }, + "volume_24h_percentage_change": { + "usd": -3.19833776698741, + "native_currency": -1.80185531390094, + }, + "number_of_unique_addresses": 4752, + "number_of_unique_addresses_24h_percentage_change": 0.08425, + "volume_in_usd_24h_percentage_change": -3.19834, + "total_supply": 8888, + "one_day_sales": 36, + "one_day_sales_24h_percentage_change": -2.7027027027027, + "one_day_average_sale_price": 11.9411943888889, + "one_day_average_sale_price_24h_percentage_change": 0.925870927379588, + "links": { + "homepage": "https://www.pudgypenguins.com/", + "twitter": "https://twitter.com/pudgypenguins", + "discord": "https://discord.gg/pudgypenguins", + }, + "floor_price_7d_percentage_change": { + "usd": -18.0014948262365, + "native_currency": -13.7931034482759, + }, + "floor_price_14d_percentage_change": { + "usd": -8.63235339431041, + "native_currency": -8.61905110022663, + }, + "floor_price_30d_percentage_change": { + "usd": -14.3765649314409, + "native_currency": -0.777901254167328, + }, + "floor_price_60d_percentage_change": { + "usd": 15.2779758703282, + "native_currency": -18.0327868852459, + }, + "floor_price_1y_percentage_change": { + "usd": 429.5685372855, + "native_currency": 196.208530805687, + }, + "explorers": [ + { + "name": "Etherscan", + "link": "https://etherscan.io/token/0xBd3531dA5CF5857e7CfAA92426877b022e612cf8", + }, + { + "name": "Ethplorer", + "link": "https://ethplorer.io/address/0xBd3531dA5CF5857e7CfAA92426877b022e612cf8", + }, + ], + "user_favorites_count": 3660, + "ath": {"native_currency": 22.9, "usd": 67535}, + "ath_change_percentage": { + "native_currency": -59.825327510917, + "usd": -64.3396788440525, + }, + "ath_date": { + "native_currency": "2024-02-17T09:25:05.056Z", + "usd": "2024-02-29T11:45:08.150Z", + }, + }, + "api_method": AsyncCoinGeckoAPI.get_nfts_by_asset_platform_id_and_contract_address, + "asset_platform_id": "ethereum", + "contract_address": "0xBd3531dA5CF5857e7CfAA92426877b022e612cf8", + } + await success_request(**data) + await failed_request(**data) + + # ---------- nfts/markets ---------- # + async def test_get_nfts_markets(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/nfts/markets", + "expected_payload": [ + { + "id": "pudgy-penguins", + "contract_address": "0xBd3531dA5CF5857e7CfAA92426877b022e612cf8", + "asset_platform_id": "ethereum", + "name": "Pudgy Penguins", + "symbol": "PPG", + "image": { + "small": "https://coin-images.coingecko.com/nft_contracts/images/38/small/pudgy.jpg?1730778323", + "small_2x": "https://coin-images.coingecko.com/nft_contracts/images/38/small_2x/pudgy.jpg?1730778323", + }, + "description": "Pudgy Penguins is a collection of 8,888 unique NFTs featuring cute cartoon penguins, which are generated from a collection of 150 different hand-drawn traits...", + "native_currency": "ethereum", + "native_currency_symbol": "ETH", + "market_cap_rank": 3, + "floor_price": {"native_currency": 12.17, "usd": 44360}, + "market_cap": {"native_currency": 108211, "usd": 394267328}, + "volume_24h": {"native_currency": 402.37, "usd": 1466028}, + "floor_price_in_usd_24h_percentage_change": 8.27604, + "floor_price_24h_percentage_change": { + "usd": 8.27603609555289, + "native_currency": 1.6277092654424, + }, + "market_cap_24h_percentage_change": { + "usd": 8.27603609555281, + "native_currency": 1.6277092654424, + }, + "volume_24h_percentage_change": { + "usd": 32.6876748821441, + "native_currency": 24.5404332508984, + }, + "number_of_unique_addresses": 4756, + "number_of_unique_addresses_24h_percentage_change": 0.10524, + "volume_in_usd_24h_percentage_change": 32.68767, + "total_supply": 8888, + "one_day_sales": 33, + "one_day_sales_24h_percentage_change": 22.2222222222222, + "one_day_average_sale_price": 12.1929990290909, + "one_day_average_sale_price_24h_percentage_change": 1.8967181143714, + } + ], + "api_method": AsyncCoinGeckoAPI.get_nfts_markets, + } + await success_request(**data) + await failed_request(**data) + + # ---------- nfts/{id}/market_chart ---------- # + async def test_get_nfts_market_chart_by_id(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/nfts/pudgy-penguins/market_chart?days=1", + "expected_payload": { + "floor_price_usd": [ + [1626912000000, 90.1675764653448], + [1626998400000, 97.3216055000018], + [1627084800000, 95.2719573507527], + ], + "floor_price_native": [ + [1626912000000, 0.045], + [1626998400000, 0.048], + [1627084800000, 0.045], + ], + "h24_volume_usd": [ + [1626912000000, 2860.11552548074], + [1626998400000, 2143.50836113754], + [1627084800000, 925.408279066978], + ], + "h24_volume_native": [ + [1626912000000, 1.4274], + [1626998400000, 1.0572], + [1627084800000, 0.4371], + ], + "market_cap_usd": [ + [1626912000000, 33281860.8681357], + [1626998400000, 38474832.8121067], + [1627084800000, 44827378.867466], + ], + "market_cap_native": [ + [1626912000000, 11012.23], + [1626998400000, 12709.84], + [1627084800000, 14220.8], + ], + }, + "api_method": AsyncCoinGeckoAPI.get_nfts_market_chart_by_id, + "id": "pudgy-penguins", + "days": "1", + } + await success_request(**data) + await failed_request(**data) + + # ---------- nfts/{asset_platform_id}/contract/{contract_address}/market_chart ---------- # + async def test_get_ntfs_market_chart_by_asset_platform_id_and_contract_address( + self, success_request, failed_request + ): + data = { + "url": "https://api.coingecko.com/api/v3/nfts/ethereum/contract/0xBd3531dA5CF5857e7CfAA92426877b022e612cf8/market_chart?days=1", + "expected_payload": { + "floor_price_usd": [ + [1626912000000, 90.1675764653448], + [1626998400000, 97.3216055000018], + [1627084800000, 95.2719573507527], + ], + "floor_price_native": [ + [1626912000000, 0.045], + [1626998400000, 0.048], + [1627084800000, 0.045], + ], + "h24_volume_usd": [ + [1626912000000, 2860.11552548074], + [1626998400000, 2143.50836113754], + [1627084800000, 925.408279066978], + ], + "h24_volume_native": [ + [1626912000000, 1.4274], + [1626998400000, 1.0572], + [1627084800000, 0.4371], + ], + "market_cap_usd": [ + [1626912000000, 33281860.8681357], + [1626998400000, 38474832.8121067], + [1627084800000, 44827378.867466], + ], + "market_cap_native": [ + [1626912000000, 11012.23], + [1626998400000, 12709.84], + [1627084800000, 14220.8], + ], + }, + "api_method": AsyncCoinGeckoAPI.get_ntfs_market_chart_by_asset_platform_id_and_contract_address, + "asset_platform_id": "ethereum", + "contract_address": "0xBd3531dA5CF5857e7CfAA92426877b022e612cf8", + "days": "1", + } + await success_request(**data) + await failed_request(**data) + + # ---------- nfts/{id}/tickers ---------- # + async def test_get_nfts_tickers_by_id(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/nfts/pudgy-penguins/tickers", + "expected_payload": { + "tickers": [ + { + "floor_price_in_native_currency": 44.21, + "h24_volume_in_native_currency": 0, + "native_currency": "ethereum", + "native_currency_symbol": "ETH", + "updated_at": "2024-04-08T15:36:00.225Z", + "nft_marketplace_id": "looksrare", + "name": "LooksRare", + "image": "https://assets.coingecko.com/nft_marketplaces/images/2/small/Looksrare.jpg?1686193414", + "nft_collection_url": "https://looksrare.org/collections/0xBd3531dA5CF5857e7CfAA92426877b022e612cf8?ref=9247712", + }, + { + "floor_price_in_native_currency": 12.17, + "h24_volume_in_native_currency": 402.37, + "native_currency": "ethereum", + "native_currency_symbol": "ETH", + "updated_at": "2024-04-08T12:28:11.797Z", + "nft_marketplace_id": "blur", + "name": "Blur", + "image": "https://assets.coingecko.com/nft_marketplaces/images/20/small/blur_logo.jpg?1690993708", + "nft_collection_url": "https://blur.io/collection/pudgypenguins", + }, + { + "floor_price_in_native_currency": 12.84, + "h24_volume_in_native_currency": 0, + "native_currency": "ethereum", + "native_currency_symbol": "ETH", + "updated_at": "2024-04-08T12:28:11.897Z", + "nft_marketplace_id": "opensea", + "name": "OpenSea", + "image": "https://assets.coingecko.com/nft_marketplaces/images/1/small/Opensea.png?1686193426", + "nft_collection_url": "https://opensea.io/collection/pudgypenguins", + }, + { + "floor_price_in_native_currency": 199, + "h24_volume_in_native_currency": 0, + "native_currency": "ethereum", + "native_currency_symbol": "ETH", + "updated_at": "2024-04-08T12:28:11.979Z", + "nft_marketplace_id": "x2y2", + "name": "X2Y2", + "image": "https://assets.coingecko.com/nft_marketplaces/images/21/small/Logo.png?1693192556", + "nft_collection_url": "https://x2y2.io/collection/0xBd3531dA5CF5857e7CfAA92426877b022e612cf8", + }, + ] + }, + "api_method": AsyncCoinGeckoAPI.get_nfts_tickers_by_id, + "id": "pudgy-penguins", + } + await success_request(**data) + await failed_request(**data) + + # endregion + + # region Exchange Rates + # ---------- exchange_rates ---------- # + async def test_get_exchange_rates(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/exchange_rates", + "expected_payload": { + "rates": { + "btc": { + "name": "Bitcoin", + "unit": "BTC", + "value": 1, + "type": "crypto", + }, + "eth": { + "name": "Ether", + "unit": "ETH", + "value": 20.656, + "type": "crypto", + }, + "ltc": { + "name": "Litecoin", + "unit": "LTC", + "value": 684.945, + "type": "crypto", + }, + "bch": { + "name": "Bitcoin Cash", + "unit": "BCH", + "value": 102.254, + "type": "crypto", + }, + "bnb": { + "name": "Binance Coin", + "unit": "BNB", + "value": 119.846, + "type": "crypto", + }, + } + }, + "api_method": AsyncCoinGeckoAPI.get_exchange_rates, + } + await success_request(**data) + await failed_request(**data) + + # endregion + + # region Search + # ---------- search ---------- # + async def test_search(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/search?query=eth", + "expected_payload": { + "coins": [ + { + "id": "ethereum", + "name": "Ethereum", + "api_symbol": "ethereum", + "symbol": "ETH", + "market_cap_rank": 2, + "thumb": "https://assets.coingecko.com/coins/images/279/thumb/ethereum.png", + "large": "https://assets.coingecko.com/coins/images/279/large/ethereum.png", + }, + { + "id": "ethereum-classic", + "name": "Ethereum Classic", + "api_symbol": "ethereum-classic", + "symbol": "ETC", + "market_cap_rank": 27, + "thumb": "https://assets.coingecko.com/coins/images/453/thumb/ethereum-classic-logo.png", + "large": "https://assets.coingecko.com/coins/images/453/large/ethereum-classic-logo.png", + }, + { + "id": "sweth", + "name": "Swell Ethereum", + "api_symbol": "sweth", + "symbol": "SWETH", + "market_cap_rank": 142, + "thumb": "https://assets.coingecko.com/coins/images/30326/thumb/_lB7zEtS_400x400.jpg", + "large": "https://assets.coingecko.com/coins/images/30326/large/_lB7zEtS_400x400.jpg", + }, + ], + "exchanges": [ + { + "id": "uniswap_v3", + "name": "Uniswap V3 (Ethereum)", + "market_type": "spot", + "thumb": "https://assets.coingecko.com/markets/images/665/thumb/uniswap-v3.png", + "large": "https://assets.coingecko.com/markets/images/665/large/uniswap-v3.png", + }, + { + "id": "uniswap_v2", + "name": "Uniswap V2 (Ethereum)", + "market_type": "spot", + "thumb": "https://assets.coingecko.com/markets/images/535/thumb/256x256_Black-1.png", + "large": "https://assets.coingecko.com/markets/images/535/large/256x256_Black-1.png", + }, + { + "id": "curve_ethereum", + "name": "Curve (Ethereum)", + "market_type": "spot", + "thumb": "https://assets.coingecko.com/markets/images/538/thumb/Curve.png", + "large": "https://assets.coingecko.com/markets/images/538/large/Curve.png", + }, + ], + "icos": [], + "categories": [ + {"id": "ethereum-ecosystem", "name": "Ethereum Ecosystem"}, + { + "id": "ethereum-classic-ecosystem", + "name": "Ethereum Classic Ecosystem", + }, + {"id": "ethereumpow-ecosystem", "name": "EthereumPoW Ecosystem"}, + ], + "nfts": [ + { + "id": "cyberkongz-genkai", + "name": "CyberKongz Genkai (Ethereum)", + "symbol": "GENKAI", + "thumb": "https://assets.coingecko.com/nft_contracts/images/3388/thumb/cyberkongz-genkai.png", + }, + { + "id": "ethereum-peppets", + "name": "Ethereum Peppets", + "symbol": "PEPPET", + "thumb": "https://assets.coingecko.com/nft_contracts/images/3880/thumb/ethereum-peppets.png", + }, + { + "id": "ens-ethereum-name-service", + "name": "ENS: Ethereum Name Service", + "symbol": "ENS", + "thumb": "https://assets.coingecko.com/nft_contracts/images/373/thumb/ens-ethereum-name-service.png", + }, + { + "id": "league-of-kingdoms-ethereum", + "name": "League of Kingdoms (Ethereum)", + "symbol": "LOKR", + "thumb": "https://assets.coingecko.com/nft_contracts/images/1001/thumb/league-of-kingdoms-ethereum.jpg", + }, + ], + }, + "api_method": AsyncCoinGeckoAPI.search, + "query": "eth", + } + await success_request(**data) + await failed_request(**data) + + # endregion + + # region Trending + # ---------- search/trending ---------- # + async def test_get_search_trending(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/search/trending", + "expected_payload": { + "coins": [ + { + "item": { + "id": "moon-tropica", + "coin_id": 28470, + "name": "Moon Tropica", + "symbol": "CAH", + "market_cap_rank": 530, + "thumb": "https://assets.coingecko.com/coins/images/28470/standard/MTLOGO.png?1696527464", + "small": "https://assets.coingecko.com/coins/images/28470/small/MTLOGO.png?1696527464", + "large": "https://assets.coingecko.com/coins/images/28470/large/MTLOGO.png?1696527464", + "slug": "moon-tropica", + "price_btc": 0.000530163474333298, + "score": 0, + "data": { + "price": 36.9717118016975, + "price_btc": "0.000530163474333299", + "price_change_percentage_24h": { + "aed": -4.04467447608756, + "sats": -5.98585375059245, + }, + "market_cap": "$99,703,583", + "market_cap_btc": "1428.83459310001", + "total_volume": "$282,142", + "total_volume_btc": "4.04583894742915", + "sparkline": "https://www.coingecko.com/coins/28470/sparkline.svg", + "content": None, + }, + } + }, + ], + "nfts": [ + { + "id": "chameleon-travel-club", + "name": "ChameleonTravelClub", + "symbol": "CTC", + "thumb": "https://assets.coingecko.com/nft_contracts/images/3610/standard/chameleon-travel-club.png?1707290106", + "nft_contract_id": 3610, + "native_currency_symbol": "eth", + "floor_price_in_native_currency": 4.29, + "floor_price_24h_percentage_change": 57.3120347225931, + "data": { + "floor_price": "4.29 ETH", + "floor_price_in_usd_24h_percentage_change": "57.3120347225931", + "h24_volume": "11.26 ETH", + "h24_average_sale_price": "2.82 ETH", + "sparkline": "https://www.coingecko.com/nft/3610/sparkline.svg", + "content": None, + }, + }, + ], + "categories": [ + { + "id": 251, + "name": "Solana Meme Coins", + "market_cap_1h_change": 1.44537649465531, + "slug": "solana-meme-coins", + "coins_count": 79, + "data": { + "market_cap": 8237562936.01112, + "market_cap_btc": 118852.276224895, + "total_volume": 1207846273.32444, + "total_volume_btc": 17426.911336459, + "market_cap_change_percentage_24h": { + "aed": 14.2303965235397, + "sats": 11.84681099263, + }, + "sparkline": "https://www.coingecko.com/categories/25211443/sparkline.svg", + }, + }, + ], + }, + "api_method": AsyncCoinGeckoAPI.get_search_trending, + } + await success_request(**data) + await failed_request(**data) + + # endregion + + # region Global + # ---------- global ---------- # + async def test_get_global(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/global", + "expected_payload": { + "date": { + "active_cryptocurrencies": 13690, + "upcoming_icos": 0, + "ongoing_icos": 49, + "ended_icos": 3376, + "markets": 1046, + "total_market_cap": { + "btc": 39003738.0847159, + "sats": 3900373808471590, + }, + "total_volume": { + "btc": 993675.225562481, + "sats": 99367522556248.1, + }, + "market_cap_percentage": { + "btc": 50.4465263233584, + "ada": 0.765987294694099, + }, + "market_cap_change_percentage_24h_usd": 1.72179506060272, + "updated_at": 1712512855, + } + }, + "api_method": AsyncCoinGeckoAPI.get_global, + } + await success_request(**data) + await failed_request(**data) + + # ---------- global/decentralized_finance_defi ---------- # + async def test_get_global_decentralized_finance_defi( + self, success_request, failed_request + ): + data = { + "url": "https://api.coingecko.com/api/v3/global/decentralized_finance_defi", + "expected_payload": { + "data": { + "defi_market_cap": "105273842288.229620442228701667", + "eth_market_cap": "406184911478.5772415794509920285", + "defi_to_eth_ratio": "25.9177136602677348904422532573101031788841174510865443130135278", + "trading_volume_24h": "5046503746.288261648853195485635", + "defi_dominance": "3.8676503084614763642371703099489945457095080090859886", + "top_coin_name": "Lido Staked Ether", + "top_coin_defi_dominance": 30.589442518868, + } + }, + "api_method": AsyncCoinGeckoAPI.get_global_decentralized_finance_defi, + } + await success_request(**data) + await failed_request(**data) + + # ---------- global/market_cap_chart ---------- # + async def test_get_global_market_cap_chart(self, success_request, failed_request): + data = { + "url": "https://api.coingecko.com/api/v3/global/market_cap_chart?days=1", + "expected_payload": { + "market_cap_chart": { + "market_cap": [ + [1367193600000, 1661441770], + [1367280000000, 1592765051], + [1367366400000, 1378705103], + ] + } + }, + "api_method": AsyncCoinGeckoAPI.get_global_market_cap_chart, + "days": "1", + } + await success_request(**data) + await failed_request(**data) + + # endregion + + # region Companies + # ---------- companies/public_treasury/{coin_id} ---------- # + async def test_get_companies_public_treasury_by_coin_id( + self, success_request, failed_request + ): + data = { + "url": "https://api.coingecko.com/api/v3/companies/public_treasury/bitcoin", + "expected_payload": { + "total_holdings": 264136, + "total_value_usd": 18403306939.1513, + "market_cap_dominance": 1.34, + "companies": [ + { + "name": "MicroStrategy Inc.", + "symbol": "NASDAQ:MSTR", + "country": "US", + "total_holdings": 174530, + "total_entry_value_usd": 4680000000, + "total_current_value_usd": 12160134022, + "percentage_of_total_supply": 0.831, + }, + { + "name": "Mogo Inc.", + "symbol": "NASDAQ:MOGO", + "country": "CA", + "total_holdings": 18, + "total_entry_value_usd": 595494, + "total_current_value_usd": 1254124, + "percentage_of_total_supply": 0, + }, + ], + }, + "api_method": AsyncCoinGeckoAPI.get_companies_public_treasury_by_coin_id, + "coin_id": "bitcoin", + } + await success_request(**data) + await failed_request(**data) + + # endregion From 0a7b577d7d2c6c06d225f0919032be7b3d673692 Mon Sep 17 00:00:00 2001 From: Vladislav Pavlov Date: Wed, 26 Feb 2025 20:14:18 +0700 Subject: [PATCH 2/2] chore: update dependencies and improve JSON parsing performance - Added dependencies: `aiohttp`, `aiohttp_retry`, and `orjson` in setup.py. - Replaced `json` with `orjson` in api.py for better performance in CoinGeckoAPI. - Updated README.md to include instructions for installing test dependencies. --- README.md | 2 +- pycoingecko/api.py | 9 ++++----- setup.py | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c9ad4e9..589f98c 100644 --- a/README.md +++ b/README.md @@ -594,7 +594,7 @@ cg.get_indexes_list() #### Installation Install required packages for testing using: ```bash -pip install pytest responses +pip install pytest-asyncio aioresponses responses ``` #### Usage diff --git a/pycoingecko/api.py b/pycoingecko/api.py index 5f2e18b..9e94017 100644 --- a/pycoingecko/api.py +++ b/pycoingecko/api.py @@ -1,6 +1,5 @@ -import json +import orjson import requests - from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry @@ -48,15 +47,15 @@ def __request(self, url, params): try: response.raise_for_status() # self._headers = response.headers - content = json.loads(response.content.decode('utf-8')) + content = orjson.loads(response.content.decode('utf-8')) return content except Exception as e: # check if json (with error message) is returned try: - content = json.loads(response.content.decode('utf-8')) + content = orjson.loads(response.content.decode('utf-8')) raise ValueError(content) # if no json - except json.decoder.JSONDecodeError: + except orjson.JSONDecodeError: pass raise diff --git a/setup.py b/setup.py index ae01535..55b7462 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ long_description_content_type="text/markdown", author='Christoforou Manolis', author_email='emchristoforou@gmail.com', - install_requires=['requests'], + install_requires=['aiohttp', 'aiohttp_retry', 'orjson', 'requests'], url='https://github.com/man-c/pycoingecko', classifiers=[ "Programming Language :: Python :: 3",