dexpaprika-sdk 0.2.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,34 @@
1
+ """
2
+ DexPaprika SDK for Python
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
4
+
5
+ A Python client library for the DexPaprika API,
6
+ providing access to token, pool, and DEX data
7
+ across multiple blockchain networks.
8
+
9
+ :copyright: (c) 2024 CoinPaprika
10
+ :license: MIT, see LICENSE for more details.
11
+ """
12
+
13
+ from .client import DexPaprikaClient
14
+ # Import models for easier access
15
+ from .models import (
16
+ Network, Dex, DexesResponse,
17
+ Token, Pool, PoolsResponse, TimeIntervalMetrics,
18
+ PoolDetails, OHLCVRecord, Transaction, TransactionsResponse,
19
+ TokenSummary, TokenDetails,
20
+ DexInfo, SearchResult,
21
+ Stats
22
+ )
23
+
24
+ __version__ = "0.2.0"
25
+ __all__ = [
26
+ "DexPaprikaClient",
27
+ # Models
28
+ "Network", "Dex", "DexesResponse",
29
+ "Token", "Pool", "PoolsResponse", "TimeIntervalMetrics",
30
+ "PoolDetails", "OHLCVRecord", "Transaction", "TransactionsResponse",
31
+ "TokenSummary", "TokenDetails",
32
+ "DexInfo", "SearchResult",
33
+ "Stats"
34
+ ]
@@ -0,0 +1,15 @@
1
+ from .base import BaseAPI
2
+ from .networks import NetworksAPI
3
+ from .pools import PoolsAPI
4
+ from .tokens import TokensAPI
5
+ from .search import SearchAPI
6
+ from .utils import UtilsAPI
7
+
8
+ __all__ = [
9
+ "BaseAPI",
10
+ "NetworksAPI",
11
+ "PoolsAPI",
12
+ "TokensAPI",
13
+ "SearchAPI",
14
+ "UtilsAPI",
15
+ ]
@@ -0,0 +1,225 @@
1
+ from typing import Any, Dict, List, Optional, Union, TYPE_CHECKING, Callable, TypeVar, Set
2
+ import hashlib
3
+ import json
4
+ from datetime import datetime, timedelta
5
+
6
+ if TYPE_CHECKING:
7
+ from ..client import DexPaprikaClient
8
+
9
+ T = TypeVar('T')
10
+
11
+ class CacheEntry:
12
+ """Class representing a cached response with an expiration time."""
13
+
14
+ def __init__(self, data: Any, expires_at: Optional[datetime] = None):
15
+ """
16
+ Initialize a new cache entry.
17
+
18
+ Args:
19
+ data: The data to cache
20
+ expires_at: The time when the cache entry expires
21
+ """
22
+ self.data = data
23
+ self.expires_at = expires_at
24
+
25
+ def is_expired(self) -> bool:
26
+ """
27
+ Check if the cache entry has expired.
28
+
29
+ Returns:
30
+ True if the cache entry has expired, False otherwise
31
+ """
32
+ return self.expires_at is not None and datetime.now() > self.expires_at
33
+
34
+
35
+ class BaseAPI:
36
+ """Base class for all API service classes."""
37
+
38
+ def __init__(self, client: "DexPaprikaClient"):
39
+ """
40
+ Initialize a new API service.
41
+
42
+ Args:
43
+ client: The DexPaprika client instance
44
+ """
45
+ self.client = client
46
+ self._cache: Dict[str, CacheEntry] = {} # TTL-based cache
47
+
48
+ # Default TTLs for different types of data
49
+ self._cache_ttls = {
50
+ "networks": timedelta(hours=24), # Network list rarely changes
51
+ "pools": timedelta(minutes=5), # Pool data changes frequently
52
+ "tokens": timedelta(minutes=10), # Token data changes moderately
53
+ "stats": timedelta(minutes=15), # Stats change moderately
54
+ "default": timedelta(minutes=5) # Default TTL for other endpoints
55
+ }
56
+
57
+ def _get_cache_key(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> str:
58
+ """
59
+ Generate a unique cache key for the request.
60
+
61
+ Args:
62
+ endpoint: API endpoint
63
+ params: Query parameters
64
+
65
+ Returns:
66
+ A unique cache key as a string
67
+ """
68
+ key_parts = [endpoint]
69
+ if params:
70
+ # Sort params to ensure consistent keys
71
+ sorted_params = json.dumps(params, sort_keys=True)
72
+ key_parts.append(sorted_params)
73
+
74
+ key_string = ":".join(key_parts)
75
+ return hashlib.md5(key_string.encode()).hexdigest()
76
+
77
+ def _get_ttl(self, endpoint: str) -> timedelta:
78
+ """
79
+ Get the TTL for a specific endpoint.
80
+
81
+ Args:
82
+ endpoint: API endpoint
83
+
84
+ Returns:
85
+ The TTL as a timedelta
86
+ """
87
+ for key, ttl in self._cache_ttls.items():
88
+ if key in endpoint:
89
+ return ttl
90
+ return self._cache_ttls["default"]
91
+
92
+ def _get(
93
+ self,
94
+ endpoint: str,
95
+ params: Optional[Dict[str, Any]] = None,
96
+ skip_cache: bool = False,
97
+ ttl: Optional[timedelta] = None
98
+ ) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
99
+ """
100
+ Make a GET request to the specified endpoint.
101
+
102
+ Args:
103
+ endpoint: API endpoint (e.g., "/networks")
104
+ params: Query parameters
105
+ skip_cache: Whether to skip the cache and force a fresh request
106
+ ttl: Custom TTL for this request
107
+
108
+ Returns:
109
+ Response data as a dictionary or list
110
+ """
111
+ if skip_cache:
112
+ return self.client.get(endpoint, params=params)
113
+
114
+ cache_key = self._get_cache_key(endpoint, params)
115
+ cache_entry = self._cache.get(cache_key)
116
+
117
+ # Return cached data if valid
118
+ if cache_entry and not cache_entry.is_expired():
119
+ return cache_entry.data
120
+
121
+ # Get fresh data
122
+ result = self.client.get(endpoint, params=params)
123
+
124
+ # Cache the result with appropriate TTL
125
+ if ttl is None:
126
+ ttl = self._get_ttl(endpoint)
127
+
128
+ expires_at = datetime.now() + ttl
129
+ self._cache[cache_key] = CacheEntry(result, expires_at)
130
+
131
+ return result
132
+
133
+ def _post(self, endpoint: str, data: Dict[str, Any], params: Optional[Dict[str, Any]] = None) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
134
+ """
135
+ Make a POST request to the specified endpoint.
136
+
137
+ Args:
138
+ endpoint: API endpoint
139
+ data: Request body
140
+ params: Query parameters
141
+
142
+ Returns:
143
+ Response data as a dictionary or list
144
+ """
145
+ return self.client.post(endpoint, data=data, params=params)
146
+
147
+ def _clean_params(self, params: Dict[str, Any]) -> Dict[str, Any]:
148
+ """Clean None values from params."""
149
+ return {k: v for k, v in params.items() if v is not None}
150
+
151
+ def _validate_required(self, param_name: str, value: Any) -> None:
152
+ """
153
+ Validate that a parameter is not None or empty.
154
+
155
+ Args:
156
+ param_name: Name of the parameter for error messages
157
+ value: Value to validate
158
+
159
+ Raises:
160
+ ValueError: If the value is None or empty
161
+ """
162
+ if value is None or (isinstance(value, str) and value.strip() == ""):
163
+ raise ValueError(f"{param_name} is required")
164
+
165
+ def _validate_enum(self, param_name: str, value: str, valid_values: Set[str]) -> None:
166
+ """
167
+ Validate that a parameter is one of a set of valid values.
168
+
169
+ Args:
170
+ param_name: Name of the parameter for error messages
171
+ value: Value to validate
172
+ valid_values: Set of valid values
173
+
174
+ Raises:
175
+ ValueError: If the value is not one of the valid values
176
+ """
177
+ if value is None:
178
+ return
179
+
180
+ if value not in valid_values:
181
+ raise ValueError(f"{param_name} must be one of: {', '.join(sorted(valid_values))}")
182
+
183
+ def _validate_range(self, param_name: str, value: Union[int, float], min_val: Optional[Union[int, float]] = None, max_val: Optional[Union[int, float]] = None) -> None:
184
+ """
185
+ Validate that a numeric parameter is within a specified range.
186
+
187
+ Args:
188
+ param_name: Name of the parameter for error messages
189
+ value: Value to validate
190
+ min_val: Minimum allowed value (inclusive)
191
+ max_val: Maximum allowed value (inclusive)
192
+
193
+ Raises:
194
+ ValueError: If the value is outside the specified range
195
+ """
196
+ if value is None:
197
+ return
198
+
199
+ if min_val is not None and value < min_val:
200
+ raise ValueError(f"{param_name} must be at least {min_val}")
201
+
202
+ if max_val is not None and value > max_val:
203
+ raise ValueError(f"{param_name} must be at most {max_val}")
204
+
205
+ def clear_cache(self, endpoint_prefix: Optional[str] = None) -> None:
206
+ """
207
+ Clear the cache, optionally only for endpoints with a specific prefix.
208
+
209
+ Args:
210
+ endpoint_prefix: Optional prefix to filter which cache entries to clear
211
+ """
212
+ if endpoint_prefix:
213
+ # Get cache keys from the original endpoints that contain the prefix
214
+ keys_to_remove = []
215
+ for key in list(self._cache.keys()):
216
+ cache_entry = self._cache[key]
217
+ if endpoint_prefix in key:
218
+ keys_to_remove.append(key)
219
+
220
+ # Remove the entries
221
+ for key in keys_to_remove:
222
+ del self._cache[key]
223
+ else:
224
+ # Clear the entire cache
225
+ self._cache.clear()
@@ -0,0 +1,35 @@
1
+ from typing import Optional
2
+
3
+ from .base import BaseAPI
4
+ from ..models.networks import DexesResponse
5
+
6
+
7
+ class DexesAPI(BaseAPI):
8
+ """API service for DEX-related endpoints."""
9
+
10
+ def list(self, network: str, page: int = 0, limit: int = 10) -> DexesResponse:
11
+ """
12
+ Get a list of available decentralized exchanges on a specific network.
13
+
14
+ Args:
15
+ network: Network ID (e.g., "ethereum", "solana")
16
+ page: Page number for pagination
17
+ limit: Number of items per page
18
+
19
+ Returns:
20
+ Response containing list of DEXes
21
+
22
+ Raises:
23
+ ValueError: If any parameter is invalid
24
+ """
25
+ # Validate parameters
26
+ self._validate_required("network", network)
27
+ self._validate_range("page", page, min_val=0)
28
+ self._validate_range("limit", limit, min_val=1, max_val=100)
29
+
30
+ params = {
31
+ "page": page,
32
+ "limit": limit
33
+ }
34
+ data = self._get(f"/networks/{network}/dexes", params=params)
35
+ return DexesResponse(**data)
@@ -0,0 +1,45 @@
1
+ from typing import List, Optional, Set
2
+
3
+ from .base import BaseAPI
4
+ from ..models.networks import Network, DexesResponse
5
+
6
+
7
+ class NetworksAPI(BaseAPI):
8
+ """API service for network-related endpoints."""
9
+
10
+ def list(self) -> List[Network]:
11
+ """
12
+ Retrieve a list of all supported blockchain networks.
13
+
14
+ Returns:
15
+ List of Network objects
16
+ """
17
+ data = self._get("/networks")
18
+ return [Network(**item) for item in data]
19
+
20
+ def list_dexes(self, network_id: str, page: int = 0, limit: int = 10) -> DexesResponse:
21
+ """
22
+ Get a list of all available dexes on a specific network.
23
+
24
+ Args:
25
+ network_id: Network ID (e.g., "ethereum", "solana")
26
+ page: Page number for pagination
27
+ limit: Number of items per page
28
+
29
+ Returns:
30
+ Response containing a list of DEXes
31
+
32
+ Raises:
33
+ ValueError: If any parameter is invalid
34
+ """
35
+ # Validate parameters
36
+ self._validate_required("network_id", network_id)
37
+ self._validate_range("page", page, min_val=0)
38
+ self._validate_range("limit", limit, min_val=1, max_val=100)
39
+
40
+ params = {
41
+ "page": page,
42
+ "limit": limit,
43
+ }
44
+ data = self._get(f"/networks/{network_id}/dexes", params=params)
45
+ return DexesResponse(**data)
@@ -0,0 +1,251 @@
1
+ from typing import List, Optional, Dict, Any, Set
2
+
3
+ from .base import BaseAPI
4
+ from ..models.pools import (
5
+ PoolsResponse, PoolDetails, OHLCVRecord, TransactionsResponse
6
+ )
7
+
8
+
9
+ class PoolsAPI(BaseAPI):
10
+ """API service for pool-related endpoints."""
11
+
12
+ # Valid values for common parameters
13
+ VALID_SORT_VALUES: Set[str] = {"asc", "desc"}
14
+ VALID_ORDER_BY_VALUES: Set[str] = {"volume_usd", "price_usd", "transactions", "last_price_change_usd_24h", "created_at"}
15
+ VALID_INTERVAL_VALUES: Set[str] = {"1m", "5m", "10m", "15m", "30m", "1h", "6h", "12h", "24h"}
16
+
17
+ def list(
18
+ self,
19
+ page: int = 0,
20
+ limit: int = 10,
21
+ sort: str = "desc",
22
+ order_by: str = "volume_usd"
23
+ ) -> PoolsResponse:
24
+ """
25
+ Get a list of top pools across all networks.
26
+
27
+ Args:
28
+ page: Page number for pagination
29
+ limit: Number of items per page
30
+ sort: Sort order ("asc" or "desc")
31
+ order_by: Field to order by ("volume_usd", "price_usd", etc.)
32
+
33
+ Returns:
34
+ Response containing a list of pools
35
+
36
+ Raises:
37
+ ValueError: If any parameter is invalid
38
+ """
39
+ # Validate parameters
40
+ self._validate_range("page", page, min_val=0)
41
+ self._validate_range("limit", limit, min_val=1, max_val=100)
42
+ self._validate_enum("sort", sort, self.VALID_SORT_VALUES)
43
+ self._validate_enum("order_by", order_by, self.VALID_ORDER_BY_VALUES)
44
+
45
+ # Get top pools
46
+ params = {"page": page, "limit": limit, "sort": sort, "order_by": order_by}
47
+ data = self._get("/pools", params=params)
48
+
49
+ # ensure pools exists
50
+ if 'pools' not in data: data['pools'] = []
51
+
52
+ return PoolsResponse(**data)
53
+
54
+ def list_by_network(
55
+ self,
56
+ network_id: str,
57
+ page: int = 0,
58
+ limit: int = 10,
59
+ sort: str = "desc",
60
+ order_by: str = "volume_usd"
61
+ ) -> PoolsResponse:
62
+ """
63
+ Get a list of pools on a specific network.
64
+
65
+ Args:
66
+ network_id: Network ID (e.g., "ethereum", "solana")
67
+ page: Page number for pagination
68
+ limit: Number of items per page
69
+ sort: Sort order ("asc" or "desc")
70
+ order_by: Field to order by ("volume_usd", "price_usd", etc.)
71
+
72
+ Returns:
73
+ Response containing a list of pools
74
+
75
+ Raises:
76
+ ValueError: If any parameter is invalid
77
+ """
78
+ # Validate parameters
79
+ self._validate_required("network_id", network_id)
80
+ self._validate_range("page", page, min_val=0)
81
+ self._validate_range("limit", limit, min_val=1, max_val=100)
82
+ self._validate_enum("sort", sort, self.VALID_SORT_VALUES)
83
+ self._validate_enum("order_by", order_by, self.VALID_ORDER_BY_VALUES)
84
+
85
+ # Get network pools
86
+ params = {"page": page, "limit": limit, "sort": sort, "order_by": order_by}
87
+ data = self._get(f"/networks/{network_id}/pools", params=params)
88
+
89
+ # ensure pools exists
90
+ if 'pools' not in data: data['pools'] = []
91
+
92
+ return PoolsResponse(**data)
93
+
94
+ def list_by_dex(
95
+ self,
96
+ network_id: str,
97
+ dex_id: str,
98
+ page: int = 0,
99
+ limit: int = 10,
100
+ sort: str = "desc",
101
+ order_by: str = "volume_usd"
102
+ ) -> PoolsResponse:
103
+ """
104
+ Get a list of pools for a specific DEX on a network.
105
+
106
+ Args:
107
+ network_id: Network ID (e.g., "ethereum", "solana")
108
+ dex_id: DEX ID (e.g., "uniswap_v3")
109
+ page: Page number for pagination
110
+ limit: Number of items per page
111
+ sort: Sort order ("asc" or "desc")
112
+ order_by: Field to order by ("volume_usd", "price_usd", etc.)
113
+
114
+ Returns:
115
+ Response containing a list of pools
116
+
117
+ Raises:
118
+ ValueError: If any parameter is invalid
119
+ """
120
+ # Validate parameters
121
+ self._validate_required("network_id", network_id)
122
+ self._validate_required("dex_id", dex_id)
123
+ self._validate_range("page", page, min_val=0)
124
+ self._validate_range("limit", limit, min_val=1, max_val=100)
125
+ self._validate_enum("sort", sort, self.VALID_SORT_VALUES)
126
+ self._validate_enum("order_by", order_by, self.VALID_ORDER_BY_VALUES)
127
+
128
+ # Get dex pools
129
+ params = {"page": page, "limit": limit, "sort": sort, "order_by": order_by}
130
+ data = self._get(f"/networks/{network_id}/dexes/{dex_id}/pools", params=params)
131
+
132
+ # ensure pools exists
133
+ if 'pools' not in data: data['pools'] = []
134
+
135
+ return PoolsResponse(**data)
136
+
137
+ def get_details(
138
+ self,
139
+ network_id: str,
140
+ pool_address: str,
141
+ inversed: bool = False
142
+ ) -> PoolDetails:
143
+ """
144
+ Get detailed information about a specific pool.
145
+
146
+ Args:
147
+ network_id: Network ID (e.g., "ethereum", "solana")
148
+ pool_address: Pool address or identifier
149
+ inversed: Whether to invert the price ratio
150
+
151
+ Returns:
152
+ Detailed pool information
153
+
154
+ Raises:
155
+ ValueError: If any parameter is invalid
156
+ """
157
+ # Validate parameters
158
+ self._validate_required("network_id", network_id)
159
+ self._validate_required("pool_address", pool_address)
160
+
161
+ # Get pool details
162
+ params = {"inversed": "true" if inversed else None}
163
+ params = self._clean_params(params)
164
+
165
+ data = self._get(f"/networks/{network_id}/pools/{pool_address}", params=params)
166
+ return PoolDetails(**data)
167
+
168
+ def get_ohlcv(
169
+ self,
170
+ network_id: str,
171
+ pool_address: str,
172
+ start: str,
173
+ end: Optional[str] = None,
174
+ limit: int = 1,
175
+ interval: str = "24h",
176
+ inversed: bool = False
177
+ ) -> List[OHLCVRecord]:
178
+ """
179
+ Get OHLCV (Open-High-Low-Close-Volume) data for a specific pool.
180
+
181
+ Args:
182
+ network_id: Network ID (e.g., "ethereum", "solana")
183
+ pool_address: Pool address or identifier
184
+ start: Start time for historical data (ISO-8601, yyyy-mm-dd, or Unix timestamp)
185
+ end: End time for historical data (max 1 year from start)
186
+ limit: Number of data points to retrieve (max 366)
187
+ interval: Interval granularity for OHLCV data (1m, 5m, 10m, 15m, 30m, 1h, 6h, 12h, 24h)
188
+ inversed: Whether to invert the price ratio in OHLCV calculations
189
+
190
+ Returns:
191
+ List of OHLCV records
192
+
193
+ Raises:
194
+ ValueError: If any parameter is invalid
195
+ """
196
+ # Validate parameters
197
+ self._validate_required("network_id", network_id)
198
+ self._validate_required("pool_address", pool_address)
199
+ self._validate_required("start", start)
200
+ self._validate_range("limit", limit, min_val=1, max_val=366)
201
+ self._validate_enum("interval", interval, self.VALID_INTERVAL_VALUES)
202
+
203
+ # Get price history
204
+ params = {
205
+ "start": start,
206
+ "end": end,
207
+ "limit": limit,
208
+ "interval": interval,
209
+ "inversed": "true" if inversed else None,
210
+ }
211
+ params = self._clean_params(params)
212
+
213
+ data = self._get(f"/networks/{network_id}/pools/{pool_address}/ohlcv", params=params)
214
+ return [OHLCVRecord(**item) for item in data]
215
+
216
+ def get_transactions(
217
+ self,
218
+ network_id: str,
219
+ pool_address: str,
220
+ page: int = 0,
221
+ limit: int = 10,
222
+ cursor: Optional[str] = None
223
+ ) -> TransactionsResponse:
224
+ """
225
+ Get transactions of a pool on a network.
226
+
227
+ Args:
228
+ network_id: Network ID (e.g., "ethereum", "solana")
229
+ pool_address: Pool address or identifier
230
+ page: Page number for pagination
231
+ limit: Number of items per page
232
+ cursor: Transaction ID used for cursor-based pagination
233
+
234
+ Returns:
235
+ Response containing a list of transactions
236
+
237
+ Raises:
238
+ ValueError: If any parameter is invalid
239
+ """
240
+ # Validate parameters
241
+ self._validate_required("network_id", network_id)
242
+ self._validate_required("pool_address", pool_address)
243
+ self._validate_range("page", page, min_val=0)
244
+ self._validate_range("limit", limit, min_val=1, max_val=100)
245
+
246
+ # Get txs
247
+ params = {"page": page, "limit": limit, "cursor": cursor}
248
+ params = self._clean_params(params)
249
+
250
+ data = self._get(f"/networks/{network_id}/pools/{pool_address}/transactions", params=params)
251
+ return TransactionsResponse(**data)
@@ -0,0 +1,28 @@
1
+ from urllib.parse import quote
2
+
3
+ from .base import BaseAPI
4
+ from ..models.search import SearchResult
5
+
6
+
7
+ class SearchAPI(BaseAPI):
8
+ """API service for search-related endpoints."""
9
+
10
+ def search(self, query: str) -> SearchResult:
11
+ """
12
+ Search for tokens, pools, and DEXes by name or identifier.
13
+
14
+ Args:
15
+ query: Search term (e.g., "uniswap", "bitcoin", or a token address)
16
+
17
+ Returns:
18
+ Search results across tokens, pools, and DEXes
19
+
20
+ Raises:
21
+ ValueError: If the query parameter is invalid
22
+ """
23
+ # Validate parameters
24
+ self._validate_required("query", query)
25
+
26
+ params = {"query": query}
27
+ data = self._get("/search", params=params)
28
+ return SearchResult(**data)