"""Common utilities for the reference curator pipeline.""" from __future__ import annotations import hashlib import logging import re import unicodedata from urllib.parse import urlparse, urlunparse, parse_qs, urlencode def normalize_url(url: str) -> str: """Normalize a URL for deduplication. - Lowercase scheme and host - Remove trailing slashes - Sort query parameters - Remove common tracking params (utm_*, ref, fbclid) - Remove fragment """ parsed = urlparse(url) scheme = parsed.scheme.lower() netloc = parsed.netloc.lower() path = parsed.path.rstrip("/") or "/" # Sort query params, removing tracking params tracking_params = {"utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term", "ref", "fbclid", "gclid", "mc_cid", "mc_eid"} params = parse_qs(parsed.query, keep_blank_values=True) filtered = {k: v for k, v in sorted(params.items()) if k not in tracking_params} query = urlencode(filtered, doseq=True) return urlunparse((scheme, netloc, path, "", query, "")) def url_hash(url: str) -> str: """SHA-256 hash of normalized URL. Matches the url_hash column in schema.sql.""" return hashlib.sha256(normalize_url(url).encode()).hexdigest() def slugify(text: str) -> str: """Convert text to a URL/folder-friendly slug. >>> slugify("Prompt Engineering Best Practices") 'prompt-engineering-best-practices' """ text = unicodedata.normalize("NFKD", text) text = text.encode("ascii", "ignore").decode() text = text.lower() text = re.sub(r"[^a-z0-9]+", "-", text) text = text.strip("-") return text or "untitled" def count_tokens(text: str) -> int: """Approximate token count using chars/4 heuristic. Good enough for compression ratio calculations without requiring tiktoken. """ return max(1, len(text) // 4) def setup_logging(level: str = "INFO", run_id: int | None = None) -> logging.Logger: """Configure and return a logger for the reference curator pipeline.""" logger = logging.getLogger("refcurator") if not logger.handlers: handler = logging.StreamHandler() fmt = "[refcurator]" if run_id: fmt += f" [run:{run_id}]" fmt += " %(levelname)s: %(message)s" handler.setFormatter(logging.Formatter(fmt)) logger.addHandler(handler) logger.setLevel(getattr(logging, level.upper(), logging.INFO)) return logger