crossref-matcher 1.2.1__tar.gz → 1.2.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 (131) hide show
  1. {crossref_matcher-1.2.1/crossref_matcher.egg-info → crossref_matcher-1.2.2}/PKG-INFO +1 -1
  2. crossref_matcher-1.2.2/crossref_matcher/resources/VERSION.txt +1 -0
  3. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/plugins.json +0 -1
  4. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/funder/funder_name_to_ror_search/strategy.py +40 -2
  5. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2/crossref_matcher.egg-info}/PKG-INFO +1 -1
  6. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher.egg-info/SOURCES.txt +0 -4
  7. crossref_matcher-1.2.1/crossref_matcher/resources/VERSION.txt +0 -1
  8. crossref_matcher-1.2.1/crossref_matcher/strategies/funder/funder_single_search_01/strategy.py +0 -481
  9. crossref_matcher-1.2.1/crossref_matcher/strategies/funder/funder_single_search_02/strategy.py +0 -520
  10. crossref_matcher-1.2.1/crossref_matcher/strategies/reference/sbmv_orig/__init__.py +0 -0
  11. crossref_matcher-1.2.1/crossref_matcher/strategies/reference/sbmv_unstructured/__init__.py +0 -0
  12. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/LICENSE +0 -0
  13. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/MANIFEST.in +0 -0
  14. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/README.md +0 -0
  15. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/MatchTask.py +0 -0
  16. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/__init__.py +0 -0
  17. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/app.py +0 -0
  18. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/evaluation/__init__.py +0 -0
  19. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/evaluation/all_links.py +0 -0
  20. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/evaluation/api_routes.py +0 -0
  21. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/evaluation/dataset_ingest.py +0 -0
  22. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/evaluation/evaluation.py +0 -0
  23. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/evaluation/google_sheets_connector.py +0 -0
  24. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/evaluation/ror_utils.py +0 -0
  25. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/evaluation/schemas.py +0 -0
  26. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/indexes/__init__.py +0 -0
  27. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/indexes/preprint_matching/__init__.py +0 -0
  28. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/indexes/preprint_matching/indexer_service.py +0 -0
  29. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/indexes/ror_organizations/__init__.py +0 -0
  30. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/indexes/ror_organizations/create_index.py +0 -0
  31. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/indexes/ror_organizations/index_data.py +0 -0
  32. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/indexes/ror_organizations/ror_opensearch_update_dag.py +0 -0
  33. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/indexes/ror_organizations/ror_zenodo_api.py +0 -0
  34. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/matching/__init__.py +0 -0
  35. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/matching/http_utils.py +0 -0
  36. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/matching/run_strategy_on_dataset.py +0 -0
  37. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/matching/schemas.py +0 -0
  38. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/matching/search.py +0 -0
  39. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/matching/utils.py +0 -0
  40. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/__init__.py +0 -0
  41. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/affiliation/__init__.py +0 -0
  42. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/__init__.py +0 -0
  43. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/common_ror_endings/__init__.py +0 -0
  44. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/common_ror_endings/common_ror_endings.txt +0 -0
  45. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/countries/__init__.py +0 -0
  46. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/countries/china_placenames.txt +0 -0
  47. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/countries/countries.txt +0 -0
  48. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/countries/countries2.txt +0 -0
  49. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/datasets/__init__.py +0 -0
  50. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/__init__.py +0 -0
  51. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/__init__.py +0 -0
  52. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/data_counters0.pkl +0 -0
  53. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/eval/events.out.tfevents.1690572611.knopfler.local +0 -0
  54. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.opt_slots0.npy.gz +0 -0
  55. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.pkl.gz +0 -0
  56. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.weights.npy.gz +0 -0
  57. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/train/events.out.tfevents.1690572611.knopfler.local +0 -0
  58. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ml/data_counters0.pkl +0 -0
  59. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ml/eval/events.out.tfevents.1690302892.knopfler.local +0 -0
  60. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ml/model.opt_slots0.npy.gz +0 -0
  61. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ml/model.pkl.gz +0 -0
  62. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ml/model.weights.npy.gz +0 -0
  63. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ml/train/events.out.tfevents.1690302891.knopfler.local +0 -0
  64. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/__init__.py +0 -0
  65. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/data_counters0.pkl +0 -0
  66. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/eval/events.out.tfevents.1681511550.knopfler.local +0 -0
  67. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.opt_slots0.npy.gz +0 -0
  68. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.pkl.gz +0 -0
  69. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.weights.npy.gz +0 -0
  70. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/train/events.out.tfevents.1681511550.knopfler.local +0 -0
  71. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/run_plugin_test.py +0 -0
  72. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/__init__.py +0 -0
  73. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/affiliation/__init__.py +0 -0
  74. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/affiliation/es_utils.py +0 -0
  75. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/affiliation/matching.py +0 -0
  76. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/affiliation/multi_search/__init__.py +0 -0
  77. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/affiliation/multi_search/strategy.py +0 -0
  78. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/affiliation/single_search/__init__.py +0 -0
  79. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/affiliation/single_search/strategy.py +0 -0
  80. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/affiliation/strategy.py +0 -0
  81. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/common.py +0 -0
  82. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/funder/__init__.py +0 -0
  83. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/funder/funder_name_to_ror_search/__init__.py +0 -0
  84. {crossref_matcher-1.2.1/crossref_matcher/strategies/funder/funder_single_search_01 → crossref_matcher-1.2.2/crossref_matcher/strategies/healthcheck}/__init__.py +0 -0
  85. {crossref_matcher-1.2.1/crossref_matcher/strategies/funder/funder_single_search_02 → crossref_matcher-1.2.2/crossref_matcher/strategies/healthcheck/healthcheck}/__init__.py +0 -0
  86. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/healthcheck/healthcheck/strategy.py +0 -0
  87. {crossref_matcher-1.2.1/crossref_matcher/strategies/healthcheck → crossref_matcher-1.2.2/crossref_matcher/strategies/preprint}/__init__.py +0 -0
  88. {crossref_matcher-1.2.1/crossref_matcher/strategies/healthcheck/healthcheck → crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/ensemble_avg}/__init__.py +0 -0
  89. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/ensemble_avg/strategy.py +0 -0
  90. {crossref_matcher-1.2.1/crossref_matcher/strategies/preprint → crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/ensemble_cascade}/__init__.py +0 -0
  91. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/ensemble_cascade/strategy.py +0 -0
  92. {crossref_matcher-1.2.1/crossref_matcher/strategies/preprint/ensemble_avg → crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/ensemble_ml}/__init__.py +0 -0
  93. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/ensemble_ml/get_train_data.py +0 -0
  94. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/ensemble_ml/strategy.py +0 -0
  95. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/ensemble_ml/train.py +0 -0
  96. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/ensemble_ml/utils.py +0 -0
  97. {crossref_matcher-1.2.1/crossref_matcher/strategies/preprint/ensemble_cascade → crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/preprint_ml}/__init__.py +0 -0
  98. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/preprint_ml/get_train_data.py +0 -0
  99. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/preprint_ml/strategy.py +0 -0
  100. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/preprint_ml/train.py +0 -0
  101. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/preprint_ml/utils.py +0 -0
  102. {crossref_matcher-1.2.1/crossref_matcher/strategies/preprint/ensemble_ml → crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/sbmv}/__init__.py +0 -0
  103. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/sbmv/strategy.py +0 -0
  104. {crossref_matcher-1.2.1/crossref_matcher/strategies/preprint/preprint_ml → crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/sbmv_es}/__init__.py +0 -0
  105. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/sbmv_es/strategy.py +0 -0
  106. {crossref_matcher-1.2.1/crossref_matcher/strategies/preprint/sbmv → crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/sbmv_rels}/__init__.py +0 -0
  107. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/sbmv_rels/strategy.py +0 -0
  108. {crossref_matcher-1.2.1/crossref_matcher/strategies/preprint/sbmv_es → crossref_matcher-1.2.2/crossref_matcher/strategies/reference}/__init__.py +0 -0
  109. {crossref_matcher-1.2.1/crossref_matcher/strategies/preprint/sbmv_rels → crossref_matcher-1.2.2/crossref_matcher/strategies/reference/cs_combined}/__init__.py +0 -0
  110. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/cs_combined/strategy.py +0 -0
  111. {crossref_matcher-1.2.1/crossref_matcher/strategies/reference → crossref_matcher-1.2.2/crossref_matcher/strategies/reference/nn_unstructured}/__init__.py +0 -0
  112. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/nn_unstructured/get_train_data.py +0 -0
  113. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/nn_unstructured/strategy.py +0 -0
  114. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/nn_unstructured/train.py +0 -0
  115. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/nn_unstructured/utils.py +0 -0
  116. {crossref_matcher-1.2.1/crossref_matcher/strategies/reference/cs_combined → crossref_matcher-1.2.2/crossref_matcher/strategies/reference/parsed_doi}/__init__.py +0 -0
  117. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/parsed_doi/strategy.py +0 -0
  118. {crossref_matcher-1.2.1/crossref_matcher/strategies/reference/nn_unstructured → crossref_matcher-1.2.2/crossref_matcher/strategies/reference/parsed_isbn}/__init__.py +0 -0
  119. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/parsed_isbn/strategy.py +0 -0
  120. {crossref_matcher-1.2.1/crossref_matcher/strategies/reference/parsed_doi → crossref_matcher-1.2.2/crossref_matcher/strategies/reference/sbmv}/__init__.py +0 -0
  121. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/sbmv/strategy.py +0 -0
  122. {crossref_matcher-1.2.1/crossref_matcher/strategies/reference/parsed_isbn → crossref_matcher-1.2.2/crossref_matcher/strategies/reference/sbmv_orig}/__init__.py +0 -0
  123. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/sbmv_orig/strategy.py +0 -0
  124. {crossref_matcher-1.2.1/crossref_matcher/strategies/reference/sbmv → crossref_matcher-1.2.2/crossref_matcher/strategies/reference/sbmv_unstructured}/__init__.py +0 -0
  125. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/sbmv_unstructured/strategy.py +0 -0
  126. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher.egg-info/dependency_links.txt +0 -0
  127. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher.egg-info/entry_points.txt +0 -0
  128. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher.egg-info/requires.txt +0 -0
  129. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher.egg-info/top_level.txt +0 -0
  130. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/setup.cfg +0 -0
  131. {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crossref-matcher
3
- Version: 1.2.1
3
+ Version: 1.2.2
4
4
  Summary: Crossref's matching API
5
5
  Home-page: https://gitlab.com/crossref/marple
6
6
  Author: Crossref
@@ -11,7 +11,6 @@
11
11
  "crossref_matcher.strategies.reference.sbmv",
12
12
  "crossref_matcher.strategies.reference.sbmv_orig",
13
13
  "crossref_matcher.strategies.reference.sbmv_unstructured",
14
- "crossref_matcher.strategies.funder.funder_single_search_01",
15
14
  "crossref_matcher.strategies.funder.funder_name_to_ror_search",
16
15
  "crossref_example_matching_strategy"
17
16
  ],
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import re
3
- from crossref_matcher.matching.utils import get_resource_path
4
3
  from typing import Iterable
4
+ from crossref_matcher.matching.utils import get_resource_path
5
5
  from dataclasses import dataclass
6
6
  import unidecode
7
7
  from crossref_matcher.strategies import Strategy, MatchTask
@@ -53,6 +53,7 @@ class FunderNameToRorSearch(Strategy):
53
53
  index = "organization"
54
54
  min_score = 96
55
55
 
56
+ # Matches acronyms: sequences of 2 or more consecutive uppercase ASCII letters as whole words (e.g., 'NASA', 'UNESCO').
56
57
  pattern_acronym = re.compile(r"(?P<acronym>\b[A-Z]{2,}\b)")
57
58
 
58
59
  def __init__(self, es_client=ES_CLIENT, max_candidates=18, extra_output=False):
@@ -117,6 +118,12 @@ class FunderNameToRorSearch(Strategy):
117
118
  def match(self, input_data):
118
119
  self.input_string_rep = StringRep(input_data)
119
120
  self.fund_countries = self.get_countries(self.input_string_rep)
121
+
122
+ # Before anything, check for exact acronym match (very conservatively)
123
+ acronym_result = self.acronym_substrategy_match(self.input_string_rep)
124
+ if acronym_result:
125
+ return [acronym_result]
126
+
120
127
  candidates = self.get_candidates(input_data, max_candidates=self.max_candidates)
121
128
  # store initial candidates for help with debugging/evaluation
122
129
  self.initial_candidates = [
@@ -200,7 +207,14 @@ class FunderNameToRorSearch(Strategy):
200
207
  )["hits"]["hits"]
201
208
 
202
209
  def _exclusion_acronym_common_ending(self, fund: StringRep, name: StringRep):
203
- # returns True if this `name` should be excluded
210
+ """
211
+ If the ROR name (`name`) being considered ends in a common ending
212
+ (e.g., "foundation", "research center") and contains an acronym
213
+ (e.g., "NASA"), then exclude it if the acronym(s) do not also occur
214
+ in the fund name (`fund`).
215
+
216
+ Returns True if this `name` should be excluded.
217
+ """
204
218
  acronyms = self.pattern_acronym.findall(name.original)
205
219
  if not acronyms:
206
220
  return False
@@ -416,3 +430,27 @@ class FunderNameToRorSearch(Strategy):
416
430
  def get_countries(self, s: StringRep) -> list[str]:
417
431
  codes = self.get_country_codes(s)
418
432
  return [self.to_region(c) for c in codes]
433
+
434
+ def acronym_substrategy_match(self, s: StringRep) -> dict | None:
435
+ stripped = s.original.strip()
436
+ whitelist = ["CNPq"]
437
+ # Must be 1 word, all uppercase, and 4 or more letters
438
+ if re.fullmatch(r"[A-Z]{4,}", stripped) or stripped in whitelist:
439
+ r = self.es_client.search(
440
+ body={
441
+ "size": 10,
442
+ "query": {
443
+ "bool": {"filter": {"term": {"acronyms": {"value": stripped}}}}
444
+ },
445
+ },
446
+ index=self.index,
447
+ )["hits"]["hits"]
448
+
449
+ # there must be one and only one match
450
+ if r and len(r) == 1:
451
+ return {
452
+ "id": r[0]["_id"],
453
+ "confidence": 1,
454
+ "strategies": [self.strategy],
455
+ }
456
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crossref-matcher
3
- Version: 1.2.1
3
+ Version: 1.2.2
4
4
  Summary: Crossref's matching API
5
5
  Home-page: https://gitlab.com/crossref/marple
6
6
  Author: Crossref
@@ -80,10 +80,6 @@ crossref_matcher/strategies/affiliation/single_search/strategy.py
80
80
  crossref_matcher/strategies/funder/__init__.py
81
81
  crossref_matcher/strategies/funder/funder_name_to_ror_search/__init__.py
82
82
  crossref_matcher/strategies/funder/funder_name_to_ror_search/strategy.py
83
- crossref_matcher/strategies/funder/funder_single_search_01/__init__.py
84
- crossref_matcher/strategies/funder/funder_single_search_01/strategy.py
85
- crossref_matcher/strategies/funder/funder_single_search_02/__init__.py
86
- crossref_matcher/strategies/funder/funder_single_search_02/strategy.py
87
83
  crossref_matcher/strategies/healthcheck/__init__.py
88
84
  crossref_matcher/strategies/healthcheck/healthcheck/__init__.py
89
85
  crossref_matcher/strategies/healthcheck/healthcheck/strategy.py
@@ -1,481 +0,0 @@
1
- import os
2
- import re
3
- from crossref_matcher.matching.utils import get_resource_path
4
- from typing import Iterable
5
- from dataclasses import dataclass
6
- import unidecode
7
- import unicodedata
8
- from functools import cached_property
9
- from crossref_matcher.strategies import Strategy, MatchTask
10
-
11
- from crossref_matcher.matching.search import ES_CLIENT
12
-
13
- from rapidfuzz import fuzz
14
-
15
- import logging
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- def matches_exactly(name, substring, ignore_case=True):
21
- if ignore_case:
22
- try:
23
- name = name.lower_
24
- except AttributeError:
25
- name = name.lower()
26
- substring = substring.lower()
27
- return re.search(rf"\b{re.escape(substring)}\b", name) is not None
28
-
29
-
30
- class StringRep(str):
31
- # This is meant to standardize all of the manipulations of the name strings
32
- def __new__(cls, s: str):
33
- obj = super().__new__(cls, s)
34
- obj.original = str(s)
35
- return obj
36
-
37
- def __str__(self):
38
- return self.original
39
-
40
- def __repr__(self):
41
- return f"StringRep({self.original!r})"
42
-
43
- def __len__(self):
44
- return len(self.original)
45
-
46
- @cached_property
47
- def lower_(self):
48
- return re.sub(r"\s+", " ", self.original.lower())
49
-
50
- @cached_property
51
- def lower_alpha(self):
52
- s = unidecode.unidecode(self.original).strip()
53
- return re.sub(
54
- r"\s+", " ", re.sub("[^a-z]", " ", s.lower())
55
- ) # Lowercase and keep only letters
56
-
57
- @cached_property
58
- def alpha(self):
59
- s = unidecode.unidecode(self.original).strip()
60
- return re.sub(r"\s+", " ", re.sub("[^a-zA-Z]", " ", s)) # Keep only letters
61
-
62
- def check_latin_chars(self, s):
63
- for ch in s:
64
- if ch.isalpha():
65
- if "LATIN" not in unicodedata.name(ch):
66
- return False
67
- return True
68
-
69
- @cached_property
70
- def normalized(self):
71
- return self._normalize(self.original)
72
-
73
- def _normalize(self, s):
74
- """Normalize a string for matching.
75
-
76
- Args:
77
- s (str): The string to normalize.
78
-
79
- Returns:
80
- str: The normalized string.
81
- """
82
- if self.check_latin_chars(s):
83
- # if these are Latin characters, remove accent marks
84
- s = re.sub(r"\s+", " ", unidecode.unidecode(s).strip().lower())
85
- else:
86
- s = re.sub(r"\s+", " ", s.strip().lower())
87
-
88
- # Replace specific abbreviations with their full forms
89
- s = re.sub(
90
- "(?<![a-z])univ$",
91
- "university",
92
- re.sub(
93
- r"(?<![a-z])univ[\. ]",
94
- "university ",
95
- re.sub(r"(?<![a-z])u\.(?! ?[a-z]\.)", "university ", s),
96
- ),
97
- )
98
- s = re.sub(
99
- "(?<![a-z])lab$",
100
- "laboratory",
101
- re.sub("(?<![a-z])lab[^a-z]", "laboratory ", s),
102
- )
103
- s = re.sub(
104
- "(?<![a-z])inst$",
105
- "institute",
106
- re.sub("(?<![a-z])inst[^a-z]", "institute ", s),
107
- )
108
- s = re.sub(
109
- "(?<![a-z])tech$",
110
- "technology",
111
- re.sub("(?<![a-z])tech[^a-z]", "technology ", s),
112
- )
113
- s = re.sub(r"(?<![a-z])u\. ?s\.", "united states", s)
114
- s = re.sub("&", " and ", re.sub("&amp;", " and ", s))
115
- s = re.sub("^the ", "", s)
116
-
117
- s = re.sub(r"\s+", " ", s.strip().lower())
118
- return s
119
-
120
-
121
- @dataclass
122
- class CandidateMatch:
123
- # Represents a candidate match in the search
124
- candidate: dict
125
- name: StringRep
126
- score: float
127
- start: int
128
- end: int
129
-
130
- def trimmed_output(self):
131
- return {
132
- "ror_id": self.candidate["_id"],
133
- "name": self.name.original,
134
- "score": self.score,
135
- "start": self.start,
136
- "end": self.end,
137
- }
138
-
139
-
140
- class FunderSingleSearch01(Strategy):
141
- id = "funder-single-search-01"
142
- task = MatchTask.FUNDER
143
- description = "Heuristic-based strategy; it performs a single candidate search against ES and uses a number of heuristics to validate the candidates."
144
-
145
- index = "organization"
146
- min_score = 96
147
-
148
- pattern_acronym = re.compile(r"(?P<acronym>\b[A-Z]{2,}\b)")
149
-
150
- def __init__(self, es_client=ES_CLIENT, max_candidates=18):
151
- self.es_client = es_client
152
- self.max_candidates = max_candidates
153
- if self.es_client is None:
154
- if os.getenv("PYTEST_CURRENT_TEST"):
155
- logger.warning(
156
- "this strategy normally requires an elasticsearch client, but pytest is running, so bypassing this error"
157
- )
158
- else:
159
- raise RuntimeError("this strategy requires an elasticsearch client")
160
-
161
- self.countries = []
162
- with open(
163
- get_resource_path(
164
- "crossref_matcher.resources.data.countries", "countries2.txt"
165
- ),
166
- "r",
167
- encoding="utf-8",
168
- ) as file:
169
- lines = [
170
- line.strip().split()
171
- for line in file
172
- if not line.startswith("#") and line.strip()
173
- ]
174
- self.countries = [(line[0], " ".join(line[1:])) for line in lines]
175
-
176
- self.common_ror_endings = set()
177
- with open(
178
- get_resource_path(
179
- "crossref_matcher.resources.data.common_ror_endings",
180
- "common_ror_endings.txt",
181
- ),
182
- "r",
183
- encoding="utf-8",
184
- ) as file:
185
- for line in file:
186
- line = line.strip()
187
- if line:
188
- self.common_ror_endings.add(line)
189
-
190
- self.fund_countries = [] # can be filled in during match()
191
- self.initial_candidates = [] # can be filled in during match()
192
- # self.word_counter = Counter() # can be filled in during match()
193
-
194
- def match(self, input_data, extra=False):
195
- self.input_string_rep = StringRep(input_data)
196
- self.fund_countries = self.get_countries(self.input_string_rep)
197
- candidates = self.get_candidates(input_data, max_candidates=self.max_candidates)
198
- # store initial candidates for help with debugging/evaluation
199
- self.initial_candidates = [
200
- {"id": c["_id"], "elasticsearch_score": c["_score"]} for c in candidates
201
- ]
202
- candidates = [c for c in candidates if self.eligible_candidates_filter(c)]
203
-
204
- # self.word_counter = Counter()
205
- # for candidate in candidates:
206
- # this_candidate_words = set()
207
- # for candidate_name in candidate["_source"]["names"]:
208
- # name = candidate_name["name"]
209
- # # count each word only once per candidate
210
- # this_candidate_words.update(re.split(r"\b", name.lower()))
211
- # self.word_counter.update(this_candidate_words)
212
- # common_words = set(word for word, freq in self.word_counter.most_common() if freq > 1 and len(word) > 1)
213
-
214
- candidates = [self.score(self.input_string_rep, c) for c in candidates]
215
- all_scored_candidates = candidates
216
- candidates = [c for c in candidates if c.score >= self.min_score]
217
-
218
- matched = self.choose_candidate(self.input_string_rep, candidates)
219
- if matched is None:
220
- return []
221
- if (
222
- self.fund_countries
223
- and self.to_region(matched.candidate["_source"]["country"])
224
- not in self.fund_countries
225
- ):
226
- return []
227
-
228
- ret = {
229
- "id": matched.candidate["_id"],
230
- "confidence": min(12, matched.candidate["_score"]) / 12,
231
- "strategies": [self.strategy],
232
- }
233
- if extra is True:
234
- candidates_out = [c.trimmed_output() for c in all_scored_candidates]
235
- candidates_out.sort(key=lambda c: c["score"], reverse=True)
236
- ret.update(
237
- {
238
- "matched_name": matched.name,
239
- "alignment_score": matched.score,
240
- "fund_countries": self.fund_countries,
241
- "all_scored_candidates": candidates_out,
242
- "elasticsearch_score": matched.candidate["_score"],
243
- }
244
- )
245
- return [ret]
246
-
247
- def eligible_candidates_filter(self, candidate) -> bool:
248
- if candidate["_source"]["status"] not in ["active", "inactive"]:
249
- return False
250
- elif (
251
- self.fund_countries
252
- and self.to_region(candidate["_source"]["country"])
253
- not in self.fund_countries
254
- ):
255
- return False
256
- else:
257
- return True
258
-
259
- def get_candidates(self, fund, max_candidates=20):
260
- return self.es_client.search(
261
- body={
262
- "size": max_candidates,
263
- # "size": 10,
264
- "query": {
265
- "nested": {
266
- "path": "names",
267
- "score_mode": "max",
268
- "query": {"match": {"names.name": {"query": fund}}},
269
- }
270
- },
271
- },
272
- index=self.index,
273
- )["hits"]["hits"]
274
-
275
- def _exclusion_acronym_common_ending(self, fund: StringRep, name: StringRep):
276
- # returns True if this `name` should be excluded
277
- acronyms = self.pattern_acronym.findall(name.original)
278
- if not acronyms:
279
- return False
280
- for ending in self.common_ror_endings:
281
- # use a regex to see if `name` ends in `ending`
282
- if re.search(rf"\b{re.escape(ending)}$", name.lower_):
283
- # acronyms from `name` must also occur in `fund` (not fuzzy)
284
- if not all(matches_exactly(fund, acronym) for acronym in acronyms):
285
- return True
286
- return False
287
-
288
- def candidate_name_match_exclusion(self, fund: StringRep, name: StringRep):
289
- # returns True if this `name` should be excluded
290
- if (
291
- # names that are too general to match:
292
- name.lower_
293
- in [
294
- "university school",
295
- "university hospital",
296
- "development fund",
297
- "korea university", # there are two korea universities (one in japan, one in south korea)
298
- ]
299
- # names that are too general to match unless a country is matched:
300
- or (
301
- not self.fund_countries
302
- and name.lower_
303
- in [
304
- "computing center",
305
- "center for international studies",
306
- "central government",
307
- "centre for international studies",
308
- "department of education",
309
- "department of conservation",
310
- "marie curie",
311
- "department of the environment",
312
- "department of veterans affairs",
313
- "development research center",
314
- "education trust",
315
- "heritage fund",
316
- "hospital research foundation",
317
- "medical research council",
318
- "medical research foundation",
319
- "ministry of agriculture, food and rural affairs",
320
- "ministry of education",
321
- "ministry of science",
322
- "national council of science and technology",
323
- "national institute of health research",
324
- "national research institute",
325
- "national science foundation",
326
- "national science fund",
327
- "prostate cancer research",
328
- "research promotion foundation",
329
- "startup foundation",
330
- "the alliance",
331
- "l’alliance",
332
- "alliance",
333
- "the arc",
334
- "the data lab",
335
- "the george",
336
- "the research council",
337
- "the research network",
338
- "the royal",
339
- "the tech",
340
- "the thousand",
341
- "the trust",
342
- "the way",
343
- "union government",
344
- "waste management",
345
- "r core",
346
- "creative foundation",
347
- "fonds de la recherche scientifique",
348
- "fund for scientific research",
349
- "fonds national de la recherche",
350
- "foundation for strategic research",
351
- ]
352
- )
353
- or len(name) >= len(fund) + 4
354
- or len(name) < 5
355
- or (" " not in name.original and fund.lower_ != name.lower_)
356
- or (" " not in fund.original and fund.lower_ != name.lower_)
357
- or self._exclusion_acronym_common_ending(fund, name)
358
- ):
359
- return True
360
- return False
361
-
362
- def score(self, fund: StringRep, candidate) -> CandidateMatch:
363
- best = CandidateMatch(
364
- candidate=candidate, name=StringRep(""), score=0, start=-1, end=-1
365
- )
366
- for candidate_name in candidate["_source"]["names"]:
367
- name = StringRep(candidate_name["name"])
368
- if self.candidate_name_match_exclusion(fund, name):
369
- continue
370
- alignment = fuzz.partial_ratio_alignment(fund.normalized, name.normalized)
371
- if alignment.score > best.score:
372
- best = CandidateMatch(
373
- candidate=candidate,
374
- name=name,
375
- score=alignment.score,
376
- start=alignment.src_start,
377
- end=alignment.src_end,
378
- )
379
- return best
380
-
381
- def is_better(
382
- self, fund: StringRep, candidate: CandidateMatch, other: CandidateMatch
383
- ) -> bool:
384
- score = 0
385
- if "univ" in candidate.name.lower_ and "univ" not in other.name.lower_:
386
- score += 1
387
- if "univ" not in candidate.name.lower_ and "univ" in other.name.lower_:
388
- score -= 1
389
- c_diff = abs(len(candidate.name) - len(fund))
390
- o_diff = abs(len(other.name) - len(fund))
391
- if o_diff - c_diff > 4:
392
- score += 1
393
- if c_diff - o_diff > 4:
394
- score -= 1
395
- if candidate.start > other.end:
396
- score += 1
397
- if other.start > candidate.end:
398
- score -= 1
399
- if candidate.score > 99 and other.score < 99:
400
- score += 1
401
- if candidate.score < 99 and other.score > 99:
402
- score -= 1
403
- return score > 0
404
-
405
- def rescore(self, fund: StringRep, candidates: Iterable[CandidateMatch]):
406
- new_scores = []
407
- for candidate in candidates:
408
- ns = 0
409
- for other in candidates:
410
- if self.is_better(fund, candidate, other):
411
- ns += 1
412
- new_scores.append(ns)
413
- for c, ns in zip(candidates, new_scores):
414
- c.score = ns
415
- yield c
416
-
417
- def last_non_overlapping(self, candidates):
418
- matched = None
419
- for candidate in candidates:
420
- overlap = False
421
- for other in candidates:
422
- if candidate.candidate["_id"] == other.candidate["_id"]:
423
- continue
424
- if (
425
- candidate.start <= other.start <= candidate.end
426
- or candidate.start <= other.end <= candidate.end
427
- or other.start <= candidate.start <= other.end
428
- or other.start <= candidate.end <= other.end
429
- ):
430
- overlap = True
431
- if not overlap:
432
- matched = candidate
433
- return matched
434
-
435
- def choose_candidate(self, fund: StringRep, candidates: Iterable[CandidateMatch]):
436
- if not candidates:
437
- return None
438
-
439
- if len(candidates) == 1:
440
- return candidates[0]
441
-
442
- rescored = list(self.rescore(fund, candidates))
443
- top_score = max([c.score for c in rescored])
444
- top_scored = [c for c in rescored if c.score == top_score]
445
-
446
- if len(top_scored) == 1:
447
- return top_scored[0]
448
-
449
- return self.last_non_overlapping(top_scored)
450
-
451
- def to_region(self, c: str) -> str:
452
- return {
453
- "GB": "GB-UK",
454
- "UK": "GB-UK",
455
- "CN": "CN-HK-TW",
456
- "HK": "CN-HK-TW",
457
- "TW": "CN-HK-TW",
458
- "PR": "US-PR",
459
- "US": "US-PR",
460
- }.get(c, c)
461
-
462
- def get_country_codes(self, s: StringRep) -> list[str]:
463
- _lower = re.sub(r"\s+", " ", unidecode.unidecode(s.original).strip().lower())
464
- codes = []
465
- for code, name in self.countries:
466
- if re.search("[^a-z]", name):
467
- # name contains not only letters (e.g., a space)
468
- score = fuzz.partial_ratio(name, _lower)
469
- elif len(name) == 2:
470
- score = max(
471
- [fuzz.ratio(name.upper(), t) for t in s.alpha.split()] + [0]
472
- )
473
- else:
474
- score = max([fuzz.ratio(name, t) for t in s.lower_alpha.split()] + [0])
475
- if score >= 90:
476
- codes.append(code.upper())
477
- return list(set(codes))
478
-
479
- def get_countries(self, s: StringRep) -> list[str]:
480
- codes = self.get_country_codes(s)
481
- return [self.to_region(c) for c in codes]