pryscraper/sessions.py
cryptorugmunch dd63022530 refactor(paths): replace 26 modules hardcoded ~/.pry/ with PRY_DATA_DIR
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.
2026-07-02 20:20:04 +02:00

112 lines
3.7 KiB
Python

"""Pry — persistent browser session management.
Saves and restores Playwright browser contexts to disk."""
from paths import PRY_DATA_DIR
# 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 logging
import os
import time
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, cast
logger = logging.getLogger(__name__)
SESSIONS_DIR = PRY_DATA_DIR / "sessions"
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
def _session_path(session_id: str) -> Path:
"""Get filesystem path for a session's saved state."""
return SESSIONS_DIR / f"{session_id}.json"
async def save_session(
session_id: str,
cookies: list[dict[str, Any]],
local_storage: dict[str, str] | None = None,
session_storage: dict[str, str] | None = None,
metadata: dict[str, Any] | None = None,
) -> None:
"""Save browser session state to disk."""
data = {
"session_id": session_id,
"saved_at": datetime.now(UTC).isoformat(),
"cookies": cookies,
"local_storage": local_storage or {},
"session_storage": session_storage or {},
"metadata": metadata or {},
}
path = _session_path(session_id)
try:
path.write_text(json.dumps(data, indent=2))
logger.info("session_saved", extra={"session_id": session_id, "path": str(path)})
except OSError:
logger.exception("session_save_failed", extra={"session_id": session_id})
async def load_session(session_id: str) -> dict[str, Any] | None:
"""Load saved browser session from disk."""
path = _session_path(session_id)
if not path.exists():
logger.info("session_not_found", extra={"session_id": session_id})
return None
try:
data: dict[str, Any] = cast("dict[str, Any]", json.loads(path.read_text()))
logger.info(
"session_loaded", extra={"session_id": session_id, "saved_at": data.get("saved_at")}
)
return data
except (json.JSONDecodeError, OSError):
logger.exception("session_load_failed", extra={"session_id": session_id})
return None
async def delete_session(session_id: str) -> bool:
"""Delete a saved session."""
path = _session_path(session_id)
if path.exists():
path.unlink()
logger.info("session_deleted", extra={"session_id": session_id})
return True
return False
async def list_sessions() -> list[dict[str, Any]]:
"""List all saved sessions with metadata."""
sessions = []
for path in sorted(SESSIONS_DIR.glob("*.json"), key=os.path.getmtime, reverse=True):
try:
data = json.loads(path.read_text())
sessions.append(
{
"session_id": data.get("session_id", path.stem),
"saved_at": data.get("saved_at", ""),
"metadata": data.get("metadata", {}),
"cookie_count": len(data.get("cookies", [])),
"size_bytes": path.stat().st_size,
}
)
except (json.JSONDecodeError, OSError):
continue
return sessions
async def cleanup_sessions(max_age_days: int = 30) -> int:
"""Remove sessions older than max_age_days. Returns count removed."""
now = time.time()
removed = 0
for path in SESSIONS_DIR.glob("*.json"):
age_days = (now - path.stat().st_mtime) / 86400
if age_days > max_age_days:
path.unlink()
removed += 1
if removed:
logger.info("session_cleanup", extra={"removed": removed, "max_age_days": max_age_days})
return removed