grantforge-api / backend /core /grants /eligibility_fields.py
GrantForge Bot
Deploy sha-565ad85979610064f6d1c18ab3b6404357d61073 — source build (no GHCR)
ce8f04a
Raw
History Blame Contribute Delete
7.8 kB
"""Parsowanie pól WYSZUKIWARKA pod filtr eligibility i matcher."""
from __future__ import annotations
import re
from typing import Any, Dict, List, Optional, Tuple
VOIVODESHIPS = (
"mazowieckie",
"małopolskie",
"malopolskie",
"śląskie",
"slaskie",
"wielkopolskie",
"dolnośląskie",
"dolnoslaskie",
"pomorskie",
"łódzkie",
"lodzkie",
"lubelskie",
"podkarpackie",
"zachodniopomorskie",
"warmińsko-mazurskie",
"warminsko-mazurskie",
"podlaskie",
"świętokrzyskie",
"swietokrzyskie",
"lubuskie",
"opolskie",
"kujawsko-pomorskie",
)
VOIVODESHIP_CANONICAL = {
"malopolskie": "małopolskie",
"slaskie": "śląskie",
"dolnoslaskie": "dolnośląskie",
"lodzkie": "łódzkie",
"warminsko-mazurskie": "warmińsko-mazurskie",
"swietokrzyskie": "świętokrzyskie",
}
NATIONAL_MARKERS = (
"cała polska",
"cala polska",
"ogólnopolsk",
"ogolnopolsk",
"krajowy",
"wszystkie województwa",
"wszystkie wojewodztwa",
"terytorium kraju",
)
BENEFICIARY_RULES: Tuple[Tuple[str, str], ...] = (
(
r"\bmśp\b|\bmsp\b|mikroprzedsi|mikro[\s,].*przedsi|mał[eych]*[\s,].*przedsi|"
r"średni[ech]*[\s,].*przedsi|mikro,\s*mał",
"mśp",
),
(r"\bngo\b|pozarządow|pozarzadow|trzeci sektor|organizacj[ae] pozarząd", "ngo"),
(r"\bjst\b|jednostk[ai] samorząd|samorząd|gmin[ay]|powiat|marszałk", "jst"),
(r"rolnik|rolnictw|gospodarstw[ao] rol|arimr|wpr\b|prow\b", "rolnik"),
(r"osob[ay] fizyczn|gospodarstw[ao] domow", "osoba_fizyczna"),
(r"duż[eych]* przedsi|duz[eych]* przedsi|large enterprise", "duże"),
(r"startup|scale-?up", "startup"),
(r"przedsiębiorc|przedsiebiorc|firma|przedsiębiorstw", "przedsiębiorca"),
(r"uczelni|uniwersytet|badawcz|naukow|instytut nauk", "nauka"),
(r"szkoł|placówek oświat|oświat", "edukacja"),
)
SIZE_FROM_TAGS = {
"mśp": ["mikro", "małe", "średnie"],
"startup": ["mikro", "małe"],
"duże": ["duże"],
}
def _norm_voiv(name: str) -> str:
low = name.lower().strip()
return VOIVODESHIP_CANONICAL.get(low, low)
def parse_beneficiary_tags(*texts: str) -> List[str]:
"""Zwraca unikalne tagi beneficjentów (MŚP, NGO, JST, rolnik…)."""
combined = " ".join(t for t in texts if t).lower()
if not combined.strip():
return []
found: List[str] = []
for pattern, tag in BENEFICIARY_RULES:
if re.search(pattern, combined, re.I):
if tag not in found:
found.append(tag)
return found
def parse_eligible_company_sizes(
beneficjenci: str = "", warunki_wejscia: str = "", name: str = ""
) -> List[str]:
"""Mapuje beneficjentów na rozmiary firm dla filtra MŚP."""
tags = parse_beneficiary_tags(beneficjenci, warunki_wejscia, name)
sizes: List[str] = []
text = f"{beneficjenci} {warunki_wejscia} {name}".lower()
if re.search(r"tylko.*duż|wyłącznie.*duż|large enterprise", text, re.I):
return ["duże"]
for tag in tags:
for size in SIZE_FROM_TAGS.get(tag, []):
if size not in sizes:
sizes.append(size)
if "mśp" in tags or re.search(r"\bmikro\b|\bmał[ey]\b|\bśredni", text, re.I):
for s in ("mikro", "małe", "średnie"):
if s not in sizes:
sizes.append(s)
if re.search(r"wyłącznie.*mikro|tylko.*mikro", text, re.I):
return ["mikro"]
if re.search(r"wyłącznie.*mał|tylko.*mał", text, re.I):
return ["małe"]
return sizes
def parse_eligible_regions(
operator: str = "",
zrodlo: str = "",
beneficjenci: str = "",
nazwa: str = "",
) -> List[str]:
"""Wyciąga województwo z operatora/źródła lub oznacza program krajowy."""
hay = f"{operator} {zrodlo} {beneficjenci} {nazwa}".lower()
regions: List[str] = []
for voiv in VOIVODESHIPS:
if voiv in hay:
canon = _norm_voiv(voiv)
if canon not in regions:
regions.append(canon)
rpo_match = re.search(r"rpo\s+([\wąćęłńóśźż\-]+)", hay, re.I)
if rpo_match and not regions:
hint = rpo_match.group(1).lower()
for voiv in VOIVODESHIPS:
if hint in voiv or voiv.startswith(hint[:4]):
regions.append(_norm_voiv(voiv))
wfos_match = re.search(r"wf[oó]śigw\s+([\wąćęłńóśźż\-]+)", hay, re.I)
if wfos_match and not regions:
hint = wfos_match.group(1).lower()
for voiv in VOIVODESHIPS:
if hint in voiv:
regions.append(_norm_voiv(voiv))
if any(m in hay for m in NATIONAL_MARKERS):
return ["cała Polska"]
national_operators = (
"parp", "ncbr", "ncn", "bgk", "kpk", "feniks", "kpo", "granty.pl",
"atlas dotacji", "fundusze europejskie.gov", "arimr", "nfosigw",
"eeagrants", "horyzont",
)
if not regions and any(op in hay for op in national_operators):
return ["cała Polska"]
return regions
def parse_amount_pln(raw: Any) -> Optional[int]:
"""Parsuje kwotę z pola kwota_max/min (tekst lub liczba)."""
if raw is None:
return None
if isinstance(raw, (int, float)):
val = int(raw)
return val if val > 0 else None
text = str(raw).lower().replace("\xa0", " ").strip()
if not text:
return None
m = re.search(r"([\d\s]+(?:[.,]\d+)?)\s*(mln|mld|tys|zł|pln)?", text)
if not m:
return None
num_str = m.group(1).replace(" ", "").replace(",", ".")
try:
num = float(num_str)
except ValueError:
return None
unit = (m.group(2) or "").lower()
if "mld" in unit:
num *= 1_000_000_000
elif "mln" in unit:
num *= 1_000_000
elif "tys" in unit:
num *= 1_000
val = int(num)
if val > 500_000_000_000:
return None
return val if val > 0 else None
def extract_pkd_hints(*texts: str) -> List[str]:
"""Wyciąga kody PKD z warunków wejścia / opisu."""
combined = " ".join(t for t in texts if t)
found: List[str] = []
for m in re.finditer(r"\b(\d{2}\.\d{2}(?:\.[A-Z])?)\b", combined, re.I):
code = m.group(1).upper()
if code not in found:
found.append(code)
return found[:15]
def parse_entry_restrictions(warunki_wejscia: str) -> List[str]:
"""Skrócone tagi ograniczeń wejścia (do matchera)."""
if not warunki_wejscia:
return []
text = warunki_wejscia.lower()
tags: List[str] = []
rules = (
(r"de minimis|pomoc publiczn", "de_minimis"),
(r"dns[hH]|zrównoważon", "dnsh"),
(r"minimum\s+\d+\s*%|wkład własn", "wkład_własny"),
(r"do\s+\d+\s+lat|wiek|41\s+r", "limit_wieku"),
(r"konsorcj|partner", "konsorcjum"),
(r"innowacj|b\+r|badania", "innowacje"),
(r"eksport", "eksport"),
(r"zatrudnien|pracownik|fte", "zatrudnienie"),
)
for pattern, tag in rules:
if re.search(pattern, text, re.I) and tag not in tags:
tags.append(tag)
return tags
def is_exclusive_beneficiary(text: str, tag: str) -> bool:
"""Czy beneficjent jest wyłącznym (tylko NGO, tylko JST…)."""
if not text:
return False
low = text.lower()
exclusive_markers = ("wyłącznie", "wylacznie", "tylko dla", "jedynie")
if not any(m in low for m in exclusive_markers):
return False
tag_patterns = {
"ngo": r"ngo|pozarządow|trzeci sektor",
"jst": r"jst|samorząd|gmin|powiat",
"rolnik": r"rolnik|rolnictw|gospodarstw rol",
"osoba_fizyczna": r"osob[ay] fizyczn|gospodarstw domow",
}
pat = tag_patterns.get(tag)
return bool(pat and re.search(pat, low, re.I))