Importer: guess names from addresses
Browse files- backend/app/importer.py +19 -1
backend/app/importer.py
CHANGED
|
@@ -45,6 +45,19 @@ def _first_email(*cells) -> tuple[str, str]:
|
|
| 45 |
return "", ("bad_email" if seen_any else "no_email")
|
| 46 |
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
def _split_keywords(s: str) -> list[str]:
|
| 49 |
out = []
|
| 50 |
for k in re.split(r"[;,\n|/]+", s):
|
|
@@ -92,7 +105,7 @@ def parse_workbook(data: bytes, classify, country_code, suppressed: set[str] = f
|
|
| 92 |
"""-> (records, report). `classify(text, body) -> [field]`, `country_code(text) -> ISO or ''` are passed in so this module
|
| 93 |
stays free of the rest of the app. `suppressed` and `existing` are sets of lower-case e-mails to leave alone."""
|
| 94 |
wb = load_workbook(io.BytesIO(data), read_only=True, data_only=True)
|
| 95 |
-
report = {"sheets": {}, "rows": 0, "no_email": [], "bad_email": [], "no_name": 0, "merged": 0, "already_accounts": 0, "suppressed": 0}
|
| 96 |
by_email: dict[str, dict] = {}
|
| 97 |
for ws in wb.worksheets:
|
| 98 |
n = 0
|
|
@@ -100,6 +113,9 @@ def parse_workbook(data: bytes, classify, country_code, suppressed: set[str] = f
|
|
| 100 |
n += 1
|
| 101 |
report["rows"] += 1
|
| 102 |
email, why = _first_email(*r["email_cells"])
|
|
|
|
|
|
|
|
|
|
| 103 |
if not r["name"] or len(r["name"]) < 2:
|
| 104 |
report["no_name"] += 1
|
| 105 |
continue
|
|
@@ -112,6 +128,8 @@ def parse_workbook(data: bytes, classify, country_code, suppressed: set[str] = f
|
|
| 112 |
if email in existing:
|
| 113 |
report["already_accounts"] += 1
|
| 114 |
continue
|
|
|
|
|
|
|
| 115 |
rec = by_email.get(email)
|
| 116 |
if rec:
|
| 117 |
report["merged"] += 1
|
|
|
|
| 45 |
return "", ("bad_email" if seen_any else "no_email")
|
| 46 |
|
| 47 |
|
| 48 |
+
GENERIC = {"info", "admin", "office", "contact", "mail", "hello", "team", "support", "secretary", "noreply", "no-reply", "sales", "press"}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _name_from_email(email: str) -> str:
|
| 52 |
+
"""A person with an address but no name in the sheet still gets an invitation; the name is a best guess from the part
|
| 53 |
+
before the @ ("dias.omarov" -> "Dias Omarov") and they correct it when they claim the profile. Shared mailboxes are not people."""
|
| 54 |
+
local = email.split("@")[0].lower()
|
| 55 |
+
if local in GENERIC:
|
| 56 |
+
return ""
|
| 57 |
+
parts = [p for p in re.split(r"[._\-+]+", re.sub(r"\d+", "", local)) if len(p) > 1]
|
| 58 |
+
return " ".join(p.capitalize() for p in parts[:3]) if len(parts) >= 1 and len(" ".join(parts)) >= 3 else ""
|
| 59 |
+
|
| 60 |
+
|
| 61 |
def _split_keywords(s: str) -> list[str]:
|
| 62 |
out = []
|
| 63 |
for k in re.split(r"[;,\n|/]+", s):
|
|
|
|
| 105 |
"""-> (records, report). `classify(text, body) -> [field]`, `country_code(text) -> ISO or ''` are passed in so this module
|
| 106 |
stays free of the rest of the app. `suppressed` and `existing` are sets of lower-case e-mails to leave alone."""
|
| 107 |
wb = load_workbook(io.BytesIO(data), read_only=True, data_only=True)
|
| 108 |
+
report = {"sheets": {}, "rows": 0, "no_email": [], "bad_email": [], "no_name": 0, "merged": 0, "already_accounts": 0, "suppressed": 0, "name_from_email": 0}
|
| 109 |
by_email: dict[str, dict] = {}
|
| 110 |
for ws in wb.worksheets:
|
| 111 |
n = 0
|
|
|
|
| 113 |
n += 1
|
| 114 |
report["rows"] += 1
|
| 115 |
email, why = _first_email(*r["email_cells"])
|
| 116 |
+
guessed = False
|
| 117 |
+
if (not r["name"] or len(r["name"]) < 2) and email:
|
| 118 |
+
r["name"], guessed = _name_from_email(email), True
|
| 119 |
if not r["name"] or len(r["name"]) < 2:
|
| 120 |
report["no_name"] += 1
|
| 121 |
continue
|
|
|
|
| 128 |
if email in existing:
|
| 129 |
report["already_accounts"] += 1
|
| 130 |
continue
|
| 131 |
+
if guessed:
|
| 132 |
+
report["name_from_email"] += 1
|
| 133 |
rec = by_email.get(email)
|
| 134 |
if rec:
|
| 135 |
report["merged"] += 1
|