pryscraper/actor_marketplace.py
cryptorugmunch 47ba268131 docs: apply fleet-template (16-artifact scaffold)
Adds missing standard artifacts:
- README.md (if missing)
- AGENTS.md (AI agent contract)
- PLAN.md (current sprint)
- STATUS.md (where we are)
- DEVELOPMENT.md (dev workflow)
- DEPLOYMENT.md (deploy procedure)
- TESTING.md (test strategy)
- DECISIONS.md (ADR index + templates)
- .github/CODEOWNERS
- .github/workflows/ci.yml

Preserves all existing artifacts.

Refs: RugMunchMedia/fleet-template
2026-07-02 02:07:13 +07:00

126 lines
4.7 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."""
import json
import logging
import os
import uuid
from datetime import UTC, datetime
from enum import StrEnum
from pathlib import Path
from typing import Any
logger = logging.getLogger(__name__)
ACTOR_DIR = Path(os.path.expanduser("~/.pry/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"}