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.
Files changed (132) hide show
  1. {crossref_matcher-1.3.0/crossref_matcher.egg-info → crossref_matcher-1.3.2}/PKG-INFO +1 -1
  2. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/README.md +93 -0
  3. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/evaluation/evaluation.py +21 -7
  4. crossref_matcher-1.3.2/crossref_matcher/evaluation/sparse_metrics_verification/verify_sparse_metrics.py +239 -0
  5. crossref_matcher-1.3.2/crossref_matcher/indexes/ror_organizations/opensearch_utils.py +44 -0
  6. crossref_matcher-1.3.2/crossref_matcher/resources/VERSION.txt +1 -0
  7. crossref_matcher-1.3.2/crossref_matcher/strategies/reference/sbmv_unstructured/__init__.py +0 -0
  8. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2/crossref_matcher.egg-info}/PKG-INFO +1 -1
  9. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher.egg-info/SOURCES.txt +3 -1
  10. crossref_matcher-1.3.0/crossref_matcher/indexes/ror_organizations/ror_opensearch_update_dag.py +0 -124
  11. crossref_matcher-1.3.0/crossref_matcher/resources/VERSION.txt +0 -1
  12. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/LICENSE +0 -0
  13. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/MANIFEST.in +0 -0
  14. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/MatchTask.py +0 -0
  15. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/__init__.py +0 -0
  16. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/app.py +0 -0
  17. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/evaluation/__init__.py +0 -0
  18. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/evaluation/all_links.py +0 -0
  19. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/evaluation/api_routes.py +0 -0
  20. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/evaluation/dataset_ingest.py +0 -0
  21. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/evaluation/google_sheets_connector.py +0 -0
  22. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/evaluation/ror_utils.py +0 -0
  23. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/evaluation/schemas.py +0 -0
  24. {crossref_matcher-1.3.0/crossref_matcher/indexes → crossref_matcher-1.3.2/crossref_matcher/evaluation/sparse_metrics_verification}/__init__.py +0 -0
  25. {crossref_matcher-1.3.0/crossref_matcher/indexes/preprint_matching → crossref_matcher-1.3.2/crossref_matcher/indexes}/__init__.py +0 -0
  26. {crossref_matcher-1.3.0/crossref_matcher/indexes/ror_organizations → crossref_matcher-1.3.2/crossref_matcher/indexes/preprint_matching}/__init__.py +0 -0
  27. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/indexes/preprint_matching/indexer_service.py +0 -0
  28. {crossref_matcher-1.3.0/crossref_matcher/matching → crossref_matcher-1.3.2/crossref_matcher/indexes/ror_organizations}/__init__.py +0 -0
  29. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/indexes/ror_organizations/create_index.py +0 -0
  30. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/indexes/ror_organizations/index_data.py +0 -0
  31. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/indexes/ror_organizations/ror_zenodo_api.py +0 -0
  32. {crossref_matcher-1.3.0/crossref_matcher/resources → crossref_matcher-1.3.2/crossref_matcher/matching}/__init__.py +0 -0
  33. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/matching/http_utils.py +0 -0
  34. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/matching/run_strategy_on_dataset.py +0 -0
  35. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/matching/schemas.py +0 -0
  36. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/matching/search.py +0 -0
  37. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/matching/utils.py +0 -0
  38. {crossref_matcher-1.3.0/crossref_matcher/resources/affiliation → crossref_matcher-1.3.2/crossref_matcher/resources}/__init__.py +0 -0
  39. {crossref_matcher-1.3.0/crossref_matcher/resources/data → crossref_matcher-1.3.2/crossref_matcher/resources/affiliation}/__init__.py +0 -0
  40. {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
  41. {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
  42. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/common_ror_endings/common_ror_endings.txt +0 -0
  43. {crossref_matcher-1.3.0/crossref_matcher/resources/data/datasets → crossref_matcher-1.3.2/crossref_matcher/resources/data/countries}/__init__.py +0 -0
  44. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/countries/china_placenames.txt +0 -0
  45. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/countries/countries.txt +0 -0
  46. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/countries/countries2.txt +0 -0
  47. {crossref_matcher-1.3.0/crossref_matcher/resources/data/models → crossref_matcher-1.3.2/crossref_matcher/resources/data/datasets}/__init__.py +0 -0
  48. {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
  49. {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
  50. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/data_counters0.pkl +0 -0
  51. {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
  52. {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
  53. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.pkl.gz +0 -0
  54. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.weights.npy.gz +0 -0
  55. {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
  56. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/preprint_ml/data_counters0.pkl +0 -0
  57. {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
  58. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/preprint_ml/model.opt_slots0.npy.gz +0 -0
  59. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/preprint_ml/model.pkl.gz +0 -0
  60. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/preprint_ml/model.weights.npy.gz +0 -0
  61. {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
  62. {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
  63. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/data_counters0.pkl +0 -0
  64. {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
  65. {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
  66. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.pkl.gz +0 -0
  67. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.weights.npy.gz +0 -0
  68. {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
  69. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/resources/plugins.json +0 -0
  70. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/run_plugin_test.py +0 -0
  71. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/__init__.py +0 -0
  72. {crossref_matcher-1.3.0/crossref_matcher/strategies/affiliation/multi_search → crossref_matcher-1.3.2/crossref_matcher/strategies/affiliation}/__init__.py +0 -0
  73. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/affiliation/es_utils.py +0 -0
  74. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/affiliation/matching.py +0 -0
  75. {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
  76. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/affiliation/multi_search/strategy.py +0 -0
  77. {crossref_matcher-1.3.0/crossref_matcher/strategies/funder → crossref_matcher-1.3.2/crossref_matcher/strategies/affiliation/single_search}/__init__.py +0 -0
  78. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/affiliation/single_search/strategy.py +0 -0
  79. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/affiliation/strategy.py +0 -0
  80. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/common.py +0 -0
  81. {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
  82. {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
  83. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/funder/funder_name_to_ror_search/strategy.py +0 -0
  84. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/funder/funder_name_to_ror_search/version.txt +0 -0
  85. {crossref_matcher-1.3.0/crossref_matcher/strategies/healthcheck → crossref_matcher-1.3.2/crossref_matcher/strategies}/healthcheck/__init__.py +0 -0
  86. {crossref_matcher-1.3.0/crossref_matcher/strategies/preprint → crossref_matcher-1.3.2/crossref_matcher/strategies/healthcheck/healthcheck}/__init__.py +0 -0
  87. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/healthcheck/healthcheck/strategy.py +0 -0
  88. {crossref_matcher-1.3.0/crossref_matcher/strategies/preprint/ensemble_avg → crossref_matcher-1.3.2/crossref_matcher/strategies/preprint}/__init__.py +0 -0
  89. {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
  90. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/ensemble_avg/strategy.py +0 -0
  91. {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
  92. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/ensemble_cascade/strategy.py +0 -0
  93. {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
  94. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/ensemble_ml/get_train_data.py +0 -0
  95. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/ensemble_ml/strategy.py +0 -0
  96. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/ensemble_ml/train.py +0 -0
  97. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/ensemble_ml/utils.py +0 -0
  98. {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
  99. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/preprint_ml/get_train_data.py +0 -0
  100. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/preprint_ml/strategy.py +0 -0
  101. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/preprint_ml/train.py +0 -0
  102. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/preprint_ml/utils.py +0 -0
  103. {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
  104. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/sbmv/strategy.py +0 -0
  105. {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
  106. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/sbmv_es/strategy.py +0 -0
  107. {crossref_matcher-1.3.0/crossref_matcher/strategies/reference → crossref_matcher-1.3.2/crossref_matcher/strategies/preprint/sbmv_rels}/__init__.py +0 -0
  108. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/preprint/sbmv_rels/strategy.py +0 -0
  109. {crossref_matcher-1.3.0/crossref_matcher/strategies/reference/cs_combined → crossref_matcher-1.3.2/crossref_matcher/strategies/reference}/__init__.py +0 -0
  110. {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
  111. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/cs_combined/strategy.py +0 -0
  112. {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
  113. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/nn_unstructured/get_train_data.py +0 -0
  114. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/nn_unstructured/strategy.py +0 -0
  115. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/nn_unstructured/train.py +0 -0
  116. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/nn_unstructured/utils.py +0 -0
  117. {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
  118. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/parsed_doi/strategy.py +0 -0
  119. {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
  120. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/parsed_isbn/strategy.py +0 -0
  121. {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
  122. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/sbmv/strategy.py +0 -0
  123. {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
  124. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/sbmv_orig/strategy.py +0 -0
  125. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/reference/sbmv_unstructured/strategy.py +0 -0
  126. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher/strategies/versioning.py +0 -0
  127. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher.egg-info/dependency_links.txt +0 -0
  128. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher.egg-info/entry_points.txt +0 -0
  129. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher.egg-info/requires.txt +0 -0
  130. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/crossref_matcher.egg-info/top_level.txt +0 -0
  131. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/setup.cfg +0 -0
  132. {crossref_matcher-1.3.0 → crossref_matcher-1.3.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crossref-matcher
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary: Crossref's matching API
5
5
  Home-page: https://gitlab.com/crossref/marple
6
6
  Author: Crossref
@@ -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
- at = y_true_transformed.toarray()
180
- af = y_pred_transformed.toarray()
181
- false_positives = int((af & (1 - at)).sum())
182
- false_negatives = int((at & (1 - af)).sum())
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
- at,
187
- af,
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
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crossref-matcher
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary: Crossref's matching API
5
5
  Home-page: https://gitlab.com/crossref/marple
6
6
  Author: Crossref
@@ -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/ror_opensearch_update_dag.py
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
@@ -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()