dexscreen 0.0.1__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 +31 -0
- dexscreen/api/__init__.py +3 -0
- dexscreen/api/client.py +672 -0
- dexscreen/config/__init__.py +0 -0
- dexscreen/core/__init__.py +27 -0
- dexscreen/core/http.py +460 -0
- dexscreen/core/models.py +106 -0
- dexscreen/stream/__init__.py +3 -0
- dexscreen/stream/polling.py +462 -0
- dexscreen/utils/__init__.py +4 -0
- dexscreen/utils/browser_selector.py +57 -0
- dexscreen/utils/filters.py +226 -0
- dexscreen/utils/ratelimit.py +65 -0
- dexscreen-0.0.1.dist-info/METADATA +278 -0
- dexscreen-0.0.1.dist-info/RECORD +17 -0
- dexscreen-0.0.1.dist-info/WHEEL +4 -0
- dexscreen-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,226 @@
|
|
1
|
+
"""
|
2
|
+
Token pair filtering utilities for reducing noise and controlling update frequency
|
3
|
+
"""
|
4
|
+
|
5
|
+
import time
|
6
|
+
from dataclasses import dataclass, field
|
7
|
+
from typing import Any, Optional
|
8
|
+
|
9
|
+
from ..core.models import TokenPair
|
10
|
+
|
11
|
+
|
12
|
+
@dataclass
|
13
|
+
class FilterConfig:
|
14
|
+
"""Configuration for token pair filtering"""
|
15
|
+
|
16
|
+
# Change detection fields - which fields to monitor for changes
|
17
|
+
change_fields: list[str] = field(default_factory=lambda: ["price_usd", "price_native", "volume.h24", "liquidity"])
|
18
|
+
|
19
|
+
# Significant change thresholds (None means any change triggers)
|
20
|
+
price_change_threshold: Optional[float] = None # e.g., 0.01 for 1%
|
21
|
+
volume_change_threshold: Optional[float] = None # e.g., 0.10 for 10%
|
22
|
+
liquidity_change_threshold: Optional[float] = None # e.g., 0.05 for 5%
|
23
|
+
|
24
|
+
# Rate limiting
|
25
|
+
max_updates_per_second: Optional[float] = None # e.g., 1.0 for max 1 update/sec
|
26
|
+
|
27
|
+
|
28
|
+
class TokenPairFilter:
|
29
|
+
"""Filter for token pair updates based on configuration"""
|
30
|
+
|
31
|
+
def __init__(self, config: Optional[FilterConfig] = None):
|
32
|
+
"""
|
33
|
+
Initialize filter with optional configuration.
|
34
|
+
If no config provided, acts as a simple change detector.
|
35
|
+
"""
|
36
|
+
self.config = config or FilterConfig()
|
37
|
+
self._cache: dict[str, dict[str, Any]] = {}
|
38
|
+
self._last_update_times: dict[str, float] = {}
|
39
|
+
|
40
|
+
def should_emit(self, key: str, pair: TokenPair) -> bool:
|
41
|
+
"""
|
42
|
+
Check if update should be emitted based on filter rules.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
key: Unique identifier for the subscription (e.g., "ethereum:0x...")
|
46
|
+
pair: The token pair data
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
True if update should be emitted, False otherwise
|
50
|
+
"""
|
51
|
+
# Check rate limiting first
|
52
|
+
if not self._check_rate_limit(key):
|
53
|
+
return False
|
54
|
+
|
55
|
+
# Check for changes
|
56
|
+
if not self._has_relevant_changes(key, pair):
|
57
|
+
return False
|
58
|
+
|
59
|
+
# Check if changes are significant enough
|
60
|
+
if not self._are_changes_significant(key, pair):
|
61
|
+
return False
|
62
|
+
|
63
|
+
# Update cache and emit
|
64
|
+
self._update_cache(key, pair)
|
65
|
+
return True
|
66
|
+
|
67
|
+
def _check_rate_limit(self, key: str) -> bool:
|
68
|
+
"""Check if rate limit allows this update"""
|
69
|
+
if self.config.max_updates_per_second is None:
|
70
|
+
return True
|
71
|
+
|
72
|
+
current_time = time.time()
|
73
|
+
last_update = self._last_update_times.get(key, 0)
|
74
|
+
|
75
|
+
min_interval = 1.0 / self.config.max_updates_per_second
|
76
|
+
if current_time - last_update < min_interval:
|
77
|
+
return False
|
78
|
+
|
79
|
+
self._last_update_times[key] = current_time
|
80
|
+
return True
|
81
|
+
|
82
|
+
def _has_relevant_changes(self, key: str, pair: TokenPair) -> bool:
|
83
|
+
"""Check if monitored fields have changed"""
|
84
|
+
if key not in self._cache:
|
85
|
+
return True # First update
|
86
|
+
|
87
|
+
cached_values = self._cache[key]
|
88
|
+
current_values = self._extract_values(pair)
|
89
|
+
|
90
|
+
# Check each monitored field
|
91
|
+
for field_name in self.config.change_fields:
|
92
|
+
if (
|
93
|
+
field_name in current_values
|
94
|
+
and field_name in cached_values
|
95
|
+
and current_values[field_name] != cached_values[field_name]
|
96
|
+
):
|
97
|
+
return True
|
98
|
+
|
99
|
+
return False
|
100
|
+
|
101
|
+
def _are_changes_significant(self, key: str, pair: TokenPair) -> bool:
|
102
|
+
"""Check if changes meet significance thresholds"""
|
103
|
+
if key not in self._cache:
|
104
|
+
return True # First update is always significant
|
105
|
+
|
106
|
+
cached_values = self._cache[key]
|
107
|
+
|
108
|
+
# Check price change threshold
|
109
|
+
if self.config.price_change_threshold is not None and not self._check_threshold(
|
110
|
+
cached_values.get("price_usd"), pair.price_usd, self.config.price_change_threshold
|
111
|
+
):
|
112
|
+
return False
|
113
|
+
|
114
|
+
# Check volume change threshold
|
115
|
+
if self.config.volume_change_threshold is not None:
|
116
|
+
current_volume = pair.volume.h24 if pair.volume else None
|
117
|
+
cached_volume = cached_values.get("volume.h24")
|
118
|
+
if not self._check_threshold(cached_volume, current_volume, self.config.volume_change_threshold):
|
119
|
+
return False
|
120
|
+
|
121
|
+
# Check liquidity change threshold
|
122
|
+
if self.config.liquidity_change_threshold is not None:
|
123
|
+
current_liquidity = pair.liquidity.usd if pair.liquidity else None
|
124
|
+
cached_liquidity = cached_values.get("liquidity.usd")
|
125
|
+
if not self._check_threshold(cached_liquidity, current_liquidity, self.config.liquidity_change_threshold):
|
126
|
+
return False
|
127
|
+
|
128
|
+
return True
|
129
|
+
|
130
|
+
def _check_threshold(self, old_value: Optional[float], new_value: Optional[float], threshold: float) -> bool:
|
131
|
+
"""Check if change exceeds threshold"""
|
132
|
+
if old_value is None or new_value is None:
|
133
|
+
return True # Can't compare, allow update
|
134
|
+
|
135
|
+
if old_value == 0:
|
136
|
+
return new_value != 0 # Any change from 0 is significant
|
137
|
+
|
138
|
+
change_ratio = abs(new_value - old_value) / abs(old_value)
|
139
|
+
return change_ratio >= threshold
|
140
|
+
|
141
|
+
def _extract_values(self, pair: TokenPair) -> dict[str, Any]:
|
142
|
+
"""Extract values for monitored fields"""
|
143
|
+
values = {}
|
144
|
+
|
145
|
+
for field_name in self.config.change_fields:
|
146
|
+
if "." in field_name:
|
147
|
+
# Handle nested fields like "volume.h24"
|
148
|
+
parts = field_name.split(".")
|
149
|
+
obj: Any = pair
|
150
|
+
|
151
|
+
for part in parts:
|
152
|
+
obj = getattr(obj, part, None)
|
153
|
+
if obj is None:
|
154
|
+
break
|
155
|
+
|
156
|
+
values[field_name] = obj
|
157
|
+
else:
|
158
|
+
# Direct field
|
159
|
+
values[field_name] = getattr(pair, field_name, None)
|
160
|
+
|
161
|
+
return values
|
162
|
+
|
163
|
+
def _update_cache(self, key: str, pair: TokenPair):
|
164
|
+
"""Update cached values"""
|
165
|
+
self._cache[key] = self._extract_values(pair)
|
166
|
+
|
167
|
+
def reset(self, key: Optional[str] = None):
|
168
|
+
"""Reset filter state for a specific key or all keys"""
|
169
|
+
if key:
|
170
|
+
self._cache.pop(key, None)
|
171
|
+
self._last_update_times.pop(key, None)
|
172
|
+
else:
|
173
|
+
self._cache.clear()
|
174
|
+
self._last_update_times.clear()
|
175
|
+
|
176
|
+
|
177
|
+
# Preset configurations
|
178
|
+
class FilterPresets:
|
179
|
+
"""Common filter configurations"""
|
180
|
+
|
181
|
+
@staticmethod
|
182
|
+
def simple_change_detection() -> FilterConfig:
|
183
|
+
"""Basic change detection (default behavior)"""
|
184
|
+
return FilterConfig()
|
185
|
+
|
186
|
+
@staticmethod
|
187
|
+
def significant_price_changes(threshold: float = 0.01) -> FilterConfig:
|
188
|
+
"""Only emit on significant price changes"""
|
189
|
+
return FilterConfig(change_fields=["price_usd"], price_change_threshold=threshold)
|
190
|
+
|
191
|
+
@staticmethod
|
192
|
+
def significant_all_changes(
|
193
|
+
price_threshold: float = 0.005,
|
194
|
+
volume_threshold: float = 0.10,
|
195
|
+
liquidity_threshold: float = 0.05,
|
196
|
+
) -> FilterConfig:
|
197
|
+
"""Only emit on significant changes in any metric"""
|
198
|
+
return FilterConfig(
|
199
|
+
price_change_threshold=price_threshold,
|
200
|
+
volume_change_threshold=volume_threshold,
|
201
|
+
liquidity_change_threshold=liquidity_threshold,
|
202
|
+
)
|
203
|
+
|
204
|
+
@staticmethod
|
205
|
+
def rate_limited(max_per_second: float = 1.0) -> FilterConfig:
|
206
|
+
"""Rate limit updates"""
|
207
|
+
return FilterConfig(max_updates_per_second=max_per_second)
|
208
|
+
|
209
|
+
@staticmethod
|
210
|
+
def ui_friendly() -> FilterConfig:
|
211
|
+
"""Suitable for UI updates - rate limited with significance thresholds"""
|
212
|
+
return FilterConfig(
|
213
|
+
price_change_threshold=0.001, # 0.1% price change
|
214
|
+
volume_change_threshold=0.05, # 5% volume change
|
215
|
+
max_updates_per_second=2.0, # Max 2 updates per second
|
216
|
+
)
|
217
|
+
|
218
|
+
@staticmethod
|
219
|
+
def monitoring() -> FilterConfig:
|
220
|
+
"""For monitoring dashboards - less frequent updates"""
|
221
|
+
return FilterConfig(
|
222
|
+
price_change_threshold=0.01, # 1% price change
|
223
|
+
volume_change_threshold=0.10, # 10% volume change
|
224
|
+
liquidity_change_threshold=0.05, # 5% liquidity change
|
225
|
+
max_updates_per_second=0.2, # Max 1 update per 5 seconds
|
226
|
+
)
|
@@ -0,0 +1,65 @@
|
|
1
|
+
import asyncio
|
2
|
+
import collections
|
3
|
+
import threading
|
4
|
+
import time
|
5
|
+
from collections import deque
|
6
|
+
|
7
|
+
|
8
|
+
class RateLimitError(Exception):
|
9
|
+
"""Raised when rate limit is exceeded"""
|
10
|
+
|
11
|
+
pass
|
12
|
+
|
13
|
+
|
14
|
+
class RateLimiter:
|
15
|
+
def __init__(self, max_calls: int, period: float):
|
16
|
+
self.calls: deque[float] = collections.deque()
|
17
|
+
|
18
|
+
self.period = period
|
19
|
+
self.max_calls = max_calls
|
20
|
+
|
21
|
+
self.sync_lock = threading.Lock()
|
22
|
+
self.async_lock = asyncio.Lock()
|
23
|
+
|
24
|
+
def __enter__(self):
|
25
|
+
with self.sync_lock:
|
26
|
+
sleep_time = self.get_sleep_time()
|
27
|
+
|
28
|
+
if sleep_time > 0:
|
29
|
+
time.sleep(sleep_time)
|
30
|
+
|
31
|
+
return self
|
32
|
+
|
33
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
34
|
+
with self.sync_lock:
|
35
|
+
self._clear_calls()
|
36
|
+
|
37
|
+
async def __aenter__(self):
|
38
|
+
async with self.async_lock:
|
39
|
+
sleep_time = self.get_sleep_time()
|
40
|
+
|
41
|
+
if sleep_time > 0:
|
42
|
+
await asyncio.sleep(sleep_time)
|
43
|
+
|
44
|
+
return self
|
45
|
+
|
46
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
47
|
+
async with self.async_lock:
|
48
|
+
self._clear_calls()
|
49
|
+
|
50
|
+
def get_sleep_time(self) -> float:
|
51
|
+
if len(self.calls) >= self.max_calls:
|
52
|
+
until = time.time() + self.period - self._timespan
|
53
|
+
return until - time.time()
|
54
|
+
|
55
|
+
return 0
|
56
|
+
|
57
|
+
def _clear_calls(self):
|
58
|
+
self.calls.append(time.time())
|
59
|
+
|
60
|
+
while self._timespan >= self.period:
|
61
|
+
self.calls.popleft()
|
62
|
+
|
63
|
+
@property
|
64
|
+
def _timespan(self) -> float:
|
65
|
+
return self.calls[-1] - self.calls[0]
|
@@ -0,0 +1,278 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: dexscreen
|
3
|
+
Version: 0.0.1
|
4
|
+
Summary: Python wrapper for Dexscreener API with stable HTTP support
|
5
|
+
Project-URL: Repository, https://github.com/solanab/dexscreen
|
6
|
+
Project-URL: Documentation, https://github.com/solanab/dexscreen#readme
|
7
|
+
Project-URL: Issues, https://github.com/solanab/dexscreen/issues
|
8
|
+
Author-email: solanab <whiredj@gmail.com>
|
9
|
+
License: MIT
|
10
|
+
License-File: LICENSE
|
11
|
+
Keywords: api,crypto,cryptocurrency,dexscreener,http
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
13
|
+
Classifier: Intended Audience :: Developers
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
15
|
+
Classifier: Operating System :: OS Independent
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
23
|
+
Requires-Python: >=3.9
|
24
|
+
Requires-Dist: curl-cffi>=0.12
|
25
|
+
Requires-Dist: orjson>=3.11
|
26
|
+
Requires-Dist: pydantic>=2.11
|
27
|
+
Description-Content-Type: text/markdown
|
28
|
+
|
29
|
+
# Dexscreen
|
30
|
+
|
31
|
+
A stable and reliable Python SDK for [Dexscreener.com](https://dexscreener.com/) API with HTTP support:
|
32
|
+
|
33
|
+
- **Single Query** - Traditional one-time API calls
|
34
|
+
- **Real-time Updates** - Live price updates with configurable intervals
|
35
|
+
|
36
|
+
[](https://pepy.tech/project/dexscreen)
|
37
|
+
[](https://badge.fury.io/py/dexscreen)
|
38
|
+
[](https://pypi.org/project/dexscreen/)
|
39
|
+
|
40
|
+
## Features
|
41
|
+
|
42
|
+
- ✅ Complete official API coverage
|
43
|
+
- ✅ Stable HTTP-based streaming
|
44
|
+
- ✅ Automatic rate limiting
|
45
|
+
- ✅ Browser impersonation for anti-bot bypass (using curl_cffi)
|
46
|
+
- ✅ Type-safe with Pydantic models
|
47
|
+
- ✅ Async/sync support
|
48
|
+
- ✅ Simple, focused interface
|
49
|
+
|
50
|
+
## Installation
|
51
|
+
|
52
|
+
use uv (Recommended)
|
53
|
+
|
54
|
+
```bash
|
55
|
+
uv add dexscreen
|
56
|
+
```
|
57
|
+
|
58
|
+
or pip
|
59
|
+
|
60
|
+
```bash
|
61
|
+
pip install dexscreen
|
62
|
+
```
|
63
|
+
|
64
|
+
## Quick Start
|
65
|
+
|
66
|
+
### Mode 1: Single Query (Traditional API calls)
|
67
|
+
|
68
|
+
```python
|
69
|
+
from dexscreen import DexscreenerClient
|
70
|
+
|
71
|
+
client = DexscreenerClient()
|
72
|
+
|
73
|
+
# Get a specific pair by token address
|
74
|
+
pairs = client.get_pairs_by_token_address("solana", "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN")
|
75
|
+
if pairs:
|
76
|
+
pair = pairs[0] # Get the first pair
|
77
|
+
print(f"{pair.base_token.symbol}: ${pair.price_usd}")
|
78
|
+
|
79
|
+
# Search for tokens
|
80
|
+
results = client.search_pairs("PEPE")
|
81
|
+
for pair in results[:5]:
|
82
|
+
print(f"{pair.base_token.symbol} on {pair.chain_id}: ${pair.price_usd}")
|
83
|
+
|
84
|
+
# Get token information
|
85
|
+
profiles = client.get_latest_token_profiles()
|
86
|
+
boosted = client.get_latest_boosted_tokens()
|
87
|
+
```
|
88
|
+
|
89
|
+
### Mode 2: Real-time Updates
|
90
|
+
|
91
|
+
```python
|
92
|
+
import asyncio
|
93
|
+
from dexscreen import DexscreenerClient
|
94
|
+
|
95
|
+
async def handle_price_update(pair):
|
96
|
+
print(f"{pair.base_token.symbol}: ${pair.price_usd} ({pair.price_change.h24:+.2f}%)")
|
97
|
+
|
98
|
+
async def main():
|
99
|
+
client = DexscreenerClient()
|
100
|
+
|
101
|
+
# Subscribe to pair updates (default interval: 0.2 seconds)
|
102
|
+
await client.subscribe_pairs(
|
103
|
+
chain_id="solana",
|
104
|
+
pair_addresses=["JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"],
|
105
|
+
callback=handle_price_update,
|
106
|
+
interval=0.2 # Poll 5 times per second (300/min)
|
107
|
+
)
|
108
|
+
|
109
|
+
# Let it run for 30 seconds
|
110
|
+
await asyncio.sleep(30)
|
111
|
+
await client.close_streams()
|
112
|
+
|
113
|
+
asyncio.run(main())
|
114
|
+
```
|
115
|
+
|
116
|
+
### Filtering Options
|
117
|
+
|
118
|
+
```python
|
119
|
+
from dexscreen import DexscreenerClient, FilterPresets
|
120
|
+
|
121
|
+
# Default filtering - only receive updates when data changes
|
122
|
+
await client.subscribe_pairs(
|
123
|
+
chain_id="solana",
|
124
|
+
pair_addresses=["JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"],
|
125
|
+
callback=handle_price_update,
|
126
|
+
filter=True, # Default value
|
127
|
+
interval=0.2
|
128
|
+
)
|
129
|
+
|
130
|
+
# No filtering - receive all polling results
|
131
|
+
await client.subscribe_pairs(
|
132
|
+
chain_id="ethereum",
|
133
|
+
pair_addresses=["0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"],
|
134
|
+
callback=handle_price_update,
|
135
|
+
filter=False, # Get all updates even if data hasn't changed
|
136
|
+
interval=1.0
|
137
|
+
)
|
138
|
+
|
139
|
+
# Advanced filtering - only significant price changes (1%)
|
140
|
+
await client.subscribe_pairs(
|
141
|
+
chain_id="ethereum",
|
142
|
+
pair_addresses=["0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"],
|
143
|
+
callback=handle_price_update,
|
144
|
+
filter=FilterPresets.significant_price_changes(0.01)
|
145
|
+
)
|
146
|
+
|
147
|
+
# Rate limited updates - max 1 update per second
|
148
|
+
await client.subscribe_pairs(
|
149
|
+
chain_id="ethereum",
|
150
|
+
pair_addresses=["0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"],
|
151
|
+
callback=handle_price_update,
|
152
|
+
filter=FilterPresets.rate_limited(1.0)
|
153
|
+
)
|
154
|
+
```
|
155
|
+
|
156
|
+
## Advanced Usage
|
157
|
+
|
158
|
+
### Price Monitoring for Arbitrage
|
159
|
+
|
160
|
+
```python
|
161
|
+
from dexscreen import DexscreenerClient
|
162
|
+
|
163
|
+
async def monitor_arbitrage():
|
164
|
+
client = DexscreenerClient()
|
165
|
+
|
166
|
+
# Monitor same token on different chains
|
167
|
+
pairs = [
|
168
|
+
("ethereum", "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"), # USDC/WETH
|
169
|
+
("bsc", "0x7213a321F1855CF1779f42c0CD85d3D95291D34C"), # USDC/BUSD
|
170
|
+
("polygon", "0x45dDa9cb7c25131DF268515131f647d726f50608"), # USDC/WETH
|
171
|
+
]
|
172
|
+
|
173
|
+
prices = {}
|
174
|
+
|
175
|
+
async def track_price(pair):
|
176
|
+
key = f"{pair.chain_id}:{pair.base_token.symbol}"
|
177
|
+
prices[key] = float(pair.price_usd)
|
178
|
+
|
179
|
+
# Check for arbitrage opportunity
|
180
|
+
if len(prices) > 1:
|
181
|
+
min_price = min(prices.values())
|
182
|
+
max_price = max(prices.values())
|
183
|
+
spread = ((max_price - min_price) / min_price) * 100
|
184
|
+
|
185
|
+
if spread > 0.5: # 0.5% spread
|
186
|
+
print(f"ARBITRAGE: {spread:.2f}% spread detected!")
|
187
|
+
|
188
|
+
# Subscribe to all pairs
|
189
|
+
for chain, address in pairs:
|
190
|
+
await client.subscribe_pairs(
|
191
|
+
chain_id=chain,
|
192
|
+
pair_addresses=[address],
|
193
|
+
callback=track_price,
|
194
|
+
interval=0.5
|
195
|
+
)
|
196
|
+
|
197
|
+
await asyncio.sleep(60) # Monitor for 1 minute
|
198
|
+
```
|
199
|
+
|
200
|
+
### High-Volume Pairs Discovery
|
201
|
+
|
202
|
+
```python
|
203
|
+
async def find_trending_tokens():
|
204
|
+
client = DexscreenerClient()
|
205
|
+
|
206
|
+
# Search for tokens
|
207
|
+
results = await client.search_pairs_async("SOL")
|
208
|
+
|
209
|
+
# Filter high volume pairs (>$1M daily volume)
|
210
|
+
high_volume = [
|
211
|
+
p for p in results
|
212
|
+
if p.volume.h24 and p.volume.h24 > 1_000_000
|
213
|
+
]
|
214
|
+
|
215
|
+
# Sort by volume
|
216
|
+
high_volume.sort(key=lambda x: x.volume.h24, reverse=True)
|
217
|
+
|
218
|
+
# Monitor top 5
|
219
|
+
for pair in high_volume[:5]:
|
220
|
+
print(f"{pair.base_token.symbol}: Vol ${pair.volume.h24/1e6:.2f}M")
|
221
|
+
|
222
|
+
await client.subscribe_pairs(
|
223
|
+
chain_id=pair.chain_id,
|
224
|
+
pair_addresses=[pair.pair_address],
|
225
|
+
callback=handle_price_update,
|
226
|
+
interval=2.0
|
227
|
+
)
|
228
|
+
```
|
229
|
+
|
230
|
+
## Documentation
|
231
|
+
|
232
|
+
📖 **[Full Documentation](docs/index.md)** - Complete API reference, guides, and examples.
|
233
|
+
|
234
|
+
### Quick API Overview
|
235
|
+
|
236
|
+
#### Main Client
|
237
|
+
|
238
|
+
```python
|
239
|
+
client = DexscreenerClient(impersonate="chrome136")
|
240
|
+
```
|
241
|
+
|
242
|
+
#### Key Methods
|
243
|
+
|
244
|
+
- `get_pairs_by_token_address(chain_id, token_address)` - Get pairs for a token
|
245
|
+
- `search_pairs(query)` - Search pairs
|
246
|
+
- `subscribe_pairs(chain_id, pair_addresses, callback)` - Real-time pair updates
|
247
|
+
- `subscribe_tokens(chain_id, token_addresses, callback)` - Monitor all pairs of tokens
|
248
|
+
|
249
|
+
#### Data Models
|
250
|
+
|
251
|
+
- `TokenPair` - Main pair data model
|
252
|
+
- `TokenInfo` - Token profile information
|
253
|
+
- `OrderInfo` - Order information
|
254
|
+
|
255
|
+
## Rate Limits
|
256
|
+
|
257
|
+
The SDK automatically handles rate limiting:
|
258
|
+
|
259
|
+
- 60 requests/minute for token profile endpoints
|
260
|
+
- 300 requests/minute for pair data endpoints
|
261
|
+
|
262
|
+
## Browser Impersonation
|
263
|
+
|
264
|
+
The SDK uses curl_cffi for browser impersonation to bypass anti-bot protection:
|
265
|
+
|
266
|
+
```python
|
267
|
+
# Use different browser versions
|
268
|
+
client = DexscreenerClient(impersonate="chrome134")
|
269
|
+
client = DexscreenerClient(impersonate="safari180")
|
270
|
+
```
|
271
|
+
|
272
|
+
## Contributing
|
273
|
+
|
274
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
275
|
+
|
276
|
+
## License
|
277
|
+
|
278
|
+
MIT License - see LICENSE file for details
|
@@ -0,0 +1,17 @@
|
|
1
|
+
dexscreen/__init__.py,sha256=W7jJhDNMyzsLsOfQefZJ4pEphfmoIYQbj7rViLplgVg,663
|
2
|
+
dexscreen/api/__init__.py,sha256=_fFBxC2rrc4nzeRFS_0MQM3cNFrz-ZtvMKrKXX9gtyI,71
|
3
|
+
dexscreen/api/client.py,sha256=_5iQjzx7X4nLhkFimLOPI6dVOcXopgZzya1UNJJns4I,28502
|
4
|
+
dexscreen/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
+
dexscreen/core/__init__.py,sha256=_QvgIu4e9RG06TrCTC-BIJMJ5duEFwp1LLTZ7sG4YuM,490
|
6
|
+
dexscreen/core/http.py,sha256=pimTHfsV1v2txnlPk8DIaKesxpZi6cFATm9Z8wlS-No,16327
|
7
|
+
dexscreen/core/models.py,sha256=7-EN63yyQK92T1ZRhfHwwXJ8Cg1VbO3CY-wdlpljIu4,2573
|
8
|
+
dexscreen/stream/__init__.py,sha256=vqoiFQ4LwLjaIEWl7p60StTfG1bSJhyEapjWILVP168,100
|
9
|
+
dexscreen/stream/polling.py,sha256=yltLwKHQJkN2RJAkgJFxzKvRwcv9M3DYHl19D_ZYSog,17517
|
10
|
+
dexscreen/utils/__init__.py,sha256=2IXzfN8UFzC4iMX3IoK_9rUY-0CtCCsdaFa66Bdv2zw,180
|
11
|
+
dexscreen/utils/browser_selector.py,sha256=OB14lfSO6zyen3Qga2Cw2Xc9u9_Y5OkEuIdgLeG2_TA,1186
|
12
|
+
dexscreen/utils/filters.py,sha256=rAgcMOLi3VgTvi4ga6p2dBQ-hWlU3DURpCrabyV-V2g,8145
|
13
|
+
dexscreen/utils/ratelimit.py,sha256=NncESbX_8RHPSA9vpxH-vnFwoPwssvooOZthqEQ0-G0,1570
|
14
|
+
dexscreen-0.0.1.dist-info/METADATA,sha256=imRHIwcAMcMZG_67e8ccYt0z0ueNPWvLgUpm4gqOyE4,7881
|
15
|
+
dexscreen-0.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
16
|
+
dexscreen-0.0.1.dist-info/licenses/LICENSE,sha256=YLNduNj40Iu4upUYI6XEXrgBlv0hxMNf6uEVAJITYN0,1064
|
17
|
+
dexscreen-0.0.1.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 solanab
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|