mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-09 20:26:33 +00:00
The documentation is built per release tag from that tag's own source tree, while the generators come from the current image. The licence generator imports utils.licensing, which a release predating that feature does not carry, so the build of 'latest' died with ModuleNotFoundError, never finished, and the server answered 500 to /latest/. Nine deploy jobs failed on Playwright specs that only ever saw that 500; the cause is in the rescue bundle's containers/docs.log, not in the job log. The import is guarded now and the page falls back to a stub carrying the same _generated-licensing label, because the table of contents references it for every version and writing nothing would only move the failure into Sphinx. COLUMNS is computed conditionally for the same reason: it is built from the rights the absent module defines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
215 lines
6.7 KiB
Python
215 lines
6.7 KiB
Python
"""Generate the licence-context page: in which context each role may be used.
|
|
|
|
Usage:
|
|
python -m infinito_docs.generators.licensing --roles-dir DIR --output FILE
|
|
|
|
Every invokable role is listed. A role whose licence could not be resolved
|
|
appears under "Not yet determined" rather than being left out, because a gap
|
|
that is invisible reads as safe.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
try:
|
|
from utils.licensing.contexts import CONTEXTS, UNKNOWN
|
|
from utils.licensing.rights import DISCLAIMERS, DUTIES, GRANTS, NOT_GRANTED
|
|
from utils.licensing.roles import Declared, declared, deploys_nothing
|
|
from utils.roles.required_by_coverage import role_is_invokable
|
|
except ImportError:
|
|
DECLARED_LICENCES = False
|
|
else:
|
|
DECLARED_LICENCES = True
|
|
|
|
COLUMNS: tuple[tuple[str, str], ...] = (
|
|
tuple(
|
|
(field, field.replace("_", " ").capitalize())
|
|
for field in GRANTS + DUTIES + DISCLAIMERS + NOT_GRANTED
|
|
)
|
|
if DECLARED_LICENCES
|
|
else ()
|
|
)
|
|
|
|
ABSENT = """\
|
|
.. _generated-licensing:
|
|
|
|
Licence contexts
|
|
================
|
|
|
|
This release predates the per-role licence declarations, so there is nothing
|
|
to tabulate for it. The page exists because the table of contents references
|
|
it for every version; newer releases carry the matrix.
|
|
"""
|
|
|
|
HEADER = """\
|
|
.. _generated-licensing:
|
|
|
|
Licence contexts
|
|
================
|
|
|
|
The Infinito.Nexus licence governs this repository's own code. It says nothing
|
|
about the software a role deploys, and that is where a restriction on
|
|
commercial use usually sits. This page answers, per role, what the deployed
|
|
software's own licence allows.
|
|
|
|
Each role declares its licence in ``meta/<role>/license.yml``; the rights of a
|
|
named licence live once in ``meta/licenses.yml``. The context headings below
|
|
are derived from those rights, not declared, so a heading can never contradict
|
|
the matrix under it.
|
|
|
|
``yes`` means the licence grants the permission, imposes the duty, or
|
|
disclaims the liability named in the column; ``no`` means it does not. This
|
|
page is not legal advice.
|
|
|
|
"""
|
|
|
|
FOOTER = """
|
|
.. note::
|
|
|
|
Generated from ``meta/<role>/license.yml`` and ``meta/licenses.yml``.
|
|
Correct a row at its source, never here.
|
|
"""
|
|
|
|
|
|
def _mark(value: bool | None) -> str:
|
|
"""Return the cell for one boolean.
|
|
|
|
Args:
|
|
value: the right's value, or None when the licence is unresolved.
|
|
|
|
A bare ``-`` would be read as a bullet marker and render as an empty
|
|
cell, which is indistinguishable from a missing value.
|
|
"""
|
|
if value is None:
|
|
return "unknown"
|
|
return "yes" if value else "no"
|
|
|
|
|
|
def _collect(
|
|
roles_dir: Path,
|
|
) -> tuple[dict[str, list[tuple[str, Declared | None]]], list[str]]:
|
|
"""Group every invokable role by its derived licence context.
|
|
|
|
Args:
|
|
roles_dir: the repository's roles directory.
|
|
|
|
Returns:
|
|
The grouping, and the roles that declare they deploy no third-party
|
|
software. Those carry no licence to classify, so listing them under
|
|
"not yet determined" would report a gap where there is none.
|
|
"""
|
|
grouped: dict[str, list[tuple[str, Declared | None]]] = {
|
|
context.key: [] for context in CONTEXTS
|
|
}
|
|
nothing: list[str] = []
|
|
for role_dir in sorted(roles_dir.iterdir()):
|
|
if not role_dir.is_dir() or not role_is_invokable(role_dir.name):
|
|
continue
|
|
if deploys_nothing(role_dir):
|
|
nothing.append(role_dir.name)
|
|
continue
|
|
entry = declared(role_dir)
|
|
key = entry.context.key if entry is not None else UNKNOWN.key
|
|
grouped[key].append((role_dir.name, entry))
|
|
return grouped, nothing
|
|
|
|
|
|
def _row(name: str, entry: Declared | None) -> list[str]:
|
|
"""Return one matrix row.
|
|
|
|
Args:
|
|
name: the role name.
|
|
entry: its declaration, or None when the role declares none.
|
|
"""
|
|
spdx = entry.spdx if entry is not None and entry.spdx else "not determined"
|
|
licence = f"`{spdx} <{entry.url}>`_" if entry is not None and entry.url else spdx
|
|
rights = entry.rights if entry is not None else None
|
|
return [
|
|
f"``{name}``",
|
|
licence,
|
|
*[
|
|
_mark(None if rights is None else bool(rights.get(field)))
|
|
for field, _ in COLUMNS
|
|
],
|
|
]
|
|
|
|
|
|
def _section(context, entries: list[tuple[str, Declared | None]]) -> str:
|
|
"""Render one context's heading, verdict and matrix.
|
|
|
|
Args:
|
|
context: the context being rendered.
|
|
entries: its roles, each with its declaration.
|
|
"""
|
|
lines = [
|
|
context.title,
|
|
"-" * len(context.title),
|
|
"",
|
|
f"**Commercial use:** {context.commercial}.",
|
|
"",
|
|
f"**You owe:** {context.duties}.",
|
|
"",
|
|
".. list-table::",
|
|
" :header-rows: 1",
|
|
" :stub-columns: 1",
|
|
"",
|
|
]
|
|
header = ["Role", "Licence", *[label for _, label in COLUMNS]]
|
|
for row in (header, *(_row(name, entry) for name, entry in entries)):
|
|
lines.append(f" * - {row[0]}")
|
|
lines += [f" - {cell}" for cell in row[1:]]
|
|
lines.append("")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def generate(roles_dir: Path, output: Path) -> int:
|
|
"""Write the licence-context page and return the number of roles on it.
|
|
|
|
Args:
|
|
roles_dir: the repository's roles directory.
|
|
output: the ``.rst`` file to write.
|
|
"""
|
|
grouped, nothing = _collect(roles_dir)
|
|
sections = [
|
|
_section(context, grouped[context.key])
|
|
for context in CONTEXTS
|
|
if grouped[context.key]
|
|
]
|
|
if nothing:
|
|
listed = ", ".join(f"``{role}``" for role in nothing)
|
|
sections.append(
|
|
"Deploying no third-party software\n"
|
|
"---------------------------------\n\n"
|
|
"These roles install nothing of their own, so the question this "
|
|
f"page answers does not arise for them: {listed}.\n"
|
|
)
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
output.write_text(HEADER + "\n".join(sections) + FOOTER, encoding="utf-8")
|
|
return sum(len(entries) for entries in grouped.values()) + len(nothing)
|
|
|
|
|
|
def main(argv=None):
|
|
"""Entry point.
|
|
|
|
Args:
|
|
argv: command line arguments, or None to read ``sys.argv``.
|
|
"""
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--roles-dir", required=True, type=Path)
|
|
parser.add_argument("--output", required=True, type=Path)
|
|
args = parser.parse_args(argv)
|
|
if not DECLARED_LICENCES:
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(ABSENT, encoding="utf-8")
|
|
print(f"licensing: release without declarations -> {args.output}")
|
|
return 0
|
|
count = generate(args.roles_dir, args.output)
|
|
print(f"licensing: {count} roles -> {args.output}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|