Squashed from chore/license-relicense. Full message preserved in the original branch commitbb77eb5. See ADR-0002 for the decision rationale. Refs: ADR-0002, commitbb77eb5
136 lines
5 KiB
Python
136 lines
5 KiB
Python
"""Pry Python SDK — simple client library.
|
|
Usage:
|
|
from pry_sdk import PryCrawl
|
|
mc = PryCrawl("http://localhost:8002")
|
|
result = await mc.scrape("https://example.com")
|
|
"""
|
|
|
|
# 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 asyncio
|
|
import logging
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_LOOP: asyncio.AbstractEventLoop | None = None
|
|
|
|
|
|
def _get_or_create_loop() -> asyncio.AbstractEventLoop:
|
|
"""Get the running event loop or create a persistent one for sync use."""
|
|
global _LOOP
|
|
try:
|
|
return asyncio.get_running_loop()
|
|
except RuntimeError:
|
|
if _LOOP is None or _LOOP.is_closed():
|
|
_LOOP = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(_LOOP)
|
|
return _LOOP
|
|
|
|
|
|
class PryCrawl:
|
|
"""Python SDK for Pry API."""
|
|
|
|
def __init__(
|
|
self, base_url: str = "http://localhost:8002", api_key: str | None = None, timeout: int = 60
|
|
):
|
|
self.base_url = base_url.rstrip("/")
|
|
self.timeout = timeout
|
|
self._headers = {"Content-Type": "application/json"}
|
|
if api_key:
|
|
self._headers["Authorization"] = f"Bearer {api_key}"
|
|
|
|
async def _post(self, path: str, data: dict[str, Any]) -> dict[str, Any]:
|
|
async with httpx.AsyncClient(timeout=self.timeout, headers=self._headers) as client:
|
|
resp = await client.post(f"{self.base_url}{path}", json=data)
|
|
resp.raise_for_status()
|
|
result: dict[str, Any] = resp.json()
|
|
return result
|
|
|
|
async def scrape(self, url: str, **options: Any) -> dict[str, Any]:
|
|
"""Scrape a single URL to markdown."""
|
|
return await self._post("/v1/scrape", {"url": url, **options})
|
|
|
|
async def scrape_json(self, url: str, schema: dict[str, str], **options: Any) -> dict[str, Any]:
|
|
"""Scrape with structured JSON extraction."""
|
|
return await self._post(
|
|
"/v1/scrape", {"url": url, "formats": ["json"], "jsonSchema": schema, **options}
|
|
)
|
|
|
|
async def crawl(self, url: str, max_pages: int = 10, **options: Any) -> dict[str, Any]:
|
|
"""Crawl multiple pages from a URL."""
|
|
return await self._post("/v1/crawl", {"url": url, "maxPages": max_pages, **options})
|
|
|
|
async def map(self, url: str, limit: int = 50) -> dict[str, Any]:
|
|
"""Discover URLs on a site."""
|
|
return await self._post("/v1/map", {"url": url, "limit": limit})
|
|
|
|
async def parse(self, url: str) -> dict[str, Any]:
|
|
"""Parse a document (PDF, DOCX, image)."""
|
|
return await self._post("/v1/parse", {"url": url})
|
|
|
|
async def automate(self, steps: list[dict[str, Any]], **options: Any) -> dict[str, Any]:
|
|
"""Run browser automation steps."""
|
|
return await self._post("/v1/automate", {"steps": steps, **options})
|
|
|
|
async def screenshot(self, url: str) -> dict[str, Any]:
|
|
"""Take a screenshot of a URL."""
|
|
return await self._post("/v1/screenshot", {"url": url})
|
|
|
|
async def health(self) -> dict[str, Any]:
|
|
"""Check service health."""
|
|
async with httpx.AsyncClient(timeout=5) as client:
|
|
resp = await client.get(f"{self.base_url}/health")
|
|
result: dict[str, Any] = resp.json()
|
|
return result
|
|
|
|
|
|
class PryCrawlSync:
|
|
"""Synchronous wrapper for Pry SDK."""
|
|
|
|
def __init__(
|
|
self, base_url: str = "http://localhost:8002", api_key: str | None = None, timeout: int = 60
|
|
):
|
|
self._client = PryCrawl(base_url, api_key, timeout)
|
|
|
|
def _run(self, coro: Any) -> Any:
|
|
loop = _get_or_create_loop()
|
|
return loop.run_until_complete(coro)
|
|
|
|
def scrape(self, url: str, **options: Any) -> dict[str, Any]:
|
|
result: dict[str, Any] = self._run(self._client.scrape(url, **options))
|
|
return result
|
|
|
|
def scrape_json(self, url: str, schema: dict[str, str], **options: Any) -> dict[str, Any]:
|
|
result: dict[str, Any] = self._run(self._client.scrape_json(url, schema, **options))
|
|
return result
|
|
|
|
def crawl(self, url: str, max_pages: int = 10, **options: Any) -> dict[str, Any]:
|
|
result: dict[str, Any] = self._run(self._client.crawl(url, max_pages, **options))
|
|
return result
|
|
|
|
def map(self, url: str, limit: int = 50) -> dict[str, Any]:
|
|
result: dict[str, Any] = self._run(self._client.map(url, limit))
|
|
return result
|
|
|
|
def parse(self, url: str) -> dict[str, Any]:
|
|
result: dict[str, Any] = self._run(self._client.parse(url))
|
|
return result
|
|
|
|
def automate(self, steps: list[dict[str, Any]], **options: Any) -> dict[str, Any]:
|
|
result: dict[str, Any] = self._run(self._client.automate(steps, **options))
|
|
return result
|
|
|
|
def screenshot(self, url: str) -> dict[str, Any]:
|
|
result: dict[str, Any] = self._run(self._client.screenshot(url))
|
|
return result
|
|
|
|
def health(self) -> dict[str, Any]:
|
|
result: dict[str, Any] = self._run(self._client.health())
|
|
return result
|