crossref-matcher 1.3.0__tar.gz → 1.3.2__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.
- {crossref_matcher-1.3.0/crossref_matcher.egg-info → crossref_matcher-1.3.2}/PKG-INFO +1 -1
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/README.md +93 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/evaluation/evaluation.py +21 -7
- crossref_matcher-1.3.2/crossref_matcher/evaluation/sparse_metrics_verification/verify_sparse_metrics.py +239 -0
- crossref_matcher-1.3.2/crossref_matcher/indexes/ror_organizations/opensearch_utils.py +44 -0
- crossref_matcher-1.3.2/crossref_matcher/resources/VERSION.txt +1 -0
- crossref_matcher-1.3.2/crossref_matcher/strategies/reference/sbmv_unstructured/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2/crossref_matcher.egg-info}/PKG-INFO +1 -1
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher.egg-info/SOURCES.txt +3 -1
- crossref_matcher-1.3.0/crossref_matcher/indexes/ror_organizations/ror_opensearch_update_dag.py +0 -124
- crossref_matcher-1.3.0/crossref_matcher/resources/VERSION.txt +0 -1
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/LICENSE +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/MANIFEST.in +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/MatchTask.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/app.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/evaluation/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/evaluation/all_links.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/evaluation/api_routes.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/evaluation/dataset_ingest.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/evaluation/google_sheets_connector.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/evaluation/ror_utils.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/evaluation/schemas.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/indexes → crossref_matcher-1.3.2/crossref_matcher/evaluation/sparse_metrics_verification}/__init__.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/indexes/preprint_matching → crossref_matcher-1.3.2/crossref_matcher/indexes}/__init__.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/indexes/ror_organizations → crossref_matcher-1.3.2/crossref_matcher/indexes/preprint_matching}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/indexes/preprint_matching/indexer_service.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/matching → crossref_matcher-1.3.2/crossref_matcher/indexes/ror_organizations}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/indexes/ror_organizations/create_index.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/indexes/ror_organizations/index_data.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/indexes/ror_organizations/ror_zenodo_api.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/resources → crossref_matcher-1.3.2/crossref_matcher/matching}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/matching/http_utils.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/matching/run_strategy_on_dataset.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/matching/schemas.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/matching/search.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/matching/utils.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/resources/affiliation → crossref_matcher-1.3.2/crossref_matcher/resources}/__init__.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/resources/data → crossref_matcher-1.3.2/crossref_matcher/resources/affiliation}/__init__.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/resources/data/common_ror_endings → crossref_matcher-1.3.2/crossref_matcher/resources/data}/__init__.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/resources/data/countries → crossref_matcher-1.3.2/crossref_matcher/resources/data/common_ror_endings}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/common_ror_endings/common_ror_endings.txt +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/resources/data/datasets → crossref_matcher-1.3.2/crossref_matcher/resources/data/countries}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/countries/china_placenames.txt +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/countries/countries.txt +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/countries/countries2.txt +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/resources/data/models → crossref_matcher-1.3.2/crossref_matcher/resources/data/datasets}/__init__.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/resources/data/models/preprint_ensemble_ml → crossref_matcher-1.3.2/crossref_matcher/resources/data/models}/__init__.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/resources/data/models/reference_nn_unstructured → crossref_matcher-1.3.2/crossref_matcher/resources/data/models/preprint_ensemble_ml}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/data_counters0.pkl +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/eval/events.out.tfevents.1690572611.knopfler.local +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.opt_slots0.npy.gz +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.pkl.gz +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.weights.npy.gz +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/train/events.out.tfevents.1690572611.knopfler.local +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/preprint_ml/data_counters0.pkl +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/preprint_ml/eval/events.out.tfevents.1690302892.knopfler.local +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/preprint_ml/model.opt_slots0.npy.gz +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/preprint_ml/model.pkl.gz +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/preprint_ml/model.weights.npy.gz +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/preprint_ml/train/events.out.tfevents.1690302891.knopfler.local +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/affiliation → crossref_matcher-1.3.2/crossref_matcher/resources/data/models/reference_nn_unstructured}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/data_counters0.pkl +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/eval/events.out.tfevents.1681511550.knopfler.local +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.opt_slots0.npy.gz +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.pkl.gz +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.weights.npy.gz +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/train/events.out.tfevents.1681511550.knopfler.local +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/plugins.json +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/run_plugin_test.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/__init__.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/affiliation/multi_search → crossref_matcher-1.3.2/crossref_matcher/strategies/affiliation}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/affiliation/es_utils.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/affiliation/matching.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/affiliation/single_search → crossref_matcher-1.3.2/crossref_matcher/strategies/affiliation/multi_search}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/affiliation/multi_search/strategy.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/funder → crossref_matcher-1.3.2/crossref_matcher/strategies/affiliation/single_search}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/affiliation/single_search/strategy.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/affiliation/strategy.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/common.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/funder/funder_name_to_ror_search → crossref_matcher-1.3.2/crossref_matcher/strategies/funder}/__init__.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/healthcheck → crossref_matcher-1.3.2/crossref_matcher/strategies/funder/funder_name_to_ror_search}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/funder/funder_name_to_ror_search/strategy.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/funder/funder_name_to_ror_search/version.txt +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/healthcheck → crossref_matcher-1.3.2/crossref_matcher/strategies}/healthcheck/__init__.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/preprint → crossref_matcher-1.3.2/crossref_matcher/strategies/healthcheck/healthcheck}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/healthcheck/healthcheck/strategy.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/preprint/ensemble_avg → crossref_matcher-1.3.2/crossref_matcher/strategies/preprint}/__init__.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/preprint/ensemble_cascade → crossref_matcher-1.3.2/crossref_matcher/strategies/preprint/ensemble_avg}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/ensemble_avg/strategy.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/preprint/ensemble_ml → crossref_matcher-1.3.2/crossref_matcher/strategies/preprint/ensemble_cascade}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/ensemble_cascade/strategy.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/preprint/preprint_ml → crossref_matcher-1.3.2/crossref_matcher/strategies/preprint/ensemble_ml}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/ensemble_ml/get_train_data.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/ensemble_ml/strategy.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/ensemble_ml/train.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/ensemble_ml/utils.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/preprint/sbmv → crossref_matcher-1.3.2/crossref_matcher/strategies/preprint/preprint_ml}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/preprint_ml/get_train_data.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/preprint_ml/strategy.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/preprint_ml/train.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/preprint_ml/utils.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/preprint/sbmv_es → crossref_matcher-1.3.2/crossref_matcher/strategies/preprint/sbmv}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/sbmv/strategy.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/preprint/sbmv_rels → crossref_matcher-1.3.2/crossref_matcher/strategies/preprint/sbmv_es}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/sbmv_es/strategy.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/reference → crossref_matcher-1.3.2/crossref_matcher/strategies/preprint/sbmv_rels}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/sbmv_rels/strategy.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/reference/cs_combined → crossref_matcher-1.3.2/crossref_matcher/strategies/reference}/__init__.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/reference/nn_unstructured → crossref_matcher-1.3.2/crossref_matcher/strategies/reference/cs_combined}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/cs_combined/strategy.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/reference/parsed_doi → crossref_matcher-1.3.2/crossref_matcher/strategies/reference/nn_unstructured}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/nn_unstructured/get_train_data.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/nn_unstructured/strategy.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/nn_unstructured/train.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/nn_unstructured/utils.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/reference/parsed_isbn → crossref_matcher-1.3.2/crossref_matcher/strategies/reference/parsed_doi}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/parsed_doi/strategy.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/reference/sbmv → crossref_matcher-1.3.2/crossref_matcher/strategies/reference/parsed_isbn}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/parsed_isbn/strategy.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/reference/sbmv_orig → crossref_matcher-1.3.2/crossref_matcher/strategies/reference/sbmv}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/sbmv/strategy.py +0 -0
- {crossref_matcher-1.3.0/crossref_matcher/strategies/reference/sbmv_unstructured → crossref_matcher-1.3.2/crossref_matcher/strategies/reference/sbmv_orig}/__init__.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/sbmv_orig/strategy.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/sbmv_unstructured/strategy.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/versioning.py +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher.egg-info/dependency_links.txt +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher.egg-info/entry_points.txt +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher.egg-info/requires.txt +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher.egg-info/top_level.txt +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/setup.cfg +0 -0
- {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/setup.py +0 -0
|
@@ -185,3 +185,96 @@ Requirements are defined in `setup.py`. Requirements files with exact versions c
|
|
|
185
185
|
* To update `requirements.txt` file: `pip-compile setup.py --extra strategies-core`
|
|
186
186
|
|
|
187
187
|
* To update `requirements-tests.txt` file: `pip-compile setup.py --extra strategies-core --extra evaluation --extra test -o requirements-tests.txt`
|
|
188
|
+
|
|
189
|
+
## Branching and deployment
|
|
190
|
+
|
|
191
|
+
CI (see [.gitlab-ci.yml](.gitlab-ci.yml)) deploys from two long-lived branches:
|
|
192
|
+
|
|
193
|
+
| Branch / trigger | Deploys to | Jobs |
|
|
194
|
+
|---|---|---|
|
|
195
|
+
| `dev` | **Research** environment | ECS deploy (`marple-research`), Airflow DAG data sync to `s3://airflow.crossref.org` (research-only bucket), `X.Y.Z-dev.N` prerelease tags |
|
|
196
|
+
| `main` | **Staging** environment | ECS deploy (`marple-staging`), GitLab-registry image for the integration project, release tags via semantic-release |
|
|
197
|
+
| Stable `vX.Y.Z` tags | PyPI | `crossref-matcher` published via OIDC trusted publishing (prerelease tags are not published) |
|
|
198
|
+
|
|
199
|
+
Both branches are protected. `main` is merged to via MR only; `dev`'s
|
|
200
|
+
protection must also allow Developers to push and merge directly
|
|
201
|
+
(**Settings → Repository → Protected branches → `dev` → Allowed to push
|
|
202
|
+
and merge**), since the workflow below pushes to it directly. Deploys are
|
|
203
|
+
automatic on push and are keyed on the commit SHA (`$CI_COMMIT_SHORT_SHA`),
|
|
204
|
+
independent of the semantic version.
|
|
205
|
+
|
|
206
|
+
### Feature workflow
|
|
207
|
+
|
|
208
|
+
1. **Branch off `main`** (not `dev`), so the feature can merge to `main`
|
|
209
|
+
cleanly regardless of what else is being tested on `dev`.
|
|
210
|
+
2. **To test on the research environment**, merge the feature into `dev` and
|
|
211
|
+
push (no MR needed — review happens at the `main` MR):
|
|
212
|
+
|
|
213
|
+
```sh
|
|
214
|
+
git checkout dev && git pull
|
|
215
|
+
git merge --no-ff my-feature
|
|
216
|
+
git push
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
3. **When validated, open an MR from the feature branch into `main`.**
|
|
220
|
+
Do not merge `dev` into `main` — `dev` may contain other unfinished work.
|
|
221
|
+
Use a [conventional commit](https://www.conventionalcommits.org/) message
|
|
222
|
+
(`feat:`/`fix:`) for the squash/merge commit; semantic-release derives the
|
|
223
|
+
release version from it.
|
|
224
|
+
4. **After the MR merges, merge `main` back into `dev`** to keep the branches
|
|
225
|
+
converged. This isn't strictly required — semantic-release's prerelease
|
|
226
|
+
branches skip the "behind the release branch" check — but without it,
|
|
227
|
+
`dev`'s next prerelease can recompute a version `main` already released,
|
|
228
|
+
or a confusing `vX.Y.Z-dev.N` alongside an already-shipped `vX.Y.Z`:
|
|
229
|
+
|
|
230
|
+
```sh
|
|
231
|
+
git checkout dev && git pull
|
|
232
|
+
git merge origin/main
|
|
233
|
+
git push
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Versioning
|
|
237
|
+
|
|
238
|
+
Releases are cut by semantic-release from conventional commit messages
|
|
239
|
+
(`fix:` → patch, `feat:` → minor, `BREAKING CHANGE` → major; other types cut
|
|
240
|
+
no release). On `dev`, release-worthy commits produce prerelease tags like
|
|
241
|
+
`v1.3.0-dev.1` (configured in [.releaserc.json](.releaserc.json)). Commits
|
|
242
|
+
without a `feat:`/`fix:` still deploy — only the tag is skipped, and
|
|
243
|
+
`VERSION.txt` keeps its `0.0.dev0` placeholder. To identify what is running
|
|
244
|
+
in an environment, use the commit SHA (the ECS image tag), not the version
|
|
245
|
+
string.
|
|
246
|
+
|
|
247
|
+
### Resetting a polluted `dev` back to `main`
|
|
248
|
+
|
|
249
|
+
`dev` is disposable: `main` never depends on its history. If `dev` has
|
|
250
|
+
accumulated abandoned experiments, reset it to `main` and re-merge only the
|
|
251
|
+
features still in flight:
|
|
252
|
+
|
|
253
|
+
1. Temporarily enable force-push for `dev` in GitLab
|
|
254
|
+
(**Settings → Repository → Protected branches → `dev` → Allowed to force push**).
|
|
255
|
+
2. Delete any `vX.Y.Z-dev.N` prerelease tags cut on the old `dev` — after the
|
|
256
|
+
reset their commits are unreachable from the branch, so semantic-release
|
|
257
|
+
won't see them and will try to recreate the same tag, failing every
|
|
258
|
+
subsequent release-worthy `dev` push until they're gone:
|
|
259
|
+
|
|
260
|
+
```sh
|
|
261
|
+
git tag -l 'v*-dev.*' | xargs -r git push origin --delete
|
|
262
|
+
git tag -l 'v*-dev.*' | xargs -r git tag -d
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
3. Reset and push:
|
|
266
|
+
|
|
267
|
+
```sh
|
|
268
|
+
git fetch origin
|
|
269
|
+
git checkout dev
|
|
270
|
+
git reset --hard origin/main
|
|
271
|
+
git push --force-with-lease origin dev
|
|
272
|
+
git merge --no-ff still-in-flight-feature # re-merge anything still being tested
|
|
273
|
+
git push
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
4. Disable force-push again.
|
|
277
|
+
|
|
278
|
+
The reset push re-deploys research and re-syncs the Airflow DAG data in
|
|
279
|
+
`s3://airflow.crossref.org` to the reset state (the sync uses `--delete`, so
|
|
280
|
+
files added by abandoned experiments are removed, not just left stale).
|
|
@@ -176,15 +176,29 @@ class EvaluateSklearn:
|
|
|
176
176
|
self.mlb.fit(y_true + y_pred)
|
|
177
177
|
y_true_transformed = self.mlb.transform(y_true)
|
|
178
178
|
y_pred_transformed = self.mlb.transform(y_pred)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
179
|
+
# Stay in sparse representation: with large label spaces (e.g. grant
|
|
180
|
+
# IDs), densifying via .toarray() blows up memory/time since the
|
|
181
|
+
# dense matrix is n_samples x n_unique_labels.
|
|
182
|
+
true_positives = y_true_transformed.multiply(y_pred_transformed).sum()
|
|
183
|
+
false_positives = int(y_pred_transformed.sum() - true_positives)
|
|
184
|
+
false_negatives = int(y_true_transformed.sum() - true_positives)
|
|
185
|
+
# sklearn's type_of_target() requires >1 column to classify a target
|
|
186
|
+
# as "multilabel-indicator" (native sparse support); with exactly one
|
|
187
|
+
# label column it's classified as "binary" instead, which sklearn
|
|
188
|
+
# handles via column_or_1d(), and that rejects sparse input outright.
|
|
189
|
+
# Densify just for that narrow edge case (cheap, since there's only
|
|
190
|
+
# one column).
|
|
191
|
+
if y_true_transformed.shape[1] <= 1:
|
|
192
|
+
y_true_for_fscore = y_true_transformed.toarray()
|
|
193
|
+
y_pred_for_fscore = y_pred_transformed.toarray()
|
|
194
|
+
else:
|
|
195
|
+
y_true_for_fscore = y_true_transformed
|
|
196
|
+
y_pred_for_fscore = y_pred_transformed
|
|
183
197
|
f_scores = []
|
|
184
198
|
for beta in f_score_betas:
|
|
185
199
|
precision, recall, fscore, _ = precision_recall_fscore_support(
|
|
186
|
-
|
|
187
|
-
|
|
200
|
+
y_true_for_fscore,
|
|
201
|
+
y_pred_for_fscore,
|
|
188
202
|
average="micro",
|
|
189
203
|
zero_division=0.0,
|
|
190
204
|
sample_weight=sample_weights,
|
|
@@ -352,7 +366,7 @@ def run_strategy_on_eval_data(
|
|
|
352
366
|
extra_data = extra_fn(strategy, data_point, this_result)
|
|
353
367
|
out_item["extra"].update(extra_data)
|
|
354
368
|
|
|
355
|
-
if hasattr(data_point, "alternates"):
|
|
369
|
+
if hasattr(data_point, "alternates") and len(data_point.alternates) > 0:
|
|
356
370
|
dataset_has_alternates = True
|
|
357
371
|
out_item["alternates"] = data_point.alternates
|
|
358
372
|
this_weight = getattr(data_point, "weight", 1.0)
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Reproduces the investigation behind the sparse-matrix rewrite of
|
|
2
|
+
`EvaluateSklearn.run_one_evaluation` in `crossref_matcher/evaluation/evaluation.py`.
|
|
3
|
+
|
|
4
|
+
Run directly to reproduce all findings:
|
|
5
|
+
|
|
6
|
+
python -m crossref_matcher.evaluation.sparse_metrics_verification.verify_sparse_metrics
|
|
7
|
+
|
|
8
|
+
See README.md in this directory for the narrative writeup of what each
|
|
9
|
+
check demonstrates.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import random
|
|
13
|
+
import time
|
|
14
|
+
import tracemalloc
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
from sklearn.metrics import precision_recall_fscore_support
|
|
18
|
+
from sklearn.preprocessing import MultiLabelBinarizer
|
|
19
|
+
from sklearn.utils.multiclass import type_of_target
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def old_dense_false_positives_negatives(at_sparse, af_sparse):
|
|
23
|
+
"""The original implementation: densify, then bitwise AND."""
|
|
24
|
+
at = at_sparse.toarray()
|
|
25
|
+
af = af_sparse.toarray()
|
|
26
|
+
false_positives = int((af & (1 - at)).sum())
|
|
27
|
+
false_negatives = int((at & (1 - af)).sum())
|
|
28
|
+
return false_positives, false_negatives
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def new_sparse_false_positives_negatives(at_sparse, af_sparse):
|
|
32
|
+
"""The replacement implementation: stay sparse throughout."""
|
|
33
|
+
true_positives = at_sparse.multiply(af_sparse).sum()
|
|
34
|
+
false_positives = int(af_sparse.sum() - true_positives)
|
|
35
|
+
false_negatives = int(at_sparse.sum() - true_positives)
|
|
36
|
+
return false_positives, false_negatives
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def fscore_inputs(at_sparse, af_sparse):
|
|
40
|
+
"""Mirrors the shape[1] <= 1 densify branch added to run_one_evaluation."""
|
|
41
|
+
if at_sparse.shape[1] <= 1:
|
|
42
|
+
return at_sparse.toarray(), af_sparse.toarray()
|
|
43
|
+
return at_sparse, af_sparse
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def check_equivalence(trials: int = 500, seed: int = 42) -> int:
|
|
47
|
+
"""Fuzz-tests old (dense bitwise) vs. new (sparse) false_positives /
|
|
48
|
+
false_negatives / precision / recall / f-score against each other over
|
|
49
|
+
randomized multi-label data, including empty label sets and duplicate
|
|
50
|
+
labels within a single sample.
|
|
51
|
+
|
|
52
|
+
Returns the number of mismatches found (0 means full equivalence).
|
|
53
|
+
"""
|
|
54
|
+
rng = random.Random(seed)
|
|
55
|
+
mismatches = 0
|
|
56
|
+
checked = 0
|
|
57
|
+
|
|
58
|
+
for trial in range(trials):
|
|
59
|
+
n_samples = rng.randint(1, 8)
|
|
60
|
+
vocab = [f"id{i}" for i in range(rng.randint(1, 6))]
|
|
61
|
+
y_true, y_pred = [], []
|
|
62
|
+
for _ in range(n_samples):
|
|
63
|
+
yt = rng.sample(vocab, k=rng.randint(0, len(vocab)))
|
|
64
|
+
if yt and rng.random() < 0.3:
|
|
65
|
+
yt = yt + [yt[0]] # duplicate label within a sample
|
|
66
|
+
yp = rng.sample(vocab, k=rng.randint(0, len(vocab)))
|
|
67
|
+
y_true.append(yt)
|
|
68
|
+
y_pred.append(yp)
|
|
69
|
+
|
|
70
|
+
if not any(y_true) and not any(y_pred):
|
|
71
|
+
continue # matches the early-return "all empty" branch in evaluation.py
|
|
72
|
+
|
|
73
|
+
mlb = MultiLabelBinarizer(sparse_output=True)
|
|
74
|
+
mlb.fit(y_true + y_pred)
|
|
75
|
+
at_sparse = mlb.transform(y_true)
|
|
76
|
+
af_sparse = mlb.transform(y_pred)
|
|
77
|
+
|
|
78
|
+
checked += 1
|
|
79
|
+
|
|
80
|
+
fp_old, fn_old = old_dense_false_positives_negatives(at_sparse, af_sparse)
|
|
81
|
+
fp_new, fn_new = new_sparse_false_positives_negatives(at_sparse, af_sparse)
|
|
82
|
+
if (fp_old, fn_old) != (fp_new, fn_new):
|
|
83
|
+
mismatches += 1
|
|
84
|
+
print(f" MISMATCH (fp/fn) trial={trial} y_true={y_true} y_pred={y_pred}")
|
|
85
|
+
print(f" old: fp={fp_old} fn={fn_old} new: fp={fp_new} fn={fn_new}")
|
|
86
|
+
|
|
87
|
+
at_dense, af_dense = at_sparse.toarray(), af_sparse.toarray()
|
|
88
|
+
p_old, r_old, f_old, _ = precision_recall_fscore_support(
|
|
89
|
+
at_dense, af_dense, average="micro", zero_division=0.0, beta=1.0
|
|
90
|
+
)
|
|
91
|
+
yt_new, yp_new = fscore_inputs(at_sparse, af_sparse)
|
|
92
|
+
p_new, r_new, f_new, _ = precision_recall_fscore_support(
|
|
93
|
+
yt_new, yp_new, average="micro", zero_division=0.0, beta=1.0
|
|
94
|
+
)
|
|
95
|
+
if not np.allclose([p_old, r_old, f_old], [p_new, r_new, f_new]):
|
|
96
|
+
mismatches += 1
|
|
97
|
+
print(f" MISMATCH (fscore) trial={trial} y_true={y_true} y_pred={y_pred}")
|
|
98
|
+
print(f" old: p={p_old} r={r_old} f={f_old}")
|
|
99
|
+
print(f" new: p={p_new} r={r_new} f={f_new}")
|
|
100
|
+
|
|
101
|
+
print(
|
|
102
|
+
f"[equivalence] checked {checked} non-trivial trials, {mismatches} mismatches"
|
|
103
|
+
)
|
|
104
|
+
return mismatches
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def check_single_column_edge_case() -> None:
|
|
108
|
+
"""Demonstrates why a densify fallback is needed when there is only one
|
|
109
|
+
unique label across a batch: sklearn's type_of_target() requires more
|
|
110
|
+
than one column to classify a target as "multilabel-indicator" (which
|
|
111
|
+
has native sparse support). With exactly one column it's classified as
|
|
112
|
+
"binary" instead, and sklearn's binary-handling path (column_or_1d)
|
|
113
|
+
rejects sparse input outright -- regardless of dense vs sparse, the
|
|
114
|
+
*classification* is the same; only the *acceptance* of sparse input
|
|
115
|
+
differs downstream.
|
|
116
|
+
"""
|
|
117
|
+
mlb = MultiLabelBinarizer(sparse_output=True)
|
|
118
|
+
y_true = [["a"], []]
|
|
119
|
+
y_pred = [["a"], ["a"]]
|
|
120
|
+
mlb.fit(y_true + y_pred)
|
|
121
|
+
at_sparse = mlb.transform(y_true)
|
|
122
|
+
af_sparse = mlb.transform(y_pred)
|
|
123
|
+
at_dense, af_dense = at_sparse.toarray(), af_sparse.toarray()
|
|
124
|
+
|
|
125
|
+
print(f"[single-column] shape={at_sparse.shape}")
|
|
126
|
+
print(f" type_of_target(sparse) = {type_of_target(at_sparse)!r}")
|
|
127
|
+
print(f" type_of_target(dense) = {type_of_target(at_dense)!r}")
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
precision_recall_fscore_support(
|
|
131
|
+
at_sparse, af_sparse, average="micro", zero_division=0.0, beta=1.0
|
|
132
|
+
)
|
|
133
|
+
print(" sparse input: unexpectedly succeeded")
|
|
134
|
+
except TypeError as e:
|
|
135
|
+
print(f" sparse input raises TypeError: {e}")
|
|
136
|
+
|
|
137
|
+
p, r, f, _ = precision_recall_fscore_support(
|
|
138
|
+
at_dense, af_dense, average="micro", zero_division=0.0, beta=1.0
|
|
139
|
+
)
|
|
140
|
+
print(f" dense input succeeds: precision={p} recall={r} fscore={f}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def check_sample_weight_equivalence() -> None:
|
|
144
|
+
"""Confirms sample_weight produces identical results for sparse vs. dense
|
|
145
|
+
once the label space has more than one column (the common case)."""
|
|
146
|
+
mlb = MultiLabelBinarizer(sparse_output=True)
|
|
147
|
+
y_true = [["a", "b"], ["c"]]
|
|
148
|
+
y_pred = [["a"], ["c", "d"]]
|
|
149
|
+
mlb.fit(y_true + y_pred)
|
|
150
|
+
at_sparse = mlb.transform(y_true)
|
|
151
|
+
af_sparse = mlb.transform(y_pred)
|
|
152
|
+
weights = [2.0, 3.0]
|
|
153
|
+
|
|
154
|
+
p1, r1, f1, _ = precision_recall_fscore_support(
|
|
155
|
+
at_sparse,
|
|
156
|
+
af_sparse,
|
|
157
|
+
average="micro",
|
|
158
|
+
zero_division=0.0,
|
|
159
|
+
beta=1.0,
|
|
160
|
+
sample_weight=weights,
|
|
161
|
+
)
|
|
162
|
+
p2, r2, f2, _ = precision_recall_fscore_support(
|
|
163
|
+
at_sparse.toarray(),
|
|
164
|
+
af_sparse.toarray(),
|
|
165
|
+
average="micro",
|
|
166
|
+
zero_division=0.0,
|
|
167
|
+
beta=1.0,
|
|
168
|
+
sample_weight=weights,
|
|
169
|
+
)
|
|
170
|
+
print(f"[sample_weight] sparse: precision={p1} recall={r1} fscore={f1}")
|
|
171
|
+
print(f"[sample_weight] dense: precision={p2} recall={r2} fscore={f2}")
|
|
172
|
+
assert np.allclose([p1, r1, f1], [p2, r2, f2])
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def benchmark_dense_vs_sparse(n_samples: int = 2000, n_labels: int = 20000) -> None:
|
|
176
|
+
"""Demonstrates the actual cost of `.toarray()` that motivated this fix:
|
|
177
|
+
a label-indicator matrix scales as n_samples x n_unique_labels, which for
|
|
178
|
+
a large ID space (e.g. grant DOIs) becomes very large very fast, even
|
|
179
|
+
though the underlying data is highly sparse (each sample only has a
|
|
180
|
+
handful of true/predicted labels)."""
|
|
181
|
+
rng = random.Random(7)
|
|
182
|
+
vocab = [f"id{i}" for i in range(n_labels)]
|
|
183
|
+
y_true = [rng.sample(vocab, k=rng.randint(0, 3)) for _ in range(n_samples)]
|
|
184
|
+
y_pred = [rng.sample(vocab, k=rng.randint(0, 3)) for _ in range(n_samples)]
|
|
185
|
+
|
|
186
|
+
mlb = MultiLabelBinarizer(sparse_output=True)
|
|
187
|
+
mlb.fit(y_true + y_pred)
|
|
188
|
+
at_sparse = mlb.transform(y_true)
|
|
189
|
+
af_sparse = mlb.transform(y_pred)
|
|
190
|
+
print(
|
|
191
|
+
f"[benchmark] n_samples={n_samples} n_labels={n_labels} "
|
|
192
|
+
f"matrix shape={at_sparse.shape}"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
tracemalloc.start()
|
|
196
|
+
start = time.perf_counter()
|
|
197
|
+
old_dense_false_positives_negatives(at_sparse, af_sparse)
|
|
198
|
+
dense_time = time.perf_counter() - start
|
|
199
|
+
_, dense_peak = tracemalloc.get_traced_memory()
|
|
200
|
+
tracemalloc.stop()
|
|
201
|
+
|
|
202
|
+
tracemalloc.start()
|
|
203
|
+
start = time.perf_counter()
|
|
204
|
+
new_sparse_false_positives_negatives(at_sparse, af_sparse)
|
|
205
|
+
sparse_time = time.perf_counter() - start
|
|
206
|
+
_, sparse_peak = tracemalloc.get_traced_memory()
|
|
207
|
+
tracemalloc.stop()
|
|
208
|
+
|
|
209
|
+
print(
|
|
210
|
+
f"[benchmark] dense (.toarray) : {dense_time:.3f}s, "
|
|
211
|
+
f"peak extra memory {dense_peak / 1e6:.1f} MB"
|
|
212
|
+
)
|
|
213
|
+
print(
|
|
214
|
+
f"[benchmark] sparse (.multiply): {sparse_time:.3f}s, "
|
|
215
|
+
f"peak extra memory {sparse_peak / 1e6:.1f} MB"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def main() -> None:
|
|
220
|
+
print(
|
|
221
|
+
"== 1. Old vs. new false_positives/false_negatives/precision/recall/f-score equivalence =="
|
|
222
|
+
)
|
|
223
|
+
mismatches = check_equivalence()
|
|
224
|
+
|
|
225
|
+
print("\n== 2. Single-label-column edge case (why the densify fallback exists) ==")
|
|
226
|
+
check_single_column_edge_case()
|
|
227
|
+
|
|
228
|
+
print("\n== 3. sample_weight equivalence (sparse vs. dense) ==")
|
|
229
|
+
check_sample_weight_equivalence()
|
|
230
|
+
|
|
231
|
+
print("\n== 4. Benchmark: dense .toarray() vs. sparse, large label space ==")
|
|
232
|
+
benchmark_dense_vs_sparse()
|
|
233
|
+
|
|
234
|
+
print("\n== Summary ==")
|
|
235
|
+
print(f"Equivalence mismatches: {mismatches} (expect 0)")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if __name__ == "__main__":
|
|
239
|
+
main()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Shared OpenSearch client utilities for ROR index DAG tasks."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import boto3
|
|
5
|
+
|
|
6
|
+
REGION = "us-east-1"
|
|
7
|
+
INDEX = "organization"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_secret(secret_name: str) -> str | bytes:
|
|
11
|
+
"""Retrieve a secret value from AWS Secrets Manager."""
|
|
12
|
+
session = boto3.session.Session()
|
|
13
|
+
client = session.client(service_name="secretsmanager", region_name=REGION)
|
|
14
|
+
|
|
15
|
+
get_secret_value_response = client.get_secret_value(SecretId=secret_name)
|
|
16
|
+
if "SecretString" in get_secret_value_response:
|
|
17
|
+
return get_secret_value_response["SecretString"]
|
|
18
|
+
else:
|
|
19
|
+
return base64.b64decode(get_secret_value_response["SecretBinary"])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_es_client(host: str | None = None):
|
|
23
|
+
"""Create an authenticated OpenSearch client using AWS credentials."""
|
|
24
|
+
from opensearchpy import (
|
|
25
|
+
OpenSearch,
|
|
26
|
+
RequestsHttpConnection,
|
|
27
|
+
AWSV4SignerAuth,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if host is None:
|
|
31
|
+
host = get_secret("es-host-matching")
|
|
32
|
+
|
|
33
|
+
credentials = boto3.Session().get_credentials()
|
|
34
|
+
auth = AWSV4SignerAuth(credentials, REGION, "es")
|
|
35
|
+
|
|
36
|
+
return OpenSearch(
|
|
37
|
+
hosts=[{"host": host, "port": 443}],
|
|
38
|
+
http_auth=auth,
|
|
39
|
+
use_ssl=True,
|
|
40
|
+
verify_certs=True,
|
|
41
|
+
connection_class=RequestsHttpConnection,
|
|
42
|
+
pool_maxsize=20,
|
|
43
|
+
timeout=120,
|
|
44
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.3.2
|
|
File without changes
|
|
@@ -20,13 +20,15 @@ crossref_matcher/evaluation/evaluation.py
|
|
|
20
20
|
crossref_matcher/evaluation/google_sheets_connector.py
|
|
21
21
|
crossref_matcher/evaluation/ror_utils.py
|
|
22
22
|
crossref_matcher/evaluation/schemas.py
|
|
23
|
+
crossref_matcher/evaluation/sparse_metrics_verification/__init__.py
|
|
24
|
+
crossref_matcher/evaluation/sparse_metrics_verification/verify_sparse_metrics.py
|
|
23
25
|
crossref_matcher/indexes/__init__.py
|
|
24
26
|
crossref_matcher/indexes/preprint_matching/__init__.py
|
|
25
27
|
crossref_matcher/indexes/preprint_matching/indexer_service.py
|
|
26
28
|
crossref_matcher/indexes/ror_organizations/__init__.py
|
|
27
29
|
crossref_matcher/indexes/ror_organizations/create_index.py
|
|
28
30
|
crossref_matcher/indexes/ror_organizations/index_data.py
|
|
29
|
-
crossref_matcher/indexes/ror_organizations/
|
|
31
|
+
crossref_matcher/indexes/ror_organizations/opensearch_utils.py
|
|
30
32
|
crossref_matcher/indexes/ror_organizations/ror_zenodo_api.py
|
|
31
33
|
crossref_matcher/matching/__init__.py
|
|
32
34
|
crossref_matcher/matching/http_utils.py
|
crossref_matcher-1.3.0/crossref_matcher/indexes/ror_organizations/ror_opensearch_update_dag.py
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
"""Airflow DAG to keep the ROR OpenSearch index up to date."""
|
|
2
|
-
|
|
3
|
-
import base64
|
|
4
|
-
import boto3
|
|
5
|
-
|
|
6
|
-
from airflow.decorators import dag, task
|
|
7
|
-
from datetime import datetime
|
|
8
|
-
from datetime import timedelta
|
|
9
|
-
from opensearchpy import (
|
|
10
|
-
OpenSearch,
|
|
11
|
-
RequestsHttpConnection,
|
|
12
|
-
AWSV4SignerAuth,
|
|
13
|
-
NotFoundError,
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
import logging
|
|
17
|
-
from typing import Any
|
|
18
|
-
|
|
19
|
-
from crossref_matcher.indexes.ror_organizations.ror_zenodo_api import (
|
|
20
|
-
get_ror_data_dump_metadata,
|
|
21
|
-
)
|
|
22
|
-
from crossref_matcher.indexes.ror_organizations.create_index import (
|
|
23
|
-
index_body,
|
|
24
|
-
initialize_index,
|
|
25
|
-
)
|
|
26
|
-
from crossref_matcher.indexes.ror_organizations.index_data import (
|
|
27
|
-
download_ror_data_and_update_index,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
logger = logging.getLogger(__name__)
|
|
31
|
-
|
|
32
|
-
REGION = "us-east-1"
|
|
33
|
-
INDEX = "organization"
|
|
34
|
-
INDEX_BATCH_SIZE = 400
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def get_secret(secret_name: str) -> str | bytes:
|
|
38
|
-
"""Retrieve a secret value from AWS Secrets Manager."""
|
|
39
|
-
session = boto3.session.Session()
|
|
40
|
-
client = session.client(service_name="secretsmanager", region_name=REGION)
|
|
41
|
-
|
|
42
|
-
get_secret_value_response = client.get_secret_value(SecretId=secret_name)
|
|
43
|
-
if "SecretString" in get_secret_value_response:
|
|
44
|
-
return get_secret_value_response["SecretString"]
|
|
45
|
-
else:
|
|
46
|
-
return base64.b64decode(get_secret_value_response["SecretBinary"])
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def get_es_client() -> OpenSearch:
|
|
50
|
-
"""Create an authenticated OpenSearch client using AWS credentials."""
|
|
51
|
-
credentials = boto3.Session().get_credentials()
|
|
52
|
-
auth = AWSV4SignerAuth(credentials, REGION, "es")
|
|
53
|
-
|
|
54
|
-
return OpenSearch(
|
|
55
|
-
hosts=[{"host": get_secret("es-host-matching"), "port": 443}],
|
|
56
|
-
http_auth=auth,
|
|
57
|
-
use_ssl=True,
|
|
58
|
-
verify_certs=True,
|
|
59
|
-
connection_class=RequestsHttpConnection,
|
|
60
|
-
pool_maxsize=20,
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@dag(
|
|
65
|
-
schedule_interval="@daily",
|
|
66
|
-
catchup=False,
|
|
67
|
-
dagrun_timeout=timedelta(hours=10),
|
|
68
|
-
start_date=datetime(2026, 1, 3),
|
|
69
|
-
tags=["experiment", "ror", "matching"],
|
|
70
|
-
)
|
|
71
|
-
def update_ror_index():
|
|
72
|
-
@task()
|
|
73
|
-
def fetch_ror_metadata() -> dict[str, Any]:
|
|
74
|
-
"""Fetch metadata for the latest ROR data dump from Zenodo."""
|
|
75
|
-
return get_ror_data_dump_metadata()
|
|
76
|
-
|
|
77
|
-
@task.short_circuit()
|
|
78
|
-
def is_update_needed(metadata: dict[str, Any]) -> bool:
|
|
79
|
-
"""Return True if the index checksum differs from the latest dump."""
|
|
80
|
-
ror_checksum = metadata["checksum"]
|
|
81
|
-
es_client = get_es_client()
|
|
82
|
-
|
|
83
|
-
# # FOR TESTING: RUN THIS TO FORCE UPDATE, OTHERWISE COMMENT OUT
|
|
84
|
-
# index_metadata = {
|
|
85
|
-
# "target_data": "",
|
|
86
|
-
# "ror_data_dump_checksum": "",
|
|
87
|
-
# "ror_data_dump_doi": "",
|
|
88
|
-
# }
|
|
89
|
-
# body = {"_meta": index_metadata}
|
|
90
|
-
# es_client.indices.put_mapping(index=INDEX, body=body)
|
|
91
|
-
|
|
92
|
-
try:
|
|
93
|
-
mapping = es_client.indices.get_mapping(index=INDEX)
|
|
94
|
-
except NotFoundError:
|
|
95
|
-
return True
|
|
96
|
-
meta = mapping[INDEX]["mappings"].get("_meta", {})
|
|
97
|
-
current_checksum = meta.get("ror_data_dump_checksum", "")
|
|
98
|
-
return current_checksum != ror_checksum
|
|
99
|
-
|
|
100
|
-
@task()
|
|
101
|
-
def create_index_if_not_exists():
|
|
102
|
-
es_client = get_es_client()
|
|
103
|
-
|
|
104
|
-
index_exists = es_client.indices.exists(index=INDEX)
|
|
105
|
-
|
|
106
|
-
if not index_exists:
|
|
107
|
-
index_body_content = index_body()
|
|
108
|
-
response = initialize_index(INDEX, es_client, index_body_content)
|
|
109
|
-
logger.info(f"Created index '{INDEX}': {response}")
|
|
110
|
-
|
|
111
|
-
@task
|
|
112
|
-
def download_and_update(metadata: dict[str, Any]) -> None:
|
|
113
|
-
"""Download the ROR data dump and update the index."""
|
|
114
|
-
es_client = get_es_client()
|
|
115
|
-
download_ror_data_and_update_index(metadata, es_client)
|
|
116
|
-
|
|
117
|
-
metadata = fetch_ror_metadata()
|
|
118
|
-
update_needed = is_update_needed(metadata)
|
|
119
|
-
# if update_needed is false, the DAG will stop here
|
|
120
|
-
|
|
121
|
-
update_needed >> create_index_if_not_exists() >> download_and_update(metadata)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
workflow = update_ror_index()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.3.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/evaluation/dataset_ingest.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|