suppfactsdaily / wordpress.py
RidhiD.
Fall back to WP slug lookup to prevent duplicate posts after container restart
e062e85
"""
WordPress API interactions for posting content.
"""
import requests
from logger import logger
from config import WP_URL, WP_USER, WP_APP_PASS, DEFAULT_POST_STATUS
class WordPressClient:
"""Handles WordPress API interactions."""
def __init__(self, wp_url=WP_URL, username=WP_USER, app_password=WP_APP_PASS):
"""Initialize WordPress client."""
if not all([wp_url, username, app_password]):
raise ValueError("WordPress credentials not set in environment")
self.wp_url = wp_url.rstrip("/")
self.auth = (username, app_password)
logger.debug("WordPressClient initialized")
def create_or_get_tags(self, tag_names):
"""
Create tags in WordPress or get existing ones.
Workflow:
1. Search for existing tag by name
2. If found, use that tag ID
3. If not found, create new tag
4. Log failures only when tag creation actually fails
Args:
tag_names (list): List of tag names
Returns:
list: List of tag IDs
Raises:
Exception: If API call fails
"""
try:
logger.info(f"Processing {len(tag_names)} tags")
tag_ids = []
for tag_name in tag_names:
try:
# Step 1: Search for existing tag
logger.debug(f"Searching for tag '{tag_name}'")
search_response = requests.get(
f"{self.wp_url}/wp-json/wp/v2/tags",
auth=self.auth,
params={"search": tag_name, "per_page": 100},
timeout=10,
)
search_response.raise_for_status()
search_results = search_response.json()
existing_tag = None
# Look for exact or close match
for tag in search_results:
if tag.get("name", "").lower() == tag_name.lower():
existing_tag = tag
break
if existing_tag:
tag_id = existing_tag.get("id")
tag_ids.append(tag_id)
logger.info(f"Tag '{tag_name}' found (ID {tag_id})")
continue
# Step 2: Tag not found, create it
logger.debug(f"Tag '{tag_name}' not found, creating new tag")
create_response = requests.post(
f"{self.wp_url}/wp-json/wp/v2/tags",
auth=self.auth,
json={"name": tag_name},
timeout=10,
)
create_response.raise_for_status()
created_tag = create_response.json()
tag_id = created_tag.get("id")
if tag_id:
tag_ids.append(tag_id)
logger.info(f"Tag '{tag_name}' created (ID {tag_id})")
else:
logger.warning(f"No ID returned for created tag '{tag_name}'")
except requests.RequestException as e:
logger.error(f"Tag creation failed for '{tag_name}': {e}")
logger.info(f"Processed {len(tag_ids)} tags successfully")
return tag_ids
except Exception as e:
logger.error(f"Failed to process tags: {e}")
raise
def create_or_get_categories(self, category_names):
"""
Create categories in WordPress or get existing ones.
Args:
category_names (list): List of category names
Returns:
list: List of category IDs
"""
try:
logger.info(f"Processing {len(category_names)} categories")
category_ids = []
for cat_name in category_names:
try:
search_response = requests.get(
f"{self.wp_url}/wp-json/wp/v2/categories",
auth=self.auth,
params={"search": cat_name, "per_page": 100},
timeout=10,
)
search_response.raise_for_status()
search_results = search_response.json()
existing_cat = None
for cat in search_results:
if cat.get("name", "").lower() == cat_name.lower():
existing_cat = cat
break
if existing_cat:
cat_id = existing_cat.get("id")
category_ids.append(cat_id)
logger.info(f"Category '{cat_name}' found (ID {cat_id})")
continue
create_response = requests.post(
f"{self.wp_url}/wp-json/wp/v2/categories",
auth=self.auth,
json={"name": cat_name},
timeout=10,
)
create_response.raise_for_status()
created_cat = create_response.json()
cat_id = created_cat.get("id")
if cat_id:
category_ids.append(cat_id)
logger.info(f"Category '{cat_name}' created (ID {cat_id})")
else:
logger.warning(f"No ID returned for created category '{cat_name}'")
except requests.RequestException as e:
logger.error(f"Category creation failed for '{cat_name}': {e}")
logger.info(f"Processed {len(category_ids)} categories successfully")
return category_ids
except Exception as e:
logger.error(f"Failed to process categories: {e}")
raise
def upload_media(self, image_data, filename, content_type="image/avif"):
"""
Upload image to WordPress media library.
Args:
image_data (bytes): Image file data
filename (str): Filename for the image
content_type (str): MIME type
Returns:
dict: Contains 'id' and 'url' keys
Raises:
Exception: If upload fails
"""
try:
logger.info(f"Uploading media: {filename}")
response = requests.post(
f"{self.wp_url}/wp-json/wp/v2/media",
auth=self.auth,
headers={
"Content-Disposition": f'attachment; filename="{filename}"',
"Content-Type": content_type,
},
data=image_data,
timeout=30,
)
response.raise_for_status()
data = response.json()
media_id = data.get("id")
media_url = data.get("source_url")
if not media_id or not media_url:
raise ValueError("Invalid media response structure")
logger.info(f"Media uploaded: ID {media_id}")
return {"id": media_id, "url": media_url}
except requests.RequestException as e:
logger.error(f"Failed to upload media: {e}")
raise
def get_post(self, post_id):
"""Retrieve an existing WordPress post by ID."""
try:
response = requests.get(
f"{self.wp_url}/wp-json/wp/v2/posts/{post_id}",
auth=self.auth,
timeout=15,
)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
logger.error(f"Failed to fetch post {post_id}: {e}")
raise
def get_post_id_by_slug(self, slug):
"""Return the post ID for a given slug, or None if not found."""
try:
response = requests.get(
f"{self.wp_url}/wp-json/wp/v2/posts",
auth=self.auth,
params={"slug": slug, "status": "any", "per_page": 1},
timeout=15,
)
response.raise_for_status()
posts = response.json()
if posts:
post_id = posts[0].get("id")
logger.info(f"Found existing WP post by slug '{slug}': ID {post_id}")
return post_id
except requests.RequestException as e:
logger.warning(f"Slug lookup failed for '{slug}': {e}")
return None
def get_media(self, media_id):
"""Retrieve an existing WordPress media item by ID."""
try:
response = requests.get(
f"{self.wp_url}/wp-json/wp/v2/media/{media_id}",
auth=self.auth,
timeout=15,
)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
logger.error(f"Failed to fetch media {media_id}: {e}")
raise
def update_post(self, post_id, title, content, tag_ids, media_id, slug=None, category_ids=None, seo_data=None):
"""
Update an existing WordPress post.
Args:
post_id (int): ID of the existing post
title (str): Post title
content (str): Post content (HTML)
tag_ids (list): List of tag IDs
media_id (int): Featured image media ID
slug (str, optional): URL slug
category_ids (list, optional): List of category IDs
seo_data (dict, optional): SEO metadata for Rank Math
Returns:
dict: Post data including 'id' and 'edit_url'
Raises:
Exception: If post update fails
"""
try:
logger.info(f"Updating existing post: ID {post_id}")
payload = {
"title": title,
"content": content,
"tags": tag_ids,
"featured_media": media_id,
}
if slug:
payload["slug"] = slug
if category_ids:
payload["categories"] = category_ids
if seo_data:
payload["meta"] = {
"rank_math_focus_keyword": seo_data.get("focus_keyword", ""),
"rank_math_description": seo_data.get("meta_description", ""),
}
logger.debug(f"Sending update payload for post {post_id}: slug={payload.get('slug')}, "
f"categories={payload.get('categories')}, meta={payload.get('meta')}")
response = requests.post(
f"{self.wp_url}/wp-json/wp/v2/posts/{post_id}",
auth=self.auth,
json=payload,
timeout=30,
)
response.raise_for_status()
post_data = response.json()
edit_url = f"{self.wp_url}/wp-admin/post.php?post={post_id}&action=edit"
returned_meta = post_data.get("meta", {})
logger.debug(f"WP returned meta for post {post_id}: {returned_meta}")
if seo_data and not returned_meta.get("rank_math_focus_keyword"):
logger.warning(
"rank_math_focus_keyword not found in WP response meta — "
"ensure Rank Math is active and has REST API support enabled"
)
logger.info(f"Post updated: ID {post_id}")
return {
"id": post_id,
"title": post_data.get("title", {}).get("rendered", title),
"edit_url": edit_url,
}
except requests.RequestException as e:
logger.error(f"Failed to update post {post_id}: {e}")
raise
def create_draft_post(self, title, content, tag_ids, media_id, slug=None, category_ids=None, seo_data=None):
"""
Create a draft post in WordPress.
Args:
title (str): Post title
content (str): Post content (HTML)
tag_ids (list): List of tag IDs
media_id (int): Featured image media ID
slug (str, optional): URL slug
category_ids (list, optional): List of category IDs
seo_data (dict, optional): SEO metadata for Rank Math
Returns:
dict: Post data including 'id' and 'edit_url'
Raises:
Exception: If post creation fails
"""
try:
logger.info(f"Creating draft post: {title}")
payload = {
"title": title,
"content": content,
"status": DEFAULT_POST_STATUS,
"tags": tag_ids,
"featured_media": media_id,
}
if slug:
payload["slug"] = slug
if category_ids:
payload["categories"] = category_ids
if seo_data:
payload["meta"] = {
"rank_math_focus_keyword": seo_data.get("focus_keyword", ""),
"rank_math_description": seo_data.get("meta_description", ""),
}
logger.debug(f"Sending create payload: slug={payload.get('slug')}, "
f"categories={payload.get('categories')}, meta={payload.get('meta')}")
response = requests.post(
f"{self.wp_url}/wp-json/wp/v2/posts",
auth=self.auth,
json=payload,
timeout=30,
)
response.raise_for_status()
post_data = response.json()
post_id = post_data.get("id")
edit_url = f"{self.wp_url}/wp-admin/post.php?post={post_id}&action=edit"
returned_meta = post_data.get("meta", {})
logger.debug(f"WP returned meta for new post {post_id}: {returned_meta}")
if seo_data and not returned_meta.get("rank_math_focus_keyword"):
logger.warning(
"rank_math_focus_keyword not found in WP response meta — "
"ensure Rank Math is active and has REST API support enabled"
)
logger.info(f"Draft post created: ID {post_id}")
return {
"id": post_id,
"title": post_data.get("title", {}).get("rendered", title),
"edit_url": edit_url,
}
except requests.RequestException as e:
logger.error(f"Failed to create draft post: {e}")
raise