Skip to content

WordPress API

wordpress

WordPress REST API integration for article publishing.

Handles the full publishing lifecycle: markdown-to-Gutenberg conversion, ACF custom field assembly, taxonomy classification, Polylang language tagging, and featured image upload.

Modules: client: Async WordPress REST API client with retry logic. publisher: High-level publish orchestrator. gutenberg: Markdown-to-Gutenberg block converter. acf: Advanced Custom Fields builder. taxonomy: WordPress taxonomy mapper (categories, tags). polylang: Polylang multilingual integration. schema: WordPress post type and field schemas.

WordPressApiError(status_code, body, endpoint)

Bases: Exception

Raised when the WordPress REST API returns an error response.

WordPressClient(base_url, username, app_password)

Async WordPress REST API client with retry on transient errors.

Use as an async context manager::

async with WordPressClient(base_url, username, app_password) as wp:
    post = await wp.create_post("/wp-json/wp/v2/article", payload)

create_post(endpoint, payload) async

Create a new post via POST to endpoint.

update_post(endpoint, post_id, payload) async

Update an existing post via POST to endpoint/post_id.

get_post(endpoint, post_id) async

Fetch a single post by ID.

get_posts_by_slug(endpoint, slug) async

Fetch posts matching the given slug.

upload_media(filename, content, content_type) async

Upload a media file to the WordPress media library.

get_taxonomies_for_type(post_type) async

Discover taxonomies registered for a WordPress post type.

Calls GET /wp-json/wp/v2/taxonomies?type={post_type} and returns a list of taxonomy objects containing slug, name, and rest_base.

get_taxonomy_terms(taxonomy) async

Fetch all terms for a taxonomy (up to 100).

get_user_by_username(username) async

Resolve a WordPress user ID from a username.

Raises :class:WordPressApiError (via _request) on HTTP errors and :class:ValueError when no user matches.

create_taxonomy_term(taxonomy, name, slug) async

Create a new taxonomy term.

close() async

Close the underlying httpx client.

PublishResult(wordpress_post_id, wordpress_url, language, created, taxonomy_assignments=dict()) dataclass

Result of publishing an article to WordPress.

WpArticleAcfPayload

Bases: BaseModel

ACF payload for article CPT (satellite + cornerstone).

WpPostPayload

Bases: BaseModel

Payload for creating or updating a WordPress post via REST API.

WpResearchAcfPayload

Bases: BaseModel

ACF payload for research CPT (research_news).

WpResearchItemAcfRow

Bases: BaseModel

Single research item in the digest ACF repeater.

WpSourceAcfRow

Bases: BaseModel

Single row in the ACF sources repeater.

TaxonomyMapper(client)

Cache-backed mapper from taxonomy slug to WordPress term ID.

Usage::

mapper = TaxonomyMapper(client)
await mapper.warm_cache(post_type="article")
ids = await mapper.resolve_terms("tinnitus_type", ["subjective"])

warm_cache(*, post_type) async

Fetch taxonomies and their terms, building slug→ID maps.

Taxonomies are discovered dynamically from WordPress via GET /wp/v2/taxonomies?type={post_type}.

Raises:

Type Description
ValueError

If post_type is empty.

get_available_terms(*, language=None)

Return {taxonomy_slug: [term_slugs]} from the warmed cache.

When language is provided, only terms matching that language are included. Pass None to return all terms regardless of language.

resolve_terms(taxonomy, slugs) async

Look up term IDs from cache for the given slugs.

Missing slugs are logged as warnings and skipped (non-fatal).

refresh_cache() async

Clear and re-warm the cache, replaying original post types.

build_acf_payload(article, brief, final_content, *, research_dossier=None, digest_output=None, faq_items=None)

Build the CPT-specific ACF metadata payload for a WordPress post.

Returns WpArticleAcfPayload for satellite/cornerstone content types, WpResearchAcfPayload for research_news.

Parameters:

Name Type Description Default
research_dossier ResearchDossier | None

Optional research dossier for building article source rows.

None
digest_output DigestOutput | None

Optional digest output for building research item rows.

None

extract_summary(working_title, final_content)

Build summary from working title and first two sentences of content.

Strips markdown headings (lines starting with #) before extracting sentences. The result is "<title>. <sentence 1>. <sentence 2>."

get_wordpress_client()

Create a :class:WordPressClient from application settings.

convert_markdown_to_gutenberg(markdown)

Convert pipeline markdown to WordPress Gutenberg block markup.

Parses markdown using markdown-it-py, walks the token stream, and emits Gutenberg block comments wrapping the rendered HTML for each block-level element.

Args: markdown: Pipeline markdown string (headings, paragraphs, tables, lists, blockquotes, callouts).

Returns: Gutenberg block markup string ready for the WordPress REST API.

Link two posts as Polylang translation pairs.

Updates the post at endpoint/post_id with {"translations": {counterpart_lang: translation_of_id}}. Polylang Pro exposes a translations REST field on standard WP post endpoints; the save_translations() callback accepts {lang_code: post_id}. Failure is non-fatal: the function logs a warning and returns without raising.

set_post_language(client, endpoint, post_id, language) async

Set the Polylang language on a WordPress post.

Updates the post at endpoint/post_id with {"lang": language} in the payload. The Polylang plugin hooks into the standard WP REST API and reads the lang field from the update body.

publish_article(article, brief, client, taxonomy_mapper, *, author_id=0, featured_image_data=None, translation_counterpart_wp_id=None) async

Publish (or update) an article on WordPress.

Orchestration sequence:

  1. Resolve CPT endpoint from article.content_type
  2. Upload featured image (if available)
  3. Convert final markdown content to Gutenberg blocks
  4. Build post payload and create or update the WordPress post (idempotent)
  5. Assign taxonomy terms via LLM classification
  6. Set ACF custom fields
  7. Set Polylang language
  8. Link translation counterpart (if provided)

Parameters:

Name Type Description Default
article Article

The article entity with final_content populated.

required
brief ArticleBrief

The editorial brief driving metadata and taxonomy choices.

required
client WordPressClient

An active WordPress REST API client.

required
taxonomy_mapper TaxonomyMapper

A warmed :class:TaxonomyMapper for slug-to-ID resolution.

required
author_id int

WordPress user ID to set as post author (0 = use WP default).

0
featured_image_data bytes | None

Optional raw image bytes for the featured image.

None
translation_counterpart_wp_id int | None

Optional WordPress post ID of the translation counterpart to link.

None

Returns:

Type Description
PublishResult

Contains the WordPress post ID, URL, language, and creation flag.

Raises:

Type Description
ValueError

If article.final_content is None.

Client

client

Async WordPress REST API client with retry and structured logging.

Provides :class:WordPressClient — an async context manager wrapping httpx.AsyncClient — for creating, updating, and fetching posts on the tinnitus WordPress site. Retries transient errors (5xx, 429) with exponential backoff; raises :class:WordPressApiError immediately on 4xx.

WordPressApiError(status_code, body, endpoint)

Bases: Exception

Raised when the WordPress REST API returns an error response.

WordPressClient(base_url, username, app_password)

Async WordPress REST API client with retry on transient errors.

Use as an async context manager::

async with WordPressClient(base_url, username, app_password) as wp:
    post = await wp.create_post("/wp-json/wp/v2/article", payload)

create_post(endpoint, payload) async

Create a new post via POST to endpoint.

update_post(endpoint, post_id, payload) async

Update an existing post via POST to endpoint/post_id.

get_post(endpoint, post_id) async

Fetch a single post by ID.

get_posts_by_slug(endpoint, slug) async

Fetch posts matching the given slug.

upload_media(filename, content, content_type) async

Upload a media file to the WordPress media library.

get_taxonomies_for_type(post_type) async

Discover taxonomies registered for a WordPress post type.

Calls GET /wp-json/wp/v2/taxonomies?type={post_type} and returns a list of taxonomy objects containing slug, name, and rest_base.

get_taxonomy_terms(taxonomy) async

Fetch all terms for a taxonomy (up to 100).

get_user_by_username(username) async

Resolve a WordPress user ID from a username.

Raises :class:WordPressApiError (via _request) on HTTP errors and :class:ValueError when no user matches.

create_taxonomy_term(taxonomy, name, slug) async

Create a new taxonomy term.

close() async

Close the underlying httpx client.

get_wordpress_client()

Create a :class:WordPressClient from application settings.

Publisher

publisher

WordPress article publisher -- orchestrates all WordPress operations.

Combines Gutenberg conversion, taxonomy mapping, ACF metadata, Polylang language tagging, and translation linking into a single publish_article entry point.

PublishResult(wordpress_post_id, wordpress_url, language, created, taxonomy_assignments=dict()) dataclass

Result of publishing an article to WordPress.

publish_article(article, brief, client, taxonomy_mapper, *, author_id=0, featured_image_data=None, translation_counterpart_wp_id=None) async

Publish (or update) an article on WordPress.

Orchestration sequence:

  1. Resolve CPT endpoint from article.content_type
  2. Upload featured image (if available)
  3. Convert final markdown content to Gutenberg blocks
  4. Build post payload and create or update the WordPress post (idempotent)
  5. Assign taxonomy terms via LLM classification
  6. Set ACF custom fields
  7. Set Polylang language
  8. Link translation counterpart (if provided)

Parameters:

Name Type Description Default
article Article

The article entity with final_content populated.

required
brief ArticleBrief

The editorial brief driving metadata and taxonomy choices.

required
client WordPressClient

An active WordPress REST API client.

required
taxonomy_mapper TaxonomyMapper

A warmed :class:TaxonomyMapper for slug-to-ID resolution.

required
author_id int

WordPress user ID to set as post author (0 = use WP default).

0
featured_image_data bytes | None

Optional raw image bytes for the featured image.

None
translation_counterpart_wp_id int | None

Optional WordPress post ID of the translation counterpart to link.

None

Returns:

Type Description
PublishResult

Contains the WordPress post ID, URL, language, and creation flag.

Raises:

Type Description
ValueError

If article.final_content is None.

Gutenberg Conversion

gutenberg

Deterministic Markdown-to-Gutenberg block converter.

Converts pipeline markdown (headings, paragraphs, tables, lists, blockquotes, callouts, citations) into WordPress Gutenberg block markup using markdown-it-py for parsing.

This module is pure Python with NO I/O -- no async, no network, no database.

convert_markdown_to_gutenberg(markdown)

Convert pipeline markdown to WordPress Gutenberg block markup.

Parses markdown using markdown-it-py, walks the token stream, and emits Gutenberg block comments wrapping the rendered HTML for each block-level element.

Args: markdown: Pipeline markdown string (headings, paragraphs, tables, lists, blockquotes, callouts).

Returns: Gutenberg block markup string ready for the WordPress REST API.

Taxonomy

taxonomy

Taxonomy mapper: resolves WordPress taxonomy slugs to term IDs.

Provides :class:TaxonomyMapper for cache-backed slug→ID resolution and :func:classify_taxonomy_terms for LLM-based term selection.

TaxonomyMapper(client)

Cache-backed mapper from taxonomy slug to WordPress term ID.

Usage::

mapper = TaxonomyMapper(client)
await mapper.warm_cache(post_type="article")
ids = await mapper.resolve_terms("tinnitus_type", ["subjective"])

warm_cache(*, post_type) async

Fetch taxonomies and their terms, building slug→ID maps.

Taxonomies are discovered dynamically from WordPress via GET /wp/v2/taxonomies?type={post_type}.

Raises:

Type Description
ValueError

If post_type is empty.

get_available_terms(*, language=None)

Return {taxonomy_slug: [term_slugs]} from the warmed cache.

When language is provided, only terms matching that language are included. Pass None to return all terms regardless of language.

resolve_terms(taxonomy, slugs) async

Look up term IDs from cache for the given slugs.

Missing slugs are logged as warnings and skipped (non-fatal).

refresh_cache() async

Clear and re-warm the cache, replaying original post types.

classify_taxonomy_terms(article_title, article_content, content_type, available_terms, *, article_id=None, session=None) async

Use LLM to select the best taxonomy terms for an article.

Returns {taxonomy_slug: [selected_term_slugs]}.