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.
160 lines
5.1 KiB
Python
160 lines
5.1 KiB
Python
"""Pry — Actor Marketplace (Apify-style).
|
|
Pre-built scrapers that can be subscribed to and run on schedule.
|
|
Users can publish their own actors. We earn from usage."""
|
|
|
|
# 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 uuid
|
|
from datetime import UTC, datetime
|
|
from enum import StrEnum
|
|
from typing import Any
|
|
|
|
from paths import PRY_DATA_DIR
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
ACTOR_DIR = PRY_DATA_DIR / "actors"
|
|
ACTOR_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
class ActorVisibility(StrEnum):
|
|
PRIVATE = "private"
|
|
PUBLIC = "public"
|
|
UNLISTED = "unlist"
|
|
|
|
|
|
class Actor:
|
|
"""A reusable scraper actor that can be run on demand or schedule."""
|
|
|
|
def __init__(
|
|
self,
|
|
actor_id: str,
|
|
name: str,
|
|
description: str,
|
|
template_id: str = "",
|
|
code: str = "",
|
|
price_per_run: float = 0.0,
|
|
visibility: ActorVisibility = ActorVisibility.PRIVATE,
|
|
schedule_cron: str = "",
|
|
tags: list[str] | None = None,
|
|
):
|
|
self.actor_id = actor_id
|
|
self.name = name
|
|
self.description = description
|
|
self.template_id = template_id
|
|
self.code = code
|
|
self.price_per_run = price_per_run
|
|
self.visibility = visibility
|
|
self.schedule_cron = schedule_cron
|
|
self.tags = tags or []
|
|
self.created_at = datetime.now(UTC).isoformat()
|
|
self.run_count = 0
|
|
self.revenue_usd = 0.0
|
|
self.author = ""
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"actor_id": self.actor_id,
|
|
"name": self.name,
|
|
"description": self.description,
|
|
"template_id": self.template_id,
|
|
"code": self.code,
|
|
"price_per_run": self.price_per_run,
|
|
"visibility": self.visibility.value,
|
|
"schedule_cron": self.schedule_cron,
|
|
"tags": self.tags,
|
|
"created_at": self.created_at,
|
|
"run_count": self.run_count,
|
|
"revenue_usd": round(self.revenue_usd, 4),
|
|
"author": self.author,
|
|
}
|
|
|
|
|
|
class ActorMarketplace:
|
|
"""Manage and discover actors."""
|
|
|
|
def __init__(self):
|
|
self.actors: dict[str, Actor] = {}
|
|
self._load()
|
|
|
|
def _load(self) -> None:
|
|
for f in ACTOR_DIR.glob("*.json"):
|
|
try:
|
|
data = json.loads(f.read_text())
|
|
actor = Actor.__new__(Actor)
|
|
actor.__dict__.update(data)
|
|
actor.visibility = ActorVisibility(data.get("visibility", "private"))
|
|
self.actors[actor.actor_id] = actor
|
|
except (json.JSONDecodeError, OSError, ValueError):
|
|
continue
|
|
|
|
def _save(self, actor: Actor) -> None:
|
|
try:
|
|
(ACTOR_DIR / f"{actor.actor_id}.json").write_text(json.dumps(actor.to_dict(), indent=2))
|
|
except OSError as e:
|
|
logger.warning("actor_save_failed", extra={"actor_id": actor.actor_id, "error": str(e)})
|
|
|
|
def create(
|
|
self,
|
|
name: str,
|
|
description: str,
|
|
template_id: str = "",
|
|
code: str = "",
|
|
price_per_run: float = 0.0,
|
|
visibility: ActorVisibility = ActorVisibility.PRIVATE,
|
|
schedule_cron: str = "",
|
|
tags: list[str] | None = None,
|
|
author: str = "",
|
|
) -> Actor:
|
|
actor = Actor(
|
|
uuid.uuid4().hex[:12],
|
|
name,
|
|
description,
|
|
template_id,
|
|
code,
|
|
price_per_run,
|
|
visibility,
|
|
schedule_cron,
|
|
tags,
|
|
)
|
|
actor.author = author
|
|
self.actors[actor.actor_id] = actor
|
|
self._save(actor)
|
|
return actor
|
|
|
|
def list(self, visibility: str = "", tag: str = "") -> list[dict[str, Any]]:
|
|
results = []
|
|
for actor in self.actors.values():
|
|
if visibility and actor.visibility.value != visibility:
|
|
continue
|
|
if tag and tag not in actor.tags:
|
|
continue
|
|
results.append(actor.to_dict())
|
|
return sorted(results, key=lambda x: -x.get("run_count", 0))
|
|
|
|
async def run(self, actor_id: str, inputs: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
actor = self.actors.get(actor_id)
|
|
if not actor:
|
|
return {"success": False, "error": f"Actor not found: {actor_id}"}
|
|
actor.run_count += 1
|
|
if actor.price_per_run > 0:
|
|
actor.revenue_usd += actor.price_per_run
|
|
self._save(actor)
|
|
if actor.template_id:
|
|
from template_engine import execute_template
|
|
|
|
url = (inputs or {}).get("url", "")
|
|
if not url:
|
|
return {"success": False, "error": "Missing 'url' input"}
|
|
result = await execute_template(actor.template_id, url)
|
|
return {"success": True, "actor_id": actor_id, "data": result.get("data", {})}
|
|
return {
|
|
"success": True,
|
|
"actor_id": actor_id,
|
|
"message": "Custom actor code not yet executed",
|
|
}
|