risforge 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.
risforge-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Amyr
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.
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.4
2
+ Name: risforge
3
+ Version: 0.1.0
4
+ Summary: Clean, deduplicate, and enrich RIS bibliographic files for systematic reviews.
5
+ Author-email: Amyr <amyrhexa@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/amyr/risforge
8
+ Project-URL: Repository, https://github.com/amyr/risforge
9
+ Project-URL: Issues, https://github.com/amyr/risforge/issues
10
+ Project-URL: Changelog, https://github.com/amyr/risforge/blob/main/CHANGELOG.md
11
+ Keywords: ris,bibliography,systematic-review,deduplication,crossref,openalex,citation-management,prisma
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Topic :: Scientific/Engineering
15
+ Classifier: Topic :: Text Processing :: Filters
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Operating System :: OS Independent
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: rispy>=0.9.0
27
+ Requires-Dist: requests>=2.31
28
+ Requires-Dist: requests-cache>=1.1
29
+ Requires-Dist: urllib3>=2.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=8.0; extra == "dev"
32
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
33
+ Requires-Dist: build>=1.2; extra == "dev"
34
+ Requires-Dist: twine>=5.0; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ # risforge
38
+
39
+ **Clean, deduplicate, and enrich RIS bibliographic files for systematic reviews.**
40
+
41
+ `risforge` takes a raw `.ris` export from Scopus, Web of Science, PubMed,
42
+ EndNote, or any other reference manager and:
43
+
44
+ 1. **Cleans & deduplicates** it — normalizing titles, DOIs, and author
45
+ names, then clustering duplicate records (by exact DOI, and by a
46
+ title + first-author fallback) and merging each cluster into one
47
+ complete, data-loss-free record.
48
+ 2. **Enriches** it — filling in missing abstracts, journal names,
49
+ volumes/issues/pages, ISSNs, keywords, and open-access PDF links by
50
+ querying [Crossref](https://www.crossref.org/), [OpenAlex](https://openalex.org/),
51
+ [Semantic Scholar](https://www.semanticscholar.org/), and
52
+ [Unpaywall](https://unpaywall.org/). Existing fields are never
53
+ overwritten — only gaps are filled.
54
+
55
+ It's built for the kind of unglamorous but essential prep work that
56
+ comes before title/abstract screening in a systematic review: getting
57
+ one clean, complete, deduplicated `.ris` file out of a pile of messy,
58
+ overlapping database exports.
59
+
60
+ ## Features
61
+
62
+ - **DOI-first deduplication** with a normalized title + first-author
63
+ fallback for records that lack a DOI, using union-find clustering so
64
+ transitively-linked duplicates across three, four, or more sources
65
+ all collapse into one record.
66
+ - **Zero data loss on merge** — the most complete record in a
67
+ duplicate cluster is used as the base, and every other record in the
68
+ cluster supplements it with whatever fields it's missing.
69
+ - **Non-destructive enrichment** — only empty fields are filled;
70
+ anything you (or an upstream export) already populated is left
71
+ alone.
72
+ - **Resilient HTTP** — automatic retries with exponential backoff on
73
+ 429/5xx responses, and a 7-day on-disk response cache so re-running
74
+ a pipeline doesn't re-hit the same APIs for records you've already
75
+ enriched.
76
+ - **Library or CLI** — use it as `import risforge` in a script/notebook,
77
+ or as a single `risforge` command.
78
+
79
+ ## Installation
80
+
81
+ ```bash
82
+ pip install risforge
83
+ ```
84
+
85
+ Requires Python 3.10+.
86
+
87
+ ## Quick start
88
+
89
+ ### Command line
90
+
91
+ ```bash
92
+ # Clean and deduplicate only
93
+ risforge clean raw_export.ris clean.ris
94
+
95
+ # Enrich an already-clean file
96
+ risforge enrich clean.ris enriched.ris --email you@example.com
97
+
98
+ # Both steps in one call
99
+ risforge pipeline raw_export.ris --email you@example.com
100
+ ```
101
+
102
+ The `pipeline` subcommand writes `<input>_clean.ris` and
103
+ `<input>_enriched.ris` next to your input file by default; pass
104
+ `--dedup-output` / `--output` to control that explicitly.
105
+
106
+ An email address is required by Crossref, OpenAlex, and Unpaywall's
107
+ "polite pool" usage policies — it's sent as a contact address in your
108
+ requests, never stored or transmitted anywhere else.
109
+
110
+ ### Python API
111
+
112
+ ```python
113
+ from risforge import clean_ris_file, RisEnricher, run_pipeline
114
+
115
+ # Clean + deduplicate only
116
+ records, errors = clean_ris_file("raw_export.ris", "clean.ris")
117
+ print(f"{len(records)} unique records, {len(errors)} parse errors")
118
+
119
+ # Enrich only
120
+ enricher = RisEnricher(email="you@example.com")
121
+ stats = enricher.enrich_file("clean.ris", "enriched.ris")
122
+ print(f"Enriched {stats['enriched']}/{stats['processed']} records")
123
+
124
+ # Both, in one call
125
+ result = run_pipeline(
126
+ input_path="raw_export.ris",
127
+ dedup_path="clean.ris",
128
+ enriched_path="enriched.ris",
129
+ email="you@example.com",
130
+ )
131
+ print(result.cleaned_record_count, result.enrichment_stats)
132
+ ```
133
+
134
+ ## Configuration
135
+
136
+ `RisEnricher` accepts a few constructor arguments beyond `email`:
137
+
138
+ ```python
139
+ RisEnricher(
140
+ email="you@example.com",
141
+ cache_name=".api_cache", # base filename for the on-disk HTTP cache
142
+ session=None, # inject your own requests.Session (mainly for testing)
143
+ )
144
+ ```
145
+
146
+ `enrich_file()` also accepts `fail_report_path` (where unresolved-DOI
147
+ records are written as JSON) and `request_delay_seconds` (delay
148
+ between records; defaults to 0.1s to stay within provider rate limits).
149
+
150
+ ## Troubleshooting
151
+
152
+ - **"Unresolved DOI via Title Matching" in the failure report** — the
153
+ record had no DOI and its title didn't match any Crossref result
154
+ above the 90% similarity threshold closely enough to resolve one.
155
+ These records are returned unmodified rather than guessed at.
156
+ - **Enrichment seems slow** — each record makes up to 5 API calls
157
+ (1 for DOI resolution if needed, 4 for metadata) with a small delay
158
+ between records. Re-running against the same input is fast, since
159
+ responses are cached for 7 days.
160
+ - **A field I already had got left alone even though enrichment "ran"** —
161
+ that's intentional. Enrichment only fills empty fields; it never
162
+ overwrites existing data.
163
+
164
+ ## Contributing
165
+
166
+ 1. Clone the repo and install in editable mode with dev dependencies:
167
+ ```bash
168
+ pip install -e ".[dev]"
169
+ ```
170
+ 2. Run the test suite:
171
+ ```bash
172
+ pytest
173
+ ```
174
+ 3. Open a PR. Please include tests for any behavioral change, and note
175
+ any deduplication/merge/enrichment logic changes explicitly in
176
+ `CHANGELOG.md` — this package's core value is *predictability* for
177
+ systematic review workflows, so silent behavior changes are treated
178
+ as bugs.
179
+
180
+ ## License
181
+
182
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,146 @@
1
+ # risforge
2
+
3
+ **Clean, deduplicate, and enrich RIS bibliographic files for systematic reviews.**
4
+
5
+ `risforge` takes a raw `.ris` export from Scopus, Web of Science, PubMed,
6
+ EndNote, or any other reference manager and:
7
+
8
+ 1. **Cleans & deduplicates** it — normalizing titles, DOIs, and author
9
+ names, then clustering duplicate records (by exact DOI, and by a
10
+ title + first-author fallback) and merging each cluster into one
11
+ complete, data-loss-free record.
12
+ 2. **Enriches** it — filling in missing abstracts, journal names,
13
+ volumes/issues/pages, ISSNs, keywords, and open-access PDF links by
14
+ querying [Crossref](https://www.crossref.org/), [OpenAlex](https://openalex.org/),
15
+ [Semantic Scholar](https://www.semanticscholar.org/), and
16
+ [Unpaywall](https://unpaywall.org/). Existing fields are never
17
+ overwritten — only gaps are filled.
18
+
19
+ It's built for the kind of unglamorous but essential prep work that
20
+ comes before title/abstract screening in a systematic review: getting
21
+ one clean, complete, deduplicated `.ris` file out of a pile of messy,
22
+ overlapping database exports.
23
+
24
+ ## Features
25
+
26
+ - **DOI-first deduplication** with a normalized title + first-author
27
+ fallback for records that lack a DOI, using union-find clustering so
28
+ transitively-linked duplicates across three, four, or more sources
29
+ all collapse into one record.
30
+ - **Zero data loss on merge** — the most complete record in a
31
+ duplicate cluster is used as the base, and every other record in the
32
+ cluster supplements it with whatever fields it's missing.
33
+ - **Non-destructive enrichment** — only empty fields are filled;
34
+ anything you (or an upstream export) already populated is left
35
+ alone.
36
+ - **Resilient HTTP** — automatic retries with exponential backoff on
37
+ 429/5xx responses, and a 7-day on-disk response cache so re-running
38
+ a pipeline doesn't re-hit the same APIs for records you've already
39
+ enriched.
40
+ - **Library or CLI** — use it as `import risforge` in a script/notebook,
41
+ or as a single `risforge` command.
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install risforge
47
+ ```
48
+
49
+ Requires Python 3.10+.
50
+
51
+ ## Quick start
52
+
53
+ ### Command line
54
+
55
+ ```bash
56
+ # Clean and deduplicate only
57
+ risforge clean raw_export.ris clean.ris
58
+
59
+ # Enrich an already-clean file
60
+ risforge enrich clean.ris enriched.ris --email you@example.com
61
+
62
+ # Both steps in one call
63
+ risforge pipeline raw_export.ris --email you@example.com
64
+ ```
65
+
66
+ The `pipeline` subcommand writes `<input>_clean.ris` and
67
+ `<input>_enriched.ris` next to your input file by default; pass
68
+ `--dedup-output` / `--output` to control that explicitly.
69
+
70
+ An email address is required by Crossref, OpenAlex, and Unpaywall's
71
+ "polite pool" usage policies — it's sent as a contact address in your
72
+ requests, never stored or transmitted anywhere else.
73
+
74
+ ### Python API
75
+
76
+ ```python
77
+ from risforge import clean_ris_file, RisEnricher, run_pipeline
78
+
79
+ # Clean + deduplicate only
80
+ records, errors = clean_ris_file("raw_export.ris", "clean.ris")
81
+ print(f"{len(records)} unique records, {len(errors)} parse errors")
82
+
83
+ # Enrich only
84
+ enricher = RisEnricher(email="you@example.com")
85
+ stats = enricher.enrich_file("clean.ris", "enriched.ris")
86
+ print(f"Enriched {stats['enriched']}/{stats['processed']} records")
87
+
88
+ # Both, in one call
89
+ result = run_pipeline(
90
+ input_path="raw_export.ris",
91
+ dedup_path="clean.ris",
92
+ enriched_path="enriched.ris",
93
+ email="you@example.com",
94
+ )
95
+ print(result.cleaned_record_count, result.enrichment_stats)
96
+ ```
97
+
98
+ ## Configuration
99
+
100
+ `RisEnricher` accepts a few constructor arguments beyond `email`:
101
+
102
+ ```python
103
+ RisEnricher(
104
+ email="you@example.com",
105
+ cache_name=".api_cache", # base filename for the on-disk HTTP cache
106
+ session=None, # inject your own requests.Session (mainly for testing)
107
+ )
108
+ ```
109
+
110
+ `enrich_file()` also accepts `fail_report_path` (where unresolved-DOI
111
+ records are written as JSON) and `request_delay_seconds` (delay
112
+ between records; defaults to 0.1s to stay within provider rate limits).
113
+
114
+ ## Troubleshooting
115
+
116
+ - **"Unresolved DOI via Title Matching" in the failure report** — the
117
+ record had no DOI and its title didn't match any Crossref result
118
+ above the 90% similarity threshold closely enough to resolve one.
119
+ These records are returned unmodified rather than guessed at.
120
+ - **Enrichment seems slow** — each record makes up to 5 API calls
121
+ (1 for DOI resolution if needed, 4 for metadata) with a small delay
122
+ between records. Re-running against the same input is fast, since
123
+ responses are cached for 7 days.
124
+ - **A field I already had got left alone even though enrichment "ran"** —
125
+ that's intentional. Enrichment only fills empty fields; it never
126
+ overwrites existing data.
127
+
128
+ ## Contributing
129
+
130
+ 1. Clone the repo and install in editable mode with dev dependencies:
131
+ ```bash
132
+ pip install -e ".[dev]"
133
+ ```
134
+ 2. Run the test suite:
135
+ ```bash
136
+ pytest
137
+ ```
138
+ 3. Open a PR. Please include tests for any behavioral change, and note
139
+ any deduplication/merge/enrichment logic changes explicitly in
140
+ `CHANGELOG.md` — this package's core value is *predictability* for
141
+ systematic review workflows, so silent behavior changes are treated
142
+ as bugs.
143
+
144
+ ## License
145
+
146
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,70 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "risforge"
7
+ version = "0.1.0"
8
+ description = "Clean, deduplicate, and enrich RIS bibliographic files for systematic reviews."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Amyr", email = "amyrhexa@gmail.com" }
14
+ ]
15
+ keywords = [
16
+ "ris",
17
+ "bibliography",
18
+ "systematic-review",
19
+ "deduplication",
20
+ "crossref",
21
+ "openalex",
22
+ "citation-management",
23
+ "prisma",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 4 - Beta",
27
+ "Intended Audience :: Science/Research",
28
+ "Topic :: Scientific/Engineering",
29
+ "Topic :: Text Processing :: Filters",
30
+ "License :: OSI Approved :: MIT License",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.10",
33
+ "Programming Language :: Python :: 3.11",
34
+ "Programming Language :: Python :: 3.12",
35
+ "Programming Language :: Python :: 3.13",
36
+ "Operating System :: OS Independent",
37
+ ]
38
+
39
+ dependencies = [
40
+ "rispy>=0.9.0",
41
+ "requests>=2.31",
42
+ "requests-cache>=1.1",
43
+ "urllib3>=2.0",
44
+ ]
45
+
46
+ [project.optional-dependencies]
47
+ dev = [
48
+ "pytest>=8.0",
49
+ "pytest-cov>=5.0",
50
+ "build>=1.2",
51
+ "twine>=5.0",
52
+ ]
53
+
54
+ [project.urls]
55
+ Homepage = "https://github.com/amyr/risforge"
56
+ Repository = "https://github.com/amyr/risforge"
57
+ Issues = "https://github.com/amyr/risforge/issues"
58
+ Changelog = "https://github.com/amyr/risforge/blob/main/CHANGELOG.md"
59
+
60
+ [project.scripts]
61
+ risforge = "risforge.cli:main"
62
+
63
+ [tool.setuptools.packages.find]
64
+ where = ["src"]
65
+
66
+ [tool.setuptools.package-dir]
67
+ "" = "src"
68
+
69
+ [tool.pytest.ini_options]
70
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,38 @@
1
+ """risforge: clean, deduplicate, and enrich RIS bibliographic files.
2
+
3
+ Typical usage::
4
+
5
+ from risforge import clean_ris_file, RisEnricher, run_pipeline
6
+
7
+ # Clean + deduplicate only
8
+ records, errors = clean_ris_file("raw.ris", "clean.ris")
9
+
10
+ # Enrich only
11
+ enricher = RisEnricher(email="you@example.com")
12
+ stats = enricher.enrich_file("clean.ris", "enriched.ris")
13
+
14
+ # Both, in one call
15
+ result = run_pipeline(
16
+ "raw.ris", "clean.ris", "enriched.ris", email="you@example.com"
17
+ )
18
+
19
+ See the ``risforge`` console script (``risforge --help``) for the
20
+ equivalent command-line interface.
21
+ """
22
+
23
+ from risforge.cleaning import clean_ris_file, process_ris_file
24
+ from risforge.enrichment import RisEnricher
25
+ from risforge.exceptions import RisForgeError, RisParsingError
26
+ from risforge.pipeline import PipelineResult, run_pipeline
27
+
28
+ __version__ = "0.1.0"
29
+
30
+ __all__ = [
31
+ "clean_ris_file",
32
+ "process_ris_file",
33
+ "RisEnricher",
34
+ "run_pipeline",
35
+ "PipelineResult",
36
+ "RisForgeError",
37
+ "RisParsingError",
38
+ ]