dexscreen 0.0.2__py3-none-any.whl → 0.0.5__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.
- dexscreen/__init__.py +87 -0
- dexscreen/api/client.py +275 -42
- dexscreen/core/exceptions.py +1067 -0
- dexscreen/core/http.py +859 -117
- dexscreen/core/validators.py +542 -0
- dexscreen/stream/polling.py +288 -78
- dexscreen/utils/__init__.py +54 -1
- dexscreen/utils/filters.py +182 -12
- dexscreen/utils/logging_config.py +421 -0
- dexscreen/utils/middleware.py +363 -0
- dexscreen/utils/ratelimit.py +212 -8
- dexscreen/utils/retry.py +357 -0
- {dexscreen-0.0.2.dist-info → dexscreen-0.0.5.dist-info}/METADATA +52 -1
- dexscreen-0.0.5.dist-info/RECORD +22 -0
- dexscreen-0.0.2.dist-info/RECORD +0 -17
- {dexscreen-0.0.2.dist-info → dexscreen-0.0.5.dist-info}/WHEEL +0 -0
- {dexscreen-0.0.2.dist-info → dexscreen-0.0.5.dist-info}/licenses/LICENSE +0 -0
dexscreen/__init__.py
CHANGED
@@ -5,6 +5,50 @@ A modern, stable, and reliable Python SDK for DexScreener API with HTTP support.
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
from .api.client import DexscreenerClient
|
8
|
+
from .core.exceptions import (
|
9
|
+
# API errors
|
10
|
+
APIError,
|
11
|
+
APILimitError,
|
12
|
+
AuthenticationError,
|
13
|
+
# Configuration errors
|
14
|
+
ConfigurationError,
|
15
|
+
ConnectionError,
|
16
|
+
DataFormatError,
|
17
|
+
# Base exceptions
|
18
|
+
DexscreenError,
|
19
|
+
FilterConfigError,
|
20
|
+
HttpConnectionError,
|
21
|
+
# HTTP errors
|
22
|
+
HttpError,
|
23
|
+
HttpRequestError,
|
24
|
+
HttpResponseParsingError,
|
25
|
+
HttpSessionError,
|
26
|
+
HttpTimeoutError,
|
27
|
+
InvalidAddressError,
|
28
|
+
InvalidChainError,
|
29
|
+
InvalidConfigError,
|
30
|
+
InvalidResponseError,
|
31
|
+
MissingConfigError,
|
32
|
+
MissingDataError,
|
33
|
+
# Network errors
|
34
|
+
NetworkError,
|
35
|
+
ProxyError,
|
36
|
+
RateLimitError,
|
37
|
+
ServerError,
|
38
|
+
StreamConnectionError,
|
39
|
+
StreamDataError,
|
40
|
+
# Streaming errors
|
41
|
+
StreamError,
|
42
|
+
StreamTimeoutError,
|
43
|
+
SubscriptionError,
|
44
|
+
TimeoutError,
|
45
|
+
# Validation errors
|
46
|
+
ValidationError,
|
47
|
+
# Utility functions
|
48
|
+
get_error_category,
|
49
|
+
is_retryable_error,
|
50
|
+
should_wait_before_retry,
|
51
|
+
)
|
8
52
|
from .core.models import (
|
9
53
|
BaseToken,
|
10
54
|
Liquidity,
|
@@ -18,14 +62,57 @@ from .utils.filters import FilterConfig, FilterPresets
|
|
18
62
|
|
19
63
|
__version__ = "1.0.0"
|
20
64
|
__all__ = [
|
65
|
+
# API errors
|
66
|
+
"APIError",
|
67
|
+
"APILimitError",
|
68
|
+
"AuthenticationError",
|
69
|
+
# Core models and client
|
21
70
|
"BaseToken",
|
71
|
+
# Configuration errors
|
72
|
+
"ConfigurationError",
|
73
|
+
"ConnectionError",
|
74
|
+
"DataFormatError",
|
75
|
+
# Base exceptions
|
76
|
+
"DexscreenError",
|
22
77
|
"DexscreenerClient",
|
23
78
|
"FilterConfig",
|
79
|
+
"FilterConfigError",
|
24
80
|
"FilterPresets",
|
81
|
+
"HttpConnectionError",
|
82
|
+
# HTTP errors
|
83
|
+
"HttpError",
|
84
|
+
"HttpRequestError",
|
85
|
+
"HttpResponseParsingError",
|
86
|
+
"HttpSessionError",
|
87
|
+
"HttpTimeoutError",
|
88
|
+
"InvalidAddressError",
|
89
|
+
"InvalidChainError",
|
90
|
+
"InvalidConfigError",
|
91
|
+
"InvalidResponseError",
|
25
92
|
"Liquidity",
|
93
|
+
"MissingConfigError",
|
94
|
+
"MissingDataError",
|
95
|
+
# Network errors
|
96
|
+
"NetworkError",
|
26
97
|
"PairTransactionCounts",
|
27
98
|
"PriceChangePeriods",
|
99
|
+
"ProxyError",
|
100
|
+
"RateLimitError",
|
101
|
+
"ServerError",
|
102
|
+
"StreamConnectionError",
|
103
|
+
"StreamDataError",
|
104
|
+
# Streaming errors
|
105
|
+
"StreamError",
|
106
|
+
"StreamTimeoutError",
|
107
|
+
"SubscriptionError",
|
108
|
+
"TimeoutError",
|
28
109
|
"TokenPair",
|
29
110
|
"TransactionCount",
|
111
|
+
# Validation errors
|
112
|
+
"ValidationError",
|
30
113
|
"VolumeChangePeriods",
|
114
|
+
# Utility functions
|
115
|
+
"get_error_category",
|
116
|
+
"is_retryable_error",
|
117
|
+
"should_wait_before_retry",
|
31
118
|
]
|
dexscreen/api/client.py
CHANGED
@@ -3,12 +3,26 @@ Simplified Dexscreener client with clean API
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
import asyncio
|
6
|
+
import logging
|
6
7
|
from typing import Any, Callable, Optional, Union
|
7
8
|
|
9
|
+
from ..core.exceptions import (
|
10
|
+
HttpError,
|
11
|
+
)
|
8
12
|
from ..core.http import HttpClientCffi
|
9
13
|
from ..core.models import OrderInfo, TokenInfo, TokenPair
|
14
|
+
from ..core.validators import (
|
15
|
+
validate_address,
|
16
|
+
validate_addresses_list,
|
17
|
+
validate_chain_id,
|
18
|
+
validate_dict_config,
|
19
|
+
validate_string,
|
20
|
+
)
|
10
21
|
from ..stream.polling import PollingStream
|
11
22
|
from ..utils.filters import FilterConfig, TokenPairFilter
|
23
|
+
from ..utils.logging_config import get_contextual_logger, log_function_call, with_correlation_id
|
24
|
+
|
25
|
+
logger = logging.getLogger(__name__)
|
12
26
|
|
13
27
|
|
14
28
|
class DexscreenerClient:
|
@@ -27,7 +41,17 @@ class DexscreenerClient:
|
|
27
41
|
Args:
|
28
42
|
impersonate: Browser to impersonate (default: None, uses random market-share based selection)
|
29
43
|
client_kwargs: Optional kwargs to pass to curl_cffi clients.
|
44
|
+
|
45
|
+
Raises:
|
46
|
+
InvalidTypeError: If parameters have invalid types
|
47
|
+
InvalidParameterError: If impersonate value is invalid
|
30
48
|
"""
|
49
|
+
# Validate inputs
|
50
|
+
if impersonate is not None:
|
51
|
+
impersonate = validate_string(impersonate, "impersonate", 1, 50)
|
52
|
+
|
53
|
+
client_kwargs = validate_dict_config(client_kwargs, "client_kwargs", allow_none=True)
|
54
|
+
|
31
55
|
# Setup client kwargs
|
32
56
|
self.client_kwargs = client_kwargs or {}
|
33
57
|
# Use provided impersonate or our custom realworld browser selection
|
@@ -54,33 +78,144 @@ class DexscreenerClient:
|
|
54
78
|
# Filters for each subscription
|
55
79
|
self._filters: dict[str, TokenPairFilter] = {}
|
56
80
|
|
81
|
+
# Enhanced logging
|
82
|
+
self.contextual_logger = get_contextual_logger(__name__)
|
83
|
+
|
84
|
+
init_context = {
|
85
|
+
"impersonate": impersonate,
|
86
|
+
"has_client_kwargs": bool(client_kwargs),
|
87
|
+
"client_kwargs_keys": list(client_kwargs.keys()) if client_kwargs else [],
|
88
|
+
}
|
89
|
+
|
90
|
+
self.contextual_logger.debug("DexscreenerClient initialized", context=init_context)
|
91
|
+
|
57
92
|
# ========== Single Query Methods ==========
|
58
93
|
|
94
|
+
@with_correlation_id()
|
95
|
+
@log_function_call(log_args=True, log_result=False)
|
59
96
|
def get_pair(self, address: str) -> Optional[TokenPair]:
|
60
|
-
"""
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
97
|
+
"""
|
98
|
+
Get a single token pair by address using search.
|
99
|
+
|
100
|
+
Args:
|
101
|
+
address: Token pair address to search for
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
TokenPair object if found, None otherwise
|
105
|
+
|
106
|
+
Raises:
|
107
|
+
InvalidAddressError: If address format is invalid
|
108
|
+
HttpError: If HTTP request fails (connection, timeout, etc.)
|
109
|
+
"""
|
110
|
+
# Validate input
|
111
|
+
address = validate_address(address)
|
112
|
+
|
113
|
+
search_context: dict[str, Any] = {
|
114
|
+
"operation": "get_pair",
|
115
|
+
"address": address[:10] + "..." if len(address) > 10 else address,
|
116
|
+
"method": "search_workaround",
|
117
|
+
}
|
118
|
+
|
119
|
+
self.contextual_logger.debug("Getting pair by address", context=search_context)
|
120
|
+
|
121
|
+
try:
|
122
|
+
# Since the API requires chain ID, we use search as a workaround
|
123
|
+
resp = self._client_300rpm.request("GET", f"latest/dex/search?q={address}")
|
124
|
+
if resp is not None and isinstance(resp, dict) and "pairs" in resp and len(resp["pairs"]) > 0:
|
125
|
+
pairs_found = len(resp["pairs"])
|
126
|
+
search_context["pairs_found"] = pairs_found
|
127
|
+
|
128
|
+
# Return the first matching pair
|
129
|
+
for pair in resp["pairs"]:
|
130
|
+
if pair.get("pairAddress", "").lower() == address.lower():
|
131
|
+
search_context["exact_match"] = True
|
132
|
+
self.contextual_logger.debug("Found exact pair match", context=search_context)
|
133
|
+
return TokenPair(**pair)
|
134
|
+
|
135
|
+
# If no exact match, return the first result
|
136
|
+
search_context["exact_match"] = False
|
137
|
+
search_context["using_first_result"] = True
|
138
|
+
self.contextual_logger.debug("No exact match, using first result", context=search_context)
|
139
|
+
return TokenPair(**resp["pairs"][0])
|
140
|
+
else:
|
141
|
+
search_context["pairs_found"] = 0
|
142
|
+
search_context["response_valid"] = resp is not None
|
143
|
+
self.contextual_logger.warning("No pairs found for address", context=search_context)
|
144
|
+
return None
|
145
|
+
except HttpError as e:
|
146
|
+
error_context = search_context.copy()
|
147
|
+
error_context["error_type"] = type(e).__name__
|
148
|
+
error_context["error_message"] = str(e)
|
149
|
+
|
150
|
+
self.contextual_logger.error(
|
151
|
+
"Failed to get pair for address: %s", str(e), context=error_context, exc_info=True
|
152
|
+
)
|
71
153
|
|
154
|
+
logger.error("Failed to get pair for address %s: %s", address, e) # Keep original logging
|
155
|
+
raise # Re-raise the HTTP error for caller to handle
|
156
|
+
|
157
|
+
@with_correlation_id()
|
158
|
+
@log_function_call(log_args=True, log_result=False)
|
72
159
|
async def get_pair_async(self, address: str) -> Optional[TokenPair]:
|
73
|
-
"""
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
160
|
+
"""
|
161
|
+
Async version of get_pair.
|
162
|
+
|
163
|
+
Args:
|
164
|
+
address: Token pair address to search for
|
165
|
+
|
166
|
+
Returns:
|
167
|
+
TokenPair object if found, None otherwise
|
168
|
+
|
169
|
+
Raises:
|
170
|
+
InvalidAddressError: If address format is invalid
|
171
|
+
HttpError: If HTTP request fails (connection, timeout, etc.)
|
172
|
+
"""
|
173
|
+
# Validate input
|
174
|
+
address = validate_address(address)
|
175
|
+
|
176
|
+
search_context: dict[str, Any] = {
|
177
|
+
"operation": "get_pair_async",
|
178
|
+
"address": address[:10] + "..." if len(address) > 10 else address,
|
179
|
+
"method": "search_workaround",
|
180
|
+
}
|
181
|
+
|
182
|
+
self.contextual_logger.debug("Getting pair by address (async)", context=search_context)
|
183
|
+
|
184
|
+
try:
|
185
|
+
# Since the API requires chain ID, we use search as a workaround
|
186
|
+
resp = await self._client_300rpm.request_async("GET", f"latest/dex/search?q={address}")
|
187
|
+
if resp is not None and isinstance(resp, dict) and "pairs" in resp and len(resp["pairs"]) > 0:
|
188
|
+
pairs_found = len(resp["pairs"])
|
189
|
+
search_context["pairs_found"] = pairs_found
|
190
|
+
|
191
|
+
# Return the first matching pair
|
192
|
+
for pair in resp["pairs"]:
|
193
|
+
if pair.get("pairAddress", "").lower() == address.lower():
|
194
|
+
search_context["exact_match"] = True
|
195
|
+
self.contextual_logger.debug("Found exact pair match (async)", context=search_context)
|
196
|
+
return TokenPair(**pair)
|
197
|
+
|
198
|
+
# If no exact match, return the first result
|
199
|
+
search_context["exact_match"] = False
|
200
|
+
search_context["using_first_result"] = True
|
201
|
+
self.contextual_logger.debug("No exact match, using first result (async)", context=search_context)
|
202
|
+
return TokenPair(**resp["pairs"][0])
|
203
|
+
else:
|
204
|
+
search_context["pairs_found"] = 0
|
205
|
+
search_context["response_valid"] = resp is not None
|
206
|
+
self.contextual_logger.warning("No pairs found for address (async)", context=search_context)
|
207
|
+
return None
|
208
|
+
except HttpError as e:
|
209
|
+
error_context = search_context.copy()
|
210
|
+
error_context["error_type"] = type(e).__name__
|
211
|
+
error_context["error_message"] = str(e)
|
212
|
+
|
213
|
+
self.contextual_logger.error(
|
214
|
+
"Failed to get pair for address (async): %s", str(e), context=error_context, exc_info=True
|
215
|
+
)
|
216
|
+
|
217
|
+
logger.error("Failed to get pair for address %s: %s", address, e) # Keep original logging
|
218
|
+
raise # Re-raise the HTTP error for caller to handle
|
84
219
|
|
85
220
|
def get_pairs_by_pairs_addresses(self, chain_id: str, pair_addresses: list[str]) -> list[TokenPair]:
|
86
221
|
"""
|
@@ -96,14 +231,19 @@ class DexscreenerClient:
|
|
96
231
|
List of TokenPair objects
|
97
232
|
|
98
233
|
Raises:
|
99
|
-
|
234
|
+
InvalidChainIdError: If chain ID is invalid
|
235
|
+
InvalidAddressError: If any address format is invalid
|
236
|
+
TooManyItemsError: If more than 30 pair addresses are provided
|
100
237
|
"""
|
238
|
+
# Validate inputs
|
239
|
+
chain_id = validate_chain_id(chain_id)
|
240
|
+
pair_addresses = validate_addresses_list(
|
241
|
+
pair_addresses, "pair_addresses", min_count=0, max_count=self.MAX_PAIRS_PER_REQUEST, chain_id=chain_id
|
242
|
+
)
|
243
|
+
|
101
244
|
if not pair_addresses:
|
102
245
|
return []
|
103
246
|
|
104
|
-
if len(pair_addresses) > self.MAX_PAIRS_PER_REQUEST:
|
105
|
-
raise ValueError(f"Maximum {self.MAX_PAIRS_PER_REQUEST} pair addresses allowed, got {len(pair_addresses)}")
|
106
|
-
|
107
247
|
addresses_str = ",".join(pair_addresses)
|
108
248
|
resp = self._client_300rpm.request("GET", f"latest/dex/pairs/{chain_id}/{addresses_str}")
|
109
249
|
if resp is None:
|
@@ -126,14 +266,19 @@ class DexscreenerClient:
|
|
126
266
|
List of TokenPair objects
|
127
267
|
|
128
268
|
Raises:
|
129
|
-
|
269
|
+
InvalidChainIdError: If chain ID is invalid
|
270
|
+
InvalidAddressError: If any address format is invalid
|
271
|
+
TooManyItemsError: If more than 30 pair addresses are provided
|
130
272
|
"""
|
273
|
+
# Validate inputs
|
274
|
+
chain_id = validate_chain_id(chain_id)
|
275
|
+
pair_addresses = validate_addresses_list(
|
276
|
+
pair_addresses, "pair_addresses", min_count=0, max_count=self.MAX_PAIRS_PER_REQUEST, chain_id=chain_id
|
277
|
+
)
|
278
|
+
|
131
279
|
if not pair_addresses:
|
132
280
|
return []
|
133
281
|
|
134
|
-
if len(pair_addresses) > self.MAX_PAIRS_PER_REQUEST:
|
135
|
-
raise ValueError(f"Maximum {self.MAX_PAIRS_PER_REQUEST} pair addresses allowed, got {len(pair_addresses)}")
|
136
|
-
|
137
282
|
addresses_str = ",".join(pair_addresses)
|
138
283
|
resp = await self._client_300rpm.request_async("GET", f"latest/dex/pairs/{chain_id}/{addresses_str}")
|
139
284
|
if resp is None:
|
@@ -154,6 +299,11 @@ class DexscreenerClient:
|
|
154
299
|
|
155
300
|
def search_pairs(self, query: str) -> list[TokenPair]:
|
156
301
|
"""Search for pairs by query"""
|
302
|
+
from ..core.validators import validate_query_string
|
303
|
+
|
304
|
+
# Validate query string
|
305
|
+
query = validate_query_string(query, max_length=100)
|
306
|
+
|
157
307
|
resp = self._client_300rpm.request("GET", f"latest/dex/search?q={query}")
|
158
308
|
if resp is not None and isinstance(resp, dict):
|
159
309
|
return [TokenPair(**pair) for pair in resp.get("pairs", [])]
|
@@ -161,6 +311,11 @@ class DexscreenerClient:
|
|
161
311
|
|
162
312
|
async def search_pairs_async(self, query: str) -> list[TokenPair]:
|
163
313
|
"""Async version of search_pairs"""
|
314
|
+
from ..core.validators import validate_query_string
|
315
|
+
|
316
|
+
# Validate query string
|
317
|
+
query = validate_query_string(query, max_length=100)
|
318
|
+
|
164
319
|
resp = await self._client_300rpm.request_async("GET", f"latest/dex/search?q={query}")
|
165
320
|
if resp is not None and isinstance(resp, dict):
|
166
321
|
return [TokenPair(**pair) for pair in resp.get("pairs", [])]
|
@@ -210,6 +365,10 @@ class DexscreenerClient:
|
|
210
365
|
|
211
366
|
def get_orders_paid_of_token(self, chain_id: str, token_address: str) -> list[OrderInfo]:
|
212
367
|
"""Get orders for a token"""
|
368
|
+
# Validate inputs
|
369
|
+
chain_id = validate_chain_id(chain_id)
|
370
|
+
token_address = validate_address(token_address, chain_id)
|
371
|
+
|
213
372
|
resp = self._client_60rpm.request("GET", f"orders/v1/{chain_id}/{token_address}")
|
214
373
|
if resp is not None:
|
215
374
|
return [OrderInfo(**order) for order in resp]
|
@@ -217,6 +376,10 @@ class DexscreenerClient:
|
|
217
376
|
|
218
377
|
async def get_orders_paid_of_token_async(self, chain_id: str, token_address: str) -> list[OrderInfo]:
|
219
378
|
"""Async version of get_orders_paid_of_token"""
|
379
|
+
# Validate inputs
|
380
|
+
chain_id = validate_chain_id(chain_id)
|
381
|
+
token_address = validate_address(token_address, chain_id)
|
382
|
+
|
220
383
|
resp = await self._client_60rpm.request_async("GET", f"orders/v1/{chain_id}/{token_address}")
|
221
384
|
if resp is not None:
|
222
385
|
return [OrderInfo(**order) for order in resp]
|
@@ -224,6 +387,10 @@ class DexscreenerClient:
|
|
224
387
|
|
225
388
|
def get_pairs_by_token_address(self, chain_id: str, token_address: str) -> list[TokenPair]:
|
226
389
|
"""Get all pairs for a single token address on a specific chain"""
|
390
|
+
# Validate inputs
|
391
|
+
chain_id = validate_chain_id(chain_id)
|
392
|
+
token_address = validate_address(token_address, chain_id)
|
393
|
+
|
227
394
|
# Use the correct endpoint format: /tokens/v1/{chain}/{address}
|
228
395
|
resp = self._client_300rpm.request("GET", f"tokens/v1/{chain_id}/{token_address}")
|
229
396
|
if resp is None:
|
@@ -236,6 +403,10 @@ class DexscreenerClient:
|
|
236
403
|
|
237
404
|
async def get_pairs_by_token_address_async(self, chain_id: str, token_address: str) -> list[TokenPair]:
|
238
405
|
"""Async version of get_pairs_by_token_address"""
|
406
|
+
# Validate inputs
|
407
|
+
chain_id = validate_chain_id(chain_id)
|
408
|
+
token_address = validate_address(token_address, chain_id)
|
409
|
+
|
239
410
|
# Use the correct endpoint format: /tokens/v1/{chain}/{address}
|
240
411
|
resp = await self._client_300rpm.request_async("GET", f"tokens/v1/{chain_id}/{token_address}")
|
241
412
|
if resp is None:
|
@@ -261,16 +432,19 @@ class DexscreenerClient:
|
|
261
432
|
List of TokenPair objects (maximum 30 pairs)
|
262
433
|
|
263
434
|
Raises:
|
264
|
-
|
435
|
+
InvalidChainIdError: If chain ID is invalid
|
436
|
+
InvalidAddressError: If any address format is invalid
|
437
|
+
TooManyItemsError: If more than 30 token addresses are provided
|
265
438
|
"""
|
439
|
+
# Validate inputs
|
440
|
+
chain_id = validate_chain_id(chain_id)
|
441
|
+
token_addresses = validate_addresses_list(
|
442
|
+
token_addresses, "token_addresses", min_count=0, max_count=self.MAX_TOKENS_PER_REQUEST, chain_id=chain_id
|
443
|
+
)
|
444
|
+
|
266
445
|
if not token_addresses:
|
267
446
|
return []
|
268
447
|
|
269
|
-
if len(token_addresses) > self.MAX_TOKENS_PER_REQUEST:
|
270
|
-
raise ValueError(
|
271
|
-
f"Maximum {self.MAX_TOKENS_PER_REQUEST} token addresses allowed, got {len(token_addresses)}"
|
272
|
-
)
|
273
|
-
|
274
448
|
if len(token_addresses) == 1:
|
275
449
|
# For single token, use the single token method
|
276
450
|
return self.get_pairs_by_token_address(chain_id, token_addresses[0])
|
@@ -297,6 +471,10 @@ class DexscreenerClient:
|
|
297
471
|
|
298
472
|
def get_pools_by_token_address(self, chain_id: str, token_address: str) -> list[TokenPair]:
|
299
473
|
"""Get pools info using token-pairs/v1 endpoint (Pool endpoint)"""
|
474
|
+
# Validate inputs
|
475
|
+
chain_id = validate_chain_id(chain_id)
|
476
|
+
token_address = validate_address(token_address, chain_id)
|
477
|
+
|
300
478
|
# Use the token-pairs/v1 endpoint
|
301
479
|
resp = self._client_300rpm.request("GET", f"token-pairs/v1/{chain_id}/{token_address}")
|
302
480
|
if resp is None:
|
@@ -309,6 +487,10 @@ class DexscreenerClient:
|
|
309
487
|
|
310
488
|
async def get_pools_by_token_address_async(self, chain_id: str, token_address: str) -> list[TokenPair]:
|
311
489
|
"""Async version of get_pools_by_token_address"""
|
490
|
+
# Validate inputs
|
491
|
+
chain_id = validate_chain_id(chain_id)
|
492
|
+
token_address = validate_address(token_address, chain_id)
|
493
|
+
|
312
494
|
# Use the token-pairs/v1 endpoint
|
313
495
|
resp = await self._client_300rpm.request_async("GET", f"token-pairs/v1/{chain_id}/{token_address}")
|
314
496
|
if resp is None:
|
@@ -334,16 +516,19 @@ class DexscreenerClient:
|
|
334
516
|
List of TokenPair objects (maximum 30 pairs)
|
335
517
|
|
336
518
|
Raises:
|
337
|
-
|
519
|
+
InvalidChainIdError: If chain ID is invalid
|
520
|
+
InvalidAddressError: If any address format is invalid
|
521
|
+
TooManyItemsError: If more than 30 token addresses are provided
|
338
522
|
"""
|
523
|
+
# Validate inputs
|
524
|
+
chain_id = validate_chain_id(chain_id)
|
525
|
+
token_addresses = validate_addresses_list(
|
526
|
+
token_addresses, "token_addresses", min_count=0, max_count=self.MAX_TOKENS_PER_REQUEST, chain_id=chain_id
|
527
|
+
)
|
528
|
+
|
339
529
|
if not token_addresses:
|
340
530
|
return []
|
341
531
|
|
342
|
-
if len(token_addresses) > self.MAX_TOKENS_PER_REQUEST:
|
343
|
-
raise ValueError(
|
344
|
-
f"Maximum {self.MAX_TOKENS_PER_REQUEST} token addresses allowed, got {len(token_addresses)}"
|
345
|
-
)
|
346
|
-
|
347
532
|
if len(token_addresses) == 1:
|
348
533
|
# For single token, use the single token method
|
349
534
|
return await self.get_pairs_by_token_address_async(chain_id, token_addresses[0])
|
@@ -411,6 +596,22 @@ class DexscreenerClient:
|
|
411
596
|
config = FilterConfig(price_change_threshold=0.02)
|
412
597
|
await client.subscribe_pairs("ethereum", ["0x..."], callback, filter=config)
|
413
598
|
"""
|
599
|
+
# Import validators at the top of the method to avoid circular imports
|
600
|
+
from ..core.validators import (
|
601
|
+
validate_addresses_list,
|
602
|
+
validate_callback,
|
603
|
+
validate_chain_id,
|
604
|
+
validate_filter_config,
|
605
|
+
validate_interval,
|
606
|
+
)
|
607
|
+
|
608
|
+
# Validate inputs before any async operations
|
609
|
+
chain_id = validate_chain_id(chain_id)
|
610
|
+
pair_addresses = validate_addresses_list(pair_addresses, "pair_addresses", chain_id=chain_id)
|
611
|
+
callback = validate_callback(callback)
|
612
|
+
filter = validate_filter_config(filter)
|
613
|
+
interval = validate_interval(interval)
|
614
|
+
|
414
615
|
# Handle single pair address for backward compatibility
|
415
616
|
for pair_address in pair_addresses:
|
416
617
|
subscription_key = f"{chain_id}:{pair_address}"
|
@@ -455,6 +656,7 @@ class DexscreenerClient:
|
|
455
656
|
|
456
657
|
actual_callback = filtered_callback
|
457
658
|
else:
|
659
|
+
# This should not happen as validate_filter_config already checks this
|
458
660
|
raise ValueError(f"Invalid filter type: {type(filter)}. Must be bool or FilterConfig")
|
459
661
|
|
460
662
|
# Store subscription info
|
@@ -498,6 +700,22 @@ class DexscreenerClient:
|
|
498
700
|
callback=handle_usdc_pairs
|
499
701
|
)
|
500
702
|
"""
|
703
|
+
# Import validators at the top of the method to avoid circular imports
|
704
|
+
from ..core.validators import (
|
705
|
+
validate_addresses_list,
|
706
|
+
validate_callback,
|
707
|
+
validate_chain_id,
|
708
|
+
validate_filter_config,
|
709
|
+
validate_interval,
|
710
|
+
)
|
711
|
+
|
712
|
+
# Validate inputs before any async operations
|
713
|
+
chain_id = validate_chain_id(chain_id)
|
714
|
+
token_addresses = validate_addresses_list(token_addresses, "token_addresses", chain_id=chain_id)
|
715
|
+
callback = validate_callback(callback)
|
716
|
+
filter = validate_filter_config(filter)
|
717
|
+
interval = validate_interval(interval)
|
718
|
+
|
501
719
|
# Handle multiple token addresses
|
502
720
|
for token_address in token_addresses:
|
503
721
|
subscription_key = f"token:{chain_id}:{token_address}"
|
@@ -550,6 +768,7 @@ class DexscreenerClient:
|
|
550
768
|
|
551
769
|
actual_callback = filtered_callback
|
552
770
|
else:
|
771
|
+
# This should not happen as validate_filter_config already checks this
|
553
772
|
raise ValueError(f"Invalid filter type: {type(filter)}. Must be bool or FilterConfig")
|
554
773
|
|
555
774
|
# Store subscription info
|
@@ -598,6 +817,13 @@ class DexscreenerClient:
|
|
598
817
|
|
599
818
|
async def unsubscribe_pairs(self, chain_id: str, pair_addresses: list[str]) -> None:
|
600
819
|
"""Unsubscribe from pair updates"""
|
820
|
+
# Import validators at the top of the method to avoid circular imports
|
821
|
+
from ..core.validators import validate_addresses_list, validate_chain_id
|
822
|
+
|
823
|
+
# Validate inputs
|
824
|
+
chain_id = validate_chain_id(chain_id)
|
825
|
+
pair_addresses = validate_addresses_list(pair_addresses, "pair_addresses", chain_id=chain_id)
|
826
|
+
|
601
827
|
for pair_address in pair_addresses:
|
602
828
|
subscription_key = f"{chain_id}:{pair_address}"
|
603
829
|
|
@@ -616,6 +842,13 @@ class DexscreenerClient:
|
|
616
842
|
|
617
843
|
async def unsubscribe_tokens(self, chain_id: str, token_addresses: list[str]) -> None:
|
618
844
|
"""Unsubscribe from token pair updates"""
|
845
|
+
# Import validators at the top of the method to avoid circular imports
|
846
|
+
from ..core.validators import validate_addresses_list, validate_chain_id
|
847
|
+
|
848
|
+
# Validate inputs
|
849
|
+
chain_id = validate_chain_id(chain_id)
|
850
|
+
token_addresses = validate_addresses_list(token_addresses, "token_addresses", chain_id=chain_id)
|
851
|
+
|
619
852
|
for token_address in token_addresses:
|
620
853
|
subscription_key = f"token:{chain_id}:{token_address}"
|
621
854
|
|