ICSAC commited on
Commit
873fee8
·
1 Parent(s): c91e4c6

Crossref deposit links the author's code and data archive: isSupplementedBy, as a DOI for a doi.org link and a URI otherwise, beside hasPreprint in the relations program; regression suite validates every variant against the 5.4.0 schema

Browse files
crossref_deposit.py CHANGED
@@ -335,17 +335,44 @@ def preprint_doi(submission: dict) -> Optional[str]:
335
  return preprint_check.normalize(ref) if ref else None
336
 
337
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  def _relations(parent: ET.Element, submission: dict) -> None:
339
- """rel:program with hasPreprint -> the preprint's DOI (schema order: after
340
- the licence program, before doi_data)."""
 
 
341
  pre = preprint_doi(submission)
342
- if not pre:
 
343
  return
344
  prog = ET.SubElement(parent, _q("program", REL_NS))
345
- item = ET.SubElement(prog, _q("related_item", REL_NS))
346
- rel = ET.SubElement(item, _q("intra_work_relation", REL_NS),
347
- {"relationship-type": "hasPreprint", "identifier-type": "doi"})
348
- rel.text = pre
 
 
 
 
 
 
349
 
350
 
351
  def _doi_data(parent: ET.Element, doi: str, landing: str, pdf_url: Optional[str]) -> None:
 
335
  return preprint_check.normalize(ref) if ref else None
336
 
337
 
338
+ _DOI_LINK = re.compile(r"^https?://(?:dx\.)?doi\.org/(10\.\d{4,9}/\S+)$", re.IGNORECASE)
339
+
340
+
341
+ def code_data_relation(submission: dict) -> Optional[tuple[str, str]]:
342
+ """(identifier-type, identifier) for the author's code and data archive,
343
+ or None. A doi.org link is registered as the DOI it resolves; any other
344
+ public link (GitHub, a lab page) as a URI. ICSAC links to the archive; the
345
+ author hosts it (policy 2026-09-28)."""
346
+ import publications
347
+ url = publications.code_data_url(submission)
348
+ if not url:
349
+ return None
350
+ m = _DOI_LINK.match(url)
351
+ if m:
352
+ return ("doi", m.group(1))
353
+ return ("uri", url)
354
+
355
+
356
  def _relations(parent: ET.Element, submission: dict) -> None:
357
+ """rel:program (schema order: after the licence program, before doi_data)
358
+ with up to two related items: hasPreprint -> the preprint's DOI, and
359
+ isSupplementedBy -> the author's code and data archive (2026-09-29), so the
360
+ archive is found from the article's own Crossref record."""
361
  pre = preprint_doi(submission)
362
+ sup = code_data_relation(submission)
363
+ if not pre and not sup:
364
  return
365
  prog = ET.SubElement(parent, _q("program", REL_NS))
366
+ if pre:
367
+ item = ET.SubElement(prog, _q("related_item", REL_NS))
368
+ rel = ET.SubElement(item, _q("intra_work_relation", REL_NS),
369
+ {"relationship-type": "hasPreprint", "identifier-type": "doi"})
370
+ rel.text = pre
371
+ if sup:
372
+ item = ET.SubElement(prog, _q("related_item", REL_NS))
373
+ rel = ET.SubElement(item, _q("inter_work_relation", REL_NS),
374
+ {"relationship-type": "isSupplementedBy", "identifier-type": sup[0]})
375
+ rel.text = sup[1]
376
 
377
 
378
  def _doi_data(parent: ET.Element, doi: str, landing: str, pdf_url: Optional[str]) -> None:
intake/scripts/test_crossref_supplement.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Checks for the code and data relation in the Crossref deposit (2026-09-29):
3
+ an article whose author gave a public code and data link carries
4
+ isSupplementedBy in its Crossref record, as a DOI when the link is a doi.org
5
+ link and as a URI otherwise; every variant validates against the Crossref
6
+ 5.4.0 schema. Offline and pure: no network, no files written.
7
+ Run from the repo root with the venv:
8
+ .venv/bin/python intake/scripts/test_crossref_supplement.py
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ ROOT = Path(__file__).resolve().parents[2]
16
+ sys.path.insert(0, str(ROOT))
17
+
18
+ failures: list[str] = []
19
+
20
+
21
+ def check(cond: bool, name: str) -> None:
22
+ print((" ok " if cond else " FAIL ") + name)
23
+ if not cond:
24
+ failures.append(name)
25
+
26
+
27
+ import crossref_deposit as cd # noqa: E402
28
+
29
+ BASE = {"sub_id": "ICSAC-SUB-00099", "title": "A test paper", "abstract": "An abstract.",
30
+ "license": "cc-by-4.0", "source": "upload",
31
+ "creators": [{"name": "Test, Author", "orcid": "0000-0002-1825-0097"}]}
32
+ ZEN = "https://doi.org/10.5281/zenodo.23042786"
33
+
34
+
35
+ def build(sub: dict, content_type: str = "journal-article") -> str:
36
+ xml = cd.build_deposit_xml(sub, doi="10.67697/icsac.2026.099",
37
+ landing_url="https://icsacinstitute.org/publications/a-test-paper",
38
+ pdf_url="https://icsacinstitute.org/papers/ICSAC-SUB-00099.pdf",
39
+ content_type=content_type)
40
+ try:
41
+ cd.validate_xml(xml)
42
+ ok = True
43
+ except Exception as exc: # noqa: BLE001
44
+ print(f" schema: {exc}")
45
+ ok = False
46
+ return xml.decode() if ok else ""
47
+
48
+
49
+ print("1. which identifier")
50
+ rel = cd.code_data_relation
51
+ check(rel(dict(BASE, code_data={"available": True, "url": ZEN})) == ("doi", "10.5281/zenodo.23042786"),
52
+ "a doi.org link is registered as its DOI")
53
+ check(rel(dict(BASE, code_data={"available": True, "url": "https://dx.doi.org/10.5061/dryad.abc123"})) == ("doi", "10.5061/dryad.abc123"),
54
+ "a dx.doi.org link too")
55
+ check(rel(dict(BASE, code_data={"available": True, "url": "https://github.com/someone/repo"})) == ("uri", "https://github.com/someone/repo"),
56
+ "any other public link is registered as a URI")
57
+ check(rel(dict(BASE, code_data={"available": False, "url": None})) is None, "no link, no relation")
58
+ check(rel(dict(BASE)) is None, "a submission from before the field existed has no relation")
59
+ check(rel(dict(BASE, code_data={"available": True, "url": " "})) is None, "a blank link is no link")
60
+
61
+ print("2. the deposit")
62
+ x = build(dict(BASE, code_data={"available": True, "url": ZEN}))
63
+ check(bool(x), "an article with a DOI archive validates against the Crossref 5.4.0 schema")
64
+ check('<rel:inter_work_relation relationship-type="isSupplementedBy" identifier-type="doi">10.5281/zenodo.23042786</rel:inter_work_relation>' in x,
65
+ "it carries isSupplementedBy -> the archive DOI")
66
+ check(x.find("<rel:program") < x.find("<doi_data") and x.find("<rel:program") > 0, "the relation sits before doi_data")
67
+ check("hasPreprint" not in x, "an upload with no preprint carries no preprint relation")
68
+
69
+ x = build(dict(BASE, code_data={"available": True, "url": "https://github.com/someone/repo"}))
70
+ check(bool(x) and 'identifier-type="uri">https://github.com/someone/repo<' in x,
71
+ "a GitHub link validates and is registered as a URI")
72
+
73
+ x = build(dict(BASE, source="doi", doi="10.5281/zenodo.123456",
74
+ code_data={"available": True, "url": ZEN}))
75
+ check(bool(x) and x.count("<rel:program") == 1 and x.count("<rel:related_item>") == 2,
76
+ "a preprint and an archive share one relations program, two related items")
77
+ check('relationship-type="hasPreprint"' in x and 'relationship-type="isSupplementedBy"' in x,
78
+ "both relations present")
79
+
80
+ x = build(dict(BASE))
81
+ check(bool(x) and "<rel:program" not in x, "no preprint and no archive: no relations program at all")
82
+
83
+ x = build(dict(BASE, code_data={"available": True, "url": ZEN}), content_type="report-paper")
84
+ check(bool(x) and "isSupplementedBy" in x, "report-paper deposits carry the relation and validate too")
85
+
86
+ x = build(dict(BASE, code_data={"available": True, "url": "https://example.org/data?x=1&y=2"}))
87
+ check(bool(x) and "x=1&amp;y=2" in x, "a link with an ampersand is escaped and still validates")
88
+
89
+ print()
90
+ if failures:
91
+ print(f"FAILED: {len(failures)}")
92
+ for f in failures:
93
+ print(" - " + f)
94
+ sys.exit(1)
95
+ print("ALL GREEN")