Spaces:
Running
Running
| """ | |
| 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) | |