crossref-matcher 1.2.2__tar.gz → 1.3.1__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 (131) hide show
  1. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/MANIFEST.in +1 -0
  2. {crossref_matcher-1.2.2/crossref_matcher.egg-info → crossref_matcher-1.3.1}/PKG-INFO +1 -1
  3. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/README.md +43 -0
  4. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/evaluation/evaluation.py +21 -7
  5. crossref_matcher-1.3.1/crossref_matcher/evaluation/sparse_metrics_verification/verify_sparse_metrics.py +239 -0
  6. crossref_matcher-1.3.1/crossref_matcher/resources/VERSION.txt +1 -0
  7. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/__init__.py +25 -5
  8. crossref_matcher-1.3.1/crossref_matcher/strategies/funder/funder_name_to_ror_search/version.txt +1 -0
  9. crossref_matcher-1.3.1/crossref_matcher/strategies/reference/sbmv_unstructured/__init__.py +0 -0
  10. crossref_matcher-1.3.1/crossref_matcher/strategies/versioning.py +77 -0
  11. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1/crossref_matcher.egg-info}/PKG-INFO +1 -1
  12. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher.egg-info/SOURCES.txt +4 -0
  13. crossref_matcher-1.2.2/crossref_matcher/resources/VERSION.txt +0 -1
  14. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/LICENSE +0 -0
  15. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/MatchTask.py +0 -0
  16. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/__init__.py +0 -0
  17. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/app.py +0 -0
  18. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/evaluation/__init__.py +0 -0
  19. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/evaluation/all_links.py +0 -0
  20. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/evaluation/api_routes.py +0 -0
  21. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/evaluation/dataset_ingest.py +0 -0
  22. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/evaluation/google_sheets_connector.py +0 -0
  23. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/evaluation/ror_utils.py +0 -0
  24. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/evaluation/schemas.py +0 -0
  25. {crossref_matcher-1.2.2/crossref_matcher/indexes → crossref_matcher-1.3.1/crossref_matcher/evaluation/sparse_metrics_verification}/__init__.py +0 -0
  26. {crossref_matcher-1.2.2/crossref_matcher/indexes/preprint_matching → crossref_matcher-1.3.1/crossref_matcher/indexes}/__init__.py +0 -0
  27. {crossref_matcher-1.2.2/crossref_matcher/indexes/ror_organizations → crossref_matcher-1.3.1/crossref_matcher/indexes/preprint_matching}/__init__.py +0 -0
  28. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/indexes/preprint_matching/indexer_service.py +0 -0
  29. {crossref_matcher-1.2.2/crossref_matcher/matching → crossref_matcher-1.3.1/crossref_matcher/indexes/ror_organizations}/__init__.py +0 -0
  30. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/indexes/ror_organizations/create_index.py +0 -0
  31. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/indexes/ror_organizations/index_data.py +0 -0
  32. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/indexes/ror_organizations/ror_opensearch_update_dag.py +0 -0
  33. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/indexes/ror_organizations/ror_zenodo_api.py +0 -0
  34. {crossref_matcher-1.2.2/crossref_matcher/resources → crossref_matcher-1.3.1/crossref_matcher/matching}/__init__.py +0 -0
  35. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/matching/http_utils.py +0 -0
  36. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/matching/run_strategy_on_dataset.py +0 -0
  37. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/matching/schemas.py +0 -0
  38. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/matching/search.py +0 -0
  39. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/matching/utils.py +0 -0
  40. {crossref_matcher-1.2.2/crossref_matcher/resources/affiliation → crossref_matcher-1.3.1/crossref_matcher/resources}/__init__.py +0 -0
  41. {crossref_matcher-1.2.2/crossref_matcher/resources/data → crossref_matcher-1.3.1/crossref_matcher/resources/affiliation}/__init__.py +0 -0
  42. {crossref_matcher-1.2.2/crossref_matcher/resources/data/common_ror_endings → crossref_matcher-1.3.1/crossref_matcher/resources/data}/__init__.py +0 -0
  43. {crossref_matcher-1.2.2/crossref_matcher/resources/data/countries → crossref_matcher-1.3.1/crossref_matcher/resources/data/common_ror_endings}/__init__.py +0 -0
  44. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/common_ror_endings/common_ror_endings.txt +0 -0
  45. {crossref_matcher-1.2.2/crossref_matcher/resources/data/datasets → crossref_matcher-1.3.1/crossref_matcher/resources/data/countries}/__init__.py +0 -0
  46. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/countries/china_placenames.txt +0 -0
  47. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/countries/countries.txt +0 -0
  48. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/countries/countries2.txt +0 -0
  49. {crossref_matcher-1.2.2/crossref_matcher/resources/data/models → crossref_matcher-1.3.1/crossref_matcher/resources/data/datasets}/__init__.py +0 -0
  50. {crossref_matcher-1.2.2/crossref_matcher/resources/data/models/preprint_ensemble_ml → crossref_matcher-1.3.1/crossref_matcher/resources/data/models}/__init__.py +0 -0
  51. {crossref_matcher-1.2.2/crossref_matcher/resources/data/models/reference_nn_unstructured → crossref_matcher-1.3.1/crossref_matcher/resources/data/models/preprint_ensemble_ml}/__init__.py +0 -0
  52. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/models/preprint_ensemble_ml/data_counters0.pkl +0 -0
  53. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/models/preprint_ensemble_ml/eval/events.out.tfevents.1690572611.knopfler.local +0 -0
  54. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.opt_slots0.npy.gz +0 -0
  55. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.pkl.gz +0 -0
  56. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.weights.npy.gz +0 -0
  57. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/models/preprint_ensemble_ml/train/events.out.tfevents.1690572611.knopfler.local +0 -0
  58. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/models/preprint_ml/data_counters0.pkl +0 -0
  59. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/models/preprint_ml/eval/events.out.tfevents.1690302892.knopfler.local +0 -0
  60. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/models/preprint_ml/model.opt_slots0.npy.gz +0 -0
  61. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/models/preprint_ml/model.pkl.gz +0 -0
  62. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/models/preprint_ml/model.weights.npy.gz +0 -0
  63. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/models/preprint_ml/train/events.out.tfevents.1690302891.knopfler.local +0 -0
  64. {crossref_matcher-1.2.2/crossref_matcher/strategies/affiliation → crossref_matcher-1.3.1/crossref_matcher/resources/data/models/reference_nn_unstructured}/__init__.py +0 -0
  65. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/models/reference_nn_unstructured/data_counters0.pkl +0 -0
  66. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/models/reference_nn_unstructured/eval/events.out.tfevents.1681511550.knopfler.local +0 -0
  67. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.opt_slots0.npy.gz +0 -0
  68. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.pkl.gz +0 -0
  69. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.weights.npy.gz +0 -0
  70. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/data/models/reference_nn_unstructured/train/events.out.tfevents.1681511550.knopfler.local +0 -0
  71. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/resources/plugins.json +0 -0
  72. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/run_plugin_test.py +0 -0
  73. {crossref_matcher-1.2.2/crossref_matcher/strategies/affiliation/multi_search → crossref_matcher-1.3.1/crossref_matcher/strategies/affiliation}/__init__.py +0 -0
  74. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/affiliation/es_utils.py +0 -0
  75. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/affiliation/matching.py +0 -0
  76. {crossref_matcher-1.2.2/crossref_matcher/strategies/affiliation/single_search → crossref_matcher-1.3.1/crossref_matcher/strategies/affiliation/multi_search}/__init__.py +0 -0
  77. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/affiliation/multi_search/strategy.py +0 -0
  78. {crossref_matcher-1.2.2/crossref_matcher/strategies/funder → crossref_matcher-1.3.1/crossref_matcher/strategies/affiliation/single_search}/__init__.py +0 -0
  79. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/affiliation/single_search/strategy.py +0 -0
  80. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/affiliation/strategy.py +0 -0
  81. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/common.py +0 -0
  82. {crossref_matcher-1.2.2/crossref_matcher/strategies/funder/funder_name_to_ror_search → crossref_matcher-1.3.1/crossref_matcher/strategies/funder}/__init__.py +0 -0
  83. {crossref_matcher-1.2.2/crossref_matcher/strategies/healthcheck → crossref_matcher-1.3.1/crossref_matcher/strategies/funder/funder_name_to_ror_search}/__init__.py +0 -0
  84. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/funder/funder_name_to_ror_search/strategy.py +0 -0
  85. {crossref_matcher-1.2.2/crossref_matcher/strategies/healthcheck → crossref_matcher-1.3.1/crossref_matcher/strategies}/healthcheck/__init__.py +0 -0
  86. {crossref_matcher-1.2.2/crossref_matcher/strategies/preprint → crossref_matcher-1.3.1/crossref_matcher/strategies/healthcheck/healthcheck}/__init__.py +0 -0
  87. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/healthcheck/healthcheck/strategy.py +0 -0
  88. {crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/ensemble_avg → crossref_matcher-1.3.1/crossref_matcher/strategies/preprint}/__init__.py +0 -0
  89. {crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/ensemble_cascade → crossref_matcher-1.3.1/crossref_matcher/strategies/preprint/ensemble_avg}/__init__.py +0 -0
  90. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/preprint/ensemble_avg/strategy.py +0 -0
  91. {crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/ensemble_ml → crossref_matcher-1.3.1/crossref_matcher/strategies/preprint/ensemble_cascade}/__init__.py +0 -0
  92. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/preprint/ensemble_cascade/strategy.py +0 -0
  93. {crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/preprint_ml → crossref_matcher-1.3.1/crossref_matcher/strategies/preprint/ensemble_ml}/__init__.py +0 -0
  94. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/preprint/ensemble_ml/get_train_data.py +0 -0
  95. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/preprint/ensemble_ml/strategy.py +0 -0
  96. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/preprint/ensemble_ml/train.py +0 -0
  97. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/preprint/ensemble_ml/utils.py +0 -0
  98. {crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/sbmv → crossref_matcher-1.3.1/crossref_matcher/strategies/preprint/preprint_ml}/__init__.py +0 -0
  99. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/preprint/preprint_ml/get_train_data.py +0 -0
  100. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/preprint/preprint_ml/strategy.py +0 -0
  101. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/preprint/preprint_ml/train.py +0 -0
  102. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/preprint/preprint_ml/utils.py +0 -0
  103. {crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/sbmv_es → crossref_matcher-1.3.1/crossref_matcher/strategies/preprint/sbmv}/__init__.py +0 -0
  104. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/preprint/sbmv/strategy.py +0 -0
  105. {crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/sbmv_rels → crossref_matcher-1.3.1/crossref_matcher/strategies/preprint/sbmv_es}/__init__.py +0 -0
  106. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/preprint/sbmv_es/strategy.py +0 -0
  107. {crossref_matcher-1.2.2/crossref_matcher/strategies/reference → crossref_matcher-1.3.1/crossref_matcher/strategies/preprint/sbmv_rels}/__init__.py +0 -0
  108. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/preprint/sbmv_rels/strategy.py +0 -0
  109. {crossref_matcher-1.2.2/crossref_matcher/strategies/reference/cs_combined → crossref_matcher-1.3.1/crossref_matcher/strategies/reference}/__init__.py +0 -0
  110. {crossref_matcher-1.2.2/crossref_matcher/strategies/reference/nn_unstructured → crossref_matcher-1.3.1/crossref_matcher/strategies/reference/cs_combined}/__init__.py +0 -0
  111. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/reference/cs_combined/strategy.py +0 -0
  112. {crossref_matcher-1.2.2/crossref_matcher/strategies/reference/parsed_doi → crossref_matcher-1.3.1/crossref_matcher/strategies/reference/nn_unstructured}/__init__.py +0 -0
  113. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/reference/nn_unstructured/get_train_data.py +0 -0
  114. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/reference/nn_unstructured/strategy.py +0 -0
  115. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/reference/nn_unstructured/train.py +0 -0
  116. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/reference/nn_unstructured/utils.py +0 -0
  117. {crossref_matcher-1.2.2/crossref_matcher/strategies/reference/parsed_isbn → crossref_matcher-1.3.1/crossref_matcher/strategies/reference/parsed_doi}/__init__.py +0 -0
  118. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/reference/parsed_doi/strategy.py +0 -0
  119. {crossref_matcher-1.2.2/crossref_matcher/strategies/reference/sbmv → crossref_matcher-1.3.1/crossref_matcher/strategies/reference/parsed_isbn}/__init__.py +0 -0
  120. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/reference/parsed_isbn/strategy.py +0 -0
  121. {crossref_matcher-1.2.2/crossref_matcher/strategies/reference/sbmv_orig → crossref_matcher-1.3.1/crossref_matcher/strategies/reference/sbmv}/__init__.py +0 -0
  122. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/reference/sbmv/strategy.py +0 -0
  123. {crossref_matcher-1.2.2/crossref_matcher/strategies/reference/sbmv_unstructured → crossref_matcher-1.3.1/crossref_matcher/strategies/reference/sbmv_orig}/__init__.py +0 -0
  124. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/reference/sbmv_orig/strategy.py +0 -0
  125. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher/strategies/reference/sbmv_unstructured/strategy.py +0 -0
  126. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher.egg-info/dependency_links.txt +0 -0
  127. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher.egg-info/entry_points.txt +0 -0
  128. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher.egg-info/requires.txt +0 -0
  129. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/crossref_matcher.egg-info/top_level.txt +0 -0
  130. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/setup.cfg +0 -0
  131. {crossref_matcher-1.2.2 → crossref_matcher-1.3.1}/setup.py +0 -0
@@ -1,5 +1,6 @@
1
1
  include crossref_matcher/resources/plugins.json
2
2
  include crossref_matcher/resources/VERSION.txt
3
+ recursive-include crossref_matcher/strategies *.txt
3
4
  recursive-include crossref_matcher/resources/data/models *.pkl *.gz *.local
4
5
  recursive-include crossref_matcher/resources/data/common_ror_endings *.txt
5
6
  recursive-include crossref_matcher/resources/data/countries *.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crossref-matcher
3
- Version: 1.2.2
3
+ Version: 1.3.1
4
4
  Summary: Crossref's matching API
5
5
  Home-page: https://gitlab.com/crossref/marple
6
6
  Author: Crossref
@@ -135,6 +135,49 @@ Strategies are located in the [strategies directory](crossref_matcher/strategies
135
135
 
136
136
  Strategies must be added to the "enabled" list in [plugins.json](crossref_matcher/resources/plugins.json) before they can be used.
137
137
 
138
+ ### Strategy versioning
139
+
140
+ Each strategy directory can contain a `version.txt` file with a two-part version number (`MAJOR.MINOR`). This versioning is independent of the project's semver.
141
+
142
+ ```
143
+ crossref_matcher/strategies/preprint/sbmv/
144
+ ├── strategy.py
145
+ └── version.txt # contains e.g. "1.2"
146
+ ```
147
+
148
+ When a strategy is loaded, its version is automatically available as a class attribute:
149
+
150
+ ```python
151
+ from crossref_matcher.strategies.preprint.sbmv.strategy import Sbmv
152
+ print(Sbmv.version) # "1.2"
153
+ ```
154
+
155
+ Strategies without a `version.txt` have `version = None`.
156
+
157
+ **Bumping a version:**
158
+
159
+ ```sh
160
+ python scripts/bump_strategy_version.py preprint/sbmv minor # 1.2 → 1.3
161
+ python scripts/bump_strategy_version.py preprint/sbmv major # 1.2 → 2.0
162
+ ```
163
+
164
+ The path argument can be relative to the `strategies/` directory or an absolute path.
165
+
166
+ **CI enforcement:**
167
+
168
+ A CI job (`check-strategy-versions` in `.gitlab-ci.yml`) fails any merge request where a strategy's files are changed but `version.txt` is not updated. To bypass for a trivial change (e.g. a typo fix):
169
+
170
+ - Pass `--force` to the check script, or
171
+ - Set the `SKIP_BUMP_CHECK=1` environment variable on the pipeline run.
172
+
173
+ **Local pre-commit hook:**
174
+
175
+ To enable the check locally (as a non-blocking warning):
176
+
177
+ ```sh
178
+ git config core.hooksPath .githooks
179
+ ```
180
+
138
181
  ### Dependencies
139
182
 
140
183
  Requirements are defined in `setup.py`. Requirements files with exact versions can be updated with `pip-compile`, which is a part of `pip-tools`. First run `pip install pip-tools`. Then:
@@ -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()
@@ -20,17 +20,25 @@ import logging
20
20
 
21
21
  logger = logging.getLogger(__name__)
22
22
 
23
+ STRATEGIES_DIR = "crossref_matcher/strategies"
23
24
 
24
- def pascal_to_snake_case(s):
25
+
26
+ def pascal_to_snake_case(s: str) -> str:
25
27
  return re.sub(r"(?<!^)(?=[A-Z])", "_", s).lower()
26
28
 
27
29
 
28
- def kebab_to_snake_case(string: str) -> str:
30
+ def kebab_to_snake_case(string: str | None) -> str | None:
29
31
  if string is None:
30
32
  return None
31
33
  return "_".join(string.split("-"))
32
34
 
33
35
 
36
+ def snake_to_kebab_case(string: str | None) -> str | None:
37
+ if string is None:
38
+ return None
39
+ return "-".join(string.split("_"))
40
+
41
+
34
42
  class NoDefaultStrategyError(ValueError):
35
43
  """Raised when no default strategy is configured for a task."""
36
44
 
@@ -57,6 +65,7 @@ class Strategy(ABC):
57
65
  id: str
58
66
  task: MatchTask
59
67
  description: str
68
+ version: str | None = None
60
69
 
61
70
  @classmethod
62
71
  def create_dummy_subclass(cls, module_name: str, error: str):
@@ -230,6 +239,13 @@ class Strategy(ABC):
230
239
  raise TypeError(
231
240
  f"Class {cls.__name__} must have a class field '{name}' of type {type.__name__}."
232
241
  )
242
+ try:
243
+ pkg = cls.__module__.removesuffix(".strategy")
244
+ version_ref = resources.files(pkg) / "version.txt"
245
+ if version_ref.is_file():
246
+ cls.version = version_ref.read_text().strip()
247
+ except (TypeError, OSError, ModuleNotFoundError):
248
+ pass
233
249
 
234
250
  @classmethod
235
251
  def is_default(cls):
@@ -274,7 +290,7 @@ def list_strategies(
274
290
 
275
291
 
276
292
  def get_strategy_by_id(
277
- strategy_id: str | None,
293
+ strategy_id: str | None = None,
278
294
  task: MatchTask | str | None = None,
279
295
  allow_disabled: bool = False,
280
296
  ) -> type[Strategy]:
@@ -298,7 +314,11 @@ def get_strategy_by_id(
298
314
  raise ValueError("Must specify at least one of [strategy_id, task]")
299
315
  return get_default_strategy(task)
300
316
  strategy_found = False
301
- for id_to_check in [strategy_id, kebab_to_snake_case(strategy_id)]:
317
+ for id_to_check in [
318
+ strategy_id,
319
+ kebab_to_snake_case(strategy_id),
320
+ snake_to_kebab_case(strategy_id),
321
+ ]:
302
322
  strategy = [
303
323
  c
304
324
  for c in list_strategies(task=task, include_disabled=allow_disabled)
@@ -354,7 +374,7 @@ def get_strategy_by_module_name(
354
374
 
355
375
 
356
376
  def get_strategy(
357
- strategy_id: str | None,
377
+ strategy_id: str | None = None,
358
378
  task: MatchTask | str | None = None,
359
379
  allow_disabled: bool = False,
360
380
  ) -> type[Strategy]:
@@ -0,0 +1,77 @@
1
+ import logging
2
+ import subprocess
3
+ from pathlib import PurePosixPath
4
+
5
+ from crossref_matcher.strategies import STRATEGIES_DIR
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ _STRATEGIES_PATH = PurePosixPath(STRATEGIES_DIR)
10
+
11
+
12
+ def _strategy_dir(path: str) -> str | None:
13
+ """Return 'task/strategy' for a path inside the strategies dir, or None."""
14
+ try:
15
+ rel = PurePosixPath(path).relative_to(_STRATEGIES_PATH)
16
+ except ValueError:
17
+ return None
18
+ if len(rel.parts) < 3: # need task / strategy / filename at minimum
19
+ return None
20
+ return f"{rel.parts[0]}/{rel.parts[1]}"
21
+
22
+
23
+ def get_changed_files(base: str | None, staged: bool) -> list[str]:
24
+ if staged:
25
+ result = subprocess.run(
26
+ ["git", "diff", "--cached", "--name-only"],
27
+ capture_output=True,
28
+ text=True,
29
+ check=True,
30
+ )
31
+ elif base:
32
+ result = subprocess.run(
33
+ ["git", "diff", "--name-only", f"{base}...HEAD"],
34
+ capture_output=True,
35
+ text=True,
36
+ check=True,
37
+ )
38
+ else:
39
+ raise ValueError("Must specify --base or --staged")
40
+ return result.stdout.splitlines()
41
+
42
+
43
+ def get_changed_strategy_dirs(changed_files: list[str]) -> set[str]:
44
+ return {
45
+ _strategy_dir(f)
46
+ for f in changed_files
47
+ if PurePosixPath(f).name != "version.txt"
48
+ if _strategy_dir(f) is not None
49
+ }
50
+
51
+
52
+ def check(base: str | None, staged: bool) -> bool:
53
+ changed_files = get_changed_files(base, staged)
54
+ changed_dirs = get_changed_strategy_dirs(changed_files)
55
+
56
+ if not changed_dirs:
57
+ return True
58
+
59
+ bumped_dirs = {
60
+ _strategy_dir(f)
61
+ for f in changed_files
62
+ if PurePosixPath(f).name == "version.txt"
63
+ if _strategy_dir(f) is not None
64
+ }
65
+
66
+ failures = sorted(changed_dirs - bumped_dirs)
67
+ if failures:
68
+ missing = "\n".join(f" {STRATEGIES_DIR}/{d}/version.txt" for d in failures)
69
+ logger.error(
70
+ "The following strategies have changes but no version bump:\n%s\n"
71
+ "\nRun: python scripts/bump_strategy_version.py <strategy-path> <major|minor>"
72
+ "\nTo skip: rerun with --force, or set SKIP_BUMP_CHECK=1",
73
+ missing,
74
+ )
75
+ return False
76
+
77
+ return True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crossref-matcher
3
- Version: 1.2.2
3
+ Version: 1.3.1
4
4
  Summary: Crossref's matching API
5
5
  Home-page: https://gitlab.com/crossref/marple
6
6
  Author: Crossref
@@ -20,6 +20,8 @@ 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
@@ -69,6 +71,7 @@ crossref_matcher/resources/data/models/reference_nn_unstructured/eval/events.out
69
71
  crossref_matcher/resources/data/models/reference_nn_unstructured/train/events.out.tfevents.1681511550.knopfler.local
70
72
  crossref_matcher/strategies/__init__.py
71
73
  crossref_matcher/strategies/common.py
74
+ crossref_matcher/strategies/versioning.py
72
75
  crossref_matcher/strategies/affiliation/__init__.py
73
76
  crossref_matcher/strategies/affiliation/es_utils.py
74
77
  crossref_matcher/strategies/affiliation/matching.py
@@ -80,6 +83,7 @@ crossref_matcher/strategies/affiliation/single_search/strategy.py
80
83
  crossref_matcher/strategies/funder/__init__.py
81
84
  crossref_matcher/strategies/funder/funder_name_to_ror_search/__init__.py
82
85
  crossref_matcher/strategies/funder/funder_name_to_ror_search/strategy.py
86
+ crossref_matcher/strategies/funder/funder_name_to_ror_search/version.txt
83
87
  crossref_matcher/strategies/healthcheck/__init__.py
84
88
  crossref_matcher/strategies/healthcheck/healthcheck/__init__.py
85
89
  crossref_matcher/strategies/healthcheck/healthcheck/strategy.py