# vyrable.py — single-file Python SDK for the Vyrable REST API. # Version 1.0.0. # # Requires: requests (`pip install requests`). # Usage: # from vyrable import VyrableClient # vy = VyrableClient(api_key=os.environ["VYRABLE_API_KEY"]) # personas = vy.list_personas() from __future__ import annotations import os from typing import Any, Iterable, Optional, Union try: import requests except ImportError as e: raise ImportError( "vyrable.py requires the 'requests' package. Run: pip install requests" ) from e class VyrableApiError(Exception): """Raised when the Vyrable API returns a non-2xx response.""" def __init__(self, status: int, code: str, message: str): super().__init__(f"[{status} {code}] {message}") self.status = status self.code = code self.message = message class VyrableClient: """Single-file client for https://vyrable.ai/api/v1/*.""" def __init__( self, api_key: Optional[str] = None, base_url: str = "https://vyrable.ai", timeout: float = 30.0, ): self.api_key = api_key or os.environ.get("VYRABLE_API_KEY", "") if not self.api_key: raise ValueError("VyrableClient: api_key is required") self.base_url = base_url.rstrip("/") self.timeout = timeout # ── Content ───────────────────────────────────────────────────── def list_content( self, *, status: Optional[str] = None, limit: int = 20, offset: int = 0, ) -> dict: return self._get( "/api/v1/content", params=_compact({"status": status, "limit": limit, "offset": offset}), ) def get_content(self, content_id: str) -> dict: return self._get(f"/api/v1/content/{content_id}") def search_content(self, q: str, *, limit: int = 10) -> dict: return self._get("/api/v1/content/search", params={"q": q, "limit": limit}) def generate_content( self, *, persona_id: str, topic: str, content_type: str = "POST", brief: Optional[str] = None, ) -> dict: return self._post( "/api/v1/content/generate", json=_compact( { "personaId": persona_id, "topic": topic, "contentType": content_type, "brief": brief, } ), ) def publish_content(self, content_id: str) -> dict: return self._post(f"/api/v1/content/{content_id}/publish") def schedule_content(self, content_id: str, scheduled_for: str) -> dict: return self._post( f"/api/v1/content/{content_id}/schedule", json={"scheduledFor": scheduled_for}, ) # ── Personas ──────────────────────────────────────────────────── def list_personas(self) -> dict: return self._get("/api/v1/personas") # ── Ideas ─────────────────────────────────────────────────────── def list_ideas( self, *, status: Optional[str] = None, priority: Optional[str] = None, persona_id: Optional[str] = None, limit: int = 25, offset: int = 0, ) -> dict: return self._get( "/api/v1/ideas", params=_compact( { "status": status, "priority": priority, "personaId": persona_id, "limit": limit, "offset": offset, } ), ) def capture_idea( self, *, title: str, notes: Optional[str] = None, source_url: Optional[str] = None, source_name: Optional[str] = None, persona_id: Optional[str] = None, tags: Optional[Iterable[str]] = None, ) -> dict: return self._post( "/api/v1/ideas", json=_compact( { "title": title, "notes": notes, "sourceUrl": source_url, "sourceName": source_name, "personaId": persona_id, "tags": list(tags) if tags else None, } ), ) # ── Campaigns ─────────────────────────────────────────────────── def list_campaigns( self, *, status: Optional[str] = None, limit: int = 20, ) -> dict: return self._get( "/api/v1/campaigns", params=_compact({"status": status, "limit": limit}), ) # ── Internal ──────────────────────────────────────────────────── def _get(self, path: str, params: Optional[dict] = None) -> dict: return self._call("GET", path, params=params, json=None) def _post(self, path: str, json: Optional[dict] = None) -> dict: return self._call("POST", path, params=None, json=json) def _call( self, method: str, path: str, *, params: Optional[dict], json: Optional[dict], ) -> dict: headers = {"Authorization": f"Bearer {self.api_key}"} res = requests.request( method, f"{self.base_url}{path}", params=params, json=json, headers=headers, timeout=self.timeout, ) if not res.ok: try: body = res.json() except Exception: body = {} raise VyrableApiError( res.status_code, str(body.get("error", "HTTPError")), str(body.get("message", res.text or res.reason)), ) return res.json() if res.text else {} def _compact(d: dict) -> dict: """Drop None / empty-string values so we don't send useless query params.""" return {k: v for k, v in d.items() if v is not None and v != ""}