""" SCImago CSV loader — builds in-memory ISSN → journal dict at startup. Used PRIVATELY for quartile lookup only. Never exposed as branded data source in UI. Changes from v1.0: - _norm_issn removed — uses utils.norm_issn - sourceid now included in ISSN index entries (was missing — broke verify_links) """ import csv import re import os from typing import Optional from utils import norm_issn _scimago_cache: dict = {} def load_scimago(csv_path: str = None) -> dict: global _scimago_cache if _scimago_cache: return _scimago_cache if csv_path is None: csv_path = os.path.join(os.path.dirname(__file__), "scimagojr_2025.csv") index = {} try: with open(csv_path, encoding="latin-1", newline="") as f: reader = csv.DictReader(f, delimiter=",") for row in reader: issn_raw = row.get("Issn", "") issn_parts = re.split(r"[,;\s]+", issn_raw) entry = { "title": row.get("Title", "").strip(), "rank": row.get("Rank", "").strip(), "quartile": row.get("SJR Best Quartile", "").strip(), "h_index": row.get("H index", "").strip(), "categories": row.get("Categories", "").strip(), "areas": row.get("Areas", "").strip(), "publisher": row.get("Publisher", "").strip(), "open_access":row.get("Open Access", "No").strip(), "country": row.get("Country", "").strip(), "sourceid": row.get("Sourceid", "").strip(), # FIX: was missing } for part in issn_parts: normed = norm_issn(part) if normed: index[normed] = entry _scimago_cache = index print(f"[SCImago] Loaded {len(index):,} ISSN entries.") except Exception as e: print(f"[SCImago] Failed: {e}") _scimago_cache = {} return _scimago_cache def lookup_any(issns: list) -> Optional[dict]: index = load_scimago() for issn in issns: normed = norm_issn(str(issn)) if issn else None if normed and normed in index: result = dict(index[normed]) result["matched_issn"] = normed return result return None # ── Subject search index (separate from ISSN index) ────────────────────── _subject_cache: list = [] def load_subject_index(csv_path: str = None) -> list: """Load full journal list for subject browsing — separate from ISSN lookup.""" global _subject_cache if _subject_cache: return _subject_cache if csv_path is None: csv_path = os.path.join(os.path.dirname(__file__), "scimagojr_2025.csv") rows = [] seen_titles = set() try: with open(csv_path, encoding="latin-1", newline="") as f: reader = csv.DictReader(f, delimiter=",") for row in reader: title = row.get("Title", "").strip() if not title or title in seen_titles: continue seen_titles.add(title) issn_raw = row.get("Issn", "") issn_parts = re.split(r"[,;\s]+", issn_raw) issns = [norm_issn(p) for p in issn_parts if norm_issn(p)] primary_issn = issns[0] if issns else None try: h = int(row.get("H index", "0").strip() or 0) except Exception: h = 0 rows.append({ "title": title, "issn": primary_issn, "quartile": row.get("SJR Best Quartile", "").strip(), "h_index": h, "categories": row.get("Categories", "").strip().lower(), "areas": row.get("Areas", "").strip().lower(), "publisher": row.get("Publisher", "").strip(), "open_access":row.get("Open Access", "No").strip(), "country": row.get("Country", "").strip(), "rank": row.get("Rank", "").strip(), "sourceid": row.get("Sourceid", "").strip(), }) _subject_cache = rows print(f"[SCImago] Subject index loaded: {len(rows):,} unique journals.") except Exception as e: print(f"[SCImago] Subject index failed: {e}") _subject_cache = [] return _subject_cache def search_by_subject(category: str, area: str, top_n: int = 15) -> list: """ Search journals by normalised category + area strings from GPT. Returns top_n sorted by h_index descending. Tries: exact category match -> partial category -> area match. """ rows = load_subject_index() cat_lower = category.lower().strip() if category else "" area_lower = area.lower().strip() if area else "" def score(r): cats = r["categories"] areas = r["areas"] if cat_lower and cat_lower in cats: return 3 if cat_lower: words = [w for w in cat_lower.split() if len(w) > 3] if any(w in cats for w in words): return 2 if area_lower and area_lower in areas: return 1 if area_lower: words = [w for w in area_lower.split() if len(w) > 3] if any(w in areas for w in words): return 1 return 0 scored = [(score(r), r) for r in rows] matched = [(s, r) for s, r in scored if s > 0] if not matched: return [] matched.sort(key=lambda x: (-x[0], -x[1]["h_index"])) return [r for _, r in matched[:top_n]] def get_all_areas() -> list: """Return sorted unique area names for frontend hints.""" rows = load_subject_index() areas = set() for r in rows: for a in r["areas"].split(";"): a = a.strip().title() if a: areas.add(a) return sorted(areas)