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

151 lines
4.5 KiB
Python

# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
"""EnrichmentResult schema. Preserves the {value, source, method} provenance shape.
Every piece of evidence keeps a ``source`` URL so any field can be traced back to
the page it was extracted from. Dataclasses serialize to plain dicts/JSON via
``to_dict()`` so the result can be returned by the engine and stored by the mapper /
run-writer (Phase 4) without any framework coupling. This module imports no framework
services on purpose -- it is the pure, unit-testable result container.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
# Canonical extraction-method labels -- every extracted value records which one
# produced it, so a reviewer can judge how much to trust it.
class Method:
JSON_LD = "JSON-LD"
META_TAG = "Meta Tag"
TITLE_TAG = "Title Tag"
BODY_TEXT = "Body Text"
FAVICON = "Favicon"
REGEX = "Regex"
TEL_LINK = "tel: link"
TEXT_HEURISTIC = "Text Heuristic"
KEYWORD_CLASSIFIER = "Keyword Classifier"
SOCIAL_RULE = "Social Rule"
@dataclass
class Email:
value: str
source: str = ""
method: str = Method.REGEX
def to_dict(self):
return {"value": self.value, "source": self.source, "method": self.method}
@dataclass
class Phone:
value: str # normalized / cleaned
raw: str = "" # exactly as found on the page
source: str = ""
method: str = Method.REGEX
def to_dict(self):
return {
"value": self.value,
"raw": self.raw,
"source": self.source,
"method": self.method,
}
@dataclass
class Field:
"""A scalar value plus where/how it came from (company_name, description,
logo, industry, ...)."""
value: object = ""
source: str = ""
method: str = ""
def to_dict(self):
return {"value": self.value, "source": self.source, "method": self.method}
@dataclass
class SocialProfile:
value: str = ""
source: str = ""
method: str = Method.SOCIAL_RULE
def to_dict(self):
return {"value": self.value, "source": self.source, "method": self.method}
@dataclass
class EnrichmentResult:
website: str = ""
# Scalar fields carry their own provenance via Field.
company_name: Field = field(default_factory=Field)
description: Field = field(default_factory=Field)
logo: Field = field(default_factory=Field) # the company link icon (favicon / apple-touch)
image: Field = field(default_factory=Field) # larger social/JSON-LD image; kept, not the logo
industry: Field = field(default_factory=Field)
industry_confidence: float = 0.0
emails: list = field(default_factory=list) # list[Email]
phones: list = field(default_factory=list) # list[Phone]
social_profiles: dict = field(default_factory=dict) # network -> SocialProfile
# Diagnostics -- not part of the required schema but invaluable for debugging.
pages_crawled: list = field(default_factory=list)
errors: list = field(default_factory=list)
notes: list = field(default_factory=list) # human-readable warnings
def to_dict(self):
"""Render the canonical JSON schema. Every field is explainable: scalar
fields expose {value, source, method}; list items embed source + method;
social profiles are {value, source, method} per network."""
return {
"company_name": self.company_name.to_dict(),
"description": self.description.to_dict(),
"logo": self.logo.to_dict(),
"image": self.image.to_dict(),
"industry": self.industry.to_dict(),
"industry_confidence": round(self.industry_confidence, 2),
"emails": [e.to_dict() for e in self.emails],
"phones": [p.to_dict() for p in self.phones],
"social_profiles": {k: v.to_dict() for k, v in self.social_profiles.items()},
"_meta": {
"website": self.website,
"pages_crawled": self.pages_crawled,
"errors": self.errors,
"notes": self.notes,
},
}
def flat(self):
"""Convenience: collapse to plain scalar values (no provenance), useful
for storing the headline fields on a Frappe document."""
return {
"company_name": self.company_name.value,
"description": self.description.value,
"logo": self.logo.value,
"industry": self.industry.value,
"industry_confidence": round(self.industry_confidence, 2),
"social_profiles": {k: v.value for k, v in self.social_profiles.items()},
}
@dataclass
class CrawledPage:
"""A single fetched page and its parsed artifacts."""
url: str
status_code: int = 0
html: str = ""
text: str = ""
title: str = ""
headings: list = field(default_factory=list)
error: str = ""
def to_dict(self):
return asdict(self)