#!/usr/bin/env python3 """ Search the OpenAlex Works API. No API key required -- set the OPENALEX_MAILTO environment variable to an email address to use OpenAlex's "polite pool" for higher and more reliable rate limits (OpenAlex's own recommendation, not a credential). Coverage is broader than Crossref alone (includes arXiv, IEEE, and most publishers' metadata in one place) and each result carries an open-access PDF link when one exists. CLI usage: python3 search_openalex.py "UAV magnetic compensation" --max 10 Importable: from search_openalex import search_openalex """ import sys import os import json import argparse import urllib.parse sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from http_utils import fetch OPENALEX_API = "https://api.openalex.org/works" SELECT_FIELDS = ",".join([ "doi", "title", "display_name", "publication_year", "authorships", "primary_location", "open_access", "cited_by_count", "abstract_inverted_index", ]) def _reconstruct_abstract(inverted_index): """OpenAlex returns abstracts as a word -> [positions] inverted index (for copyright reasons) instead of plain text; rebuild the plain text.""" if not inverted_index: return None positions = {} for word, idxs in inverted_index.items(): for i in idxs: positions[i] = word if not positions: return None return " ".join(positions[i] for i in sorted(positions)) def search_openalex(query, max_results=10, timeout=20): params = {"search": query, "per_page": max_results, "select": SELECT_FIELDS} mailto = os.environ.get("OPENALEX_MAILTO") if mailto: params["mailto"] = mailto url = f"{OPENALEX_API}?{urllib.parse.urlencode(params)}" data = json.loads(fetch(url, timeout=timeout)) results = [] for item in data.get("results", []) or []: title = item.get("title") or item.get("display_name") authors = [ a.get("author", {}).get("display_name") for a in (item.get("authorships") or []) if a.get("author") ] doi = item.get("doi") if doi and doi.startswith("https://doi.org/"): doi = doi[len("https://doi.org/"):] primary_location = item.get("primary_location") or {} source = primary_location.get("source") or {} open_access = item.get("open_access") or {} results.append({ "source": "openalex", "title": title, "authors": authors, "year": item.get("publication_year"), "venue": source.get("display_name"), "doi": doi, "arxiv_id": None, "abstract": _reconstruct_abstract(item.get("abstract_inverted_index")), "citation_count": item.get("cited_by_count"), "pdf_url": open_access.get("oa_url") or primary_location.get("pdf_url"), }) return results if __name__ == "__main__": ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("query") ap.add_argument("--max", type=int, default=10) args = ap.parse_args() try: out = search_openalex(args.query, args.max) print(json.dumps(out, ensure_ascii=False, indent=2)) except Exception as e: print(json.dumps({"error": str(e)}, ensure_ascii=False)) sys.exit(1)