feat: add boilerplate

This commit is contained in:
2026-10-26 21:56:23 +01:00
commit cc6675f7c6
29 changed files with 2231 additions and 0 deletions

189
app/ral.py Normal file
View 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"