"""Decode Graph fileAttachment contentBytes — PDF only, in memory (no disk). Email / Manual CV flows upload bytes to S3 after the DB row exists. Nothing writes under inbox/decoded_attachments anymore. """ from __future__ import annotations import base64 import binascii from pathlib import Path from typing import Any class AttachmentDecodeError(ValueError): """Raised when contentBytes is malformed or is not a PDF.""" def _decode_bytes(attachment: dict) -> bytes: """base64 -> raw bytes.""" b64=attachment.get("contentBytes") if not b64: raise AttachmentDecodeError(f"{attachment.get('name')!r}: no contentBytes") try: return base64.b64decode(b64,validate=True) except binascii.Error as exc: raise AttachmentDecodeError( f"{attachment.get('name')!r}: bad base64: {exc}" ) from exc def _normalize_attachments(attachments: Any) -> list[dict]: if attachments is None: return [] if isinstance(attachments,dict): return [attachments] if isinstance(attachments,list): return [a for a in attachments if isinstance(a,dict)] return [] def extract_pdf_attachments(attachments: Any) -> list[dict]: """Return ``[{name, body}]`` for PDF Graph attachments — no disk writes. Non-PDF / empty / reference attachments are skipped. PDF gate is extension + ``%PDF-`` header (same bar as assert_pdf / Manual create). """ out: list[dict]=[] for attachment in _normalize_attachments(attachments): if not attachment.get("contentBytes"): continue name=Path(attachment.get("name") or "resume.pdf").name or "resume.pdf" if not name.lower().endswith(".pdf"): continue try: raw=_decode_bytes(attachment) except AttachmentDecodeError: continue if not raw.startswith(b"%PDF-"): continue out.append({"name":name,"body":raw}) return out