erga 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- erga-0.1.0/.github/workflows/ci.yml +24 -0
- erga-0.1.0/.github/workflows/release.yml +39 -0
- erga-0.1.0/.gitignore +16 -0
- erga-0.1.0/CLAUDE.md +61 -0
- erga-0.1.0/LICENSE +21 -0
- erga-0.1.0/PKG-INFO +91 -0
- erga-0.1.0/README.md +70 -0
- erga-0.1.0/docs/requirements-v1.md +349 -0
- erga-0.1.0/docs/todo.md +22 -0
- erga-0.1.0/pyproject.toml +60 -0
- erga-0.1.0/src/erga/__init__.py +7 -0
- erga-0.1.0/src/erga/cli.py +96 -0
- erga-0.1.0/src/erga/config.py +139 -0
- erga-0.1.0/src/erga/crossref.py +50 -0
- erga-0.1.0/src/erga/curation.py +300 -0
- erga-0.1.0/src/erga/dedup.py +120 -0
- erga-0.1.0/src/erga/errors.py +13 -0
- erga-0.1.0/src/erga/http.py +108 -0
- erga-0.1.0/src/erga/model.py +100 -0
- erga-0.1.0/src/erga/normalize.py +132 -0
- erga-0.1.0/src/erga/openalex.py +167 -0
- erga-0.1.0/src/erga/output.py +71 -0
- erga-0.1.0/src/erga/pipeline.py +142 -0
- erga-0.1.0/src/erga/verify.py +59 -0
- erga-0.1.0/tests/conftest.py +43 -0
- erga-0.1.0/tests/fixtures/crossref/cracked.json +7 -0
- erga-0.1.0/tests/fixtures/golden/erga.yml +11 -0
- erga-0.1.0/tests/fixtures/golden/expected-publications.json +233 -0
- erga-0.1.0/tests/fixtures/golden/manual.yml +15 -0
- erga-0.1.0/tests/fixtures/golden/overrides.yml +7 -0
- erga-0.1.0/tests/fixtures/golden/previous-publications.json +11 -0
- erga-0.1.0/tests/fixtures/golden/tags.yml +4 -0
- erga-0.1.0/tests/fixtures/openalex/authors-a5000000002.json +6 -0
- erga-0.1.0/tests/fixtures/openalex/authors-orcid.json +11 -0
- erga-0.1.0/tests/fixtures/openalex/works-page1.json +137 -0
- erga-0.1.0/tests/fixtures/openalex/works-page2.json +186 -0
- erga-0.1.0/tests/test_config.py +106 -0
- erga-0.1.0/tests/test_crossref.py +45 -0
- erga-0.1.0/tests/test_curation.py +238 -0
- erga-0.1.0/tests/test_dedup.py +130 -0
- erga-0.1.0/tests/test_golden.py +93 -0
- erga-0.1.0/tests/test_http.py +58 -0
- erga-0.1.0/tests/test_model.py +57 -0
- erga-0.1.0/tests/test_normalize.py +165 -0
- erga-0.1.0/tests/test_openalex.py +143 -0
- erga-0.1.0/tests/test_output.py +35 -0
- erga-0.1.0/tests/test_package.py +5 -0
- erga-0.1.0/tests/test_verify.py +83 -0
- erga-0.1.0/uv.lock +622 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v7
|
|
17
|
+
- uses: astral-sh/setup-uv@v9.0.0
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- run: uv sync --locked
|
|
21
|
+
- run: uv run ruff check
|
|
22
|
+
- run: uv run ruff format --check
|
|
23
|
+
- run: uv run mypy
|
|
24
|
+
- run: uv run pytest
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v7
|
|
12
|
+
- uses: astral-sh/setup-uv@v9.0.0
|
|
13
|
+
- run: uv sync --locked
|
|
14
|
+
- run: uv run pytest
|
|
15
|
+
- name: Check tag matches package version
|
|
16
|
+
run: |
|
|
17
|
+
version="$(uv run python -c 'import erga; print(erga.__version__)')"
|
|
18
|
+
test "v${version}" = "${{ github.event.release.tag_name }}"
|
|
19
|
+
- run: uv build
|
|
20
|
+
- run: uvx twine check --strict dist/*
|
|
21
|
+
- uses: actions/upload-artifact@v4
|
|
22
|
+
with:
|
|
23
|
+
name: dist
|
|
24
|
+
path: dist/
|
|
25
|
+
|
|
26
|
+
publish:
|
|
27
|
+
needs: build
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
environment:
|
|
30
|
+
name: pypi
|
|
31
|
+
url: https://pypi.org/p/erga
|
|
32
|
+
permissions:
|
|
33
|
+
id-token: write
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/download-artifact@v4
|
|
36
|
+
with:
|
|
37
|
+
name: dist
|
|
38
|
+
path: dist/
|
|
39
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
erga-0.1.0/.gitignore
ADDED
erga-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# erga
|
|
2
|
+
|
|
3
|
+
Automated publications pipeline for academic websites. One config file lists
|
|
4
|
+
authors (ORCID iDs); erga fetches their works from OpenAlex, normalizes and
|
|
5
|
+
deduplicates them across registrars, applies the maintainer's curation files,
|
|
6
|
+
backfills venues from Crossref, and writes a canonical `publications.json`
|
|
7
|
+
for any static site to render. Delivered as a GitHub Action and a CLI.
|
|
8
|
+
|
|
9
|
+
Design principle: the fetch is disposable, the curated JSON is the durable,
|
|
10
|
+
reviewable artifact. Curation (manual additions, per-record overrides,
|
|
11
|
+
highlights) lives in separate files that survive every automated refresh.
|
|
12
|
+
|
|
13
|
+
## Status
|
|
14
|
+
|
|
15
|
+
Pre-0.1. The pipeline is implemented end-to-end (config, fetch, normalize,
|
|
16
|
+
dedup, curation, Crossref backfill, deterministic output, `build`/`verify`
|
|
17
|
+
CLI) with unit suites plus a byte-exact golden test, and validated against
|
|
18
|
+
live OpenAlex: smoke-tested on real author profiles and run in parallel
|
|
19
|
+
with the origin lab site's embedded pipeline to full convergence (187/187
|
|
20
|
+
records, no field diffs, 2026-08-05). Not yet released. See
|
|
21
|
+
`docs/requirements-v1.md` for the v1 design and `docs/todo.md` for open
|
|
22
|
+
work.
|
|
23
|
+
|
|
24
|
+
Module map (`src/erga/`): `config` (erga.yml), `http` (injectable transport
|
|
25
|
+
+ retry), `openalex`/`crossref` (clients), `normalize` (raw work → canonical
|
|
26
|
+
record), `dedup` (DOI + title clustering), `curation` (manual/overrides/
|
|
27
|
+
tags), `pipeline` (stage orchestration), `output` (deterministic JSON),
|
|
28
|
+
`verify` (disambiguation report), `cli`.
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
- `uv sync`: install the dev environment
|
|
33
|
+
- `uv run pytest`: run tests
|
|
34
|
+
- `uv run ruff check` / `uv run ruff format`: lint / format
|
|
35
|
+
- `uv run mypy`: type-check (strict)
|
|
36
|
+
|
|
37
|
+
## Layout
|
|
38
|
+
|
|
39
|
+
- `src/erga/`: package source (src layout)
|
|
40
|
+
- `tests/`: pytest suite; fixtures are handcrafted records plus recorded
|
|
41
|
+
OpenAlex responses (synthetic or CC0 data only, never real curated
|
|
42
|
+
personal data)
|
|
43
|
+
- `docs/`: design docs and `todo.md`
|
|
44
|
+
- `local/`: gitignored scratch space (session notes, reference material).
|
|
45
|
+
Session continuity lives here: `local/next-session-prompt.md`, never a
|
|
46
|
+
tracked `feedback/` — public-from-commit-1 discipline, no session state
|
|
47
|
+
in the repo or its history
|
|
48
|
+
|
|
49
|
+
## Conventions
|
|
50
|
+
|
|
51
|
+
- Python ≥ 3.10; CI runs the matrix 3.10–3.13 on GitHub Actions
|
|
52
|
+
- uv manages the environment and lockfile; ruff lints and formats; mypy is
|
|
53
|
+
strict; all four checks must pass in CI
|
|
54
|
+
- Releases: GitHub Releases only, no CHANGELOG file; semver from 0.x
|
|
55
|
+
(v1.0 = the JSON schema is declared stable)
|
|
56
|
+
- PyPI publishing via Trusted Publishing (OIDC) from a release workflow;
|
|
57
|
+
no stored tokens
|
|
58
|
+
- Config samples, docs, and fixtures use placeholder mailto/ORCID values,
|
|
59
|
+
never real contact details
|
|
60
|
+
- v1 non-goals: no rendering or UI, no Google Scholar scraping, no database,
|
|
61
|
+
no hosted service, no sources beyond OpenAlex + manual entries
|
erga-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas Kogias
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
erga-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: erga
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Keep a website's academic publications list current: fetch from OpenAlex, deduplicate across registrars, apply curation that survives every refresh.
|
|
5
|
+
Project-URL: Repository, https://github.com/belalik/erga
|
|
6
|
+
Author: Thomas Kogias
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: academic,bibliography,openalex,orcid,publications,static-site
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: pyyaml>=6
|
|
19
|
+
Requires-Dist: requests>=2.32
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# erga
|
|
23
|
+
|
|
24
|
+
Keep a website's academic publications list current, automatically, without
|
|
25
|
+
giving up control of the data.
|
|
26
|
+
|
|
27
|
+
**Status: alpha (v0.1).** The CLI pipeline works end-to-end and its
|
|
28
|
+
output has converged with a production lab site's existing pipeline in a
|
|
29
|
+
parallel run against live OpenAlex (187/187 records, zero field diffs).
|
|
30
|
+
The JSON schema may still change before v1.0.
|
|
31
|
+
|
|
32
|
+
## What it does
|
|
33
|
+
|
|
34
|
+
You list your authors (ORCID iDs) in one config file. erga fetches their works
|
|
35
|
+
from OpenAlex, normalizes and deduplicates them across registrars (arXiv,
|
|
36
|
+
Zenodo, publisher records), applies your curation files, and writes a
|
|
37
|
+
canonical `publications.json` into your site repository. Your site (Jekyll,
|
|
38
|
+
Astro, Hugo, anything) renders it however it likes.
|
|
39
|
+
|
|
40
|
+
- **Curation that survives refresh**: manual additions, per-record overrides,
|
|
41
|
+
and highlights live in their own files and are re-applied on every
|
|
42
|
+
automated run.
|
|
43
|
+
- **Proper APIs, no scraping**: OpenAlex (CC0 data) plus Crossref venue
|
|
44
|
+
backfill, with API etiquette built in (keys, delays, retries).
|
|
45
|
+
- **Git-owned data**: the output is a diffable, PR-reviewable file in your
|
|
46
|
+
repo: no hosted embed, no runtime dependency, publications present in the
|
|
47
|
+
initial HTML.
|
|
48
|
+
- **Delivery**: a pip-installable CLI today; GitHub Action packaging is
|
|
49
|
+
planned once the CLI is proven on consumer sites.
|
|
50
|
+
|
|
51
|
+
The name: έργα, "works" — the same term OpenAlex uses for publications.
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
Install with `uv tool install erga` or `pip install erga` (or run one-off
|
|
56
|
+
with `uvx erga`). Write an `erga.yml`:
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
mailto: you@example.org # identifies requests to Crossref/OpenAlex
|
|
60
|
+
authors:
|
|
61
|
+
- name: Josiah Carberry
|
|
62
|
+
orcid: 0000-0002-1825-0097
|
|
63
|
+
- name: Another Person
|
|
64
|
+
openalex_id: A5000000000 # alternative when ORCID is missing/wrong
|
|
65
|
+
|
|
66
|
+
openalex:
|
|
67
|
+
api_key_env: OPENALEX_API_KEY # optional; env var name, never the key itself
|
|
68
|
+
|
|
69
|
+
output:
|
|
70
|
+
path: publications.json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then:
|
|
74
|
+
|
|
75
|
+
- `erga build [--config PATH] [--dry-run]` runs the pipeline and writes
|
|
76
|
+
`publications.json`. With `--dry-run` it prints a summary (fetched, merged,
|
|
77
|
+
deduplicated, excluded, backfilled) without writing.
|
|
78
|
+
- `erga verify [--config PATH]` prints the author-disambiguation report:
|
|
79
|
+
what each configured author resolves to on OpenAlex, with warnings for
|
|
80
|
+
split profiles, zero-work authors, and implausible works counts. Run it
|
|
81
|
+
once when setting up, and whenever a build looks off.
|
|
82
|
+
|
|
83
|
+
Three optional curation files next to the config survive every refresh:
|
|
84
|
+
`manual.yml` (records the APIs miss), `overrides.yml` (per-record patches,
|
|
85
|
+
exclusions, dedup exemptions), and `tags.yml` (tag name to DOI/id lists;
|
|
86
|
+
tag semantics are entirely yours). The full schema and pipeline design live
|
|
87
|
+
in [docs/requirements-v1.md](docs/requirements-v1.md).
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
erga-0.1.0/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# erga
|
|
2
|
+
|
|
3
|
+
Keep a website's academic publications list current, automatically, without
|
|
4
|
+
giving up control of the data.
|
|
5
|
+
|
|
6
|
+
**Status: alpha (v0.1).** The CLI pipeline works end-to-end and its
|
|
7
|
+
output has converged with a production lab site's existing pipeline in a
|
|
8
|
+
parallel run against live OpenAlex (187/187 records, zero field diffs).
|
|
9
|
+
The JSON schema may still change before v1.0.
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
You list your authors (ORCID iDs) in one config file. erga fetches their works
|
|
14
|
+
from OpenAlex, normalizes and deduplicates them across registrars (arXiv,
|
|
15
|
+
Zenodo, publisher records), applies your curation files, and writes a
|
|
16
|
+
canonical `publications.json` into your site repository. Your site (Jekyll,
|
|
17
|
+
Astro, Hugo, anything) renders it however it likes.
|
|
18
|
+
|
|
19
|
+
- **Curation that survives refresh**: manual additions, per-record overrides,
|
|
20
|
+
and highlights live in their own files and are re-applied on every
|
|
21
|
+
automated run.
|
|
22
|
+
- **Proper APIs, no scraping**: OpenAlex (CC0 data) plus Crossref venue
|
|
23
|
+
backfill, with API etiquette built in (keys, delays, retries).
|
|
24
|
+
- **Git-owned data**: the output is a diffable, PR-reviewable file in your
|
|
25
|
+
repo: no hosted embed, no runtime dependency, publications present in the
|
|
26
|
+
initial HTML.
|
|
27
|
+
- **Delivery**: a pip-installable CLI today; GitHub Action packaging is
|
|
28
|
+
planned once the CLI is proven on consumer sites.
|
|
29
|
+
|
|
30
|
+
The name: έργα, "works" — the same term OpenAlex uses for publications.
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
Install with `uv tool install erga` or `pip install erga` (or run one-off
|
|
35
|
+
with `uvx erga`). Write an `erga.yml`:
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
mailto: you@example.org # identifies requests to Crossref/OpenAlex
|
|
39
|
+
authors:
|
|
40
|
+
- name: Josiah Carberry
|
|
41
|
+
orcid: 0000-0002-1825-0097
|
|
42
|
+
- name: Another Person
|
|
43
|
+
openalex_id: A5000000000 # alternative when ORCID is missing/wrong
|
|
44
|
+
|
|
45
|
+
openalex:
|
|
46
|
+
api_key_env: OPENALEX_API_KEY # optional; env var name, never the key itself
|
|
47
|
+
|
|
48
|
+
output:
|
|
49
|
+
path: publications.json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then:
|
|
53
|
+
|
|
54
|
+
- `erga build [--config PATH] [--dry-run]` runs the pipeline and writes
|
|
55
|
+
`publications.json`. With `--dry-run` it prints a summary (fetched, merged,
|
|
56
|
+
deduplicated, excluded, backfilled) without writing.
|
|
57
|
+
- `erga verify [--config PATH]` prints the author-disambiguation report:
|
|
58
|
+
what each configured author resolves to on OpenAlex, with warnings for
|
|
59
|
+
split profiles, zero-work authors, and implausible works counts. Run it
|
|
60
|
+
once when setting up, and whenever a build looks off.
|
|
61
|
+
|
|
62
|
+
Three optional curation files next to the config survive every refresh:
|
|
63
|
+
`manual.yml` (records the APIs miss), `overrides.yml` (per-record patches,
|
|
64
|
+
exclusions, dedup exemptions), and `tags.yml` (tag name to DOI/id lists;
|
|
65
|
+
tag semantics are entirely yours). The full schema and pipeline design live
|
|
66
|
+
in [docs/requirements-v1.md](docs/requirements-v1.md).
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# erga v1 Requirements
|
|
2
|
+
|
|
3
|
+
Status: adopted 2026-08-03. Maintained as a living spec: current state only,
|
|
4
|
+
superseded content is replaced rather than appended. Product identity and
|
|
5
|
+
scope were fixed at kickoff; this document records the research-backed design
|
|
6
|
+
decisions and the concrete v1 surface. Sources: OpenAlex API documentation and live API
|
|
7
|
+
verification (2026-08-03), CSL-JSON schema analysis, GitHub Action delivery
|
|
8
|
+
research, and an audit of the origin pipeline (a production Jekyll lab site
|
|
9
|
+
that this tool generalizes).
|
|
10
|
+
|
|
11
|
+
## 1. Purpose
|
|
12
|
+
|
|
13
|
+
erga keeps a website's academic publications list current while the
|
|
14
|
+
maintainer stays in control of the data. One config file lists authors
|
|
15
|
+
(ORCID iDs). The tool fetches their works from OpenAlex, normalizes and
|
|
16
|
+
deduplicates them, applies curation files that survive every refresh,
|
|
17
|
+
backfills missing venues from Crossref, and writes a canonical
|
|
18
|
+
`publications.json` into the site repository. The site renders it however it
|
|
19
|
+
likes; erga renders nothing.
|
|
20
|
+
|
|
21
|
+
The fetch is disposable. The curated JSON is the durable, reviewable
|
|
22
|
+
artifact.
|
|
23
|
+
|
|
24
|
+
## 2. Decisions on the kickoff open questions
|
|
25
|
+
|
|
26
|
+
### 2.1 Canonical schema: own minimal JSON, with emitters later
|
|
27
|
+
|
|
28
|
+
CSL-JSON was evaluated as a candidate canonical format and rejected:
|
|
29
|
+
|
|
30
|
+
- Five fields this tool needs most have no first-class CSL-JSON home:
|
|
31
|
+
open-access URL, citation count, ORCID on author names (the name schema
|
|
32
|
+
forbids extra properties), keyword arrays (CSL `keyword` is a single
|
|
33
|
+
string), and curation flags. All would land in the unstructured `custom`
|
|
34
|
+
bag.
|
|
35
|
+
- No `preprint` item type exists in CSL 1.0.2.
|
|
36
|
+
- CSL dates are `date-parts` nested arrays; Jekyll, Astro, and Hugo
|
|
37
|
+
templates all want plain ISO strings and would each need glue code.
|
|
38
|
+
- The Jekyll academic ecosystem (jekyll-scholar, al-folio) consumes BibTeX,
|
|
39
|
+
not CSL-JSON, so canonical CSL-JSON would buy no adoption there anyway.
|
|
40
|
+
- OpenAlex offers no native CSL-JSON output and no reusable mapping library
|
|
41
|
+
exists in Python, so the mapping is hand-written under either choice.
|
|
42
|
+
|
|
43
|
+
Decision: a purpose-built minimal schema (section 4) is canonical.
|
|
44
|
+
CSL-JSON and BibTeX emitters come after v0.1 as strategic outputs that turn
|
|
45
|
+
the existing theme ecosystem into potential adopters. v1.0 means the
|
|
46
|
+
canonical schema is declared stable.
|
|
47
|
+
|
|
48
|
+
### 2.2 Featured/tags: generalize to tags
|
|
49
|
+
|
|
50
|
+
The origin pipeline had a `featured` boolean fed by a flat DOI list. v1
|
|
51
|
+
generalizes this to a single mechanism: every record carries `tags`
|
|
52
|
+
(list of strings), and a curation file maps tag names to DOI/id lists.
|
|
53
|
+
Manual entries may declare their own tags inline. Tag names carry no
|
|
54
|
+
semantics for erga: sites decide what a tag means and whether it exists at
|
|
55
|
+
all. A "featured" highlight list is one pattern a site can implement (the
|
|
56
|
+
origin site does); the docs present it as an example, never a default.
|
|
57
|
+
|
|
58
|
+
### 2.3 Action output mode: compose, do not embed
|
|
59
|
+
|
|
60
|
+
Research across comparable data-updating Actions shows two viable patterns:
|
|
61
|
+
built-in output-mode switches (lowlighter/metrics) and composition with
|
|
62
|
+
single-purpose commit/PR actions (the dominant pattern for thin tool
|
|
63
|
+
wrappers). Decision: the erga Action only produces the output file and
|
|
64
|
+
exits. Delivery is composed in the user's workflow, and the docs ship two
|
|
65
|
+
copy-paste recipes:
|
|
66
|
+
|
|
67
|
+
1. Commit-back inside the site's existing build workflow (default recipe):
|
|
68
|
+
run erga, commit the JSON if changed, then build and deploy in the same
|
|
69
|
+
run. Production-proven by the origin site; sidesteps the GITHUB_TOKEN
|
|
70
|
+
restriction that bot pushes never trigger downstream workflows, because
|
|
71
|
+
the build happens in the same workflow.
|
|
72
|
+
2. Pull-request mode (cautious recipe): peter-evans/create-pull-request.
|
|
73
|
+
Repeated runs update one branch and PR; quiet weeks produce no noise;
|
|
74
|
+
merges are normal pushes so separately-triggered deploy workflows fire.
|
|
75
|
+
Right default for maintainers who want a review gate, and the only clean
|
|
76
|
+
path for protected branches.
|
|
77
|
+
|
|
78
|
+
Workflow guidance the docs must include: declare `permissions` explicitly,
|
|
79
|
+
use a concurrency group, schedule cron off the top of the hour, and note
|
|
80
|
+
GitHub's 60-day auto-disable of scheduled workflows on inactive repos.
|
|
81
|
+
|
|
82
|
+
### 2.4 Department scale: batching, cursor pagination, cost budget
|
|
83
|
+
|
|
84
|
+
For 10 to 100 authors and thousands of works per run:
|
|
85
|
+
|
|
86
|
+
- Batch authors with the OR-pipe filter (`author.id:A1|A2|...`), at most
|
|
87
|
+
100 values per filter, and deduplicate fetched works by OpenAlex id
|
|
88
|
+
(co-authored works arrive once per matching batch).
|
|
89
|
+
- `per-page=100` with cursor pagination (`cursor=*`, then
|
|
90
|
+
`meta.next_cursor`). Basic paging caps at 10,000 results; cursor does not.
|
|
91
|
+
The live API still accepts `per-page=200` but the documented maximum is
|
|
92
|
+
100; the spec uses 100.
|
|
93
|
+
- Trim payloads with `select=` (root-level fields only).
|
|
94
|
+
- Cost: a 100-author department at roughly 10,000 works is on the order of
|
|
95
|
+
100 to 200 list calls per run, about $0.02 against the $1/day allowance of
|
|
96
|
+
a free API key. A small lab fits even the keyless $0.10/day allowance.
|
|
97
|
+
Runtime is bounded by politeness delays, not data volume.
|
|
98
|
+
|
|
99
|
+
## 3. OpenAlex operational facts (verified 2026-08-03)
|
|
100
|
+
|
|
101
|
+
The landscape changed materially in 2025-2026; these facts supersede the
|
|
102
|
+
founding documents where they conflict.
|
|
103
|
+
|
|
104
|
+
- **API keys are the access model** (since 2026-02-13). The mailto "polite
|
|
105
|
+
pool" no longer affects OpenAlex rate limits. Free key: $1/day usage
|
|
106
|
+
allowance. Keyless: $0.10/day. List/filter calls cost $0.0001. Verified
|
|
107
|
+
live: keyless requests succeed and return `x-ratelimit-limit-usd: 0.1`
|
|
108
|
+
headers. Hard throttle 100 req/s. The key is passed as `api_key` query
|
|
109
|
+
parameter; erga reads it from an env var and must never write it to disk.
|
|
110
|
+
Keyless quota can only be tracked per IP, and GitHub-hosted runners share
|
|
111
|
+
IPs, so CI runs must not rely on the keyless allowance; the Action docs
|
|
112
|
+
treat a free key as required setup.
|
|
113
|
+
Crossref, unlike OpenAlex, still operates a mailto polite pool, so the
|
|
114
|
+
config mailto remains first-class for the venue-backfill stage.
|
|
115
|
+
- **XPAC subset**: the Nov 2025 backend rewrite added ~190M works (DataCite,
|
|
116
|
+
institutional repositories) that are excluded from queries by default;
|
|
117
|
+
`include_xpac=true` opts in, and works carry an `is_xpac` field (verified
|
|
118
|
+
live). Default off in erga, configurable, because XPAC metadata quality is
|
|
119
|
+
explicitly lower and a publications page wants precision over recall.
|
|
120
|
+
Revisit with department-scale evidence.
|
|
121
|
+
- **Type vocabulary drifts**: a July 2026 reclassification changed `type` on
|
|
122
|
+
~10% of the catalog and added first-class `conference-paper`, `software`
|
|
123
|
+
and `software-paper`; the classifier re-runs daily. `type_crossref` and
|
|
124
|
+
`raw_type` are absent from live responses (verified). Consequence: rely
|
|
125
|
+
on `type` alone, expect drift, and make drift visible — a raw type that
|
|
126
|
+
is neither mapped nor deliberately "other" raises a build warning
|
|
127
|
+
(a silent catch-all misfiled sixty conference papers on the origin site
|
|
128
|
+
for months). `software-paper` at a journal source is classified `journal`:
|
|
129
|
+
it is a peer-reviewed article about software (SoftwareX, JOSS), not a
|
|
130
|
+
software artifact. Gotcha, should stats ever use it: `group_by=type`
|
|
131
|
+
returns full URI keys (`https://openalex.org/types/...`) while `type` on
|
|
132
|
+
works is the bare string.
|
|
133
|
+
- **OpenAlex already merges many cross-registrar copies**: a single work can
|
|
134
|
+
carry multiple `locations[]` (publisher, DOAJ, Zenodo deposits) with the
|
|
135
|
+
top-level DOI pointing at the published version (verified live on a work
|
|
136
|
+
with 5 locations). erga's own dedup remains necessary for what OpenAlex
|
|
137
|
+
misses, for manual entries, and as a guard, but it is a second line of
|
|
138
|
+
defense rather than the only one.
|
|
139
|
+
- **Abstracts** still arrive as `abstract_inverted_index`; standard
|
|
140
|
+
positional reconstruction is unchanged. Coverage is uneven and skews
|
|
141
|
+
recent.
|
|
142
|
+
- **Authorships** carry `author.id`, `display_name`, `orcid`, plus
|
|
143
|
+
`raw_author_name`; only ~30% of recent works have publisher-asserted
|
|
144
|
+
ORCID data, so ORCID cannot be the only identity signal. `is_retracted`
|
|
145
|
+
is available and reliable (Retraction Watch data).
|
|
146
|
+
|
|
147
|
+
## 4. Canonical output schema
|
|
148
|
+
|
|
149
|
+
Top-level object, not a bare array, so the schema version has a home:
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"schema_version": 1,
|
|
154
|
+
"works": [ ... ]
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Per work, all keys always present:
|
|
159
|
+
|
|
160
|
+
| field | type | notes |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| `id` | string | OpenAlex work id without host (`"W4406028178"`), or `"manual-<slug>"` |
|
|
163
|
+
| `title` | string | |
|
|
164
|
+
| `authors` | array | `{ "name": str, "orcid": str\|null, "tracked": bool }`; `tracked` = matches a configured author by resolved OpenAlex id, ORCID, or name/alias |
|
|
165
|
+
| `year` | int \| null | |
|
|
166
|
+
| `date` | string \| null | ISO publication date `"2026-01-15"` |
|
|
167
|
+
| `venue` | string \| null | null when unknown (origin pipeline used `""`) |
|
|
168
|
+
| `type` | string | `journal`, `conference`, `book`, `book-chapter`, `thesis`, `preprint`, `dataset`, `software`, `other` |
|
|
169
|
+
| `doi` | string \| null | full `https://doi.org/...` URL |
|
|
170
|
+
| `cited_by_count` | int | |
|
|
171
|
+
| `abstract` | string \| null | reconstructed plaintext |
|
|
172
|
+
| `open_access` | object \| null | `{ "url": str }`; object form leaves room for license/version later |
|
|
173
|
+
| `tags` | array of string | from the tags curation file and manual entries |
|
|
174
|
+
| `is_retracted` | bool | |
|
|
175
|
+
| `source` | string | `"openalex"` or `"manual"` |
|
|
176
|
+
|
|
177
|
+
Output is deterministic: sorted by year descending then id, UTF-8,
|
|
178
|
+
2-space indent, `ensure_ascii=False`, trailing newline. Unchanged inputs
|
|
179
|
+
produce a byte-identical file, so "did anything change" is exactly
|
|
180
|
+
`git diff`.
|
|
181
|
+
|
|
182
|
+
Schema changes vs the origin pipeline (its consumer migrates with a
|
|
183
|
+
template tweak during the parallel run): `featured` boolean replaced by
|
|
184
|
+
`tags`, `is_lab_member` renamed `tracked`, empty-string venue becomes null,
|
|
185
|
+
`date` and `is_retracted` added, `dissertation` renamed `thesis`, `software`
|
|
186
|
+
type added, top-level wrapper added.
|
|
187
|
+
|
|
188
|
+
## 5. Configuration
|
|
189
|
+
|
|
190
|
+
One YAML file, default `erga.yml`. All examples use placeholder contacts
|
|
191
|
+
(0000-0002-1825-0097 is ORCID's fictitious researcher Josiah Carberry).
|
|
192
|
+
|
|
193
|
+
```yaml
|
|
194
|
+
mailto: you@example.org # identifies requests to Crossref/OpenAlex
|
|
195
|
+
authors:
|
|
196
|
+
- name: Josiah Carberry
|
|
197
|
+
orcid: 0000-0002-1825-0097
|
|
198
|
+
aliases: ["J. S. Carberry"] # optional, for matching manual entries
|
|
199
|
+
- name: Another Person
|
|
200
|
+
openalex_id: A5000000000 # alternative when ORCID is missing/wrong
|
|
201
|
+
- name: Third Person # no ids at all: tracked by name only
|
|
202
|
+
|
|
203
|
+
openalex:
|
|
204
|
+
api_key_env: OPENALEX_API_KEY # optional; env var name, never the key itself
|
|
205
|
+
include_xpac: false
|
|
206
|
+
|
|
207
|
+
output:
|
|
208
|
+
path: publications.json
|
|
209
|
+
|
|
210
|
+
curation: # optional; defaults shown, relative to config
|
|
211
|
+
manual: manual.yml
|
|
212
|
+
overrides: overrides.yml
|
|
213
|
+
tags: tags.yml
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Author resolution: ORCID resolves via the OpenAlex authors endpoint
|
|
217
|
+
(singleton lookups are free). An author entry may pin `openalex_id`
|
|
218
|
+
explicitly, and both may coexist (some profiles are split across multiple
|
|
219
|
+
OpenAlex author IDs). An entry with neither id is a tracking-only author:
|
|
220
|
+
it contributes its name and aliases to the `tracked` flag and to manual-
|
|
221
|
+
entry matching but resolves and fetches nothing (for authors without any
|
|
222
|
+
registrar identity, or whose works OpenAlex misassigns to a conflated
|
|
223
|
+
homonym profile that must not be fetched).
|
|
224
|
+
|
|
225
|
+
## 6. Curation files
|
|
226
|
+
|
|
227
|
+
All three survive every refresh; a missing file means "none".
|
|
228
|
+
|
|
229
|
+
- **`manual.yml`**: list of records the APIs miss. Fields mirror the output
|
|
230
|
+
schema loosely: `title`, `authors` (string or list), `venue`, `year`,
|
|
231
|
+
`doi`, `type`, `tags`. Authors are matched to configured authors by
|
|
232
|
+
name/alias for the `tracked` flag.
|
|
233
|
+
- **`overrides.yml`**: list of patches keyed by `doi` (case-insensitive) or
|
|
234
|
+
`id`. Any other key overwrites that field on the merged record. Special
|
|
235
|
+
keys: `exclude: true` drops the record; `keep_distinct: true` exempts it
|
|
236
|
+
from title clustering. A field patch that no longer changes anything
|
|
237
|
+
(upstream caught up) raises a build warning, measured against the
|
|
238
|
+
pre-patch record — measuring against the output would be circular. The
|
|
239
|
+
warning is information, not an instruction to delete: a redundant
|
|
240
|
+
override may stay as insurance against upstream regressing.
|
|
241
|
+
- **`tags.yml`**: mapping of tag name to list of DOIs/ids:
|
|
242
|
+
|
|
243
|
+
```yaml
|
|
244
|
+
featured:
|
|
245
|
+
- https://doi.org/10.5555/12345678
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Tag names are arbitrary; "featured" above is only an example.
|
|
249
|
+
|
|
250
|
+
## 7. Pipeline stages
|
|
251
|
+
|
|
252
|
+
Ported from the production origin pipeline with generalization deltas noted.
|
|
253
|
+
|
|
254
|
+
1. Load config and curation files.
|
|
255
|
+
2. Resolve authors to OpenAlex author IDs (section 5).
|
|
256
|
+
3. Fetch works: OR-pipe author batches, `select=` trimmed fields,
|
|
257
|
+
`per-page=100`, cursor pagination, retry with backoff on 429/5xx,
|
|
258
|
+
politeness delay between calls. Deduplicate by work id. Any fetch
|
|
259
|
+
failure aborts the run without touching existing output; a transient
|
|
260
|
+
API failure must never shrink a published list.
|
|
261
|
+
4. Normalize to the canonical schema: type mapping, abstract
|
|
262
|
+
reconstruction, OA URL from `best_oa_location`/`open_access.oa_url`,
|
|
263
|
+
author `tracked` flags.
|
|
264
|
+
5. Merge manual entries; their DOIs seed the dedup set so manual always
|
|
265
|
+
wins.
|
|
266
|
+
6. DOI-level dedup, case-insensitive.
|
|
267
|
+
7. Title-cluster dedup. Normalize (NFKD, lowercase, fold dash variants,
|
|
268
|
+
strip non-alphanumerics, collapse whitespace); group by
|
|
269
|
+
(normalized title, is-dataset) so datasets never merge with papers;
|
|
270
|
+
titles under 12 normalized characters and `keep_distinct` records bypass
|
|
271
|
+
clustering. Rank within a cluster: manual first, then version-of-record
|
|
272
|
+
over repository deposits (known repository DOI prefixes: arXiv, Zenodo,
|
|
273
|
+
figshare, Research Square, bio/medRxiv, SSRN, OSF, Fraunhofer publica,
|
|
274
|
+
and `preprint` type), then has-DOI, then citation count, then newest
|
|
275
|
+
OpenAlex record (numeric W-id; publication dates deliberately play no
|
|
276
|
+
part — within a same-title cluster they differ by deposit-version
|
|
277
|
+
artifacts and favor the wrong copies). The winner inherits `abstract`
|
|
278
|
+
and `open_access` from absorbed copies when it lacks them.
|
|
279
|
+
8. Apply overrides (patch or exclude).
|
|
280
|
+
9. Crossref venue backfill with the last-known-good ratchet: reuse venues
|
|
281
|
+
from the previous output first, then query Crossref (polite mailto
|
|
282
|
+
User-Agent) only for records still lacking one; DataCite DOIs 404 there
|
|
283
|
+
and are skipped silently.
|
|
284
|
+
10. Apply tags.
|
|
285
|
+
11. Sort deterministically and write.
|
|
286
|
+
|
|
287
|
+
## 8. CLI
|
|
288
|
+
|
|
289
|
+
`erga` console entry point, two subcommands in v1:
|
|
290
|
+
|
|
291
|
+
- `erga build [--config PATH] [--dry-run]`: run the pipeline. `--dry-run`
|
|
292
|
+
prints a summary (fetched, merged, deduplicated, excluded, backfilled)
|
|
293
|
+
without writing. Exit 0 on success (changed or not; change detection is
|
|
294
|
+
git's job), nonzero on any failure.
|
|
295
|
+
- `erga verify [--config PATH]`: the author-disambiguation report, a
|
|
296
|
+
first-class feature because OpenAlex author IDs split and conflate
|
|
297
|
+
people. Per configured author: resolved ID(s), works count, name
|
|
298
|
+
variants, most recent titles; warnings for ORCIDs resolving to multiple
|
|
299
|
+
author IDs, zero-work authors, and implausible works counts.
|
|
300
|
+
|
|
301
|
+
Python >= 3.10. Runtime dependencies: `requests` and `PyYAML` only.
|
|
302
|
+
|
|
303
|
+
## 9. GitHub Action
|
|
304
|
+
|
|
305
|
+
A composite action in this repo (`action.yml`): pinned `setup-uv`, then
|
|
306
|
+
`uvx erga==<version> build`. Inputs: `config` (path), `version`. No commit
|
|
307
|
+
or PR logic inside the action (section 2.3). Full semver tags plus a moving
|
|
308
|
+
`v1` major tag, actions/checkout convention. The README pitch stays "one
|
|
309
|
+
workflow file plus one config file", with the two delivery recipes.
|
|
310
|
+
|
|
311
|
+
Action packaging lands after the CLI is proven (see milestones); the
|
|
312
|
+
consumer sites can run the CLI directly in their workflows meanwhile.
|
|
313
|
+
|
|
314
|
+
## 10. Testing and fixtures
|
|
315
|
+
|
|
316
|
+
- Two-tier fixtures, per the scaffold decisions: small handcrafted records
|
|
317
|
+
exercising dedup/curation logic (they double as documentation of the
|
|
318
|
+
ranking rules), plus a few recorded OpenAlex responses for the fetch
|
|
319
|
+
layer. Synthetic or clearly-public CC0 data only; never the origin
|
|
320
|
+
site's curated real-people data.
|
|
321
|
+
- The fetch layer takes an injectable transport so recorded fixtures need
|
|
322
|
+
no HTTP mocking library.
|
|
323
|
+
- One end-to-end golden test: fixture config plus recorded responses in,
|
|
324
|
+
byte-exact `publications.json` out.
|
|
325
|
+
- CI: ruff, ruff format, mypy strict, pytest across Python 3.10-3.13
|
|
326
|
+
(already in place).
|
|
327
|
+
|
|
328
|
+
## 11. Milestones
|
|
329
|
+
|
|
330
|
+
- **v0.1**: CLI end-to-end (config in, correct curated JSON out), tested,
|
|
331
|
+
documented. No Action, no emitters.
|
|
332
|
+
- **v0.2**: consumer #1, the origin Jekyll lab site (14 authors), runs erga
|
|
333
|
+
in parallel with its embedded pipeline until outputs match, then
|
|
334
|
+
switches.
|
|
335
|
+
- **v0.3**: GitHub Action packaging; consumer #2, an Astro 5 department
|
|
336
|
+
site (tens of authors), stress-tests scale and disambiguation.
|
|
337
|
+
- **CSL-JSON and BibTeX emitters** slot in after v0.1 as demand warrants,
|
|
338
|
+
before the public flip.
|
|
339
|
+
- **Public + v1.0**: strong README (before/after dedup story, head-on
|
|
340
|
+
"why not BibBase" answer), schema declared stable, PyPI via Trusted
|
|
341
|
+
Publishing, promotion in the channels where the demand already sits.
|
|
342
|
+
|
|
343
|
+
## 12. Non-goals (v1)
|
|
344
|
+
|
|
345
|
+
No rendering or UI components, no Google Scholar (scraping is the failure
|
|
346
|
+
mode this tool exists to replace), no database, no hosted service, no
|
|
347
|
+
sources beyond OpenAlex plus manual entries. Multi-source merging (PubMed,
|
|
348
|
+
ADS, DBLP) stays a documented architectural possibility only.
|
|
349
|
+
|