Add Stage 2.8 recall, quality gate, retries, and publish idempotency
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime, timezone
|
||||
from pathlib import Path
|
||||
@@ -20,6 +21,9 @@ class PublishResult:
|
||||
|
||||
|
||||
class BlogClient(Protocol):
|
||||
def get_post_by_slug(self, slug: str) -> dict[str, Any] | None:
|
||||
...
|
||||
|
||||
def create_post(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
...
|
||||
|
||||
@@ -153,6 +157,18 @@ def dry_run_publish(slug: str, base_url: str) -> PublishResult:
|
||||
)
|
||||
|
||||
|
||||
def _content_hash(value: str) -> str:
|
||||
return hashlib.sha256((value or "").encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _get_existing_post(client: BlogClient, slug: str) -> dict[str, Any] | None:
|
||||
getter = getattr(client, "get_post_by_slug", None)
|
||||
if getter is None:
|
||||
return None
|
||||
existing = getter(slug)
|
||||
return existing if isinstance(existing, dict) else None
|
||||
|
||||
|
||||
def publish_markdown(
|
||||
*,
|
||||
title: str,
|
||||
@@ -163,6 +179,7 @@ def publish_markdown(
|
||||
mode: str,
|
||||
markdown_report: dict[str, Any],
|
||||
client: BlogClient | None,
|
||||
idempotency_config: dict[str, Any] | None = None,
|
||||
) -> PublishResult:
|
||||
blocking_errors = markdown_report.get("blocking_errors", []) or []
|
||||
blog_url = f"{base_url.rstrip('/')}/posts/{slug}"
|
||||
@@ -187,6 +204,39 @@ def publish_markdown(
|
||||
error="missing_blog_client",
|
||||
)
|
||||
|
||||
idempotency_config = idempotency_config or {}
|
||||
if bool(idempotency_config.get("enabled", False)):
|
||||
try:
|
||||
existing_post = _get_existing_post(client, slug)
|
||||
except Exception as exc:
|
||||
return PublishResult(
|
||||
mode=mode,
|
||||
status="failed",
|
||||
slug=slug,
|
||||
blog_url=blog_url,
|
||||
public_ok=False,
|
||||
error=f"idempotency_check_failed:{type(exc).__name__}: {exc}",
|
||||
)
|
||||
if existing_post is not None:
|
||||
existing_content = str(existing_post.get("content") or existing_post.get("markdown") or "")
|
||||
if _content_hash(existing_content) == _content_hash(markdown):
|
||||
return PublishResult(
|
||||
mode=mode,
|
||||
status="already_published",
|
||||
slug=slug,
|
||||
blog_url=blog_url,
|
||||
public_ok=True,
|
||||
)
|
||||
if not bool(idempotency_config.get("allow_republish", False)):
|
||||
return PublishResult(
|
||||
mode=mode,
|
||||
status="blocked",
|
||||
slug=slug,
|
||||
blog_url=blog_url,
|
||||
public_ok=False,
|
||||
error="slug_already_exists",
|
||||
)
|
||||
|
||||
payload = {"title": title, "content": markdown, "tags": tags, "slug": slug}
|
||||
try:
|
||||
create_resp = client.create_post(payload)
|
||||
|
||||
Reference in New Issue
Block a user