HR-ATS-Portal/backend/linkedin_utils.py

59 lines
2.1 KiB
Python

"""LinkedIn profile-link extraction and normalization.
One shared vocabulary for "the same person" across the two places a LinkedIn
identity appears: sourced talent profiles (a normalized URL from the Apify
actor) and CV text (a link the candidate wrote, often mangled by PDF
extraction). The match key is the lowercase public slug from /in/<slug>.
Top-level module on purpose: talent/, inbox/ and job/ all need it, and any
package-local home would invite an import cycle.
"""
import re
from urllib.parse import unquote
# CV text arrives from PDF extraction: URLs may carry percent-escapes, no
# scheme ("linkedin.com/in/jane-doe"), or trailing sentence punctuation glued
# on by layout. /pub/ is the legacy public-profile path some older CVs still
# carry.
_SLUG_RE = re.compile(r"linkedin\.com/(?:in|pub)/([A-Za-z0-9\-_.%]+)", re.IGNORECASE)
# Sentinel stored on application rows: NULL means "never scanned", the empty
# string means "scanned, no link found". The distinction is what lets the lazy
# backfill converge instead of rescanning every CV on every request.
NO_SLUG = ""
def normalize_slug(raw) -> str | None:
"""Lowercase, percent-decoded, stripped of trailing sentence punctuation."""
if not raw:
return None
slug = unquote(str(raw)).strip().lower().rstrip(".")
return slug or None
def slug_from_url(url) -> str | None:
"""Slug from an already-normalized profile URL (talent_profiles.linkedin_url)."""
if not url:
return None
match = _SLUG_RE.search(str(url))
return normalize_slug(match.group(1)) if match else None
def slugs_from_text(text) -> list[str]:
"""Every distinct slug mentioned in a CV, in order of first appearance."""
if not text:
return []
found: list[str] = []
for match in _SLUG_RE.finditer(text):
slug = normalize_slug(match.group(1))
if slug and slug not in found:
found.append(slug)
return found
def primary_slug_from_text(text) -> str:
"""The slug to persist on an application row; NO_SLUG when the CV has none."""
slugs = slugs_from_text(text)
return slugs[0] if slugs else NO_SLUG