Spaces:
Sleeping
Sleeping
| # src/analyzer/data_loader_supporting.py | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from typing import Dict, List, Optional, Iterable, Union | |
| import json, re | |
| from .utils.text import to_number | |
| class SupportingDoc: | |
| grant_id: str | |
| url: str | |
| title: str | |
| open_date: Optional[str] | |
| close_date: Optional[str] | |
| notify_date: Optional[str] | |
| funding_min: Optional[float] | |
| funding_max: Optional[float] | |
| total_pot: Optional[float] | |
| funding_rates: Optional[str] | |
| duration_min: Optional[int] | |
| duration_max: Optional[int] | |
| text: str # flattened blob for retrieval | |
| sections: Dict[str, str] # raw sections, if present | |
| # Number parsing moved to utils.text.to_number() | |
| # Keeping wrapper for backward compatibility | |
| def _num(x): | |
| return to_number(x) | |
| def _int(x): | |
| try: | |
| return int(x) if x is not None else None | |
| except Exception: | |
| try: | |
| return int(float(str(x).replace(",", ""))) | |
| except Exception: | |
| return None | |
| def _infer_grant_id(obj: dict, fallback_name: str = "") -> Optional[str]: | |
| # 1) explicit fields | |
| for k in ("grant_id","id","competition_id","competitionId"): | |
| if obj.get(k): | |
| return str(obj[k]).replace("competition-","").strip() | |
| # 2) from URL: .../competition/2185/... | |
| url = obj.get("url") or obj.get("source_url") or obj.get("page_url") or "" | |
| m = re.search(r"/competition/(\d+)", url) | |
| if m: | |
| return m.group(1) | |
| # 3) from filename | |
| m2 = re.search(r"competition-(\d+)", fallback_name) | |
| if m2: | |
| return m2.group(1) | |
| return None | |
| def _make_text_blob(title: str, url: str, sections: Dict[str,str]) -> str: | |
| parts = [f"TITLE: {title}", f"URL: {url}"] | |
| for k in ("summary_raw","eligibility_raw","scope_raw","dates_raw","how_to_apply_raw","supporting_information_raw"): | |
| v = sections.get(k) | |
| if v: | |
| parts.append(f"\n[{k}]\n{v}") | |
| # also tolerate alt keys from other crawlers | |
| for k in ("summary","eligibility","scope","dates","how_to_apply","supporting_information"): | |
| v = sections.get(k) | |
| if v and f"[{k}_raw]" not in "".join(parts): | |
| parts.append(f"\n[{k}]\n{v}") | |
| return "\n".join(parts) | |
| def _read_obj(obj: dict, fallback_name: str = "") -> Optional[SupportingDoc]: | |
| gid = _infer_grant_id(obj, fallback_name) | |
| if not gid: | |
| return None | |
| url = obj.get("url") or obj.get("source_url") or obj.get("page_url") or "" | |
| title = (obj.get("title") or obj.get("name") or "").strip() | |
| # Common normalised fields | |
| open_date = obj.get("open_date") | |
| close_date = obj.get("close_date") or obj.get("deadline") or obj.get("closeDate") | |
| notify_date = obj.get("notify_date") | |
| # Funding block: either nested or flat | |
| funding = obj.get("funding") or {} | |
| fmin = _num(funding.get("min") or obj.get("funding_min") or obj.get("min_award") or obj.get("grant_min")) | |
| fmax = _num(funding.get("max") or obj.get("funding_max") or obj.get("max_award") or obj.get("grant_max")) | |
| total_pot = _num(funding.get("total_pot") or obj.get("total_pot") or obj.get("competition_total") or obj.get("total_funding")) | |
| rates = funding.get("rates") if isinstance(funding.get("rates"), str) else obj.get("funding_rates") | |
| # Duration block | |
| dur = obj.get("duration_months") or {} | |
| dmin = _int(dur.get("min") or obj.get("duration_min") or obj.get("project_duration_min_months")) | |
| dmax = _int(dur.get("max") or obj.get("duration_max") or obj.get("project_duration_max_months")) | |
| # Sections: tolerate both nested and flat naming | |
| sections: Dict[str, str] = {} | |
| for k in ("summary_raw","eligibility_raw","scope_raw","dates_raw","how_to_apply_raw","supporting_information_raw", | |
| "summary","eligibility","scope","dates","how_to_apply","supporting_information"): | |
| v = obj.get(k) or (obj.get("sections") or {}).get(k) | |
| if isinstance(v, str) and v.strip(): | |
| sections[k] = v | |
| text = _make_text_blob(title, url, sections) | |
| return SupportingDoc( | |
| grant_id=gid, | |
| url=url, | |
| title=title, | |
| open_date=open_date, | |
| close_date=close_date, | |
| notify_date=notify_date, | |
| funding_min=fmin, | |
| funding_max=fmax, | |
| total_pot=total_pot, | |
| funding_rates=rates if isinstance(rates, str) else None, | |
| duration_min=dmin, | |
| duration_max=dmax, | |
| text=text, | |
| sections=sections, | |
| ) | |
| def _read_json_file(p: Path) -> Optional[SupportingDoc]: | |
| try: | |
| obj = json.loads(p.read_text(encoding="utf-8")) | |
| return _read_obj(obj, fallback_name=p.name) | |
| except Exception: | |
| return None | |
| def _iter_jsonl(p: Path) -> Iterable[SupportingDoc]: | |
| with p.open("r", encoding="utf-8") as f: | |
| for i, line in enumerate(f, start=1): | |
| line = line.strip() | |
| if not line: | |
| continue | |
| try: | |
| obj = json.loads(line) | |
| except Exception: | |
| continue | |
| doc = _read_obj(obj, fallback_name=f"{p.name}:{i}") | |
| if doc: | |
| yield doc | |
| def iter_supporting_docs(folder: Path) -> Iterable[SupportingDoc]: | |
| folder = Path(folder) | |
| # Prefer explicit competition-*.json first | |
| found = False | |
| for p in sorted(folder.glob("competition-*.json")): | |
| found = True | |
| doc = _read_json_file(p) | |
| if doc: | |
| yield doc | |
| # Then any *.json | |
| if not found: | |
| for p in sorted(folder.glob("*.json")): | |
| doc = _read_json_file(p) | |
| if doc: | |
| yield doc | |
| # Then *.jsonl (one object per line) | |
| for p in sorted(folder.glob("*.jsonl")): | |
| for doc in _iter_jsonl(p): | |
| yield doc | |
| def load_supporting_docs(folder: Path) -> List[SupportingDoc]: | |
| return list(iter_supporting_docs(folder)) |