133 lines
4.3 KiB
Python
133 lines
4.3 KiB
Python
"""Inbox helpers — attachment loading, resume text extraction, read-status sync."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import os
|
|
from pathlib import Path
|
|
from urllib.parse import quote
|
|
|
|
import httpx
|
|
from dotenv import load_dotenv
|
|
|
|
from inbox.models import Inbox_Messages
|
|
from job.candidate.views import FileRead
|
|
|
|
load_dotenv()
|
|
|
|
EMAIL_URL=os.getenv("EMAIL_URL")
|
|
EMAIL_API_TOKEN=os.getenv("EMAIL_API_TOKEN")
|
|
|
|
_ATTACHMENTS_DIR=Path(__file__).resolve().parent/"decoded_attachments"
|
|
|
|
|
|
async def fetch_read_status_delta(folder, since=None, limit=1000, max_pages=10, token=None):
|
|
"""GET /sync/read-status -> the raw round dict."""
|
|
if not EMAIL_URL:
|
|
raise RuntimeError("EMAIL_URL must be set")
|
|
auth_token=token or EMAIL_API_TOKEN
|
|
if not auth_token:
|
|
raise RuntimeError("EMAIL_API_TOKEN must be set")
|
|
params={"folder":folder,"limit":limit,"max_pages":max_pages}
|
|
if since:
|
|
params["since"]=since
|
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
response=await client.get(
|
|
f"{EMAIL_URL.rstrip('/')}/sync/read-status",
|
|
params=params,
|
|
headers={"Authorization":f"Bearer {auth_token}"},
|
|
)
|
|
if response.status_code>=400:
|
|
raise httpx.HTTPStatusError(
|
|
response.text,
|
|
request=response.request,
|
|
response=response,
|
|
)
|
|
return response.json()
|
|
|
|
|
|
async def fetch_message_read_status(message_id, token=None):
|
|
"""GET /sync/read-status/message/{id} -> record dict, or None on 404."""
|
|
if not EMAIL_URL:
|
|
raise RuntimeError("EMAIL_URL must be set")
|
|
auth_token=token or EMAIL_API_TOKEN
|
|
if not auth_token:
|
|
raise RuntimeError("EMAIL_API_TOKEN must be set")
|
|
encoded_id=quote(str(message_id),safe="")
|
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
response=await client.get(
|
|
f"{EMAIL_URL.rstrip('/')}/sync/read-status/message/{encoded_id}",
|
|
headers={"Authorization":f"Bearer {auth_token}"},
|
|
)
|
|
if response.status_code==404:
|
|
return None
|
|
if response.status_code>=400:
|
|
raise httpx.HTTPStatusError(
|
|
response.text,
|
|
request=response.request,
|
|
response=response,
|
|
)
|
|
return response.json()
|
|
|
|
|
|
def resolve_attachment_path(path_str:str) -> Path:
|
|
"""Prefer stored path; fall back to basename under decoded_attachments.
|
|
|
|
Stored paths may be Windows absolutes written by the host API. The Taskiq
|
|
worker runs in Linux, where ``Path(r"D:\\...\\file.pdf").name`` is the
|
|
whole string (backslash is not a separator), so normalize separators before
|
|
taking the basename for the mounted attachments dir.
|
|
"""
|
|
raw=path_str.strip()
|
|
path=Path(raw)
|
|
if path.is_file():
|
|
return path
|
|
basename=Path(raw.replace("\\","/")).name
|
|
fallback=_ATTACHMENTS_DIR/basename
|
|
if fallback.is_file():
|
|
return fallback
|
|
return path
|
|
|
|
|
|
def load_message_files(message:Inbox_Messages) -> list[dict]:
|
|
if not message.file_path:
|
|
return []
|
|
files=[]
|
|
for path_str in message.file_path.split(","):
|
|
path=resolve_attachment_path(path_str)
|
|
if not path.is_file():
|
|
continue
|
|
try:
|
|
raw=path.read_bytes()
|
|
except OSError:
|
|
continue
|
|
files.append({
|
|
"file_name":path.name,
|
|
"content_base64":base64.b64encode(raw).decode("ascii"),
|
|
"size":len(raw),
|
|
})
|
|
return files
|
|
|
|
|
|
async def extract_resume_text(file_paths:list[str]) -> tuple[str,str]:
|
|
candidates=[resolve_attachment_path(p) for p in (file_paths or []) if p and p.strip()]
|
|
existing=[p for p in candidates if p.is_file() and p.suffix.lower()==".pdf"]
|
|
if not existing:
|
|
return "","no PDF attachment to extract (.doc/.docx not supported)"
|
|
|
|
texts=[]
|
|
errors=[]
|
|
for path in existing:
|
|
try:
|
|
raw=path.read_bytes()
|
|
result=await FileRead(session=None,filename=path.name,file=raw).read_file()
|
|
text=(result.get("text") or "").strip()
|
|
if text:
|
|
texts.append(text)
|
|
except Exception as exc:
|
|
errors.append(f"{path.name}: {exc}")
|
|
|
|
if not texts:
|
|
return "","; ".join(errors) if errors else "no text extracted from PDF"
|
|
return "\n\n---\n\n".join(texts),""
|