webscout 8.3.6__py3-none-any.whl → 2025.10.11__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.

Potentially problematic release.


This version of webscout might be problematic. Click here for more details.

Files changed (304) hide show
  1. webscout/AIauto.py +250 -250
  2. webscout/AIbase.py +379 -379
  3. webscout/AIutel.py +60 -58
  4. webscout/Bard.py +1012 -1012
  5. webscout/Bing_search.py +417 -417
  6. webscout/DWEBS.py +529 -529
  7. webscout/Extra/Act.md +309 -309
  8. webscout/Extra/GitToolkit/__init__.py +10 -10
  9. webscout/Extra/GitToolkit/gitapi/README.md +110 -110
  10. webscout/Extra/GitToolkit/gitapi/__init__.py +11 -11
  11. webscout/Extra/GitToolkit/gitapi/repository.py +195 -195
  12. webscout/Extra/GitToolkit/gitapi/user.py +96 -96
  13. webscout/Extra/GitToolkit/gitapi/utils.py +61 -61
  14. webscout/Extra/YTToolkit/README.md +375 -375
  15. webscout/Extra/YTToolkit/YTdownloader.py +956 -956
  16. webscout/Extra/YTToolkit/__init__.py +2 -2
  17. webscout/Extra/YTToolkit/transcriber.py +475 -475
  18. webscout/Extra/YTToolkit/ytapi/README.md +44 -44
  19. webscout/Extra/YTToolkit/ytapi/__init__.py +6 -6
  20. webscout/Extra/YTToolkit/ytapi/channel.py +307 -307
  21. webscout/Extra/YTToolkit/ytapi/errors.py +13 -13
  22. webscout/Extra/YTToolkit/ytapi/extras.py +118 -118
  23. webscout/Extra/YTToolkit/ytapi/https.py +88 -88
  24. webscout/Extra/YTToolkit/ytapi/patterns.py +61 -61
  25. webscout/Extra/YTToolkit/ytapi/playlist.py +58 -58
  26. webscout/Extra/YTToolkit/ytapi/pool.py +7 -7
  27. webscout/Extra/YTToolkit/ytapi/query.py +39 -39
  28. webscout/Extra/YTToolkit/ytapi/stream.py +62 -62
  29. webscout/Extra/YTToolkit/ytapi/utils.py +62 -62
  30. webscout/Extra/YTToolkit/ytapi/video.py +232 -232
  31. webscout/Extra/autocoder/__init__.py +9 -9
  32. webscout/Extra/autocoder/autocoder.py +1105 -1105
  33. webscout/Extra/autocoder/autocoder_utiles.py +332 -332
  34. webscout/Extra/gguf.md +429 -429
  35. webscout/Extra/gguf.py +1213 -1213
  36. webscout/Extra/tempmail/README.md +487 -487
  37. webscout/Extra/tempmail/__init__.py +27 -27
  38. webscout/Extra/tempmail/async_utils.py +140 -140
  39. webscout/Extra/tempmail/base.py +160 -160
  40. webscout/Extra/tempmail/cli.py +186 -186
  41. webscout/Extra/tempmail/emailnator.py +84 -84
  42. webscout/Extra/tempmail/mail_tm.py +360 -360
  43. webscout/Extra/tempmail/temp_mail_io.py +291 -291
  44. webscout/Extra/weather.md +281 -281
  45. webscout/Extra/weather.py +193 -193
  46. webscout/Litlogger/README.md +10 -10
  47. webscout/Litlogger/__init__.py +15 -15
  48. webscout/Litlogger/formats.py +13 -13
  49. webscout/Litlogger/handlers.py +121 -121
  50. webscout/Litlogger/levels.py +13 -13
  51. webscout/Litlogger/logger.py +134 -134
  52. webscout/Provider/AISEARCH/Perplexity.py +332 -332
  53. webscout/Provider/AISEARCH/README.md +279 -279
  54. webscout/Provider/AISEARCH/__init__.py +33 -11
  55. webscout/Provider/AISEARCH/felo_search.py +206 -206
  56. webscout/Provider/AISEARCH/genspark_search.py +323 -323
  57. webscout/Provider/AISEARCH/hika_search.py +185 -185
  58. webscout/Provider/AISEARCH/iask_search.py +410 -410
  59. webscout/Provider/AISEARCH/monica_search.py +219 -219
  60. webscout/Provider/AISEARCH/scira_search.py +316 -314
  61. webscout/Provider/AISEARCH/stellar_search.py +177 -177
  62. webscout/Provider/AISEARCH/webpilotai_search.py +255 -255
  63. webscout/Provider/Aitopia.py +314 -315
  64. webscout/Provider/Andi.py +3 -3
  65. webscout/Provider/Apriel.py +306 -0
  66. webscout/Provider/ChatGPTClone.py +236 -236
  67. webscout/Provider/ChatSandbox.py +343 -342
  68. webscout/Provider/Cloudflare.py +324 -324
  69. webscout/Provider/Cohere.py +208 -207
  70. webscout/Provider/Deepinfra.py +370 -369
  71. webscout/Provider/ExaAI.py +260 -260
  72. webscout/Provider/ExaChat.py +308 -387
  73. webscout/Provider/Flowith.py +221 -221
  74. webscout/Provider/GMI.py +293 -0
  75. webscout/Provider/Gemini.py +164 -162
  76. webscout/Provider/GeminiProxy.py +167 -166
  77. webscout/Provider/GithubChat.py +371 -370
  78. webscout/Provider/Groq.py +800 -800
  79. webscout/Provider/HeckAI.py +383 -379
  80. webscout/Provider/Jadve.py +282 -297
  81. webscout/Provider/K2Think.py +308 -0
  82. webscout/Provider/Koboldai.py +206 -384
  83. webscout/Provider/LambdaChat.py +423 -425
  84. webscout/Provider/Nemotron.py +244 -245
  85. webscout/Provider/Netwrck.py +248 -247
  86. webscout/Provider/OLLAMA.py +395 -394
  87. webscout/Provider/OPENAI/Cloudflare.py +394 -395
  88. webscout/Provider/OPENAI/FalconH1.py +452 -457
  89. webscout/Provider/OPENAI/FreeGemini.py +297 -299
  90. webscout/Provider/OPENAI/{monochat.py → K2Think.py} +432 -329
  91. webscout/Provider/OPENAI/NEMOTRON.py +241 -244
  92. webscout/Provider/OPENAI/PI.py +428 -427
  93. webscout/Provider/OPENAI/README.md +959 -959
  94. webscout/Provider/OPENAI/TogetherAI.py +345 -345
  95. webscout/Provider/OPENAI/TwoAI.py +466 -467
  96. webscout/Provider/OPENAI/__init__.py +33 -59
  97. webscout/Provider/OPENAI/ai4chat.py +313 -303
  98. webscout/Provider/OPENAI/base.py +249 -269
  99. webscout/Provider/OPENAI/chatglm.py +528 -0
  100. webscout/Provider/OPENAI/chatgpt.py +593 -588
  101. webscout/Provider/OPENAI/chatgptclone.py +521 -524
  102. webscout/Provider/OPENAI/chatsandbox.py +202 -177
  103. webscout/Provider/OPENAI/deepinfra.py +319 -315
  104. webscout/Provider/OPENAI/e2b.py +1665 -1665
  105. webscout/Provider/OPENAI/exaai.py +420 -420
  106. webscout/Provider/OPENAI/exachat.py +452 -452
  107. webscout/Provider/OPENAI/friendli.py +232 -232
  108. webscout/Provider/OPENAI/{refact.py → gmi.py} +324 -274
  109. webscout/Provider/OPENAI/groq.py +364 -364
  110. webscout/Provider/OPENAI/heckai.py +314 -311
  111. webscout/Provider/OPENAI/llmchatco.py +337 -337
  112. webscout/Provider/OPENAI/netwrck.py +355 -354
  113. webscout/Provider/OPENAI/oivscode.py +290 -290
  114. webscout/Provider/OPENAI/opkfc.py +518 -518
  115. webscout/Provider/OPENAI/pydantic_imports.py +1 -1
  116. webscout/Provider/OPENAI/scirachat.py +535 -529
  117. webscout/Provider/OPENAI/sonus.py +308 -308
  118. webscout/Provider/OPENAI/standardinput.py +442 -442
  119. webscout/Provider/OPENAI/textpollinations.py +340 -348
  120. webscout/Provider/OPENAI/toolbaz.py +419 -413
  121. webscout/Provider/OPENAI/typefully.py +362 -362
  122. webscout/Provider/OPENAI/utils.py +295 -295
  123. webscout/Provider/OPENAI/venice.py +436 -436
  124. webscout/Provider/OPENAI/wisecat.py +387 -387
  125. webscout/Provider/OPENAI/writecream.py +166 -166
  126. webscout/Provider/OPENAI/x0gpt.py +378 -378
  127. webscout/Provider/OPENAI/yep.py +389 -389
  128. webscout/Provider/OpenGPT.py +230 -230
  129. webscout/Provider/Openai.py +244 -496
  130. webscout/Provider/PI.py +405 -404
  131. webscout/Provider/Perplexitylabs.py +430 -431
  132. webscout/Provider/QwenLM.py +272 -254
  133. webscout/Provider/STT/__init__.py +32 -2
  134. webscout/Provider/{Llama3.py → Sambanova.py} +257 -258
  135. webscout/Provider/StandardInput.py +309 -309
  136. webscout/Provider/TTI/README.md +82 -82
  137. webscout/Provider/TTI/__init__.py +33 -12
  138. webscout/Provider/TTI/aiarta.py +413 -413
  139. webscout/Provider/TTI/base.py +136 -136
  140. webscout/Provider/TTI/bing.py +243 -243
  141. webscout/Provider/TTI/gpt1image.py +149 -149
  142. webscout/Provider/TTI/imagen.py +196 -196
  143. webscout/Provider/TTI/infip.py +211 -211
  144. webscout/Provider/TTI/magicstudio.py +232 -232
  145. webscout/Provider/TTI/monochat.py +219 -219
  146. webscout/Provider/TTI/piclumen.py +214 -214
  147. webscout/Provider/TTI/pixelmuse.py +232 -232
  148. webscout/Provider/TTI/pollinations.py +232 -232
  149. webscout/Provider/TTI/together.py +288 -288
  150. webscout/Provider/TTI/utils.py +12 -12
  151. webscout/Provider/TTI/venice.py +367 -367
  152. webscout/Provider/TTS/README.md +192 -192
  153. webscout/Provider/TTS/__init__.py +33 -10
  154. webscout/Provider/TTS/parler.py +110 -110
  155. webscout/Provider/TTS/streamElements.py +333 -333
  156. webscout/Provider/TTS/utils.py +280 -280
  157. webscout/Provider/TeachAnything.py +237 -236
  158. webscout/Provider/TextPollinationsAI.py +311 -318
  159. webscout/Provider/TogetherAI.py +356 -357
  160. webscout/Provider/TwoAI.py +313 -569
  161. webscout/Provider/TypliAI.py +312 -311
  162. webscout/Provider/UNFINISHED/ChatHub.py +208 -208
  163. webscout/Provider/UNFINISHED/ChutesAI.py +313 -313
  164. webscout/Provider/{GizAI.py → UNFINISHED/GizAI.py} +294 -294
  165. webscout/Provider/{Marcus.py → UNFINISHED/Marcus.py} +198 -198
  166. webscout/Provider/{Qodo.py → UNFINISHED/Qodo.py} +477 -477
  167. webscout/Provider/UNFINISHED/VercelAIGateway.py +338 -338
  168. webscout/Provider/{XenAI.py → UNFINISHED/XenAI.py} +324 -324
  169. webscout/Provider/UNFINISHED/Youchat.py +330 -330
  170. webscout/Provider/UNFINISHED/liner.py +334 -0
  171. webscout/Provider/UNFINISHED/liner_api_request.py +262 -262
  172. webscout/Provider/UNFINISHED/puterjs.py +634 -634
  173. webscout/Provider/UNFINISHED/samurai.py +223 -223
  174. webscout/Provider/UNFINISHED/test_lmarena.py +119 -119
  175. webscout/Provider/Venice.py +251 -250
  176. webscout/Provider/VercelAI.py +256 -255
  177. webscout/Provider/WiseCat.py +232 -231
  178. webscout/Provider/WrDoChat.py +367 -366
  179. webscout/Provider/__init__.py +33 -86
  180. webscout/Provider/ai4chat.py +174 -174
  181. webscout/Provider/akashgpt.py +331 -334
  182. webscout/Provider/cerebras.py +446 -340
  183. webscout/Provider/chatglm.py +394 -214
  184. webscout/Provider/cleeai.py +211 -212
  185. webscout/Provider/deepseek_assistant.py +1 -1
  186. webscout/Provider/elmo.py +282 -282
  187. webscout/Provider/geminiapi.py +208 -208
  188. webscout/Provider/granite.py +261 -261
  189. webscout/Provider/hermes.py +263 -265
  190. webscout/Provider/julius.py +223 -222
  191. webscout/Provider/learnfastai.py +309 -309
  192. webscout/Provider/llama3mitril.py +214 -214
  193. webscout/Provider/llmchat.py +243 -243
  194. webscout/Provider/llmchatco.py +290 -290
  195. webscout/Provider/meta.py +801 -801
  196. webscout/Provider/oivscode.py +309 -309
  197. webscout/Provider/scira_chat.py +384 -457
  198. webscout/Provider/searchchat.py +292 -291
  199. webscout/Provider/sonus.py +258 -258
  200. webscout/Provider/toolbaz.py +370 -364
  201. webscout/Provider/turboseek.py +274 -265
  202. webscout/Provider/typefully.py +208 -207
  203. webscout/Provider/x0gpt.py +1 -0
  204. webscout/Provider/yep.py +372 -371
  205. webscout/__init__.py +30 -31
  206. webscout/__main__.py +5 -5
  207. webscout/auth/api_key_manager.py +189 -189
  208. webscout/auth/config.py +175 -175
  209. webscout/auth/models.py +185 -185
  210. webscout/auth/routes.py +664 -664
  211. webscout/auth/simple_logger.py +236 -236
  212. webscout/cli.py +523 -523
  213. webscout/conversation.py +438 -438
  214. webscout/exceptions.py +361 -361
  215. webscout/litagent/Readme.md +298 -298
  216. webscout/litagent/__init__.py +28 -28
  217. webscout/litagent/agent.py +581 -581
  218. webscout/litagent/constants.py +59 -59
  219. webscout/litprinter/__init__.py +58 -58
  220. webscout/models.py +181 -181
  221. webscout/optimizers.py +419 -419
  222. webscout/prompt_manager.py +288 -288
  223. webscout/sanitize.py +1078 -1078
  224. webscout/scout/README.md +401 -401
  225. webscout/scout/__init__.py +8 -8
  226. webscout/scout/core/__init__.py +6 -6
  227. webscout/scout/core/crawler.py +297 -297
  228. webscout/scout/core/scout.py +706 -706
  229. webscout/scout/core/search_result.py +95 -95
  230. webscout/scout/core/text_analyzer.py +62 -62
  231. webscout/scout/core/text_utils.py +277 -277
  232. webscout/scout/core/web_analyzer.py +51 -51
  233. webscout/scout/element.py +599 -599
  234. webscout/scout/parsers/__init__.py +69 -69
  235. webscout/scout/parsers/html5lib_parser.py +172 -172
  236. webscout/scout/parsers/html_parser.py +236 -236
  237. webscout/scout/parsers/lxml_parser.py +178 -178
  238. webscout/scout/utils.py +37 -37
  239. webscout/swiftcli/Readme.md +323 -323
  240. webscout/swiftcli/__init__.py +95 -95
  241. webscout/swiftcli/core/__init__.py +7 -7
  242. webscout/swiftcli/core/cli.py +308 -308
  243. webscout/swiftcli/core/context.py +104 -104
  244. webscout/swiftcli/core/group.py +241 -241
  245. webscout/swiftcli/decorators/__init__.py +28 -28
  246. webscout/swiftcli/decorators/command.py +221 -221
  247. webscout/swiftcli/decorators/options.py +220 -220
  248. webscout/swiftcli/decorators/output.py +302 -302
  249. webscout/swiftcli/exceptions.py +21 -21
  250. webscout/swiftcli/plugins/__init__.py +9 -9
  251. webscout/swiftcli/plugins/base.py +135 -135
  252. webscout/swiftcli/plugins/manager.py +269 -269
  253. webscout/swiftcli/utils/__init__.py +59 -59
  254. webscout/swiftcli/utils/formatting.py +252 -252
  255. webscout/swiftcli/utils/parsing.py +267 -267
  256. webscout/update_checker.py +117 -117
  257. webscout/version.py +1 -1
  258. webscout/webscout_search.py +1183 -1183
  259. webscout/webscout_search_async.py +649 -649
  260. webscout/yep_search.py +346 -346
  261. webscout/zeroart/README.md +89 -89
  262. webscout/zeroart/__init__.py +134 -134
  263. webscout/zeroart/base.py +66 -66
  264. webscout/zeroart/effects.py +100 -100
  265. webscout/zeroart/fonts.py +1238 -1238
  266. {webscout-8.3.6.dist-info → webscout-2025.10.11.dist-info}/METADATA +937 -936
  267. webscout-2025.10.11.dist-info/RECORD +300 -0
  268. webscout/Provider/AISEARCH/DeepFind.py +0 -254
  269. webscout/Provider/AllenAI.py +0 -440
  270. webscout/Provider/Blackboxai.py +0 -793
  271. webscout/Provider/FreeGemini.py +0 -250
  272. webscout/Provider/GptOss.py +0 -207
  273. webscout/Provider/Hunyuan.py +0 -283
  274. webscout/Provider/Kimi.py +0 -445
  275. webscout/Provider/MCPCore.py +0 -322
  276. webscout/Provider/MiniMax.py +0 -207
  277. webscout/Provider/OPENAI/BLACKBOXAI.py +0 -1045
  278. webscout/Provider/OPENAI/MiniMax.py +0 -298
  279. webscout/Provider/OPENAI/Qwen3.py +0 -304
  280. webscout/Provider/OPENAI/autoproxy.py +0 -1067
  281. webscout/Provider/OPENAI/copilot.py +0 -321
  282. webscout/Provider/OPENAI/gptoss.py +0 -288
  283. webscout/Provider/OPENAI/kimi.py +0 -469
  284. webscout/Provider/OPENAI/mcpcore.py +0 -431
  285. webscout/Provider/OPENAI/multichat.py +0 -378
  286. webscout/Provider/OPENAI/qodo.py +0 -630
  287. webscout/Provider/OPENAI/xenai.py +0 -514
  288. webscout/Provider/Reka.py +0 -214
  289. webscout/Provider/UNFINISHED/fetch_together_models.py +0 -90
  290. webscout/Provider/asksteve.py +0 -220
  291. webscout/Provider/copilot.py +0 -441
  292. webscout/Provider/freeaichat.py +0 -294
  293. webscout/Provider/koala.py +0 -182
  294. webscout/Provider/lmarena.py +0 -198
  295. webscout/Provider/monochat.py +0 -275
  296. webscout/Provider/multichat.py +0 -375
  297. webscout/Provider/scnet.py +0 -244
  298. webscout/Provider/talkai.py +0 -194
  299. webscout/tempid.py +0 -128
  300. webscout-8.3.6.dist-info/RECORD +0 -327
  301. {webscout-8.3.6.dist-info → webscout-2025.10.11.dist-info}/WHEEL +0 -0
  302. {webscout-8.3.6.dist-info → webscout-2025.10.11.dist-info}/entry_points.txt +0 -0
  303. {webscout-8.3.6.dist-info → webscout-2025.10.11.dist-info}/licenses/LICENSE.md +0 -0
  304. {webscout-8.3.6.dist-info → webscout-2025.10.11.dist-info}/top_level.txt +0 -0
@@ -1,1067 +0,0 @@
1
- """
2
- Auto-proxy module for OpenAI-compatible providers.
3
- This module provides automatic proxy injection for HTTP sessions using a remote proxy list.
4
- """
5
-
6
- import random
7
- import time
8
- from abc import ABCMeta
9
- from typing import Dict, List, Optional, Any, Callable, Union
10
- import requests
11
- import functools
12
- from contextlib import contextmanager
13
- import types
14
-
15
- # Optional imports for different HTTP clients
16
- try:
17
- import httpx
18
- except ImportError:
19
- httpx = None
20
-
21
- try:
22
- from curl_cffi.requests import Session as CurlSession
23
- from curl_cffi.requests import AsyncSession as CurlAsyncSession
24
- except ImportError:
25
- CurlSession = None
26
- CurlAsyncSession = None
27
-
28
- # Global proxy cache
29
- _proxy_cache = {
30
- 'proxies': [],
31
- 'last_updated': 0,
32
- 'cache_duration': 300 # 5 minutes
33
- }
34
-
35
- PROXY_SOURCE_URL = "https://proxies.typegpt.net/ips.txt"
36
-
37
- # --- Static Proxy Lists ---
38
- # NordVPN proxies (format: https://host:port:user:pass)
39
- STATIC_NORDVPN_PROXIES = [
40
- "https://pl128.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
41
- "https://be148.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
42
- "https://hu48.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
43
- "https://us5063.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
44
- "https://at86.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
45
- "https://ch217.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
46
- "https://dk152.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
47
- "https://no151.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
48
- "https://ch218.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
49
- "https://uk1784.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
50
- "https://fr555.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
51
- "https://ch219.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
52
- "https://us5064.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
53
- "https://uk765.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
54
- "https://uk812.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
55
- "https://uk813.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
56
- "https://uk814.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
57
- "https://uk871.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
58
- "https://uk873.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
59
- "https://uk875.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
60
- "https://uk877.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
61
- "https://uk879.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
62
- "https://uk884.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
63
- "https://uk886.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
64
- "https://be149.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
65
- "https://uk1806.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
66
- "https://uk888.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
67
- "https://uk890.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
68
- "https://uk892.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
69
- "https://uk894.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
70
- "https://uk896.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
71
- "https://uk898.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
72
- "https://us5055.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
73
- "https://jp429.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
74
- "https://it132.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
75
- "https://us4735.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
76
- "https://pl122.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
77
- "https://cz93.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
78
- "https://at80.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
79
- "https://ro59.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
80
- "https://ch198.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
81
- "https://bg38.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
82
- "https://hu47.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
83
- "https://jp454.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
84
- "https://dk150.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
85
- "https://de750.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
86
- "https://pl125.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
87
- "https://us5057.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
88
- "https://us5058.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
89
- "https://us5059.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
90
- "https://us5060.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJVZdy8KKcEW3ZE5",
91
- "https://no141.nordvpn.com:89:WZBVNB9MCuZu3FLX3D1rUc8a:XRBU8tofEJ"
92
- ]
93
-
94
- # Webshare rotating proxies (format: http://user:pass@host:port)
95
- STATIC_WEBSHARE_PROXIES = [
96
- "http://kkuafwyh-rotate:kl6esmu21js3@p.webshare.io:80",
97
- "http://stzaxffz-rotate:ax92ravj1pmm@p.webshare.io:80",
98
- "http://nfokjhhu-rotate:ez248bgee4z9@p.webshare.io:80",
99
- "http://fiupzkjx-rotate:0zlrd2in3mrh@p.webshare.io:80",
100
- "http://xukpnkpr-rotate:hcmwl8cl4iyw@p.webshare.io:80",
101
- "http://tndgqbid-rotate:qb1cgkl4irh4@p.webshare.io:80",
102
- "http://nnpnjrmj-rotate:8bj089tzcwhz@p.webshare.io:80",
103
- ]
104
-
105
- # Combine all static proxies
106
- STATIC_PROXIES = STATIC_NORDVPN_PROXIES + STATIC_WEBSHARE_PROXIES
107
-
108
-
109
- def fetch_proxies() -> List[str]:
110
- """
111
- Fetch proxy list from the remote source.
112
-
113
- Returns:
114
- List[str]: List of proxy URLs in format 'http://user:pass@host:port'
115
- """
116
- try:
117
- response = requests.get(PROXY_SOURCE_URL, timeout=10)
118
- response.raise_for_status()
119
-
120
- proxies = []
121
- for line in response.text.strip().split('\n'):
122
- line = line.strip()
123
- if line and line.startswith('http://'):
124
- proxies.append(line)
125
-
126
- return proxies
127
-
128
- except Exception:
129
- return []
130
-
131
-
132
- def get_cached_proxies() -> List[str]:
133
- """
134
- Get proxies from cache or fetch new ones if cache is expired.
135
-
136
- Returns:
137
- List[str]: List of proxy URLs
138
- """
139
- current_time = time.time()
140
-
141
- # Check if cache is expired or empty
142
- if (current_time - _proxy_cache['last_updated'] > _proxy_cache['cache_duration'] or
143
- not _proxy_cache['proxies']):
144
-
145
- new_proxies = fetch_proxies()
146
- if new_proxies:
147
- _proxy_cache['proxies'] = new_proxies
148
- _proxy_cache['last_updated'] = current_time
149
- else:
150
- pass
151
-
152
- # Priority: Webshare -> remote -> NordVPN
153
- proxies = STATIC_WEBSHARE_PROXIES + _proxy_cache['proxies'] + STATIC_NORDVPN_PROXIES
154
- proxies = list(dict.fromkeys(proxies)) # Remove duplicates, preserve order
155
- return proxies
156
-
157
-
158
- def get_auto_proxy() -> Optional[str]:
159
- """
160
- Get a random proxy, prioritizing Webshare proxies if available.
161
-
162
- Returns:
163
- Optional[str]: A proxy URL or None if no proxies available
164
- """
165
- proxies = get_cached_proxies()
166
- # Try Webshare proxies first
167
- webshare = [p for p in proxies if p in STATIC_WEBSHARE_PROXIES]
168
- if webshare:
169
- return random.choice(webshare)
170
- if proxies:
171
- return random.choice(proxies)
172
- return None
173
-
174
-
175
- def get_proxy_dict(proxy_url: Optional[str] = None) -> Dict[str, str]:
176
- """
177
- Convert a proxy URL to a dictionary format suitable for requests/httpx.
178
-
179
- Args:
180
- proxy_url: Proxy URL, if None will get one automatically
181
-
182
- Returns:
183
- Dict[str, str]: Proxy dictionary with 'http' and 'https' keys
184
- """
185
- if proxy_url is None:
186
- proxy_url = get_auto_proxy()
187
-
188
- if proxy_url is None:
189
- return {}
190
-
191
- return {
192
- 'http': proxy_url,
193
- 'https': proxy_url
194
- }
195
-
196
-
197
- def test_proxy(proxy_url: str, timeout: int = 10) -> bool:
198
- """
199
- Test if a proxy is working by making a request to a test URL.
200
-
201
- Args:
202
- proxy_url: The proxy URL to test
203
- timeout: Request timeout in seconds
204
-
205
- Returns:
206
- bool: True if proxy is working, False otherwise
207
- """
208
- try:
209
- test_url = "https://httpbin.org/ip"
210
- proxies = {'http': proxy_url, 'https': proxy_url}
211
-
212
- response = requests.get(test_url, proxies=proxies, timeout=timeout)
213
- return response.status_code == 200
214
-
215
- except Exception:
216
- return False
217
-
218
-
219
- class ProxyAutoMeta(ABCMeta):
220
- """
221
- Metaclass to ensure all OpenAICompatibleProvider subclasses automatically get proxy support.
222
- This will inject proxies into any requests.Session, httpx.Client, or curl_cffi session attributes found on the instance.
223
-
224
- To disable automatic proxy injection, set disable_auto_proxy=True in the constructor or
225
- set the class attribute DISABLE_AUTO_PROXY = True.
226
- """
227
-
228
- def __call__(cls, *args, **kwargs):
229
- instance = super().__call__(*args, **kwargs)
230
-
231
- # Check if auto proxy is disabled
232
- disable_auto_proxy = kwargs.get('disable_auto_proxy', False) or getattr(cls, 'DISABLE_AUTO_PROXY', False)
233
-
234
- # Get proxies from various sources
235
- proxies = getattr(instance, 'proxies', None) or kwargs.get('proxies', None)
236
-
237
- if proxies is None and not disable_auto_proxy:
238
- try:
239
- proxy_url = get_auto_proxy()
240
- if proxy_url:
241
- proxies = get_proxy_dict(proxy_url)
242
- else:
243
- proxies = {}
244
- except Exception:
245
- proxies = {}
246
- elif proxies is None:
247
- proxies = {}
248
-
249
- instance.proxies = proxies
250
-
251
- # Set default max proxy attempts for auto-retry functionality
252
- if not hasattr(instance, '_max_proxy_attempts'):
253
- instance._max_proxy_attempts = kwargs.get('max_proxy_attempts', 2)
254
-
255
- # Always patch existing sessions (for both proxy and auto-retry functionality)
256
- _patch_instance_sessions(instance, proxies)
257
-
258
- # Provide helper methods for creating proxied sessions
259
- _add_proxy_helpers(instance, proxies)
260
-
261
- return instance
262
-
263
-
264
- def _patch_instance_sessions(instance: Any, proxies: Dict[str, str]) -> None:
265
- """
266
- Patch existing session objects on the instance with proxy configuration and auto-retry functionality.
267
-
268
- Args:
269
- instance: The class instance to patch
270
- proxies: Proxy dictionary to apply
271
- """
272
- for attr_name in dir(instance):
273
- if attr_name.startswith('_'):
274
- continue
275
-
276
- try:
277
- attr_obj = getattr(instance, attr_name)
278
-
279
- # Patch requests.Session objects
280
- if isinstance(attr_obj, requests.Session):
281
- if proxies:
282
- attr_obj.proxies.update(proxies)
283
- _add_auto_retry_to_session(attr_obj, instance)
284
-
285
- # Patch httpx.Client objects
286
- elif httpx and isinstance(attr_obj, httpx.Client):
287
- try:
288
- # httpx uses different proxy format
289
- if proxies:
290
- attr_obj._proxies = proxies
291
- _add_auto_retry_to_httpx_client(attr_obj, instance)
292
- except Exception:
293
- pass
294
-
295
- # Patch curl_cffi Session objects
296
- elif CurlSession and isinstance(attr_obj, CurlSession):
297
- try:
298
- if proxies:
299
- attr_obj.proxies.update(proxies)
300
- _add_auto_retry_to_curl_session(attr_obj, instance)
301
- except Exception:
302
- pass
303
-
304
- # Patch curl_cffi AsyncSession objects
305
- elif CurlAsyncSession and isinstance(attr_obj, CurlAsyncSession):
306
- try:
307
- if proxies:
308
- attr_obj.proxies.update(proxies)
309
- _add_auto_retry_to_curl_async_session(attr_obj, instance)
310
- except Exception:
311
- pass
312
-
313
- except Exception:
314
- continue
315
-
316
-
317
- def _add_auto_retry_to_session(session: requests.Session, instance: Any) -> None:
318
- """
319
- Add auto-retry functionality to a requests.Session object.
320
-
321
- Args:
322
- session: The requests.Session to patch
323
- instance: The provider instance for context
324
- """
325
- if hasattr(session, '_auto_retry_patched'):
326
- return # Already patched
327
-
328
- original_request = session.request
329
-
330
- def request_with_auto_retry(method, url, **kwargs):
331
- max_proxy_attempts = getattr(instance, '_max_proxy_attempts', 2)
332
- original_proxies = session.proxies.copy()
333
- first_error = None
334
-
335
- # First attempt with current proxy configuration
336
- try:
337
- return original_request(method, url, **kwargs)
338
- except Exception as e:
339
- first_error = e
340
-
341
- # If we have proxies configured, try different ones
342
- if original_proxies:
343
- proxy_attempts = 0
344
-
345
- while proxy_attempts < max_proxy_attempts:
346
- try:
347
- # Get a new proxy
348
- new_proxy_url = get_auto_proxy()
349
- if new_proxy_url:
350
- new_proxies = get_proxy_dict(new_proxy_url)
351
- session.proxies.clear()
352
- session.proxies.update(new_proxies)
353
-
354
- # Try the request with new proxy
355
- return original_request(method, url, **kwargs)
356
- else:
357
- break # No more proxies available
358
-
359
- except Exception:
360
- proxy_attempts += 1
361
- continue
362
-
363
- # All proxy attempts failed, try without proxy
364
- try:
365
- session.proxies.clear()
366
- return original_request(method, url, **kwargs)
367
- except Exception:
368
- # Restore original proxy settings and re-raise the first error
369
- session.proxies.clear()
370
- session.proxies.update(original_proxies)
371
- raise first_error
372
- else:
373
- # No proxies were configured, just re-raise the original error
374
- raise first_error
375
-
376
- session.request = request_with_auto_retry
377
- session._auto_retry_patched = True
378
-
379
-
380
- def _add_auto_retry_to_httpx_client(client, instance: Any) -> None:
381
- """
382
- Add auto-retry functionality to an httpx.Client object.
383
-
384
- Args:
385
- client: The httpx.Client to patch
386
- instance: The provider instance for context
387
- """
388
- if not httpx or hasattr(client, '_auto_retry_patched'):
389
- return # Not available or already patched
390
-
391
- try:
392
- original_request = client.request
393
-
394
- def request_with_auto_retry(method, url, **kwargs):
395
- max_proxy_attempts = getattr(instance, '_max_proxy_attempts', 2)
396
- original_proxies = getattr(client, '_proxies', {}).copy()
397
- first_error = None
398
-
399
- # First attempt with current proxy configuration
400
- try:
401
- return original_request(method, url, **kwargs)
402
- except Exception as e:
403
- first_error = e
404
-
405
- # If we have proxies configured, try different ones
406
- if original_proxies:
407
- proxy_attempts = 0
408
-
409
- while proxy_attempts < max_proxy_attempts:
410
- try:
411
- # Get a new proxy
412
- new_proxy_url = get_auto_proxy()
413
- if new_proxy_url:
414
- new_proxies = get_proxy_dict(new_proxy_url)
415
- client._proxies = new_proxies
416
-
417
- # Try the request with new proxy
418
- return original_request(method, url, **kwargs)
419
- else:
420
- break # No more proxies available
421
-
422
- except Exception:
423
- proxy_attempts += 1
424
- continue
425
-
426
- # All proxy attempts failed, try without proxy
427
- try:
428
- client._proxies = {}
429
- return original_request(method, url, **kwargs)
430
- except Exception:
431
- # Restore original proxy settings and re-raise the first error
432
- client._proxies = original_proxies
433
- raise first_error
434
- else:
435
- # No proxies were configured, just re-raise the original error
436
- raise first_error
437
-
438
- client.request = request_with_auto_retry
439
- client._auto_retry_patched = True
440
- except Exception:
441
- pass
442
-
443
-
444
- def _add_auto_retry_to_curl_session(session, instance: Any) -> None:
445
- """
446
- Add auto-retry functionality to a curl_cffi.Session object.
447
-
448
- Args:
449
- session: The curl_cffi.Session to patch
450
- instance: The provider instance for context
451
- """
452
- if not CurlSession or hasattr(session, '_auto_retry_patched'):
453
- return # Not available or already patched
454
-
455
- try:
456
- original_request = session.request
457
-
458
- def request_with_auto_retry(method, url, **kwargs):
459
- max_proxy_attempts = getattr(instance, '_max_proxy_attempts', 2)
460
- original_proxies = session.proxies.copy()
461
- first_error = None
462
-
463
- # First attempt with current proxy configuration
464
- try:
465
- return original_request(method, url, **kwargs)
466
- except Exception as e:
467
- first_error = e
468
-
469
- # If we have proxies configured, try different ones
470
- if original_proxies:
471
- proxy_attempts = 0
472
-
473
- while proxy_attempts < max_proxy_attempts:
474
- try:
475
- # Get a new proxy
476
- new_proxy_url = get_auto_proxy()
477
- if new_proxy_url:
478
- new_proxies = get_proxy_dict(new_proxy_url)
479
- session.proxies.clear()
480
- session.proxies.update(new_proxies)
481
-
482
- # Try the request with new proxy
483
- return original_request(method, url, **kwargs)
484
- else:
485
- break # No more proxies available
486
-
487
- except Exception:
488
- proxy_attempts += 1
489
- continue
490
-
491
- # All proxy attempts failed, try without proxy
492
- try:
493
- session.proxies.clear()
494
- return original_request(method, url, **kwargs)
495
- except Exception:
496
- # Restore original proxy settings and re-raise the first error
497
- session.proxies.clear()
498
- session.proxies.update(original_proxies)
499
- raise first_error
500
- else:
501
- # No proxies were configured, just re-raise the original error
502
- raise first_error
503
-
504
- session.request = request_with_auto_retry
505
- session._auto_retry_patched = True
506
- except Exception:
507
- pass
508
-
509
-
510
- def _add_auto_retry_to_curl_async_session(session, instance: Any) -> None:
511
- """
512
- Add auto-retry functionality to a curl_cffi.AsyncSession object.
513
-
514
- Args:
515
- session: The curl_cffi.AsyncSession to patch
516
- instance: The provider instance for context
517
- """
518
- if not CurlAsyncSession or hasattr(session, '_auto_retry_patched'):
519
- return # Not available or already patched
520
-
521
- try:
522
- original_request = session.request
523
-
524
- async def request_with_auto_retry(method, url, **kwargs):
525
- max_proxy_attempts = getattr(instance, '_max_proxy_attempts', 2)
526
- original_proxies = session.proxies.copy()
527
- first_error = None
528
-
529
- # First attempt with current proxy configuration
530
- try:
531
- return await original_request(method, url, **kwargs)
532
- except Exception as e:
533
- first_error = e
534
-
535
- # If we have proxies configured, try different ones
536
- if original_proxies:
537
- proxy_attempts = 0
538
-
539
- while proxy_attempts < max_proxy_attempts:
540
- try:
541
- # Get a new proxy
542
- new_proxy_url = get_auto_proxy()
543
- if new_proxy_url:
544
- new_proxies = get_proxy_dict(new_proxy_url)
545
- session.proxies.clear()
546
- session.proxies.update(new_proxies)
547
-
548
- # Try the request with new proxy
549
- return await original_request(method, url, **kwargs)
550
- else:
551
- break # No more proxies available
552
-
553
- except Exception:
554
- proxy_attempts += 1
555
- continue
556
-
557
- # All proxy attempts failed, try without proxy
558
- try:
559
- session.proxies.clear()
560
- return await original_request(method, url, **kwargs)
561
- except Exception:
562
- # Restore original proxy settings and re-raise the first error
563
- session.proxies.clear()
564
- session.proxies.update(original_proxies)
565
- raise first_error
566
- else:
567
- # No proxies were configured, just re-raise the original error
568
- raise first_error
569
-
570
- session.request = request_with_auto_retry
571
- session._auto_retry_patched = True
572
- except Exception:
573
- pass
574
-
575
-
576
- def _add_proxy_helpers(instance: Any, proxies: Dict[str, str]) -> None:
577
- """
578
- Add helper methods to the instance for creating proxied sessions.
579
-
580
- Args:
581
- instance: The class instance to add methods to
582
- proxies: Proxy dictionary to use in helper methods
583
- """
584
-
585
- def get_proxied_session():
586
- """Get a requests.Session with proxies configured"""
587
- session = requests.Session()
588
- session.proxies.update(proxies)
589
- return session
590
-
591
- def get_proxied_httpx_client(**kwargs):
592
- """Get an httpx.Client with proxies configured"""
593
- if httpx:
594
- return httpx.Client(proxies=proxies, **kwargs)
595
- else:
596
- raise ImportError("httpx is not installed")
597
-
598
- def get_proxied_curl_session(impersonate="chrome120", **kwargs):
599
- """Get a curl_cffi Session with proxies configured"""
600
- if CurlSession:
601
- return CurlSession(proxies=proxies, impersonate=impersonate, **kwargs)
602
- else:
603
- raise ImportError("curl_cffi is not installed")
604
-
605
- def get_proxied_curl_async_session(impersonate="chrome120", **kwargs):
606
- """Get a curl_cffi AsyncSession with proxies configured"""
607
- if CurlAsyncSession:
608
- return CurlAsyncSession(proxies=proxies, impersonate=impersonate, **kwargs)
609
- else:
610
- raise ImportError("curl_cffi is not installed")
611
-
612
- def get_auto_retry_session(max_proxy_attempts: int = 2):
613
- """Get a requests.Session with automatic proxy retry and fallback functionality"""
614
- return create_auto_retry_session(max_proxy_attempts)
615
-
616
- def make_auto_retry_request(method: str, url: str, max_proxy_attempts: int = 2, **kwargs):
617
- """Make a request with automatic proxy retry and fallback"""
618
- return make_request_with_auto_retry(
619
- method=method,
620
- url=url,
621
- max_proxy_attempts=max_proxy_attempts,
622
- **kwargs
623
- )
624
-
625
- def patch_session_with_auto_retry(session_obj):
626
- """Patch any session object with auto-retry functionality"""
627
- if isinstance(session_obj, requests.Session):
628
- _add_auto_retry_to_session(session_obj, instance)
629
- elif httpx and isinstance(session_obj, httpx.Client):
630
- _add_auto_retry_to_httpx_client(session_obj, instance)
631
- elif CurlSession and isinstance(session_obj, CurlSession):
632
- _add_auto_retry_to_curl_session(session_obj, instance)
633
- elif CurlAsyncSession and isinstance(session_obj, CurlAsyncSession):
634
- _add_auto_retry_to_curl_async_session(session_obj, instance)
635
- return session_obj
636
-
637
- # Add methods to instance
638
- instance.get_proxied_session = get_proxied_session
639
- instance.get_proxied_httpx_client = get_proxied_httpx_client
640
- instance.get_proxied_curl_session = get_proxied_curl_session
641
- instance.get_proxied_curl_async_session = get_proxied_curl_async_session
642
- instance.get_auto_retry_session = get_auto_retry_session
643
- instance.make_auto_retry_request = make_auto_retry_request
644
- instance.patch_session_with_auto_retry = patch_session_with_auto_retry
645
-
646
-
647
- def get_working_proxy(max_attempts: int = 5, timeout: int = 10) -> Optional[str]:
648
- """
649
- Get a working proxy by testing multiple proxies from the list.
650
-
651
- Args:
652
- max_attempts: Maximum number of proxies to test
653
- timeout: Timeout for each proxy test
654
-
655
- Returns:
656
- Optional[str]: A working proxy URL or None if none found
657
- """
658
- proxies = get_cached_proxies()
659
- if not proxies:
660
- return None
661
-
662
- # Shuffle to avoid always testing the same proxies first
663
- test_proxies = random.sample(proxies, min(max_attempts, len(proxies)))
664
-
665
- for proxy in test_proxies:
666
- if test_proxy(proxy, timeout):
667
- return proxy
668
-
669
- return None
670
-
671
-
672
- def refresh_proxy_cache() -> int:
673
- """
674
- Force refresh the proxy cache.
675
-
676
- Returns:
677
- int: Number of proxies loaded
678
- """
679
- global _proxy_cache
680
- _proxy_cache['last_updated'] = 0 # Force refresh
681
- proxies = get_cached_proxies()
682
- return len(proxies)
683
-
684
-
685
- def get_proxy_stats() -> Dict[str, Any]:
686
- """
687
- Get statistics about the proxy cache.
688
-
689
- Returns:
690
- Dict[str, Any]: Statistics including count, last update time, etc.
691
- """
692
- return {
693
- 'proxy_count': len(_proxy_cache['proxies']),
694
- 'last_updated': _proxy_cache['last_updated'],
695
- 'cache_duration': _proxy_cache['cache_duration'],
696
- 'cache_age_seconds': time.time() - _proxy_cache['last_updated'],
697
- 'source_url': PROXY_SOURCE_URL
698
- }
699
-
700
-
701
- def set_proxy_cache_duration(duration: int) -> None:
702
- """
703
- Set the proxy cache duration.
704
-
705
- Args:
706
- duration: Cache duration in seconds
707
- """
708
- global _proxy_cache
709
- _proxy_cache['cache_duration'] = duration
710
-
711
-
712
- def auto_retry_with_fallback(max_proxy_attempts: int = 2, timeout: int = 10):
713
- """
714
- Decorator that automatically retries requests with different proxies and falls back to no proxy.
715
-
716
- This decorator will:
717
- 1. Try the request with the current proxy
718
- 2. If it fails, try with up to max_proxy_attempts different proxies
719
- 3. If all proxies fail, retry without any proxy
720
-
721
- Args:
722
- max_proxy_attempts: Maximum number of proxy attempts before falling back to no proxy
723
- timeout: Timeout for each request attempt
724
-
725
- Returns:
726
- Decorator function
727
- """
728
- def decorator(func: Callable) -> Callable:
729
- @functools.wraps(func)
730
- def wrapper(*args, **kwargs):
731
- # Track the original instance and its proxy settings
732
- instance = args[0] if args else None
733
- original_proxies = getattr(instance, 'proxies', {}) if instance else {}
734
-
735
- # First attempt with current proxy configuration
736
- try:
737
- return func(*args, **kwargs)
738
- except Exception as e:
739
- first_error = e
740
-
741
- # If we have proxies configured, try different ones
742
- if original_proxies and instance:
743
- proxy_attempts = 0
744
-
745
- while proxy_attempts < max_proxy_attempts:
746
- try:
747
- # Get a new proxy
748
- new_proxy_url = get_auto_proxy()
749
- if new_proxy_url:
750
- new_proxies = get_proxy_dict(new_proxy_url)
751
- instance.proxies = new_proxies
752
-
753
- # Update any existing sessions with new proxy
754
- _patch_instance_sessions(instance, new_proxies)
755
-
756
- # Try the request with new proxy
757
- return func(*args, **kwargs)
758
- else:
759
- break # No more proxies available
760
-
761
- except Exception:
762
- proxy_attempts += 1
763
- continue
764
-
765
- # All proxy attempts failed, try without proxy
766
- try:
767
- instance.proxies = {}
768
- _patch_instance_sessions(instance, {})
769
- return func(*args, **kwargs)
770
- except Exception:
771
- # Restore original proxy settings and re-raise the first error
772
- instance.proxies = original_proxies
773
- _patch_instance_sessions(instance, original_proxies)
774
- raise first_error
775
- else:
776
- # No proxies were configured, just re-raise the original error
777
- raise first_error
778
-
779
- return wrapper
780
- return decorator
781
-
782
-
783
- def make_request_with_auto_retry(
784
- method: str,
785
- url: str,
786
- session: Optional[Union[requests.Session, Any]] = None,
787
- max_proxy_attempts: int = 2,
788
- timeout: int = 10,
789
- **kwargs
790
- ) -> requests.Response:
791
- """
792
- Make an HTTP request with automatic proxy retry and fallback.
793
-
794
- This function will:
795
- 1. Try the request with the current session configuration
796
- 2. If it fails and proxies are configured, try with different proxies
797
- 3. If all proxies fail, retry without any proxy
798
-
799
- Args:
800
- method: HTTP method (GET, POST, etc.)
801
- url: Request URL
802
- session: Optional session object to use
803
- max_proxy_attempts: Maximum number of proxy attempts before falling back
804
- timeout: Request timeout
805
- **kwargs: Additional arguments to pass to the request
806
-
807
- Returns:
808
- requests.Response: The successful response
809
-
810
- Raises:
811
- Exception: If all attempts fail
812
- """
813
- if session is None:
814
- session = requests.Session()
815
-
816
- original_proxies = getattr(session, 'proxies', {}).copy()
817
- first_error = None
818
-
819
- # First attempt with current configuration
820
- try:
821
- return session.request(method, url, timeout=timeout, **kwargs)
822
- except Exception as e:
823
- first_error = e
824
-
825
- # If we have proxies configured, try different ones
826
- if original_proxies:
827
- proxy_attempts = 0
828
-
829
- while proxy_attempts < max_proxy_attempts:
830
- try:
831
- # Get a new proxy
832
- new_proxy_url = get_auto_proxy()
833
- if new_proxy_url:
834
- new_proxies = get_proxy_dict(new_proxy_url)
835
- session.proxies.clear()
836
- session.proxies.update(new_proxies)
837
-
838
- # Try the request with new proxy
839
- return session.request(method, url, timeout=timeout, **kwargs)
840
- else:
841
- break # No more proxies available
842
-
843
- except Exception:
844
- proxy_attempts += 1
845
- continue
846
-
847
- # All proxy attempts failed, try without proxy
848
- try:
849
- session.proxies.clear()
850
- return session.request(method, url, timeout=timeout, **kwargs)
851
- except Exception:
852
- # Restore original proxy settings and re-raise the first error
853
- session.proxies.clear()
854
- session.proxies.update(original_proxies)
855
- raise first_error
856
- else:
857
- # No proxies were configured, just re-raise the original error
858
- raise first_error
859
-
860
-
861
- def create_auto_retry_session(max_proxy_attempts: int = 2) -> requests.Session:
862
- """
863
- Create a requests.Session with automatic proxy retry functionality.
864
-
865
- Args:
866
- max_proxy_attempts: Maximum number of proxy attempts before falling back
867
-
868
- Returns:
869
- requests.Session: Session with auto-retry functionality
870
- """
871
- session = requests.Session()
872
-
873
- # Get initial proxy configuration
874
- proxy_url = get_auto_proxy()
875
- if proxy_url:
876
- proxies = get_proxy_dict(proxy_url)
877
- session.proxies.update(proxies)
878
-
879
- # Store the max_proxy_attempts for use in retry logic
880
- session._max_proxy_attempts = max_proxy_attempts
881
-
882
- # Override the request method to add auto-retry functionality
883
- original_request = session.request
884
-
885
- def request_with_retry(method, url, **kwargs):
886
- return make_request_with_auto_retry(
887
- method=method,
888
- url=url,
889
- session=session,
890
- max_proxy_attempts=max_proxy_attempts,
891
- **kwargs
892
- )
893
-
894
- session.request = request_with_retry
895
- return session
896
-
897
-
898
- def enable_auto_retry_for_provider(provider_instance, max_proxy_attempts: int = 2):
899
- """
900
- Enable auto-retry functionality for an existing provider instance.
901
-
902
- This function can be used to add auto-retry functionality to providers
903
- that were created without it, or to update the max_proxy_attempts setting.
904
-
905
- Args:
906
- provider_instance: The provider instance to enable auto-retry for
907
- max_proxy_attempts: Maximum number of proxy attempts before falling back
908
- """
909
- # Set the max proxy attempts
910
- provider_instance._max_proxy_attempts = max_proxy_attempts
911
-
912
- # Get current proxies or empty dict
913
- current_proxies = getattr(provider_instance, 'proxies', {})
914
-
915
- # Patch all existing sessions
916
- _patch_instance_sessions(provider_instance, current_proxies)
917
-
918
- # Add helper methods if they don't exist
919
- if not hasattr(provider_instance, 'get_auto_retry_session'):
920
- _add_proxy_helpers(provider_instance, current_proxies)
921
-
922
-
923
- def disable_auto_retry_for_provider(provider_instance):
924
- """
925
- Disable auto-retry functionality for a provider instance.
926
-
927
- This will restore the original request methods for all sessions.
928
- Note: This is a best-effort approach and may not work for all session types.
929
-
930
- Args:
931
- provider_instance: The provider instance to disable auto-retry for
932
- """
933
- for attr_name in dir(provider_instance):
934
- if attr_name.startswith('_'):
935
- continue
936
-
937
- try:
938
- attr_obj = getattr(provider_instance, attr_name)
939
-
940
- # Remove auto-retry from requests.Session objects
941
- if isinstance(attr_obj, requests.Session) and hasattr(attr_obj, '_auto_retry_patched'):
942
- # This is a simplified approach - in practice, restoring original methods
943
- # would require storing references to them, which we don't do here
944
- delattr(attr_obj, '_auto_retry_patched')
945
-
946
- # Similar for other session types
947
- elif httpx and isinstance(attr_obj, httpx.Client) and hasattr(attr_obj, '_auto_retry_patched'):
948
- delattr(attr_obj, '_auto_retry_patched')
949
-
950
- elif CurlSession and isinstance(attr_obj, CurlSession) and hasattr(attr_obj, '_auto_retry_patched'):
951
- delattr(attr_obj, '_auto_retry_patched')
952
-
953
- elif CurlAsyncSession and isinstance(attr_obj, CurlAsyncSession) and hasattr(attr_obj, '_auto_retry_patched'):
954
- delattr(attr_obj, '_auto_retry_patched')
955
-
956
- except Exception:
957
- continue
958
-
959
-
960
- def proxy():
961
- """
962
- Return a working proxy dict or None. One-liner for easy use.
963
- Example:
964
- proxies = autoproxy.proxy()
965
- requests.get(url, proxies=proxies)
966
- """
967
- proxy_url = get_working_proxy()
968
- return get_proxy_dict(proxy_url) if proxy_url else None
969
-
970
-
971
- def patch(obj, proxy_url=None):
972
- """
973
- Patch a function, class, or object to use proxies automatically.
974
- - For functions: inject proxies kwarg if not present.
975
- - For requests.Session: set .proxies.
976
- - For classes: patch all methods that look like HTTP calls.
977
- """
978
- if isinstance(obj, requests.Session):
979
- obj.proxies.update(get_proxy_dict(proxy_url))
980
- return obj
981
- if httpx and isinstance(obj, httpx.Client):
982
- obj._proxies = get_proxy_dict(proxy_url)
983
- return obj
984
- if isinstance(obj, types.FunctionType):
985
- def wrapper(*args, **kwargs):
986
- if 'proxies' not in kwargs:
987
- kwargs['proxies'] = get_proxy_dict(proxy_url)
988
- return obj(*args, **kwargs)
989
- return wrapper
990
- if isinstance(obj, type): # class
991
- for attr in dir(obj):
992
- if attr.startswith('get') or attr.startswith('post'):
993
- method = getattr(obj, attr)
994
- if callable(method):
995
- setattr(obj, attr, patch(method, proxy_url))
996
- return obj
997
- # fallback: return as is
998
- return obj
999
-
1000
-
1001
- @contextmanager
1002
- def use_proxy(proxy_url=None):
1003
- """
1004
- Context manager to temporarily patch requests and httpx to use a proxy globally.
1005
- Example:
1006
- with autoproxy.use_proxy():
1007
- requests.get(url) # uses proxy automatically
1008
- """
1009
- orig_request = requests.Session.request
1010
- def request_with_proxy(self, method, url, **kwargs):
1011
- if 'proxies' not in kwargs:
1012
- kwargs['proxies'] = get_proxy_dict(proxy_url)
1013
- return orig_request(self, method, url, **kwargs)
1014
- requests.Session.request = request_with_proxy
1015
- # Optionally patch httpx if available
1016
- orig_httpx = None
1017
- if httpx:
1018
- orig_httpx = httpx.Client.request
1019
- def httpx_request_with_proxy(self, method, url, **kwargs):
1020
- if 'proxies' not in kwargs:
1021
- kwargs['proxies'] = get_proxy_dict(proxy_url)
1022
- return orig_httpx(self, method, url, **kwargs)
1023
- httpx.Client.request = httpx_request_with_proxy
1024
- try:
1025
- yield
1026
- finally:
1027
- requests.Session.request = orig_request
1028
- if httpx and orig_httpx:
1029
- httpx.Client.request = orig_httpx
1030
-
1031
-
1032
- def proxyify(func):
1033
- """
1034
- Decorator to auto-inject proxies into any function.
1035
- Example:
1036
- @autoproxy.proxyify
1037
- def my_request(...): ...
1038
- """
1039
- def wrapper(*args, **kwargs):
1040
- if 'proxies' not in kwargs:
1041
- kwargs['proxies'] = proxy()
1042
- return func(*args, **kwargs)
1043
- return wrapper
1044
-
1045
-
1046
- def list_proxies():
1047
- """
1048
- List all available proxies (Webshare, remote, NordVPN).
1049
- """
1050
- return get_cached_proxies()
1051
-
1052
-
1053
- def test_all_proxies(timeout=5):
1054
- """
1055
- Test all proxies and return a dict of proxy_url: True/False.
1056
- """
1057
- results = {}
1058
- for proxy in get_cached_proxies():
1059
- results[proxy] = test_proxy(proxy, timeout=timeout)
1060
- return results
1061
-
1062
-
1063
- def current_proxy():
1064
- """
1065
- Return a random proxy that would be used now (Webshare preferred).
1066
- """
1067
- return get_auto_proxy()