fix(lint): resolve remaining ruff errors and unblock MCP SSE test #1
17 changed files with 60 additions and 87 deletions
4
api.py
4
api.py
|
|
@ -26,6 +26,8 @@ from typing import Any, cast
|
||||||
from urllib.parse import urljoin, urlparse
|
from urllib.parse import urljoin, urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
import pydantic
|
||||||
|
import redis
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import Body, FastAPI, Request, Response, WebSocket, WebSocketDisconnect
|
from fastapi import Body, FastAPI, Request, Response, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
@ -53,6 +55,7 @@ from parser import DocumentParser
|
||||||
from pipeline import HOOK_POINTS, get_pipeline, run_pipeline
|
from pipeline import HOOK_POINTS, get_pipeline, run_pipeline
|
||||||
from pryextras import BatchProcessor, TransformEngine, recorder, streams
|
from pryextras import BatchProcessor, TransformEngine, recorder, streams
|
||||||
from ratelimit import RateLimiter
|
from ratelimit import RateLimiter
|
||||||
|
from routers.auth import router as auth_router
|
||||||
from scraper import BlockDetector, PryScraper
|
from scraper import BlockDetector, PryScraper
|
||||||
from settings import settings
|
from settings import settings
|
||||||
from x402_middleware import X402Middleware
|
from x402_middleware import X402Middleware
|
||||||
|
|
@ -3135,7 +3138,6 @@ async def list_schemas() -> dict[str, Any]:
|
||||||
|
|
||||||
# ── Auth ──
|
# ── Auth ──
|
||||||
# (split into routers/auth.py on the api-router-split refactor)
|
# (split into routers/auth.py on the api-router-split refactor)
|
||||||
from routers.auth import router as auth_router
|
|
||||||
|
|
||||||
app.include_router(auth_router)
|
app.include_router(auth_router)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,10 +73,8 @@ class PryAutomator:
|
||||||
"""Periodically clean up stale sessions to prevent memory leaks."""
|
"""Periodically clean up stale sessions to prevent memory leaks."""
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(SESSION_CLEANUP_INTERVAL)
|
await asyncio.sleep(SESSION_CLEANUP_INTERVAL)
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
await self._cleanup_stale_sessions()
|
await self._cleanup_stale_sessions()
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def _get_or_create_session(
|
async def _get_or_create_session(
|
||||||
self, session_id: str | None = None, headless: bool = True, viewport: dict | None = None
|
self, session_id: str | None = None, headless: bool = True, viewport: dict | None = None
|
||||||
|
|
@ -281,10 +279,8 @@ class PryAutomator:
|
||||||
) -> str:
|
) -> str:
|
||||||
session = await self._get_or_create_session()
|
session = await self._get_or_create_session()
|
||||||
if cookies:
|
if cookies:
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
await session.context.add_cookies(cookies)
|
await session.context.add_cookies(cookies)
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
pass
|
|
||||||
await session.page.goto(url, wait_until="networkidle")
|
await session.page.goto(url, wait_until="networkidle")
|
||||||
return session.id
|
return session.id
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,17 @@ for Playwright that focuses on stealth."""
|
||||||
# Licensed under Business Source License 1.1 — see LICENSE-BSL-STEALTH.
|
# Licensed under Business Source License 1.1 — see LICENSE-BSL-STEALTH.
|
||||||
# Change Date: 2029-01-01 (converts to MIT).
|
# Change Date: 2029-01-01 (converts to MIT).
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Check for camoufox
|
# Check for camoufox
|
||||||
try:
|
try:
|
||||||
from camoufox import AsyncCamoufox
|
from camoufox import AsyncCamoufox
|
||||||
from camoufox.utils import DefaultAddons
|
|
||||||
|
|
||||||
_has_camoufox = True
|
_has_camoufox = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
@ -30,7 +30,7 @@ except ImportError:
|
||||||
class CamoufoxBrowser:
|
class CamoufoxBrowser:
|
||||||
"""Anti-detection Firefox browser using Camoufox."""
|
"""Anti-detection Firefox browser using Camoufox."""
|
||||||
|
|
||||||
DEFAULT_CONFIGS = {
|
DEFAULT_CONFIGS: ClassVar[dict[str, dict[str, Any]]] = {
|
||||||
"chrome_windows": {
|
"chrome_windows": {
|
||||||
"headless": True,
|
"headless": True,
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
|
|
@ -108,10 +108,8 @@ class CamoufoxBrowser:
|
||||||
await page.context.add_cookies([cookie])
|
await page.context.add_cookies([cookie])
|
||||||
await page.goto(url, wait_until="domcontentloaded")
|
await page.goto(url, wait_until="domcontentloaded")
|
||||||
if wait_selector:
|
if wait_selector:
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
await page.wait_for_selector(wait_selector, timeout=wait_time)
|
await page.wait_for_selector(wait_selector, timeout=wait_time)
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
pass
|
|
||||||
else:
|
else:
|
||||||
await page.wait_for_timeout(wait_time)
|
await page.wait_for_timeout(wait_time)
|
||||||
content = await page.content()
|
content = await page.content()
|
||||||
|
|
|
||||||
5
db.py
5
db.py
|
|
@ -84,6 +84,7 @@ try:
|
||||||
event,
|
event,
|
||||||
)
|
)
|
||||||
from sqlalchemy.engine import Engine
|
from sqlalchemy.engine import Engine
|
||||||
|
from sqlalchemy.exc import OperationalError, ProgrammingError, SQLAlchemyError
|
||||||
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
|
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
|
||||||
|
|
||||||
_HAS_SA = True
|
_HAS_SA = True
|
||||||
|
|
@ -691,7 +692,7 @@ def db_health() -> dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
result = conn.exec_driver_sql("SELECT sqlite_version()").fetchone()
|
result = conn.exec_driver_sql("SELECT sqlite_version()").fetchone()
|
||||||
sqlite_version = result[0] if result else None
|
sqlite_version = result[0] if result else None
|
||||||
except Exception:
|
except (OperationalError, ProgrammingError):
|
||||||
sqlite_version = None
|
sqlite_version = None
|
||||||
return {
|
return {
|
||||||
"available": True,
|
"available": True,
|
||||||
|
|
@ -700,5 +701,5 @@ def db_health() -> dict[str, Any]:
|
||||||
else _resolve_database_url(),
|
else _resolve_database_url(),
|
||||||
"sqlite_version": sqlite_version,
|
"sqlite_version": sqlite_version,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except SQLAlchemyError as e:
|
||||||
return {"available": False, "error": str(e)[:200]}
|
return {"available": False, "error": str(e)[:200]}
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ async def _write_to_sheets_api(
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return {"success": False, "error": "Invalid credentials JSON"}
|
return {"success": False, "error": "Invalid credentials JSON"}
|
||||||
except (json.JSONDecodeError, ValueError) as e:
|
except ValueError as e:
|
||||||
return {"success": False, "error": str(e)[:200]}
|
return {"success": False, "error": str(e)[:200]}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ class JobQueue:
|
||||||
try:
|
try:
|
||||||
self._redis = await aioredis.from_url(self.redis_url, socket_timeout=3)
|
self._redis = await aioredis.from_url(self.redis_url, socket_timeout=3)
|
||||||
await self._redis.ping()
|
await self._redis.ping()
|
||||||
except:
|
except (aioredis.RedisError, OSError, TimeoutError):
|
||||||
self._redis = None # Fallback to local storage
|
self._redis = None # Fallback to local storage
|
||||||
return self._redis
|
return self._redis
|
||||||
|
|
||||||
|
|
@ -121,5 +121,5 @@ class JobQueue:
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except:
|
except (httpx.RequestError, OSError):
|
||||||
pass # Webhook fire is best-effort
|
pass # Webhook fire is best-effort
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,13 @@
|
||||||
# Licensed under MIT. See LICENSE.
|
# Licensed under MIT. See LICENSE.
|
||||||
"""Concrete LLM provider implementations."""
|
"""Concrete LLM provider implementations."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from llm_providers.base import LLMProvider, LLMResponse
|
from llm_providers.base import LLMProvider, LLMResponse
|
||||||
|
from llm_providers.registry import LLMRegistry
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -348,7 +351,7 @@ class OpenRouterProvider(LLMProvider):
|
||||||
raise NotImplementedError("OpenRouter focuses on chat; use dedicated embedding providers")
|
raise NotImplementedError("OpenRouter focuses on chat; use dedicated embedding providers")
|
||||||
|
|
||||||
|
|
||||||
def register_default_providers(registry: "LLMRegistry") -> None:
|
def register_default_providers(registry: LLMRegistry) -> None:
|
||||||
"""Register all providers whose API keys are set in environment."""
|
"""Register all providers whose API keys are set in environment."""
|
||||||
api_key_map = {
|
api_key_map = {
|
||||||
"openai": os.getenv("OPENAI_API_KEY"),
|
"openai": os.getenv("OPENAI_API_KEY"),
|
||||||
|
|
|
||||||
|
|
@ -147,10 +147,6 @@ def _add_service_name(_, __, event_dict: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
|
||||||
def _add_request_id_if_present(_, __, event_dict: dict[str, Any]) -> dict[str, Any]:
|
def _add_request_id_if_present(_, __, event_dict: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Pull request_id from contextvars (set by middleware) if present."""
|
"""Pull request_id from contextvars (set by middleware) if present."""
|
||||||
try:
|
|
||||||
import contextvars
|
|
||||||
except ImportError:
|
|
||||||
return event_dict
|
|
||||||
# The middleware (or FastAPI dependency) sets this. We don't import
|
# The middleware (or FastAPI dependency) sets this. We don't import
|
||||||
# the middleware here to avoid circular imports.
|
# the middleware here to avoid circular imports.
|
||||||
return event_dict
|
return event_dict
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,7 @@ classifiers = [
|
||||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
"Typing :: Typed",
|
"Typing :: Typed",
|
||||||
]
|
]
|
||||||
[project.license-files]
|
license-files = ["LICENSE", "LICENSE-BSL-STEALTH"]
|
||||||
paths = ["LICENSE", "LICENSE-BSL-STEALTH"]
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi>=0.115.0",
|
"fastapi>=0.115.0",
|
||||||
"uvicorn[standard]>=0.32.0",
|
"uvicorn[standard]>=0.32.0",
|
||||||
|
|
|
||||||
|
|
@ -388,7 +388,7 @@ async def llm_enhance_reconciliation(
|
||||||
groups.setdefault(gid, []).append(e)
|
groups.setdefault(gid, []).append(e)
|
||||||
verified = 0
|
verified = 0
|
||||||
refuted = 0
|
refuted = 0
|
||||||
for gid, group in groups.items():
|
for _gid, group in groups.items():
|
||||||
result = await llm_entity_reconcile(group, vertical="product")
|
result = await llm_entity_reconcile(group, vertical="product")
|
||||||
if result.get("is_same_entity"):
|
if result.get("is_same_entity"):
|
||||||
verified += 1
|
verified += 1
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ Beats every anti-bot system: Cloudflare, DataDome, Akamai, PerimeterX."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any, ClassVar
|
||||||
from urllib.parse import urljoin, urlparse
|
from urllib.parse import urljoin, urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
@ -55,7 +55,7 @@ CLOUDFLARE_INDICATORS = [
|
||||||
class BlockDetector:
|
class BlockDetector:
|
||||||
"""Detect anti-bot blocks by analyzing HTML, status codes, and headers."""
|
"""Detect anti-bot blocks by analyzing HTML, status codes, and headers."""
|
||||||
|
|
||||||
VENDOR_PATTERNS = {
|
VENDOR_PATTERNS: ClassVar[dict[str, list[str]]] = {
|
||||||
"cloudflare": [
|
"cloudflare": [
|
||||||
"cloudflare",
|
"cloudflare",
|
||||||
"cf-",
|
"cf-",
|
||||||
|
|
@ -70,7 +70,7 @@ class BlockDetector:
|
||||||
"imperva": ["imperva", "incapsula"],
|
"imperva": ["imperva", "incapsula"],
|
||||||
}
|
}
|
||||||
|
|
||||||
GENERIC_PATTERNS = [
|
GENERIC_PATTERNS: ClassVar[list[str]] = [
|
||||||
"captcha",
|
"captcha",
|
||||||
"access denied",
|
"access denied",
|
||||||
"blocked",
|
"blocked",
|
||||||
|
|
@ -533,7 +533,7 @@ class PryScraper:
|
||||||
links = await self._extract_links(
|
links = await self._extract_links(
|
||||||
current_url, {"limit": max_pages - len(visited)}
|
current_url, {"limit": max_pages - len(visited)}
|
||||||
)
|
)
|
||||||
to_visit.extend((l, depth + 1) for l in links if l not in visited)
|
to_visit.extend((link, depth + 1) for link in links if link not in visited)
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
continue
|
continue
|
||||||
return results
|
return results
|
||||||
|
|
|
||||||
8
tasks.py
8
tasks.py
|
|
@ -7,6 +7,7 @@ Long-running jobs like crawls, monitors, and bulk imports should not block reque
|
||||||
# Part of Pry — https://git.rugmunch.io/RugMunchMedia/pryscraper
|
# Part of Pry — https://git.rugmunch.io/RugMunchMedia/pryscraper
|
||||||
# Licensed under MIT. See LICENSE.
|
# Licensed under MIT. See LICENSE.
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import importlib.util
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
|
@ -20,12 +21,7 @@ from paths import PRY_DATA_DIR
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Try Celery
|
# Try Celery
|
||||||
try:
|
_has_celery = importlib.util.find_spec("celery") is not None
|
||||||
from celery import Celery
|
|
||||||
|
|
||||||
_has_celery = True
|
|
||||||
except ImportError:
|
|
||||||
_has_celery = False
|
|
||||||
|
|
||||||
JOBS_DIR = PRY_DATA_DIR / "jobs"
|
JOBS_DIR = PRY_DATA_DIR / "jobs"
|
||||||
JOBS_DIR.mkdir(parents=True, exist_ok=True)
|
JOBS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,15 @@ import json
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from api import app
|
from api import app
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
def _find_free_port() -> int:
|
def _find_free_port() -> int:
|
||||||
"""Return an available TCP port on 127.0.0.1."""
|
"""Return an available TCP port on 127.0.0.1."""
|
||||||
|
|
@ -31,9 +34,9 @@ def _wait_for_server(url: str, timeout: float = 30.0) -> None:
|
||||||
deadline = time.time() + timeout
|
deadline = time.time() + timeout
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
try:
|
try:
|
||||||
httpx.get(url, timeout=1.0)
|
httpx.get(url, timeout=5.0)
|
||||||
return
|
return
|
||||||
except Exception:
|
except httpx.RequestError:
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
raise TimeoutError(f"Server at {url} did not start within {timeout}s")
|
raise TimeoutError(f"Server at {url} did not start within {timeout}s")
|
||||||
|
|
||||||
|
|
@ -194,7 +197,7 @@ class TestMCPSSE:
|
||||||
"--log-level",
|
"--log-level",
|
||||||
"warning",
|
"warning",
|
||||||
],
|
],
|
||||||
cwd="/home/dev/pry",
|
cwd=str(REPO_ROOT),
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -101,19 +101,19 @@ async def test_seo_llm_enhancement_fills_empty_critical_fields():
|
||||||
patch("seo_monitor._get_attr", return_value=""),
|
patch("seo_monitor._get_attr", return_value=""),
|
||||||
patch("seo_monitor._count_links", return_value=0),
|
patch("seo_monitor._count_links", return_value=0),
|
||||||
patch("llm_features.llm_seo_analyze", fake_llm),
|
patch("llm_features.llm_seo_analyze", fake_llm),
|
||||||
|
patch("client.get_client") as mock_get_client,
|
||||||
):
|
):
|
||||||
# Mock the HTTP client to return a fake HTML response
|
# Mock the HTTP client to return a fake HTML response
|
||||||
with patch("client.get_client") as mock_get_client:
|
mock_resp = MagicMock()
|
||||||
mock_resp = MagicMock()
|
mock_resp.is_success = True
|
||||||
mock_resp.is_success = True
|
mock_resp.text = "<html><body><h1>placeholder</h1></body></html>"
|
||||||
mock_resp.text = "<html><body><h1>placeholder</h1></body></html>"
|
mock_resp.status_code = 200
|
||||||
mock_resp.status_code = 200
|
mock_resp.headers = {"content-type": "text/html", "last-modified": ""}
|
||||||
mock_resp.headers = {"content-type": "text/html", "last-modified": ""}
|
mock_client = MagicMock()
|
||||||
mock_client = MagicMock()
|
mock_client.get = AsyncMock(return_value=mock_resp)
|
||||||
mock_client.get = AsyncMock(return_value=mock_resp)
|
mock_get_client.return_value = mock_client
|
||||||
mock_get_client.return_value = mock_client
|
|
||||||
|
|
||||||
result = await analyze_seo("https://example.com/")
|
result = await analyze_seo("https://example.com/")
|
||||||
|
|
||||||
assert fake_llm.called
|
assert fake_llm.called
|
||||||
assert result["title"] == "Pry - Web Intelligence"
|
assert result["title"] == "Pry - Web Intelligence"
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ def test_json_output_has_required_fields(capsys):
|
||||||
log.info("test_event", key="value", count=42)
|
log.info("test_event", key="value", count=42)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
# Find the JSON line
|
# Find the JSON line
|
||||||
lines = [l for l in captured.out.splitlines() if l.strip().startswith("{")]
|
lines = [line for line in captured.out.splitlines() if line.strip().startswith("{")]
|
||||||
assert lines, f"no JSON output: {captured.out!r}"
|
assert lines, f"no JSON output: {captured.out!r}"
|
||||||
record = json.loads(lines[0])
|
record = json.loads(lines[0])
|
||||||
# Per CONVENTIONS.md Part 5, required fields: timestamp, level, service, event
|
# Per CONVENTIONS.md Part 5, required fields: timestamp, level, service, event
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,7 @@ def test_normalize_name_adds_pry_prefix():
|
||||||
def test_get_secret_default_when_missing():
|
def test_get_secret_default_when_missing():
|
||||||
# Use a name that definitely doesn't exist anywhere
|
# Use a name that definitely doesn't exist anywhere
|
||||||
with patch.dict(os.environ, {"PRY_SECRET_BACKEND": "env"}, clear=False):
|
with patch.dict(os.environ, {"PRY_SECRET_BACKEND": "env"}, clear=False):
|
||||||
# Clear any env vars that might match
|
value = get_secret("definitely_not_a_real_secret_name_xyz_12345", default="missing")
|
||||||
with patch.dict(os.environ, {}, clear=False):
|
|
||||||
value = get_secret("definitely_not_a_real_secret_name_xyz_12345", default="missing")
|
|
||||||
assert value == "missing"
|
assert value == "missing"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ Beats Cloudflare, DataDome, Akamai, Imperva, PerimeterX, and generic blocks."""
|
||||||
# Licensed under Business Source License 1.1 — see LICENSE-BSL-STEALTH.
|
# Licensed under Business Source License 1.1 — see LICENSE-BSL-STEALTH.
|
||||||
# Change Date: 2029-01-01 (converts to MIT).
|
# Change Date: 2029-01-01 (converts to MIT).
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
|
@ -19,38 +20,14 @@ from urllib.parse import urlparse
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Try importing optional bypass libraries
|
# Try importing optional bypass libraries
|
||||||
_has_cloudscraper = False
|
_has_cloudscraper = importlib.util.find_spec("cloudscraper") is not None
|
||||||
_has_undetected = False
|
_has_undetected = importlib.util.find_spec("undetected_chromedriver") is not None
|
||||||
_has_playwright = False
|
_has_playwright = importlib.util.find_spec("playwright") is not None
|
||||||
|
_has_tor = importlib.util.find_spec("aiohttp_socks") is not None
|
||||||
|
|
||||||
try:
|
if _has_playwright:
|
||||||
import cloudscraper
|
|
||||||
|
|
||||||
_has_cloudscraper = True
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
import undetected_chromedriver as uc
|
|
||||||
|
|
||||||
_has_undetected = True
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
from playwright.async_api import async_playwright
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
_has_playwright = True
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
from aiohttp_socks import ProxyConnector
|
|
||||||
|
|
||||||
_has_tor = True
|
|
||||||
except ImportError:
|
|
||||||
_has_tor = False
|
|
||||||
|
|
||||||
|
|
||||||
class UltimateScraper:
|
class UltimateScraper:
|
||||||
"""10-tier anti-detection scraper with automatic fallback.
|
"""10-tier anti-detection scraper with automatic fallback.
|
||||||
|
|
@ -293,6 +270,8 @@ class UltimateScraper:
|
||||||
if not _has_cloudscraper:
|
if not _has_cloudscraper:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
|
import cloudscraper
|
||||||
|
|
||||||
scraper = cloudscraper.create_scraper(
|
scraper = cloudscraper.create_scraper(
|
||||||
browser={"browser": "chrome", "platform": "windows", "mobile": False}
|
browser={"browser": "chrome", "platform": "windows", "mobile": False}
|
||||||
)
|
)
|
||||||
|
|
@ -446,14 +425,16 @@ class UltimateScraper:
|
||||||
from aiohttp_socks import ProxyConnector
|
from aiohttp_socks import ProxyConnector
|
||||||
|
|
||||||
connector = ProxyConnector.from_url("socks5://127.0.0.1:9050")
|
connector = ProxyConnector.from_url("socks5://127.0.0.1:9050")
|
||||||
async with aiohttp.ClientSession(connector=connector) as session:
|
async with (
|
||||||
async with session.get(
|
aiohttp.ClientSession(connector=connector) as session,
|
||||||
|
session.get(
|
||||||
url,
|
url,
|
||||||
headers=self._build_headers(url),
|
headers=self._build_headers(url),
|
||||||
timeout=aiohttp.ClientTimeout(total=60),
|
timeout=aiohttp.ClientTimeout(total=60),
|
||||||
) as resp:
|
) as resp,
|
||||||
if resp.status == 200:
|
):
|
||||||
return await resp.text()
|
if resp.status == 200:
|
||||||
|
return await resp.text()
|
||||||
except aiohttp.ClientError as e:
|
except aiohttp.ClientError as e:
|
||||||
logger.debug("tor_failed", extra={"error": str(e)[:50]})
|
logger.debug("tor_failed", extra={"error": str(e)[:50]})
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue