From 44bb4cef48e8fc0fb3f105c74c36f17b9391663f Mon Sep 17 00:00:00 2001 From: Nick Date: Sun, 24 Nov 2024 11:06:28 -0800 Subject: [PATCH] Add remaining Token api methods (#9) * feat: add remaning Token api methods [#3] * chore: version bump [#3[ * docs: add usage examples to quickstart docs [#3] * docs: use stable link instead of latest [#3] * docs: re-add rtd badge [#3] --- README.md | 6 +- birdeyepy/birdeye.py | 2 +- birdeyepy/resources/token.py | 248 +++++++++++++++++- birdeyepy/utils/enums.py | 16 ++ docs/source/getting_started/quick_start.rst | 54 +++- pyproject.toml | 2 +- tests/unit/resources/test_token.py | 266 ++++++++++++++++++++ uv.lock | 2 +- 8 files changed, 584 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 866d703..0ba1f60 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Python Versions Shield - + Read The Docs Badge Download Shield @@ -22,7 +22,7 @@

## Features -- 🪙 **BirdEye** Only supports [standard](https://docs.birdeye.so/docs/data-accessibility-by-packages#1-standard-package) subscription api urls (package is still in active development) +- 🪙 **BirdEye** Supports all BirdEye data services [apis](https://docs.birdeye.so/docs/overview). - ♻️ **Retry Strategy** Sensible defaults to reliably retry/back-off fetching data from the api - ✏️ **Code Formatting** Fully typed with [mypy](https://mypy-lang.org/) and code formatters [black](https://github.com/psf/black) / [isort](https://pycqa.github.io/isort/) - ⚒️ **Modern tooling** using [uv](https://docs.astral.sh/uv/), [ruff](https://docs.astral.sh/ruff/), and [pre-commit](https://pre-commit.com/) @@ -49,7 +49,7 @@ client.defi.price( ``` ## Documentation -See ful documentation [here](https://birdeye-py.readthedocs.io/en/latest/), or API [docs](https://docs.birdeye.so/docs/overview) +See ful documentation [here](https://birdeye-py.readthedocs.io/en/stable/), or API [docs](https://docs.birdeye.so/docs/overview) --- diff --git a/birdeyepy/birdeye.py b/birdeyepy/birdeye.py index c60e5cd..7c95237 100644 --- a/birdeyepy/birdeye.py +++ b/birdeyepy/birdeye.py @@ -7,7 +7,7 @@ ) -__version__ = "0.0.5" +__version__ = "0.0.6" class BirdEye: diff --git a/birdeyepy/resources/token.py b/birdeyepy/resources/token.py index eac9766..fb3d62e 100644 --- a/birdeyepy/resources/token.py +++ b/birdeyepy/resources/token.py @@ -1,6 +1,6 @@ from typing import Optional, cast -from birdeyepy.utils import BirdEyeApiUrls, BirdEyeRequestParams, IHttp +from birdeyepy.utils import BirdEyeApiUrls, BirdEyeRequestParams, IHttp, as_api_args class Token: @@ -15,7 +15,7 @@ def list_all( offset: Optional[int] = 0, limit: Optional[int] = 50, min_liquidity: Optional[int] = 50, - ) -> list: + ) -> dict: """Get token list of any supported chains. :param sort_by: The field to sort by. @@ -35,4 +35,246 @@ def list_all( request: BirdEyeRequestParams = {"params": params} response = self.http.send(path=BirdEyeApiUrls.DEFI_TOKEN_LIST, **request) - return cast(list, response) + return cast(dict, response) + + def security(self, address: str) -> dict: + """Get token security of any supported chains. + + :param address: The address of the token. + """ + params = {"address": address} + + request: BirdEyeRequestParams = {"params": params} + response = self.http.send(path=BirdEyeApiUrls.TOKEN_SECURITY, **request) + + return cast(dict, response) + + def overview(self, address: str) -> dict: + """Get overview of a token. + + :param address: The address of the token. + """ + params = {"address": address} + + request: BirdEyeRequestParams = {"params": params} + response = self.http.send(path=BirdEyeApiUrls.TOKEN_OVERVIEW, **request) + + return cast(dict, response) + + def creation_info(self, address: str) -> dict: + """Get creation info of a token. + + :param address: The address of the token. + """ + params = {"address": address} + + request: BirdEyeRequestParams = {"params": params} + response = self.http.send(path=BirdEyeApiUrls.TOKEN_CREATION_INFO, **request) + + return cast(dict, response) + + def trending( + self, + sort_by: str = "rank", + sort_type: str = "asc", + offset: int = 0, + limit: int = 10, + ) -> dict: + """Retrieve a dynamic and up-to-date list of trending tokens based on specified sorting criteria. + + :param sort_by: The field to sort by. + :param sort_type: The type of sorting. + :param offset: The offset. + :param limit: The limit. + """ + params = { + "sort_by": sort_by, + "sort_type": sort_type, + "offset": offset, + "limit": limit, + } + + request: BirdEyeRequestParams = {"params": params} + response = self.http.send(path=BirdEyeApiUrls.TOKEN_TRENDING, **request) + + return cast(dict, response) + + def list_all_v2(self) -> dict: + """This endpoint facilitates the retrieval of a list of tokens on a specified blockchain network. This upgraded version is exclusive to business and enterprise packages. By simply including the header for the requested blockchain without any query parameters, business and enterprise users can get the full list of tokens on the specified blockchain in the URL returned in the response. This removes the need for the limit response of the previous version and reduces the workload of making multiple calls.""" + response = self.http.send(path=BirdEyeApiUrls.TOKEN_LIST_V2, method="POST") + + return cast(dict, response) + + @as_api_args + def new_listing( + self, + time_to: int, + limit: Optional[int] = None, + meme_platform_enabled: Optional[bool] = None, + ) -> dict: + """Get newly listed tokens of any supported chains. + + :param time_to: Specify the end time using Unix timestamps in seconds + :param limit: The limit + :param meme_platform_enabled: Enable to receive token new listing from meme platforms (eg: pump.fun). This filter only supports Solana + """ + params = { + "time_to": time_to, + } + + if limit is not None: + params["limit"] = limit + + if meme_platform_enabled is not None: + params["meme_platform_enabled"] = meme_platform_enabled + + request: BirdEyeRequestParams = {"params": params} + response = self.http.send(path=BirdEyeApiUrls.TOKEN_NEW_LISTING, **request) + + return cast(dict, response) + + def top_traders( + self, + address: str, + time_frame: Optional[str] = "24h", + sort_type: Optional[str] = "desc", + sort_by: Optional[str] = "volume", + offset: Optional[int] = 0, + limit: Optional[int] = 10, + ) -> dict: + """Get top traders of given token. + + :param address: The address of the token. + :param time_frame: The time frame for the data (e.g., '24h', '7d'). + :param sort_type: The type of sorting. + :param sort_by: The field to sort by. + :param offset: The offset. + :param limit: The limit. + """ + params = { + "address": address, + "time_frame": time_frame, + "sort_type": sort_type, + "sort_by": sort_by, + "offset": offset, + "limit": limit, + } + + request: BirdEyeRequestParams = {"params": params} + response = self.http.send(path=BirdEyeApiUrls.TOKEN_TOP_TRADERS, **request) + + return cast(dict, response) + + def all_markets( + self, + address: str, + time_frame: Optional[str] = "24h", + sort_type: Optional[str] = "desc", + sort_by: Optional[str] = "liquidity", + offset: Optional[int] = 0, + limit: Optional[int] = 10, + ) -> dict: + """The API provides detailed information about the markets for a specific cryptocurrency token on a specified blockchain. Users can retrieve data for one or multiple markets related to a single token. This endpoint requires the specification of a token address and the blockchain to filter results. Additionally, it supports optional query parameters such as offset, limit, and required sorting by liquidity or sort type (ascending or descending) to refine the output. + + :param address: The address of the token. + :param time_frame: The time frame for the data (e.g., '24h', '7d'). + :param sort_type: The type of sorting. + :param sort_by: The field to sort by. + :param offset: The offset. + :param limit: The limit. + """ + params = { + "address": address, + "time_frame": time_frame, + "sort_type": sort_type, + "sort_by": sort_by, + "offset": offset, + "limit": limit, + } + + request: BirdEyeRequestParams = {"params": params} + response = self.http.send(path=BirdEyeApiUrls.TOKEN_ALL_MARKETS, **request) + + return cast(dict, response) + + def market_metadata_single(self, address: str) -> dict: + """Get metadata of single token + + :param address: The address of the token. + """ + params = {"address": address} + + request: BirdEyeRequestParams = {"params": params} + response = self.http.send(path=BirdEyeApiUrls.TOKEN_METADATA_SINGLE, **request) + + return cast(dict, response) + + @as_api_args + def market_metadata_multiple(self, addresses: str | list[str]) -> dict: + """Get metadata of multiple tokens + + :param addresses: The address of the token...can be comma separated string or list of strings. + """ + params = {"list_address": addresses} + + request: BirdEyeRequestParams = {"params": params} + response = self.http.send( + path=BirdEyeApiUrls.TOKEN_METADATA_MULTIPLE, **request + ) + + return cast(dict, response) + + def market_data(self, address: str) -> dict: + """Get market data of single token. + + :param address: The address of the token. + """ + params = {"address": address} + + request: BirdEyeRequestParams = {"params": params} + response = self.http.send(path=BirdEyeApiUrls.TOKEN_MARKET_DATA, **request) + + return cast(dict, response) + + def trade_data_single(self, address: str) -> dict: + """Get trade data of single token + + :param addresses: The address of the token. + """ + params = {"address": address} + + request: BirdEyeRequestParams = {"params": params} + response = self.http.send( + path=BirdEyeApiUrls.TOKEN_TRADE_DATA_SINGLE, **request + ) + + return cast(dict, response) + + @as_api_args + def trade_data_multiple(self, addresses: str | list[str]) -> dict: + """Get trade data of multiple tokens. + + :param addresses: The address of the token...can be comma separated string or list of strings. + """ + params = {"list_address": addresses} + + request: BirdEyeRequestParams = {"params": params} + response = self.http.send( + path=BirdEyeApiUrls.TOKEN_TRADE_DATA_MULTIPLE, **request + ) + + return cast(dict, response) + + def holder(self, address: str, offset: int = 0, limit: int = 100) -> dict: + """Get top holder list of the given token. + + :param address: The address of the token. + :param offset: The offset. + :param limit: The limit. + """ + params = {"address": address, "offset": offset, "limit": limit} + + request: BirdEyeRequestParams = {"params": params} + response = self.http.send(path=BirdEyeApiUrls.TOKEN_HOLDER, **request) + + return cast(dict, response) diff --git a/birdeyepy/utils/enums.py b/birdeyepy/utils/enums.py index b6701b2..fcac58e 100644 --- a/birdeyepy/utils/enums.py +++ b/birdeyepy/utils/enums.py @@ -14,6 +14,22 @@ class BirdEyeApiUrls: TRADER_GAINERS_LOSERS = "trader/gainers-losers" TRADER_SEEK_BY_TIME = "trader/txs/seek_by_time" + # TOKEN + TOKEN_SECURITY = "defi/token_security" + TOKEN_OVERVIEW = "defi/token_overview" + TOKEN_CREATION_INFO = "defi/token_creation_info" + TOKEN_TRENDING = "defi/token_trending" + TOKEN_LIST_V2 = "/defi/v2/tokens/all" + TOKEN_NEW_LISTING = "defi/v2/tokens/new_listing" + TOKEN_TOP_TRADERS = "defi/v2/tokens/top_traders" + TOKEN_ALL_MARKETS = "/defi/v2/markets" + TOKEN_METADATA_SINGLE = "defi/v3/token/meta-data/single" + TOKEN_METADATA_MULTIPLE = "defi/v3/token/meta-data/multiple" + TOKEN_MARKET_DATA = "defi/v3/token/market-data" + TOKEN_HOLDER = "defi/v3/token/holder" + TOKEN_TRADE_DATA_SINGLE = "defi/v3/token/trade-data/single" + TOKEN_TRADE_DATA_MULTIPLE = "defi/v3/token/trade-data/multiple" + class BirdEyeChainEnum(SimpleEnum): # Solana diff --git a/docs/source/getting_started/quick_start.rst b/docs/source/getting_started/quick_start.rst index 1c680ee..b68ddf7 100644 --- a/docs/source/getting_started/quick_start.rst +++ b/docs/source/getting_started/quick_start.rst @@ -11,7 +11,7 @@ Get the price of a token on the Solana blockchain # DEFI - # https://public-api.birdeye.so/defi/price + # https://docs.birdeye.so/reference/get_defi-price client.defi.price() # defaults to So11111111111111111111111111111111111111112 client.defi.price( @@ -19,7 +19,7 @@ Get the price of a token on the Solana blockchain include_liquidity=True, # can also use strings 'true' or 'false' ) - # https://public-api.birdeye.so/defi/history_price + # https://docs.birdeye.so/reference/get_defi-history-price client.defi.history(time_from=1732398942, time_to=1732398961) # defaults to So11111111111111111111111111111111111111112 client.defi.history( @@ -32,9 +32,57 @@ Get the price of a token on the Solana blockchain # TOKEN - # https://public-api.birdeye.so/defi/tokenlist + # https://docs.birdeye.so/reference/get_defi-tokenlist client.token.list_all() + # https://docs.birdeye.so/reference/get_defi-token-security + client.token.security(address="Gr11mosZNZjwpqnemXNnWs9E2Bnv7R6vzaKwJTdjo8zQ") + + # https://docs.birdeye.so/reference/get_defi-token-overview + client.token.overview(address="Gr11mosZNZjwpqnemXNnWs9E2Bnv7R6vzaKwJTdjo8zQ") + + # https://docs.birdeye.so/reference/get_defi-token-creation-info + client.token.creation_info(address="Gr11mosZNZjwpqnemXNnWs9E2Bnv7R6vzaKwJTdjo8zQ") + + # https://docs.birdeye.so/reference/get_defi-token-trending + client.token.trending() + + # https://docs.birdeye.so/reference/post_defi-v2-tokens-all + client.token.list_all_v2() + + # https://docs.birdeye.so/reference/get_defi-v2-tokens-new-listing + client.token.new_listing( + time_to=1732398961, + meme_platform_enabled=True, # can also use strings 'true' or 'false' + ) + + # https://docs.birdeye.so/reference/get_defi-v2-tokens-top-traders + client.token.top_traders(address="Gr11mosZNZjwpqnemXNnWs9E2Bnv7R6vzaKwJTdjo8zQ") + + # https://docs.birdeye.so/reference/get_defi-v2-markets + client.token.all_markets(address="Gr11mosZNZjwpqnemXNnWs9E2Bnv7R6vzaKwJTdjo8zQ") + + # https://docs.birdeye.so/reference/get_defi-v3-token-meta-data-single + client.token.market_metadata_single(address="Gr11mosZNZjwpqnemXNnWs9E2Bnv7R6vzaKwJTdjo8zQ") + + # https://docs.birdeye.so/reference/get_defi-v3-token-meta-data-multiple + client.token.market_metadata_multiple( + addresses=["Gr11mosZNZjwpqnemXNnWs9E2Bnv7R6vzaKwJTdjo8zQ", "AGQZRtz7hZtz3VJ1CoXRMNMyh2ZMZ1g6pv4aGMUSpump"], + ) # can also use comma separated strings 'Gr11mosZNZjwpqnemXNnWs9E2Bnv7R6vzaKwJTdjo8zQ,AGQZRtz7hZtz3VJ1CoXRMNMyh2ZMZ1g6pv4aGMUSpump' + + # https://docs.birdeye.so/reference/get_defi-v3-token-market-data + client.token.market_data(address="Gr11mosZNZjwpqnemXNnWs9E2Bnv7R6vzaKwJTdjo8zQ") + + # https://docs.birdeye.so/reference/get_defi-v3-token-trade-data-single + client.token.trade_data_single(address="Gr11mosZNZjwpqnemXNnWs9E2Bnv7R6vzaKwJTdjo8zQ") + + # https://docs.birdeye.so/reference/get_defi-v3-token-trade-data-multiple + client.token.trade_data_multiple( + addresses=["Gr11mosZNZjwpqnemXNnWs9E2Bnv7R6vzaKwJTdjo8zQ", "AGQZRtz7hZtz3VJ1CoXRMNMyh2ZMZ1g6pv4aGMUSpump"], + ) # can also use comma separated strings 'Gr11mosZNZjwpqnemXNnWs9E2Bnv7R6vzaKwJTdjo8zQ,AGQZRtz7hZtz3VJ1CoXRMNMyh2ZMZ1g6pv4aGMUSpump' + + # https://docs.birdeye.so/reference/get_defi-v3-token-holder + client.token.holder(address="Gr11mosZNZjwpqnemXNnWs9E2Bnv7R6vzaKwJTdjo8zQ") Get the price of a token on the Ethereum blockchain diff --git a/pyproject.toml b/pyproject.toml index 05ce626..5692c68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "birdeye-py" -version = "0.0.5" +version = "0.0.6" description = "Python wrapper for birdeye.so api" readme = "README.md" requires-python = ">=3.9" diff --git a/tests/unit/resources/test_token.py b/tests/unit/resources/test_token.py index 8bd8f20..ff1b792 100644 --- a/tests/unit/resources/test_token.py +++ b/tests/unit/resources/test_token.py @@ -1,5 +1,7 @@ from unittest.mock import MagicMock +import pytest + from birdeyepy.resources.token import Token from birdeyepy.utils import BirdEyeApiUrls @@ -23,3 +25,267 @@ def test_token_list_all_api_called_with_expected_args() -> None: "min_liquidity": 50, }, ) + + +def test_token_security_api_called_with_expected_args() -> None: + # Arrange + mock_http = MagicMock() + + # Act + client = Token(http=mock_http) + client.security(address="test") + + # Assert + mock_http.send.assert_called_once_with( + path=BirdEyeApiUrls.TOKEN_SECURITY, + params={"address": "test"}, + ) + + +def test_token_overview_api_called_with_expected_args() -> None: + # Arrange + mock_http = MagicMock() + + # Act + client = Token(http=mock_http) + client.overview(address="test") + + # Assert + mock_http.send.assert_called_once_with( + path=BirdEyeApiUrls.TOKEN_OVERVIEW, + params={"address": "test"}, + ) + + +def test_token_creation_info_api_called_with_expected_args() -> None: + # Arrange + mock_http = MagicMock() + + # Act + client = Token(http=mock_http) + client.creation_info(address="test") + + # Assert + mock_http.send.assert_called_once_with( + path=BirdEyeApiUrls.TOKEN_CREATION_INFO, + params={"address": "test"}, + ) + + +def test_token_trending_api_called_with_expected_args() -> None: + # Arrange + mock_http = MagicMock() + + # Act + client = Token(http=mock_http) + client.trending() + + # Assert + mock_http.send.assert_called_once_with( + path=BirdEyeApiUrls.TOKEN_TRENDING, + params={ + "sort_by": "rank", + "sort_type": "asc", + "offset": 0, + "limit": 10, + }, + ) + + +def test_token_list_all_v2_api_called_with_expected_args() -> None: + # Arrange + mock_http = MagicMock() + + # Act + client = Token(http=mock_http) + client.list_all_v2() + + # Assert + mock_http.send.assert_called_once_with( + method="POST", + path=BirdEyeApiUrls.TOKEN_LIST_V2, + ) + + +def test_token_new_listing_api_called_with_expected_args_default() -> None: + # Arrange + mock_http = MagicMock() + + # Act + client = Token(http=mock_http) + client.new_listing(time_to=1732472285) + + # Assert + mock_http.send.assert_called_once_with( + path=BirdEyeApiUrls.TOKEN_NEW_LISTING, + params={"time_to": 1732472285}, + ) + + +def test_token_new_listing_api_called_with_expected_args() -> None: + # Arrange + mock_http = MagicMock() + + # Act + client = Token(http=mock_http) + client.new_listing(time_to=1732472285, limit=10, meme_platform_enabled=True) + + # Assert + mock_http.send.assert_called_once_with( + path=BirdEyeApiUrls.TOKEN_NEW_LISTING, + params={"time_to": 1732472285, "limit": 10, "meme_platform_enabled": "true"}, + ) + + +def test_token_top_traders_api_called_with_expected_args() -> None: + # Arrange + mock_http = MagicMock() + + # Act + client = Token(http=mock_http) + client.top_traders(address="test") + + # Assert + mock_http.send.assert_called_once_with( + path=BirdEyeApiUrls.TOKEN_TOP_TRADERS, + params={ + "address": "test", + "time_frame": "24h", + "sort_type": "desc", + "sort_by": "volume", + "offset": 0, + "limit": 10, + }, + ) + + +def test_token_all_markets_api_called_with_expected_args() -> None: + # Arrange + mock_http = MagicMock() + + # Act + client = Token(http=mock_http) + client.all_markets(address="test") + + # Assert + mock_http.send.assert_called_once_with( + path=BirdEyeApiUrls.TOKEN_ALL_MARKETS, + params={ + "address": "test", + "time_frame": "24h", + "sort_type": "desc", + "sort_by": "liquidity", + "offset": 0, + "limit": 10, + }, + ) + + +def test_token_market_metadata_single_api_called_with_expected_args() -> None: + # Arrange + mock_http = MagicMock() + + # Act + client = Token(http=mock_http) + client.market_metadata_single(address="test") + + # Assert + mock_http.send.assert_called_once_with( + path=BirdEyeApiUrls.TOKEN_METADATA_SINGLE, + params={"address": "test"}, + ) + + +@pytest.mark.parametrize( + "addresses,expected_params", + [ + ("test", {"list_address": "test"}), + (["test1", "test2"], {"list_address": "test1,test2"}), + ("test1,test2", {"list_address": "test1,test2"}), + ], +) +def test_token_market_metadata_multiple_api_called_with_expected_args( + addresses: str | list[str], expected_params: dict +) -> None: + # Arrange + mock_http = MagicMock() + + # Act + client = Token(http=mock_http) + client.market_metadata_multiple(addresses=addresses) + + # Assert + mock_http.send.assert_called_once_with( + path=BirdEyeApiUrls.TOKEN_METADATA_MULTIPLE, + params=expected_params, + ) + + +def test_token_market_data_api_called_with_expected_args() -> None: + # Arrange + mock_http = MagicMock() + + # Act + client = Token(http=mock_http) + client.market_data(address="test") + + # Assert + mock_http.send.assert_called_once_with( + path=BirdEyeApiUrls.TOKEN_MARKET_DATA, + params={"address": "test"}, + ) + + +def test_token_trade_data_single_api_called_with_expected_args() -> None: + # Arrange + mock_http = MagicMock() + + # Act + client = Token(http=mock_http) + client.trade_data_single(address="test") + + # Assert + mock_http.send.assert_called_once_with( + path=BirdEyeApiUrls.TOKEN_TRADE_DATA_SINGLE, + params={"address": "test"}, + ) + + +@pytest.mark.parametrize( + "addresses,expected_params", + [ + ("test", {"list_address": "test"}), + (["test1", "test2"], {"list_address": "test1,test2"}), + ("test1,test2", {"list_address": "test1,test2"}), + ], +) +def test_token_trade_data_multiple_api_called_with_expected_args( + addresses: str | list[str], expected_params: dict +) -> None: + # Arrange + mock_http = MagicMock() + + # Act + client = Token(http=mock_http) + client.trade_data_multiple(addresses=addresses) + + # Assert + mock_http.send.assert_called_once_with( + path=BirdEyeApiUrls.TOKEN_TRADE_DATA_MULTIPLE, + params=expected_params, + ) + + +def test_token_holder_api_called_with_expected_args() -> None: + # Arrange + mock_http = MagicMock() + + # Act + client = Token(http=mock_http) + client.holder(address="test") + + # Assert + mock_http.send.assert_called_once_with( + path=BirdEyeApiUrls.TOKEN_HOLDER, + params={"address": "test", "offset": 0, "limit": 100}, + ) diff --git a/uv.lock b/uv.lock index bc1a4f7..22eaedc 100644 --- a/uv.lock +++ b/uv.lock @@ -42,7 +42,7 @@ wheels = [ [[package]] name = "birdeye-py" -version = "0.0.5" +version = "0.0.6" source = { virtual = "." } dependencies = [ { name = "requests" },