Files
seobuild-onpage/scripts/lib/dataforseo.py
T
Greg BessoniandClaude Opus 4.6 ae47fcccba seo-agi: The GEO framework skill for Claude Code + OpenClaw
GEO framework that writes pages ranking on Google AND getting cited by
LLMs. 500-token chunk architecture, Reddit Test quality gates,
verification tags, Not For You blocks, information gain enforcement.

Data layer: DataForSEO, GSC, Ahrefs MCP, SEMRush MCP.
21 files, all tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 10:56:28 -04:00

245 lines
7.6 KiB
Python

"""
DataForSEO API client for SEO-AGI.
Handles SERP results, keyword data, People Also Ask, and content parsing.
"""
import json
import base64
import urllib.request
import urllib.error
from typing import Optional
class DataForSEOClient:
"""Client for DataForSEO REST API v3."""
BASE_URL = "https://api.dataforseo.com/v3"
def __init__(self, login: str, password: str):
self.login = login
self.password = password
self._auth_header = self._make_auth_header(login, password)
@staticmethod
def _make_auth_header(login: str, password: str) -> str:
token = base64.b64encode(f"{login}:{password}".encode()).decode()
return f"Basic {token}"
def _request(self, endpoint: str, payload: list[dict]) -> dict:
"""Make a POST request to DataForSEO API."""
url = f"{self.BASE_URL}{endpoint}"
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
url,
data=data,
headers={
"Authorization": self._auth_header,
"Content-Type": "application/json",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
body = e.read().decode() if e.fp else ""
raise RuntimeError(
f"DataForSEO API error {e.code}: {body}"
) from e
except urllib.error.URLError as e:
raise RuntimeError(
f"DataForSEO connection error: {e.reason}"
) from e
def serp_live(
self,
keyword: str,
location_code: int = 2840,
language_code: str = "en",
depth: int = 10,
) -> dict:
"""
Get live SERP results for a keyword.
Returns organic results with position, URL, title, description.
"""
payload = [
{
"keyword": keyword,
"location_code": location_code,
"language_code": language_code,
"depth": depth,
"se_type": "organic",
}
]
result = self._request(
"/serp/google/organic/live/advanced", payload
)
return self._extract_serp(result)
def related_keywords(
self,
keyword: str,
location_code: int = 2840,
language_code: str = "en",
limit: int = 30,
) -> list[dict]:
"""Get related keywords with search volume and difficulty."""
payload = [
{
"keyword": keyword,
"location_code": location_code,
"language_code": language_code,
"limit": limit,
}
]
result = self._request(
"/dataforseo_labs/google/related_keywords/live", payload
)
return self._extract_keywords(result)
def keyword_suggestions(
self,
keyword: str,
location_code: int = 2840,
language_code: str = "en",
limit: int = 30,
) -> list[dict]:
"""Get keyword suggestions (broader ideation)."""
payload = [
{
"keyword": keyword,
"location_code": location_code,
"language_code": language_code,
"limit": limit,
}
]
result = self._request(
"/dataforseo_labs/google/keyword_suggestions/live", payload
)
return self._extract_keywords(result)
def content_parse(self, url: str) -> Optional[dict]:
"""Parse content from a URL (headings, word count, structure)."""
payload = [{"url": url}]
try:
result = self._request(
"/on_page/content_parsing/live", payload
)
return self._extract_content(result)
except RuntimeError:
return None
def _extract_serp(self, raw: dict) -> dict:
"""Extract clean SERP data from API response."""
tasks = raw.get("tasks", [])
if not tasks:
return {"organic": [], "paa": [], "featured_snippet": None}
result = tasks[0].get("result", [])
if not result:
return {"organic": [], "paa": [], "featured_snippet": None}
items = result[0].get("items", [])
organic = []
paa_questions = []
featured_snippet = None
for item in items:
item_type = item.get("type", "")
if item_type == "organic":
organic.append(
{
"position": item.get("rank_absolute", 0),
"url": item.get("url", ""),
"domain": item.get("domain", ""),
"title": item.get("title", ""),
"description": item.get("description", ""),
}
)
elif item_type == "people_also_ask":
for paa_item in item.get("items", []):
q = paa_item.get("title", "")
if q:
paa_questions.append(q)
elif item_type == "featured_snippet":
featured_snippet = {
"url": item.get("url", ""),
"title": item.get("title", ""),
"description": item.get("description", ""),
}
return {
"organic": organic,
"paa": paa_questions,
"featured_snippet": featured_snippet,
"total_results": result[0].get("se_results_count", 0),
}
def _extract_keywords(self, raw: dict) -> list[dict]:
"""Extract keyword data from labs API response."""
tasks = raw.get("tasks", [])
if not tasks:
return []
result = tasks[0].get("result", [])
if not result:
return []
items = result[0].get("items", [])
keywords = []
for item in items:
kw_data = item.get("keyword_data", item)
keyword_info = kw_data.get("keyword_info", {})
keywords.append(
{
"keyword": kw_data.get("keyword", ""),
"volume": keyword_info.get("search_volume", 0),
"cpc": keyword_info.get("cpc", 0),
"competition": keyword_info.get("competition", 0),
"difficulty": kw_data.get(
"keyword_properties", {}
).get("keyword_difficulty", 0),
}
)
return sorted(keywords, key=lambda x: x["volume"], reverse=True)
def _extract_content(self, raw: dict) -> Optional[dict]:
"""Extract content structure from on-page parsing."""
tasks = raw.get("tasks", [])
if not tasks:
return None
result = tasks[0].get("result", [])
if not result:
return None
items = result[0].get("items", [])
if not items:
return None
page = items[0].get("page_content", {})
return {
"title": page.get("header", {}).get("title", ""),
"word_count": page.get("plain_text_word_count", 0),
"headings": self._extract_headings(page),
"plain_text_size": page.get("plain_text_size", 0),
}
@staticmethod
def _extract_headings(page_content: dict) -> list[str]:
"""Pull heading tags from parsed content."""
headings = []
for level in ["h1", "h2", "h3"]:
for heading in page_content.get(level, []):
headings.append(f"{level.upper()}: {heading}")
return headings