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