feat: add boilerplate
This commit is contained in:
189
app/ral.py
Normal file
189
app/ral.py
Normal file
@@ -0,0 +1,189 @@
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
SALARY_PATTERNS = [
|
||||
r"ral\s*(?:lorda|annua|annuale)?\s*[:\-]?\s*",
|
||||
r"retribuzione\s*(?:annua\s*)?lorda\s*[:\-]?\s*",
|
||||
r"stipendio\s*(?:annuo|lordo)?\s*[:\-]?\s*",
|
||||
r"compensation\s*[:\-]?\s*",
|
||||
r"salary\s*(?:range)?\s*[:\-]?\s*",
|
||||
r"ctc\s*[:\-]?\s*",
|
||||
r"package\s*[:\-]?\s*",
|
||||
]
|
||||
|
||||
NUMBER = r"(?:€|eur|euro)?\s*(\d{1,3}(?:[.\s]\d{3})+|\d{2,6})\s*(?:€|eur|k|mila)?"
|
||||
RANGE = (
|
||||
r"(?:tra|from|between)?\s*"
|
||||
+ NUMBER
|
||||
+ r"\s*(?:-|–|—|/|e|and|to|a)\s*"
|
||||
+ NUMBER
|
||||
)
|
||||
|
||||
BANDS = [
|
||||
(0, 25000, "Less than 25k"),
|
||||
(25000, 30000, "Between 25k and 30k"),
|
||||
(30000, 35000, "Between 30k and 35k"),
|
||||
(35000, 40000, "Between 35k and 40k"),
|
||||
(40000, 45000, "Between 40k and 45k"),
|
||||
(45000, 50000, "Between 45k and 50k"),
|
||||
(50000, 55000, "Between 50k and 55k"),
|
||||
(55000, 60000, "Between 55k and 60k"),
|
||||
(60000, 70000, "Between 60k and 70k"),
|
||||
(70000, 80000, "Between 70k and 80k"),
|
||||
(80000, 90000, "Between 80k and 90k"),
|
||||
(90000, 100000, "Between 90k and 100k"),
|
||||
(100000, 120000, "Between 100k and 120k"),
|
||||
(120000, 150000, "Between 120k and 150k"),
|
||||
(150000, 10**9, "Above 150k"),
|
||||
]
|
||||
|
||||
|
||||
def parse_amount(raw: str) -> int:
|
||||
compact = raw.lower().replace(" ", "").replace("€", "").replace("eur", "")
|
||||
multiplier = 1000 if compact.endswith("k") or compact.endswith("mila") else 1
|
||||
compact = compact.replace("k", "").replace("mila", "").replace(".", "")
|
||||
digits = re.sub(r"[^\d]", "", compact)
|
||||
if not digits:
|
||||
return 0
|
||||
value = int(digits)
|
||||
if multiplier == 1000 and value < 1000:
|
||||
value *= 1000
|
||||
if 1000 <= value < 10000 and not raw.lower().endswith(("k", "mila")):
|
||||
# 35.000 style already handled; 35000 stays
|
||||
pass
|
||||
return value
|
||||
|
||||
|
||||
def extract_salary(text: str) -> dict[str, int | None]:
|
||||
blob = text.lower().replace("\xa0", " ")
|
||||
ral_min = ral_max = ral_exact = None
|
||||
|
||||
for match in re.finditer(RANGE, blob, flags=re.I):
|
||||
a, b = parse_amount(match.group(1)), parse_amount(match.group(2))
|
||||
if 15000 <= a <= 400000 and 15000 <= b <= 400000:
|
||||
ral_min, ral_max = min(a, b), max(a, b)
|
||||
break
|
||||
|
||||
if ral_min is None:
|
||||
labeled = re.search(
|
||||
r"(?:ral|retribuzione|salary|stipendio|package)[^\n]{0,40}" + NUMBER,
|
||||
blob,
|
||||
flags=re.I,
|
||||
)
|
||||
if labeled:
|
||||
value = parse_amount(labeled.group(1))
|
||||
if 15000 <= value <= 400000:
|
||||
ral_exact = value
|
||||
|
||||
if ral_min is None and ral_exact is None:
|
||||
above = re.search(r"(?:above|oltre|maggiore di|>)\s*" + NUMBER, blob, flags=re.I)
|
||||
if above:
|
||||
value = parse_amount(above.group(1))
|
||||
if 15000 <= value <= 400000:
|
||||
ral_min = value
|
||||
ral_max = value + 20000
|
||||
less = re.search(r"(?:less than|fino a|inferiore a|<)\s*" + NUMBER, blob, flags=re.I)
|
||||
if less and ral_min is None:
|
||||
value = parse_amount(less.group(1))
|
||||
if 15000 <= value <= 400000:
|
||||
ral_min = max(15000, value - 15000)
|
||||
ral_max = value
|
||||
|
||||
return {"ral_min": ral_min, "ral_max": ral_max, "ral_exact": ral_exact}
|
||||
|
||||
|
||||
def hide_salary(text: str) -> str:
|
||||
cleaned = text
|
||||
replacements = [
|
||||
RANGE,
|
||||
r"(?:ral|retribuzione\s*annua\s*lorda|stipendio|salary|compensation|ctc)\s*[:\-]?\s*(?:€|eur|euro)?\s*\d[\d.\sKkmila€euro\-–toaeand/]{1,48}",
|
||||
r"(?:€|eur)\s*\d{2,3}(?:[.\s]\d{3})+(?:\s*(?:-|–|to|a)\s*(?:€|eur)?\s*\d{2,3}(?:[.\s]\d{3})+)?",
|
||||
r"\b\d{2,3}\s*k(?:\s*(?:-|–|to|a)\s*\d{2,3}\s*k)?\b",
|
||||
r"\b\d{5,6}\s*(?:€|eur|euro)?\b",
|
||||
]
|
||||
for pattern in replacements:
|
||||
cleaned = re.sub(pattern, "[RAL HIDDEN]", cleaned, flags=re.I)
|
||||
cleaned = re.sub(r"(\[RAL HIDDEN\]\s*){2,}", "[RAL HIDDEN] ", cleaned)
|
||||
return cleaned.strip()
|
||||
|
||||
|
||||
def midpoint(job: Any) -> int:
|
||||
if job.ral_exact:
|
||||
return job.ral_exact
|
||||
if job.ral_min and job.ral_max:
|
||||
return (job.ral_min + job.ral_max) // 2
|
||||
return job.ral_min or job.ral_max or 40000
|
||||
|
||||
|
||||
def correct_band(value: int) -> tuple[int, int, str]:
|
||||
for low, high, label in BANDS:
|
||||
if low <= value < high:
|
||||
return low, high, label
|
||||
return BANDS[-1]
|
||||
|
||||
|
||||
def generate_options(job: Any) -> tuple[list[dict], str]:
|
||||
value = midpoint(job)
|
||||
idx = next(i for i, (low, high, _) in enumerate(BANDS) if low <= value < high)
|
||||
|
||||
start = max(0, idx - 2)
|
||||
end = min(len(BANDS), start + 4)
|
||||
start = max(0, end - 4)
|
||||
chosen = BANDS[start:end]
|
||||
if len(chosen) < 4:
|
||||
chosen = BANDS[:4]
|
||||
|
||||
options = []
|
||||
correct = ""
|
||||
for low, high, label in chosen:
|
||||
if high >= 10**9:
|
||||
key = f"above_{low}"
|
||||
text = f"Above {low // 1000}k"
|
||||
elif low == 0:
|
||||
key = f"less_{high}"
|
||||
text = f"Less than {high // 1000}k"
|
||||
else:
|
||||
key = f"between_{low}_{high}"
|
||||
text = f"Between {low // 1000}k and {high // 1000}k"
|
||||
is_correct = low <= value < high
|
||||
if is_correct:
|
||||
correct = key
|
||||
options.append({"key": key, "label": text})
|
||||
|
||||
random.shuffle(options)
|
||||
return options, correct
|
||||
|
||||
|
||||
def option_payload(job: Any) -> str:
|
||||
options, correct = generate_options(job)
|
||||
return json.dumps({"options": options, "correct": correct})
|
||||
|
||||
|
||||
def detect_work_type(text: str) -> str:
|
||||
blob = text.lower()
|
||||
if any(w in blob for w in ["full remote", "fully remote", "100% remote", "full-remote", "da remoto"]):
|
||||
return "remote"
|
||||
if any(w in blob for w in ["hybrid", "ibrido", "smart working"]):
|
||||
return "hybrid"
|
||||
if any(w in blob for w in ["on-site", "onsite", "in sede", "in office", "presenza"]):
|
||||
return "onsite"
|
||||
return "hybrid"
|
||||
|
||||
|
||||
def detect_subject(title: str, description: str) -> str:
|
||||
blob = f"{title} {description}".lower()
|
||||
mapping = {
|
||||
"technology": ["developer", "engineer", "software", "data", "devops", "it ", "python", "java", "cloud"],
|
||||
"financial": ["bank", "finance", "fintech", "accounting", "auditor", "investment"],
|
||||
"business": ["sales", "account manager", "business", "marketing", "product manager"],
|
||||
"healthcare": ["nurse", "medical", "pharma", "clinic", "health"],
|
||||
"legal": ["lawyer", "legal", "compliance", "avvocato"],
|
||||
"hr": ["recruiter", "human resources", "hr ", "people"],
|
||||
"design": ["designer", "ux", "ui", "graphic"],
|
||||
}
|
||||
for subject, words in mapping.items():
|
||||
if any(w in blob for w in words):
|
||||
return subject
|
||||
return "other"
|
||||
Reference in New Issue
Block a user