chore(lint): auto-fix 253 of 283 ruff issues (F401, I001, E402, RUF100, UP037, SIM105)
Mass ruff auto-fix:
- ruff check --fix: 109 issues fixed (F401 unused imports,
I001 unsorted imports, UP037 quoted annotations, SIM105
suppressible exception, RUF100 unused-noqa)
- ruff check --fix --unsafe-fixes: 22 additional issues
- ruff format: 70 files reformatted
- Manual pass: fix 16 misplaced import httpx lines
- Manual pass: fix remaining E402 (import-after-docstring)
Result: 283 errors -> 30 errors.
The remaining 30 are real issues that need manual review:
5 F401 unused-import (likely auto-generated stubs)
5 F821 undefined-name (real bugs in code that references
redis/pydantic/LLMRegistry without imports)
3 BLE001 (the compliance LLM fallback is intentional; the
other two are real)
3 RUF012 mutable-class-default
3 SIM105, 3 SIM117, 2 E722, 2 E741
1 B007, 1 B025, 1 E402, 1 RUF200 (pyproject.toml issue)
Tests: 436/437 pass (1 pre-existing SSE sandbox failure).
format check + import sort: now clean.
make ci: still gated on the 30 remaining real issues.
Follow-up: triage the 30 issues file-by-file.
This commit is contained in:
parent
e60a62a07a
commit
a7c30b12cd
85 changed files with 2374 additions and 1071 deletions
|
|
@ -21,15 +21,25 @@ logger = logging.getLogger(__name__)
|
|||
class CaptchaSolver:
|
||||
"""Unified CAPTCHA solver with 6+ providers and automatic fallback chain."""
|
||||
|
||||
PROVIDER_PRIORITY: ClassVar[list[str]] = ["capsolver", "2captcha", "anti_captcha", "capmonster", "deathbycaptcha", "nextcaptcha"]
|
||||
PROVIDER_PRIORITY: ClassVar[list[str]] = [
|
||||
"capsolver",
|
||||
"2captcha",
|
||||
"anti_captcha",
|
||||
"capmonster",
|
||||
"deathbycaptcha",
|
||||
"nextcaptcha",
|
||||
]
|
||||
|
||||
def __init__(self, api_keys: dict[str, str] | None = None):
|
||||
self.api_keys = api_keys or {}
|
||||
self.provider_stats: dict[str, dict[str, Any]] = {
|
||||
p: {"success": 0, "failed": 0, "avg_time": 0, "last_error": ""} for p in self.PROVIDER_PRIORITY
|
||||
p: {"success": 0, "failed": 0, "avg_time": 0, "last_error": ""}
|
||||
for p in self.PROVIDER_PRIORITY
|
||||
}
|
||||
|
||||
async def solve_recaptcha_v2(self, site_key: str, page_url: str, provider: str = "") -> dict[str, Any]:
|
||||
async def solve_recaptcha_v2(
|
||||
self, site_key: str, page_url: str, provider: str = ""
|
||||
) -> dict[str, Any]:
|
||||
"""Solve reCAPTCHA v2."""
|
||||
providers = [provider] if provider else self.PROVIDER_PRIORITY
|
||||
for prov in providers:
|
||||
|
|
@ -41,24 +51,41 @@ class CaptchaSolver:
|
|||
result = await self._solve_with(prov, "ReCaptchaV2Task", site_key, page_url, key)
|
||||
elapsed = time.time() - start
|
||||
self._record(prov, True, elapsed)
|
||||
return {"success": True, "provider": prov, "token": result, "elapsed": round(elapsed, 2)}
|
||||
return {
|
||||
"success": True,
|
||||
"provider": prov,
|
||||
"token": result,
|
||||
"elapsed": round(elapsed, 2),
|
||||
}
|
||||
except Exception as e: # noqa: BLE001
|
||||
self._record(prov, False, 0, str(e))
|
||||
logger.warning("captcha_provider_failed", extra={"provider": prov, "error": str(e)[:80]})
|
||||
logger.warning(
|
||||
"captcha_provider_failed", extra={"provider": prov, "error": str(e)[:80]}
|
||||
)
|
||||
return {"success": False, "error": "All CAPTCHA providers failed"}
|
||||
|
||||
async def solve_recaptcha_v3(self, site_key: str, page_url: str, action: str = "verify", min_score: float = 0.3) -> dict[str, Any]:
|
||||
async def solve_recaptcha_v3(
|
||||
self, site_key: str, page_url: str, action: str = "verify", min_score: float = 0.3
|
||||
) -> dict[str, Any]:
|
||||
"""Solve reCAPTCHA v3 (invisible, returns score)."""
|
||||
for prov in self.PROVIDER_PRIORITY:
|
||||
key = self.api_keys.get(prov) or self.api_keys.get(prov + "_api_key", "")
|
||||
if not key:
|
||||
continue
|
||||
try:
|
||||
result = await self._solve_with(prov, "ReCaptchaV3Task", site_key, page_url, key,
|
||||
extra={"action": action, "minScore": min_score})
|
||||
result = await self._solve_with(
|
||||
prov,
|
||||
"ReCaptchaV3Task",
|
||||
site_key,
|
||||
page_url,
|
||||
key,
|
||||
extra={"action": action, "minScore": min_score},
|
||||
)
|
||||
return {"success": True, "provider": prov, "token": result}
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("recaptcha_v3_failed", extra={"provider": prov, "error": str(e)[:80]})
|
||||
logger.warning(
|
||||
"recaptcha_v3_failed", extra={"provider": prov, "error": str(e)[:80]}
|
||||
)
|
||||
return {"success": False, "error": "All reCAPTCHA v3 providers failed"}
|
||||
|
||||
async def solve_turnstile(self, site_key: str, page_url: str) -> dict[str, Any]:
|
||||
|
|
@ -97,10 +124,20 @@ class CaptchaSolver:
|
|||
result = await self._solve_image_with(prov, image_base64, key, case_sensitive)
|
||||
return {"success": True, "provider": prov, "text": result}
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("image_captcha_failed", extra={"provider": prov, "error": str(e)[:80]})
|
||||
logger.warning(
|
||||
"image_captcha_failed", extra={"provider": prov, "error": str(e)[:80]}
|
||||
)
|
||||
return {"success": False, "error": "All image CAPTCHA providers failed"}
|
||||
|
||||
async def _solve_with(self, provider: str, task_type: str, site_key: str, page_url: str, api_key: str, extra: dict | None = None) -> str:
|
||||
async def _solve_with(
|
||||
self,
|
||||
provider: str,
|
||||
task_type: str,
|
||||
site_key: str,
|
||||
page_url: str,
|
||||
api_key: str,
|
||||
extra: dict | None = None,
|
||||
) -> str:
|
||||
if provider == "capsolver":
|
||||
return await self._capsolver(task_type, site_key, page_url, api_key, extra)
|
||||
elif provider == "2captcha":
|
||||
|
|
@ -115,22 +152,30 @@ class CaptchaSolver:
|
|||
return await self._nextcaptcha(task_type, site_key, page_url, api_key, extra)
|
||||
raise ValueError(f"Unknown provider: {provider}")
|
||||
|
||||
async def _capsolver(self, task_type: str, site_key: str, page_url: str, api_key: str, extra: dict | None = None) -> str:
|
||||
async def _capsolver(
|
||||
self, task_type: str, site_key: str, page_url: str, api_key: str, extra: dict | None = None
|
||||
) -> str:
|
||||
from client import get_client
|
||||
|
||||
client = await get_client()
|
||||
task: dict[str, Any] = {"type": task_type, "websiteKey": site_key, "websiteURL": page_url}
|
||||
if extra:
|
||||
task.update(extra)
|
||||
resp = await client.post("https://api.capsolver.com/createTask",
|
||||
json={"clientKey": api_key, "task": task}, timeout=30)
|
||||
resp = await client.post(
|
||||
"https://api.capsolver.com/createTask",
|
||||
json={"clientKey": api_key, "task": task},
|
||||
timeout=30,
|
||||
)
|
||||
data = resp.json()
|
||||
task_id = data.get("taskId")
|
||||
if not task_id:
|
||||
raise Exception(data.get("errorDescription", "Capsolver error"))
|
||||
for _ in range(60):
|
||||
await asyncio.sleep(2)
|
||||
r = await client.post("https://api.capsolver.com/getTaskResult",
|
||||
json={"clientKey": api_key, "taskId": task_id})
|
||||
r = await client.post(
|
||||
"https://api.capsolver.com/getTaskResult",
|
||||
json={"clientKey": api_key, "taskId": task_id},
|
||||
)
|
||||
rd = r.json()
|
||||
if rd.get("status") == "ready":
|
||||
return rd["solution"].get("gRecaptchaResponse") or rd["solution"].get("token", "")
|
||||
|
|
@ -138,13 +183,25 @@ class CaptchaSolver:
|
|||
raise Exception("Capsolver failed")
|
||||
raise Exception("Capsolver timed out")
|
||||
|
||||
async def _two_captcha(self, task_type: str, site_key: str, page_url: str, api_key: str, extra: dict | None = None) -> str:
|
||||
async def _two_captcha(
|
||||
self, task_type: str, site_key: str, page_url: str, api_key: str, extra: dict | None = None
|
||||
) -> str:
|
||||
from client import get_client
|
||||
|
||||
client = await get_client()
|
||||
method = {"ReCaptchaV2Task": "userrecaptcha", "ReCaptchaV3Task": "recaptcha_v3",
|
||||
"HCaptchaTask": "hcaptcha", "TurnstileTask": "turnstile"}.get(task_type, "userrecaptcha")
|
||||
params: dict[str, Any] = {"key": api_key, "method": method, "googlekey": site_key, "pageurl": page_url,
|
||||
"json": 1}
|
||||
method = {
|
||||
"ReCaptchaV2Task": "userrecaptcha",
|
||||
"ReCaptchaV3Task": "recaptcha_v3",
|
||||
"HCaptchaTask": "hcaptcha",
|
||||
"TurnstileTask": "turnstile",
|
||||
}.get(task_type, "userrecaptcha")
|
||||
params: dict[str, Any] = {
|
||||
"key": api_key,
|
||||
"method": method,
|
||||
"googlekey": site_key,
|
||||
"pageurl": page_url,
|
||||
"json": 1,
|
||||
}
|
||||
if extra:
|
||||
params.update(extra)
|
||||
resp = await client.post("https://2captcha.com/in.php", data=params, timeout=30)
|
||||
|
|
@ -154,7 +211,9 @@ class CaptchaSolver:
|
|||
request_id = data["request"]
|
||||
for _ in range(60):
|
||||
await asyncio.sleep(3)
|
||||
r = await client.get(f"https://2captcha.com/res.php?key={api_key}&action=get&id={request_id}&json=1")
|
||||
r = await client.get(
|
||||
f"https://2captcha.com/res.php?key={api_key}&action=get&id={request_id}&json=1"
|
||||
)
|
||||
rd = r.json()
|
||||
if rd.get("status") == 1:
|
||||
return rd["request"]
|
||||
|
|
@ -162,24 +221,40 @@ class CaptchaSolver:
|
|||
raise Exception(f"2captcha: {rd['request']}")
|
||||
raise Exception("2captcha timed out")
|
||||
|
||||
async def _anti_captcha(self, task_type: str, site_key: str, page_url: str, api_key: str, extra: dict | None = None) -> str:
|
||||
async def _anti_captcha(
|
||||
self, task_type: str, site_key: str, page_url: str, api_key: str, extra: dict | None = None
|
||||
) -> str:
|
||||
from client import get_client
|
||||
|
||||
client = await get_client()
|
||||
type_map = {"ReCaptchaV2Task": "NoCaptchaTaskProxyless", "ReCaptchaV3Task": "RecaptchaV3TaskProxyless",
|
||||
"HCaptchaTask": "HCaptchaTaskProxyless", "TurnstileTask": "TurnstileTaskProxyless"}
|
||||
task = {"type": type_map.get(task_type, "NoCaptchaTaskProxyless"), "websiteURL": page_url, "websiteKey": site_key}
|
||||
type_map = {
|
||||
"ReCaptchaV2Task": "NoCaptchaTaskProxyless",
|
||||
"ReCaptchaV3Task": "RecaptchaV3TaskProxyless",
|
||||
"HCaptchaTask": "HCaptchaTaskProxyless",
|
||||
"TurnstileTask": "TurnstileTaskProxyless",
|
||||
}
|
||||
task = {
|
||||
"type": type_map.get(task_type, "NoCaptchaTaskProxyless"),
|
||||
"websiteURL": page_url,
|
||||
"websiteKey": site_key,
|
||||
}
|
||||
if extra:
|
||||
task.update(extra)
|
||||
resp = await client.post("https://api.anti-captcha.com/createTask",
|
||||
json={"clientKey": api_key, "task": task}, timeout=30)
|
||||
resp = await client.post(
|
||||
"https://api.anti-captcha.com/createTask",
|
||||
json={"clientKey": api_key, "task": task},
|
||||
timeout=30,
|
||||
)
|
||||
data = resp.json()
|
||||
if data.get("errorId") != 0:
|
||||
raise Exception(data.get("errorDescription", "Anti-Captcha error"))
|
||||
task_id = data["taskId"]
|
||||
for _ in range(60):
|
||||
await asyncio.sleep(2)
|
||||
r = await client.post("https://api.anti-captcha.com/getTaskResult",
|
||||
json={"clientKey": api_key, "taskId": task_id})
|
||||
r = await client.post(
|
||||
"https://api.anti-captcha.com/getTaskResult",
|
||||
json={"clientKey": api_key, "taskId": task_id},
|
||||
)
|
||||
rd = r.json()
|
||||
if rd.get("status") == "ready":
|
||||
return rd["solution"].get("gRecaptchaResponse", "")
|
||||
|
|
@ -187,17 +262,25 @@ class CaptchaSolver:
|
|||
raise Exception(rd.get("errorDescription", ""))
|
||||
raise Exception("Anti-Captcha timed out")
|
||||
|
||||
async def _capmonster(self, task_type: str, site_key: str, page_url: str, api_key: str, extra: dict | None = None) -> str:
|
||||
async def _capmonster(
|
||||
self, task_type: str, site_key: str, page_url: str, api_key: str, extra: dict | None = None
|
||||
) -> str:
|
||||
"""CapMonster Cloud — self-hosted option. Can run locally with 0 per-solve fees."""
|
||||
return await self._anti_captcha(task_type, site_key, page_url, api_key, extra) # Same API as Anti-Captcha
|
||||
return await self._anti_captcha(
|
||||
task_type, site_key, page_url, api_key, extra
|
||||
) # Same API as Anti-Captcha
|
||||
|
||||
async def _deathbycaptcha(self, task_type: str, site_key: str, page_url: str, api_key: str, extra: dict | None = None) -> str:
|
||||
async def _deathbycaptcha(
|
||||
self, task_type: str, site_key: str, page_url: str, api_key: str, extra: dict | None = None
|
||||
) -> str:
|
||||
"""Solve via DeathByCaptcha API."""
|
||||
from client import get_client
|
||||
|
||||
client = await get_client()
|
||||
user, pw = api_key.split(":", 1) if ":" in api_key else (api_key, "")
|
||||
method = {"ReCaptchaV2Task": "userrecaptcha", "HCaptchaTask": "hcaptcha"}.get(task_type, "userrecaptcha")
|
||||
method = {"ReCaptchaV2Task": "userrecaptcha", "HCaptchaTask": "hcaptcha"}.get(
|
||||
task_type, "userrecaptcha"
|
||||
)
|
||||
payload = {
|
||||
"username": user,
|
||||
"password": pw,
|
||||
|
|
@ -217,7 +300,9 @@ class CaptchaSolver:
|
|||
return rd["text"]
|
||||
raise Exception("DBC timed out")
|
||||
|
||||
async def _nextcaptcha(self, task_type: str, site_key: str, page_url: str, api_key: str, extra: dict | None = None) -> str:
|
||||
async def _nextcaptcha(
|
||||
self, task_type: str, site_key: str, page_url: str, api_key: str, extra: dict | None = None
|
||||
) -> str:
|
||||
"""Solve via NextCaptcha API."""
|
||||
from client import get_client
|
||||
|
||||
|
|
@ -256,7 +341,9 @@ class CaptchaSolver:
|
|||
raise Exception(rd.get("errorDescription", ""))
|
||||
raise Exception("NextCaptcha timed out")
|
||||
|
||||
async def _solve_image_with(self, provider: str, image_base64: str, api_key: str, case_sensitive: bool) -> str:
|
||||
async def _solve_image_with(
|
||||
self, provider: str, image_base64: str, api_key: str, case_sensitive: bool
|
||||
) -> str:
|
||||
if provider == "capsolver":
|
||||
return await self._capsolver_image(image_base64, api_key, case_sensitive)
|
||||
elif provider == "2captcha":
|
||||
|
|
@ -265,38 +352,60 @@ class CaptchaSolver:
|
|||
|
||||
async def _capsolver_image(self, image_base64: str, api_key: str, case_sensitive: bool) -> str:
|
||||
from client import get_client
|
||||
|
||||
client = await get_client()
|
||||
body = image_base64 if not image_base64.startswith("data:") else image_base64.split(",", 1)[1]
|
||||
resp = await client.post("https://api.capsolver.com/createTask",
|
||||
json={"clientKey": api_key, "task": {"type": "ImageToTextTask", "body": body[:100000]}},
|
||||
timeout=30)
|
||||
body = (
|
||||
image_base64 if not image_base64.startswith("data:") else image_base64.split(",", 1)[1]
|
||||
)
|
||||
resp = await client.post(
|
||||
"https://api.capsolver.com/createTask",
|
||||
json={"clientKey": api_key, "task": {"type": "ImageToTextTask", "body": body[:100000]}},
|
||||
timeout=30,
|
||||
)
|
||||
data = resp.json()
|
||||
task_id = data.get("taskId")
|
||||
if not task_id:
|
||||
raise Exception(data.get("errorDescription", ""))
|
||||
for _ in range(30):
|
||||
await asyncio.sleep(2)
|
||||
r = await client.post("https://api.capsolver.com/getTaskResult",
|
||||
json={"clientKey": api_key, "taskId": task_id})
|
||||
r = await client.post(
|
||||
"https://api.capsolver.com/getTaskResult",
|
||||
json={"clientKey": api_key, "taskId": task_id},
|
||||
)
|
||||
rd = r.json()
|
||||
if rd.get("status") == "ready":
|
||||
return rd["solution"].get("text", "")
|
||||
raise Exception("Image solve timed out")
|
||||
|
||||
async def _two_captcha_image(self, image_base64: str, api_key: str, case_sensitive: bool) -> str:
|
||||
async def _two_captcha_image(
|
||||
self, image_base64: str, api_key: str, case_sensitive: bool
|
||||
) -> str:
|
||||
from client import get_client
|
||||
|
||||
client = await get_client()
|
||||
body = image_base64 if not image_base64.startswith("data:") else image_base64.split(",", 1)[1]
|
||||
resp = await client.post("https://2captcha.com/in.php",
|
||||
data={"key": api_key, "method": "base64", "body": body, "json": 1,
|
||||
"regsense": 1 if case_sensitive else 0}, timeout=30)
|
||||
body = (
|
||||
image_base64 if not image_base64.startswith("data:") else image_base64.split(",", 1)[1]
|
||||
)
|
||||
resp = await client.post(
|
||||
"https://2captcha.com/in.php",
|
||||
data={
|
||||
"key": api_key,
|
||||
"method": "base64",
|
||||
"body": body,
|
||||
"json": 1,
|
||||
"regsense": 1 if case_sensitive else 0,
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
data = resp.json()
|
||||
if data.get("status") != 1:
|
||||
raise Exception(data.get("request", ""))
|
||||
request_id = data["request"]
|
||||
for _ in range(30):
|
||||
await asyncio.sleep(3)
|
||||
r = await client.get(f"https://2captcha.com/res.php?key={api_key}&action=get&id={request_id}&json=1")
|
||||
r = await client.get(
|
||||
f"https://2captcha.com/res.php?key={api_key}&action=get&id={request_id}&json=1"
|
||||
)
|
||||
rd = r.json()
|
||||
if rd.get("status") == 1:
|
||||
return rd["request"]
|
||||
|
|
@ -306,10 +415,15 @@ class CaptchaSolver:
|
|||
stats = self.provider_stats[provider]
|
||||
if success:
|
||||
stats["success"] += 1
|
||||
stats["avg_time"] = (stats["avg_time"] * (stats["success"] + stats["failed"] - 1) + elapsed) / (stats["success"] + stats["failed"])
|
||||
stats["avg_time"] = (
|
||||
stats["avg_time"] * (stats["success"] + stats["failed"] - 1) + elapsed
|
||||
) / (stats["success"] + stats["failed"])
|
||||
else:
|
||||
stats["failed"] += 1
|
||||
stats["last_error"] = error[:100]
|
||||
|
||||
def get_stats(self) -> dict[str, Any]:
|
||||
return {"providers": self.provider_stats, "healthy_providers": sum(1 for p in self.provider_stats.values() if p["failed"] < 3)}
|
||||
return {
|
||||
"providers": self.provider_stats,
|
||||
"healthy_providers": sum(1 for p in self.provider_stats.values() if p["failed"] < 3),
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue