Each module did:
X_DIR = Path(os.path.expanduser("~/.pry/x"))
After:
from paths import PRY_DATA_DIR
X_DIR = PRY_DATA_DIR / "x"
The module-level Path construction is preserved, so the rest of the
code is unchanged. PRY_DATA_DIR is read once at import (overridable via
the env var of the same name).
Verified:
- 407 tests collect (was 5 collection errors from a misplaced import)
- 83 sampled tests pass (intelligence, proxy_manager, x402, agency,
gdpr, referrals, marketplace, api)
- 0 remaining hardcoded ~/.pry references in .py files
Follow-up: paths.py adds subdir(name) helper for new code that wants
auto-mkdir; existing modules still call .mkdir(exist_ok=True) themselves
to preserve the eager-init behavior they had before.
117 lines
4.4 KiB
Python
117 lines
4.4 KiB
Python
# 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.
|
|
"""Pry — Account pool management, session persistence, proxy scoring."""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import time
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from paths import PRY_DATA_DIR
|
|
logger = logging.getLogger(__name__)
|
|
|
|
ACCOUNTS_DIR = PRY_DATA_DIR / "accounts"
|
|
ACCOUNTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
class AccountPool:
|
|
"""Manage pool of registered accounts with session persistence."""
|
|
|
|
def store(self, site: str, credentials: dict[str, Any], profile_id: str = "", metadata: dict | None = None) -> str:
|
|
import uuid
|
|
account_id = uuid.uuid4().hex[:12]
|
|
account = {
|
|
"id": account_id, "site": site, "credentials": credentials,
|
|
"profile_id": profile_id, "metadata": metadata or {},
|
|
"status": "active", "created_at": datetime.now(UTC).isoformat(),
|
|
"last_used": None, "use_count": 0, "errors": [],
|
|
}
|
|
path = ACCOUNTS_DIR / f"{site}_{account_id}.json"
|
|
path.write_text(json.dumps(account, indent=2))
|
|
logger.info("account_stored", extra={"site": site, "account_id": account_id})
|
|
return account_id
|
|
|
|
def get_active(self, site: str) -> dict[str, Any] | None:
|
|
"""Get a random active account for a site."""
|
|
accounts = []
|
|
for path in ACCOUNTS_DIR.glob(f"{site}_*.json"):
|
|
try:
|
|
acct = json.loads(path.read_text())
|
|
if acct.get("status") == "active":
|
|
accounts.append(acct)
|
|
except (json.JSONDecodeError, OSError):
|
|
continue
|
|
if not accounts:
|
|
return None
|
|
import random
|
|
return random.choice(accounts)
|
|
|
|
def mark_error(self, account_id: str, error: str) -> None:
|
|
for path in ACCOUNTS_DIR.glob(f"*_{account_id}.json"):
|
|
try:
|
|
acct = json.loads(path.read_text())
|
|
acct["errors"].append({"time": datetime.now(UTC).isoformat(), "error": error[:200]})
|
|
if len(acct["errors"]) > 5:
|
|
acct["status"] = "suspended"
|
|
path.write_text(json.dumps(acct, indent=2))
|
|
except Exception:
|
|
pass
|
|
|
|
def record_use(self, account_id: str) -> None:
|
|
for path in ACCOUNTS_DIR.glob(f"*_{account_id}.json"):
|
|
try:
|
|
acct = json.loads(path.read_text())
|
|
acct["last_used"] = datetime.now(UTC).isoformat()
|
|
acct["use_count"] = acct.get("use_count", 0) + 1
|
|
path.write_text(json.dumps(acct, indent=2))
|
|
except Exception:
|
|
pass
|
|
|
|
def list_accounts(self, site: str = "") -> list[dict[str, Any]]:
|
|
accounts = []
|
|
pattern = f"{site}_*.json" if site else "*_*.json"
|
|
for path in sorted(ACCOUNTS_DIR.glob(pattern), key=os.path.getmtime, reverse=True)[:50]:
|
|
try:
|
|
data = json.loads(path.read_text())
|
|
data.pop("credentials", None) # Don't expose passwords
|
|
accounts.append(data)
|
|
except Exception:
|
|
continue
|
|
return accounts
|
|
|
|
|
|
class ProxyScorer:
|
|
"""Score and rank proxies for reliability."""
|
|
|
|
async def test_proxy(self, proxy_url: str, test_url: str = "https://httpbin.org/ip", timeout: int = 10) -> dict[str, Any]:
|
|
from client import get_client
|
|
client = await get_client()
|
|
start = time.time()
|
|
try:
|
|
resp = await client.get(test_url, timeout=timeout)
|
|
elapsed = time.time() - start
|
|
return {"proxy": proxy_url, "working": resp.is_success, "latency": round(elapsed, 2),
|
|
"status": resp.status_code, "ip": resp.text[:50] if resp.is_success else ""}
|
|
except Exception as e:
|
|
return {"proxy": proxy_url, "working": False, "error": str(e)[:80]}
|
|
|
|
def score(self, test_result: dict[str, Any]) -> int:
|
|
"""Score proxy 0-100."""
|
|
if not test_result.get("working"):
|
|
return 0
|
|
latency = test_result.get("latency", 99)
|
|
if latency < 1:
|
|
return 100
|
|
if latency < 2:
|
|
return 90
|
|
if latency < 5:
|
|
return 75
|
|
if latency < 10:
|
|
return 50
|
|
return 25
|