fix(lint): resolve remaining ruff errors and unblock MCP SSE test #1

Merged
cryptorugmunch merged 1 commit from fix/lint-cleanup into main 2026-07-02 23:18:40 +02:00
17 changed files with 60 additions and 87 deletions

4
api.py
View file

@ -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)

View file

@ -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

View file

@ -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
View file

@ -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]}

View file

@ -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]}

View file

@ -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

View file

@ -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"),

View file

@ -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

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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,
) )

View file

@ -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"

View file

@ -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

View file

@ -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"

View file

@ -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