uofa-demo / space /leadcapture.py
cloudronin's picture
push build context (uofa source + packs + space app)
a28ec65 verified
Raw
History Blame Contribute Delete
4.24 kB
"""Lead capture - append a minimal contact record to a private HF Dataset.
We store ONLY {email, timestamp, pack, x_of_n, weakener_count} - never any
evidence content. Write-failure policy (no silent loss, no punishing the user):
1. Try the HF Dataset upload with a couple of retries + backoff.
2. On exhaustion, DO NOT block the unlock - the user already did their part;
`accepted` stays True so the deeper write-up unlocks anyway.
3. To avoid silent loss, the lead is written to a fallback sink - a JSONL file
if a path is writable, else a structured log line (a lead email is not
evidence/payload, so this is consistent with the no-payload-logging rule).
Config via env (set as Space secrets at deploy):
HF_DATASET_REPO e.g. "your-org/uofa-leads" (private dataset)
HF_TOKEN a write token
LEAD_FALLBACK_PATH optional JSONL path (default /tmp/uofa-leads-fallback.jsonl)
"""
from __future__ import annotations
import io
import json
import logging
import os
import time
from dataclasses import dataclass
from datetime import datetime, timezone
_log = logging.getLogger("uofa.space.leads")
@dataclass
class CaptureResult:
accepted: bool # True whenever the email was valid - the unlock proceeds
sink: str # "dataset" | "fallback" | "fallback-log" | "invalid"
detail: str
def _valid_email(email: str) -> bool:
return "@" in email and "." in email.split("@")[-1]
def build_record(email: str, pack: str | None, summary: dict | None, *, now: str | None = None) -> dict:
"""Minimal lead record - no evidence content ever."""
c = (summary or {}).get("completeness", {})
return {
"email": email,
"timestamp": now or datetime.now(timezone.utc).isoformat(),
"pack": pack,
"x_of_n": f"{c.get('n_assessed')}/{c.get('n_expected')}" if c else None,
"weakener_count": len((summary or {}).get("weakeners", [])),
}
def _push_to_dataset(record: dict) -> None:
"""Upload one lead as a uniquely-named JSON file (avoids read-modify-write
races). Raises if not configured or the upload fails."""
repo = os.environ.get("HF_DATASET_REPO")
token = os.environ.get("HF_TOKEN")
if not repo or not token:
raise RuntimeError("HF dataset not configured (HF_DATASET_REPO/HF_TOKEN unset)")
from huggingface_hub import HfApi
stamp = record["timestamp"].replace(":", "-")
fname = f"leads/{stamp}-{abs(hash(record['email'])) % 10**8:08d}.json"
HfApi(token=token).upload_file(
path_or_fileobj=io.BytesIO(json.dumps(record).encode("utf-8")),
path_in_repo=fname,
repo_id=repo,
repo_type="dataset",
)
def _fallback_sink(record: dict) -> str:
"""Persist the lead locally; if even that fails, log it. Never raises."""
line = json.dumps(record)
path = os.environ.get("LEAD_FALLBACK_PATH", "/tmp/uofa-leads-fallback.jsonl")
try:
with open(path, "a", encoding="utf-8") as fh:
fh.write(line + "\n")
return "fallback"
except OSError:
_log.warning("LEAD (fallback-log): %s", line)
return "fallback-log"
def capture_lead(
email: str,
*,
pack: str | None = None,
summary: dict | None = None,
retries: int = 2,
sleep=time.sleep,
push=None,
fallback=None,
) -> CaptureResult:
"""Capture a lead. Returns a CaptureResult; never raises and never blocks
the unlock on a sink failure."""
email = (email or "").strip()
if not _valid_email(email):
return CaptureResult(accepted=False, sink="invalid", detail="invalid email")
record = build_record(email, pack, summary)
push = push or _push_to_dataset
fallback = fallback or _fallback_sink
last_err: Exception | None = None
for attempt in range(retries + 1):
try:
push(record)
return CaptureResult(accepted=True, sink="dataset", detail="stored")
except Exception as exc: # noqa: BLE001 - any sink error falls through to fallback
last_err = exc
if attempt < retries:
sleep(2 ** attempt)
sink = fallback(record)
return CaptureResult(accepted=True, sink=sink, detail=f"queued to fallback ({last_err})")