From 8211bf300c411dcbc8d93f1db51bc9b461c9970f Mon Sep 17 00:00:00 2001 From: RepoDynamicsBot <80158628+AAriam@users.noreply.github.com> Date: Thu, 10 Oct 2024 20:49:54 +0200 Subject: [PATCH] release first dev --- .github/workflows/_pkg_publish.yaml | 61 +++ .gitignore | 149 ++++++ pyproject.toml | 29 ++ src/licenseman/__init__.py | 0 src/licenseman/data/__init__.py | 29 ++ src/licenseman/data/__test_file__ | 1 + .../data/spdx/trove_classifiers.yaml | 299 +++++++++++++ src/licenseman/exception/__init__.py | 5 + src/licenseman/exception/data.py | 35 ++ src/licenseman/license.py | 423 ++++++++++++++++++ 10 files changed, 1031 insertions(+) create mode 100644 .github/workflows/_pkg_publish.yaml create mode 100644 .gitignore create mode 100644 pyproject.toml create mode 100644 src/licenseman/__init__.py create mode 100644 src/licenseman/data/__init__.py create mode 100644 src/licenseman/data/__test_file__ create mode 100644 src/licenseman/data/spdx/trove_classifiers.yaml create mode 100644 src/licenseman/exception/__init__.py create mode 100644 src/licenseman/exception/data.py create mode 100644 src/licenseman/license.py diff --git a/.github/workflows/_pkg_publish.yaml b/.github/workflows/_pkg_publish.yaml new file mode 100644 index 0000000..50dbd55 --- /dev/null +++ b/.github/workflows/_pkg_publish.yaml @@ -0,0 +1,61 @@ + +name: 'Package - Build' +run-name: 'Package: Build from ${{github.ref}}' + + +on: + workflow_dispatch: + push: + paths: + - 'pyproject.toml' + +jobs: + + publish-pypi: + runs-on: ubuntu-latest + permissions: + id-token: write + environment: + name: PyPI + steps: + - name: 'Checkout repository from ${{github.ref}}' + uses: actions/checkout@v4 + + - name: 'Build sdist' + run: | + pipx run build --sdist --wheel --outdir dist/ + + - name: 'Upload package' + uses: pypa/gh-action-pypi-publish@release/v1 + # https://github.com/marketplace/actions/pypi-publish + with: + packages-dir: dist + verify-metadata: false + verbose: true + print-hash: true + skip-existing: false + + publish-testpypi: + runs-on: ubuntu-latest + permissions: + id-token: write + environment: + name: TestPyPI + steps: + - name: 'Checkout repository from ${{github.ref}}' + uses: actions/checkout@v4 + + - name: 'Build sdist' + run: | + pipx run build --sdist --wheel --outdir dist/ + + - name: 'Upload package' + uses: pypa/gh-action-pypi-publish@release/v1 + # https://github.com/marketplace/actions/pypi-publish + with: + packages-dir: dist + repository-url: https://test.pypi.org/legacy/ + verify-metadata: false + verbose: true + print-hash: true + skip-existing: false \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0134662 --- /dev/null +++ b/.gitignore @@ -0,0 +1,149 @@ +# Ref: https://github.com/github/gitignore + +# 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/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# 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/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/website/_build/ +docs/website/source/api/_autosummary + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# 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/ +.dmypy.json +dmypy.json + +# Pyre type checker +# There are reports this comes from LLVM profiling, but also Xcode 9. +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# profraw files from LLVM? Unclear exactly what triggers this +# There are reports this comes from LLVM profiling, but also Xcode 9. +*profraw + +# In-tree generated files +*/_version.py + +# VSCode +.vscode/ + +# PyCharm +.idea/ + +# MacOS system files +.DS_Store + +# PyPackIT +data/_local_reports/ +data/_cache/ +!/dev/build/ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..16f2e75 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ + +[build-system] +requires = ["setuptools>=61.0", "versioningit"] +build-backend = "setuptools.build_meta" + + +# ----------------------------------------- setuptools ------------------------------------------- +[tool.setuptools] +include-package-data = true +zip-safe = false + +[tool.setuptools.packages.find] +where = ["src"] +namespaces = true + + +# ----------------------------------------- Project Metadata ------------------------------------- +# +[project] +version = "0.0.0.dev1" +name = "LicenseMan" +requires-python = ">=3.10" +dependencies = [ + "LoggerMan == 0.0.0.dev49", + "PyLinks", + "PkgData", + "MDit == 0.0.0.dev20", + "ExceptionMan == 0.0.0.dev20", +] diff --git a/src/licenseman/__init__.py b/src/licenseman/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/licenseman/data/__init__.py b/src/licenseman/data/__init__.py new file mode 100644 index 0000000..3291611 --- /dev/null +++ b/src/licenseman/data/__init__.py @@ -0,0 +1,29 @@ +"""Functions for retrieving package data files.""" + +from __future__ import annotations as _annotations + +from pathlib import Path as _Path + +import pkgdata as _pkgdata + +__all__ = ["get"] + + +def get(relative_path: str) -> _Path: + """Get the absolute path to a package data file. + + Parameters + ---------- + relative_path + Path to the file relative to the package's data directory. + """ + path_data_dir = _Path(_pkgdata.get_package_path_from_caller(top_level=False)) + filepath = path_data_dir / relative_path + if not filepath.is_file(): + from licenseman.exception.data import DataFileNotFoundError + + raise DataFileNotFoundError( + path_relative=relative_path, + path_absolute=filepath, + ) + return filepath diff --git a/src/licenseman/data/__test_file__ b/src/licenseman/data/__test_file__ new file mode 100644 index 0000000..ffff831 --- /dev/null +++ b/src/licenseman/data/__test_file__ @@ -0,0 +1 @@ +This is a test file used by our test suite to verify that package data is being included correctly. diff --git a/src/licenseman/data/spdx/trove_classifiers.yaml b/src/licenseman/data/spdx/trove_classifiers.yaml new file mode 100644 index 0000000..983996e --- /dev/null +++ b/src/licenseman/data/spdx/trove_classifiers.yaml @@ -0,0 +1,299 @@ +# Mapping of SPDX license identifiers to PyPI Trove classifiers. + +map: + 'Aladdin': 'License :: Aladdin Free Public License (AFPL)' + + 'CC0-1.0': 'License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication' + + 'CECILL-B': 'License :: CeCILL-B Free Software License Agreement (CECILL-B)' + + 'CECILL-C': 'License :: CeCILL-C Free Software License Agreement (CECILL-C)' + + 'NPL-1.0': 'License :: Netscape Public License (NPL)' + 'NPL-1.1': 'License :: Netscape Public License (NPL)' + + 'AFL-1.1': 'License :: OSI Approved :: Academic Free License (AFL)' + 'AFL-1.2': 'License :: OSI Approved :: Academic Free License (AFL)' + 'AFL-2.0': 'License :: OSI Approved :: Academic Free License (AFL)' + 'AFL-2.1': 'License :: OSI Approved :: Academic Free License (AFL)' + 'AFL-3.0': 'License :: OSI Approved :: Academic Free License (AFL)' + + 'Apache-1.0': 'License :: OSI Approved :: Apache Software License' + 'Apache-1.1': 'License :: OSI Approved :: Apache Software License' + 'Apache-2.0': 'License :: OSI Approved :: Apache Software License' + + 'APSL-1.0': 'License :: OSI Approved :: Apple Public Source License' + 'APSL-1.1': 'License :: OSI Approved :: Apple Public Source License' + 'APSL-1.2': 'License :: OSI Approved :: Apple Public Source License' + 'APSL-2.0': 'License :: OSI Approved :: Apple Public Source License' + + 'Artistic-1.0': 'License :: OSI Approved :: Artistic License' + 'Artistic-1.0-cl8': 'License :: OSI Approved :: Artistic License' + 'Artistic-1.0-Perl': 'License :: OSI Approved :: Artistic License' + 'Artistic-2.0': 'License :: OSI Approved :: Artistic License' + 'ClArtistic': 'License :: OSI Approved :: Artistic License' + + 'AAL': 'License :: OSI Approved :: Attribution Assurance License' + + 'BSD-1-Clause': 'License :: OSI Approved :: BSD License' + 'BSD-2-Clause': 'License :: OSI Approved :: BSD License' + 'BSD-2-Clause-Darwin': 'License :: OSI Approved :: BSD License' + 'BSD-2-Clause-FreeBSD': 'License :: OSI Approved :: BSD License' + 'BSD-2-Clause-NetBSD': 'License :: OSI Approved :: BSD License' + 'BSD-2-Clause-Patent': 'License :: OSI Approved :: BSD License' + 'BSD-2-Clause-Views': 'License :: OSI Approved :: BSD License' + 'BSD-2-Clause-first-lines': 'License :: OSI Approved :: BSD License' + 'BSD-3-Clause': 'License :: OSI Approved :: BSD License' + 'BSD-3-Clause-Attribution': 'License :: OSI Approved :: BSD License' + 'BSD-3-Clause-Clear': 'License :: OSI Approved :: BSD License' + 'BSD-3-Clause-HP': 'License :: OSI Approved :: BSD License' + 'BSD-3-Clause-LBNL': 'License :: OSI Approved :: BSD License' + 'BSD-3-Clause-Modification': 'License :: OSI Approved :: BSD License' + 'BSD-3-Clause-No-Military-License': 'License :: OSI Approved :: BSD License' + 'BSD-3-Clause-No-Nuclear-License': 'License :: OSI Approved :: BSD License' + 'BSD-3-Clause-No-Nuclear-License-2014': 'License :: OSI Approved :: BSD License' + 'BSD-3-Clause-No-Nuclear-Warranty': 'License :: OSI Approved :: BSD License' + 'BSD-3-Clause-Open-MPI': 'License :: OSI Approved :: BSD License' + 'BSD-3-Clause-Sun': 'License :: OSI Approved :: BSD License' + 'BSD-3-Clause-acpica': 'License :: OSI Approved :: BSD License' + 'BSD-3-Clause-flex': 'License :: OSI Approved :: BSD License' + 'BSD-4-Clause': 'License :: OSI Approved :: BSD License' + 'BSD-4-Clause-Shortened': 'License :: OSI Approved :: BSD License' + 'BSD-4-Clause-UC': 'License :: OSI Approved :: BSD License' + 'BSD-4.3RENO': 'License :: OSI Approved :: BSD License' + 'BSD-4.3TAHOE': 'License :: OSI Approved :: BSD License' + 'BSD-Advertising-Acknowledgement': 'License :: OSI Approved :: BSD License' + 'BSD-Attribution-HPND-disclaimer': 'License :: OSI Approved :: BSD License' + 'BSD-Inferno-Nettverk': 'License :: OSI Approved :: BSD License' + 'BSD-Protection': 'License :: OSI Approved :: BSD License' + 'BSD-Source-Code': 'License :: OSI Approved :: BSD License' + 'BSD-Source-beginning-file': 'License :: OSI Approved :: BSD License' + 'BSD-Systemics': 'License :: OSI Approved :: BSD License' + 'BSD-Systemics-W3Works': 'License :: OSI Approved :: BSD License' + + 'BlueOak-1.0.0': 'License :: OSI Approved :: Blue Oak Model License (BlueOak-1.0.0)' + + 'BSL-1.0': 'License :: OSI Approved :: Boost Software License 1.0 (BSL-1.0)' + + 'CECILL-2.1': 'License :: OSI Approved :: CEA CNRS Inria Logiciel Libre License, version 2.1 (CeCILL-2.1)' + + 'CDDL-1.0': 'License :: OSI Approved :: Common Development and Distribution License 1.0 (CDDL-1.0)' + + 'CPL-1.0': 'License :: OSI Approved :: Common Public License' + + 'EPL-1.0': 'License :: OSI Approved :: Eclipse Public License 1.0 (EPL-1.0)' + + 'EPL-2.0': 'License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)' + + 'ECL-2.0': 'License :: OSI Approved :: Educational Community License, Version 2.0 (ECL-2.0)' + + 'EFL-1.0': 'License :: OSI Approved :: Eiffel Forum License' + 'EFL-2.0': 'License :: OSI Approved :: Eiffel Forum License' + + 'EUPL-1.0': 'License :: OSI Approved :: European Union Public Licence 1.0 (EUPL 1.0)' + + 'EUPL-1.1': 'License :: OSI Approved :: European Union Public Licence 1.1 (EUPL 1.1)' + + 'EUPL-1.2': 'License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1.2)' + + 'AGPL-3.0-only': 'License :: OSI Approved :: GNU Affero General Public License v3' + 'AGPL-3.0': 'License :: OSI Approved :: GNU Affero General Public License v3' + + 'AGPL-3.0-or-later': 'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)' + + 'GFDL-1.1': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.1-invariants-only': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.1-invariants-or-later': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.1-no-invariants-only': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.1-no-invariants-or-later': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.1-only': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.1-or-later': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.2': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.2-invariants-only': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.2-invariants-or-later': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.2-no-invariants-only': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.2-no-invariants-or-later': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.2-only': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.2-or-later': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.3': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.3-invariants-only': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.3-invariants-or-later': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.3-no-invariants-only': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.3-no-invariants-or-later': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.3-only': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + 'GFDL-1.3-or-later': 'License :: OSI Approved :: GNU Free Documentation License (FDL)' + + 'GPL-1.0-only': 'License :: OSI Approved :: GNU General Public License (GPL)' + 'GPL-1.0-or-later': 'License :: OSI Approved :: GNU General Public License (GPL)' + 'GPL-1.0': 'License :: OSI Approved :: GNU General Public License (GPL)' + 'GPL-1.0+': 'License :: OSI Approved :: GNU General Public License (GPL)' + + 'GPL-2.0-only': 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)' + 'GPL-2.0': 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)' + 'GPL-2.0-with-autoconf-exception': 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)' + 'GPL-2.0-with-bison-exception': 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)' + 'GPL-2.0-with-classpath-exception': 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)' + 'GPL-2.0-with-font-exception': 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)' + 'GPL-2.0-with-GCC-exception': 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)' + + 'GPL-2.0-or-later': 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)' + 'GPL-2.0+': 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)' + + 'GPL-3.0-only': 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)' + 'GPL-3.0': 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)' + 'GPL-3.0-with-autoconf-exception': 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)' + 'GPL-3.0-with-GCC-exception': 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)' + + 'GPL-3.0-or-later': 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)' + 'GPL-3.0+': 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)' + + 'LGPL-2.1-only': 'License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)' + 'LGPL-2.1': 'License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)' + + 'LGPL-2.1-or-later': 'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)' + 'LGPL-2.1+': 'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)' + + 'LGPL-3.0-only': 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)' + 'LGPL-3.0': 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)' + + 'LGPL-3.0-or-later': 'License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)' + 'LGPL-3.0+': 'License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)' + + 'LGPL-2.0-only': 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)' + 'LGPL-2.0-or-later': 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)' + 'LGPL-2.0': 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)' + 'LGPL-2.0+': 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)' + + 'HPND': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-DEC': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-Fenneberg-Livingston': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-INRIA-IMAG': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-Intel': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-Kevlin-Henney': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-MIT-disclaimer': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-Markus-Kuhn': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-Netrek': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-Pbmplus': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-UC': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-UC-export-US': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-doc': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-doc-sell': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-export-US': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-export-US-acknowledgement': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-export-US-modify': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-export2-US': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-merchantability-variant': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-sell-MIT-disclaimer-xserver': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-sell-regexpr': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-sell-variant': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-sell-variant-MIT-disclaimer': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + 'HPND-sell-variant-MIT-disclaimer-rev': 'License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)' + + 'IPL-1.0': 'License :: OSI Approved :: IBM Public License' + + 'ISC': 'License :: OSI Approved :: ISC License (ISCL)' + 'ISC-Veillard': 'License :: OSI Approved :: ISC License (ISCL)' + + 'Intel': 'License :: OSI Approved :: Intel Open Source License' + + 'MIT': 'License :: OSI Approved :: MIT License' + 'MIT-0': 'License :: OSI Approved :: MIT License' + 'MIT-CMU': 'License :: OSI Approved :: MIT License' + 'MIT-Festival': 'License :: OSI Approved :: MIT License' + 'MIT-Khronos-old': 'License :: OSI Approved :: MIT License' + 'MIT-Modern-Variant': 'License :: OSI Approved :: MIT License' + 'MIT-Wu': 'License :: OSI Approved :: MIT License' + 'MIT-advertising': 'License :: OSI Approved :: MIT License' + 'MIT-enna': 'License :: OSI Approved :: MIT License' + 'MIT-feh': 'License :: OSI Approved :: MIT License' + 'MIT-open-group': 'License :: OSI Approved :: MIT License' + 'MIT-testregex': 'License :: OSI Approved :: MIT License' + 'MITNFA': 'License :: OSI Approved :: MIT License' + + 'MirOS': 'License :: OSI Approved :: MirOS License (MirOS)' + + 'Motosoto': 'License :: OSI Approved :: Motosoto License' + + 'MPL-1.0': 'License :: OSI Approved :: Mozilla Public License 1.0 (MPL)' + + 'MPL-1.1': 'License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1)' + + 'MPL-2.0': 'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)' + 'MPL-2.0-no-copyleft-exception': 'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)' + + 'MulanPSL-2.0': 'License :: OSI Approved :: Mulan Permissive Software License v2 (MulanPSL-2.0)' + + 'NASA-1.3': 'License :: OSI Approved :: NASA Open Source Agreement v1.3 (NASA-1.3)' + + 'NGPL': 'License :: OSI Approved :: Nethack General Public License' + + 'Nokia': 'License :: OSI Approved :: Nokia Open Source License' + + 'OGTSL': 'License :: OSI Approved :: Open Group Test Suite License' + + 'OSL-3.0': 'License :: OSI Approved :: Open Software License 3.0 (OSL-3.0)' + + 'PostgreSQL': 'License :: OSI Approved :: PostgreSQL License' + + 'CNRI-Python': 'License :: OSI Approved :: Python License (CNRI Python License)' + 'CNRI-Python-GPL-Compatible': 'License :: OSI Approved :: Python License (CNRI Python License)' + + 'PSF-2.0': 'License :: OSI Approved :: Python Software Foundation License' + + 'QPL-1.0': 'License :: OSI Approved :: Qt Public License (QPL)' + 'QPL-1.0-INRIA-2004': 'License :: OSI Approved :: Qt Public License (QPL)' + + 'RSCPL': 'License :: OSI Approved :: Ricoh Source Code Public License' + + 'OFL-1.1': 'License :: OSI Approved :: SIL Open Font License 1.1 (OFL-1.1)' + 'OFL-1.1-no-RFN': 'License :: OSI Approved :: SIL Open Font License 1.1 (OFL-1.1)' + 'OFL-1.1-RFN': 'License :: OSI Approved :: SIL Open Font License 1.1 (OFL-1.1)' + + 'Sleepycat': 'License :: OSI Approved :: Sleepycat License' + + 'SISSL': 'License :: OSI Approved :: Sun Industry Standards Source License (SISSL)' + 'SISSL-1.2': 'License :: OSI Approved :: Sun Industry Standards Source License (SISSL)' + + 'SPL-1.0': 'License :: OSI Approved :: Sun Public License' + + 'Unlicense': 'License :: OSI Approved :: The Unlicense (Unlicense)' + + 'UPL-1.0': 'License :: OSI Approved :: Universal Permissive License (UPL)' + + 'NCSA': 'License :: OSI Approved :: University of Illinois/NCSA Open Source License' + + 'VSL-1.0': 'License :: OSI Approved :: Vovida Software License 1.0' + + 'W3C': 'License :: OSI Approved :: W3C License' + 'W3C-19980720': 'License :: OSI Approved :: W3C License' + 'W3C-20150513': 'License :: OSI Approved :: W3C License' + + 'Xnet': 'License :: OSI Approved :: X.Net License' + + '0BSD': 'License :: OSI Approved :: Zero-Clause BSD (0BSD)' + + 'ZPL-1.1': 'License :: OSI Approved :: Zope Public License' + 'ZPL-2.0': 'License :: OSI Approved :: Zope Public License' + 'ZPL-2.1': 'License :: OSI Approved :: Zope Public License' + + 'Zlib': 'License :: OSI Approved :: zlib/libpng License' + 'zlib-acknowledgement': 'License :: OSI Approved :: zlib/libpng License' + +orphans: + - 'License :: DFSG approved' + - 'License :: Eiffel Forum License (EFL)' # Duplicate of 'License :: OSI Approved :: Eiffel Forum License' + - 'License :: Free For Educational Use' + - 'License :: Free For Home Use' + - 'License :: Free To Use But Restricted' + - 'License :: Free for non-commercial use' + - 'License :: Freely Distributable' + - 'License :: Freeware' + - 'License :: GUST Font License 1.0' + - 'License :: GUST Font License 2006-09-30' + - 'License :: Nokia Open Source License (NOKOS)' # Duplicate of 'License :: OSI Approved :: Nokia Open Source License' + - 'License :: OSI Approved' + - 'License :: OSI Approved :: Jabber Open Source License' + - 'License :: OSI Approved :: MITRE Collaborative Virtual Workspace License (CVW)' + - 'License :: Other/Proprietary License' + - 'License :: Public Domain' + - 'License :: Repoze Public License' \ No newline at end of file diff --git a/src/licenseman/exception/__init__.py b/src/licenseman/exception/__init__.py new file mode 100644 index 0000000..971ebcb --- /dev/null +++ b/src/licenseman/exception/__init__.py @@ -0,0 +1,5 @@ +"""Exceptions raised by the package.""" + + +class LicenseManError(Exception): # noqa: N818 + """Base class for all exceptions raised by this package.""" diff --git a/src/licenseman/exception/data.py b/src/licenseman/exception/data.py new file mode 100644 index 0000000..5cb2095 --- /dev/null +++ b/src/licenseman/exception/data.py @@ -0,0 +1,35 @@ +"""Exceptions raised by the data module.""" + +from __future__ import annotations as _annotations + +from typing import TYPE_CHECKING as _TYPE_CHECKING + +from licenseman.exception import LicenseManError as _LicenseManError + +if _TYPE_CHECKING: + from pathlib import Path + + +class DataFileNotFoundError(_LicenseManError): + """Raised when a requested package data file is not found. + + Parameters + ---------- + path_relative + Path to the file relative to the package's data directory. + path_absolute + Absolute path to the file. + """ + + def __init__( + self, + path_relative: str, + path_absolute: Path, + ): + self.path_relative = path_relative + self.path_absolute = path_absolute + super().__init__( + "Could not find the requested package data file " + f"'{path_relative}' at '{path_absolute}'.", + ) + return diff --git a/src/licenseman/license.py b/src/licenseman/license.py new file mode 100644 index 0000000..0f54da2 --- /dev/null +++ b/src/licenseman/license.py @@ -0,0 +1,423 @@ +import re as _re +from xml.etree import ElementTree as ET +from textwrap import TextWrapper as _TextWrapper +import pylinks as pl +from pylinks.exception.api import WebAPIError as _WebAPIError + + +class SPDXLicense: + + def __init__( + self, + license_id: str, + ): + self._id = license_id + self._data: ET.Element = None + self._ns = {'': 'http://www.spdx.org/license'} + return + + def run(self): + self._data = self.get_license() + + @property + def xml_url(self) -> str: + return f'https://raw.githubusercontent.com/spdx/license-list-data/refs/heads/main/license-list-XML/{self._id}.xml' + + def get_license(self) -> ET.Element: + """ + Takes an SPDX license ID, downloads the license XML, and returns a parsed data structure. + + Parameters: + license_id (str): The SPDX license ID (e.g., 'MIT', 'GPL-2.0-or-later'). + + Returns: + ElementTree.Element: The license element of the parsed XML tree. + """ + # Construct the URL for the raw XML file in the GitHub repository + try: + data = pl.http.request(self.xml_url, response_type="str") + except _WebAPIError as e: + raise Exception(f"Error downloading license XML for ID '{self._id}") from e + try: + root = ET.fromstring(data) + except ET.ParseError as e: + raise Exception(f"Error parsing XML content for ID '{self._id}'") from e + self._data = root.find('spdx:license', self._ns) + return self._data + + def fullname(self): + return self._data.attrib.get('name') + + def osi_approved(self) -> bool | None: + val = self._data.attrib.get('isOsiApproved') + return val == 'true' + + def cross_refs(self) -> list[str]: + cross_refs = self._data.find('crossRefs', self._ns) + if not cross_refs: + return [] + return [ref.text for ref in cross_refs.findall('crossRef', self._ns)] + + def notes(self) -> str: + return self._data.find('notes', self._ns).text + + def text(self) -> ET.Element: + return self._data.find('text', self._ns) + + def header(self) -> ET.Element: + return self._data.find('.//standardLicenseHeader', self._ns) + + + +class SPDXLicenseTextParser: + """ + Parses the element from an SPDX license XML and generates a plain-text version of the license. + + Parameters + ---------- + text_element : xml.etree.ElementTree.Element + The XML element to parse. + """ + + def __init__(self, text_element: ET.Element): + self._text = text_element + + self._ns_uri = 'http://www.spdx.org/license' + self._ns = {'': self._ns_uri} + self._element_processor = { + "text": self.process_generic, + "titleText": self.process_title_text, + "copyrightText": self.process_copyright_text, + "standardLicenseHeader": self.process_generic, + "list": self.process_list, + "p": self.process_p, + "br": lambda x: "\n\n", + "item": self.process_list_item, + "bullet": self.process_generic, + "optional": self.process_optional, + "alt": self.process_alt, + } + + self._title: str | bool = True + self._copyright: str | bool = False + self._include_optional: bool = True + self._alt: dict = {} + self._line_len: int = 88 + self._list_item_indent: int = 1 + self._list_item_vertical_spacing: int = 1 + self._current_list_nesting: int = 0 + self._list_indent: int = 4 + self._list_bullet_prefer_default: bool = True + self._list_bullet_ordered: bool = True + self._list_bullet_unordered_char: str = "–" + self._text_wrapper: _TextWrapper | None = None + self._curr_bullet_len: int = 0 + self._alts = [] + return + + def parse( + self, + title: str | bool = True, + copyright: str | bool = False, + include_optional: bool = True, + alt: dict[str, str] | None = None, + line_length: int = 88, + list_item_indent: int = 2, + list_item_vertical_spacing: int = 2, + list_indent: int = 3, + list_bullet_prefer_default: bool = True, + list_bullet_ordered: bool = True, + list_bullet_unordered_char: str = "–", + ) -> tuple[str, str | None]: + """Parses the element and generates the plain-text license. + + Parameters + ---------- + title + Determines how to treat the license title, if any. + Since the title is [optional](https://spdx.github.io/spdx-spec/v3.0.1/annexes/license-matching-guidelines-and-templates/#license-name-or-title) + and not used in matching, it can be omitted or replaced with a custom title. + If True, the title is included as-is. If False, the title is omitted. + If a string, the title is replaced with the custom string, if a title is present. + copyright + Determines how to treat the copyright notice, if any. + Since the copyright notice is [optional](https://spdx.github.io/spdx-spec/v3.0.1/annexes/license-matching-guidelines-and-templates/#copyright-notice) + and not used in matching, it can be omitted or replaced with a custom notice. + If True, the notice is included as-is. If False, the notice is omitted. + If a string, the notice is replaced with the custom string, if a notice is present. + include_optional : bool, optional + Whether to include elements in the output, by default True. + alt : dict[str, int] | None, optional + A dictionary specifying choices for elements. Keys are 'name' attributes, + and values are the value to use. + line_length + The maximum line length for the plain-text output. + list_item_indent + The number of spaces separating list items from the bullet character. + Returns + ------- + The plain-text version of the license, + and the license header text, if present. + """ + self._title = title + self._copyright = copyright + self._include_optional = include_optional + self._alt = alt or {} + self._line_len = line_length + self._text_wrapper = _TextWrapper( + width=line_length, + replace_whitespace=True, + drop_whitespace=True, + break_long_words=False, + break_on_hyphens=False, + ) + self._current_list_nesting = 0 + self._curr_bullet_len = 0 + self._list_indent = list_indent + self._list_item_indent = list_item_indent + self._list_item_vertical_spacing = list_item_vertical_spacing + self._list_bullet_prefer_default = list_bullet_prefer_default + self._list_bullet_ordered = list_bullet_ordered + self._list_bullet_unordered_char = list_bullet_unordered_char + fulltext = self.process_element(self._text).strip("\n").rstrip() + "\n" + header = self._text.find('.//standardLicenseHeader', self._ns) + notice = (self.process_element(header).strip("\n").rstrip() + "\n") if header else None + return fulltext, notice + + def get_processor(self, tag: str) -> callable: + if tag not in self._element_processor: + raise ValueError(f"Unsupported element: {tag}") + return self._element_processor[tag] + + def process_element( + self, + element: ET.Element, + ) -> str: + processor = self.get_processor(self.clean_tag(element.tag)) + return processor(element) + + def process_text(self, text: str) -> str: + text_norm = _re.sub(r'\s+', ' ', text) + if text_norm == " ": + return "" + return self.wrap_text(text_norm) + + def process_generic( + self, + element: ET.Element, + return_list: bool = False, + ) -> str: + """Recursively processes an XML element and its children. + + Parameters + ---------- + element : xml.etree.ElementTree.Element + The XML element to process. + """ + out = [] + if element.text: + out.append(self.process_text(element.text)) + for child in element: + tag_name = self.clean_tag(child.tag) + if tag_name not in self._element_processor: + raise ValueError(f"Unsupported element: {tag_name}") + content = self._element_processor[tag_name](child) + if content: + out.append(content) + if child.tail: + out.append(self.process_text(child.tail)) + if element.tail: + out.append(self.process_text(element.tail)) + if return_list: + return out + # full_raw = "".join([line.rstrip(" ") if line.strip() else "\n" for elem in out for line in elem.splitlines()]) + # paragraphs = [paragraph.strip() for paragraph in _re.split(r'\n\s*\n+', full_raw)] + # processed = [self.wrap_text(paragraph) for paragraph in paragraphs] + # return "\n\n".join(processed) + return _re.sub(r'\n\s*\n\s*\n+', "\n\n", "".join(out)) + + def process_title_text(self, element: ET.Element) -> str: + """Process a element.""" + if self._title is False: + return "" + title = self.process_generic(element) if self._title is True else self._title + title_lines_centered = [line.strip().center(self._line_len) for line in title.splitlines() if line.strip()] + title_centered = "\n".join(title_lines_centered) + return f"{title_centered}\n{'=' * self._line_len}\n\n" + + def process_copyright_text(self, element: ET.Element) -> str: + """Process a element.""" + if self._copyright is False: + return "" + copyright_text = self.process_generic(element) if self._copyright is True else self._copyright + return f"\n\n{copyright_text.strip()}\n\n" + + def process_p(self, element: ET.Element) -> str: + """ + Processes a

element and appends its text to the output. + + Parameters + ---------- + element : xml.etree.ElementTree.Element + The

element. + """ + out = [[]] + if element.text: + out[-1].append(element.text) + for child in element: + tag_name = self.clean_tag(child.tag) + if tag_name != "bullet" and tag_name not in self._element_processor: + raise ValueError(f"Unsupported element: {tag_name}") + if tag_name == "br": + out.append([]) + elif tag_name != "bullet": + content = self._element_processor[tag_name](child) + if content: + out[-1].append(content) + if child.tail: + out[-1].append(child.tail) + if element.tail: + out[-1].append(element.tail) + + paragraphs = [] + for paragraph_components in out: + paragraph_raw = " ".join(paragraph_components) + paragraph_normalized = _re.sub(r'\s+', ' ', paragraph_raw).strip() + paragraphs.append(self.wrap_text(paragraph_normalized)) + return f"\n\n{"\n\n".join(paragraphs)}\n\n" + + def process_list(self, elem: ET.Element) -> str: + """ + Processes a element containing elements. + + Parameters + ---------- + elem : xml.etree.ElementTree.Element + The element. + """ + self._current_list_nesting += 1 + + if elem.text and elem.text.strip(): + raise ValueError("List element should not have text content") + items = [] + for idx, child in enumerate(elem): + tag = self.clean_tag(child.tag) + if tag != 'item': + raise ValueError(f"List element should only contain item elements, not {tag}") + item_str = self.process_list_item(child, idx) + item_str_indented = "\n".join([f"{' ' * self._list_indent}{line}" for line in item_str.splitlines()]) + items.append(item_str_indented) + self._current_list_nesting -= 1 + newlines = max(1, self._list_item_vertical_spacing) * "\n" + list_str = newlines.join(items) + return f"{newlines}{list_str}{newlines}" + + def process_list_item(self, elem: ET.Element, idx: int) -> str: + bullet_elems = elem.findall("./bullet", self._ns) + elem.findall("./p/bullet", self._ns) + if len(bullet_elems) > 1: + raise ValueError("Item element should contain at most one bullet element") + if len(bullet_elems) == 1: + bullet = bullet_elems[0].text.strip() if self._list_bullet_prefer_default else ( + f"{idx + 1}." if self._list_bullet_ordered else self._list_bullet_unordered_char + ) + bullet += self._list_item_indent * " " + subsequent_indent = len(bullet) * " " + else: + bullet = "" + subsequent_indent = "" + self._curr_bullet_len += len(bullet) + content = [] + if elem.text: + text = self.process_text(elem.text).lstrip() + if text: + content.append(text) + for child in elem: + tag = self.clean_tag(child.tag) + if tag != 'bullet': + child_str = self.process_element(child) + if child_str: + content.append(child_str.lstrip(" ")) + if child.tail: + tail = self.process_text(child.tail) + if tail: + needs_dedent = not content or content[-1].endswith("\n") + content.append(tail.lstrip() if needs_dedent else tail) + content_raw = "".join(content).strip() + + lines = content_raw.splitlines() + wrapped = "\n".join( + [f"{bullet}{lines[0] if lines else ""}"] + [f"{subsequent_indent}{line}" for line in lines[1:]] + ) + self._curr_bullet_len -= len(bullet) + return wrapped + + def process_optional(self, element: ET.Element) -> str: + """ + Processes an element based on the include_optional flag. + + Parameters + ---------- + element : xml.etree.ElementTree.Element + The element. + """ + if not self._include_optional: + return "" + return self.process_generic(element) + + def process_alt(self, element: ET.Element) -> str: + """Process an element by selecting the appropriate alternative based on `self._alt`. + + Parameters + ---------- + element : xml.etree.ElementTree.Element + The element. + """ + name = element.get('name') + match = element.get('match') + if not name: + raise ValueError("Alt element must have a 'name' attribute") + if not match: + raise ValueError("Alt element must have a 'match' attribute") + self._alts.append({"name": name, "match": match, "text": element.text}) + text = self._alt.get(name) + if not text: + return element.text + if not _re.match(match, text): + raise ValueError(f"Alt element '{name}' does not match '{match}'") + return text + + def wrap_text(self, text: str) -> str: + """Wrap text to the specified line length, preserving indentation. + + Parameters + ---------- + text : str + The text to wrap. + current_indent : int + The current indentation level. + """ + if self._current_list_nesting: + extra_width = (self._current_list_nesting * self._list_indent) + self._curr_bullet_len + else: + extra_width = 0 + self._text_wrapper.width = self._line_len - extra_width + wrapped = self._text_wrapper.fill(text) + return wrapped + + def clean_tag(self, tag: str) -> str: + """Strip the namespace URI from XML tag. + + Parameters + ---------- + tag + The XML tag with possible namespace. + + Returns + ------- + The tag without namespace. + """ + return tag.removeprefix(f'{{{self._ns_uri}}}') + + +def get_all_licenses() -> dict: + return pl.http.request("https://spdx.org/licenses/licenses.json", response_type="json") \ No newline at end of file