Skip to content

Commit 055a9bc

Browse files
authored
Merge pull request #6 from dhruvan2006/dune
Add support for Dune
2 parents c4ff38c + 508c8b4 commit 055a9bc

File tree

9 files changed

+128
-3
lines changed

9 files changed

+128
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ handles the heavy lifting so you can focus on insights.
3636
- [Blockchain.com](https://www.blockchain.com/explorer/charts)
3737
- [Glassnode](https://studio.glassnode.com/charts/)
3838
- [The Block](https://www.theblock.co/data/)
39+
- [Dune](https://dune.com/)
3940

4041
## Installation
4142
To install the `chaindl` package, use pip:

assets/dune.png

187 KB
Loading

chaindl/download.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ def download(url, start=None, end=None, **kwargs):
3030
- CryptoQuant: "https://cryptoquant.com"
3131
- Bitcoin Magazine Pro: "https://www.bitcoinmagazinepro.com"
3232
- Blockchain.com: "https://www.blockchain.com/explorer/charts"
33+
- Glassnode: "https://studio.glassnode.com/charts"
34+
- The Block: "https://www.theblock.co"
35+
- Dune: "https://dune.com"
3336
3437
Example:
3538
>>> df = download("https://charts.checkonchain.com/path/to/indicator")
@@ -45,6 +48,7 @@ def download(url, start=None, end=None, **kwargs):
4548
BLOCKCHAIN_BASE_URL = "https://www.blockchain.com/explorer/charts"
4649
GLASSNODE_BASE_URL = "https://studio.glassnode.com/charts"
4750
THEBLOCK_BASE_URL = "https://www.theblock.co"
51+
DUNE_BASE_URL = "https://dune.com"
4852

4953
data = pd.DataFrame()
5054

@@ -66,6 +70,8 @@ def download(url, start=None, end=None, **kwargs):
6670
data = scraper.glassnode._download(url, **kwargs)
6771
elif url.startswith(THEBLOCK_BASE_URL):
6872
data = scraper.theblock._download(url)
73+
elif url.startswith(DUNE_BASE_URL):
74+
data = scraper.dune._download(url)
6975
else:
7076
raise ValueError("Unsupported source. Find the list of supported websites here: https://chaindl.readthedocs.io/")
7177

chaindl/scraper/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
from .blockchain import _download
88
from .glassnode import _download
99
from .theblock import _download
10+
from .dune import _download

chaindl/scraper/dune.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import pandas as pd
2+
import cloudscraper
3+
from urllib.parse import urlparse
4+
5+
def _download(url: str) -> pd.DataFrame:
6+
# Parse the query ID from URL
7+
parsed = urlparse(url)
8+
parts = [part for part in parsed.path.split("/") if part]
9+
if len(parts) < 2 or parts[0] != "queries":
10+
raise ValueError("URL is not a valid Dune query URL. Follow the guide at: https://chaindl.readthedocs.io/#dune-dune-com")
11+
query_id = int(parts[1])
12+
13+
scraper = cloudscraper.create_scraper()
14+
15+
# Get latest result set ID
16+
graphql_url = "https://dune.com/public/graphql"
17+
payload = {
18+
"operationName": "GetLatestResultSetIds",
19+
"variables": {"queryId": query_id, "parameters": [], "canRefresh": True},
20+
"query": """
21+
query GetLatestResultSetIds($canRefresh: Boolean!, $queryId: Int!, $parameters: [ExecutionParameterInput!]) {
22+
resultSetForQuery(canRefresh: $canRefresh, queryId: $queryId, parameters: $parameters) {
23+
completedExecutionId
24+
failedExecutionId
25+
pendingExecutionId
26+
__typename
27+
}
28+
}
29+
"""
30+
}
31+
32+
response = scraper.post(graphql_url, json=payload)
33+
if response.status_code != 200:
34+
raise ConnectionError(f"Failed to get result set ID: {response.status_code}")
35+
json_data = response.json()
36+
execution_id = json_data["data"]["resultSetForQuery"]["completedExecutionId"]
37+
if execution_id is None:
38+
raise ValueError("No completed execution found for this query.")
39+
40+
# Fetch all execution data with pagination
41+
all_data = []
42+
offset = 0
43+
limit = 9999999
44+
45+
execution_url = "https://core-api.dune.com/public/execution"
46+
headers = {
47+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0",
48+
"Accept": "*/*",
49+
"Accept-Language": "en-US,en;q=0.5",
50+
"Content-Type": "application/json",
51+
"Origin": "https://dune.com",
52+
"Referer": "https://dune.com/",
53+
"Connection": "keep-alive",
54+
"Pragma": "no-cache",
55+
"Cache-Control": "no-cache",
56+
}
57+
58+
while True:
59+
payload = {
60+
"execution_id": execution_id,
61+
"query_id": query_id,
62+
"parameters": [],
63+
"pagination": {"limit": limit, "offset": offset}
64+
}
65+
response = scraper.post(execution_url, headers=headers, json=payload)
66+
if response.status_code != 200:
67+
raise ConnectionError(f"Failed to fetch execution data: {response.status_code}")
68+
json_data = response.json()
69+
execution_result = json_data.get('execution_succeeded')
70+
if not execution_result:
71+
raise ValueError("Execution failed or no data returned.")
72+
73+
data = execution_result['data']
74+
total_row_count = execution_result['total_row_count']
75+
all_data.extend(data)
76+
77+
if len(all_data) >= total_row_count:
78+
break
79+
offset = len(all_data)
80+
81+
# Convert to DataFrame
82+
df = pd.DataFrame(all_data)
83+
return df

docs/index.rst

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ It supports:
3434
- `Blockchain.com <https://www.blockchain.com/explorer/charts/>`__
3535
- `Glassnode <https://studio.glassnode.com/charts/>`__
3636
- `The Block <https://www.theblock.co/data/>`__
37+
- `Dune <https://dune.com/>`__
3738

3839
---
3940

@@ -192,7 +193,7 @@ Example:
192193
url = "https://studio.glassnode.com/charts/addresses.ActiveCount?a=BTC"
193194
df = chaindl.download(url)
194195
195-
The BLock (`theblock.co/data <https://www.theblock.co/data/>`__)
196+
The Block (`theblock.co/data <https://www.theblock.co/data/>`__)
196197
---------------------------------------------------------------------------
197198

198199
Click 'Share' and 'Copy Link' to get the URL of the respective metric.
@@ -204,6 +205,20 @@ Example:
204205
url = "https://www.theblock.co/data/crypto-markets/spot/total-exchange-volume-daily"
205206
df = chaindl.download(url)
206207
208+
Dune (`dune.com <https://dune.com/>`__)
209+
---------------------------------------
210+
211+
When on a Dune dashboard, you need to open the specific indicator/chart to access the queries page that shows the SQL and results. The URL should be in the format `https://dune.com/queries/{query_id}/{result_id}`.
212+
213+
.. image:: ../assets/dune.png
214+
215+
Example:
216+
217+
.. code-block:: python
218+
219+
url = "https://dune.com/queries/5583538/9204329"
220+
df = chaindl.download(url)
221+
207222
Optional Arguments
208223
==================
209224

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ dependencies = [
2828
"seleniumbase",
2929
"selenium",
3030
"selenium-wire",
31-
"blinker==1.7.0"
31+
"blinker==1.7.0",
32+
"cloudscraper"
3233
]
3334

3435
[project.urls]

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ blinker==1.7.0
88
python-dotenv
99
pytest
1010
pytest-mock
11-
pytest-xdist
11+
pytest-xdist
12+
cloudscraper

tests/test_dune.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import pytest
2+
import pandas as pd
3+
from chaindl.scraper.dune import _download
4+
5+
def test_download_dune():
6+
url = "https://dune.com/queries/3265994/5466888"
7+
df = _download(url)
8+
assert isinstance(df, pd.DataFrame)
9+
assert not df.empty
10+
assert "date" in df.columns
11+
assert "BTC_Price" in df.columns
12+
assert "mv_ratio" in df.columns
13+
14+
def test_download_dashboard():
15+
url = "https://dune.com/cryptokoryo/crypto-buy-signal"
16+
with pytest.raises(ValueError, match="URL is not a valid Dune query URL"):
17+
_download(url)

0 commit comments

Comments
 (0)