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_file

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.

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 None

Fetch 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 None

Fetch 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 None

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

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 None

Fetch 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 LICENSE at 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_identifier

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/

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, and WITH because 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: str

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.

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.