Files
boltage/container/collect-python-licenses.py
Bart BroereandClaude Opus 5.5 f0fb9ddc74 Use Starlette, uvicorn and websockets; rename Docker files to container ones
Serve neo4j-browser with Starlette and uvicorn (the optional browser extra)
instead of a hand-written HTTP server, and handle Bolt over WebSocket with the
websockets library's Sans-I/O protocol. Rename Dockerfile, .dockerignore and
docker/ to Containerfile, .containerignore and container/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 11:27:25 +02:00

89 lines
3.3 KiB
Python

# SPDX-FileCopyrightText: 2026 Bart Broere
# SPDX-License-Identifier: GPL-3.0-or-later
"""Make sure every Python distribution in an environment ships its license text.
Most wheels include their license files in ``*.dist-info``. For the ones that
do not, copy its license files to ``<output>/<name>-<version>/``, taking them
from ``<vendored>/<name>/`` when present and otherwise from the source
distribution on PyPI. Run with the interpreter of the environment to inspect:
``/opt/pgadmin/bin/python collect-python-licenses.py <output> <vendored>``. Only
the environment's own site-packages are inspected, not the system packages a
``--system-site-packages`` venv can see: those come with Debian copyright files.
"""
import io
import json
import re
import sys
import sysconfig
import tarfile
import urllib.request
import zipfile
from importlib import metadata
from pathlib import Path
LICENSE_FILE = re.compile(r"(^|/)(LICEN[CS]E|COPYING|NOTICE|AUTHORS)[^/]*$", re.IGNORECASE)
def has_license_file(dist):
return any(LICENSE_FILE.search(str(f)) for f in dist.files or [])
def sdist_members(name, version):
with urllib.request.urlopen(f"https://pypi.org/pypi/{name}/{version}/json", timeout=60) as r:
urls = json.load(r)["urls"]
sdist = next((u for u in urls if u["packagetype"] == "sdist"), None)
if sdist is None:
return {}
with urllib.request.urlopen(sdist["url"], timeout=120) as r:
data = r.read()
found = {}
if sdist["filename"].endswith(".zip"):
with zipfile.ZipFile(io.BytesIO(data)) as z:
for n in z.namelist():
if n.count("/") == 1 and LICENSE_FILE.search(n):
found[n.split("/", 1)[1]] = z.read(n)
else:
with tarfile.open(fileobj=io.BytesIO(data)) as t:
for m in t.getmembers():
if m.isfile() and m.name.count("/") == 1 and LICENSE_FILE.search(m.name):
found[m.name.split("/", 1)[1]] = t.extractfile(m).read()
return found
def vendored_files(vendored, name):
directory = vendored / name
if not directory.is_dir():
return {}
return {f.name: f.read_bytes() for f in directory.iterdir() if f.is_file()}
def main(output, vendored):
output, vendored = Path(output), Path(vendored)
missing = []
site_packages = sysconfig.get_paths()["purelib"]
for dist in sorted(metadata.distributions(path=[site_packages]), key=lambda d: d.metadata["Name"].lower()):
if has_license_file(dist):
continue
name, version = dist.metadata["Name"], dist.version
files = vendored_files(vendored, name)
if not files:
try:
files = sdist_members(name, version)
except Exception as e: # noqa: BLE001 - report and carry on
print(f"{name} {version}: could not fetch sdist: {e}", file=sys.stderr)
if not files:
missing.append(f"{name} {version}")
continue
target = output / f"{name}-{version}"
target.mkdir(parents=True, exist_ok=True)
for filename, content in files.items():
(target / filename).write_bytes(content)
print(f"{name} {version}: added {', '.join(files)}")
for item in missing:
print(f"WARNING: no license file found for {item}", file=sys.stderr)
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2])