mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 16:27:08 +00:00
Add option to extract licenses [ci] (#129095)
This commit is contained in:
parent
99ed39b26c
commit
be8b5a8aeb
16
.github/workflows/ci.yaml
vendored
16
.github/workflows/ci.yaml
vendored
@ -615,6 +615,10 @@ jobs:
|
|||||||
&& github.event.inputs.mypy-only != 'true'
|
&& github.event.inputs.mypy-only != 'true'
|
||||||
|| github.event.inputs.audit-licenses-only == 'true')
|
|| github.event.inputs.audit-licenses-only == 'true')
|
||||||
&& needs.info.outputs.requirements == 'true'
|
&& needs.info.outputs.requirements == 'true'
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
@ -633,19 +637,19 @@ jobs:
|
|||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Run pip-licenses
|
- name: Extract license data
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
pip-licenses --format=json --output-file=licenses.json
|
python -m script.licenses extract --output-file=licenses-${{ matrix.python-version }}.json
|
||||||
- name: Upload licenses
|
- name: Upload licenses
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: actions/upload-artifact@v4.4.3
|
||||||
with:
|
with:
|
||||||
name: licenses
|
name: licenses-${{ github.run_number }}-${{ matrix.python-version }}
|
||||||
path: licenses.json
|
path: licenses-${{ matrix.python-version }}.json
|
||||||
- name: Process licenses
|
- name: Check licenses
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python -m script.licenses licenses.json
|
python -m script.licenses check licenses-${{ matrix.python-version }}.json
|
||||||
|
|
||||||
pylint:
|
pylint:
|
||||||
name: Check pylint
|
name: Check pylint
|
||||||
|
@ -17,7 +17,6 @@ pydantic==1.10.18
|
|||||||
pylint==3.3.1
|
pylint==3.3.1
|
||||||
pylint-per-file-ignores==1.3.2
|
pylint-per-file-ignores==1.3.2
|
||||||
pipdeptree==2.23.4
|
pipdeptree==2.23.4
|
||||||
pip-licenses==5.0.0
|
|
||||||
pytest-asyncio==0.24.0
|
pytest-asyncio==0.24.0
|
||||||
pytest-aiohttp==1.0.5
|
pytest-aiohttp==1.0.5
|
||||||
pytest-cov==5.0.0
|
pytest-cov==5.0.0
|
||||||
|
@ -2,16 +2,28 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser, Namespace
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from importlib import metadata
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
|
from typing import TypedDict, cast
|
||||||
|
|
||||||
from awesomeversion import AwesomeVersion
|
from awesomeversion import AwesomeVersion
|
||||||
|
|
||||||
|
|
||||||
|
class PackageMetadata(TypedDict):
|
||||||
|
"""Package metadata."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
version: str
|
||||||
|
license_expression: str | None
|
||||||
|
license_metadata: str | None
|
||||||
|
license_classifier: list[str]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PackageDefinition:
|
class PackageDefinition:
|
||||||
"""Package definition."""
|
"""Package definition."""
|
||||||
@ -21,12 +33,16 @@ class PackageDefinition:
|
|||||||
version: AwesomeVersion
|
version: AwesomeVersion
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, str]) -> PackageDefinition:
|
def from_dict(cls, data: PackageMetadata) -> PackageDefinition:
|
||||||
"""Create a package definition from a dictionary."""
|
"""Create a package definition from PackageMetadata."""
|
||||||
|
if not (license_str := "; ".join(data["license_classifier"])):
|
||||||
|
license_str = (
|
||||||
|
data["license_metadata"] or data["license_expression"] or "UNKNOWN"
|
||||||
|
)
|
||||||
return cls(
|
return cls(
|
||||||
license=data["License"],
|
license=license_str,
|
||||||
name=data["Name"],
|
name=data["name"],
|
||||||
version=AwesomeVersion(data["Version"]),
|
version=AwesomeVersion(data["version"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -128,7 +144,6 @@ EXCEPTIONS = {
|
|||||||
"aioecowitt", # https://github.com/home-assistant-libs/aioecowitt/pull/180
|
"aioecowitt", # https://github.com/home-assistant-libs/aioecowitt/pull/180
|
||||||
"aioopenexchangerates", # https://github.com/MartinHjelmare/aioopenexchangerates/pull/94
|
"aioopenexchangerates", # https://github.com/MartinHjelmare/aioopenexchangerates/pull/94
|
||||||
"aiooui", # https://github.com/Bluetooth-Devices/aiooui/pull/8
|
"aiooui", # https://github.com/Bluetooth-Devices/aiooui/pull/8
|
||||||
"aioruuvigateway", # https://github.com/akx/aioruuvigateway/pull/6
|
|
||||||
"apple_weatherkit", # https://github.com/tjhorner/python-weatherkit/pull/3
|
"apple_weatherkit", # https://github.com/tjhorner/python-weatherkit/pull/3
|
||||||
"asyncio", # PSF License
|
"asyncio", # PSF License
|
||||||
"chacha20poly1305", # LGPL
|
"chacha20poly1305", # LGPL
|
||||||
@ -159,14 +174,10 @@ EXCEPTIONS = {
|
|||||||
"pyvera", # https://github.com/maximvelichko/pyvera/pull/164
|
"pyvera", # https://github.com/maximvelichko/pyvera/pull/164
|
||||||
"pyxeoma", # https://github.com/jeradM/pyxeoma/pull/11
|
"pyxeoma", # https://github.com/jeradM/pyxeoma/pull/11
|
||||||
"repoze.lru",
|
"repoze.lru",
|
||||||
"ruuvitag-ble", # https://github.com/Bluetooth-Devices/ruuvitag-ble/pull/10
|
|
||||||
"sensirion-ble", # https://github.com/akx/sensirion-ble/pull/9
|
|
||||||
"sharp_aquos_rc", # https://github.com/jmoore987/sharp_aquos_rc/pull/14
|
"sharp_aquos_rc", # https://github.com/jmoore987/sharp_aquos_rc/pull/14
|
||||||
"tapsaff", # https://github.com/bazwilliams/python-taps-aff/pull/5
|
"tapsaff", # https://github.com/bazwilliams/python-taps-aff/pull/5
|
||||||
"vincenty", # Public domain
|
"vincenty", # Public domain
|
||||||
"zeversolar", # https://github.com/kvanzuijlen/zeversolar/pull/46
|
"zeversolar", # https://github.com/kvanzuijlen/zeversolar/pull/46
|
||||||
# Using License-Expression (with hatchling)
|
|
||||||
"ftfy", # Apache-2.0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TODO = {
|
TODO = {
|
||||||
@ -176,22 +187,9 @@ TODO = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def main(argv: Sequence[str] | None = None) -> int:
|
def check_licenses(args: CheckArgs) -> int:
|
||||||
"""Run the main script."""
|
"""Check licenses are OSI approved."""
|
||||||
exit_code = 0
|
exit_code = 0
|
||||||
|
|
||||||
parser = ArgumentParser()
|
|
||||||
parser.add_argument(
|
|
||||||
"path",
|
|
||||||
nargs="?",
|
|
||||||
metavar="PATH",
|
|
||||||
default="licenses.json",
|
|
||||||
help="Path to json licenses file",
|
|
||||||
)
|
|
||||||
|
|
||||||
argv = argv or sys.argv[1:]
|
|
||||||
args = parser.parse_args(argv)
|
|
||||||
|
|
||||||
raw_licenses = json.loads(Path(args.path).read_text())
|
raw_licenses = json.loads(Path(args.path).read_text())
|
||||||
package_definitions = [PackageDefinition.from_dict(data) for data in raw_licenses]
|
package_definitions = [PackageDefinition.from_dict(data) for data in raw_licenses]
|
||||||
for package in package_definitions:
|
for package in package_definitions:
|
||||||
@ -244,8 +242,92 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||||||
return exit_code
|
return exit_code
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def extract_licenses(args: ExtractArgs) -> int:
|
||||||
exit_code = main()
|
"""Extract license data for installed packages."""
|
||||||
if exit_code == 0:
|
licenses = sorted(
|
||||||
|
[get_package_metadata(dist) for dist in list(metadata.distributions())],
|
||||||
|
key=lambda dist: dist["name"],
|
||||||
|
)
|
||||||
|
Path(args.output_file).write_text(json.dumps(licenses, indent=2))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_package_metadata(dist: metadata.Distribution) -> PackageMetadata:
|
||||||
|
"""Get package metadata for distribution."""
|
||||||
|
return {
|
||||||
|
"name": dist.name,
|
||||||
|
"version": dist.version,
|
||||||
|
"license_expression": dist.metadata.get("License-Expression"),
|
||||||
|
"license_metadata": dist.metadata.get("License"),
|
||||||
|
"license_classifier": extract_license_classifier(
|
||||||
|
dist.metadata.get_all("Classifier")
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def extract_license_classifier(classifiers: list[str] | None) -> list[str]:
|
||||||
|
"""Extract license from list of classifiers.
|
||||||
|
|
||||||
|
E.g. 'License :: OSI Approved :: MIT License' -> 'MIT License'.
|
||||||
|
Filter out bare 'License :: OSI Approved'.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
license_classifier
|
||||||
|
for classifier in classifiers or ()
|
||||||
|
if classifier.startswith("License")
|
||||||
|
and (license_classifier := classifier.rpartition(" :: ")[2])
|
||||||
|
and license_classifier != "OSI Approved"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ExtractArgs(Namespace):
|
||||||
|
"""Extract arguments."""
|
||||||
|
|
||||||
|
output_file: str
|
||||||
|
|
||||||
|
|
||||||
|
class CheckArgs(Namespace):
|
||||||
|
"""Check arguments."""
|
||||||
|
|
||||||
|
path: str
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
|
"""Run the main script."""
|
||||||
|
parser = ArgumentParser()
|
||||||
|
subparsers = parser.add_subparsers(title="Subcommands", required=True)
|
||||||
|
|
||||||
|
parser_extract = subparsers.add_parser("extract")
|
||||||
|
parser_extract.set_defaults(action="extract")
|
||||||
|
parser_extract.add_argument(
|
||||||
|
"--output-file",
|
||||||
|
default="licenses.json",
|
||||||
|
help="Path to store the licenses file",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser_check = subparsers.add_parser("check")
|
||||||
|
parser_check.set_defaults(action="check")
|
||||||
|
parser_check.add_argument(
|
||||||
|
"path",
|
||||||
|
nargs="?",
|
||||||
|
metavar="PATH",
|
||||||
|
default="licenses.json",
|
||||||
|
help="Path to json licenses file",
|
||||||
|
)
|
||||||
|
|
||||||
|
argv = argv or sys.argv[1:]
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
if args.action == "extract":
|
||||||
|
args = cast(ExtractArgs, args)
|
||||||
|
return extract_licenses(args)
|
||||||
|
if args.action == "check":
|
||||||
|
args = cast(CheckArgs, args)
|
||||||
|
if (exit_code := check_licenses(args)) == 0:
|
||||||
print("All licenses are approved!")
|
print("All licenses are approved!")
|
||||||
sys.exit(exit_code)
|
return exit_code
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
Loading…
x
Reference in New Issue
Block a user