Module continuous_delivery_scripts.spdx_report.project_licensing
Project-level licence fetching, validation, and obligations reporting.
References: - SPDX licence list and JSON details: https://spdx.org/licenses/ - SPDX licence expression guidance: https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/ - SPDX matching guidance: https://spdx.github.io/spdx-spec/v2.3/license-matching-guidelines-and-templates/ - ScanCode LicenseDB: https://scancode-licensedb.aboutcode.org/ - GitHub licences API: https://docs.github.com/en/rest/licenses/licenses - choosealicense.com: https://choosealicense.com/ - OSADL licence obligations checklists: https://www.osadl.org/License-Checklists.oss-compliance-lists.0.html
Functions
def describe_licence_obligations(identifier: str | None = None) ‑> str-
Expand source code
def describe_licence_obligations(identifier: Optional[str] = None) -> str: """Return a human-readable obligations summary for one project licence. The rendered report combines: - SPDX.org licence metadata - choosealicense.com summary data - OSADL obligations text This workflow only supports projects configured with one exact SPDX licence identifier. """ resolved_identifier = get_project_licence_identifier(identifier) if is_proprietary_licence(resolved_identifier): return _render_licence_obligations_report( { "identifier": LICENSE_REF_PROPRIETARY, "name": None, "is_osi_approved": None, "summary": None, "permissions": (), "conditions": (), "limitations": (), "osadl_obligations": None, "is_proprietary": True, } ) spdx_details = fetch_spdx_licence_details(resolved_identifier) or {} choosealicense = fetch_choosealicense_metadata(resolved_identifier) osadl = fetch_osadl_obligations(resolved_identifier) return _render_licence_obligations_report( { "identifier": resolved_identifier, "name": spdx_details.get(SPDX_NAME_KEY) if isinstance(spdx_details.get(SPDX_NAME_KEY), str) else None, "is_osi_approved": ( "yes" if spdx_details.get(SPDX_IS_OSI_APPROVED_KEY) is True else "no" if spdx_details.get(SPDX_IS_OSI_APPROVED_KEY) is False else None ), "summary": choosealicense.summary if choosealicense else None, "permissions": choosealicense.permissions if choosealicense else (), "conditions": choosealicense.conditions if choosealicense else (), "limitations": choosealicense.limitations if choosealicense else (), "osadl_obligations": osadl.splitlines() if osadl else None, "is_proprietary": False, } )Return a human-readable obligations summary for one project licence.
The rendered report combines: - SPDX.org licence metadata - choosealicense.com summary data - OSADL obligations text
This workflow only supports projects configured with one exact SPDX licence identifier.
def ensure_project_licence_file_conforms(create_if_missing: bool, update_if_mismatched: bool = False) ‑> pathlib.Path-
Expand source code
def ensure_project_licence_file_conforms(create_if_missing: bool, update_if_mismatched: bool = False) -> Path: """Ensure the configured project licence file exists and matches the configured SPDX identifier. For non-proprietary single-identifier projects, the file content is checked against canonical public licence text fetched from the supported services. For proprietary projects, this helper only verifies presence because CDS has no public canonical reference text to compare against. """ identifier = get_project_licence_identifier() licence_file = get_project_licence_file_path() if is_proprietary_licence(identifier): if licence_file.exists() and licence_file.is_file(): return licence_file raise ValueError(f"The project uses a proprietary licence. The author must provide {licence_file} manually.") fetched = fetch_project_licence_text(identifier) if not fetched: raise ValueError( f"Could not fetch licence text for {identifier} from ScanCode LicenseDB, SPDX.org, " "GitHub API, or choosealicense.com." ) if licence_file.exists(): if not licence_file.is_file(): raise ValueError(f"Configured licence path exists but is not a file: {licence_file}") actual_text = licence_file.read_text(encoding="utf8") if _normalise_licence_text_for_comparison(actual_text) != _normalise_licence_text_for_comparison(fetched.text): _log_licence_text_diff(actual_text, fetched.text, licence_file, fetched.source) if update_if_mismatched: licence_file.write_text(fetched.text.rstrip() + "\n", encoding="utf8") print(f"Updated {licence_file} from {fetched.source}: {fetched.url}") return licence_file raise ValueError(f"Configured SPDX identifier {identifier} does not match the contents of {licence_file}.") return licence_file if not create_if_missing: raise ValueError(f"Configured SPDX identifier {identifier} requires a project licence file at {licence_file}.") licence_file.parent.mkdir(parents=True, exist_ok=True) licence_file.write_text(fetched.text.rstrip() + "\n", encoding="utf8") print(f"Created {licence_file} from {fetched.source}: {fetched.url}") return licence_fileEnsure the configured project licence file exists and matches the configured SPDX identifier.
For non-proprietary single-identifier projects, the file content is checked against canonical public licence text fetched from the supported services. For proprietary projects, this helper only verifies presence because CDS has no public canonical reference text to compare against.
def fetch_choosealicense_metadata(identifier: str) ‑> ChooseALicenseMetadata | None-
Expand source code
def fetch_choosealicense_metadata(identifier: str) -> Optional[ChooseALicenseMetadata]: """Fetch descriptive choosealicense.com metadata when supported. This is used for the user-facing permissions/conditions/limitations summary. Reference: https://choosealicense.com/ """ for slug in _choosealicense_slugs(identifier): url = f"https://choosealicense.com/licenses/{slug}/" try: page = _read_text(url) return ChooseALicenseMetadata( summary=_extract_choosealicense_summary(page), permissions=tuple(_extract_choosealicense_list(page, "license-permissions")), conditions=tuple(_extract_choosealicense_list(page, "license-conditions")), limitations=tuple(_extract_choosealicense_list(page, "license-limitations")), text=_extract_choosealicense_text(page), url=url, ) except Exception as error: logger.debug("choosealicense.com metadata unavailable for %s at %s: %s", identifier, url, error) return NoneFetch descriptive choosealicense.com metadata when supported.
This is used for the user-facing permissions/conditions/limitations summary.
Reference: https://choosealicense.com/
def fetch_osadl_obligations(identifier: str) ‑> str | None-
Expand source code
def fetch_osadl_obligations(identifier: str) -> Optional[str]: """Fetch the OSADL checklist text for one identifier when available. Reference: https://www.osadl.org/License-Checklists.oss-compliance-lists.0.html """ try: return normalise_licence_text( _read_text(f"https://www.osadl.org/fileadmin/checklists/unreflicenses/{identifier}.txt") ) except Exception as error: logger.debug("OSADL obligations unavailable for %s: %s", identifier, error) return NoneFetch the OSADL checklist text for one identifier when available.
Reference: https://www.osadl.org/License-Checklists.oss-compliance-lists.0.html
def fetch_project_licence_text(identifier: str) ‑> RetrievedProjectLicenceText | None-
Expand source code
def fetch_project_licence_text(identifier: str) -> Optional[RetrievedProjectLicenceText]: """Fetch canonical licence text for one SPDX identifier from public sources. Source order: 1. ScanCode LicenseDB 2. SPDX.org 3. GitHub licences API 4. choosealicense.com """ for fetcher in ( _fetch_scancode_licence_text, _fetch_spdx_licence_text, _fetch_github_licence_text, _fetch_choosealicense_licence_text, ): try: result = fetcher(identifier) except Exception as error: logger.debug("Could not fetch %s from %s: %s", identifier, fetcher.__name__, error) continue if result and result.text.strip(): return result return NoneFetch canonical licence text for one SPDX identifier from public sources.
Source order: 1. ScanCode LicenseDB 2. SPDX.org 3. GitHub licences API 4. choosealicense.com
def fetch_spdx_licence_details(identifier: str) ‑> Dict[str, object] | None-
Expand source code
def fetch_spdx_licence_details(identifier: str) -> Optional[Dict[str, object]]: """Fetch SPDX licence metadata for one identifier. Reference: https://spdx.org/licenses/ """ try: return _read_json(f"https://spdx.org/licenses/{identifier}.json") except Exception as error: logger.debug("SPDX licence details unavailable for %s: %s", identifier, error) return NoneFetch SPDX licence metadata for one identifier.
Reference: https://spdx.org/licenses/
def get_project_licence_file_path() ‑> pathlib.Path-
Expand source code
def get_project_licence_file_path() -> Path: """Resolve the configured path of the project licence file. The default configuration is ``LICENSE`` at the project root. """ configured_path = Path(str(configuration.get_value(ConfigurationVariable.LICENCE_FILE_PATH)).strip()) if configured_path.is_absolute(): return configured_path return Path(str(configuration.get_value(ConfigurationVariable.PROJECT_ROOT))).joinpath(configured_path)Resolve the configured path of the project licence file.
The default configuration is
LICENSEat the project root. def get_project_licence_identifier(identifier: str | None = None) ‑> str-
Expand source code
def get_project_licence_identifier(identifier: Optional[str] = None) -> str: """Return the project's single SPDX licence identifier. This helper accepts one exact SPDX identifier only. Compound expressions are intentionally rejected for commands whose output assumes one canonical text or one obligations profile. References: - https://spdx.org/licenses/ - https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/ """ raw_identifier: str = identifier or str(configuration.get_value(ConfigurationVariable.FILE_LICENCE_IDENTIFIER)) normalised_identifier: str = normalise_proprietary_licence(raw_identifier.strip()) if is_proprietary_licence(raw_identifier): return normalised_identifier if not has_single_project_licence_identifier(raw_identifier): raise ValueError( "This command only covers one exact SPDX licence identifier and does not cover " "compound licence expressions such as AND, OR, or WITH." ) licence = OPENSOURCE_LICENCES.get_licence(normalised_identifier, allow_fuzzy=False) if not licence or licence.identifier != normalised_identifier: raise ValueError(f"'{normalised_identifier}' is not a recognised SPDX licence identifier.") return normalised_identifierReturn the project's single SPDX licence identifier.
This helper accepts one exact SPDX identifier only. Compound expressions are intentionally rejected for commands whose output assumes one canonical text or one obligations profile.
References: - https://spdx.org/licenses/ - https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/
def has_single_project_licence_identifier(identifier: str | None = None) ‑> bool-
Expand source code
def has_single_project_licence_identifier(identifier: Optional[str] = None) -> bool: """Return whether the configured project licence is one exact identifier. This excludes compound SPDX expressions such as ``AND``, ``OR``, and ``WITH`` because the project-level licence-file and obligations workflows in this module only cover one canonical licence at a time. """ raw_identifier: str = identifier or str(configuration.get_value(ConfigurationVariable.FILE_LICENCE_IDENTIFIER)) normalised_identifier: str = normalise_proprietary_licence(raw_identifier.strip()) if is_proprietary_licence(raw_identifier): return True return not any(token in normalised_identifier for token in (" AND ", " OR ", " WITH ", "(", ")"))Return whether the configured project licence is one exact identifier.
This excludes compound SPDX expressions such as
AND,OR, andWITHbecause the project-level licence-file and obligations workflows in this module only cover one canonical licence at a time. def normalise_licence_text(text: str) ‑> str-
Expand source code
def normalise_licence_text(text: str) -> str: """Normalise licence text while preserving meaningful formatting. This is a lightweight normalization step used before comparison against canonical public licence texts. It is intentionally simpler than the full SPDX matching guidelines. Reference: - https://spdx.github.io/spdx-spec/v2.3/license-matching-guidelines-and-templates/ """ lines = [line.rstrip() for line in html.unescape(text).replace("\r\n", "\n").replace("\r", "\n").split("\n")] while lines and not lines[0].strip(): lines.pop(0) while lines and not lines[-1].strip(): lines.pop() return "\n".join(lines)Normalise licence text while preserving meaningful formatting.
This is a lightweight normalization step used before comparison against canonical public licence texts. It is intentionally simpler than the full SPDX matching guidelines.
Reference: - https://spdx.github.io/spdx-spec/v2.3/license-matching-guidelines-and-templates/
def sync_project_licence_file() ‑> pathlib.Path-
Expand source code
def sync_project_licence_file() -> Path: """Create the configured project licence file when it is missing. This workflow only supports projects configured with one exact SPDX licence identifier. """ return ensure_project_licence_file_conforms(create_if_missing=True, update_if_mismatched=True)Create the configured project licence file when it is missing.
This workflow only supports projects configured with one exact SPDX licence identifier.
Classes
class ChooseALicenseMetadata (summary: str | None,
permissions: tuple[str, ...],
conditions: tuple[str, ...],
limitations: tuple[str, ...],
text: str | None,
url: str | None)-
Expand source code
@dataclass(frozen=True) class ChooseALicenseMetadata: """Summary information exposed by choosealicense.com. Reference: https://choosealicense.com/ """ summary: Optional[str] permissions: tuple[str, ...] conditions: tuple[str, ...] limitations: tuple[str, ...] text: Optional[str] url: Optional[str]Summary information exposed by choosealicense.com.
Reference: https://choosealicense.com/
Instance variables
var conditions : tuple[str, ...]-
The type of the None singleton.
var limitations : tuple[str, ...]-
The type of the None singleton.
var permissions : tuple[str, ...]-
The type of the None singleton.
var summary : str | None-
The type of the None singleton.
var text : str | None-
The type of the None singleton.
var url : str | None-
The type of the None singleton.
class RetrievedProjectLicenceText (identifier: str, text: str, source: str, url: str)-
Expand source code
@dataclass(frozen=True) class RetrievedProjectLicenceText: """Text fetched for a project licence file from a public source. The text may come from ScanCode LicenseDB, SPDX.org, the GitHub licences API, or choosealicense.com. """ identifier: str text: str source: str url: strText fetched for a project licence file from a public source.
The text may come from ScanCode LicenseDB, SPDX.org, the GitHub licences API, or choosealicense.com.
Instance variables
var identifier : str-
The type of the None singleton.
var source : str-
The type of the None singleton.
var text : str-
The type of the None singleton.
var url : str-
The type of the None singleton.