Skip to content

Commit c8f577c

Browse files
Merge pull request #1 from Unique-Divine/defillama/tests
tests Defillama and Messari: All unit tests are green.
2 parents 11f1c0b + 11b5ca5 commit c8f577c

File tree

13 files changed

+374
-182
lines changed

13 files changed

+374
-182
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
"""Module to handle initialization, imports, for DeFiLlama class"""
1+
"""Module to handle initialization, imports, for DeFiLlama class
2+
3+
DeFiLLama API Docs: https://defillama.com/docs/api
4+
"""
25

36

47
from .defillama import *
Lines changed: 82 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,60 @@
1-
"""This module is meant to contain the DeFiLlama class"""
1+
"""This module is meant to contain the DeFiLlama class
2+
3+
DeFiLLama API Docs: https://defillama.com/docs/api
4+
"""
25

3-
# Global imports
46
import datetime
5-
from string import Template
6-
from typing import Union, List, Dict
7+
8+
from pycaw.defillama import helpers
9+
from pycaw.messari import dataloader
10+
from pycaw.messari import utils
711

812
import pandas as pd
13+
from typing import Union, List, Dict
914

10-
from messari.dataloader import DataLoader
11-
# Local imports
12-
from messari.utils import validate_input, get_taxonomy_dict, time_filter_df
13-
from .helpers import format_df
1415

1516
##########################
1617
# URL Endpoints
1718
##########################
18-
DL_PROTOCOLS_URL = "https://api.llama.fi/protocols"
19-
DL_GLOBAL_TVL_URL = "https://api.llama.fi/charts/"
20-
DL_CURRENT_PROTOCOL_TVL_URL = Template("https://api.llama.fi/tvl/$slug")
21-
DL_CHAIN_TVL_URL = Template("https://api.llama.fi/charts/$chain")
22-
DL_GET_PROTOCOL_TVL_URL = Template("https://api.llama.fi/protocol/$slug")
2319

2420

25-
class DeFiLlama(DataLoader):
26-
"""This class is a wrapper around the DeFi Llama API
27-
"""
21+
class DeFiLlama(dataloader.DataLoader):
22+
"""This class is a wrapper around the DeFi Llama API"""
2823

29-
def __init__(self):
30-
messari_to_dl_dict = get_taxonomy_dict("messari_to_dl.json")
31-
DataLoader.__init__(self, api_dict=None, taxonomy_dict=messari_to_dl_dict)
24+
api_urls: Dict[str, str]
3225

33-
def get_protocol_tvl_timeseries(self, asset_slugs: Union[str, List],
34-
start_date: Union[str, datetime.datetime] = None,
35-
end_date: Union[str, datetime.datetime] = None) -> pd.DataFrame:
26+
def __init__(self):
27+
messari_to_dl_dict = utils.get_taxonomy_dict("messari_to_dl.json")
28+
dataloader.DataLoader.__init__(
29+
self, api_dict=None, taxonomy_dict=messari_to_dl_dict
30+
)
31+
32+
@property
33+
def api_urls(self) -> Dict[str, str]:
34+
_endpoint_preamble: str = "https://api.llama.fi"
35+
urls = dict(
36+
# List all protocols on defillama along with their tvl
37+
protocols="/".join([_endpoint_preamble, "protocols"]),
38+
# Get historical TVL of a protocol and breakdowns by token and chain
39+
get_protocol_tvl="/".join([_endpoint_preamble, "protocol", "{_slug}"]),
40+
# Get historical TVL on DeFi on all chains
41+
global_tvl="/".join([_endpoint_preamble, "charts"]),
42+
# Get historical TVL of a chain
43+
chain_tvl="/".join([_endpoint_preamble, "charts", "{_chain}"]),
44+
# Get current TVL of a protocol
45+
current_protocol_tvl="/".join([_endpoint_preamble, "tvl", "{_slug}"]),
46+
# Get current TVL of all chains
47+
all_chains_tvl="/".join([_endpoint_preamble, "chains"]),
48+
)
49+
return urls
50+
# TODO test: Check that the urls aren't broken now.
51+
52+
def get_protocol_tvl_timeseries(
53+
self,
54+
asset_slugs: Union[str, List],
55+
start_date: Union[str, datetime.datetime] = None,
56+
end_date: Union[str, datetime.datetime] = None,
57+
) -> pd.DataFrame:
3658
"""Returns times TVL of a protocol with token amounts as a pandas DataFrame.
3759
Returned DataFrame is indexed by df[protocol][chain][asset].
3860
@@ -59,7 +81,7 @@ def get_protocol_tvl_timeseries(self, asset_slugs: Union[str, List],
5981

6082
slug_df_list: List = []
6183
for slug in slugs:
62-
endpoint_url = DL_GET_PROTOCOL_TVL_URL.substitute(slug=slug)
84+
endpoint_url = self.api_urls["get_protocol_tvl"].format(_slug=slug)
6385
protocol = self.get_response(endpoint_url)
6486

6587
###########################
@@ -95,13 +117,15 @@ def get_protocol_tvl_timeseries(self, asset_slugs: Union[str, List],
95117
chain_tvl_tokens_usd_df = pd.DataFrame(chain_tvl_tokens_usd)
96118

97119
# fix indexes
98-
chain_tvl_df = format_df(chain_tvl_df)
99-
chain_tvl_tokens_df = format_df(chain_tvl_tokens_df)
100-
chain_tvl_tokens_usd_df = format_df(chain_tvl_tokens_usd_df)
120+
chain_tvl_df = helpers.format_df(chain_tvl_df)
121+
chain_tvl_tokens_df = helpers.format_df(chain_tvl_tokens_df)
122+
chain_tvl_tokens_usd_df = helpers.format_df(chain_tvl_tokens_usd_df)
101123
chain_tvl_tokens_usd_df = chain_tvl_tokens_usd_df.add_suffix("_usd")
102124

103125
# concat tokens and tokensInUsd
104-
joint_tokens_df = pd.concat([chain_tvl_tokens_df, chain_tvl_tokens_usd_df], axis=1)
126+
joint_tokens_df = pd.concat(
127+
[chain_tvl_tokens_df, chain_tvl_tokens_usd_df], axis=1
128+
)
105129
# Join total chain TVL w/ token TVL
106130
chain_df = chain_tvl_df.join(joint_tokens_df)
107131
chain_df_list.append(chain_df)
@@ -119,7 +143,7 @@ def get_protocol_tvl_timeseries(self, asset_slugs: Union[str, List],
119143
token[key] = value
120144
token.pop("tokens", None)
121145
tokens_df = pd.DataFrame(tokens)
122-
tokens_df = format_df(tokens_df)
146+
tokens_df = helpers.format_df(tokens_df)
123147

124148
## tokens in USD
125149
tokens_usd = protocol["tokensInUsd"]
@@ -128,13 +152,13 @@ def get_protocol_tvl_timeseries(self, asset_slugs: Union[str, List],
128152
token[key] = value
129153
token.pop("tokens", None)
130154
tokens_usd_df = pd.DataFrame(tokens_usd)
131-
tokens_usd_df = format_df(tokens_usd_df)
155+
tokens_usd_df = helpers.format_df(tokens_usd_df)
132156
tokens_usd_df = tokens_usd_df.add_suffix("_usd")
133157

134158
# Get total tvl across chains
135159
tvl = protocol["tvl"]
136160
total_tvl_df = pd.DataFrame(tvl)
137-
total_tvl_df = format_df(total_tvl_df)
161+
total_tvl_df = helpers.format_df(total_tvl_df)
138162

139163
# Working
140164
joint_tokens_df = pd.concat([tokens_df, tokens_usd_df], axis=1)
@@ -150,11 +174,16 @@ def get_protocol_tvl_timeseries(self, asset_slugs: Union[str, List],
150174
total_slugs_df = pd.concat(slug_df_list, keys=slugs, axis=1)
151175
total_slugs_df.sort_index(inplace=True)
152176

153-
total_slugs_df = time_filter_df(total_slugs_df, start_date=start_date, end_date=end_date)
177+
total_slugs_df = utils.time_filter_df(
178+
total_slugs_df, start_date=start_date, end_date=end_date
179+
)
154180
return total_slugs_df
155181

156-
def get_global_tvl_timeseries(self, start_date: Union[str, datetime.datetime] = None,
157-
end_date: Union[str, datetime.datetime] = None) -> pd.DataFrame:
182+
def get_global_tvl_timeseries(
183+
self,
184+
start_date: Union[str, datetime.datetime] = None,
185+
end_date: Union[str, datetime.datetime] = None,
186+
) -> pd.DataFrame:
158187
"""Returns timeseries TVL from total of all Defi Llama supported protocols
159188
160189
Parameters
@@ -170,15 +199,20 @@ def get_global_tvl_timeseries(self, start_date: Union[str, datetime.datetime] =
170199
DataFrame
171200
DataFrame containing timeseries tvl data for every protocol
172201
"""
173-
global_tvl = self.get_response(DL_GLOBAL_TVL_URL)
202+
global_tvl = self.get_response(self.api_urls["global_tvl"])
174203
global_tvl_df = pd.DataFrame(global_tvl)
175-
global_tvl_df = format_df(global_tvl_df)
176-
global_tvl_df = time_filter_df(global_tvl_df, start_date=start_date, end_date=end_date)
204+
global_tvl_df = helpers.format_df(global_tvl_df)
205+
global_tvl_df = utils.time_filter_df(
206+
global_tvl_df, start_date=start_date, end_date=end_date
207+
)
177208
return global_tvl_df
178209

179-
def get_chain_tvl_timeseries(self, chains_in: Union[str, List],
180-
start_date: Union[str, datetime.datetime] = None,
181-
end_date: Union[str, datetime.datetime] = None) -> pd.DataFrame:
210+
def get_chain_tvl_timeseries(
211+
self,
212+
chains_in: Union[str, List],
213+
start_date: Union[str, datetime.datetime] = None,
214+
end_date: Union[str, datetime.datetime] = None,
215+
) -> pd.DataFrame:
182216
"""Retrive timeseries TVL for a given chain
183217
184218
Parameters
@@ -197,20 +231,22 @@ def get_chain_tvl_timeseries(self, chains_in: Union[str, List],
197231
DataFrame
198232
DataFrame containing timeseries tvl data for each chain
199233
"""
200-
chains = validate_input(chains_in)
234+
chains = utils.validate_input(chains_in)
201235

202236
chain_df_list = []
203237
for chain in chains:
204-
endpoint_url = DL_CHAIN_TVL_URL.substitute(chain=chain)
238+
endpoint_url = self.api_urls["chain_tvl"].format(_chain=chain)
205239
response = self.get_response(endpoint_url)
206240
chain_df = pd.DataFrame(response)
207-
chain_df = format_df(chain_df)
241+
chain_df = helpers.format_df(chain_df)
208242
chain_df_list.append(chain_df)
209243

210244
# Join DataFrames from each chain & return
211245
chains_df = pd.concat(chain_df_list, axis=1)
212246
chains_df.columns = chains
213-
chains_df = time_filter_df(chains_df, start_date=start_date, end_date=end_date)
247+
chains_df = utils.time_filter_df(
248+
chains_df, start_date=start_date, end_date=end_date
249+
)
214250
return chains_df
215251

216252
def get_current_tvl(self, asset_slugs: Union[str, List]) -> Dict:
@@ -226,11 +262,11 @@ def get_current_tvl(self, asset_slugs: Union[str, List]) -> Dict:
226262
DataFrame
227263
Pandas Series for tvl indexed by each slug {slug: tvl, ...}
228264
"""
229-
slugs = validate_input(asset_slugs)
265+
slugs = utils.validate_input(asset_slugs)
230266

231267
tvl_dict = {}
232268
for slug in slugs:
233-
endpoint_url = DL_CURRENT_PROTOCOL_TVL_URL.substitute(slug=slug)
269+
endpoint_url = self.api_urls["current_protocol_tvl"].format(_slug=slug)
234270
tvl = self.get_response(endpoint_url)
235271
if isinstance(tvl, float):
236272
tvl_dict[slug] = tvl
@@ -250,7 +286,7 @@ def get_protocols(self) -> pd.DataFrame:
250286
DataFrame
251287
DataFrame with one column per DeFi Llama supported protocol
252288
"""
253-
protocols = self.get_response(DL_PROTOCOLS_URL)
289+
protocols = self.get_response(self.api_urls["protocols"])
254290

255291
protocol_dict = {}
256292
for protocol in protocols:
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
"""This module is dedicated to helpers for the DeFiLlama class"""
1+
"""A module with helper functions for the DeFiLlama class
2+
3+
Methods:
4+
format_df: Replaces dates and drops duplicates.
5+
"""
26

37

48
import pandas as pd
@@ -7,27 +11,23 @@
711
def format_df(df_in: pd.DataFrame) -> pd.DataFrame:
812
"""format a typical DF from DL, replace date & drop duplicates
913
10-
Parameters
11-
----------
12-
df_in: pd.DataFrame
13-
input DataFrame
14+
Args:
15+
df_in (pd.DataFrame): input DataFrame
1416
15-
Returns
16-
-------
17-
DataFrame
18-
formated pandas DataFrame
17+
Returns:
18+
(pd.DataFrame): formated pandas DataFrame
1919
"""
2020

2121
# set date to index
2222
df_new = df_in
23-
if 'date' in df_in.columns:
24-
df_new.set_index('date', inplace=True)
25-
df_new.index = pd.to_datetime(df_new.index, unit='s', origin='unix')
23+
if "date" in df_in.columns:
24+
df_new.set_index("date", inplace=True)
25+
df_new.index = pd.to_datetime(df_new.index, unit="s", origin="unix")
2626
df_new.index = df_new.index.date
2727

2828
# drop duplicates
2929
# NOTE: sometimes DeFi Llama has duplicate dates, choosing to just keep the last
3030
# NOTE: Data for duplicates is not the same
3131
# TODO: Investigate which data should be kept (currently assuming last is more recent
32-
df_new = df_new[~df_new.index.duplicated(keep='last')]
32+
df_new = df_new[~df_new.index.duplicated(keep="last")]
3333
return df_new

pycaw/etherscan/etherscan_connector.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,6 @@ def _validate_timestamp_format(self,
2626
timestamp: Union[int, str, pd.Timestamp]):
2727
raise NotImplementedError() # TODO
2828

29-
"""
30-
@tenacity.retry(stop=tenacity.stop_after_attempt(3),
31-
wait=tenacity.wait_exponential(min=0.1, max=5, multiplier=2))
32-
@ratelimit.sleep_and_retry
33-
@ratelimit.limits(calls=30, period=1) # period (float) is in seconds.
34-
"""
3529
def run_query(self, query: str, rate_limit: bool = True) -> Dict[str, Any]:
3630
"""Func is wrapped with some ultimate limiters to ensure this method is
3731
never callled too much. However, the batch-call function should also

pycaw/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
"""Tests for the pycaw package."""
22
import dotenv
3+
34
dotenv.load_dotenv()

pycaw/tests/cmc_test.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/env python
2+
3+
import os
4+
import json
5+
import pytest
6+
from pycaw import cmc
7+
from typing import Any, Dict, List, Union
8+
9+
10+
class TestCoinMarketCapAPI:
11+
@pytest.fixture
12+
def cmc_api(self) -> cmc.CoinMarketCapAPI:
13+
return cmc.CoinMarketCapAPI()
14+
15+
def test_cmc_id_map(self, cmc_api: cmc.CoinMarketCapAPI):
16+
symbols: List[str] = ["BTC", "ETH"]
17+
cmc_id_maps: List[dict] = cmc_api.cmc_id_map(symbols=symbols)
18+
assert isinstance(cmc_id_maps, list)
19+
assert isinstance(cmc_id_maps[0], dict)
20+
assert all([k in cmc_id_maps[0].keys() for k in ["id", "slug", "name"]])
21+
22+
@pytest.fixture
23+
def cmc_id_maps(self) -> List[dict]:
24+
return [
25+
{
26+
"id": 1,
27+
"name": "Bitcoin",
28+
"symbol": "BTC",
29+
"slug": "bitcoin",
30+
"rank": 1,
31+
"is_active": 1,
32+
"first_historical_data": "2013-04-28T18:47:21.000Z",
33+
"last_historical_data": "2021-11-19T00:59:02.000Z",
34+
"platform": None,
35+
},
36+
{
37+
"id": 1027,
38+
"name": "Ethereum",
39+
"symbol": "ETH",
40+
"slug": "ethereum",
41+
"rank": 2,
42+
"is_active": 1,
43+
"first_historical_data": "2015-08-07T14:49:30.000Z",
44+
"last_historical_data": "2021-11-19T00:59:02.000Z",
45+
"platform": None,
46+
},
47+
]
48+
49+
def test_save_cmc_id_maps(
50+
self, cmc_api: cmc.CoinMarketCapAPI, cmc_id_maps: List[dict]
51+
):
52+
"""Tests whether the CMC ID Map query saves correctly."""
53+
54+
temp_filename: str = "temp-foo.json"
55+
temp_save_path = temp_filename
56+
57+
assert not os.path.exists(temp_save_path)
58+
cmc_api._save_cmc_id_maps(cmc_id_maps=cmc_id_maps, filename=temp_filename)
59+
with open(temp_save_path, mode="r") as f:
60+
saved_cmc_id_maps: List[dict] = json.load(f)
61+
assert isinstance(saved_cmc_id_maps, list)
62+
assert len(saved_cmc_id_maps) == 2
63+
assert all(
64+
[
65+
[k in dict_.keys() for k in ["id", "name", "symbol"]]
66+
for dict_ in saved_cmc_id_maps
67+
]
68+
)
69+
70+
os.remove(temp_save_path)
71+
assert not os.path.exists(temp_save_path)

0 commit comments

Comments
 (0)