swiftshadow 2.0.0__py3-none-any.whl → 2.1.0__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.
- 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
|