63 lines
1.9 KiB
Python
63 lines
1.9 KiB
Python
"""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
|