Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
89 lines
3.5 KiB
Python
89 lines
3.5 KiB
Python
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
|
|
# For license information, please see license.txt
|
|
|
|
"""Whitelisted entry points: ``enrich`` (enqueue a full run for a record) and
|
|
``retry`` (re-run one from its Run history).
|
|
|
|
Both are type-annotated -- hooks.py sets ``require_type_annotated_api_methods``.
|
|
Security: each enforces the doctype allow-list (from Settings) + a ``write``
|
|
permission check, and is rate-limited per user (see ``ENRICH_RATE_LIMIT``).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import frappe
|
|
from frappe import _
|
|
from frappe.rate_limiter import rate_limit
|
|
|
|
from .config import ENRICHABLE_DOCTYPES, EnrichmentConfig, get_config
|
|
from .tasks import enqueue_enrichment
|
|
|
|
# Per-user, per-minute cap on each enrich entry point (each gets its own bucket).
|
|
# Both only enqueue a per-doc-deduplicated job -- 10/min is far above any real human
|
|
# burst while capping scripted queue-flooding.
|
|
ENRICH_RATE_LIMIT = 10
|
|
|
|
|
|
def _enabled_doctypes(cfg: EnrichmentConfig) -> list[str]:
|
|
"""The doctypes enrichment is enabled for, per Settings (allow-list)."""
|
|
if not cfg.setting("enabled"):
|
|
return []
|
|
return list(ENRICHABLE_DOCTYPES)
|
|
|
|
|
|
def _enqueue_run(cfg, reference_doctype: str, reference_name: str, website: str) -> dict:
|
|
"""Validate + enqueue one enrichment run for a target record. Shared by
|
|
``enrich`` (initial trigger from the record) and ``retry`` (re-run from a Run).
|
|
|
|
Caller must have already resolved the website. Enforces the Settings allow-list
|
|
and ``write`` permission on the target. Uses the per-doc ``job_id`` +
|
|
``deduplicate`` so a second trigger while one is in-flight is a no-op.
|
|
"""
|
|
if reference_doctype not in _enabled_doctypes(cfg):
|
|
frappe.throw(
|
|
_("Enrichment is not enabled for {0}.").format(reference_doctype),
|
|
frappe.ValidationError,
|
|
)
|
|
|
|
doc = frappe.get_doc(reference_doctype, reference_name)
|
|
doc.check_permission("write")
|
|
|
|
website = (website or "").strip()
|
|
if not website:
|
|
frappe.throw(_("Set a website on this record before enriching."), frappe.ValidationError)
|
|
|
|
return enqueue_enrichment(reference_doctype, reference_name, website, frappe.session.user)
|
|
|
|
|
|
@frappe.whitelist()
|
|
@rate_limit(limit=ENRICH_RATE_LIMIT, seconds=60)
|
|
def enrich(reference_doctype: str, reference_name: str) -> dict:
|
|
"""Enqueue a full enrichment run for one CRM record, using the record's own
|
|
``website`` field. The initial trigger (the "Enrich from Website" button).
|
|
|
|
Returns ``{queued: bool, job_id: str, website: str}``.
|
|
"""
|
|
cfg = get_config()
|
|
doc = frappe.get_doc(reference_doctype, reference_name)
|
|
website = (doc.get("website") or "").strip()
|
|
return _enqueue_run(cfg, reference_doctype, reference_name, website)
|
|
|
|
|
|
@frappe.whitelist()
|
|
@rate_limit(limit=ENRICH_RATE_LIMIT, seconds=60)
|
|
def retry(run: str) -> dict:
|
|
"""Re-run the enrichment recorded by a ``CRM Enrichment Run`` (the desk "Retry"
|
|
button on each run). Re-enriches the run's linked record, preferring the record's
|
|
current ``website`` and falling back to the website this run originally scraped.
|
|
|
|
Returns ``{queued: bool, job_id: str, website: str}``.
|
|
"""
|
|
run_doc = frappe.get_doc("CRM Enrichment Run", run)
|
|
if not run_doc.reference_doctype or not run_doc.reference_name:
|
|
frappe.throw(_("This run has no linked record to re-enrich."), frappe.ValidationError)
|
|
|
|
cfg = get_config()
|
|
target = frappe.get_doc(run_doc.reference_doctype, run_doc.reference_name)
|
|
website = (target.get("website") or "").strip() or (run_doc.source_website or "").strip()
|
|
return _enqueue_run(cfg, run_doc.reference_doctype, run_doc.reference_name, website)
|