Files
upstream-mirror db69de1c11
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
snapshot of https://github.com/frappe/crm.git (develop, 2026-09-18; blobs > 131072B omitted)
2026-09-18 01:36:01 +05:00

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)