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.
@@ -0,0 +1,542 @@
1
+ """
2
+ Input validation utilities for Dexscreen API
3
+ """
4
+
5
+ import re
6
+ from typing import Any, Callable, Optional, Union
7
+ from urllib.parse import urlparse
8
+
9
+ from .exceptions import (
10
+ EmptyListError,
11
+ InvalidAddressError,
12
+ InvalidCallbackError,
13
+ InvalidChainIdError,
14
+ InvalidFilterError,
15
+ InvalidIntervalError,
16
+ InvalidParameterError,
17
+ InvalidRangeError,
18
+ InvalidTypeError,
19
+ InvalidUrlError,
20
+ TooManyItemsError,
21
+ )
22
+
23
+ # Common blockchain networks supported by Dexscreener
24
+ VALID_CHAIN_IDS = {
25
+ "ethereum",
26
+ "bsc",
27
+ "polygon",
28
+ "avalanche",
29
+ "fantom",
30
+ "cronos",
31
+ "arbitrum",
32
+ "optimism",
33
+ "solana",
34
+ "base",
35
+ "linea",
36
+ "scroll",
37
+ "blast",
38
+ "manta",
39
+ "mantle",
40
+ "mode",
41
+ "sei",
42
+ "pulsechain",
43
+ "metis",
44
+ "moonbeam",
45
+ "moonriver",
46
+ "celo",
47
+ "fuse",
48
+ "harmony",
49
+ "kava",
50
+ "evmos",
51
+ "milkomeda",
52
+ "aurora",
53
+ "near",
54
+ "telos",
55
+ "wax",
56
+ "eos",
57
+ "tron",
58
+ "aptos",
59
+ "sui",
60
+ "starknet",
61
+ "zksync",
62
+ "polygonzkevm",
63
+ "immutablex",
64
+ "loopring",
65
+ "dydx",
66
+ "osmosis",
67
+ "cosmos",
68
+ "terra",
69
+ "thorchain",
70
+ "bitcoin",
71
+ "litecoin",
72
+ "dogecoin",
73
+ "cardano",
74
+ "polkadot",
75
+ "kusama",
76
+ "algorand",
77
+ "tezos",
78
+ "flow",
79
+ "hedera",
80
+ "icp",
81
+ "waves",
82
+ "stellar",
83
+ "xrp",
84
+ "chia",
85
+ "elrond",
86
+ "zilliqa",
87
+ "vechain",
88
+ "nuls",
89
+ "nem",
90
+ "symbol",
91
+ "iotex",
92
+ "ontology",
93
+ "qtum",
94
+ "conflux",
95
+ "nervos",
96
+ "syscoin",
97
+ "digibyte",
98
+ "ravencoin",
99
+ "zcash",
100
+ "dash",
101
+ "monero",
102
+ "decred",
103
+ "horizen",
104
+ "beam",
105
+ "grin",
106
+ }
107
+
108
+ # Address format patterns for different blockchains
109
+ ADDRESS_PATTERNS = {
110
+ # Ethereum-style (hex, 40 chars + 0x prefix)
111
+ "ethereum": re.compile(r"^0x[a-fA-F0-9]{40}$"),
112
+ "bsc": re.compile(r"^0x[a-fA-F0-9]{40}$"),
113
+ "polygon": re.compile(r"^0x[a-fA-F0-9]{40}$"),
114
+ "arbitrum": re.compile(r"^0x[a-fA-F0-9]{40}$"),
115
+ "optimism": re.compile(r"^0x[a-fA-F0-9]{40}$"),
116
+ "avalanche": re.compile(r"^0x[a-fA-F0-9]{40}$"),
117
+ "fantom": re.compile(r"^0x[a-fA-F0-9]{40}$"),
118
+ "base": re.compile(r"^0x[a-fA-F0-9]{40}$"),
119
+ # Solana (base58, 32-44 chars)
120
+ "solana": re.compile(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$"),
121
+ # Bitcoin (base58, starts with 1, 3, or bc1)
122
+ "bitcoin": re.compile(r"^(1[1-9A-HJ-NP-Za-km-z]{25,34}|3[1-9A-HJ-NP-Za-km-z]{25,34}|bc1[a-z0-9]{39,59})$"),
123
+ # Generic fallback (alphanumeric, 20-65 chars)
124
+ "default": re.compile(r"^[a-zA-Z0-9]{20,65}$"),
125
+ }
126
+
127
+
128
+ def validate_string(
129
+ value: Any, parameter_name: str, min_length: int = 1, max_length: int = 1000, allow_empty: bool = False
130
+ ) -> str:
131
+ """
132
+ Validate string parameter.
133
+
134
+ Args:
135
+ value: Value to validate
136
+ parameter_name: Name of parameter for error messages
137
+ min_length: Minimum string length
138
+ max_length: Maximum string length
139
+ allow_empty: Whether to allow empty strings
140
+
141
+ Returns:
142
+ Validated string
143
+
144
+ Raises:
145
+ InvalidTypeError: If value is not a string
146
+ InvalidParameterError: If string is empty when not allowed or outside length bounds
147
+ """
148
+ if not isinstance(value, str):
149
+ raise InvalidTypeError(parameter_name, value, "string")
150
+
151
+ if not allow_empty and len(value) == 0:
152
+ raise InvalidParameterError(parameter_name, value, "non-empty string")
153
+
154
+ if not (min_length <= len(value) <= max_length):
155
+ raise InvalidRangeError(parameter_name, len(value), min_length, max_length)
156
+
157
+ return value
158
+
159
+
160
+ def validate_chain_id(chain_id: Any) -> str:
161
+ """
162
+ Validate blockchain chain ID.
163
+
164
+ Args:
165
+ chain_id: Chain ID to validate
166
+
167
+ Returns:
168
+ Validated chain ID (lowercase)
169
+
170
+ Raises:
171
+ InvalidChainIdError: If chain ID is invalid
172
+ """
173
+ if not isinstance(chain_id, str):
174
+ raise InvalidTypeError("chain_id", chain_id, "string")
175
+
176
+ chain_id = chain_id.lower().strip()
177
+
178
+ if not chain_id:
179
+ raise InvalidParameterError("chain_id", chain_id, "non-empty string")
180
+
181
+ if chain_id not in VALID_CHAIN_IDS:
182
+ # Get similar chain IDs for better error message
183
+ similar = [c for c in VALID_CHAIN_IDS if chain_id in c or c in chain_id][:5]
184
+ raise InvalidChainIdError(chain_id, similar if similar else list(VALID_CHAIN_IDS)[:10])
185
+
186
+ return chain_id
187
+
188
+
189
+ def validate_address(address: Any, chain_id: Optional[str] = None) -> str:
190
+ """
191
+ Validate blockchain address format.
192
+
193
+ Args:
194
+ address: Address to validate
195
+ chain_id: Optional chain ID for chain-specific validation
196
+
197
+ Returns:
198
+ Validated address
199
+
200
+ Raises:
201
+ InvalidAddressError: If address format is invalid
202
+ """
203
+ if not isinstance(address, str):
204
+ raise InvalidTypeError("address", address, "string")
205
+
206
+ address = address.strip()
207
+
208
+ if not address:
209
+ raise InvalidAddressError(address, "Address cannot be empty")
210
+
211
+ # Basic length check
212
+ if len(address) < 20 or len(address) > 70:
213
+ raise InvalidAddressError(address, "Address length must be between 20 and 70 characters")
214
+
215
+ # Chain-specific validation
216
+ if chain_id:
217
+ pattern = ADDRESS_PATTERNS.get(chain_id.lower(), ADDRESS_PATTERNS["default"])
218
+ if not pattern.match(address):
219
+ raise InvalidAddressError(address, f"Invalid address format for {chain_id}")
220
+
221
+ return address
222
+
223
+
224
+ def validate_addresses_list(
225
+ addresses: Any,
226
+ parameter_name: str = "addresses",
227
+ min_count: int = 1,
228
+ max_count: int = 30,
229
+ chain_id: Optional[str] = None,
230
+ ) -> list[str]:
231
+ """
232
+ Validate list of addresses.
233
+
234
+ Args:
235
+ addresses: List of addresses to validate
236
+ parameter_name: Name of parameter for error messages
237
+ min_count: Minimum number of addresses
238
+ max_count: Maximum number of addresses
239
+ chain_id: Optional chain ID for address validation
240
+
241
+ Returns:
242
+ Validated list of addresses
243
+
244
+ Raises:
245
+ InvalidTypeError: If not a list
246
+ EmptyListError: If list is empty when not allowed
247
+ TooManyItemsError: If too many addresses
248
+ InvalidAddressError: If any address is invalid
249
+ """
250
+ if not isinstance(addresses, (list, tuple)):
251
+ raise InvalidTypeError(parameter_name, addresses, "list")
252
+
253
+ addresses = list(addresses)
254
+
255
+ if len(addresses) < min_count:
256
+ if min_count == 1:
257
+ raise EmptyListError(parameter_name)
258
+ else:
259
+ raise InvalidParameterError(parameter_name, addresses, f"at least {min_count} items")
260
+
261
+ if len(addresses) > max_count:
262
+ raise TooManyItemsError(parameter_name, len(addresses), max_count)
263
+
264
+ # Validate each address
265
+ validated_addresses = []
266
+ for i, addr in enumerate(addresses):
267
+ try:
268
+ validated_addr = validate_address(addr, chain_id)
269
+ validated_addresses.append(validated_addr)
270
+ except InvalidAddressError as e:
271
+ raise InvalidAddressError(addr, f"Address at index {i}: {e.reason}") from e
272
+
273
+ # Check for duplicates
274
+ if len(set(validated_addresses)) != len(validated_addresses):
275
+ raise InvalidParameterError(parameter_name, addresses, "unique addresses (duplicates found)")
276
+
277
+ return validated_addresses
278
+
279
+
280
+ def validate_numeric(
281
+ value: Any,
282
+ parameter_name: str,
283
+ expected_type: type = float,
284
+ min_value: Optional[Union[int, float]] = None,
285
+ max_value: Optional[Union[int, float]] = None,
286
+ allow_none: bool = False,
287
+ ) -> Union[int, float, None]:
288
+ """
289
+ Validate numeric parameter.
290
+
291
+ Args:
292
+ value: Value to validate
293
+ parameter_name: Name of parameter for error messages
294
+ expected_type: Expected numeric type (int or float)
295
+ min_value: Minimum allowed value
296
+ max_value: Maximum allowed value
297
+ allow_none: Whether to allow None values
298
+
299
+ Returns:
300
+ Validated numeric value
301
+
302
+ Raises:
303
+ InvalidTypeError: If value is not numeric
304
+ InvalidRangeError: If value is outside valid range
305
+ """
306
+ if value is None and allow_none:
307
+ return None
308
+
309
+ if not isinstance(value, (int, float)):
310
+ raise InvalidTypeError(parameter_name, value, expected_type.__name__)
311
+
312
+ # Convert to expected type
313
+ try:
314
+ if expected_type is int:
315
+ value = int(value)
316
+ elif expected_type is float:
317
+ value = float(value)
318
+ except (ValueError, OverflowError) as e:
319
+ raise InvalidTypeError(parameter_name, value, expected_type.__name__) from e
320
+
321
+ # Check for special float values
322
+ if expected_type is float and (value != value or value == float("inf") or value == float("-inf")):
323
+ raise InvalidParameterError(parameter_name, value, "finite number")
324
+
325
+ # Range validation
326
+ if min_value is not None and value < min_value:
327
+ raise InvalidRangeError(parameter_name, value, min_value, max_value)
328
+
329
+ if max_value is not None and value > max_value:
330
+ raise InvalidRangeError(parameter_name, value, min_value, max_value)
331
+
332
+ return value
333
+
334
+
335
+ def validate_interval(interval: Any, min_interval: float = 0.1, max_interval: float = 3600.0) -> float:
336
+ """
337
+ Validate polling interval.
338
+
339
+ Args:
340
+ interval: Interval to validate
341
+ min_interval: Minimum allowed interval
342
+ max_interval: Maximum allowed interval
343
+
344
+ Returns:
345
+ Validated interval
346
+
347
+ Raises:
348
+ InvalidIntervalError: If interval is invalid
349
+ """
350
+ try:
351
+ interval = validate_numeric(interval, "interval", float, min_interval, max_interval)
352
+ except (InvalidTypeError, InvalidRangeError) as e:
353
+ raise InvalidIntervalError(interval, min_interval, max_interval) from e
354
+
355
+ return interval
356
+
357
+
358
+ def validate_callback(callback: Any) -> Callable:
359
+ """
360
+ Validate callback function.
361
+
362
+ Args:
363
+ callback: Callback to validate
364
+
365
+ Returns:
366
+ Validated callback
367
+
368
+ Raises:
369
+ InvalidCallbackError: If callback is invalid
370
+ """
371
+ if not callable(callback):
372
+ raise InvalidCallbackError(callback, "Must be callable")
373
+
374
+ return callback
375
+
376
+
377
+ def validate_url(url: Any, require_https: bool = False) -> str:
378
+ """
379
+ Validate URL format.
380
+
381
+ Args:
382
+ url: URL to validate
383
+ require_https: Whether to require HTTPS scheme
384
+
385
+ Returns:
386
+ Validated URL
387
+
388
+ Raises:
389
+ InvalidUrlError: If URL is invalid
390
+ """
391
+ if not isinstance(url, str):
392
+ raise InvalidTypeError("url", url, "string")
393
+
394
+ url = url.strip()
395
+
396
+ if not url:
397
+ raise InvalidUrlError(url, "URL cannot be empty")
398
+
399
+ try:
400
+ parsed = urlparse(url)
401
+
402
+ if not parsed.scheme:
403
+ raise InvalidUrlError(url, "URL must include scheme (http/https)")
404
+
405
+ if require_https and parsed.scheme != "https":
406
+ raise InvalidUrlError(url, "URL must use HTTPS")
407
+
408
+ if not parsed.netloc:
409
+ raise InvalidUrlError(url, "URL must include domain")
410
+
411
+ except Exception as e:
412
+ raise InvalidUrlError(url, "Invalid URL format") from e
413
+
414
+ return url
415
+
416
+
417
+ def validate_filter_config(filter_config: Any) -> Any:
418
+ """
419
+ Validate filter configuration.
420
+
421
+ Args:
422
+ filter_config: Filter config to validate
423
+
424
+ Returns:
425
+ Validated filter config
426
+
427
+ Raises:
428
+ InvalidFilterError: If filter config is invalid
429
+ """
430
+ from ..utils.filters import FilterConfig
431
+
432
+ if filter_config is None:
433
+ return None
434
+
435
+ if isinstance(filter_config, bool):
436
+ return filter_config
437
+
438
+ if not isinstance(filter_config, FilterConfig):
439
+ raise InvalidFilterError(f"Must be bool or FilterConfig, got {type(filter_config).__name__}")
440
+
441
+ # Validate filter config fields
442
+ if hasattr(filter_config, "price_change_threshold") and filter_config.price_change_threshold is not None:
443
+ validate_numeric(filter_config.price_change_threshold, "price_change_threshold", float, 0.0, 1.0)
444
+
445
+ if hasattr(filter_config, "volume_change_threshold") and filter_config.volume_change_threshold is not None:
446
+ validate_numeric(filter_config.volume_change_threshold, "volume_change_threshold", float, 0.0, 10.0)
447
+
448
+ if hasattr(filter_config, "max_updates_per_second") and filter_config.max_updates_per_second is not None:
449
+ validate_numeric(filter_config.max_updates_per_second, "max_updates_per_second", float, 0.01, 100.0)
450
+
451
+ return filter_config
452
+
453
+
454
+ def validate_query_string(query: Any, max_length: int = 200) -> str:
455
+ """
456
+ Validate search query string.
457
+
458
+ Args:
459
+ query: Query to validate
460
+ max_length: Maximum query length
461
+
462
+ Returns:
463
+ Validated query
464
+
465
+ Raises:
466
+ InvalidParameterError: If query is invalid
467
+ """
468
+ query = validate_string(query, "query", 1, max_length)
469
+
470
+ # Remove potentially dangerous characters
471
+ if any(char in query for char in ["<", ">", '"', "'"]):
472
+ raise InvalidParameterError("query", query, "query without HTML/script characters")
473
+
474
+ return query.strip()
475
+
476
+
477
+ def validate_dict_config(config: Any, parameter_name: str = "config", allow_none: bool = True) -> Optional[dict]:
478
+ """
479
+ Validate dictionary configuration.
480
+
481
+ Args:
482
+ config: Config dict to validate
483
+ parameter_name: Name of parameter for error messages
484
+ allow_none: Whether to allow None values
485
+
486
+ Returns:
487
+ Validated config dict
488
+
489
+ Raises:
490
+ InvalidTypeError: If config is not dict or None
491
+ """
492
+ if config is None and allow_none:
493
+ return None
494
+
495
+ if not isinstance(config, dict):
496
+ raise InvalidTypeError(parameter_name, config, "dict")
497
+
498
+ return config
499
+
500
+
501
+ def validate_boolean(value: Any, parameter_name: str) -> bool:
502
+ """
503
+ Validate boolean parameter.
504
+
505
+ Args:
506
+ value: Value to validate
507
+ parameter_name: Name of parameter for error messages
508
+
509
+ Returns:
510
+ Validated boolean
511
+
512
+ Raises:
513
+ InvalidTypeError: If value is not boolean
514
+ """
515
+ if not isinstance(value, bool):
516
+ raise InvalidTypeError(parameter_name, value, "bool")
517
+
518
+ return value
519
+
520
+
521
+ def validate_list_not_empty(value: Any, parameter_name: str) -> list:
522
+ """
523
+ Validate that list is not empty.
524
+
525
+ Args:
526
+ value: List to validate
527
+ parameter_name: Name of parameter for error messages
528
+
529
+ Returns:
530
+ Validated list
531
+
532
+ Raises:
533
+ InvalidTypeError: If value is not a list
534
+ EmptyListError: If list is empty
535
+ """
536
+ if not isinstance(value, (list, tuple)):
537
+ raise InvalidTypeError(parameter_name, value, "list")
538
+
539
+ if len(value) == 0:
540
+ raise EmptyListError(parameter_name)
541
+
542
+ return list(value)