Skip to content

Commit 3062e73

Browse files
committed
feat(harvester): add ROR assignment in mapping
1 parent edfd471 commit 3062e73

8 files changed

Lines changed: 125 additions & 12 deletions

File tree

site/cds_rdm/inspire_harvester/load/files.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ def __init__(self, retry_config: RetryConfig = None):
4545

4646
def compute_diff(self, existing_files, new_files) -> FileDiff:
4747
"""Return the set difference between existing and new file checksums."""
48-
4948
existing_checksums = [value["checksum"] for value in existing_files.values()]
5049
new_checksums = [value["checksum"] for value in new_files.values()]
5150

@@ -102,6 +101,7 @@ def fetch(self, url, logger) -> BytesIO:
102101
)
103102

104103
def check_files_should_update(self, record, incoming_record, logger):
104+
"""Check if files should be updated."""
105105
if not record:
106106
return True
107107
record_dict = record.to_dict()

site/cds_rdm/inspire_harvester/transform/config.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77

88
"""INSPIRE to CDS harvester config module."""
99
from cds_rdm.inspire_harvester.transform.mappers.article import (
10+
ArticleDescriptionMapper,
1011
ArticleDOIMapper,
11-
ArticleFilesMapper, ArticleTitleMapper, ArticleDescriptionMapper,
12+
ArticleFilesMapper,
13+
ArticleTitleMapper,
1214
)
1315
from cds_rdm.inspire_harvester.transform.mappers.basic_metadata import (
1416
AdditionalDescriptionsMapper,
@@ -38,8 +40,10 @@
3840
RelatedIdentifiersMapper,
3941
)
4042
from cds_rdm.inspire_harvester.transform.mappers.preprint import (
43+
PreprintDescriptionMapper,
4144
PreprintDOIMapper,
42-
PreprintFilesMapper, PreprintTitleMapper, PreprintDescriptionMapper,
45+
PreprintFilesMapper,
46+
PreprintTitleMapper,
4347
)
4448
from cds_rdm.inspire_harvester.transform.mappers.thesis import (
4549
ThesisContributorsMapper,

site/cds_rdm/inspire_harvester/transform/mappers/contributors.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
from dataclasses import dataclass
1111

12+
from idutils.normalizers import normalize_ror
13+
1214
from cds_rdm.inspire_harvester.transform.mappers.mapper import MapperBase
1315

1416

@@ -38,16 +40,31 @@ def _transform_author_identifiers(self, author):
3840
return processed_identifiers
3941

4042
def _transform_author_affiliations(self, author):
41-
"""Transform affiliations."""
43+
"""Transform affiliations.
44+
45+
If affiliations_identifiers is present, ROR values are matched by index
46+
to the affiliations list and used as vocabulary IDs. Affiliations without
47+
a matching ROR fall back to free-text name.
48+
"""
4249
affiliations = author.get("affiliations", [])
50+
affiliations_identifiers = author.get("affiliations_identifiers")
4351
mapped_affiliations = []
4452

45-
for affiliation in affiliations:
46-
value = affiliation.get("value")
47-
48-
if value:
49-
value = value.rstrip(".").strip()
50-
mapped_affiliations.append({"name": value})
53+
ror_ids = []
54+
if affiliations_identifiers:
55+
ror_ids = [
56+
normalize_ror(ai["value"])
57+
for ai in affiliations_identifiers
58+
if ai.get("schema") == "ROR"
59+
]
60+
61+
for i, affiliation in enumerate(affiliations):
62+
if i < len(ror_ids):
63+
mapped_affiliations.append({"id": ror_ids[i]})
64+
else:
65+
value = affiliation.get("value")
66+
if value:
67+
mapped_affiliations.append({"name": value.rstrip(".").strip()})
5168

5269
return mapped_affiliations
5370

site/cds_rdm/inspire_harvester/transform/splitter.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class InspireVersionSplitter:
3333
"""
3434

3535
def __init__(self, inspire_record, ctx, cds_id, policy=mapper_policy):
36+
"""Constructor."""
3637
self.inspire_record = inspire_record
3738
self.inspire_id = self.inspire_record.get("id")
3839
self.policy = policy
@@ -42,6 +43,7 @@ def __init__(self, inspire_record, ctx, cds_id, policy=mapper_policy):
4243
self.logger = Logger(inspire_id=self.inspire_id)
4344

4445
def needs_split(self) -> bool:
46+
"""Determine whether the record needs a split."""
4547
metadata = self.inspire_record.get("metadata", {})
4648
doc_types = metadata.get("document_type", [])
4749
if len(doc_types) <= 1:
@@ -58,7 +60,6 @@ def needs_split(self) -> bool:
5860

5961
def split(self):
6062
"""Return [preprint_record, publication_record], or None if split is not applicable."""
61-
6263
versions = []
6364

6465
if not self.needs_split():

site/cds_rdm/inspire_harvester/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717

1818
def compare_metadata(a, b):
19+
"""Compare metadata based on id key only."""
1920
# If both are dicts
2021
if isinstance(a, dict) and isinstance(b, dict):
2122
# If both have an id → compare only the id

site/cds_rdm/inspire_harvester/writer.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,15 @@ def _process_entry(self, stream_entry):
5858
op_type = self._route(stream_entry)
5959
except WriterError as e:
6060
error_message = f"Error while processing entry : {str(e)}."
61+
import traceback
62+
traceback.print_exc()
6163
except ValidationError as e:
6264
error_message = f"Validation error while processing entry: {str(e)}."
65+
import traceback
66+
traceback.print_exc()
67+
except Exception as e:
68+
import traceback
69+
traceback.print_exc()
6370

6471
if error_message:
6572
logger.error(error_message)

site/tests/conftest.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ def create_app(instance_path):
325325
"funders_v",
326326
"awards_v",
327327
"licenses_v",
328+
"affiliations_v",
328329
"contributors_role_v",
329330
"description_type_v",
330331
"relation_type_v",
@@ -348,6 +349,7 @@ def running_app(
348349
funders_v,
349350
awards_v,
350351
licenses_v,
352+
affiliations_v,
351353
contributors_role_v,
352354
description_type_v,
353355
relation_type_v,
@@ -372,6 +374,7 @@ def running_app(
372374
funders_v,
373375
awards_v,
374376
licenses_v,
377+
affiliations_v,
375378
contributors_role_v,
376379
description_type_v,
377380
relation_type_v,
@@ -1249,6 +1252,40 @@ def licenses_v(app, licenses):
12491252
return [cc_zero, cc_by]
12501253

12511254

1255+
@pytest.fixture(scope="module")
1256+
def affiliations_v(app):
1257+
"""Affiliation records with ROR IDs from test data.
1258+
1259+
Affiliations are a dedicated record type (not a generic Vocabulary),
1260+
so they must be created via the affiliations service, not vocabulary_service.
1261+
The RDMDraft relation field validates against the Affiliation PID table.
1262+
"""
1263+
from invenio_vocabularies.contrib.affiliations.api import Affiliation
1264+
1265+
affiliations_service = current_service_registry.get("affiliations")
1266+
entries = [
1267+
("03gc1p724", "IJCLab, Orsay", "https://ror.org/03gc1p724"),
1268+
("01ggx4157", "CERN", "https://ror.org/01ggx4157"),
1269+
("013meh722", "University of Cambridge", "https://ror.org/013meh722"),
1270+
("01a77tt86", "University of Warwick", "https://ror.org/01a77tt86"),
1271+
("01v29qb04", "Durham University, IPPP", "https://ror.org/01v29qb04"),
1272+
]
1273+
result = None
1274+
for ror_id, name, ror_url in entries:
1275+
result = affiliations_service.create(
1276+
system_identity,
1277+
{
1278+
"id": ror_id,
1279+
"name": name,
1280+
"identifiers": [{"identifier": ror_url, "scheme": "ror"}],
1281+
},
1282+
)
1283+
1284+
Affiliation.index.refresh()
1285+
1286+
return result
1287+
1288+
12521289
@pytest.fixture(scope="module")
12531290
def contributors_role_type(app):
12541291
"""Contributor role vocabulary type."""

site/tests/inspire_harvester/test_transformer.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ def test_transform_author_identifiers(running_app):
433433

434434

435435
def test_transform_author_affiliations(running_app):
436-
"""Test CreatibutorsMapper._transform_author_affiliations."""
436+
"""Test CreatibutorsMapper._transform_author_affiliations without ROR identifiers."""
437437
author = {
438438
"affiliations": [
439439
{"value": "CERN"},
@@ -450,6 +450,52 @@ def test_transform_author_affiliations(running_app):
450450
assert {"name": "MIT"} in result
451451

452452

453+
def test_transform_author_affiliations_with_ror(running_app):
454+
"""Test CreatibutorsMapper._transform_author_affiliations with ROR identifiers."""
455+
author = {
456+
"affiliations": [
457+
{"value": "IJCLab, Orsay"},
458+
{"value": "CERN"},
459+
],
460+
"affiliations_identifiers": [
461+
{"value": "grid.460789.4", "schema": "GRID"},
462+
{"value": "grid.9132.9", "schema": "GRID"},
463+
{"value": "https://ror.org/03gc1p724", "schema": "ROR"},
464+
{"value": "https://ror.org/01ggx4157", "schema": "ROR"},
465+
],
466+
}
467+
468+
mapper = CreatibutorsMapper()
469+
result = mapper._transform_author_affiliations(author)
470+
471+
assert len(result) == 2
472+
assert {"id": "03gc1p724"} in result
473+
assert {"id": "01ggx4157"} in result
474+
475+
476+
def test_transform_author_affiliations_with_ror_partial(running_app):
477+
"""Test fallback to name when ROR count is less than affiliations count."""
478+
author = {
479+
"affiliations": [
480+
{"value": "IJCLab, Orsay"},
481+
{"value": "CERN"},
482+
{"value": "Unknown Institute"},
483+
],
484+
"affiliations_identifiers": [
485+
{"value": "https://ror.org/03gc1p724", "schema": "ROR"},
486+
{"value": "https://ror.org/01ggx4157", "schema": "ROR"},
487+
],
488+
}
489+
490+
mapper = CreatibutorsMapper()
491+
result = mapper._transform_author_affiliations(author)
492+
493+
assert len(result) == 3
494+
assert {"id": "03gc1p724"} in result
495+
assert {"id": "01ggx4157"} in result
496+
assert {"name": "Unknown Institute"} in result
497+
498+
453499
def test_transform_copyrights_complete(running_app):
454500
"""Test CopyrightMapper with complete copyright info."""
455501
src_metadata = {

0 commit comments

Comments
 (0)