HR-ATS-Portal/backend/inbox/file_decoder.py

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