././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1729576273.51959 packageurl_python-0.16.0/0000755000175100001770000000000014705636522014734 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/CHANGELOG.rst0000644000175100001770000002050014705636514016753 0ustar00runnerdockerChangelog ========= 0.16.0 (2024-10-22) ------------------- - Add support for Python 3.13. - Drop support for Python 3.7. https://github.com/package-url/packageurl-python/issues/160 - Improve support for 3 path segments NPM URLs in ``url2purl``. https://github.com/package-url/packageurl-python/issues/167 0.15.6 (2024-07-25) ------------------- - Refine support for GitHub /archive/refs/tags/ URLs in ``url2purl``. The whole tag is now captured as the version. This allows to properly reconstruct valid URLs in ``purl2url``. 0.15.5 (2024-07-24) ------------------- - Capture the whole git tag as the version for GutHub URL in ``url2purl`` instead of adding a version_prefix qualifier. Note that the version_prefix qualifier is still supported in ``purl2url`` for backward compatibility. https://github.com/package-url/packageurl-python/pull/159 0.15.4 (2024-07-15) ------------------- - Add ``with_package_url``, ``without_package_url``, and ``order_by_package_url`` to the ``PackageURLQuerySetMixin`` contrib class. 0.15.3 (2024-07-09) ------------------- - Add support for namespace in ``build_npm_download_url``. 0.15.2 (2024-07-04) ------------------- - Update GitHub generated Download URL for maximum compatibility. https://github.com/package-url/packageurl-python/issues/157 0.15.1 (2024-06-13) ------------------- - Add support for Composer in ``purl2url`` and ``url2purl``. https://github.com/package-url/packageurl-python/pull/144 - Add an option for ``exact_match`` purl QuerySet lookups in the ``PackageURLQuerySetMixin.for_package_url``method. https://github.com/package-url/packageurl-python/issues/118 0.15.0 (2024-03-12) ------------------- - Add support to get PackageURL from ``go_package`` or go module ``name version`` string as seen in a go.mod file. https://github.com/package-url/packageurl-python/pull/148 - Add cran ecosystem support for url2purl https://github.com/package-url/packageurl-python/pull/149 0.14.0 (2024-02-29) ------------------- - Add support for getting golang purl from go import. https://github.com/nexB/purldb/issues/259 - Fix the "gem" type in the README docs. https://github.com/package-url/packageurl-python/pull/114 0.13.4 (2024-01-08) ------------------- - Improve support for SourceForge URLs in `url2purl`. https://github.com/package-url/packageurl-python/issues/139 0.13.3 (2024-01-04) ------------------- - Improve support for SourceForge URLs in `url2purl`. https://github.com/package-url/packageurl-python/issues/139 0.13.2 (2024-01-04) ------------------- - Improve support for SourceForge URLs in `url2purl`. https://github.com/package-url/packageurl-python/issues/139 0.13.1 (2023-12-11) ------------------- - Add support for Python 3.12 https://github.com/package-url/packageurl-python/pull/135 0.13.0 (2023-12-08) ------------------- - Revert changes from https://github.com/package-url/packageurl-python/pull/115/ In above PR we dropped namespaces for a golang purl and stored whole namespace and name in name itself, which was further discussed again and decided we will like to keep namespace back. 0.12.0 (2023-12-08) ------------------- - Modified `PackageURL.from_string` to properly handle golang purls. https://github.com/package-url/packageurl-python/pull/115/ - Improve support for PyPI URLs in `url2purl`. https://github.com/package-url/packageurl-python/pull/128 - Return the "gem" type instead of "rubygems" for "https://rubygems.org/" URLs in `url2purl`. The `pkg:rubygems/` purls are backward-compatible in `purl2url`. https://github.com/package-url/packageurl-python/pull/114/ 0.11.3 (2023-12-08) -------------------- - Add support for GitLab "/archive/" URLs in `url2purl`. https://github.com/package-url/packageurl-python/issues/133 0.11.2 (2022-07-25) -------------------- - Remove deprecated `purl_to_lookups` and `without_empty_values` import compatibility from `packageurl.contrib.django.models`. Replace those functions import using `packageurl.contrib.django.utils`. - Add download purl2url support for bitbucket and gitlab. 0.11.1 (2022-03-24) ------------------- - Add support for the golang type in `purl2url.get_repo_url()` #107 0.11.0rc1 (2022-12-29) ---------------------- - Apply typing - Add support for Python 3.11 - Fix minor typos - Drop Python 3.6 0.10.5rc1 (2022-12-28) ---------------------- - Fixed `PackageURL.from_string` to properly handle npm purls with namespace. 0.10.4 (2022-10-17) ------------------- - Refactor the purl2url functions and utilities #42 - Split purl2url into `get_repo_url()` and `get_download_url()` returning accordingly a "Repository URL" and a "Download URL". - A new `get_inferred_urls` function is available to get return all inferred URLs (repository and download) values. - Add support in purl2url for npm, pypi, hackage, and nuget. - Package URL qualifiers can now be provided to `purl_from_pattern()`. - The `download_url` qualifier is returned in `get_download_url()` when available. - Usage of `purl2url.purl2url` and `purl2url.get_url` is still available for backward compatibility but should be migrated to `purl2url.get_repo_url`. - Include the `version_prefix` ("v" or "V") as a qualifier in build_github_purl #42 This allow to infer valid URLs in the context of purl2url. 0.10.3 (2022-09-15) ------------------- - Fix named arguments in purl_to_lookups. 0.10.2 (2022-09-15) ------------------- - Add encode option in purl_lookups #94 (`purl_to_lookups`, `without_empty_values` is moved from packageurl.contrib.django.models to packageurl.contrib.django.utils) 0.10.1 (2022-08-02) ------------------- - Add ability to filter objects with EMPTY purls in PackageURLFilter #92 0.10.0 (2022-06-27) ------------------- - Upgrade virtualenv.pyz to latest version #85 - Replace Travis CI by GitHub Actions #84 - Add black to the CI and apply formatting on whole codebase #91 - Improve url2purl support for nom URLs - Improve url2purl support for rubygems.org URLs #89 0.9.9 (2022-02-15) ------------------ - Update version to be semver compliant. No changes to the code have been made. 0.9.8.1 (2022-02-11) -------------------- - Fix generic sourceforge PackageURL generation #79 0.9.8 (2022-02-11) ------------------ - Do not create a generic PackageURL for URLs without a path in url2purl #72 - Use project name as the Package name when creating generic sourceforge PackageURLs #74 - Update PyPI route pattern in url2purl to handle different file name formats #76 - Create generic PackageURL for code.google.com archive URLs #78 - Capture more download types for bitbucket URLs 0.9.7 (2022-02-07) ------------------ - Create a generic PackageURL for URLs that do not fit existing routes in url2purl #68 0.9.6 (2021-10-05) ------------------ - Drop support for Python 2 #61 - Add support for new github URLs in url2purl #47 0.9.5 (2021-10-04) ------------------ - Add support for "archive/refs/tags/" github URLs in url2purl #47 0.9.4 (2021-02-02) ------------------ - Fix Python 2 compatibility issue #57 0.9.3 (2020-10-06) ------------------ - Add QuerySet utils to lookup and filter along the PackageURLMixin Django class #48 - Add a PackageURLFilter class for Django FilterSet implementations #48 - Move the django_models module to django.models #48 Replace `packageurl.contrib.django_models` imports with `packageurl.contrib.django.models`. 0.9.2 (2020-09-15) ------------------ - Document usage in README - Adopt SPDX license identifier - Add support for GitHub "raw" URLs in url2purl #43 - Improve GitHub support for "v" prefixed version in url2purl #43 0.9.1 (2020-08-05) ------------------ - Add and improve URL <-> Package URL conversion for gitlab, github, cargo, bitbucket and hackage URL conversions - Add new purl2url conversion utility - Remove the null=True on Django CharField fields of the PackageURLMixin - PackageURL.to_dict() now takes an optional "empty" argument with the value that empty values to have. It defaults to None which was the current behaviour. For some use cases, having an empty string may be a better option and this enables this. 0.9.0 (2020-05-21) ------------------ - Make PackageURL hashable. - Add cargo type or url2purl - Increase the size of the Django model contrib version to 100 chars. - Remove Python 3 idioms (f strings) 0.8.7 (2019-08-15) ------------------ - Add max length validation to the Django model contrib. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/CONTRIBUTING.rst0000644000175100001770000000203314705636514017374 0ustar00runnerdocker============ Contributing ============ Contributions are welcome and appreciated! Every little bit helps, and credit will always be given. When contributing to purl (such as code, bugs, documentation, etc.) you agree to the Developer Certificate of Origin http://developercertificate.org/ and its license (see the mit.LICENSE file). The same approach is used by the Linux Kernel developers and several other projects. To ensure formatting validation on commit, you can setup a pre-commit hook that will automatically run "black" before the code is commited. Copy the "pre-commit.sample" file from the .git/hooks directory to a new file named "pre-commit" and add the following line to this file:: black --line-length 100 . Now rename the file to pre-commit by removing the .sample extension. For commits, it is best to simply add a line like this to your commit message, with your name and email:: Signed-off-by: Jane Doe Please try to write a good commit message. See https://chris.beams.io/posts/git-commit/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/MANIFEST.in0000644000175100001770000000033214705636514016471 0ustar00runnerdockergraft src graft tests include mit.LICENSE include setup.py include setup.cfg include README.rst include Makefile include MANIFEST.in include CHANGELOG.rst include CONTRIBUTING.rst global-exclude *.py[co] __pycache__ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/Makefile0000644000175100001770000000473514705636514016406 0ustar00runnerdocker# Copyright (c) the purl authors # SPDX-License-Identifier: MIT # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Visit https://github.com/package-url/packageurl-python for support and # download. # Python version can be specified with `$ PYTHON_EXE=python3.x make conf` PYTHON_EXE?=python3 ACTIVATE?=. bin/activate; VIRTUALENV_PYZ=thirdparty/virtualenv.pyz BLACK_ARGS=--exclude=".cache|lib|bin|var" --line-length 100 virtualenv: @echo "-> Bootstrap the virtualenv with PYTHON_EXE=${PYTHON_EXE}" @${PYTHON_EXE} ${VIRTUALENV_PYZ} --never-download --no-periodic-update . conf: virtualenv @echo "-> Install dependencies" @${ACTIVATE} pip install -e . dev: virtualenv @echo "-> Configure and install development dependencies" @${ACTIVATE} pip install -e .[test] clean: @echo "-> Clean the Python env" rm -rf bin/ lib*/ include/ build/ dist/ .*cache/ pip-selfcheck.json pyvenv.cfg find . -type f -name '*.py[co]' -delete -o -type d -name __pycache__ \ -delete -type d -name '*.egg-info' -delete isort: @echo "-> Apply isort changes to ensure proper imports ordering" @${ACTIVATE} isort --profile black src/ tests/ black: @echo "-> Apply black code formatter" @${ACTIVATE} black ${BLACK_ARGS} . mypy: @echo "-> Type check the Python code." @${ACTIVATE} mypy valid: @${ACTIVATE} pip install -e .[lint] @$(MAKE) isort @$(MAKE) black @$(MAKE) mypy test: @echo "-> Run the test suite" ${MANAGE} test --noinput bin/py.test tests .PHONY: virtualenv conf dev clean isort black mypy valid test ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1729576273.51959 packageurl_python-0.16.0/PKG-INFO0000644000175100001770000001174114705636522016035 0ustar00runnerdockerMetadata-Version: 2.1 Name: packageurl-python Version: 0.16.0 Summary: A purl aka. Package URL parser and builder Home-page: https://github.com/package-url/packageurl-python Author: the purl authors License: MIT Keywords: package,url,package manager,package url Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities Classifier: Typing :: Typed Requires-Python: >=3.8 Provides-Extra: lint Requires-Dist: isort; extra == "lint" Requires-Dist: black; extra == "lint" Requires-Dist: mypy; extra == "lint" Provides-Extra: test Requires-Dist: pytest; extra == "test" Provides-Extra: build Requires-Dist: setuptools; extra == "build" Requires-Dist: wheel; extra == "build" Provides-Extra: sqlalchemy Requires-Dist: sqlalchemy>=2.0.0; extra == "sqlalchemy" ================= packageurl-python ================= Python library to parse and build "purl" aka. Package URLs. See https://github.com/package-url/purl-spec for details. Join the discussion at https://gitter.im/package-url/Lobby or enter a ticket for support. License: MIT Tests and build status ====================== +----------------------+ | **Tests and build** | +======================+ | |ci-tests| | +----------------------+ Install ======= :: pip install packageurl-python Usage ===== :: >>> from packageurl import PackageURL >>> purl = PackageURL.from_string("pkg:maven/org.apache.commons/io@1.3.4") >>> print(purl.to_dict()) {'type': 'maven', 'namespace': 'org.apache.commons', 'name': 'io', 'version': '1.3.4', 'qualifiers': None, 'subpath': None} >>> print(purl.to_string()) pkg:maven/org.apache.commons/io@1.3.4 >>> print(str(purl)) pkg:maven/org.apache.commons/io@1.3.4 >>> print(repr(purl)) PackageURL(type='maven', namespace='org.apache.commons', name='io', version='1.3.4', qualifiers={}, subpath=None) Utilities ========= Django models ^^^^^^^^^^^^^ `packageurl.contrib.django.models.PackageURLMixin` is a Django abstract model mixin to use Package URLs in Django. SQLAlchemy mixin ^^^^^^^^^^^^^^^^ `packageurl.contrib.sqlalchemy.mixin.PackageURLMixin` is a SQLAlchemy declarative mixin to use Package URLs in SQLAlchemy models. URL to PURL ^^^^^^^^^^^ `packageurl.contrib.url2purl.get_purl(url)` returns a Package URL inferred from an URL. :: >>> from packageurl.contrib import url2purl >>> url2purl.get_purl("https://github.com/package-url/packageurl-python") PackageURL(type='github', namespace='package-url', name='packageurl-python', version=None, qualifiers={}, subpath=None) PURL to URL ^^^^^^^^^^^ - `packageurl.contrib.purl2url.get_repo_url(purl)` returns a repository URL inferred from a Package URL. - `packageurl.contrib.purl2url.get_download_url(purl)` returns a download URL inferred from a Package URL. - `packageurl.contrib.purl2url.get_inferred_urls(purl)` return all inferred URLs (repository, download) from a Package URL. :: >>> from packageurl.contrib import purl2url >>> purl2url.get_repo_url("pkg:gem/bundler@2.3.23") "https://rubygems.org/gems/bundler/versions/2.3.23" >>> purl2url.get_download_url("pkg:gem/bundler@2.3.23") "https://rubygems.org/downloads/bundler-2.3.23.gem" >>> purl2url.get_inferred_urls("pkg:gem/bundler@2.3.23") ["https://rubygems.org/gems/bundler/versions/2.3.23", "https://rubygems.org/downloads/bundler-2.3.23.gem"] Run tests ========= Install test dependencies:: python3 thirdparty/virtualenv.pyz --never-download --no-periodic-update . bin/pip install -e ."[test]" Run tests:: bin/pytest tests Make a new release ================== - Start a new release branch - Update the CHANGELOG.rst, AUTHORS.rst, and README.rst if needed - Bump version in setup.cfg - Run all tests - Install restview and validate that all .rst docs are correct - Commit and push this branch - Make a PR and merge once approved - Tag and push that tag. This triggers the pypi-release.yml workflow that takes care of building the dist release files and upload those to pypi:: VERSION=v0.x.x git tag -a $VERSION -m "Tag $VERSION" git push origin $VERSION - Review the GitHub release created by the workflow at https://github.com/package-url/packageurl-python/releases .. |ci-tests| image:: https://github.com/package-url/packageurl-python/actions/workflows/ci.yml/badge.svg?branch=main :target: https://github.com/package-url/packageurl-python/actions/workflows/ci.yml :alt: CI Tests and build status ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/README.rst0000644000175100001770000000720714705636514016432 0ustar00runnerdocker================= packageurl-python ================= Python library to parse and build "purl" aka. Package URLs. See https://github.com/package-url/purl-spec for details. Join the discussion at https://gitter.im/package-url/Lobby or enter a ticket for support. License: MIT Tests and build status ====================== +----------------------+ | **Tests and build** | +======================+ | |ci-tests| | +----------------------+ Install ======= :: pip install packageurl-python Usage ===== :: >>> from packageurl import PackageURL >>> purl = PackageURL.from_string("pkg:maven/org.apache.commons/io@1.3.4") >>> print(purl.to_dict()) {'type': 'maven', 'namespace': 'org.apache.commons', 'name': 'io', 'version': '1.3.4', 'qualifiers': None, 'subpath': None} >>> print(purl.to_string()) pkg:maven/org.apache.commons/io@1.3.4 >>> print(str(purl)) pkg:maven/org.apache.commons/io@1.3.4 >>> print(repr(purl)) PackageURL(type='maven', namespace='org.apache.commons', name='io', version='1.3.4', qualifiers={}, subpath=None) Utilities ========= Django models ^^^^^^^^^^^^^ `packageurl.contrib.django.models.PackageURLMixin` is a Django abstract model mixin to use Package URLs in Django. SQLAlchemy mixin ^^^^^^^^^^^^^^^^ `packageurl.contrib.sqlalchemy.mixin.PackageURLMixin` is a SQLAlchemy declarative mixin to use Package URLs in SQLAlchemy models. URL to PURL ^^^^^^^^^^^ `packageurl.contrib.url2purl.get_purl(url)` returns a Package URL inferred from an URL. :: >>> from packageurl.contrib import url2purl >>> url2purl.get_purl("https://github.com/package-url/packageurl-python") PackageURL(type='github', namespace='package-url', name='packageurl-python', version=None, qualifiers={}, subpath=None) PURL to URL ^^^^^^^^^^^ - `packageurl.contrib.purl2url.get_repo_url(purl)` returns a repository URL inferred from a Package URL. - `packageurl.contrib.purl2url.get_download_url(purl)` returns a download URL inferred from a Package URL. - `packageurl.contrib.purl2url.get_inferred_urls(purl)` return all inferred URLs (repository, download) from a Package URL. :: >>> from packageurl.contrib import purl2url >>> purl2url.get_repo_url("pkg:gem/bundler@2.3.23") "https://rubygems.org/gems/bundler/versions/2.3.23" >>> purl2url.get_download_url("pkg:gem/bundler@2.3.23") "https://rubygems.org/downloads/bundler-2.3.23.gem" >>> purl2url.get_inferred_urls("pkg:gem/bundler@2.3.23") ["https://rubygems.org/gems/bundler/versions/2.3.23", "https://rubygems.org/downloads/bundler-2.3.23.gem"] Run tests ========= Install test dependencies:: python3 thirdparty/virtualenv.pyz --never-download --no-periodic-update . bin/pip install -e ."[test]" Run tests:: bin/pytest tests Make a new release ================== - Start a new release branch - Update the CHANGELOG.rst, AUTHORS.rst, and README.rst if needed - Bump version in setup.cfg - Run all tests - Install restview and validate that all .rst docs are correct - Commit and push this branch - Make a PR and merge once approved - Tag and push that tag. This triggers the pypi-release.yml workflow that takes care of building the dist release files and upload those to pypi:: VERSION=v0.x.x git tag -a $VERSION -m "Tag $VERSION" git push origin $VERSION - Review the GitHub release created by the workflow at https://github.com/package-url/packageurl-python/releases .. |ci-tests| image:: https://github.com/package-url/packageurl-python/actions/workflows/ci.yml/badge.svg?branch=main :target: https://github.com/package-url/packageurl-python/actions/workflows/ci.yml :alt: CI Tests and build status ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/mit.LICENSE0000644000175100001770000000203614705636514016533 0ustar00runnerdockerCopyright (c) the purl authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729576273.5235898 packageurl_python-0.16.0/setup.cfg0000644000175100001770000000362514705636522016563 0ustar00runnerdocker[metadata] name = packageurl-python version = 0.16.0 license = MIT description = A purl aka. Package URL parser and builder long_description = file:README.rst author = the purl authors url = https://github.com/package-url/packageurl-python classifiers = Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.13 Topic :: Software Development :: Libraries Topic :: Utilities Typing :: Typed keywords = package url package manager package url license_files = - mit.LICENSE - AUTHORS.rst - README.rst - CONTRIBUTING.rst - CHANGELOG.rst [options] python_requires = >=3.8 packages = find: package_dir = =src include_package_data = true zip_safe = false install_requires = [options.packages.find] where = src [options.package_data] packageurl-python = py.typed [options.extras_require] lint = isort black mypy test = pytest build = setuptools wheel sqlalchemy = sqlalchemy >= 2.0.0 [isort] force_single_line = True line_length = 100 known_django = django sections = FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER [mypy] python_version = 3.8 files = src/packageurl/__init__.py show_error_codes = True pretty = True strict = True [tool:pytest] norecursedirs = .git tmp dist build _build local ci docs man share samples .cache .settings Include include Lib lib Scripts thirdparty tmp src/packageurl/contrib python_files = *.py python_classes = Test python_functions = test addopts = -rfExXw --strict-markers --ignore setup.py --doctest-modules [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/setup.py0000644000175100001770000000022614705636514016447 0ustar00runnerdocker#!/usr/bin/env python # -*- encoding: utf-8 -*- # SPDX-License-Identifier: MIT import setuptools if __name__ == "__main__": setuptools.setup() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729576273.5155897 packageurl_python-0.16.0/src/0000755000175100001770000000000014705636522015523 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729576273.5155897 packageurl_python-0.16.0/src/packageurl/0000755000175100001770000000000014705636522017641 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/src/packageurl/__init__.py0000644000175100001770000004171214705636514021760 0ustar00runnerdocker# -*- coding: utf-8 -*- # # Copyright (c) the purl authors # SPDX-License-Identifier: MIT # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Visit https://github.com/package-url/packageurl-python for support and # download. import string from collections import namedtuple from typing import TYPE_CHECKING from typing import Any from typing import AnyStr from typing import Dict from typing import Optional from typing import Tuple from typing import Union from typing import overload from urllib.parse import quote as _percent_quote from urllib.parse import unquote as _percent_unquote from urllib.parse import urlsplit as _urlsplit if TYPE_CHECKING: from collections.abc import Callable from collections.abc import Iterable from typing_extensions import Literal # Python 3 basestring = ( bytes, str, ) # NOQA """ A purl (aka. Package URL) implementation as specified at: https://github.com/package-url/purl-spec """ def quote(s: AnyStr) -> str: """ Return a percent-encoded unicode string, except for colon :, given an `s` byte or unicode string. """ if isinstance(s, str): s_bytes = s.encode("utf-8") else: s_bytes = s quoted = _percent_quote(s_bytes) if not isinstance(quoted, str): quoted = quoted.decode("utf-8") quoted = quoted.replace("%3A", ":") return quoted def unquote(s: AnyStr) -> str: """ Return a percent-decoded unicode string, given an `s` byte or unicode string. """ unquoted = _percent_unquote(s) # type:ignore[arg-type] # typeshed is incorrect here if not isinstance(unquoted, str): unquoted = unquoted.decode("utf-8") return unquoted @overload def get_quoter(encode: bool = True) -> "Callable[[AnyStr], str]": ... @overload def get_quoter(encode: None) -> "Callable[[str], str]": ... def get_quoter( encode: Optional[bool] = True, ) -> "Union[Callable[[AnyStr], str], Callable[[str], str]]": """ Return quoting callable given an `encode` tri-boolean (True, False or None) """ if encode is True: return quote elif encode is False: return unquote elif encode is None: return lambda x: x def normalize_type(type: Optional[AnyStr], encode: Optional[bool] = True) -> Optional[str]: # NOQA if not type: return None if not isinstance(type, str): type_str = type.decode("utf-8") # NOQA else: type_str = type quoter = get_quoter(encode) type_str = quoter(type_str) # NOQA return type_str.strip().lower() or None def normalize_namespace( namespace: Optional[AnyStr], ptype: Optional[str], encode: Optional[bool] = True ) -> Optional[str]: # NOQA if not namespace: return None if not isinstance(namespace, str): namespace_str = namespace.decode("utf-8") else: namespace_str = namespace namespace_str = namespace_str.strip().strip("/") if ptype in ("bitbucket", "github", "pypi", "gitlab"): namespace_str = namespace_str.lower() segments = [seg for seg in namespace_str.split("/") if seg.strip()] segments_quoted = map(get_quoter(encode), segments) return "/".join(segments_quoted) or None def normalize_name( name: Optional[AnyStr], ptype: Optional[str], encode: Optional[bool] = True ) -> Optional[str]: # NOQA if not name: return None if not isinstance(name, str): name_str = name.decode("utf-8") else: name_str = name quoter = get_quoter(encode) name_str = quoter(name_str) name_str = name_str.strip().strip("/") if ptype in ("bitbucket", "github", "pypi", "gitlab"): name_str = name_str.lower() if ptype == "pypi": name_str = name_str.replace("_", "-") return name_str or None def normalize_version( version: Optional[AnyStr], encode: Optional[bool] = True ) -> Optional[str]: # NOQA if not version: return None if not isinstance(version, str): version_str = version.decode("utf-8") else: version_str = version quoter = get_quoter(encode) version_str = quoter(version_str.strip()) return version_str or None @overload def normalize_qualifiers( qualifiers: Union[AnyStr, Dict[str, str], None], encode: "Literal[True]" = ... ) -> Optional[str]: ... @overload def normalize_qualifiers( qualifiers: Union[AnyStr, Dict[str, str], None], encode: "Optional[Literal[False]]" ) -> Optional[Dict[str, str]]: ... @overload def normalize_qualifiers( qualifiers: Union[AnyStr, Dict[str, str], None], encode: Optional[bool] = ... ) -> Union[str, Dict[str, str], None]: ... def normalize_qualifiers( qualifiers: Union[AnyStr, Dict[str, str], None], encode: Optional[bool] = True ) -> Union[str, Dict[str, str], None]: # NOQA """ Return normalized `qualifiers` as a mapping (or as a string if `encode` is True). The `qualifiers` arg is either a mapping or a string. Always return a mapping if decode is True (and never None). Raise ValueError on errors. """ if not qualifiers: return None if encode else dict() if isinstance(qualifiers, basestring): if not isinstance(qualifiers, str): qualifiers_str = qualifiers.decode("utf-8") else: qualifiers_str = qualifiers # decode string to list of tuples qualifiers_list = qualifiers_str.split("&") if not all("=" in kv for kv in qualifiers_list): raise ValueError( f"Invalid qualifier. Must be a string of key=value pairs:{repr(qualifiers_list)}" ) qualifiers_parts = [kv.partition("=") for kv in qualifiers_list] qualifiers_pairs: "Iterable[Tuple[str, str]]" = [(k, v) for k, _, v in qualifiers_parts] elif isinstance(qualifiers, dict): qualifiers_pairs = qualifiers.items() else: raise ValueError(f"Invalid qualifier. Must be a string or dict:{repr(qualifiers)}") quoter = get_quoter(encode) qualifiers_map = { k.strip().lower(): quoter(v) for k, v in qualifiers_pairs if k and k.strip() and v and v.strip() } valid_chars = string.ascii_letters + string.digits + ".-_" for key in qualifiers_map: if not key: raise ValueError("A qualifier key cannot be empty") if "%" in key: raise ValueError(f"A qualifier key cannot be percent encoded: {repr(key)}") if " " in key: raise ValueError(f"A qualifier key cannot contain spaces: {repr(key)}") if not all(c in valid_chars for c in key): raise ValueError( f"A qualifier key must be composed only of ASCII letters and numbers" f"period, dash and underscore: {repr(key)}" ) if key[0] in string.digits: raise ValueError(f"A qualifier key cannot start with a number: {repr(key)}") qualifiers_map = dict(sorted(qualifiers_map.items())) if encode: qualifiers_list = [f"{key}={value}" for key, value in qualifiers_map.items()] qualifiers_str = "&".join(qualifiers_list) return qualifiers_str or None else: return qualifiers_map def normalize_subpath( subpath: Optional[AnyStr], encode: Optional[bool] = True ) -> Optional[str]: # NOQA if not subpath: return None if not isinstance(subpath, str): subpath_str = subpath.decode("utf-8") else: subpath_str = subpath quoter = get_quoter(encode) segments = subpath_str.split("/") segments = [quoter(s) for s in segments if s.strip() and s not in (".", "..")] subpath_str = "/".join(segments) return subpath_str or None @overload def normalize( type: Optional[AnyStr], namespace: Optional[AnyStr], name: Optional[AnyStr], version: Optional[AnyStr], qualifiers: Union[AnyStr, Dict[str, str], None], subpath: Optional[AnyStr], encode: "Literal[True]" = ..., ) -> Tuple[str, Optional[str], str, Optional[str], Optional[str], Optional[str]]: ... @overload def normalize( type: Optional[AnyStr], namespace: Optional[AnyStr], name: Optional[AnyStr], version: Optional[AnyStr], qualifiers: Union[AnyStr, Dict[str, str], None], subpath: Optional[AnyStr], encode: "Optional[Literal[False]]", ) -> Tuple[str, Optional[str], str, Optional[str], Optional[Dict[str, str]], Optional[str]]: ... @overload def normalize( type: Optional[AnyStr], namespace: Optional[AnyStr], name: Optional[AnyStr], version: Optional[AnyStr], qualifiers: Union[AnyStr, Dict[str, str], None], subpath: Optional[AnyStr], encode: Optional[bool] = ..., ) -> Tuple[ str, Optional[str], str, Optional[str], Union[str, Dict[str, str], None], Optional[str] ]: ... def normalize( type: Optional[AnyStr], namespace: Optional[AnyStr], name: Optional[AnyStr], version: Optional[AnyStr], qualifiers: Union[AnyStr, Dict[str, str], None], subpath: Optional[AnyStr], encode: Optional[bool] = True, ) -> Tuple[ Optional[str], Optional[str], Optional[str], Optional[str], Union[str, Dict[str, str], None], Optional[str], ]: # NOQA """ Return normalized purl components """ type_norm = normalize_type(type, encode) # NOQA namespace_norm = normalize_namespace(namespace, type_norm, encode) name_norm = normalize_name(name, type_norm, encode) version_norm = normalize_version(version, encode) qualifiers_norm = normalize_qualifiers(qualifiers, encode) subpath_norm = normalize_subpath(subpath, encode) return type_norm, namespace_norm, name_norm, version_norm, qualifiers_norm, subpath_norm class PackageURL( namedtuple("PackageURL", ("type", "namespace", "name", "version", "qualifiers", "subpath")) ): """ A purl is a package URL as defined at https://github.com/package-url/purl-spec """ name: str namespace: Optional[str] qualifiers: Union[str, Dict[str, str], None] subpath: Optional[str] type: str version: Optional[str] def __new__( self, type: Optional[AnyStr] = None, namespace: Optional[AnyStr] = None, name: Optional[AnyStr] = None, # NOQA version: Optional[AnyStr] = None, qualifiers: Union[AnyStr, Dict[str, str], None] = None, subpath: Optional[AnyStr] = None, ) -> "PackageURL": # this should be 'Self' https://github.com/python/mypy/pull/13133 required = dict(type=type, name=name) for key, value in required.items(): if value: continue raise ValueError(f"Invalid purl: {key} is a required argument.") strings = dict( type=type, namespace=namespace, name=name, version=version, subpath=subpath, ) for key, value in strings.items(): if value and isinstance(value, basestring) or not value: continue raise ValueError(f"Invalid purl: {key} argument must be a string: {repr(value)}.") if qualifiers and not isinstance( qualifiers, ( basestring, dict, ), ): raise ValueError( f"Invalid purl: qualifiers argument must be a dict or a string: {repr(qualifiers)}." ) ( type_norm, namespace_norm, name_norm, version_norm, qualifiers_norm, subpath_norm, ) = normalize( # NOQA type, namespace, name, version, qualifiers, subpath, encode=None ) return super().__new__( PackageURL, type=type_norm, namespace=namespace_norm, name=name_norm, version=version_norm, qualifiers=qualifiers_norm, subpath=subpath_norm, ) def __str__(self, *args: Any, **kwargs: Any) -> str: return self.to_string() def __hash__(self) -> int: return hash(self.to_string()) def to_dict(self, encode: Optional[bool] = False, empty: Any = None) -> Dict[str, Any]: """ Return an ordered dict of purl components as {key: value}. If `encode` is True, then "qualifiers" are encoded as a normalized string. Otherwise, qualifiers is a mapping. You can provide a value for `empty` to be used in place of default None. """ data = self._asdict() if encode: data["qualifiers"] = normalize_qualifiers(self.qualifiers, encode=encode) for field, value in data.items(): data[field] = value or empty return data def to_string(self) -> str: """ Return a purl string built from components. """ type, namespace, name, version, qualifiers, subpath = normalize( # NOQA self.type, self.namespace, self.name, self.version, self.qualifiers, self.subpath, encode=True, ) purl = ["pkg:", type, "/"] if namespace: purl.append(namespace) purl.append("/") purl.append(name) if version: purl.append("@") purl.append(version) if qualifiers: purl.append("?") purl.append(qualifiers) if subpath: purl.append("#") purl.append(subpath) return "".join(purl) @classmethod def from_string(cls, purl: str) -> "PackageURL": """ Return a PackageURL object parsed from a string. Raise ValueError on errors. """ if not purl or not isinstance(purl, str) or not purl.strip(): raise ValueError("A purl string argument is required.") scheme, sep, remainder = purl.partition(":") if not sep or scheme != "pkg": raise ValueError(f'purl is missing the required "pkg" scheme component: {repr(purl)}.') # this strip '/, // and /// as possible in :// or :/// remainder = remainder.strip().lstrip("/") version: Optional[str] # this line is just for type hinting subpath: Optional[str] # this line is just for type hinting type, sep, remainder = remainder.partition("/") # NOQA if not type or not sep: raise ValueError(f"purl is missing the required type component: {repr(purl)}.") type = type.lower() scheme, authority, path, qualifiers_str, subpath = _urlsplit( url=remainder, scheme="", allow_fragments=True ) if scheme or authority: msg = ( f'Invalid purl {repr(purl)} cannot contain a "user:pass@host:port" ' f"URL Authority component: {repr(authority)}." ) raise ValueError(msg) path = path.lstrip("/") namespace: Optional[str] = "" # NPM purl have a namespace in the path # and the namespace in an npm purl is # different from others because it starts with `@` # so we need to handle this case separately if type == "npm" and path.startswith("@"): namespace, sep, path = path.partition("/") remainder, sep, version = path.rpartition("@") if not sep: remainder = version version = None ns_name = remainder.strip().strip("/") ns_name_parts = ns_name.split("/") ns_name_parts = [seg for seg in ns_name_parts if seg and seg.strip()] name = "" if not namespace and len(ns_name_parts) > 1: name = ns_name_parts[-1] ns = ns_name_parts[0:-1] namespace = "/".join(ns) elif len(ns_name_parts) == 1: name = ns_name_parts[0] if not name: raise ValueError(f"purl is missing the required name component: {repr(purl)}") type, namespace, name, version, qualifiers, subpath = normalize( # NOQA type, namespace, name, version, qualifiers_str, subpath, encode=False, ) return PackageURL(type, namespace, name, version, qualifiers, subpath) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729576273.5155897 packageurl_python-0.16.0/src/packageurl/contrib/0000755000175100001770000000000014705636522021301 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/src/packageurl/contrib/__init__.py0000644000175100001770000000000014705636514023401 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1729576273.51959 packageurl_python-0.16.0/src/packageurl/contrib/django/0000755000175100001770000000000014705636522022543 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/src/packageurl/contrib/django/__init__.py0000644000175100001770000000000014705636514024643 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/src/packageurl/contrib/django/filters.py0000644000175100001770000000460614705636514024574 0ustar00runnerdocker# -*- coding: utf-8 -*- # # Copyright (c) the purl authors # SPDX-License-Identifier: MIT # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Visit https://github.com/package-url/packageurl-python for support and # download. import django_filters class PackageURLFilter(django_filters.CharFilter): """ Filter by an exact Package URL string. The special "EMPTY" value allows retrieval of objects with an empty Package URL. This filter depends on `for_package_url` and `empty_package_url` methods to be available on the Model Manager, see for example `PackageURLQuerySetMixin`. When exact_match_only is True, the filter will match only exact Package URL strings. """ is_empty = "EMPTY" exact_match_only = False help_text = ( 'Match Package URL. Use "EMPTY" as value to retrieve objects with empty Package URL.' ) def __init__(self, *args, **kwargs): self.exact_match_only = kwargs.pop("exact_match_only", False) kwargs.setdefault("help_text", self.help_text) super().__init__(*args, **kwargs) def filter(self, qs, value): none_values = ([], (), {}, "", None) if value in none_values: return qs if self.distinct: qs = qs.distinct() if value == self.is_empty: return qs.empty_package_url() return qs.for_package_url(value, exact_match=self.exact_match_only) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/src/packageurl/contrib/django/models.py0000644000175100001770000001345214705636514024406 0ustar00runnerdocker# -*- coding: utf-8 -*- # # Copyright (c) the purl authors # SPDX-License-Identifier: MIT # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Visit https://github.com/package-url/packageurl-python for support and # download. from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import gettext_lazy as _ from packageurl import PackageURL from packageurl.contrib.django.utils import purl_to_lookups PACKAGE_URL_FIELDS = ("type", "namespace", "name", "version", "qualifiers", "subpath") class PackageURLQuerySetMixin: """ Add Package URL filtering methods to a django.db.models.QuerySet. """ def for_package_url(self, purl_str, encode=True, exact_match=False): """ Filter the QuerySet based on a Package URL (purl) string with an option for exact match filtering. When `exact_match` is False (default), the method will match any purl with the same base fields as `purl_str` and allow variations in other fields. When `exact_match` is True, only the identical purl will be returned. """ lookups = purl_to_lookups( purl_str=purl_str, encode=encode, include_empty_fields=exact_match ) if lookups: return self.filter(**lookups) return self.none() def with_package_url(self): """Return objects with Package URL defined.""" return self.filter(~models.Q(type="") & ~models.Q(name="")) def without_package_url(self): """Return objects with empty Package URL.""" return self.filter(models.Q(type="") | models.Q(name="")) def empty_package_url(self): """Return objects with empty Package URL. Alias of without_package_url.""" return self.without_package_url() def order_by_package_url(self): """Order by Package URL fields.""" return self.order_by(*PACKAGE_URL_FIELDS) class PackageURLQuerySet(PackageURLQuerySetMixin, models.QuerySet): pass class PackageURLMixin(models.Model): """ Abstract Model for Package URL "purl" fields support. """ type = models.CharField( max_length=16, blank=True, help_text=_( "A short code to identify the type of this package. " "For example: gem for a Rubygem, docker for a container, " "pypi for a Python Wheel or Egg, maven for a Maven Jar, " "deb for a Debian package, etc." ), ) namespace = models.CharField( max_length=255, blank=True, help_text=_( "Package name prefix, such as Maven groupid, Docker image owner, " "GitHub user or organization, etc." ), ) name = models.CharField( max_length=100, blank=True, help_text=_("Name of the package."), ) version = models.CharField( max_length=100, blank=True, help_text=_("Version of the package."), ) qualifiers = models.CharField( max_length=1024, blank=True, help_text=_( "Extra qualifying data for a package such as the name of an OS, " "architecture, distro, etc." ), ) subpath = models.CharField( max_length=200, blank=True, help_text=_("Extra subpath within a package, relative to the package root."), ) objects = PackageURLQuerySet.as_manager() class Meta: abstract = True @property def package_url(self): """ Return the Package URL "purl" string. """ try: package_url = self.get_package_url() except ValueError: return "" return str(package_url) def get_package_url(self): """ Get the PackageURL instance. """ return PackageURL( self.type, self.namespace, self.name, self.version, self.qualifiers, self.subpath, ) def set_package_url(self, package_url): """ Set each field values to the values of the provided `package_url` string or PackageURL object. Existing values are always overwritten, forcing the new value or an empty string on all the `package_url` fields since we do not want to keep any previous values. """ if not isinstance(package_url, PackageURL): package_url = PackageURL.from_string(package_url) package_url_dict = package_url.to_dict(encode=True, empty="") for field_name, value in package_url_dict.items(): model_field = self._meta.get_field(field_name) if value and len(value) > model_field.max_length: message = _(f'Value too long for field "{field_name}".') raise ValidationError(message) setattr(self, field_name, value) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/src/packageurl/contrib/django/utils.py0000644000175100001770000000476314705636514024270 0ustar00runnerdocker# -*- coding: utf-8 -*- # # Copyright (c) the purl authors # SPDX-License-Identifier: MIT # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Visit https://github.com/package-url/packageurl-python for support and # download. from packageurl import PackageURL def purl_to_lookups(purl_str, encode=True, include_empty_fields=False): """ Return a lookups dictionary built from the provided `purl` (Package URL) string. These lookups can be used as QuerySet filters. If include_empty_fields is provided, the resulting dictionary will include fields with empty values. This is useful to get exact match. Note that empty values are always returned as empty strings as the model fields are defined with `blank=True` and `null=False`. """ if not purl_str.startswith("pkg:"): purl_str = "pkg:" + purl_str try: package_url = PackageURL.from_string(purl_str) except ValueError: return # Not a valid PackageURL package_url_dict = package_url.to_dict(encode=encode, empty="") if include_empty_fields: return package_url_dict else: return without_empty_values(package_url_dict) def without_empty_values(input_dict): """ Return a new dict not including empty value entries from `input_dict`. `None`, empty string, empty list, and empty dict/set are cleaned. `0` and `False` values are kept. """ empty_values = ([], (), {}, "", None) return {key: value for key, value in input_dict.items() if value not in empty_values} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/src/packageurl/contrib/purl2url.py0000644000175100001770000002643314705636514023453 0ustar00runnerdocker# -*- coding: utf-8 -*- # # Copyright (c) the purl authors # SPDX-License-Identifier: MIT # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Visit https://github.com/package-url/packageurl-python for support and # download. from packageurl import PackageURL from packageurl.contrib.route import NoRouteAvailable from packageurl.contrib.route import Router def get_repo_download_url_by_package_type( type, namespace, name, version, archive_extension="tar.gz" ): """ Return the download URL for a hosted git repository given a package type or None. """ if archive_extension not in ("zip", "tar.gz"): raise ValueError("Only zip and tar.gz extensions are supported") download_url_by_type = { "github": f"https://github.com/{namespace}/{name}/archive/{version}.{archive_extension}", "bitbucket": f"https://bitbucket.org/{namespace}/{name}/get/{version}.{archive_extension}", "gitlab": f"https://gitlab.com/{namespace}/{name}/-/archive/{version}/{name}-{version}.{archive_extension}", } return download_url_by_type.get(type) repo_router = Router() download_router = Router() def _get_url_from_router(router, purl): if purl: try: return router.process(purl) except NoRouteAvailable: return def get_repo_url(purl): """ Return a repository URL inferred from the `purl` string. """ return _get_url_from_router(repo_router, purl) def get_download_url(purl): """ Return a download URL inferred from the `purl` string. """ download_url = _get_url_from_router(download_router, purl) if download_url: return download_url # Fallback on the `download_url` qualifier when available. purl_data = PackageURL.from_string(purl) return purl_data.qualifiers.get("download_url", None) def get_inferred_urls(purl): """ Return all inferred URLs (repo, download) from the `purl` string. """ url_functions = ( get_repo_url, get_download_url, ) inferred_urls = [] for url_func in url_functions: url = url_func(purl) if url: inferred_urls.append(url) return inferred_urls # Backward compatibility purl2url = get_repo_url get_url = get_repo_url @repo_router.route("pkg:cargo/.*") def build_cargo_repo_url(purl): """ Return a cargo repo URL from the `purl` string. """ purl_data = PackageURL.from_string(purl) name = purl_data.name version = purl_data.version if name and version: return f"https://crates.io/crates/{name}/{version}" elif name: return f"https://crates.io/crates/{name}" @repo_router.route("pkg:bitbucket/.*") def build_bitbucket_repo_url(purl): """ Return a bitbucket repo URL from the `purl` string. """ purl_data = PackageURL.from_string(purl) namespace = purl_data.namespace name = purl_data.name if name and namespace: return f"https://bitbucket.org/{namespace}/{name}" @repo_router.route("pkg:github/.*") def build_github_repo_url(purl): """ Return a github repo URL from the `purl` string. """ purl_data = PackageURL.from_string(purl) namespace = purl_data.namespace name = purl_data.name version = purl_data.version qualifiers = purl_data.qualifiers if not (name and namespace): return repo_url = f"https://github.com/{namespace}/{name}" if version: version_prefix = qualifiers.get("version_prefix", "") repo_url = f"{repo_url}/tree/{version_prefix}{version}" return repo_url @repo_router.route("pkg:gitlab/.*") def build_gitlab_repo_url(purl): """ Return a gitlab repo URL from the `purl` string. """ purl_data = PackageURL.from_string(purl) namespace = purl_data.namespace name = purl_data.name if name and namespace: return f"https://gitlab.com/{namespace}/{name}" @repo_router.route("pkg:(gem|rubygems)/.*") def build_rubygems_repo_url(purl): """ Return a rubygems repo URL from the `purl` string. """ purl_data = PackageURL.from_string(purl) name = purl_data.name version = purl_data.version if name and version: return f"https://rubygems.org/gems/{name}/versions/{version}" elif name: return f"https://rubygems.org/gems/{name}" @repo_router.route("pkg:cran/.*") def build_cran_repo_url(purl): """ Return a cran repo URL from the `purl` string. """ purl_data = PackageURL.from_string(purl) name = purl_data.name version = purl_data.version return f"https://cran.r-project.org/src/contrib/{name}_{version}.tar.gz" @repo_router.route("pkg:npm/.*") def build_npm_repo_url(purl): """ Return a npm repo URL from the `purl` string. """ purl_data = PackageURL.from_string(purl) namespace = purl_data.namespace name = purl_data.name version = purl_data.version repo_url = "https://www.npmjs.com/package/" if namespace: repo_url += f"{namespace}/" repo_url += f"{name}" if version: repo_url += f"/v/{version}" return repo_url @repo_router.route("pkg:pypi/.*") def build_pypi_repo_url(purl): """ Return a pypi repo URL from the `purl` string. """ purl_data = PackageURL.from_string(purl) name = (purl_data.name or "").replace("_", "-") version = purl_data.version if name and version: return f"https://pypi.org/project/{name}/{version}/" elif name: return f"https://pypi.org/project/{name}/" @repo_router.route("pkg:composer/.*") def build_composer_repo_url(purl): """ Return a composer repo URL from the `purl` string. """ purl_data = PackageURL.from_string(purl) name = purl_data.name version = purl_data.version namespace = purl_data.namespace if name and version: return f"https://packagist.org/packages/{namespace}/{name}#{version}" elif name: return f"https://packagist.org/packages/{namespace}/{name}" @repo_router.route("pkg:nuget/.*") def build_nuget_repo_url(purl): """ Return a nuget repo URL from the `purl` string. """ purl_data = PackageURL.from_string(purl) name = purl_data.name version = purl_data.version if name and version: return f"https://www.nuget.org/packages/{name}/{version}" elif name: return f"https://www.nuget.org/packages/{name}" @repo_router.route("pkg:hackage/.*") def build_hackage_repo_url(purl): """ Return a hackage repo URL from the `purl` string. """ purl_data = PackageURL.from_string(purl) name = purl_data.name version = purl_data.version if name and version: return f"https://hackage.haskell.org/package/{name}-{version}" elif name: return f"https://hackage.haskell.org/package/{name}" @repo_router.route("pkg:golang/.*") def build_golang_repo_url(purl): """ Return a golang repo URL from the `purl` string. """ purl_data = PackageURL.from_string(purl) namespace = purl_data.namespace name = purl_data.name version = purl_data.version if name and version: return f"https://pkg.go.dev/{namespace}/{name}@{version}" elif name: return f"https://pkg.go.dev/{namespace}/{name}" @repo_router.route("pkg:cocoapods/.*") def build_cocoapods_repo_url(purl): """ Return a CocoaPods repo URL from the `purl` string. """ purl_data = PackageURL.from_string(purl) name = purl_data.name return name and f"https://cocoapods.org/pods/{name}" # Download URLs: @download_router.route("pkg:cargo/.*") def build_cargo_download_url(purl): """ Return a cargo download URL from the `purl` string. """ purl_data = PackageURL.from_string(purl) name = purl_data.name version = purl_data.version if name and version: return f"https://crates.io/api/v1/crates/{name}/{version}/download" @download_router.route("pkg:(gem|rubygems)/.*") def build_rubygems_download_url(purl): """ Return a rubygems download URL from the `purl` string. """ purl_data = PackageURL.from_string(purl) name = purl_data.name version = purl_data.version if name and version: return f"https://rubygems.org/downloads/{name}-{version}.gem" @download_router.route("pkg:npm/.*") def build_npm_download_url(purl): """ Return a npm download URL from the `purl` string. """ purl_data = PackageURL.from_string(purl) namespace = purl_data.namespace name = purl_data.name version = purl_data.version base_url = "https://registry.npmjs.org" if namespace: base_url += f"/{namespace}" if name and version: return f"{base_url}/{name}/-/{name}-{version}.tgz" @download_router.route("pkg:hackage/.*") def build_hackage_download_url(purl): """ Return a hackage download URL from the `purl` string. """ purl_data = PackageURL.from_string(purl) name = purl_data.name version = purl_data.version if name and version: return f"https://hackage.haskell.org/package/{name}-{version}/{name}-{version}.tar.gz" @download_router.route("pkg:nuget/.*") def build_nuget_download_url(purl): """ Return a nuget download URL from the `purl` string. """ purl_data = PackageURL.from_string(purl) name = purl_data.name version = purl_data.version if name and version: return f"https://www.nuget.org/api/v2/package/{name}/{version}" @download_router.route("pkg:gitlab/.*", "pkg:bitbucket/.*", "pkg:github/.*") def build_repo_download_url(purl): """ Return a gitlab download URL from the `purl` string. """ return get_repo_download_url(purl) def get_repo_download_url(purl): """ Return ``download_url`` if present in ``purl`` qualifiers or if ``namespace``, ``name`` and ``version`` are present in ``purl`` else return None. """ purl_data = PackageURL.from_string(purl) namespace = purl_data.namespace type = purl_data.type name = purl_data.name version = purl_data.version qualifiers = purl_data.qualifiers download_url = qualifiers.get("download_url") if download_url: return download_url if not (namespace and name and version): return version_prefix = qualifiers.get("version_prefix", "") version = f"{version_prefix}{version}" return get_repo_download_url_by_package_type( type=type, namespace=namespace, name=name, version=version ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/src/packageurl/contrib/route.py0000644000175100001770000001746714705636514023031 0ustar00runnerdocker# -*- coding: utf-8 -*- # # Copyright (c) the purl authors # SPDX-License-Identifier: MIT # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Visit https://github.com/package-url/packageurl-python for support and # download. import inspect import re from functools import wraps """ Given a URI regex (or some string), this module can route execution to a callable. There are several routing implementations available in Rails, Django, Flask, Paste, etc. However, these all assume that the routed processing is to craft a response to an incoming external HTTP request. Here we are instead doing the opposite: given a URI (and no request yet) we are routing the processing to emit a request externally (HTTP or other protocol) and handling its response. Also we crawl a lot and not only HTTP: git, svn, ftp, rsync and more. This simple library support this kind of arbitrary URI routing. This is inspired by Guido's http://www.artima.com/weblogs/viewpost.jsp?thread=101605 and Django, Flask, Werkzeug and other url dispatch and routing design from web frameworks. https://github.com/douban/brownant has a similar approach, using Werkzeug with the limitation that it does not route based on URI scheme and is limited to HTTP. """ class Rule(object): """ A rule is a mapping between a pattern (typically a URI) and a callable (typically a function). The pattern is a regex string pattern and must match entirely a string (typically a URI) for the rule to be considered, i.e. for the endpoint to be resolved and eventually invoked for a given string (typically a URI). """ def __init__(self, pattern, endpoint): # To ensure the pattern will match entirely, we wrap the pattern # with start of line ^ and end of line $. self.pattern = pattern.lstrip("^").rstrip("$") self.pattern_match = re.compile("^" + self.pattern + "$").match # ensure the endpoint is callable assert callable(endpoint) # classes are not always callable, make an extra check if inspect.isclass(endpoint): obj = endpoint() assert callable(obj) self.endpoint = endpoint def __repr__(self): return f'Rule(r"""{self.pattern}""", {self.endpoint.__module__}.{self.endpoint.__name__})' def match(self, string): """ Match a string with the rule pattern, return True is matching. """ return self.pattern_match(string) class RouteAlreadyDefined(TypeError): """ Raised when this route Rule already exists in the route map. """ class NoRouteAvailable(TypeError): """ Raised when there are no route available. """ class MultipleRoutesDefined(TypeError): """ Raised when there are more than one route possible. """ class Router(object): """ A router is: - a container for a route map, consisting of several rules, stored in an ordered dictionary keyed by pattern text - a way to process a route, i.e. given a string (typically a URI), find the correct rule and invoke its callable endpoint - and a convenience decorator for routed callables (either a function or something with a __call__ method) Multiple routers can co-exist as needed, such as a router to collect, another to fetch, etc. """ def __init__(self, route_map=None): """ 'route_map' is an ordered mapping of pattern -> Rule. """ self.route_map = route_map or dict() # lazy cached pre-compiled regex match() for all route patterns self._is_routable = None def __repr__(self): return repr(self.route_map) def __iter__(self): return iter(self.route_map.items()) def keys(self): return self.route_map.keys() def append(self, pattern, endpoint): """ Append a new pattern and endpoint Rule at the end of the map. Use this as an alternative to the route decorator. """ if pattern in self.route_map: raise RouteAlreadyDefined(pattern) self.route_map[pattern] = Rule(pattern, endpoint) def route(self, *patterns): """ Decorator to make a callable 'endpoint' routed to one or more patterns. Example: >>> my_router = Router() >>> @my_router.route('http://nexb.com', 'http://deja.com') ... def somefunc(uri): ... pass """ def decorator(endpoint): assert patterns for pat in patterns: self.append(pat, endpoint) @wraps(endpoint) def decorated(*args, **kwargs): return self.process(*args, **kwargs) return decorated return decorator def process(self, string, *args, **kwargs): """ Given a string (typically a URI), resolve this string to an endpoint by searching available rules then execute the endpoint callable for that string passing down all arguments to the endpoint invocation. """ endpoint = self.resolve(string) if inspect.isclass(endpoint): # instantiate a class, that must define a __call__ method # TODO: consider passing args to the constructor? endpoint = endpoint() # call the callable return endpoint(string, *args, **kwargs) def resolve(self, string): """ Resolve a string: given a string (typically a URI) resolve and return the best endpoint function for that string. Ambiguous resolution is not allowed in order to keep things in check when there are hundreds rules: if multiple routes are possible for a string (typically a URI), a MultipleRoutesDefined TypeError is raised. """ # TODO: we could improve the performance of this by using a single # regex and named groups if this ever becomes a bottleneck. candidates = [r for r in self.route_map.values() if r.match(string)] if not candidates: raise NoRouteAvailable(string) if len(candidates) > 1: # this can happen when multiple patterns match the same string # we raise an exception with enough debugging information pats = repr([r.pattern for r in candidates]) msg = "%(string)r matches multiple patterns %(pats)r" % locals() raise MultipleRoutesDefined(msg) return candidates[0].endpoint def is_routable(self, string): """ Return True if `string` is routable by this router, e.g. if it matches any of the route patterns. """ if not string: return if not self._is_routable: # build an alternation regex routables = "^(" + "|".join(pat for pat in self.route_map) + ")$" self._is_routable = re.compile(routables, re.UNICODE).match return bool(self._is_routable(string)) ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1729576273.51959 packageurl_python-0.16.0/src/packageurl/contrib/sqlalchemy/0000755000175100001770000000000014705636522023443 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/src/packageurl/contrib/sqlalchemy/mixin.py0000644000175100001770000000766714705636514025162 0ustar00runnerdocker# -*- coding: utf-8 -*- # # Copyright (c) the purl authors # SPDX-License-Identifier: MIT # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Visit https://github.com/package-url/packageurl-python for support and # download. from sqlalchemy import String from sqlalchemy.orm import Mapped from sqlalchemy.orm import declarative_mixin from sqlalchemy.orm import mapped_column from packageurl import PackageURL @declarative_mixin class PackageURLMixin: """ SQLAlchemy declarative mixin class for Package URL "purl" fields support. """ type: Mapped[str] = mapped_column( String(16), nullable=False, comment=( "A short code to identify the type of this package. " "For example: gem for a Rubygem, docker for a container, " "pypi for a Python Wheel or Egg, maven for a Maven Jar, " "deb for a Debian package, etc." ), ) namespace: Mapped[str] = mapped_column( String(255), nullable=True, comment=( "Package name prefix, such as Maven groupid, Docker image owner, " "GitHub user or organization, etc." ), ) name: Mapped[str] = mapped_column(String(100), nullable=False, comment="Name of the package.") version: Mapped[str] = mapped_column( String(100), nullable=True, comment="Version of the package." ) qualifiers: Mapped[str] = mapped_column( String(1024), nullable=True, comment=( "Extra qualifying data for a package such as the name of an OS, " "architecture, distro, etc." ), ) subpath: Mapped[str] = mapped_column( String(200), nullable=True, comment="Extra subpath within a package, relative to the package root.", ) @property def package_url(self) -> str: """ Return the Package URL "purl" string. Returns ------- str """ try: package_url = self.get_package_url() except ValueError: return "" return str(package_url) def get_package_url(self) -> PackageURL: """ Get the PackageURL instance. Returns ------- PackageURL """ return PackageURL( self.type, self.namespace, self.name, self.version, self.qualifiers, self.subpath, ) def set_package_url(self, package_url: PackageURL) -> None: """ Set or update the PackageURL object attributes. Parameters ---------- package_url: PackageURL The PackageURL object to set get attributes from. """ if not isinstance(package_url, PackageURL): package_url = PackageURL.from_string(package_url) package_url_dict = package_url.to_dict(encode=True, empty="") for key, value in package_url_dict.items(): setattr(self, key, value) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/src/packageurl/contrib/url2purl.py0000644000175100001770000005753014705636514023455 0ustar00runnerdocker# -*- coding: utf-8 -*- # # Copyright (c) the purl authors # SPDX-License-Identifier: MIT # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Visit https://github.com/package-url/packageurl-python for support and # download. import os import re from urllib.parse import unquote_plus from urllib.parse import urlparse from packageurl import PackageURL from packageurl.contrib.route import NoRouteAvailable from packageurl.contrib.route import Router """ This module helps build a PackageURL from an arbitrary URL. This uses the a routing mechanism available in the route.py module. In order to make it easy to use, it contains all the conversion functions in this single Python script. """ purl_router = Router() def url2purl(url): """ Return a PackageURL inferred from the `url` string or None. """ if url: try: return purl_router.process(url) except NoRouteAvailable: # If `url` does not fit in one of the existing routes, # we attempt to create a generic PackageURL for `url` return build_generic_purl(url) get_purl = url2purl def purl_from_pattern(type_, pattern, url, qualifiers=None): url = unquote_plus(url) compiled_pattern = re.compile(pattern, re.VERBOSE) match = compiled_pattern.match(url) if not match: return purl_data = { field: value for field, value in match.groupdict().items() if field in PackageURL._fields } qualifiers = qualifiers or {} # Include the `version_prefix` as a qualifier to infer valid URLs in purl2url version_prefix = match.groupdict().get("version_prefix") if version_prefix: qualifiers.update({"version_prefix": version_prefix}) if qualifiers: if "qualifiers" in purl_data: purl_data["qualifiers"].update(qualifiers) else: purl_data["qualifiers"] = qualifiers return PackageURL(type_, **purl_data) def register_pattern(type_, pattern, router=purl_router): """ Register a pattern with its type. """ def endpoint(url): return purl_from_pattern(type_, pattern, url) router.append(pattern, endpoint) def get_path_segments(url): """ Return a list of path segments from a `url` string. """ path = unquote_plus(urlparse(url).path) segments = [seg for seg in path.split("/") if seg] return segments def build_generic_purl(uri): """ Return a PackageURL from `uri`, if `uri` is a parsable URL, or None `uri` is assumed to be a download URL, e.g. https://example.com/example.tar.gz """ parsed_uri = urlparse(uri) if parsed_uri.scheme and parsed_uri.netloc and parsed_uri.path: # Get file name from `uri` uri_path_segments = get_path_segments(uri) if uri_path_segments: file_name = uri_path_segments[-1] return PackageURL(type="generic", name=file_name, qualifiers={"download_url": uri}) @purl_router.route( "https?://registry.npmjs.*/.*", "https?://registry.yarnpkg.com/.*", "https?://(www\\.)?npmjs.*/package.*", "https?://(www\\.)?yarnpkg.com/package.*", ) def build_npm_purl(uri): # npm URLs are difficult to disambiguate with regex if "/package/" in uri: return build_npm_web_purl(uri) elif "/-/" in uri: return build_npm_download_purl(uri) else: return build_npm_api_purl(uri) def build_npm_api_purl(uri): path = unquote_plus(urlparse(uri).path) segments = [seg for seg in path.split("/") if seg] if len(segments) < 2: return # /@esbuild/freebsd-arm64/0.21.5 if len(segments) == 3: return PackageURL("npm", namespace=segments[0], name=segments[1], version=segments[2]) # /@invisionag/eslint-config-ivx if segments[0].startswith("@"): return PackageURL("npm", namespace=segments[0], name=segments[1]) # /angular/1.6.6 return PackageURL("npm", name=segments[0], version=segments[1]) def build_npm_download_purl(uri): path = unquote_plus(urlparse(uri).path) segments = [seg for seg in path.split("/") if seg and seg != "-"] len_segments = len(segments) # /@invisionag/eslint-config-ivx/-/eslint-config-ivx-0.0.2.tgz if len_segments == 3: namespace, name, filename = segments # /automatta/-/automatta-0.0.1.tgz elif len_segments == 2: namespace = None name, filename = segments else: return base_filename, ext = os.path.splitext(filename) version = base_filename.replace(name, "") if version.startswith("-"): version = version[1:] # Removes the "-" prefix return PackageURL("npm", namespace, name, version) def build_npm_web_purl(uri): path = unquote_plus(urlparse(uri).path) if path.startswith("/package/"): path = path[9:] segments = [seg for seg in path.split("/") if seg] len_segments = len(segments) namespace = version = None # @angular/cli/v/10.1.2 if len_segments == 4: namespace = segments[0] name = segments[1] version = segments[3] # express/v/4.17.1 elif len_segments == 3: namespace = None name = segments[0] version = segments[2] # @angular/cli elif len_segments == 2: namespace = segments[0] name = segments[1] # express elif len_segments == 1 and len(segments) > 0 and segments[0][0] != "@": name = segments[0] else: return return PackageURL("npm", namespace, name, version) @purl_router.route( "https?://repo1.maven.org/maven2/.*", "https?://central.maven.org/maven2/.*", "maven-index://repo1.maven.org/.*", ) def build_maven_purl(uri): path = unquote_plus(urlparse(uri).path) segments = [seg for seg in path.split("/") if seg and seg != "maven2"] if len(segments) < 3: return before_last_segment, last_segment = segments[-2:] has_filename = before_last_segment in last_segment filename = None if has_filename: filename = segments.pop() version = segments[-1] name = segments[-2] namespace = ".".join(segments[:-2]) qualifiers = {} if filename: name_version = f"{name}-{version}" _, _, classifier_ext = filename.rpartition(name_version) classifier, _, extension = classifier_ext.partition(".") if not extension: return qualifiers["classifier"] = classifier.strip("-") valid_types = ("aar", "ear", "mar", "pom", "rar", "rpm", "sar", "tar.gz", "war", "zip") if extension in valid_types: qualifiers["type"] = extension return PackageURL("maven", namespace, name, version, qualifiers) # https://rubygems.org/gems/i18n-js-3.0.11.gem @purl_router.route("https?://rubygems.org/(downloads|gems)/.*") def build_rubygems_purl(uri): # We use a more general route pattern instead of using `rubygems_pattern` # below by itself because we want to capture all rubygems download URLs, # even the ones that are not completely formed. This helps prevent url2purl # from attempting to create a generic PackageURL from an invalid rubygems # download URL. # https://rubygems.org/downloads/jwt-0.1.8.gem # https://rubygems.org/gems/i18n-js-3.0.11.gem rubygems_pattern = ( r"^https?://rubygems.org/(downloads|gems)/(?P.+)-(?P.+)(\.gem)$" ) return purl_from_pattern("gem", rubygems_pattern, uri) # https://cran.r-project.org/src/contrib/jsonlite_1.8.8.tar.gz # https://packagemanager.rstudio.com/cran/2022-06-23/src/contrib/curl_4.3.2.tar.gz" @purl_router.route( "https?://cran.r-project.org/.*", "https?://packagemanager.rstudio.com/cran/.*", ) def build_cran_purl(uri): cran_pattern = r"^https?://(cran\.r-project\.org|packagemanager\.rstudio\.com/cran)/.*?src/contrib/(?P.+)_(?P.+)\.tar.gz$" qualifiers = {} if "//cran.r-project.org/" not in uri: qualifiers["download_url"] = uri return purl_from_pattern("cran", cran_pattern, uri, qualifiers) # https://pypi.org/packages/source/a/anyjson/anyjson-0.3.3.tar.gz # https://pypi.python.org/packages/source/a/anyjson/anyjson-0.3.3.tar.gz # https://pypi.python.org/packages/2.6/t/threadpool/threadpool-1.2.7-py2.6.egg # https://pypi.python.org/packages/any/s/setuptools/setuptools-0.6c11-1.src.rpm # https://files.pythonhosted.org/packages/84/d8/451842a5496844bb5c7634b231a2e4caf0d867d2e25f09b840d3b07f3d4b/multi_key_dict-2.0.win32.exe pypi_pattern = r"(?P(\w\.?)+(-\w+)*)-(?P.+)\.(zip|tar.gz|tar.bz2|tgz|egg|rpm|exe)$" # This pattern can be found in the following locations: # - wheel.wheelfile.WHEEL_INFO_RE # - distlib.wheel.FILENAME_RE # - setuptools.wheel.WHEEL_NAME # - pip._internal.wheel.Wheel.wheel_file_re wheel_file_re = re.compile( r"^(?P(?P.+?)-(?P.*?))" r"((-(?P\d[^-]*?))?-(?P.+?)-(?P.+?)-(?P.+?)" r"\.whl)$", re.VERBOSE, ) @purl_router.route( "https?://pypi.org/(packages|project)/.+", "https?://.+python.+org/(packages|project)/.*", ) def build_pypi_purl(uri): path = unquote_plus(urlparse(uri).path) segments = path.split("/") last_segment = segments[-1] # /wheel-0.29.0-py2.py3-none-any.whl if last_segment.endswith(".whl"): match = wheel_file_re.match(last_segment) if match: return PackageURL( "pypi", name=match.group("name"), version=match.group("version"), ) if segments[1] == "project": return PackageURL( "pypi", name=segments[2], version=segments[3] if len(segments) > 3 else None, ) return purl_from_pattern("pypi", pypi_pattern, last_segment) # https://packagist.org/packages/webmozart/assert#1.9.1 @purl_router.route("https?://packagist.org/packages/.*") def build_composer_purl(uri): # We use a more general route pattern instead of using `composer_pattern` # below by itself because we want to capture all packagist download URLs, # even the ones that are not completely formed. This helps prevent url2purl # from attempting to create a generic PackageURL from an invalid packagist # download URL. # https://packagist.org/packages/ralouphie/getallheaders # https://packagist.org/packages/symfony/process#v7.0.0-BETA3 composer_pattern = r"^https?://packagist\.org/packages/(?P[^/]+)/(?P[^\#]+?)(\#(?P.+))?$" return purl_from_pattern("composer", composer_pattern, uri) # http://nuget.org/packages/EntityFramework/4.2.0.0 # https://www.nuget.org/api/v2/package/Newtonsoft.Json/11.0.1 nuget_www_pattern = r"^https?://.*nuget.org/(api/v2/)?packages?/(?P.+)/(?P.+)$" register_pattern("nuget", nuget_www_pattern) # https://api.nuget.org/v3-flatcontainer/newtonsoft.json/10.0.1/newtonsoft.json.10.0.1.nupkg nuget_api_pattern = ( r"^https?://api.nuget.org/v3-flatcontainer/" r"(?P.+)/" r"(?P.+)/" r".*(nupkg)$" # ends with "nupkg" ) register_pattern("nuget", nuget_api_pattern) # https://sourceforge.net/projects/turbovnc/files/3.1/turbovnc-3.1.tar.gz/download # https://sourceforge.net/projects/scribus/files/scribus/1.6.0/scribus-1.6.0.tar.gz/download # https://sourceforge.net/projects/ventoy/files/v1.0.96/Ventoy%201.0.96%20release%20source%20code.tar.gz/download # https://sourceforge.net/projects/geoserver/files/GeoServer/2.23.4/geoserver-2.23.4-war.zip/download sourceforge_download_pattern = ( r"^https?://.*sourceforge.net/projects/" r"(?P.+)/" r"files/" r"(?i:(?P=name)/)?" # optional case-insensitive name segment repeated r"v?(?P[0-9\.]+)/" # version restricted to digits and dots r"(?i:(?P=name)).*(?P=version).*" # case-insensitive matching for {name}-{version} r"(/download)$" # ending with "/download" ) register_pattern("sourceforge", sourceforge_download_pattern) # https://sourceforge.net/projects/spacesniffer/files/spacesniffer_1_3_0_2.zip/download sourceforge_download_pattern_bis = ( r"^https?://.*sourceforge.net/projects/" r"(?P.+)/" r"files/" r"(?i:(?P=name))_*(?P[0-9_]+).*" r"(/download)$" # ending with "/download" ) register_pattern("sourceforge", sourceforge_download_pattern_bis) @purl_router.route("https?://.*sourceforge.net/project/.*") def build_sourceforge_purl(uri): # We use a more general route pattern instead of using `sourceforge_pattern` # below by itself because we want to capture all sourceforge download URLs, # even the ones that do not fit `sourceforge_pattern`. This helps prevent # url2purl from attempting to create a generic PackageURL from a sourceforge # URL that we can't handle. # http://master.dl.sourceforge.net/project/libpng/zlib/1.2.3/zlib-1.2.3.tar.bz2 sourceforge_pattern = ( r"^https?://.*sourceforge.net/projects?/" r"(?P([^/]+))/" # do not allow more "/" segments r"(OldFiles/)?" r"(?P.+)/" r"(?P[v0-9\.]+)/" # version restricted to digits and dots r"(?P=name).*(?P=version).*" # {name}-{version} repeated in the filename r"[^/]$" # not ending with "/" ) sourceforge_purl = purl_from_pattern("sourceforge", sourceforge_pattern, uri) if not sourceforge_purl: # Get the project name from `uri` and use that as the Package name # http://master.dl.sourceforge.net/project/aloyscore/aloyscore/0.1a1%2520stable/0.1a1_stable_AloysCore.zip split_uri = uri.split("/project/") # http://master.dl.sourceforge.net, aloyscore/aloyscore/0.1a1%2520stable/0.1a1_stable_AloysCore.zip if len(split_uri) >= 2: # aloyscore/aloyscore/0.1a1%2520stable/0.1a1_stable_AloysCore.zip remaining_uri_path = split_uri[1] # aloyscore, aloyscore, 0.1a1%2520stable, 0.1a1_stable_AloysCore.zip remaining_uri_path_segments = remaining_uri_path.split("/") if remaining_uri_path_segments: project_name = remaining_uri_path_segments[0] # aloyscore sourceforge_purl = PackageURL( type="sourceforge", name=project_name, qualifiers={"download_url": uri} ) return sourceforge_purl # https://crates.io/api/v1/crates/rand/0.7.2/download cargo_pattern = r"^https?://crates.io/api/v1/crates/(?P.+)/(?P.+)(\/download)$" register_pattern("cargo", cargo_pattern) # https://raw.githubusercontent.com/volatilityfoundation/dwarf2json/master/LICENSE.txt github_raw_content_pattern = ( r"https?://raw.githubusercontent.com/(?P[^/]+)/(?P[^/]+)/" r"(?P[^/]+)/(?P.*)$" ) register_pattern("github", github_raw_content_pattern) @purl_router.route("https?://api.github\\.com/repos/.*") def build_github_api_purl(url): """ Return a PackageURL object from GitHub API `url`. For example: https://api.github.com/repos/nexB/scancode-toolkit/commits/40593af0df6c8378d2b180324b97cb439fa11d66 https://api.github.com/repos/nexB/scancode-toolkit/ and returns a `PackageURL` object """ segments = get_path_segments(url) if not (len(segments) >= 3): return namespace = segments[1] name = segments[2] version = None # https://api.github.com/repos/nexB/scancode-toolkit/ if len(segments) == 4 and segments[3] != "commits": version = segments[3] # https://api.github.com/repos/nexB/scancode-toolkit/commits/40593af0df6c8378d2b180324b97cb439fa11d66 if len(segments) == 5 and segments[3] == "commits": version = segments[4] return PackageURL(type="github", namespace=namespace, name=name, version=version) # https://codeload.github.com/nexB/scancode-toolkit/tar.gz/v3.1.1 # https://codeload.github.com/berngp/grails-rest/zip/release/0.7 github_codeload_pattern = ( r"https?://codeload.github.com/(?P.+)/(?P.+)/" r"(zip|tar.gz|tar.bz2|tgz)/(.*/)*" r"(?P.+)$" ) register_pattern("github", github_codeload_pattern) @purl_router.route("https?://github\\.com/.*") def build_github_purl(url): """ Return a PackageURL object from GitHub `url`. """ # https://github.com/apache/nifi/archive/refs/tags/rel/nifi-2.0.0-M3.tar.gz archive_tags_pattern = ( r"https?://github.com/(?P.+)/(?P.+)" r"/archive/refs/tags/" r"(?P.+).(zip|tar.gz|tar.bz2|.tgz)" ) # https://github.com/nexB/scancode-toolkit/archive/v3.1.1.zip archive_pattern = ( r"https?://github.com/(?P.+)/(?P.+)" r"/archive/(.*/)*" r"((?P=name)(-|_|@))?" r"(?P.+).(zip|tar.gz|tar.bz2|.tgz)" ) # https://github.com/downloads/mozilla/rhino/rhino1_7R4.zip download_pattern = ( r"https?://github.com/downloads/(?P.+)/(?P.+)/" r"((?P=name)(-|@)?)?" r"(?P.+).(zip|tar.gz|tar.bz2|.tgz)" ) # https://github.com/pypa/get-virtualenv/raw/20.0.31/public/virtualenv.pyz raw_pattern = ( r"https?://github.com/(?P.+)/(?P.+)" r"/raw/(?P[^/]+)/(?P.*)$" ) # https://github.com/fanf2/unifdef/blob/master/unifdef.c blob_pattern = ( r"https?://github.com/(?P.+)/(?P.+)" r"/blob/(?P[^/]+)/(?P.*)$" ) releases_download_pattern = ( r"https?://github.com/(?P.+)/(?P.+)" r"/releases/download/(?P[^/]+)/.*$" ) # https://github.com/pombredanne/schematics.git git_pattern = r"https?://github.com/(?P.+)/(?P.+).(git)" patterns = ( archive_tags_pattern, archive_pattern, raw_pattern, blob_pattern, releases_download_pattern, download_pattern, git_pattern, ) for pattern in patterns: matches = re.search(pattern, url) qualifiers = {} if matches: if pattern == releases_download_pattern: qualifiers["download_url"] = url return purl_from_pattern( type_="github", pattern=pattern, url=url, qualifiers=qualifiers ) segments = get_path_segments(url) if not len(segments) >= 2: return namespace = segments[0] name = segments[1] version = None subpath = None # https://github.com/TG1999/fetchcode/master if len(segments) >= 3 and segments[2] != "tree": version = segments[2] subpath = "/".join(segments[3:]) # https://github.com/TG1999/fetchcode/tree/master if len(segments) >= 4 and segments[2] == "tree": version = segments[3] subpath = "/".join(segments[4:]) return PackageURL( type="github", namespace=namespace, name=name, version=version, subpath=subpath, ) @purl_router.route("https?://bitbucket\\.org/.*") def build_bitbucket_purl(url): """ Return a PackageURL object from BitBucket `url`. For example: https://bitbucket.org/TG1999/first_repo/src/master or https://bitbucket.org/TG1999/first_repo/src or https://bitbucket.org/TG1999/first_repo/src/master/new_folder """ segments = get_path_segments(url) if not len(segments) >= 2: return namespace = segments[0] name = segments[1] bitbucket_download_pattern = ( r"https?://bitbucket.org/" r"(?P.+)/(?P.+)/downloads/" r"(?P.+).(zip|tar.gz|tar.bz2|.tgz|exe|msi)" ) matches = re.search(bitbucket_download_pattern, url) qualifiers = {} if matches: qualifiers["download_url"] = url return PackageURL(type="bitbucket", namespace=namespace, name=name, qualifiers=qualifiers) version = None subpath = None # https://bitbucket.org/TG1999/first_repo/new_folder/ if len(segments) >= 3 and segments[2] != "src": version = segments[2] subpath = "/".join(segments[3:]) # https://bitbucket.org/TG1999/first_repo/src/master/new_folder/ if len(segments) >= 4 and segments[2] == "src": version = segments[3] subpath = "/".join(segments[4:]) return PackageURL( type="bitbucket", namespace=namespace, name=name, version=version, subpath=subpath, ) @purl_router.route("https?://gitlab\\.com/(?!.*/archive/).*") def build_gitlab_purl(url): """ Return a PackageURL object from Gitlab `url`. For example: https://gitlab.com/TG1999/firebase/-/tree/1a122122/views https://gitlab.com/TG1999/firebase/-/tree https://gitlab.com/TG1999/firebase/-/master https://gitlab.com/tg1999/Firebase/-/tree/master """ segments = get_path_segments(url) if not len(segments) >= 2: return namespace = segments[0] name = segments[1] version = None subpath = None # https://gitlab.com/TG1999/firebase/master if (len(segments) >= 3) and segments[2] != "-" and segments[2] != "tree": version = segments[2] subpath = "/".join(segments[3:]) # https://gitlab.com/TG1999/firebase/-/tree/master if len(segments) >= 5 and (segments[2] == "-" and segments[3] == "tree"): version = segments[4] subpath = "/".join(segments[5:]) return PackageURL( type="gitlab", namespace=namespace, name=name, version=version, subpath=subpath, ) # https://gitlab.com/hoppr/hoppr/-/archive/v1.11.1-dev.2/hoppr-v1.11.1-dev.2.tar.gz gitlab_archive_pattern = ( r"^https?://gitlab.com/" r"(?P.+)/(?P.+)/-/archive/(?P.+)/" r"(?P=name)-(?P=version).*" r"[^/]$" ) register_pattern("gitlab", gitlab_archive_pattern) # https://hackage.haskell.org/package/cli-extras-0.2.0.0/cli-extras-0.2.0.0.tar.gz hackage_download_pattern = ( r"^https?://hackage.haskell.org/package/" r"(?P.+)-(?P.+)/" r"(?P=name)-(?P=version).*" r"[^/]$" ) register_pattern("hackage", hackage_download_pattern) # https://hackage.haskell.org/package/cli-extras-0.2.0.0/ hackage_project_pattern = r"^https?://hackage.haskell.org/package/(?P.+)-(?P[^/]+)/" register_pattern("hackage", hackage_project_pattern) @purl_router.route( "https?://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/.*" ) def build_generic_google_code_archive_purl(uri): # https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com # /android-notifier/android-notifier-desktop-0.5.1-1.i386.rpm _, remaining_uri = uri.split( "https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/" ) if remaining_uri: # android-notifier/android-notifier-desktop-0.5.1-1.i386.rpm split_remaining_uri = remaining_uri.split("/") # android-notifier, android-notifier-desktop-0.5.1-1.i386.rpm if split_remaining_uri: name = split_remaining_uri[0] # android-notifier return PackageURL( type="generic", namespace="code.google.com", name=name, qualifiers={"download_url": uri}, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/src/packageurl/py.typed0000644000175100001770000000000014705636514021327 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/src/packageurl/utils.py0000644000175100001770000000437314705636514021363 0ustar00runnerdocker# -*- coding: utf-8 -*- # # Copyright (c) the purl authors # SPDX-License-Identifier: MIT # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Visit https://github.com/package-url/packageurl-python for support and # download. from packageurl import PackageURL def get_golang_purl(go_package: str): """ Return a PackageURL object given an imported ``go_package`` or go module "name version" string as seen in a go.mod file. >>> get_golang_purl(go_package="github.com/gorilla/mux v1.8.1") PackageURL(type='golang', namespace='github.com/gorilla', name='mux', version='v1.8.1', qualifiers={}, subpath=None) """ if not go_package: return version = None # Go package in *.mod files is represented like this # package version # github.com/gorilla/mux v1.8.1 # https://github.com/moby/moby/blob/6c10086976d07d4746e03dcfd188972a2f07e1c9/vendor.mod#L51 if "@" in go_package: raise Exception(f"{go_package} should not contain ``@``") if " " in go_package: go_package, _, version = go_package.rpartition(" ") parts = go_package.split("/") if not parts: return name = parts[-1] namespace = "/".join(parts[:-1]) return PackageURL(type="golang", namespace=namespace, name=name, version=version) ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1729576273.51959 packageurl_python-0.16.0/src/packageurl_python.egg-info/0000755000175100001770000000000014705636522022734 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576273.0 packageurl_python-0.16.0/src/packageurl_python.egg-info/PKG-INFO0000644000175100001770000001174114705636521024034 0ustar00runnerdockerMetadata-Version: 2.1 Name: packageurl-python Version: 0.16.0 Summary: A purl aka. Package URL parser and builder Home-page: https://github.com/package-url/packageurl-python Author: the purl authors License: MIT Keywords: package,url,package manager,package url Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities Classifier: Typing :: Typed Requires-Python: >=3.8 Provides-Extra: lint Requires-Dist: isort; extra == "lint" Requires-Dist: black; extra == "lint" Requires-Dist: mypy; extra == "lint" Provides-Extra: test Requires-Dist: pytest; extra == "test" Provides-Extra: build Requires-Dist: setuptools; extra == "build" Requires-Dist: wheel; extra == "build" Provides-Extra: sqlalchemy Requires-Dist: sqlalchemy>=2.0.0; extra == "sqlalchemy" ================= packageurl-python ================= Python library to parse and build "purl" aka. Package URLs. See https://github.com/package-url/purl-spec for details. Join the discussion at https://gitter.im/package-url/Lobby or enter a ticket for support. License: MIT Tests and build status ====================== +----------------------+ | **Tests and build** | +======================+ | |ci-tests| | +----------------------+ Install ======= :: pip install packageurl-python Usage ===== :: >>> from packageurl import PackageURL >>> purl = PackageURL.from_string("pkg:maven/org.apache.commons/io@1.3.4") >>> print(purl.to_dict()) {'type': 'maven', 'namespace': 'org.apache.commons', 'name': 'io', 'version': '1.3.4', 'qualifiers': None, 'subpath': None} >>> print(purl.to_string()) pkg:maven/org.apache.commons/io@1.3.4 >>> print(str(purl)) pkg:maven/org.apache.commons/io@1.3.4 >>> print(repr(purl)) PackageURL(type='maven', namespace='org.apache.commons', name='io', version='1.3.4', qualifiers={}, subpath=None) Utilities ========= Django models ^^^^^^^^^^^^^ `packageurl.contrib.django.models.PackageURLMixin` is a Django abstract model mixin to use Package URLs in Django. SQLAlchemy mixin ^^^^^^^^^^^^^^^^ `packageurl.contrib.sqlalchemy.mixin.PackageURLMixin` is a SQLAlchemy declarative mixin to use Package URLs in SQLAlchemy models. URL to PURL ^^^^^^^^^^^ `packageurl.contrib.url2purl.get_purl(url)` returns a Package URL inferred from an URL. :: >>> from packageurl.contrib import url2purl >>> url2purl.get_purl("https://github.com/package-url/packageurl-python") PackageURL(type='github', namespace='package-url', name='packageurl-python', version=None, qualifiers={}, subpath=None) PURL to URL ^^^^^^^^^^^ - `packageurl.contrib.purl2url.get_repo_url(purl)` returns a repository URL inferred from a Package URL. - `packageurl.contrib.purl2url.get_download_url(purl)` returns a download URL inferred from a Package URL. - `packageurl.contrib.purl2url.get_inferred_urls(purl)` return all inferred URLs (repository, download) from a Package URL. :: >>> from packageurl.contrib import purl2url >>> purl2url.get_repo_url("pkg:gem/bundler@2.3.23") "https://rubygems.org/gems/bundler/versions/2.3.23" >>> purl2url.get_download_url("pkg:gem/bundler@2.3.23") "https://rubygems.org/downloads/bundler-2.3.23.gem" >>> purl2url.get_inferred_urls("pkg:gem/bundler@2.3.23") ["https://rubygems.org/gems/bundler/versions/2.3.23", "https://rubygems.org/downloads/bundler-2.3.23.gem"] Run tests ========= Install test dependencies:: python3 thirdparty/virtualenv.pyz --never-download --no-periodic-update . bin/pip install -e ."[test]" Run tests:: bin/pytest tests Make a new release ================== - Start a new release branch - Update the CHANGELOG.rst, AUTHORS.rst, and README.rst if needed - Bump version in setup.cfg - Run all tests - Install restview and validate that all .rst docs are correct - Commit and push this branch - Make a PR and merge once approved - Tag and push that tag. This triggers the pypi-release.yml workflow that takes care of building the dist release files and upload those to pypi:: VERSION=v0.x.x git tag -a $VERSION -m "Tag $VERSION" git push origin $VERSION - Review the GitHub release created by the workflow at https://github.com/package-url/packageurl-python/releases .. |ci-tests| image:: https://github.com/package-url/packageurl-python/actions/workflows/ci.yml/badge.svg?branch=main :target: https://github.com/package-url/packageurl-python/actions/workflows/ci.yml :alt: CI Tests and build status ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576273.0 packageurl_python-0.16.0/src/packageurl_python.egg-info/SOURCES.txt0000644000175100001770000000174614705636521024627 0ustar00runnerdockerCHANGELOG.rst CONTRIBUTING.rst MANIFEST.in Makefile README.rst mit.LICENSE setup.cfg setup.py src/packageurl/__init__.py src/packageurl/py.typed src/packageurl/utils.py src/packageurl/contrib/__init__.py src/packageurl/contrib/purl2url.py src/packageurl/contrib/route.py src/packageurl/contrib/url2purl.py src/packageurl/contrib/django/__init__.py src/packageurl/contrib/django/filters.py src/packageurl/contrib/django/models.py src/packageurl/contrib/django/utils.py src/packageurl/contrib/sqlalchemy/mixin.py src/packageurl_python.egg-info/PKG-INFO src/packageurl_python.egg-info/SOURCES.txt src/packageurl_python.egg-info/dependency_links.txt src/packageurl_python.egg-info/not-zip-safe src/packageurl_python.egg-info/requires.txt src/packageurl_python.egg-info/top_level.txt tests/test_packageurl.py tests/contrib/test_get_path_segments.py tests/contrib/test_purl2url.py tests/contrib/test_url2purl.py tests/contrib/test_utils.py tests/contrib/data/url2purl.json tests/data/test-suite-data.json././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576273.0 packageurl_python-0.16.0/src/packageurl_python.egg-info/dependency_links.txt0000644000175100001770000000000114705636521027001 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576273.0 packageurl_python-0.16.0/src/packageurl_python.egg-info/not-zip-safe0000644000175100001770000000000114705636521025161 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576273.0 packageurl_python-0.16.0/src/packageurl_python.egg-info/requires.txt0000644000175100001770000000014214705636521025330 0ustar00runnerdocker [build] setuptools wheel [lint] isort black mypy [sqlalchemy] sqlalchemy>=2.0.0 [test] pytest ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576273.0 packageurl_python-0.16.0/src/packageurl_python.egg-info/top_level.txt0000644000175100001770000000001314705636521025457 0ustar00runnerdockerpackageurl ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1729576273.51959 packageurl_python-0.16.0/tests/0000755000175100001770000000000014705636522016076 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1729576273.51959 packageurl_python-0.16.0/tests/contrib/0000755000175100001770000000000014705636522017536 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1729576273.51959 packageurl_python-0.16.0/tests/contrib/data/0000755000175100001770000000000014705636522020447 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/tests/contrib/data/url2purl.json0000644000175100001770000010345714705636514023144 0ustar00runnerdocker{ "http://central.maven.org/maven2/ant-contrib/ant-contrib/1.0b3/ant-contrib-1.0b3.jar": "pkg:maven/ant-contrib/ant-contrib@1.0b3", "http://repo1.maven.org/maven2/ant-contrib/ant-contrib/1.0b3/ant-contrib-1.0b3.jar": "pkg:maven/ant-contrib/ant-contrib@1.0b3", "maven-index://repo1.maven.org/ant-contrib/ant-contrib/1.0b3/ant-contrib-1.0b3.jar": "pkg:maven/ant-contrib/ant-contrib@1.0b3", "maven-index://repo1.maven.org/ant-contrib/ant-contrib/1.0b3/": "pkg:maven/ant-contrib/ant-contrib@1.0b3", "maven-index://repo1.maven.org/ant-contrib/ant-contrib/1.0b3": "pkg:maven/ant-contrib/ant-contrib@1.0b3", "http://repo1.maven.org/maven2/": null, "http://repo1.maven.org/maven2/jdbm/jdbm/": null, "http://repo1.maven.org/maven2/jdbm/jdbm/0.20-dev/": "pkg:maven/jdbm/jdbm@0.20-dev", "http://repo1.maven.org/maven2/jdbm/jdbm/020-dev/jdbm-020-dev": null, "http://repo1.maven.org/maven2/jdbm/jdbm/0.20-dev/jdbm-0.20-dev.jar": "pkg:maven/jdbm/jdbm@0.20-dev", "http://repo1.maven.org/maven2/org/apache/commons/commons-math3/3.6.1/commons-math3-3.6.1.jar": "pkg:maven/org.apache.commons/commons-math3@3.6.1", "http://central.maven.org/maven2/org/apache/commons/commons-math3/3.6.1/commons-math3-3.6.1-sources.jar": "pkg:maven/org.apache.commons/commons-math3@3.6.1?classifier=sources", "http://repo1.maven.org/maven2/jdbm/jdbm/0.20-dev/jdbm-0.20-dev.pom": "pkg:maven/jdbm/jdbm@0.20-dev?type=pom", "http://central.maven.org/maven2/ant/ant-optional/1.5.3-1/ant-optional-1.5.3-1.jar": "pkg:maven/ant/ant-optional@1.5.3-1", "http://central.maven.org/maven2/ant/ant/1.5/ant-1.5.jar": "pkg:maven/ant/ant@1.5", "http://central.maven.org/maven2/antlr/antlr/2.7.7/antlr-2.7.7.jar": "pkg:maven/antlr/antlr@2.7.7", "http://central.maven.org/maven2/aopalliance/aopalliance/1.0/aopalliance-1.0.jar": "pkg:maven/aopalliance/aopalliance@1.0", "http://central.maven.org/maven2/fr/opensagres/xdocreport/fr.opensagres.xdocreport.converter.docx.xwpf/1.0.5/fr.opensagres.xdocreport.converter.docx.xwpf-1.0.5.jar": "pkg:maven/fr.opensagres.xdocreport/fr.opensagres.xdocreport.converter.docx.xwpf@1.0.5", "http://central.maven.org/maven2/org/eclipse/jetty/orbit/org.apache.jasper.glassfish/2.2.2.v201112011158/org.apache.jasper.glassfish-2.2.2.v201112011158-sources.jar": "pkg:maven/org.eclipse.jetty.orbit/org.apache.jasper.glassfish@2.2.2.v201112011158?classifier=sources", "http://central.maven.org/maven2/org/eclipse/jetty/orbit/org.apache.taglibs.standard.glassfish/1.2.0.v201112081803/org.apache.taglibs.standard.glassfish-1.2.0.v201112081803-sources.jar": "pkg:maven/org.eclipse.jetty.orbit/org.apache.taglibs.standard.glassfish@1.2.0.v201112081803?classifier=sources", "http://central.maven.org/maven2/org/springframework/security/kerberos/spring-security-kerberos-core/1.0.1.RELEASE/spring-security-kerberos-core-1.0.1.RELEASE-sources.jar": "pkg:maven/org.springframework.security.kerberos/spring-security-kerberos-core@1.0.1.RELEASE?classifier=sources", "http://central.maven.org/maven2/org/springframework/security/kerberos/spring-security-kerberos-web/1.0.1.RELEASE/spring-security-kerberos-web-1.0.1.RELEASE-sources.jar": "pkg:maven/org.springframework.security.kerberos/spring-security-kerberos-web@1.0.1.RELEASE?classifier=sources", "http://central.maven.org/maven2/xmlunit/xmlunit/1.1/xmlunit-1.1.jar": "pkg:maven/xmlunit/xmlunit@1.1", "http://central.maven.org/maven2/xom/xom/1.0/xom-1.0.jar": "pkg:maven/xom/xom@1.0", "http://central.maven.org/maven2/xom/xom/1.1/xom-1.1-sources.jar": "pkg:maven/xom/xom@1.1?classifier=sources", "http://central.maven.org/maven2/xpp3/xpp3/1.1.3.4.O/xpp3-1.1.3.4.O.jar": "pkg:maven/xpp3/xpp3@1.1.3.4.O", "http://central.maven.org/maven2/xpp3/xpp3_min/1.1.4c/xpp3_min-1.1.4c.jar": "pkg:maven/xpp3/xpp3_min@1.1.4c", "http://central.maven.org/maven2/org/apache/zookeeper/zookeeper/3.4.6/": "pkg:maven/org.apache.zookeeper/zookeeper@3.4.6", "http://central.maven.org/maven2/org/apache/zookeeper/zookeeper/3.4.6": "pkg:maven/org.apache.zookeeper/zookeeper@3.4.6", "http://central.maven.org/maven2/org/apache/zookeeper/zookeeper/3.4.6/zookeeper-3.4.6.jar": "pkg:maven/org.apache.zookeeper/zookeeper@3.4.6", "http://central.maven.org/maven2/org/apache/zookeeper/zookeeper/3.4.6/zookeeper-3.4.6.jar.asc": "pkg:maven/org.apache.zookeeper/zookeeper@3.4.6", "http://central.maven.org/maven2/org/apache/zookeeper/zookeeper/3.4.6/zookeeper-3.4.6.jar.asc.md5": "pkg:maven/org.apache.zookeeper/zookeeper@3.4.6", "http://central.maven.org/maven2/org/apache/zookeeper/zookeeper/3.4.6/zookeeper-3.4.6.jar.asc.sha1": "pkg:maven/org.apache.zookeeper/zookeeper@3.4.6", "http://central.maven.org/maven2/org/apache/zookeeper/zookeeper/3.4.6/zookeeper-3.4.6.jar.md5": "pkg:maven/org.apache.zookeeper/zookeeper@3.4.6", "http://central.maven.org/maven2/org/apache/zookeeper/zookeeper/3.4.6/zookeeper-3.4.6.jar.sha1": "pkg:maven/org.apache.zookeeper/zookeeper@3.4.6", "http://central.maven.org/maven2/org/apache/zookeeper/zookeeper/3.4.6/zookeeper-3.4.6-javadoc.jar": "pkg:maven/org.apache.zookeeper/zookeeper@3.4.6?classifier=javadoc", "http://central.maven.org/maven2/org/apache/zookeeper/zookeeper/3.4.6/zookeeper-3.4.6-sources.jar": "pkg:maven/org.apache.zookeeper/zookeeper@3.4.6?classifier=sources", "http://central.maven.org/maven2/org/apache/zookeeper/zookeeper/3.4.6/zookeeper-3.4.6-tests.jar": "pkg:maven/org.apache.zookeeper/zookeeper@3.4.6?classifier=tests", "http://central.maven.org/maven2/javax/activation/activation/1.1/activation-1.1-sources.jar": "pkg:maven/javax.activation/activation@1.1?classifier=sources", "http://central.maven.org/maven2/com/amazonaws/aws-java-sdk/1.8.5/aws-java-sdk-1.8.5.jar.asc": "pkg:maven/com.amazonaws/aws-java-sdk@1.8.5", "http://central.maven.org/maven2/org/mongodb/casbah-commons_2.10/2.6.1/casbah-commons_2.10-2.6.1-test.jar": "pkg:maven/org.mongodb/casbah-commons_2.10@2.6.1?classifier=test", "http://central.maven.org/maven2/commons-codec/commons-codec/1.6/commons-codec-1.6-javadoc.jar": "pkg:maven/commons-codec/commons-codec@1.6?classifier=javadoc", "http://central.maven.org/maven2/commons-codec/commons-codec/1.6/commons-codec-1.6-tests.jar": "pkg:maven/commons-codec/commons-codec@1.6?classifier=tests", "http://central.maven.org/maven2/commons-io/commons-io/2.3/commons-io-2.3-test-sources.jar": "pkg:maven/commons-io/commons-io@2.3?classifier=test-sources", "http://central.maven.org/maven2/org/drools/drools-guvnor/5.1.0/drools-guvnor-5.1.0.war": "pkg:maven/org.drools/drools-guvnor@5.1.0?type=war", "http://central.maven.org/maven2/org/apache/geronimo/specs/geronimo-servlet_3.0_spec/1.0/geronimo-servlet_3.0_spec-1.0-source-release.tar.gz": "pkg:maven/org.apache.geronimo.specs/geronimo-servlet_3.0_spec@1.0?classifier=source-release&type=tar.gz", "http://central.maven.org/maven2/org/apache/geronimo/gshell/gshell-assembly/1.0-alpha-1/gshell-assembly-1.0-alpha-1-full.zip": "pkg:maven/org.apache.geronimo.gshell/gshell-assembly@1.0-alpha-1?classifier=full&type=zip", "http://central.maven.org/maven2/org/jasypt/jasypt/1.9.0/jasypt-1.9.0-lite.jar": "pkg:maven/org.jasypt/jasypt@1.9.0?classifier=lite", "http://central.maven.org/maven2/com/sun/jersey/jersey-archive/1.19/jersey-archive-1.19.zip": "pkg:maven/com.sun.jersey/jersey-archive@1.19?type=zip", "http://central.maven.org/maven2/org/eclipse/jetty/jetty-distribution/9.4.11.v20180605/jetty-distribution-9.4.11.v20180605.tar.gz": "pkg:maven/org.eclipse.jetty/jetty-distribution@9.4.11.v20180605?type=tar.gz", "http://central.maven.org/maven2/com/github/jnr/jffi/1.2.10/jffi-1.2.10-native.jar": "pkg:maven/com.github.jnr/jffi@1.2.10?classifier=native", "http://central.maven.org/maven2/org/jmxtrans/jmxtrans/251/jmxtrans-251.rpm": "pkg:maven/org.jmxtrans/jmxtrans@251?type=rpm", "http://central.maven.org/maven2/net/sf/json-lib/json-lib/2.3/json-lib-2.3-jdk15.jar": "pkg:maven/net.sf.json-lib/json-lib@2.3?classifier=jdk15", "http://central.maven.org/maven2/org/apache/kafka/kafka_2.11/0.10.1.0/kafka_2.11-0.10.1.0-scaladoc.jar": "pkg:maven/org.apache.kafka/kafka_2.11@0.10.1.0?classifier=scaladoc", "http://central.maven.org/maven2/org/apache/axis2/mex/1.6.2/mex-1.6.2.mar": "pkg:maven/org.apache.axis2/mex@1.6.2?type=mar", "http://central.maven.org/maven2/servicemix/servicemix/1.0/servicemix-1.0-src.zip": "pkg:maven/servicemix/servicemix@1.0?classifier=src&type=zip", "http://central.maven.org/maven2/org/apache/yoko/yoko/1.0/yoko-1.0.pom": "pkg:maven/org.apache.yoko/yoko@1.0?type=pom", "https://registry.yarnpkg.com/@invisionag/": null, "https://registry.yarnpkg.com/@invisionag": null, "https://registry.yarnpkg.com/@invisionag/eslint-config-ivx": "pkg:npm/%40invisionag/eslint-config-ivx", "https://registry.yarnpkg.com/@invisionag%2feslint-config-ivx": "pkg:npm/%40invisionag/eslint-config-ivx", "https://yarnpkg.com/package/@invisionag/": null, "https://yarnpkg.com/package/@invisionag/eslint-config-ivx": "pkg:npm/%40invisionag/eslint-config-ivx", "https://yarnpkg.com/package/@invisionag%2feslint-config-ivx": "pkg:npm/%40invisionag/eslint-config-ivx", "https://www.yarnpkg.com/package/@invisionag/": null, "https://www.yarnpkg.com/package/@invisionag": null, "https://www.yarnpkg.com/package/@invisionag/eslint-config-ivx": "pkg:npm/%40invisionag/eslint-config-ivx", "https://www.yarnpkg.com/package/@invisionag%2feslint-config-ivx": "pkg:npm/%40invisionag/eslint-config-ivx", "https://registry.npmjs.org/automatta/-/automatta-0.0.1.tgz": "pkg:npm/automatta@0.0.1", "http://registry.npmjs.org/1to2/-/1to2-1.0.0.tgz": "pkg:npm/1to2@1.0.0", "http://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz": "pkg:npm/abbrev@1.0.9", "http://registry.npmjs.org/accepts/-/accepts-1.2.2.tgz": "pkg:npm/accepts@1.2.2", "http://registry.npmjs.org/acorn/-/acorn-0.11.0.tgz": "pkg:npm/acorn@0.11.0", "http://registry.npmjs.org/co/-/co-4.6.0.tgz": "pkg:npm/co@4.6.0", "http://registry.npmjs.org/d/-/d-0.1.1.tgz": "pkg:npm/d@0.1.1", "http://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz": "pkg:npm/functional-red-black-tree@1.0.1", "http://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz": "pkg:npm/json-stable-stringify-without-jsonify@1.0.1", "http://registry.npmjs.org/ms/-/ms-0.7.1.tgz": "pkg:npm/ms@0.7.1", "http://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz": "pkg:npm/validate-npm-package-license@3.0.1", "https://registry.npmjs.org/@invisionag/eslint-config-ivx/-/eslint-config-ivx-0.0.2.tgz": "pkg:npm/%40invisionag/eslint-config-ivx@0.0.2", "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz": "pkg:npm/fast-json-stable-stringify@2.0.0", "https://registry.npmjs.org/q/-/q-1.5.1.tgz": "pkg:npm/q@1.5.1", "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz": "pkg:npm/remove-trailing-separator@1.1.0", "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz": "pkg:npm/wide-align@1.1.2", "https://registry.npmjs.org/widest-line/-/widest-line-2.0.0.tgz": "pkg:npm/widest-line@2.0.0", "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz": "pkg:npm/write-file-atomic@2.3.0", "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz": "pkg:npm/xdg-basedir@3.0.0", "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz": "pkg:npm/yallist@2.1.2", "https://registry.npmjs.org/@theia/plugin-ext/-/plugin-ext-0.9.0-next.007f41ae.tgz": "pkg:npm/%40theia/plugin-ext@0.9.0-next.007f41ae", "https://registry.npmjs.org/@esbuild/freebsd-arm64/0.21.5": "pkg:npm/%40esbuild/freebsd-arm64@0.21.5", "https://npmjs.com/package/abbrev": "pkg:npm/abbrev", "https://npmjs.com/package/accepts/v/1.3.7": "pkg:npm/accepts@1.3.7", "https://npmjs.com/package/@angular/cli": "pkg:npm/%40angular/cli", "https://npmjs.com/package/": null, "https://www.npmjs.com/package/abbrev": "pkg:npm/abbrev", "https://www.npmjs.com/package/accepts/v/1.3.7": "pkg:npm/accepts@1.3.7", "https://www.npmjs.com/package/@angular/cli": "pkg:npm/%40angular/cli", "https://www.npmjs.com/package/": null, "https://npmjs.org/package/abbrev": "pkg:npm/abbrev", "https://npmjs.org/package/accepts/v/1.3.7": "pkg:npm/accepts@1.3.7", "https://npmjs.org/package/@angular/cli": "pkg:npm/%40angular/cli", "https://npmjs.org/package": null, "https://npmjs.org/package/": null, "https://www.npmjs.org/package/abbrev": "pkg:npm/abbrev", "https://www.npmjs.org/package/accepts/v/1.3.7": "pkg:npm/accepts@1.3.7", "https://www.npmjs.org/package/@angular/cli": "pkg:npm/%40angular/cli", "https://www.npmjs.org/package": null, "https://www.npmjs.org/package/": null, "http://rubygems.org/downloads/": null, "http://rubygems.org/downloads/macaddr-1.6.1": null, "http://rubygems.org/downloads/macaddr-1.6.1.gem": "pkg:gem/macaddr@1.6.1", "http://rubygems.org/downloads/open4-1.3.0.gem": "pkg:gem/open4@1.3.0", "https://rubygems.org/downloads/actionmailer-4.0.3.gem": "pkg:gem/actionmailer@4.0.3", "https://rubygems.org/downloads/activerecord-deprecated_finders-1.0.3.gem": "pkg:gem/activerecord-deprecated_finders@1.0.3", "https://rubygems.org/downloads/ejs-1.1.1.gem": "pkg:gem/ejs@1.1.1", "https://rubygems.org/downloads/eventmachine-0.12.11.cloudfoundry.3.gem": "pkg:gem/eventmachine@0.12.11.cloudfoundry.3", "https://rubygems.org/downloads/ffi-1.9.3.gem": "pkg:gem/ffi@1.9.3", "https://rubygems.org/downloads/jwt-0.1.8.gem": "pkg:gem/jwt@0.1.8", "https://rubygems.org/downloads/ref-1.0.5.gem": "pkg:gem/ref@1.0.5", "https://rubygems.org/downloads/talentbox-delayed_job_sequel-4.0.0.gem": "pkg:gem/talentbox-delayed_job_sequel@4.0.0", "https://rubygems.org/downloads/unf-0.1.3.gem": "pkg:gem/unf@0.1.3", "https://rubygems.org/downloads/yajl-ruby-1.2.0.gem": "pkg:gem/yajl-ruby@1.2.0", "https://rubygems.org/gems/i18n-js-3.0.11.gem": "pkg:gem/i18n-js@3.0.11", "https://packagist.org/packages/webmozart/assert":"pkg:composer/webmozart/assert", "https://packagist.org/packages/guzzlehttp/psr7#2.6.1":"pkg:composer/guzzlehttp/psr7@2.6.1", "https://packagist.org/packages/symfony/process#v7.0.0-BETA3":"pkg:composer/symfony/process@v7.0.0-BETA3", "https://pypi.org/packages/source/z/zc.recipe.egg/zc.recipe.egg-2.0.0.tar.gz": "pkg:pypi/zc.recipe.egg@2.0.0", "https://pypi.org/project/widgetsnbextension": "pkg:pypi/widgetsnbextension", "https://pypi.org/project/widgetsnbextension/3.0.7/": "pkg:pypi/widgetsnbextension@3.0.7", "https://pypi.org/project/widgetsnbextension/3.0.7/#files": "pkg:pypi/widgetsnbextension@3.0.7", "https://pypi.python.org/project/widgetsnbextension/3.0.7/": "pkg:pypi/widgetsnbextension@3.0.7", "https://pypi.python.org/packages/source/z/zc.recipe.egg/zc.recipe.egg-2.0.0.tar.gz": "pkg:pypi/zc.recipe.egg@2.0.0", "https://pypi.python.org/packages/source/p/python-openid/python-openid-2.2.5.zip": "pkg:pypi/python-openid@2.2.5", "https://pypi.python.org/packages/38/e2/b23434f4030bbb1af3bcdbb2ecff6b11cf2e467622446ce66a08e99f2ea9/pluggy-0.4.0.zip#md5=447a92368175965d2fbacaef9f3df842": "pkg:pypi/pluggy@0.4.0", "https://pypi.python.org/packages/py2.py3/w/wheel/bad-wheel-name-any.whl": null, "https://pypi.python.org/packages/py2.py3/w/wheel/wheel-0.29.0-py2.py3-none-any.whl": "pkg:pypi/wheel@0.29.0", "https://pypi.python.org/packages/py2.py3/w/wheel/wheel-0.29.0-py2.py3-none-any.whl#md5=d7db45db5c131af262b8ffccde46a88a": "pkg:pypi/wheel@0.29.0", "https://files.pythonhosted.org/packages/87/44/0fa8e9d0cccb8eb86fc1b5170208229dc6d6e9fd6e57ea1fe19cbeea68f5/aboutcode_toolkit-3.4.0rc1-py2.py3-none-any.whl": "pkg:pypi/aboutcode-toolkit@3.4.0rc1", "https://files.pythonhosted.org/packages/7f/cf/12d4611fc67babd4ae250c9e8249c5650ae1933395488e9e7e3562b4ff24/amqp-2.3.2-py2.py3-none-any.whl#sha256=eed41946890cd43e8dee44a316b85cf6fee5a1a34bb4a562b660a358eb529e1b": "pkg:pypi/amqp@2.3.2", "https://pypi.python.org/packages/34/c1/8806f99713ddb993c5366c362b2f908f18269f8d792aff1abfd700775a77/click-6.7-py2.py3-none-any.whl#md5=5e7a4e296b3212da2ff11017675d7a4d": "pkg:pypi/click@6.7", "https://pypi.python.org/packages/f6/ae/bbc6a204f33d9d57c798fb3857a072cd14b836792244eea4b446fdb674c6/pycryptodome-3.4.7-cp27-cp27m-win32.whl#md5=78b341de1cd686077745cd9e3a93d8d3": "pkg:pypi/pycryptodome@3.4.7", "https://pypi.python.org/packages/bd/e8/ea44ba5357a0b4fd16e5fb60c355fc8722eae31b93d7597eec50f7c35a52/pycryptodome-3.4.7-cp27-cp27m-win_amd64.whl#md5=f20bb847322baf7ae24700e5cbb15e07": "pkg:pypi/pycryptodome@3.4.7", "https://pypi.python.org/packages/1e/75/8005d086cac4cc41d3b320d338972c5e5c6a21f88472f21ac9d0e031d300/pyahocorasick-1.1.4.tar.bz2#md5=ad445b6648dc06e9040705ce1ccb4384": "pkg:pypi/pyahocorasick@1.1.4", "https://pypi.python.org/packages/2.6/t/threadpool/threadpool-1.2.7-py2.6.egg": "pkg:pypi/threadpool@1.2.7-py2.6", "https://pypi.python.org/packages/any/s/setuptools/setuptools-0.6c11-1.src.rpm": "pkg:pypi/setuptools@0.6c11-1.src", "https://files.pythonhosted.org/packages/84/d8/451842a5496844bb5c7634b231a2e4caf0d867d2e25f09b840d3b07f3d4b/multi_key_dict-2.0.win32.exe": "pkg:pypi/multi-key-dict@2.0.win32", "https://pypi.python.org/packages/source/d/django-contrib-comments/django-contrib-comments-1.5.tar.gz": "pkg:pypi/django-contrib-comments@1.5", "https://files.pythonhosted.org/packages/40/90/df4cb5541c4f5016bbbe04dd09135c7f5af294efa3421f9ab6332cf30dc2/zc.buildout.languageserver-0.6.2.tar.gz": "pkg:pypi/zc.buildout.languageserver@0.6.2", "https://files.pythonhosted.org/packages/b0/42/cac00d0570ff45c8d3b66aa32bf1aba7a527e5908123b0164e42f6af6ae1/zc.buildout.languageserver-0.6.2-py3-none-any.whl": "pkg:pypi/zc.buildout.languageserver@0.6.2", "http://nuget.org/packages/EntityFramework/4.2.0.0": "pkg:nuget/EntityFramework@4.2.0.0", "http://www.nuget.org/packages/SharpGIS.GZipWebClient/1.2.0": "pkg:nuget/SharpGIS.GZipWebClient@1.2.0", "https://www.nuget.org/api/v2/package/Newtonsoft.Json/11.0.1": "pkg:nuget/Newtonsoft.Json@11.0.1", "http://www.nuget.org/api/v2/package/EntityFramework/6.1.0": "pkg:nuget/EntityFramework@6.1.0", "https://www.nuget.org/api/v2/package/MvvmLightLibs/4.1.23": "pkg:nuget/MvvmLightLibs@4.1.23", "https://www.nuget.org/api/v2/package/Twilio/3.4.1": "pkg:nuget/Twilio@3.4.1", "https://api.nuget.org/v3-flatcontainer/newtonsoft.json/10.0.1/newtonsoft.json.10.0.1.nupkg": "pkg:nuget/newtonsoft.json@10.0.1", "http://master.dl.sourceforge.net/project/zznotes/zznotes/1.1.2/zznotes-1.1.2.tar.gz": "pkg:sourceforge/zznotes/zznotes@1.1.2", "http://master.dl.sourceforge.net/project/zapping/zvbi/0.2.35/zvbi-0.2.35.tar.bz2": "pkg:sourceforge/zapping/zvbi@0.2.35", "http://master.dl.sourceforge.net/project/libpng/zlib/1.2.3/zlib-1.2.3.tar.bz2": "pkg:sourceforge/libpng/zlib@1.2.3", "http://master.dl.sourceforge.net/project/xmlstar/xmlstarlet/1.0.0/xmlstarlet-1.0.0-1.src.rpm": "pkg:sourceforge/xmlstar/xmlstarlet@1.0.0", "http://master.dl.sourceforge.net/project/wxmozilla/wxMozilla/0.5.5/wxMozilla-0.5.5.exe": "pkg:sourceforge/wxmozilla/wxMozilla@0.5.5", "http://iweb.dl.sourceforge.net/project/sblim/sblim-cim-client2/2.2.5/sblim-cim-client2-2.2.5-src.zip": "pkg:sourceforge/sblim/sblim-cim-client2@2.2.5", "http://master.dl.sourceforge.net/project/zinnia/zinnia-win32/0.06/zinnia-win32-0.06.zip": "pkg:sourceforge/zinnia/zinnia-win32@0.06", "http://iweb.dl.sourceforge.net/project/findbugs/findbugs/1.3.4/findbugs-1.3.4.tar.gz/": "pkg:sourceforge/findbugs?download_url=http://iweb.dl.sourceforge.net/project/findbugs/findbugs/1.3.4/findbugs-1.3.4.tar.gz/", "http://master.dl.sourceforge.net/project/arestc/net/sf/arestc/arestc/0.1.4/arestc-0.1.4-javadoc.jar": "pkg:sourceforge/arestc?download_url=http://master.dl.sourceforge.net/project/arestc/net/sf/arestc/arestc/0.1.4/arestc-0.1.4-javadoc.jar", "http://master.dl.sourceforge.net/project/intraperson/OldFiles/intraperson/0.28/intraperson-0.28.tar.gz": "pkg:sourceforge/intraperson/intraperson@0.28", "http://master.dl.sourceforge.net/project/pwiki/pwiki/0.1.2/0.1.2.zip": "pkg:sourceforge/pwiki?download_url=http://master.dl.sourceforge.net/project/pwiki/pwiki/0.1.2/0.1.2.zip", "http://master.dl.sourceforge.net/project/iswraid/iswraid/0.1.4.3/2.4.28-pre3-iswraid.patch.gz": "pkg:sourceforge/iswraid?download_url=http://master.dl.sourceforge.net/project/iswraid/iswraid/0.1.4.3/2.4.28-pre3-iswraid.patch.gz", "http://master.dl.sourceforge.net/project/aloyscore/aloyscore/0.1a1%20stable/0.1a1_stable_AloysCore.zip": "pkg:sourceforge/aloyscore?download_url=http://master.dl.sourceforge.net/project/aloyscore/aloyscore/0.1a1%2520stable/0.1a1_stable_AloysCore.zip", "http://master.dl.sourceforge.net/project/myenterprise/OldFiles/1.0.0.2.MyEnterprise.Source.zip": "pkg:sourceforge/myenterprise?download_url=http://master.dl.sourceforge.net/project/myenterprise/OldFiles/1.0.0.2.MyEnterprise.Source.zip", "http://master.dl.sourceforge.net/project/wxhaskell/wxhaskell/wxhaskell-0.9/wxhaskell-src-0.9.zip": "pkg:sourceforge/wxhaskell?download_url=http://master.dl.sourceforge.net/project/wxhaskell/wxhaskell/wxhaskell-0.9/wxhaskell-src-0.9.zip", "http://master.dl.sourceforge.net/project/a2freedom/A2/1.2/a2freedom-1.2.zip": "pkg:sourceforge/a2freedom?download_url=http://master.dl.sourceforge.net/project/a2freedom/A2/1.2/a2freedom-1.2.zip", "http://master.dl.sourceforge.net/project/tinyos/OldFiles/tinyos/1.1.0/tinyos-1.1.0.tar.gz": "pkg:sourceforge/tinyos/tinyos@1.1.0", "http://master.dl.sourceforge.net/project/urlchecker/lu/ng/urlchecker/urlchecker/1.7/urlchecker-1.7-javadoc.jar": "pkg:sourceforge/urlchecker?download_url=http://master.dl.sourceforge.net/project/urlchecker/lu/ng/urlchecker/urlchecker/1.7/urlchecker-1.7-javadoc.jar", "http://master.dl.sourceforge.net/project/zclasspath/maven2/org/zclasspath/zclasspath/1.5/zclasspath-1.5.jar": "pkg:sourceforge/zclasspath?download_url=http://master.dl.sourceforge.net/project/zclasspath/maven2/org/zclasspath/zclasspath/1.5/zclasspath-1.5.jar", "http://master.dl.sourceforge.net/project/googleimagedown/project/v1.1/GoogleImageDownloader-v1.1-src.tar.bz2": "pkg:sourceforge/googleimagedown?download_url=http://master.dl.sourceforge.net/project/googleimagedown/project/v1.1/GoogleImageDownloader-v1.1-src.tar.bz2", "https://sourceforge.net/projects/scribus/files/scribus/1.6.0/scribus-1.6.0.tar.gz/download": "pkg:sourceforge/scribus@1.6.0", "https://sourceforge.net/projects/turbovnc/files/3.1/turbovnc-3.1.tar.gz/download": "pkg:sourceforge/turbovnc@3.1", "https://sourceforge.net/projects/ventoy/files/v1.0.96/Ventoy%201.0.96%20release%20source%20code.tar.gz/download": "pkg:sourceforge/ventoy@1.0.96", "https://sourceforge.net/projects/geoserver/files/GeoServer/2.23.4/geoserver-2.23.4-war.zip/download": "pkg:sourceforge/geoserver@2.23.4", "https://sourceforge.net/projects/spacesniffer/files/spacesniffer_1_3_0_2.zip/download": "pkg:sourceforge/spacesniffer@1_3_0_2", "https://crates.io/api/v1/crates/rand/0.7.2/download": "pkg:cargo/rand@0.7.2", "https://crates.io/api/v1/crates/clap/2.33.0/download": "pkg:cargo/clap@2.33.0", "https://crates.io/api/v1/crates/structopt/0.3.11/download": "pkg:cargo/structopt@0.3.11", "https://github.com/TG1999/fetchcode/tree/documentation/fetchcode": "pkg:github/tg1999/fetchcode@documentation#fetchcode", "https://github.com/nexB/scancode-toolkit/tree/develop/plugins/scancode-ctags-macosx_10_9_intel": "pkg:github/nexb/scancode-toolkit@develop#plugins/scancode-ctags-macosx_10_9_intel", "https://github.com/package-url/packageurl-js/tree/master/test/data": "pkg:github/package-url/packageurl-js@master#test/data", "https://github.com/package-url/packageurl-js/tree/master": "pkg:github/package-url/packageurl-js@master", "https://github.com/package-url/packageurl-js": "pkg:github/package-url/packageurl-js", "https://github.com/TG1999": null, "https://github.com/TG1999/fetchcode/tree": "pkg:github/tg1999/fetchcode", "https://github.com/TG1999/fetchcode/master": "pkg:github/tg1999/fetchcode@master", "https://github.com/TG1999/fetchcode/fetchcode/src": "pkg:github/tg1999/fetchcode@fetchcode#src", "https://github.com/NEXB/SCANCODE-TOOLKIT/tree/develop/PLUGINS/scancode-ctags-macosx_10_9_intel": "pkg:github/nexb/scancode-toolkit@develop#PLUGINS/scancode-ctags-macosx_10_9_intel", "https://github.com/NEXB/SCANCODE-TOOLKIT/tree/DEVELOP/PLUGINS/scancode-ctags-macosx_10_9_intel": "pkg:github/nexb/scancode-toolkit@DEVELOP#PLUGINS/scancode-ctags-macosx_10_9_intel", "https://github.com/apache/nifi/archive/refs/tags/rel/nifi-2.0.0-M3.tar.gz": "pkg:github/apache/nifi@rel/nifi-2.0.0-M3", "https://raw.githubusercontent.com/volatilityfoundation/dwarf2json/master/LICENSE.txt": "pkg:github/volatilityfoundation/dwarf2json@master#LICENSE.txt", "https://raw.githubusercontent.com/LeZuse/flex-sdk/master/frameworks/projects/mx/src/mx/containers/accordionClasses/AccordionHeader.as": "pkg:github/lezuse/flex-sdk@master#frameworks/projects/mx/src/mx/containers/accordionClasses/AccordionHeader.as", "https://raw.githubusercontent.com/NCIP/lexevs/master/lgSharedLibraries/jettison/jettison-1.1.jar": "pkg:github/ncip/lexevs@master#lgSharedLibraries/jettison/jettison-1.1.jar", "https://raw.githubusercontent.com/jgallen23/routie/master/dist/routie.min.js": "pkg:github/jgallen23/routie@master#dist/routie.min.js", "https://raw.githubusercontent.com/jquery/jquery-ui/1.8.14/ui/jquery.ui.mouse.js": "pkg:github/jquery/jquery-ui@1.8.14#ui/jquery.ui.mouse.js", "https://raw.githubusercontent.com/mariopacio/projeto.dprf.info/master/assets/plugins/xbreadcrumbs/xbreadcrumbs.js": "pkg:github/mariopacio/projeto.dprf.info@master#assets/plugins/xbreadcrumbs/xbreadcrumbs.js", "https://api.github.com/repos/nexB/scancode-toolkit": "pkg:github/nexb/scancode-toolkit", "https://api.github.com/repos/nexB/scancode-toolkit/commits/40593af0df6c8378d2b180324b97cb439fa11d66": "pkg:github/nexb/scancode-toolkit@40593af0df6c8378d2b180324b97cb439fa11d66", "https://codeload.github.com/nexB/scancode-toolkit/tar.gz/3.1.1": "pkg:github/nexb/scancode-toolkit@3.1.1", "https://codeload.github.com/nexB/scancode-toolkit/tar.gz/v3.1.1": "pkg:github/nexb/scancode-toolkit@v3.1.1", "https://codeload.github.com/nexB/scancode-toolkit/zip/3.1.1": "pkg:github/nexb/scancode-toolkit@3.1.1", "https://codeload.github.com/nexB/scancode-toolkit/zip/v3.1.1": "pkg:github/nexb/scancode-toolkit@v3.1.1", "https://codeload.github.com/nexB/scancode.io/tar.gz/1.0": "pkg:github/nexb/scancode.io@1.0", "https://codeload.github.com/nexB/scancode.io/tar.gz/V1.0": "pkg:github/nexb/scancode.io@V1.0", "https://codeload.github.com/berngp/grails-rest/zip/release/0.7": "pkg:github/berngp/grails-rest@0.7", "https://codeload.github.com/eclipse/m2e-core/zip/releases/1.2/1.2.0.20120903-1050": "pkg:github/eclipse/m2e-core@1.2.0.20120903-1050", "https://github.com/nexB/scancode-toolkit/archive/3.1.1.zip": "pkg:github/nexb/scancode-toolkit@3.1.1", "https://github.com/nexB/scancode-toolkit/archive/v3.1.1.zip": "pkg:github/nexb/scancode-toolkit@v3.1.1", "https://github.com/pypa/get-virtualenv/raw/20.0.31/public/virtualenv.pyz": "pkg:github/pypa/get-virtualenv@20.0.31#public/virtualenv.pyz", "https://github.com/pypa/get-virtualenv/raw/v20.0.31/public/virtualenv.pyz": "pkg:github/pypa/get-virtualenv@v20.0.31#public/virtualenv.pyz", "https://github.com/fanf2/unifdef/blob/master/unifdef.c": "pkg:github/fanf2/unifdef@master#unifdef.c", "https://github.com/joebeeson/amazon/blob/master/vendors/aws-sdk/sdk.class.php": "pkg:github/joebeeson/amazon@master#vendors/aws-sdk/sdk.class.php", "https://github.com/modelfabric/yowl/blob/master/bin/yowl": "pkg:github/modelfabric/yowl@master#bin/yowl", "https://github.com/syncthing/syncthing/releases/download/v0.14.36/syncthing-source-v0.14.36.tar.gz": "pkg:github/syncthing/syncthing@v0.14.36?download_url=https://github.com/syncthing/syncthing/releases/download/v0.14.36/syncthing-source-v0.14.36.tar.gz", "https://github.com/torakiki/pdfsam/releases/download/v3.3.2/pdfsam-3.3.2-bin.zip": "pkg:github/torakiki/pdfsam@v3.3.2?download_url=https://github.com/torakiki/pdfsam/releases/download/v3.3.2/pdfsam-3.3.2-bin.zip", "https://github.com/yarnpkg/yarn/releases/download/v1.3.2/yarn-v1.3.2.tar.gz": "pkg:github/yarnpkg/yarn@v1.3.2?download_url=https://github.com/yarnpkg/yarn/releases/download/v1.3.2/yarn-v1.3.2.tar.gz", "https://github.com/z3APA3A/3proxy/releases/download/0.8.11/3proxy-0.8.11.zip": "pkg:github/z3apa3a/3proxy@0.8.11?download_url=https://github.com/z3APA3A/3proxy/releases/download/0.8.11/3proxy-0.8.11.zip", "https://github.com/FasterXML/woodstox/archive/woodstox-core-5.0.2.zip": "pkg:github/fasterxml/woodstox@core-5.0.2", "https://github.com/adobe-fonts/source-code-pro/archive/2.030R-ro/1.050R-it.tar.gz": "pkg:github/adobe-fonts/source-code-pro@1.050R-it", "https://github.com/cassandra-rb/simple_uuid/archive/simple_uuid-0.3.0.zip": "pkg:github/cassandra-rb/simple_uuid@0.3.0", "https://github.com/djberg96/sys-filesystem/archive/sys-filesystem-1.1.4.zip": "pkg:github/djberg96/sys-filesystem@1.1.4", "https://github.com/freedesktop/xorg-intel-gpu-tools/archive/igt-gpu-tools-1.23.tar.gz": "pkg:github/freedesktop/xorg-intel-gpu-tools@igt-gpu-tools-1.23", "https://github.com/grnet/synnefo/archive/synnefo/v0.12.3.zip": "pkg:github/grnet/synnefo@v0.12.3", "https://github.com/n8n-io/n8n/archive/n8n@0.23.0.tar.gz": "pkg:github/n8n-io/n8n@0.23.0", "https://github.com/nginx/nginx/archive/branches/stable-0.7.zip": "pkg:github/nginx/nginx@stable-0.7", "https://github.com/swagger-api/swagger-codegen/archive/refs/tags/v3.0.25.tar.gz": "pkg:github/swagger-api/swagger-codegen@v3.0.25", "https://github.com/bareos/bareos/archive/Release/16.2.6.zip": "pkg:github/bareos/bareos@16.2.6", "https://github.com/hessu/bchunk/archive/release/1.2.2.tar.gz": "pkg:github/hessu/bchunk@1.2.2", "https://github.com/downloads/mozilla/rhino/rhino1_7R4.zip": "pkg:github/mozilla/rhino@1_7R4", "https://github.com/pombredanne/schematics.git": "pkg:github/pombredanne/schematics", "https://github.com/jgoerzen/configfile/archive/upstream/1.1.4.tar.gz": "pkg:github/jgoerzen/configfile@1.1.4", "https://github.com/JetBrains/intellij-community/archive/idea/173.4710.11.zip": "pkg:github/jetbrains/intellij-community@173.4710.11", "https://github.com/knime/knime-core/archive/analytics-platform/3.6.0.zip": "pkg:github/knime/knime-core@3.6.0", "https://github.com/renozao/NMF/archive/hotfix/0.20.1.zip": "pkg:github/renozao/nmf@0.20.1", "https://github.com/apache/cordova-osx/archive/rel/4.0.0.zip": "pkg:github/apache/cordova-osx@4.0.0", "https://github.com/sehmaschine/django-grappelli/archive/stable/2.13.x.zip": "pkg:github/sehmaschine/django-grappelli@2.13.x", "https://bitbucket.org/TG1999/first_repo/src/qa/": "pkg:bitbucket/tg1999/first_repo@qa", "https://bitbucket.org/TG1999/first_repo/src/QA/": "pkg:bitbucket/tg1999/first_repo@QA", "https://bitbucket.org/TG1999/first_repo/": "pkg:bitbucket/tg1999/first_repo", "https://bitbucket.org/TG1999/FIRST_REPO/": "pkg:bitbucket/tg1999/first_repo", "https://bitbucket.org/TG1999/": null, "https://bitbucket.org/TG1999/first_repo/src/": "pkg:bitbucket/tg1999/first_repo", "https://bitbucket.org/TG1999/first_repo/src/master/new_folder/": "pkg:bitbucket/tg1999/first_repo@master#new_folder", "https://bitbucket.org/TG1999/first_repo/src/MASTER/NEW_FOLDER/": "pkg:bitbucket/tg1999/first_repo@MASTER#NEW_FOLDER", "https://bitbucket.org/TG1999/first_repo/new_folder/": "pkg:bitbucket/tg1999/first_repo@new_folder", "https://bitbucket.org/multicoreware/x265/downloads/x265_2.6.tar.gz": "pkg:bitbucket/multicoreware/x265?download_url=https://bitbucket.org/multicoreware/x265/downloads/x265_2.6.tar.gz", "https://bitbucket.org/robeden/trove/downloads/trove-3.0.3.zip": "pkg:bitbucket/robeden/trove?download_url=https://bitbucket.org/robeden/trove/downloads/trove-3.0.3.zip", "https://bitbucket.org/efotinis/deskpins/downloads/DeskPins-1.31-setup.exe": "pkg:bitbucket/efotinis/deskpins?download_url=https://bitbucket.org/efotinis/deskpins/downloads/DeskPins-1.31-setup.exe", "https://gitlab.com/TG1999/firebase/-/tree/1a122122/views": "pkg:gitlab/tg1999/firebase@1a122122#views", "https://gitlab.com/tg1999/firebase": "pkg:gitlab/tg1999/firebase", "https://gitlab.com/TG1999/firebase/-/": "pkg:gitlab/tg1999/firebase", "https://gitlab.com/TG1999/firebase/-/tree": "pkg:gitlab/tg1999/firebase", "https://gitlab.com/TG1999/firebase/-/master": "pkg:gitlab/tg1999/firebase", "https://gitlab.com/TG1999/firebase/tree/": "pkg:gitlab/tg1999/firebase", "https://gitlab.com/TG1999/firebase/master": "pkg:gitlab/tg1999/firebase@master", "https://gitlab.com/TG1999/firebase/-/tree/master": "pkg:gitlab/tg1999/firebase@master", "https://gitlab.com/tg1999/Firebase/-/tree/master": "pkg:gitlab/tg1999/firebase@master", "https://gitlab.com/TG1999/FIREBASE": "pkg:gitlab/tg1999/firebase", "https://gitlab.com/hoppr/hoppr/-/archive/v1.11.1-dev.2/hoppr-v1.11.1-dev.2.tar.gz": "pkg:gitlab/hoppr/hoppr@v1.11.1-dev.2", "https://hackage.haskell.org/package/a50-0.5/a50-0.5.tar.gz": "pkg:hackage/a50@0.5", "https://hackage.haskell.org/package/AC-HalfInteger-1.2.1/AC-HalfInteger-1.2.1.tar.gz": "pkg:hackage/AC-HalfInteger@1.2.1", "https://hackage.haskell.org/package/3d-graphics-examples-0.0.0.2/3d-graphics-examples-0.0.0.2.tar.gz": "pkg:hackage/3d-graphics-examples@0.0.0.2", "https://hackage.haskell.org/package/cli-extras-0.2.0.0": "pkg:hackage/cli-extras@0.2.0.0", "https://hackage.haskell.org/package/cli-extras-0.2.0.0/": "pkg:hackage/cli-extras@0.2.0.0", "https://salsa.debian.org/lxc-team/lxc/-/archive/master/lxc-master.tar.gz": "pkg:generic/lxc-master.tar.gz?download_url=https://salsa.debian.org/lxc-team/lxc/-/archive/master/lxc-master.tar.gz", "http://apt-rpm.org/": null, "": null, "https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/android-notifier/android-notifier-desktop-0.5.1-1.i386.rpm": "pkg:generic/code.google.com/android-notifier?download_url=https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/android-notifier/android-notifier-desktop-0.5.1-1.i386.rpm", "https://cran.r-project.org/src/contrib/jsonlite_1.8.8.tar.gz": "pkg:cran/jsonlite@1.8.8", "https://packagemanager.rstudio.com/cran/2022-06-23/src/contrib/curl_4.3.2.tar.gz": "pkg:cran/curl@4.3.2?download_url=https://packagemanager.rstudio.com/cran/2022-06-23/src/contrib/curl_4.3.2.tar.gz" } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/tests/contrib/test_get_path_segments.py0000644000175100001770000000337114705636514024654 0ustar00runnerdocker# -*- coding: utf-8 -*- # # Copyright (c) the purl authors # SPDX-License-Identifier: MIT # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Visit https://github.com/package-url/packageurl-python for support and # download. from packageurl.contrib.url2purl import get_path_segments def test_parsing_with_quoted_uri(): url = "https://github.com/Hello+world%21/Hello%2Bworld%2521/master" segments = get_path_segments(url) assert "Hello world!" == segments[0] assert "Hello+world%21" == segments[1] def test_parsing_empty_string(): url = "" segments = get_path_segments(url) assert [] == segments def test_parsing_with_one_segment(): url = "https://github.com/TG1999" segments = get_path_segments(url) assert ["TG1999"] == segments ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/tests/contrib/test_purl2url.py0000644000175100001770000002536714705636514022754 0ustar00runnerdocker# -*- coding: utf-8 -*- # # Copyright (c) the purl authors # SPDX-License-Identifier: MIT # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Visit https://github.com/package-url/packageurl-python for support and # download. import pytest from packageurl.contrib import purl2url def test_purl2url_get_repo_url(): purls_url = { "pkg:github/tg1999/fetchcode": "https://github.com/tg1999/fetchcode", "pkg:github/tg1999/fetchcode@master": "https://github.com/tg1999/fetchcode/tree/master", "pkg:github/tg1999/fetchcode@master#tests": "https://github.com/tg1999/fetchcode/tree/master", "pkg:github/nexb/scancode-toolkit@3.1.1?version_prefix=v": "https://github.com/nexb/scancode-toolkit/tree/v3.1.1", "pkg:github/tg1999": None, "pkg:cargo/rand@0.7.2": "https://crates.io/crates/rand/0.7.2", "pkg:cargo/abc": "https://crates.io/crates/abc", "pkg:gem/bundler@2.3.23": "https://rubygems.org/gems/bundler/versions/2.3.23", "pkg:rubygems/bundler@2.3.23": "https://rubygems.org/gems/bundler/versions/2.3.23", "pkg:rubygems/package-name": "https://rubygems.org/gems/package-name", "pkg:bitbucket/birkenfeld/pygments-main": "https://bitbucket.org/birkenfeld/pygments-main", "pkg:bitbucket/birkenfeld/pygments-main@244fd47e07d1014f0aed9c": "https://bitbucket.org/birkenfeld/pygments-main", "pkg:bitbucket/birkenfeld/pygments-main@master#views": "https://bitbucket.org/birkenfeld/pygments-main", "pkg:bitbucket/birkenfeld": None, "pkg:gitlab/tg1999/firebase@master": "https://gitlab.com/tg1999/firebase", "pkg:gitlab/tg1999/firebase@1a122122#views": "https://gitlab.com/tg1999/firebase", "pkg:gitlab/tg1999/firebase": "https://gitlab.com/tg1999/firebase", "pkg:gitlab/tg1999": None, "pkg:gitlab/hoppr/hoppr@v1.11.1-dev.2": "https://gitlab.com/hoppr/hoppr", "pkg:pypi/sortedcontainers": "https://pypi.org/project/sortedcontainers/", "pkg:pypi/sortedcontainers@2.4.0": "https://pypi.org/project/sortedcontainers/2.4.0/", "pkg:pypi/packageurl_python": "https://pypi.org/project/packageurl-python/", "pkg:composer/psr/log": "https://packagist.org/packages/psr/log", "pkg:composer/psr/log@1.1.3": "https://packagist.org/packages/psr/log#1.1.3", "pkg:npm/is-npm": "https://www.npmjs.com/package/is-npm", "pkg:npm/@clayui/tooltip@3.1.0": "https://www.npmjs.com/package/@clayui/tooltip/v/3.1.0", "pkg:npm/%40clayui/tooltip@3.1.0": "https://www.npmjs.com/package/@clayui/tooltip/v/3.1.0", "pkg:npm/%40esbuild/freebsd-arm64@0.21.5": "https://www.npmjs.com/package/@esbuild/freebsd-arm64/v/0.21.5", "pkg:nuget/System.Text.Json": "https://www.nuget.org/packages/System.Text.Json", "pkg:nuget/System.Text.Json@6.0.6": "https://www.nuget.org/packages/System.Text.Json/6.0.6", "pkg:hackage/cli-extras": "https://hackage.haskell.org/package/cli-extras", "pkg:hackage/cli-extras@0.2.0.0": "https://hackage.haskell.org/package/cli-extras-0.2.0.0", "pkg:golang/xorm.io/xorm": "https://pkg.go.dev/xorm.io/xorm", "pkg:golang/xorm.io/xorm@v0.8.2": "https://pkg.go.dev/xorm.io/xorm@v0.8.2", "pkg:golang/gopkg.in/ldap.v3@v3.1.0": "https://pkg.go.dev/gopkg.in/ldap.v3@v3.1.0", "pkg:cocoapods/AFNetworking@4.0.1": "https://cocoapods.org/pods/AFNetworking", "pkg:cocoapods/MapsIndoors@3.24.0": "https://cocoapods.org/pods/MapsIndoors", } for purl, url in purls_url.items(): assert url == purl2url.get_repo_url(purl) def test_purl2url_get_download_url(): purls_url = { # Generated "pkg:cargo/rand@0.7.2": "https://crates.io/api/v1/crates/rand/0.7.2/download", "pkg:gem/bundler@2.3.23": "https://rubygems.org/downloads/bundler-2.3.23.gem", "pkg:npm/is-npm@1.0.0": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", "pkg:npm/@clayui/tooltip@3.1.0": "https://registry.npmjs.org/@clayui/tooltip/-/tooltip-3.1.0.tgz", "pkg:npm/%40clayui/tooltip@3.1.0": "https://registry.npmjs.org/@clayui/tooltip/-/tooltip-3.1.0.tgz", "pkg:npm/%40esbuild/freebsd-arm64@0.21.5": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", "pkg:hackage/cli-extras@0.2.0.0": "https://hackage.haskell.org/package/cli-extras-0.2.0.0/cli-extras-0.2.0.0.tar.gz", "pkg:nuget/System.Text.Json@6.0.6": "https://www.nuget.org/api/v2/package/System.Text.Json/6.0.6", "pkg:github/nexb/scancode-toolkit@3.1.1?version_prefix=v": "https://github.com/nexb/scancode-toolkit/archive/v3.1.1.tar.gz", "pkg:github/StonyShi/reactor-netty-jersey@ac525d91ff1724395640531df08e3e4eabef207d": "https://github.com/stonyshi/reactor-netty-jersey/archive/ac525d91ff1724395640531df08e3e4eabef207d.tar.gz", "pkg:bitbucket/robeden/trove@3.0.3": "https://bitbucket.org/robeden/trove/get/3.0.3.tar.gz", "pkg:bitbucket/robeden/trove@3.0.3?version_prefix=v": "https://bitbucket.org/robeden/trove/get/v3.0.3.tar.gz", "pkg:gitlab/tg1999/firebase@1a122122": "https://gitlab.com/tg1999/firebase/-/archive/1a122122/firebase-1a122122.tar.gz", "pkg:gitlab/tg1999/firebase@1a122122?version_prefix=v": "https://gitlab.com/tg1999/firebase/-/archive/v1a122122/firebase-v1a122122.tar.gz", "pkg:gitlab/hoppr/hoppr@v1.11.1-dev.2": "https://gitlab.com/hoppr/hoppr/-/archive/v1.11.1-dev.2/hoppr-v1.11.1-dev.2.tar.gz", # From `download_url` qualifier "pkg:github/yarnpkg/yarn@1.3.2?download_url=https://github.com/yarnpkg/yarn/releases/download/v1.3.2/yarn-v1.3.2.tar.gz&version_prefix=v": "https://github.com/yarnpkg/yarn/releases/download/v1.3.2/yarn-v1.3.2.tar.gz", "pkg:generic/lxc-master.tar.gz?download_url=https://salsa.debian.org/lxc-team/lxc/-/archive/master/lxc-master.tar.gz": "https://salsa.debian.org/lxc-team/lxc/-/archive/master/lxc-master.tar.gz", "pkg:generic/code.google.com/android-notifier?download_url=https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/android-notifier/android-notifier-desktop-0.5.1-1.i386.rpm": "https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/android-notifier/android-notifier-desktop-0.5.1-1.i386.rpm", "pkg:bitbucket/robeden/trove?download_url=https://bitbucket.org/robeden/trove/downloads/trove-3.0.3.zip": "https://bitbucket.org/robeden/trove/downloads/trove-3.0.3.zip", "pkg:sourceforge/zclasspath?download_url=http://master.dl.sourceforge.net/project/zclasspath/maven2/org/zclasspath/zclasspath/1.5/zclasspath-1.5.jar": "http://master.dl.sourceforge.net/project/zclasspath/maven2/org/zclasspath/zclasspath/1.5/zclasspath-1.5.jar", "pkg:pypi/aboutcode-toolkit@3.4.0rc1?download_url=https://files.pythonhosted.org/packages/87/44/0fa8e9d0cccb8eb86fc1b5170208229dc6d6e9fd6e57ea1fe19cbeea68f5/aboutcode_toolkit-3.4.0rc1-py2.py3-none-any.whl": "https://files.pythonhosted.org/packages/87/44/0fa8e9d0cccb8eb86fc1b5170208229dc6d6e9fd6e57ea1fe19cbeea68f5/aboutcode_toolkit-3.4.0rc1-py2.py3-none-any.whl", # Not-supported "pkg:github/tg1999/fetchcode": None, "pkg:cargo/abc": None, "pkg:rubygems/package-name": None, "pkg:bitbucket/birkenfeld": None, "pkg:pypi/sortedcontainers@2.4.0": None, "pkg:composer/psr/log@1.1.3": None, "pkg:golang/xorm.io/xorm@v0.8.2": None, "pkg:golang/gopkg.in/ldap.v3@v3.1.0": None, } for purl, url in purls_url.items(): assert url == purl2url.get_download_url(purl) def test_purl2url_get_inferred_urls(): purls_url = { "pkg:cargo/rand@0.7.2": [ "https://crates.io/crates/rand/0.7.2", "https://crates.io/api/v1/crates/rand/0.7.2/download", ], "pkg:gem/bundler@2.3.23": [ "https://rubygems.org/gems/bundler/versions/2.3.23", "https://rubygems.org/downloads/bundler-2.3.23.gem", ], "pkg:npm/is-npm@1.0.0": [ "https://www.npmjs.com/package/is-npm/v/1.0.0", "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", ], "pkg:npm/%40clayui/tooltip@3.1.0": [ "https://www.npmjs.com/package/@clayui/tooltip/v/3.1.0", "https://registry.npmjs.org/@clayui/tooltip/-/tooltip-3.1.0.tgz", ], "pkg:hackage/cli-extras@0.2.0.0": [ "https://hackage.haskell.org/package/cli-extras-0.2.0.0", "https://hackage.haskell.org/package/cli-extras-0.2.0.0/cli-extras-0.2.0.0.tar.gz", ], "pkg:nuget/System.Text.Json@6.0.6": [ "https://www.nuget.org/packages/System.Text.Json/6.0.6", "https://www.nuget.org/api/v2/package/System.Text.Json/6.0.6", ], "pkg:cargo/abc": ["https://crates.io/crates/abc"], "pkg:github/tg1999/fetchcode": ["https://github.com/tg1999/fetchcode"], "pkg:gitlab/tg1999/firebase@1a122122": [ "https://gitlab.com/tg1999/firebase", "https://gitlab.com/tg1999/firebase/-/archive/1a122122/firebase-1a122122.tar.gz", ], "pkg:pypi/sortedcontainers@2.4.0": ["https://pypi.org/project/sortedcontainers/2.4.0/"], "pkg:cocoapods/AFNetworking@4.0.1": ["https://cocoapods.org/pods/AFNetworking"], "pkg:composer/psr/log@1.1.3": ["https://packagist.org/packages/psr/log#1.1.3"], "pkg:rubygems/package-name": ["https://rubygems.org/gems/package-name"], "pkg:bitbucket/birkenfeld": [], } for purl, url in purls_url.items(): assert url == purl2url.get_inferred_urls(purl) def test_purl2url_get_repo_url_with_invalid_purls(): purls = [ "pkg:github", "pkg:cargo", "pkg:gem", "pkg:bitbucket", "pkg:gitlab", None, ] for purl in purls: with pytest.raises(Exception) as e_info: purl2url.get_repo_url(purl) assert "Invalid PURL" == e_info ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/tests/contrib/test_url2purl.py0000644000175100001770000000637614705636514022753 0ustar00runnerdocker# -*- coding: utf-8 -*- # # Copyright (c) the purl authors # SPDX-License-Identifier: MIT # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Visit https://github.com/package-url/packageurl-python for support and # download. import io import json import os import re from unittest import TestCase from packageurl.contrib.url2purl import get_purl as purl_getter def get_purl(url): purl = purl_getter(url) return purl and str(purl.to_string()) class TestURL2PURL(TestCase): def test_get_purl_empty_string(self): self.assertEqual(None, get_purl("")) def test_get_purl_none(self): self.assertEqual(None, get_purl(None)) def test_get_purl_unroutable_uri(self): self.assertEqual(None, get_purl("dsf.example")) def python_safe(s): """ Return a name safe to use as a python function name. """ safe_chars = re.compile(r"[\W_]", re.MULTILINE) s = s.strip().lower() s = [x for x in safe_chars.split(s) if x] return "_".join(s) def get_url2purl_test_method(test_url, expected_purl): def test_method(self): self.assertEqual(expected_purl, get_purl(test_url), msg=test_url) return test_method def build_tests(clazz, test_file="url2purl.json", regen=False): """ Dynamically build test methods for Package URL inference from a JSON test file. The JSON test file is a key-sorted mapping of {test url: expected purl}. """ test_data_dir = os.path.join(os.path.dirname(__file__), "data") test_file = os.path.join(test_data_dir, test_file) with io.open(test_file, encoding="utf-8") as tests: tests_data = json.load(tests) if regen: tests_data = {test_url: get_purl(test_url) for test_url in tests_data.keys()} with io.open(test_file, "w") as regened: json.dump(tests_data, regened, indent=2) for test_url, expected_purl in sorted(tests_data.items()): test_name = f"test_url2purl_{test_url}" test_name = python_safe(test_name) test_method = get_url2purl_test_method(test_url, expected_purl) test_method.funcname = test_name # attach that method to our test class setattr(clazz, test_name, test_method) class TestURL2PURLDataDriven(TestCase): pass build_tests(clazz=TestURL2PURLDataDriven, regen=False) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/tests/contrib/test_utils.py0000644000175100001770000000636214705636514022317 0ustar00runnerdocker# -*- coding: utf-8 -*- # # Copyright (c) the purl authors # SPDX-License-Identifier: MIT # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Visit https://github.com/package-url/packageurl-python for support and # download. import pytest from packageurl.contrib.django.utils import purl_to_lookups from packageurl.utils import get_golang_purl def test_purl_to_lookups_without_encode(): assert purl_to_lookups( purl_str="pkg:alpine/openssl@0?arch=aarch64&distroversion=edge&reponame=main", encode=False, ) == { "type": "alpine", "name": "openssl", "version": "0", "qualifiers": { "arch": "aarch64", "distroversion": "edge", "reponame": "main", }, } def test_purl_to_lookups_with_encode(): assert purl_to_lookups( purl_str="pkg:alpine/openssl@0?arch=aarch64&distroversion=edge&reponame=main", encode=True, ) == { "type": "alpine", "name": "openssl", "version": "0", "qualifiers": "arch=aarch64&distroversion=edge&reponame=main", } def test_purl_to_lookups_include_empty_fields(): purl_str = "pkg:alpine/openssl" assert purl_to_lookups(purl_str) == { "type": "alpine", "name": "openssl", } assert purl_to_lookups(purl_str, include_empty_fields=True) == { "type": "alpine", "namespace": "", "name": "openssl", "version": "", "qualifiers": "", "subpath": "", } def test_get_golang_purl(): assert None == get_golang_purl(None) golang_purl_1 = get_golang_purl( "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" ) assert "pkg:golang/github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" == str( golang_purl_1 ) assert golang_purl_1.name == "v3" assert golang_purl_1.namespace == "github.com/envoyproxy/go-control-plane/envoy/config/listener" golang_purl_2 = get_golang_purl( go_package="github.com/grpc-ecosystem/go-grpc-middleware v1.3.0" ) assert "pkg:golang/github.com/grpc-ecosystem/go-grpc-middleware@v1.3.0" == str(golang_purl_2) with pytest.raises(Exception): get_golang_purl("github.com/envoyproxy/go-control-plane/envoy/config/listener@v3.1") ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1729576273.51959 packageurl_python-0.16.0/tests/data/0000755000175100001770000000000014705636522017007 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/tests/data/test-suite-data.json0000644000175100001770000003052614705636514022726 0ustar00runnerdocker[ { "description": "valid maven purl", "purl": "pkg:maven/org.apache.commons/io@1.3.4", "canonical_purl": "pkg:maven/org.apache.commons/io@1.3.4", "type": "maven", "namespace": "org.apache.commons", "name": "io", "version": "1.3.4", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "basic valid maven purl without version", "purl": "pkg:maven/org.apache.commons/io", "canonical_purl": "pkg:maven/org.apache.commons/io", "type": "maven", "namespace": "org.apache.commons", "name": "io", "version": null, "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "valid go purl without version and with subpath", "purl": "pkg:GOLANG/google.golang.org/genproto#/googleapis/api/annotations/", "canonical_purl": "pkg:golang/google.golang.org/genproto#googleapis/api/annotations", "type": "golang", "namespace": "google.golang.org", "name": "genproto", "version": null, "qualifiers": null, "subpath": "googleapis/api/annotations", "is_invalid": false }, { "description": "valid go purl with version and subpath", "purl": "pkg:GOLANG/google.golang.org/genproto@abcdedf#/googleapis/api/annotations/", "canonical_purl": "pkg:golang/google.golang.org/genproto@abcdedf#googleapis/api/annotations", "type": "golang", "namespace": "google.golang.org", "name": "genproto", "version": "abcdedf", "qualifiers": null, "subpath": "googleapis/api/annotations", "is_invalid": false }, { "description": "valid golang purl", "purl": "pkg:golang/github.com/nats-io/nats-server/v2/server@v1.2.9", "canonical_purl": "pkg:golang/github.com/nats-io/nats-server/v2/server@v1.2.9", "type": "golang", "namespace": "github.com/nats-io/nats-server/v2/", "name": "server", "version": "v1.2.9", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "bitbucket namespace and name should be lowercased", "purl": "pkg:bitbucket/birKenfeld/pyGments-main@244fd47e07d1014f0aed9c", "canonical_purl": "pkg:bitbucket/birkenfeld/pygments-main@244fd47e07d1014f0aed9c", "type": "bitbucket", "namespace": "birkenfeld", "name": "pygments-main", "version": "244fd47e07d1014f0aed9c", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "github namespace and name should be lowercased", "purl": "pkg:github/Package-url/purl-Spec@244fd47e07d1004f0aed9c", "canonical_purl": "pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c", "type": "github", "namespace": "package-url", "name": "purl-spec", "version": "244fd47e07d1004f0aed9c", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "debian can use qualifiers", "purl": "pkg:deb/debian/curl@7.50.3-1?arch=i386&distro=jessie", "canonical_purl": "pkg:deb/debian/curl@7.50.3-1?arch=i386&distro=jessie", "type": "deb", "namespace": "debian", "name": "curl", "version": "7.50.3-1", "qualifiers": {"arch": "i386", "distro": "jessie"}, "subpath": null, "is_invalid": false }, { "description": "docker uses qualifiers and hash image id as versions", "purl": "pkg:docker/customer/dockerimage@sha256:244fd47e07d1004f0aed9c?repository_url=gcr.io", "canonical_purl": "pkg:docker/customer/dockerimage@sha256:244fd47e07d1004f0aed9c?repository_url=gcr.io", "type": "docker", "namespace": "customer", "name": "dockerimage", "version": "sha256:244fd47e07d1004f0aed9c", "qualifiers": {"repository_url": "gcr.io"}, "subpath": null, "is_invalid": false }, { "description": "Java gem can use a qualifier", "purl": "pkg:gem/jruby-launcher@1.1.2?Platform=java", "canonical_purl": "pkg:gem/jruby-launcher@1.1.2?platform=java", "type": "gem", "namespace": null, "name": "jruby-launcher", "version": "1.1.2", "qualifiers": {"platform": "java"}, "subpath": null, "is_invalid": false }, { "description": "maven often uses qualifiers", "purl": "pkg:Maven/org.apache.xmlgraphics/batik-anim@1.9.1?repositorY_url=repo.spring.io/release&classifier=sources", "canonical_purl": "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?classifier=sources&repository_url=repo.spring.io/release", "type": "maven", "namespace": "org.apache.xmlgraphics", "name": "batik-anim", "version": "1.9.1", "qualifiers": {"classifier": "sources", "repository_url": "repo.spring.io/release"}, "subpath": null, "is_invalid": false }, { "description": "maven pom reference", "purl": "pkg:Maven/org.apache.xmlgraphics/batik-anim@1.9.1?repositorY_url=repo.spring.io/release&extension=pom", "canonical_purl": "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?extension=pom&repository_url=repo.spring.io/release", "type": "maven", "namespace": "org.apache.xmlgraphics", "name": "batik-anim", "version": "1.9.1", "qualifiers": {"extension": "pom", "repository_url": "repo.spring.io/release"}, "subpath": null, "is_invalid": false }, { "description": "maven can come with a type qualifier", "purl": "pkg:Maven/net.sf.jacob-project/jacob@1.14.3?type=dll&classifier=x86", "canonical_purl": "pkg:maven/net.sf.jacob-project/jacob@1.14.3?classifier=x86&type=dll", "type": "maven", "namespace": "net.sf.jacob-project", "name": "jacob", "version": "1.14.3", "qualifiers": {"classifier": "x86", "type": "dll"}, "subpath": null, "is_invalid": false }, { "description": "npm can be scoped", "purl": "pkg:npm/%40angular/animation@12.3.1", "canonical_purl": "pkg:npm/%40angular/animation@12.3.1", "type": "npm", "namespace": "@angular", "name": "animation", "version": "12.3.1", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "nuget names are case sensitive", "purl": "pkg:Nuget/EnterpriseLibrary.Common@6.0.1304", "canonical_purl": "pkg:nuget/EnterpriseLibrary.Common@6.0.1304", "type": "nuget", "namespace": null, "name": "EnterpriseLibrary.Common", "version": "6.0.1304", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "pypi names have special rules and not case sensitive", "purl": "pkg:PYPI/Django_package@1.11.1.dev1", "canonical_purl": "pkg:pypi/django-package@1.11.1.dev1", "type": "pypi", "namespace": null, "name": "django-package", "version": "1.11.1.dev1", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "valid packagist purl", "purl": "pkg:composer/guzzlehttp/promises@2.0.2", "canonical_purl": "pkg:composer/guzzlehttp/promises@2.0.2", "type": "composer", "namespace": "guzzlehttp", "name": "promises", "version": "2.0.2", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "rpm often use qualifiers", "purl": "pkg:Rpm/fedora/curl@7.50.3-1.fc25?Arch=i386&Distro=fedora-25", "canonical_purl": "pkg:rpm/fedora/curl@7.50.3-1.fc25?arch=i386&distro=fedora-25", "type": "rpm", "namespace": "fedora", "name": "curl", "version": "7.50.3-1.fc25", "qualifiers": {"arch": "i386", "distro": "fedora-25"}, "subpath": null, "is_invalid": false }, { "description": "a scheme is always required", "purl": "EnterpriseLibrary.Common@6.0.1304", "canonical_purl": "EnterpriseLibrary.Common@6.0.1304", "type": null, "namespace": null, "name": "EnterpriseLibrary.Common", "version": null, "qualifiers": null, "subpath": null, "is_invalid": true }, { "description": "a type is always required", "purl": "pkg:EnterpriseLibrary.Common@6.0.1304", "canonical_purl": "pkg:EnterpriseLibrary.Common@6.0.1304", "type": null, "namespace": null, "name": "EnterpriseLibrary.Common", "version": null, "qualifiers": null, "subpath": null, "is_invalid": true }, { "description": "a name is required", "purl": "pkg:maven/@1.3.4", "canonical_purl": "pkg:maven/@1.3.4", "type": "maven", "namespace": null, "name": null, "version": null, "qualifiers": null, "subpath": null, "is_invalid": true }, { "description": "slash / after scheme is not significant", "purl": "pkg:/maven/org.apache.commons/io", "canonical_purl": "pkg:maven/org.apache.commons/io", "type": "maven", "namespace": "org.apache.commons", "name": "io", "version": null, "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "double slash // after scheme is not significant", "purl": "pkg://maven/org.apache.commons/io", "canonical_purl": "pkg:maven/org.apache.commons/io", "type": "maven", "namespace": "org.apache.commons", "name": "io", "version": null, "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "slash /// after type is not significant", "purl": "pkg:///maven/org.apache.commons/io", "canonical_purl": "pkg:maven/org.apache.commons/io", "type": "maven", "namespace": "org.apache.commons", "name": "io", "version": null, "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "valid maven purl with case sensitive namespace and name", "purl": "pkg:maven/HTTPClient/HTTPClient@0.3-3", "canonical_purl": "pkg:maven/HTTPClient/HTTPClient@0.3-3", "type": "maven", "namespace": "HTTPClient", "name": "HTTPClient", "version": "0.3-3", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "valid maven purl containing a space in the version and qualifier", "purl": "pkg:maven/mygroup/myartifact@1.0.0%20Final?mykey=my%20value", "canonical_purl": "pkg:maven/mygroup/myartifact@1.0.0%20Final?mykey=my%20value", "type": "maven", "namespace": "mygroup", "name": "myartifact", "version": "1.0.0 Final", "qualifiers": {"mykey": "my value"}, "subpath": null, "is_invalid": false }, { "description": "checks for invalid qualifier keys", "purl": "pkg:npm/myartifact@1.0.0?in%20production=true", "canonical_purl": null, "type": "npm", "namespace": null, "name": "myartifact", "version": "1.0.0", "qualifiers": {"in production": "true"}, "subpath": null, "is_invalid": true }, { "description": "valid npm purl without version and with subpath", "purl": "pkg:npm/@babel/core#/googleapis/api/annotations/", "canonical_purl": "pkg:npm/%40babel/core#googleapis/api/annotations", "type": "npm", "namespace": "@babel", "name": "core", "version": null, "qualifiers": null, "subpath": "googleapis/api/annotations", "is_invalid": false }, { "description": "valid npm purl with namespace, version and subpath", "purl": "pkg:npm/@babel/core@1.0.2#/googleapis/api/annotations/", "canonical_purl": "pkg:npm/%40babel/core@1.0.2#googleapis/api/annotations", "type": "npm", "namespace": "@babel", "name": "core", "version": "1.0.2", "qualifiers": null, "subpath": "googleapis/api/annotations", "is_invalid": false }, { "description": "valid npm purl without namespace and with subpath", "purl": "pkg:npm/core@1.0.2#/googleapis/api/annotations/", "canonical_purl": "pkg:npm/core@1.0.2#googleapis/api/annotations", "type": "npm", "namespace": null, "name": "core", "version": "1.0.2", "qualifiers": null, "subpath": "googleapis/api/annotations", "is_invalid": false }, { "description": "valid npm purl without namespace, version and subpath", "purl": "pkg:npm/core#/googleapis/api/annotations/", "canonical_purl": "pkg:npm/core#googleapis/api/annotations", "type": "npm", "namespace": null, "name": "core", "version": null, "qualifiers": null, "subpath": "googleapis/api/annotations", "is_invalid": false }, { "description": "valid npm purl without namespace, version and subpath", "purl": "pkg:NPM/core#/googleapis/api/annotations/", "canonical_purl": "pkg:npm/core#googleapis/api/annotations", "type": "npm", "namespace": null, "name": "core", "version": null, "qualifiers": null, "subpath": "googleapis/api/annotations", "is_invalid": false } ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729576268.0 packageurl_python-0.16.0/tests/test_packageurl.py0000644000175100001770000002574214705636514021640 0ustar00runnerdocker# -*- coding: utf-8 -*- # # Copyright (c) the purl authors # SPDX-License-Identifier: MIT # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Visit https://github.com/package-url/packageurl-python for support and # download. import json import os import re import unittest from packageurl import PackageURL from packageurl import normalize from packageurl import normalize_qualifiers def create_test_function( description, purl, canonical_purl, is_invalid, type, name, namespace, version, qualifiers, subpath, # NOQA test_func_prefix="test_purl_pkg_", **kwargs ): """ Return a new (test function, test_name) where the test_function closed on test arguments. If is_error is True the tests are expected to raise an Exception. """ if is_invalid: def test_purl(self): try: PackageURL.from_string(purl) self.fail("Should raise a ValueError") except ValueError: pass try: PackageURL.from_string(canonical_purl) self.fail("Should raise a ValueError") except ValueError: pass try: PackageURL(type, namespace, name, version, qualifiers, subpath) except ValueError: pass else: def test_purl(self): # parsing the test canonical `purl` then re-building a `purl` from these # parsed components should return the test canonical `purl` cano = PackageURL.from_string(purl) assert canonical_purl == cano.to_string() # parsing the test `purl` should return the components parsed from the # test canonical `purl` parsed = PackageURL.from_string(canonical_purl) assert cano.to_dict() == parsed.to_dict() # parsing the test `purl` then re-building a `purl` from these parsed # components should return the test canonical `purl` assert canonical_purl == parsed.to_string() # building a `purl` from the test components should return the test # canonical `purl` built = PackageURL(type, namespace, name, version, qualifiers, subpath) assert canonical_purl == built.to_string() # create a good function name for use in test discovery if not description: description = purl if is_invalid: test_func_prefix += "is_invalid_" test_name = python_safe_name(test_func_prefix + description) test_purl.__name__ = test_name test_purl.funcname = test_name return test_purl, test_name def python_safe_name(s): """ Return a name derived from string `s` safe to use as a Python function name. For example: >>> s = "not `\\a /`good` -safe name ??" >>> assert python_safe_name(s) == 'not_good_safe_name' """ no_punctuation = re.compile(r"[\W_]", re.MULTILINE).sub s = s.lower() s = no_punctuation(" ", s) s = "_".join(s.split()) return s class PurlTest(unittest.TestCase): pass def build_tests(clazz=PurlTest, test_file="test-suite-data.json"): """ Dynamically build test methods for each purl test found in the `test_file` JSON file and attach a test method to the `clazz` class. """ test_data_dir = os.path.join(os.path.dirname(__file__), "data") test_file = os.path.join(test_data_dir, test_file) with open(test_file) as tf: tests_data = json.load(tf) for items in tests_data: test_func, test_name = create_test_function(**items) # attach that method to the class setattr(clazz, test_name, test_func) build_tests() class NormalizePurlTest(unittest.TestCase): def test_normalize_qualifiers_as_string(self): qualifiers_as_dict = {"classifier": "sources", "repository_url": "repo.spring.io/release"} qualifiers_as_string = "classifier=sources&repository_url=repo.spring.io/release" assert qualifiers_as_string == normalize_qualifiers(qualifiers_as_dict, encode=True) def test_normalize_qualifiers_as_dict(self): qualifiers_as_dict = {"classifier": "sources", "repository_url": "repo.spring.io/release"} qualifiers_as_string = "classifier=sources&repository_url=repo.spring.io/release" assert qualifiers_as_dict == normalize_qualifiers(qualifiers_as_string, encode=False) def test_create_PackageURL_from_qualifiers_string(self): canonical_purl = "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?classifier=sources&repository_url=repo.spring.io/release" type = "maven" # NOQA namespace = "org.apache.xmlgraphics" name = "batik-anim" version = "1.9.1" qualifiers_as_string = "classifier=sources&repository_url=repo.spring.io/release" subpath = None purl = PackageURL(type, namespace, name, version, qualifiers_as_string, subpath) assert canonical_purl == purl.to_string() def test_create_PackageURL_from_qualifiers_dict(self): canonical_purl = "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?classifier=sources&repository_url=repo.spring.io/release" type = "maven" # NOQA namespace = "org.apache.xmlgraphics" name = "batik-anim" version = "1.9.1" qualifiers_as_dict = {"classifier": "sources", "repository_url": "repo.spring.io/release"} subpath = None purl = PackageURL(type, namespace, name, version, qualifiers_as_dict, subpath) assert canonical_purl == purl.to_string() def test_normalize_encode_can_take_unicode_with_non_ascii_with_slash(self): uncd = "núcleo/núcleo" normal = normalize( type=uncd, namespace=uncd, name=uncd, version=uncd, qualifiers="a=" + uncd, subpath=uncd, encode=True, ) expected = ( "n%c3%bacleo/n%c3%bacleo", "n%C3%BAcleo/n%C3%BAcleo", "n%C3%BAcleo/n%C3%BAcleo", "n%C3%BAcleo/n%C3%BAcleo", "a=n%C3%BAcleo/n%C3%BAcleo", "n%C3%BAcleo/n%C3%BAcleo", ) assert expected == normal def test_normalize_decode_can_take_unicode_with_non_ascii_with_slash(self): uncd = "núcleo/núcleo" normal = normalize( type=uncd, namespace=uncd, name=uncd, version=uncd, qualifiers="a=" + uncd, subpath=uncd, encode=False, ) expected = ( "núcleo/núcleo", "núcleo/núcleo", "núcleo/núcleo", "núcleo/núcleo", {"a": "núcleo/núcleo"}, "núcleo/núcleo", ) assert expected == normal def test_normalize_encode_always_reencodes(self): uncd = "n%c3%bacleo/n%c3%bacleo" normal = normalize( type=uncd, namespace=uncd, name=uncd, version=uncd, qualifiers="a=" + uncd, subpath=uncd, encode=True, ) expected = ( "n%25c3%25bacleo/n%25c3%25bacleo", "n%25c3%25bacleo/n%25c3%25bacleo", "n%25c3%25bacleo/n%25c3%25bacleo", "n%25c3%25bacleo/n%25c3%25bacleo", "a=n%25c3%25bacleo/n%25c3%25bacleo", "n%25c3%25bacleo/n%25c3%25bacleo", ) assert expected == normal def test_qualifiers_must_be_key_value_pairs(self): purl = "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?this+is+not+a+key_value" try: PackageURL.from_string(purl) self.fail("Failed to raise exception for invalid qualifiers") except ValueError as ve: assert "Invalid qualifier. Must be a string of key=value pairs" in str(ve) def test_to_dict_optionally_returns_qualifiers_as_string(self): purl = PackageURL( type="maven", namespace="org.apache", name="commons-logging", version="12.3", qualifiers="this=12&that=13", subpath="this/is/a/path", ) expected = dict( [ ("type", "maven"), ("namespace", "org.apache"), ("name", "commons-logging"), ("version", "12.3"), ( "qualifiers", dict( [ ("that", "13"), ("this", "12"), ] ), ), ("subpath", "this/is/a/path"), ] ) assert expected == purl.to_dict() expected = dict( [ ("type", "maven"), ("namespace", "org.apache"), ("name", "commons-logging"), ("version", "12.3"), ("qualifiers", "that=13&this=12"), ("subpath", "this/is/a/path"), ] ) assert expected == purl.to_dict(encode=True) def test_to_dict_custom_empty_value(self): purl = PackageURL( type="maven", namespace="", name="commons-logging", version="12.3", qualifiers=None, ) expected = dict( [ ("type", "maven"), ("namespace", None), ("name", "commons-logging"), ("version", "12.3"), ("qualifiers", None), ("subpath", None), ] ) assert expected == purl.to_dict() assert expected == purl.to_dict(empty=None) expected = dict( [ ("type", "maven"), ("namespace", ""), ("name", "commons-logging"), ("version", "12.3"), ("qualifiers", ""), ("subpath", ""), ] ) assert expected == purl.to_dict(empty="") def test_purl_is_hashable(): s = {PackageURL(name="hashable", type="pypi")} assert len(s) == 1