ICSAC commited on
Commit
c1941ad
·
1 Parent(s): 618e46c

register --live accepts the Institute's own Zenodo community inclusion for the archive copy it publishes; never fatal, reported in the curator ping, retried on a re-run

Browse files
crossref_deposit.py CHANGED
@@ -875,6 +875,17 @@ def register(sub_dir: Path, *, live: bool = False, override_window: bool = False
875
  _write_json_atomic(state_p, state)
876
  outcome["archive"] = ck.get("archive_url")
877
 
 
 
 
 
 
 
 
 
 
 
 
878
  # 5. author notice, DRAFTED
879
  if not ck.get("author_drafted_at"):
880
  ok = _notify_author(sub_dir, submission, doi, state.get("publications_slug"),
@@ -894,12 +905,27 @@ def register(sub_dir: Path, *, live: bool = False, override_window: bool = False
894
  _ping(f"DOI REGISTERED for {sub_dir.name}: https://doi.org/{doi}\n"
895
  f"Landing: {outcome.get('landing') or '(publications not configured)'}\n"
896
  f"Zenodo archive: {outcome.get('archive')}\n"
 
897
  f"Author 'published' email: {outcome['author_draft']}"
898
  + (" (Gmail Drafts -- review and send by hand)" if ck.get("author_drafted_at") else "")
899
  + ("\n(resumed from an earlier partial run)" if outcome["resumed"] else ""))
900
  return {**outcome, **(ck.get("crossref_result") or {})}
901
 
902
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
903
  def _publish_zenodo_archive(sub_dir: Path, doi: str, *, log) -> Optional[str]:
904
  """Publish the Zenodo draft staged at accept. PREFLIGHT first (audit 2026-09-27 item 3): the
905
  remote draft must carry OUR DOI as its external DOI, or we refuse -- publishing
 
875
  _write_json_atomic(state_p, state)
876
  outcome["archive"] = ck.get("archive_url")
877
 
878
+ # 4b. the Institute's Zenodo community (his yes, 2026-09-29): publishing opens an
879
+ # inclusion request the community must accept, and --live is his approval, so it
880
+ # is accepted here. Never fatal: a failure is reported and a re-run tries again.
881
+ if not ck.get("zenodo_community_at") and ck.get("archive_url") not in (None, "none"):
882
+ result = _accept_zenodo_community(state.get("deposit_record_id"), log=log)
883
+ ck["zenodo_community"] = result
884
+ if result in ("accepted", "already"):
885
+ ck["zenodo_community_at"] = _now_iso()
886
+ _save_staged()
887
+ outcome["community"] = ck.get("zenodo_community")
888
+
889
  # 5. author notice, DRAFTED
890
  if not ck.get("author_drafted_at"):
891
  ok = _notify_author(sub_dir, submission, doi, state.get("publications_slug"),
 
905
  _ping(f"DOI REGISTERED for {sub_dir.name}: https://doi.org/{doi}\n"
906
  f"Landing: {outcome.get('landing') or '(publications not configured)'}\n"
907
  f"Zenodo archive: {outcome.get('archive')}\n"
908
+ f"ICSAC Zenodo community: {outcome.get('community') or 'not attempted'}\n"
909
  f"Author 'published' email: {outcome['author_draft']}"
910
  + (" (Gmail Drafts -- review and send by hand)" if ck.get("author_drafted_at") else "")
911
  + ("\n(resumed from an earlier partial run)" if outcome["resumed"] else ""))
912
  return {**outcome, **(ck.get("crossref_result") or {})}
913
 
914
 
915
+ def _accept_zenodo_community(record_id, *, log) -> str:
916
+ """Accept the ICSAC community inclusion for the published archive copy; never raises."""
917
+ if not record_id:
918
+ return "none"
919
+ try:
920
+ import repository_deposit
921
+ result = repository_deposit.accept_community_inclusion(str(record_id))
922
+ log(f" crossref: ICSAC Zenodo community for record {record_id}: {result}")
923
+ return result
924
+ except Exception as exc:
925
+ log(f" crossref: ICSAC Zenodo community for record {record_id} FAILED: {str(exc)[:200]}")
926
+ return f"failed: {str(exc)[:120]}"
927
+
928
+
929
  def _publish_zenodo_archive(sub_dir: Path, doi: str, *, log) -> Optional[str]:
930
  """Publish the Zenodo draft staged at accept. PREFLIGHT first (audit 2026-09-27 item 3): the
931
  remote draft must carry OUR DOI as its external DOI, or we refuse -- publishing
intake/scripts/test_zenodo_community.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Checks for the ICSAC Zenodo community step of register --live (his yes,
3
+ 2026-09-29): the inclusion request that publishing opens is accepted as part
4
+ of the approved publish; a record already in the community is left alone; a
5
+ failure never stops the run and a re-run tries again. Offline: the Zenodo API
6
+ and every other network step are stubbed; writes land in one temporary
7
+ directory that is removed at the end, pass or fail. Run from the repo root:
8
+ .venv/bin/python intake/scripts/test_zenodo_community.py
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import shutil
14
+ import sys
15
+ import tempfile
16
+ from pathlib import Path
17
+
18
+ ROOT = Path(__file__).resolve().parents[2]
19
+ sys.path.insert(0, str(ROOT))
20
+
21
+ failures: list[str] = []
22
+
23
+
24
+ def check(cond: bool, name: str) -> None:
25
+ print((" ok " if cond else " FAIL ") + name)
26
+ if not cond:
27
+ failures.append(name)
28
+
29
+
30
+ tmp = Path(tempfile.mkdtemp(prefix="zenodo-community-"))
31
+ try:
32
+ import repository_deposit as rd
33
+
34
+ COMM = "130c51d8-icsac"
35
+ calls: list = []
36
+
37
+ def fake(members=(), requests=(), accept_link=True):
38
+ def _req(method, url, *, token, body=None, **k):
39
+ calls.append((method, url))
40
+ if url.endswith("/communities/icsac"):
41
+ return {"id": COMM}
42
+ if url.endswith("/communities"):
43
+ return {"hits": {"hits": [{"id": c} for c in members]}}
44
+ if url.endswith("/requests"):
45
+ hits = []
46
+ for rid, status in requests:
47
+ links = {"actions": {"accept": f"https://zenodo.org/api/requests/{rid}/actions/accept"}} if accept_link else {}
48
+ hits.append({"id": rid, "type": "community-inclusion", "status": status,
49
+ "receiver": {"community": COMM}, "links": links})
50
+ return {"hits": {"hits": hits}}
51
+ if "/actions/accept" in url:
52
+ return {"status": "accepted"}
53
+ raise AssertionError(url)
54
+ return _req
55
+
56
+ print("1. the helper")
57
+ calls.clear(); rd._request_json = fake(requests=[("r1", "submitted")])
58
+ check(rd.accept_community_inclusion("9") == "accepted"
59
+ and ("POST", "https://zenodo.org/api/requests/r1/actions/accept") in calls,
60
+ "a pending request is accepted")
61
+ calls.clear(); rd._request_json = fake(members=[COMM])
62
+ check(rd.accept_community_inclusion("9") == "already" and not any(m == "POST" for m, _ in calls),
63
+ "a record already in the community is left alone")
64
+ calls.clear(); rd._request_json = fake(requests=[("r2", "accepted")])
65
+ check(rd.accept_community_inclusion("9") == "none" and not any(m == "POST" for m, _ in calls),
66
+ "no pending request, nothing done")
67
+ calls.clear(); rd._request_json = fake(requests=[("r3", "submitted")], accept_link=False)
68
+ check(rd.accept_community_inclusion("9") == "not-permitted", "a token that may not accept says so")
69
+
70
+ print("2. the live run")
71
+ import crossref_deposit as cd
72
+ from intake import author_approval
73
+ sub = tmp / "ICSAC-SUB-09998"
74
+ (sub / "crossref").mkdir(parents=True)
75
+ (sub / "submission.json").write_text(json.dumps({"sub_id": sub.name, "title": "T", "source": "upload"}))
76
+ (sub / "state.json").write_text(json.dumps({"decision": "accept", "state": "completed", "deposit_record_id": "555"}))
77
+ (sub / "crossref/deposit.xml").write_bytes(b"<x/>")
78
+ (sub / "crossref/staged.json").write_text(json.dumps({"doi": "10.67697/icsac.2026.998", "registered": False}))
79
+ cd.stage = lambda *a, **k: None
80
+ cd.validate_xml = lambda b: None
81
+ cd.deposit = lambda *a, **k: "batch-1"
82
+ cd.poll_result = lambda *a, **k: {"status": "completed", "success": 1, "failure": 0}
83
+ pings: list = []
84
+ cd._ping = lambda msg: pings.append(msg)
85
+ cd._push_publications = lambda *a, **k: {"slug": "t"}
86
+ cd._publish_zenodo_archive = lambda *a, **k: "https://zenodo.org/record/555"
87
+ cd._notify_author = lambda *a, **k: True
88
+ author_approval.gate = lambda *a, **k: (True, "approved (stub)")
89
+ seen: list = []
90
+
91
+ def flaky(record_id, *, log):
92
+ seen.append(record_id)
93
+ return "failed: HTTP 503" if len(seen) == 1 else "accepted"
94
+ cd._accept_zenodo_community = flaky
95
+ out = cd.register(sub, live=True, log=lambda m: None)
96
+ ck = json.loads((sub / "crossref/staged.json").read_text())["checkpoints"]
97
+ check(seen == ["555"] and out.get("community", "").startswith("failed")
98
+ and "zenodo_community_at" not in ck, "a failure is reported, not recorded as done")
99
+ check(bool(ck.get("author_drafted_at")) and "ICSAC Zenodo community: failed" in pings[-1],
100
+ "the run still finishes, and the ping says what happened")
101
+ out = cd.register(sub, live=True, log=lambda m: None)
102
+ ck = json.loads((sub / "crossref/staged.json").read_text())["checkpoints"]
103
+ check(seen == ["555", "555"] and ck.get("zenodo_community") == "accepted" and ck.get("zenodo_community_at"),
104
+ "a re-run tries again and records the accept")
105
+ out = cd.register(sub, live=True, log=lambda m: None)
106
+ check(seen == ["555", "555"], "once accepted, a later run leaves it alone")
107
+ finally:
108
+ shutil.rmtree(tmp, ignore_errors=True)
109
+
110
+ print()
111
+ if failures:
112
+ print(f"FAILED: {len(failures)}")
113
+ for f in failures:
114
+ print(" - " + f)
115
+ sys.exit(1)
116
+ print("ALL GREEN")
repository_deposit.py CHANGED
@@ -303,6 +303,38 @@ def stage_deposit_draft(submission: dict, paper_pdf_path: Path,
303
  return {"record_id": record_id, "draft_url": draft_url}
304
 
305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  def publish_draft(record_id: str, *, log=None) -> dict:
307
  """Publish a previously-staged draft deposit. Mints the DOI, makes the
308
  record live, triggers icsac-community membership.
 
303
  return {"record_id": record_id, "draft_url": draft_url}
304
 
305
 
306
+ def accept_community_inclusion(record_id: str, *, community: str | None = None) -> str:
307
+ """Accept the inclusion request that publishing opened for the Institute's own
308
+ Zenodo community (the community reviews every submission, the Institute's too).
309
+ Called only from register-doi.sh --live, whose typed confirmation is the
310
+ curator's approval (his yes, 2026-09-29). Never used for an author's own records:
311
+ ICSAC links to authors' code and data, it does not curate them.
312
+
313
+ Returns "accepted", "already" (the record is in the community), "none" (no
314
+ pending request to act on) or "not-permitted" (the token may not accept).
315
+ Raises DepositFailed on an HTTP error.
316
+ """
317
+ api = config.ZENODO_API
318
+ token = config.ZENODO_TOKEN
319
+ slug = community or config.COMMUNITY_ID
320
+ comm_id = _request_json("GET", f"{api}/communities/{slug}", token=token).get("id")
321
+ if not comm_id:
322
+ return "none"
323
+ members = _request_json("GET", f"{api}/records/{record_id}/communities", token=token)
324
+ if any(h.get("id") == comm_id for h in (members.get("hits") or {}).get("hits") or []):
325
+ return "already"
326
+ reqs = _request_json("GET", f"{api}/records/{record_id}/requests", token=token)
327
+ for r in (reqs.get("hits") or {}).get("hits") or []:
328
+ if (r.get("type") == "community-inclusion" and r.get("status") == "submitted"
329
+ and (r.get("receiver") or {}).get("community") == comm_id):
330
+ accept = ((r.get("links") or {}).get("actions") or {}).get("accept")
331
+ if not accept:
332
+ return "not-permitted"
333
+ _request_json("POST", accept, token=token, body={})
334
+ return "accepted"
335
+ return "none"
336
+
337
+
338
  def publish_draft(record_id: str, *, log=None) -> dict:
339
  """Publish a previously-staged draft deposit. Mints the DOI, makes the
340
  record live, triggers icsac-community membership.