"""CC origin notices for a published source. The duties follow the license named on the source. CC BY and CC BY-NC need creator attribution, a license link, a link to the material, a retrieval date, the pinned origin, and an explicit modification notice. CC0 does not require attribution. A missing duty is a gap, not a silent default. """ from __future__ import annotations from typing import Any LICENSE_URLS = { "CC0-1.0": "https://creativecommons.org/publicdomain/zero/1.0/", "CC-BY-4.0": "https://creativecommons.org/licenses/by/4.0/", "CC-BY-NC-4.0": "https://creativecommons.org/licenses/by-nc/4.0/", "MIT": "https://opensource.org/license/mit", } _BY = frozenset({"CC-BY-4.0", "CC-BY-NC-4.0"}) def attribution_gaps(source: dict[str, Any]) -> list[str]: """Names of CC duties this source does not yet satisfy.""" license_name = str(source.get("license") or "") gaps: list[str] = [] if not license_name: gaps.append("license") if not str(source.get("title") or "").strip(): gaps.append("title") if not str(source.get("uri") or "").strip(): gaps.append("uri") if license_name in _BY or license_name == "MIT": if not str(source.get("attribution") or "").strip(): gaps.append("attribution") if license_name in _BY: expected = LICENSE_URLS[license_name] if str(source.get("licenseUrl") or "") != expected: gaps.append("licenseUrl") if not str(source.get("retrievedAt") or "").strip(): gaps.append("retrievedAt") if not str(source.get("originSha") or "").strip(): gaps.append("originSha") if not str(source.get("modificationNotice") or "").strip(): gaps.append("modificationNotice") if license_name == "CC-BY-NC-4.0": attribution = str(source.get("attribution") or "") if "onco.cc" not in attribution.lower() and "data from onco" not in attribution.lower(): # The OnCo corpus duty is the OnCo line. Other BY-NC sources may # name a different creator; only the OnCo wording is checked when # the attribution itself claims OnCo. pass if "Data from OnCo" in attribution and "onco.cc" not in attribution: gaps.append("onco-link") return gaps def notice_complete(source: dict[str, Any]) -> bool: return not attribution_gaps(source)