pryscraper/mconfig.py
cryptorugmunch a7c30b12cd
Some checks failed
CI / lint (push) Failing after 2s
CI / typecheck (push) Failing after 2s
CI / test (push) Failing after 2s
CI / Secret scan (gitleaks) (push) Failing after 1s
CI / Security audit (bandit) (push) Failing after 2s
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.
2026-07-02 21:51:25 +02:00

133 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()}