File size: 5,141 Bytes
54d594e
 
 
7d94330
 
 
 
 
 
54d594e
7d94330
 
54d594e
7d94330
54d594e
 
 
 
7d94330
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54d594e
 
7d94330
54d594e
7d94330
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54d594e
 
 
 
7d94330
 
 
 
 
54d594e
7d94330
 
54d594e
7d94330
 
 
54d594e
7d94330
 
 
 
 
54d594e
 
7d94330
 
 
 
 
54d594e
7d94330
 
 
 
 
 
54d594e
7d94330
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54d594e
 
 
 
7d94330
54d594e
7d94330
 
 
54d594e
7d94330
 
54d594e
 
 
 
 
7d94330
54d594e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
"""
OpenAlex enrichment — journal metadata + peer percentile calculation.
Free API, no key required. Uses polite pool with mailto header.

Changes from v1.0:
- Shared module-level httpx.AsyncClient (init_client / close_client for lifespan)
- compute_percentile accepts an optional concept_cache dict to deduplicate
  identical concept peer-set fetches across journals in one request
- _norm_issn removed — uses utils.norm_issn
"""
import httpx
import asyncio
from typing import Optional
from utils import norm_issn

UA = "ResearchNavigator/1.0 (mailto:library@ku.ac.ae)"
BASE = "https://api.openalex.org"

_http_client: Optional[httpx.AsyncClient] = None


def init_client() -> None:
    global _http_client
    _http_client = httpx.AsyncClient(
        timeout=httpx.Timeout(10.0, connect=5.0),
        headers={"User-Agent": UA},
    )


async def close_client() -> None:
    global _http_client
    if _http_client and not _http_client.is_closed:
        await _http_client.aclose()
    _http_client = None


def _get_client() -> httpx.AsyncClient:
    if _http_client is None or _http_client.is_closed:
        init_client()
    return _http_client


async def enrich_journal(name: str, issn: Optional[str]) -> Optional[dict]:
    client = _get_client()
    try:
        if issn:
            clean = norm_issn(issn) or issn.strip()
            r = await client.get(f"{BASE}/sources/issn:{clean}")
        else:
            r = await client.get(
                f"{BASE}/sources", params={"search": name, "per_page": 1}
            )
        if not r.is_success:
            return None
        d = r.json()
        s = d if issn else (d.get("results") or [None])[0]
        if not s:
            return None
        return {
            "openalex_id":           s.get("id"),
            "display_name":          s.get("display_name"),
            "issn_l":                s.get("issn_l"),
            "issns":                 s.get("issn", []),
            "host_organization":     s.get("host_organization_name"),
            "is_oa":                 s.get("is_oa"),
            "is_in_doaj":            s.get("is_in_doaj"),
            "works_count":           s.get("works_count"),
            "cited_by_count":        s.get("cited_by_count"),
            "h_index":               (s.get("summary_stats") or {}).get("h_index"),
            "two_yr_mean_citedness": (s.get("summary_stats") or {}).get("2yr_mean_citedness"),
            "homepage_url":          s.get("homepage_url"),
            "top_concept":           ((s.get("x_concepts") or [{}])[0]).get("display_name"),
            "top_concept_id":        ((s.get("x_concepts") or [{}])[0]).get("id"),
        }
    except Exception as e:
        print(f"[OpenAlex] enrich error for {name}: {e}")
        return None


async def compute_percentile(
    openalex: dict,
    concept_cache: Optional[dict] = None,
) -> Optional[dict]:
    try:
        if not openalex or not openalex.get("top_concept_id"):
            return None
        val = openalex.get("two_yr_mean_citedness")
        if val is None:
            return None

        concept_id = str(openalex["top_concept_id"]).split("/")[-1]

        if concept_cache is not None and concept_id in concept_cache:
            peers = concept_cache[concept_id]
        else:
            client = _get_client()
            r = await client.get(
                f"{BASE}/sources",
                params={
                    "filter": f"concepts.id:{concept_id},type:journal",
                    "per_page": 200,
                    "sort": "summary_stats.2yr_mean_citedness:desc",
                },
            )
            if not r.is_success:
                return None
            peers = [
                s.get("summary_stats", {}).get("2yr_mean_citedness")
                for s in r.json().get("results", [])
            ]
            peers = [v for v in peers if isinstance(v, (int, float))]
            if concept_cache is not None:
                concept_cache[concept_id] = peers

        if len(peers) < 10:
            return None

        below = sum(1 for v in peers if v < val)
        pct = round(below / len(peers) * 100)
        q = "Q1" if pct >= 75 else "Q2" if pct >= 50 else "Q3" if pct >= 25 else "Q4"
        return {
            "percentile": pct,
            "quartile": q,
            "peer_count": len(peers),
            "concept": openalex.get("top_concept"),
        }
    except Exception as e:
        print(f"[OpenAlex] percentile error: {e}")
        return None


async def enrich_all(journals: list) -> list:
    """Enrich a list of journal dicts in parallel, deduplicating concept peer-sets."""
    concept_cache: dict = {}

    async def _enrich_one(j: dict) -> dict:
        oa = await enrich_journal(j.get("name", ""), j.get("issn"))
        pct = await compute_percentile(oa, concept_cache) if oa else None
        j["openalex"] = oa
        j["oa_percentile"] = pct
        if not j.get("issn") and oa:
            j["issn"] = oa.get("issn_l") or (oa.get("issns") or [None])[0]
        return j

    return list(await asyncio.gather(*[_enrich_one(j) for j in journals]))