from __future__ import annotations from datetime import datetime import re import unicodedata from urllib.parse import urlsplit CATEGORIES = ("وكلاء", "نماذج", "استرجاع", "سلامة", "أدوات", "أعمال") ACCESSES = ("free", "vip") STATUSES = ("pending", "publishing", "published", "blocked", "republish") BLOCKED_REASONS = ("sanitizer", "generation", "article_schema", "cover", "video") SLUG_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") _INVISIBLE = re.compile(r"[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069\ufeff]") _NO_BREAK = re.compile(r"[\u00a0\u202f]") _FIELD_ORDER = ( "status", "access", "category", "queued_at", "slug", "published_at", "url", "blocked_reason", ) _REQUIRED = { "pending": {"status", "access", "category", "queued_at"}, "publishing": {"status", "access", "category", "queued_at", "slug"}, "published": {"status", "access", "category", "queued_at", "slug", "published_at", "url"}, "blocked": {"status", "access", "category", "queued_at", "blocked_reason"}, "republish": {"status", "access", "category", "queued_at", "slug", "published_at", "url"}, } def clean(line: str) -> str: line = _INVISIBLE.sub(" ", line) line = _NO_BREAK.sub(" ", line) return unicodedata.normalize("NFC", line).strip() def _aware_datetime(value: str, field: str) -> datetime: try: parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) except ValueError as exc: raise ValueError(f"{field} must be a valid ISO-8601 datetime") from exc if parsed.tzinfo is None or parsed.utcoffset() is None: raise ValueError(f"{field} must include a timezone offset") return parsed def _validate_url(value: str, slug: str) -> None: parsed = urlsplit(value) expected_path = f"/posts/{slug}" if ( parsed.scheme != "https" or parsed.netloc != "anwarpy.ai" or parsed.path != expected_path or parsed.query or parsed.fragment ): raise ValueError(f"url must equal https://anwarpy.ai{expected_path}") def parse_meta(raw: str) -> dict[str, str]: line = clean(raw) if not line.startswith("anwarpy:"): raise ValueError("marker must start with exact prefix anwarpy:") body = line.removeprefix("anwarpy:").strip() if not body: raise ValueError("marker fields are required") meta: dict[str, str] = {} for part in body.split("|"): segment = clean(part) if "=" not in segment: raise ValueError(f"invalid marker field: {segment}") key, value = (clean(item) for item in segment.split("=", 1)) if key not in _FIELD_ORDER: raise ValueError(f"unknown marker field: {key}") if key in meta: raise ValueError(f"duplicate marker field: {key}") if not value: raise ValueError(f"empty marker field: {key}") meta[key] = value status = meta.get("status") if status not in STATUSES: raise ValueError("status is invalid") required = _REQUIRED[status] missing = required - meta.keys() if missing: raise ValueError(f"missing required fields: {', '.join(sorted(missing))}") forbidden = meta.keys() - required if forbidden: raise ValueError(f"fields not allowed for {status}: {', '.join(sorted(forbidden))}") if meta["access"] not in ACCESSES: raise ValueError("access is invalid") if meta["category"] not in CATEGORIES: raise ValueError("category is invalid") _aware_datetime(meta["queued_at"], "queued_at") slug = meta.get("slug") if slug is not None and not SLUG_RE.fullmatch(slug): raise ValueError("slug must be lowercase ASCII kebab-case") if "published_at" in meta: _aware_datetime(meta["published_at"], "published_at") if "url" in meta: _validate_url(meta["url"], meta["slug"]) if "blocked_reason" in meta and meta["blocked_reason"] not in BLOCKED_REASONS: raise ValueError("blocked_reason is invalid") return meta def render_meta(meta: dict[str, str]) -> str: validated = parse_meta( "anwarpy: " + " | ".join(f"{key}={value}" for key, value in meta.items()) ) return "anwarpy: " + " | ".join( f"{key}={validated[key]}" for key in _FIELD_ORDER if key in validated ) def same_meta(expected_raw: str, actual_raw: str) -> bool: return parse_meta(expected_raw) == parse_meta(actual_raw) def mark_publishing(raw: str, slug: str) -> str: meta = parse_meta(raw) if meta["status"] not in ("pending", "republish"): raise ValueError("only pending or republish markers can be claimed") normalized_slug = clean(slug) if meta["status"] == "republish" and normalized_slug != meta["slug"]: raise ValueError("republish slug cannot change") meta = { "status": "publishing", "access": meta["access"], "category": meta["category"], "queued_at": meta["queued_at"], "slug": normalized_slug, } return render_meta(meta) def mark_published(raw: str, published_at: str, url: str) -> str: meta = parse_meta(raw) if meta["status"] not in ("publishing", "republish"): raise ValueError("only publishing or republish markers can become published") meta["status"] = "published" meta["published_at"] = clean(published_at) meta["url"] = clean(url) return render_meta(meta) def mark_blocked(raw: str, reason: str) -> str: meta = parse_meta(raw) if meta["status"] not in ("pending", "publishing"): raise ValueError("only pending or publishing markers can be blocked") meta = { "status": "blocked", "access": meta["access"], "category": meta["category"], "queued_at": meta["queued_at"], "blocked_reason": clean(reason), } return render_meta(meta)