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_translation(client, endpoint, post_id, translation_of_id, counterpart_lang)
async
¶
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:
- Resolve CPT endpoint from
article.content_type - Upload featured image (if available)
- Convert final markdown content to Gutenberg blocks
- Build post payload and create or update the WordPress post (idempotent)
- Assign taxonomy terms via LLM classification
- Set ACF custom fields
- Set Polylang language
- Link translation counterpart (if provided)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
article
|
Article
|
The article entity with |
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: |
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 |
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:
- Resolve CPT endpoint from
article.content_type - Upload featured image (if available)
- Convert final markdown content to Gutenberg blocks
- Build post payload and create or update the WordPress post (idempotent)
- Assign taxonomy terms via LLM classification
- Set ACF custom fields
- Set Polylang language
- Link translation counterpart (if provided)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
article
|
Article
|
The article entity with |
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: |
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 |
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]}.