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.
@@ -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
+ [![Downloads](https://pepy.tech/badge/dexscreen)](https://pepy.tech/project/dexscreen)
37
+ [![PyPI version](https://badge.fury.io/py/dexscreen.svg)](https://badge.fury.io/py/dexscreen)
38
+ [![Python Version](https://img.shields.io/pypi/pyversions/dexscreen)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.