Verified Commit 1553b755 authored by frnmst's avatar frnmst
Browse files

First commit.

parents
Pipfile.lock
*.yml
*.yaml
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
.static_storage/
.media/
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
Contributing
============
See https://docs.franco.net.eu.org/licheck/contributing.html
This diff is collapsed.
global-include LICENSE.txt
global-include README.rst
#!/usr/bin/env make
#
# Makefile
#
# Copyright (C) 2021 frnmst (Franco Masotti) <franco.masotti@live.com>
#
# This file is part of licheck.
#
# licheck is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# licheck is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with licheck. If not, see <http://www.gnu.org/licenses/>.
#
export PACKAGE_NAME=licheck
default: doc
doc: clean
pipenv run $(MAKE) -C docs html
install:
pip3 install . --user
uninstall:
pip3 uninstall --yes $(PACKAGE_NAME)
install-dev:
pipenv install --dev
pipenv run pre-commit install
uninstall-dev:
pipenv --rm
demo:
asciinema/licheck_asciinema_$$(git describe --tags $$(git rev-list --tags --max-count=1) | tr '.' '_')_demo.sh
test:
pipenv run python -m unittest $(PACKAGE_NAME).tests.tests --failfast --locals --verbose
dist:
pipenv run python setup.py sdist
pipenv run python setup.py bdist_wheel
pipenv run twine check dist/*
upload:
pipenv run twine upload dist/*
clean:
rm -rf build dist *.egg-info tests/benchmark-results *.md
pipenv run $(MAKE) -C docs clean
.PHONY: default doc install uninstall install-dev uninstall-dev test clean demo benchmark
#
# Pipfile
#
# Copyright (C) 2021 frnmst (Franco Masotti) <franco.masotti@live.com>
#
# This file is part of licheck.
#
# licheck is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# licheck is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with licheck. If not, see <http://www.gnu.org/licenses/>.
#
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
# Documentation.
Sphinx = "~=4.0"
sphinx-rtd-theme = "~=0.5"
# Tools.
twine = "~=3.4"
pre-commit = "~=2.13"
[packages]
PyYAML = "~=5.4"
tabulate = "~=0.8"
appdirs = "~=1.4"
requests = "~=2.25"
[requires]
python_version = "*"
licheck
=======
|buymeacoffee|
.. |buymeacoffee| image:: assets/buy_me_a_coffee.svg
:alt: Buy me a coffee
:target: https://buymeacoff.ee/frnmst
Automatically check the licenses of package dependencies.
Description
-----------
TODO
Documentation
-------------
https://docs.franco.net.eu.org/licheck/
API examples
------------
TODO
CLI Helps
---------
::
$ licheck --help
License
-------
Copyright (C) 2021 frnmst (Franco Masotti) <franco.masotti@live.com>
licheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
licheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with licheck. If not, see <http://www.gnu.org/licenses/>.
Trusted source
--------------
You can check the authenticity of new releases using my public key.
Instructions, sources and keys can be found at `blog.franco.net.eu.org/software <https://blog.franco.net.eu.org/software/>`_.
Crypto donations
----------------
- Bitcoin: bc1qnkflazapw3hjupawj0lm39dh9xt88s7zal5mwu
- Monero: 84KHWDTd9hbPyGwikk33Qp5GW7o7zRwPb8kJ6u93zs4sNMpDSnM5ZTWVnUp2cudRYNT6rNqctnMQ9NbUewbj7MzCBUcrQEY
- Dogecoin: DMB5h2GhHiTNW7EcmDnqkYpKs6Da2wK3zP
- Vertcoin: vtc1qd8n3jvkd2vwrr6cpejkd9wavp4ld6xfu9hkhh0
#
# __init__.py
#
# Copyright (C) 2021 frnmst (Franco Masotti) <franco.masotti@live.com>
#
# This file is part of licheck.
#
# licheck is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# licheck is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with licheck. If not, see <http://www.gnu.org/licenses/>.
#
"""Python discovery file."""
from .api import (build_command, check_cache_structure,
check_configuration_structure, check_licenses,
create_common_data_structure, create_files_data_structure,
get_binary_and_program, get_data, pipeline, print_errors,
read_configuration_file, read_remote_files, read_yaml_file,
transform_cache_to_data, write_cache)
from .cli import CliInterface
from .exceptions import (BinaryDoesNotExist, InvalidCache,
InvalidCommonDataStructure, InvalidConfiguration,
InvalidOutput)
#
# __main__.py
#
# Copyright (C) 2021 frnmst (Franco Masotti) <franco.masotti@live.com>
#
# This file is part of licheck.
#
# licheck is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# licheck is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with licheck. If not, see <http://www.gnu.org/licenses/>.
#
"""Call the CLI parser."""
import sys
import traceback
from .cli import CliInterface
def main(args=None):
"""Call the CLI interface and wait for the result."""
retcode = 0
try:
ci = CliInterface()
args = ci.parser.parse_args()
result = args.func(args)
if result is not None:
print(result)
retcode = 0
except Exception:
retcode = 1
traceback.print_exc()
sys.exit(retcode)
if __name__ == '__main__':
main()
#
# api.py
#
# Copyright (C) 2021 frnmst (Franco Masotti) <franco.masotti@live.com>
#
# This file is part of licheck.
#
# licheck is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# licheck is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with licheck. If not, see <http://www.gnu.org/licenses/>.
#
import hashlib
import json
import logging
import pathlib
import re
import shlex
import shutil
import subprocess
from urllib.parse import urlparse
import requests
import yaml
from appdirs import AppDirs
from tabulate import tabulate
from .constants import common_defaults
from .exceptions import (BinaryDoesNotExist, InvalidCache,
InvalidCommonDataStructure, InvalidConfiguration,
InvalidOutput)
def build_command(binary: str, license_program: str, file: str) -> str:
r"""Build a command to let the license programs discover all packages."""
command = str()
if license_program == 'dep_license':
output_format = 'json'
command = binary + ' --dev --format ' + output_format + ' ' + file
return command
def create_common_data_structure(input: list, license_program: str, file: str) -> list:
r"""Create a data structure common to all outputs."""
output = list()
if license_program == 'dep_license':
for element in range(0, len(input)):
output.append(element)
output[element] = dict()
output[element]['package'] = input[element]['Name']
# This element should be a list of licenses.
output[element]['license_short'] = [input[element]['Meta']]
# This element should be a list of licenses.
output[element]['license_long'] = [input[element]['Classifier']]
# Add the file name.
output[element]['file'] = file
output[element]['version'] = str()
return output
def get_data(command: str, license_program: str, file: str) -> dict:
r"""Return an object with the output of the licenses."""
output = str()
p = subprocess.run(
shlex.split(command),
capture_output=True,
check=True
)
output = p.stdout.decode('UTF-8').strip()
data = dict()
if license_program == 'dep_license':
# Output must conform.
if not re.match('(Found dependencies: \d+|no dependencies found)', output):
raise InvalidOutput
# Sanitize output so json can be loaded without problems.
output = re.sub('Found dependencies: \d+', '', output)
output = re.sub('no dependencies found', '{}', output)
data = json.loads(output)
return data
def transform_cache_to_data(cache: dict, file: str, file_checksum: str) -> list:
if not check_cache_structure(cache):
raise InvalidCache
i = 0
output = list()
for file_id in cache:
if file_checksum == file_id:
x = 0
while x < len(cache[file_id]):
output.append(dict())
# Required.
output[i]['package'] = cache[file_id][x]['p']
# This element should be a list of licenses.
output[i]['license_short'] = cache[file_id][x]['s']
# This element should be a list of licenses.
# Required.
output[i]['license_long'] = cache[file_id][x]['l']
# Add the file name.
# Required.
output[i]['file'] = file
output[i]['version'] = cache[file_id][x]['v']
x += 1
i += 1
return output
def read_yaml_file(file: str) -> dict:
data = dict()
if pathlib.Path(file).is_file():
data = yaml.load(open(file, 'r'), Loader=yaml.SafeLoader)
return data
def write_cache(packages: list, file_checksum: str, cache_file: str):
if not check_common_data_structure(packages):
raise InvalidCommonDataStructure
table = dict()
id = file_checksum
table[id] = list()
for package in packages:
table[id].append({
'p': package['package'],
's': package['license_short'],
'l': package['license_long'],
'v': package['version'],
})
with open(cache_file, 'a') as f:
f.write(yaml.dump(table))
def check_licenses(packages: list, licenses_allowed: list) -> list:
if not check_common_data_structure(packages):
raise InvalidCommonDataStructure
errors = list()
for package in packages:
for p in package['license_long']:
if p not in licenses_allowed:
errors.append(package)
if len(package['license_long']) == 0:
errors.append(package)
return errors
def print_errors(packages: list, cut_output: bool = False) -> str:
if not check_common_data_structure(packages):
raise InvalidCommonDataStructure
# Re-format the output.
j = 0
while j < len(packages):
i = 0
for x in packages[j]['license_short']:
if cut_output and len(x) > common_defaults['table element max length']:
packages[j]['license_short'][i] = x[:common_defaults['table element max length']] + '...'
i += 1
i = 0
for x in packages[j]['license_long']:
if cut_output and len(x) > common_defaults['table element max length']:
packages[j]['license_long'][i] = x[:common_defaults['table element max length']] + '...'
i += 1
if packages[j]['version'] == str():
packages[j]['version'] = '-'
j += 1
logging.basicConfig(format='WARNING:licheck:\n%(message)s')
logging.warning('unapproved licenses')
logging.warning(tabulate(packages, headers="keys"))
def get_binary_and_program(language: str) -> str:
r"""Get the license binary path and the program name."""
binary = str()
program_name = str()
if language == 'python':
binary = 'deplic'
program_name = 'dep_license'
if shutil.which(binary) is None:
raise BinaryDoesNotExist
else:
print(shutil.which(binary))
return shutil.which(binary), program_name
def check_configuration_structure(data_struct: dict, local: bool = True) -> bool:
ok = False
if local:
if ('language' in data_struct
and isinstance(data_struct['language'], str)
and 'include' in data_struct
and isinstance(data_struct['include'], list)
and 'files to check' in data_struct
and isinstance(data_struct['files to check'], list)
and 'allowed licenses' in data_struct
and isinstance(data_struct['allowed licenses'], list)):
ok = True
for url in data_struct['include']:
if urlparse(url).scheme not in ['http', 'https']:
ok = False
else:
if ('language' in data_struct
and isinstance(data_struct['language'], str)
and 'include' in data_struct
and isinstance(data_struct['include'], list)
and data_struct['include'] == list()
and 'files to check' in data_struct
and isinstance(data_struct['files to check'], list)
and data_struct['files to check'] == list()
and 'allowed licenses' in data_struct
and isinstance(data_struct['allowed licenses'], list)):
ok = True
for l in data_struct['allowed licenses']:
if not isinstance(l, str):
ok = False
return ok
def check_cache_structure(cache: dict) -> bool:
ok = False
for element in cache:
for package in cache[element]:
if ('l' in package
and 'p' in package
and 's' in package
and 'v' in package
and isinstance(package['l'], list)
and isinstance(package['p'], str)
and isinstance(package['s'], list)
and isinstance(package['v'], str)):
ok = True
ok_1 = True
ok_2 = True
if ok: