crossref-matcher 1.2.2__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/MANIFEST.in +1 -0
  2. {crossref_matcher-1.2.2/crossref_matcher.egg-info → crossref_matcher-1.3.0}/PKG-INFO +1 -1
  3. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/README.md +43 -0
  4. crossref_matcher-1.3.0/crossref_matcher/resources/VERSION.txt +1 -0
  5. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/__init__.py +25 -5
  6. crossref_matcher-1.3.0/crossref_matcher/strategies/funder/funder_name_to_ror_search/version.txt +1 -0
  7. crossref_matcher-1.3.0/crossref_matcher/strategies/versioning.py +77 -0
  8. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0/crossref_matcher.egg-info}/PKG-INFO +1 -1
  9. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher.egg-info/SOURCES.txt +2 -0
  10. crossref_matcher-1.2.2/crossref_matcher/resources/VERSION.txt +0 -1
  11. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/LICENSE +0 -0
  12. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/MatchTask.py +0 -0
  13. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/__init__.py +0 -0
  14. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/app.py +0 -0
  15. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/evaluation/__init__.py +0 -0
  16. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/evaluation/all_links.py +0 -0
  17. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/evaluation/api_routes.py +0 -0
  18. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/evaluation/dataset_ingest.py +0 -0
  19. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/evaluation/evaluation.py +0 -0
  20. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/evaluation/google_sheets_connector.py +0 -0
  21. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/evaluation/ror_utils.py +0 -0
  22. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/evaluation/schemas.py +0 -0
  23. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/indexes/__init__.py +0 -0
  24. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/indexes/preprint_matching/__init__.py +0 -0
  25. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/indexes/preprint_matching/indexer_service.py +0 -0
  26. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/indexes/ror_organizations/__init__.py +0 -0
  27. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/indexes/ror_organizations/create_index.py +0 -0
  28. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/indexes/ror_organizations/index_data.py +0 -0
  29. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/indexes/ror_organizations/ror_opensearch_update_dag.py +0 -0
  30. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/indexes/ror_organizations/ror_zenodo_api.py +0 -0
  31. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/matching/__init__.py +0 -0
  32. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/matching/http_utils.py +0 -0
  33. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/matching/run_strategy_on_dataset.py +0 -0
  34. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/matching/schemas.py +0 -0
  35. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/matching/search.py +0 -0
  36. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/matching/utils.py +0 -0
  37. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/__init__.py +0 -0
  38. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/affiliation/__init__.py +0 -0
  39. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/__init__.py +0 -0
  40. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/common_ror_endings/__init__.py +0 -0
  41. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/common_ror_endings/common_ror_endings.txt +0 -0
  42. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/countries/__init__.py +0 -0
  43. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/countries/china_placenames.txt +0 -0
  44. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/countries/countries.txt +0 -0
  45. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/countries/countries2.txt +0 -0
  46. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/datasets/__init__.py +0 -0
  47. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/__init__.py +0 -0
  48. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/preprint_ensemble_ml/__init__.py +0 -0
  49. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/preprint_ensemble_ml/data_counters0.pkl +0 -0
  50. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/preprint_ensemble_ml/eval/events.out.tfevents.1690572611.knopfler.local +0 -0
  51. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.opt_slots0.npy.gz +0 -0
  52. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.pkl.gz +0 -0
  53. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.weights.npy.gz +0 -0
  54. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/preprint_ensemble_ml/train/events.out.tfevents.1690572611.knopfler.local +0 -0
  55. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/preprint_ml/data_counters0.pkl +0 -0
  56. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/preprint_ml/eval/events.out.tfevents.1690302892.knopfler.local +0 -0
  57. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/preprint_ml/model.opt_slots0.npy.gz +0 -0
  58. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/preprint_ml/model.pkl.gz +0 -0
  59. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/preprint_ml/model.weights.npy.gz +0 -0
  60. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/preprint_ml/train/events.out.tfevents.1690302891.knopfler.local +0 -0
  61. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/reference_nn_unstructured/__init__.py +0 -0
  62. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/reference_nn_unstructured/data_counters0.pkl +0 -0
  63. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/reference_nn_unstructured/eval/events.out.tfevents.1681511550.knopfler.local +0 -0
  64. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.opt_slots0.npy.gz +0 -0
  65. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.pkl.gz +0 -0
  66. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.weights.npy.gz +0 -0
  67. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/data/models/reference_nn_unstructured/train/events.out.tfevents.1681511550.knopfler.local +0 -0
  68. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/resources/plugins.json +0 -0
  69. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/run_plugin_test.py +0 -0
  70. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/affiliation/__init__.py +0 -0
  71. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/affiliation/es_utils.py +0 -0
  72. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/affiliation/matching.py +0 -0
  73. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/affiliation/multi_search/__init__.py +0 -0
  74. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/affiliation/multi_search/strategy.py +0 -0
  75. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/affiliation/single_search/__init__.py +0 -0
  76. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/affiliation/single_search/strategy.py +0 -0
  77. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/affiliation/strategy.py +0 -0
  78. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/common.py +0 -0
  79. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/funder/__init__.py +0 -0
  80. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/funder/funder_name_to_ror_search/__init__.py +0 -0
  81. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/funder/funder_name_to_ror_search/strategy.py +0 -0
  82. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/healthcheck/__init__.py +0 -0
  83. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/healthcheck/healthcheck/__init__.py +0 -0
  84. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/healthcheck/healthcheck/strategy.py +0 -0
  85. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/__init__.py +0 -0
  86. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/ensemble_avg/__init__.py +0 -0
  87. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/ensemble_avg/strategy.py +0 -0
  88. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/ensemble_cascade/__init__.py +0 -0
  89. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/ensemble_cascade/strategy.py +0 -0
  90. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/ensemble_ml/__init__.py +0 -0
  91. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/ensemble_ml/get_train_data.py +0 -0
  92. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/ensemble_ml/strategy.py +0 -0
  93. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/ensemble_ml/train.py +0 -0
  94. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/ensemble_ml/utils.py +0 -0
  95. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/preprint_ml/__init__.py +0 -0
  96. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/preprint_ml/get_train_data.py +0 -0
  97. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/preprint_ml/strategy.py +0 -0
  98. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/preprint_ml/train.py +0 -0
  99. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/preprint_ml/utils.py +0 -0
  100. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/sbmv/__init__.py +0 -0
  101. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/sbmv/strategy.py +0 -0
  102. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/sbmv_es/__init__.py +0 -0
  103. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/sbmv_es/strategy.py +0 -0
  104. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/sbmv_rels/__init__.py +0 -0
  105. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/preprint/sbmv_rels/strategy.py +0 -0
  106. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/reference/__init__.py +0 -0
  107. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/reference/cs_combined/__init__.py +0 -0
  108. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/reference/cs_combined/strategy.py +0 -0
  109. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/reference/nn_unstructured/__init__.py +0 -0
  110. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/reference/nn_unstructured/get_train_data.py +0 -0
  111. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/reference/nn_unstructured/strategy.py +0 -0
  112. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/reference/nn_unstructured/train.py +0 -0
  113. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/reference/nn_unstructured/utils.py +0 -0
  114. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/reference/parsed_doi/__init__.py +0 -0
  115. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/reference/parsed_doi/strategy.py +0 -0
  116. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/reference/parsed_isbn/__init__.py +0 -0
  117. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/reference/parsed_isbn/strategy.py +0 -0
  118. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/reference/sbmv/__init__.py +0 -0
  119. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/reference/sbmv/strategy.py +0 -0
  120. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/reference/sbmv_orig/__init__.py +0 -0
  121. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/reference/sbmv_orig/strategy.py +0 -0
  122. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/reference/sbmv_unstructured/__init__.py +0 -0
  123. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher/strategies/reference/sbmv_unstructured/strategy.py +0 -0
  124. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher.egg-info/dependency_links.txt +0 -0
  125. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher.egg-info/entry_points.txt +0 -0
  126. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher.egg-info/requires.txt +0 -0
  127. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/crossref_matcher.egg-info/top_level.txt +0 -0
  128. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/setup.cfg +0 -0
  129. {crossref_matcher-1.2.2 → crossref_matcher-1.3.0}/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.0
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:
@@ -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.0
4
4
  Summary: Crossref's matching API
5
5
  Home-page: https://gitlab.com/crossref/marple
6
6
  Author: Crossref
@@ -69,6 +69,7 @@ crossref_matcher/resources/data/models/reference_nn_unstructured/eval/events.out
69
69
  crossref_matcher/resources/data/models/reference_nn_unstructured/train/events.out.tfevents.1681511550.knopfler.local
70
70
  crossref_matcher/strategies/__init__.py
71
71
  crossref_matcher/strategies/common.py
72
+ crossref_matcher/strategies/versioning.py
72
73
  crossref_matcher/strategies/affiliation/__init__.py
73
74
  crossref_matcher/strategies/affiliation/es_utils.py
74
75
  crossref_matcher/strategies/affiliation/matching.py
@@ -80,6 +81,7 @@ crossref_matcher/strategies/affiliation/single_search/strategy.py
80
81
  crossref_matcher/strategies/funder/__init__.py
81
82
  crossref_matcher/strategies/funder/funder_name_to_ror_search/__init__.py
82
83
  crossref_matcher/strategies/funder/funder_name_to_ror_search/strategy.py
84
+ crossref_matcher/strategies/funder/funder_name_to_ror_search/version.txt
83
85
  crossref_matcher/strategies/healthcheck/__init__.py
84
86
  crossref_matcher/strategies/healthcheck/healthcheck/__init__.py
85
87
  crossref_matcher/strategies/healthcheck/healthcheck/strategy.py