Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/#75 add support for project license auditing #76

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,25 @@ jobs:
- name: Check Version(s)
run: poetry run version-check `poetry run python -c "from noxconfig import PROJECT_CONFIG; print(PROJECT_CONFIG.version_file)"`

license-check-job:
name: Check Licences
runs-on: ubuntu-latest

steps:
- name: SCM Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Setup Python & Poetry Environment
uses: ./.github/actions/python-environment

- name: Check Version(s)
run: poetry run python -m nox -s audit

build-documentation-job:
name: Build Documentation
needs: [ version-check-job ]
needs: [ version-check-job, license-check-job ]
runs-on: ubuntu-latest

steps:
Expand All @@ -42,7 +58,7 @@ jobs:

lint-job:
name: Linting (Python-${{ matrix.python-version }})
needs: [ version-check-job ]
needs: [ version-check-job, license-check-job ]
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand All @@ -69,7 +85,7 @@ jobs:

type-check-job:
name: Type Checking (Python-${{ matrix.python-version }})
needs: [ version-check-job ]
needs: [ version-check-job, license-check-job ]
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand Down
85 changes: 85 additions & 0 deletions exasol/toolbox/license.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from collections import defaultdict
from dataclasses import dataclass
from typing import (
Dict,
List,
Tuple,
)


@dataclass(frozen=True)
class Package:
name: str
license: str
version: str


def _packages(package_info):
for p in package_info:
kwargs = {key.lower(): value for key, value in p.items()}
yield Package(**kwargs)


def _normalize(license):
def is_mulit_license(l):
return ";" in l

def select_most_permissive(l):
licenses = [_normalize(l.strip()) for l in l.split(";")]
priority = defaultdict(
lambda: 9999,
{"Unlicense": 0, "BSD": 1, "MIT": 2, "MPLv2": 3, "LGPLv2": 4, "GPLv2": 5},
)
priority_to_license = defaultdict(
lambda: "Unknown", {v: k for k, v in priority.items()}
)
selected = min(*[priority[lic] for lic in licenses])
return priority_to_license[selected]

mapping = {
"BSD License": "BSD",
"MIT License": "MIT",
"The Unlicense (Unlicense)": "Unlicense",
"Mozilla Public License 2.0 (MPL 2.0)": "MPLv2",
"GNU Lesser General Public License v2 (LGPLv2)": "LGPLv2",
"GNU General Public License v2 (GPLv2)": "GPLv2",
}
if is_mulit_license(license):
return select_most_permissive(license)

if license not in mapping:
return license

return mapping[license]


def audit(
licenses: List[Dict[str, str]], acceptable: List[str], exceptions: Dict[str, str]
) -> Tuple[List[Package], List[Package]]:
"""
Audit package licenses.

Args:
licenses: a list of dictionaries containing license information for packages.
This information e.g. can be obtained by running `pip-licenses --format=json`.

example: [{"License": "BSD License", "Name": "Babel", "Version": "2.12.1"}, ...]

acceptable: A list of licenses which shall be accepted.
example: ["BSD License", "MIT License", ...]

exceptions: A dictionary containing package names and justifications for packages to ignore/skip.
example: {'packagename': 'justification why this is/can be an exception'}

Returns:
Two lists containing found violations and ignored packages.
"""
packages = list(_packages(licenses))
acceptable = [_normalize(a) for a in acceptable]
ignored = [p for p in packages if p.name in exceptions and exceptions[p.name]]
violations = [
p
for p in packages
if _normalize(p.license) not in acceptable and p not in ignored
]
return violations, ignored
38 changes: 38 additions & 0 deletions exasol/toolbox/nox/tasks.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from __future__ import annotations

from io import (
BytesIO,
TextIOWrapper,
)
from subprocess import run

__all__ = [
"Mode",
"fix",
Expand Down Expand Up @@ -239,6 +245,38 @@ def _coverage(
session.run(*command)


@nox.session(name="audit", python=False)
def audit(session: Session) -> None:
"""Audit project dependencies in regard of their license."""

def _packages():
result = run(
["poetry", "show", "--no-ansi", "--no-interaction", "--only", "main"],
capture_output=True,
check=True,
)
stream = TextIOWrapper(BytesIO(result.stdout))
lines = (line for line in stream)
return (line.split(" ")[0] for line in lines)

packages = [
package
for package in _packages()
if package not in PROJECT_CONFIG.audit_exceptions
]
session.run(
"poetry",
"run",
"python",
"-m",
"piplicenses",
"--packages",
*packages,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in that case you must hope, that you don't have more packages as the command line allows parameters.https://www.cyberciti.biz/faq/linux-unix-arg_max-maximum-length-of-arguments/

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"--allow-only",
";".join(PROJECT_CONFIG.audit_licenses),
)


@nox.session(name="build-docs", python=False)
def build_docs(session: Session) -> None:
"""Builds the project documentation"""
Expand Down
21 changes: 21 additions & 0 deletions noxconfig.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from dataclasses import dataclass
from inspect import cleandoc
from pathlib import Path
from typing import (
Any,
Expand All @@ -18,6 +19,26 @@ class Config:
version_file: Path = Path(__file__).parent / "exasol" / "toolbox" / "version.py"
path_filters: Iterable[str] = ("dist", ".eggs", "venv")

audit_licenses = ["Apache Software License"]
audit_exceptions = {
"pylint": cleandoc(
"""
The project only makes use of pylint command line.

It only was added as normal dependency to save the "clients" the step
of manually adding it as dependency.

Note(s):

Pylint could be marked, added as optional (extra) dependency to make it obvious
that it is an opt in, controlled by the "user/client".

Replacing pylint with an alternative (like `ruff <https://github.com/astral-sh/ruff>`_)
with a more would remove the ambiguity and need for justification.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
with a more would remove the ambiguity and need for justification.
with a more permissive license would remove the ambiguity and need for justification.

"""
)
}

@staticmethod
def pre_integration_tests_hook(
_session: Session, _config: Config, _context: MutableMapping[str, Any]
Expand Down
Loading