import httpx import asyncio import sys import json from typing import Dict, Any, Optional from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type class PorkbunAPI: BASE_URL = "https://api.porkbun.com/api/json/v3" def __init__(self, api_key: str, secret_key: str, debug: bool = False): self.api_key = api_key self.secret_key = secret_key self.debug = debug self.client = httpx.AsyncClient(timeout=30.0) async def _make_request(self, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: payload = { "apikey": self.api_key, "secretapikey": self.secret_key } if data: payload.update(data) url = f"{self.BASE_URL}/{endpoint}" if self.debug: print("=" * 80, file=sys.stderr) print(f"DEBUG: REQUEST", file=sys.stderr) print(f"URL: POST {url}", file=sys.stderr) print(f"Headers: {dict(self.client.headers)}", file=sys.stderr) print(f"Payload: {json.dumps(payload, indent=2)}", file=sys.stderr) print("=" * 80, file=sys.stderr) while True: response = await self.client.post(url, json=payload) if self.debug: print("=" * 80, file=sys.stderr) print(f"DEBUG: RESPONSE", file=sys.stderr) print(f"Status: {response.status_code} {response.reason_phrase}", file=sys.stderr) print(f"Headers: {dict(response.headers)}", file=sys.stderr) print(f"Body: {response.text[:1000]}", file=sys.stderr) if len(response.text) > 1000: print(f"... (truncated, total length: {len(response.text)})", file=sys.stderr) print("=" * 80, file=sys.stderr) if response.status_code == 429: retry_after = int(response.headers.get("Retry-After", 1)) if self.debug: print(f"DEBUG: Rate limited, waiting {retry_after}s", file=sys.stderr) await asyncio.sleep(retry_after) continue response.raise_for_status() result = response.json() if result.get("status") == "ERROR": raise Exception(f"API Error: {result.get('message', 'Unknown error')}") return result async def ping(self) -> Dict[str, Any]: return await self._make_request("ping") async def get_pricing(self) -> Dict[str, Any]: return await self._make_request("pricing/get") async def check_domain_availability(self, domain: str) -> Dict[str, Any]: return await self._make_request(f"domain/checkDomain/{domain}") async def get_domain_pricing(self, domain: str) -> Dict[str, Any]: pricing_data = await self.get_pricing() if "pricing" not in pricing_data: raise Exception("Invalid pricing response from API") parts = domain.split('.') if len(parts) < 2: raise ValueError(f"Invalid domain format: {domain}") tld = '.'.join(parts[1:]) result = { "domain": domain, "tld": tld } # Get pricing info if tld in pricing_data["pricing"]: result["pricing"] = pricing_data["pricing"][tld] else: result["error"] = "TLD not found in pricing data" return result # Check domain availability try: availability = await self.check_domain_availability(domain) if availability.get("status") == "SUCCESS": response_data = availability.get("response", {}) result["available"] = response_data.get("avail") == "yes" if response_data.get("premium") == "yes": result["premium"] = True else: result["available"] = None except Exception as e: result["available"] = None return result async def close(self): await self.client.aclose()