pryscraper/mconfig.py
cryptorugmunch 0200bf3e16 refactor(exceptions): add ruff BLE001; convert 103 broad except Exception
Per CONVENTIONS.md Part 2 ("Never bare except") and CONVENTIONS.md
Part 7 (pre-commit hooks: ruff), blind `except Exception` is now a
lint failure. Pre-existing sites are marked `# noqa: BLE001` for
later manual review; new code must use specific exception types.

Changes:
- pyproject.toml: added "BLE" to ruff lint select. BLE001 is now enforced
- 103 of 166 `except Exception` sites were auto-converted to specific
  types based on context (httpx, json, OSError, subprocess, etc.)
- 62 remaining sites marked with `# noqa: BLE001` for later review
  (mostly generic try/except wrappers that legitimately need broad catch
  for graceful degradation: e.g. compliance LLM fallback must catch
  any error to preserve the regex result)
- 1 manual fix: reverted compliance.py LLM fallback to broad except
  with explicit "must catch all errors" comment + noqa
- 2 files (commerce_sync.py, crm_sync.py) needed `import httpx` added
  so the auto-converted exception references would resolve
- 5 source files (agency, monitor, pipelines, auth_connector,
  llm_providers/registry) renamed "name" -> "<scope>_name" in
  extra={...} dicts because "name" is a reserved LogRecord field

Test impact:
- 14 failing tests -> 1 (the SSE subprocess test is a sandbox limitation,
  pre-existing and unrelated)
- New `test_ble_temp.py` verifies BLE001 catches new violations

Follow-up:
- Each `# noqa: BLE001` site should be reviewed and replaced with a
  specific exception type where possible. The most common legitimate
  broad-catch case is the LLM fallback path; everything else probably
  can be narrowed.
2026-07-02 21:04:53 +02:00

135 lines
5.1 KiB
Python

"""Pry Config — runtime configuration for proxy, Tor, VPN, and anti-detection.
Users can configure at startup (env vars) or at runtime (API calls)."""
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Rug Munch Media LLC
#
# Part of Pry — https://git.rugmunch.io/RugMunchMedia/pryscraper
# Licensed under MIT. See LICENSE.
import json
import os
CONFIG_FILE = "/app/config.json"
DEFAULT_CONFIG = {
"proxy": {"enabled": False, "type": "", "url": "", "username": "", "password": ""},
"tor": {"enabled": False, "socks5_host": "tor", "socks5_port": 9050, "control_port": 9051},
"rotation": {
"user_agent": "rotate",
"ip": "off",
"timing": "random",
"delay_range": [0.5, 3.0],
},
"stealth": {
"webdriver_override": True,
"canvas_noise": True,
"webrtc_disable": True,
"geolocation_spoof": True,
},
"retry": {"max_attempts": 3, "min_quality": 20, "backoff": "exponential"},
"output": {"default_format": "markdown", "max_chars": 100000, "include_links": True},
"rate_limit": {"rpm": 120, "burst": 200},
}
class PryConfig:
"""Configuration manager — loads from env vars, config file, and API overrides."""
def __init__(self):
self.config = json.loads(json.dumps(DEFAULT_CONFIG)) # Deep copy
self._load_env()
self._load_file()
self._resolve_proxy_chain()
def _load_env(self):
"""Environment variables override defaults."""
if os.getenv("TOR_ENABLED", "").lower() in ("1", "true", "yes"):
self.config["tor"]["enabled"] = True
if os.getenv("TOR_SOCKS5_HOST"):
self.config["tor"]["socks5_host"] = os.getenv("TOR_SOCKS5_HOST")
if os.getenv("TOR_SOCKS5_PORT"):
self.config["tor"]["socks5_port"] = int(os.getenv("TOR_SOCKS5_PORT"))
if os.getenv("PROXY_URL"):
self.config["proxy"]["enabled"] = True
self.config["proxy"]["url"] = os.getenv("PROXY_URL")
self.config["proxy"]["type"] = os.getenv("PROXY_TYPE", "http")
if os.getenv("PROXY_USERNAME"):
self.config["proxy"]["username"] = os.getenv("PROXY_USERNAME")
if os.getenv("PROXY_PASSWORD"):
self.config["proxy"]["password"] = os.getenv("PROXY_PASSWORD")
if os.getenv("IP_ROTATION"):
self.config["rotation"]["ip"] = os.getenv("IP_ROTATION")
if os.getenv("MAX_RETRIES"):
self.config["retry"]["max_attempts"] = int(os.getenv("MAX_RETRIES"))
if os.getenv("MIN_QUALITY"):
self.config["retry"]["min_quality"] = int(os.getenv("MIN_QUALITY"))
if os.getenv("RATE_LIMIT_RPM"):
self.config["rate_limit"]["rpm"] = int(os.getenv("RATE_LIMIT_RPM"))
def _load_file(self):
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE) as f:
user_config = json.load(f)
self._deep_merge(self.config, user_config)
except OSError:
pass
def _deep_merge(self, base: dict, override: dict):
for key, value in override.items():
if key in base and isinstance(base[key], dict) and isinstance(value, dict):
self._deep_merge(base[key], value)
else:
base[key] = value
def _resolve_proxy_chain(self):
"""Build the proxy chain: Tor -> User Proxy -> Direct"""
proxies = []
if self.config["tor"]["enabled"]:
proxies.append(
f"socks5://{self.config['tor']['socks5_host']}:{self.config['tor']['socks5_port']}"
)
if self.config["proxy"]["enabled"]:
url = self.config["proxy"]["url"]
if self.config["proxy"]["username"]:
netloc = url.split("://")[1] if "://" in url else url
proxies.append(
f"{self.config['proxy']['type']}://{self.config['proxy']['username']}:{self.config['proxy']['password']}@{netloc}"
)
else:
proxies.append(url)
self.config["_proxy_chain"] = proxies
self.config["_proxy_url"] = proxies[0] if proxies else None
def get_proxy_url(self) -> str | None:
return self.config.get("_proxy_url")
def get_proxy_chain(self) -> list:
return self.config.get("_proxy_chain", [])
def get(self, key: str, default=None):
keys = key.split(".")
val = self.config
for k in keys:
if isinstance(val, dict):
val = val.get(k)
else:
return default
if val is None:
return default
return val if val is not None else default
def to_dict(self) -> dict:
return {k: v for k, v in self.config.items() if not k.startswith("_")}
def update(self, updates: dict) -> dict:
self._deep_merge(self.config, updates)
self._resolve_proxy_chain()
try:
with open(CONFIG_FILE, "w") as f:
json.dump(self.to_dict(), f, indent=2)
except OSError:
pass
return {"status": "ok", "config": self.to_dict()}