History of inspire_hep
The changes to the source code of this entry in the repository, newest first, each with the differences it made to the entry's files. They are also listed on GitLab.
[entries] Add the date when each entry was added, and remove icon_hash beff7c9
The dates are those of the commits that first added the entries' files (2020-08-11 for the entries of the initial commit); renamed entries keep the date of their first name. Changed: none
diff --git a/entries/inspire_hep/__init__.py b/entries/inspire_hep/__init__.py
index 4b3980f..c87e491 100644
--- a/entries/inspire_hep/__init__.py
+++ b/entries/inspire_hep/__init__.py
@@ -54,7 +54,7 @@ url = ['https://inspirehep.net/',
'https://github.com/inspirehep/rest-api-doc']
citation = [('R. Ivanov, L. Raae, "INSPIRE: A new scientific information system for HEP." Journal of Physics: Conference Series 219, 082010 (2010)',
'https://doi.org/10.1088/1742-6596/219/8/082010')]
-icon_hash = None
+date_added = '2026-10-31'
upstream_license = ("CC0 (INSPIRE terms of use)",
"https://help.inspirehep.net/knowledge-base/terms-of-use/")
huge = True # the harvest takes several hours; run by hand on the server
[entries] Prefix the properties in the descriptions with (Node), (Edge) or (Graph) bc3dbbd
The lists of properties at the end of the descriptions marked edge properties with "Edge" but left node properties unmarked, or grouped them in "Nodes: `a`, ...; `b`, ..." items, so the kind of a property was often unclear. Every item now starts with (Node), (Edge) or (Graph), followed by the property names, a colon and the explanation, one item per property or group of properties; qualifications (kinds of nodes, networks) follow the names in parentheses. Properties that were only named got a short explanation, and a few items that named the wrong kind of property were corrected.
diff --git a/entries/inspire_hep/__init__.py b/entries/inspire_hep/__init__.py
index 2eb7a7e..4b3980f 100644
--- a/entries/inspire_hep/__init__.py
+++ b/entries/inspire_hep/__init__.py
@@ -37,11 +37,18 @@ The abstracts, keywords, e-mail addresses (whose download is not permitted by th
The networks have the following properties:
-- `is_author` (`authorships`): whether the node is an author (otherwise it is a record).
-- `name`: the record number (control number) of the literature record (its page is `https://inspirehep.net/literature/<name>`), or of the author profile (`https://inspirehep.net/authors/<name>`), or, for authors without a profile, empty.
-- Records: `title`, `date` (the earliest date of the record, as given, e.g. `1998-10-18` or `2020`), `document_type` (e.g. `article`, `conference paper`, `thesis`; several separated by `|`), `arxiv_eprint` (the arXiv identifier), `arxiv_categories` (separated by `|`), `doi`, `collaborations` (e.g. `ATLAS`, separated by `|`), `citation_count` (the number of citations according to INSPIRE).
-- Authors (`authorships`): `full_name` (as given in the first record of the author).
-- Edge `affiliations` (`authorships`): the affiliations of the author given in the record, separated by `|`."""
+- (Node) `is_author` (`authorships`): whether the node is an author (otherwise it is a record).
+- (Node) `name`: the record number (control number) of the literature record (its page is `https://inspirehep.net/literature/<name>`), or of the author profile (`https://inspirehep.net/authors/<name>`), or, for authors without a profile, empty.
+- (Node) `title` (records): the title of the record.
+- (Node) `date` (records): the earliest date of the record, as given, e.g. `1998-10-18` or `2020`.
+- (Node) `document_type` (records): the type of document (e.g. `article`, `conference paper`, `thesis`; several separated by `|`).
+- (Node) `arxiv_eprint` (records): the arXiv identifier.
+- (Node) `arxiv_categories` (records): the arXiv categories, separated by `|`.
+- (Node) `doi` (records): the DOI of the record.
+- (Node) `collaborations` (records): the collaborations (e.g. `ATLAS`), separated by `|`.
+- (Node) `citation_count` (records): the number of citations according to INSPIRE.
+- (Node) `full_name` (authors, in `authorships`): the name of the author, as given in the first record of the author.
+- (Edge) `affiliations` (`authorships`): the affiliations of the author given in the record, separated by `|`."""
tags = ['Informational', 'Citation', 'Collaboration', 'Bipartite', 'Unweighted', 'Metadata']
url = ['https://inspirehep.net/',
'https://github.com/inspirehep/rest-api-doc']
[entries] New entry: inspire_hep 5cb5ed8
diff --git a/entries/inspire_hep/__init__.py b/entries/inspire_hep/__init__.py
new file mode 100644
index 0000000..2eb7a7e
--- /dev/null
+++ b/entries/inspire_hep/__init__.py
@@ -0,0 +1,217 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2026 Tiago de Paula Peixoto <tiago@skewed.de>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import json
+import time
+import urllib.error
+import urllib.parse
+
+from .. import *
+import process_entry
+
+title = "INSPIRE-HEP high-energy physics literature: citations and authorships"
+description = r"""Citations and authorships of the literature of high-energy physics (particle physics, and related fields of astrophysics, nuclear physics, gravitation and mathematical physics) from INSPIRE-HEP, the information system of the field, run by CERN, DESY, Fermilab, IHEP, IN2P3 and SLAC (the successor of SPIRES, started in 1974). INSPIRE covers about 1.9 million articles, preprints, conference papers, theses and books, whose references are matched to the INSPIRE records, and whose authors are disambiguated (partly by the authors themselves through their INSPIRE profiles).
+
+The records are harvested from the INSPIRE REST API (by ranges of record numbers), so that the networks are of the date of the harvest; the API gives no versioned bulk dump.
+
+- `citations`: the citation network: a directed edge goes from a record to each record of INSPIRE that it cites (references not matched to an INSPIRE record are left out).
+- `authorships`: the undirected bipartite network of the records and their authors, where an edge links a record to each of its authors. Authors are identified by their INSPIRE author profile when the record links to one, and otherwise by their name as given (so that different persons with the same name are merged, and an author with several spellings is split).
+
+The abstracts, keywords, e-mail addresses (whose download is not permitted by the terms of use of INSPIRE) and other metadata are left out.
+
+The networks have the following properties:
+
+- `is_author` (`authorships`): whether the node is an author (otherwise it is a record).
+- `name`: the record number (control number) of the literature record (its page is `https://inspirehep.net/literature/<name>`), or of the author profile (`https://inspirehep.net/authors/<name>`), or, for authors without a profile, empty.
+- Records: `title`, `date` (the earliest date of the record, as given, e.g. `1998-10-18` or `2020`), `document_type` (e.g. `article`, `conference paper`, `thesis`; several separated by `|`), `arxiv_eprint` (the arXiv identifier), `arxiv_categories` (separated by `|`), `doi`, `collaborations` (e.g. `ATLAS`, separated by `|`), `citation_count` (the number of citations according to INSPIRE).
+- Authors (`authorships`): `full_name` (as given in the first record of the author).
+- Edge `affiliations` (`authorships`): the affiliations of the author given in the record, separated by `|`."""
+tags = ['Informational', 'Citation', 'Collaboration', 'Bipartite', 'Unweighted', 'Metadata']
+url = ['https://inspirehep.net/',
+ 'https://github.com/inspirehep/rest-api-doc']
+citation = [('R. Ivanov, L. Raae, "INSPIRE: A new scientific information system for HEP." Journal of Physics: Conference Series 219, 082010 (2010)',
+ 'https://doi.org/10.1088/1742-6596/219/8/082010')]
+icon_hash = None
+upstream_license = ("CC0 (INSPIRE terms of use)",
+ "https://help.inspirehep.net/knowledge-base/terms-of-use/")
+huge = True # the harvest takes several hours; run by hand on the server
+upstream_prefix = "https://inspirehep.net/api/literature"
+
+# The harvest: records by ranges of control numbers, one upstream file per
+# range (written when the range is complete, so that an interrupted harvest
+# continues where it stopped)
+_step = 10000
+_max_recid = 3400000 # (above the largest control number, 3.21 million, in 2026)
+_fields = ["control_number", "titles.title", "earliest_date", "document_type",
+ "arxiv_eprints", "references.record", "authors.full_name",
+ "authors.record", "authors.affiliations.value",
+ "collaborations.value", "citation_count", "dois.value"]
+_ranges = list(range(0, _max_recid, _step))
+_fnames = tuple(f"literature/{a:08d}.jsonl" for a in _ranges)
+files = [(_fnames, "citations", None), (_fnames, "authorships", None)]
+
+# Pause between requests (seconds), and contact in the User-Agent, as
+# INSPIRE asks of harvesters
+_pause = 2
+_headers = {"User-Agent": "netzschleuder postmaster@skewed.de"}
+
+def api_get(url, tries=8):
+ for i in range(tries):
+ time.sleep(_pause)
+ req = urllib.request.Request(url, headers=_headers)
+ try:
+ with urllib.request.urlopen(req, timeout=300) as r:
+ return json.load(r)
+ except urllib.error.HTTPError as e:
+ if e.code not in (429, 500, 502, 503, 504) or i == tries - 1:
+ raise
+ wait = e.headers.get("Retry-After")
+ time.sleep(int(wait) if wait and wait.isdigit() else 60 * (i + 1))
+ except (OSError, ValueError):
+ if i == tries - 1:
+ raise
+ time.sleep(60 * (i + 1))
+
+def harvest_range(a, b):
+ "The records with control numbers in [a, b)."
+ page = 1
+ out = []
+ while True:
+ q = urllib.parse.urlencode({"q": f"control_number:{a}->{b - 1}",
+ "size": 1000, "page": page,
+ "sort": "mostrecent",
+ "fields": ",".join(_fields)})
+ d = api_get(f"{upstream_prefix}?{q}")
+ hits = d["hits"]["hits"]
+ out.extend(h["metadata"] for h in hits)
+ if len(hits) < 1000 or len(out) >= d["hits"]["total"]:
+ return out
+ page += 1
+
+def fetch_upstream(force=False, ranges=None):
+ name = __name__.split(".")[-1]
+ if process_entry._check_health:
+ api_get(f"{upstream_prefix}?size=1&fields=control_number")
+ return
+ for a, fname in zip(_ranges, _fnames):
+ if ranges is not None and a not in ranges:
+ continue
+ if not force and upstream_file_exists(name, fname):
+ continue
+ records = harvest_range(a, a + _step)
+ with open_upstream_file(name, fname, "w") as f:
+ for r in records:
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
+
+def recid(ref):
+ "The record number of a {'$ref': .../<number>} link (-1 if none)."
+ try:
+ return int(ref["$ref"].rsplit("/", 1)[-1])
+ except (KeyError, TypeError, ValueError, AttributeError):
+ return -1
+
+# For checks only: if True, the ranges not harvested are skipped
+_check_partial = False
+
+def read_records(name, fnames):
+ for fname in fnames:
+ if _check_partial and not upstream_file_exists(name, fname):
+ continue
+ with open_upstream_file(name, fname, "r") as f:
+ for line in f:
+ yield json.loads(line)
+
+_rprops = ["title", "date", "document_type", "arxiv_eprint",
+ "arxiv_categories", "doi", "collaborations"]
+
+def record_props(r):
+ arxiv = r.get("arxiv_eprints") or [{}]
+ return dict(title=((r.get("titles") or [{}])[0].get("title") or ""),
+ date=r.get("earliest_date") or "",
+ document_type="|".join(r.get("document_type") or []),
+ arxiv_eprint=arxiv[0].get("value") or "",
+ arxiv_categories="|".join(arxiv[0].get("categories") or []),
+ doi=((r.get("dois") or [{}])[0].get("value") or ""),
+ collaborations="|".join(c.get("value", "") for c in
+ r.get("collaborations") or []))
+
+@cache_network()
+@coerce_props()
+@annotate()
+def parse(alts=None):
+ name = __name__.split(".")[-1]
+ for fnames, alt, fmt in files:
+ if alts is not None and alt not in alts:
+ continue
+ g = Graph(directed=alt == "citations")
+ g.vp.name = g.new_vp("string")
+ for k in _rprops:
+ g.vp[k] = g.new_vp("string")
+ g.vp.citation_count = g.new_vp("int")
+ if alt == "authorships":
+ g.vp.is_author = g.new_vp("bool")
+ g.vp.full_name = g.new_vp("string")
+ index = {}
+ authors = {}
+ edges, affs = [], []
+ def vertex(n):
+ v = index.get(n)
+ if v is None:
+ v = index[n] = int(g.add_vertex())
+ g.vp.name[v] = str(n)
+ g.vp.citation_count[v] = -1
+ return v
+ for r in read_records(name, fnames):
+ n = r.get("control_number")
+ if n is None:
+ continue
+ v = vertex(n)
+ for k, x in record_props(r).items():
+ if x:
+ g.vp[k][v] = x
+ g.vp.citation_count[v] = r.get("citation_count", -1)
+ if alt == "citations":
+ for ref in r.get("references") or []:
+ m = recid(ref.get("record"))
+ if m >= 0:
+ edges.append((v, vertex(m)))
+ else:
+ for a in r.get("authors") or []:
+ aid = recid(a.get("record"))
+ fn = a.get("full_name") or ""
+ key = aid if aid >= 0 else fn
+ u = authors.get(key)
+ if u is None:
+ u = authors[key] = int(g.add_vertex())
+ g.vp.is_author[u] = True
+ g.vp.citation_count[u] = -1
+ if aid >= 0:
+ g.vp.name[u] = str(aid)
+ g.vp.full_name[u] = fn
+ edges.append((v, u))
+ affs.append("|".join(x.get("value", "") for x in
+ a.get("affiliations") or []))
+ g.add_edge_list(edges)
+ if alt == "authorships":
+ p = g.new_ep("string")
+ for e, x in zip(g.edges(), affs):
+ if x:
+ p[e] = x
+ g.ep.affiliations = p
+ yield alt, g