defillama-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,71 @@
1
+ """DefiLlama Python SDK entry point."""
2
+
3
+ from typing import Optional
4
+
5
+ from .client import BaseClient, DefiLlamaConfig
6
+ from .errors import ApiError, ApiKeyRequiredError, DefiLlamaError, NotFoundError, RateLimitError
7
+ from .modules.account import AccountModule
8
+ from .modules.bridges import BridgesModule
9
+ from .modules.dat import DatModule
10
+ from .modules.ecosystem import EcosystemModule
11
+ from .modules.emissions import EmissionsModule
12
+ from .modules.etfs import EtfsModule
13
+ from .modules.fees import FeesModule
14
+ from .modules.prices import PricesModule
15
+ from .modules.stablecoins import StablecoinsModule
16
+ from .modules.tvl import TvlModule
17
+ from .modules.volumes import VolumesModule
18
+ from .modules.yields import YieldsModule
19
+
20
+
21
+ class DefiLlama:
22
+ """DefiLlama API client for accessing DeFi data."""
23
+
24
+ def __init__(self, config: Optional[DefiLlamaConfig] = None) -> None:
25
+ """Create a new DefiLlama client."""
26
+
27
+ self._client = BaseClient(config)
28
+ self.tvl = TvlModule(self._client)
29
+ self.prices = PricesModule(self._client)
30
+ self.stablecoins = StablecoinsModule(self._client)
31
+ self.yields = YieldsModule(self._client)
32
+ self.volumes = VolumesModule(self._client)
33
+ self.fees = FeesModule(self._client)
34
+ self.emissions = EmissionsModule(self._client)
35
+ self.bridges = BridgesModule(self._client)
36
+ self.ecosystem = EcosystemModule(self._client)
37
+ self.etfs = EtfsModule(self._client)
38
+ self.dat = DatModule(self._client)
39
+ self.account = AccountModule(self._client)
40
+
41
+ @property
42
+ def isPro(self) -> bool:
43
+ """Return True when the client has a Pro API key configured."""
44
+
45
+ return self._client.has_api_key
46
+
47
+
48
+ __all__ = [
49
+ "DefiLlama",
50
+ "DefiLlamaConfig",
51
+ "ApiError",
52
+ "ApiKeyRequiredError",
53
+ "DefiLlamaError",
54
+ "NotFoundError",
55
+ "RateLimitError",
56
+ "TvlModule",
57
+ "PricesModule",
58
+ "StablecoinsModule",
59
+ "YieldsModule",
60
+ "VolumesModule",
61
+ "FeesModule",
62
+ "EmissionsModule",
63
+ "BridgesModule",
64
+ "EcosystemModule",
65
+ "EtfsModule",
66
+ "DatModule",
67
+ "AccountModule",
68
+ ]
69
+
70
+ from .constants import *
71
+ from .types import *
@@ -0,0 +1,157 @@
1
+ """HTTP client for the DefiLlama API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, Mapping, Optional, Literal, TypedDict
6
+
7
+ import requests
8
+
9
+ from .errors import ApiError, ApiKeyRequiredError, NotFoundError, RateLimitError
10
+
11
+
12
+ class DefiLlamaConfig(TypedDict, total=False):
13
+ api_key: str
14
+ apiKey: str
15
+ timeout: int
16
+
17
+
18
+ BaseUrl = Literal["main", "v2", "bridges", "coins", "stablecoins"]
19
+
20
+ BASE_URLS: Dict[str, str] = {
21
+ "main": "https://api.llama.fi",
22
+ "v2": "https://api.llama.fi/v2",
23
+ "pro": "https://pro-api.llama.fi",
24
+ "bridges": "https://bridges.llama.fi",
25
+ "coins": "https://coins.llama.fi",
26
+ "stablecoins": "https://stablecoins.llama.fi",
27
+ }
28
+
29
+
30
+ class BaseClient:
31
+ """Low-level HTTP client used by all modules."""
32
+
33
+ def __init__(self, config: Optional[Mapping[str, Any]] = None) -> None:
34
+ config = config or {}
35
+ api_key = None
36
+ if "api_key" in config:
37
+ api_key = config.get("api_key")
38
+ elif "apiKey" in config:
39
+ api_key = config.get("apiKey")
40
+ self._api_key = api_key
41
+ timeout_ms = config.get("timeout", 30000)
42
+ self._timeout = timeout_ms / 1000.0
43
+
44
+ @property
45
+ def has_api_key(self) -> bool:
46
+ """Return True when a Pro API key is configured."""
47
+
48
+ return bool(self._api_key)
49
+
50
+ @property
51
+ def timeout(self) -> float:
52
+ """Return the configured request timeout in seconds."""
53
+
54
+ return self._timeout
55
+
56
+ def get_api_key(self) -> Optional[str]:
57
+ """Return the configured API key, if any."""
58
+
59
+ return self._api_key
60
+
61
+ def _build_url(
62
+ self,
63
+ endpoint: str,
64
+ requires_auth: bool,
65
+ base: BaseUrl = "main",
66
+ api_namespace: str = "api",
67
+ ) -> str:
68
+ if base == "v2":
69
+ if not self._api_key:
70
+ raise ApiKeyRequiredError(endpoint)
71
+ return f"{BASE_URLS['pro']}/{self._api_key}/api/v2{endpoint}"
72
+ if base == "bridges":
73
+ if not self._api_key:
74
+ raise ApiKeyRequiredError(endpoint)
75
+ return f"{BASE_URLS['pro']}/{self._api_key}/bridges{endpoint}"
76
+ if base == "coins":
77
+ return f"{BASE_URLS['coins']}{endpoint}"
78
+ if base == "stablecoins":
79
+ return f"{BASE_URLS['stablecoins']}{endpoint}"
80
+ if requires_auth:
81
+ if not self._api_key:
82
+ raise ApiKeyRequiredError(endpoint)
83
+ namespace = f"/{api_namespace}" if api_namespace else ""
84
+ return f"{BASE_URLS['pro']}/{self._api_key}{namespace}{endpoint}"
85
+ return f"{BASE_URLS['main']}{endpoint}"
86
+
87
+ def _handle_response(self, response: requests.Response, endpoint: str) -> Any:
88
+ if response.ok:
89
+ return response.json()
90
+ text = response.text
91
+ if response.status_code == 404:
92
+ raise NotFoundError(endpoint)
93
+ if response.status_code == 429:
94
+ retry_after = response.headers.get("Retry-After")
95
+ retry_value = int(retry_after) if retry_after and retry_after.isdigit() else None
96
+ raise RateLimitError(retry_value)
97
+ try:
98
+ error_body = response.json()
99
+ except ValueError:
100
+ error_body = text
101
+ raise ApiError(
102
+ response.status_code,
103
+ f"API request failed: {response.reason}",
104
+ error_body,
105
+ )
106
+
107
+ def _filter_params(self, params: Optional[Mapping[str, Any]]) -> Optional[Dict[str, Any]]:
108
+ if not params:
109
+ return None
110
+ filtered: Dict[str, Any] = {}
111
+ for key, value in params.items():
112
+ if value is not None:
113
+ filtered[key] = value
114
+ return filtered or None
115
+
116
+ def get(
117
+ self,
118
+ endpoint: str,
119
+ *,
120
+ params: Optional[Mapping[str, Any]] = None,
121
+ requires_auth: bool = False,
122
+ base: BaseUrl = "main",
123
+ api_namespace: str = "api",
124
+ ) -> Any:
125
+ """Perform a GET request and return parsed JSON."""
126
+
127
+ url = self._build_url(endpoint, requires_auth, base, api_namespace)
128
+ response = requests.get(
129
+ url,
130
+ params=self._filter_params(params),
131
+ timeout=self._timeout,
132
+ headers={"Accept": "application/json"},
133
+ )
134
+ return self._handle_response(response, endpoint)
135
+
136
+ def post(
137
+ self,
138
+ endpoint: str,
139
+ body: Any,
140
+ *,
141
+ requires_auth: bool = False,
142
+ base: BaseUrl = "main",
143
+ api_namespace: str = "api",
144
+ ) -> Any:
145
+ """Perform a POST request and return parsed JSON."""
146
+
147
+ url = self._build_url(endpoint, requires_auth, base, api_namespace)
148
+ response = requests.post(
149
+ url,
150
+ json=body,
151
+ timeout=self._timeout,
152
+ headers={
153
+ "Accept": "application/json",
154
+ "Content-Type": "application/json",
155
+ },
156
+ )
157
+ return self._handle_response(response, endpoint)
@@ -0,0 +1,11 @@
1
+ """Constants for DefiLlama API dimensions."""
2
+
3
+ from .dimensions import AdapterType, FeeDataType, VolumeDataType, DataTypeShortKeys, DataTypeShortKey
4
+
5
+ __all__ = [
6
+ "AdapterType",
7
+ "FeeDataType",
8
+ "VolumeDataType",
9
+ "DataTypeShortKeys",
10
+ "DataTypeShortKey",
11
+ ]
@@ -0,0 +1,69 @@
1
+ """Dimension constants used by fees and volume endpoints."""
2
+
3
+ from enum import Enum
4
+ from typing import Dict, Literal
5
+
6
+
7
+ class AdapterType(str, Enum):
8
+ DEXS = "dexs"
9
+ FEES = "fees"
10
+ AGGREGATORS = "aggregators"
11
+ DERIVATIVES = "derivatives"
12
+ AGGREGATOR_DERIVATIVES = "aggregator-derivatives"
13
+ OPTIONS = "options"
14
+ BRIDGE_AGGREGATORS = "bridge-aggregators"
15
+ OPEN_INTEREST = "open-interest"
16
+
17
+
18
+ class FeeDataType(str, Enum):
19
+ DAILY_FEES = "dailyFees"
20
+ DAILY_REVENUE = "dailyRevenue"
21
+ DAILY_HOLDERS_REVENUE = "dailyHoldersRevenue"
22
+ DAILY_SUPPLY_SIDE_REVENUE = "dailySupplySideRevenue"
23
+ DAILY_BRIBES_REVENUE = "dailyBribesRevenue"
24
+ DAILY_TOKEN_TAXES = "dailyTokenTaxes"
25
+ DAILY_APP_FEES = "dailyAppFees"
26
+ DAILY_APP_REVENUE = "dailyAppRevenue"
27
+ DAILY_EARNINGS = "dailyEarnings"
28
+
29
+
30
+ class VolumeDataType(str, Enum):
31
+ DAILY_VOLUME = "dailyVolume"
32
+ TOTAL_VOLUME = "totalVolume"
33
+ DAILY_NOTIONAL_VOLUME = "dailyNotionalVolume"
34
+ DAILY_PREMIUM_VOLUME = "dailyPremiumVolume"
35
+ DAILY_BRIDGE_VOLUME = "dailyBridgeVolume"
36
+ OPEN_INTEREST_AT_END = "openInterestAtEnd"
37
+
38
+
39
+ DataTypeShortKeys: Dict[str, str] = {
40
+ "dailyFees": "df",
41
+ "dailyRevenue": "dr",
42
+ "dailyHoldersRevenue": "dhr",
43
+ "dailySupplySideRevenue": "dssr",
44
+ "dailyBribesRevenue": "dbr",
45
+ "dailyTokenTaxes": "dtt",
46
+ "dailyAppRevenue": "dar",
47
+ "dailyAppFees": "daf",
48
+ "dailyNotionalVolume": "dnv",
49
+ "dailyPremiumVolume": "dpv",
50
+ "openInterestAtEnd": "doi",
51
+ "dailyVolume": "dv",
52
+ "dailyBridgeVolume": "dbv",
53
+ }
54
+
55
+ DataTypeShortKey = Literal[
56
+ "dailyFees",
57
+ "dailyRevenue",
58
+ "dailyHoldersRevenue",
59
+ "dailySupplySideRevenue",
60
+ "dailyBribesRevenue",
61
+ "dailyTokenTaxes",
62
+ "dailyAppRevenue",
63
+ "dailyAppFees",
64
+ "dailyNotionalVolume",
65
+ "dailyPremiumVolume",
66
+ "openInterestAtEnd",
67
+ "dailyVolume",
68
+ "dailyBridgeVolume",
69
+ ]
@@ -0,0 +1,41 @@
1
+ """Error types for the DefiLlama SDK."""
2
+
3
+ from typing import Optional
4
+
5
+
6
+ class DefiLlamaError(Exception):
7
+ """Base error class for DefiLlama SDK errors."""
8
+
9
+ def __init__(self, message: str) -> None:
10
+ super().__init__(message)
11
+
12
+
13
+ class ApiKeyRequiredError(DefiLlamaError):
14
+ """Raised when a Pro API key is required for an endpoint."""
15
+
16
+ def __init__(self, endpoint: str) -> None:
17
+ super().__init__(f"API key required for endpoint: {endpoint}")
18
+
19
+
20
+ class RateLimitError(DefiLlamaError):
21
+ """Raised when the API rate limit is exceeded."""
22
+
23
+ def __init__(self, retry_after: Optional[int] = None) -> None:
24
+ super().__init__("Rate limit exceeded")
25
+ self.retry_after = retry_after
26
+
27
+
28
+ class NotFoundError(DefiLlamaError):
29
+ """Raised when a requested resource is not found."""
30
+
31
+ def __init__(self, resource: str) -> None:
32
+ super().__init__(f"Resource not found: {resource}")
33
+
34
+
35
+ class ApiError(DefiLlamaError):
36
+ """Raised for non-success API responses."""
37
+
38
+ def __init__(self, status_code: int, message: str, response: Optional[object] = None) -> None:
39
+ super().__init__(message)
40
+ self.status_code = status_code
41
+ self.response = response
@@ -0,0 +1,29 @@
1
+ """Module exports for the DefiLlama SDK."""
2
+
3
+ from .account import AccountModule
4
+ from .bridges import BridgesModule
5
+ from .dat import DatModule
6
+ from .ecosystem import EcosystemModule
7
+ from .emissions import EmissionsModule
8
+ from .etfs import EtfsModule
9
+ from .fees import FeesModule
10
+ from .prices import PricesModule
11
+ from .stablecoins import StablecoinsModule
12
+ from .tvl import TvlModule
13
+ from .volumes import VolumesModule
14
+ from .yields import YieldsModule
15
+
16
+ __all__ = [
17
+ "AccountModule",
18
+ "BridgesModule",
19
+ "DatModule",
20
+ "EcosystemModule",
21
+ "EmissionsModule",
22
+ "EtfsModule",
23
+ "FeesModule",
24
+ "PricesModule",
25
+ "StablecoinsModule",
26
+ "TvlModule",
27
+ "VolumesModule",
28
+ "YieldsModule",
29
+ ]
@@ -0,0 +1,29 @@
1
+ """Account module implementation."""
2
+
3
+ import requests
4
+
5
+ from ..client import BaseClient
6
+ from ..errors import ApiKeyRequiredError, ApiError
7
+ from ..types.account import UsageResponse
8
+
9
+
10
+ class AccountModule:
11
+ """Access API account and usage data."""
12
+
13
+ def __init__(self, client: BaseClient) -> None:
14
+ self._client = client
15
+
16
+ def getUsage(self) -> UsageResponse:
17
+ """Get API usage statistics for the configured key."""
18
+
19
+ api_key = self._client.get_api_key()
20
+ if not api_key:
21
+ raise ApiKeyRequiredError("/usage")
22
+ response = requests.get(
23
+ f"https://pro-api.llama.fi/usage/{api_key}",
24
+ headers={"Accept": "application/json"},
25
+ timeout=self._client.timeout,
26
+ )
27
+ if not response.ok:
28
+ raise ApiError(response.status_code, f"Failed to fetch usage: {response.reason}")
29
+ return response.json()
@@ -0,0 +1,81 @@
1
+ """Bridges module implementation."""
2
+
3
+ from typing import List, Optional
4
+ from urllib.parse import quote
5
+
6
+ from ..client import BaseClient
7
+ from ..types.bridges import (
8
+ BridgeDayStatsResponse,
9
+ BridgeDetail,
10
+ BridgeTransaction,
11
+ BridgeTransactionsOptions,
12
+ BridgeVolumeDataPoint,
13
+ BridgesOptions,
14
+ BridgesResponse,
15
+ )
16
+
17
+
18
+ class BridgesModule:
19
+ """Access bridge volume and transaction data."""
20
+
21
+ def __init__(self, client: BaseClient) -> None:
22
+ self._client = client
23
+
24
+ def getAll(self, options: Optional[BridgesOptions] = None) -> BridgesResponse:
25
+ """Get all bridges with volume data."""
26
+
27
+ params = None
28
+ if options and options.get("includeChains") is not None:
29
+ params = {"includeChains": options.get("includeChains")}
30
+ return self._client.get(
31
+ "/bridges",
32
+ base="bridges",
33
+ params=params,
34
+ )
35
+
36
+ def getById(self, bridge_id: int) -> BridgeDetail:
37
+ """Get detailed bridge information by ID."""
38
+
39
+ return self._client.get(
40
+ f"/bridge/{bridge_id}",
41
+ base="bridges",
42
+ )
43
+
44
+ def getVolumeByChain(self, chain: str) -> List[BridgeVolumeDataPoint]:
45
+ """Get bridge volume for a chain."""
46
+
47
+ return self._client.get(
48
+ f"/bridgevolume/{quote(chain)}",
49
+ base="bridges",
50
+ )
51
+
52
+ def getDayStats(self, timestamp: int, chain: str) -> BridgeDayStatsResponse:
53
+ """Get daily bridge statistics for a chain."""
54
+
55
+ return self._client.get(
56
+ f"/bridgedaystats/{timestamp}/{quote(chain)}",
57
+ base="bridges",
58
+ )
59
+
60
+ def getTransactions(
61
+ self, bridge_id: int, options: Optional[BridgeTransactionsOptions] = None
62
+ ) -> List[BridgeTransaction]:
63
+ """Get bridge transactions."""
64
+
65
+ params = {}
66
+ if options:
67
+ if options.get("limit") is not None:
68
+ params["limit"] = options.get("limit")
69
+ if options.get("startTimestamp") is not None:
70
+ params["startTimestamp"] = options.get("startTimestamp")
71
+ if options.get("endTimestamp") is not None:
72
+ params["endTimestamp"] = options.get("endTimestamp")
73
+ if options.get("sourceChain"):
74
+ params["sourceChain"] = options.get("sourceChain")
75
+ if options.get("address"):
76
+ params["address"] = options.get("address")
77
+ return self._client.get(
78
+ f"/transactions/{bridge_id}",
79
+ base="bridges",
80
+ params=params or None,
81
+ )
@@ -0,0 +1,31 @@
1
+ """DAT module implementation."""
2
+
3
+ from urllib.parse import quote
4
+
5
+ from ..client import BaseClient
6
+ from ..types.dat import DatInstitutionResponse, DatInstitutionsResponse
7
+
8
+
9
+ class DatModule:
10
+ """Access Digital Asset Treasury data from DefiLlama."""
11
+
12
+ def __init__(self, client: BaseClient) -> None:
13
+ self._client = client
14
+
15
+ def getInstitutions(self) -> DatInstitutionsResponse:
16
+ """Get comprehensive DAT data for all institutions."""
17
+
18
+ return self._client.get(
19
+ "/dat/institutions",
20
+ requires_auth=True,
21
+ api_namespace="",
22
+ )
23
+
24
+ def getInstitution(self, symbol: str) -> DatInstitutionResponse:
25
+ """Get detailed DAT data for a specific institution."""
26
+
27
+ return self._client.get(
28
+ f"/dat/institutions/{quote(symbol)}",
29
+ requires_auth=True,
30
+ api_namespace="",
31
+ )
@@ -0,0 +1,54 @@
1
+ """Ecosystem module implementation."""
2
+
3
+ from ..client import BaseClient
4
+ from ..types.ecosystem import (
5
+ CategoriesResponse,
6
+ Entity,
7
+ ForksResponse,
8
+ Hack,
9
+ OraclesResponse,
10
+ RaisesResponse,
11
+ Treasury,
12
+ )
13
+
14
+
15
+ class EcosystemModule:
16
+ """Access ecosystem-level data from DefiLlama."""
17
+
18
+ def __init__(self, client: BaseClient) -> None:
19
+ self._client = client
20
+
21
+ def getCategories(self) -> CategoriesResponse:
22
+ """Get TVL grouped by protocol category."""
23
+
24
+ return self._client.get("/categories", requires_auth=True)
25
+
26
+ def getForks(self) -> ForksResponse:
27
+ """Get protocol fork relationships and TVL."""
28
+
29
+ return self._client.get("/forks", requires_auth=True)
30
+
31
+ def getOracles(self) -> OraclesResponse:
32
+ """Get oracle usage data across protocols."""
33
+
34
+ return self._client.get("/oracles", requires_auth=True)
35
+
36
+ def getEntities(self) -> list[Entity]:
37
+ """Get entity treasury and holdings data."""
38
+
39
+ return self._client.get("/entities", requires_auth=True)
40
+
41
+ def getTreasuries(self) -> list[Treasury]:
42
+ """Get protocol treasury balances."""
43
+
44
+ return self._client.get("/treasuries", requires_auth=True)
45
+
46
+ def getHacks(self) -> list[Hack]:
47
+ """Get security incidents and exploit data."""
48
+
49
+ return self._client.get("/hacks", requires_auth=True)
50
+
51
+ def getRaises(self) -> RaisesResponse:
52
+ """Get funding rounds database."""
53
+
54
+ return self._client.get("/raises", requires_auth=True)
@@ -0,0 +1,32 @@
1
+ """Emissions module implementation."""
2
+
3
+ import json
4
+ from urllib.parse import quote
5
+
6
+ from ..client import BaseClient
7
+ from ..types.emissions import EmissionDetailResponse, EmissionToken
8
+
9
+
10
+ class EmissionsModule:
11
+ """Access emission and unlock schedule data."""
12
+
13
+ def __init__(self, client: BaseClient) -> None:
14
+ self._client = client
15
+
16
+ def getAll(self) -> list[EmissionToken]:
17
+ """Get all tokens with unlock schedules."""
18
+
19
+ return self._client.get("/emissions", requires_auth=True)
20
+
21
+ def getByProtocol(self, protocol: str) -> EmissionDetailResponse:
22
+ """Get detailed emission data for a protocol."""
23
+
24
+ response = self._client.get(
25
+ f"/emission/{quote(protocol)}",
26
+ requires_auth=True,
27
+ )
28
+ body = json.loads(response.get("body", "{}"))
29
+ return {
30
+ "body": body,
31
+ "lastModified": response.get("lastModified"),
32
+ }
@@ -0,0 +1,58 @@
1
+ """ETFs module implementation."""
2
+
3
+ from urllib.parse import quote
4
+
5
+ from ..client import BaseClient
6
+ from ..types.etfs import EtfHistoryItem, EtfOverviewItem, FdvPeriod, FdvPerformanceItem
7
+
8
+
9
+ class EtfsModule:
10
+ """Access ETF data from DefiLlama."""
11
+
12
+ def __init__(self, client: BaseClient) -> None:
13
+ self._client = client
14
+
15
+ def getOverview(self) -> list[EtfOverviewItem]:
16
+ """Get Bitcoin ETF overview."""
17
+
18
+ return self._client.get(
19
+ "/overview",
20
+ requires_auth=True,
21
+ api_namespace="etfs",
22
+ )
23
+
24
+ def getOverviewEth(self) -> list[EtfOverviewItem]:
25
+ """Get Ethereum ETF overview."""
26
+
27
+ return self._client.get(
28
+ "/overviewEth",
29
+ requires_auth=True,
30
+ api_namespace="etfs",
31
+ )
32
+
33
+ def getHistory(self) -> list[EtfHistoryItem]:
34
+ """Get Bitcoin ETF flow history."""
35
+
36
+ return self._client.get(
37
+ "/history",
38
+ requires_auth=True,
39
+ api_namespace="etfs",
40
+ )
41
+
42
+ def getHistoryEth(self) -> list[EtfHistoryItem]:
43
+ """Get Ethereum ETF flow history."""
44
+
45
+ return self._client.get(
46
+ "/historyEth",
47
+ requires_auth=True,
48
+ api_namespace="etfs",
49
+ )
50
+
51
+ def getFdvPerformance(self, period: FdvPeriod) -> list[FdvPerformanceItem]:
52
+ """Get FDV performance data."""
53
+
54
+ return self._client.get(
55
+ f"/performance/{quote(period)}",
56
+ requires_auth=True,
57
+ api_namespace="fdv",
58
+ )