swiftshadow 2.0.0__py3-none-any.whl → 2.1.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- swiftshadow/__init__.py +3 -2
- swiftshadow/cache.py +1 -1
- swiftshadow/classes.py +84 -8
- swiftshadow/helpers.py +1 -0
- swiftshadow/providers.py +1 -1
- swiftshadow/validator.py +47 -1
- {swiftshadow-2.0.0.dist-info → swiftshadow-2.1.0.dist-info}/METADATA +2 -1
- swiftshadow-2.1.0.dist-info/RECORD +14 -0
- swiftshadow-2.0.0.dist-info/RECORD +0 -14
- {swiftshadow-2.0.0.dist-info → swiftshadow-2.1.0.dist-info}/WHEEL +0 -0
- {swiftshadow-2.0.0.dist-info → swiftshadow-2.1.0.dist-info}/entry_points.txt +0 -0
- {swiftshadow-2.0.0.dist-info → swiftshadow-2.1.0.dist-info}/licenses/LICENSE +0 -0
swiftshadow/__init__.py
CHANGED
swiftshadow/cache.py
CHANGED
swiftshadow/classes.py
CHANGED
@@ -5,13 +5,15 @@ from pathlib import Path
|
|
5
5
|
from appdirs import user_cache_dir
|
6
6
|
from logging import FileHandler, getLogger, Formatter, StreamHandler, INFO, DEBUG
|
7
7
|
from sys import stdout
|
8
|
-
from pickle import load, dump
|
8
|
+
from pickle import load, dump, loads, dumps
|
9
9
|
from swiftshadow.cache import checkExpiry, getExpiry
|
10
10
|
from swiftshadow.models import CacheData, Proxy as Proxy
|
11
11
|
|
12
12
|
from swiftshadow.exceptions import UnsupportedProxyProtocol
|
13
13
|
from swiftshadow.providers import Providers
|
14
14
|
from asyncio import run
|
15
|
+
import aiofiles
|
16
|
+
|
15
17
|
|
16
18
|
logger = getLogger("swiftshadow")
|
17
19
|
logger.setLevel(INFO)
|
@@ -33,6 +35,7 @@ class ProxyInterface:
|
|
33
35
|
protocol (Literal['https', 'http']): Proxy protocol to use. Defaults to 'http'.
|
34
36
|
maxproxies (int): Maximum number of proxies to collect from providers. Defaults to 10.
|
35
37
|
autorotate (bool): Whether to automatically rotate proxy on each get() call. Defaults to False.
|
38
|
+
autoUpdate (bool): Whether to automatically update proxies upon class initalisation. Defaults to True.
|
36
39
|
cachePeriod (int): Number of minutes before cache is considered expired. Defaults to 10.
|
37
40
|
cacheFolderPath (Path): Filesystem path for cache storage. Uses system cache dir by default.
|
38
41
|
proxies (list[Proxy]): List of available proxy objects.
|
@@ -60,6 +63,7 @@ class ProxyInterface:
|
|
60
63
|
protocol: Literal["https", "http"] = "http",
|
61
64
|
maxProxies: int = 10,
|
62
65
|
autoRotate: bool = False,
|
66
|
+
autoUpdate: bool = True,
|
63
67
|
cachePeriod: int = 10,
|
64
68
|
cacheFolderPath: Path | None = None,
|
65
69
|
debug: bool = False,
|
@@ -72,6 +76,7 @@ class ProxyInterface:
|
|
72
76
|
protocol: Proxy protocol to retrieve. Choose between 'http' or 'https'.
|
73
77
|
maxProxies: Maximum proxies to collect from all providers combined.
|
74
78
|
autoRotate: Enable automatic proxy rotation on every get() call.
|
79
|
+
autoUpdate (bool): Whether to automatically update proxies upon class initalisation.
|
75
80
|
cachePeriod: Cache validity duration in minutes.
|
76
81
|
cacheFolderPath: Custom path for cache storage. Uses system cache dir if None.
|
77
82
|
debug: Enable debug logging level when True.
|
@@ -108,8 +113,74 @@ class ProxyInterface:
|
|
108
113
|
self.proxies: list[Proxy] = []
|
109
114
|
self.current: Proxy | None = None
|
110
115
|
self.cacheExpiry: datetime | None = None
|
116
|
+
self.autoUpdate = autoUpdate
|
117
|
+
|
118
|
+
if self.autoUpdate:
|
119
|
+
self.update()
|
120
|
+
|
121
|
+
async def async_update(self):
|
122
|
+
"""
|
123
|
+
Updates proxy list from providers or cache in async.
|
124
|
+
|
125
|
+
First attempts to load valid proxies from cache. If cache is expired/missing,
|
126
|
+
fetches fresh proxies from registered providers that match country and protocol filters.
|
127
|
+
Updates cache file with new proxies if fetched from providers.
|
128
|
+
|
129
|
+
Raises:
|
130
|
+
ValueError: If no proxies found after provider scraping.
|
131
|
+
"""
|
132
|
+
try:
|
133
|
+
async with aiofiles.open(
|
134
|
+
self.cacheFolderPath.joinpath("swiftshadow.pickle"), "rb"
|
135
|
+
) as cacheFile:
|
136
|
+
pickled_bytes = await cacheFile.read()
|
137
|
+
cache: CacheData = loads(pickled_bytes)
|
111
138
|
|
112
|
-
|
139
|
+
if not checkExpiry(cache.expiryIn):
|
140
|
+
self.proxies = cache.proxies
|
141
|
+
logger.info("Loaded proxies from cache.")
|
142
|
+
logger.debug(
|
143
|
+
f"Cache with {len(cache.proxies)} proxies, expire in {cache.expiryIn}"
|
144
|
+
)
|
145
|
+
self.current = self.proxies[0]
|
146
|
+
self.cacheExpiry = cache.expiryIn
|
147
|
+
logger.debug(f"Cache set to expire at {cache.expiryIn}")
|
148
|
+
return
|
149
|
+
else:
|
150
|
+
logger.info("Cache Expired")
|
151
|
+
except FileNotFoundError:
|
152
|
+
logger.info("No cache found, will be created after update.")
|
153
|
+
|
154
|
+
self.proxies = []
|
155
|
+
|
156
|
+
for provider in Providers:
|
157
|
+
if self.protocol not in provider.protocols:
|
158
|
+
continue
|
159
|
+
if (len(self.countries) != 0) and (not provider.countryFilter):
|
160
|
+
continue
|
161
|
+
providerProxies: list[Proxy] = await provider.providerFunction(
|
162
|
+
self.countries, self.protocol
|
163
|
+
)
|
164
|
+
logger.debug(
|
165
|
+
f"{len(providerProxies)} proxies from {provider.providerFunction.__name__}"
|
166
|
+
)
|
167
|
+
self.proxies.extend(providerProxies)
|
168
|
+
|
169
|
+
if len(self.proxies) >= self.maxproxies:
|
170
|
+
break
|
171
|
+
|
172
|
+
if len(self.proxies) == 0:
|
173
|
+
raise ValueError("No proxies where found for the current filter settings.")
|
174
|
+
|
175
|
+
async with aiofiles.open(
|
176
|
+
self.cacheFolderPath.joinpath("swiftshadow.pickle"), "wb+"
|
177
|
+
) as cacheFile:
|
178
|
+
cacheExpiry = getExpiry(self.cachePeriod)
|
179
|
+
self.cacheExpiry = cacheExpiry
|
180
|
+
cache = CacheData(cacheExpiry, self.proxies)
|
181
|
+
pickled_bytes = dumps(cache)
|
182
|
+
_ = await cacheFile.write(pickled_bytes)
|
183
|
+
self.current = self.proxies[0]
|
113
184
|
|
114
185
|
def update(self):
|
115
186
|
"""
|
@@ -135,6 +206,7 @@ class ProxyInterface:
|
|
135
206
|
f"Cache with {len(cache.proxies)} proxies, expire in {cache.expiryIn}"
|
136
207
|
)
|
137
208
|
self.current = self.proxies[0]
|
209
|
+
logger.debug(f"Cache set to expire at {cache.expiryIn}")
|
138
210
|
self.cacheExpiry = cache.expiryIn
|
139
211
|
return
|
140
212
|
else:
|
@@ -142,6 +214,8 @@ class ProxyInterface:
|
|
142
214
|
except FileNotFoundError:
|
143
215
|
logger.info("No cache found, will be created after update.")
|
144
216
|
|
217
|
+
self.proxies = []
|
218
|
+
|
145
219
|
for provider in Providers:
|
146
220
|
if self.protocol not in provider.protocols:
|
147
221
|
continue
|
@@ -184,11 +258,13 @@ class ProxyInterface:
|
|
184
258
|
Raises:
|
185
259
|
ValueError: If validate_cache=True but no cache exists.
|
186
260
|
"""
|
187
|
-
if
|
188
|
-
if
|
189
|
-
self.
|
190
|
-
|
191
|
-
|
261
|
+
if validate_cache:
|
262
|
+
if self.cacheExpiry:
|
263
|
+
if checkExpiry(self.cacheExpiry):
|
264
|
+
logger.debug("Cache Expired on rotate call, updating.")
|
265
|
+
self.update()
|
266
|
+
else:
|
267
|
+
raise ValueError("No cache available but validate_cache is true.")
|
192
268
|
self.current = choice(self.proxies)
|
193
269
|
|
194
270
|
def get(self) -> Proxy:
|
@@ -206,7 +282,7 @@ class ProxyInterface:
|
|
206
282
|
"""
|
207
283
|
|
208
284
|
if self.autorotate:
|
209
|
-
self.rotate()
|
285
|
+
self.rotate(validate_cache=self.autoUpdate)
|
210
286
|
if self.current:
|
211
287
|
return self.current
|
212
288
|
else:
|
swiftshadow/helpers.py
CHANGED
swiftshadow/providers.py
CHANGED
@@ -3,7 +3,7 @@ from typing import Literal
|
|
3
3
|
from requests import get
|
4
4
|
|
5
5
|
from swiftshadow.helpers import plaintextToProxies
|
6
|
-
from swiftshadow.models import
|
6
|
+
from swiftshadow.models import Provider, Proxy
|
7
7
|
from swiftshadow.types import MonosansProxyDict
|
8
8
|
from swiftshadow.validator import validate_proxies
|
9
9
|
|
swiftshadow/validator.py
CHANGED
@@ -1,10 +1,47 @@
|
|
1
1
|
import asyncio
|
2
|
+
import re
|
2
3
|
|
3
4
|
import aiohttp
|
4
5
|
|
5
6
|
from swiftshadow.models import Proxy
|
6
7
|
|
7
8
|
|
9
|
+
def find_ipv4_in_string(input_string: str) -> str | None:
|
10
|
+
"""
|
11
|
+
Extract IPv4 Address from input and return the same.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
input_string: Input string
|
15
|
+
|
16
|
+
Returns:
|
17
|
+
ipv4_address: If found
|
18
|
+
"""
|
19
|
+
ipv4_pattern = r"\b((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\b"
|
20
|
+
|
21
|
+
match = re.search(ipv4_pattern, input_string)
|
22
|
+
|
23
|
+
if match:
|
24
|
+
return match.group(0)
|
25
|
+
else:
|
26
|
+
return None
|
27
|
+
|
28
|
+
|
29
|
+
async def get_host_ip(async_session: aiohttp.ClientSession) -> str | None:
|
30
|
+
"""
|
31
|
+
Gets the hosts external IP for validation.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
async_session: AioHTTP client session object
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
text: Host IP
|
38
|
+
"""
|
39
|
+
async with async_session.get("http://checkip.amazonaws.com") as response:
|
40
|
+
text = await response.text()
|
41
|
+
ip = find_ipv4_in_string(text)
|
42
|
+
return ip
|
43
|
+
|
44
|
+
|
8
45
|
async def check_proxy(async_session: aiohttp.ClientSession, proxy: Proxy) -> str:
|
9
46
|
"""
|
10
47
|
Check one proxy abject.
|
@@ -38,13 +75,22 @@ async def validate_proxies(proxies: list[Proxy]) -> list[Proxy]:
|
|
38
75
|
working_proxies: list[Proxy] = []
|
39
76
|
async with aiohttp.ClientSession() as async_session:
|
40
77
|
tasks = []
|
78
|
+
|
79
|
+
host_task = asyncio.create_task(coro=get_host_ip(async_session))
|
80
|
+
tasks.append(host_task)
|
81
|
+
|
41
82
|
for proxy in proxies:
|
42
83
|
task = asyncio.create_task(coro=check_proxy(async_session, proxy))
|
43
84
|
tasks.append(task)
|
85
|
+
|
44
86
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
87
|
+
host_ip = results[0]
|
88
|
+
results = results[1:]
|
89
|
+
|
45
90
|
for proxy, result in zip(proxies, results):
|
46
91
|
if type(result) is not str:
|
47
92
|
continue
|
48
|
-
|
93
|
+
result_ip = find_ipv4_in_string(result)
|
94
|
+
if result_ip and result_ip != host_ip:
|
49
95
|
working_proxies.append(proxy)
|
50
96
|
return working_proxies
|
@@ -1,10 +1,11 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: swiftshadow
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.1.0
|
4
4
|
Summary: Free IP Proxy rotator for python
|
5
5
|
Author-email: sachin-sankar <mail.sachinsankar@gmail.com>
|
6
6
|
License-File: LICENSE
|
7
7
|
Requires-Python: >=3.12
|
8
|
+
Requires-Dist: aiofiles>=24.1.0
|
8
9
|
Requires-Dist: aiohttp>=3.11.11
|
9
10
|
Requires-Dist: appdirs>=1.4.4
|
10
11
|
Requires-Dist: requests>=2.32.3
|
@@ -0,0 +1,14 @@
|
|
1
|
+
swiftshadow/__init__.py,sha256=oxJDmG0YO0XyYGQpa9V_o38lZnoie7U_d4Ik_h46UXs,922
|
2
|
+
swiftshadow/cache.py,sha256=Mg8xsD6K3K012sILBwD2EZH6CE5kWCQNKCfZ5yadalI,800
|
3
|
+
swiftshadow/classes.py,sha256=nTR_zykns2oxJ4Bj2UXxoXmLqNOMtkLqy17p_LH8odc,11423
|
4
|
+
swiftshadow/exceptions.py,sha256=qu4eXyrkWD9qd4HCIR-8vRfVcqLlTupo4sD72alCdug,129
|
5
|
+
swiftshadow/helpers.py,sha256=kC5PvfvDCQwigAmfkxlhb4PhcmgdCNpJksMiqyx9lU4,960
|
6
|
+
swiftshadow/models.py,sha256=YyfZV98tPdLnF1O3WmTNUNoK4t0GuchfEftzjiM03ck,1678
|
7
|
+
swiftshadow/providers.py,sha256=Z_NEOlMx0rr1Z5rfz-rA1kQ4YHKGRuI65jBxMGOyJkE,5660
|
8
|
+
swiftshadow/types.py,sha256=Alyw3n54OESX1vSR-0kTvpYTlJ8LKfy5J9WZbtglHpE,894
|
9
|
+
swiftshadow/validator.py,sha256=z0dmRKxhvPETSXfC2hTImL0t8pw0zjSYIhUaDMJcJhU,2469
|
10
|
+
swiftshadow-2.1.0.dist-info/METADATA,sha256=NTPUMBc5xlCEkcryOEC81Au_MCD8JPO8QxrhkEDo7AM,3293
|
11
|
+
swiftshadow-2.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
12
|
+
swiftshadow-2.1.0.dist-info/entry_points.txt,sha256=yMj0uEagcmXK2dmMmNXWebTpTT9j5K03oaRrd2wkyLA,49
|
13
|
+
swiftshadow-2.1.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
14
|
+
swiftshadow-2.1.0.dist-info/RECORD,,
|
@@ -1,14 +0,0 @@
|
|
1
|
-
swiftshadow/__init__.py,sha256=DCxCxaMrluQDVJLyK5K61NxSaadD1d-nyTrFKsjfNDI,921
|
2
|
-
swiftshadow/cache.py,sha256=eA_AWa8EsPdB6xD__ButvQdqETC4i89qEgxmHQV2XWU,800
|
3
|
-
swiftshadow/classes.py,sha256=4hr0h2aOb-CXYPrf_ESf05RSEdRVLmYtvx09npnUVhQ,8242
|
4
|
-
swiftshadow/exceptions.py,sha256=qu4eXyrkWD9qd4HCIR-8vRfVcqLlTupo4sD72alCdug,129
|
5
|
-
swiftshadow/helpers.py,sha256=hHJ_JjRx2UFC5Ircl75LeYKBNDYTY_xMy2iWCk-UPqo,959
|
6
|
-
swiftshadow/models.py,sha256=YyfZV98tPdLnF1O3WmTNUNoK4t0GuchfEftzjiM03ck,1678
|
7
|
-
swiftshadow/providers.py,sha256=myJ6t-WD20wrjc7qDhxCpX1-oi7ipQutp34XKM2tjeI,5660
|
8
|
-
swiftshadow/types.py,sha256=Alyw3n54OESX1vSR-0kTvpYTlJ8LKfy5J9WZbtglHpE,894
|
9
|
-
swiftshadow/validator.py,sha256=dBcb6Lev6ojsk9f45IjyEUHZXsCcylyn6RoZA1WPDNQ,1345
|
10
|
-
swiftshadow-2.0.0.dist-info/METADATA,sha256=MSM2WB68lsN__C1JiNwMH2sPMah2GrPUoCwyNbPoF9U,3261
|
11
|
-
swiftshadow-2.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
12
|
-
swiftshadow-2.0.0.dist-info/entry_points.txt,sha256=yMj0uEagcmXK2dmMmNXWebTpTT9j5K03oaRrd2wkyLA,49
|
13
|
-
swiftshadow-2.0.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
14
|
-
swiftshadow-2.0.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|